Add the Evelyn IOU buyback-and-burn service #72

Closed
opened 2026-08-12 19:18:37 +02:00 by minimons · 0 comments
Owner

Background

The lower-level components required for automated Evelyn IOU buyback and burn are now available:

  • SolanaWallet can provide native SOL and SPL-token balances.
  • JupiterSwapService can swap an exact input amount into another token and waits for CONFIRMED execution.
  • SolanaWallet.burnSPLToken(...) can submit an Evelyn IOU burn transaction.
  • SolanaBlockChain.awaitTransaction(...) can wait for the burn transaction to reach FINALIZED.
  • BoundNotificationService can synchronously deliver the resulting notification through a configured Discord webhook.
  • TickerService can provide the prices needed for swap limits and notification values.
  • CurrencyIdentityService can translate stable AssetAZ currency identities into their external Solana representations.

A dedicated Evelyn-specific service is now needed to combine these components safely.

Goal

Implement EvelynIOUBurnerService, a continuously running service that:

  1. Inspects a dedicated burner wallet for configured input currencies.
  2. Swaps at most a configured USDC-equivalent amount of one input currency into Evelyn IOU per iteration.
  3. Burns the complete available Evelyn IOU balance.
  4. Waits for the burn transaction to reach FINALIZED.
  5. Sends one notification containing the burned amount, its dollar value, and the Solscan proof link.

This is specifically an Evelyn IOU service and must not be designed as a generic token burner.

Packages and public types

Add the API under:

com.fanitas.evelyn.service.burner

Add the reference implementation under:

com.fanitas.evelyn.service.burner.impl.ref

The public service interface shall be:

public interface EvelynIOUBurnerService {
    void start();

    void stop();
}

The interface JavaDoc must describe the complete iteration order and the transaction-safety rules defined in this issue.

Input-currency configuration

The accepted input currencies and their minimum reserves shall be configured as an ordered list of the existing MoneyAmount value type:

@NotNull List<MoneyAmount> inputCurrencyMinimumReserves

Each MoneyAmount has the following meaning:

  • currencyType() identifies one accepted input currency through its stable AssetAZ UUID.
  • amount() specifies the minimum amount of that currency that must remain in the burner wallet.
  • Inclusion in the list means that the currency is accepted as a buyback input.
  • List order defines input-currency priority.

Example:

List<MoneyAmount> inputCurrencyMinimumReserves = List.of(
        new MoneyAmount(
                new BigDecimal("0.1"),
                currencyIdentityService.resolve(
                        CurrencyTypeIds.SOLANA_ID
                )
        ),
        new MoneyAmount(
                new BigDecimal("1"),
                currencyIdentityService.resolve(
                        CurrencyTypeIds.USDC_ID
                )
        ),
        new MoneyAmount(
                new BigDecimal("5"),
                currencyIdentityService.resolve(
                        ETH_ID
                )
        )
);

This example means:

  1. Native SOL is the first-priority input currency, but at least 0.1 SOL must remain.
  2. USDC is the second-priority input currency, but at least 1 USDC must remain.
  3. ETH is the third-priority input currency, but at least 5 ETH must remain.

The configured MoneyAmount.amount() is a minimum reserve. It is not the amount that shall always be swapped.

An empty list is valid and allows the service to burn EVE deposited directly into the wallet without performing buybacks.

Do not add burner-specific input configuration types such as:

EvelynIOUBurnerInputAsset
EvelynIOUBurnerSolanaInput
EvelynIOUBurnerSPLTokenInput

Native SOL, legacy SPL tokens, and Token-2022 tokens are different Solana representations, but this distinction must not leak into Evelyn’s domain configuration.

Currency identity and Solana representation

AssetAZ currency UUIDs are the authoritative identities in the input configuration.

For each configured MoneyAmount, the implementation shall:

  1. Read the UUID from moneyAmount.currencyType().id().
  2. Resolve that UUID through the injected CurrencyIdentityService.
  3. Use the canonical CurrencyType returned by that service for price pairs and internal processing.
  4. Ignore potentially stale name or symbol metadata carried by the supplied CurrencyType.

The UUID determines whether the input represents native SOL or a token:

  • CurrencyTypeIds.SOLANA_ID means native SOL.
  • Every other supported input currency must resolve to exactly one external reference in ExternalCurrencyReference.SOLANA_MINT_NAMESPACE.

For a non-SOL input currency, the service shall use:

currencyIdentityService.findExternalReferences(currencyType.id())

and filter the returned references by:

ExternalCurrencyReference.SOLANA_MINT_NAMESPACE

The resulting external ID is the SPL-token mint used for balance lookup and Jupiter execution.

The configured MoneyAmount must not contain a Solana mint address or token program. Those are technical integration details derived by the service.

Ambiguous or missing Solana representations

A configured non-SOL currency must have exactly one solana-mint external reference.

Construction shall fail if a configured non-SOL currency has:

  • No Solana mint reference.
  • More than one Solana mint reference.
  • A null or blank Solana mint external ID.

If a future AssetAZ currency can be represented by multiple Solana mints and the burner must distinguish between them, that ambiguity should be solved through a future canonical/preferred external-reference concept in CIS. It must not be solved preemptively by leaking mint addresses back into this burner configuration.

SPL token-program detection

The SPL token program must not be a constructor parameter or part of the input configuration.

When processing a non-SOL input currency, the service shall inspect the resolved mint account through SolanaBlockChain and determine whether the mint is owned by:

  • The original SPL Token Program.
  • The Token-2022 Program.

The detected program is then used when reading the wallet balance.

A missing mint account or a mint owned by an unsupported program shall cause that input currency to be skipped with a warning. It must not prevent the service from checking later configured currencies or burning an existing EVE balance.

Reference implementation constructor

The authoritative constructor shall receive both its runtime dependencies and all currently configurable behavior:

public EvelynIOUBurnerServiceImpl(
        @NotNull SolanaWallet burnerWallet,
        @NotNull JupiterSwapService swapService,
        @NotNull SolanaBlockChain solanaBlockChain,
        @NotNull TickerService tickerService,
        @NotNull CurrencyIdentityService currencyIdentityService,
        @NotNull BoundNotificationService notificationService,
        @NotNull List<MoneyAmount> inputCurrencyMinimumReserves,
        @NotNull ΩUSDCAmountΩ maximumSwapAmountInUSDC,
        int maximumSlippageBps,
        ΩmilliSecondsΩ maximumPriceAge,
        @NotNull Duration burnFinalizationTimeout,
        @NotNull Duration iterationInterval
)

Constructor validation must require:

  • All injected services and configuration objects to be non-null.
  • No null entries in inputCurrencyMinimumReserves.
  • Every configured MoneyAmount.amount() to be non-null and zero or greater.
  • Every configured MoneyAmount.currencyType() and its UUID to be non-null.
  • Every supplied currency UUID to be known to the injected CurrencyIdentityService.
  • No duplicate currency UUIDs in the list.
  • Evelyn IOU not to appear as an input currency.
  • Every configured non-SOL currency to have exactly one non-blank solana-mint external reference.
  • A defensively copied input-currency list preserving its order.
  • maximumSwapAmountInUSDC greater than zero.
  • maximumSlippageBps between 0 and 10000, inclusive.
  • maximumPriceAge greater than zero milliseconds.
  • Positive, non-zero finalization timeout and iteration interval.

Burner-specific validation belongs in this constructor. Do not modify the general-purpose MoneyAmount type solely to enforce burner-specific rules.

A later issue may add a configuration-file constructor. Such a constructor should eventually load the configuration and delegate to this authoritative constructor. Configuration-file loading is not part of this issue.

Fixed Evelyn domain values

The following values are part of the service’s fixed Evelyn-specific behavior and must not be constructor parameters.

Evelyn IOU mint

meveYG2iXYSkgSUn1T1uxcthH1EGMZdRHGgCntXZA3Y

The service shall resolve the mint’s supported SPL token program through the existing Solana APIs when it needs to obtain the EVE balance.

SolanaWallet.burnSPLToken(...) remains responsible for its own mint and token-program validation.

Native SOL representation used by Jupiter

Native SOL is identified in the burner configuration by:

CurrencyTypeIds.SOLANA_ID

Its wallet balance is obtained through:

burnerWallet.getSolanaBalance()

It must not be read as an SPL-token balance.

When native SOL is supplied to Jupiter, the service shall use the canonical Solana mint external reference associated with SOLANA_ID through CIS:

So11111111111111111111111111111111111111112

This canonical SOL/WSOL identifier is a Jupiter and Solana integration detail. It is not part of the burner’s constructor configuration.

Price pairs

The EVE dollar value used in notifications always comes from the canonical EVE_USDT pair constructed from:

CurrencyTypeIds.EVE_ID
CurrencyTypeIds.USDT_ID

USDT is treated as USD for the notification value.

Input-currency swap limits use <INPUT_CURRENCY>_USDC, with the quote currency resolved through:

CurrencyTypeIds.USDC_ID

These pairs are constructed internally through the injected CurrencyIdentityService; they are not constructor parameters.

Notification wording

The notification format is fixed Evelyn behavior and is not configurable in this issue.

Service lifecycle

The implementation shall use one dedicated background worker.

  • The first iteration begins immediately after start().
  • Later iterations use fixed delay: iterationInterval begins after the previous iteration has completed.
  • Iterations must never overlap.
  • Only one worker may be active for a service instance.
  • stop() prevents new iterations and stops the worker cleanly.
  • Interruption must be handled correctly and the thread interrupt status must not be silently lost.
  • Process-local pending and suspended state is reset by an explicit stop/start cycle.

No Nenjim autorun wiring shall be added in this issue. The wallet, signer, webhook, and other production configuration must not be hardcoded in the composition root.

Required iteration order

The complete order below is part of the public service contract and must be documented in the JavaDoc of EvelynIOUBurnerService.

1. Handle a pending burn first

If a previous burn returned a transaction signature but did not reach a definitive FINALIZED result, the iteration shall only continue waiting for that exact signature.

It must not submit another burn while the previous burn remains pending.

If the pending burn still has an unknown outcome, end the iteration without performing a new swap or burn.

If the pending burn becomes finalized successfully, perform the single notification attempt associated with that burn and then end the iteration.

If the pending burn becomes definitively failed, log the on-chain failure details, clear the pending state, send no notification, and allow a later iteration to reload the wallet balance and decide whether a new burn is required.

2. Select at most one input currency

Unless swaps are suspended due to an earlier unknown swap outcome, inspect inputCurrencyMinimumReserves in its configured order.

For each MoneyAmount minimumReserve:

  1. Resolve its canonical CurrencyType through CIS using the configured UUID.
  2. Determine whether the currency is native SOL or a mint-backed Solana token.
  3. Read the corresponding current wallet balance.
  4. Subtract minimumReserve.amount() from that balance.
  5. Skip the currency if the remaining amount is zero or negative.
  6. Resolve a sufficiently fresh USDC price.
  7. Skip the candidate with a warning if its balance, Solana representation, token program, or price cannot be obtained.
  8. Continue until the first candidate with a positive spendable balance and usable price is found.

For native SOL:

ΩAmountΩ balance =
        burnerWallet.getSolanaBalance().amount();

For an SPL token, use the CIS-resolved mint and detected token program with:

burnerWallet.getSPLTokenBalance(mintAddress, tokenProgram)

A missing SPL-token account is equivalent to a zero balance for that configured currency.

At most one swap attempt may be made per iteration. Once a swap has been attempted, no later input currency may be attempted in the same iteration, even if the swap fails.

Failure or absence of an input currency must not prevent already available EVE from being burned later in the iteration.

An InterruptedException must preserve interruption and end processing. It must not be converted into a normal skipped input.

3. Calculate the exact maximum input amount

For a normal input currency whose price represents USDC per unit:

maximumInputAmount =
    maximumSwapAmountInUSDC / inputCurrencyPriceInUSDC

The actual amount is:

availableForSwap =
    max(balance - configuredMinimumReserve, 0)

amountToSwap =
    min(availableForSwap, maximumInputAmount)

The result must be rounded down to the input currency’s supported decimal precision so that the configured USDC maximum is never exceeded through rounding.

If the rounded amount is zero, skip the swap.

Example:

maximumSwapAmountInUSDC = 1
ETH_USDC price = 100 USDC per ETH
wallet balance = 10 ETH
configured minimum reserve = 5 ETH

The service may swap:

availableForSwap = 10 ETH - 5 ETH = 5 ETH
maximumInputAmount = 1 / 100 ETH = 0.01 ETH
amountToSwap = min(5 ETH, 0.01 ETH) = 0.01 ETH

For USDC itself, use a price of exactly 1 without requiring a ticker observation.

Native SOL uses its native wallet balance, nine-decimal precision, and the canonical CIS-resolved SOL/WSOL mint when invoking Jupiter.

SPL-token precision must be obtained from existing Solana mint metadata.

4. Perform no more than one swap

Invoke the injected JupiterSwapService using:

  • The selected input currency’s CIS-resolved Jupiter input mint.
  • The calculated exact input amount.
  • The hardcoded Evelyn IOU output mint.
  • maximumSlippageBps.

For native SOL, the Jupiter input mint is the canonical SOL/WSOL mint resolved from the SOLANA_ID external references.

For an SPL token, the Jupiter input mint is the unique solana-mint reference resolved from that currency’s UUID.

Normal return from JupiterSwapService.swap(...) already means that the swap reached Solana CONFIRMED. The burner service must not perform a second confirmation wait for a normally returned swap.

The amount subsequently burned must not be derived only from JupiterSwapResult.receivedOutputTokenAmount(). The wallet can already contain EVE from previous transfers or swaps. Reload the complete EVE balance after the swap attempt.

5. Reload the complete EVE balance

Read the complete available Evelyn IOU balance from the burner wallet.

  • Resolve the EVE mint’s supported token program through the existing Solana APIs.
  • If the EVE token account is absent or its balance is zero, no burn or notification is performed.
  • Every positive EVE balance is eligible for burning.
  • There is no minimum EVE burn threshold.
  • maximumSwapAmountInUSDC limits buybacks only. It must never limit the amount of EVE burned.
  • Existing EVE must still be considered for burning when no swap was performed or a swap failed.

6. Obtain a fresh EVE/USDT price

Before submitting the burn, obtain the latest price for the internally constructed canonical EVE_USDT pair.

The price must:

  • Be positive.
  • Have a non-null observation timestamp.
  • Be no more than maximumPriceAge milliseconds old.

If the EVE price is missing, invalid, unsupported, or too old:

  • Log a warning.
  • Skip the burn for this iteration.
  • Send no notification.
  • Try again during a later iteration.

The burn is deliberately postponed in this situation because every successful burn must be announced with a dollar value.

The dollar value is:

complete EVE amount to burn × latest EVE/USDT price

7. Submit the complete EVE burn once

Call:

burnerWallet.burnSPLToken(
        EVELYN_IOU_MINT,
        completeEvelynIOUBalance
)

The complete observed EVE balance must be submitted as one burn.

Do not automatically retry, rebuild, re-sign, or resubmit the burn within the iteration.

After a signature has been returned, retain process-local pending information containing at least:

  • The transaction signature.
  • The submitted EVE amount.
  • The calculated dollar value or the complete already-formatted notification data.

8. Await FINALIZED

Wait for the returned burn signature through:

solanaBlockChain.awaitTransaction(
        signature,
        SolanaCommitment.FINALIZED,
        burnFinalizationTimeout
)

Handle the result as follows:

  • SUCCEEDED: the burn is complete and notification may be attempted.
  • FAILED: log the slot and complete on-chain failure details, clear the pending burn, and do not notify.
  • TIMED_OUT: keep the pending burn and end the iteration without resubmission.
  • IOException while awaiting: keep the pending signature because the outcome remains unknown. A later iteration shall wait for the same signature again.
  • InterruptedException: preserve interruption and do not resubmit.

A timeout is not proof that a transaction failed. Retrying an equivalent burn after a timeout could burn newly received EVE unexpectedly and is therefore forbidden.

If burn submission fails without returning a signature and its outcome may be unknown, the implementation must conservatively suspend further burn submissions for the remainder of the current service lifecycle. An explicit stop/start resets this process-local suspension.

9. Send exactly one notification attempt

Only after the burn reaches FINALIZED with SUCCEEDED, call the injected BoundNotificationService once.

Use this exact message structure:

🔥🔥🔥 We have bought back and burned <EVE_AMOUNT> EVE (Evelyn IOU tokens, value $<USD_VALUE>) to reduce the circulating supply! 🔥🔥🔥

Proof:
https://solscan.io/tx/<BURN_TRANSACTION_SIGNATURE>

Formatting requirements:

  • The EVE amount must use plain decimal notation.
  • Remove unnecessary trailing zeroes from the EVE amount.
  • Do not use scientific notation.
  • The dollar value must have exactly two decimal places.
  • Use normal monetary half-up rounding for the displayed dollar value.
  • Preserve the blank line before Proof:.
  • Use the burn transaction signature, not a preceding swap signature.

Example:

🔥🔥🔥 We have bought back and burned 10 EVE (Evelyn IOU tokens, value $150.23) to reduce the circulating supply! 🔥🔥🔥

Proof:
https://solscan.io/tx/66WGtmNuMUeydKroYXyfYzSXvXviGzfsgZ3yM5ByMEgpMzY8ax5Dx4VFTvdz823gyvEGteZ5gHMjKqaiUh65WTc9

Notification delivery is not part of the blockchain transaction.

If notification delivery throws:

  • Log the delivery failure without exposing notification secrets.
  • Do not retry the notification.
  • Do not resubmit or repeat the burn.
  • Clear the completed pending burn after this one delivery attempt.

Price handling

The canonical CurrencyType resolved from each configured MoneyAmount.currencyType().id() shall be used as the base currency for pricing.

For each non-USDC input currency, construct:

new TradingPair(
        canonicalInputCurrency,
        currencyIdentityService.resolve(USDC_ID)
)

For USDC itself, identified through CurrencyTypeIds.USDC_ID, use a price of exactly 1.

If an input price cannot be resolved or is older than maximumPriceAge:

  • Log a warning containing the canonical input-currency identity but no secret data.
  • Do not use the stale or missing price.
  • Continue to a later configured input currency.
  • Still proceed to the EVE burn step after the input-currency search.

All age calculations shall use the observation’s observedAt() timestamp and the current iteration time.

Per-currency reserve behavior

Each configured MoneyAmount combines one accepted input currency with its minimum reserve.

The service must never intentionally swap more than:

wallet balance - minimumReserve.amount()

The amount and balance are denominated in the currency identified by the same configuration entry.

For native SOL, compare minimumReserve.amount() with:

burnerWallet.getSolanaBalance().amount()

Do not estimate or subtract future Solana transaction fees when enforcing the native SOL reserve.

It is accepted that a configured reserve of 0.01 SOL may become approximately 0.009995 SOL after transaction fees. Likewise, a zero SOL reserve may cause a swap to fail because insufficient SOL remains for fees. The service must not attempt to compensate for this automatically.

There is no separate minimumSolanaReserve constructor parameter.

Unknown swap-result policy

JupiterSwapService performs only one managed execution attempt, and an error after submission can leave the result unknown.

The burner service must distinguish definitive on-chain failure from an unknown outcome when the available exception information allows it:

  • A definitive on-chain FAILED result is logged and does not permanently suspend later swaps.
  • A timed-out result with unknown status suspends further swaps.
  • An IOException whose contract permits an unknown post-submission result must conservatively suspend further swaps.
  • No second input currency is attempted in the same iteration.
  • The service may still reload and burn any EVE currently visible in the wallet.
  • Later iterations continue their burn step while skipping swaps.
  • The suspended-swap state is process-local and is reset by an explicit stop/start.

This safety rule exists to prevent duplicate buybacks when a transaction may already have executed.

Process-local pending state

Pending burn signatures and swap/burn suspension flags shall remain in memory only.

Do not add persistence in this issue.

This is an intentional first-version trade-off because:

  • Unknown outcomes are expected to be rare.
  • Iterations may be spaced approximately one hour apart.
  • Wallet balances are always reloaded before later actions.
  • Persisting lifecycle state would significantly expand this issue.

An explicit stop/start clears pending and suspended state. This limitation and its safety implications must be documented in the interface JavaDoc and OpenSpec design.

Jupiter native SOL contract

The current JupiterSwapService API and OpenSpec describe only SPL-token swaps, while native SOL can be supplied to Jupiter through:

So11111111111111111111111111111111111111112

Update JupiterSwapService JavaDoc and its OpenSpec contract to explicitly support native SOL as an input represented by this canonical address.

Requirements:

  • Preserve the existing public swap(...) signature if the current implementation can already handle this representation.
  • Do not introduce a separate native-SOL swap method unless strictly required.
  • Native SOL support is required as input for this burner service.
  • Existing SPL Token and Token-2022 behavior must remain unchanged.
  • Jupiter must continue to receive the canonical SOL/WSOL mint as inputMint.
  • The burner service remains responsible for identifying SOLANA_ID, reading the native SOL balance, and applying its configured MoneyAmount reserve.
  • The canonical Jupiter mint is resolved from CIS and is not supplied separately in the burner constructor.

If a narrow implementation adjustment is required for native SOL, keep it inside the Jupiter reference implementation and document it in the OpenSpec delta.

JavaDoc requirements

JavaDoc is part of the deliverable.

The EvelynIOUBurnerService interface must document:

  • Its Evelyn-specific buyback-and-burn purpose.
  • The complete iteration order.
  • That accepted currencies and minimum reserves are configured as an ordered List<MoneyAmount>.
  • That MoneyAmount.currencyType() supplies the AssetAZ identity and MoneyAmount.amount() supplies the minimum reserve.
  • That Solana mint addresses and token programs are resolved internally and are not part of Evelyn’s configuration.
  • Currency priority and the one-swap-per-iteration rule.
  • USDC-equivalent swap limiting.
  • Per-currency reserves.
  • Fresh-price requirements.
  • Complete EVE balance burning.
  • FINALIZED burn handling.
  • Pending and unknown transaction behavior.
  • The no-resubmission rule and its reason.
  • Single-attempt notification behavior.
  • Process-local state and stop/start reset behavior.
  • Non-overlapping lifecycle behavior.

The implementation constructor must document every parameter, unit, validation rule, and the exact meaning of each MoneyAmount in inputCurrencyMinimumReserves.

Do not leave these rules only as implementation comments.

OpenSpec requirements

Create an active OpenSpec change for this issue.

Add a new capability specification for the Evelyn IOU burner service covering all behavior in this issue.

Also add the required delta to jupiter-swap-service for native SOL input.

The OpenSpec proposal, design, requirements, scenarios, and tasks must explicitly preserve:

  • AssetAZ currency UUIDs as the authoritative input identities.
  • The ordered List<MoneyAmount> configuration model.
  • Internal CIS translation from AssetAZ identities to Solana representations.
  • The deliberate absence of mint addresses and token programs from Evelyn’s input configuration.
  • Exactly one Solana mint representation per configured non-SOL currency.
  • One swap per iteration.
  • No automatic transaction resubmission.
  • Pending burn signatures.
  • Unknown swap suspension.
  • FINALIZED before notification.
  • No notification retry after a successful burn.
  • Fresh EVE price required before burn.
  • Process-local pending state rather than persistence.
  • Native SOL reserve excluding transaction-fee estimation.

Do not archive or synchronize the OpenSpec change as part of implementation unless explicitly requested separately.

Out of scope

The following are not part of this issue:

  • Configuration-file parsing or a configuration-file constructor.
  • Grouping constructor parameters into configuration objects.
  • Burner-specific input-asset interfaces, records, or class hierarchies.
  • Adding mint addresses or token programs to the burner input configuration.
  • A general CIS redesign for selecting between multiple Solana mints for one AssetAZ currency.
  • Nenjim autorun or production composition wiring.
  • Hardcoded private keys, signer names, wallet addresses, or Discord webhook details.
  • Persistence of pending transactions or suspended state.
  • Automatic retry of swaps, burns, awaits, or notifications.
  • Token-account closure or rent recovery after burning.
  • Solana transaction-fee estimation for reserve calculations.
  • A generic reusable token-burner abstraction.
  • Changes to Evelyn Mission Control.
  • Unit tests or other new automated tests.

Testing and verification

Do not add unit tests or other automated tests for this issue.

Do not add test-only hooks to production code.

Verification shall be limited to:

  • Successful production compilation/assembly.
  • Strict OpenSpec validation.
  • Review of the resulting JavaDoc and lifecycle behavior.
  • Any narrowly scoped manual verification the implementer considers safe.

Existing tests must remain untouched unless a production API change makes a minimal compile-only adjustment unavoidable.

Acceptance criteria

  • EvelynIOUBurnerService and its reference implementation exist in the specified packages.
  • Accepted input currencies and minimum reserves are represented by an ordered List<MoneyAmount>.
  • No burner-specific input-asset interface, record, or class hierarchy is introduced.
  • Each configured currency is identified by its stable AssetAZ UUID.
  • The service canonicalizes configured currency metadata through the injected CIS.
  • Native SOL is recognized through CurrencyTypeIds.SOLANA_ID.
  • Every configured non-SOL currency resolves to exactly one solana-mint external reference.
  • SPL token programs are detected internally instead of being constructor configuration.
  • All dependencies and configurable values enter through the authoritative constructor.
  • EVE mint, canonical price pairs, and notification wording remain fixed service behavior.
  • Input currencies are inspected in configured priority order.
  • At most one swap is attempted per iteration.
  • The swap amount never intentionally exceeds maximumSwapAmountInUSDC.
  • Per-currency reserves are preserved without Solana fee estimation.
  • Missing or stale prices are never used.
  • The complete visible EVE balance is burned, including EVE that existed before the current swap.
  • A burn is never announced before reaching FINALIZED.
  • Unknown transaction outcomes never cause automatic resubmission.
  • Discord receives at most one delivery attempt per successful finalized burn.
  • Notification failure never causes another burn.
  • Pending and suspended state remains process-local and resets on stop/start.
  • Iterations run sequentially and never overlap.
  • Native SOL input is explicitly supported by the Jupiter JavaDoc and OpenSpec contract.
  • OpenSpec fully documents both the required behavior and its safety rationale.
  • Production assembly and strict OpenSpec validation succeed.
  • No unit tests or other new automated tests are added.
## Background The lower-level components required for automated Evelyn IOU buyback and burn are now available: * `SolanaWallet` can provide native SOL and SPL-token balances. * `JupiterSwapService` can swap an exact input amount into another token and waits for `CONFIRMED` execution. * `SolanaWallet.burnSPLToken(...)` can submit an Evelyn IOU burn transaction. * `SolanaBlockChain.awaitTransaction(...)` can wait for the burn transaction to reach `FINALIZED`. * `BoundNotificationService` can synchronously deliver the resulting notification through a configured Discord webhook. * `TickerService` can provide the prices needed for swap limits and notification values. * `CurrencyIdentityService` can translate stable AssetAZ currency identities into their external Solana representations. A dedicated Evelyn-specific service is now needed to combine these components safely. ## Goal Implement `EvelynIOUBurnerService`, a continuously running service that: 1. Inspects a dedicated burner wallet for configured input currencies. 2. Swaps at most a configured USDC-equivalent amount of one input currency into Evelyn IOU per iteration. 3. Burns the complete available Evelyn IOU balance. 4. Waits for the burn transaction to reach `FINALIZED`. 5. Sends one notification containing the burned amount, its dollar value, and the Solscan proof link. This is specifically an Evelyn IOU service and must not be designed as a generic token burner. ## Packages and public types Add the API under: ```java com.fanitas.evelyn.service.burner ``` Add the reference implementation under: ```java com.fanitas.evelyn.service.burner.impl.ref ``` The public service interface shall be: ```java public interface EvelynIOUBurnerService { void start(); void stop(); } ``` The interface JavaDoc must describe the complete iteration order and the transaction-safety rules defined in this issue. ## Input-currency configuration The accepted input currencies and their minimum reserves shall be configured as an ordered list of the existing `MoneyAmount` value type: ```java @NotNull List<MoneyAmount> inputCurrencyMinimumReserves ``` Each `MoneyAmount` has the following meaning: * `currencyType()` identifies one accepted input currency through its stable AssetAZ UUID. * `amount()` specifies the minimum amount of that currency that must remain in the burner wallet. * Inclusion in the list means that the currency is accepted as a buyback input. * List order defines input-currency priority. Example: ```java List<MoneyAmount> inputCurrencyMinimumReserves = List.of( new MoneyAmount( new BigDecimal("0.1"), currencyIdentityService.resolve( CurrencyTypeIds.SOLANA_ID ) ), new MoneyAmount( new BigDecimal("1"), currencyIdentityService.resolve( CurrencyTypeIds.USDC_ID ) ), new MoneyAmount( new BigDecimal("5"), currencyIdentityService.resolve( ETH_ID ) ) ); ``` This example means: 1. Native SOL is the first-priority input currency, but at least `0.1 SOL` must remain. 2. USDC is the second-priority input currency, but at least `1 USDC` must remain. 3. ETH is the third-priority input currency, but at least `5 ETH` must remain. The configured `MoneyAmount.amount()` is a minimum reserve. It is not the amount that shall always be swapped. An empty list is valid and allows the service to burn EVE deposited directly into the wallet without performing buybacks. Do not add burner-specific input configuration types such as: ```java EvelynIOUBurnerInputAsset EvelynIOUBurnerSolanaInput EvelynIOUBurnerSPLTokenInput ``` Native SOL, legacy SPL tokens, and Token-2022 tokens are different Solana representations, but this distinction must not leak into Evelyn’s domain configuration. ## Currency identity and Solana representation AssetAZ currency UUIDs are the authoritative identities in the input configuration. For each configured `MoneyAmount`, the implementation shall: 1. Read the UUID from `moneyAmount.currencyType().id()`. 2. Resolve that UUID through the injected `CurrencyIdentityService`. 3. Use the canonical `CurrencyType` returned by that service for price pairs and internal processing. 4. Ignore potentially stale name or symbol metadata carried by the supplied `CurrencyType`. The UUID determines whether the input represents native SOL or a token: * `CurrencyTypeIds.SOLANA_ID` means native SOL. * Every other supported input currency must resolve to exactly one external reference in `ExternalCurrencyReference.SOLANA_MINT_NAMESPACE`. For a non-SOL input currency, the service shall use: ```java currencyIdentityService.findExternalReferences(currencyType.id()) ``` and filter the returned references by: ```java ExternalCurrencyReference.SOLANA_MINT_NAMESPACE ``` The resulting external ID is the SPL-token mint used for balance lookup and Jupiter execution. The configured `MoneyAmount` must not contain a Solana mint address or token program. Those are technical integration details derived by the service. ### Ambiguous or missing Solana representations A configured non-SOL currency must have exactly one `solana-mint` external reference. Construction shall fail if a configured non-SOL currency has: * No Solana mint reference. * More than one Solana mint reference. * A null or blank Solana mint external ID. If a future AssetAZ currency can be represented by multiple Solana mints and the burner must distinguish between them, that ambiguity should be solved through a future canonical/preferred external-reference concept in CIS. It must not be solved preemptively by leaking mint addresses back into this burner configuration. ### SPL token-program detection The SPL token program must not be a constructor parameter or part of the input configuration. When processing a non-SOL input currency, the service shall inspect the resolved mint account through `SolanaBlockChain` and determine whether the mint is owned by: * The original SPL Token Program. * The Token-2022 Program. The detected program is then used when reading the wallet balance. A missing mint account or a mint owned by an unsupported program shall cause that input currency to be skipped with a warning. It must not prevent the service from checking later configured currencies or burning an existing EVE balance. ## Reference implementation constructor The authoritative constructor shall receive both its runtime dependencies and all currently configurable behavior: ```java public EvelynIOUBurnerServiceImpl( @NotNull SolanaWallet burnerWallet, @NotNull JupiterSwapService swapService, @NotNull SolanaBlockChain solanaBlockChain, @NotNull TickerService tickerService, @NotNull CurrencyIdentityService currencyIdentityService, @NotNull BoundNotificationService notificationService, @NotNull List<MoneyAmount> inputCurrencyMinimumReserves, @NotNull ΩUSDCAmountΩ maximumSwapAmountInUSDC, int maximumSlippageBps, ΩmilliSecondsΩ maximumPriceAge, @NotNull Duration burnFinalizationTimeout, @NotNull Duration iterationInterval ) ``` Constructor validation must require: * All injected services and configuration objects to be non-null. * No null entries in `inputCurrencyMinimumReserves`. * Every configured `MoneyAmount.amount()` to be non-null and zero or greater. * Every configured `MoneyAmount.currencyType()` and its UUID to be non-null. * Every supplied currency UUID to be known to the injected `CurrencyIdentityService`. * No duplicate currency UUIDs in the list. * Evelyn IOU not to appear as an input currency. * Every configured non-SOL currency to have exactly one non-blank `solana-mint` external reference. * A defensively copied input-currency list preserving its order. * `maximumSwapAmountInUSDC` greater than zero. * `maximumSlippageBps` between `0` and `10000`, inclusive. * `maximumPriceAge` greater than zero milliseconds. * Positive, non-zero finalization timeout and iteration interval. Burner-specific validation belongs in this constructor. Do not modify the general-purpose `MoneyAmount` type solely to enforce burner-specific rules. A later issue may add a configuration-file constructor. Such a constructor should eventually load the configuration and delegate to this authoritative constructor. Configuration-file loading is not part of this issue. ## Fixed Evelyn domain values The following values are part of the service’s fixed Evelyn-specific behavior and must not be constructor parameters. ### Evelyn IOU mint ```text meveYG2iXYSkgSUn1T1uxcthH1EGMZdRHGgCntXZA3Y ``` The service shall resolve the mint’s supported SPL token program through the existing Solana APIs when it needs to obtain the EVE balance. `SolanaWallet.burnSPLToken(...)` remains responsible for its own mint and token-program validation. ### Native SOL representation used by Jupiter Native SOL is identified in the burner configuration by: ```java CurrencyTypeIds.SOLANA_ID ``` Its wallet balance is obtained through: ```java burnerWallet.getSolanaBalance() ``` It must not be read as an SPL-token balance. When native SOL is supplied to Jupiter, the service shall use the canonical Solana mint external reference associated with `SOLANA_ID` through CIS: ```text So11111111111111111111111111111111111111112 ``` This canonical SOL/WSOL identifier is a Jupiter and Solana integration detail. It is not part of the burner’s constructor configuration. ### Price pairs The EVE dollar value used in notifications always comes from the canonical `EVE_USDT` pair constructed from: ```java CurrencyTypeIds.EVE_ID CurrencyTypeIds.USDT_ID ``` USDT is treated as USD for the notification value. Input-currency swap limits use `<INPUT_CURRENCY>_USDC`, with the quote currency resolved through: ```java CurrencyTypeIds.USDC_ID ``` These pairs are constructed internally through the injected `CurrencyIdentityService`; they are not constructor parameters. ### Notification wording The notification format is fixed Evelyn behavior and is not configurable in this issue. ## Service lifecycle The implementation shall use one dedicated background worker. * The first iteration begins immediately after `start()`. * Later iterations use fixed delay: `iterationInterval` begins after the previous iteration has completed. * Iterations must never overlap. * Only one worker may be active for a service instance. * `stop()` prevents new iterations and stops the worker cleanly. * Interruption must be handled correctly and the thread interrupt status must not be silently lost. * Process-local pending and suspended state is reset by an explicit stop/start cycle. No Nenjim autorun wiring shall be added in this issue. The wallet, signer, webhook, and other production configuration must not be hardcoded in the composition root. ## Required iteration order The complete order below is part of the public service contract and must be documented in the JavaDoc of `EvelynIOUBurnerService`. ### 1. Handle a pending burn first If a previous burn returned a transaction signature but did not reach a definitive `FINALIZED` result, the iteration shall only continue waiting for that exact signature. It must not submit another burn while the previous burn remains pending. If the pending burn still has an unknown outcome, end the iteration without performing a new swap or burn. If the pending burn becomes finalized successfully, perform the single notification attempt associated with that burn and then end the iteration. If the pending burn becomes definitively failed, log the on-chain failure details, clear the pending state, send no notification, and allow a later iteration to reload the wallet balance and decide whether a new burn is required. ### 2. Select at most one input currency Unless swaps are suspended due to an earlier unknown swap outcome, inspect `inputCurrencyMinimumReserves` in its configured order. For each `MoneyAmount minimumReserve`: 1. Resolve its canonical `CurrencyType` through CIS using the configured UUID. 2. Determine whether the currency is native SOL or a mint-backed Solana token. 3. Read the corresponding current wallet balance. 4. Subtract `minimumReserve.amount()` from that balance. 5. Skip the currency if the remaining amount is zero or negative. 6. Resolve a sufficiently fresh USDC price. 7. Skip the candidate with a warning if its balance, Solana representation, token program, or price cannot be obtained. 8. Continue until the first candidate with a positive spendable balance and usable price is found. For native SOL: ```java ΩAmountΩ balance = burnerWallet.getSolanaBalance().amount(); ``` For an SPL token, use the CIS-resolved mint and detected token program with: ```java burnerWallet.getSPLTokenBalance(mintAddress, tokenProgram) ``` A missing SPL-token account is equivalent to a zero balance for that configured currency. At most one swap attempt may be made per iteration. Once a swap has been attempted, no later input currency may be attempted in the same iteration, even if the swap fails. Failure or absence of an input currency must not prevent already available EVE from being burned later in the iteration. An `InterruptedException` must preserve interruption and end processing. It must not be converted into a normal skipped input. ### 3. Calculate the exact maximum input amount For a normal input currency whose price represents USDC per unit: ```text maximumInputAmount = maximumSwapAmountInUSDC / inputCurrencyPriceInUSDC ``` The actual amount is: ```text availableForSwap = max(balance - configuredMinimumReserve, 0) amountToSwap = min(availableForSwap, maximumInputAmount) ``` The result must be rounded down to the input currency’s supported decimal precision so that the configured USDC maximum is never exceeded through rounding. If the rounded amount is zero, skip the swap. Example: ```text maximumSwapAmountInUSDC = 1 ETH_USDC price = 100 USDC per ETH wallet balance = 10 ETH configured minimum reserve = 5 ETH ``` The service may swap: ```text availableForSwap = 10 ETH - 5 ETH = 5 ETH maximumInputAmount = 1 / 100 ETH = 0.01 ETH amountToSwap = min(5 ETH, 0.01 ETH) = 0.01 ETH ``` For USDC itself, use a price of exactly `1` without requiring a ticker observation. Native SOL uses its native wallet balance, nine-decimal precision, and the canonical CIS-resolved SOL/WSOL mint when invoking Jupiter. SPL-token precision must be obtained from existing Solana mint metadata. ### 4. Perform no more than one swap Invoke the injected `JupiterSwapService` using: * The selected input currency’s CIS-resolved Jupiter input mint. * The calculated exact input amount. * The hardcoded Evelyn IOU output mint. * `maximumSlippageBps`. For native SOL, the Jupiter input mint is the canonical SOL/WSOL mint resolved from the `SOLANA_ID` external references. For an SPL token, the Jupiter input mint is the unique `solana-mint` reference resolved from that currency’s UUID. Normal return from `JupiterSwapService.swap(...)` already means that the swap reached Solana `CONFIRMED`. The burner service must not perform a second confirmation wait for a normally returned swap. The amount subsequently burned must not be derived only from `JupiterSwapResult.receivedOutputTokenAmount()`. The wallet can already contain EVE from previous transfers or swaps. Reload the complete EVE balance after the swap attempt. ### 5. Reload the complete EVE balance Read the complete available Evelyn IOU balance from the burner wallet. * Resolve the EVE mint’s supported token program through the existing Solana APIs. * If the EVE token account is absent or its balance is zero, no burn or notification is performed. * Every positive EVE balance is eligible for burning. * There is no minimum EVE burn threshold. * `maximumSwapAmountInUSDC` limits buybacks only. It must never limit the amount of EVE burned. * Existing EVE must still be considered for burning when no swap was performed or a swap failed. ### 6. Obtain a fresh EVE/USDT price Before submitting the burn, obtain the latest price for the internally constructed canonical `EVE_USDT` pair. The price must: * Be positive. * Have a non-null observation timestamp. * Be no more than `maximumPriceAge` milliseconds old. If the EVE price is missing, invalid, unsupported, or too old: * Log a warning. * Skip the burn for this iteration. * Send no notification. * Try again during a later iteration. The burn is deliberately postponed in this situation because every successful burn must be announced with a dollar value. The dollar value is: ```text complete EVE amount to burn × latest EVE/USDT price ``` ### 7. Submit the complete EVE burn once Call: ```java burnerWallet.burnSPLToken( EVELYN_IOU_MINT, completeEvelynIOUBalance ) ``` The complete observed EVE balance must be submitted as one burn. Do not automatically retry, rebuild, re-sign, or resubmit the burn within the iteration. After a signature has been returned, retain process-local pending information containing at least: * The transaction signature. * The submitted EVE amount. * The calculated dollar value or the complete already-formatted notification data. ### 8. Await `FINALIZED` Wait for the returned burn signature through: ```java solanaBlockChain.awaitTransaction( signature, SolanaCommitment.FINALIZED, burnFinalizationTimeout ) ``` Handle the result as follows: * `SUCCEEDED`: the burn is complete and notification may be attempted. * `FAILED`: log the slot and complete on-chain failure details, clear the pending burn, and do not notify. * `TIMED_OUT`: keep the pending burn and end the iteration without resubmission. * `IOException` while awaiting: keep the pending signature because the outcome remains unknown. A later iteration shall wait for the same signature again. * `InterruptedException`: preserve interruption and do not resubmit. A timeout is not proof that a transaction failed. Retrying an equivalent burn after a timeout could burn newly received EVE unexpectedly and is therefore forbidden. If burn submission fails without returning a signature and its outcome may be unknown, the implementation must conservatively suspend further burn submissions for the remainder of the current service lifecycle. An explicit stop/start resets this process-local suspension. ### 9. Send exactly one notification attempt Only after the burn reaches `FINALIZED` with `SUCCEEDED`, call the injected `BoundNotificationService` once. Use this exact message structure: ```text 🔥🔥🔥 We have bought back and burned <EVE_AMOUNT> EVE (Evelyn IOU tokens, value $<USD_VALUE>) to reduce the circulating supply! 🔥🔥🔥 Proof: https://solscan.io/tx/<BURN_TRANSACTION_SIGNATURE> ``` Formatting requirements: * The EVE amount must use plain decimal notation. * Remove unnecessary trailing zeroes from the EVE amount. * Do not use scientific notation. * The dollar value must have exactly two decimal places. * Use normal monetary half-up rounding for the displayed dollar value. * Preserve the blank line before `Proof:`. * Use the burn transaction signature, not a preceding swap signature. Example: ```text 🔥🔥🔥 We have bought back and burned 10 EVE (Evelyn IOU tokens, value $150.23) to reduce the circulating supply! 🔥🔥🔥 Proof: https://solscan.io/tx/66WGtmNuMUeydKroYXyfYzSXvXviGzfsgZ3yM5ByMEgpMzY8ax5Dx4VFTvdz823gyvEGteZ5gHMjKqaiUh65WTc9 ``` Notification delivery is not part of the blockchain transaction. If notification delivery throws: * Log the delivery failure without exposing notification secrets. * Do not retry the notification. * Do not resubmit or repeat the burn. * Clear the completed pending burn after this one delivery attempt. ## Price handling The canonical `CurrencyType` resolved from each configured `MoneyAmount.currencyType().id()` shall be used as the base currency for pricing. For each non-USDC input currency, construct: ```java new TradingPair( canonicalInputCurrency, currencyIdentityService.resolve(USDC_ID) ) ``` For USDC itself, identified through `CurrencyTypeIds.USDC_ID`, use a price of exactly `1`. If an input price cannot be resolved or is older than `maximumPriceAge`: * Log a warning containing the canonical input-currency identity but no secret data. * Do not use the stale or missing price. * Continue to a later configured input currency. * Still proceed to the EVE burn step after the input-currency search. All age calculations shall use the observation’s `observedAt()` timestamp and the current iteration time. ## Per-currency reserve behavior Each configured `MoneyAmount` combines one accepted input currency with its minimum reserve. The service must never intentionally swap more than: ```text wallet balance - minimumReserve.amount() ``` The amount and balance are denominated in the currency identified by the same configuration entry. For native SOL, compare `minimumReserve.amount()` with: ```java burnerWallet.getSolanaBalance().amount() ``` Do not estimate or subtract future Solana transaction fees when enforcing the native SOL reserve. It is accepted that a configured reserve of `0.01 SOL` may become approximately `0.009995 SOL` after transaction fees. Likewise, a zero SOL reserve may cause a swap to fail because insufficient SOL remains for fees. The service must not attempt to compensate for this automatically. There is no separate `minimumSolanaReserve` constructor parameter. ## Unknown swap-result policy `JupiterSwapService` performs only one managed execution attempt, and an error after submission can leave the result unknown. The burner service must distinguish definitive on-chain failure from an unknown outcome when the available exception information allows it: * A definitive on-chain `FAILED` result is logged and does not permanently suspend later swaps. * A timed-out result with unknown status suspends further swaps. * An `IOException` whose contract permits an unknown post-submission result must conservatively suspend further swaps. * No second input currency is attempted in the same iteration. * The service may still reload and burn any EVE currently visible in the wallet. * Later iterations continue their burn step while skipping swaps. * The suspended-swap state is process-local and is reset by an explicit stop/start. This safety rule exists to prevent duplicate buybacks when a transaction may already have executed. ## Process-local pending state Pending burn signatures and swap/burn suspension flags shall remain in memory only. Do not add persistence in this issue. This is an intentional first-version trade-off because: * Unknown outcomes are expected to be rare. * Iterations may be spaced approximately one hour apart. * Wallet balances are always reloaded before later actions. * Persisting lifecycle state would significantly expand this issue. An explicit stop/start clears pending and suspended state. This limitation and its safety implications must be documented in the interface JavaDoc and OpenSpec design. ## Jupiter native SOL contract The current `JupiterSwapService` API and OpenSpec describe only SPL-token swaps, while native SOL can be supplied to Jupiter through: ```text So11111111111111111111111111111111111111112 ``` Update `JupiterSwapService` JavaDoc and its OpenSpec contract to explicitly support native SOL as an input represented by this canonical address. Requirements: * Preserve the existing public `swap(...)` signature if the current implementation can already handle this representation. * Do not introduce a separate native-SOL swap method unless strictly required. * Native SOL support is required as input for this burner service. * Existing SPL Token and Token-2022 behavior must remain unchanged. * Jupiter must continue to receive the canonical SOL/WSOL mint as `inputMint`. * The burner service remains responsible for identifying `SOLANA_ID`, reading the native SOL balance, and applying its configured `MoneyAmount` reserve. * The canonical Jupiter mint is resolved from CIS and is not supplied separately in the burner constructor. If a narrow implementation adjustment is required for native SOL, keep it inside the Jupiter reference implementation and document it in the OpenSpec delta. ## JavaDoc requirements JavaDoc is part of the deliverable. The `EvelynIOUBurnerService` interface must document: * Its Evelyn-specific buyback-and-burn purpose. * The complete iteration order. * That accepted currencies and minimum reserves are configured as an ordered `List<MoneyAmount>`. * That `MoneyAmount.currencyType()` supplies the AssetAZ identity and `MoneyAmount.amount()` supplies the minimum reserve. * That Solana mint addresses and token programs are resolved internally and are not part of Evelyn’s configuration. * Currency priority and the one-swap-per-iteration rule. * USDC-equivalent swap limiting. * Per-currency reserves. * Fresh-price requirements. * Complete EVE balance burning. * `FINALIZED` burn handling. * Pending and unknown transaction behavior. * The no-resubmission rule and its reason. * Single-attempt notification behavior. * Process-local state and stop/start reset behavior. * Non-overlapping lifecycle behavior. The implementation constructor must document every parameter, unit, validation rule, and the exact meaning of each `MoneyAmount` in `inputCurrencyMinimumReserves`. Do not leave these rules only as implementation comments. ## OpenSpec requirements Create an active OpenSpec change for this issue. Add a new capability specification for the Evelyn IOU burner service covering all behavior in this issue. Also add the required delta to `jupiter-swap-service` for native SOL input. The OpenSpec proposal, design, requirements, scenarios, and tasks must explicitly preserve: * AssetAZ currency UUIDs as the authoritative input identities. * The ordered `List<MoneyAmount>` configuration model. * Internal CIS translation from AssetAZ identities to Solana representations. * The deliberate absence of mint addresses and token programs from Evelyn’s input configuration. * Exactly one Solana mint representation per configured non-SOL currency. * One swap per iteration. * No automatic transaction resubmission. * Pending burn signatures. * Unknown swap suspension. * `FINALIZED` before notification. * No notification retry after a successful burn. * Fresh EVE price required before burn. * Process-local pending state rather than persistence. * Native SOL reserve excluding transaction-fee estimation. Do not archive or synchronize the OpenSpec change as part of implementation unless explicitly requested separately. ## Out of scope The following are not part of this issue: * Configuration-file parsing or a configuration-file constructor. * Grouping constructor parameters into configuration objects. * Burner-specific input-asset interfaces, records, or class hierarchies. * Adding mint addresses or token programs to the burner input configuration. * A general CIS redesign for selecting between multiple Solana mints for one AssetAZ currency. * Nenjim autorun or production composition wiring. * Hardcoded private keys, signer names, wallet addresses, or Discord webhook details. * Persistence of pending transactions or suspended state. * Automatic retry of swaps, burns, awaits, or notifications. * Token-account closure or rent recovery after burning. * Solana transaction-fee estimation for reserve calculations. * A generic reusable token-burner abstraction. * Changes to Evelyn Mission Control. * Unit tests or other new automated tests. ## Testing and verification Do not add unit tests or other automated tests for this issue. Do not add test-only hooks to production code. Verification shall be limited to: * Successful production compilation/assembly. * Strict OpenSpec validation. * Review of the resulting JavaDoc and lifecycle behavior. * Any narrowly scoped manual verification the implementer considers safe. Existing tests must remain untouched unless a production API change makes a minimal compile-only adjustment unavoidable. ## Acceptance criteria * `EvelynIOUBurnerService` and its reference implementation exist in the specified packages. * Accepted input currencies and minimum reserves are represented by an ordered `List<MoneyAmount>`. * No burner-specific input-asset interface, record, or class hierarchy is introduced. * Each configured currency is identified by its stable AssetAZ UUID. * The service canonicalizes configured currency metadata through the injected CIS. * Native SOL is recognized through `CurrencyTypeIds.SOLANA_ID`. * Every configured non-SOL currency resolves to exactly one `solana-mint` external reference. * SPL token programs are detected internally instead of being constructor configuration. * All dependencies and configurable values enter through the authoritative constructor. * EVE mint, canonical price pairs, and notification wording remain fixed service behavior. * Input currencies are inspected in configured priority order. * At most one swap is attempted per iteration. * The swap amount never intentionally exceeds `maximumSwapAmountInUSDC`. * Per-currency reserves are preserved without Solana fee estimation. * Missing or stale prices are never used. * The complete visible EVE balance is burned, including EVE that existed before the current swap. * A burn is never announced before reaching `FINALIZED`. * Unknown transaction outcomes never cause automatic resubmission. * Discord receives at most one delivery attempt per successful finalized burn. * Notification failure never causes another burn. * Pending and suspended state remains process-local and resets on stop/start. * Iterations run sequentially and never overlap. * Native SOL input is explicitly supported by the Jupiter JavaDoc and OpenSpec contract. * OpenSpec fully documents both the required behavior and its safety rationale. * Production assembly and strict OpenSpec validation succeed. * No unit tests or other new automated tests are added.
minimons added the enhancement label 2026-08-12 19:18:37 +02:00
minimons self-assigned this 2026-08-12 19:18:37 +02:00
minimons added this to the Evelyn project 2026-08-12 19:18:37 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: r35157/com_r35157_nenjim-hubd-impl_ref#72