SolanaBlockChain.sendTransaction(...) submits a complete signed transaction through Solana's standard JSON-RPC sendTransaction method and returns the transaction signature accepted by the RPC server.
A successful response only proves that the RPC server accepted the transaction for forwarding. It does not prove that the transaction was processed or confirmed on-chain, and transactions may be dropped before inclusion.
This operation can determine whether an already submitted transaction reaches a requested commitment level, fails on-chain or remains unknown when its timeout expires.
Issue #68 integrated that operation into Jupiter Swap and Jupiter Perps. Those services submit through Jupiter-managed execution endpoints and must never rebroadcast the returned transaction.
This issue now concerns the remaining direct Solana RPC use case: reliable submission of a signed transaction whose exact serialized bytes and blockhash-validity metadata are available to the caller.
Objective
Add a generic, expiry-aware transaction execution operation to SolanaBlockChain that:
submits a signed transaction directly through Solana RPC
monitors its signature using the functionality introduced in issue #67
safely rebroadcasts the exact same signed transaction while its blockhash remains valid
stops rebroadcasting when the transaction is observed on-chain or its blockhash expires
reports confirmed success, definitive rejection, proven expiration or an unknown timeout without losing the transaction signature
never rebuilds or re-signs the transaction automatically
The implementation must remain independent of Jupiter and work for arbitrary signed legacy or versioned Solana transactions.
Relationship to sendTransaction() and awaitTransaction()
The existing low-level operations must remain unchanged:
sendTransaction()
-> performs one submission and returns the RPC response signature
awaitTransaction()
-> monitors an already submitted signature without submitting anything
This issue adds a separate higher-level operation for callers that explicitly require expiry-aware direct RPC delivery.
The exact method name may be finalized during the OpenSpec design, but the intended API direction is:
The existing sendTransaction() and awaitTransaction() contracts must not be changed.
Preserve transaction-validity metadata
The current SolanaUnsignedTransaction contains:
the serialized transaction
its recent blockhash
its lastValidBlockHeight
However, SolanaWallet.signTransaction() currently returns a SolanaSignedTransaction containing only the signed serialized transaction. The validity metadata is therefore lost during signing.
Extend SolanaSignedTransaction so it also preserves:
SolanaWalletImpl.signTransaction() must copy the blockhash and lastValidBlockHeight unchanged from the supplied SolanaUnsignedTransaction into the resulting SolanaSignedTransaction.
Update all existing construction sites accordingly.
The following must be validated locally:
the serialized signed transaction is present and valid Base64
the blockhash is present and non-blank
lastValidBlockHeight is greater than zero
The reliable execution operation may trust that the preserved metadata originated from the unsigned transaction being signed. It does not need to implement a general-purpose Solana transaction parser solely to compare the stored blockhash with the serialized message.
Determine the signature before submission
The reliable flow must know the transaction signature even if the first sendTransaction request reaches the RPC server but its response is lost.
Before the first submission, extract the transaction ID from the first signature embedded in the serialized signed transaction.
The extracted signature must:
contain exactly 64 bytes before Base58 encoding
be available before the first RPC request
be used for all subsequent calls to awaitTransaction()
be stored in every result or structured timeout
be compared with every successful signature returned by sendTransaction
If an RPC response returns a different signature from the one embedded in the transaction, fail with an IOException containing both signatures and clear protocol context.
the requested commitment level was reached or exceeded
no recoverable submission, transport or observation warnings occurred
the confirmation slot is available
failure details are absent
CONFIRMED_WITH_WARNINGS
CONFIRMED_WITH_WARNINGS means:
awaitTransaction() ultimately returned SUCCEEDED
the requested commitment level was reached or exceeded
one or more earlier submission, transport or observation attempts encountered recoverable problems
the confirmation slot is available
the warnings list is non-empty
A warning must never cause an otherwise successful confirmed transaction to be reported as rejected or expired.
REJECTED
REJECTED means that the transaction was definitively rejected.
This includes:
awaitTransaction() returned FAILED at the requested commitment level
a structured preflight rejection proves that the transaction was not forwarded, provided that no earlier submission attempt may already have been accepted
For an on-chain rejection:
the confirmation slot must be available
the complete Solana err value must be preserved as compact JSON
For a definitive preflight rejection:
the complete JSON-RPC error must be preserved
the slot may be absent
A communication failure, lost response, malformed response or unknown timeout is not a definitive rejection.
Once any earlier submission attempt may have reached the RPC server, a later preflight rejection must not by itself cause REJECTED, because the earlier attempt may still land.
EXPIRED
EXPIRED means:
the RPC node has reported a current block height strictly greater than the transaction's lastValidBlockHeight
the transaction has not reached the requested commitment
a final status observation does not show that the transaction is still present at any commitment level
the transaction can no longer be newly accepted using its original blockhash
EXPIRED is different from a timeout. Expiration is a proven terminal state for the original signed transaction. A timeout leaves the outcome unknown.
The method must not automatically build or sign a replacement transaction after expiration. Any replacement is a separate caller decision.
Safe rebroadcast requirements
Every submission attempt must send the exact same Base64-encoded signed transaction bytes.
The implementation must never:
rebuild the transaction
obtain a new blockhash
modify instructions or fees
add or replace a signature
re-sign the transaction
submit an equivalent transaction with different serialized bytes
Rebroadcasting the exact same signed transaction preserves the same transaction ID and cannot create multiple independent executions.
Count every started sendTransaction RPC request as one submission attempt, including attempts whose response is lost or fails.
For the reliable execution path, use manual rebroadcast control:
maxRetries: 0 prevents hidden RPC-node retries from being layered underneath the explicit application-controlled rebroadcast loop.
The existing low-level sendTransaction() configuration and behavior must remain unchanged.
Do not add a separate simulateTransaction request. The existing sendTransaction preflight behavior is sufficient.
Reuse awaitTransaction()
The implementation must reuse the commitment and on-chain outcome semantics introduced in issue #67.
Handle awaitTransaction() results as follows:
awaitTransaction() outcome
Reliable execution behavior
SUCCEEDED
Return CONFIRMED or CONFIRMED_WITH_WARNINGS.
FAILED
Return REJECTED with the complete outcome details.
TIMED_OUT
Treat the observation as inconclusive; check transaction validity and, if still valid, rebroadcast the exact same signed transaction.
A TIMED_OUT result from an individual awaitTransaction() call must not be returned as EXPIRED without independent block-height evidence.
Each internal awaitTransaction() timeout must be bounded by the remaining overall execution deadline.
awaitTransaction() must remain the public source of commitment and definitive on-chain outcome semantics. A narrow private one-shot status observation may be added if required to avoid incorrectly reporting a lower-commitment transaction as expired, but no competing public transaction-status API should be introduced.
Expiration handling
Add the necessary internal Solana RPC support for obtaining the current block height.
Use Solana's getBlockHeight RPC method with the same confirmed commitment currently used by getLatestBlockhash().
The block-height response must be validated as a non-negative integer representable as a Java long.
Rebroadcasting is permitted only while:
currentBlockHeight <= lastValidBlockHeight
Once:
currentBlockHeight > lastValidBlockHeight
the implementation must stop submitting the transaction permanently.
Before returning EXPIRED, perform a final status check using searchTransactionHistory: true.
Do not report EXPIRED solely because the block height has advanced if the transaction is already visible at a lower commitment level. In that situation:
stop rebroadcasting
continue monitoring the existing signature
return success or rejection if it reaches the requested commitment
otherwise end with an unknown timeout if the overall deadline expires before a definitive outcome can be established
If a previously observed lower-commitment transaction subsequently disappears after its blockhash has expired, it may then be reported as EXPIRED.
Overall timeout
The Duration timeout parameter defines one overall monotonic deadline for the complete execution operation.
It includes:
local preparation and signature extraction
waiting for the shared RPC gate
existing five-second RPC throttling
every submission attempt
status polling through awaitTransaction()
block-height requests
all HTTP requests
all recoverable retry attempts
The implementation must use a monotonic time source such as System.nanoTime().
No new RPC request may start after the overall deadline.
If the deadline expires before success, definitive rejection or proven expiration can be established, throw:
SolanaTransactionExecutionTimeoutException
The checked exception must provide structured access to at least:
transaction signature
requested commitment
lastValidBlockHeight
most recently observed block height, if known
number of submission attempts
accumulated warnings
Its message and Javadoc must clearly state:
the transaction outcome remains unknown
the original transaction may still have landed
no equivalent transaction was automatically submitted
the caller must not automatically rebuild or re-sign the operation
A timeout must never be converted into REJECTED or EXPIRED without the required evidence.
Communication failures and warnings
An individual transport, HTTP or RPC communication failure does not prove that the transaction was rejected. The request may have reached the server even if the response was lost.
Recoverable failures may therefore be recorded as warnings while the method continues to:
monitor the locally extracted signature
check blockhash validity
rebroadcast only the exact same signed bytes
respect the overall deadline
If the transaction later succeeds, return CONFIRMED_WITH_WARNINGS.
If a non-recoverable communication or protocol error causes the method to throw IOException after a submission attempt has started:
the message must include the transaction signature
the message must state that the on-chain outcome may be unknown
the original exception must be preserved as the cause
no rebuilt or re-signed transaction may be submitted
Malformed arguments and invalid signed transaction data must fail before the first RPC request.
Interruption
If the calling thread is interrupted while:
waiting for the RPC gate
waiting for throttling
submitting
awaiting transaction status
checking block height
or performing any other internal wait
InterruptedException must propagate directly.
Interruption must not:
be converted into a warning
be converted into timeout or expiration
be wrapped in IOException
trigger another RPC request
trigger another submission attempt
Existing RPC throttling
All new Solana RPC calls must use the existing shared, interruptible five-second throttling mechanism in SolanaBlockChainImpl.
Do not introduce:
a second rate limiter
a busy loop
an uninterruptible sleep
concurrent background submission threads
RPC calls that bypass the existing gate
The implementation may coordinate status, submission and block-height calls within the shared throttle, but all waiting must remain interruptible and bounded by the overall deadline.
It must delegate directly to the underlying SolanaBlockChain.
Execution results, warnings and timeouts must not be:
cached
reused
deduplicated
inferred from cached blockchain data
Every invocation represents a distinct execution workflow, even though all rebroadcasts within one invocation use the same signed transaction.
Existing callers and services
This issue must not change the submission-only semantics of:
SolanaWallet.sendSolana()
SolanaWallet.sendAllSolana()
SolanaWallet.sendSPLToken()
SolanaWallet.burnSPLToken()
SolanaWallet.sendTransaction()
SolanaWallet.signAndSendTransaction()
These methods must continue to submit once and return the transaction signature without automatically waiting or rebroadcasting.
This issue must also not change:
JupiterSwapService
Jupiter Perps increase or decrease operations
JupiterTransactionOutcomeException
EvelynBurnerService
Discord integration
Jupiter operations must continue using the confirmation behavior implemented in issue #68 and must never use this direct-RPC rebroadcast flow.
The future EvelynBurnerService may explicitly choose between the existing submission-only flow and this new reliable direct-RPC operation according to its own domain policy.
OpenSpec and documentation
Follow the normal OpenSpec workflow for issue #32.
Create an active change describing:
reliable direct Solana RPC submission
preservation of signed transaction validity metadata
exact-byte rebroadcast invariants
confirmation, rejection, expiration and timeout semantics
warning collection
interaction with awaitTransaction()
unchanged wallet and Jupiter submission policies
Update the relevant main specifications through the normal sync-and-archive process after implementation and review.
Add comprehensive Javadoc to:
the reliable execution API
the execution result type
all result statuses
the structured timeout exception
the expanded SolanaSignedTransaction
The documentation must clearly distinguish:
RPC acceptance
on-chain confirmation
definitive rejection
proven expiration
unknown timeout
safe rebroadcast of identical bytes
unsafe automatic rebuilding or re-signing
Testing constraint
Do not add, generate or modify unit tests or other automated tests as part of this issue.
Existing production and test sources may be compiled to verify compatibility, but no new automated-test implementation is requested.
Do not perform a live financial Solana transaction as part of agent verification.
Verification
Verify at least:
the normal Gradle production and test-source compilation succeeds
strict OpenSpec validation succeeds
all construction sites preserve the signed transaction's blockhash and lastValidBlockHeight
the embedded transaction signature is determined before submission
every successful RPC signature is checked against the embedded signature
every rebroadcast uses byte-for-byte identical serialized transaction data
the reliable path uses maxRetries: 0
awaitTransaction() is reused for definitive commitment outcomes
no rebroadcast occurs after proven expiration
a timeout remains an unknown outcome
InterruptedException propagates directly
the low-level sendTransaction() behavior remains unchanged
existing wallet and Jupiter callers retain their current semantics
no generated Java files, runtime data or automated tests are committed
diff and whitespace checks pass
Out of scope
The following are explicitly outside the scope of this issue:
building replacement transactions
obtaining a replacement blockhash after expiration
automatically re-signing an expired transaction
durable nonce transactions
WebSocket or signatureSubscribe support
automatic switching between RPC providers
TPU-client submission
priority-fee calculation or modification
a separate simulateTransaction flow
changes to Jupiter-managed transaction submission
changes to wallet transfer confirmation policy
persistent storage or reconciliation of unknown transactions
Evelyn burner orchestration
Discord notifications
new automated tests
Acceptance criteria
SolanaSignedTransaction preserves the blockhash and lastValidBlockHeight from the unsigned transaction.
The reliable operation determines the transaction signature locally before submission.
SolanaBlockChain exposes a generic expiry-aware direct-RPC execution operation.
CachedSolanaBlockChain delegates the operation without caching.
The existing sendTransaction() and awaitTransaction() contracts remain unchanged.
Every submission and rebroadcast uses the exact same signed serialized bytes.
The reliable submission path uses preflight and maxRetries: 0.
awaitTransaction() supplies the definitive commitment outcome semantics.
CONFIRMED and CONFIRMED_WITH_WARNINGS require on-chain success at the requested commitment.
REJECTED represents only a definitive preflight or on-chain rejection.
EXPIRED requires proof that the current block height exceeds lastValidBlockHeight and that no lower-commitment transaction remains visible.
An overall timeout produces a structured unknown-outcome exception.
Communication failures are never silently reported as success or definitive rejection.
No transaction is rebroadcast after proven expiration.
No transaction is rebuilt, modified or re-signed automatically.
InterruptedException propagates directly without further RPC calls.
## Background
`SolanaBlockChain.sendTransaction(...)` submits a complete signed transaction through Solana's standard JSON-RPC `sendTransaction` method and returns the transaction signature accepted by the RPC server.
A successful response only proves that the RPC server accepted the transaction for forwarding. It does not prove that the transaction was processed or confirmed on-chain, and transactions may be dropped before inclusion.
Issue #67 subsequently added the generic:
```java
SolanaTransactionOutcome awaitTransaction(
ΩSolanaTransactionSignatureΩ signature,
SolanaCommitment commitment,
Duration timeout
) throws IOException, InterruptedException;
```
This operation can determine whether an already submitted transaction reaches a requested commitment level, fails on-chain or remains unknown when its timeout expires.
Issue #68 integrated that operation into Jupiter Swap and Jupiter Perps. Those services submit through Jupiter-managed execution endpoints and must never rebroadcast the returned transaction.
This issue now concerns the remaining direct Solana RPC use case: reliable submission of a signed transaction whose exact serialized bytes and blockhash-validity metadata are available to the caller.
## Objective
Add a generic, expiry-aware transaction execution operation to `SolanaBlockChain` that:
1. submits a signed transaction directly through Solana RPC
2. monitors its signature using the functionality introduced in issue #67
3. safely rebroadcasts the exact same signed transaction while its blockhash remains valid
4. stops rebroadcasting when the transaction is observed on-chain or its blockhash expires
5. reports confirmed success, definitive rejection, proven expiration or an unknown timeout without losing the transaction signature
6. never rebuilds or re-signs the transaction automatically
The implementation must remain independent of Jupiter and work for arbitrary signed legacy or versioned Solana transactions.
## Relationship to `sendTransaction()` and `awaitTransaction()`
The existing low-level operations must remain unchanged:
```text
sendTransaction()
-> performs one submission and returns the RPC response signature
awaitTransaction()
-> monitors an already submitted signature without submitting anything
```
This issue adds a separate higher-level operation for callers that explicitly require expiry-aware direct RPC delivery.
The exact method name may be finalized during the OpenSpec design, but the intended API direction is:
```java
SolanaTransactionExecutionResult executeTransaction(
SolanaSignedTransaction transaction,
SolanaCommitment commitment,
Duration timeout
) throws IOException,
InterruptedException,
SolanaTransactionExecutionTimeoutException;
```
The existing `sendTransaction()` and `awaitTransaction()` contracts must not be changed.
## Preserve transaction-validity metadata
The current `SolanaUnsignedTransaction` contains:
- the serialized transaction
- its recent blockhash
- its `lastValidBlockHeight`
However, `SolanaWallet.signTransaction()` currently returns a `SolanaSignedTransaction` containing only the signed serialized transaction. The validity metadata is therefore lost during signing.
Extend `SolanaSignedTransaction` so it also preserves:
```java
ΩSolanaBlockhashΩ blockhash
long lastValidBlockHeight
```
`SolanaWalletImpl.signTransaction()` must copy the blockhash and `lastValidBlockHeight` unchanged from the supplied `SolanaUnsignedTransaction` into the resulting `SolanaSignedTransaction`.
Update all existing construction sites accordingly.
The following must be validated locally:
- the serialized signed transaction is present and valid Base64
- the blockhash is present and non-blank
- `lastValidBlockHeight` is greater than zero
The reliable execution operation may trust that the preserved metadata originated from the unsigned transaction being signed. It does not need to implement a general-purpose Solana transaction parser solely to compare the stored blockhash with the serialized message.
## Determine the signature before submission
The reliable flow must know the transaction signature even if the first `sendTransaction` request reaches the RPC server but its response is lost.
Before the first submission, extract the transaction ID from the first signature embedded in the serialized signed transaction.
The extracted signature must:
- contain exactly 64 bytes before Base58 encoding
- be available before the first RPC request
- be used for all subsequent calls to `awaitTransaction()`
- be stored in every result or structured timeout
- be compared with every successful signature returned by `sendTransaction`
If an RPC response returns a different signature from the one embedded in the transaction, fail with an `IOException` containing both signatures and clear protocol context.
## Result model
Add a public structured result type:
```text
com.r35157.libs.solana.SolanaTransactionExecutionResult
```
The exact record layout may be finalized during the OpenSpec design, but it must provide structured access to at least:
- execution status
- transaction signature
- requested commitment
- confirmation slot when known
- rejection details when known
- `lastValidBlockHeight`
- block height used to prove expiration, when applicable
- number of submission attempts
- an immutable list of warnings encountered during execution
The result status must contain:
```java
public enum Status {
CONFIRMED,
CONFIRMED_WITH_WARNINGS,
REJECTED,
EXPIRED
}
```
### `CONFIRMED`
`CONFIRMED` means:
- `awaitTransaction()` returned `SUCCEEDED`
- the requested commitment level was reached or exceeded
- no recoverable submission, transport or observation warnings occurred
- the confirmation slot is available
- failure details are absent
### `CONFIRMED_WITH_WARNINGS`
`CONFIRMED_WITH_WARNINGS` means:
- `awaitTransaction()` ultimately returned `SUCCEEDED`
- the requested commitment level was reached or exceeded
- one or more earlier submission, transport or observation attempts encountered recoverable problems
- the confirmation slot is available
- the warnings list is non-empty
A warning must never cause an otherwise successful confirmed transaction to be reported as rejected or expired.
### `REJECTED`
`REJECTED` means that the transaction was definitively rejected.
This includes:
- `awaitTransaction()` returned `FAILED` at the requested commitment level
- a structured preflight rejection proves that the transaction was not forwarded, provided that no earlier submission attempt may already have been accepted
For an on-chain rejection:
- the confirmation slot must be available
- the complete Solana `err` value must be preserved as compact JSON
For a definitive preflight rejection:
- the complete JSON-RPC error must be preserved
- the slot may be absent
A communication failure, lost response, malformed response or unknown timeout is not a definitive rejection.
Once any earlier submission attempt may have reached the RPC server, a later preflight rejection must not by itself cause `REJECTED`, because the earlier attempt may still land.
### `EXPIRED`
`EXPIRED` means:
- the RPC node has reported a current block height strictly greater than the transaction's `lastValidBlockHeight`
- the transaction has not reached the requested commitment
- a final status observation does not show that the transaction is still present at any commitment level
- the transaction can no longer be newly accepted using its original blockhash
`EXPIRED` is different from a timeout. Expiration is a proven terminal state for the original signed transaction. A timeout leaves the outcome unknown.
The method must not automatically build or sign a replacement transaction after expiration. Any replacement is a separate caller decision.
## Safe rebroadcast requirements
Every submission attempt must send the exact same Base64-encoded signed transaction bytes.
The implementation must never:
- rebuild the transaction
- obtain a new blockhash
- modify instructions or fees
- add or replace a signature
- re-sign the transaction
- submit an equivalent transaction with different serialized bytes
Rebroadcasting the exact same signed transaction preserves the same transaction ID and cannot create multiple independent executions.
Count every started `sendTransaction` RPC request as one submission attempt, including attempts whose response is lost or fails.
For the reliable execution path, use manual rebroadcast control:
```json
{
"encoding": "base64",
"skipPreflight": false,
"preflightCommitment": "confirmed",
"maxRetries": 0
}
```
`maxRetries: 0` prevents hidden RPC-node retries from being layered underneath the explicit application-controlled rebroadcast loop.
The existing low-level `sendTransaction()` configuration and behavior must remain unchanged.
Do not add a separate `simulateTransaction` request. The existing `sendTransaction` preflight behavior is sufficient.
## Reuse `awaitTransaction()`
The implementation must reuse the commitment and on-chain outcome semantics introduced in issue #67.
Handle `awaitTransaction()` results as follows:
| `awaitTransaction()` outcome | Reliable execution behavior |
|---|---|
| `SUCCEEDED` | Return `CONFIRMED` or `CONFIRMED_WITH_WARNINGS`. |
| `FAILED` | Return `REJECTED` with the complete outcome details. |
| `TIMED_OUT` | Treat the observation as inconclusive; check transaction validity and, if still valid, rebroadcast the exact same signed transaction. |
A `TIMED_OUT` result from an individual `awaitTransaction()` call must not be returned as `EXPIRED` without independent block-height evidence.
Each internal `awaitTransaction()` timeout must be bounded by the remaining overall execution deadline.
`awaitTransaction()` must remain the public source of commitment and definitive on-chain outcome semantics. A narrow private one-shot status observation may be added if required to avoid incorrectly reporting a lower-commitment transaction as expired, but no competing public transaction-status API should be introduced.
## Expiration handling
Add the necessary internal Solana RPC support for obtaining the current block height.
Use Solana's `getBlockHeight` RPC method with the same `confirmed` commitment currently used by `getLatestBlockhash()`.
The block-height response must be validated as a non-negative integer representable as a Java `long`.
Rebroadcasting is permitted only while:
```text
currentBlockHeight <= lastValidBlockHeight
```
Once:
```text
currentBlockHeight > lastValidBlockHeight
```
the implementation must stop submitting the transaction permanently.
Before returning `EXPIRED`, perform a final status check using `searchTransactionHistory: true`.
Do not report `EXPIRED` solely because the block height has advanced if the transaction is already visible at a lower commitment level. In that situation:
- stop rebroadcasting
- continue monitoring the existing signature
- return success or rejection if it reaches the requested commitment
- otherwise end with an unknown timeout if the overall deadline expires before a definitive outcome can be established
If a previously observed lower-commitment transaction subsequently disappears after its blockhash has expired, it may then be reported as `EXPIRED`.
## Overall timeout
The `Duration timeout` parameter defines one overall monotonic deadline for the complete execution operation.
It includes:
- local preparation and signature extraction
- waiting for the shared RPC gate
- existing five-second RPC throttling
- every submission attempt
- status polling through `awaitTransaction()`
- block-height requests
- all HTTP requests
- all recoverable retry attempts
The implementation must use a monotonic time source such as `System.nanoTime()`.
No new RPC request may start after the overall deadline.
If the deadline expires before success, definitive rejection or proven expiration can be established, throw:
```text
SolanaTransactionExecutionTimeoutException
```
The checked exception must provide structured access to at least:
- transaction signature
- requested commitment
- `lastValidBlockHeight`
- most recently observed block height, if known
- number of submission attempts
- accumulated warnings
Its message and Javadoc must clearly state:
- the transaction outcome remains unknown
- the original transaction may still have landed
- no equivalent transaction was automatically submitted
- the caller must not automatically rebuild or re-sign the operation
A timeout must never be converted into `REJECTED` or `EXPIRED` without the required evidence.
## Communication failures and warnings
An individual transport, HTTP or RPC communication failure does not prove that the transaction was rejected. The request may have reached the server even if the response was lost.
Recoverable failures may therefore be recorded as warnings while the method continues to:
- monitor the locally extracted signature
- check blockhash validity
- rebroadcast only the exact same signed bytes
- respect the overall deadline
If the transaction later succeeds, return `CONFIRMED_WITH_WARNINGS`.
If a non-recoverable communication or protocol error causes the method to throw `IOException` after a submission attempt has started:
- the message must include the transaction signature
- the message must state that the on-chain outcome may be unknown
- the original exception must be preserved as the cause
- no rebuilt or re-signed transaction may be submitted
Malformed arguments and invalid signed transaction data must fail before the first RPC request.
## Interruption
If the calling thread is interrupted while:
- waiting for the RPC gate
- waiting for throttling
- submitting
- awaiting transaction status
- checking block height
- or performing any other internal wait
`InterruptedException` must propagate directly.
Interruption must not:
- be converted into a warning
- be converted into timeout or expiration
- be wrapped in `IOException`
- trigger another RPC request
- trigger another submission attempt
## Existing RPC throttling
All new Solana RPC calls must use the existing shared, interruptible five-second throttling mechanism in `SolanaBlockChainImpl`.
Do not introduce:
- a second rate limiter
- a busy loop
- an uninterruptible sleep
- concurrent background submission threads
- RPC calls that bypass the existing gate
The implementation may coordinate status, submission and block-height calls within the shared throttle, but all waiting must remain interruptible and bounded by the overall deadline.
## `CachedSolanaBlockChain`
Add the reliable execution operation to:
```text
com.r35157.libs.solana.impl.cached.CachedSolanaBlockChain
```
It must delegate directly to the underlying `SolanaBlockChain`.
Execution results, warnings and timeouts must not be:
- cached
- reused
- deduplicated
- inferred from cached blockchain data
Every invocation represents a distinct execution workflow, even though all rebroadcasts within one invocation use the same signed transaction.
## Existing callers and services
This issue must not change the submission-only semantics of:
- `SolanaWallet.sendSolana()`
- `SolanaWallet.sendAllSolana()`
- `SolanaWallet.sendSPLToken()`
- `SolanaWallet.burnSPLToken()`
- `SolanaWallet.sendTransaction()`
- `SolanaWallet.signAndSendTransaction()`
These methods must continue to submit once and return the transaction signature without automatically waiting or rebroadcasting.
This issue must also not change:
- `JupiterSwapService`
- Jupiter Perps increase or decrease operations
- `JupiterTransactionOutcomeException`
- `EvelynBurnerService`
- Discord integration
Jupiter operations must continue using the confirmation behavior implemented in issue #68 and must never use this direct-RPC rebroadcast flow.
The future `EvelynBurnerService` may explicitly choose between the existing submission-only flow and this new reliable direct-RPC operation according to its own domain policy.
## OpenSpec and documentation
Follow the normal OpenSpec workflow for issue #32.
Create an active change describing:
- reliable direct Solana RPC submission
- preservation of signed transaction validity metadata
- exact-byte rebroadcast invariants
- confirmation, rejection, expiration and timeout semantics
- warning collection
- interaction with `awaitTransaction()`
- unchanged wallet and Jupiter submission policies
Update the relevant main specifications through the normal sync-and-archive process after implementation and review.
Add comprehensive Javadoc to:
- the reliable execution API
- the execution result type
- all result statuses
- the structured timeout exception
- the expanded `SolanaSignedTransaction`
The documentation must clearly distinguish:
- RPC acceptance
- on-chain confirmation
- definitive rejection
- proven expiration
- unknown timeout
- safe rebroadcast of identical bytes
- unsafe automatic rebuilding or re-signing
## Testing constraint
Do not add, generate or modify unit tests or other automated tests as part of this issue.
Existing production and test sources may be compiled to verify compatibility, but no new automated-test implementation is requested.
Do not perform a live financial Solana transaction as part of agent verification.
## Verification
Verify at least:
- the normal Gradle production and test-source compilation succeeds
- strict OpenSpec validation succeeds
- all construction sites preserve the signed transaction's blockhash and `lastValidBlockHeight`
- the embedded transaction signature is determined before submission
- every successful RPC signature is checked against the embedded signature
- every rebroadcast uses byte-for-byte identical serialized transaction data
- the reliable path uses `maxRetries: 0`
- `awaitTransaction()` is reused for definitive commitment outcomes
- no rebroadcast occurs after proven expiration
- a timeout remains an unknown outcome
- `InterruptedException` propagates directly
- the low-level `sendTransaction()` behavior remains unchanged
- existing wallet and Jupiter callers retain their current semantics
- no generated Java files, runtime data or automated tests are committed
- diff and whitespace checks pass
## Out of scope
The following are explicitly outside the scope of this issue:
- building replacement transactions
- obtaining a replacement blockhash after expiration
- automatically re-signing an expired transaction
- durable nonce transactions
- WebSocket or `signatureSubscribe` support
- automatic switching between RPC providers
- TPU-client submission
- priority-fee calculation or modification
- a separate `simulateTransaction` flow
- changes to Jupiter-managed transaction submission
- changes to wallet transfer confirmation policy
- persistent storage or reconciliation of unknown transactions
- Evelyn burner orchestration
- Discord notifications
- new automated tests
## Acceptance criteria
- [ ] `SolanaSignedTransaction` preserves the blockhash and `lastValidBlockHeight` from the unsigned transaction.
- [ ] The reliable operation determines the transaction signature locally before submission.
- [ ] `SolanaBlockChain` exposes a generic expiry-aware direct-RPC execution operation.
- [ ] `CachedSolanaBlockChain` delegates the operation without caching.
- [ ] The existing `sendTransaction()` and `awaitTransaction()` contracts remain unchanged.
- [ ] Every submission and rebroadcast uses the exact same signed serialized bytes.
- [ ] The reliable submission path uses preflight and `maxRetries: 0`.
- [ ] `awaitTransaction()` supplies the definitive commitment outcome semantics.
- [ ] `CONFIRMED` and `CONFIRMED_WITH_WARNINGS` require on-chain success at the requested commitment.
- [ ] `REJECTED` represents only a definitive preflight or on-chain rejection.
- [ ] `EXPIRED` requires proof that the current block height exceeds `lastValidBlockHeight` and that no lower-commitment transaction remains visible.
- [ ] An overall timeout produces a structured unknown-outcome exception.
- [ ] Communication failures are never silently reported as success or definitive rejection.
- [ ] No transaction is rebroadcast after proven expiration.
- [ ] No transaction is rebuilt, modified or re-signed automatically.
- [ ] `InterruptedException` propagates directly without further RPC calls.
- [ ] Existing wallet operations remain single-submission operations.
- [ ] Existing Jupiter operations remain unchanged.
- [ ] No new automated tests are added or modified.
- [ ] Gradle compilation and strict OpenSpec validation succeed.
## References
- Issue #67: [Add generic `awaitTransaction()` support to `SolanaBlockChain`](https://git.r35157.com/r35157/com_r35157_nenjim-hubd-impl_ref/issues/67)
- Issue #68: [Await `CONFIRMED` on-chain outcomes for Jupiter Swap and Perps operations](https://git.r35157.com/r35157/com_r35157_nenjim-hubd-impl_ref/issues/68)
- [Solana `sendTransaction`](https://solana.com/docs/rpc/http/sendtransaction)
- [Solana `getSignatureStatuses`](https://solana.com/docs/rpc/http/getsignaturestatuses)
- [Solana `getBlockHeight`](https://solana.com/docs/rpc/http/getblockheight)
- [Solana guide to retrying transactions](https://solana.com/developers/cookbook/transactions/retry)
minimons
self-assigned this 2026-07-19 01:43:46 +02:00
minimons
added this to the AssetAZ project 2026-07-19 01:43:46 +02:00
minimons
changed title from Add reliable Solana transaction submission and confirmation to Add expiry-aware rebroadcast for direct Solana RPC transactions2026-08-11 15:26:36 +02:00
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(...)submits a complete signed transaction through Solana's standard JSON-RPCsendTransactionmethod and returns the transaction signature accepted by the RPC server.A successful response only proves that the RPC server accepted the transaction for forwarding. It does not prove that the transaction was processed or confirmed on-chain, and transactions may be dropped before inclusion.
Issue #67 subsequently added the generic:
This operation can determine whether an already submitted transaction reaches a requested commitment level, fails on-chain or remains unknown when its timeout expires.
Issue #68 integrated that operation into Jupiter Swap and Jupiter Perps. Those services submit through Jupiter-managed execution endpoints and must never rebroadcast the returned transaction.
This issue now concerns the remaining direct Solana RPC use case: reliable submission of a signed transaction whose exact serialized bytes and blockhash-validity metadata are available to the caller.
Objective
Add a generic, expiry-aware transaction execution operation to
SolanaBlockChainthat:The implementation must remain independent of Jupiter and work for arbitrary signed legacy or versioned Solana transactions.
Relationship to
sendTransaction()andawaitTransaction()The existing low-level operations must remain unchanged:
This issue adds a separate higher-level operation for callers that explicitly require expiry-aware direct RPC delivery.
The exact method name may be finalized during the OpenSpec design, but the intended API direction is:
The existing
sendTransaction()andawaitTransaction()contracts must not be changed.Preserve transaction-validity metadata
The current
SolanaUnsignedTransactioncontains:lastValidBlockHeightHowever,
SolanaWallet.signTransaction()currently returns aSolanaSignedTransactioncontaining only the signed serialized transaction. The validity metadata is therefore lost during signing.Extend
SolanaSignedTransactionso it also preserves:SolanaWalletImpl.signTransaction()must copy the blockhash andlastValidBlockHeightunchanged from the suppliedSolanaUnsignedTransactioninto the resultingSolanaSignedTransaction.Update all existing construction sites accordingly.
The following must be validated locally:
lastValidBlockHeightis greater than zeroThe reliable execution operation may trust that the preserved metadata originated from the unsigned transaction being signed. It does not need to implement a general-purpose Solana transaction parser solely to compare the stored blockhash with the serialized message.
Determine the signature before submission
The reliable flow must know the transaction signature even if the first
sendTransactionrequest reaches the RPC server but its response is lost.Before the first submission, extract the transaction ID from the first signature embedded in the serialized signed transaction.
The extracted signature must:
awaitTransaction()sendTransactionIf an RPC response returns a different signature from the one embedded in the transaction, fail with an
IOExceptioncontaining both signatures and clear protocol context.Result model
Add a public structured result type:
The exact record layout may be finalized during the OpenSpec design, but it must provide structured access to at least:
lastValidBlockHeightThe result status must contain:
CONFIRMEDCONFIRMEDmeans:awaitTransaction()returnedSUCCEEDEDCONFIRMED_WITH_WARNINGSCONFIRMED_WITH_WARNINGSmeans:awaitTransaction()ultimately returnedSUCCEEDEDA warning must never cause an otherwise successful confirmed transaction to be reported as rejected or expired.
REJECTEDREJECTEDmeans that the transaction was definitively rejected.This includes:
awaitTransaction()returnedFAILEDat the requested commitment levelFor an on-chain rejection:
errvalue must be preserved as compact JSONFor a definitive preflight rejection:
A communication failure, lost response, malformed response or unknown timeout is not a definitive rejection.
Once any earlier submission attempt may have reached the RPC server, a later preflight rejection must not by itself cause
REJECTED, because the earlier attempt may still land.EXPIREDEXPIREDmeans:lastValidBlockHeightEXPIREDis different from a timeout. Expiration is a proven terminal state for the original signed transaction. A timeout leaves the outcome unknown.The method must not automatically build or sign a replacement transaction after expiration. Any replacement is a separate caller decision.
Safe rebroadcast requirements
Every submission attempt must send the exact same Base64-encoded signed transaction bytes.
The implementation must never:
Rebroadcasting the exact same signed transaction preserves the same transaction ID and cannot create multiple independent executions.
Count every started
sendTransactionRPC request as one submission attempt, including attempts whose response is lost or fails.For the reliable execution path, use manual rebroadcast control:
maxRetries: 0prevents hidden RPC-node retries from being layered underneath the explicit application-controlled rebroadcast loop.The existing low-level
sendTransaction()configuration and behavior must remain unchanged.Do not add a separate
simulateTransactionrequest. The existingsendTransactionpreflight behavior is sufficient.Reuse
awaitTransaction()The implementation must reuse the commitment and on-chain outcome semantics introduced in issue #67.
Handle
awaitTransaction()results as follows:awaitTransaction()outcomeSUCCEEDEDCONFIRMEDorCONFIRMED_WITH_WARNINGS.FAILEDREJECTEDwith the complete outcome details.TIMED_OUTA
TIMED_OUTresult from an individualawaitTransaction()call must not be returned asEXPIREDwithout independent block-height evidence.Each internal
awaitTransaction()timeout must be bounded by the remaining overall execution deadline.awaitTransaction()must remain the public source of commitment and definitive on-chain outcome semantics. A narrow private one-shot status observation may be added if required to avoid incorrectly reporting a lower-commitment transaction as expired, but no competing public transaction-status API should be introduced.Expiration handling
Add the necessary internal Solana RPC support for obtaining the current block height.
Use Solana's
getBlockHeightRPC method with the sameconfirmedcommitment currently used bygetLatestBlockhash().The block-height response must be validated as a non-negative integer representable as a Java
long.Rebroadcasting is permitted only while:
Once:
the implementation must stop submitting the transaction permanently.
Before returning
EXPIRED, perform a final status check usingsearchTransactionHistory: true.Do not report
EXPIREDsolely because the block height has advanced if the transaction is already visible at a lower commitment level. In that situation:If a previously observed lower-commitment transaction subsequently disappears after its blockhash has expired, it may then be reported as
EXPIRED.Overall timeout
The
Duration timeoutparameter defines one overall monotonic deadline for the complete execution operation.It includes:
awaitTransaction()The implementation must use a monotonic time source such as
System.nanoTime().No new RPC request may start after the overall deadline.
If the deadline expires before success, definitive rejection or proven expiration can be established, throw:
The checked exception must provide structured access to at least:
lastValidBlockHeightIts message and Javadoc must clearly state:
A timeout must never be converted into
REJECTEDorEXPIREDwithout the required evidence.Communication failures and warnings
An individual transport, HTTP or RPC communication failure does not prove that the transaction was rejected. The request may have reached the server even if the response was lost.
Recoverable failures may therefore be recorded as warnings while the method continues to:
If the transaction later succeeds, return
CONFIRMED_WITH_WARNINGS.If a non-recoverable communication or protocol error causes the method to throw
IOExceptionafter a submission attempt has started:Malformed arguments and invalid signed transaction data must fail before the first RPC request.
Interruption
If the calling thread is interrupted while:
InterruptedExceptionmust propagate directly.Interruption must not:
IOExceptionExisting RPC throttling
All new Solana RPC calls must use the existing shared, interruptible five-second throttling mechanism in
SolanaBlockChainImpl.Do not introduce:
The implementation may coordinate status, submission and block-height calls within the shared throttle, but all waiting must remain interruptible and bounded by the overall deadline.
CachedSolanaBlockChainAdd the reliable execution operation to:
It must delegate directly to the underlying
SolanaBlockChain.Execution results, warnings and timeouts must not be:
Every invocation represents a distinct execution workflow, even though all rebroadcasts within one invocation use the same signed transaction.
Existing callers and services
This issue must not change the submission-only semantics of:
SolanaWallet.sendSolana()SolanaWallet.sendAllSolana()SolanaWallet.sendSPLToken()SolanaWallet.burnSPLToken()SolanaWallet.sendTransaction()SolanaWallet.signAndSendTransaction()These methods must continue to submit once and return the transaction signature without automatically waiting or rebroadcasting.
This issue must also not change:
JupiterSwapServiceJupiterTransactionOutcomeExceptionEvelynBurnerServiceJupiter operations must continue using the confirmation behavior implemented in issue #68 and must never use this direct-RPC rebroadcast flow.
The future
EvelynBurnerServicemay explicitly choose between the existing submission-only flow and this new reliable direct-RPC operation according to its own domain policy.OpenSpec and documentation
Follow the normal OpenSpec workflow for issue #32.
Create an active change describing:
awaitTransaction()Update the relevant main specifications through the normal sync-and-archive process after implementation and review.
Add comprehensive Javadoc to:
SolanaSignedTransactionThe documentation must clearly distinguish:
Testing constraint
Do not add, generate or modify unit tests or other automated tests as part of this issue.
Existing production and test sources may be compiled to verify compatibility, but no new automated-test implementation is requested.
Do not perform a live financial Solana transaction as part of agent verification.
Verification
Verify at least:
lastValidBlockHeightmaxRetries: 0awaitTransaction()is reused for definitive commitment outcomesInterruptedExceptionpropagates directlysendTransaction()behavior remains unchangedOut of scope
The following are explicitly outside the scope of this issue:
signatureSubscribesupportsimulateTransactionflowAcceptance criteria
SolanaSignedTransactionpreserves the blockhash andlastValidBlockHeightfrom the unsigned transaction.SolanaBlockChainexposes a generic expiry-aware direct-RPC execution operation.CachedSolanaBlockChaindelegates the operation without caching.sendTransaction()andawaitTransaction()contracts remain unchanged.maxRetries: 0.awaitTransaction()supplies the definitive commitment outcome semantics.CONFIRMEDandCONFIRMED_WITH_WARNINGSrequire on-chain success at the requested commitment.REJECTEDrepresents only a definitive preflight or on-chain rejection.EXPIREDrequires proof that the current block height exceedslastValidBlockHeightand that no lower-commitment transaction remains visible.InterruptedExceptionpropagates directly without further RPC calls.References
awaitTransaction()support toSolanaBlockChainCONFIRMEDon-chain outcomes for Jupiter Swap and Perps operationssendTransactiongetSignatureStatusesgetBlockHeightAdd reliable Solana transaction submission and confirmationto Add expiry-aware rebroadcast for direct Solana RPC transactions