Skip to content

Commit 4fffc8f

Browse files
authored
Merge pull request #73 from solana-developers/improve-send-transaction-helpers
Combine prepareCompute and Retries
2 parents 854f3ed + 32dedd7 commit 4fffc8f

File tree

6 files changed

+213
-46
lines changed

6 files changed

+213
-46
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.7
2+
3+
- Added `sendTransaction()` to send transactions with compute unit optimization and automatic retries.
4+
- Removed `sendTransactionWithRetry()` as sendTransaction() is more convenient.
5+
16
## 2.6
27

38
- Added Transaction send helpers. `prepareTransactionWithCompute()` and `sendTransactionWithRetry()`

README.md

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -496,77 +496,73 @@ To just run tests matching the name `getCustomErrorMessage`.
496496

497497
### Transaction Utilities
498498

499-
#### `sendTransactionWithRetry`
499+
#### `sendTransaction`
500500

501-
Sends a transaction with automatic retries and status updates. This function implements a robust retry mechanism that:
501+
Sends a transaction with compute unit optimization and automatic retries. This function:
502502

503-
1. Signs the transaction (if signers are provided)
504-
2. Sends the transaction only once
505-
3. Monitors the transaction status until confirmation
506-
4. Retries on failure with a fixed delay
507-
5. Provides detailed status updates through a callback
503+
1. Automatically sets compute units based on simulation
504+
2. Adds priority fees for faster processing
505+
3. Handles retries and confirmation
506+
4. Provides detailed status updates
508507

509508
```typescript
510-
const signature = await sendTransactionWithRetry(
509+
const signature = await sendTransaction(connection, transaction, [payer]);
510+
```
511+
512+
The function is also customizable if you do not like the defaults:
513+
514+
```typescript
515+
const signature = await sendTransaction(
511516
connection,
512517
transaction,
513-
signers,
518+
[payer],
519+
10000, // priority fee in microLamports
514520
{
515-
commitment: "confirmed",
521+
computeUnitBuffer: { multiplier: 1.1 }, // add 10% buffer to compute units
516522
onStatusUpdate: (status) => console.log(status),
517-
maxRetries: 30,
523+
commitment: "confirmed",
524+
maxRetries: 10,
518525
initialDelayMs: 2000,
519526
},
520527
);
521528
```
522529

523-
Best combined with `prepareTransactionWithCompute` to ensure the transaction requests the minimum compute units and sets priority fees.
530+
The function will:
524531

525-
```typescript
526-
// This could be really nice if RPC providers would all have the same API...
527-
// Please fall back to the fee api of your favourite RPC provider to get a good value.
528-
const priorityFee = 1000;
532+
- Skip compute preparation if transaction is already signed
533+
- Skip compute preparation if transaction already has compute budget instructions
534+
- Add compute budget instructions if needed
535+
- Handle retries and confirmation automatically
536+
- Provide status updates: "created" → "signed" → "sent" → "confirmed"
529537

530-
await prepareTransactionWithCompute(
531-
connection,
532-
tx,
533-
keyPair.publicKey,
534-
priorityFee
535-
);
536-
537-
// can either sign the transaction here, or in the sendTransactionWithRetry function
538-
tx.sign(keyPair);
539-
540-
var signature = await sendTransactionWithRetry(connection, tx, [], {
541-
onStatusUpdate: (status) => {
542-
console.log("Transaction status:", status);
543-
},
544-
});
538+
For RPC providers that support priority fees:
545539

546-
```
540+
- Helius: minimum 10000 microLamports
541+
- Triton: see their [priority fee API](https://docs.triton.one/chains/solana/improved-priority-fees-api)
542+
- Quicknode: see their [priority fee estimation](https://www.quicknode.com/docs/solana/qn_estimatePriorityFees)
547543

548544
#### `prepareTransactionWithCompute`
549545

550-
Prepares a transaction with compute unit calculations and limits. This function:
551-
552-
1. Simulates the transaction to determine required compute units
553-
2. Adds compute budget instructions for both price and unit limit
554-
3. Supports buffer settings to add safety margins (This is useful when inteacting with defi for examples where the price or route may change during the transaction)
546+
If you need more control, you can prepare compute units separately:
555547

556548
```typescript
557549
await prepareTransactionWithCompute(
558550
connection,
559551
transaction,
560552
payer.publicKey,
561-
1000, // priority fee in microLamports
553+
10000, // priority fee
562554
{
563555
multiplier: 1.1, // add 10% buffer
564556
fixed: 100, // add fixed amount of CUs
565557
},
566558
);
567559
```
568560

569-
Both functions help with common transaction handling tasks in Solana, making it easier to send reliable transactions with appropriate compute unit settings.
561+
This will:
562+
563+
1. Simulate the transaction to determine required compute units
564+
2. Add compute budget instructions for both price and unit limit
565+
3. Apply any specified compute unit buffers
570566

571567
## Anchor IDL Utilities
572568

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"@coral-xyz/anchor": "^0.30.1"
6969
},
7070
"devDependencies": {
71+
"@types/bn.js": "^5.1.6",
7172
"@types/node": "^20.16.1",
7273
"esbuild": "^0.23.1",
7374
"esbuild-register": "^3.6.0",

src/lib/transaction.ts

Lines changed: 109 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
TransactionMessage,
1212
VersionedTransaction,
1313
Message,
14-
MessageV0,
1514
MessageCompiledInstruction,
1615
} from "@solana/web3.js";
1716
import { getErrorFromRPCResponse } from "./logs";
@@ -22,8 +21,8 @@ import {
2221
EventParser,
2322
BorshAccountsCoder,
2423
BorshInstructionCoder,
25-
BN,
2624
} from "@coral-xyz/anchor";
25+
import BN from "bn.js";
2726
import * as fs from "fs";
2827
import * as path from "path";
2928

@@ -82,7 +81,10 @@ export const getSimulationComputeUnits = async (
8281

8382
if (rpcResponse?.value?.err) {
8483
const logs = rpcResponse.value.logs?.join("\n • ") || "No logs available";
85-
throw new Error(`Transaction simulation failed:\n •${logs}`);
84+
throw new Error(
85+
`Transaction simulation failed:\n •${logs}` +
86+
JSON.stringify(rpcResponse?.value?.err),
87+
);
8688
}
8789

8890
return rpcResponse.value.unitsConsumed || null;
@@ -188,7 +190,7 @@ export const DEFAULT_SEND_OPTIONS: Required<
188190
* );
189191
* ```
190192
*/
191-
export async function sendTransactionWithRetry(
193+
async function sendTransactionWithRetry(
192194
connection: Connection,
193195
transaction: Transaction,
194196
signers: Keypair[],
@@ -207,6 +209,18 @@ export async function sendTransactionWithRetry(
207209
transaction.sign(...signers);
208210
}
209211

212+
if (transaction.recentBlockhash === undefined) {
213+
console.log("No blockhash provided. Setting recent blockhash");
214+
const { blockhash } = await connection.getLatestBlockhash(commitment);
215+
transaction.recentBlockhash = blockhash;
216+
}
217+
if (transaction.feePayer === undefined) {
218+
if (signers.length === 0) {
219+
throw new Error("No signers or fee payer provided");
220+
}
221+
transaction.feePayer = signers[0].publicKey;
222+
}
223+
210224
onStatusUpdate?.({ status: "signed" });
211225

212226
let signature: string | null = null;
@@ -624,3 +638,94 @@ function formatData(data: any): any {
624638
}
625639
return data;
626640
}
641+
642+
/**
643+
* Sends a transaction with compute unit optimization and automatic retries
644+
*
645+
* @param connection - The Solana connection object
646+
* @param transaction - The transaction to send
647+
* @param signers - Array of signers needed for the transaction
648+
* @param priorityFee - Priority fee in microLamports (default: 10000 which is the minimum required for helius to see a transaction as priority)
649+
* @param options - Optional configuration for retry mechanism and compute units
650+
* @returns Promise that resolves to the transaction signature
651+
*
652+
* @example
653+
* ```typescript
654+
* const signature = await sendTransaction(
655+
* connection,
656+
* transaction,
657+
* [payer],
658+
* 10000,
659+
* {
660+
* computeUnitBuffer: { multiplier: 1.1 },
661+
* onStatusUpdate: (status) => console.log(status),
662+
* }
663+
* );
664+
* ```
665+
*/
666+
export async function sendTransaction(
667+
connection: Connection,
668+
transaction: Transaction,
669+
signers: Keypair[],
670+
priorityFee: number = 10000,
671+
options: SendTransactionOptions & {
672+
computeUnitBuffer?: ComputeUnitBuffer;
673+
} = {},
674+
): Promise<string> {
675+
const {
676+
computeUnitBuffer: userComputeBuffer, // Rename to make clear it's user provided
677+
commitment = "confirmed",
678+
...sendOptions
679+
} = options;
680+
681+
// Use user provided buffer or default to 1.1 multiplier
682+
const computeUnitBuffer = userComputeBuffer ?? { multiplier: 1.1 };
683+
684+
if (transaction.recentBlockhash === undefined) {
685+
console.log("No blockhash provided. Setting recent blockhash");
686+
const { blockhash } = await connection.getLatestBlockhash(commitment);
687+
transaction.recentBlockhash = blockhash;
688+
}
689+
if (transaction.feePayer === undefined) {
690+
if (signers.length === 0) {
691+
throw new Error("No signers or fee payer provided");
692+
}
693+
transaction.feePayer = signers[0].publicKey;
694+
}
695+
696+
// Skip compute preparation if transaction is already signed or has compute instructions
697+
if (transaction.signatures.length > 0) {
698+
console.log("Transaction already signed, skipping compute preparation");
699+
return sendTransactionWithRetry(connection, transaction, signers, {
700+
commitment,
701+
...sendOptions,
702+
});
703+
}
704+
705+
const hasComputeInstructions = transaction.instructions.some((ix) =>
706+
ix.programId.equals(ComputeBudgetProgram.programId),
707+
);
708+
709+
if (hasComputeInstructions) {
710+
console.log(
711+
"Transaction already has compute instructions, skipping compute preparation",
712+
);
713+
return sendTransactionWithRetry(connection, transaction, signers, {
714+
commitment,
715+
...sendOptions,
716+
});
717+
}
718+
await prepareTransactionWithCompute(
719+
connection,
720+
transaction,
721+
transaction.feePayer,
722+
priorityFee,
723+
computeUnitBuffer,
724+
commitment,
725+
);
726+
727+
return sendTransactionWithRetry(connection, transaction, signers, {
728+
commitment,
729+
...sendOptions,
730+
});
731+
}

tests/src/transaction.test.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import {
77
SystemProgram,
88
TransactionInstruction,
99
PublicKey,
10+
ComputeBudgetProgram,
1011
} from "@solana/web3.js";
1112
import {
1213
airdropIfRequired,
1314
confirmTransaction,
1415
getSimulationComputeUnits,
1516
prepareTransactionWithCompute,
16-
sendTransactionWithRetry,
17+
sendTransaction,
1718
} from "../../src";
1819
import { sendAndConfirmTransaction } from "@solana/web3.js";
1920
import assert from "node:assert";
@@ -99,7 +100,7 @@ describe("getSimulationComputeUnits", () => {
99100
});
100101

101102
describe("Transaction utilities", () => {
102-
test("sendTransactionWithRetry should send and confirm a transaction", async () => {
103+
test("sendTransaction without priority fee should send and confirm a transaction", async () => {
103104
const connection = new Connection(LOCALHOST);
104105
const sender = Keypair.generate();
105106
await airdropIfRequired(
@@ -124,10 +125,11 @@ describe("Transaction utilities", () => {
124125
transaction.feePayer = sender.publicKey;
125126

126127
const statusUpdates: any[] = [];
127-
const signature = await sendTransactionWithRetry(
128+
const signature = await sendTransaction(
128129
connection,
129130
transaction,
130131
[sender],
132+
0,
131133
{
132134
onStatusUpdate: (status) => {
133135
statusUpdates.push(status);
@@ -192,4 +194,52 @@ describe("Transaction utilities", () => {
192194
);
193195
});
194196
});
197+
198+
test("sendTransaction should prepare and send a transaction and add priority fee instructions", async () => {
199+
const connection = new Connection(LOCALHOST);
200+
const sender = Keypair.generate();
201+
await airdropIfRequired(
202+
connection,
203+
sender.publicKey,
204+
2 * LAMPORTS_PER_SOL,
205+
1 * LAMPORTS_PER_SOL,
206+
);
207+
const recipient = Keypair.generate();
208+
209+
const transaction = new Transaction().add(
210+
SystemProgram.transfer({
211+
fromPubkey: sender.publicKey,
212+
toPubkey: recipient.publicKey,
213+
lamports: LAMPORTS_PER_SOL * 0.1,
214+
}),
215+
);
216+
217+
const statusUpdates: any[] = [];
218+
const signature = await sendTransaction(
219+
connection,
220+
transaction,
221+
[sender],
222+
1000,
223+
{
224+
computeUnitBuffer: { multiplier: 1.1 },
225+
onStatusUpdate: (status) => {
226+
statusUpdates.push(status);
227+
console.log("status", status);
228+
},
229+
},
230+
);
231+
232+
assert.ok(signature);
233+
assert.deepEqual(
234+
statusUpdates.map((s) => s.status),
235+
["created", "signed", "sent", "confirmed"],
236+
);
237+
238+
// Verify compute budget instructions were added
239+
assert.equal(transaction.instructions.length, 3); // transfer + 2 compute budget instructions
240+
const computeInstructions = transaction.instructions.filter((ix) =>
241+
ix.programId.equals(ComputeBudgetProgram.programId),
242+
);
243+
assert.equal(computeInstructions.length, 2);
244+
});
195245
});

0 commit comments

Comments
 (0)