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

Add send transaction to TS and Rust code examples #589

Merged
merged 13 commits into from
Dec 26, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ This document covers the essential setup required to start building on Orca’s
Install the necessary packages:

```bash
npm install typescript @orca-so/whirlpools @solana/web3.js@rc
npm install typescript @orca-so/whirlpools @solana/web3.js@2
Copy link
Member

Choose a reason for hiding this comment

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

Is version 2 now recommended (meaning you can remove the tag)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

npm install @solana/web3.js installs v1.98.0

```

Initialize the project as a TypeScript project:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ To execute a token swap in an Orca Whirlpool, follow these steps:

await setWhirlpoolsConfig('solanaDevnet');
const devnetRpc = createSolanaRpc(devnet('https://api.devnet.solana.com'));
const wallet = await loadWallet(); // CAUTION: This wallet is not persistent.
const wallet = await loadWallet();
const whirlpoolAddress = address("3KBZiL2g8C7tiJ32hTv5v3KM7aK9htpqTw4cTXz1HvPt");
const mintAddress = address("BRjpCHtyQLNCo8gqRUr8jtdAj5AjPYQaoqbvcZiHok1k");
const inputAmount = 1_000_000n;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
---
sidebar_label: Send Transactions
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Sending and Landing Transactions
In this guide, we'll explore how to send the instructions using the Solana SDK - both in Typescript and Rust. We'll cover the following key topics:

- Client-side retry
- Prioritization fees
- Compute budget estimation

We also cover key considerations for sending transactions in web applications with wallet extensions, along with additional steps to improve transaction landing.

Make sure you check out [this doc](./02-Environment%20Setup.mdx) to set up your environment.

## Code Overview
### 1. Dependencies
Let's start by importing the necessary dependencies from Solana's SDKs.

<Tabs groupId="programming-languages">
<TabItem value="ts" label="Typescript" default>
```json title="package.json"
"dependencies": {
"@solana-program/compute-budget": "^0.6.1",
"@solana/web3.js": "^2.0.0",
},
```
```tsx title="sendTransaction.ts"
import {
createSolanaRpc,
address,
pipe,
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
prependTransactionMessageInstructions,
signTransactionMessageWithSigners,
getComputeUnitEstimateForTransactionMessageFactory,
getBase64EncodedWireTransaction,
setTransactionMessageFeePayerSigner
} from '@solana/web3.js';
import {
getSetComputeUnitLimitInstruction,
getSetComputeUnitPriceInstruction
} from '@solana-program/compute-budget';
```
</TabItem>
<TabItem value="rust" label="Rust">
```toml title="Cargo.toml"
serde_json = { version = "^1.0" }
solana-client = { version = "^1.18" }
solana-sdk = { version = "^1.18" }
tokio = { version = "^1.41.1" }
```
```rust title="main.rs"
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_client::rpc_config::RpcSendTransactionConfig;
use solana_sdk::commitment_config::CommitmentLevel;
use solana_sdk::compute_budget::ComputeBudgetInstruction;
use solana_sdk::message::Message;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Signature;
use solana_sdk::transaction::Transaction;
use solana_sdk::{signature::Keypair, signer::Signer};
use std::fs;
use std::str::FromStr;
use tokio::time::{sleep, Duration, Instant};
```
</TabItem>
</Tabs>

### 2. Create Transaction Message From Instructions
To send a transaction on Solana, you need to include a blockhash to the transaction. A blockhash acts as a timestamp and ensures the transaction has a limited lifetime. Validators use the blockhash to verify the recency of a transaction before including it in a block. A transaction referencing a blockhash is only valid for 150 blocks (~1-2 minutes, depending on slot time). After that, the blockhash expires, and the transaction will be rejected.
Copy link
Member

Choose a reason for hiding this comment

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

You can also sign using a durable nonce. You could add a small note that that is also possible but not what we are using in this example


You also need to add the signers to the transactions. With Solana web3.js v2, you can create instructions and add additional signers as `TransactionSigner` to the instructions. The Typescript Whirlpools SDK leverages this functioanlity and appends all additional signers to the instructions for you. In Rust, this feautures is not available. Therefore, the Rust Whirlpools SDK may return `instruction_result.additional_signers` if there are any, and you need to manually append them to the transaction.

Here's how the transaction message is created:

<Tabs groupId="programming-languages">
<TabItem value="ts" label="Typescript" default>
```tsx title="sendTransaction.ts"
const { instructions } = // get instructions from Whirlpools SDK
const latestBlockHash = await rpc.getLatestBlockhash().send();
const transactionMessage = await pipe(
createTransactionMessage({ version: 0}),
tx => setTransactionMessageFeePayer(wallet.address, tx),
tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockHash.value, tx),
tx => appendTransactionMessageInstructions(instructions, tx)
)
```
</TabItem>
<TabItem value="rust" label="Rust">
```rust title="main.rs"
#[tokio::main]
async fn main() {
// ...
let instructions_result = // get instructions from Whirlpools SDK
let message = Message::new(
&instructions_result.instructions,
Some(&wallet.pubkey()),
);
let mut signers: Vec<&dyn Signer> = vec![&wallet];
signers.extend(
instructions_result
.additional_signers
.iter()
.map(|kp| kp as &dyn Signer),
);
let recent_blockhash = rpc.get_latest_blockhash().await.unwrap();
let transaction = Transaction::new(&signers, message, recent_blockhash);
// ...
}
```
</TabItem>
</Tabs>

### 3. Estimating Compute Unit Limit and Prioritization Fee
Before sending a transaction, it's important to set a compute unit limit and an appropriate prioritization fee.

Transactions that request fewer compute units get high priority for the same amount of prioritization fee (which is defined per compute unit). Setting the compute units too low will result in a failed transaction.
Copy link
Member

Choose a reason for hiding this comment

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

Nuance: will result in a failed transaction -> might result in your transaction not landing/being included in a block and expiring or somethuing like that

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 thought that if you set the compute units too low, you have a higher chance of it being picked up and included, but you risk transaction failure due to compute unit limit.

Copy link
Member

Choose a reason for hiding this comment

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

Ahh right yep compute units too low will fail the transaction. My bad read it wrong


You can get an estimate of the compute units by simulating the transaction on the RPC. To avoid transaction failures caused by underestimating this limit, you can add an additional 100,000 compute units, but you can adjust this based on your own tests.

The prioritization fee per compute unit also incentivizes validators to prioritize your transaction, especially during times of network congestion. You can get a list of recently paid prioritization fees, sort them and select a value from that list. In this example, we select the 50th percentile, but you can adjust this if needed. The prioritization fee is provided in micro-lamports per compute unit. The total priority fee in lamports you will pay is calculated as $(\text{estimated compute units} \cdot \text{prioritization fee}) / 10^6$.
Copy link
Member

Choose a reason for hiding this comment

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

Might be good to mention that the endpoint returns the lowest priority fee payed of all the transactions that landed in a given slot.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice, I didn't know this



<Tabs groupId="programming-languages">
<TabItem value="ts" label="Typescript" default>
```tsx title="sendTransaction.ts"
const getComputeUnitEstimateForTransactionMessage =
getComputeUnitEstimateForTransactionMessageFactory({
rpc
});
const computeUnitEstimate = await getComputeUnitEstimateForTransactionMessage(transactionMessage) + 100_000;
const medianPrioritizationFee = await rpc.getRecentPrioritizationFees()
.send()
.then(fees =>
fees.sort((a, b) => a.prioritizationFee - b.prioritizationFee)
[Math.floor(fees.length / 2)].prioritizationFee
)
const transactionMessageWithComputeUnitInstructions = await prependTransactionMessageInstructions([
getSetComputeUnitLimitInstruction({ units: computeUnitEstimate }),
getSetComputeUnitPriceInstruction({ microLamports: medianPrioritizationFee })
], transactionMessage);
```
</TabItem>
<TabItem value="rust" label="Rust">
```rust title="main.rs"
#[tokio::main]
async fn main() {
// ...
let simulated_transaction = rpc.simulate_transaction(&transaction).await.unwrap();

let mut all_instructions = vec![];
if let Some(units_consumed) = simulated_transaction.value.units_consumed {
let units_consumed_safe = units_consumed as u32 + 100_000;
let compute_limit_instruction =
ComputeBudgetInstruction::set_compute_unit_limit(units_consumed_safe);
all_instructions.push(compute_limit_instruction);

let prioritization_fees = rpc
.get_recent_prioritization_fees(&[whirlpool_address])
.await
.unwrap();
let mut prioritization_fees_array: Vec<u64> = prioritization_fees
.iter()
.map(|fee| fee.prioritization_fee)
.collect();
prioritization_fees_array.sort_unstable();
let prioritization_fee = prioritization_fees_array
.get(prioritization_fees_array.len() / 2)
.cloned();

if let Some(prioritization_fee) = prioritization_fee {
let priority_fee_instruction =
ComputeBudgetInstruction::set_compute_unit_price(prioritization_fee);
all_instructions.push(priority_fee_instruction);
}
}
// ...
}
```
</TabItem>
</Tabs>

### 4. Sign and Submit Transaction
Finally, the transaction needs to be signed, encoded, and submitted to the network. A client-side time-base retry mechanism ensures that the transaction is repeatedly sent until it is confirmed or the time runs out. We use a time-based loop, because we know that the lifetime of a transaction is 150 blocks, which on average takes about 79-80 seconds. The signing of the transactions is an idempotent operation and produces a transaction hash, which acts as the transaction ID. Since transactions can be added only once to the block chain, we can keep sending the transaction during the lifetime of the trnsaction.

You're probably wondering why we don't just use the widely used `sendAndConfirm` method. This is because the retry mechanism of the `sendAndConfirm` method is executed on the RPC. By default, RPC nodes will try to forward (rebroadcast) transactions to leaders every two seconds until either the transaction is finalized, or the transaction's blockhash expires. If the outstanding rebroadcast queue size is greater than 10,000 transaction, newly submitted transactions are dropped. This means that at times of congestion, your transaction might not even arrive at the RPC in the first place. Moreover, the `confirmTransaction` RPC method that `sendAndConfirm` calls is deprecated.

<Tabs groupId="programming-languages">
<TabItem value="ts" label="Typescript" default>
```tsx title="sendTransaction.ts"
const signedTransaction = await signTransactionMessageWithSigners(transactionMessageWithComputeUnitInstructions)
Copy link
Member

Choose a reason for hiding this comment

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

Maybe good to explain that signTransactionMessageWithSigners only signs with signers you provide. If you provided a noopSigner (for example for a frontend wallet) you need to still manually sign with that wallet.

const base64EncodedWireTransaction = getBase64EncodedWireTransaction(signedTransaction);

const timeoutMs = 90000;
const startTime = Date.now();

while (Date.now() - startTime < timeoutMs) {
const transactionStartTime = Date.now();

const signature = await rpc.sendTransaction(base64EncodedWireTransaction, {
maxRetries: 0n,
skipPreflight: true,
encoding: 'base64'
}).send();

const statuses = await rpc.getSignatureStatuses([signature]).send();
if (statuses.value[0]) {
if (!statuses.value[0].err) {
console.log(`Transaction confirmed: ${signature}`);
break;
} else {
console.error(`Transaction failed: ${statuses.value[0].err.toString()}`);
break;
}
}

const elapsedTime = Date.now() - transactionStartTime;
const remainingTime = Math.max(0, 1000 - elapsedTime);
if (remainingTime > 0) {
await new Promise(resolve => setTimeout(resolve, remainingTime));
}
}
```
</TabItem>
<TabItem value="rust" label="Rust">
```rust title="main.rs"
#[tokio::main]
async fn main() {
// ...
all_instructions.extend(open_position_instructions.instructions);
let message = Message::new(&all_instructions, Some(&wallet.pubkey()));

let transaction = Transaction::new(& , message, recent_blockhash);
Copy link
Member

Choose a reason for hiding this comment

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

?

let transaction_config = RpcSendTransactionConfig {
skip_preflight: true,
preflight_commitment: Some(CommitmentLevel::Confirmed),
max_retries: Some(0),
..Default::default()
};

let start_time = Instant::now();
let timeout = Duration::from_secs(90);
let send_transaction_result = loop {
if start_time.elapsed() >= timeout {
break Err(Box::<dyn std::error::Error>::from("Transaction timed out"));
}
let transaction_start_time = Instant::now();

let signature: Signature = rpc
.send_transaction_with_config(&transaction, transaction_config)
.await
.unwrap();
let statuses = rpc
.get_signature_statuses(&[signature])
.await
.unwrap()
.value;

if let Some(status) = statuses[0].clone() {
break Ok((status, signature));
}

let elapsed_time = transaction_start_time.elapsed();
let remaining_time = Duration::from_millis(1000).saturating_sub(elapsed_time);
if remaining_time > Duration::ZERO {
sleep(remaining_time).await;
}
};

let signature = send_transaction_result.and_then(|(status, signature)| {
if let Some(err) = status.err {
Err(Box::new(err))
} else {
Ok(signature)
}
});
println!("Result: {:?}", signature);
}
```
</TabItem>
</Tabs>
Copy link
Member

Choose a reason for hiding this comment

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

Might be nice to add a section about further improvements you could make:

  • Sending to multiple rpc nodes concurrently
  • Sending to jito block engine and adding a jito tip
  • swQoS
  • More?


## Handling transactions with Wallets in web apps.
#### Creating Noop Signers
When sending transactions from your web application, users need to sign the transaction using their wallet. Since the transaction needs to assembled beforehand, you can create a `noopSigner` (no-operation signer). This will act as a placeholder for you instructions, indicating that a given account is a signer and the signature wil be added later. After the user of your web app signs the transaction with their wallet, you need to manually add that signature to the transaction.
Copy link
Member

Choose a reason for hiding this comment

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

Think you can just pass the serialized transaction to the wallet extension and they will return a serialized transaction (with the added signature). add the signature manually might sound a little more complicated than it actually is


#### Prioritization Fees
Browser wallets, like Phantom, will calculate and apply priority fees for your transactions, provided:
Copy link
Member

Choose a reason for hiding this comment

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

Browser wallets -> Some wallets? Think Phantom app also adds prio fee

- The transaction does not already have signatures present.
- The transaction does not have existing compute-budget instructions.
- The transactions will still be less than the maximum transaction size fo 1232 bytes, after applying compute-budget instructions.

## Additional Improvements for Landing Transactions
- You could send your transaction to multiple RPC nodes at the same time, all within each iteration of the time-based loop.
- At the time of writing, 85% of Solana validators are Jito validators. Jito validators happily accept an additional tip, in the form a SOL transfer, to prioritize a transaction. A good place to get familiarized with Jito is here: https://www.jito.network/blog/jito-solana-is-now-open-source/
- Solana gives staked validators more reliable performance when sending transactions by routing them through prioritized connections. This mechanism is referred to as stake-weighted Quality of Service (swQoS). Validators can extend this service to RPC nodes, essentially giving staked connections to RPC nodes as if they were validators with that much stake in the network. RPC providers, like Helius and Titan, expose such peered RPC nodes to paid users, allowing users to send transactions to RPC nodes which use the validator's staked connections. From the RPC, the transaction is then sent over the staked connection with a lower likelihood of being delayed or dropped.
18 changes: 10 additions & 8 deletions ts-sdk/whirlpool/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@ The Orca Whirlpools SDK provides a comprehensive set of tools to interact with t
To install the SDK, use the following command:

```sh
npm install @orca-so/whirlpools
npm install @orca-so/whirlpools @solana/web3.js@2
```

## Basic Usage

### 1. Wallet Creation
You can create a wallet using `generateKeyPairSigner()` from the Solana SDK.
You can [generate a file system wallet using the Solana CLI](https://docs.solanalabs.com/cli/wallets/file-system) and load it in your program.

```tsx
import { generateKeyPairSigner } from '@solana/web3.js';
import { createKeyPairSignerFromBytes } from '@solana/web3.js';
import fs from 'fs';

const wallet = await generateKeyPairSigner();
const keyPairBytes = new Uint8Array(JSON.parse(fs.readFileSync('path/to/solana-keypair.json', 'utf8')));
const wallet = await createKeyPairSignerFromBytes(keyPairBytes);
```

### 2. Configure the Whirlpools SDK for Your Network
Expand Down Expand Up @@ -53,7 +55,7 @@ import { swapInstructions } from '@orca-so/whirlpools';
const poolAddress = "POOL_ADDRESS";
const mintAddress = "TOKEN_MINT";
const amount = 1_000_000n;
const slippageTolerance = 100; // 1bps
const slippageTolerance = 100; // 100 bps = 1%

const { instructions, quote } = await swapInstructions(
devnetRpc,
Expand All @@ -74,7 +76,7 @@ import { generateKeyPairSigner, createSolanaRpc, devnet } from '@solana/web3.js'

const devnetRpc = createSolanaRpc(devnet('https://api.devnet.solana.com'));
await setWhirlpoolsConfig('solanaDevnet');
const wallet = await generateKeyPairSigner();
const wallet = loadWallet();
await devnetRpc.requestAirdrop(wallet.address, lamports(1000000000n)).send();

/* Example Devnet Addresses:
Expand All @@ -86,8 +88,8 @@ await devnetRpc.requestAirdrop(wallet.address, lamports(1000000000n)).send();

const poolAddress = "3KBZiL2g8C7tiJ32hTv5v3KM7aK9htpqTw4cTXz1HvPt";
const mintAddress = "So11111111111111111111111111111111111111112";
const amount = 1_000_000n;
const slippageTolerance = 100; // 1bps
const amount = 1_000_000n; // 0.001 WSOL (SOL has 9 decimals)
const slippageTolerance = 100; // 100bps = 1%

const { instructions, quote } = await swapInstructions(
devnetRpc,
Expand Down
Loading