92: Add per-currency minimum buyback amounts to the Evelyn IOU burner
This commit is contained in:
@@ -13,10 +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
|
||||
* Each accepted input has exactly one strictly positive minimum and maximum
|
||||
* buyback amount supplied as two further lists of {@link MoneyAmount} values,
|
||||
* matched by UUID independently of list order/name/symbol. The minimum must not
|
||||
* exceed its maximum. All three configurations are copied defensively.
|
||||
* Minima and 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
|
||||
@@ -41,7 +42,10 @@ import java.util.List;
|
||||
* <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>
|
||||
* estimated when applying its reserve. Skip actual rounded candidates
|
||||
* below that input's minimum and continue to later inputs in the same
|
||||
* iteration; equality qualifies. Never round upward or use the reserve
|
||||
* to reach a minimum. Input minima do not suppress EVE burns.</li>
|
||||
* <li>Attempt no more than one exact-input Jupiter swap. Normal return is
|
||||
* already {@code CONFIRMED}; it is not awaited a second time. A definitive
|
||||
* failure or proven pre-submission error permits a later iteration to try
|
||||
|
||||
+55
-54
@@ -78,6 +78,13 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
* that input currency's own units, without an input-price conversion or
|
||||
* a guarantee about its USD value.</p>
|
||||
*
|
||||
* <p>{@code inputCurrencyMinimumSwapAmounts} follows the same canonical
|
||||
* UUID matching and immutable-copy rules. Every accepted input needs one
|
||||
* strictly positive minimum no greater than its maximum. A rounded
|
||||
* candidate below the minimum is skipped so later inputs remain eligible;
|
||||
* equality qualifies without rounding up or spending the reserve. Three
|
||||
* empty input lists permit direct-EVE-only burning.</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
|
||||
@@ -94,6 +101,10 @@ 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 inputCurrencyMinimumSwapAmounts exactly one strictly positive
|
||||
* token-denominated buyback minimum per accepted input UUID, no
|
||||
* greater than its maximum; qualification uses the actual rounded
|
||||
* input amount and does not restrict burns
|
||||
* @param inputCurrencyMaximumSwapAmounts exactly one strictly positive
|
||||
* token-denominated buyback maximum per accepted input UUID;
|
||||
* it does not limit burns
|
||||
@@ -106,14 +117,16 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
* @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 or maximum amount,
|
||||
* object, list entry, reserve, minimum or maximum amount,
|
||||
* configured currency, or UUID is null
|
||||
* @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,
|
||||
* or maximum UUIDs do not match inputs
|
||||
* or minimum/maximum UUIDs do not match inputs,
|
||||
* a swap bound is non-positive, or a
|
||||
* minimum exceeds its maximum
|
||||
*/
|
||||
public EvelynIOUBurnerServiceImpl(
|
||||
@NotNull SolanaWallet burnerWallet,
|
||||
@@ -123,6 +136,7 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
@NotNull CurrencyIdentityService currencyIdentityService,
|
||||
@NotNull BoundNotificationService notificationService,
|
||||
@NotNull List<MoneyAmount> inputCurrencyMinimumReserves,
|
||||
@NotNull List<MoneyAmount> inputCurrencyMinimumSwapAmounts,
|
||||
@NotNull List<MoneyAmount> inputCurrencyMaximumSwapAmounts,
|
||||
int maximumSlippageBps,
|
||||
ΩmilliSecondsΩ maximumPriceAge,
|
||||
@@ -204,9 +218,19 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
this.inputCurrencyMinimumReserves = validateAndCopyInputs(
|
||||
inputCurrencyMinimumReserves
|
||||
);
|
||||
this.inputCurrencyMaximumSwapAmounts = validateAndCopyMaximums(
|
||||
inputCurrencyMaximumSwapAmounts
|
||||
this.inputCurrencyMaximumSwapAmounts = validateAndCopySwapAmounts(
|
||||
inputCurrencyMaximumSwapAmounts, "inputCurrencyMaximumSwapAmounts"
|
||||
);
|
||||
this.inputCurrencyMinimumSwapAmounts = validateAndCopySwapAmounts(
|
||||
inputCurrencyMinimumSwapAmounts, "inputCurrencyMinimumSwapAmounts"
|
||||
);
|
||||
for (Map.Entry<UUID, MoneyAmount> minimum : this.inputCurrencyMinimumSwapAmounts.entrySet()) {
|
||||
if (minimum.getValue().amount().compareTo(
|
||||
this.inputCurrencyMaximumSwapAmounts.get(minimum.getKey()).amount()) > 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Minimum swap amount exceeds maximum for input currency UUID: " + minimum.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -385,7 +409,8 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
ΩAmountΩ amountToSwap = availableForSwap
|
||||
.min(maximumInputAmount)
|
||||
.setScale(decimals, RoundingMode.DOWN);
|
||||
if (amountToSwap.signum() <= 0) {
|
||||
if (amountToSwap.signum() <= 0
|
||||
|| amountToSwap.compareTo(inputCurrencyMinimumSwapAmounts.get(currencyId).amount()) < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -859,70 +884,45 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
return List.copyOf(validatedInputs);
|
||||
}
|
||||
|
||||
private Map<UUID, MoneyAmount> validateAndCopyMaximums(
|
||||
List<MoneyAmount> configuredMaximums
|
||||
private Map<UUID, MoneyAmount> validateAndCopySwapAmounts(
|
||||
List<MoneyAmount> configuredAmounts,
|
||||
String configurationName
|
||||
) {
|
||||
Objects.requireNonNull(
|
||||
configuredMaximums,
|
||||
"inputCurrencyMaximumSwapAmounts"
|
||||
);
|
||||
Objects.requireNonNull(configuredAmounts, configurationName);
|
||||
Set<UUID> acceptedCurrencyIds = new HashSet<>();
|
||||
for (MoneyAmount minimumReserve : inputCurrencyMinimumReserves) {
|
||||
acceptedCurrencyIds.add(minimumReserve.currencyType().id());
|
||||
}
|
||||
Map<UUID, MoneyAmount> validatedMaximums = new HashMap<>();
|
||||
Map<UUID, MoneyAmount> validatedAmounts = 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
|
||||
);
|
||||
for (int index = 0; index < configuredAmounts.size(); index++) {
|
||||
String entryName = configurationName + "[" + index + "]";
|
||||
MoneyAmount bound = Objects.requireNonNull(configuredAmounts.get(index), entryName);
|
||||
Objects.requireNonNull(bound.amount(), entryName + ".amount");
|
||||
if (bound.amount().signum() <= 0) {
|
||||
throw new IllegalArgumentException(entryName + " amount must be greater than zero");
|
||||
}
|
||||
|
||||
CurrencyType configuredCurrency = Objects.requireNonNull(
|
||||
maximum.currencyType(),
|
||||
"inputCurrencyMaximumSwapAmounts[" + index
|
||||
+ "].currencyType"
|
||||
);
|
||||
UUID currencyId = Objects.requireNonNull(
|
||||
configuredCurrency.id(),
|
||||
"inputCurrencyMaximumSwapAmounts[" + index
|
||||
+ "].currencyType.id"
|
||||
);
|
||||
requireCanonicalCurrency(currencyId, "configured input maximum");
|
||||
|
||||
bound.currencyType(), entryName + ".currencyType");
|
||||
UUID currencyId = Objects.requireNonNull(configuredCurrency.id(), entryName + ".currencyType.id");
|
||||
CurrencyType canonicalCurrency = requireCanonicalCurrency(currencyId, configurationName);
|
||||
if (!acceptedCurrencyIds.contains(currencyId)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Maximum swap amount configured for unaccepted input "
|
||||
+ "currency UUID: " + currencyId
|
||||
);
|
||||
throw new IllegalArgumentException(configurationName
|
||||
+ " configured for unaccepted input currency UUID: " + currencyId);
|
||||
}
|
||||
if (validatedMaximums.putIfAbsent(currencyId, maximum) != null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Duplicate maximum swap amount currency UUID: "
|
||||
+ currencyId
|
||||
);
|
||||
if (validatedAmounts.putIfAbsent(currencyId,
|
||||
new MoneyAmount(bound.amount(), canonicalCurrency)) != null) {
|
||||
throw new IllegalArgumentException("Duplicate " + configurationName
|
||||
+ " currency UUID: " + currencyId);
|
||||
}
|
||||
}
|
||||
|
||||
for (UUID currencyId : acceptedCurrencyIds) {
|
||||
if (!validatedMaximums.containsKey(currencyId)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Missing maximum swap amount for input currency UUID: "
|
||||
+ currencyId
|
||||
);
|
||||
if (!validatedAmounts.containsKey(currencyId)) {
|
||||
throw new IllegalArgumentException("Missing " + configurationName
|
||||
+ " for input currency UUID: " + currencyId);
|
||||
}
|
||||
}
|
||||
return Map.copyOf(validatedMaximums);
|
||||
return Map.copyOf(validatedAmounts);
|
||||
}
|
||||
|
||||
private CurrencyType requireCanonicalCurrency(
|
||||
@@ -1057,6 +1057,7 @@ public final class EvelynIOUBurnerServiceImpl
|
||||
private final CurrencyIdentityService currencyIdentityService;
|
||||
private final BoundNotificationService notificationService;
|
||||
private final List<MoneyAmount> inputCurrencyMinimumReserves;
|
||||
private final Map<UUID, MoneyAmount> inputCurrencyMinimumSwapAmounts;
|
||||
private final Map<UUID, MoneyAmount> inputCurrencyMaximumSwapAmounts;
|
||||
private final int maximumSlippageBps;
|
||||
private final ΩmilliSecondsΩ maximumPriceAge;
|
||||
|
||||
@@ -307,6 +307,10 @@ public final class NenjimHubImpl implements NenjimHub {
|
||||
new MoneyAmount(new ΩAmountΩ("0.1"), cis.resolve(CurrencyTypeIds.USDT_ID)),
|
||||
new MoneyAmount(new ΩAmountΩ("0.095"), cis.resolve(CurrencyTypeIds.SYRUPUSDC_ID))
|
||||
);
|
||||
List<MoneyAmount> inputCurrencyMinimumSwapAmounts = List.of(
|
||||
new MoneyAmount(new ΩAmountΩ("0.05"), cis.resolve(CurrencyTypeIds.USDT_ID)),
|
||||
new MoneyAmount(new ΩAmountΩ("0.045"), 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))
|
||||
@@ -323,6 +327,7 @@ public final class NenjimHubImpl implements NenjimHub {
|
||||
cis,
|
||||
discordAssetAZTokenNotificationService,
|
||||
inputCurrencyMinimumReserves,
|
||||
inputCurrencyMinimumSwapAmounts,
|
||||
inputCurrencyMaximumSwapAmounts,
|
||||
200,
|
||||
2*1000*60L,
|
||||
|
||||
Reference in New Issue
Block a user