From 49df1ac9d0dc8ec126e40243e955385aa3e60b32 Mon Sep 17 00:00:00 2001 From: Minimons Date: Tue, 6 Oct 2026 08:05:37 +0200 Subject: [PATCH] 92: Add per-currency minimum buyback amounts to the Evelyn IOU burner --- docs/Evelyn.md | 25 ++-- .../.openspec.yaml | 2 + .../design.md | 28 ++++ .../proposal.md | 27 ++++ .../specs/evelyn-iou-burner-service/spec.md | 125 ++++++++++++++++++ .../tasks.md | 12 ++ .../verification.md | 25 ++++ .../specs/evelyn-iou-burner-service/spec.md | 62 +++++++-- .../burner/EvelynIOUBurnerService.tjava | 14 +- .../impl/ref/EvelynIOUBurnerServiceImpl.tjava | 109 +++++++-------- .../nenjim/hubd/impl/ref/NenjimHubImpl.tjava | 5 + 11 files changed, 353 insertions(+), 81 deletions(-) create mode 100644 openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/.openspec.yaml create mode 100644 openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/design.md create mode 100644 openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/proposal.md create mode 100644 openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/specs/evelyn-iou-burner-service/spec.md create mode 100644 openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/tasks.md create mode 100644 openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/verification.md diff --git a/docs/Evelyn.md b/docs/Evelyn.md index e649057..5008efe 100644 --- a/docs/Evelyn.md +++ b/docs/Evelyn.md @@ -106,18 +106,25 @@ udnyttes at ikke alle investorer sælger på samme tid, da de har forskellige fo Produktionsprocessen `evelyn.iou-burner.prod` accepterer følgende inputs i rækkefølge: -| Token | Reserve | Maksimum pr. iteration | -| --- | ---: | ---: | -| USDT | 0,1 USDT | 0,25 USDT | -| SyrupUSDC | 0,095 SyrupUSDC | 0,2 SyrupUSDC | +| Token | Reserve | Minimum swap-input | Maksimum pr. iteration | +| --- | ---: | ---: | ---: | +| USDT | 0,1 USDT | 0,05 USDT | 0,25 USDT | +| SyrupUSDC | 0,095 SyrupUSDC | 0,045 SyrupUSDC | 0,2 SyrupUSDC | -Hub'en angiver reserver og maksimummer som `MoneyAmount`. Reservelisten bestemmer -accepterede valutaer og prioritet; maksimummer matches efter valutaens UUID, -uanset maksimumlistens rækkefølge. Hver accepteret valuta skal have præcis ét -positivt maksimum. Begge lister kopieres ved oprettelse af burneren. +Hub'en angiver reserver, minimumsbeløb og maksimummer som `MoneyAmount`. +Reservelisten bestemmer accepterede valutaer og prioritet; minimumsbeløb og +maksimummer matches efter valutaens kanoniske UUID uanset listernes rækkefølge, +navn og symbol. Hver accepteret valuta skal have præcis ét positivt minimum og +maksimum, og minimum må ikke overstige maksimum. Alle tre lister kopieres ved +oprettelse af burneren; tre tomme lister tillader direkte EVE-burn. Burneren vælger det mindste af saldo minus reserve og valutaens eget maksimum, -afrundet ned til mintens præcision. Grænserne angives i tokens: SyrupUSDC er ikke +afrundet ned til mintens præcision. Er dette faktiske beløb under valutaens +minimum, springes inputtet over, og næste input undersøges i samme iteration. +Lighed med minimum accepteres; der rundes aldrig op eller bruges af reserven for +at nå minimum. For eksempel springes 0,000001 spendabel USDT over, så en +SyrupUSDC-saldo på 0,502048 kan give et swap på 0,2 SyrupUSDC. Inputminimummer +begrænser ikke burn af eksisterende EVE. Grænserne angives i tokens: SyrupUSDC er ikke lig med USDC, og 0,2 SyrupUSDC garanterer ikke en bestemt USD-værdi. Der kræves ingen inputtoken/USDC-pris eller nye Ticker-kilder og historikfiler. diff --git a/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/.openspec.yaml b/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/.openspec.yaml new file mode 100644 index 0000000..aa32566 --- /dev/null +++ b/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-06 diff --git a/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/design.md b/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/design.md new file mode 100644 index 0000000..360a9ef --- /dev/null +++ b/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/design.md @@ -0,0 +1,28 @@ +# Design + +## Context + +See proposal.md and issue #92. The constructor currently owns ordered reserve configuration and immutable UUID-indexed maxima. Candidate selection already rounds down before invoking Jupiter; pending recovery precedes selection. + +## Goals / Non-Goals + +**Goals:** Extend existing token amount policy safely with a lower bound on actual attempted inputs. + +**Non-Goals:** Introduce a configuration hierarchy, compatibility bypass, precision rounding for minima, or change execution/recovery behavior. + +## Decisions + +1. Add required minima between reserves and maxima. Reuse one private positive per-UUID amount validator for minima and maxima, copy them to immutable maps with canonical CurrencyType values, and reject minimum > maximum after both maps exist. Reserve order alone retains priority. A duplicated independent validator would unnecessarily duplicate the existing identity/set rules. +2. Compare the unchanged rounded candidate with its minimum using `compareTo`, preserving numeric equality across decimal scales. A pre-round comparison would permit attempted inputs below minimum; rounding upward or reserve top-up would violate existing spend bounds. Minima need not be representable at mint precision: they remain exact lower bounds, and an unachievable minimum simply makes that candidate ineligible. +3. Make production minima explicit MoneyAmount values in the existing Hub factory and update the single current call site. No old-signature overload bypasses the required policy. +4. Keep offline verification temporary, covering issue examples, precision/scale edges, config validation/copying, direct EVE and the prior recovery harness with only its constructor adapted. + +## Risks / Trade-offs + +- Incorrect UUID matching or list reordering → use canonical UUID maps and verify stale display metadata/reordered lists. +- Precision makes a configured minimum unachievable → skip the rounded candidate; never promote amount. +- Constructor signature changes → update the only current construction site and compile all existing main/test sources. + +## Migration Plan + +Implementation adds explicit production configuration; deployment remains a separate task. No persistent data, ticker history or ValueTag migration is required. diff --git a/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/proposal.md b/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/proposal.md new file mode 100644 index 0000000..1515df2 --- /dev/null +++ b/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/proposal.md @@ -0,0 +1,27 @@ +# Proposal + +## Why + +Issue #92 prevents unsuitable tiny buybacks above the reserve from blocking useful later inputs. Token-denominated minima let Evelyn skip those candidates while preserving reserve safety, priority and existing recovery. + +## What Changes + +- **BREAKING** Add a required `List` of per-currency minimum swap inputs to the reference constructor beside reserves and maxima; match canonical UUIDs and copy configuration defensively. +- Reject missing, extra, unknown, duplicate or null minimum identities/values, non-positive minima and minima above their matched maxima. +- Compare the actual downward-rounded candidate with its minimum. Skip smaller candidates and continue input inspection; accept equality without spending reserves or rounding upward. +- Configure production USDT minimum 0.05 and SyrupUSDC minimum 0.045, with current reserve/maxima values and USDT priority retained. +- Update directly affected documentation/specs and perform focused offline verification without permanent tests or live transactions. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `evelyn-iou-burner-service`: Per-input minimum configuration, validation and qualification after precision rounding. + +## Impact + +The burner reference constructor and selection, the Hub production call site, domain/constructor JavaDoc and Evelyn configuration documentation change. Jupiter, Ticker, token discovery, lifecycle, pending swap/burn recovery, notification and ValueTags remain unchanged. Implementation and deployment remain separate. diff --git a/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/specs/evelyn-iou-burner-service/spec.md b/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/specs/evelyn-iou-burner-service/spec.md new file mode 100644 index 0000000..4a0b036 --- /dev/null +++ b/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/specs/evelyn-iou-burner-service/spec.md @@ -0,0 +1,125 @@ +# Spec Delta + +## MODIFIED Requirements + +### Requirement: Ordered AssetAZ input configuration +The authoritative constructor SHALL accept a non-null ordered `List` named `inputCurrencyMinimumReserves`, defensively copy it without changing order, and interpret inclusion as acceptance, list position as priority, each `currencyType().id()` as the authoritative AssetAZ identity, and each `amount()` as the minimum balance that must remain. It SHALL ignore supplied currency name and symbol metadata by resolving each UUID through the injected Currency Identity Service and using the canonical returned currency for processing. The constructor SHALL also accept a non-null `List` named `inputCurrencyMaximumSwapAmounts`, defensively copy its values, and match each maximum to an accepted input by canonical currency UUID, independently of maximum-list order or supplied metadata. Each maximum SHALL be expressed in that same currency's own units. The constructor SHALL additionally accept `inputCurrencyMinimumSwapAmounts`, defensively copy its values and match strictly positive token-denominated minima by canonical UUID independently of order/name/symbol. Empty reserves, minima and maxima lists SHALL be valid and SHALL still permit directly deposited EVE to be burned. + +#### Scenario: Stale configured metadata +- **WHEN** a configured `MoneyAmount` carries a known UUID but stale name or symbol metadata +- **THEN** the service processes the current canonical currency resolved for that UUID + +#### Scenario: Priority is preserved +- **WHEN** multiple configured entries have rounded amounts at/above their own minimum and usable Solana data +- **THEN** the earliest entry in the defensively copied list is the only input selected in that iteration + +#### Scenario: Empty input list +- **WHEN** no buyback inputs are configured but the wallet contains EVE +- **THEN** the service skips input selection and still evaluates the complete EVE balance for burning + +#### Scenario: Maximum list order is independent +- **WHEN** maxima are supplied in a different order from the reserve list or carry stale display metadata for known UUIDs +- **THEN** each input receives the maximum for its UUID and reserve-list priority is preserved + +#### Scenario: Caller changes its configuration lists +- **WHEN** a caller changes any supplied list after construction +- **THEN** the accepted currencies, their priority, reserves, minima and maxima remain as validated during construction + +#### Scenario: Minimum list order and display metadata are independent +- **WHEN** minima are reordered or use stale names/symbols for known canonical UUIDs +- **THEN** each input receives its UUID's minimum and only reserve-list order controls priority + +### Requirement: Complete constructor validation +Construction SHALL require all injected services, all three input configuration lists, and durations to be non-null; every list entry, entry amount, entry currency, and currency UUID to be non-null; each reserve to be zero or greater; each minimum and maximum to be strictly positive; each minimum to be no greater than its matched maximum; each UUID to be known to the injected Currency Identity Service; UUIDs to be unique within each list; both minimum and maximum UUID sets to exactly equal the accepted reserve UUID set; and EVE not to be an input currency. It SHALL require slippage to be from 0 through 10000 basis points inclusive, `maximumPriceAge` to be greater than zero milliseconds, and both finalization timeout and iteration interval to be positive and non-zero. + +Every configured non-SOL UUID SHALL resolve through reverse CIS lookup to exactly one external reference in the `solana-mint` namespace, and that reference's external ID SHALL be non-null and non-blank. Construction SHALL fail for a missing, ambiguous, null, or blank non-SOL mint representation. These burner-specific rules SHALL NOT change the general-purpose `MoneyAmount` contract. + +#### Scenario: Valid configuration +- **WHEN** every dependency and value is valid, reserves are non-negative, every accepted input has exactly one positive minimum and maximum with minimum <= maximum, UUIDs are distinct and known within each list, EVE is absent, and every non-SOL input has one non-blank Solana mint reference +- **THEN** construction succeeds and preserves the configured priority order + +#### Scenario: Invalid reserve or operating limit +- **WHEN** a reserve is negative or null, a token maximum is null or non-positive, slippage is outside its inclusive range, maximum price age is non-positive, or either duration is null or non-positive +- **THEN** construction fails before a worker or transaction is created + +#### Scenario: Invalid input identity +- **WHEN** an entry or its currency or UUID is null, a UUID is unknown or duplicated, or the EVE UUID is configured as input +- **THEN** construction fails with a diagnostic identifying the invalid configuration + +#### Scenario: Non-SOL representation is not unique and usable +- **WHEN** a configured non-SOL currency has zero or multiple `solana-mint` references or its sole external ID is null or blank +- **THEN** construction fails without adding mint or token-program selection to Evelyn's configuration model + +#### Scenario: Maximum configuration is incomplete or ambiguous +- **WHEN** a maximum is missing for an accepted input, duplicated by UUID, or supplied for an unaccepted currency +- **THEN** construction fails before a worker or transaction is created + +#### Scenario: Invalid maximum identity or value +- **WHEN** a maximum entry, amount, currency or UUID is null, or its UUID is unknown or resolves to an invalid canonical identity +- **THEN** construction fails with a diagnostic identifying the invalid maximum configuration + +#### Scenario: Invalid minimum configuration +- **WHEN** the minimum list or an entry, amount, currency or UUID is null; a UUID is unknown, duplicated, extra or missing; a minimum is non-positive; or it exceeds that UUID's maximum +- **THEN** construction fails before a worker or transaction is created + +#### Scenario: Minimum equals maximum +- **WHEN** a strictly positive minimum is numerically equal to its matched maximum +- **THEN** that amount configuration is accepted without requiring equal decimal scales + +#### Scenario: Direct EVE only configuration +- **WHEN** all three configuration lists are empty +- **THEN** construction succeeds and direct EVE remains eligible for burning + +### Requirement: Exact token-capped amount and per-currency reserve +The service SHALL select the lesser of positive `balance - minimumReserve` and the input UUID's maximum token amount, then round downward to mint precision. It SHALL skip non-positive results and actual rounded amounts below that UUID's minimum, continuing to later inputs in the same iteration without counting a swap attempt. Equality SHALL qualify. It SHALL never round upward, top up from reserves or exceed either existing bound. Input selection SHALL require no input/USDC Ticker observation. Minima and maxima SHALL constrain buyback inputs only; EVE burns SHALL remain uncapped and unaffected. Native SOL reserves SHALL use observed balance without estimating future fees. + +#### Scenario: SyrupUSDC maximum is lower than spendable balance +- **WHEN** SyrupUSDC balance is 0.5, its reserve is 0.095, its minimum is 0.045 and its maximum is 0.2 +- **THEN** 0.2 SyrupUSDC is selected without requesting a SyrupUSDC/USDC observation + +#### Scenario: SyrupUSDC spendable balance is lower than the maximum +- **WHEN** SyrupUSDC balance is 0.245, its reserve is 0.095, its minimum is 0.045 and its maximum is 0.2 +- **THEN** 0.150 SyrupUSDC is selected before downward mint-precision rounding + +#### Scenario: Reserve consumes the balance +- **WHEN** the current balance is equal to or below that entry's minimum reserve +- **THEN** no amount of that currency is intentionally swapped + +#### Scenario: USDT maximum preserves the production bound +- **WHEN** USDT balance is 0.35, its reserve is 0.1, its minimum is 0.05 and its maximum is 0.25 +- **THEN** 0.25 USDT is selected before downward mint-precision rounding + +#### Scenario: Precision rounding never increases the spend +- **WHEN** the lesser of spendable balance and maximum is 0.1509999, the mint supports six decimals and its minimum is no greater than 0.150999 +- **THEN** the service selects 0.150999, and if downward rounding instead produces zero the input is skipped + +#### Scenario: Native transaction fees +- **WHEN** native SOL is selected with a configured reserve +- **THEN** the service enforces the reserve against the observed balance only and does not estimate fees, even though later fees may reduce the final balance or cause execution to fail + +#### Scenario: USDT below and at its minimum +- **WHEN** USDT reserve is 0.1 and minimum is 0.05 +- **THEN** balances 0.14 and 0.149999 are skipped, while balance 0.15 selects exactly 0.05 + +#### Scenario: Tiny first input permits useful later input +- **WHEN** USDT balance is 0.100001 with reserve 0.1/minimum 0.05, and SyrupUSDC balance is 0.502048 with reserve 0.095/minimum 0.045/maximum 0.2 +- **THEN** the USDT remainder is skipped and 0.2 SyrupUSDC is the only attempted swap + +#### Scenario: SyrupUSDC minimum boundary +- **WHEN** SyrupUSDC reserve is 0.095 and minimum is 0.045 +- **THEN** balance 0.14 selects exactly 0.045 while 0.139999 is skipped + +#### Scenario: Actual rounded candidate falls below a fractional minimum +- **WHEN** six-decimal input precision rounds candidate 0.0500009 down to 0.050000 and its minimum is 0.0500001 +- **THEN** the candidate is skipped without rounding up or using reserves + +#### Scenario: Input minima do not suppress direct EVE burn +- **WHEN** every configured input is below its minimum but the wallet contains EVE with a usable EVE/USDT price +- **THEN** the complete EVE balance is still evaluated and burned by the existing rules + +### Requirement: Production burner accepts USDT and SyrupUSDC +The production Hub SHALL configure USDT first with reserve 0.1, minimum input 0.05 and maximum input 0.25 USDT, and SyrupUSDC second with reserve 0.095, minimum input 0.045 and maximum input 0.2 SyrupUSDC. These SHALL be token-denominated limits without a USD-value guarantee. The existing burner wallet, Jupiter route selection, registration, hourly interval, slippage, EVE/USDT freshness, confirmation settings and burn recovery behavior SHALL be preserved without new Ticker sources or history storage. + +#### Scenario: Production inputs and settings +- **WHEN** the Hub constructs the production burner +- **THEN** it supplies the exact reserves, minima and maxima as MoneyAmount values in USDT-then-SyrupUSDC priority and retains the other existing production arguments diff --git a/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/tasks.md b/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/tasks.md new file mode 100644 index 0000000..549682f --- /dev/null +++ b/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/tasks.md @@ -0,0 +1,12 @@ +# Tasks + +## 1. Configure and qualify minimum buybacks + +- [x] 1.1 Add immutable canonical-UUID minima and constructor validation; verify reordered/copied configuration, invalid values and minimum <= maximum with a temporary offline harness. +- [x] 1.2 Apply minima to actual rounded candidates and update domain/constructor JavaDoc; verify issue examples, precision/scale boundaries, later-input continuation and unchanged reserve/max bounds offline. +- [x] 1.3 Add exact production minima and update Evelyn configuration documentation; review USDT priority and all other unchanged production arguments. + +## 2. Verify and finalize integration + +- [x] 2.1 Compile existing main/test sources and run focused temporary selection/config checks plus the adapted #91 recovery harness; record results and review unchanged execution, Ticker, burn and recovery code. +- [x] 2.2 Strictly validate the change, sync and validate the main spec, check whitespace and archive the completed change after confirming all requirements/tasks are satisfied. diff --git a/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/verification.md b/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/verification.md new file mode 100644 index 0000000..0d5566b --- /dev/null +++ b/openspec/changes/archive/2026-10-06-92-add-minimum-buyback-amounts/verification.md @@ -0,0 +1,25 @@ +# Verification + +Verified on 2026-10-06 with JDK `/usr/local/software/java/jfx-25`. + +## Compilation and controlled checks + +- Parent compilation: `JAVA_HOME=/usr/local/software/java/jfx-25 ./gradlew compileJava compileTestJava --offline --no-daemon` → **BUILD SUCCESSFUL in 6s**; `detagMain`, `compileJava` and `compileTestJava` all executed against the final source. +- Temporary harnesses compiled with `javac --enable-preview --release 25 -cp 'build/classes/java/main:build/libs/*' -d /tmp/evelyn-issue92 /tmp/evelyn-issue92/Issue92Verification.java /tmp/evelyn-issue92/Issue91Verification.java`. +- `java --enable-preview -cp '/tmp/evelyn-issue92:build/classes/java/main:build/libs/*' Issue92Verification` → **PASS: 89 focused minimum-amount assertions**. +- The same invocation for `Issue91Verification` → **PASS: 102 recovery assertions**. Its original temporary file is preserved in `/tmp/evelyn-issue91`; the copy changes only the burner constructor call to add SyrupUSDC minimum 0.045. +- All wallet, swap, HTTP and pricing dependencies are controlled. No live financial operation or real RPC request occurs. + +The #92 harness covers every issue example, equality across decimal scales, minimum == maximum, fractional minima above rounded candidates, downward rounding of maximum, zero after precision rounding, next-input continuation, canonical UUID matching with reordered/stale display metadata, caller mutations of all three copied lists, invalid minima, preserved maximum validation, direct EVE when inputs are below minima or all three lists are empty, stale EVE price and at most one attempted qualifying swap even after failure. + +An initial temporary null-UUID fixture attempted to construct an invalid CurrencyType, whose existing constructor already rejects null IDs before burner configuration. The check was moved to that proper pre-burner rejection point, and the #92 harness rerun passed. No production fix was required. + +## Review and scope + +Parent independent source/documentation review found no material findings. Static diff review confirms the changes are confined to the reference constructor/amount configuration validator, rounded-candidate qualification, domain JavaDoc, production minima and Evelyn configuration docs. Reserves/maxima, USDT priority, pending recovery, Jupiter, Ticker, token discovery, burn/notification and lifecycle are preserved. No ValueTag, deployment file or permanent automated test changed. + +Strict validation of the active change and synced main specification passed. All four delta requirements match the main spec exactly; unrelated requirements/scenarios remain intact. Existing long-requirement informational hints remain under the rule that modified requirement blocks stay whole. `git diff --check` passed. All five tasks and four planning artifacts are complete. + +After archival, `openspec list --json` reports no active changes. `openspec validate --archived --json --report findings` passed for all 30 archived changes, with 0 failures and 0 findings. + +No live integration, deployment, push or Gitea write was performed. Temporary harnesses remain outside the repository; no permanent automated tests were added. diff --git a/openspec/specs/evelyn-iou-burner-service/spec.md b/openspec/specs/evelyn-iou-burner-service/spec.md index 264697a..b6f7fa4 100644 --- a/openspec/specs/evelyn-iou-burner-service/spec.md +++ b/openspec/specs/evelyn-iou-burner-service/spec.md @@ -16,14 +16,14 @@ The system SHALL expose an empty lifecycle-free `EvelynIOUBurnerService` under ` - **THEN** it uses existing `MoneyAmount` values rather than burner-specific Solana, SPL-token, or input-asset types ### Requirement: Ordered AssetAZ input configuration -The authoritative constructor SHALL accept a non-null ordered `List` named `inputCurrencyMinimumReserves`, defensively copy it without changing order, and interpret inclusion as acceptance, list position as priority, each `currencyType().id()` as the authoritative AssetAZ identity, and each `amount()` as the minimum balance that must remain. It SHALL ignore supplied currency name and symbol metadata by resolving each UUID through the injected Currency Identity Service and using the canonical returned currency for processing. The constructor SHALL also accept a non-null `List` named `inputCurrencyMaximumSwapAmounts`, defensively copy its values, and match each maximum to an accepted input by canonical currency UUID, independently of maximum-list order or supplied metadata. Each maximum SHALL be expressed in that same currency's own units. Empty reserves and maxima lists SHALL be valid and SHALL still permit directly deposited EVE to be burned. +The authoritative constructor SHALL accept a non-null ordered `List` named `inputCurrencyMinimumReserves`, defensively copy it without changing order, and interpret inclusion as acceptance, list position as priority, each `currencyType().id()` as the authoritative AssetAZ identity, and each `amount()` as the minimum balance that must remain. It SHALL ignore supplied currency name and symbol metadata by resolving each UUID through the injected Currency Identity Service and using the canonical returned currency for processing. The constructor SHALL also accept a non-null `List` named `inputCurrencyMaximumSwapAmounts`, defensively copy its values, and match each maximum to an accepted input by canonical currency UUID, independently of maximum-list order or supplied metadata. Each maximum SHALL be expressed in that same currency's own units. The constructor SHALL additionally accept `inputCurrencyMinimumSwapAmounts`, defensively copy its values and match strictly positive token-denominated minima by canonical UUID independently of order/name/symbol. Empty reserves, minima and maxima lists SHALL be valid and SHALL still permit directly deposited EVE to be burned. #### Scenario: Stale configured metadata - **WHEN** a configured `MoneyAmount` carries a known UUID but stale name or symbol metadata - **THEN** the service processes the current canonical currency resolved for that UUID #### Scenario: Priority is preserved -- **WHEN** multiple configured entries have spendable balances and usable Solana data +- **WHEN** multiple configured entries have rounded amounts at/above their own minimum and usable Solana data - **THEN** the earliest entry in the defensively copied list is the only input selected in that iteration #### Scenario: Empty input list @@ -35,16 +35,20 @@ The authoritative constructor SHALL accept a non-null ordered `List - **THEN** each input receives the maximum for its UUID and reserve-list priority is preserved #### Scenario: Caller changes its configuration lists -- **WHEN** a caller changes either supplied list after construction -- **THEN** the accepted currencies, their priority, reserves and maxima remain as validated during construction +- **WHEN** a caller changes any supplied list after construction +- **THEN** the accepted currencies, their priority, reserves, minima and maxima remain as validated during construction + +#### Scenario: Minimum list order and display metadata are independent +- **WHEN** minima are reordered or use stale names/symbols for known canonical UUIDs +- **THEN** each input receives its UUID's minimum and only reserve-list order controls priority ### Requirement: Complete constructor validation -Construction SHALL require all injected services, both input configuration lists, and durations to be non-null; every list entry, entry amount, entry currency, and currency UUID to be non-null; each reserve to be zero or greater; each maximum to be strictly positive; each UUID to be known to the injected Currency Identity Service; UUIDs to be unique within each list; the maximum UUID set to exactly equal the accepted reserve UUID set; and EVE not to be an input currency. It SHALL require slippage to be from 0 through 10000 basis points inclusive, `maximumPriceAge` to be greater than zero milliseconds, and both finalization timeout and iteration interval to be positive and non-zero. +Construction SHALL require all injected services, all three input configuration lists, and durations to be non-null; every list entry, entry amount, entry currency, and currency UUID to be non-null; each reserve to be zero or greater; each minimum and maximum to be strictly positive; each minimum to be no greater than its matched maximum; each UUID to be known to the injected Currency Identity Service; UUIDs to be unique within each list; both minimum and maximum UUID sets to exactly equal the accepted reserve UUID set; and EVE not to be an input currency. It SHALL require slippage to be from 0 through 10000 basis points inclusive, `maximumPriceAge` to be greater than zero milliseconds, and both finalization timeout and iteration interval to be positive and non-zero. Every configured non-SOL UUID SHALL resolve through reverse CIS lookup to exactly one external reference in the `solana-mint` namespace, and that reference's external ID SHALL be non-null and non-blank. Construction SHALL fail for a missing, ambiguous, null, or blank non-SOL mint representation. These burner-specific rules SHALL NOT change the general-purpose `MoneyAmount` contract. #### Scenario: Valid configuration -- **WHEN** every dependency and value is valid, reserves are non-negative, every accepted input has exactly one positive maximum, UUIDs are distinct and known within each list, EVE is absent, and every non-SOL input has one non-blank Solana mint reference +- **WHEN** every dependency and value is valid, reserves are non-negative, every accepted input has exactly one positive minimum and maximum with minimum <= maximum, UUIDs are distinct and known within each list, EVE is absent, and every non-SOL input has one non-blank Solana mint reference - **THEN** construction succeeds and preserves the configured priority order #### Scenario: Invalid reserve or operating limit @@ -67,6 +71,18 @@ Every configured non-SOL UUID SHALL resolve through reverse CIS lookup to exactl - **WHEN** a maximum entry, amount, currency or UUID is null, or its UUID is unknown or resolves to an invalid canonical identity - **THEN** construction fails with a diagnostic identifying the invalid maximum configuration +#### Scenario: Invalid minimum configuration +- **WHEN** the minimum list or an entry, amount, currency or UUID is null; a UUID is unknown, duplicated, extra or missing; a minimum is non-positive; or it exceeds that UUID's maximum +- **THEN** construction fails before a worker or transaction is created + +#### Scenario: Minimum equals maximum +- **WHEN** a strictly positive minimum is numerically equal to its matched maximum +- **THEN** that amount configuration is accepted without requiring equal decimal scales + +#### Scenario: Direct EVE only configuration +- **WHEN** all three configuration lists are empty +- **THEN** construction succeeds and direct EVE remains eligible for burning + ### Requirement: Fixed Evelyn domain values and internally resolved integration values The service SHALL fix the EVE mint as `meveYG2iXYSkgSUn1T1uxcthH1EGMZdRHGgCntXZA3Y`, construct the EVE/USDT notification-price pair from canonical CIS resolutions of the stable EVE and USDT UUIDs, and use fixed notification wording. Native SOL SHALL be identified only by the stable SOL UUID; when sent to Jupiter, its input mint SHALL be the canonical CIS Solana-mint reference `So11111111111111111111111111111111111111112`. Mint addresses, token programs, price pairs, notification wording, and a separate SOL reserve SHALL NOT be constructor configuration. @@ -215,14 +231,14 @@ The EVE amount SHALL use plain decimal notation without unnecessary trailing zer - **THEN** the service logs a non-secret failure, clears completed state, and never retries either notification or burn ### Requirement: Exact token-capped amount and per-currency reserve -The service SHALL select the lesser of positive `balance - minimumReserve` and the input UUID's maximum token amount, then round downward to mint precision. It SHALL skip non-positive results and SHALL never exceed either bound through rounding. Input selection SHALL require no input/USDC Ticker observation. Maxima SHALL limit buybacks only; EVE burns SHALL remain uncapped. Native SOL reserves SHALL use observed balance without estimating future fees. +The service SHALL select the lesser of positive `balance - minimumReserve` and the input UUID's maximum token amount, then round downward to mint precision. It SHALL skip non-positive results and actual rounded amounts below that UUID's minimum, continuing to later inputs in the same iteration without counting a swap attempt. Equality SHALL qualify. It SHALL never round upward, top up from reserves or exceed either existing bound. Input selection SHALL require no input/USDC Ticker observation. Minima and maxima SHALL constrain buyback inputs only; EVE burns SHALL remain uncapped and unaffected. Native SOL reserves SHALL use observed balance without estimating future fees. #### Scenario: SyrupUSDC maximum is lower than spendable balance -- **WHEN** SyrupUSDC balance is 0.5, its reserve is 0.095, and its maximum is 0.2 +- **WHEN** SyrupUSDC balance is 0.5, its reserve is 0.095, its minimum is 0.045 and its maximum is 0.2 - **THEN** 0.2 SyrupUSDC is selected without requesting a SyrupUSDC/USDC observation #### Scenario: SyrupUSDC spendable balance is lower than the maximum -- **WHEN** SyrupUSDC balance is 0.245, its reserve is 0.095, and its maximum is 0.2 +- **WHEN** SyrupUSDC balance is 0.245, its reserve is 0.095, its minimum is 0.045 and its maximum is 0.2 - **THEN** 0.150 SyrupUSDC is selected before downward mint-precision rounding #### Scenario: Reserve consumes the balance @@ -230,23 +246,43 @@ The service SHALL select the lesser of positive `balance - minimumReserve` and t - **THEN** no amount of that currency is intentionally swapped #### Scenario: USDT maximum preserves the production bound -- **WHEN** USDT balance is 0.35, its reserve is 0.1, and its maximum is 0.25 +- **WHEN** USDT balance is 0.35, its reserve is 0.1, its minimum is 0.05 and its maximum is 0.25 - **THEN** 0.25 USDT is selected before downward mint-precision rounding #### Scenario: Precision rounding never increases the spend -- **WHEN** the lesser of spendable balance and maximum is 0.1509999 and the mint supports six decimals +- **WHEN** the lesser of spendable balance and maximum is 0.1509999, the mint supports six decimals and its minimum is no greater than 0.150999 - **THEN** the service selects 0.150999, and if downward rounding instead produces zero the input is skipped #### Scenario: Native transaction fees - **WHEN** native SOL is selected with a configured reserve - **THEN** the service enforces the reserve against the observed balance only and does not estimate fees, even though later fees may reduce the final balance or cause execution to fail +#### Scenario: USDT below and at its minimum +- **WHEN** USDT reserve is 0.1 and minimum is 0.05 +- **THEN** balances 0.14 and 0.149999 are skipped, while balance 0.15 selects exactly 0.05 + +#### Scenario: Tiny first input permits useful later input +- **WHEN** USDT balance is 0.100001 with reserve 0.1/minimum 0.05, and SyrupUSDC balance is 0.502048 with reserve 0.095/minimum 0.045/maximum 0.2 +- **THEN** the USDT remainder is skipped and 0.2 SyrupUSDC is the only attempted swap + +#### Scenario: SyrupUSDC minimum boundary +- **WHEN** SyrupUSDC reserve is 0.095 and minimum is 0.045 +- **THEN** balance 0.14 selects exactly 0.045 while 0.139999 is skipped + +#### Scenario: Actual rounded candidate falls below a fractional minimum +- **WHEN** six-decimal input precision rounds candidate 0.0500009 down to 0.050000 and its minimum is 0.0500001 +- **THEN** the candidate is skipped without rounding up or using reserves + +#### Scenario: Input minima do not suppress direct EVE burn +- **WHEN** every configured input is below its minimum but the wallet contains EVE with a usable EVE/USDT price +- **THEN** the complete EVE balance is still evaluated and burned by the existing rules + ### Requirement: Production burner accepts USDT and SyrupUSDC -The production Hub SHALL configure USDT first with reserve 0.1 and maximum input 0.25 USDT, and SyrupUSDC second with reserve 0.095 and maximum input 0.2 SyrupUSDC. These SHALL be token-denominated limits without a USD-value guarantee. The existing burner wallet, Jupiter route selection, registration, hourly interval, slippage, EVE/USDT freshness, confirmation settings and burn recovery behavior SHALL be preserved without new Ticker sources or history storage. +The production Hub SHALL configure USDT first with reserve 0.1, minimum input 0.05 and maximum input 0.25 USDT, and SyrupUSDC second with reserve 0.095, minimum input 0.045 and maximum input 0.2 SyrupUSDC. These SHALL be token-denominated limits without a USD-value guarantee. The existing burner wallet, Jupiter route selection, registration, hourly interval, slippage, EVE/USDT freshness, confirmation settings and burn recovery behavior SHALL be preserved without new Ticker sources or history storage. #### Scenario: Production inputs and settings - **WHEN** the Hub constructs the production burner -- **THEN** it supplies the two exact reserves and maxima as MoneyAmount values in USDT-then-SyrupUSDC priority and retains the other existing production arguments +- **THEN** it supplies the exact reserves, minima and maxima as MoneyAmount values in USDT-then-SyrupUSDC priority and retains the other existing production arguments ### Requirement: Pending swaps reconcile before new input selection After pending-burn priority, a retained swap SHALL be checked read-only by its original signature at CONFIRMED or stronger in a bounded operation. Success, on-chain failure or demonstrated non-executed expiry SHALL clear it and permit newly read balances to drive at most one new swap. Unknown, absent without expiry proof or I/O outcomes SHALL remain pending without a new swap while allowing existing EVE burns. Interruption SHALL propagate with pending state intact. diff --git a/src/main/tjava/com/fanitas/evelyn/service/burner/EvelynIOUBurnerService.tjava b/src/main/tjava/com/fanitas/evelyn/service/burner/EvelynIOUBurnerService.tjava index 17bd6e0..27cbe9b 100644 --- a/src/main/tjava/com/fanitas/evelyn/service/burner/EvelynIOUBurnerService.tjava +++ b/src/main/tjava/com/fanitas/evelyn/service/burner/EvelynIOUBurnerService.tjava @@ -13,10 +13,11 @@ import java.util.List; * {@link MoneyAmount#currencyType() currency type}'s stable AssetAZ UUID is * the authoritative identity, and each {@link MoneyAmount#amount() amount} is * the minimum balance that must remain. List order is currency priority. - * Each accepted input has exactly one strictly positive maximum buyback amount - * supplied as another list of {@link MoneyAmount} values, matched by UUID - * independently of list order. Both configurations are copied defensively. - * Maxima are expressed in the input currency's own units, without guaranteeing + * Each accepted input has exactly one strictly positive minimum and maximum + * buyback amount supplied as two further lists of {@link MoneyAmount} values, + * matched by UUID independently of list order/name/symbol. The minimum must not + * exceed its maximum. All three configurations are copied defensively. + * Minima and maxima are expressed in the input currency's own units, without guaranteeing * a particular USD value. * Solana mint addresses and token programs are integration details resolved * internally through the Currency Identity Service and Solana and are not @@ -41,7 +42,10 @@ import java.util.List; *
  • Select at most that input's configured token maximum, rounded down * to the input currency's supported precision. Native SOL uses its native * balance and nine-decimal precision; no future transaction fee is - * estimated when applying its reserve.
  • + * estimated when applying its reserve. Skip actual rounded candidates + * below that input's minimum and continue to later inputs in the same + * iteration; equality qualifies. Never round upward or use the reserve + * to reach a minimum. Input minima do not suppress EVE burns. *
  • Attempt no more than one exact-input Jupiter swap. Normal return is * already {@code CONFIRMED}; it is not awaited a second time. A definitive * failure or proven pre-submission error permits a later iteration to try diff --git a/src/main/tjava/com/fanitas/evelyn/service/burner/impl/ref/EvelynIOUBurnerServiceImpl.tjava b/src/main/tjava/com/fanitas/evelyn/service/burner/impl/ref/EvelynIOUBurnerServiceImpl.tjava index 2dd2f37..d7c1cdd 100644 --- a/src/main/tjava/com/fanitas/evelyn/service/burner/impl/ref/EvelynIOUBurnerServiceImpl.tjava +++ b/src/main/tjava/com/fanitas/evelyn/service/burner/impl/ref/EvelynIOUBurnerServiceImpl.tjava @@ -78,6 +78,13 @@ public final class EvelynIOUBurnerServiceImpl * that input currency's own units, without an input-price conversion or * a guarantee about its USD value.

    * + *

    {@code inputCurrencyMinimumSwapAmounts} follows the same canonical + * UUID matching and immutable-copy rules. Every accepted input needs one + * strictly positive minimum no greater than its maximum. A rounded + * candidate below the minimum is skipped so later inputs remain eligible; + * equality qualifies without rounding up or spending the reserve. Three + * empty input lists permit direct-EVE-only burning.

    + * * @param burnerWallet dedicated wallet whose balances are inspected and * whose complete EVE balance is burned * @param swapService exact-input Jupiter service; normal return already @@ -94,6 +101,10 @@ public final class EvelynIOUBurnerServiceImpl * non-null reserve amount greater than or equal to zero; every * non-SOL UUID must have exactly one non-blank CIS * {@code solana-mint} external reference + * @param inputCurrencyMinimumSwapAmounts exactly one strictly positive + * token-denominated buyback minimum per accepted input UUID, no + * greater than its maximum; qualification uses the actual rounded + * input amount and does not restrict burns * @param inputCurrencyMaximumSwapAmounts exactly one strictly positive * token-denominated buyback maximum per accepted input UUID; * it does not limit burns @@ -106,14 +117,16 @@ public final class EvelynIOUBurnerServiceImpl * @param iterationInterval strictly positive fixed delay measured from one * completed iteration to the next * @throws NullPointerException if an injected service, configuration - * object, list entry, reserve or maximum amount, + * object, list entry, reserve, minimum or maximum amount, * configured currency, or UUID is null * @throws IllegalArgumentException if a reserve or operating limit is out of * range, a duration is non-positive, a * UUID is unknown or duplicated, EVE is * configured as input, or a non-SOL input * lacks exactly one usable Solana mint, - * or maximum UUIDs do not match inputs + * or minimum/maximum UUIDs do not match inputs, + * a swap bound is non-positive, or a + * minimum exceeds its maximum */ public EvelynIOUBurnerServiceImpl( @NotNull SolanaWallet burnerWallet, @@ -123,6 +136,7 @@ public final class EvelynIOUBurnerServiceImpl @NotNull CurrencyIdentityService currencyIdentityService, @NotNull BoundNotificationService notificationService, @NotNull List inputCurrencyMinimumReserves, + @NotNull List inputCurrencyMinimumSwapAmounts, @NotNull List inputCurrencyMaximumSwapAmounts, int maximumSlippageBps, ΩmilliSecondsΩ maximumPriceAge, @@ -204,9 +218,19 @@ public final class EvelynIOUBurnerServiceImpl this.inputCurrencyMinimumReserves = validateAndCopyInputs( inputCurrencyMinimumReserves ); - this.inputCurrencyMaximumSwapAmounts = validateAndCopyMaximums( - inputCurrencyMaximumSwapAmounts + this.inputCurrencyMaximumSwapAmounts = validateAndCopySwapAmounts( + inputCurrencyMaximumSwapAmounts, "inputCurrencyMaximumSwapAmounts" ); + this.inputCurrencyMinimumSwapAmounts = validateAndCopySwapAmounts( + inputCurrencyMinimumSwapAmounts, "inputCurrencyMinimumSwapAmounts" + ); + for (Map.Entry minimum : this.inputCurrencyMinimumSwapAmounts.entrySet()) { + if (minimum.getValue().amount().compareTo( + this.inputCurrencyMaximumSwapAmounts.get(minimum.getKey()).amount()) > 0) { + throw new IllegalArgumentException( + "Minimum swap amount exceeds maximum for input currency UUID: " + minimum.getKey()); + } + } } @Override @@ -385,7 +409,8 @@ public final class EvelynIOUBurnerServiceImpl ΩAmountΩ amountToSwap = availableForSwap .min(maximumInputAmount) .setScale(decimals, RoundingMode.DOWN); - if (amountToSwap.signum() <= 0) { + if (amountToSwap.signum() <= 0 + || amountToSwap.compareTo(inputCurrencyMinimumSwapAmounts.get(currencyId).amount()) < 0) { continue; } @@ -859,70 +884,45 @@ public final class EvelynIOUBurnerServiceImpl return List.copyOf(validatedInputs); } - private Map validateAndCopyMaximums( - List configuredMaximums + private Map validateAndCopySwapAmounts( + List configuredAmounts, + String configurationName ) { - Objects.requireNonNull( - configuredMaximums, - "inputCurrencyMaximumSwapAmounts" - ); + Objects.requireNonNull(configuredAmounts, configurationName); Set acceptedCurrencyIds = new HashSet<>(); for (MoneyAmount minimumReserve : inputCurrencyMinimumReserves) { acceptedCurrencyIds.add(minimumReserve.currencyType().id()); } - Map validatedMaximums = new HashMap<>(); + Map validatedAmounts = new HashMap<>(); - for (int index = 0; index < configuredMaximums.size(); index++) { - MoneyAmount maximum = Objects.requireNonNull( - configuredMaximums.get(index), - "inputCurrencyMaximumSwapAmounts[" + index + "]" - ); - Objects.requireNonNull( - maximum.amount(), - "inputCurrencyMaximumSwapAmounts[" + index + "].amount" - ); - if (maximum.amount().signum() <= 0) { - throw new IllegalArgumentException( - "Input currency maximum swap amount must be greater " - + "than zero at index " + index - ); + for (int index = 0; index < configuredAmounts.size(); index++) { + String entryName = configurationName + "[" + index + "]"; + MoneyAmount bound = Objects.requireNonNull(configuredAmounts.get(index), entryName); + Objects.requireNonNull(bound.amount(), entryName + ".amount"); + if (bound.amount().signum() <= 0) { + throw new IllegalArgumentException(entryName + " amount must be greater than zero"); } - CurrencyType configuredCurrency = Objects.requireNonNull( - maximum.currencyType(), - "inputCurrencyMaximumSwapAmounts[" + index - + "].currencyType" - ); - UUID currencyId = Objects.requireNonNull( - configuredCurrency.id(), - "inputCurrencyMaximumSwapAmounts[" + index - + "].currencyType.id" - ); - requireCanonicalCurrency(currencyId, "configured input maximum"); - + bound.currencyType(), entryName + ".currencyType"); + UUID currencyId = Objects.requireNonNull(configuredCurrency.id(), entryName + ".currencyType.id"); + CurrencyType canonicalCurrency = requireCanonicalCurrency(currencyId, configurationName); if (!acceptedCurrencyIds.contains(currencyId)) { - throw new IllegalArgumentException( - "Maximum swap amount configured for unaccepted input " - + "currency UUID: " + currencyId - ); + throw new IllegalArgumentException(configurationName + + " configured for unaccepted input currency UUID: " + currencyId); } - if (validatedMaximums.putIfAbsent(currencyId, maximum) != null) { - throw new IllegalArgumentException( - "Duplicate maximum swap amount currency UUID: " - + currencyId - ); + if (validatedAmounts.putIfAbsent(currencyId, + new MoneyAmount(bound.amount(), canonicalCurrency)) != null) { + throw new IllegalArgumentException("Duplicate " + configurationName + + " currency UUID: " + currencyId); } } - for (UUID currencyId : acceptedCurrencyIds) { - if (!validatedMaximums.containsKey(currencyId)) { - throw new IllegalArgumentException( - "Missing maximum swap amount for input currency UUID: " - + currencyId - ); + if (!validatedAmounts.containsKey(currencyId)) { + throw new IllegalArgumentException("Missing " + configurationName + + " for input currency UUID: " + currencyId); } } - return Map.copyOf(validatedMaximums); + return Map.copyOf(validatedAmounts); } private CurrencyType requireCanonicalCurrency( @@ -1057,6 +1057,7 @@ public final class EvelynIOUBurnerServiceImpl private final CurrencyIdentityService currencyIdentityService; private final BoundNotificationService notificationService; private final List inputCurrencyMinimumReserves; + private final Map inputCurrencyMinimumSwapAmounts; private final Map inputCurrencyMaximumSwapAmounts; private final int maximumSlippageBps; private final ΩmilliSecondsΩ maximumPriceAge; diff --git a/src/main/tjava/com/r35157/nenjim/hubd/impl/ref/NenjimHubImpl.tjava b/src/main/tjava/com/r35157/nenjim/hubd/impl/ref/NenjimHubImpl.tjava index 4732f03..ea61789 100644 --- a/src/main/tjava/com/r35157/nenjim/hubd/impl/ref/NenjimHubImpl.tjava +++ b/src/main/tjava/com/r35157/nenjim/hubd/impl/ref/NenjimHubImpl.tjava @@ -307,6 +307,10 @@ public final class NenjimHubImpl implements NenjimHub { new MoneyAmount(new ΩAmountΩ("0.1"), cis.resolve(CurrencyTypeIds.USDT_ID)), new MoneyAmount(new ΩAmountΩ("0.095"), cis.resolve(CurrencyTypeIds.SYRUPUSDC_ID)) ); + List inputCurrencyMinimumSwapAmounts = List.of( + new MoneyAmount(new ΩAmountΩ("0.05"), cis.resolve(CurrencyTypeIds.USDT_ID)), + new MoneyAmount(new ΩAmountΩ("0.045"), cis.resolve(CurrencyTypeIds.SYRUPUSDC_ID)) + ); List inputCurrencyMaximumSwapAmounts = List.of( new MoneyAmount(new ΩAmountΩ("0.25"), cis.resolve(CurrencyTypeIds.USDT_ID)), new MoneyAmount(new ΩAmountΩ("0.2"), cis.resolve(CurrencyTypeIds.SYRUPUSDC_ID)) @@ -323,6 +327,7 @@ public final class NenjimHubImpl implements NenjimHub { cis, discordAssetAZTokenNotificationService, inputCurrencyMinimumReserves, + inputCurrencyMinimumSwapAmounts, inputCurrencyMaximumSwapAmounts, 200, 2*1000*60L,