Add expiry-aware rebroadcast for direct Solana RPC transactions #32

Open
opened 2026-07-19 01:43:46 +02:00 by minimons · 0 comments
Owner

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:

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:

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:

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:

Ω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:

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:

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:

{
  "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:

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.

CachedSolanaBlockChain

Add the reliable execution operation to:

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

## 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 added the enhancement label 2026-07-19 01:43:46 +02:00
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 transactions 2026-08-11 15:26:36 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: r35157/com_r35157_nenjim-hubd-impl_ref#32