Files

10 KiB

Context

See proposal.md for motivation. The lower-level services already separate wallet state and signing, Jupiter's single managed execution plus CONFIRMED wait, generic Solana FINALIZED awaiting, price observations, AssetAZ identity translation, and synchronous bound notification delivery. The new component must orchestrate those boundaries without widening their APIs, leaking Solana details into Evelyn configuration, or mistaking timeout and communication failure for permission to repeat a transaction.

Goals / Non-Goals

Goals:

  • Keep the public API minimal while documenting the complete externally significant iteration and safety contract.
  • Make stable AssetAZ UUIDs and an ordered List<MoneyAmount> the only input-selection model, with CIS and Solana resolving technical representations internally.
  • Encode swap and burn uncertainty as conservative process-local lifecycle state so a periodic worker cannot duplicate an operation automatically.
  • Keep each iteration sequential, permit existing EVE to be burned independently of swap success, and bind notification data to the exact submitted burn.

Non-Goals:

  • Persisting reconciliation state or providing operator recovery tooling after restart.
  • Adding configuration objects, parsers, extra constructors, Nenjim autorun/composition wiring, or wallet/webhook secrets.
  • Introducing burner-specific input types, a reusable generic burner, new ValueTags, dependencies, tests, or production test hooks.
  • Retrying transactions, transaction awaits, or notifications, closing token accounts, or estimating native SOL fees.

Decisions

Keep input configuration unchanged and canonicalize on use

The implementation stores List.copyOf(inputCurrencyMinimumReserves) after validating every entry. It does not replace MoneyAmount, attach mint/program fields, or create an input-specific interface, record, or hierarchy. Construction resolves every configured UUID to prove it is known, detects duplicates and EVE, and verifies exactly one non-blank solana-mint reference for every non-SOL input. Each iteration resolves the UUID again and uses the returned canonical CurrencyType, deliberately ignoring configured display metadata.

Caching pre-resolved integration DTOs was rejected because it would create the prohibited parallel burner input model and weaken the visible rule that UUID is authoritative. Extending MoneyAmount was rejected because reserve validity and accepted-input policy belong only to this service.

Resolve representation and precision at the point of use

The input loop handles candidates inline in priority order rather than materializing burner-specific candidate objects. SOL is recognized by CurrencyTypeIds.SOLANA_ID, read through getSolanaBalance(), assigned its protocol-defined nine decimals, and translated to the unique canonical CIS SOL/WSOL mint only when calling Jupiter. Other currencies resolve their unique CIS mint, inspect the mint-account owner against the two supported SolanaSPLTokenProgram values, read balance with that program, and obtain decimals from current mint supply metadata.

A constructor-supplied program or mint was rejected because those are mutable integration facts rather than Evelyn domain configuration. Treating native SOL as a wallet SPL holding was rejected because it would observe the wrong balance. A general CIS preferred-mint redesign was rejected because the current burner contract deliberately fails ambiguous non-SOL configuration.

Round the exact-input limit downward at mint precision

For a candidate with positive spendable balance and usable price, the implementation divides the positive USDC cap by price using the input precision and RoundingMode.DOWN, takes the lesser of that cap and balance - reserve, then applies the same downward scale once more. USDC bypasses ticker lookup with an exact price of one. This produces an exactly representable Jupiter amount that cannot exceed the USDC cap through rounding.

Using arbitrary high precision followed by Jupiter rejection was rejected because supported precision is already available. Half-up rounding was rejected for transaction amounts because it can cross either the configured cap or reserve boundary. SOL fees remain intentionally outside the calculation, so execution may consume part of the reserve or fail when no fee balance remains.

Use one daemon scheduled worker and fixed delay

Each start creates one single-thread scheduled executor, resets process-local state, and schedules with zero initial delay plus the validated interval as fixed delay. The task boundary catches ordinary failures so one bad iteration does not silently cancel future scheduling. Interruption is restored and terminates current processing. Stop prevents scheduling, interrupts the executor, waits for bounded clean termination, and clears state only after the worker has ended; a still-active or stopping instance cannot start another worker.

A timer that dispatches concurrent jobs was rejected because overlap would violate transaction ordering. A global worker was rejected because state and lifecycle belong to the service instance. Fixed rate was rejected because a slow blockchain wait could cause catch-up behavior instead of delaying from completion.

Model transaction uncertainty as two suspensions plus one pending burn

The worker owns separate boolean flags for swap and burn suspension and one private pending-burn value containing signature, exact submitted EVE amount, and calculated USD value. A pending signature always short-circuits the next iteration before input inspection. Timeout and await I/O retain it; definitive failure clears it without same-iteration work; success proceeds to the associated notification only.

For swaps, JupiterTransactionOutcomeException exposes the needed distinction: FAILED is definitive and allows later-iteration swaps, while TIMED_OUT suspends them. Any swap IOException is treated as potentially post-submission because the public contract permits that state. Interruption is also conservative because it can occur during execution or confirmation. In every case the input loop returns after its first Jupiter invocation.

For burns, a returned signature is stored before awaiting. An IOException, interruption, blank signature, or other submission-contract failure that can have occurred after send suspends future burn submissions when no usable signature is available. Local validation failures that are known to precede submission may be reconsidered after a later state reload. Swap and burn suspension remain separate: one unknown operation does not authorize repetition and does not erase the other safe parts of an iteration.

Persisting flags/signatures was rejected by issue scope. Consequently, an explicit stop/start or process restart intentionally forgets unknown state. Wallet reloading reduces stale-state risk but cannot prove an unknown transaction is dead; operators should reconcile known chain activity before deliberately resetting a suspended or pending lifecycle.

Bind price and notification data before the burn

The complete EVE wallet balance is always reloaded after the optional swap attempt rather than trusting JupiterSwapResult.receivedOutputTokenAmount(). Before submission, the implementation validates the latest canonical EVE/USDT observation against the current time and stores the resulting unrounded USD value together with the returned burn signature. This postpones every burn that could not be truthfully announced and ensures a later price movement cannot change the value associated with a pending burn.

On successful FINALIZED, the pending state is made ineligible for another delivery before calling the synchronous notification service. Amount formatting uses stripTrailingZeros().toPlainString(); USD formatting uses scale two and HALF_UP. Delivery exceptions are logged by type and transaction identity without including destination details, and pending state is never restored.

Deriving the burn from Jupiter output was rejected because the wallet can already contain EVE. Pricing after finalization was rejected because a future missing price would leave a completed burn without its required announcement. Keeping pending state until notification returns was rejected because an unchecked delivery failure could otherwise cause a second attempt.

Clarify Jupiter native SOL support without widening its API

The existing Jupiter reference implementation already treats its inputTokenMint as a mint used for precision validation and passes it unchanged to Swap V2. The canonical SOL/WSOL mint is a valid Solana mint, and Jupiter interprets it for native SOL input from the taker. Therefore only the public JavaDoc and jupiter-swap-service contract need clarification; no separate method or implementation branch is required, and legacy SPL plus Token-2022 behavior remains unchanged.

Risks / Trade-offs

  • [A process or explicit lifecycle reset forgets a pending signature or suspension] → Document the safety limitation prominently, reload balances on every later decision, and require deliberate external reconciliation before an operator resets unknown state.
  • [A wallet or network exception may have occurred before submission but cannot prove that] → Prefer conservative lifecycle suspension over a possible duplicate buyback or burn.
  • [A stale mint reference was valid at construction but CIS changes dynamically] → Resolve canonical identity and external references again during processing and skip unusable representations safely.
  • [A mint balance or supply changes between reads and submission] → Submit only exactly representable observed amounts once and let wallet/Solana validation reject races without automatic retry.
  • [Notification succeeds remotely but the response is lost] → Count the single local invocation as the only attempt and never retry it, because notification delivery is not transaction completion.
  • [Stop cannot promptly interrupt a misbehaving dependency] → Use interruption plus bounded termination and refuse a new worker while termination remains unconfirmed.

Migration Plan

  1. Add the Evelyn burner API and reference implementation without composing or starting it in production.
  2. Clarify the existing Jupiter JavaDoc while preserving its public method signature and reference implementation.
  3. Compile and assemble production sources, perform strict OpenSpec validation, and use structural/manual review only; add no automated tests.
  4. Leave this change active for review. Rollback removes the new service and documentation delta; there is no persisted state, configuration, or data migration.