Marketplace has launched, further enabling blockchain developers! Learn more

Solana Transaction Propagation: Handling Dropped Transactions

November 29, 2022

Overview

Solana has a unique feature that allows transactions to expire if not committed to a block in a certain amount of time. This helps ensure that validators do not have to process the same transaction multiple times. Additionally, transaction expiration creates a cleaner user experience by giving users certainty that a transaction has succeeded or failed relatively quickly.

Because Solana allows transaction expiration, in your dApps, you must check to make sure that the transaction has succeeded or failed before handling your user's next steps. Solana/web3.js makes it easy to verify that a transaction has succeeded or failed with the confirmTransaction() method. This is probably the best tool for most cases, but what if you want to learn how to confirm transactions and add your own retry logic? In this guide, we will create a tool that monitors a Solana transaction to verify that it has been successfully added to a block or expired.

What You Will Need

Set Up Your Project

Create a new project directory in your terminal with the following:

set up your project

Copy
mkdir solana-tx-propagation
cd solana-tx-propagation

Create a file for your app, app.ts:

set up your project

Copy
echo > app.ts

Initialize your project with the "yes" flag to use default values for your new package:

set up your project

Copy
yarn init --yes
#or
npm init --yes

Create a tsconfig.json with .json importing enabled:

set up your project

Copy
tsc -init --resolveJsonModule true

Install Solana Web3 Dependency

We will need to add the Solana Web3 library for this exercise. In your terminal, type:

set up your project

Copy
yarn add @solana/web3.js
#or
npm install @solana/web3.js

Create a Wallet and Airdrop SOL

You'll need to create a Solana File System Wallet (secret key written to a guideSecret.json file) and airdrop some SOL to it. You can do this using Solana CLI or use this script we have created for you.

Make sure you save your wallet to your project directory as guideSecret.json (you can copy this directly from a previous QuickNode Solana project such as mint-nft).

You will also need to ensure your wallet has some devnet SOL. If you ran wallet.ts, you should have already received an airdrop. Otherwise, you can get some at the QuickNode Multi-chain Faucet by pasting your wallet address and selecting Solana Devnet:



Import Necessary Dependencies

Open app.ts, and paste the following imports on lines 1-2:

set up your project

Copy
import { Connection, Keypair, LAMPORTS_PER_SOL, SystemProgram, TransactionInstruction, TransactionMessage, VersionedTransaction } from '@solana/web3.js';
import secret from './guideSecret.json';

Line 1 imports necessary classes and constants from the Solana Web3 library, and line 2 imports the wallet we created in the previous step.

Set Up Your QuickNode Endpoint

To build on Solana, you'll need an API endpoint to connect with the network. You're welcome to use public nodes or deploy and manage your own infrastructure; however, if you'd like 8x faster response times, you can leave the heavy lifting to us. See why over 50% of projects on Solana choose QuickNode and sign up for a free account here. We're going to use a Solana Devnet node.

Copy the HTTP Provider link:



Inside app.ts under your import statements, declare your RPC and establish your Connection to Solana:

set up your project

Copy
const QUICKNODE_RPC = 'https://example.solana-devnet.quiknode.pro/0123456/';
const SOLANA_CONNECTION = new Connection(QUICKNODE_RPC);

After set up, your environment should look something like this:


Set Up Your App

Define Wallets and Transfer Instructions

Create two wallet variables, DESTINATION_WALLET and SIGNER_WALLET, that we will use for our transaction. We will use Keypair.generate() to generate a random wallet to send SOL to, and we will use Keypair.fromSecretKey() to derive our wallet from the imported secret key:

set up your app

Copy
const DESTINATION_WALLET = Keypair.generate();
const SIGNER_WALLET = Keypair.fromSecretKey(new Uint8Array(secret));

Pass those wallets into a new TransactionInstruction that sends 0.1 SOL from SIGNER_WALLET to DESTINATION_WALLET:

set up your app

Copy
const INSTRUCTIONS: TransactionInstruction = 
    SystemProgram.transfer({
        fromPubkey: SIGNER_WALLET.publicKey,
        toPubkey: DESTINATION_WALLET.publicKey,
        lamports: 0.01 * LAMPORTS_PER_SOL,
    });

Fetch the Starting Time

Create a new variable START_TIME that gets the time that we start our app (we will use this to calculate how long the process takes):

set up your app

Copy
const START_TIME = new Date();

Create a Sleep Function

We will need to create a delay function, sleep, that will allow us to wait a period of time before re-querying the chain:

set up your app

Copy
const sleep = (ms: number) => {
    return new Promise(resolve => setTimeout(resolve, ms));
}

Alright, we're all set. Let's build out our app!

Check if a Blockhash is Expired

First, let's create a function, isBlockhashExpired, to check if the block has expired. We will need to pass in a Solana network Connection and an initial lastValidBlockHeight to check against:

check if a blockhash is expired

Copy
async function isBlockhashExpired(connection: Connection, lastValidBlockHeight: number) {
    let currentBlockHeight = (await connection.getBlockHeight('finalized'));
    console.log('                           ');
    console.log('Current Block height:             ', currentBlockHeight);
    console.log('Last Valid Block height - 150:     ', lastValidBlockHeight - 150);
    console.log('--------------------------------------------');    
    console.log('Difference:                      ',currentBlockHeight - (lastValidBlockHeight-150)); // If Difference is positive, blockhash has expired.
    console.log('                           ');

    return (currentBlockHeight > lastValidBlockHeight - 150);
}

Our function does 3 things:

  1. Fetches the current block height using getBlockHeight.
  2. (optional) Prints the blockhash expiration over time. (This is optionally added to help demonstrate how the expirations change)
  3. Returns a boolean by checking if our current block height is larger than our original block height (less 150).

Why do we subtract 150? Solana requires that a transaction's blockhash is not older than 150 slots.

Create and Send a Transaction

Create an async code block where we will run our script:

create and send a transaction

Copy
(async()=>{

})()

Within your code block, let's create and send a transaction based on the TransactionInstruction we defined earlier:

create and send a transaction

Copy
(async()=>{
    // Step 1 - Get Latest Blockhash
    const blockhashResponse = await SOLANA_CONNECTION.getLatestBlockhashAndContext('finalized');
    const lastValidHeight = blockhashResponse.value.lastValidBlockHeight; 

    // Step 2 - Create a SOL Transfer Transaction
    const messageV0 = new TransactionMessage({
        payerKey: SIGNER_WALLET.publicKey,
        recentBlockhash: blockhashResponse.value.blockhash,
        instructions: [INSTRUCTIONS]
    }).compileToV0Message();
    const transaction = new VersionedTransaction(messageV0);
    transaction.sign([SIGNER_WALLET]);

    // Step 3 - Send Transaction to the Network
    const txId = await SOLANA_CONNECTION.sendTransaction(transaction);
})()
Let's break that down:

  • Step 1: we fetch the latest blockhash. We need this for our Transaction (and we will pass it into our isBlockhashExpired later).
  • Step 2: Create a Transfer Transaction. We're going to use Versioned Transactions. If you have not used these before, check out our Guide: How to Use Versioned Transactions on Solana.
  • Step 3: Call sendTransaction to send the transaction to the network. This will give us a transaction signature, txId, that we can query later.

Check if the Transaction Succeeded or if Blockhash has Expired

Alright! We have sent a transaction to the network--now we need to run two checks: First, we want to check if our transaction has been confirmed or finalized by the network, and if it has not, then we will check to see if the blockhash has expired. If neither is true, we will wait for a couple of seconds and then retry until one is true.

Inside of your async code block, add Step 4:

check if the transaction succeeded or if blockhash has expired

Copy
    // Step 4 - Check transaction status and blockhash status until the transaction succeeds or blockhash expires
    let hashExpired = false;
    let txSuccess = false;
    while (!hashExpired && !txSuccess) {
        const { value: status } = await SOLANA_CONNECTION.getSignatureStatus(txId);

        // Break loop if transaction has succeeded
        if (status && ((status.confirmationStatus === 'confirmed' || 'finalized'))) {
            txSuccess = true;
            const endTime = new Date();
            const elapsed = (endTime.getTime() - START_TIME.getTime())/1000;
            console.log(`Transaction Success. Elapsed time: ${elapsed} seconds.`);
            console.log(`https://explorer.solana.com/tx/${txId}?cluster=devnet`);
            break;
        }

        hashExpired = await isBlockhashExpired(SOLANA_CONNECTION, lastValidHeight);
        
        // Break loop if blockhash has expired
        if (hashExpired) {
            const endTime = new Date();
            const elapsed = (endTime.getTime() - START_TIME.getTime())/1000;
            console.log(`Blockhash has expired. Elapsed time: ${elapsed} seconds.`);
            // (add your own logic to Fetch a new blockhash and resend the transaction or throw an error)
            break;
        }

        // Check again after 2.5 sec
        await sleep(2500);
    }

There's a lot here--let's break it down:

  • First, we define two boolean variables, hashExpired and txSuccess, that we will use to toggle true if either condition is met.
  • Create a while loop that will run until either the blockhash has expired or the transaction has succeeded.
  • Pass our txId into getSignatureStatus to check the status of our transaction.
  • Check if the returned status is confirmed or finalized. If so, we log the time elapsed and a link to our transaction on Solana Explorer. Then we break the loop.
  • If the transaction has not yet succeeded, we check if our hash has expired by passing lastValidHeight into our isBlockhashExpired function.
  • If the blockhash has expired, we log the time elapsed. Then we break the loop. You can add your own retry logic to resend the transaction or throw an error if you would like.
  • If neither condition is met, we will wait for 2.5 seconds using our sleep function. Then the process will repeat.

Run Your Code

Alright, you are all set. In your terminal, run your app:

run your code

Copy
ts-node app

Awesome! More likely than not, you will see one or two iterations of the blockhash loop and a successful transaction:



Nice job. We have found it helpful to rerun this script without sending the transaction to the network to observe the blockhash expiration. We can do this by commenting out a few lines of code (our sendTransaction call and our transaction status check):

run your code

Copy
    //const txId = await SOLANA_CONNECTION.sendTransaction(transaction);

and

run your code

Copy
        //const { value: status } = await SOLANA_CONNECTION.getSignatureStatus(txId);

        // Break loop if the transaction has succeeded
        /*if (status && ((status.confirmationStatus === 'confirmed' || 'finalized'))) {
            txSuccess = true;
            const endTime = new Date();
            const elapsed = (endTime.getTime() - START_TIME.getTime())/1000;
            console.log(`Transaction Success. Elapsed time: ${elapsed} seconds.`);
            console.log(`https://explorer.solana.com/tx/${txId}?cluster=devnet`);
            break;
        } */
Now, go ahead and rerun your code:

run your code

Copy
ts-node app
You should see the console log every few slots and the difference between slots progressing from -150 to 0. The process should take about a minute, and you should see something like this:

Wrap Up

That's it! You're now on your way to providing a better user experience. As the Solana ecosystem continues to mature, expect processes around transaction handling to evolve. To put these concepts into practice, check out some of our other Solana tutorials here.

To stay up to date on the latest Solana news and information, subscribe to our newsletter or follow us on Twitter. If you have any questions or want to discuss your project, drop us a line on Discord.

We ❤️ Feedback!

If you have any feedback or questions on this guide, let us know. We’d love to hear from you!

Related articles 33

Como crear un NFT en SOLANA
Published: Dec 27, 2021
Updated: Sep 23, 2022

¡Hola querido lector! Bienvenidos a una nueva guía de Solana.Solana es una blockchain que promete mucho a la hora de intentar resolver los problemas de escalabilidad que...

Continue reading
How to Get Transaction Logs on Solana
Published: Jun 24, 2022
Updated: Oct 27, 2022

Ever need to pull all the transactions associated with a Wallet? Want to see all of the mint transactions associated with a Candy Machine? Or maybe see transaction history of an NFT? Solana's...

Continue reading
How to Transfer SPL Tokens on Solana
Published: Sep 23, 2022
Updated: Sep 23, 2022

Sending Solana Program Library (SPL) Tokens is a critical mechanism for Solana development. Whether you are airdropping whitelist tokens to your community, bulk sending NFTs to another wallet,...

Continue reading
How to Mint an NFT on Solana
Published: Aug 27, 2021
Updated: Sep 23, 2022

Updated at: April 10, 2022Welcome to another QuickNode guide on Solana - the up-and-coming blockchain that seeks to solve the scalability issues of Ethereum. We will be walking through...

Continue reading
How to Send Bulk Transactions on Solana
Published: Aug 31, 2022
Updated: Oct 3, 2022

Are you running a batch process that has many transactions? Perhaps an airdrop to your community's NFT holders or a token distribution to early users of your dApp. Solana transaction...

Continue reading
Solana Fundamentals Reference Guide
Published: Oct 27, 2022
Updated: Oct 27, 2022

The Solana blockchain is a powerful tool, delivering thousands of transactions per second with almost no-cost transaction fees. If you are new to Web3 or have developed on EVM-based...

Continue reading