63: Add Raydium Pool PriceSource with periodic polling
This commit is contained in:
+2
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-08
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
## Context
|
||||
|
||||
See `proposal.md` for motivation. Commit `2fc553b` established package ownership that anticipates later extraction into Nenjim modules while the code remains in one Gradle project. The existing Raydium Pool PriceSource is an unimplemented scaffold in its intended implementation package. The Ticker already activates registered sources only when their exact history file exists and persists callbacks before updating its in-memory latest observation.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Complete the existing pool-source scaffold as an independently constructible, restartable PriceSource.
|
||||
- Preserve the package and future artifact dependency direction introduced by `2fc553b`.
|
||||
- Compose one EVE/USDT pool source beside the existing hardcoded EVE/USDC source using today's temporary Cauldron wiring.
|
||||
- Make timing deterministic enough for verification through package-private clock and delay injection.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Dynamic Nenjim discovery, configuration files, streaming, backoff, health APIs, history creation or migration, visualization, currency conversion, or further physical module splitting.
|
||||
- Changes to the generic Ticker behavior or canonical Ticker specification.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Preserve future module and package ownership
|
||||
|
||||
The future boundaries remain:
|
||||
|
||||
```text
|
||||
Ticker API
|
||||
com.r35157.assetaz.services.ticker
|
||||
- TickerService
|
||||
- PriceObservation
|
||||
|
||||
PriceSource plugin API
|
||||
com.r35157.assetaz.services.ticker.plugins.pricesource
|
||||
- PriceSource
|
||||
- PriceSink
|
||||
|
||||
Ticker reference implementation
|
||||
com.r35157.assetaz.services.ticker.impl.ref
|
||||
-> Ticker API
|
||||
-> PriceSource plugin API
|
||||
|
||||
Hardcoded PriceSource implementation
|
||||
com.r35157.assetaz.services.ticker.plugins.pricesource.impl.hardcoded
|
||||
-> PriceSource plugin API
|
||||
|
||||
Raydium Pool PriceSource implementation
|
||||
com.r35157.assetaz.services.ticker.plugins.pricesource.impl.raydiumpool
|
||||
-> PriceSource plugin API
|
||||
-> Raydium API and value types
|
||||
```
|
||||
|
||||
`PriceSink` stays in the plugin API so implementations do not depend on the Ticker API or reference implementation. `TickerServiceImpl` implements `PriceSink`, owns source lifecycle, and temporarily receives sources through its varargs constructor. `NenjimHubImpl` remains the temporary composition root; dynamic discovery is deferred. The alternative of moving the sink or source packages would recreate future circular or implementation dependencies and is explicitly rejected.
|
||||
|
||||
### Use a source-owned single-thread fixed-delay scheduler
|
||||
|
||||
Each source creates a daemon single-thread scheduled executor when started. The immediate attempt is submitted to that executor with zero initial delay, followed by fixed-delay scheduling so a slow or paused call cannot produce catch-up observations. A package-private constructor injects `Clock`, delay, and time unit; the public constructor fixes UTC and one minute. A shared scheduler was rejected because lifecycle and termination must belong to the source instance.
|
||||
|
||||
### Contain each attempt at the scheduler boundary
|
||||
|
||||
One safe polling method catches retrieval, validation, and sink-publication failures, logs pool and pair context, and returns normally so fixed-delay scheduling continues. Pair mismatch is treated as an invalid observation rather than a fatal lifecycle error. The timestamp is read only after a successful matching response and truncated to milliseconds before the sink callback.
|
||||
|
||||
### Keep startup and shutdown state synchronized
|
||||
|
||||
Startup checks the running state before validating or assigning the new sink, creates fresh scheduling resources, and schedules the immediate task. Stop detaches the scheduler under synchronization, requests immediate shutdown, and waits for termination without holding the lifecycle monitor. Interrupted waiting restores the caller's interrupt flag and ensures shutdown remains requested. This avoids replacing an active sink and permits restart after resource termination.
|
||||
|
||||
### Resolve EVE/USDT through the shared Currency Identity Service
|
||||
|
||||
The API identifier class gains only a stable USDT UUID. The hardcoded catalogue owns canonical metadata and the official Solana mint mapping. NenjimHub constructs the expected pair by resolving EVE and USDT through its single shared CIS instance, constructs Raydium with that same CIS dependency, and supplies both hardcoded and Raydium sources to the temporary Ticker constructor. The pool ID is market/source identity and is not registered in CIS.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [A blocking Raydium call can delay shutdown until the call honors interruption] → request immediate shutdown, preserve interruption, and keep the polling thread daemonized.
|
||||
- [A missing manually provisioned history silently leaves the new source inactive] → retain the existing explicit warning and document the exact pool-based history path.
|
||||
- [Temporary constructor injection can be mistaken for permanent discovery] → document it as Cauldron wiring and preserve interfaces and packages for later Nenjim discovery.
|
||||
- [A response may reverse or otherwise change the configured pair] → validate exact pair equality and publish nothing on mismatch.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Add USDT identity data, complete the existing source, and extend only the NenjimHub composition graph. Deployment requires manually creating the exact empty EVE/USDT pool history file to activate polling. Rollback removes that file or removes the temporary source wiring; existing hardcoded EVE/USDC history is unchanged.
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
## Why
|
||||
|
||||
AssetAZ Ticker currently has only a hardcoded price source and therefore cannot observe Evelyn IOU's real Raydium market price. A pool-specific plugin is needed to poll the EVE/USDT pool while preserving the PriceSource lifecycle and future Nenjim module boundaries.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Complete the existing Raydium Pool PriceSource scaffold as a lifecycle-managed, fixed-delay polling source representing one Raydium pool.
|
||||
- Add Tether USD (USDT) as a stable, externally resolvable AssetAZ currency distinct from USDC.
|
||||
- Temporarily compose the EVE/USDT Raydium source in NenjimHub alongside the existing hardcoded EVE/USDC source.
|
||||
- Preserve history activation, persistence-before-publication, package ownership, and future artifact boundaries without adding discovery, configuration, streaming, conversion, or automatic history creation.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `assetaz-raydium-pool-price-source`: Defines pool identity, polling, validation, publication, failure isolation, and restartable lifecycle behavior for a Raydium-backed PriceSource.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `assetaz-currency-identity-service`: Adds the stable canonical USDT identity and official Solana mint mapping while keeping it distinct from USDC.
|
||||
|
||||
## Impact
|
||||
|
||||
The change affects the AssetAZ Currency Identity Service catalogue and identifiers, the existing Raydium Pool PriceSource implementation scaffold, and temporary NenjimHub composition. It uses the existing Raydium and PriceSource APIs and does not change the generic Ticker service contract or create new Gradle subprojects.
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Stable Tether USD identity
|
||||
The Currency Identity Service SHALL provide Tether USD as a stable AssetAZ currency with canonical name `Tether USD`, canonical symbol `USDT`, and an implementation-independent UUID identifier. It SHALL resolve that currency through both the stable UUID and official Solana mint `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB`. USDT and USDC SHALL remain distinct currency identities, and resolution SHALL NOT imply conversion between them.
|
||||
|
||||
#### Scenario: Resolve USDT by stable UUID
|
||||
- **WHEN** a client resolves the stable USDT UUID
|
||||
- **THEN** the service returns the canonical Tether USD currency with symbol USDT
|
||||
|
||||
#### Scenario: Resolve USDT by official Solana mint
|
||||
- **WHEN** a client resolves the official USDT mint in the Solana-mint namespace
|
||||
- **THEN** the service returns the same canonical USDT identity as UUID resolution
|
||||
|
||||
#### Scenario: Compare USDT and USDC
|
||||
- **WHEN** a client resolves both USDT and USDC
|
||||
- **THEN** they have different UUIDs and no implicit conversion is performed
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
## Purpose
|
||||
|
||||
Provides independently lifecycle-managed price observations for one Raydium pool through the AssetAZ PriceSource plugin contract.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: One source represents one stable Raydium pool
|
||||
A Raydium Pool PriceSource instance SHALL represent exactly one Raydium pool, SHALL expose the expected trading pair supplied at construction, and SHALL expose the stable source name `Raydium-<poolId>`. Construction SHALL validate all dependencies and identity values, leave the source stopped, perform no network access, and require no price sink until startup.
|
||||
|
||||
#### Scenario: Construct a pool source
|
||||
- **WHEN** a client constructs a source with a Raydium service, pool ID, and expected trading pair
|
||||
- **THEN** the stopped source reports that trading pair and a source name containing that exact pool ID without performing a price retrieval
|
||||
|
||||
#### Scenario: Reject invalid construction
|
||||
- **WHEN** a required constructor argument is absent or invalid
|
||||
- **THEN** construction fails without starting polling or accessing the network
|
||||
|
||||
### Requirement: Poll immediately with fixed delay
|
||||
On startup, the source SHALL begin its first retrieval attempt immediately and SHALL begin each subsequent attempt one minute after the preceding attempt completes. Polling SHALL execute on a daemon scheduler thread owned exclusively by that source instance.
|
||||
|
||||
#### Scenario: Start a source
|
||||
- **WHEN** a stopped source is started with a price sink
|
||||
- **THEN** its first pool-price retrieval begins immediately on its source-owned daemon scheduler
|
||||
|
||||
#### Scenario: Complete a periodic attempt
|
||||
- **WHEN** a retrieval attempt completes
|
||||
- **THEN** the next attempt begins after the configured one-minute delay without fixed-rate catch-up attempts
|
||||
|
||||
### Requirement: Validate and publish pool observations
|
||||
The source SHALL retrieve the configured pool price and compare the response trading pair with its expected pair. For a matching response, it SHALL create the observation timestamp after the successful response, truncate it to milliseconds, and publish the returned price value through the sink supplied at startup.
|
||||
|
||||
#### Scenario: Receive the expected pair
|
||||
- **WHEN** a successful pool response contains the expected trading pair
|
||||
- **THEN** the source publishes its price with a post-response timestamp truncated to milliseconds
|
||||
|
||||
#### Scenario: Receive a different pair
|
||||
- **WHEN** a pool response contains a trading pair different from the expected pair
|
||||
- **THEN** the source publishes no observation, logs the pool ID plus expected and received pairs, and schedules the next attempt normally
|
||||
|
||||
### Requirement: Isolate temporary polling failures
|
||||
Temporary network, Raydium, parsing, interruption, or downstream persistence failures SHALL be logged with the pool ID and expected trading pair and SHALL NOT cancel later polling attempts or stop the Ticker service.
|
||||
|
||||
#### Scenario: A polling attempt fails temporarily
|
||||
- **WHEN** a retrieval or publication attempt throws an exception
|
||||
- **THEN** the source logs the contextual failure and retries on the next fixed-delay interval
|
||||
|
||||
### Requirement: Restartable source lifecycle
|
||||
The source SHALL reject a repeated startup before replacing its existing sink. Stopping SHALL be idempotent, terminate source-owned scheduling resources, correctly preserve interruption, and permit a later startup with a newly supplied sink.
|
||||
|
||||
#### Scenario: Start an already-started source
|
||||
- **WHEN** startup is invoked while the source is already running
|
||||
- **THEN** startup fails and the currently active sink remains unchanged
|
||||
|
||||
#### Scenario: Stop a running source
|
||||
- **WHEN** stop is invoked on a running source
|
||||
- **THEN** its scheduler terminates and no further polling begins
|
||||
|
||||
#### Scenario: Stop an already-stopped source
|
||||
- **WHEN** stop is invoked while the source is stopped
|
||||
- **THEN** it completes without error
|
||||
|
||||
#### Scenario: Restart a stopped source
|
||||
- **WHEN** a previously stopped source is started with a sink
|
||||
- **THEN** it creates fresh source-owned scheduling resources and begins an immediate retrieval attempt
|
||||
|
||||
### Requirement: Activation uses independent Ticker history
|
||||
The pool source SHALL participate in the existing Ticker activation contract under its exact trading pair and pool-based source name. It SHALL be active only when that exact history file already exists and SHALL NOT create or migrate history files.
|
||||
|
||||
#### Scenario: Pool history exists
|
||||
- **WHEN** the EVE/USDT history file exists beneath the EVE UUID, USDT UUID, and exact pool-based source-name path
|
||||
- **THEN** the Ticker activates the Raydium source independently of the hardcoded EVE/USDC history
|
||||
|
||||
#### Scenario: Pool history is absent
|
||||
- **WHEN** the exact Raydium source history file is absent
|
||||
- **THEN** the Ticker leaves that source inactive and creates no file or directory
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
## 1. Currency Identity
|
||||
|
||||
- [x] 1.1 Add the API-owned stable USDT UUID and hardcoded canonical Tether USD catalogue entry with the official Solana mint.
|
||||
- [x] 1.2 Verify USDT resolution by UUID and mint, distinct identity from USDC, and absence of conversion or parallel registry data.
|
||||
|
||||
## 2. Raydium Pool PriceSource
|
||||
|
||||
- [x] 2.1 Complete the existing `impl.raydiumpool.RaydiumPoolPriceSource` scaffold with validated pool identity and no construction-time sink or network access.
|
||||
- [x] 2.2 Implement immediate source-owned daemon polling with one-minute fixed delay, response-pair validation, millisecond observation timestamps, sink publication, and contextual failure isolation.
|
||||
- [x] 2.3 Implement repeated-start rejection, idempotent resource-terminating stop, interruption handling, and restart with fresh scheduling resources.
|
||||
|
||||
## 3. Temporary Composition
|
||||
|
||||
- [x] 3.1 Construct the EVE/USDT pair and Raydium pool source in `NenjimHubImpl` using the shared Currency Identity Service and hardcoded pool ID.
|
||||
- [x] 3.2 Inject the Raydium source alongside the unchanged running `HardcodedPriceSource` through the temporary Ticker constructor.
|
||||
|
||||
## 4. Boundary and Behavior Review
|
||||
|
||||
- [x] 4.1 Verify the `2fc553b` package and future module boundaries: Ticker API, PriceSource/PriceSink plugin API, Ticker reference implementation, and separate hardcoded and Raydium source implementations.
|
||||
- [x] 4.2 Verify exact pool-based source naming and EVE/USDT history path, existing-file-only activation, persistence before publication, polling recovery, and stop/restart semantics without adding automated tests.
|
||||
- [x] 4.3 Review the complete diff for stale imports, duplicate implementations, incomplete wiring, unintended HardcodedPriceSource changes, and excluded features.
|
||||
|
||||
## 5. Build and Specification Verification
|
||||
|
||||
- [x] 5.1 Compile main and test source sets without running unit tests.
|
||||
- [x] 5.2 Run strict OpenSpec validation and `git diff --check`.
|
||||
- [x] 5.3 Sync both deltas into canonical specifications, archive the completed change, and rerun strict OpenSpec validation.
|
||||
@@ -112,3 +112,18 @@ Raydium SHALL receive a Currency Identity Service dependency and SHALL build a f
|
||||
#### 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
|
||||
|
||||
### Requirement: Stable Tether USD identity
|
||||
The Currency Identity Service SHALL provide Tether USD as a stable AssetAZ currency with canonical name `Tether USD`, canonical symbol `USDT`, and an implementation-independent UUID identifier. It SHALL resolve that currency through both the stable UUID and official Solana mint `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB`. USDT and USDC SHALL remain distinct currency identities, and resolution SHALL NOT imply conversion between them.
|
||||
|
||||
#### Scenario: Resolve USDT by stable UUID
|
||||
- **WHEN** a client resolves the stable USDT UUID
|
||||
- **THEN** the service returns the canonical Tether USD currency with symbol USDT
|
||||
|
||||
#### Scenario: Resolve USDT by official Solana mint
|
||||
- **WHEN** a client resolves the official USDT mint in the Solana-mint namespace
|
||||
- **THEN** the service returns the same canonical USDT identity as UUID resolution
|
||||
|
||||
#### Scenario: Compare USDT and USDC
|
||||
- **WHEN** a client resolves both USDT and USDC
|
||||
- **THEN** they have different UUIDs and no implicit conversion is performed
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# assetaz-raydium-pool-price-source Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides independently lifecycle-managed price observations for one Raydium pool through the AssetAZ PriceSource plugin contract.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: One source represents one stable Raydium pool
|
||||
A Raydium Pool PriceSource instance SHALL represent exactly one Raydium pool, SHALL expose the expected trading pair supplied at construction, and SHALL expose the stable source name `Raydium-<poolId>`. Construction SHALL validate all dependencies and identity values, leave the source stopped, perform no network access, and require no price sink until startup.
|
||||
|
||||
#### Scenario: Construct a pool source
|
||||
- **WHEN** a client constructs a source with a Raydium service, pool ID, and expected trading pair
|
||||
- **THEN** the stopped source reports that trading pair and a source name containing that exact pool ID without performing a price retrieval
|
||||
|
||||
#### Scenario: Reject invalid construction
|
||||
- **WHEN** a required constructor argument is absent or invalid
|
||||
- **THEN** construction fails without starting polling or accessing the network
|
||||
|
||||
### Requirement: Poll immediately with fixed delay
|
||||
On startup, the source SHALL begin its first retrieval attempt immediately and SHALL begin each subsequent attempt one minute after the preceding attempt completes. Polling SHALL execute on a daemon scheduler thread owned exclusively by that source instance.
|
||||
|
||||
#### Scenario: Start a source
|
||||
- **WHEN** a stopped source is started with a price sink
|
||||
- **THEN** its first pool-price retrieval begins immediately on its source-owned daemon scheduler
|
||||
|
||||
#### Scenario: Complete a periodic attempt
|
||||
- **WHEN** a retrieval attempt completes
|
||||
- **THEN** the next attempt begins after the configured one-minute delay without fixed-rate catch-up attempts
|
||||
|
||||
### Requirement: Validate and publish pool observations
|
||||
The source SHALL retrieve the configured pool price and compare the response trading pair with its expected pair. For a matching response, it SHALL create the observation timestamp after the successful response, truncate it to milliseconds, and publish the returned price value through the sink supplied at startup.
|
||||
|
||||
#### Scenario: Receive the expected pair
|
||||
- **WHEN** a successful pool response contains the expected trading pair
|
||||
- **THEN** the source publishes its price with a post-response timestamp truncated to milliseconds
|
||||
|
||||
#### Scenario: Receive a different pair
|
||||
- **WHEN** a pool response contains a trading pair different from the expected pair
|
||||
- **THEN** the source publishes no observation, logs the pool ID plus expected and received pairs, and schedules the next attempt normally
|
||||
|
||||
### Requirement: Isolate temporary polling failures
|
||||
Temporary network, Raydium, parsing, interruption, or downstream persistence failures SHALL be logged with the pool ID and expected trading pair and SHALL NOT cancel later polling attempts or stop the Ticker service.
|
||||
|
||||
#### Scenario: A polling attempt fails temporarily
|
||||
- **WHEN** a retrieval or publication attempt throws an exception
|
||||
- **THEN** the source logs the contextual failure and retries on the next fixed-delay interval
|
||||
|
||||
### Requirement: Restartable source lifecycle
|
||||
The source SHALL reject a repeated startup before replacing its existing sink. Stopping SHALL be idempotent, terminate source-owned scheduling resources, correctly preserve interruption, and permit a later startup with a newly supplied sink.
|
||||
|
||||
#### Scenario: Start an already-started source
|
||||
- **WHEN** startup is invoked while the source is already running
|
||||
- **THEN** startup fails and the currently active sink remains unchanged
|
||||
|
||||
#### Scenario: Stop a running source
|
||||
- **WHEN** stop is invoked on a running source
|
||||
- **THEN** its scheduler terminates and no further polling begins
|
||||
|
||||
#### Scenario: Stop an already-stopped source
|
||||
- **WHEN** stop is invoked while the source is stopped
|
||||
- **THEN** it completes without error
|
||||
|
||||
#### Scenario: Restart a stopped source
|
||||
- **WHEN** a previously stopped source is started with a sink
|
||||
- **THEN** it creates fresh source-owned scheduling resources and begins an immediate retrieval attempt
|
||||
|
||||
### Requirement: Activation uses independent Ticker history
|
||||
The pool source SHALL participate in the existing Ticker activation contract under its exact trading pair and pool-based source name. It SHALL be active only when that exact history file already exists and SHALL NOT create or migrate history files.
|
||||
|
||||
#### Scenario: Pool history exists
|
||||
- **WHEN** the EVE/USDT history file exists beneath the EVE UUID, USDT UUID, and exact pool-based source-name path
|
||||
- **THEN** the Ticker activates the Raydium source independently of the hardcoded EVE/USDC history
|
||||
|
||||
#### Scenario: Pool history is absent
|
||||
- **WHEN** the exact Raydium source history file is absent
|
||||
- **THEN** the Ticker leaves that source inactive and creates no file or directory
|
||||
@@ -21,6 +21,10 @@ public final class CurrencyTypeIds {
|
||||
public static final UUID USDC_ID =
|
||||
UUID.fromString("019c3f9f-41d1-7a73-b1df-d4c11c7ff302");
|
||||
|
||||
/** Stable UUID for Tether USD. */
|
||||
public static final UUID USDT_ID =
|
||||
UUID.fromString("c8669973-0045-468e-8b2b-781d06d123b2");
|
||||
|
||||
/** Stable UUID for Solana. */
|
||||
public static final UUID SOLANA_ID =
|
||||
UUID.fromString("019e0116-fce5-792f-a647-fa6da4dffec5");
|
||||
|
||||
+3
@@ -18,6 +18,7 @@ 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;
|
||||
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.USDT_ID;
|
||||
|
||||
public final class HardcodedCurrencyIdentityService implements CurrencyIdentityService {
|
||||
public HardcodedCurrencyIdentityService() {
|
||||
@@ -26,6 +27,8 @@ public final class HardcodedCurrencyIdentityService implements CurrencyIdentityS
|
||||
solanaMint("meveYG2iXYSkgSUn1T1uxcthH1EGMZdRHGgCntXZA3Y", "EVE")),
|
||||
entry(USDC_ID, "USD Coin", "USDC",
|
||||
solanaMint("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "USDC")),
|
||||
entry(USDT_ID, "Tether USD", "USDT",
|
||||
solanaMint("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", "USDT")),
|
||||
entry(SOLANA_ID, "Solana", "SOL",
|
||||
solanaMint("So11111111111111111111111111111111111111112", "SOL")),
|
||||
entry(SYRUPUSDC_ID, "SyrupUSDC", "SyrupUSDC",
|
||||
|
||||
+191
-5
@@ -2,27 +2,213 @@ package com.r35157.assetaz.services.ticker.plugins.pricesource.impl.raydiumpool;
|
||||
|
||||
import com.r35157.assetaz.services.ticker.plugins.pricesource.PriceSink;
|
||||
import com.r35157.assetaz.services.ticker.plugins.pricesource.PriceSource;
|
||||
import com.r35157.libs.raydium.Raydium;
|
||||
import com.r35157.libs.valuetypes.basic.AssetPrice;
|
||||
import com.r35157.libs.valuetypes.basic.TradingPair;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Clock;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class RaydiumPoolPriceSource implements PriceSource {
|
||||
public RaydiumPoolPriceSource(
|
||||
@NotNull Raydium raydium,
|
||||
@NotNull ΩRaydiumLiquidityPoolIdΩ poolId,
|
||||
@NotNull TradingPair expectedTradingPair
|
||||
) {
|
||||
this(
|
||||
raydium,
|
||||
poolId,
|
||||
expectedTradingPair,
|
||||
Clock.systemUTC(),
|
||||
POLLING_DELAY_MINUTES,
|
||||
TimeUnit.MINUTES
|
||||
);
|
||||
}
|
||||
|
||||
RaydiumPoolPriceSource(
|
||||
@NotNull Raydium raydium,
|
||||
@NotNull ΩRaydiumLiquidityPoolIdΩ poolId,
|
||||
@NotNull TradingPair expectedTradingPair,
|
||||
@NotNull Clock clock,
|
||||
long pollingDelay,
|
||||
@NotNull TimeUnit pollingDelayUnit
|
||||
) {
|
||||
this.raydium = Objects.requireNonNull(raydium, "raydium");
|
||||
this.poolId = Objects.requireNonNull(poolId, "poolId");
|
||||
if (poolId.isBlank()) {
|
||||
throw new IllegalArgumentException("poolId must not be blank");
|
||||
}
|
||||
this.expectedTradingPair = Objects.requireNonNull(
|
||||
expectedTradingPair,
|
||||
"expectedTradingPair"
|
||||
);
|
||||
Objects.requireNonNull(expectedTradingPair.base(), "expectedTradingPair.base");
|
||||
Objects.requireNonNull(expectedTradingPair.quote(), "expectedTradingPair.quote");
|
||||
this.sourceName = "Raydium-" + poolId;
|
||||
this.clock = Objects.requireNonNull(clock, "clock");
|
||||
if (pollingDelay <= 0) {
|
||||
throw new IllegalArgumentException("pollingDelay must be positive");
|
||||
}
|
||||
this.pollingDelay = pollingDelay;
|
||||
this.pollingDelayUnit = Objects.requireNonNull(
|
||||
pollingDelayUnit,
|
||||
"pollingDelayUnit"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull TradingPair getTradingPair() {
|
||||
throw new UnsupportedOperationException("Not Implemented");
|
||||
return expectedTradingPair;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ΩPriceSourceNameΩ getSourceName() {
|
||||
throw new UnsupportedOperationException("Not Implemented");
|
||||
return sourceName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(@NotNull PriceSink priceSink) {
|
||||
throw new UnsupportedOperationException("Not Implemented");
|
||||
public synchronized void start(@NotNull PriceSink priceSink) {
|
||||
if (scheduler != null || stopping) {
|
||||
throw new IllegalStateException("Raydium pool price source is already started");
|
||||
}
|
||||
|
||||
PriceSink newPriceSink = Objects.requireNonNull(priceSink, "priceSink");
|
||||
ScheduledExecutorService newScheduler = Executors.newSingleThreadScheduledExecutor(
|
||||
runnable -> {
|
||||
Thread thread = new Thread(
|
||||
runnable,
|
||||
"assetaz-raydium-pool-price-source-" + poolId
|
||||
);
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
);
|
||||
|
||||
this.priceSink = newPriceSink;
|
||||
this.scheduler = newScheduler;
|
||||
newScheduler.scheduleWithFixedDelay(
|
||||
() -> pollSafely(newScheduler, newPriceSink),
|
||||
0,
|
||||
pollingDelay,
|
||||
pollingDelayUnit
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
throw new UnsupportedOperationException("Not Implemented");
|
||||
ScheduledExecutorService schedulerToStop;
|
||||
synchronized (this) {
|
||||
if (scheduler == null) {
|
||||
return;
|
||||
}
|
||||
if (stopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
stopping = true;
|
||||
schedulerToStop = scheduler;
|
||||
priceSink = null;
|
||||
}
|
||||
|
||||
schedulerToStop.shutdownNow();
|
||||
try {
|
||||
if (!schedulerToStop.awaitTermination(
|
||||
TERMINATION_TIMEOUT_SECONDS,
|
||||
TimeUnit.SECONDS
|
||||
)) {
|
||||
log.error(
|
||||
"Raydium pool price source scheduler did not terminate: poolId={}, tradingPair={}",
|
||||
poolId,
|
||||
expectedTradingPair
|
||||
);
|
||||
}
|
||||
} catch (InterruptedException exception) {
|
||||
schedulerToStop.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
synchronized (this) {
|
||||
if (schedulerToStop.isTerminated()) {
|
||||
scheduler = null;
|
||||
}
|
||||
stopping = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void pollSafely(
|
||||
ScheduledExecutorService pollingScheduler,
|
||||
PriceSink pollingSink
|
||||
) {
|
||||
try {
|
||||
AssetPrice assetPrice = raydium.fetchPoolPrice(poolId);
|
||||
TradingPair receivedTradingPair = assetPrice.tradingPair();
|
||||
if (!expectedTradingPair.equals(receivedTradingPair)) {
|
||||
log.error(
|
||||
"Raydium pool returned unexpected trading pair: poolId={}, expected={}, received={}",
|
||||
poolId,
|
||||
expectedTradingPair,
|
||||
receivedTradingPair
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isCurrentRun(pollingScheduler, pollingSink)) {
|
||||
return;
|
||||
}
|
||||
pollingSink.announce(
|
||||
this,
|
||||
assetPrice.price(),
|
||||
clock.instant().truncatedTo(ChronoUnit.MILLIS)
|
||||
);
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
if (!isCurrentRun(pollingScheduler, pollingSink)) {
|
||||
return;
|
||||
}
|
||||
log.error(
|
||||
"Raydium pool price polling was interrupted: poolId={}, tradingPair={}",
|
||||
poolId,
|
||||
expectedTradingPair,
|
||||
exception
|
||||
);
|
||||
} catch (Exception exception) {
|
||||
log.error(
|
||||
"Raydium pool price polling failed: poolId={}, tradingPair={}",
|
||||
poolId,
|
||||
expectedTradingPair,
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized boolean isCurrentRun(
|
||||
ScheduledExecutorService pollingScheduler,
|
||||
PriceSink pollingSink
|
||||
) {
|
||||
return scheduler == pollingScheduler && priceSink == pollingSink && !stopping;
|
||||
}
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(RaydiumPoolPriceSource.class);
|
||||
private static final long POLLING_DELAY_MINUTES = 1;
|
||||
private static final long TERMINATION_TIMEOUT_SECONDS = 10;
|
||||
|
||||
private final Raydium raydium;
|
||||
private final ΩRaydiumLiquidityPoolIdΩ poolId;
|
||||
private final TradingPair expectedTradingPair;
|
||||
private final ΩPriceSourceNameΩ sourceName;
|
||||
private final Clock clock;
|
||||
private final long pollingDelay;
|
||||
private final TimeUnit pollingDelayUnit;
|
||||
|
||||
private PriceSink priceSink;
|
||||
private ScheduledExecutorService scheduler;
|
||||
private boolean stopping;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package com.r35157.nenjim.hubd.impl.ref;
|
||||
|
||||
import com.fanitas.evelyn.core.Evelyn;
|
||||
import com.fanitas.evelyn.core.impl.ref.EvelynImpl;
|
||||
import com.r35157.assetaz.services.ticker.plugins.pricesource.PriceSource;
|
||||
import com.r35157.assetaz.services.ticker.plugins.pricesource.impl.hardcoded.HardcodedPriceSource;
|
||||
import com.r35157.assetaz.services.ticker.plugins.pricesource.impl.raydiumpool.RaydiumPoolPriceSource;
|
||||
import com.r35157.assetaz.services.ticker.impl.ref.TickerServiceImpl;
|
||||
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
|
||||
import com.r35157.assetaz.services.cis.impl.hc.HardcodedCurrencyIdentityService;
|
||||
@@ -16,6 +18,11 @@ import com.r35157.nenjim.npm.NenjimProcessManager;
|
||||
import com.r35157.nenjim.npm.impl.ref.NenjimProcessManagerImpl;
|
||||
import com.r35157.nenjim.ntt.NenjimTestTool;
|
||||
import com.r35157.nenjim.ntt.impl.ref.NenjimTestToolImpl;
|
||||
import com.r35157.libs.raydium.Raydium;
|
||||
import com.r35157.libs.raydium.impl.ref.RaydiumImpl;
|
||||
import com.r35157.libs.solana.SolanaBlockChain;
|
||||
import com.r35157.libs.solana.impl.ref.SolanaBlockChainImpl;
|
||||
import com.r35157.libs.valuetypes.basic.TradingPair;
|
||||
import com.r35157.stm.SodaTaskManager;
|
||||
import com.r35157.stm.impl.ref.SodaTaskManagerImpl;
|
||||
import com.r35157.suwimo.hub.client.SuwimoClient;
|
||||
@@ -27,6 +34,9 @@ import org.slf4j.LoggerFactory;
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.EVE_ID;
|
||||
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.USDT_ID;
|
||||
|
||||
public class NenjimHubImpl implements NenjimHub {
|
||||
public NenjimHubImpl() throws Exception {
|
||||
log.info("Initializing NenjimHub...");
|
||||
@@ -61,10 +71,17 @@ public class NenjimHubImpl implements NenjimHub {
|
||||
}
|
||||
|
||||
private void startAutoRunProcesses() {
|
||||
CurrencyIdentityService currencyIdentityService =
|
||||
new HardcodedCurrencyIdentityService();
|
||||
startAssetAZTickerService(currencyIdentityService);
|
||||
startJupiterPerpsAlarm(currencyIdentityService); // TODO: Hardcoded/hacky way to auto-start but good enough for now.
|
||||
CurrencyIdentityService cis = new HardcodedCurrencyIdentityService();
|
||||
SolanaBlockChain solanaBlockChain = new SolanaBlockChainImpl(cis);
|
||||
Raydium raydium = new RaydiumImpl(solanaBlockChain, cis);
|
||||
|
||||
PriceSource hardcodedPriceSource = new HardcodedPriceSource(cis);
|
||||
PriceSource raydiumPoolPriceSource = createEVEUSDTPriceSource(cis, raydium);
|
||||
|
||||
startAssetAZTickerService(hardcodedPriceSource, raydiumPoolPriceSource);
|
||||
|
||||
startJupiterPerpsAlarm(cis);
|
||||
|
||||
Evelyn evelynProd = new EvelynImpl();
|
||||
Evelyn evelynTest = new EvelynImpl();
|
||||
startEvelynMissionControl(evelynProd, evelynTest);
|
||||
@@ -92,18 +109,16 @@ public class NenjimHubImpl implements NenjimHub {
|
||||
*/
|
||||
}
|
||||
|
||||
private void startAssetAZTickerService(
|
||||
CurrencyIdentityService currencyIdentityService
|
||||
) {
|
||||
// Nenjim creates this unstarted PriceSource first...
|
||||
HardcodedPriceSource priceSource = new HardcodedPriceSource(
|
||||
currencyIdentityService
|
||||
);
|
||||
private PriceSource createEVEUSDTPriceSource(CurrencyIdentityService cis, Raydium raydium) {
|
||||
TradingPair eveUsdt = new TradingPair(cis.resolve(EVE_ID), cis.resolve(USDT_ID));
|
||||
PriceSource priceSource = new RaydiumPoolPriceSource(raydium, EVE_USDT_RAYDIUM_POOL_ID, eveUsdt);
|
||||
|
||||
return priceSource;
|
||||
}
|
||||
|
||||
private void startAssetAZTickerService(PriceSource... priceSources) {
|
||||
TickerServiceImpl tickerService = new TickerServiceImpl(priceSources);
|
||||
|
||||
// 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
|
||||
// not be injected in the constructor but TickerServiceImpl will ask Nenjim for them.
|
||||
TickerServiceImpl tickerService = new TickerServiceImpl(priceSource);
|
||||
tickerService.start();
|
||||
}
|
||||
|
||||
@@ -257,6 +272,8 @@ public class NenjimHubImpl implements NenjimHub {
|
||||
}
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(NenjimHubImpl.class);
|
||||
private static final ΩRaydiumLiquidityPoolIdΩ EVE_USDT_RAYDIUM_POOL_ID =
|
||||
"8rN4BTEzbogQosEQYgsEu18XwfKS5Yoxqwit8zEVFwEe";
|
||||
private final CountDownLatch shutdownLatch = new CountDownLatch(1);
|
||||
|
||||
private HashMap<Integer, NenjimProcess> processes;
|
||||
|
||||
Reference in New Issue
Block a user