Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 56 additions & 24 deletions packages/sdk-provider-solana/src/actions/sendAndConfirmBundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@solana/kit'

import { getJitoRpcs } from '../rpc/registry.js'
import { extractBlockhash } from '../utils/extractBlockhash.js'

type SignatureStatus = {
slot: bigint
Expand Down Expand Up @@ -48,6 +49,8 @@ export async function sendAndConfirmBundle(
getBase64EncodedWireTransaction(tx)
)

const txBlockhash = extractBlockhash(signedTransactions[0])

const abortController = new AbortController()

const confirmPromises = jitoRpcs.map(async (jitoRpc) => {
Expand All @@ -60,26 +63,18 @@ export async function sendAndConfirmBundle(
return null
}

const [{ value: blockhashResult }, initialBlockHeight] =
await Promise.all([
jitoRpc
.getLatestBlockhash({
commitment: 'confirmed',
})
.send(),
jitoRpc
.getBlockHeight({
commitment: 'confirmed',
})
.send(),
])

let currentBlockHeight = initialBlockHeight

while (
currentBlockHeight < blockhashResult.lastValidBlockHeight &&
!abortController.signal.aborted
) {
let blockhashValid = true

// For durable nonce txs, fall back to block-height-based timeout
let expiryBlockHeight: bigint | undefined
if (!txBlockhash) {
const { value: blockhashResult } = await jitoRpc
.getLatestBlockhash({ commitment: 'confirmed' })
.send()
expiryBlockHeight = blockhashResult.lastValidBlockHeight
}

while (blockhashValid && !abortController.signal.aborted) {
const statusResponse = await jitoRpc
.getBundleStatuses([bundleId])
.send()
Expand Down Expand Up @@ -117,11 +112,48 @@ export async function sendAndConfirmBundle(

await sleep(400)
if (!abortController.signal.aborted) {
currentBlockHeight = await jitoRpc
.getBlockHeight({
commitment: 'confirmed',
})
if (txBlockhash) {
const { value: isValid } = await jitoRpc
.isBlockhashValid(txBlockhash, {
commitment: 'confirmed',
})
.send()
blockhashValid = isValid
} else {
const blockHeight = await jitoRpc
.getBlockHeight({ commitment: 'confirmed' })
.send()
blockhashValid = blockHeight < expiryBlockHeight!
}
}
}

// Final status check — the bundle may have confirmed between the
// last poll and the blockhash expiring.
if (!abortController.signal.aborted) {
const statusResponse = await jitoRpc
.getBundleStatuses([bundleId])
.send()

const bundleStatus = statusResponse.value[0]
if (
bundleStatus &&
(bundleStatus.confirmation_status === 'confirmed' ||
bundleStatus.confirmation_status === 'finalized')
) {
const txSignatures = bundleStatus.transactions
const sigResponse = await jitoRpc
.getSignatureStatuses(txSignatures)
.send()

if (sigResponse?.value && Array.isArray(sigResponse.value)) {
abortController.abort()
return {
bundleId,
txSignatures,
signatureResults: sigResponse.value,
}
}
}
}

Expand Down
110 changes: 68 additions & 42 deletions packages/sdk-provider-solana/src/actions/sendAndConfirmTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type TransactionError,
} from '@solana/kit'
import { getSolanaRpcs } from '../rpc/registry.js'
import { extractBlockhash } from '../utils/extractBlockhash.js'

type SignatureStatus = {
slot: bigint
Expand All @@ -21,6 +22,22 @@ type ConfirmedTransactionResult = {
txSignature: string
}

function getConfirmedStatus(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this into it's own util function and reuse in sendAndConfirmBundle?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Moved getConfirmedStatus and SignatureStatus into utils/getConfirmedStatus.ts. Also added isConfirmedCommitment() to replace the inline checks in the bundle path.

statusResponse: Readonly<{
value: readonly (SignatureStatus | null)[]
}>
): SignatureStatus | null {
const status = statusResponse.value[0]
if (
status &&
(status.confirmationStatus === 'confirmed' ||
status.confirmationStatus === 'finalized')
) {
return status
}
return null
}

/**
* Sends a Solana transaction to multiple RPC endpoints and returns the confirmation
* as soon as any of them confirm the transaction.
Expand All @@ -35,13 +52,14 @@ export async function sendAndConfirmTransaction(
const solanaRpcs = await getSolanaRpcs(client)

const signedTxSerialized = getBase64EncodedWireTransaction(signedTransaction)
// Create transaction hash (signature)
const txSignature = getSignatureFromTransaction(signedTransaction)

if (!txSignature) {
throw new Error('Transaction signature is missing.')
}

const txBlockhash = extractBlockhash(signedTransaction)

const rawTransactionOptions = {
// We can skip preflight check after the first transaction has been sent
// https://solana.com/docs/advanced/retry#the-cost-of-skipping-preflight
Expand All @@ -66,51 +84,52 @@ export async function sendAndConfirmTransaction(
// Continue with confirmation even if initial send fails
}

const [{ value: blockhashResult }, initialBlockHeight] =
await Promise.all([
rpc
.getLatestBlockhash({
commitment: 'confirmed',
})
.send(),
rpc
.getBlockHeight({
commitment: 'confirmed',
})
.send(),
])

let signatureResult: SignatureStatus | null = null
let blockHeight = initialBlockHeight
let blockhashValid = true

// For durable nonce txs, fall back to block-height-based timeout
let expiryBlockHeight: bigint | undefined
if (!txBlockhash) {
const { value: blockhashResult } = await rpc
.getLatestBlockhash({ commitment: 'confirmed' })
.send()
expiryBlockHeight = blockhashResult.lastValidBlockHeight
}

const pollingPromise = (async () => {
while (
blockHeight < blockhashResult.lastValidBlockHeight &&
!abortController.signal.aborted
) {
const statusResponse = await rpc
.getSignatureStatuses([txSignature])
.send()

const status = statusResponse.value[0]
if (
status &&
(status.confirmationStatus === 'confirmed' ||
status.confirmationStatus === 'finalized')
) {
signatureResult = status
// Immediately abort all other RPCs when we find a result
while (blockhashValid && !abortController.signal.aborted) {
const confirmed = getConfirmedStatus(
await rpc.getSignatureStatuses([txSignature]).send()
)
if (confirmed) {
signatureResult = confirmed
abortController.abort()
return status
return confirmed
}

await sleep(400)
}

// Final status check — the tx may have confirmed between the last
// poll and the blockhash expiring.
if (!abortController.signal.aborted) {
const confirmed = getConfirmedStatus(
await rpc.getSignatureStatuses([txSignature]).send()
)
if (confirmed) {
signatureResult = confirmed
abortController.abort()
return confirmed
}
}

return null
})()

// Sending loop runs in the background — only pollingPromise produces results.
const sendingPromise = (async () => {
while (
blockHeight < blockhashResult.lastValidBlockHeight &&
blockhashValid &&
!abortController.signal.aborted &&
!signatureResult
) {
Expand All @@ -124,18 +143,25 @@ export async function sendAndConfirmTransaction(

await sleep(1000)
if (!abortController.signal.aborted) {
blockHeight = await rpc
.getBlockHeight({
commitment: 'confirmed',
})
.send()
if (txBlockhash) {
const { value: isValid } = await rpc
.isBlockhashValid(txBlockhash, {
commitment: 'confirmed',
})
.send()
blockhashValid = isValid
} else {
const blockHeight = await rpc
.getBlockHeight({ commitment: 'confirmed' })
.send()
blockhashValid = blockHeight < expiryBlockHeight!
}
}
}
return null
})()
sendingPromise.catch(() => {})

@tomiiide tomiiide May 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we swallowing errors here. Won't this cause problems?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. The sending loop is fire-and-forget — confirmation is handled by pollingPromise, so errors here don't affect the result. Added a comment explaining this.

Also wrapped the isBlockhashValid/getBlockHeight calls in try-catch inside the loop. Previously, if those RPC calls threw, the loop would die silently while blockhashValid stayed true. Now transient failures are handled and the loop continues.


// Wait for polling to find the result
const result = await Promise.race([pollingPromise, sendingPromise])
const result = await pollingPromise
return result
} catch (error) {
if (abortController.signal.aborted) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export class SolanaStandardWaitForTransactionTask extends BaseStepExecutionTask
connection
.simulateTransaction(encodedTransaction, {
commitment: 'confirmed',
replaceRecentBlockhash: true,
encoding: 'base64',
})
.send()
Expand Down
18 changes: 18 additions & 0 deletions packages/sdk-provider-solana/src/utils/extractBlockhash.ts
Comment thread
effie-ms marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {
type Blockhash,
getCompiledTransactionMessageDecoder,
isTransactionWithDurableNonceLifetime,
type Transaction,
} from '@solana/kit'

const decoder = getCompiledTransactionMessageDecoder()

export function extractBlockhash(
signedTransaction: Transaction
): Blockhash | null {
if (isTransactionWithDurableNonceLifetime(signedTransaction)) {
return null
}
const compiledMessage = decoder.decode(signedTransaction.messageBytes)
return compiledMessage.lifetimeToken as Blockhash
}