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
+1
View File
@@ -41,6 +41,7 @@ val detag = configurations.create("detag") {
dependencies {
detag("com.r35157.tools:detag-impl_ref:0.1.0")
compileOnly("org.jetbrains:annotations:26.1.0")
testCompileOnly("org.jetbrains:annotations:26.1.0")
runtimeOnly("org.apache.logging.log4j:log4j-core:2.26.0")
runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:2.26.0")
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-07
@@ -0,0 +1,62 @@
## Context
See `proposal.md` for motivation. Currency metadata currently lives in `WellKnownCurrencyTypes`, while `WellKnownTradingPairs` captures static pairs and `RaydiumImpl` hardcodes one of them. The repository temporarily hosts both logical AssetAZ API and implementation packages, and Nenjim runtime discovery is outside this issue.
## Goals / Non-Goals
**Goals:**
- Separate the public identity API from its hardcoded implementation package.
- Make UUID identity robust across service instances and metadata revisions.
- Migrate every production caller in one step so no parallel authoritative registry remains.
- Let Raydium translate the pool response's actual mint identities into its returned pair.
**Non-Goals:**
- Moving value types other than `CurrencyType`.
- Runtime service discovery, remote/database catalogues, public registration, or constructor restriction.
- Unit tests or Nenjim Test Tool changes.
## Decisions
### External identity is a private composite key
`ExternalCurrencyReference` is the public immutable carrier and explicitly implements equality and hashing from `namespace + externalId`; optional symbol metadata is excluded. The implementation may retain a private composite key for indexing and conflict diagnostics, but public value equality has the same semantic identity. The hardcoded catalogue separately compares supplied symbol metadata so conflicting catalogue entries are still rejected.
### Canonical and external metadata have different completeness rules
Every `CurrencyType` has a non-null UUID, name, and canonical symbol, even though only its UUID participates in equality. An external reference may omit its observed symbol because integrations can provide a valid namespace and external identifier without display metadata. A currency may also have zero external references; reverse lookup then returns an immutable empty set. Evelyn IOU is not such an example because its Solana mint is configured explicitly.
### Catalogue construction validates before freezing
The hardcoded implementation builds local maps, detects duplicate UUIDs with conflicting metadata and duplicate external identities targeting different UUIDs, then stores immutable copies. Reverse-reference sets are also immutable. This gives lock-free concurrent reads and contains mutation entirely within construction. A public registration API was rejected because version 1 is deliberately read-only.
### NenjimHub owns the service instance
`NenjimHubImpl.startAutoRunProcesses()` is the temporary Cauldron composition root. It creates exactly one `HardcodedCurrencyIdentityService`, then passes the interface-typed instance through constructors to the ticker source, Solana, Jupiter, Raydium, Evelyn, and any other autorun component that needs currency identities. Identity consumers never instantiate the hardcoded implementation themselves and do not use a global singleton. Multiple local service instances were rejected because they create independent lifecycles and prevent Nenjim from replacing the context's implementation coherently.
### Existing UUIDs move with their metadata
The four existing UUIDs move into an API-only `CurrencyTypeIds` constants class. It contains no metadata, mappings, or value instances and is therefore not a currency registry. The hardcoded implementation owns current names, symbols, and external mappings—including the Evelyn IOU name and mint—while production consumers resolve API UUIDs or available external identities through their injected service. No consumer imports `impl.hc`, and no static `CurrencyType` or `TradingPair` objects remain.
### Raydium uses response mint order
`RaydiumImpl` receives `CurrencyIdentityService` beside `SolanaBlockChain`. `fetchPoolPrice` extracts both response mint addresses and optional symbol metadata, creates references in a Solana-mint namespace, resolves each through the service, and constructs `TradingPair(mintA, mintB)`. Missing external symbol fields do not prevent lookup. Other Raydium calculations and State pool accounting likewise resolve actual pool mints and preserve A/B order rather than assuming SOL/SyrupUSDC. Constructor injection is the temporary integration seam until Nenjim provides dynamic dependencies.
## Risks / Trade-offs
- [Existing constructors and imports break during migration] → Update all production call sites and compile every affected Gradle module before completion.
- [Raydium response shape differs between endpoints] → Reuse the existing pool-node extraction path and validate address/symbol fields explicitly with clear IO errors.
- [A second registry survives unnoticed] → Search the complete production tree for old types, static pairs, and legacy imports during final review.
- [No new automated regression coverage] → Respect the explicit issue scope and rely on compilation, strict OpenSpec validation, and focused diff/static review.
## Migration Plan
1. Introduce the AssetAZ API and hardcoded implementation with the preserved catalogue and required Solana mint mappings.
2. Create one implementation instance in NenjimHub's autorun composition root and pass it through every identity-dependent construction chain.
3. Move `CurrencyType`, migrate production imports, and replace static trading-pair use with resolutions from the injected service.
4. Inject the service into Raydium and resolve response mint identities.
5. Delete the legacy registries only after all production references are gone.
6. Compile affected modules without running tests, validate OpenSpec strictly, and inspect the complete diff.
Rollback consists of reverting the change as one unit because the package move, registry deletion, and constructor change are intentionally atomic.
@@ -0,0 +1,29 @@
## Why
Currency identities are currently duplicated in static registries and coupled to callers such as Raydium. AssetAZ needs one service-owned UUID identity model that can resolve external identifiers without making symbols or integrations the canonical identity.
## What Changes
- Add a public AssetAZ Currency Identity Service API and immutable external-reference value type.
- Publish stable AssetAZ currency UUID constants in an API-only identifier class so consumers never depend on the hardcoded implementation package.
- Add a hardcoded reference implementation containing the current currency definitions and Solana mint mappings.
- **BREAKING** Move `CurrencyType` from the shared basic value-types package to `com.r35157.assetaz.valuetypes` and define equality by UUID alone.
- **BREAKING** Remove `WellKnownCurrencyTypes` and `WellKnownTradingPairs`; construct trading pairs from identities resolved by the service.
- Inject the Currency Identity Service into `RaydiumImpl` and resolve pool mint identities when producing prices and ranges.
- Create the hardcoded service exactly once in NenjimHub's autorun composition root and pass that shared service instance to every component that requires currency identities.
- Preserve canonical currency metadata as non-null while allowing external references to omit symbol metadata and allowing currencies to have no external references.
- Do not add or rewrite unit tests or Nenjim Test Tool code as part of this change.
## Capabilities
### New Capabilities
- `assetaz-currency-identity-service`: Defines UUID-based currency identity, external-identifier resolution, the immutable hardcoded catalogue, and its use by Raydium.
### Modified Capabilities
None.
## Impact
The change affects the AssetAZ value-type and service packages, public UUID identifiers, all production imports of `CurrencyType`, NenjimHub dependency wiring, hardcoded ticker data, Raydium construction and pricing, State pool accounting, and callers that construct identity-dependent components. It removes the two legacy static currency/trading-pair registries. No external dependency or ValueTag configuration change is required.
@@ -0,0 +1,112 @@
## Purpose
Provides one authoritative AssetAZ identity model for currencies and translates stable UUIDs and namespaced external identifiers without treating display symbols as identity.
## ADDED Requirements
### Requirement: Stable AssetAZ currency identity
The system SHALL represent a currency as an immutable `CurrencyType` with a non-null UUID, name, and symbol. Its UUID is its complete stable identity. Equality and hash codes SHALL depend only on that UUID, regardless of name, symbol, instance identity, or metadata changes.
#### Scenario: Metadata changes for the same UUID
- **WHEN** two currency values have the same UUID but different names or symbols
- **THEN** they compare equal and have the same hash code
#### Scenario: Matching metadata for different UUIDs
- **WHEN** two currency values have different UUIDs but identical names and symbols
- **THEN** they do not compare equal
#### Scenario: Missing canonical metadata
- **WHEN** construction omits a currency name or symbol
- **THEN** construction fails without producing an incomplete currency value
### Requirement: Resolve canonical currencies
The Currency Identity Service SHALL resolve a known AssetAZ UUID to a non-null current currency value and SHALL fail with a clear exception for an unknown UUID.
#### Scenario: Resolve known UUID
- **WHEN** a client resolves a configured AssetAZ currency UUID
- **THEN** the service returns the current currency metadata for that UUID
#### Scenario: Resolve unknown UUID
- **WHEN** a client resolves an unconfigured UUID
- **THEN** the service throws an exception that clearly identifies the unknown UUID
### Requirement: Resolve namespaced external identities
The Currency Identity Service SHALL resolve external currencies by the pair `namespace + externalId`. External symbols MAY be absent and SHALL be retained as metadata when present, but SHALL NOT participate in lookup identity or equality. A namespace SHALL describe the external identification system rather than an observing price source.
#### Scenario: Resolve a Solana mint
- **WHEN** a client supplies a configured Solana-mint namespace and mint address with symbol metadata
- **THEN** the service returns the currency mapped to that namespace and mint address
#### Scenario: Symbol differs from configured metadata
- **WHEN** the namespace and external identifier are configured but the supplied symbol differs
- **THEN** the external references compare equal and the service resolves the same currency because the symbol is not part of external identity
#### Scenario: External symbol is absent
- **WHEN** a client supplies a configured namespace and external identifier without symbol metadata
- **THEN** the service resolves the currency and returns its non-null canonical symbol
#### Scenario: Unknown external identity
- **WHEN** a client supplies an unconfigured namespace and external identifier
- **THEN** the service throws an exception that clearly identifies the unknown external identity
### Requirement: Reverse external-reference lookup
The Currency Identity Service SHALL return all configured external references for a known AssetAZ UUID as a non-null immutable set, including an empty set when none exist, and SHALL fail clearly for an unknown UUID.
#### Scenario: Currency has multiple references
- **WHEN** a known UUID has references in multiple external systems
- **THEN** reverse lookup returns every reference without exposing mutable registry state
#### Scenario: Known currency has no references
- **WHEN** a known UUID has no configured external references
- **THEN** reverse lookup returns an empty immutable set
#### Scenario: Reverse lookup Evelyn IOU
- **WHEN** a client requests external references for the Evelyn IOU UUID
- **THEN** the result contains its configured Solana mint reference
### Requirement: Immutable conflict-free hardcoded catalogue
The hardcoded Currency Identity Service SHALL preserve the existing AssetAZ currency UUIDs, maintain one current currency value for each UUID, support multiple external references per UUID, reject conflicting UUID or external-identity mappings during initialization, expose no public mutation operation, and be safe for concurrent reads after construction. NenjimHub SHALL create exactly one hardcoded service instance for its autorun context and SHALL pass that same instance to every autorun component that requires currency identities.
#### Scenario: Conflicting external mapping
- **WHEN** initialization maps the same namespace and external identifier to two different AssetAZ UUIDs
- **THEN** initialization fails with a clear conflict exception
#### Scenario: Conflicting currency metadata
- **WHEN** initialization provides conflicting current values for the same UUID
- **THEN** initialization fails with a clear conflict exception
#### Scenario: Compose autorun components
- **WHEN** NenjimHub constructs its autorun component graph
- **THEN** it creates one hardcoded Currency Identity Service and injects that same instance into every component that requires it
### Requirement: Trading pairs use service-owned identities
Production code SHALL construct trading pairs from currency values obtained from the current Currency Identity Service and SHALL NOT maintain a parallel static registry of canonical currency or trading-pair instances.
#### Scenario: Construct a configured pair
- **WHEN** production code needs a trading pair for configured currencies
- **THEN** it resolves both UUIDs or external identities through the current service before constructing the pair
### Requirement: Stable UUID identifiers are implementation-independent
The API SHALL expose stable AssetAZ currency UUID constants without names, symbols, external mappings, or `CurrencyType` instances. Consumers that require a known AssetAZ UUID SHALL use these API identifiers and SHALL NOT import the hardcoded Currency Identity Service implementation.
#### Scenario: Consumer resolves a known UUID
- **WHEN** a consumer needs a known AssetAZ currency by UUID
- **THEN** it obtains the UUID from the API identifier class and resolves it through its injected Currency Identity Service
### Requirement: Pool amounts follow actual mint identities
Pool accounting SHALL resolve mint A and mint B through the Currency Identity Service using their actual namespaced Solana mint addresses and SHALL preserve the pool's A/B order when assigning currency types to amounts.
#### Scenario: State loads a pool
- **WHEN** State receives pool information with mint A and mint B
- **THEN** amount A uses the currency resolved from mint A and amount B uses the currency resolved from mint B
### Requirement: Raydium resolves response currencies
Raydium SHALL receive a Currency Identity Service dependency and SHALL build a fetched pool price's trading pair in Raydium response order by resolving `mintA` and `mintB` as namespaced Solana mint identities. It SHALL NOT resolve currencies by symbol alone.
#### Scenario: Fetch a configured Raydium pool price
- **WHEN** Raydium returns a price plus configured mint A and mint B identifiers, with or without symbols
- **THEN** the returned asset price uses a trading pair whose base is the service resolution of mint A and whose quote is the service resolution of mint B
#### Scenario: Raydium returns an unknown mint
- **WHEN** either returned mint identity is not configured
- **THEN** pool-price creation fails with the Currency Identity Service's clear unknown-identity exception
@@ -0,0 +1,35 @@
## 1. Currency Identity API and Catalogue
- [x] 1.1 Move `CurrencyType` into the AssetAZ value-types package and make equality and hashing UUID-only.
- [x] 1.2 Add the annotated immutable `ExternalCurrencyReference` and read-only `CurrencyIdentityService` API.
- [x] 1.3 Implement the immutable hardcoded catalogue with preserved UUIDs, reverse lookup, Solana mint mappings, and construction-time conflict detection.
## 2. Production Migration
- [x] 2.1 Migrate every production `CurrencyType` import and replace hardcoded ticker pair construction with identities from the current service.
- [x] 2.2 Inject the Currency Identity Service into `RaydiumImpl` and resolve Raydium mint identities and response-ordered trading pairs.
- [x] 2.3 Update production construction/configuration sites for the new dependencies and remove `WellKnownCurrencyTypes` and `WellKnownTradingPairs`.
## 3. Verification
- [x] 3.1 Compile every affected module without running unit tests.
- [x] 3.2 Run strict OpenSpec validation and `git diff --check`.
- [x] 3.3 Review the complete diff and production tree for incomplete migrations, old imports, parallel currency registries, and unintended test or Nenjim Test Tool changes.
## 4. Composition-Root Correction
- [x] 4.1 Create the hardcoded Currency Identity Service exactly once in `NenjimHubImpl.startAutoRunProcesses()` and pass it into every autorun construction chain that needs it.
- [x] 4.2 Replace identity consumers' local hardcoded-service construction with required `CurrencyIdentityService` constructor dependencies.
- [x] 4.3 Compile main and test source sets, strict-validate OpenSpec, run `git diff --check`, and confirm no hardcoded service construction remains outside the NenjimHub composition root.
## 5. Public API Documentation
- [x] 5.1 Document the public Currency Identity Service, external-reference, and currency value APIs, including identity semantics, nullability, and failure behavior.
- [x] 5.2 Compile main and test source sets and rerun strict OpenSpec validation and `git diff --check`.
## 6. Review Corrections
- [x] 6.1 Require complete canonical currency metadata, add API-owned UUID identifiers, and preserve the Evelyn IOU name and Solana mint mapping.
- [x] 6.2 Make external-reference equality use namespace plus external ID while retaining optional symbols and catalogue conflict detection.
- [x] 6.3 Resolve State pool currencies from actual mint A/B identities and make Raydium symbol extraction optional.
- [x] 6.4 Remove consumer dependencies on `impl.hc`, compile without tests, strict-validate OpenSpec, run `git diff --check`, and review the complete migration.
@@ -0,0 +1,114 @@
# assetaz-currency-identity-service Specification
## Purpose
Provides one authoritative AssetAZ identity model for currencies and translates stable UUIDs and namespaced external identifiers without treating display symbols as identity.
## Requirements
### Requirement: Stable AssetAZ currency identity
The system SHALL represent a currency as an immutable `CurrencyType` with a non-null UUID, name, and symbol. Its UUID is its complete stable identity. Equality and hash codes SHALL depend only on that UUID, regardless of name, symbol, instance identity, or metadata changes.
#### Scenario: Metadata changes for the same UUID
- **WHEN** two currency values have the same UUID but different names or symbols
- **THEN** they compare equal and have the same hash code
#### Scenario: Matching metadata for different UUIDs
- **WHEN** two currency values have different UUIDs but identical names and symbols
- **THEN** they do not compare equal
#### Scenario: Missing canonical metadata
- **WHEN** construction omits a currency name or symbol
- **THEN** construction fails without producing an incomplete currency value
### Requirement: Resolve canonical currencies
The Currency Identity Service SHALL resolve a known AssetAZ UUID to a non-null current currency value and SHALL fail with a clear exception for an unknown UUID.
#### Scenario: Resolve known UUID
- **WHEN** a client resolves a configured AssetAZ currency UUID
- **THEN** the service returns the current currency metadata for that UUID
#### Scenario: Resolve unknown UUID
- **WHEN** a client resolves an unconfigured UUID
- **THEN** the service throws an exception that clearly identifies the unknown UUID
### Requirement: Resolve namespaced external identities
The Currency Identity Service SHALL resolve external currencies by the pair `namespace + externalId`. External symbols MAY be absent and SHALL be retained as metadata when present, but SHALL NOT participate in lookup identity or equality. A namespace SHALL describe the external identification system rather than an observing price source.
#### Scenario: Resolve a Solana mint
- **WHEN** a client supplies a configured Solana-mint namespace and mint address with symbol metadata
- **THEN** the service returns the currency mapped to that namespace and mint address
#### Scenario: Symbol differs from configured metadata
- **WHEN** the namespace and external identifier are configured but the supplied symbol differs
- **THEN** the external references compare equal and the service resolves the same currency because the symbol is not part of external identity
#### Scenario: External symbol is absent
- **WHEN** a client supplies a configured namespace and external identifier without symbol metadata
- **THEN** the service resolves the currency and returns its non-null canonical symbol
#### Scenario: Unknown external identity
- **WHEN** a client supplies an unconfigured namespace and external identifier
- **THEN** the service throws an exception that clearly identifies the unknown external identity
### Requirement: Reverse external-reference lookup
The Currency Identity Service SHALL return all configured external references for a known AssetAZ UUID as a non-null immutable set, including an empty set when none exist, and SHALL fail clearly for an unknown UUID.
#### Scenario: Currency has multiple references
- **WHEN** a known UUID has references in multiple external systems
- **THEN** reverse lookup returns every reference without exposing mutable registry state
#### Scenario: Known currency has no references
- **WHEN** a known UUID has no configured external references
- **THEN** reverse lookup returns an empty immutable set
#### Scenario: Reverse lookup Evelyn IOU
- **WHEN** a client requests external references for the Evelyn IOU UUID
- **THEN** the result contains its configured Solana mint reference
### Requirement: Immutable conflict-free hardcoded catalogue
The hardcoded Currency Identity Service SHALL preserve the existing AssetAZ currency UUIDs, maintain one current currency value for each UUID, support multiple external references per UUID, reject conflicting UUID or external-identity mappings during initialization, expose no public mutation operation, and be safe for concurrent reads after construction. NenjimHub SHALL create exactly one hardcoded service instance for its autorun context and SHALL pass that same instance to every autorun component that requires currency identities.
#### Scenario: Conflicting external mapping
- **WHEN** initialization maps the same namespace and external identifier to two different AssetAZ UUIDs
- **THEN** initialization fails with a clear conflict exception
#### Scenario: Conflicting currency metadata
- **WHEN** initialization provides conflicting current values for the same UUID
- **THEN** initialization fails with a clear conflict exception
#### Scenario: Compose autorun components
- **WHEN** NenjimHub constructs its autorun component graph
- **THEN** it creates one hardcoded Currency Identity Service and injects that same instance into every component that requires it
### Requirement: Trading pairs use service-owned identities
Production code SHALL construct trading pairs from currency values obtained from the current Currency Identity Service and SHALL NOT maintain a parallel static registry of canonical currency or trading-pair instances.
#### Scenario: Construct a configured pair
- **WHEN** production code needs a trading pair for configured currencies
- **THEN** it resolves both UUIDs or external identities through the current service before constructing the pair
### Requirement: Stable UUID identifiers are implementation-independent
The API SHALL expose stable AssetAZ currency UUID constants without names, symbols, external mappings, or `CurrencyType` instances. Consumers that require a known AssetAZ UUID SHALL use these API identifiers and SHALL NOT import the hardcoded Currency Identity Service implementation.
#### Scenario: Consumer resolves a known UUID
- **WHEN** a consumer needs a known AssetAZ currency by UUID
- **THEN** it obtains the UUID from the API identifier class and resolves it through its injected Currency Identity Service
### Requirement: Pool amounts follow actual mint identities
Pool accounting SHALL resolve mint A and mint B through the Currency Identity Service using their actual namespaced Solana mint addresses and SHALL preserve the pool's A/B order when assigning currency types to amounts.
#### Scenario: State loads a pool
- **WHEN** State receives pool information with mint A and mint B
- **THEN** amount A uses the currency resolved from mint A and amount B uses the currency resolved from mint B
### Requirement: Raydium resolves response currencies
Raydium SHALL receive a Currency Identity Service dependency and SHALL build a fetched pool price's trading pair in Raydium response order by resolving `mintA` and `mintB` as namespaced Solana mint identities. It SHALL NOT resolve currencies by symbol alone.
#### Scenario: Fetch a configured Raydium pool price
- **WHEN** Raydium returns a price plus configured mint A and mint B identifiers, with or without symbols
- **THEN** the returned asset price uses a trading pair whose base is the service resolution of mint A and whose quote is the service resolution of mint B
#### Scenario: Raydium returns an unknown mint
- **WHEN** either returned mint identity is not configured
- **THEN** pool-price creation fails with the Currency Identity Service's clear unknown-identity exception
@@ -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 =