From 1734e7c4408011b8d95482fe2c53688442eb1a2a Mon Sep 17 00:00:00 2001 From: calintje Date: Thu, 12 Dec 2024 03:10:39 +0100 Subject: [PATCH 01/12] Update TS swap example with sendTransaction --- .../01-Whirlpools/05-Trade.mdx | 66 ++++++++++++++++--- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/05-Trade.mdx b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/05-Trade.mdx index ecfe0128..25645507 100644 --- a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/05-Trade.mdx +++ b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/05-Trade.mdx @@ -41,18 +41,20 @@ To execute a token swap in an Orca Whirlpool, follow these steps: ```tsx import { setWhirlpoolsConfig, swapInstructions } from '@orca-so/whirlpools'; - import { createSolanaRpc, devnet, address } from '@solana/web3.js'; + import { createSolanaRpc, address, createTransactionMessage, pipe, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, getComputeUnitEstimateForTransactionMessageFactory, appendTransactionMessageInstructions, prependTransactionMessageInstructions, signTransactionMessageWithSigners, getBase64EncodedWireTransaction } from '@solana/web3.js'; import { loadWallet } from './utils'; + import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction } from '@solana-program/compute-budget'; - await setWhirlpoolsConfig('solanaDevnet'); - const devnetRpc = createSolanaRpc(devnet('https://api.devnet.solana.com')); - const wallet = await loadWallet(); // CAUTION: This wallet is not persistent. - const whirlpoolAddress = address("3KBZiL2g8C7tiJ32hTv5v3KM7aK9htpqTw4cTXz1HvPt"); - const mintAddress = address("BRjpCHtyQLNCo8gqRUr8jtdAj5AjPYQaoqbvcZiHok1k"); - const inputAmount = 1_000_000n; + await setWhirlpoolsConfig("solanaMainnet"); + const rpc = createSolanaRpc(""); + const wallet = await loadWallet(); + const whirlpoolAddress = address("5Z66YYYaTmmx1R4mATAGLSc8aV4Vfy5tNdJQzk1GP9RF"); // ORCA/USDC + const mintAddress = address("orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE"); // ORCA + const inputAmount = 100_000n; // 0.1 ORCA + // Get instructions const { instructions, quote } = await swapInstructions( - devnetRpc, + rpc, { inputAmount, mint: mintAddress }, whirlpoolAddress, 100, @@ -60,7 +62,53 @@ To execute a token swap in an Orca Whirlpool, follow these steps: ); console.log(`Quote estimated token out: ${quote.tokenEstOut}`); - console.log(`Number of instructions:, ${instructions.length}`); + console.log(`Number of instructions: ${instructions.length}`); + + // Create encoded transaction message + 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) + ) + const getComputeUnitEstimateForTransactionMessage = + getComputeUnitEstimateForTransactionMessageFactory({ + rpc + }); + const computeUnitEstimate = await getComputeUnitEstimateForTransactionMessage(transactionMessage) + 100_000; + const medianPrioritizationFee = await rpc.getRecentPrioritizationFees() + .send() + .then(fees => fees[Math.floor(fees.length / 2)].prioritizationFee) + const transactionMessageWithComputeUnitInstructions = await prependTransactionMessageInstructions([ + getSetComputeUnitLimitInstruction({ units: computeUnitEstimate }), + getSetComputeUnitPriceInstruction({ microLamports: medianPrioritizationFee }) + ], transactionMessage); + const signedTransaction = await signTransactionMessageWithSigners(transactionMessageWithComputeUnitInstructions) + const base64EncodedWireTransaction = getBase64EncodedWireTransaction(signedTransaction); + + // Submit transaction with client-side retry + let blockHashLifetime = 150n; + let currentBlockHeight = latestBlockHash.value.lastValidBlockHeight - blockHashLifetime; + while (latestBlockHash.value.lastValidBlockHeight >= currentBlockHeight) { + 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; + } + } + currentBlockHeight = await rpc.getBlockHeight().send(); + await new Promise(resolve => setTimeout(resolve, 100)); + } ``` From c464ddfe0453f284a37cefccdbb7f1853049eb46 Mon Sep 17 00:00:00 2001 From: calintje Date: Tue, 17 Dec 2024 04:05:38 +0100 Subject: [PATCH 02/12] Finish doc --- .../01-Whirlpools/06-Send Transaction.mdx | 330 ++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx diff --git a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx new file mode 100644 index 00000000..4613f8b0 --- /dev/null +++ b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx @@ -0,0 +1,330 @@ +--- +sidebar_label: Send Transactions +--- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Sending and Landing Transactions using the Solana SDK | Rust & TypeScript +In this guide, we'll explore how to use the Whirlpools SDK in conjunction with the Solana SDK to send and successfully land transactions on the Solana blockchain. We'll cover key features such as: + +- Client-side retry +- Prioritization fees +- Compute budget estimation + +For this example, we'll demonstrate how to open a position with liquidity on a Whirlpool. However, you can use the same approach to send transactions for other purposes as well, such as swaps, adding/removing liquidity, and more. + +Make sure you check out [this doc](./02-Environment%20Setup.mdx) to set up your environment. + +## Code Overview +### Imports +Let's start by importing the necessary dependencies, making it easier to copy and paste the code. + + + + ```tsx + import { openPositionInstructions, setWhirlpoolsConfig } from '@orca-so/whirlpools'; + import { createSolanaRpc, address, pipe, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, getComputeUnitEstimateForTransactionMessageFactory, prependTransactionMessageInstructions, signTransactionMessageWithSigners, getBase64EncodedWireTransaction, setTransactionMessageFeePayerSigner } from '@solana/web3.js'; + import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction } from '@solana-program/compute-budget'; + ``` + + + ```rust + use orca_whirlpools::{ + open_position_instructions, set_whirlpools_config_address, IncreaseLiquidityParam, + WhirlpoolsConfigInput, + }; + 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}; + ``` + + + + +### Step 1: Configure RPC Client and SDK +To reliably land transactions, avoid using publicly available RPC nodes such as https://api.mainnet-beta.solana.com. Instead, consider using an RPC provider like Helius, Titan, or others. Many providers offer free tiers, which can be sufficient for testing purposes. You may need to adjust the retry mechanism in your code, specifically the waiting time between retries, to accommodate rate limits. + + + + ```tsx + const rpc = createSolanaRpc(""); + await setWhirlpoolsConfig("solanaMainnet"); + ``` + + + ```rust + let rpc = RpcClient::new("".to_string()); + set_whirlpools_config_address(WhirlpoolsConfigInput::SolanaMainnet).unwrap(); + ``` + + + + +#### What is WhirlpoolsConfig? +The `WhirlpoolsConfig` account is the top-level configuration account that governs the pools it owns. The owner of a WhirlpoolsConfig account can define key parameters such as: + +- Default pool fees +- Protocol fee collection authorities +- Other operational parameters for the pools + +All Whirlpools visible on the Orca UI are derived from and controlled by a `WhirlpoolsConfig` account owned by the Orca Foundation. + +The `RPC_URL` should match the network where the specific WhirlpoolsConfig is deployed. For example, if you are working with a `WhirlpoolsConfig` on Solana Mainnet, ensure that the RPC endpoint points to a Solana Mainnet node. + +### Step 2: Load Wallet +You can [generate a file system wallet using the Solana CLI](https://docs.solanalabs.com/cli/wallets/file-system), which stores a keypair as a JSON file, and load it in your program. You can load the wallet using the following code: + + + + ```tsx + const keyPairBytes = new Uint8Array(JSON.parse(fs.readFileSync('path/to/solana-keypair.json', 'utf8'))); + const wallet = await createKeyPairSignerFromBytes(keyPairBytes); + ``` + + + ```rust + let wallet_string = fs::read_to_string("wallet.json").unwrap(); + let keypair_bytes: Vec = serde_json::from_str(&wallet_string).unwrap(); + let wallet = Keypair::from_bytes(&keypair_bytes).unwrap(); + ``` + + + + +### Step 3: Generate Instructions +Next, we generate the necessary instructions for opening a position in a Whirlpool. In this example, we're opening a position with liquidity in the 0.5% fee tier SOL/ORCA pool. We'll be adding 0.1 ORCA (Token B) to the position. Based on the current price of the pool and the price range of the position, the SDK will calculate the estimated amount of SOL needed to match the input amount of ORCA. + +The `openPositionInstructions` function generates the transaction instructions and provides additional details about the operation, including: + +- **Estimated Amount Token A**: This is the estimated maximum amount of SOL that you will deposit into the pool when opening the position. This value reflects the expected result based on the current state of the pool. +- **Initialization Cost**: The one-time non-refundable cost required to initialize the position. You can incur these costs if at least one of the boundaries of the position lies in a tick array account that has not been initialized yet (Read more [here](../../02-Architecture%20Overview/03-Understanding%20Tick%20Arrays.md)). +- **Position Mint**: The mint address for the new liquidity position. Each position has a unique mint that represents the position as an NFT. This keypair must sign the transaction for the position to be successfully initialized. This is done automatically by the Solana web3.js SDK V2, but requires additional steps using the Solana Rust SDK (Step 4). + +Here's how the swap instructions and quote are generated: + + + + ```tsx + const whirlpoolAddress = address("5Z66YYYaTmmx1R4mATAGLSc8aV4Vfy5tNdJQzk1GP9RF"); + const param = { tokenB: 100_000n }; + + const { quote, instructions, initializationCost, positionMint } = await openPositionInstructions( + rpc, + whirlpoolAddress, + param, + 30, // ORCA/SOL + 70, + 100, + wallet + ); + + console.log(`Quote estimated token max B: ${quote.tokenEstB}`); + console.log(`Initialization cost: ${initializationCost}`); + console.log(`Position mint: ${positionMint}`); + console.log(`Number of instructions: ${instructions.length}`); + ``` + + + ```rust + let whirlpool_address = + Pubkey::from_str("Hxw77h9fEx598afiiZunwHaX3vYu9UskDk9EpPNZp1mG").unwrap(); + + let increase_liquidity_param = IncreaseLiquidityParam::TokenB(100_000); + let open_position_instructions = open_position_instructions( + &rpc, + whirlpool_address, + 30.0, + 70.0, + increase_liquidity_param, + Some(100), + Some(wallet.pubkey()), + ) + .await + .unwrap(); + ``` + + + +### Step 4: Create Transaction Message +To send a transaction on Solana, you need to include a blockhash. 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 slots (~1-2 minutes, depending on slot time). After that, the blockhash expires, and the transaction will be rejected. + +Here's how the transaction message is created: + + + + ```tsx + 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) + ) + ``` + + + ```rust + let message = Message::new( + &open_position_instructions.instructions, + Some(&wallet.pubkey()), + ); + let mut signers: Vec<&dyn Signer> = vec![&wallet]; + signers.extend( + open_position_instructions + .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); + ``` + + + + + +### Step 5: Estimating Compute Unit Limit and Prioritization Fee +When preparing a transaction, it's important to set a compute unit limit and an appropriate prioritization fee. Setting the compute units too low and the transaction will fail. Setting it too high will make the transaction less favorable for validators to process.. + +You can get an estimation of the compute units by simulating the transaction on the RPC. To avoid transaction failures caused by underestimating this limit, an additional 100,000 compute units are added, but you can adjust this based on your own tests. + +The prioritization fee incentivizes validators to prioritize your transaction, especially during times of network congestion. You can get a list of recently paid prioritization fees, sorting them and selecting a value from that list. In this example, we select the median of the sorted list. The prioritization fee is provided in micro-lamports per compute unit. The estimated total priority fee is calculated as `estimated compute units * prioritization fee`. However, the actual fee charged will depend on the compute units your transaction ultimately consumes. + + + + ```tsx + const getComputeUnitEstimateForTransactionMessage = + getComputeUnitEstimateForTransactionMessageFactory({ + rpc + }); + const computeUnitEstimate = await getComputeUnitEstimateForTransactionMessage(transactionMessage) + 100_000; + const medianPrioritizationFee = await rpc.getRecentPrioritizationFees() + .send() + .then(fees => fees[Math.floor(fees.length / 2)].prioritizationFee) + const transactionMessageWithComputeUnitInstructions = await prependTransactionMessageInstructions([ + getSetComputeUnitLimitInstruction({ units: computeUnitEstimate }), + getSetComputeUnitPriceInstruction({ microLamports: medianPrioritizationFee }) + ], transactionMessage); + ``` + + + ```rust + 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 = 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); + } + } + ``` + + + + + +### Step 6: Sign and Submit Transaction +Finally, the transaction is signed, encoded, and submitted to the network. A client-side retry mechanism ensures that the transaction is repeatedly sent until it is confirmed or the blockhash expires. The same signed transaction is sent in a loop with retries at fixed intervals (100ms), while polling the status using getSignatureStatuses. + + + + ```tsx + const signedTransaction = await signTransactionMessageWithSigners(transactionMessageWithComputeUnitInstructions) + const base64EncodedWireTransaction = getBase64EncodedWireTransaction(signedTransaction); + + let blockHashLifetime = 150n; + let currentBlockHeight = latestBlockHash.value.lastValidBlockHeight - blockHashLifetime; + while (latestBlockHash.value.lastValidBlockHeight >= currentBlockHeight) { + 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; + } + } + currentBlockHeight = await rpc.getBlockHeight().send(); + await new Promise(resolve => setTimeout(resolve, 100)); + } + ``` + + + ```rust + all_instructions.extend(open_position_instructions.instructions); + let message = Message::new(&all_instructions, Some(&wallet.pubkey())); + + let transaction = Transaction::new(&signers, message, recent_blockhash); + 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::::from("Transaction timed out")); + } + 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)); + } + sleep(Duration::from_millis(100)).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); + ``` + + From 59d27ec260a9287708cbc05de0fd053ef27217a7 Mon Sep 17 00:00:00 2001 From: calintje Date: Tue, 17 Dec 2024 04:08:04 +0100 Subject: [PATCH 03/12] Revert trade.mdx --- .../01-Whirlpools/05-Trade.mdx | 64 +++---------------- 1 file changed, 8 insertions(+), 56 deletions(-) diff --git a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/05-Trade.mdx b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/05-Trade.mdx index 25645507..5280f7ce 100644 --- a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/05-Trade.mdx +++ b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/05-Trade.mdx @@ -41,20 +41,18 @@ To execute a token swap in an Orca Whirlpool, follow these steps: ```tsx import { setWhirlpoolsConfig, swapInstructions } from '@orca-so/whirlpools'; - import { createSolanaRpc, address, createTransactionMessage, pipe, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, getComputeUnitEstimateForTransactionMessageFactory, appendTransactionMessageInstructions, prependTransactionMessageInstructions, signTransactionMessageWithSigners, getBase64EncodedWireTransaction } from '@solana/web3.js'; + import { createSolanaRpc, devnet, address } from '@solana/web3.js'; import { loadWallet } from './utils'; - import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction } from '@solana-program/compute-budget'; - await setWhirlpoolsConfig("solanaMainnet"); - const rpc = createSolanaRpc(""); + await setWhirlpoolsConfig('solanaDevnet'); + const devnetRpc = createSolanaRpc(devnet('https://api.devnet.solana.com')); const wallet = await loadWallet(); - const whirlpoolAddress = address("5Z66YYYaTmmx1R4mATAGLSc8aV4Vfy5tNdJQzk1GP9RF"); // ORCA/USDC - const mintAddress = address("orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE"); // ORCA - const inputAmount = 100_000n; // 0.1 ORCA + const whirlpoolAddress = address("3KBZiL2g8C7tiJ32hTv5v3KM7aK9htpqTw4cTXz1HvPt"); + const mintAddress = address("BRjpCHtyQLNCo8gqRUr8jtdAj5AjPYQaoqbvcZiHok1k"); + const inputAmount = 1_000_000n; - // Get instructions const { instructions, quote } = await swapInstructions( - rpc, + devnetRpc, { inputAmount, mint: mintAddress }, whirlpoolAddress, 100, @@ -62,53 +60,7 @@ To execute a token swap in an Orca Whirlpool, follow these steps: ); console.log(`Quote estimated token out: ${quote.tokenEstOut}`); - console.log(`Number of instructions: ${instructions.length}`); - - // Create encoded transaction message - 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) - ) - const getComputeUnitEstimateForTransactionMessage = - getComputeUnitEstimateForTransactionMessageFactory({ - rpc - }); - const computeUnitEstimate = await getComputeUnitEstimateForTransactionMessage(transactionMessage) + 100_000; - const medianPrioritizationFee = await rpc.getRecentPrioritizationFees() - .send() - .then(fees => fees[Math.floor(fees.length / 2)].prioritizationFee) - const transactionMessageWithComputeUnitInstructions = await prependTransactionMessageInstructions([ - getSetComputeUnitLimitInstruction({ units: computeUnitEstimate }), - getSetComputeUnitPriceInstruction({ microLamports: medianPrioritizationFee }) - ], transactionMessage); - const signedTransaction = await signTransactionMessageWithSigners(transactionMessageWithComputeUnitInstructions) - const base64EncodedWireTransaction = getBase64EncodedWireTransaction(signedTransaction); - - // Submit transaction with client-side retry - let blockHashLifetime = 150n; - let currentBlockHeight = latestBlockHash.value.lastValidBlockHeight - blockHashLifetime; - while (latestBlockHash.value.lastValidBlockHeight >= currentBlockHeight) { - 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; - } - } - currentBlockHeight = await rpc.getBlockHeight().send(); - await new Promise(resolve => setTimeout(resolve, 100)); - } + console.log(`Number of instructions:, ${instructions.length}`); ``` From b7e4c853f4d39e39b8ca692a79f2daac8d660811 Mon Sep 17 00:00:00 2001 From: calintje Date: Tue, 17 Dec 2024 04:13:11 +0100 Subject: [PATCH 04/12] Format --- .../03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx index 4613f8b0..5b486537 100644 --- a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx +++ b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx @@ -190,8 +190,6 @@ Here's how the transaction message is created: - - ### Step 5: Estimating Compute Unit Limit and Prioritization Fee When preparing a transaction, it's important to set a compute unit limit and an appropriate prioritization fee. Setting the compute units too low and the transaction will fail. Setting it too high will make the transaction less favorable for validators to process.. @@ -250,8 +248,6 @@ The prioritization fee incentivizes validators to prioritize your transaction, e - - ### Step 6: Sign and Submit Transaction Finally, the transaction is signed, encoded, and submitted to the network. A client-side retry mechanism ensures that the transaction is repeatedly sent until it is confirmed or the blockhash expires. The same signed transaction is sent in a loop with retries at fixed intervals (100ms), while polling the status using getSignatureStatuses. From 0941bd6106abe76f9a61fb31753a118bf48359e9 Mon Sep 17 00:00:00 2001 From: calintje Date: Tue, 17 Dec 2024 04:30:46 +0100 Subject: [PATCH 05/12] Update code blocks. Add package.json and cargo.yaml --- .../01-Whirlpools/06-Send Transaction.mdx | 272 ++++++++++-------- 1 file changed, 159 insertions(+), 113 deletions(-) diff --git a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx index 5b486537..d9dfbee3 100644 --- a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx +++ b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx @@ -4,7 +4,7 @@ sidebar_label: Send Transactions import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -# Sending and Landing Transactions using the Solana SDK | Rust & TypeScript +# Sending and Landing Transactions In this guide, we'll explore how to use the Whirlpools SDK in conjunction with the Solana SDK to send and successfully land transactions on the Solana blockchain. We'll cover key features such as: - Client-side retry @@ -16,19 +16,37 @@ For this example, we'll demonstrate how to open a position with liquidity on a W Make sure you check out [this doc](./02-Environment%20Setup.mdx) to set up your environment. ## Code Overview -### Imports -Let's start by importing the necessary dependencies, making it easier to copy and paste the code. +### Dependencies +Let's start by importing the necessary dependencies. - ```tsx + ```json title="package.json" + "dependencies": { + "@orca-so/whirlpools": "^1.0.0", + "@solana-program/compute-budget": "^0.6.1", + "@solana/web3.js": "^2.0.0", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + }, + ``` + ```tsx title="sendTransaction.ts" import { openPositionInstructions, setWhirlpoolsConfig } from '@orca-so/whirlpools'; import { createSolanaRpc, address, pipe, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, getComputeUnitEstimateForTransactionMessageFactory, prependTransactionMessageInstructions, signTransactionMessageWithSigners, getBase64EncodedWireTransaction, setTransactionMessageFeePayerSigner } from '@solana/web3.js'; import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction } from '@solana-program/compute-budget'; ``` - ```rust + ```toml title="Cargo.toml" + orca_whirlpools_client = "^1" + orca_whirlpools_core = "^1" + orca_whirlpools = "^1" + 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 orca_whirlpools::{ open_position_instructions, set_whirlpools_config_address, IncreaseLiquidityParam, WhirlpoolsConfigInput, @@ -55,15 +73,19 @@ To reliably land transactions, avoid using publicly available RPC nodes such as - ```tsx + ```tsx title="sendTransaction.ts" const rpc = createSolanaRpc(""); await setWhirlpoolsConfig("solanaMainnet"); ``` - ```rust - let rpc = RpcClient::new("".to_string()); - set_whirlpools_config_address(WhirlpoolsConfigInput::SolanaMainnet).unwrap(); + ```rust title="main.rs" + #[tokio::main] + async fn main() { + let rpc = RpcClient::new("".to_string()); + set_whirlpools_config_address(WhirlpoolsConfigInput::SolanaMainnet).unwrap(); + // Rest of the code + } ``` @@ -85,16 +107,21 @@ You can [generate a file system wallet using the Solana CLI](https://docs.solana - ```tsx + ```tsx title="sendTransaction.ts" const keyPairBytes = new Uint8Array(JSON.parse(fs.readFileSync('path/to/solana-keypair.json', 'utf8'))); const wallet = await createKeyPairSignerFromBytes(keyPairBytes); ``` - ```rust - let wallet_string = fs::read_to_string("wallet.json").unwrap(); - let keypair_bytes: Vec = serde_json::from_str(&wallet_string).unwrap(); - let wallet = Keypair::from_bytes(&keypair_bytes).unwrap(); + ```rust title="main.rs" + #[tokio::main] + async fn main() { + // ... + let wallet_string = fs::read_to_string("wallet.json").unwrap(); + let keypair_bytes: Vec = serde_json::from_str(&wallet_string).unwrap(); + let wallet = Keypair::from_bytes(&keypair_bytes).unwrap(); + // ... + } ``` @@ -113,7 +140,7 @@ Here's how the swap instructions and quote are generated: - ```tsx + ```tsx title="sendTransaction.ts" const whirlpoolAddress = address("5Z66YYYaTmmx1R4mATAGLSc8aV4Vfy5tNdJQzk1GP9RF"); const param = { tokenB: 100_000n }; @@ -134,22 +161,27 @@ Here's how the swap instructions and quote are generated: ``` - ```rust - let whirlpool_address = + ```rust title="main.rs" + #[tokio::main] + async fn main() { + // ... + let whirlpool_address = Pubkey::from_str("Hxw77h9fEx598afiiZunwHaX3vYu9UskDk9EpPNZp1mG").unwrap(); - let increase_liquidity_param = IncreaseLiquidityParam::TokenB(100_000); - let open_position_instructions = open_position_instructions( - &rpc, - whirlpool_address, - 30.0, - 70.0, - increase_liquidity_param, - Some(100), - Some(wallet.pubkey()), - ) - .await - .unwrap(); + let increase_liquidity_param = IncreaseLiquidityParam::TokenB(100_000); + let open_position_instructions = open_position_instructions( + &rpc, + whirlpool_address, + 30.0, + 70.0, + increase_liquidity_param, + Some(100), + Some(wallet.pubkey()), + ) + .await + .unwrap(); + // ... + } ``` @@ -161,7 +193,7 @@ Here's how the transaction message is created: - ```tsx + ```tsx title="sendTransaction.ts" const latestBlockHash = await rpc.getLatestBlockhash().send(); const transactionMessage = await pipe( createTransactionMessage({ version: 0}), @@ -172,20 +204,25 @@ Here's how the transaction message is created: ``` - ```rust - let message = Message::new( - &open_position_instructions.instructions, - Some(&wallet.pubkey()), - ); - let mut signers: Vec<&dyn Signer> = vec![&wallet]; - signers.extend( - open_position_instructions - .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); + ```rust title="main.rs" + #[tokio::main] + async fn main() { + // ... + let message = Message::new( + &open_position_instructions.instructions, + Some(&wallet.pubkey()), + ); + let mut signers: Vec<&dyn Signer> = vec![&wallet]; + signers.extend( + open_position_instructions + .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); + // ... + } ``` @@ -199,7 +236,7 @@ The prioritization fee incentivizes validators to prioritize your transaction, e - ```tsx + ```tsx title="sendTransaction.ts" const getComputeUnitEstimateForTransactionMessage = getComputeUnitEstimateForTransactionMessageFactory({ rpc @@ -215,34 +252,39 @@ The prioritization fee incentivizes validators to prioritize your transaction, e ``` - ```rust - 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 = 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); + ```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 = 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); + } } + // ... } ``` @@ -253,7 +295,7 @@ Finally, the transaction is signed, encoded, and submitted to the network. A cli - ```tsx + ```tsx title="sendTransaction.ts" const signedTransaction = await signTransactionMessageWithSigners(transactionMessageWithComputeUnitInstructions) const base64EncodedWireTransaction = getBase64EncodedWireTransaction(signedTransaction); @@ -281,46 +323,50 @@ Finally, the transaction is signed, encoded, and submitted to the network. A cli ``` - ```rust - all_instructions.extend(open_position_instructions.instructions); - let message = Message::new(&all_instructions, Some(&wallet.pubkey())); - - let transaction = Transaction::new(&signers, message, recent_blockhash); - 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::::from("Transaction timed out")); - } - 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)); - } - sleep(Duration::from_millis(100)).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); + ```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(&signers, message, recent_blockhash); + 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::::from("Transaction timed out")); + } + 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)); + } + sleep(Duration::from_millis(100)).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); + } ``` From 329c3968d43be329a44c91134bac03b783df6ffd Mon Sep 17 00:00:00 2001 From: calintje Date: Tue, 17 Dec 2024 04:40:54 +0100 Subject: [PATCH 06/12] Remove 'Step' --- .../01-Whirlpools/06-Send Transaction.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx index d9dfbee3..3249698f 100644 --- a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx +++ b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx @@ -68,7 +68,7 @@ Let's start by importing the necessary dependencies. -### Step 1: Configure RPC Client and SDK +### 1: Configure RPC Client and SDK To reliably land transactions, avoid using publicly available RPC nodes such as https://api.mainnet-beta.solana.com. Instead, consider using an RPC provider like Helius, Titan, or others. Many providers offer free tiers, which can be sufficient for testing purposes. You may need to adjust the retry mechanism in your code, specifically the waiting time between retries, to accommodate rate limits. @@ -102,7 +102,7 @@ All Whirlpools visible on the Orca UI are derived from and controlled by a `Whir The `RPC_URL` should match the network where the specific WhirlpoolsConfig is deployed. For example, if you are working with a `WhirlpoolsConfig` on Solana Mainnet, ensure that the RPC endpoint points to a Solana Mainnet node. -### Step 2: Load Wallet +### 2: Load Wallet You can [generate a file system wallet using the Solana CLI](https://docs.solanalabs.com/cli/wallets/file-system), which stores a keypair as a JSON file, and load it in your program. You can load the wallet using the following code: @@ -127,7 +127,7 @@ You can [generate a file system wallet using the Solana CLI](https://docs.solana -### Step 3: Generate Instructions +### 3: Generate Instructions Next, we generate the necessary instructions for opening a position in a Whirlpool. In this example, we're opening a position with liquidity in the 0.5% fee tier SOL/ORCA pool. We'll be adding 0.1 ORCA (Token B) to the position. Based on the current price of the pool and the price range of the position, the SDK will calculate the estimated amount of SOL needed to match the input amount of ORCA. The `openPositionInstructions` function generates the transaction instructions and provides additional details about the operation, including: @@ -186,7 +186,7 @@ Here's how the swap instructions and quote are generated: -### Step 4: Create Transaction Message +### 4: Create Transaction Message To send a transaction on Solana, you need to include a blockhash. 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 slots (~1-2 minutes, depending on slot time). After that, the blockhash expires, and the transaction will be rejected. Here's how the transaction message is created: @@ -227,7 +227,7 @@ Here's how the transaction message is created: -### Step 5: Estimating Compute Unit Limit and Prioritization Fee +### 5: Estimating Compute Unit Limit and Prioritization Fee When preparing a transaction, it's important to set a compute unit limit and an appropriate prioritization fee. Setting the compute units too low and the transaction will fail. Setting it too high will make the transaction less favorable for validators to process.. You can get an estimation of the compute units by simulating the transaction on the RPC. To avoid transaction failures caused by underestimating this limit, an additional 100,000 compute units are added, but you can adjust this based on your own tests. @@ -290,7 +290,7 @@ The prioritization fee incentivizes validators to prioritize your transaction, e -### Step 6: Sign and Submit Transaction +### 6: Sign and Submit Transaction Finally, the transaction is signed, encoded, and submitted to the network. A client-side retry mechanism ensures that the transaction is repeatedly sent until it is confirmed or the blockhash expires. The same signed transaction is sent in a loop with retries at fixed intervals (100ms), while polling the status using getSignatureStatuses. From b39eeb91f01d9c8bdf324b3b69a902cbf50c61ef Mon Sep 17 00:00:00 2001 From: calintje Date: Tue, 17 Dec 2024 20:59:07 +0100 Subject: [PATCH 07/12] Update version tag --- .../03-Whirlpools SDKs/01-Whirlpools/02-Environment Setup.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/02-Environment Setup.mdx b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/02-Environment Setup.mdx index 27ead333..0ab48767 100644 --- a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/02-Environment Setup.mdx +++ b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/02-Environment Setup.mdx @@ -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 ``` Initialize the project as a TypeScript project: From 3e28c5f86abf585e1f8a606ef76d10e95d0182b3 Mon Sep 17 00:00:00 2001 From: calintje Date: Mon, 23 Dec 2024 05:35:08 +0100 Subject: [PATCH 08/12] Resolve comments --- .../01-Whirlpools/06-Send Transaction.mdx | 233 ++++++------------ 1 file changed, 80 insertions(+), 153 deletions(-) diff --git a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx index 3249698f..a7f8ec77 100644 --- a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx +++ b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx @@ -5,52 +5,57 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Sending and Landing Transactions -In this guide, we'll explore how to use the Whirlpools SDK in conjunction with the Solana SDK to send and successfully land transactions on the Solana blockchain. We'll cover key features such as: +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 -For this example, we'll demonstrate how to open a position with liquidity on a Whirlpool. However, you can use the same approach to send transactions for other purposes as well, such as swaps, adding/removing liquidity, and more. +We also go over some aspects related to sending transactions in web apps when working with wallet extensions and provided additional steps you can take to improve transaction landing. Make sure you check out [this doc](./02-Environment%20Setup.mdx) to set up your environment. ## Code Overview -### Dependencies -Let's start by importing the necessary dependencies. +### 1. Dependencies +Let's start by importing the necessary dependencies from Solana's SDKs. ```json title="package.json" "dependencies": { - "@orca-so/whirlpools": "^1.0.0", "@solana-program/compute-budget": "^0.6.1", "@solana/web3.js": "^2.0.0", - "tsx": "^4.19.2", - "typescript": "^5.7.2" }, ``` ```tsx title="sendTransaction.ts" - import { openPositionInstructions, setWhirlpoolsConfig } from '@orca-so/whirlpools'; - import { createSolanaRpc, address, pipe, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, getComputeUnitEstimateForTransactionMessageFactory, prependTransactionMessageInstructions, signTransactionMessageWithSigners, getBase64EncodedWireTransaction, setTransactionMessageFeePayerSigner } from '@solana/web3.js'; - import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction } from '@solana-program/compute-budget'; + 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'; ``` ```toml title="Cargo.toml" - orca_whirlpools_client = "^1" - orca_whirlpools_core = "^1" - orca_whirlpools = "^1" 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 orca_whirlpools::{ - open_position_instructions, set_whirlpools_config_address, IncreaseLiquidityParam, - WhirlpoolsConfigInput, - }; use solana_client::nonblocking::rpc_client::RpcClient; use solana_client::rpc_config::RpcSendTransactionConfig; use solana_sdk::commitment_config::CommitmentLevel; @@ -67,133 +72,17 @@ Let's start by importing the necessary dependencies. +### 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. -### 1: Configure RPC Client and SDK -To reliably land transactions, avoid using publicly available RPC nodes such as https://api.mainnet-beta.solana.com. Instead, consider using an RPC provider like Helius, Titan, or others. Many providers offer free tiers, which can be sufficient for testing purposes. You may need to adjust the retry mechanism in your code, specifically the waiting time between retries, to accommodate rate limits. - - - - ```tsx title="sendTransaction.ts" - const rpc = createSolanaRpc(""); - await setWhirlpoolsConfig("solanaMainnet"); - ``` - - - ```rust title="main.rs" - #[tokio::main] - async fn main() { - let rpc = RpcClient::new("".to_string()); - set_whirlpools_config_address(WhirlpoolsConfigInput::SolanaMainnet).unwrap(); - // Rest of the code - } - ``` - - - - -#### What is WhirlpoolsConfig? -The `WhirlpoolsConfig` account is the top-level configuration account that governs the pools it owns. The owner of a WhirlpoolsConfig account can define key parameters such as: - -- Default pool fees -- Protocol fee collection authorities -- Other operational parameters for the pools - -All Whirlpools visible on the Orca UI are derived from and controlled by a `WhirlpoolsConfig` account owned by the Orca Foundation. - -The `RPC_URL` should match the network where the specific WhirlpoolsConfig is deployed. For example, if you are working with a `WhirlpoolsConfig` on Solana Mainnet, ensure that the RPC endpoint points to a Solana Mainnet node. - -### 2: Load Wallet -You can [generate a file system wallet using the Solana CLI](https://docs.solanalabs.com/cli/wallets/file-system), which stores a keypair as a JSON file, and load it in your program. You can load the wallet using the following code: - - - - ```tsx title="sendTransaction.ts" - const keyPairBytes = new Uint8Array(JSON.parse(fs.readFileSync('path/to/solana-keypair.json', 'utf8'))); - const wallet = await createKeyPairSignerFromBytes(keyPairBytes); - ``` - - - ```rust title="main.rs" - #[tokio::main] - async fn main() { - // ... - let wallet_string = fs::read_to_string("wallet.json").unwrap(); - let keypair_bytes: Vec = serde_json::from_str(&wallet_string).unwrap(); - let wallet = Keypair::from_bytes(&keypair_bytes).unwrap(); - // ... - } - ``` - - - - -### 3: Generate Instructions -Next, we generate the necessary instructions for opening a position in a Whirlpool. In this example, we're opening a position with liquidity in the 0.5% fee tier SOL/ORCA pool. We'll be adding 0.1 ORCA (Token B) to the position. Based on the current price of the pool and the price range of the position, the SDK will calculate the estimated amount of SOL needed to match the input amount of ORCA. - -The `openPositionInstructions` function generates the transaction instructions and provides additional details about the operation, including: - -- **Estimated Amount Token A**: This is the estimated maximum amount of SOL that you will deposit into the pool when opening the position. This value reflects the expected result based on the current state of the pool. -- **Initialization Cost**: The one-time non-refundable cost required to initialize the position. You can incur these costs if at least one of the boundaries of the position lies in a tick array account that has not been initialized yet (Read more [here](../../02-Architecture%20Overview/03-Understanding%20Tick%20Arrays.md)). -- **Position Mint**: The mint address for the new liquidity position. Each position has a unique mint that represents the position as an NFT. This keypair must sign the transaction for the position to be successfully initialized. This is done automatically by the Solana web3.js SDK V2, but requires additional steps using the Solana Rust SDK (Step 4). - -Here's how the swap instructions and quote are generated: - - - - ```tsx title="sendTransaction.ts" - const whirlpoolAddress = address("5Z66YYYaTmmx1R4mATAGLSc8aV4Vfy5tNdJQzk1GP9RF"); - const param = { tokenB: 100_000n }; - - const { quote, instructions, initializationCost, positionMint } = await openPositionInstructions( - rpc, - whirlpoolAddress, - param, - 30, // ORCA/SOL - 70, - 100, - wallet - ); - - console.log(`Quote estimated token max B: ${quote.tokenEstB}`); - console.log(`Initialization cost: ${initializationCost}`); - console.log(`Position mint: ${positionMint}`); - console.log(`Number of instructions: ${instructions.length}`); - ``` - - - ```rust title="main.rs" - #[tokio::main] - async fn main() { - // ... - let whirlpool_address = - Pubkey::from_str("Hxw77h9fEx598afiiZunwHaX3vYu9UskDk9EpPNZp1mG").unwrap(); - - let increase_liquidity_param = IncreaseLiquidityParam::TokenB(100_000); - let open_position_instructions = open_position_instructions( - &rpc, - whirlpool_address, - 30.0, - 70.0, - increase_liquidity_param, - Some(100), - Some(wallet.pubkey()), - ) - .await - .unwrap(); - // ... - } - ``` - - - -### 4: Create Transaction Message -To send a transaction on Solana, you need to include a blockhash. 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 slots (~1-2 minutes, depending on slot time). After that, the blockhash expires, and the transaction will be rejected. +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: ```tsx title="sendTransaction.ts" + const { instructions } = // get instructions from Whirlpools SDK const latestBlockHash = await rpc.getLatestBlockhash().send(); const transactionMessage = await pipe( createTransactionMessage({ version: 0}), @@ -208,13 +97,14 @@ Here's how the transaction message is created: #[tokio::main] async fn main() { // ... + let instructions_result = // get instructions from Whirlpools SDK let message = Message::new( - &open_position_instructions.instructions, + &instructions_result.instructions, Some(&wallet.pubkey()), ); let mut signers: Vec<&dyn Signer> = vec![&wallet]; signers.extend( - open_position_instructions + instructions_result .additional_signers .iter() .map(|kp| kp as &dyn Signer), @@ -227,12 +117,15 @@ Here's how the transaction message is created: -### 5: Estimating Compute Unit Limit and Prioritization Fee -When preparing a transaction, it's important to set a compute unit limit and an appropriate prioritization fee. Setting the compute units too low and the transaction will fail. Setting it too high will make the transaction less favorable for validators to process.. +### 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. + +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. -You can get an estimation of the compute units by simulating the transaction on the RPC. To avoid transaction failures caused by underestimating this limit, an additional 100,000 compute units are added, 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$. -The prioritization fee incentivizes validators to prioritize your transaction, especially during times of network congestion. You can get a list of recently paid prioritization fees, sorting them and selecting a value from that list. In this example, we select the median of the sorted list. The prioritization fee is provided in micro-lamports per compute unit. The estimated total priority fee is calculated as `estimated compute units * prioritization fee`. However, the actual fee charged will depend on the compute units your transaction ultimately consumes. @@ -290,8 +183,10 @@ The prioritization fee incentivizes validators to prioritize your transaction, e -### 6: Sign and Submit Transaction -Finally, the transaction is signed, encoded, and submitted to the network. A client-side retry mechanism ensures that the transaction is repeatedly sent until it is confirmed or the blockhash expires. The same signed transaction is sent in a loop with retries at fixed intervals (100ms), while polling the status using getSignatureStatuses. +### 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. @@ -299,14 +194,18 @@ Finally, the transaction is signed, encoded, and submitted to the network. A cli const signedTransaction = await signTransactionMessageWithSigners(transactionMessageWithComputeUnitInstructions) const base64EncodedWireTransaction = getBase64EncodedWireTransaction(signedTransaction); - let blockHashLifetime = 150n; - let currentBlockHeight = latestBlockHash.value.lastValidBlockHeight - blockHashLifetime; - while (latestBlockHash.value.lastValidBlockHeight >= currentBlockHeight) { + 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) { @@ -317,8 +216,12 @@ Finally, the transaction is signed, encoded, and submitted to the network. A cli break; } } - currentBlockHeight = await rpc.getBlockHeight().send(); - await new Promise(resolve => setTimeout(resolve, 100)); + + const elapsedTime = Date.now() - transactionStartTime; + const remainingTime = Math.max(0, 1000 - elapsedTime); + if (remainingTime > 0) { + await new Promise(resolve => setTimeout(resolve, remainingTime)); + } } ``` @@ -330,20 +233,22 @@ Finally, the transaction is signed, encoded, and submitted to the network. A cli all_instructions.extend(open_position_instructions.instructions); let message = Message::new(&all_instructions, Some(&wallet.pubkey())); - let transaction = Transaction::new(&signers, message, recent_blockhash); + let transaction = Transaction::new(& , message, recent_blockhash); 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::::from("Transaction timed out")); } + let transaction_start_time = Instant::now(); + let signature: Signature = rpc .send_transaction_with_config(&transaction, transaction_config) .await @@ -353,11 +258,18 @@ Finally, the transaction is signed, encoded, and submitted to the network. A cli .await .unwrap() .value; + if let Some(status) = statuses[0].clone() { break Ok((status, signature)); } - sleep(Duration::from_millis(100)).await; + + 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)) @@ -370,3 +282,18 @@ Finally, the transaction is signed, encoded, and submitted to the network. A cli ``` + +## 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. + +#### Prioritization Fees +Browser wallets, like Phantom, will calculate and apply priority fees for your transactions, provided: +- 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. From d4342f5bc4bb1e22b643b37ad730bbfe4a3816fe Mon Sep 17 00:00:00 2001 From: calintje Date: Mon, 23 Dec 2024 05:42:59 +0100 Subject: [PATCH 09/12] Fix typo --- .../03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx index a7f8ec77..60d28dfd 100644 --- a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx +++ b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx @@ -11,7 +11,7 @@ In this guide, we'll explore how to send the instructions using the Solana SDK - - Prioritization fees - Compute budget estimation -We also go over some aspects related to sending transactions in web apps when working with wallet extensions and provided additional steps you can take to improve transaction landing. +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. From 535501c6739c6f35f42588425d97da623cbfdc82 Mon Sep 17 00:00:00 2001 From: calintje Date: Wed, 25 Dec 2024 18:00:41 +0100 Subject: [PATCH 10/12] Sort fees before selecting --- .../03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx index 60d28dfd..df665007 100644 --- a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx +++ b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx @@ -137,7 +137,10 @@ The prioritization fee per compute unit also incentivizes validators to prioriti const computeUnitEstimate = await getComputeUnitEstimateForTransactionMessage(transactionMessage) + 100_000; const medianPrioritizationFee = await rpc.getRecentPrioritizationFees() .send() - .then(fees => fees[Math.floor(fees.length / 2)].prioritizationFee) + .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 }) From 05e32c8b235d0908a58bab988b851b9764f7d0b6 Mon Sep 17 00:00:00 2001 From: calintje Date: Wed, 25 Dec 2024 22:01:16 +0100 Subject: [PATCH 11/12] Update NPMjs README --- ts-sdk/whirlpool/README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/ts-sdk/whirlpool/README.md b/ts-sdk/whirlpool/README.md index f93b7f9d..a8541961 100644 --- a/ts-sdk/whirlpool/README.md +++ b/ts-sdk/whirlpool/README.md @@ -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 @@ -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, @@ -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: @@ -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, From 1cc403ca7664058bde0f95698bd3dad9e2b42d5d Mon Sep 17 00:00:00 2001 From: calintje Date: Thu, 26 Dec 2024 14:48:55 +0100 Subject: [PATCH 12/12] Resolve comments --- .../01-Whirlpools/06-Send Transaction.mdx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx index df665007..7d8e5318 100644 --- a/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx +++ b/docs/whirlpool/docs/03-Whirlpools SDKs/01-Whirlpools/06-Send Transaction.mdx @@ -75,6 +75,8 @@ Let's start by importing the necessary dependencies from Solana's SDKs. ### 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. +> **Durable Nonces**: In some cases, you might need a transaction to remain valid for longer than the typical blockhash lifespan, such as when scheduling future payments or collecting multi-signature approvals over time. In that case, you can use [durable nonces](https://solana.com/developers/courses/offline-transactions/durable-nonces) to sign the transaction, which includes a nonce in place of a recent blockhash. + 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: @@ -124,7 +126,7 @@ Transactions that request fewer compute units get high priority for the same amo 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$. +The prioritization fee per compute unit also incentivizes validators to prioritize your transaction, especially during times of network congestion. You can call the `getRecentPrioritizationFees` RPC method to retrieve an array of 150 values, where each value represents the lowest priority fee paid for transactions that landed in each of the past 150 blocks. In this example, we sort that list and 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$. @@ -236,7 +238,7 @@ You're probably wondering why we don't just use the widely used `sendAndConfirm` all_instructions.extend(open_position_instructions.instructions); let message = Message::new(&all_instructions, Some(&wallet.pubkey())); - let transaction = Transaction::new(& , message, recent_blockhash); + let transaction = Transaction::new(&signers ,message , recent_blockhash); let transaction_config = RpcSendTransactionConfig { skip_preflight: true, preflight_commitment: Some(CommitmentLevel::Confirmed), @@ -288,10 +290,10 @@ You're probably wondering why we don't just use the widely used `sendAndConfirm` ## 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. +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) and add it to the instructions. 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 assembling the transaction you can pass it to the wallet extension. If the user signs, it will return a serialized transaction with the added signature. #### Prioritization Fees -Browser wallets, like Phantom, will calculate and apply priority fees for your transactions, provided: +Some wallets will calculate and apply priority fees for your transactions, provided: - 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.