62: Add AssetAZ Currency Identity Service and migrate CurrencyType

This commit is contained in:
2026-08-08 00:40:24 +02:00
parent ef1b5e0adc
commit bc936e029d
29 changed files with 923 additions and 163 deletions
@@ -4,6 +4,8 @@ import com.r35157.libs.basic.Pair;
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionConcentrated;
import com.fanitas.evelyn.core.DesiredPositionCalculator;
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
import com.r35157.assetaz.valuetypes.CurrencyType;
import com.r35157.libs.valuetypes.basic.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -14,15 +16,21 @@ import java.math.RoundingMode;
import java.util.*;
import static com.fanitas.evelyn.math.BigDecimalUtils.*;
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.SYRUPUSDC_ID;
import static java.math.BigDecimal.ONE;
import static java.math.BigDecimal.ZERO;
public class DesiredPositionCalculatorImpl implements DesiredPositionCalculator {
public DesiredPositionCalculatorImpl(
@NotNull CurrencyIdentityService currencyIdentityService,
ΩCurveWidthΩ curveWidth,
Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> liquidityProviderPositions
) {
this.currencyIdentityService = Objects.requireNonNull(
currencyIdentityService,
"currencyIdentityService"
);
this.curveWidth = curveWidth;
this.liquidityProviderPositions = liquidityProviderPositions;
}
@@ -223,7 +231,7 @@ public class DesiredPositionCalculatorImpl implements DesiredPositionCalculator
intervalPriceFrom.price().compareTo(lookupPrice.price()) >= 0;
if (intervalIsCompletelyBeforeCurve || intervalIsCompletelyAfterLookupLimit) {
return new ΩSyrupAmountΩ(ZERO, WellKnownCurrencyTypes.SYRUPUSDC.getCurrencyType());
return new ΩSyrupAmountΩ(ZERO, currencyIdentityService.resolve(SYRUPUSDC_ID));
}
ΩPriceΩ effectiveIntervalFrom = max(intervalPriceFrom.price(), curveStartPrice);
@@ -254,7 +262,7 @@ public class DesiredPositionCalculatorImpl implements DesiredPositionCalculator
totalSyrupToDistribution.amount()
.multiply(intervalWeight, MC)
.divide(totalWeightInActiveCurve, MC),
WellKnownCurrencyTypes.SYRUPUSDC.getCurrencyType()
currencyIdentityService.resolve(SYRUPUSDC_ID)
);
return dps;
@@ -367,7 +375,7 @@ public class DesiredPositionCalculatorImpl implements DesiredPositionCalculator
}
private static final MathContext MC = new MathContext(20, RoundingMode.HALF_UP);
private final CurrencyIdentityService currencyIdentityService;
private final ΩCurveWidthΩ curveWidth;
private final Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> liquidityProviderPositions;
}
@@ -22,8 +22,6 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import static com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes.SOLANA;
import static com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes.SYRUPUSDC;
import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.SPL_TOKEN_PROGRAM;
import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.TOKEN_2022_PROGRAM;
@@ -96,7 +94,7 @@ public class EvelynImpl implements Evelyn {
}
private MoneyAmount add(MoneyAmount a, MoneyAmount b) {
if(a.currencyType() != b.currencyType()) {
if(!a.currencyType().equals(b.currencyType())) {
String errTxt = "Cannot add " + b.currencyType()
+ " to " + a.currencyType();
throw new IllegalArgumentException(errTxt);
@@ -107,7 +105,7 @@ public class EvelynImpl implements Evelyn {
}
private MoneyAmount subtract(MoneyAmount a, MoneyAmount b) {
if(a.currencyType() != b.currencyType()) {
if(!a.currencyType().equals(b.currencyType())) {
String errTxt = "Cannot subtract " + b.currencyType()
+ " from " + a.currencyType();
throw new IllegalArgumentException(errTxt);
@@ -3,9 +3,11 @@ package com.fanitas.evelyn.core.impl.ref;
import com.fanitas.evelyn.core.State;
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionAccounting;
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionConcentrated;
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
import com.r35157.assetaz.services.cis.ExternalCurrencyReference;
import com.r35157.assetaz.valuetypes.CurrencyType;
import com.r35157.libs.configuration.ConfigurationFormatVersionValidator;
import com.r35157.libs.raydium.*;
import com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes;
import com.r35157.libs.valuetypes.basic.*;
import java.io.IOException;
@@ -17,12 +19,23 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static com.r35157.assetaz.services.cis.ExternalCurrencyReference.SOLANA_MINT_NAMESPACE;
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.SYRUPUSDC_ID;
public class StateImpl implements State {
public StateImpl(Raydium raydium) throws IOException, InterruptedException {
public StateImpl(
Raydium raydium,
CurrencyIdentityService currencyIdentityService
) throws IOException, InterruptedException {
this.raydium = raydium;
this.currencyIdentityService = Objects.requireNonNull(
currencyIdentityService,
"currencyIdentityService"
);
update();
}
@@ -114,8 +127,8 @@ public class StateImpl implements State {
RaydiumConcentratedPoolState poolState =
raydium.fetchConcentratedPoolState(positionState.poolId());
CurrencyType ctSol = WellKnownCurrencyTypes.SOLANA.getCurrencyType();
CurrencyType ctSyrup = WellKnownCurrencyTypes.SYRUPUSDC.getCurrencyType();
CurrencyType currencyTypeA = resolveSolanaMint(poolInfo.mintA());
CurrencyType currencyTypeB = resolveSolanaMint(poolInfo.mintB());
int mintBDecimals = poolInfo.mintBDecimals();
Range<AssetPrice> priceRange = raydium.calculateConcentratedPositionPriceRange(
@@ -131,28 +144,36 @@ public class StateImpl implements State {
poolState
);
MoneyAmount solAmount = new MoneyAmount(tokenAmounts.amountA(), ctSol);
MoneyAmount syrupAmount = new MoneyAmount(tokenAmounts.amountB(), ctSyrup);
MoneyAmount amountA = new MoneyAmount(tokenAmounts.amountA(), currencyTypeA);
MoneyAmount amountB = new MoneyAmount(tokenAmounts.amountB(), currencyTypeB);
RaydiumLiquidityPoolPositionConcentrated position =
new RaydiumLiquidityPoolPositionConcentrated(
positionState.poolId(),
positionNftId,
priceRange,
solAmount,
syrupAmount,
amountA,
amountB,
null // The accounting info will be added from 'conf/positions.conf' later.
);
positions.put(positionNftId, position);
System.out.println(" Added '" + position.nftId() + "': "
+ "Range:" + position.priceRange()
+ ", Liquidity:" + solAmount + "," + syrupAmount
+ ", Liquidity:" + amountA + "," + amountB
);
}
liquidityPositions = Map.copyOf(positions);
}
private CurrencyType resolveSolanaMint(ΩSPLMintAddressΩ mintAddress) {
return currencyIdentityService.resolve(new ExternalCurrencyReference(
SOLANA_MINT_NAMESPACE,
mintAddress,
null
));
}
private void updateStateFromPositionsFile() throws IOException {
Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionAccounting> accountingEntries =
readPositionAccounting();
@@ -276,7 +297,7 @@ public class StateImpl implements State {
private ΩSyrupAmountΩ readSyrupOwnedByEvelyn(Iterator<String> lineIterator) throws IOException {
String syrupOwnedByEvelynStr = readLineFromFile(lineIterator);
CurrencyType ct = WellKnownCurrencyTypes.SYRUPUSDC.getCurrencyType();
CurrencyType ct = currencyIdentityService.resolve(SYRUPUSDC_ID);
ΩSyrupAmountΩ ma = stringToMoneyAmount(syrupOwnedByEvelynStr, ct);
return ma;
}
@@ -309,8 +330,8 @@ public class StateImpl implements State {
private static final Path PATH_POSITIONS = Path.of("conf/positions.conf");
private static final int SUPPORTED_EVELYN_FORMAT_VERSION = 1;
private static final int SUPPORTED_POSITIONS_FORMAT_VERSION = 1;
private final Raydium raydium;
private final CurrencyIdentityService currencyIdentityService;
private ΩmilliSecondsΩ iterationInterval;
private ΩSyrupAmountΩ syrupOwnedByEvelyn;
@@ -2,6 +2,7 @@ package com.r35157.assetaz.core.service.ticker.impl.ref;
import com.r35157.assetaz.core.service.ticker.PriceSink;
import com.r35157.assetaz.core.service.ticker.PriceSource;
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
import com.r35157.libs.valuetypes.basic.TradingPair;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
@@ -15,21 +16,34 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static com.r35157.libs.valuetypes.basic.WellKnownTradingPairs.EVE_USDC;
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.EVE_ID;
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.USDC_ID;
/**
* Temporary reference source that produces a constant EVE/USDC price.
*/
public final class HardcodedPriceSource implements PriceSource {
public HardcodedPriceSource() {
this(Clock.systemUTC(), OBSERVATION_DELAY_MINUTES, TimeUnit.MINUTES);
public HardcodedPriceSource(
@NotNull CurrencyIdentityService currencyIdentityService
) {
this(
currencyIdentityService,
Clock.systemUTC(),
OBSERVATION_DELAY_MINUTES,
TimeUnit.MINUTES
);
}
HardcodedPriceSource(
@NotNull CurrencyIdentityService currencyIdentityService,
@NotNull Clock clock,
long observationDelay,
@NotNull TimeUnit observationDelayUnit
) {
this.currencyIdentityService = Objects.requireNonNull(
currencyIdentityService,
"currencyIdentityService"
);
this.clock = Objects.requireNonNull(clock, "clock");
if (observationDelay <= 0) {
throw new IllegalArgumentException("observationDelay must be positive");
@@ -43,7 +57,10 @@ public final class HardcodedPriceSource implements PriceSource {
@Override
public @NotNull TradingPair getTradingPair() {
return EVE_USDC.getTradingPair();
return new TradingPair(
currencyIdentityService.resolve(EVE_ID),
currencyIdentityService.resolve(USDC_ID)
);
}
@Override
@@ -106,6 +123,7 @@ public final class HardcodedPriceSource implements PriceSource {
private static final long OBSERVATION_DELAY_MINUTES = 1;
private PriceSink priceSink;
private final CurrencyIdentityService currencyIdentityService;
private final Clock clock;
private final long observationDelay;
private final TimeUnit observationDelayUnit;
@@ -0,0 +1,60 @@
package com.r35157.assetaz.services.cis;
import com.r35157.assetaz.valuetypes.CurrencyType;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
import java.util.UUID;
/**
* Resolves AssetAZ currency identities and their identifiers in external
* systems.
*
* <p>An AssetAZ currency is identified by its stable {@link UUID}. An external
* currency is identified by the combination of
* {@link ExternalCurrencyReference#namespace() namespace} and
* {@link ExternalCurrencyReference#externalId() external ID}; its symbol is
* metadata and does not participate in resolution.</p>
*
* <p>This interface is read-only. Implementations may reuse returned
* {@link CurrencyType} instances, but callers must compare currency values with
* {@link Object#equals(Object)} rather than reference identity.</p>
*/
public interface CurrencyIdentityService {
/**
* Resolves an AssetAZ currency UUID to its current currency metadata.
*
* @param currencyTypeId the stable AssetAZ currency UUID
* @return the current currency value for {@code currencyTypeId}
* @throws NullPointerException if {@code currencyTypeId} is {@code null}
* @throws IllegalArgumentException if {@code currencyTypeId} is unknown
*/
@NotNull CurrencyType resolve(@NotNull UUID currencyTypeId);
/**
* Resolves a namespaced external currency identifier.
*
* <p>Resolution uses only the reference namespace and external ID. The
* supplied symbol is descriptive metadata and is not a lookup key.</p>
*
* @param externalReference the namespaced external identifier to resolve
* @return the AssetAZ currency mapped to the external identifier
* @throws NullPointerException if {@code externalReference} is {@code null}
* @throws IllegalArgumentException if the external identity is unknown
*/
@NotNull CurrencyType resolve(
@NotNull ExternalCurrencyReference externalReference
);
/**
* Finds every external reference associated with an AssetAZ currency.
*
* @param currencyTypeId the stable AssetAZ currency UUID
* @return an immutable, possibly empty set of external references
* @throws NullPointerException if {@code currencyTypeId} is {@code null}
* @throws IllegalArgumentException if {@code currencyTypeId} is unknown
*/
@NotNull Set<ExternalCurrencyReference> findExternalReferences(
@NotNull UUID currencyTypeId
);
}
@@ -0,0 +1,31 @@
package com.r35157.assetaz.services.cis;
import java.util.UUID;
/**
* Stable UUID identifiers for well-known AssetAZ currencies.
*
* <p>This class contains identifiers only. Current names, symbols, external
* references, and {@code CurrencyType} instances are owned by the active
* {@link CurrencyIdentityService} implementation.</p>
*/
public final class CurrencyTypeIds {
private CurrencyTypeIds() {
}
/** Stable UUID for Evelyn IOU. */
public static final UUID EVE_ID =
UUID.fromString("019c3f9f-41d1-7a73-b1df-d4c11c7ff301");
/** Stable UUID for USD Coin. */
public static final UUID USDC_ID =
UUID.fromString("019c3f9f-41d1-7a73-b1df-d4c11c7ff302");
/** Stable UUID for Solana. */
public static final UUID SOLANA_ID =
UUID.fromString("019e0116-fce5-792f-a647-fa6da4dffec5");
/** Stable UUID for SyrupUSDC. */
public static final UUID SYRUPUSDC_ID =
UUID.fromString("019e1d51-0600-7956-8231-f3b7058a91c2");
}
@@ -0,0 +1,49 @@
package com.r35157.assetaz.services.cis;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
/**
* Identifies a currency in an external identification system.
*
* <p>The semantic identity is the combination of {@link #namespace()} and
* {@link #externalId()}. {@link #symbol()} is optional display metadata and
* must not be used as a lookup key. Equality and hash codes therefore depend
* only on the namespace and external ID.</p>
*
* @param namespace the external identification system, such as
* {@value #SOLANA_MINT_NAMESPACE}; never {@code null}
* @param externalId the identifier within the namespace, such as a token mint
* address; never {@code null}
* @param symbol optional symbol metadata supplied by the external system
*/
public record ExternalCurrencyReference(
@NotNull String namespace,
@NotNull String externalId,
@Nullable String symbol
) {
public ExternalCurrencyReference {
Objects.requireNonNull(namespace, "namespace");
Objects.requireNonNull(externalId, "externalId");
}
@Override
public boolean equals(Object object) {
return this == object
|| object instanceof ExternalCurrencyReference other
&& namespace.equals(other.namespace)
&& externalId.equals(other.externalId);
}
@Override
public int hashCode() {
return Objects.hash(namespace, externalId);
}
/**
* Namespace for tokens identified by their Solana mint address.
*/
public static final String SOLANA_MINT_NAMESPACE = "solana-mint";
}
@@ -0,0 +1,157 @@
package com.r35157.assetaz.services.cis.impl.hc;
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
import com.r35157.assetaz.services.cis.ExternalCurrencyReference;
import com.r35157.assetaz.valuetypes.CurrencyType;
import org.jetbrains.annotations.NotNull;
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;
import static com.r35157.assetaz.services.cis.ExternalCurrencyReference.SOLANA_MINT_NAMESPACE;
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.SYRUPUSDC_ID;
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.USDC_ID;
public final class HardcodedCurrencyIdentityService implements CurrencyIdentityService {
public HardcodedCurrencyIdentityService() {
this(List.of(
entry(EVE_ID, "Evelyn IOU", "EVE",
solanaMint("meveYG2iXYSkgSUn1T1uxcthH1EGMZdRHGgCntXZA3Y", "EVE")),
entry(USDC_ID, "USD Coin", "USDC",
solanaMint("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "USDC")),
entry(SOLANA_ID, "Solana", "SOL",
solanaMint("So11111111111111111111111111111111111111112", "SOL")),
entry(SYRUPUSDC_ID, "SyrupUSDC", "SyrupUSDC",
solanaMint("AvZZF1YaZDziPY2RCK4oJrRVrbN3mTD9NL24hPeaZeUj", "SyrupUSDC"))
));
}
private HardcodedCurrencyIdentityService(List<CatalogueEntry> entries) {
Map<UUID, CurrencyType> currencies = new HashMap<>();
Map<ExternalIdentity, UUID> externalIdentities = new HashMap<>();
Map<ExternalIdentity, ExternalCurrencyReference> referencesByIdentity = new HashMap<>();
Map<UUID, Set<ExternalCurrencyReference>> references = new HashMap<>();
for (CatalogueEntry entry : entries) {
CurrencyType previousCurrency = currencies.putIfAbsent(
entry.currencyType().id(),
entry.currencyType()
);
if (previousCurrency != null && !sameMetadata(previousCurrency, entry.currencyType())) {
throw new IllegalStateException(
"Conflicting currency definitions for UUID " + entry.currencyType().id()
);
}
Set<ExternalCurrencyReference> currencyReferences = references.computeIfAbsent(
entry.currencyType().id(),
ignored -> new HashSet<>()
);
for (ExternalCurrencyReference reference : entry.externalReferences()) {
ExternalIdentity identity = ExternalIdentity.from(reference);
UUID previousId = externalIdentities.putIfAbsent(identity, entry.currencyType().id());
if (previousId != null && !previousId.equals(entry.currencyType().id())) {
throw new IllegalStateException(
"External currency identity " + identity + " maps to both "
+ previousId + " and " + entry.currencyType().id()
);
}
ExternalCurrencyReference previousReference = referencesByIdentity.putIfAbsent(
identity,
reference
);
if (previousReference != null
&& !Objects.equals(previousReference.symbol(), reference.symbol())) {
throw new IllegalStateException(
"Conflicting symbol metadata for external currency identity " + identity
);
}
currencyReferences.add(reference);
}
}
Map<UUID, Set<ExternalCurrencyReference>> immutableReferences = new HashMap<>();
currencies.keySet().forEach(id -> immutableReferences.put(
id,
Set.copyOf(references.getOrDefault(id, Set.of()))
));
this.currencies = Map.copyOf(currencies);
this.externalIdentities = Map.copyOf(externalIdentities);
this.references = Map.copyOf(immutableReferences);
}
@Override
public @NotNull CurrencyType resolve(@NotNull UUID currencyTypeId) {
Objects.requireNonNull(currencyTypeId, "currencyTypeId");
CurrencyType currencyType = currencies.get(currencyTypeId);
if (currencyType == null) {
throw new IllegalArgumentException("Unknown AssetAZ currency UUID: " + currencyTypeId);
}
return currencyType;
}
@Override
public @NotNull CurrencyType resolve(
@NotNull ExternalCurrencyReference externalReference
) {
Objects.requireNonNull(externalReference, "externalReference");
ExternalIdentity identity = ExternalIdentity.from(externalReference);
UUID currencyTypeId = externalIdentities.get(identity);
if (currencyTypeId == null) {
throw new IllegalArgumentException("Unknown external currency identity: " + identity);
}
return resolve(currencyTypeId);
}
@Override
public @NotNull Set<ExternalCurrencyReference> findExternalReferences(
@NotNull UUID currencyTypeId
) {
resolve(currencyTypeId);
return references.get(currencyTypeId);
}
private static boolean sameMetadata(CurrencyType left, CurrencyType right) {
return left.name().equals(right.name()) && left.symbol().equals(right.symbol());
}
private static CatalogueEntry entry(
UUID id,
String name,
String symbol,
ExternalCurrencyReference... externalReferences
) {
return new CatalogueEntry(
new CurrencyType(id, name, symbol),
List.of(externalReferences)
);
}
private static ExternalCurrencyReference solanaMint(String mintAddress, String symbol) {
return new ExternalCurrencyReference(SOLANA_MINT_NAMESPACE, mintAddress, symbol);
}
private record CatalogueEntry(
CurrencyType currencyType,
List<ExternalCurrencyReference> externalReferences
) {
}
private record ExternalIdentity(String namespace, String externalId) {
private static ExternalIdentity from(ExternalCurrencyReference reference) {
return new ExternalIdentity(reference.namespace(), reference.externalId());
}
}
private final Map<UUID, CurrencyType> currencies;
private final Map<ExternalIdentity, UUID> externalIdentities;
private final Map<UUID, Set<ExternalCurrencyReference>> references;
}
@@ -0,0 +1,48 @@
package com.r35157.assetaz.valuetypes;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.UUID;
/**
* Immutable AssetAZ currency identity and its current display metadata.
*
* <p>The {@link #id()} UUID is the complete and stable identity. The
* {@link #name()} and {@link #symbol()} values may change without creating a
* different currency. Equality and hash codes therefore depend only on the
* UUID, and callers must use {@link #equals(Object)} rather than reference
* comparison.</p>
*
* @param id the stable AssetAZ currency UUID; never {@code null}
* @param name the current human-readable currency name; never {@code null}
* @param symbol the current display symbol; never {@code null}
*/
public record CurrencyType(
@NotNull UUID id,
@NotNull String name,
@NotNull String symbol
) {
public CurrencyType {
Objects.requireNonNull(id, "id");
Objects.requireNonNull(name, "name");
Objects.requireNonNull(symbol, "symbol");
}
@Override
public boolean equals(Object object) {
return this == object
|| object instanceof CurrencyType other
&& id.equals(other.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public @NotNull String toString() {
return symbol;
}
}
@@ -1,5 +1,6 @@
package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
import com.r35157.cryptowallet.solana.SolanaWallet;
import com.r35157.cryptowallet.solana.impl.ref.SolanaWalletImpl;
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
@@ -21,7 +22,10 @@ import java.util.concurrent.CountDownLatch;
public final class JupiterPerpsAlarmImpl {
public static void main(String[] args) throws Exception {
public static void start(
String[] args,
CurrencyIdentityService currencyIdentityService
) throws Exception {
Config config;
System.out.println("Starting Jupiter Perps Alarms...");
try {
@@ -57,7 +61,9 @@ public final class JupiterPerpsAlarmImpl {
System.out.print("Initializing dependencies... ");
ObjectCache objectCache = new ObjectCacheImpl();
SolanaBlockChain realSolanaBlockChain = new SolanaBlockChainImpl();
SolanaBlockChain realSolanaBlockChain = new SolanaBlockChainImpl(
currencyIdentityService
);
SolanaBlockChain solanaBlockChain = new CachedSolanaBlockChain(
realSolanaBlockChain,
objectCache
@@ -83,7 +89,8 @@ public final class JupiterPerpsAlarmImpl {
JupiterPerpsService positionIncreaseJupiter =
new AnchorIdlJupiterPerpsServiceImpl(
solanaBlockChain,
positionIncreaseWallet
positionIncreaseWallet,
currencyIdentityService
);
JupiterPerpsPositionDecreaseAlarmActionConfiguration
@@ -106,7 +113,8 @@ public final class JupiterPerpsAlarmImpl {
JupiterPerpsService positionDecreaseJupiter =
new AnchorIdlJupiterPerpsServiceImpl(
solanaBlockChain,
positionDecreaseWallet
positionDecreaseWallet,
currencyIdentityService
);
JupiterPerpsEntryPriceVariableRefresher entryPriceVariableRefresher =
@@ -1,6 +1,7 @@
package com.r35157.libs.jupiter.perps.impl.anchoridl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
import com.r35157.cryptowallet.solana.SolanaWallet;
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
import com.r35157.libs.jupiter.perps.JupiterPerpsPositionDirection;
@@ -18,7 +19,6 @@ import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter;
import com.r35157.libs.solana.SolanaSignedTransaction;
import com.r35157.libs.solana.SolanaUnsignedTransaction;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
import com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -34,19 +34,25 @@ import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.SOLANA_ID;
import static java.math.BigDecimal.ZERO;
public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
public AnchorIdlJupiterPerpsServiceImpl(
SolanaBlockChain solanaBlockChain,
SolanaWallet wallet
SolanaWallet wallet,
CurrencyIdentityService currencyIdentityService
) {
this.solanaBlockChain = Objects.requireNonNull(
solanaBlockChain,
"solanaBlockChain"
);
this.wallet = Objects.requireNonNull(wallet, "wallet");
this.currencyIdentityService = Objects.requireNonNull(
currencyIdentityService,
"currencyIdentityService"
);
this.positionDecoder = new AnchorIdlJupiterPerpsPositionDecoder();
this.custodyDecoder = new AnchorIdlJupiterPerpsCustodyDecoder();
}
@@ -416,7 +422,7 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
ΩUSDCAmountΩ totalFees = ZERO; // TODO - Dummy
ΩSolanaAmountΩ accountRent = new MoneyAmount( // TODO - Dummy
ZERO,
WellKnownCurrencyTypes.SOLANA.getCurrencyType()
currencyIdentityService.resolve(SOLANA_ID)
);
JupiterPerpsPosition pos = new JupiterPerpsPosition(
@@ -808,9 +814,9 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
private static final int POSITION_OWNER_OFFSET = 8;
private static final int MAX_LEVERAGE = 500;
private static final int USDC_DECIMALS = 6;
private final SolanaBlockChain solanaBlockChain;
private final SolanaWallet wallet;
private final CurrencyIdentityService currencyIdentityService;
private final AnchorIdlJupiterPerpsPositionDecoder positionDecoder;
private final AnchorIdlJupiterPerpsCustodyDecoder custodyDecoder;
}
@@ -1,8 +1,10 @@
package com.r35157.libs.raydium.impl.ref;
import com.r35157.libs.valuetypes.basic.WellKnownTradingPairs;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
import com.r35157.assetaz.services.cis.ExternalCurrencyReference;
import com.r35157.assetaz.valuetypes.CurrencyType;
import com.r35157.libs.raydium.Raydium;
import com.r35157.libs.raydium.RaydiumConcentratedPositionState;
import com.r35157.libs.raydium.RaydiumConcentratedPoolInfo;
@@ -15,6 +17,7 @@ import com.r35157.libs.solana.SolanaBlockChain;
import com.r35157.libs.solana.SolanaProgramAddressSeed;
import com.r35157.libs.solana.valuetypes.SolanaProgramDerivedAddress;
import com.r35157.libs.valuetypes.basic.*;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.math.BigDecimal;
@@ -28,15 +31,24 @@ import java.net.http.HttpResponse;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import static com.r35157.assetaz.services.cis.ExternalCurrencyReference.SOLANA_MINT_NAMESPACE;
import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.SPL_TOKEN_PROGRAM;
import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.TOKEN_2022_PROGRAM;
public class RaydiumImpl implements Raydium {
public RaydiumImpl(SolanaBlockChain solanaBlockChain) {
this.solanaBlockChain = solanaBlockChain;
public RaydiumImpl(
SolanaBlockChain solanaBlockChain,
CurrencyIdentityService currencyIdentityService
) {
this.solanaBlockChain = Objects.requireNonNull(solanaBlockChain, "solanaBlockChain");
this.currencyIdentityService = Objects.requireNonNull(
currencyIdentityService,
"currencyIdentityService"
);
this.httpClient = HttpClient.newHttpClient();
this.objectMapper = new ObjectMapper();
}
@@ -54,8 +66,11 @@ public class RaydiumImpl implements Raydium {
throw new IOException("Could NOT find field 'price' in JSON!");
}
// TODO: Find out how not to hardcode the trading pair here
TradingPair tp = WellKnownTradingPairs.SOL_SYRUPUSDC.getTradingPair();
ΩSPLMintAddressΩ mintA = extractMintAddress(firstPool, "mintA");
String symbolA = extractMintSymbol(firstPool, "mintA");
ΩSPLMintAddressΩ mintB = extractMintAddress(firstPool, "mintB");
String symbolB = extractMintSymbol(firstPool, "mintB");
TradingPair tp = resolveTradingPair(mintA, symbolA, mintB, symbolB);
ΩPriceΩ p = new ΩPriceΩ(priceNode.toString());
AssetPrice ap = new AssetPrice(p, tp);
@@ -263,7 +278,12 @@ public class RaydiumImpl implements Raydium {
RaydiumConcentratedPoolInfo poolInfo,
int decimalPlaces
) {
TradingPair tp = WellKnownTradingPairs.SOL_SYRUPUSDC.getTradingPair();
TradingPair tp = resolveTradingPair(
poolInfo.mintA(),
null,
poolInfo.mintB(),
null
);
ΩPriceΩ priceFrom = calculatePriceFromTick(
positionState.tickLowerIndex(),
@@ -509,6 +529,38 @@ public class RaydiumImpl implements Raydium {
return addressNode.asText();
}
private @Nullable String extractMintSymbol(
JsonNode poolNode,
String mintFieldName
) throws IOException {
JsonNode symbolNode = poolNode.path(mintFieldName).path("symbol");
if (symbolNode.isMissingNode() || symbolNode.isNull()) {
return null;
}
if (!symbolNode.isTextual()) {
throw new IOException("Could NOT find textual field '" + mintFieldName + ".symbol' in pool JSON!");
}
return symbolNode.asText();
}
private TradingPair resolveTradingPair(
ΩSPLMintAddressΩ mintA,
@Nullable String symbolA,
ΩSPLMintAddressΩ mintB,
@Nullable String symbolB
) {
CurrencyType base = currencyIdentityService.resolve(
new ExternalCurrencyReference(SOLANA_MINT_NAMESPACE, mintA, symbolA)
);
CurrencyType quote = currencyIdentityService.resolve(
new ExternalCurrencyReference(SOLANA_MINT_NAMESPACE, mintB, symbolB)
);
return new TradingPair(base, quote);
}
private ΩAmountΩ extractAmount(JsonNode poolNode, String amountFieldName) throws IOException {
JsonNode amountNode = poolNode.path(amountFieldName);
@@ -957,6 +1009,7 @@ public class RaydiumImpl implements Raydium {
private static final Base64.Decoder base64 = Base64.getDecoder();
private final SolanaBlockChain solanaBlockChain;
private final CurrencyIdentityService currencyIdentityService;
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
@@ -4,12 +4,12 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
import com.r35157.assetaz.valuetypes.CurrencyType;
import com.r35157.libs.solana.*;
import com.r35157.libs.solana.valuetypes.SolanaProgramDerivedAddress;
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
import com.r35157.libs.valuetypes.basic.CurrencyType;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
import com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -29,11 +29,16 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.SOLANA_ID;
import static com.r35157.libs.solana.SolanaConstants.RPC_URL;
public class SolanaBlockChainImpl implements SolanaBlockChain {
public SolanaBlockChainImpl() {
public SolanaBlockChainImpl(CurrencyIdentityService currencyIdentityService) {
this.currencyIdentityService = Objects.requireNonNull(
currencyIdentityService,
"currencyIdentityService"
);
this.httpClient = HttpClient.newHttpClient();
this.objectMapper = new ObjectMapper();
}
@@ -42,7 +47,7 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
public ΩSolanaAmountΩ getBalanceInSolana(ΩSolanaAddressΩ address) throws IOException, InterruptedException {
ΩlamportsΩ lamport = getBalanceInLamport(address);
ΩAmountΩ bd = ΩAmountΩ.valueOf(lamport).divide(LAMPORTS_PER_SOL);
CurrencyType type = WellKnownCurrencyTypes.SOLANA.getCurrencyType();
CurrencyType type = currencyIdentityService.resolve(SOLANA_ID);
ΩSolanaAmountΩ sa = new ΩSolanaAmountΩ(bd, type);
return sa;
@@ -938,9 +943,7 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
);
}
if (!WellKnownCurrencyTypes.SOLANA
.getCurrencyType()
.equals(amount.currencyType())) {
if (!currencyIdentityService.resolve(SOLANA_ID).equals(amount.currencyType())) {
throw new IllegalArgumentException(
"SOL transfer amount must use the SOL currency type"
);
@@ -1405,7 +1408,7 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
BigInteger.valueOf(-121665)
.multiply(BigInteger.valueOf(121666).modInverse(ED25519_P))
.mod(ED25519_P);
private final CurrencyIdentityService currencyIdentityService;
private final ObjectMapper objectMapper;
private final HttpClient httpClient;
private ΩmilliSecondsΩ previousRemoteCallTime = 0L;
@@ -1,6 +1,6 @@
package com.r35157.libs.solana.valuetypes.economic;
import com.r35157.libs.valuetypes.basic.CurrencyType;
import com.r35157.assetaz.valuetypes.CurrencyType;
/**
* Represents an SPL token known to the Solana integration.
@@ -18,4 +18,4 @@ public record SolanaSPLToken (
ΩSPLMintAddressΩ mintAddress,
ΩSolanaAddressΩ tokenAddress
) {
}
}
@@ -1,16 +0,0 @@
package com.r35157.libs.valuetypes.basic;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public record CurrencyType(
UUID id,
String name,
String symbol
) {
@Override
public @NotNull String toString() {
return symbol;
}
}
@@ -1,5 +1,6 @@
package com.r35157.libs.valuetypes.basic;
import com.r35157.assetaz.valuetypes.CurrencyType;
import org.jetbrains.annotations.NotNull;
import java.math.BigDecimal;
@@ -1,5 +1,7 @@
package com.r35157.libs.valuetypes.basic;
import com.r35157.assetaz.valuetypes.CurrencyType;
import java.math.BigDecimal;
public record MoneyPrice(
@@ -1,5 +1,6 @@
package com.r35157.libs.valuetypes.basic;
import com.r35157.assetaz.valuetypes.CurrencyType;
import org.jetbrains.annotations.NotNull;
public record TradingPair(
@@ -1,67 +0,0 @@
package com.r35157.libs.valuetypes.basic;
import java.util.UUID;
/**
* Defines well-known currencies used across the system.
*
* <p>Each enum value provides a stable {@link CurrencyType} identity for a
* currency that may be referenced by multiple services and integrations.</p>
*/
public enum WellKnownCurrencyTypes {
/**
* Evelyn IOU Token
*/
EVE(new CurrencyType(
UUID.fromString("019c3f9f-41d1-7a73-b1df-d4c11c7ff301"),
"EVE",
"EVE")
),
/**
* USD Coin
*/
USDC(new CurrencyType(
UUID.fromString("019c3f9f-41d1-7a73-b1df-d4c11c7ff302"),
"USD Coin",
"USDC")
),
/**
* Native Solana currency
*/
SOLANA(new CurrencyType(
UUID.fromString("019e0116-fce5-792f-a647-fa6da4dffec5"),
"Solana",
"SOL")
),
/**
* SyrupUSDC currency
*/
SYRUPUSDC(new CurrencyType(
UUID.fromString("019e1d51-0600-7956-8231-f3b7058a91c2"),
"SyrupUSDC",
"SyrupUSDC")
);
/**
* Creates a well-known currency entry.
*
* @param currencyType the stable currency identity represented by the entry
*/
WellKnownCurrencyTypes(CurrencyType currencyType) {
this.currencyType = currencyType;
}
/**
* Returns the represented currency type.
*
* @return the represented currency type
*/
public CurrencyType getCurrencyType() {
return currencyType;
}
private final CurrencyType currencyType;
}
@@ -1,21 +0,0 @@
package com.r35157.libs.valuetypes.basic;
import static com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes.EVE;
import static com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes.SOLANA;
import static com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes.SYRUPUSDC;
import static com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes.USDC;
public enum WellKnownTradingPairs {
SOL_SYRUPUSDC(new TradingPair(SOLANA.getCurrencyType(), SYRUPUSDC.getCurrencyType())),
EVE_USDC(new TradingPair(EVE.getCurrencyType(), USDC.getCurrencyType()));
WellKnownTradingPairs(TradingPair tradingPair) {
this.tradingPair = tradingPair;
}
public TradingPair getTradingPair() {
return tradingPair;
}
private final TradingPair tradingPair;
}
@@ -4,6 +4,8 @@ import com.fanitas.evelyn.core.Evelyn;
import com.fanitas.evelyn.core.impl.ref.EvelynImpl;
import com.r35157.assetaz.core.service.ticker.impl.ref.HardcodedPriceSource;
import com.r35157.assetaz.core.service.ticker.impl.ref.TickerServiceImpl;
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
import com.r35157.assetaz.services.cis.impl.hc.HardcodedCurrencyIdentityService;
import com.r35157.evelyn.emc.EvelynMissionControl;
import com.r35157.evelyn.emc.impl.ref.EvelynMissionControlImpl;
import com.r35157.jupiterperpsalarm.impl.ref.JupiterPerpsAlarmImpl;
@@ -59,9 +61,11 @@ public class NenjimHubImpl implements NenjimHub {
System.out.println("Nenjim is now shutdown (stopped all processes)!");
}
private void startAutoRunProcesses() throws Exception {
startAssetAZTickerService();
startJupiterPerpsAlarm(); // TODO: Hardcoded/hacky way to auto-start but good enough for now.
private void startAutoRunProcesses() {
CurrencyIdentityService currencyIdentityService =
new HardcodedCurrencyIdentityService();
startAssetAZTickerService(currencyIdentityService);
startJupiterPerpsAlarm(currencyIdentityService); // TODO: Hardcoded/hacky way to auto-start but good enough for now.
Evelyn evelynProd = new EvelynImpl();
Evelyn evelynTest = new EvelynImpl();
startEvelynMissionControl(evelynProd, evelynTest);
@@ -89,9 +93,13 @@ public class NenjimHubImpl implements NenjimHub {
*/
}
private void startAssetAZTickerService() {
private void startAssetAZTickerService(
CurrencyIdentityService currencyIdentityService
) {
// Nenjim creates this unstarted PriceSource first...
HardcodedPriceSource priceSource = new HardcodedPriceSource();
HardcodedPriceSource priceSource = new HardcodedPriceSource(
currencyIdentityService
);
// The TickerServiceImpl will ask Nenjim for implementers of the PriceSource interface in this context
// This do not work yet - so we will just inject it in the constructor now. In the future it will
@@ -141,12 +149,15 @@ public class NenjimHubImpl implements NenjimHub {
shutdownLatch.await();
}
private void startJupiterPerpsAlarm() {
private void startJupiterPerpsAlarm(
CurrencyIdentityService currencyIdentityService
) {
Thread thread = new Thread(() -> {
try {
JupiterPerpsAlarmImpl.main(new String[] {
"--config=conf/alarms.conf"
});
JupiterPerpsAlarmImpl.start(
new String[] {"--config=conf/alarms.conf"},
currencyIdentityService
);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
@@ -1,8 +1,12 @@
package com.r35157.libs.solana.impl.ref;
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
import com.r35157.assetaz.services.cis.ExternalCurrencyReference;
import com.r35157.assetaz.valuetypes.CurrencyType;
import com.r35157.libs.solana.SolanaLatestBlockhash;
import com.r35157.libs.solana.SolanaUnsignedTransaction;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
@@ -10,9 +14,11 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Base64;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import static com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes.SOLANA;
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.SOLANA_ID;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -32,7 +38,7 @@ class SolanaBlockChainImplTest {
RECIPIENT,
new MoneyAmount(
new BigDecimal("0.123456789"),
SOLANA.getCurrencyType()
SOLANA
)
);
@@ -137,7 +143,7 @@ class SolanaBlockChainImplTest {
RECIPIENT,
new MoneyAmount(
new BigDecimal("0.0000000001"),
SOLANA.getCurrencyType()
SOLANA
)
)
);
@@ -148,7 +154,7 @@ class SolanaBlockChainImplTest {
long fee,
AtomicReference<String> feeMessage
) {
return new SolanaBlockChainImpl() {
return new SolanaBlockChainImpl(CURRENCY_IDENTITIES) {
@Override
public long getBalanceInLamport(String address) {
return balance;
@@ -181,6 +187,34 @@ class SolanaBlockChainImplTest {
.getLong();
}
private static final CurrencyType SOLANA = new CurrencyType(
SOLANA_ID,
"Solana",
"SOL"
);
private static final CurrencyIdentityService CURRENCY_IDENTITIES =
new CurrencyIdentityService() {
@Override
public @NotNull CurrencyType resolve(@NotNull UUID currencyTypeId) {
if (!SOLANA_ID.equals(currencyTypeId)) {
throw new IllegalArgumentException("Unknown test currency: " + currencyTypeId);
}
return SOLANA;
}
@Override
public @NotNull CurrencyType resolve(@NotNull ExternalCurrencyReference externalReference) {
throw new IllegalArgumentException(
"No external currencies configured for this test"
);
}
@Override
public @NotNull Set<ExternalCurrencyReference> findExternalReferences(@NotNull UUID currencyTypeId) {
resolve(currencyTypeId);
return Set.of();
}
};
private static final String SENDER =
"So11111111111111111111111111111111111111112";
private static final String RECIPIENT =