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

This commit is contained in:
2026-10-04 19:19:17 +02:00
parent e8d4dbe7ae
commit 124b0d49c6
31 changed files with 2186 additions and 29 deletions
@@ -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