67: Add generic awaitTransaction() support to SolanaBlockChain
This commit is contained in:
+2
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-11
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
## Context
|
||||
|
||||
See `proposal.md` for motivation. `SolanaBlockChainImpl` currently serializes every RPC request through a synchronized `sendThrottled()` method, sleeps until five seconds have elapsed since the preceding RPC response, performs HTTP while holding the monitor, and records completion time in `finally`. Monitor acquisition is neither interruptible nor deadline-aware, while issue #67 requires both without changing existing RPC behavior.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Preserve one shared five-second gate for all RPC calls while allowing awaiting calls to stop at a monotonic deadline.
|
||||
- Keep definitive transaction state, timeout, protocol failure, and interruption as distinct outcomes.
|
||||
- Strictly validate all wire data that controls a returned outcome.
|
||||
- Keep the API and outcome types generic to Solana rather than any consuming domain.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Combining submission and awaiting, resubmission, WebSockets, signature subscriptions, or blockhash-expiration inference.
|
||||
- Updating any existing wallet, Jupiter, burn, Evelyn, or Discord consumer.
|
||||
- Adding automated tests or test-only transport, time, or throttling seams in this implementation.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Replace the intrinsic monitor with one interruptible shared lock
|
||||
|
||||
A private `ReentrantLock` will serialize all RPC traffic. Existing calls acquire it interruptibly, perform the same five-second wait and HTTP exchange, update the shared completion timestamp in `finally`, and release it. Awaiting calls use timed `tryLock` with their remaining deadline. A separate polling lock or rate limiter was rejected because it would violate the shared throttle and permit polling to interfere unpredictably with other calls.
|
||||
|
||||
### Measure shared throttling and awaiting deadlines monotonically
|
||||
|
||||
The completion timestamp and deadline calculations use `System.nanoTime()`. A small private deadline value tracks the call's start and saturated timeout nanoseconds and computes remaining time by elapsed duration. Wall-clock time was rejected because clock adjustments could extend or shorten both throttling and the caller's timeout.
|
||||
|
||||
### Build each status HTTP request only after gate and throttle admission
|
||||
|
||||
The deadline-aware send path acquires the gate and waits only up to the remaining deadline. Once admitted, it creates a request whose `HttpRequest.timeout` equals the then-current remaining duration. A request-timeout exception is translated to `TIMED_OUT`; other I/O failures remain `IOException`. Building the request after admission avoids assigning the full original timeout to an HTTP exchange that starts after gate/throttle delay.
|
||||
|
||||
### Return an internal no-request signal on deadline expiry
|
||||
|
||||
The deadline-aware send helper returns no response when the deadline expires before request start. The public loop converts this only to the invariant `TIMED_OUT` value. This keeps deadline expiry separate from communication/protocol exceptions and ensures a previously seen lower-commitment slot cannot leak into the timeout result.
|
||||
|
||||
### Parse only authoritative signature-status fields
|
||||
|
||||
Each response must contain a successful HTTP result, no JSON-RPC error, and exactly one `result.value` element. A null element remains inconclusive. A status object requires a Long-representable non-negative `slot` and recognized `confirmationStatus`; only after its commitment satisfies the request is `err` interpreted as success or failure. `result.context`, `confirmations`, and legacy `status` are ignored.
|
||||
|
||||
### Preserve the complete on-chain error as compact JSON
|
||||
|
||||
Jackson's compact serialization of the non-null `err` node becomes `failureDetails`. The blockchain layer does not classify program-specific errors. Any issue outside this `err` node remains an exception or timeout rather than `FAILED`.
|
||||
|
||||
### Validate transaction signatures with the existing Base58 implementation
|
||||
|
||||
The reference implementation reuses its existing private Base58 decoder and requires exactly 64 decoded bytes before entering the RPC gate. Introducing a second codec dependency or general transaction parser was rejected as unrelated scope.
|
||||
|
||||
### Make cached awaiting a transparent pass-through
|
||||
|
||||
`CachedSolanaBlockChain.awaitTransaction` calls its delegate directly. Transaction status is deadline-sensitive and mutable, so cache keys, reuse, and in-flight deduplication would violate the contract.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [An HTTP client can complete at the edge of its timeout after the monotonic deadline] → Use the remaining deadline as the request timeout and re-check the deadline before interpreting an inconclusive response; this is the strongest practical bound offered by Java `HttpClient`.
|
||||
- [Replacing synchronization changes the internal locking primitive] → Keep the same lock scope, completion-based five-second spacing, and exception behavior for every existing RPC operation.
|
||||
- [No automated deterministic coverage is added now] → Keep deadline, parsing, and gate concerns in narrow private methods and document that focused tests remain deferred by explicit task instruction.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
This is additive at the public API. Existing implementations in this repository are updated together, while existing consumers remain unchanged. Rollback removes the new method/types and restores the prior private monitor without data migration.
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
## Why
|
||||
|
||||
Submitting a Solana transaction only proves RPC acceptance, leaving callers unable to distinguish a requested on-chain commitment outcome from an unknown pending or dropped transaction. A generic wait operation is needed so future consumers can explicitly await processed, confirmed, or finalized outcomes without coupling submission to confirmation.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add public Solana commitment and transaction-outcome types with strict status invariants and comprehensive timeout semantics.
|
||||
- Add `SolanaBlockChain.awaitTransaction(...)` to poll `getSignatureStatuses` for an already submitted signature.
|
||||
- Reuse the existing shared five-second RPC throttle while making gate acquisition interruptible and bounded by an overall monotonic deadline.
|
||||
- Validate signatures and timeouts locally, strictly validate RPC responses, and reserve `FAILED` exclusively for on-chain execution errors observed at the requested commitment.
|
||||
- Delegate every cached-wrapper call directly without caching or deduplication.
|
||||
- Add the `SolanaSlot` ValueTag backed by nullable `Long`.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `solana-transaction-awaiting`: Generic blocking observation of an existing Solana transaction through a requested commitment level, with definitive success/failure and unknown-timeout semantics.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
None.
|
||||
|
||||
## Impact
|
||||
|
||||
The Solana public API, reference RPC implementation, cached decorator, and shared Detag configuration are extended. Existing RPC operations retain their public behavior and common throttling; no wallet, Jupiter, burn, Evelyn, WebSocket, or transaction-submission consumer is changed to wait automatically.
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
## Purpose
|
||||
|
||||
Defines how callers await a definitive Solana transaction outcome at an explicitly requested commitment without resubmitting the transaction.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Separate transaction awaiting API
|
||||
The Solana blockchain API SHALL expose a blocking operation that accepts an existing 64-byte Base58 transaction signature, a requested commitment of `PROCESSED`, `CONFIRMED`, or `FINALIZED`, and a strictly positive overall timeout. Submission and awaiting SHALL remain separate operations, and existing consumers SHALL NOT begin awaiting automatically.
|
||||
|
||||
#### Scenario: Valid await request
|
||||
- **WHEN** a caller supplies a valid signature, non-null commitment, and positive timeout
|
||||
- **THEN** the service observes that already submitted transaction without submitting or resubmitting it
|
||||
|
||||
#### Scenario: Invalid arguments
|
||||
- **WHEN** the signature is null, blank, invalid Base58, or does not decode to exactly 64 bytes, the commitment is null, or the timeout is null, zero, or negative
|
||||
- **THEN** the service rejects the call locally before making an RPC request
|
||||
|
||||
### Requirement: Commitment-aware definitive outcomes
|
||||
The commitment order SHALL be `PROCESSED < CONFIRMED < FINALIZED`, and an observed level SHALL satisfy the same or any lower requested level. The service SHALL return `SUCCEEDED` or `FAILED` only after the observed transaction has reached or exceeded the requested commitment. `FAILED` SHALL exclusively represent a non-null Solana on-chain `err`; lower-commitment success or error observations SHALL remain inconclusive.
|
||||
|
||||
#### Scenario: Success reaches requested commitment
|
||||
- **WHEN** the transaction is observed at or above the requested commitment with `err: null`
|
||||
- **THEN** the service returns `SUCCEEDED` with the transaction slot and no failure details
|
||||
|
||||
#### Scenario: Failure reaches requested commitment
|
||||
- **WHEN** the transaction is observed at or above the requested commitment with a non-null `err`
|
||||
- **THEN** the service returns `FAILED` with the transaction slot and the complete `err` value as compact JSON
|
||||
|
||||
#### Scenario: Observation is below requested commitment
|
||||
- **WHEN** a successful or failed transaction is observed below the requested commitment
|
||||
- **THEN** the service continues polling rather than returning a definitive outcome
|
||||
|
||||
### Requirement: Transaction outcome invariants
|
||||
Every public transaction outcome SHALL enforce that `SUCCEEDED` has a non-null slot and null failure details, `FAILED` has a non-null slot and non-blank failure details, and `TIMED_OUT` has null slot and null failure details.
|
||||
|
||||
#### Scenario: Invalid outcome construction
|
||||
- **WHEN** an outcome is constructed with fields inconsistent with its status
|
||||
- **THEN** construction fails immediately
|
||||
|
||||
### Requirement: Signature status polling contract
|
||||
The service SHALL poll HTTP JSON-RPC `getSignatureStatuses` with exactly the supplied signature and `searchTransactionHistory: true`. It SHALL use only `result.value[0].slot`, `err`, and `confirmationStatus`; a null sole value SHALL remain inconclusive. A present status SHALL contain a Long-representable slot and one of `processed`, `confirmed`, or `finalized`.
|
||||
|
||||
#### Scenario: Signature is not found yet
|
||||
- **WHEN** `result.value` contains exactly one null element
|
||||
- **THEN** the service continues polling until a definitive outcome or timeout
|
||||
|
||||
#### Scenario: Valid status is observed
|
||||
- **WHEN** the sole status contains a valid slot, recognized confirmation status, and `err` value
|
||||
- **THEN** the service evaluates it against the requested commitment
|
||||
|
||||
### Requirement: Overall monotonic deadline
|
||||
The timeout SHALL define one monotonic deadline covering RPC-gate acquisition, shared throttling, HTTP calls, response processing, and subsequent polls. Gate and throttle waiting SHALL be interruptible and deadline-aware, no request SHALL start after the deadline, and an HTTP request SHALL as far as reasonably possible be bounded by the remaining time.
|
||||
|
||||
#### Scenario: Deadline expires before RPC access
|
||||
- **WHEN** the deadline expires while waiting for the shared gate or remaining throttle interval
|
||||
- **THEN** the service returns `TIMED_OUT` without starting another RPC request
|
||||
|
||||
#### Scenario: Deadline expires during HTTP request
|
||||
- **WHEN** the deadline-bound status HTTP request times out
|
||||
- **THEN** the service returns `TIMED_OUT`
|
||||
|
||||
#### Scenario: Caller is interrupted
|
||||
- **WHEN** interruption occurs during gate acquisition, throttling, HTTP communication, or another internal wait
|
||||
- **THEN** `InterruptedException` propagates directly and no subsequent request is made
|
||||
|
||||
### Requirement: Unknown timeout semantics
|
||||
`TIMED_OUT` SHALL mean the requested definitive outcome remains unknown and SHALL NOT imply rejection, failure, expiration, or permission to resubmit. It SHALL not retain a slot previously observed below the requested commitment.
|
||||
|
||||
#### Scenario: Deadline passes without definitive outcome
|
||||
- **WHEN** the signature remains unseen or below the requested commitment until the deadline
|
||||
- **THEN** the service returns `TIMED_OUT` with null slot and null failure details and does not resubmit the transaction
|
||||
|
||||
### Requirement: Shared RPC throttling
|
||||
Status polling SHALL reuse the same shared mechanism that preserves at least five seconds between Solana RPC calls. It SHALL add no independent polling sleep, interval, or limiter, and existing RPC operations SHALL preserve their public behavior and common throttling.
|
||||
|
||||
#### Scenario: Inconclusive poll
|
||||
- **WHEN** a status response is inconclusive and time remains
|
||||
- **THEN** the next poll is naturally delayed by the shared five-second RPC throttle only
|
||||
|
||||
### Requirement: Communication and protocol errors
|
||||
Non-success HTTP responses, JSON-RPC errors, invalid JSON, missing or invalid `result.value`, arrays with other than exactly one element, invalid slots, missing or unknown confirmation status, and other network or protocol failures SHALL result in `IOException`, not `FAILED`. Error messages SHALL identify `getSignatureStatuses` and include relevant context. The service SHALL NOT automatically retry after an `IOException`.
|
||||
|
||||
#### Scenario: Invalid RPC response
|
||||
- **WHEN** a status response violates the required HTTP, JSON-RPC, result-array, slot, or confirmation-status contract
|
||||
- **THEN** the service throws a contextual `IOException`
|
||||
|
||||
#### Scenario: Communication failure
|
||||
- **WHEN** status communication fails for a reason other than expiration of the overall deadline or thread interruption
|
||||
- **THEN** the service throws `IOException` without automatically polling again
|
||||
|
||||
### Requirement: Cached decorator delegates directly
|
||||
The cached Solana blockchain decorator SHALL delegate every transaction-await call directly to its underlying blockchain without caching, reuse, or deduplication.
|
||||
|
||||
#### Scenario: Repeated awaits through cached decorator
|
||||
- **WHEN** callers invoke transaction awaiting multiple times through the cached decorator
|
||||
- **THEN** every invocation reaches the delegate independently
|
||||
@@ -0,0 +1,24 @@
|
||||
## 1. Public Solana API
|
||||
|
||||
- [x] 1.1 Add the nullable-Long `SolanaSlot` ValueTag through the shared Detag configuration.
|
||||
- [x] 1.2 Add documented `SolanaCommitment` and invariant-enforcing `SolanaTransactionOutcome` public types.
|
||||
- [x] 1.3 Add the documented `awaitTransaction(...)` contract to `SolanaBlockChain`.
|
||||
|
||||
## 2. RPC Gate and Polling
|
||||
|
||||
- [x] 2.1 Replace intrinsic RPC synchronization with one interruptible shared gate while preserving completion-based five-second throttling for existing operations.
|
||||
- [x] 2.2 Add local signature, commitment, and timeout validation plus a monotonic overall deadline.
|
||||
- [x] 2.3 Implement deadline-aware `getSignatureStatuses` polling with `searchTransactionHistory: true` and no independent polling delay.
|
||||
- [x] 2.4 Strictly validate HTTP, JSON-RPC, result-array, slot, confirmation-status, and error payload semantics.
|
||||
- [x] 2.5 Return definitive outcomes only at the requested commitment and preserve direct interruption, unknown timeout, and no-retry behavior.
|
||||
|
||||
## 3. Cached Decorator and Scope
|
||||
|
||||
- [x] 3.1 Delegate cached-wrapper awaits directly without caching, reuse, or deduplication.
|
||||
- [x] 3.2 Confirm no existing wallet, Jupiter, burn, Evelyn, Discord, or submission flow adopts automatic awaiting.
|
||||
|
||||
## 4. Verification
|
||||
|
||||
- [x] 4.1 Compile main and test source sets with normal Detag generation and run existing relevant verification without adding tests.
|
||||
- [x] 4.2 Run strict OpenSpec validation and `git diff --check`.
|
||||
- [x] 4.3 Review the complete diff for protocol gaps, deadline violations, generated-file edits, new tests, and unrelated changes.
|
||||
@@ -0,0 +1,98 @@
|
||||
# solana-transaction-awaiting Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines how callers await a definitive Solana transaction outcome at an explicitly requested commitment without resubmitting the transaction.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Separate transaction awaiting API
|
||||
The Solana blockchain API SHALL expose a blocking operation that accepts an existing 64-byte Base58 transaction signature, a requested commitment of `PROCESSED`, `CONFIRMED`, or `FINALIZED`, and a strictly positive overall timeout. Submission and awaiting SHALL remain separate operations, and existing consumers SHALL NOT begin awaiting automatically.
|
||||
|
||||
#### Scenario: Valid await request
|
||||
- **WHEN** a caller supplies a valid signature, non-null commitment, and positive timeout
|
||||
- **THEN** the service observes that already submitted transaction without submitting or resubmitting it
|
||||
|
||||
#### Scenario: Invalid arguments
|
||||
- **WHEN** the signature is null, blank, invalid Base58, or does not decode to exactly 64 bytes, the commitment is null, or the timeout is null, zero, or negative
|
||||
- **THEN** the service rejects the call locally before making an RPC request
|
||||
|
||||
### Requirement: Commitment-aware definitive outcomes
|
||||
The commitment order SHALL be `PROCESSED < CONFIRMED < FINALIZED`, and an observed level SHALL satisfy the same or any lower requested level. The service SHALL return `SUCCEEDED` or `FAILED` only after the observed transaction has reached or exceeded the requested commitment. `FAILED` SHALL exclusively represent a non-null Solana on-chain `err`; lower-commitment success or error observations SHALL remain inconclusive.
|
||||
|
||||
#### Scenario: Success reaches requested commitment
|
||||
- **WHEN** the transaction is observed at or above the requested commitment with `err: null`
|
||||
- **THEN** the service returns `SUCCEEDED` with the transaction slot and no failure details
|
||||
|
||||
#### Scenario: Failure reaches requested commitment
|
||||
- **WHEN** the transaction is observed at or above the requested commitment with a non-null `err`
|
||||
- **THEN** the service returns `FAILED` with the transaction slot and the complete `err` value as compact JSON
|
||||
|
||||
#### Scenario: Observation is below requested commitment
|
||||
- **WHEN** a successful or failed transaction is observed below the requested commitment
|
||||
- **THEN** the service continues polling rather than returning a definitive outcome
|
||||
|
||||
### Requirement: Transaction outcome invariants
|
||||
Every public transaction outcome SHALL enforce that `SUCCEEDED` has a non-null slot and null failure details, `FAILED` has a non-null slot and non-blank failure details, and `TIMED_OUT` has null slot and null failure details.
|
||||
|
||||
#### Scenario: Invalid outcome construction
|
||||
- **WHEN** an outcome is constructed with fields inconsistent with its status
|
||||
- **THEN** construction fails immediately
|
||||
|
||||
### Requirement: Signature status polling contract
|
||||
The service SHALL poll HTTP JSON-RPC `getSignatureStatuses` with exactly the supplied signature and `searchTransactionHistory: true`. It SHALL use only `result.value[0].slot`, `err`, and `confirmationStatus`; a null sole value SHALL remain inconclusive. A present status SHALL contain a Long-representable slot and one of `processed`, `confirmed`, or `finalized`.
|
||||
|
||||
#### Scenario: Signature is not found yet
|
||||
- **WHEN** `result.value` contains exactly one null element
|
||||
- **THEN** the service continues polling until a definitive outcome or timeout
|
||||
|
||||
#### Scenario: Valid status is observed
|
||||
- **WHEN** the sole status contains a valid slot, recognized confirmation status, and `err` value
|
||||
- **THEN** the service evaluates it against the requested commitment
|
||||
|
||||
### Requirement: Overall monotonic deadline
|
||||
The timeout SHALL define one monotonic deadline covering RPC-gate acquisition, shared throttling, HTTP calls, response processing, and subsequent polls. Gate and throttle waiting SHALL be interruptible and deadline-aware, no request SHALL start after the deadline, and an HTTP request SHALL as far as reasonably possible be bounded by the remaining time.
|
||||
|
||||
#### Scenario: Deadline expires before RPC access
|
||||
- **WHEN** the deadline expires while waiting for the shared gate or remaining throttle interval
|
||||
- **THEN** the service returns `TIMED_OUT` without starting another RPC request
|
||||
|
||||
#### Scenario: Deadline expires during HTTP request
|
||||
- **WHEN** the deadline-bound status HTTP request times out
|
||||
- **THEN** the service returns `TIMED_OUT`
|
||||
|
||||
#### Scenario: Caller is interrupted
|
||||
- **WHEN** interruption occurs during gate acquisition, throttling, HTTP communication, or another internal wait
|
||||
- **THEN** `InterruptedException` propagates directly and no subsequent request is made
|
||||
|
||||
### Requirement: Unknown timeout semantics
|
||||
`TIMED_OUT` SHALL mean the requested definitive outcome remains unknown and SHALL NOT imply rejection, failure, expiration, or permission to resubmit. It SHALL not retain a slot previously observed below the requested commitment.
|
||||
|
||||
#### Scenario: Deadline passes without definitive outcome
|
||||
- **WHEN** the signature remains unseen or below the requested commitment until the deadline
|
||||
- **THEN** the service returns `TIMED_OUT` with null slot and null failure details and does not resubmit the transaction
|
||||
|
||||
### Requirement: Shared RPC throttling
|
||||
Status polling SHALL reuse the same shared mechanism that preserves at least five seconds between Solana RPC calls. It SHALL add no independent polling sleep, interval, or limiter, and existing RPC operations SHALL preserve their public behavior and common throttling.
|
||||
|
||||
#### Scenario: Inconclusive poll
|
||||
- **WHEN** a status response is inconclusive and time remains
|
||||
- **THEN** the next poll is naturally delayed by the shared five-second RPC throttle only
|
||||
|
||||
### Requirement: Communication and protocol errors
|
||||
Non-success HTTP responses, JSON-RPC errors, invalid JSON, missing or invalid `result.value`, arrays with other than exactly one element, invalid slots, missing or unknown confirmation status, and other network or protocol failures SHALL result in `IOException`, not `FAILED`. Error messages SHALL identify `getSignatureStatuses` and include relevant context. The service SHALL NOT automatically retry after an `IOException`.
|
||||
|
||||
#### Scenario: Invalid RPC response
|
||||
- **WHEN** a status response violates the required HTTP, JSON-RPC, result-array, slot, or confirmation-status contract
|
||||
- **THEN** the service throws a contextual `IOException`
|
||||
|
||||
#### Scenario: Communication failure
|
||||
- **WHEN** status communication fails for a reason other than expiration of the overall deadline or thread interruption
|
||||
- **THEN** the service throws `IOException` without automatically polling again
|
||||
|
||||
### Requirement: Cached decorator delegates directly
|
||||
The cached Solana blockchain decorator SHALL delegate every transaction-await call directly to its underlying blockchain without caching, reuse, or deduplication.
|
||||
|
||||
#### Scenario: Repeated awaits through cached decorator
|
||||
- **WHEN** callers invoke transaction awaiting multiple times through the cached decorator
|
||||
- **THEN** every invocation reaches the delegate independently
|
||||
@@ -5,6 +5,7 @@ import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
|
||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -277,4 +278,38 @@ public interface SolanaBlockChain {
|
||||
ΩSolanaTransactionSignatureΩ sendTransaction(
|
||||
SolanaSignedTransaction transaction
|
||||
) throws IOException, InterruptedException;
|
||||
|
||||
/**
|
||||
* Waits for an already submitted transaction to reach a commitment level.
|
||||
*
|
||||
* <p>This operation does not submit or resubmit the transaction. Both
|
||||
* successful and failed transactions must reach or exceed the requested
|
||||
* commitment before a definitive result is returned.</p>
|
||||
*
|
||||
* <p>{@link SolanaTransactionOutcome.Status#TIMED_OUT TIMED_OUT} means the
|
||||
* outcome remains unknown: the transaction may still be confirmed,
|
||||
* finalized, dropped, or otherwise unresolved. Callers must not
|
||||
* automatically resubmit an equivalent transaction after a timeout.</p>
|
||||
*
|
||||
* @param signature Base58 transaction signature that decodes to exactly
|
||||
* 64 bytes
|
||||
* @param commitment minimum commitment required for a definitive outcome
|
||||
* @param timeout strictly positive overall timeout, including RPC-gate,
|
||||
* throttling, and HTTP time
|
||||
* @return definitive success or on-chain failure at the requested
|
||||
* commitment, or an unknown timed-out outcome
|
||||
* @throws IllegalArgumentException if the signature is {@code null},
|
||||
* blank, invalid Base58, not 64 bytes, or
|
||||
* the timeout is zero or negative
|
||||
* @throws NullPointerException if the commitment or timeout is null
|
||||
* @throws IOException if communication fails or Solana returns an invalid
|
||||
* HTTP, JSON-RPC, or protocol response
|
||||
* @throws InterruptedException if the calling thread is interrupted while
|
||||
* waiting for the RPC gate, throttling, or HTTP
|
||||
*/
|
||||
SolanaTransactionOutcome awaitTransaction(
|
||||
ΩSolanaTransactionSignatureΩ signature,
|
||||
SolanaCommitment commitment,
|
||||
Duration timeout
|
||||
) throws IOException, InterruptedException;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.r35157.libs.solana;
|
||||
|
||||
/**
|
||||
* Commitment level required when observing a Solana transaction.
|
||||
*
|
||||
* <p>The constants are ordered from the weakest to the strongest commitment:
|
||||
* {@link #PROCESSED}, {@link #CONFIRMED}, then {@link #FINALIZED}. An observed
|
||||
* level satisfies the same requested level and every weaker level.</p>
|
||||
*/
|
||||
public enum SolanaCommitment {
|
||||
/**
|
||||
* The transaction has been processed by the connected node.
|
||||
*/
|
||||
PROCESSED,
|
||||
|
||||
/**
|
||||
* The transaction has been voted on by a supermajority of the cluster.
|
||||
*/
|
||||
CONFIRMED,
|
||||
|
||||
/**
|
||||
* The transaction has been finalized by the cluster.
|
||||
*/
|
||||
FINALIZED
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.r35157.libs.solana;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Outcome observed for a Solana transaction at a requested commitment level.
|
||||
*
|
||||
* <p>A timed-out outcome remains unknown. It does not prove that the
|
||||
* transaction failed, was rejected, expired, or will never land, and callers
|
||||
* must not automatically resubmit an equivalent transaction.</p>
|
||||
*
|
||||
* @param status definitive success, definitive on-chain failure, or timeout
|
||||
* @param slot transaction slot for a definitive outcome, otherwise
|
||||
* {@code null}
|
||||
* @param failureDetails compact JSON containing Solana's complete on-chain
|
||||
* {@code err} value for a failed outcome, otherwise
|
||||
* {@code null}
|
||||
*/
|
||||
public record SolanaTransactionOutcome(
|
||||
@NotNull Status status,
|
||||
@Nullable ΩSolanaSlotΩ slot,
|
||||
@Nullable String failureDetails
|
||||
) {
|
||||
/**
|
||||
* Validates the field invariants associated with the outcome status.
|
||||
*/
|
||||
public SolanaTransactionOutcome {
|
||||
Objects.requireNonNull(status, "status");
|
||||
|
||||
switch (status) {
|
||||
case SUCCEEDED -> {
|
||||
if (slot == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"A successful transaction outcome requires a slot"
|
||||
);
|
||||
}
|
||||
if (failureDetails != null) {
|
||||
throw new IllegalArgumentException(
|
||||
"A successful transaction outcome must not contain "
|
||||
+ "failure details"
|
||||
);
|
||||
}
|
||||
}
|
||||
case FAILED -> {
|
||||
if (slot == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"A failed transaction outcome requires a slot"
|
||||
);
|
||||
}
|
||||
if (failureDetails == null || failureDetails.isBlank()) {
|
||||
throw new IllegalArgumentException(
|
||||
"A failed transaction outcome requires failure "
|
||||
+ "details"
|
||||
);
|
||||
}
|
||||
}
|
||||
case TIMED_OUT -> {
|
||||
if (slot != null || failureDetails != null) {
|
||||
throw new IllegalArgumentException(
|
||||
"A timed-out transaction outcome must not contain "
|
||||
+ "a slot or failure details"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Classification of a transaction observation.
|
||||
*/
|
||||
public enum Status {
|
||||
/**
|
||||
* The transaction reached the requested commitment without an
|
||||
* on-chain execution error.
|
||||
*/
|
||||
SUCCEEDED,
|
||||
|
||||
/**
|
||||
* The transaction reached the requested commitment with a non-null
|
||||
* Solana on-chain {@code err} value.
|
||||
*/
|
||||
FAILED,
|
||||
|
||||
/**
|
||||
* The requested definitive outcome remained unknown at the deadline.
|
||||
*/
|
||||
TIMED_OUT
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,12 @@ import com.r35157.libs.solana.SPLTokenHolding;
|
||||
import com.r35157.libs.solana.SPLTokenSupply;
|
||||
import com.r35157.libs.solana.SolanaAccountInfo;
|
||||
import com.r35157.libs.solana.SolanaBlockChain;
|
||||
import com.r35157.libs.solana.SolanaCommitment;
|
||||
import com.r35157.libs.solana.SolanaLatestBlockhash;
|
||||
import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter;
|
||||
import com.r35157.libs.solana.SolanaProgramAddressSeed;
|
||||
import com.r35157.libs.solana.SolanaSignedTransaction;
|
||||
import com.r35157.libs.solana.SolanaTransactionOutcome;
|
||||
import com.r35157.libs.solana.SolanaUnsignedTransaction;
|
||||
import com.r35157.libs.solana.valuetypes.SolanaProgramDerivedAddress;
|
||||
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
|
||||
@@ -17,6 +19,7 @@ import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -210,6 +213,15 @@ public final class CachedSolanaBlockChain implements SolanaBlockChain {
|
||||
return delegate.sendTransaction(transaction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SolanaTransactionOutcome awaitTransaction(
|
||||
ΩSolanaTransactionSignatureΩ signature,
|
||||
SolanaCommitment commitment,
|
||||
Duration timeout
|
||||
) throws IOException, InterruptedException {
|
||||
return delegate.awaitTransaction(signature, commitment, timeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SolanaUnsignedTransaction buildSPLTokenTransferTransaction(
|
||||
ΩSolanaAddressΩ sender,
|
||||
|
||||
@@ -24,10 +24,13 @@ import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.SOLANA_ID;
|
||||
import static com.r35157.libs.solana.SolanaConstants.RPC_URL;
|
||||
@@ -803,6 +806,256 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
|
||||
return signature;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SolanaTransactionOutcome awaitTransaction(
|
||||
ΩSolanaTransactionSignatureΩ signature,
|
||||
SolanaCommitment commitment,
|
||||
Duration timeout
|
||||
) throws IOException, InterruptedException {
|
||||
long startedNanos = System.nanoTime();
|
||||
validateAwaitTransactionArguments(signature, commitment, timeout);
|
||||
Deadline deadline = Deadline.startingAt(startedNanos, timeout);
|
||||
String requestBody = createGetSignatureStatusesBody(signature);
|
||||
|
||||
while (!deadline.hasExpired()) {
|
||||
HttpResponse<String> response;
|
||||
try {
|
||||
response = sendThrottled(requestBody, deadline);
|
||||
} catch (IOException e) {
|
||||
if (deadline.hasExpired()) {
|
||||
return timedOutTransactionOutcome();
|
||||
}
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses HTTP communication "
|
||||
+ "failed for "
|
||||
+ RPC_URL
|
||||
+ ": "
|
||||
+ e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
if (response == null || deadline.hasExpired()) {
|
||||
return timedOutTransactionOutcome();
|
||||
}
|
||||
|
||||
SolanaTransactionOutcome outcome = parseSignatureStatus(
|
||||
response,
|
||||
commitment
|
||||
);
|
||||
if (outcome != null) {
|
||||
return deadline.hasExpired()
|
||||
? timedOutTransactionOutcome()
|
||||
: outcome;
|
||||
}
|
||||
}
|
||||
|
||||
return timedOutTransactionOutcome();
|
||||
}
|
||||
|
||||
private void validateAwaitTransactionArguments(
|
||||
ΩSolanaTransactionSignatureΩ signature,
|
||||
SolanaCommitment commitment,
|
||||
Duration timeout
|
||||
) {
|
||||
if (signature == null || signature.isBlank()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Solana transaction signature must not be blank"
|
||||
);
|
||||
}
|
||||
|
||||
byte[] decodedSignature;
|
||||
try {
|
||||
decodedSignature = base58Decode(signature);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Solana transaction signature must be valid Base58",
|
||||
e
|
||||
);
|
||||
}
|
||||
if (decodedSignature.length != SOLANA_SIGNATURE_LENGTH) {
|
||||
throw new IllegalArgumentException(
|
||||
"Solana transaction signature must decode to exactly "
|
||||
+ SOLANA_SIGNATURE_LENGTH
|
||||
+ " bytes, but decoded to "
|
||||
+ decodedSignature.length
|
||||
);
|
||||
}
|
||||
|
||||
Objects.requireNonNull(commitment, "commitment");
|
||||
Objects.requireNonNull(timeout, "timeout");
|
||||
if (timeout.isZero() || timeout.isNegative()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Solana transaction await timeout must be greater than "
|
||||
+ "zero"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private String createGetSignatureStatusesBody(
|
||||
ΩSolanaTransactionSignatureΩ signature
|
||||
) throws IOException {
|
||||
ObjectNode request = objectMapper.createObjectNode();
|
||||
request.put("jsonrpc", "2.0");
|
||||
request.put("id", 1);
|
||||
request.put("method", "getSignatureStatuses");
|
||||
|
||||
ArrayNode params = request.putArray("params");
|
||||
params.addArray().add(signature);
|
||||
params.addObject().put("searchTransactionHistory", true);
|
||||
|
||||
return objectMapper.writeValueAsString(request);
|
||||
}
|
||||
|
||||
private SolanaTransactionOutcome parseSignatureStatus(
|
||||
HttpResponse<String> response,
|
||||
SolanaCommitment requestedCommitment
|
||||
) throws IOException {
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses RPC call failed: HTTP "
|
||||
+ response.statusCode()
|
||||
+ ": "
|
||||
+ response.body()
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode root;
|
||||
try {
|
||||
root = objectMapper.readTree(response.body());
|
||||
} catch (IOException e) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses response contained invalid "
|
||||
+ "JSON: "
|
||||
+ response.body(),
|
||||
e
|
||||
);
|
||||
}
|
||||
if (root == null || !root.isObject()) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses response was not a JSON "
|
||||
+ "object: "
|
||||
+ response.body()
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode errorNode = root.get("error");
|
||||
if (errorNode != null && !errorNode.isNull()) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses RPC error: "
|
||||
+ errorNode.toString()
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode resultNode = root.get("result");
|
||||
JsonNode valueNode = resultNode == null
|
||||
? null
|
||||
: resultNode.get("value");
|
||||
if (valueNode == null || !valueNode.isArray()) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses response did not contain a "
|
||||
+ "result.value array: "
|
||||
+ response.body()
|
||||
);
|
||||
}
|
||||
if (valueNode.size() != 1) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses result.value must contain "
|
||||
+ "exactly one element, but contained "
|
||||
+ valueNode.size()
|
||||
+ ": "
|
||||
+ response.body()
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode statusNode = valueNode.get(0);
|
||||
if (statusNode == null || statusNode.isNull()) {
|
||||
return null;
|
||||
}
|
||||
if (!statusNode.isObject()) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses result contained a non-object "
|
||||
+ "status: "
|
||||
+ statusNode
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode slotNode = statusNode.get("slot");
|
||||
if (slotNode == null
|
||||
|| !slotNode.isIntegralNumber()
|
||||
|| !slotNode.canConvertToLong()
|
||||
|| slotNode.longValue() < 0) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses status contained an invalid "
|
||||
+ "slot: "
|
||||
+ slotNode
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode confirmationStatusNode = statusNode.get(
|
||||
"confirmationStatus"
|
||||
);
|
||||
if (confirmationStatusNode == null
|
||||
|| !confirmationStatusNode.isTextual()) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses status did not contain a "
|
||||
+ "textual confirmationStatus: "
|
||||
+ statusNode
|
||||
);
|
||||
}
|
||||
SolanaCommitment observedCommitment = parseCommitment(
|
||||
confirmationStatusNode.asText()
|
||||
);
|
||||
|
||||
if (!statusNode.has("err")) {
|
||||
throw new IOException(
|
||||
"Solana getSignatureStatuses status did not contain err: "
|
||||
+ statusNode
|
||||
);
|
||||
}
|
||||
if (observedCommitment.ordinal() < requestedCommitment.ordinal()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ΩSolanaSlotΩ slot = slotNode.longValue();
|
||||
JsonNode transactionError = statusNode.get("err");
|
||||
if (transactionError == null || transactionError.isNull()) {
|
||||
return new SolanaTransactionOutcome(
|
||||
SolanaTransactionOutcome.Status.SUCCEEDED,
|
||||
slot,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
return new SolanaTransactionOutcome(
|
||||
SolanaTransactionOutcome.Status.FAILED,
|
||||
slot,
|
||||
transactionError.toString()
|
||||
);
|
||||
}
|
||||
|
||||
private SolanaCommitment parseCommitment(String confirmationStatus)
|
||||
throws IOException {
|
||||
return switch (confirmationStatus) {
|
||||
case "processed" -> SolanaCommitment.PROCESSED;
|
||||
case "confirmed" -> SolanaCommitment.CONFIRMED;
|
||||
case "finalized" -> SolanaCommitment.FINALIZED;
|
||||
default -> throw new IOException(
|
||||
"Solana getSignatureStatuses status contained unknown "
|
||||
+ "confirmationStatus: "
|
||||
+ confirmationStatus
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private static SolanaTransactionOutcome timedOutTransactionOutcome() {
|
||||
return new SolanaTransactionOutcome(
|
||||
SolanaTransactionOutcome.Status.TIMED_OUT,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private String createSendTransactionBody(
|
||||
SolanaSignedTransaction transaction
|
||||
) throws IOException {
|
||||
@@ -1091,7 +1344,10 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
|
||||
""".formatted(programId, filtersJson);
|
||||
}
|
||||
|
||||
private synchronized HttpResponse<String> sendThrottled(HttpRequest request) throws IOException, InterruptedException {
|
||||
private HttpResponse<String> sendThrottled(HttpRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
rpcGate.lockInterruptibly();
|
||||
try {
|
||||
waitBeforeRemoteCall();
|
||||
|
||||
try {
|
||||
@@ -1100,20 +1356,86 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
|
||||
HttpResponse.BodyHandlers.ofString()
|
||||
);
|
||||
} finally {
|
||||
previousRemoteCallTime = System.currentTimeMillis();
|
||||
previousRemoteCallTimeNanos = System.nanoTime();
|
||||
hasCompletedRemoteCall = true;
|
||||
}
|
||||
} finally {
|
||||
rpcGate.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void waitBeforeRemoteCall() throws InterruptedException {
|
||||
long now = System.currentTimeMillis();
|
||||
long elapsed = now - previousRemoteCallTime;
|
||||
|
||||
if (elapsed < MINIMUM_REMOTE_CALL_INTERVAL) {
|
||||
ΩmilliSecondsΩ sleepTime = MINIMUM_REMOTE_CALL_INTERVAL - elapsed;
|
||||
//System.out.println("Throttling Solana request for " + sleepTime + "ms...");
|
||||
Thread.sleep(sleepTime);
|
||||
//System.out.println("Ready");
|
||||
while (hasCompletedRemoteCall) {
|
||||
long elapsed = System.nanoTime() - previousRemoteCallTimeNanos;
|
||||
long remaining = MINIMUM_REMOTE_CALL_INTERVAL_NANOS - elapsed;
|
||||
if (remaining <= 0) {
|
||||
return;
|
||||
}
|
||||
TimeUnit.NANOSECONDS.sleep(remaining);
|
||||
}
|
||||
}
|
||||
|
||||
private HttpResponse<String> sendThrottled(
|
||||
String requestBody,
|
||||
Deadline deadline
|
||||
) throws IOException, InterruptedException {
|
||||
long remaining = deadline.remainingNanos();
|
||||
if (remaining <= 0
|
||||
|| !rpcGate.tryLock(remaining, TimeUnit.NANOSECONDS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean requestStarted = false;
|
||||
try {
|
||||
if (!waitBeforeRemoteCall(deadline)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
remaining = deadline.remainingNanos();
|
||||
if (remaining <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(RPC_URL))
|
||||
.timeout(Duration.ofNanos(remaining))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||
.build();
|
||||
|
||||
requestStarted = true;
|
||||
return httpClient.send(
|
||||
request,
|
||||
HttpResponse.BodyHandlers.ofString()
|
||||
);
|
||||
} finally {
|
||||
if (requestStarted) {
|
||||
previousRemoteCallTimeNanos = System.nanoTime();
|
||||
hasCompletedRemoteCall = true;
|
||||
}
|
||||
rpcGate.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean waitBeforeRemoteCall(Deadline deadline)
|
||||
throws InterruptedException {
|
||||
while (hasCompletedRemoteCall) {
|
||||
long elapsed = System.nanoTime() - previousRemoteCallTimeNanos;
|
||||
long throttleRemaining = MINIMUM_REMOTE_CALL_INTERVAL_NANOS
|
||||
- elapsed;
|
||||
if (throttleRemaining <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
long deadlineRemaining = deadline.remainingNanos();
|
||||
if (deadlineRemaining <= 0) {
|
||||
return false;
|
||||
}
|
||||
TimeUnit.NANOSECONDS.sleep(
|
||||
Math.min(throttleRemaining, deadlineRemaining)
|
||||
);
|
||||
}
|
||||
return !deadline.hasExpired();
|
||||
}
|
||||
|
||||
private boolean isSolanaNFTCandidate(SPLTokenHolding tokenHolding) {
|
||||
@@ -1389,6 +1711,33 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
|
||||
) {
|
||||
}
|
||||
|
||||
private record Deadline(long startedNanos, long timeoutNanos) {
|
||||
private static Deadline startingAt(
|
||||
long startedNanos,
|
||||
Duration timeout
|
||||
) {
|
||||
long timeoutNanos;
|
||||
try {
|
||||
timeoutNanos = timeout.toNanos();
|
||||
} catch (ArithmeticException e) {
|
||||
timeoutNanos = Long.MAX_VALUE;
|
||||
}
|
||||
return new Deadline(startedNanos, timeoutNanos);
|
||||
}
|
||||
|
||||
private long remainingNanos() {
|
||||
long elapsed = System.nanoTime() - startedNanos;
|
||||
if (elapsed < 0 || elapsed >= timeoutNanos) {
|
||||
return 0;
|
||||
}
|
||||
return timeoutNanos - elapsed;
|
||||
}
|
||||
|
||||
private boolean hasExpired() {
|
||||
return remainingNanos() <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String SYSTEM_PROGRAM_ADDRESS =
|
||||
"11111111111111111111111111111111";
|
||||
private static final String ASSOCIATED_TOKEN_PROGRAM_ADDRESS =
|
||||
@@ -1398,7 +1747,8 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
|
||||
private static final int SOLANA_ADDRESS_LENGTH = 32;
|
||||
private static final int SOLANA_SIGNATURE_LENGTH = 64;
|
||||
private static final ΩAmountΩ LAMPORTS_PER_SOL = new BigDecimal("1000000000");
|
||||
private static final ΩmilliSecondsΩ MINIMUM_REMOTE_CALL_INTERVAL = 5000L;
|
||||
private static final long MINIMUM_REMOTE_CALL_INTERVAL_NANOS =
|
||||
TimeUnit.SECONDS.toNanos(5);
|
||||
private static final byte[] PROGRAM_DERIVED_ADDRESS_MARKER = "ProgramDerivedAddress".getBytes(StandardCharsets.UTF_8);
|
||||
private static final String BASE58_ALPHABET_STRING = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
private static final char[] BASE58_ALPHABET = BASE58_ALPHABET_STRING.toCharArray();
|
||||
@@ -1411,5 +1761,7 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
|
||||
private final CurrencyIdentityService currencyIdentityService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final HttpClient httpClient;
|
||||
private ΩmilliSecondsΩ previousRemoteCallTime = 0L;
|
||||
private final ReentrantLock rpcGate = new ReentrantLock();
|
||||
private long previousRemoteCallTimeNanos;
|
||||
private boolean hasCompletedRemoteCall;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user