90: Support SyrupUSDC in the Evelyn IOU burner with per-currency swap limits
This commit is contained in:
@@ -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.</p>
|
||||
@@ -26,10 +31,10 @@ import java.util.List;
|
||||
* submitted in an iteration that begins with a pending burn.</li>
|
||||
* <li>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.</li>
|
||||
* <li>Select at most the configured USDC-equivalent maximum, rounded down
|
||||
* <li>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.</li>
|
||||
@@ -40,7 +45,7 @@ import java.util.List;
|
||||
* lifecycle.</li>
|
||||
* <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 USDC buyback limit never caps
|
||||
* swap does not suppress this step, and the input buyback limit never caps
|
||||
* the amount eligible for burning.</li>
|
||||
* <li>Require a positive, sufficiently fresh canonical EVE/USDT price and
|
||||
* calculate the complete balance's dollar value before submitting a burn.
|
||||
|
||||
+92
-39
@@ -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.</p>
|
||||
*
|
||||
* <p>{@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.</p>
|
||||
*
|
||||
* @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<MoneyAmount> inputCurrencyMinimumReserves,
|
||||
@NotNull ΩUSDCAmountΩ maximumSwapAmountInUSDC,
|
||||
@NotNull List<MoneyAmount> 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<UUID, MoneyAmount> validateAndCopyMaximums(
|
||||
List<MoneyAmount> configuredMaximums
|
||||
) {
|
||||
Objects.requireNonNull(
|
||||
configuredMaximums,
|
||||
"inputCurrencyMaximumSwapAmounts"
|
||||
);
|
||||
Set<UUID> acceptedCurrencyIds = new HashSet<>();
|
||||
for (MoneyAmount minimumReserve : inputCurrencyMinimumReserves) {
|
||||
acceptedCurrencyIds.add(minimumReserve.currencyType().id());
|
||||
}
|
||||
Map<UUID, MoneyAmount> 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<MoneyAmount> inputCurrencyMinimumReserves;
|
||||
private final ΩUSDCAmountΩ maximumSwapAmountInUSDC;
|
||||
private final Map<UUID, MoneyAmount> 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;
|
||||
|
||||
@@ -303,9 +303,14 @@ public final class NenjimHubImpl implements NenjimHub {
|
||||
}
|
||||
|
||||
private NenjimProcess constructEvelynIOUBurnerProdProcess() {
|
||||
List<MoneyAmount> burnTokenCandidates = List.of(new MoneyAmount(new ΩAmountΩ("0.1"), cis.resolve(CurrencyTypeIds.USDT_ID)));
|
||||
|
||||
ΩUSDCAmountΩ maximumSwapAmountPerIterationInUSD = new ΩUSDCAmountΩ("0.25");
|
||||
List<MoneyAmount> 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<MoneyAmount> 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),
|
||||
|
||||
Reference in New Issue
Block a user