89: Add EvelynLiquidityService to withdraw CLMM proceeds, swap to USDC and send payouts
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
# Evelyn liquidity proceeds
|
||||
|
||||
`com.r35157.evelyn.service.liquidity.EvelynLiquidityService` is a domain-only
|
||||
Nenjim component. `impl.ref.EvelynLiquidityServiceImpl` additionally implements
|
||||
`NenjimProcess`; NenjimHub owns startup. The optional process ID is
|
||||
`evelyn.liquidity.prod`.
|
||||
|
||||
The owner creates/funds EVE/SyrupUSDC CLMM positions manually (for example,
|
||||
10 EVE each) using the dedicated liquidity wallet. This service withdraws,
|
||||
converts and pays proceeds; it never creates, refills, reranges, closes or burns
|
||||
positions, NFTs or source token accounts. A falling price leaves nonqualifying
|
||||
positions intact, including positions whose inventory has returned to EVE.
|
||||
|
||||
## Deployment
|
||||
|
||||
Copy `conf/evelyn-liquidity.conf.example` to `conf/evelyn-liquidity.conf` and
|
||||
replace every placeholder. The local runtime file is intentionally ignored by
|
||||
Git. Both begin with `FORMAT_VERSION=1`; the example is never loaded as a
|
||||
fallback. Unknown, duplicate and missing settings fail validation.
|
||||
|
||||
| Setting | Contract |
|
||||
| --- | --- |
|
||||
| `LIQUIDITY_WALLET` | Dedicated public Solana wallet address |
|
||||
| `SIGNER_KEY_NAME` | Existing external jup key name controlling exactly that address |
|
||||
| `CLMM_POOL` | Existing EVE/SyrupUSDC Raydium CLMM pool address |
|
||||
| `HARVEST_DISTANCE_PERCENT` | `0 <= h < 100`; `10` means ten percent |
|
||||
| `ITERATION_INTERVAL_SECONDS` | Positive integer delay after an iteration completes; example `300` |
|
||||
| `USDC_RECIPIENT` | Recipient wallet owner address, not a token account; must differ from the liquidity wallet |
|
||||
| `MINIMUM_SYRUPUSDC_RESERVE` | Nonnegative SyrupUSDC amount; example `0` |
|
||||
| `MINIMUM_USDC_RESERVE` | Nonnegative USDC amount; example `0` |
|
||||
| `MAXIMUM_SWAP_SLIPPAGE_BPS` | Explicit Jupiter limit in `[0,10000]` basis points |
|
||||
| `MAXIMUM_WITHDRAWAL_SLIPPAGE_BPS` | Explicit CLMM limit in `[0,10000)` basis points |
|
||||
|
||||
The existing SolanaWallet implementation checks the external jup version and
|
||||
key/address binding during construction. No key material belongs in these files.
|
||||
Fund the wallet with SOL for fees and any destination token-account rent.
|
||||
Reserve amounts must be exactly representable at the corresponding mint's
|
||||
precision; `0.01` SyrupUSDC and `0.5` USDC are permitted operating choices.
|
||||
No hidden positive reserve is added.
|
||||
|
||||
Only after configuring and funding the wallet, explicitly add
|
||||
`evelyn.liquidity.prod` to the existing `[AUTO_START]` section of private
|
||||
`conf/NenjimHub.conf`. Keep Registry first and Journal second, with no blank line
|
||||
inside the active section. Preserve all existing process selections and their
|
||||
order. This process requires no Ticker, Evelyn status, JavaFX or burner process.
|
||||
The checked-in Hub example continues to select only Registry and Journal.
|
||||
|
||||
Deployment configuration and the dedicated signer/wallet, Jupiter and direct
|
||||
Raydium dependencies are constructed only when the selected entry is reached.
|
||||
Registry IDs are `solana.wallet.evelyn-liquidity-prod`,
|
||||
`jupiter.swap.evelyn-liquidity-prod`, `raydium.service.evelyn-liquidity-prod`
|
||||
and `evelyn.liquidity.prod`. Jupiter signs with this wallet. Currency/mint
|
||||
identities come from CIS; token programs and precision come from chain metadata.
|
||||
|
||||
## Iteration and recovery
|
||||
|
||||
A single worker runs immediately on startup and then with fixed delay after
|
||||
completion. Every iteration follows the same three independent steps:
|
||||
|
||||
1. Discover the wallet's position NFTs. For each owned, positive-liquidity
|
||||
position in the configured pool, read fresh on-chain pool state in
|
||||
SyrupUSDC per EVE after discovery and any earlier withdrawals. Withdraw complete
|
||||
liquidity when its normalized **upper** boundary satisfies
|
||||
`U <= P * (1 - h/100)`. Equality qualifies. Both token orientations use
|
||||
exact squared Q64.64 comparisons, without display-price rounding. At
|
||||
`P=15, h=10`, `13–13.5` qualifies and `13.5–14` does not. Withdrawals retain
|
||||
the position and NFT, and return pool-token fees to the same wallet.
|
||||
Each position uses its own fresh state for both eligibility and withdrawal
|
||||
minimums; the price is not reused across positions.
|
||||
2. Read current SyrupUSDC balance and swap the full balance above its reserve
|
||||
into USDC through the confirmed exact-input Jupiter operation.
|
||||
3. Read current USDC balance and send the full balance above its reserve to
|
||||
the recipient owner, creating its ATA at the sender's expense if needed.
|
||||
The transfer submits once and is awaited at `CONFIRMED`.
|
||||
|
||||
Missing/zero balances and balances equal to their reserves are skipped. Amounts
|
||||
are rounded down to mint precision. An unavailable or unusable pool price stops
|
||||
the remaining withdrawals in that iteration while retaining steps 2 and 3. EVE returned as principal/fees and
|
||||
other reward tokens remain in the wallet and never enter the conversion/payout
|
||||
pipeline. All available SyrupUSDC/USDC is eligible, including manual deposits.
|
||||
|
||||
An ordinary position failure does not block other readable positions. An
|
||||
ordinary step failure does not block later independent steps. Logs identify
|
||||
steps, position NFTs, liquidity/token amounts and returned signatures. Unknown
|
||||
outcomes are logged with signatures when available. Subsequent iterations and
|
||||
restarts reread chain state; there is no service journal, attribution, pending
|
||||
signature gate, immediate retry or transaction replay. Empty positions are
|
||||
skipped and can be manually reused. Shutdown interrupts the current operation
|
||||
and prevents following steps; the reference implementation can also be closed
|
||||
by its local owner through `AutoCloseable`.
|
||||
|
||||
## Withdrawal support
|
||||
|
||||
The transaction follows the official
|
||||
[decrease_liquidity_v2 accounts](https://github.com/raydium-io/raydium-clmm/blob/master/programs/amm/src/instructions/decrease_liquidity_v2.rs)
|
||||
and [decrease/minimum-output behavior](https://github.com/raydium-io/raydium-clmm/blob/master/programs/amm/src/instructions/decrease_liquidity.rs),
|
||||
also checked against the
|
||||
[official SDK encoder](https://github.com/raydium-io/raydium-sdk-V2/blob/master/src/raydium/clmm/instrument.ts).
|
||||
Withdrawal and closure are separate actions in
|
||||
[Raydium's liquidity documentation](https://github.com/raydium-io/raydium-docs-v1/blob/main/user-flows/add-remove-liquidity.mdx).
|
||||
|
||||
Raydium validates pool/personal-position discriminators, the owned NFT holding,
|
||||
current complete liquidity, mint precision, vaults and tick arrays before signing.
|
||||
For every pool or reward mint, existing owner token accounts are reused after
|
||||
validating their mint, owner, token program and initialized state, including
|
||||
empty accounts and accounts outside the owner's ATA. Only when no owner holding
|
||||
exists does the withdrawal use or idempotently create its ATA. A failed holdings
|
||||
read aborts rather than creating an additional account. This preserves the
|
||||
existing wallet's single-account-per-mint balance/transfer contract. The
|
||||
transaction requests a 600,000 compute-unit limit.
|
||||
The bitmap extension and initialized reward triplets are included when required.
|
||||
The integer principal math matches Raydium's
|
||||
[tick algorithm](https://github.com/raydium-io/raydium-clmm/blob/master/programs/amm/src/libraries/tick_math.rs).
|
||||
Minimum principal amounts are floored after applying explicit withdrawal
|
||||
slippage. An amount too small to protect at least one raw unit is rejected.
|
||||
No withdrawal always substitutes zero for both minimums.
|
||||
Price movement after the per-position read remains possible before execution;
|
||||
principal minimums protect received amounts, not the harvest-distance threshold.
|
||||
|
||||
The current EVE mint uses Token-2022 metadata pointer/token metadata extensions;
|
||||
SyrupUSDC uses legacy SPL. Read-only mainnet inspection on 2026-10-04 found the
|
||||
EVE/SyrupUSDC pool `EpZXeeShz64DK8HfKnjWPTkmhN7dZe77SjvzNDX8CARq` with decimals
|
||||
8/6, tick spacing 60 and no initialized rewards. Deployment still requires an
|
||||
explicit pool setting and repeats validation. Mint close-authority and metadata/
|
||||
group extensions are supported; other transfer-affecting extensions, including
|
||||
transfer fees/hooks, fail before signing. The withdrawal encoder supports legacy
|
||||
transactions fitting Solana's packet limit and rejects larger account sets.
|
||||
|
||||
## Offline verification
|
||||
|
||||
The Gradle JUnit fixtures exercise exact threshold equality, inversion, raw
|
||||
principal amounts, owner/pool filtering, reserves, independent failures,
|
||||
restart recovery and shutdown. Captured serialized transactions verify account
|
||||
order, full liquidity, minimum output, NFT retention, bitmap/reward accounts and
|
||||
one submission/confirmation attempt. Review regression fixtures also cover price
|
||||
changes during discovery or earlier withdrawals in both token orientations,
|
||||
quote consistency, and reuse/validation of existing pool and reward token accounts
|
||||
under both token programs. All financial dependencies are controlled;
|
||||
no verification submits live withdrawals, swaps or transfers.
|
||||
|
||||
```sh
|
||||
./gradlew compileJava compileTestJava test \
|
||||
--tests '*RaydiumClmm*Test' --tests '*EvelynLiquidityServiceImplTest' \
|
||||
--tests '*NenjimHubLiquidityConfigurationTest'
|
||||
openspec validate 89-add-evelyn-liquidity-service --strict
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Stop the process/JVM and remove its explicit Hub selection to disable operation.
|
||||
Existing positions and wallet balances remain the recovery state.
|
||||
Reference in New Issue
Block a user