Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Combine prepareCompute and Retries #73

Merged
merged 3 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 2.7

- Added `sendTransaction()` to send transactions with compute unit optimization and automatic retries.
- Removed `sendTransactionWithRetry()` as sendTransaction() is more convenient.

## 2.6

- Added Transaction send helpers. `prepareTransactionWithCompute()` and `sendTransactionWithRetry()`
Expand Down
74 changes: 35 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,77 +496,73 @@ To just run tests matching the name `getCustomErrorMessage`.

### Transaction Utilities

#### `sendTransactionWithRetry`
#### `sendTransaction`

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

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

```typescript
const signature = await sendTransactionWithRetry(
const signature = await sendTransaction(connection, transaction, [payer]);
```

The function is also customizable if you do not like the defaults:

```typescript
const signature = await sendTransaction(
connection,
transaction,
signers,
[payer],
10000, // priority fee in microLamports
{
commitment: "confirmed",
computeUnitBuffer: { multiplier: 1.1 }, // add 10% buffer to compute units
onStatusUpdate: (status) => console.log(status),
maxRetries: 30,
commitment: "confirmed",
maxRetries: 10,
initialDelayMs: 2000,
},
);
```

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

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

await prepareTransactionWithCompute(
connection,
tx,
keyPair.publicKey,
priorityFee
);

// can either sign the transaction here, or in the sendTransactionWithRetry function
tx.sign(keyPair);

var signature = await sendTransactionWithRetry(connection, tx, [], {
onStatusUpdate: (status) => {
console.log("Transaction status:", status);
},
});
For RPC providers that support priority fees:

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

#### `prepareTransactionWithCompute`

Prepares a transaction with compute unit calculations and limits. This function:

1. Simulates the transaction to determine required compute units
2. Adds compute budget instructions for both price and unit limit
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)
If you need more control, you can prepare compute units separately:

```typescript
await prepareTransactionWithCompute(
connection,
transaction,
payer.publicKey,
1000, // priority fee in microLamports
10000, // priority fee
{
multiplier: 1.1, // add 10% buffer
fixed: 100, // add fixed amount of CUs
},
);
```

Both functions help with common transaction handling tasks in Solana, making it easier to send reliable transactions with appropriate compute unit settings.
This will:

1. Simulate the transaction to determine required compute units
2. Add compute budget instructions for both price and unit limit
3. Apply any specified compute unit buffers

## Anchor IDL Utilities

Expand Down
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@
"bs58": "^6.0.0",
"dotenv": "^16.4.5",
"@coral-xyz/anchor": "^0.30.1"

},
"devDependencies": {
"@types/bn.js": "^5.1.6",
"@types/node": "^20.16.1",
"esbuild": "^0.23.1",
"esbuild-register": "^3.6.0",
Expand Down
113 changes: 109 additions & 4 deletions src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
TransactionMessage,
VersionedTransaction,
Message,
MessageV0,
MessageCompiledInstruction,
} from "@solana/web3.js";
import { getErrorFromRPCResponse } from "./logs";
Expand All @@ -22,8 +21,8 @@ import {
EventParser,
BorshAccountsCoder,
BorshInstructionCoder,
BN,
} from "@coral-xyz/anchor";
import BN from "bn.js";
import * as fs from "fs";
import * as path from "path";

Expand Down Expand Up @@ -82,7 +81,10 @@ export const getSimulationComputeUnits = async (

if (rpcResponse?.value?.err) {
const logs = rpcResponse.value.logs?.join("\n • ") || "No logs available";
throw new Error(`Transaction simulation failed:\n •${logs}`);
throw new Error(
`Transaction simulation failed:\n •${logs}` +
JSON.stringify(rpcResponse?.value?.err),
);
}

return rpcResponse.value.unitsConsumed || null;
Expand Down Expand Up @@ -188,7 +190,7 @@ export const DEFAULT_SEND_OPTIONS: Required<
* );
* ```
*/
export async function sendTransactionWithRetry(
Copy link
Contributor

Choose a reason for hiding this comment

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

Why no longer exporting this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it will be confusing people and they will think the sendTransactionWithRetry is better than sendTransaction. SendTransaction uses sendwithRetry internally

async function sendTransactionWithRetry(
connection: Connection,
transaction: Transaction,
signers: Keypair[],
Expand All @@ -207,6 +209,18 @@ export async function sendTransactionWithRetry(
transaction.sign(...signers);
}

if (transaction.recentBlockhash === undefined) {
console.log("No blockhash provided. Setting recent blockhash");
const { blockhash } = await connection.getLatestBlockhash(commitment);
transaction.recentBlockhash = blockhash;
}
if (transaction.feePayer === undefined) {
if (signers.length === 0) {
throw new Error("No signers or fee payer provided");
}
transaction.feePayer = signers[0].publicKey;
}

onStatusUpdate?.({ status: "signed" });

let signature: string | null = null;
Expand Down Expand Up @@ -624,3 +638,94 @@ function formatData(data: any): any {
}
return data;
}

/**
* Sends a transaction with compute unit optimization and automatic retries
*
* @param connection - The Solana connection object
* @param transaction - The transaction to send
* @param signers - Array of signers needed for the transaction
* @param priorityFee - Priority fee in microLamports (default: 10000 which is the minimum required for helius to see a transaction as priority)
* @param options - Optional configuration for retry mechanism and compute units
* @returns Promise that resolves to the transaction signature
*
* @example
* ```typescript
* const signature = await sendTransaction(
* connection,
* transaction,
* [payer],
* 10000,
* {
* computeUnitBuffer: { multiplier: 1.1 },
* onStatusUpdate: (status) => console.log(status),
* }
* );
* ```
*/
export async function sendTransaction(
connection: Connection,
transaction: Transaction,
signers: Keypair[],
priorityFee: number = 10000,
options: SendTransactionOptions & {
computeUnitBuffer?: ComputeUnitBuffer;
} = {},
): Promise<string> {
const {
computeUnitBuffer: userComputeBuffer, // Rename to make clear it's user provided
commitment = "confirmed",
...sendOptions
} = options;

// Use user provided buffer or default to 1.1 multiplier
const computeUnitBuffer = userComputeBuffer ?? { multiplier: 1.1 };

if (transaction.recentBlockhash === undefined) {
console.log("No blockhash provided. Setting recent blockhash");
const { blockhash } = await connection.getLatestBlockhash(commitment);
transaction.recentBlockhash = blockhash;
}
if (transaction.feePayer === undefined) {
if (signers.length === 0) {
throw new Error("No signers or fee payer provided");
}
transaction.feePayer = signers[0].publicKey;
}

// Skip compute preparation if transaction is already signed or has compute instructions
if (transaction.signatures.length > 0) {
console.log("Transaction already signed, skipping compute preparation");
return sendTransactionWithRetry(connection, transaction, signers, {
commitment,
...sendOptions,
});
}

const hasComputeInstructions = transaction.instructions.some((ix) =>
ix.programId.equals(ComputeBudgetProgram.programId),
);

if (hasComputeInstructions) {
console.log(
"Transaction already has compute instructions, skipping compute preparation",
);
return sendTransactionWithRetry(connection, transaction, signers, {
commitment,
...sendOptions,
});
}
await prepareTransactionWithCompute(
connection,
transaction,
transaction.feePayer,
priorityFee,
computeUnitBuffer,
commitment,
);

return sendTransactionWithRetry(connection, transaction, signers, {
commitment,
...sendOptions,
});
}
Loading