Week 4 • Send Confirm Retry And Lifetime Management
Send, Confirm, Retry & Lifetime Management
Handle transaction delivery robustly with explicit confirmation strategy and retry policy.
Step 1: 0-to-1 Theory
Primitives
send: publish signed bytes to clusterconfirm: wait for chain commitment outcomeretry: controlled rebuild/resend policy
Mental Model
Sending a transaction is like dropping a letter into a mailbox. Confirmation is waiting for a receipt.
On Solana, confirmation depends on:
- which commitment level you choose (
processed,confirmed,finalized) - whether your transaction reached the leader and propagated
- whether it expired (blockhash lifetime)
Invariants
- "Sent" is not "Confirmed".
- Retrying stale signed bytes is invalid.
- User rejection is non-retryable.
Quick Checks
- Which failures are retryable?
- Why should retries be policy-driven, not ad hoc?
Step 2: Real-World Implementation (Code Solution)
Create scripts/send-confirm.ts:
import {
appendTransactionMessageInstructions,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners,
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
// Use local validator for predictable learning and quick iteration.
const RPC_URL = "http://127.0.0.1:8899";
async function buildAndSign() {
const rpc = createSolanaRpc(RPC_URL);
// Generate signers so the script is self-contained.
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
// Blockhash is the transaction lifetime. You must refresh it for retries.
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const ix = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: lamports(1_000_000n),
});
const msg = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(sender, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([ix], tx),
);
return signTransactionMessageWithSigners(msg);
}
async function main() {
const rpc = createSolanaRpc(RPC_URL);
// Subscriptions typically use WebSocket. For local validator, ws is the same host/port.
const subs = createSolanaRpcSubscriptions(RPC_URL.replace("http", "ws"));
// This helper sends bytes and waits for confirmation.
const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions: subs });
for (let attempt = 1; attempt <= 2; attempt++) {
try {
const signed = await buildAndSign();
await sendAndConfirm(signed, { commitment: "confirmed" });
console.log("confirmed", getSignatureFromTransaction(signed));
return;
} catch (e: any) {
const msg = String(e?.message ?? e);
// Keep a simple retry policy for beginners:
// retry only on lifetime/timeouts, and rebuild with a fresh blockhash.
const retryable = msg.includes("Blockhash") || msg.includes("timeout");
if (!retryable || attempt === 2) throw e;
console.log("retrying with fresh blockhash...");
}
}
}
main().catch((e) => {
console.error("send/confirm failed", e);
process.exit(1);
});Run:
pnpm tsx scripts/send-confirm.tsExpected result:
- script prints confirmed signature or clear failure reason.
Step 3: Mastery Test
- Easy: why do we rebuild transaction on retry?
- Medium: what should UI show between send and confirm?
- Hard: how would you avoid duplicate user actions during confirm wait?