閱讀時間 10 分鐘
概覽
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. 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.
您需要準備的物品
- 已安裝Node.js(版本 16.15 或更高)
- TypeScript experience and ts-node installed
- Experience with running basic Solana Transactions. Check out this (Quicknode guide titled How to Send Transactions on Solana using JavaScript for more information.)
設定您的專案
Create a new project directory in your terminal with the following:
mkdir solana-tx-propagation
cd solana-tx-propagation
為您的應用程式建立一個名為app.ts 的檔案:
echo > app.ts
請使用「yes」參數初始化您的專案,以便為新套件採用預設值:
yarn init --yes
#或
npm init --yes
建立一個已啟用 .json 匯入功能的tsconfig.json檔案:
tsc -init --resolveJsonModule true
安裝 Solana Web3 依賴項
在這個練習中,我們需要加入 Solana Web3 函式庫。請在終端機中輸入:
yarn add @solana/web3.js@1
#或
npm install @solana/web3.js@1
建立錢包並領取 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. If you already have a paper wallet and just need some devnet SOL, you can request an airdrop below:
注意:若過於頻繁地發送空投請求,可能會觸發 429(請求過多)錯誤。
Make sure you save your wallet to your project directory as guideSecret.json.
匯入必要的依賴項
Open app.ts, and paste the following imports on lines 1-2:
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.
設定您的 Quicknode 端點
若要在 Solana 上進行開發,您需要一個 API 端點來連線至該網路。您可以使用公開節點,或自行部署並管理基礎架構;不過,若您希望將回應時間縮短 8 倍,不妨將繁重的工作交給我們處理。
了解為何超過 50% 的 Solana 專案都選擇 Quicknode,並在此註冊帳號。我們將使用一個 Solana Devnet 節點。
複製 HTTP 提供者連結:

在app.ts檔案中,於 import 語句下方,宣告您的 RPC 並建立與 Solana 的連線:
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:

設定您的應用程式
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:
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:
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):
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:
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:
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:
- Fetches the current block height using getBlockHeight.
- (optional) Prints the blockhash expiration over time. (This is optionally added to help demonstrate how the expirations change)
- 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:
(async()=>{
})()
Within your code block, let's create and send a transaction based on the TransactionInstruction we defined earlier:
(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);
})()
讓我們來分析一下:
- 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:
// 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: statuses } = await SOLANA_CONNECTION.getSignatureStatuses([txId]);
if (!statuses || statuses.length === 0) {
throw new Error('Failed to get signature status');
}
const status = statuses[0];
if (status.err) {
throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
}
// Break loop if transaction has succeeded
if (status && ((status.confirmationStatus === 'confirmed' || status.confirmationStatus === '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 getSignatureStatuses 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.
執行您的程式碼
Alright, you are all set. In your terminal, run your app:
ts-node 應用程式
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):
//const txId = await SOLANA_CONNECTION.sendTransaction(transaction);
以及
/* const { value: statuses } = await SOLANA_CONNECTION.getSignatureStatuses([txId]);
if (!statuses || statuses.length === 0) {
throw new Error('Failed to get signature status');
}
const status = statuses[0];
if (status.err) {
throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
} */
// Break loop if the transaction has succeeded
/*if (status && ((status.confirmationStatus === 'confirmed' || status.confirmationStatus === '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:
ts-node 應用程式
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:

總結
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.
我們 ❤️ 您的回饋!
如果您對這份指南有任何意見或疑問,請告訴我們。我們非常樂意聽取您的意見!
