Skip to content

Commit

Permalink
Merge pull request #72 from solana-developers:improve-send-transactio…
Browse files Browse the repository at this point in the history
…n-helpers

Improve transaction sending helpers
  • Loading branch information
nickfrosty authored Jan 27, 2025
2 parents e7654af + 22a8361 commit 41cbf50
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 35 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 2.6

- Added Transaction send helpers. `prepareTransactionWithCompute()` and `sendTransactionWithRetry()`
- Added Transaction Parser helper functions `getIdlParsedAccountData()`, `parseAnchorTransactionEvents()` and `getIdlParsedInstructionData()`
- Fixed `createAccountsMintsAndTokenAccounts()` function to use correct commitment and not `max` blockhash
- Fixed `confirmTransaction()` to not use correct commitment

## 2.5

- Add `makeTokenMint()`
Expand Down
5 changes: 2 additions & 3 deletions src/lib/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,11 @@ const makeAndSendAndConfirmTransaction = async (
signers: Array<Signer>,
payer: Keypair,
) => {
const latestBlockhash = (await connection.getLatestBlockhash("max"))
.blockhash;
const latestBlockhash = await connection.getLatestBlockhash("confirmed");

const messageV0 = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: latestBlockhash,
recentBlockhash: latestBlockhash.blockhash,
instructions,
}).compileToV0Message();
const transaction = new VersionedTransaction(messageV0);
Expand Down
69 changes: 42 additions & 27 deletions src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ import * as path from "path";
export const confirmTransaction = async (
connection: Connection,
signature: string,
commitment: Commitment = "finalized",
commitment: Commitment = "confirmed",
): Promise<string> => {
const block = await connection.getLatestBlockhash();
const block = await connection.getLatestBlockhash(commitment);
const rpcResponse = await connection.confirmTransaction(
{
signature,
Expand All @@ -54,6 +54,7 @@ export const getSimulationComputeUnits = async (
instructions: Array<TransactionInstruction>,
payer: PublicKey,
lookupTables: Array<AddressLookupTableAccount> | [],
commitment: Commitment = "confirmed",
): Promise<number | null> => {
const testInstructions = [
// Set an arbitrarily high number in simulation
Expand All @@ -76,6 +77,7 @@ export const getSimulationComputeUnits = async (
const rpcResponse = await connection.simulateTransaction(testTransaction, {
replaceRecentBlockhash: true,
sigVerify: false,
commitment,
});

if (rpcResponse?.value?.err) {
Expand All @@ -90,7 +92,8 @@ export const getSimulationComputeUnits = async (
* Constants for transaction retry configuration
*/
export const RETRY_INTERVAL_MS = 2000;
export const MAX_RETRIES = 30;
export const RETRY_INTERVAL_INCREASE = 200;
export const MAX_RETRIES = 15;

/**
* Represents the different states of a transaction during its lifecycle
Expand All @@ -102,6 +105,7 @@ export type TxStatusUpdate =
| { status: "created" }
| { status: "signed" }
| { status: "sent"; signature: string }
| { status: "retry"; signature: string | null }
| { status: "confirmed"; result: SignatureStatus };

/**
Expand Down Expand Up @@ -193,7 +197,7 @@ export async function sendTransactionWithRetry(
initialDelayMs = DEFAULT_SEND_OPTIONS.initialDelayMs,
commitment = DEFAULT_SEND_OPTIONS.commitment,
skipPreflight = DEFAULT_SEND_OPTIONS.skipPreflight,
onStatusUpdate = () => {},
onStatusUpdate = (status) => console.log("Transaction status:", status),
}: SendTransactionOptions = {},
): Promise<string> {
onStatusUpdate?.({ status: "created" });
Expand All @@ -208,34 +212,41 @@ export async function sendTransactionWithRetry(
let signature: string | null = null;
let status: SignatureStatus | null = null;
let retries = 0;
// Setting a minimum to decrease spam and for the confirmation to work
let delayBetweenRetries = Math.max(initialDelayMs, 500);

while (retries < maxRetries) {
try {
const isFirstSend = signature === null;

// Send transaction if not sent yet
if (!signature) {
signature = await connection.sendRawTransaction(
transaction.serialize(),
{
skipPreflight,
preflightCommitment: commitment,
maxRetries: 0,
},
);
signature = await connection.sendRawTransaction(transaction.serialize(), {
skipPreflight,
preflightCommitment: commitment,
maxRetries: 0,
});

if (isFirstSend) {
onStatusUpdate?.({ status: "sent", signature });
}

// Check status
const response = await connection.getSignatureStatus(signature);
if (response?.value) {
status = response.value;

if (
status.confirmationStatus === "confirmed" ||
status.confirmationStatus === "finalized"
) {
onStatusUpdate?.({ status: "confirmed", result: status });
return signature;
// Poll for confirmation
let pollTimeout = delayBetweenRetries;
const timeBetweenPolls = 500;
while (pollTimeout > 0) {
await new Promise((resolve) => setTimeout(resolve, timeBetweenPolls));
const response = await connection.getSignatureStatus(signature);
if (response?.value) {
status = response.value;
if (
status.confirmationStatus === "confirmed" ||
status.confirmationStatus === "finalized"
) {
onStatusUpdate?.({ status: "confirmed", result: status });
return signature;
}
}
pollTimeout -= timeBetweenPolls;
}
} catch (error: unknown) {
if (error instanceof Error) {
Expand All @@ -246,8 +257,10 @@ export async function sendTransactionWithRetry(
}

retries++;

if (retries < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, initialDelayMs));
onStatusUpdate?.({ status: "retry", signature: signature ?? null });
delayBetweenRetries += RETRY_INTERVAL_INCREASE;
}
}

Expand All @@ -260,7 +273,7 @@ export async function sendTransactionWithRetry(
* @param connection - The Solana connection object
* @param tx - The transaction to prepare
* @param payer - The public key of the transaction payer
* @param priorityFee - Priority fee in microLamports (default: 1000)
* @param priorityFee - Priority fee in microLamports (default: 10000 which is the minimum required for helius to see a transaction as priority)
* @param computeUnitBuffer - Optional buffer to add to simulated compute units
*
* @remarks
Expand Down Expand Up @@ -299,8 +312,9 @@ export async function prepareTransactionWithCompute(
connection: Connection,
tx: Transaction,
payer: PublicKey,
priorityFee: number = 1000,
priorityFee: number = 10000,
computeUnitBuffer: ComputeUnitBuffer = {},
commitment: Commitment = "confirmed",
): Promise<void> {
tx.add(
ComputeBudgetProgram.setComputeUnitPrice({
Expand All @@ -313,6 +327,7 @@ export async function prepareTransactionWithCompute(
tx.instructions,
payer,
[],
commitment,
);

if (simulatedCompute === null) {
Expand Down
2 changes: 1 addition & 1 deletion tests/src/token.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe("makeTokenMint", () => {
describe("createAccountsMintsAndTokenAccounts", () => {
test("createAccountsMintsAndTokenAccounts works", async () => {
const payer = Keypair.generate();
const connection = new Connection(LOCALHOST);
const connection = new Connection(LOCALHOST, "confirmed");
await airdropIfRequired(
connection,
payer.publicKey,
Expand Down
11 changes: 7 additions & 4 deletions tests/src/transaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ describe("getSimulationComputeUnits", () => {
});

describe("Transaction utilities", () => {
test.only("sendTransactionWithRetry should send and confirm a transaction", async () => {
test("sendTransactionWithRetry should send and confirm a transaction", async () => {
const connection = new Connection(LOCALHOST);
const sender = Keypair.generate();
await airdropIfRequired(
Expand Down Expand Up @@ -129,8 +129,10 @@ describe("Transaction utilities", () => {
transaction,
[sender],
{
commitment: "confirmed",
onStatusUpdate: (status) => statusUpdates.push(status),
onStatusUpdate: (status) => {
statusUpdates.push(status);
console.log("status", status);
},
},
);

Expand All @@ -141,9 +143,10 @@ describe("Transaction utilities", () => {
);
});

test.only("prepareTransactionWithCompute should add compute budget instructions", async () => {
test("prepareTransactionWithCompute should add compute budget instructions", async () => {
const connection = new Connection(LOCALHOST);
const sender = Keypair.generate();

await airdropIfRequired(
connection,
sender.publicKey,
Expand Down

0 comments on commit 41cbf50

Please sign in to comment.