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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

calintje
Copy link
Contributor

@calintje calintje commented Dec 12, 2024

Title
Add Docs for Sending and Landing Transactions with Whirlpools SDK in Rust and TS

Details
This PR adds documentation on sending and landing transactions using the Whirlpools SDK and Solana SDK in both Rust and TypeScript.

  • Includes Rust and TS code snippets for generating instructions, creating transaction messages, estimating compute units and fees, and submitting transactions with client side retry.
  • Added explanations on blockhashes, transaction lifetimes, compute unit limits, prioritization fees, and retry logic.
  • Added a section geared to web app developers interacting with browser wallets
  • Added a section with additional recommendations for improving transaction landing.

Screenshot 2024-12-23 054112

Screenshot 2024-12-23 054131

Screenshot 2024-12-23 054154

@calintje calintje marked this pull request as ready for review December 17, 2024 03:37
Comment on lines 8 to 9
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:

Copy link
Collaborator

Choose a reason for hiding this comment

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

perhaps we need a section to explain why the vanila sendAndConfirmTransaction doesn't work reliably and confirm that users can't rely on some wallets' auto-append PF fees (ex. phantom). (i think it's because we appended a signer in our tx somewhere)

@@ -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

</Tabs>


#### What is WhirlpoolsConfig?
Copy link
Member

Choose a reason for hiding this comment

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

This feels kinda weird here. We are talking about sending and landing transactions. Then all of a sudden there is an explanation about what whirlpoolsconfig is


Here's how the swap instructions and quote are generated:

<Tabs groupId="programming-languages">
Copy link
Member

Choose a reason for hiding this comment

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

Since we are generating the instructions in other docs pages already. Would it make sense to just take from there (so you don't have to explain any whirlpool stuff).

So just start from

const instructions = [] // <- your actual instructions

// build into a transaction
// sing transaction
// etc.

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 initially thought to create an all-in-one doc, and I went for openPositionInstructions because it has additional signers. But in the context of the current doc-structure, and the location of this doc, it makes more sense to cut all of that out.

<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.


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.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe explain here that we take the 50th percentile in this example but that you can play around with it.


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.
Copy link
Member

Choose a reason for hiding this comment

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

the actual fee charged will depend on the compute units your transaction ultimately consumes

Not 100% sure this is true. I thought it always takes your requested compute units.

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 this to be true from previous experiences. But I just tested this and you are right.

### 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.
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 add here that txs that request fewer compute units get high priority for the same amount of prio fee. Thus lower = better but too low and the tx fails


let blockHashLifetime = 150n;
let currentBlockHeight = latestBlockHash.value.lastValidBlockHeight - blockHashLifetime;
while (latestBlockHash.value.lastValidBlockHeight >= currentBlockHeight) {
Copy link
Member

Choose a reason for hiding this comment

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

I think in most places we just have a time-based loop. This is advantageous if you send to multiple rpc nodes because some might be running behind more than others (meaning that rpc.getBlockHeight().send() is never the actual blockheight of the chain)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I know this. My thinking was that this approach shows how blockhashes work, but it makes more sense to give the example with the time-based loop

}
}
currentBlockHeight = await rpc.getBlockHeight().send();
await new Promise(resolve => setTimeout(resolve, 100));
Copy link
Member

Choose a reason for hiding this comment

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

rpc.sendTransaction and rpc.getSignatureStatuses already take some time to execute (but actual time is not known). Adding the wait here adds on top of that (and 100ms is very low).

If you do a time-based loop you could make sure that for example the transaction is not sent more than 1x per second (instead of this manual wait).

}
```
</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?

@calintje
Copy link
Contributor Author

Thank you @wjthieme @odcheung for the comments. I think I'm ready for another round of review.

Copy link
Member

@wjthieme wjthieme left a comment

Choose a reason for hiding this comment

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

Couple nits and small remarks but LGTM!


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$.
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

### 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

@@ -67,133 +72,17 @@ Let's start by importing the necessary dependencies.
</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

@@ -330,20 +236,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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn 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

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:
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants