89: Add EvelynLiquidityService to withdraw CLMM proceeds, swap to USDC and send payouts

This commit is contained in:
2026-10-04 19:42:21 +02:00
parent 9c636bc93d
commit b9369a870f
31 changed files with 2186 additions and 29 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-04
@@ -0,0 +1,35 @@
# Design
## Context
See proposal.md for motivation. Raydium already discovers position NFTs and decodes pool/position state but does not execute liquidity decrease. SolanaWallet provides external jup signer validation, signing/submission and SPL transfer with ATA creation; JupiterSwapService confirms exact-input swaps. The Hub has a constructor catalogue and explicit ordered process selection.
## Goals / Non-Goals
**Goals:** Implement the specified pipeline with fresh direct RPC reads and exact eligibility math; leave an offline-verifiable, inactive deployment template.
**Non-Goals:** No generic strategy, service-container framework, durable pending transaction queue or new currency ValueTags.
## Decisions
- Keep the public service a domain marker, mirroring the burner; the reference implementation additionally owns NenjimProcess startup and AutoCloseable shutdown. One scheduled executor uses fixed delay and propagates interruption across step boundaries.
- Keep the existing shared Hub graph eager; construct/register liquidity deployment configuration and dedicated passive dependencies only at the selected liquidity entry, as reflected by the Hub/Registry deltas.
- Build a dedicated direct Raydium instance when selected so cached pool/position state cannot trigger stale decisions. Resolve currencies through CIS and programs/precision from validated mint accounts.
- Finish NFT discovery first, then read a fresh pool state immediately before evaluating each owned positive-liquidity position. Pass that same state to withdrawal minimum-output math. Do not reuse a price across discovery or earlier withdrawals; an unusable pool read stops the remaining withdrawals in that iteration while swap and payout remain independent.
- Compare squared Q64.64 values by exact cross multiplication. Decimal factors cancel because pool and position share mints. For reversed ordering compare current raw price to lower-bound raw price times the retained percentage. This avoids display rounding and inversion rounding.
- Port the official CLMM integer tick algorithm into a small Raydium math helper, reusing it for exact integer principal amounts. Existing display math remains unchanged.
- Execute decrease_liquidity_v2 (legacy and Token-2022) with owner NFT holding, personal/protocol position PDA, vaults, tick arrays, owner destination token accounts and bitmap extension. Reuse an existing owner holding for each pool/reward mint, validating its account's mint, owner, program and initialized state. Only fall back to an existing or idempotently created ATA when no holding exists; failed holdings discovery must not create an extra account. This preserves the existing wallet's single-account-per-mint contract. Request a 600,000 compute-unit limit for the combined operation. Account details stay in the Raydium implementation; a private legacy transaction encoder uses existing address codec, PDA and wallet primitives.
- Validate pool and personal-position discriminators, token metadata and owner holding before signing. Apply explicit slippage to integer expected principal. Fail closed on unsupported transfer-affecting Token-2022 extensions rather than silently weakening minimums. Required reward accounts, if present, only collect into this wallet and never enter swaps/payouts.
- Parse only conf/evelyn-liquidity.conf with format-version validation, unknown/duplicate key rejection and required keys. Deployment addresses/signer remain placeholders in the ignored local runtime file and tracked example. Existing Hub example stays unchanged.
- Use controlled JUnit fixtures explicitly requested by issue #89, including transaction bytes. No test constructs a real signer or submits a financial operation.
## Risks / Trade-offs
- [Pool moves during discovery or earlier withdrawals] → Refresh price and eligibility for each position. Minimum principal limits protect received principal, not the harvest-distance threshold. Market movement after the final decision remains possible before execution; later iterations reread state.
- [Unknown transaction outcome] → Log signature when available, preserve ordinary underlying confirmation; subsequent iterations use current balances/positions as requested.
- [Mint extension or program layout changes] → Validate discriminators/layout and fail safely on unsupported extensions; document official source references and exact supported behavior.
- [SOL starvation] → Document funding for transaction fees and token-account rent; no hidden token reserves.
## Migration Plan
Copy/edit the example, bind a dedicated external signer, fund SOL and manually select evelyn.liquidity.prod in the Hub runtime plan. Stop/remove the selection to disable; positions and wallet balances remain the recovery state. Keep the OpenSpec change active for user review; no commit, push or deployment.
@@ -0,0 +1,30 @@
# Proposal
## Why
Evelyn needs to realize proceeds from manually funded EVE/SyrupUSDC CLMM positions before a falling price converts them back to EVE. Issue #89 adds the smallest automated realization and USDC payout pipeline supporting Evelyn's sustained 100 USD/day objective.
## What Changes
- Add a domain-only EvelynLiquidityService and a Hub-owned sequential process with three independent withdrawal, exact-input swap and payout steps.
- Add full Raydium CLMM liquidity decrease with slippage protection and retained position accounts/NFTs.
- Add strict versioned deployment configuration and a public placeholder example with both reserves zero.
- Lazily construct dedicated liquidity wallet, Jupiter and direct Raydium dependencies only for the selected process.
- Verify with controlled offline fixtures and document operation/recovery.
- Correct the review findings from commit `7dfd43d`: refresh the pool price for each position after discovery and reuse existing owner token accounts before creating destination ATAs.
## Capabilities
### New Capabilities
- `evelyn-liquidity-service`: Distance-based position realization, reserve-aware conversion/payout, configuration and runtime behavior.
- `raydium-clmm-liquidity-withdrawal`: Protected full liquidity decrease preserving positions and NFTs.
### Modified Capabilities
- `nenjim-hub-configuration`: Add the optional process to the known catalogue and defer its dedicated dependencies until selection.
- `nenjim-component-registry`: Preserve eager shared composition while registering dedicated liquidity components only for the selected process.
## Impact
Adds API and reference implementation packages under com.r35157.evelyn.service.liquidity, extends Raydium, and adds one optional Hub constructor. Deployment configuration and signer are required only when explicitly selected. No position creation/refill/closure, reference-price dependency, notifications or persistent service queue. No live financial verification, commits or pushes.
@@ -0,0 +1,64 @@
## Purpose
Realize Evelyn's manually funded EVE/SyrupUSDC concentrated liquidity into USDC payouts using one dedicated wallet and current on-chain state.
## ADDED Requirements
### Requirement: Owned positions qualify by their normalized upper boundary
The service SHALL discover positions owned by its wallet in its configured CLMM pool and withdraw positive liquidity only when the upper EVE/SyrupUSDC boundary U satisfies U <= P*(1-h/100). P SHALL come from a fresh on-chain pool read for each owned positive-liquidity position after NFT discovery and previous withdrawals have completed. The same state SHALL be used for that position's eligibility decision and withdrawal quote; an earlier position's price SHALL NOT be reused. Both orientations SHALL be normalized, and decisions SHALL use pool precision. Unusable P SHALL skip the remaining withdrawals in that iteration while allowing later steps.
#### Scenario: Inclusive threshold and upper boundary
- **WHEN** P is 15, h is 10 and positions span 13–13.5 and 13.5–14
- **THEN** only the first qualifies, including equality
#### Scenario: Reversed pool ordering
- **WHEN** mint A is SyrupUSDC and mint B is EVE
- **THEN** the inverted lower raw boundary determines the normalized upper price and the same eligibility rule applies
#### Scenario: Other or empty positions and falling market
- **WHEN** a position belongs to another wallet or pool, has zero liquidity, or its upper boundary exceeds the threshold after a price decline
- **THEN** it retains its state and the service neither withdraws it nor creates or refills positions
#### Scenario: Price declines during discovery or an earlier withdrawal
- **WHEN** a position qualifies before discovery or an earlier withdrawal, but its upper boundary exceeds the threshold at the new pool price
- **THEN** the service reads that new price before deciding and preserves the position's liquidity in either token orientation
#### Scenario: Pool read fails between positions
- **WHEN** an earlier position was withdrawn but the next current pool price is unavailable or unusable
- **THEN** no remaining position is withdrawn in that iteration and swap and payout still run on their independent balances
### Requirement: Swap and payout use independent current balances
The service SHALL read SyrupUSDC at step 2 and USDC at step 3 and operate on the entire respective balance above its nonnegative reserve. Absent or at/below-reserve balances SHALL be skipped. Exact-input swap SHALL use explicit maximum slippage. SPL payout SHALL target the configured wallet owner with existing account-creation support. Amounts SHALL never round upward. EVE and reward tokens SHALL remain untouched by this pipeline.
#### Scenario: Nonzero reserves
- **WHEN** SyrupUSDC balance is 1.01 with reserve 0.01 and USDC balance is 2.5 with reserve 0.5
- **THEN** 1.00 SyrupUSDC is swapped and 2.0 USDC is sent
#### Scenario: Zero reserves and empty accounts
- **WHEN** reserves are zero
- **THEN** full positive balances are eligible, absent/zero balances are skipped and empty source accounts are not closed
### Requirement: Failure recovery uses on-chain state
Each iteration SHALL attempt withdrawal, swap and payout independently in that order, isolating ordinary per-position failures. Subsequent iterations and restarts SHALL reread positions and balances without a persistent queue, attribution, pending-signature gate, immediate retries or replay. Logs SHALL identify operations, positions, amounts and returned signatures without signer secrets. Interruption SHALL stop work.
#### Scenario: Independent failures and restart
- **WHEN** withdrawal or swap fails or returns an unknown outcome
- **THEN** independent later work can proceed and the next iteration or restart acts on current state, skipping completed empty positions
### Requirement: Configuration is validated before worker start
A dedicated version-1 runtime configuration SHALL validate wallet/signer binding, pool type/token identity, address syntax, 0<=h<100, positive fixed delay, nonnegative reserves with mint precision and explicit slippage limits. Currencies, mints and programs SHALL use canonical identity and chain metadata. The public example SHALL have zero reserves and SHALL never be a runtime fallback.
#### Scenario: Invalid input or missing deployment file
- **WHEN** configuration is invalid or only the example exists
- **THEN** startup fails before financial work
### Requirement: Optional process owns a sequential worker
The Hub SHALL select evelyn.liquidity.prod through its existing catalogue and Registry, constructing deployment configuration and dedicated signer/swap dependencies only when selected. Existing selections and ordering SHALL be preserved. The domain interface SHALL expose no lifecycle operations. A single worker SHALL run the first and subsequent iterations identically, with a fixed delay after completion and ordinary shutdown interruption.
#### Scenario: Optional process is omitted
- **WHEN** the Hub plan omits evelyn.liquidity.prod
- **THEN** no liquidity configuration or dedicated signer is loaded and the checked-in Hub example activates no financial operation
#### Scenario: Slow iteration and shutdown
- **WHEN** an iteration exceeds the configured interval or shutdown interrupts it
- **THEN** no iterations overlap and interruption prevents further steps
@@ -0,0 +1,68 @@
## MODIFIED Requirements
### Requirement: NenjimHub owns composition and runtime startup
`NenjimHub` SHALL remain the global runtime owner exposing only `start() throws Exception` plus an immutable ordered snapshot of successfully started process IDs. It SHALL be neither component nor process and SHALL not register itself. Its only accepted startup attempt SHALL validate the complete fixed-path Hub configuration before starting anything, construct and start Registry first, register Registry as `nenjim.registry.service`, record it, construct and register the existing shared passive graph, then lazily construct, register, start, and record only the remaining configured processes in exact order beginning with Journal. The selected liquidity process SHALL additionally compose its dedicated passive dependencies lazily at its configured entry. Registry and Journal managers SHALL remain distinct passive, unregistered handles retained privately by the Hub. Registration SHALL never imply startup. The Hub SHALL preserve the online point and blocking wait only after all configured processes succeed, without retry, rollback, dependency inference, or reordering.
#### Scenario: Registry starts before passive graph construction
- **WHEN** the Hub has validated a complete startup plan and performs its first startup attempt
- **THEN** it starts, registers, and records Registry first before constructing the passive component graph or any selected non-Registry process
#### Scenario: Journal follows passive composition but starts second
- **WHEN** Registry bootstrap and passive graph construction have succeeded
- **THEN** the Hub constructs, registers, starts, and records Journal before any later configured process, so Journal remains the second successful process
#### Scenario: Registry is process-discoverable
- **WHEN** the Registry registration has completed
- **THEN** the same object is retrievable as both `NenjimRegistryService` and `NenjimProcess`
#### Scenario: Journal is process-discoverable
- **WHEN** the configured Journal construction and registration have completed
- **THEN** the same object is retrievable as both `JournalService` and `NenjimProcess` under `nenjim.journal.service`, while its manager is not registered
#### Scenario: Selected JavaFX runtime is process-discoverable
- **WHEN** `nenjim.javafx-runtime.service` is selected and its registration has completed
- **THEN** one object is retrievable as both `JavaFxRuntimeService` and `NenjimProcess` under that ID
#### Scenario: Selected JavaFX applications share the runtime
- **WHEN** JavaFX runtime precedes selected Mission Control or Process Manager entries
- **THEN** each selected application retains the same already-constructed `JavaFxRuntimeService` instance registered by the Hub
#### Scenario: Selected Process Manager is process-discoverable
- **WHEN** `nenjim.process-manager.default` is selected after JavaFX runtime and its construction and registration complete
- **THEN** the object constructed with the current Hub and shared JavaFX runtime is retrievable as both `NenjimProcessManager` and `NenjimProcess`
#### Scenario: Required passive bindings are registered
- **WHEN** Hub passive composition completes
- **THEN** all current passive component IDs and implementation choices from issue #76 are present exactly as configured, while process IDs other than Registry remain absent until selected
#### Scenario: Configured startup subset preserves order
- **WHEN** Hub startup completes successfully
- **THEN** successful process IDs exactly equal the validated `[AUTO_START]` entries in their declared order
#### Scenario: Omitted processes do not enter the Registry
- **WHEN** a modern process ID is omitted from `[AUTO_START]`
- **THEN** that process is neither constructed, registered, started, nor recorded even though its implementation is known to the Hub
#### Scenario: Journal failure is fail-fast
- **WHEN** Journal construction, registration, or process startup throws
- **THEN** Registry remains the only recorded process, no later configured process is constructed or started, the exception propagates, and the Hub neither announces online nor enters its final wait
#### Scenario: JavaFX runtime failure is fail-fast
- **WHEN** selected JavaFX runtime construction, registration, or startup throws
- **THEN** its ID is not recorded, no later configured process is constructed or started, the exception propagates, and the Hub neither announces online nor enters its final wait
#### Scenario: Hub start is not retried
- **WHEN** any first Hub startup attempt has begun and `start()` is requested again
- **THEN** the later request fails with `IllegalStateException` whether the first attempt succeeded or failed
#### Scenario: Startup failure is fail-fast
- **WHEN** any passive composition, selected construction, registration, or process startup operation throws
- **THEN** the exception propagates, no later process is constructed or started, the Hub neither announces online nor enters its final wait, and no generic rollback or retry is attempted
#### Scenario: Dedicated liquidity bindings use the liquidity signer
- **WHEN** `evelyn.liquidity.prod` is selected and its construction succeeds
- **THEN** `solana.wallet.evelyn-liquidity-prod`, `jupiter.swap.evelyn-liquidity-prod` and `raydium.service.evelyn-liquidity-prod` are registered, the swap service uses the dedicated liquidity wallet and Raydium uses direct chain reads
#### Scenario: Liquidity service has orthogonal domain and lifecycle views
- **WHEN** the selected liquidity process is registered
- **THEN** the same object resolves as `EvelynLiquidityService` and `NenjimProcess`, while the domain interface exposes no lifecycle operations
@@ -0,0 +1,57 @@
## MODIFIED Requirements
### Requirement: Registry bootstraps eager passive composition
After validating the entire plan, the Hub SHALL construct the configured Registry subsystem, start Registry, register its service and process object as `nenjim.registry.service`, and record that ID only after successful startup. The Hub SHALL then eagerly construct and register the existing shared passive component graph. Dedicated liquidity deployment configuration, wallet signer, Jupiter swap service and direct Raydium service SHALL instead be constructed and registered only when the configured `evelyn.liquidity.prod` entry is reached. This passive phase SHALL not construct any canonical `NenjimProcess`, start background workers, initialize JavaFX, add startup IDs, infer process dependencies, or implicitly create an omitted process. Plain internal dependencies that are not `NenjimComponent` objects MAY be constructed as part of passive composition.
#### Scenario: Registry surrounds passive composition
- **WHEN** a valid Hub startup begins
- **THEN** Registry starts, is registered, and is recorded before the existing shared passive component graph is constructed and registered
#### Scenario: Passive composition completes
- **WHEN** Registry bootstrap succeeds
- **THEN** all existing shared non-process building blocks, adapters, and services are constructed and registered without starting a process
#### Scenario: Passive construction fails
- **WHEN** any passive component cannot be constructed or registered
- **THEN** startup stops immediately with Registry remaining recorded and no configured process after Registry constructed or started
#### Scenario: Liquidity is omitted
- **WHEN** the plan omits `evelyn.liquidity.prod`
- **THEN** its deployment configuration, dedicated signer/wallet, Jupiter service and direct Raydium service are neither loaded nor constructed nor registered
### Requirement: Selected processes are constructed and started lazily in exact order
The Hub SHALL support the modern process IDs `nenjim.registry.service`, `nenjim.journal.service`, `assetaz.ticker.default`, `jupiter-perps-alarm.default`, `evelyn.status.prod`, `evelyn.status.test`, `evelyn.iou-burner.prod`, `evelyn.liquidity.prod`, `nenjim.javafx-runtime.service`, `evelyn.mission-control.default`, and `nenjim.process-manager.default` through implementation-only construction capability that creates no process instance during validation. After passive composition, the Hub SHALL visit every configured entry after Registry in declared order, beginning with Journal. At each entry it SHALL construct only that process and any explicitly composed process-specific passive dependencies or administration view, register the process under the exact configured ID, resolve it through the canonical `NenjimProcess` contract where applicable, call `start()`, and record the ID only after successful return.
The configuration SHALL be the complete manually ordered process dependency closure. The Hub SHALL not discover, insert, recursively construct, reorder, or automatically start dependencies. A process omitted from `[AUTO_START]` SHALL not be constructed, registered, started, or recorded, even when its implementation is available. Legacy process prototypes outside the modern Hub graph SHALL not be selectable.
#### Scenario: Configured process starts successfully
- **WHEN** a configured process entry is reached and its dependencies are already available
- **THEN** that process is constructed, registered, started, and recorded before the next entry is reached
#### Scenario: Process is omitted
- **WHEN** a known process ID does not appear in `[AUTO_START]`
- **THEN** no instance of that process is constructed or registered and its lifecycle is never started or recorded
#### Scenario: Jupiter Perps Alarm is selected
- **WHEN** `jupiter-perps-alarm.default` is explicitly listed after its required predecessors
- **THEN** the otherwise inactive modern alarm process is constructed, registered, started and recorded at that exact position
#### Scenario: Dependency order is invalid
- **WHEN** a selected process is reached before a process dependency required by its current constructor or startup behavior
- **THEN** construction or startup fails clearly without implicit dependency construction, repair, or reordering
#### Scenario: JavaFX processes are omitted
- **WHEN** a valid headless plan omits `nenjim.javafx-runtime.service`, `evelyn.mission-control.default`, and `nenjim.process-manager.default`
- **THEN** none of those objects is constructed or registered and the JavaFX toolkit remains uninitialized by the Hub
#### Scenario: Renamed status services retain dependency wiring
- **WHEN** `evelyn.status.prod` and `evelyn.status.test` are listed after Ticker and before Mission Control
- **THEN** the Hub constructs separate Production/Test status services, registers the renamed IDs, and injects the corresponding typed service views into Mission Control in the configured order
#### Scenario: Deployment still contains an old status ID
- **WHEN** a private deployment configuration contains `evelyn.service.prod` or `evelyn.service.test`
- **THEN** validation rejects the unknown ID before startup; deployment migration replaces only that ID with `evelyn.status.prod` or `evelyn.status.test`, preserving selection and order
#### Scenario: Liquidity is explicitly selected
- **WHEN** `evelyn.liquidity.prod` is reached in a valid startup plan
- **THEN** the Hub loads its runtime configuration, binds the dedicated signer/wallet and Jupiter service to each other, constructs direct Raydium access and registers/starts the liquidity process without inserting a Ticker or status dependency or reordering the plan
@@ -0,0 +1,35 @@
## Purpose
Provide a narrowly scoped protected Raydium CLMM full liquidity decrease using the owner's existing Solana signing and submission primitives.
## ADDED Requirements
### Requirement: Full withdrawal preserves the reusable position
The operation SHALL validate the NFT owner, pool and positive current liquidity, decrease that complete liquidity into the same wallet, collect pool-token fees and preserve the position account, NFT and NFT token account. For each pool or reward mint it SHALL reuse an existing owner token holding, including a non-associated account, after validating its mint, owner, program and initialized state. It SHALL use or create an ATA only when no owner holding exists and SHALL NOT fall back to account creation when holdings discovery fails. Transaction/account details SHALL remain behind Raydium. It SHALL submit once and use existing confirmation behavior without close, burn or service-level retry.
#### Scenario: Full protected withdrawal
- **WHEN** a wallet withdraws its position's complete liquidity
- **THEN** the transaction names the expected wallet, pool, liquidity and nonzero minimum output protection and contains no close-position, NFT burn or token-account close instruction
#### Scenario: Wrong owner or changed position
- **WHEN** ownership, pool or current position state is inconsistent with the requested withdrawal
- **THEN** the operation fails before signing
#### Scenario: Existing non-associated token accounts
- **WHEN** the wallet already has a pool-token or reward holding outside its ATA under either supported token program
- **THEN** the withdrawal targets that validated account and creates no additional account for that mint, preserving subsequent wallet balance reads
#### Scenario: Existing holding cannot be trusted
- **WHEN** holdings discovery fails or the selected account has a wrong mint, owner, token program or initialized state
- **THEN** withdrawal fails before signing without creating a replacement token account
### Requirement: Minimum output derives from precise state and explicit slippage
Withdrawal minimums SHALL derive from current Q64.64 pool state, exact CLMM tick/liquidity math and configured slippage with downward raw-token rounding. Both minimums SHALL NOT be unconditionally zero. Required legacy/Token-2022 accounts, tick arrays and bitmap extension SHALL follow the official decrease instruction. Unsupported token extensions SHALL fail safely before signing.
#### Scenario: One-sided principal and fees
- **WHEN** the range is fully below the normalized current price
- **THEN** SyrupUSDC principal is protected, any EVE fees return to the wallet and all received EVE remains there
#### Scenario: Precision and tiny position
- **WHEN** expected principal is too small to provide a positive raw-unit minimum
- **THEN** the operation fails rather than submitting an unchecked withdrawal
@@ -0,0 +1,38 @@
# Tasks
## 1. Protected Raydium withdrawal
- [x] 1.1 Add exact CLMM tick, eligibility and raw principal math; verify official boundary fixtures, equality and inversion.
- [x] 1.2 Add full decrease operation with owner/account validation, slippage and retained NFT; verify captured transaction bytes with offline fixtures and document API/source references.
## 2. Liquidity service and deployment configuration
- [x] 2.1 Add versioned strict configuration and zero-reserve public template; verify invalid ranges, addresses, precision, duplicate/unknown keys and missing-file behavior.
- [x] 2.2 Implement independent withdrawal/swap/payout steps and sequential interruptible lifecycle; verify reserves, failure/restart recovery, empty/foreign positions, price decline, no overlap and shutdown using controlled dependencies.
- [x] 2.3 Document configuration, dedicated signer/SOL requirements and pipeline/recovery behavior; inspect documentation against issue #89.
## 3. Hub integration and overall validation
- [x] 3.1 Lazily register the optional process and dedicated wallet/Jupiter/direct Raydium dependencies; verify optional selection preserves existing startup plan and checked-in example inactivity.
- [x] 3.2 Compile main/verification sources, run affected offline tests, validate OpenSpec strictly and run git diff --check; record results without live financial operations, commits or pushes.
## 4. Review corrections for commit 7dfd43d
- [x] 4.1 Refresh pool state per position after discovery and previous withdrawals; verify price declines in both orientations, quote consistency and independent later steps after a pool-read failure.
- [x] 4.2 Reuse validated existing pool/reward token holdings before creating ATAs; verify both token programs, invalid accounts, failed holdings reads and absence of duplicate-account creation.
- [x] 4.3 Update operation documentation, compile and run all offline tests, validate OpenSpec strictly and check the diff without live financial operations, commits or pushes.
## Verification result (2026-10-04)
- Main and verification compilation passed with JavaFX JDK 25 and Gradle 9.6.1.
- All 34 repository JUnit tests passed (25 liquidity/math/withdrawal/Hub tests and 9 existing tests); no failures, errors or skips.
- Strict OpenSpec change validation passed; git diff --check and new-file whitespace checks passed.
- Public mainnet inspection was read-only. All financial operation verification used controlled offline dependencies; no live withdrawal, swap, transfer, signer invocation, commit or push.
- Runtime configuration contains deployment placeholders and is ignored by Git. Both example reserves are zero; the Hub runtime selection and checked-in Hub example are unchanged.
## Review correction verification (2026-10-04)
- Eight new regression tests reproduced the findings before their respective fixes and passed afterward: four cover per-position price freshness, quote consistency and independent later steps; four cover existing pool/reward accounts, invalid accounts and failed holdings reads.
- Main/verification compilation and all 42 repository JUnit tests passed with no failures, errors or skips.
- Strict OpenSpec validation and git diff --check passed. The active change has 10/10 completed tasks.
- Verification used controlled offline dependencies only. No live financial operation, real signer invocation, commit, push or deployment was performed.
@@ -0,0 +1,66 @@
# evelyn-liquidity-service Specification
## Purpose
Realize Evelyn's manually funded EVE/SyrupUSDC concentrated liquidity into USDC payouts using one dedicated wallet and current on-chain state.
## Requirements
### Requirement: Owned positions qualify by their normalized upper boundary
The service SHALL discover positions owned by its wallet in its configured CLMM pool and withdraw positive liquidity only when the upper EVE/SyrupUSDC boundary U satisfies U <= P*(1-h/100). P SHALL come from a fresh on-chain pool read for each owned positive-liquidity position after NFT discovery and previous withdrawals have completed. The same state SHALL be used for that position's eligibility decision and withdrawal quote; an earlier position's price SHALL NOT be reused. Both orientations SHALL be normalized, and decisions SHALL use pool precision. Unusable P SHALL skip the remaining withdrawals in that iteration while allowing later steps.
#### Scenario: Inclusive threshold and upper boundary
- **WHEN** P is 15, h is 10 and positions span 13–13.5 and 13.5–14
- **THEN** only the first qualifies, including equality
#### Scenario: Reversed pool ordering
- **WHEN** mint A is SyrupUSDC and mint B is EVE
- **THEN** the inverted lower raw boundary determines the normalized upper price and the same eligibility rule applies
#### Scenario: Other or empty positions and falling market
- **WHEN** a position belongs to another wallet or pool, has zero liquidity, or its upper boundary exceeds the threshold after a price decline
- **THEN** it retains its state and the service neither withdraws it nor creates or refills positions
#### Scenario: Price declines during discovery or an earlier withdrawal
- **WHEN** a position qualifies before discovery or an earlier withdrawal, but its upper boundary exceeds the threshold at the new pool price
- **THEN** the service reads that new price before deciding and preserves the position's liquidity in either token orientation
#### Scenario: Pool read fails between positions
- **WHEN** an earlier position was withdrawn but the next current pool price is unavailable or unusable
- **THEN** no remaining position is withdrawn in that iteration and swap and payout still run on their independent balances
### Requirement: Swap and payout use independent current balances
The service SHALL read SyrupUSDC at step 2 and USDC at step 3 and operate on the entire respective balance above its nonnegative reserve. Absent or at/below-reserve balances SHALL be skipped. Exact-input swap SHALL use explicit maximum slippage. SPL payout SHALL target the configured wallet owner with existing account-creation support. Amounts SHALL never round upward. EVE and reward tokens SHALL remain untouched by this pipeline.
#### Scenario: Nonzero reserves
- **WHEN** SyrupUSDC balance is 1.01 with reserve 0.01 and USDC balance is 2.5 with reserve 0.5
- **THEN** 1.00 SyrupUSDC is swapped and 2.0 USDC is sent
#### Scenario: Zero reserves and empty accounts
- **WHEN** reserves are zero
- **THEN** full positive balances are eligible, absent/zero balances are skipped and empty source accounts are not closed
### Requirement: Failure recovery uses on-chain state
Each iteration SHALL attempt withdrawal, swap and payout independently in that order, isolating ordinary per-position failures. Subsequent iterations and restarts SHALL reread positions and balances without a persistent queue, attribution, pending-signature gate, immediate retries or replay. Logs SHALL identify operations, positions, amounts and returned signatures without signer secrets. Interruption SHALL stop work.
#### Scenario: Independent failures and restart
- **WHEN** withdrawal or swap fails or returns an unknown outcome
- **THEN** independent later work can proceed and the next iteration or restart acts on current state, skipping completed empty positions
### Requirement: Configuration is validated before worker start
A dedicated version-1 runtime configuration SHALL validate wallet/signer binding, pool type/token identity, address syntax, 0<=h<100, positive fixed delay, nonnegative reserves with mint precision and explicit slippage limits. Currencies, mints and programs SHALL use canonical identity and chain metadata. The public example SHALL have zero reserves and SHALL never be a runtime fallback.
#### Scenario: Invalid input or missing deployment file
- **WHEN** configuration is invalid or only the example exists
- **THEN** startup fails before financial work
### Requirement: Optional process owns a sequential worker
The Hub SHALL select evelyn.liquidity.prod through its existing catalogue and Registry, constructing deployment configuration and dedicated signer/swap dependencies only when selected. Existing selections and ordering SHALL be preserved. The domain interface SHALL expose no lifecycle operations. A single worker SHALL run the first and subsequent iterations identically, with a fixed delay after completion and ordinary shutdown interruption.
#### Scenario: Optional process is omitted
- **WHEN** the Hub plan omits evelyn.liquidity.prod
- **THEN** no liquidity configuration or dedicated signer is loaded and the checked-in Hub example activates no financial operation
#### Scenario: Slow iteration and shutdown
- **WHEN** an iteration exceeds the configured interval or shutdown interrupts it
- **THEN** no iterations overlap and interruption prevents further steps
@@ -186,7 +186,7 @@ The reference Registry construction boundary SHALL create one Registry object ex
- **THEN** no component ID identifies the manager and no lookup returns it as a component or process
### Requirement: NenjimHub owns composition and runtime startup
`NenjimHub` SHALL remain the global runtime owner exposing only `start() throws Exception` plus an immutable ordered snapshot of successfully started process IDs. It SHALL be neither component nor process and SHALL not register itself. Its only accepted startup attempt SHALL validate the complete fixed-path Hub configuration before starting anything, construct and start Registry first, register Registry as `nenjim.registry.service`, record it, construct and register the complete passive graph, then lazily construct, register, start, and record only the remaining configured processes in exact order beginning with Journal. Registry and Journal managers SHALL remain distinct passive, unregistered handles retained privately by the Hub. Registration SHALL never imply startup. The Hub SHALL preserve the online point and blocking wait only after all configured processes succeed, without retry, rollback, dependency inference, or reordering.
`NenjimHub` SHALL remain the global runtime owner exposing only `start() throws Exception` plus an immutable ordered snapshot of successfully started process IDs. It SHALL be neither component nor process and SHALL not register itself. Its only accepted startup attempt SHALL validate the complete fixed-path Hub configuration before starting anything, construct and start Registry first, register Registry as `nenjim.registry.service`, record it, construct and register the existing shared passive graph, then lazily construct, register, start, and record only the remaining configured processes in exact order beginning with Journal. The selected liquidity process SHALL additionally compose its dedicated passive dependencies lazily at its configured entry. Registry and Journal managers SHALL remain distinct passive, unregistered handles retained privately by the Hub. Registration SHALL never imply startup. The Hub SHALL preserve the online point and blocking wait only after all configured processes succeed, without retry, rollback, dependency inference, or reordering.
#### Scenario: Registry starts before passive graph construction
- **WHEN** the Hub has validated a complete startup plan and performs its first startup attempt
@@ -244,6 +244,14 @@ The reference Registry construction boundary SHALL create one Registry object ex
- **WHEN** any passive composition, selected construction, registration, or process startup operation throws
- **THEN** the exception propagates, no later process is constructed or started, the Hub neither announces online nor enters its final wait, and no generic rollback or retry is attempted
#### Scenario: Dedicated liquidity bindings use the liquidity signer
- **WHEN** `evelyn.liquidity.prod` is selected and its construction succeeds
- **THEN** `solana.wallet.evelyn-liquidity-prod`, `jupiter.swap.evelyn-liquidity-prod` and `raydium.service.evelyn-liquidity-prod` are registered, the swap service uses the dedicated liquidity wallet and Raydium uses direct chain reads
#### Scenario: Liquidity service has orthogonal domain and lifecycle views
- **WHEN** the selected liquidity process is registered
- **THEN** the same object resolves as `EvelynLiquidityService` and `NenjimProcess`, while the domain interface exposes no lifecycle operations
### Requirement: Started-process snapshots are ordered immutable and truthful
`NenjimHub.getStartedProcessIds()` SHALL be safe before, during, and after startup and SHALL return an immutable point-in-time snapshot in successful startup order. A process ID SHALL be appended only after that process's `start()` call returns successfully, including `nenjim.javafx-runtime.service` and `nenjim.process-manager.default`. Earlier successful entries SHALL remain visible after a later failure, while a failing process SHALL never be included. The snapshot SHALL make no health or continued-liveness guarantee.
@@ -70,22 +70,26 @@ Every active `[AUTO_START]` line SHALL be parsed through the canonical `NenjimCo
- **THEN** validation fails before Registry startup and identifies the required placement
### Requirement: Registry bootstraps eager passive composition
After validating the entire plan, the Hub SHALL construct the configured Registry subsystem, start Registry, register its service and process object as `nenjim.registry.service`, and record that ID only after successful startup. The Hub SHALL then eagerly construct and register the complete current passive component graph needed by selectable processes. This passive phase SHALL not construct any canonical `NenjimProcess`, start background workers, initialize JavaFX, add startup IDs, infer process dependencies, or implicitly create an omitted process. Plain internal dependencies that are not `NenjimComponent` objects MAY be constructed as part of passive composition.
After validating the entire plan, the Hub SHALL construct the configured Registry subsystem, start Registry, register its service and process object as `nenjim.registry.service`, and record that ID only after successful startup. The Hub SHALL then eagerly construct and register the existing shared passive component graph. Dedicated liquidity deployment configuration, wallet signer, Jupiter swap service and direct Raydium service SHALL instead be constructed and registered only when the configured `evelyn.liquidity.prod` entry is reached. This passive phase SHALL not construct any canonical `NenjimProcess`, start background workers, initialize JavaFX, add startup IDs, infer process dependencies, or implicitly create an omitted process. Plain internal dependencies that are not `NenjimComponent` objects MAY be constructed as part of passive composition.
#### Scenario: Registry surrounds passive composition
- **WHEN** a valid Hub startup begins
- **THEN** Registry starts, is registered, and is recorded before the complete passive component graph is constructed and registered
- **THEN** Registry starts, is registered, and is recorded before the existing shared passive component graph is constructed and registered
#### Scenario: Passive composition completes
- **WHEN** Registry bootstrap succeeds
- **THEN** all current non-process building blocks, adapters, and services are constructed and registered without starting a process
- **THEN** all existing shared non-process building blocks, adapters, and services are constructed and registered without starting a process
#### Scenario: Passive construction fails
- **WHEN** any passive component cannot be constructed or registered
- **THEN** startup stops immediately with Registry remaining recorded and no configured process after Registry constructed or started
#### Scenario: Liquidity is omitted
- **WHEN** the plan omits `evelyn.liquidity.prod`
- **THEN** its deployment configuration, dedicated signer/wallet, Jupiter service and direct Raydium service are neither loaded nor constructed nor registered
### Requirement: Selected processes are constructed and started lazily in exact order
The Hub SHALL support the modern process IDs `nenjim.registry.service`, `nenjim.journal.service`, `assetaz.ticker.default`, `jupiter-perps-alarm.default`, `evelyn.status.prod`, `evelyn.status.test`, `evelyn.iou-burner.prod`, `nenjim.javafx-runtime.service`, `evelyn.mission-control.default`, and `nenjim.process-manager.default` through implementation-only construction capability that creates no process instance during validation. After passive composition, the Hub SHALL visit every configured entry after Registry in declared order, beginning with Journal. At each entry it SHALL construct only that process and any process-specific passive administration view, register the process under the exact configured ID, resolve it through the canonical `NenjimProcess` contract where applicable, call `start()`, and record the ID only after successful return.
The Hub SHALL support the modern process IDs `nenjim.registry.service`, `nenjim.journal.service`, `assetaz.ticker.default`, `jupiter-perps-alarm.default`, `evelyn.status.prod`, `evelyn.status.test`, `evelyn.iou-burner.prod`, `evelyn.liquidity.prod`, `nenjim.javafx-runtime.service`, `evelyn.mission-control.default`, and `nenjim.process-manager.default` through implementation-only construction capability that creates no process instance during validation. After passive composition, the Hub SHALL visit every configured entry after Registry in declared order, beginning with Journal. At each entry it SHALL construct only that process and any explicitly composed process-specific passive dependencies or administration view, register the process under the exact configured ID, resolve it through the canonical `NenjimProcess` contract where applicable, call `start()`, and record the ID only after successful return.
The configuration SHALL be the complete manually ordered process dependency closure. The Hub SHALL not discover, insert, recursively construct, reorder, or automatically start dependencies. A process omitted from `[AUTO_START]` SHALL not be constructed, registered, started, or recorded, even when its implementation is available. Legacy process prototypes outside the modern Hub graph SHALL not be selectable.
@@ -117,6 +121,10 @@ The configuration SHALL be the complete manually ordered process dependency clos
- **WHEN** a private deployment configuration contains `evelyn.service.prod` or `evelyn.service.test`
- **THEN** validation rejects the unknown ID before startup; deployment migration replaces only that ID with `evelyn.status.prod` or `evelyn.status.test`, preserving selection and order
#### Scenario: Liquidity is explicitly selected
- **WHEN** `evelyn.liquidity.prod` is reached in a valid startup plan
- **THEN** the Hub loads its runtime configuration, binds the dedicated signer/wallet and Jupiter service to each other, constructs direct Raydium access and registers/starts the liquidity process without inserting a Ticker or status dependency or reordering the plan
### Requirement: Configured startup is fail-fast and reports actual milestones
Registry and every selected process SHALL be registered before its `start()` call and appended to the Hub's immutable ordered startup snapshot only after `start()` returns successfully. Construction failure SHALL leave the selected process unregistered. Registration or startup failure SHALL stop the plan before any later entry is constructed or started; a startup failure MAY leave that process registered but SHALL not record it. Earlier successful processes SHALL remain in snapshots, no rollback or retry SHALL occur, and the Hub SHALL announce its existing online point and enter its blocking runtime wait only after every configured process succeeds.
@@ -0,0 +1,37 @@
# raydium-clmm-liquidity-withdrawal Specification
## Purpose
Provide a narrowly scoped protected Raydium CLMM full liquidity decrease using the owner's existing Solana signing and submission primitives.
## Requirements
### Requirement: Full withdrawal preserves the reusable position
The operation SHALL validate the NFT owner, pool and positive current liquidity, decrease that complete liquidity into the same wallet, collect pool-token fees and preserve the position account, NFT and NFT token account. For each pool or reward mint it SHALL reuse an existing owner token holding, including a non-associated account, after validating its mint, owner, program and initialized state. It SHALL use or create an ATA only when no owner holding exists and SHALL NOT fall back to account creation when holdings discovery fails. Transaction/account details SHALL remain behind Raydium. It SHALL submit once and use existing confirmation behavior without close, burn or service-level retry.
#### Scenario: Full protected withdrawal
- **WHEN** a wallet withdraws its position's complete liquidity
- **THEN** the transaction names the expected wallet, pool, liquidity and nonzero minimum output protection and contains no close-position, NFT burn or token-account close instruction
#### Scenario: Wrong owner or changed position
- **WHEN** ownership, pool or current position state is inconsistent with the requested withdrawal
- **THEN** the operation fails before signing
#### Scenario: Existing non-associated token accounts
- **WHEN** the wallet already has a pool-token or reward holding outside its ATA under either supported token program
- **THEN** the withdrawal targets that validated account and creates no additional account for that mint, preserving subsequent wallet balance reads
#### Scenario: Existing holding cannot be trusted
- **WHEN** holdings discovery fails or the selected account has a wrong mint, owner, token program or initialized state
- **THEN** withdrawal fails before signing without creating a replacement token account
### Requirement: Minimum output derives from precise state and explicit slippage
Withdrawal minimums SHALL derive from current Q64.64 pool state, exact CLMM tick/liquidity math and configured slippage with downward raw-token rounding. Both minimums SHALL NOT be unconditionally zero. Required legacy/Token-2022 accounts, tick arrays and bitmap extension SHALL follow the official decrease instruction. Unsupported token extensions SHALL fail safely before signing.
#### Scenario: One-sided principal and fees
- **WHEN** the range is fully below the normalized current price
- **THEN** SyrupUSDC principal is protected, any EVE fees return to the wallet and all received EVE remains there
#### Scenario: Precision and tiny position
- **WHEN** expected principal is too small to provide a positive raw-unit minimum
- **THEN** the operation fails rather than submitting an unchecked withdrawal