SolanaBlockChain.sendTransaction() currently submits a signed transaction and returns the signature accepted by the Solana RPC server.
However, a successful response from sendTransaction() only means that the RPC server has received the transaction. It does not necessarily mean that the transaction has been processed, confirmed, or finalized on-chain.
We therefore need a generic blocking operation that can subsequently wait for the outcome of an already submitted transaction.
This functionality will be needed by, among others:
the upcoming SPL token burn functionality, which must be able to wait for FINALIZED
the upcoming EvelynBurnerService, which must not send a Discord notification until the transaction is FINALIZED
JupiterSwapService, which may later use CONFIRMED as an independent on-chain confirmation
Jupiter Perps increase/decrease operations, which may later wait for CONFIRMED inside the Perps service
other callers that choose to wait after an ordinary wallet transfer
The starting point for this task is commit:
e7f551b8dd4cfe364a15a13956336cfea98ad7c8
Objective
Extend:
com.r35157.libs.solana.SolanaBlockChain
with a generic method that polls Solana's getSignatureStatuses RPC method until the transaction:
succeeds at the requested commitment level
fails at the requested commitment level
or the specified timeout expires
sendTransaction() and awaitTransaction() must remain separate operations:
sendTransaction()
-> submits the transaction and returns its signature
awaitTransaction()
-> subsequently waits for the requested on-chain outcome
A combined sendAndAwaitTransaction() method must not be added as part of this task.
The method and the new public types must include comprehensive Javadoc, including the important meaning of TIMED_OUT: the transaction outcome remains unknown, and the transaction must not automatically be resubmitted.
The record constructor must enforce the following invariants:
Status
slot
failureDetails
SUCCEEDED
Must contain the transaction slot and must not be null
Must be null
FAILED
Must contain the transaction slot and must not be null
Must contain Solana's err value as compact JSON and must not be blank
TIMED_OUT
Must be null
Must be null
If the method times out, it must not return a slot that may have been observed earlier at a lower commitment level. The result describes the outcome at the commitment level requested by the caller.
New ValueTag
Add the following ValueTag below Long in conf/detag.conf:
Long
SolanaSlot
This generates:
ΩSolanaSlotΩ
The backing type must be Long, allowing slot to be null for a TIMED_OUT outcome.
SolanaSlot is a distinct domain concept and must not be replaced with another existing Long-based ValueTag.
Commitment and outcome semantics
SUCCEEDED means:
the transaction was found
it reached or exceeded the requested commitment level
Solana returned err: null
FAILED means:
the transaction was found
it reached or exceeded the requested commitment level
Solana returned an execution error in err
TIMED_OUT means:
the requested definitive outcome was not known before the deadline
the transaction may still later be confirmed, finalized, or dropped
a timeout must not be interpreted as a rejected or failed transaction
the caller must therefore not automatically submit an equivalent transaction again
Both successful and failed transactions must reach the commitment level requested by the caller before a definitive outcome is returned.
Examples:
Observed status
Requested commitment
err
Action
processed
PROCESSED
null
Return SUCCEEDED
confirmed
PROCESSED
null
Return SUCCEEDED
processed
CONFIRMED
null
Continue polling
confirmed
CONFIRMED
null
Return SUCCEEDED
confirmed
FINALIZED
null
Continue polling
finalized
FINALIZED
null
Return SUCCEEDED
processed
FINALIZED
execution error
Continue polling
finalized
FINALIZED
execution error
Return FAILED
An execution error observed at PROCESSED must therefore not be returned as FAILED if the caller requested FINALIZED. The block containing the failed transaction must first reach the requested commitment level.
RPC implementation
The first version must use polling through Solana's HTTP JSON-RPC method:
getSignatureStatuses
WebSocket support and signatureSubscribe are explicitly outside the scope of this task.
searchTransactionHistory must be true because awaitTransaction() accepts an existing signature and must also be able to locate transactions that are no longer available in the RPC server's recent status cache.
result.context and the confirmations and status fields must not be used.
Interpreting the RPC response
If:
result.value[0] == null
the signature has not yet been found. Polling must continue until the deadline expires.
If a status object is present:
slot must be a valid integer representable as a Java Long.
confirmationStatus must be a string containing one of the following values:
processed
confirmed
finalized
The observed commitment level must be compared with the requested commitment level.
Polling must continue if the requested commitment level has not yet been reached.
Once the requested commitment level has been reached or exceeded:
err == null results in SUCCEEDED
err != null results in FAILED
For FAILED, the complete err value must be stored as compact JSON in failureDetails. This generic blockchain class must not attempt to interpret domain-specific error codes from individual Solana programs.
A missing or unknown confirmationStatus must be treated as an invalid RPC response and result in an IOException.
Polling and existing RPC throttling
SolanaBlockChainImpl already contains a shared, synchronized sendThrottled() mechanism that ensures at least five seconds between Solana RPC calls.
awaitTransaction() must use the existing shared throttling mechanism. It must not introduce:
a separate polling interval
a separate rate limiter
an additional Thread.sleep() after each status request
The first status request must be performed as soon as the shared throttling mechanism permits. After an inconclusive response, the next iteration will naturally be limited by the existing five-second throttling.
This also means that other concurrent RPC operations using the same SolanaBlockChainImpl may delay the status checks. This waiting time is part of the awaitTransaction() timeout.
Timeout and deadline
The timeout must define one overall deadline for the entire method call. It includes:
waiting for access to the shared RPC gate
any remaining throttling delay
the HTTP request itself
all subsequent status checks
The deadline must be measured using a monotonic time source, such as System.nanoTime(), so changes to the system clock cannot affect the timeout.
The implementation must not start a new RPC request after the deadline has been reached.
If the timeout expires before the next status request can be performed, return:
If the remaining time is shorter than the normal throttling delay, the implementation must wait only until the deadline and must not subsequently perform the RPC request.
An ongoing HTTP request must, as far as reasonably possible, be limited by the remaining time before the overall deadline. If this deadline expires during the status request, the result is TIMED_OUT, not an ordinary communication failure.
Interruptible and deadline-aware RPC gate
The current synchronized sendThrottled() implementation makes monitor acquisition non-interruptible and may block beyond the awaitTransaction() deadline if another thread is performing an RPC request.
The internal throttling mechanism or RPC gate must therefore be adjusted so awaitTransaction() can:
be interrupted while waiting for access to the RPC gate
stop waiting when its deadline is reached
avoid starting an RPC request after the deadline
The implementation may, for example, use an interruptible and time-bounded locking mechanism, but the specific internal solution is not part of the public API.
Existing Solana RPC operations must preserve their current five-second throttling and public behavior.
Argument validation
The following must be validated before the first RPC request:
signature must not be null or blank
the signature must be valid Base58
the decoded signature must be exactly 64 bytes
commitment must not be null
timeout must not be null
timeout must be strictly greater than zero
Invalid arguments must be rejected locally and must not cause an RPC request.
The project's existing conventions may be used when choosing between NullPointerException and IllegalArgumentException, but the behavior must be documented consistently in the API Javadoc.
Error model
FAILED must only represent an on-chain execution error reported in:
result.value[0].err
The following conditions must result in an IOException:
a non-success HTTP status
a JSON-RPC error
invalid JSON
missing or invalid result.value
a result.value array containing anything other than exactly one element
a non-null status object without a valid slot
a missing or unknown confirmationStatus
any other network or communication failure
Error messages must mention getSignatureStatuses and include relevant HTTP, RPC, or response context.
The method must not automatically retry after an IOException. A caller can safely invoke awaitTransaction() again with the same signature because doing so does not resubmit the transaction.
The following conditions are not FAILED:
a signature that the RPC server cannot find yet
a dropped transaction
a transaction that may have expired
a transaction that does not reach the requested commitment level before the timeout
a communication failure
Using only the transaction signature, the method cannot prove whether an unseen transaction never existed, may still land, was dropped, or expired because of its blockhash. If no communication failure occurs, these situations must therefore result in TIMED_OUT.
Every call must be delegated directly to the underlying implementation.
Tests and verification
Add focused automated tests covering at least the following cases:
PROCESSED succeeds when PROCESSED is requested
CONFIRMED satisfies a requested PROCESSED
PROCESSED continues polling when CONFIRMED is requested
CONFIRMED continues polling when FINALIZED is requested
FINALIZED succeeds when FINALIZED is requested
an execution error returns FAILED with the slot and compact err JSON
an execution error at a lower commitment level continues polling until the requested commitment level is reached
result.value[0] == null continues polling
a timeout returns the required null fields
the deadline can expire while waiting for RPC throttling
interruption propagates without a subsequent RPC request
a JSON-RPC error results in IOException
a non-success HTTP status results in IOException
a malformed or incomplete response results in IOException
an unknown confirmationStatus results in IOException
an invalid signature or timeout is rejected without an RPC request
CachedSolanaBlockChain delegates every call without caching
The tests must not contact a real Solana RPC server or wait for five actual seconds. If necessary, introduce a narrow internal test seam for transport, time, and throttling without expanding the public API.
The normal Gradle build and all relevant tests must pass.
Scope
This task includes only:
SolanaCommitment
SolanaTransactionOutcome
the ΩSolanaSlotΩ ValueTag
SolanaBlockChain.awaitTransaction()
the polling implementation in SolanaBlockChainImpl
the necessary deadline-aware adjustment of the existing internal RPC throttling
direct delegation in CachedSolanaBlockChain
relevant tests and documentation
This task must not modify:
SolanaBlockChain.sendTransaction()
SolanaWallet or wallet transfers
JupiterSwapService
Jupiter Perps increase/decrease operations
SPL token burn functionality
EvelynBurnerService
Discord integration
existing domain-specific validation of Jupiter responses or actual token amounts
These components will become separate consumers of the new API in subsequent tasks.
Acceptance criteria
SolanaBlockChain exposes the described awaitTransaction() API.
All three commitment levels are supported.
Both successful transactions and execution failures must reach the requested commitment level.
Polling uses getSignatureStatuses.
searchTransactionHistory is always true.
The existing five-second Solana RPC throttling is reused.
The overall timeout includes the RPC gate, throttling, and HTTP requests.
Waiting for the RPC gate can both be interrupted and terminated by the deadline.
SUCCEEDED, FAILED, and TIMED_OUT follow the described invariants.
FAILED is used only for Solana's on-chain err.
TIMED_OUT represents an unknown outcome and does not cause resubmission.
Communication and protocol errors result in IOException.
Interruption propagates as InterruptedException.
CachedSolanaBlockChain delegates without caching.
No existing caller is changed to wait automatically.
No WebSocket or automatic retry mechanism is added.
## Background
`SolanaBlockChain.sendTransaction()` currently submits a signed transaction and returns the signature accepted by the Solana RPC server.
However, a successful response from `sendTransaction()` only means that the RPC server has received the transaction. It does not necessarily mean that the transaction has been processed, confirmed, or finalized on-chain.
We therefore need a generic blocking operation that can subsequently wait for the outcome of an already submitted transaction.
This functionality will be needed by, among others:
- the upcoming SPL token burn functionality, which must be able to wait for `FINALIZED`
- the upcoming `EvelynBurnerService`, which must not send a Discord notification until the transaction is `FINALIZED`
- `JupiterSwapService`, which may later use `CONFIRMED` as an independent on-chain confirmation
- Jupiter Perps increase/decrease operations, which may later wait for `CONFIRMED` inside the Perps service
- other callers that choose to wait after an ordinary wallet transfer
The starting point for this task is commit:
```text
e7f551b8dd4cfe364a15a13956336cfea98ad7c8
```
## Objective
Extend:
```text
com.r35157.libs.solana.SolanaBlockChain
```
with a generic method that polls Solana's `getSignatureStatuses` RPC method until the transaction:
- succeeds at the requested commitment level
- fails at the requested commitment level
- or the specified timeout expires
`sendTransaction()` and `awaitTransaction()` must remain separate operations:
```text
sendTransaction()
-> submits the transaction and returns its signature
awaitTransaction()
-> subsequently waits for the requested on-chain outcome
```
A combined `sendAndAwaitTransaction()` method must not be added as part of this task.
## Public API
Add the following method to `SolanaBlockChain`:
```java
SolanaTransactionOutcome awaitTransaction(
ΩSolanaTransactionSignatureΩ signature,
SolanaCommitment commitment,
Duration timeout
) throws IOException, InterruptedException;
```
The method and the new public types must include comprehensive Javadoc, including the important meaning of `TIMED_OUT`: the transaction outcome remains unknown, and the transaction must not automatically be resubmitted.
### `SolanaCommitment`
Add the following public enum in:
```text
com.r35157.libs.solana
```
```java
public enum SolanaCommitment {
PROCESSED,
CONFIRMED,
FINALIZED
}
```
The commitment levels are ordered as follows:
```text
PROCESSED < CONFIRMED < FINALIZED
```
An observed commitment level therefore also satisfies any lower requested commitment level.
### `SolanaTransactionOutcome`
Add the following public record in:
```text
com.r35157.libs.solana
```
```java
public record SolanaTransactionOutcome(
Status status,
ΩSolanaSlotΩ slot,
String failureDetails
) {
public enum Status {
SUCCEEDED,
FAILED,
TIMED_OUT
}
}
```
The record constructor must enforce the following invariants:
| Status | `slot` | `failureDetails` |
|---|---|---|
| `SUCCEEDED` | Must contain the transaction slot and must not be `null` | Must be `null` |
| `FAILED` | Must contain the transaction slot and must not be `null` | Must contain Solana's `err` value as compact JSON and must not be blank |
| `TIMED_OUT` | Must be `null` | Must be `null` |
If the method times out, it must not return a slot that may have been observed earlier at a lower commitment level. The result describes the outcome at the commitment level requested by the caller.
## New ValueTag
Add the following ValueTag below `Long` in `conf/detag.conf`:
```text
Long
SolanaSlot
```
This generates:
```java
ΩSolanaSlotΩ
```
The backing type must be `Long`, allowing `slot` to be `null` for a `TIMED_OUT` outcome.
`SolanaSlot` is a distinct domain concept and must not be replaced with another existing `Long`-based ValueTag.
## Commitment and outcome semantics
`SUCCEEDED` means:
- the transaction was found
- it reached or exceeded the requested commitment level
- Solana returned `err: null`
`FAILED` means:
- the transaction was found
- it reached or exceeded the requested commitment level
- Solana returned an execution error in `err`
`TIMED_OUT` means:
- the requested definitive outcome was not known before the deadline
- the transaction may still later be confirmed, finalized, or dropped
- a timeout must not be interpreted as a rejected or failed transaction
- the caller must therefore not automatically submit an equivalent transaction again
Both successful and failed transactions must reach the commitment level requested by the caller before a definitive outcome is returned.
Examples:
| Observed status | Requested commitment | `err` | Action |
|---|---|---|---|
| `processed` | `PROCESSED` | `null` | Return `SUCCEEDED` |
| `confirmed` | `PROCESSED` | `null` | Return `SUCCEEDED` |
| `processed` | `CONFIRMED` | `null` | Continue polling |
| `confirmed` | `CONFIRMED` | `null` | Return `SUCCEEDED` |
| `confirmed` | `FINALIZED` | `null` | Continue polling |
| `finalized` | `FINALIZED` | `null` | Return `SUCCEEDED` |
| `processed` | `FINALIZED` | execution error | Continue polling |
| `finalized` | `FINALIZED` | execution error | Return `FAILED` |
An execution error observed at `PROCESSED` must therefore not be returned as `FAILED` if the caller requested `FINALIZED`. The block containing the failed transaction must first reach the requested commitment level.
## RPC implementation
The first version must use polling through Solana's HTTP JSON-RPC method:
```text
getSignatureStatuses
```
WebSocket support and `signatureSubscribe` are explicitly outside the scope of this task.
Each status request must be equivalent to:
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "getSignatureStatuses",
"params": [
[
"<signature>"
],
{
"searchTransactionHistory": true
}
]
}
```
`searchTransactionHistory` must be `true` because `awaitTransaction()` accepts an existing signature and must also be able to locate transactions that are no longer available in the RPC server's recent status cache.
The following fields must be read from:
```text
result.value[0].slot
result.value[0].err
result.value[0].confirmationStatus
```
`result.context` and the `confirmations` and `status` fields must not be used.
### Interpreting the RPC response
If:
```text
result.value[0] == null
```
the signature has not yet been found. Polling must continue until the deadline expires.
If a status object is present:
1. `slot` must be a valid integer representable as a Java `Long`.
2. `confirmationStatus` must be a string containing one of the following values:
```text
processed
confirmed
finalized
```
3. The observed commitment level must be compared with the requested commitment level.
4. Polling must continue if the requested commitment level has not yet been reached.
5. Once the requested commitment level has been reached or exceeded:
- `err == null` results in `SUCCEEDED`
- `err != null` results in `FAILED`
For `FAILED`, the complete `err` value must be stored as compact JSON in `failureDetails`. This generic blockchain class must not attempt to interpret domain-specific error codes from individual Solana programs.
A missing or unknown `confirmationStatus` must be treated as an invalid RPC response and result in an `IOException`.
## Polling and existing RPC throttling
`SolanaBlockChainImpl` already contains a shared, synchronized `sendThrottled()` mechanism that ensures at least five seconds between Solana RPC calls.
`awaitTransaction()` must use the existing shared throttling mechanism. It must not introduce:
- a separate polling interval
- a separate rate limiter
- an additional `Thread.sleep()` after each status request
The first status request must be performed as soon as the shared throttling mechanism permits. After an inconclusive response, the next iteration will naturally be limited by the existing five-second throttling.
This also means that other concurrent RPC operations using the same `SolanaBlockChainImpl` may delay the status checks. This waiting time is part of the `awaitTransaction()` timeout.
## Timeout and deadline
The timeout must define one overall deadline for the entire method call. It includes:
- waiting for access to the shared RPC gate
- any remaining throttling delay
- the HTTP request itself
- all subsequent status checks
The deadline must be measured using a monotonic time source, such as `System.nanoTime()`, so changes to the system clock cannot affect the timeout.
The implementation must not start a new RPC request after the deadline has been reached.
If the timeout expires before the next status request can be performed, return:
```java
new SolanaTransactionOutcome(
SolanaTransactionOutcome.Status.TIMED_OUT,
null,
null
)
```
If the remaining time is shorter than the normal throttling delay, the implementation must wait only until the deadline and must not subsequently perform the RPC request.
An ongoing HTTP request must, as far as reasonably possible, be limited by the remaining time before the overall deadline. If this deadline expires during the status request, the result is `TIMED_OUT`, not an ordinary communication failure.
### Interruptible and deadline-aware RPC gate
The current synchronized `sendThrottled()` implementation makes monitor acquisition non-interruptible and may block beyond the `awaitTransaction()` deadline if another thread is performing an RPC request.
The internal throttling mechanism or RPC gate must therefore be adjusted so `awaitTransaction()` can:
- be interrupted while waiting for access to the RPC gate
- stop waiting when its deadline is reached
- avoid starting an RPC request after the deadline
The implementation may, for example, use an interruptible and time-bounded locking mechanism, but the specific internal solution is not part of the public API.
Existing Solana RPC operations must preserve their current five-second throttling and public behavior.
## Argument validation
The following must be validated before the first RPC request:
- `signature` must not be `null` or blank
- the signature must be valid Base58
- the decoded signature must be exactly 64 bytes
- `commitment` must not be `null`
- `timeout` must not be `null`
- `timeout` must be strictly greater than zero
Invalid arguments must be rejected locally and must not cause an RPC request.
The project's existing conventions may be used when choosing between `NullPointerException` and `IllegalArgumentException`, but the behavior must be documented consistently in the API Javadoc.
## Error model
`FAILED` must only represent an on-chain execution error reported in:
```text
result.value[0].err
```
The following conditions must result in an `IOException`:
- a non-success HTTP status
- a JSON-RPC `error`
- invalid JSON
- missing or invalid `result.value`
- a `result.value` array containing anything other than exactly one element
- a non-null status object without a valid `slot`
- a missing or unknown `confirmationStatus`
- any other network or communication failure
Error messages must mention `getSignatureStatuses` and include relevant HTTP, RPC, or response context.
The method must not automatically retry after an `IOException`. A caller can safely invoke `awaitTransaction()` again with the same signature because doing so does not resubmit the transaction.
The following conditions are not `FAILED`:
- a signature that the RPC server cannot find yet
- a dropped transaction
- a transaction that may have expired
- a transaction that does not reach the requested commitment level before the timeout
- a communication failure
Using only the transaction signature, the method cannot prove whether an unseen transaction never existed, may still land, was dropped, or expired because of its blockhash. If no communication failure occurs, these situations must therefore result in `TIMED_OUT`.
## `InterruptedException`
If the calling Java thread is interrupted while:
- waiting for access to the RPC gate
- waiting for throttling
- performing the HTTP request
- performing any other internal wait
the `InterruptedException` must propagate directly.
An interruption must not:
- be converted into `TIMED_OUT`
- be wrapped in an `IOException`
- be ignored
- result in a subsequent RPC request
## `CachedSolanaBlockChain`
Add the same method to:
```text
com.r35157.libs.solana.impl.cached.CachedSolanaBlockChain
```
The implementation must delegate directly:
```java
@Override
public SolanaTransactionOutcome awaitTransaction(
ΩSolanaTransactionSignatureΩ signature,
SolanaCommitment commitment,
Duration timeout
) throws IOException, InterruptedException {
return delegate.awaitTransaction(signature, commitment, timeout);
}
```
The outcome must not be:
- cached
- reused between calls
- deduplicated
- determined from previously cached blockchain data
Every call must be delegated directly to the underlying implementation.
## Tests and verification
Add focused automated tests covering at least the following cases:
- `PROCESSED` succeeds when `PROCESSED` is requested
- `CONFIRMED` satisfies a requested `PROCESSED`
- `PROCESSED` continues polling when `CONFIRMED` is requested
- `CONFIRMED` continues polling when `FINALIZED` is requested
- `FINALIZED` succeeds when `FINALIZED` is requested
- an execution error returns `FAILED` with the slot and compact `err` JSON
- an execution error at a lower commitment level continues polling until the requested commitment level is reached
- `result.value[0] == null` continues polling
- a timeout returns the required `null` fields
- the deadline can expire while waiting for RPC throttling
- interruption propagates without a subsequent RPC request
- a JSON-RPC error results in `IOException`
- a non-success HTTP status results in `IOException`
- a malformed or incomplete response results in `IOException`
- an unknown `confirmationStatus` results in `IOException`
- an invalid signature or timeout is rejected without an RPC request
- `CachedSolanaBlockChain` delegates every call without caching
The tests must not contact a real Solana RPC server or wait for five actual seconds. If necessary, introduce a narrow internal test seam for transport, time, and throttling without expanding the public API.
The normal Gradle build and all relevant tests must pass.
## Scope
This task includes only:
- `SolanaCommitment`
- `SolanaTransactionOutcome`
- the `ΩSolanaSlotΩ` ValueTag
- `SolanaBlockChain.awaitTransaction()`
- the polling implementation in `SolanaBlockChainImpl`
- the necessary deadline-aware adjustment of the existing internal RPC throttling
- direct delegation in `CachedSolanaBlockChain`
- relevant tests and documentation
This task must not modify:
- `SolanaBlockChain.sendTransaction()`
- `SolanaWallet` or wallet transfers
- `JupiterSwapService`
- Jupiter Perps increase/decrease operations
- SPL token burn functionality
- `EvelynBurnerService`
- Discord integration
- existing domain-specific validation of Jupiter responses or actual token amounts
These components will become separate consumers of the new API in subsequent tasks.
## Acceptance criteria
- [ ] `SolanaBlockChain` exposes the described `awaitTransaction()` API.
- [ ] All three commitment levels are supported.
- [ ] Both successful transactions and execution failures must reach the requested commitment level.
- [ ] Polling uses `getSignatureStatuses`.
- [ ] `searchTransactionHistory` is always `true`.
- [ ] The existing five-second Solana RPC throttling is reused.
- [ ] The overall timeout includes the RPC gate, throttling, and HTTP requests.
- [ ] Waiting for the RPC gate can both be interrupted and terminated by the deadline.
- [ ] `SUCCEEDED`, `FAILED`, and `TIMED_OUT` follow the described invariants.
- [ ] `FAILED` is used only for Solana's on-chain `err`.
- [ ] `TIMED_OUT` represents an unknown outcome and does not cause resubmission.
- [ ] Communication and protocol errors result in `IOException`.
- [ ] Interruption propagates as `InterruptedException`.
- [ ] `CachedSolanaBlockChain` delegates without caching.
- [ ] No existing caller is changed to wait automatically.
- [ ] No WebSocket or automatic retry mechanism is added.
- [ ] The Gradle build and relevant tests pass.
## References
- Solana `sendTransaction`: https://solana.com/docs/rpc/http/sendtransaction
- Solana `getSignatureStatuses`: https://solana.com/docs/rpc/http/getsignaturestatuses
- Solana commitment levels: https://docs.anza.xyz/consensus/commitments
- Solana transaction confirmation and expiration: https://solana.com/developers/cookbook/transactions/confirmation
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Background
SolanaBlockChain.sendTransaction()currently submits a signed transaction and returns the signature accepted by the Solana RPC server.However, a successful response from
sendTransaction()only means that the RPC server has received the transaction. It does not necessarily mean that the transaction has been processed, confirmed, or finalized on-chain.We therefore need a generic blocking operation that can subsequently wait for the outcome of an already submitted transaction.
This functionality will be needed by, among others:
FINALIZEDEvelynBurnerService, which must not send a Discord notification until the transaction isFINALIZEDJupiterSwapService, which may later useCONFIRMEDas an independent on-chain confirmationCONFIRMEDinside the Perps serviceThe starting point for this task is commit:
Objective
Extend:
with a generic method that polls Solana's
getSignatureStatusesRPC method until the transaction:sendTransaction()andawaitTransaction()must remain separate operations:A combined
sendAndAwaitTransaction()method must not be added as part of this task.Public API
Add the following method to
SolanaBlockChain:The method and the new public types must include comprehensive Javadoc, including the important meaning of
TIMED_OUT: the transaction outcome remains unknown, and the transaction must not automatically be resubmitted.SolanaCommitmentAdd the following public enum in:
The commitment levels are ordered as follows:
An observed commitment level therefore also satisfies any lower requested commitment level.
SolanaTransactionOutcomeAdd the following public record in:
The record constructor must enforce the following invariants:
slotfailureDetailsSUCCEEDEDnullnullFAILEDnullerrvalue as compact JSON and must not be blankTIMED_OUTnullnullIf the method times out, it must not return a slot that may have been observed earlier at a lower commitment level. The result describes the outcome at the commitment level requested by the caller.
New ValueTag
Add the following ValueTag below
Longinconf/detag.conf:This generates:
The backing type must be
Long, allowingslotto benullfor aTIMED_OUToutcome.SolanaSlotis a distinct domain concept and must not be replaced with another existingLong-based ValueTag.Commitment and outcome semantics
SUCCEEDEDmeans:err: nullFAILEDmeans:errTIMED_OUTmeans:Both successful and failed transactions must reach the commitment level requested by the caller before a definitive outcome is returned.
Examples:
errprocessedPROCESSEDnullSUCCEEDEDconfirmedPROCESSEDnullSUCCEEDEDprocessedCONFIRMEDnullconfirmedCONFIRMEDnullSUCCEEDEDconfirmedFINALIZEDnullfinalizedFINALIZEDnullSUCCEEDEDprocessedFINALIZEDfinalizedFINALIZEDFAILEDAn execution error observed at
PROCESSEDmust therefore not be returned asFAILEDif the caller requestedFINALIZED. The block containing the failed transaction must first reach the requested commitment level.RPC implementation
The first version must use polling through Solana's HTTP JSON-RPC method:
WebSocket support and
signatureSubscribeare explicitly outside the scope of this task.Each status request must be equivalent to:
searchTransactionHistorymust betruebecauseawaitTransaction()accepts an existing signature and must also be able to locate transactions that are no longer available in the RPC server's recent status cache.The following fields must be read from:
result.contextand theconfirmationsandstatusfields must not be used.Interpreting the RPC response
If:
the signature has not yet been found. Polling must continue until the deadline expires.
If a status object is present:
slotmust be a valid integer representable as a JavaLong.confirmationStatusmust be a string containing one of the following values:The observed commitment level must be compared with the requested commitment level.
Polling must continue if the requested commitment level has not yet been reached.
Once the requested commitment level has been reached or exceeded:
err == nullresults inSUCCEEDEDerr != nullresults inFAILEDFor
FAILED, the completeerrvalue must be stored as compact JSON infailureDetails. This generic blockchain class must not attempt to interpret domain-specific error codes from individual Solana programs.A missing or unknown
confirmationStatusmust be treated as an invalid RPC response and result in anIOException.Polling and existing RPC throttling
SolanaBlockChainImplalready contains a shared, synchronizedsendThrottled()mechanism that ensures at least five seconds between Solana RPC calls.awaitTransaction()must use the existing shared throttling mechanism. It must not introduce:Thread.sleep()after each status requestThe first status request must be performed as soon as the shared throttling mechanism permits. After an inconclusive response, the next iteration will naturally be limited by the existing five-second throttling.
This also means that other concurrent RPC operations using the same
SolanaBlockChainImplmay delay the status checks. This waiting time is part of theawaitTransaction()timeout.Timeout and deadline
The timeout must define one overall deadline for the entire method call. It includes:
The deadline must be measured using a monotonic time source, such as
System.nanoTime(), so changes to the system clock cannot affect the timeout.The implementation must not start a new RPC request after the deadline has been reached.
If the timeout expires before the next status request can be performed, return:
If the remaining time is shorter than the normal throttling delay, the implementation must wait only until the deadline and must not subsequently perform the RPC request.
An ongoing HTTP request must, as far as reasonably possible, be limited by the remaining time before the overall deadline. If this deadline expires during the status request, the result is
TIMED_OUT, not an ordinary communication failure.Interruptible and deadline-aware RPC gate
The current synchronized
sendThrottled()implementation makes monitor acquisition non-interruptible and may block beyond theawaitTransaction()deadline if another thread is performing an RPC request.The internal throttling mechanism or RPC gate must therefore be adjusted so
awaitTransaction()can:The implementation may, for example, use an interruptible and time-bounded locking mechanism, but the specific internal solution is not part of the public API.
Existing Solana RPC operations must preserve their current five-second throttling and public behavior.
Argument validation
The following must be validated before the first RPC request:
signaturemust not benullor blankcommitmentmust not benulltimeoutmust not benulltimeoutmust be strictly greater than zeroInvalid arguments must be rejected locally and must not cause an RPC request.
The project's existing conventions may be used when choosing between
NullPointerExceptionandIllegalArgumentException, but the behavior must be documented consistently in the API Javadoc.Error model
FAILEDmust only represent an on-chain execution error reported in:The following conditions must result in an
IOException:errorresult.valueresult.valuearray containing anything other than exactly one elementslotconfirmationStatusError messages must mention
getSignatureStatusesand include relevant HTTP, RPC, or response context.The method must not automatically retry after an
IOException. A caller can safely invokeawaitTransaction()again with the same signature because doing so does not resubmit the transaction.The following conditions are not
FAILED:Using only the transaction signature, the method cannot prove whether an unseen transaction never existed, may still land, was dropped, or expired because of its blockhash. If no communication failure occurs, these situations must therefore result in
TIMED_OUT.InterruptedExceptionIf the calling Java thread is interrupted while:
the
InterruptedExceptionmust propagate directly.An interruption must not:
TIMED_OUTIOExceptionCachedSolanaBlockChainAdd the same method to:
The implementation must delegate directly:
The outcome must not be:
Every call must be delegated directly to the underlying implementation.
Tests and verification
Add focused automated tests covering at least the following cases:
PROCESSEDsucceeds whenPROCESSEDis requestedCONFIRMEDsatisfies a requestedPROCESSEDPROCESSEDcontinues polling whenCONFIRMEDis requestedCONFIRMEDcontinues polling whenFINALIZEDis requestedFINALIZEDsucceeds whenFINALIZEDis requestedFAILEDwith the slot and compacterrJSONresult.value[0] == nullcontinues pollingnullfieldsIOExceptionIOExceptionIOExceptionconfirmationStatusresults inIOExceptionCachedSolanaBlockChaindelegates every call without cachingThe tests must not contact a real Solana RPC server or wait for five actual seconds. If necessary, introduce a narrow internal test seam for transport, time, and throttling without expanding the public API.
The normal Gradle build and all relevant tests must pass.
Scope
This task includes only:
SolanaCommitmentSolanaTransactionOutcomeΩSolanaSlotΩValueTagSolanaBlockChain.awaitTransaction()SolanaBlockChainImplCachedSolanaBlockChainThis task must not modify:
SolanaBlockChain.sendTransaction()SolanaWalletor wallet transfersJupiterSwapServiceEvelynBurnerServiceThese components will become separate consumers of the new API in subsequent tasks.
Acceptance criteria
SolanaBlockChainexposes the describedawaitTransaction()API.getSignatureStatuses.searchTransactionHistoryis alwaystrue.SUCCEEDED,FAILED, andTIMED_OUTfollow the described invariants.FAILEDis used only for Solana's on-chainerr.TIMED_OUTrepresents an unknown outcome and does not cause resubmission.IOException.InterruptedException.CachedSolanaBlockChaindelegates without caching.References
sendTransaction: https://solana.com/docs/rpc/http/sendtransactiongetSignatureStatuses: https://solana.com/docs/rpc/http/getsignaturestatuses