From adb5f0235a9b5ef93165ff8a97f2829c7fe36f76 Mon Sep 17 00:00:00 2001
From: Minimons
Date: Mon, 5 Oct 2026 20:35:56 +0200
Subject: [PATCH] 90: Support SyrupUSDC in the Evelyn IOU burner with
per-currency swap limits
---
docs/Evelyn.md | 25 ++++
.../.openspec.yaml | 2 +
.../design.md | 30 ++++
.../proposal.md | 26 ++++
.../specs/evelyn-iou-burner-service/spec.md | 129 +++++++++++++++++
.../tasks.md | 17 +++
.../verification.md | 33 +++++
.../specs/evelyn-iou-burner-service/spec.md | 92 +++++++-----
.../burner/EvelynIOUBurnerService.tjava | 13 +-
.../impl/ref/EvelynIOUBurnerServiceImpl.tjava | 131 ++++++++++++------
.../nenjim/hubd/impl/ref/NenjimHubImpl.tjava | 15 +-
11 files changed, 430 insertions(+), 83 deletions(-)
create mode 100644 openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/.openspec.yaml
create mode 100644 openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/design.md
create mode 100644 openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/proposal.md
create mode 100644 openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/specs/evelyn-iou-burner-service/spec.md
create mode 100644 openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/tasks.md
create mode 100644 openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/verification.md
diff --git a/docs/Evelyn.md b/docs/Evelyn.md
index 037acf4..b9adb5c 100644
--- a/docs/Evelyn.md
+++ b/docs/Evelyn.md
@@ -104,6 +104,31 @@ dem permanent. Dette reducerer det samlede udbud og skaber et kontinuerligt defl
positivt for investorerne. Buyback-strategien sikrer, at investorerne får en god pris for deres tokens, når det
udnyttes at ikke alle investorer sælger på samme tid, da de har forskellige forventninger til prisen.
+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 |
+
+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.
+
+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
+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.
+
+Højst ét input forsøges vekslet pr. iteration, med en times pause efter hver
+afsluttet iteration. Jupiter vælger pool og rute til EVE. USDT har fortsat første
+prioritet, så en vedvarende spendabel USDT-saldo kan udsætte SyrupUSDC-veksling.
+Hele den synlige EVE-saldo er derefter berettiget til burn, også ved direkte
+EVE-indbetalinger. Burn kræver stadig en frisk EVE/USDT-observation gennem det
+uændrede `TickerService.getLatestPrice()` og bekræftes ved `FINALIZED` før det
+eneste notifikationsforsøg. Inputgrænserne begrænser ikke EVE-burn.
+
### 2.3 Finansiering af udvikling
Udvikling i Evelyn sker gennem en task-baseret model. Alle opgaver oprettes i IntelliJ YouTrack og bliver
individuelt prissat i 'Evelyn IOU'. Når en opgave er funded – enten af Evelyn selv (via overskud) eller af en
diff --git a/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/.openspec.yaml b/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/.openspec.yaml
new file mode 100644
index 0000000..e3966d7
--- /dev/null
+++ b/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-10-05
diff --git a/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/design.md b/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/design.md
new file mode 100644
index 0000000..0b0ba8a
--- /dev/null
+++ b/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/design.md
@@ -0,0 +1,30 @@
+# Design
+
+## Context
+
+See proposal.md for motivation and issue #90 for the agreed operating values. The existing burner already supports ordered SOL/SPL inputs, downward precision rounding and Jupiter exact-input swaps. Its reserve list is the authoritative accepted-input configuration. The constructor's scalar USDC cap and input-price lookup are the only blockers to selecting SyrupUSDC without additional Ticker observations.
+
+## Goals / Non-Goals
+
+**Goals:** Keep selection independent of input prices while rejecting incomplete or ambiguous limits before any worker starts. Change the amount policy without changing transaction outcomes, recovery or burning.
+
+**Non-Goals:** Introduce another input model, a general configuration subsystem, or change how the latest EVE/USDT observation is selected.
+
+## Decisions
+
+1. Accept maxima as a second `List` and copy their validated values into an immutable UUID-keyed map. The reserve list keeps its original defensive copy and order. UUID lookup permits the maxima list to arrive in any order; matching positions or metadata would create avoidable ambiguity. Existing immutable `MoneyAmount` values suffice without new types or ValueTags.
+2. Validate maxima entries, amounts, currencies and UUIDs, canonical CIS identity, positivity and uniqueness, then require exact agreement with the accepted reserve UUIDs. Keep existing reserve and Solana-representation validation unchanged. Empty lists match and retain direct-EVE handling. A compatibility scalar constructor would preserve obsolete price semantics, so it is not retained.
+3. Select `min(balance - reserve, maximum.amount())` only for positive spendable balance, then use the existing downward mint-precision rounding and zero skip. Remove the USDC canonical field and all input price construction/queries. Keep EVE/USDT canonical construction and fresh-price enforcement intact.
+4. Add the agreed two reserve and maximum entries in Hub construction. Retain every other argument and the existing wallet/Jupiter bindings. Jupiter continues to choose pool and route; no Raydium transaction path is added.
+5. Compile through the integrated Gradle/Detag build, and verify the compiled implementation with temporary offline controlled dependencies outside the repository. Do not add a permanent test artifact. Verify amount boundaries, UUID matching, invalid configuration, rounding, and existing failure/pending-burn behavior without network or real transaction submission.
+
+## Risks / Trade-offs
+
+- Token limits vary in USD value → document that 0.2 SyrupUSDC is a token amount and retain explicit small limits.
+- Constructor signature changes → update the single repository call site and compile all relevant sources.
+- USDT can defer SyrupUSDC indefinitely while spendable → preserve the agreed priority and one-attempt rule rather than changing scheduling.
+- Unknown transaction outcomes must not be retried → leave outcome handlers unchanged and verify suspension/pending behavior offline.
+
+## Migration Plan
+
+Implementation is local and reviewable; deployment is a separate task. A later deployment replaces the application with the compiled change while preserving wallet, startup configuration and existing Ticker history. External callers must replace a scalar cap with exactly one token-denominated maximum per accepted reserve UUID. Rollback restores the prior implementation and USDT-only Hub configuration after reconciling any unknown transaction outcomes.
diff --git a/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/proposal.md b/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/proposal.md
new file mode 100644
index 0000000..613790d
--- /dev/null
+++ b/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/proposal.md
@@ -0,0 +1,26 @@
+# Proposal
+
+## Why
+
+The production Evelyn IOU burner accepts only USDT, and its common USDC-equivalent ceiling needs input prices for other tokens. Issue #90 requires deposited SyrupUSDC to fund the same Jupiter buyback and burn pipeline using a small, explicit token-denominated limit.
+
+## What Changes
+
+- **BREAKING**: Replace the constructor's scalar `maximumSwapAmountInUSDC` with `List inputCurrencyMaximumSwapAmounts`, validated and matched to accepted inputs by canonical currency UUID.
+- Calculate each exact input amount from spendable balance and that currency's maximum, rounding downward to mint precision without querying an input/USDC price.
+- Keep USDT first with reserve 0.1 and maximum 0.25; add SyrupUSDC second with reserve 0.095 and maximum 0.2 in `constructEvelynIOUBurnerProdProcess()`.
+- Preserve Jupiter routing, EVE/USDT freshness before burning, all transaction/recovery behavior, and Hub-owned lifecycle.
+
+## Capabilities
+
+### New Capabilities
+
+None.
+
+### Modified Capabilities
+
+- `evelyn-iou-burner-service`: Token-denominated maxima, UUID-matched configuration validation, price-independent input selection, and production SyrupUSDC support.
+
+## Impact
+
+Changes are limited to the burner reference constructor/selection logic, its Hub construction, directly affected documentation and OpenSpec requirements. Verification compiles normal sources and uses controlled offline dependencies without live financial transactions. No Ticker sources, API behavior, history paths, ValueTags, direct Raydium swaps, deployment, or unrelated refactoring are included.
diff --git a/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/specs/evelyn-iou-burner-service/spec.md b/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/specs/evelyn-iou-burner-service/spec.md
new file mode 100644
index 0000000..fdcdf81
--- /dev/null
+++ b/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/specs/evelyn-iou-burner-service/spec.md
@@ -0,0 +1,129 @@
+# 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. Empty reserves 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
+- **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 either supplied list after construction
+- **THEN** the accepted currencies, their priority, reserves and maxima remain as validated during construction
+
+### 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.
+
+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
+- **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
+
+### 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.
+
+#### Scenario: Native SOL configuration
+- **WHEN** a configured input UUID is the stable SOL UUID
+- **THEN** the service reads the native wallet balance and later supplies Jupiter with the CIS-resolved canonical SOL/WSOL mint rather than treating SOL as an SPL-token holding
+
+#### Scenario: Fixed EVE behavior
+- **WHEN** the service evaluates a burn and notification
+- **THEN** it uses the fixed EVE mint and the canonical EVE/USDT pair without accepting replacements through construction
+
+### Requirement: Prioritized input inspection with internal Solana translation
+Unless swaps are suspended, an iteration SHALL inspect configured input entries in order until it finds the first usable candidate. For every entry it SHALL resolve the configured UUID to its canonical currency. Native SOL SHALL use `getSolanaBalance().amount()` and nine-decimal precision. Every other input SHALL use its unique CIS-resolved Solana mint; the service SHALL inspect the mint account owner to detect the original SPL Token Program or Token-2022, use that program for wallet balance lookup, and obtain precision from Solana mint metadata. A missing SPL-token account SHALL equal zero balance.
+
+For each entry the service SHALL subtract its configured reserve and skip non-positive spendable balance. Missing or unusable balance, mint account, supported program, or mint metadata SHALL produce a warning and allow later configured entries to be examined. Interruption SHALL preserve the thread interrupt state and end processing rather than becoming a skipped candidate. Failure to select an input SHALL NOT prevent the later complete-EVE burn evaluation.
+
+#### Scenario: Missing token account
+- **WHEN** a configured non-SOL mint is valid but the wallet has no token account under its detected program
+- **THEN** that input is treated as a zero balance and later inputs are considered
+
+#### Scenario: Missing or unsupported mint account
+- **WHEN** a configured token's mint account is absent or owned by an unsupported program
+- **THEN** the service warns, skips that input, considers later priorities, and still reaches the EVE-balance step
+
+#### Scenario: Candidate read is interrupted
+- **WHEN** wallet, CIS-adjacent processing, or Solana access is interrupted while inspecting an input
+- **THEN** the service preserves interruption and ends processing without treating the input as an ordinary failure
+
+## ADDED Requirements
+
+### 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.
+
+#### 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
+- **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
+- **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, 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
+- **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
+
+### 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 and 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
+
+## REMOVED Requirements
+
+### Requirement: Fresh canonical input prices
+**Reason**: Buyback limits are expressed in each accepted input's own units, so input/USDC observations are no longer needed.
+**Migration**: Configure exactly one token-denominated maximum per accepted input; keep the existing fresh EVE/USDT observation before burning.
+
+### Requirement: Exact USDC-capped amount and per-currency reserve
+**Reason**: Replaced by exact token-capped amount and per-currency reserve.
+**Migration**: Replace the scalar USDC-equivalent constructor maximum with the UUID-matched per-currency MoneyAmount list.
diff --git a/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/tasks.md b/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/tasks.md
new file mode 100644
index 0000000..23e5937
--- /dev/null
+++ b/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/tasks.md
@@ -0,0 +1,17 @@
+# Tasks
+
+## 1. Token-denominated constructor and selection
+
+- [x] 1.1 Replace the scalar maximum with an immutable UUID-matched configuration, preserve reserve validation, and verify reordered maxima, defensive copying, empty lists and invalid maximum configurations with offline controlled dependencies.
+- [x] 1.2 Remove input/USDC resolution and pricing, select the lesser spendable/token-maximum amount with existing downward precision rounding, and verify the SyrupUSDC balance examples, reserve boundary and zero-rounding skip offline.
+- [x] 1.3 Update constructor and domain-service JavaDoc for token limits and EVE-only price freshness; verify no obsolete input-price or scalar-limit contract remains in current source documentation.
+
+## 2. Production configuration and documentation
+
+- [x] 2.1 Configure USDT reserve 0.1/maximum 0.25 first and SyrupUSDC reserve 0.095/maximum 0.2 second in Hub construction; verify exact values, unchanged remaining arguments and successful Gradle compilation.
+- [x] 2.2 Document the production limits, Jupiter routing, unchanged latest-price API and direct-EVE behavior; verify documentation matches the source configuration and delta requirements.
+
+## 3. Integration verification and OpenSpec completion
+
+- [x] 3.1 Verify one-attempt priority, confirmed/failed/unknown swap handling, pending FINALIZED burns, complete EVE burning and stale/missing EVE observations offline without financial transactions; record results and limitations in the change.
+- [x] 3.2 Validate the OpenSpec change, perform diff/whitespace checks, synchronize directly affected main requirements and verify the change is ready for archive with all implementation tasks and requirements complete.
diff --git a/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/verification.md b/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/verification.md
new file mode 100644
index 0000000..64f037a
--- /dev/null
+++ b/openspec/changes/archive/2026-10-05-90-add-syrupusdc-burner-input-with-per-currency-limits/verification.md
@@ -0,0 +1,33 @@
+# Verification
+
+## Completed checks
+
+- `openspec validate 90-add-syrupusdc-burner-input-with-per-currency-limits --strict`: passed.
+- `openspec validate evelyn-iou-burner-service --type spec --strict`: passed, with informational long-description hints for existing/modified requirements.
+- `git diff --check`: passed.
+- Main-spec merge comparison: all four modified requirements and both added requirements match the delta, both removed requirements are absent, and unaffected requirements are preserved.
+- Source comparison against HEAD confirms that lifecycle and iteration sequencing, swap execution and outcome handling, complete EVE reload/burn, pending burn recovery, FINALIZED waiting, notification behavior, fresh-price validation, Solana precision resolution and reserve validation are unchanged byte for byte.
+- Current source documentation contains no obsolete scalar USDC limit, USDC canonical field, input-price conversion or fresh input/USDC requirement. The production values and priority match issue #90, including SyrupUSDC reserve 0.095.
+
+## Compilation and controlled checks
+
+The parent agent compiled the updated main sources and existing test sources successfully with the normal integrated Gradle/Detag workflow:
+
+```text
+JAVA_HOME=/usr/local/software/java/jfx-25 ./gradlew compileJava compileTestJava --offline --no-daemon
+```
+
+Result: `BUILD SUCCESSFUL in 13s`, four tasks executed. No permanent automated tests were added.
+
+The temporary harness at `/tmp/evelyn-issue90/BurnerVerification.java` was compiled and executed with the same JDK against the newly built classes. Result: **PASS: 52 focused offline assertions**. These cover:
+
+- SyrupUSDC balances 0.5 → 0.2 and 0.245 → 0.150, at/below 0.095 → skipped.
+- USDT balance 0.35 → 0.25, with USDT first even when maxima are reversed.
+- Downward six-decimal rounding and positive amounts that round to zero.
+- Defensive configuration copying, empty lists and ten invalid-maxima configurations, including missing, duplicate, extra, non-positive, null and unknown values/identities.
+- No input/USDC Ticker observation, and both missing and stale EVE/USDT observations postponing burns while still permitting input selection.
+- Complete direct-EVE burning with empty input lists, FINALIZED confirmation, unchanged timeout and one proof notification using the burn signature.
+
+All financial dependencies and notification delivery were controlled. No worker, live transaction or external notification was started. Swap/burn failure and pending recovery paths were verified by exact source comparison against HEAD; the harness does not claim exhaustive runtime coverage of those unchanged branches.
+
+Main requirements are synchronized and all implementation tasks are complete. No deployment, push, Gitea write or live financial transaction has been performed.
diff --git a/openspec/specs/evelyn-iou-burner-service/spec.md b/openspec/specs/evelyn-iou-burner-service/spec.md
index 078642c..baf4055 100644
--- a/openspec/specs/evelyn-iou-burner-service/spec.md
+++ b/openspec/specs/evelyn-iou-burner-service/spec.md
@@ -16,31 +16,39 @@ 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 pricing and processing. An empty list 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. Empty reserves 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 prices
+- **WHEN** multiple configured entries have spendable balances 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 either supplied list after construction
+- **THEN** the accepted currencies, their priority, reserves and maxima remain as validated during construction
+
### Requirement: Complete constructor validation
-Construction SHALL require all injected services, the input list, maximum swap amount, 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 UUID to be known to the injected Currency Identity Service; UUIDs to be unique; and EVE not to be an input currency. It SHALL require `maximumSwapAmountInUSDC` to be greater than zero, 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, 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.
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, UUIDs are distinct and known, 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 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, the USDC cap is non-positive, slippage is outside its inclusive range, maximum price age is non-positive, or either duration is null or non-positive
+- **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
@@ -51,8 +59,16 @@ Every configured non-SOL UUID SHALL resolve through reverse CIS lookup to exactl
- **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
+
### 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, construct input/USDC pairs with the canonical USDC UUID, 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.
+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.
#### Scenario: Native SOL configuration
- **WHEN** a configured input UUID is the stable SOL UUID
@@ -97,7 +113,7 @@ At the start of an iteration, if a prior burn signature is pending, the service
### Requirement: Prioritized input inspection with internal Solana translation
Unless swaps are suspended, an iteration SHALL inspect configured input entries in order until it finds the first usable candidate. For every entry it SHALL resolve the configured UUID to its canonical currency. Native SOL SHALL use `getSolanaBalance().amount()` and nine-decimal precision. Every other input SHALL use its unique CIS-resolved Solana mint; the service SHALL inspect the mint account owner to detect the original SPL Token Program or Token-2022, use that program for wallet balance lookup, and obtain precision from Solana mint metadata. A missing SPL-token account SHALL equal zero balance.
-For each entry the service SHALL subtract its configured reserve and skip non-positive spendable balance. Missing or unusable balance, mint account, supported program, mint metadata, or price SHALL produce a warning and allow later configured entries to be examined. Interruption SHALL preserve the thread interrupt state and end processing rather than becoming a skipped candidate. Failure to select an input SHALL NOT prevent the later complete-EVE burn evaluation.
+For each entry the service SHALL subtract its configured reserve and skip non-positive spendable balance. Missing or unusable balance, mint account, supported program, or mint metadata SHALL produce a warning and allow later configured entries to be examined. Interruption SHALL preserve the thread interrupt state and end processing rather than becoming a skipped candidate. Failure to select an input SHALL NOT prevent the later complete-EVE burn evaluation.
#### Scenario: Missing token account
- **WHEN** a configured non-SOL mint is valid but the wallet has no token account under its detected program
@@ -111,34 +127,6 @@ For each entry the service SHALL subtract its configured reserve and skip non-po
- **WHEN** wallet, CIS-adjacent processing, or Solana access is interrupted while inspecting an input
- **THEN** the service preserves interruption and ends processing without treating the input as an ordinary failure
-### Requirement: Fresh canonical input prices
-For each non-USDC candidate, the service SHALL request the canonical `/USDC` pair and accept only a positive price with a non-null observation timestamp no more than `maximumPriceAge` milliseconds old at the current iteration time. The stable USDC UUID SHALL instead receive an exact price of one without a ticker observation. A missing, unsupported, invalid, or stale input price SHALL be warned about using canonical currency identity, SHALL allow the next configured input to be considered, and SHALL NOT prevent later EVE burning.
-
-#### Scenario: USDC is the input
-- **WHEN** the canonical input UUID is USDC and its spendable balance is positive
-- **THEN** the service uses exactly one USDC per USDC without querying the ticker for that input price
-
-#### Scenario: Input price is unusable
-- **WHEN** an input observation is missing, unsupported, non-positive, lacks a timestamp, or is older than the maximum age
-- **THEN** the service does not swap that currency and continues with later configured priorities
-
-### Requirement: Exact USDC-capped amount and per-currency reserve
-For a usable candidate, the service SHALL calculate `availableForSwap = max(balance - minimumReserve, 0)`, calculate `maximumInputAmount = maximumSwapAmountInUSDC / inputPriceInUSDC`, select the lesser amount, and round downward to the input currency's supported decimal precision. It SHALL skip a result that rounds to zero and SHALL never intentionally swap more than either the spendable balance or the configured USDC-equivalent maximum through rounding.
-
-The SOL reserve comparison SHALL use the native balance without estimating or subtracting future transaction fees. The USDC maximum SHALL limit buybacks only and SHALL NOT cap how much EVE may be burned.
-
-#### Scenario: Price cap is lower than spendable balance
-- **WHEN** a currency has five spendable units, its price is 100 USDC per unit, and the USDC maximum is one
-- **THEN** no more than 0.01 unit is selected before downward 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: 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
-
### Requirement: At most one confirmed swap attempt per iteration
After selecting a positive exact amount, the service SHALL invoke the existing Jupiter exact-input operation once with the selected CIS-resolved input mint, fixed EVE output mint, selected amount, and configured slippage. Normal return SHALL be treated as already `CONFIRMED` and SHALL NOT trigger a second confirmation wait. Once invoked, no later input currency SHALL be attempted in that iteration regardless of success or failure, and the service SHALL proceed to reload the complete EVE balance whenever processing was not interrupted.
@@ -225,3 +213,37 @@ The EVE amount SHALL use plain decimal notation without unnecessary trailing zer
#### Scenario: Notification delivery fails
- **WHEN** the sole notification invocation throws
- **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.
+
+#### 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
+- **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
+- **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, 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
+- **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
+
+### 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 and 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
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 f2ef9a1..950fd42 100644
--- a/src/main/tjava/com/fanitas/evelyn/service/burner/EvelynIOUBurnerService.tjava
+++ b/src/main/tjava/com/fanitas/evelyn/service/burner/EvelynIOUBurnerService.tjava
@@ -13,6 +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
+ * 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
* part of Evelyn's input configuration.
@@ -26,10 +31,10 @@ import java.util.List;
* submitted in an iteration that begins with a pending burn.
* Unless swaps are suspended, inspect accepted currencies in priority
* order. Canonicalize each AssetAZ UUID, resolve its native-SOL or SPL
- * representation internally, apply its own minimum reserve, and require a
- * fresh input/USDC price. Missing or unusable candidate data does not
+ * representation internally and apply its own minimum reserve without an
+ * input-price lookup. Missing or unusable candidate data does not
* prevent later currencies from being considered.
- * Select at most the configured USDC-equivalent maximum, rounded down
+ * 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.
@@ -40,7 +45,7 @@ import java.util.List;
* lifecycle.
* Reload the complete visible Evelyn IOU balance, including tokens
* deposited or acquired before this iteration. Absence or failure of a
- * swap does not suppress this step, and the USDC buyback limit never caps
+ * swap does not suppress this step, and the input buyback limit never caps
* the amount eligible for burning.
* Require a positive, sufficiently fresh canonical EVE/USDT price and
* calculate the complete balance's dollar value before submitting a burn.
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 c2ef066..8aed43a 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
@@ -30,8 +30,10 @@ import java.math.RoundingMode;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@@ -41,7 +43,6 @@ import java.util.concurrent.TimeUnit;
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.EVE_ID;
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.SOLANA_ID;
-import static com.r35157.assetaz.services.cis.CurrencyTypeIds.USDC_ID;
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.USDT_ID;
import static com.r35157.assetaz.services.cis.ExternalCurrencyReference.SOLANA_MINT_NAMESPACE;
@@ -65,13 +66,20 @@ public final class EvelynIOUBurnerServiceImpl
* metadata. Solana mint addresses and token programs are resolved
* internally and are not constructor configuration.
*
+ * {@code inputCurrencyMaximumSwapAmounts} must contain exactly one
+ * strictly positive maximum for every accepted input and no other currency.
+ * Maxima are copied defensively and matched by canonical currency UUID,
+ * independently of list order or supplied metadata. Each maximum is in
+ * that input currency's own units, without an input-price conversion or
+ * a guarantee about its USD value.
+ *
* @param burnerWallet dedicated wallet whose balances are inspected and
* whose complete EVE balance is burned
* @param swapService exact-input Jupiter service; normal return already
* means the swap reached {@code CONFIRMED}
* @param solanaBlockChain Solana access used for mint ownership, token
* precision, and burn finalization
- * @param tickerService latest-price source for input/USDC and EVE/USDT
+ * @param tickerService latest-price source for EVE/USDT before burning
* @param currencyIdentityService authoritative resolver for AssetAZ UUIDs
* and Solana mint references
* @param notificationService fully bound synchronous destination for the
@@ -81,24 +89,26 @@ 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 maximumSwapAmountInUSDC strictly positive USDC-equivalent ceiling
- * for one buyback; it does not limit burns
+ * @param inputCurrencyMaximumSwapAmounts exactly one strictly positive
+ * token-denominated buyback maximum per accepted input UUID;
+ * it does not limit burns
* @param maximumSlippageBps Jupiter slippage limit in basis points from 0
* through 10000 inclusive
* @param maximumPriceAge strictly positive maximum observation age in
- * milliseconds for both input and EVE prices
+ * milliseconds for the EVE/USDT price
* @param burnFinalizationTimeout strictly positive timeout passed unchanged
* to each {@code FINALIZED} burn await
* @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 amount,
+ * object, list entry, reserve or maximum amount,
* configured currency, or UUID is null
- * @throws IllegalArgumentException if a reserve or scalar limit is out of
+ * @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
+ * lacks exactly one usable Solana mint,
+ * or maximum UUIDs do not match inputs
*/
public EvelynIOUBurnerServiceImpl(
@NotNull SolanaWallet burnerWallet,
@@ -108,7 +118,7 @@ public final class EvelynIOUBurnerServiceImpl
@NotNull CurrencyIdentityService currencyIdentityService,
@NotNull BoundNotificationService notificationService,
@NotNull List inputCurrencyMinimumReserves,
- @NotNull ΩUSDCAmountΩ maximumSwapAmountInUSDC,
+ @NotNull List inputCurrencyMaximumSwapAmounts,
int maximumSlippageBps,
ΩmilliSecondsΩ maximumPriceAge,
@NotNull Duration burnFinalizationTimeout,
@@ -139,15 +149,6 @@ public final class EvelynIOUBurnerServiceImpl
"notificationService"
);
- Objects.requireNonNull(
- maximumSwapAmountInUSDC,
- "maximumSwapAmountInUSDC"
- );
- if (maximumSwapAmountInUSDC.signum() <= 0) {
- throw new IllegalArgumentException(
- "maximumSwapAmountInUSDC must be greater than zero"
- );
- }
if (maximumSlippageBps < 0
|| maximumSlippageBps > MAXIMUM_SLIPPAGE_BPS) {
throw new IllegalArgumentException(
@@ -179,7 +180,6 @@ public final class EvelynIOUBurnerServiceImpl
);
}
- this.maximumSwapAmountInUSDC = maximumSwapAmountInUSDC;
this.maximumSlippageBps = maximumSlippageBps;
this.maximumPriceAge = maximumPriceAge;
this.maximumPriceAgeDuration = Duration.ofMillis(maximumPriceAge);
@@ -188,7 +188,6 @@ public final class EvelynIOUBurnerServiceImpl
EVE_ID,
"Evelyn IOU"
);
- this.canonicalUsdc = requireCanonicalCurrency(USDC_ID, "USDC");
CurrencyType canonicalUsdt = requireCanonicalCurrency(
USDT_ID,
"USDT"
@@ -200,6 +199,9 @@ public final class EvelynIOUBurnerServiceImpl
this.inputCurrencyMinimumReserves = validateAndCopyInputs(
inputCurrencyMinimumReserves
);
+ this.inputCurrencyMaximumSwapAmounts = validateAndCopyMaximums(
+ inputCurrencyMaximumSwapAmounts
+ );
}
@Override
@@ -353,18 +355,6 @@ public final class EvelynIOUBurnerServiceImpl
continue;
}
- ΩPriceΩ inputPriceInUsdc;
- if (USDC_ID.equals(currencyId) || USDT_ID.equals(currencyId)) {
- inputPriceInUsdc = BigDecimal.ONE;
- } else {
- inputPriceInUsdc = findFreshPrice(
- new TradingPair(canonicalCurrency, canonicalUsdc)
- );
- if (inputPriceInUsdc == null) {
- continue;
- }
- }
-
if (!nativeSolana) {
try {
decimals = resolveMintDecimals(
@@ -381,11 +371,9 @@ public final class EvelynIOUBurnerServiceImpl
}
}
- ΩAmountΩ maximumInputAmount = maximumSwapAmountInUSDC.divide(
- inputPriceInUsdc,
- decimals,
- RoundingMode.DOWN
- );
+ ΩAmountΩ maximumInputAmount = inputCurrencyMaximumSwapAmounts
+ .get(currencyId)
+ .amount();
ΩAmountΩ amountToSwap = availableForSwap
.min(maximumInputAmount)
.setScale(decimals, RoundingMode.DOWN);
@@ -830,6 +818,72 @@ public final class EvelynIOUBurnerServiceImpl
return List.copyOf(validatedInputs);
}
+ private Map validateAndCopyMaximums(
+ List configuredMaximums
+ ) {
+ Objects.requireNonNull(
+ configuredMaximums,
+ "inputCurrencyMaximumSwapAmounts"
+ );
+ Set acceptedCurrencyIds = new HashSet<>();
+ for (MoneyAmount minimumReserve : inputCurrencyMinimumReserves) {
+ acceptedCurrencyIds.add(minimumReserve.currencyType().id());
+ }
+ Map validatedMaximums = 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
+ );
+ }
+
+ CurrencyType configuredCurrency = Objects.requireNonNull(
+ maximum.currencyType(),
+ "inputCurrencyMaximumSwapAmounts[" + index
+ + "].currencyType"
+ );
+ UUID currencyId = Objects.requireNonNull(
+ configuredCurrency.id(),
+ "inputCurrencyMaximumSwapAmounts[" + index
+ + "].currencyType.id"
+ );
+ requireCanonicalCurrency(currencyId, "configured input maximum");
+
+ if (!acceptedCurrencyIds.contains(currencyId)) {
+ throw new IllegalArgumentException(
+ "Maximum swap amount configured for unaccepted input "
+ + "currency UUID: " + currencyId
+ );
+ }
+ if (validatedMaximums.putIfAbsent(currencyId, maximum) != null) {
+ throw new IllegalArgumentException(
+ "Duplicate maximum swap amount currency UUID: "
+ + currencyId
+ );
+ }
+ }
+
+ for (UUID currencyId : acceptedCurrencyIds) {
+ if (!validatedMaximums.containsKey(currencyId)) {
+ throw new IllegalArgumentException(
+ "Missing maximum swap amount for input currency UUID: "
+ + currencyId
+ );
+ }
+ }
+ return Map.copyOf(validatedMaximums);
+ }
+
private CurrencyType requireCanonicalCurrency(
UUID currencyId,
String purpose
@@ -961,13 +1015,12 @@ public final class EvelynIOUBurnerServiceImpl
private final CurrencyIdentityService currencyIdentityService;
private final BoundNotificationService notificationService;
private final List inputCurrencyMinimumReserves;
- private final ΩUSDCAmountΩ maximumSwapAmountInUSDC;
+ private final Map inputCurrencyMaximumSwapAmounts;
private final int maximumSlippageBps;
private final ΩmilliSecondsΩ maximumPriceAge;
private final Duration maximumPriceAgeDuration;
private final Duration burnFinalizationTimeout;
private final long iterationIntervalNanos;
- private final CurrencyType canonicalUsdc;
private final TradingPair eveUsdtTradingPair;
private ScheduledExecutorService worker;
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 1977011..4732f03 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
@@ -303,9 +303,14 @@ public final class NenjimHubImpl implements NenjimHub {
}
private NenjimProcess constructEvelynIOUBurnerProdProcess() {
- List burnTokenCandidates = List.of(new MoneyAmount(new ΩAmountΩ("0.1"), cis.resolve(CurrencyTypeIds.USDT_ID)));
-
- ΩUSDCAmountΩ maximumSwapAmountPerIterationInUSD = new ΩUSDCAmountΩ("0.25");
+ List inputCurrencyMinimumReserves = List.of(
+ new MoneyAmount(new ΩAmountΩ("0.1"), cis.resolve(CurrencyTypeIds.USDT_ID)),
+ new MoneyAmount(new ΩAmountΩ("0.095"), 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))
+ );
EvelynIOUBurnerServiceImpl process = new EvelynIOUBurnerServiceImpl(
solanaWalletEvelynBurnerProd,
@@ -317,8 +322,8 @@ public final class NenjimHubImpl implements NenjimHub {
TICKER_SERVICE_ID),
cis,
discordAssetAZTokenNotificationService,
- burnTokenCandidates,
- maximumSwapAmountPerIterationInUSD,
+ inputCurrencyMinimumReserves,
+ inputCurrencyMaximumSwapAmounts,
200,
2*1000*60L,
Duration.ofSeconds(10),