Solana Trackers Standalone
The Solana tracker of @tuwaio/pulsar-solana also works without the Pulsar store. Use it when you:
- keep transactions in your own state (Redux, MobX, another Zustand store, a database);
- track transactions on a server, where there is no
localStorageand no wallet; - need a callback for each check instead of store updates.
Without the store nothing is persisted and nothing is resumed after a restart: you decide what to save and when to start tracking again. The tracker uses only @solana/kit RPC calls, so it runs in the browser and in Node.js.
Why solanaFetcher Instead of Manual Polling
| Situation | Manual getSignatureStatuses polling | solanaFetcher with initializePollingTracker |
|---|---|---|
| The RPC has not seen the signature | You decide how long to wait. | With lastValidBlockHeight, fails as “expired” as soon as the chain passes it; otherwise one hour after localTimestamp. |
| Transient RPC errors | You write the retry logic. | Errors are retried on the next tick; tracking gives up after maxRetries consecutive errors. |
| Tracking resumed later | Recent statuses only, unless you pass searchTransactionHistory. | Always searches the transaction history, so transactions that finished while you were away are found. |
| Transaction details | A separate getTransaction call. | Fetches fee, recentBlockhash and instructions once and adds them to every reported status; a getTransaction error never hides the status or fails the transaction. |
| Progress and final status | You compare commitment levels yourself. | onIntervalTick with processed and confirmed, onSuccess at finalized, onFailure as soon as there is an error. |
Tracking a Signature: solanaFetcher
Pass solanaFetcher to initializePollingTracker from @tuwaio/pulsar-core. It starts polling in the background and returns immediately:
import { OrbitAdapter, SOLANA_CHAIN_IDS } from '@tuwaio/orbit-core';
import { initializePollingTracker } from '@tuwaio/pulsar-core';
import { solanaFetcher } from '@tuwaio/pulsar-solana';
export function trackSignature(signature: string, lastValidBlockHeight?: number) {
initializePollingTracker({
tx: {
adapter: OrbitAdapter.SOLANA,
txKey: signature,
chainId: SOLANA_CHAIN_IDS.devnet, // the CAIP-2 chain ID; picks the public endpoint when rpcUrl is not set
rpcUrl: 'https://api.devnet.solana.com', // prefer your own RPC provider
localTimestamp: Math.floor(Date.now() / 1000), // when the transaction was sent, in seconds
lastValidBlockHeight, // from the blockhash the transaction was signed with; optional (see below)
pending: true, // polling starts only for pending transactions
},
fetcher: solanaFetcher,
pollingInterval: 1000,
onIntervalTick: (status) => console.log(status.confirmationStatus, status.confirmations), // 'confirmed' within about a second
onSuccess: (status) => console.log('Finalized in slot', status.slot, 'fee', status.fee),
onFailure: (status) => {
if (status?.expired) console.error('Expired: the blockhash is no longer valid, send the transaction again');
else console.error(status ? 'Failed on-chain' : 'Not finalized in time or too many RPC errors', status?.err);
},
});
}How each check works:
getSignatureStatuseswithsearchTransactionHistory: true. If the signature is unknown and the transaction has alastValidBlockHeight(orsignAndSendSolanaTxsent it in this page, which records the height), the fetcher sendsgetBlockHeight(commitmentconfirmed): once the chain is past that height, it checks the signature once more and, still unknown, callsonFailurewithexpired: true, because the blockhash expired and the transaction can never land. Otherwise it waits for the next tick, until one hour afterlocalTimestamp.getTransaction(commitmentconfirmed,maxSupportedTransactionVersion: 1, so legacy, version 0 and version 1 transactions are read) for the fee, blockhash and instructions, only until it has returned them once for thistxobject: the details are cached in memory for the tracking run, or taken fromtxif it already has them. AgetTransactionerror is not thrown: the check goes on with the status alone and asks again on the next tick.onIntervalTickwith the status (and the details once known); thenonFailurewith the status if it has anerr, right away, oronSuccesswhen it isfinalizedand the details are known. A finalized transaction whose details are still missing on its 10th check getsonSuccesswithout them, with a warning in the console. A transaction that is not finalized one hour afterlocalTimestampcallsonFailure; a finalized one never does.
lastValidBlockHeight comes from the blockhash you signed with (getLatestBlockhash returns both). Durable-nonce transactions have no such limit: leave it out for them.
Errors of getSignatureStatuses and getBlockHeight are thrown to initializePollingTracker, which retries on the next tick and calls onFailure() without arguments after maxRetries (default 10) consecutive errors.
Helpers
signAndSendSolanaTx
Builds a version 0 transaction from one or more instructions, with the signer as fee payer and the latest blockhash, and has the signer sign and send it. It returns the base58 signature, ready for solanaFetcher:
import type { Instruction, TransactionSendingSigner } from '@solana/kit';
import type { SolanaClient } from '@tuwaio/orbit-solana';
import { signAndSendSolanaTx } from '@tuwaio/pulsar-solana';
export async function send(client: SolanaClient, signer: TransactionSendingSigner, instructions: Instruction[]) {
const signature = await signAndSendSolanaTx({ client, signer, instruction: instructions });
console.log('Sent', signature);
return signature;
}It rejects with the RPC or signer error, for example when the user rejects the transaction in the wallet.
checkSolanaChain
Compares two cluster identifiers exactly and throws SolanaChainMismatchError when they differ. The Solana adapter uses it to compare desiredChainID with the cluster saved for the wallet connection after turning both into cluster monikers with getCluster from @tuwaio/orbit-solana, so devnet, solana:devnet and the genesis-hash chain ID solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 match:
import { checkSolanaChain, SolanaChainMismatchError } from '@tuwaio/pulsar-solana';
export function isOnCluster(requiredCluster: string, walletCluster: string): boolean {
try {
checkSolanaChain(requiredCluster, walletCluster);
return true;
} catch (error) {
if (error instanceof SolanaChainMismatchError) {
console.warn(`Switch the wallet from ${error.currentChain} to ${error.requiredChain}.`);
return false;
}
throw error;
}
}The full list of exports is in the @tuwaio/pulsar-solana reference.