93: Add a minimum EVE burn amount and retained reserve to the Evelyn IOU burner
This commit is contained in:
@@ -23,6 +23,11 @@ import java.util.List;
|
||||
* internally through the Currency Identity Service and Solana and are not
|
||||
* part of Evelyn's input configuration.</p>
|
||||
*
|
||||
* <p>Two independent EVE-denominated {@link MoneyAmount} values configure the
|
||||
* minimum per burn and retained EVE reserve. Their canonical EVE UUID is
|
||||
* authoritative; the minimum must be strictly positive and the reserve
|
||||
* non-negative. A burn threshold does not suppress independent buybacks.</p>
|
||||
*
|
||||
* <p>Every non-overlapping iteration has this order:</p>
|
||||
* <ol>
|
||||
* <li>If a prior burn has a pending signature, await only that exact
|
||||
@@ -56,11 +61,16 @@ import java.util.List;
|
||||
* <li>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 input buyback limit never caps
|
||||
* the amount eligible for burning.</li>
|
||||
* the amount eligible for burning. Subtract the retained EVE reserve and
|
||||
* round the positive remainder down to EVE mint precision. If the actual
|
||||
* rounded amount is zero or below the burn minimum, keep the EVE for a
|
||||
* later iteration and send no burn notification. Equality qualifies;</n+ * never round upward or consume reserve to reach the minimum.</li>
|
||||
* <li>Require a positive, sufficiently fresh canonical EVE/USDT price and
|
||||
* calculate the complete balance's dollar value before submitting a burn.
|
||||
* calculate the actual eligible burn amount's dollar value, excluding
|
||||
* the retained reserve, before submitting a burn.
|
||||
* Missing, invalid, or stale price data postpones the burn.</li>
|
||||
* <li>Submit the complete EVE balance once and retain its signature,
|
||||
* <li>Submit all eligible rounded EVE above reserve once, without a burn
|
||||
* maximum or fixed batch size, and retain its signature,
|
||||
* amount, and notification value as pending before waiting. The service
|
||||
* never automatically retries, rebuilds, re-signs, or resubmits a swap or
|
||||
* burn because timeout, interruption, or communication failure can leave
|
||||
|
||||
+79
-7
@@ -85,8 +85,16 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
* equality qualifies without rounding up or spending the reserve. Three
|
||||
* empty input lists permit direct-EVE-only burning.</p>
|
||||
*
|
||||
* <p>{@code minimumBurnAmount} and {@code eveReserve} must identify EVE by
|
||||
* its canonical UUID, regardless of supplied name/symbol. The minimum must
|
||||
* be strictly positive and the reserve non-negative. A new burn uses the
|
||||
* entire balance above reserve, rounded down to EVE mint precision, only
|
||||
* when that actual amount is positive and at least the minimum. Equality
|
||||
* qualifies; fractional remainders stay in the wallet. This policy does
|
||||
* not suppress independent small buybacks or re-evaluate pending burns.</p>
|
||||
*
|
||||
* @param burnerWallet dedicated wallet whose balances are inspected and
|
||||
* whose complete EVE balance is burned
|
||||
* whose eligible EVE above reserve 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
|
||||
@@ -108,6 +116,10 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
* @param inputCurrencyMaximumSwapAmounts exactly one strictly positive
|
||||
* token-denominated buyback maximum per accepted input UUID;
|
||||
* it does not limit burns
|
||||
* @param minimumBurnAmount strictly positive EVE-denominated minimum for
|
||||
* the actual rounded amount of each new burn
|
||||
* @param eveReserve non-negative EVE-denominated amount retained after a
|
||||
* successful burn; zero is valid
|
||||
* @param maximumSlippageBps Jupiter slippage limit in basis points from 0
|
||||
* through 10000 inclusive
|
||||
* @param maximumPriceAge strictly positive maximum observation age in
|
||||
@@ -126,7 +138,9 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
* lacks exactly one usable Solana mint,
|
||||
* or minimum/maximum UUIDs do not match inputs,
|
||||
* a swap bound is non-positive, or a
|
||||
* minimum exceeds its maximum
|
||||
* minimum exceeds its maximum, or the
|
||||
* burn minimum/reserve does not identify
|
||||
* EVE or has an invalid numeric value
|
||||
*/
|
||||
public EvelynIOUBurnerServiceImpl(
|
||||
@NotNull SolanaWallet burnerWallet,
|
||||
@@ -138,6 +152,8 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
@NotNull List<MoneyAmount> inputCurrencyMinimumReserves,
|
||||
@NotNull List<MoneyAmount> inputCurrencyMinimumSwapAmounts,
|
||||
@NotNull List<MoneyAmount> inputCurrencyMaximumSwapAmounts,
|
||||
@NotNull MoneyAmount minimumBurnAmount,
|
||||
@NotNull MoneyAmount eveReserve,
|
||||
int maximumSlippageBps,
|
||||
ΩmilliSecondsΩ maximumPriceAge,
|
||||
@NotNull Duration burnFinalizationTimeout,
|
||||
@@ -211,6 +227,12 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
USDT_ID,
|
||||
"USDT"
|
||||
);
|
||||
this.minimumBurnAmount = validateEveAmount(
|
||||
minimumBurnAmount, "minimumBurnAmount", canonicalEve, true
|
||||
);
|
||||
this.eveReserve = validateEveAmount(
|
||||
eveReserve, "eveReserve", canonicalEve, false
|
||||
);
|
||||
this.eveUsdtTradingPair = new TradingPair(
|
||||
canonicalEve,
|
||||
canonicalUsdt
|
||||
@@ -519,9 +541,9 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
|
||||
private void processCompleteEveBalance() throws InterruptedException {
|
||||
ΩAmountΩ eveBalance;
|
||||
SolanaSPLTokenProgram eveTokenProgram;
|
||||
try {
|
||||
SolanaSPLTokenProgram eveTokenProgram =
|
||||
resolveSupportedTokenProgram(EVELYN_IOU_MINT);
|
||||
eveTokenProgram = resolveSupportedTokenProgram(EVELYN_IOU_MINT);
|
||||
eveBalance = burnerWallet.getSPLTokenBalance(
|
||||
EVELYN_IOU_MINT,
|
||||
eveTokenProgram
|
||||
@@ -554,17 +576,40 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
return;
|
||||
}
|
||||
|
||||
ΩAmountΩ availableForBurn = eveBalance.subtract(eveReserve.amount());
|
||||
if (availableForBurn.signum() <= 0) {
|
||||
return;
|
||||
}
|
||||
ΩAmountΩ amountToBurn;
|
||||
try {
|
||||
ΩamountDecimalsΩ decimals = resolveMintDecimals(
|
||||
EVELYN_IOU_MINT, eveTokenProgram
|
||||
);
|
||||
amountToBurn = availableForBurn.setScale(decimals, RoundingMode.DOWN);
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
log.warn(
|
||||
"Could not resolve Evelyn IOU mint precision; burn is "
|
||||
+ "postponed: failureType={}",
|
||||
exception.getClass().getName()
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (amountToBurn.signum() <= 0
|
||||
|| amountToBurn.compareTo(minimumBurnAmount.amount()) < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
ΩPriceΩ evePriceInUsdt = findFreshPrice(eveUsdtTradingPair);
|
||||
if (evePriceInUsdt == null) {
|
||||
return;
|
||||
}
|
||||
ΩAmountΩ dollarValue = eveBalance.multiply(evePriceInUsdt);
|
||||
ΩAmountΩ dollarValue = amountToBurn.multiply(evePriceInUsdt);
|
||||
|
||||
ΩSolanaTransactionSignatureΩ burnSignature;
|
||||
try {
|
||||
burnSignature = burnerWallet.burnSPLToken(
|
||||
EVELYN_IOU_MINT,
|
||||
eveBalance
|
||||
amountToBurn
|
||||
);
|
||||
} catch (IOException exception) {
|
||||
burnsSuspended = true;
|
||||
@@ -607,7 +652,7 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
|
||||
pendingBurn = new PendingBurn(
|
||||
burnSignature,
|
||||
eveBalance,
|
||||
amountToBurn,
|
||||
dollarValue
|
||||
);
|
||||
awaitPendingBurn();
|
||||
@@ -826,6 +871,31 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
return supply.decimals();
|
||||
}
|
||||
|
||||
private static MoneyAmount validateEveAmount(
|
||||
MoneyAmount configuredAmount,
|
||||
String configurationName,
|
||||
CurrencyType canonicalEve,
|
||||
boolean strictlyPositive
|
||||
) {
|
||||
Objects.requireNonNull(configuredAmount, configurationName);
|
||||
Objects.requireNonNull(configuredAmount.amount(), configurationName + ".amount");
|
||||
CurrencyType currency = Objects.requireNonNull(
|
||||
configuredAmount.currencyType(), configurationName + ".currencyType"
|
||||
);
|
||||
UUID currencyId = Objects.requireNonNull(
|
||||
currency.id(), configurationName + ".currencyType.id"
|
||||
);
|
||||
if (!EVE_ID.equals(currencyId)) {
|
||||
throw new IllegalArgumentException(configurationName + " must identify EVE by UUID");
|
||||
}
|
||||
if (configuredAmount.amount().signum() < 0
|
||||
|| (strictlyPositive && configuredAmount.amount().signum() == 0)) {
|
||||
throw new IllegalArgumentException(configurationName
|
||||
+ (strictlyPositive ? " must be greater than zero" : " must be zero or greater"));
|
||||
}
|
||||
return new MoneyAmount(configuredAmount.amount(), canonicalEve);
|
||||
}
|
||||
|
||||
private List<MoneyAmount> validateAndCopyInputs(
|
||||
List<MoneyAmount> configuredInputs
|
||||
) {
|
||||
@@ -1059,6 +1129,8 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
private final List<MoneyAmount> inputCurrencyMinimumReserves;
|
||||
private final Map<UUID, MoneyAmount> inputCurrencyMinimumSwapAmounts;
|
||||
private final Map<UUID, MoneyAmount> inputCurrencyMaximumSwapAmounts;
|
||||
private final MoneyAmount minimumBurnAmount;
|
||||
private final MoneyAmount eveReserve;
|
||||
private final int maximumSlippageBps;
|
||||
private final ΩmilliSecondsΩ maximumPriceAge;
|
||||
private final Duration maximumPriceAgeDuration;
|
||||
|
||||
@@ -329,6 +329,8 @@ public final class NenjimHubImpl implements NenjimHub {
|
||||
inputCurrencyMinimumReserves,
|
||||
inputCurrencyMinimumSwapAmounts,
|
||||
inputCurrencyMaximumSwapAmounts,
|
||||
new MoneyAmount(new ΩAmountΩ("0.1"), cis.resolve(CurrencyTypeIds.EVE_ID)),
|
||||
new MoneyAmount(new ΩAmountΩ("0.01"), cis.resolve(CurrencyTypeIds.EVE_ID)),
|
||||
200,
|
||||
2*1000*60L,
|
||||
Duration.ofSeconds(10),
|
||||
|
||||
Reference in New Issue
Block a user