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.
|
||||
Reference in New Issue
Block a user