Soldev

Best practices

Last updated:

Here are some essential principles to consider when building on-chain programs in Solana:

Store Keys in the Account

It's beneficial to store keys in the account when creating Program Derived Accounts (PDAs) using seeds. While this may increase account rent slightly, it offers significant advantages.

By having all the necessary keys in the account, it becomes effortless to locate the account (since you can recreate its public key). Additionally, this approach works seamlessly with Anchor's has_one clause, streamlining the process.

Keep seeds simple

When creating PDA seeds, prioritize simplicity. Using a straightforward logic for seeds makes it easier to remember and clarifies the relationship between accounts.

A logical approach is to first include the seeds of the parent account and then use the current object's identifiers, preferably in alphabetical order.

Minimize Instruction's Scope

Keeping each instruction's scope as small as possible is crucial for several reasons.

However, a trade-off to consider is that it may lead to an increase in Lines Of Code (LOC).

Use InitSpace

It will automatically determine the right space to allocate the account with

use anchor_lang::prelude::*;

#[account]
#[derive(InitSpace)]
pub struct Offer {
    pub id: u64,
    pub maker: Pubkey,
    pub token_mint_a: Pubkey,
    pub token_mint_b: Pubkey,
    pub token_b_wanted_amount: u64,
    pub bump: u8,
}

Testing

When it comes to testing, there are no canonical best practices. However some fundamental principles applies to unique projects that handle large TVL:

There are efficient testing tools and frameworks in the Solana tooling ecosystem that could help you achieve these aim but they are adapted for unique situations:

Use appropriate preflight commitment level

Even if you use skipPreflight you should always set the preflightCommitment to the same commitment level used to fetch your transaction's blockhash for both sendTransaction and simulateTransaction.

Turn skipPreflight on when testing on chain programs to get better errors.

Avoid lagging RPC nodes

For sendTransaction requests, clients should keep resending a transaction to a RPC node on a frequent interval so that if an RPC node is slightly lagging behind the cluster, it will eventually catch up and detect your transaction’s expiration properly.

For simulateTransaction requests, clients should use the replaceRecentBlockhash parameter to tell the RPC node to replace the simulated transaction’s blockhash with a blockhash that will always be valid for simulation.

Avoid reusing stale blockhashes

Even if your application has fetched a very recent blockhash, be sure that you’re not reusing that blockhash in transactions for too long. The ideal scenario is that a recent blockhash is fetched right before a user signs their transaction.

Recommendation for applications

Poll for new recent blockhashes on a frequent basis to ensure that whenever a user triggers an action that creates a transaction, your application already has a fresh blockhash that’s ready to go.

Recommendation for wallets

Poll for new recent blockhashes on a frequent basis and replace a transaction’s recent blockhash right before they sign the transaction to ensure the blockhash is as fresh as possible.

Monitor RPC nodes for health

Monitor the health of your RPC nodes to ensure that they have an up-to-date view of the cluster state with one of the following methods:

  1. Fetch your RPC node’s highest processed slot by using the getSlot RPC API with the processed commitment level and then call the getMaxShredInsertSlot RPC API to get the highest slot that your RPC node has received a “shred” of a block for. If the difference between these responses is very large, the cluster is producing blocks far ahead of what the RPC node has processed.

  2. Call the getLatestBlockhash RPC API with the confirmed commitment level on a few different RPC API nodes and use the blockhash from the node that returns the highest slot for its context slot.