Solana durable nonce: sign now, send later
Project overview
SolTool is a collection of Solana power tools for builders and traders. Visit the live app: soltool.dev
- Atomic swap bundling via Jito
- Durable nonce swaps
- Token creator with on‑chain metadata + Irys storage
- Web3 storage uploads with transparent pricing
- Multisig approvals via WebRTC/WebSocket relay (coordination demo)
- Priority fee assistant
- On‑chain utilities: ATA creation, token splitter, account closer, batch sender, transaction executor
Frontend: React/TypeScript with @solana/web3.js and @solana/spl-token. Backend: Node/Express.
Durable-Nonce Swaps (Sign Now, Send Later)
Why?
Recent blockhashes expire quickly. If you need to stage a swap for later, say, align with a signal, coordinate across time zones, or wait for approvals, the usual blockhash flow can fail before you’re ready. You need a transaction that stays valid until you decide to submit it.
What?
Durable‑nonce swaps let you sign now and broadcast later. You create a nonce account once, generate unsigned swaps anchored to that nonce, sign and store them, then submit anytime.
- Where to try: soltool.dev/nonce-swap
- Who it’s for: Scheduled traders, teams coordinating manual execution, and anyone who hates racing the clock on blockhash expiry.
How?
Backend API:
- `POST /api/v1/nonce/generate-nonce-account-tx`:
Returns an unsigned create‑nonce transaction, the nonce account pubkey and rent, so you can sign (wallet + nonce keypair) and send once.
- `POST /api/v1/nonce/generate-swap`:
Validates request (mints, amounts, token accounts) and returns an unsigned nonce‑based swap transaction.
Get Elton Thomas’s stories in your inbox
Join Medium for free to get updates from this writer.
Frontend flow:
1) Create nonce account (one‑time): decode returned tx, sign with wallet + nonce keypair, send and confirm; store `nonceAccount` locally.
2) Generate swap: call `generate-swap` with the nonce account, pool details, token accounts, and slippage/min‑out.
3) Sign + store: decode base58, sign with wallet, re‑encode and save (e.g., `localStorage`).
4) Execute later: load the saved signed bytes and `sendRawTransaction`, then confirm.
Tips:
- Pre‑create ATAs for source/destination to avoid surprises at execution time.
- Prefer exact `minOutAmount` for high‑precision control; we convert using live pool decimals when available.
Challenges & Solutions
- Secure nonce account creation
- Challenge: The nonce account needs its own keypair and must co‑sign the creation tx with the user wallet; mishandling the secret could leak keys.
- Solution: The backend only returns the unsigned tx and nonce account data; signing happens client‑side. We immediately use and discard the nonce account secret after creation, storing only the public key.
// Backend: Generate unsigned nonce account creation tx
public async generateNonceAccountTransaction(payer: PublicKey) {
const nonceAccountKeypair = Keypair.generate();
const rentExemptBalance = await this.connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH);
const tx = new Transaction().add(
// Create nonce account
SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: nonceAccountKeypair.publicKey,
lamports: rentExemptBalance,
space: NONCE_ACCOUNT_LENGTH,
programId: SystemProgram.programId,
}),
// Initialize nonce account
SystemProgram.nonceInitialize({
noncePubkey: nonceAccountKeypair.publicKey,
authorizedPubkey: payer,
})
);
tx.feePayer = payer;
tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
return {
nonceAccountKeypair,
unsignedTransaction: tx,
rentExemptBalance
};
}
// Frontend: Sign with wallet + nonce keypair, then discard nonce secret
const tx = Transaction.from(bs58.decode(unsignedTransaction));
const nonceAccountKeypair = Keypair.fromSecretKey(bs58.decode(nonceAccountSecretKey));
// User wallet signs first
const signedTx = await signTransaction(tx);
// Nonce account must also sign (partial sign)
signedTx.partialSign(nonceAccountKeypair);
// Send and confirm
const signature = await connection.sendRawTransaction(signedTx.serialize());
await connection.confirmTransaction(signature, 'confirmed');
// Store only the public key, discard the secret
localStorage.setItem(`nonceAccount_${walletAddress}`, nonceAccountPubkey);- Nonce reuse and freshness
- Challenge: Reusing a consumed nonce fails; using a stale nonce leads to rejection.
- Solution: Each generated swap fetches the latest nonce state on the backend; the unsigned tx encodes a fresh nonce instruction.
// Backend: Build swap transaction with nonce advance as FIRST instruction
async buildNonceSwapTransaction(params: BuildNonceSwapTransactionParams): Promise<string> {
const { nonceAccount, nonce, payer, sourceMint, destinationMint, inAmount, minOutAmount, ... } = params;
const swapTransaction = new Transaction();
// 1. Add nonce advance as the FIRST instruction (critical!)
swapTransaction.add(
SystemProgram.nonceAdvance({
noncePubkey: nonceAccount,
authorizedPubkey: payer
})
);
// 2. Add compute budget instructions
swapTransaction.add(ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnitLimit }));
swapTransaction.add(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priorityFeeMicroLamports }));
// 3. Add wrap SOL if needed
if (sourceMint.equals(NATIVE_MINT)) {
const wrapIxs = wrapSOLInstruction(payer, userSourceTokenAddr, BigInt(inAmount.toString()));
wrapIxs.forEach(ix => swapTransaction.add(ix));
}
// 4. Add the actual swap instruction
const swapIx = await buildSwapInstruction(...);
swapTransaction.add(swapIx);
// 5. Use nonce as recentBlockhash (this is the key!)
swapTransaction.recentBlockhash = nonce;
swapTransaction.feePayer = payer;
return bs58.encode(swapTransaction.serialize({ requireAllSignatures: false }));
}- Coordinating token accounts
- Challenge: Users lacked destination ATAs when generating the swap, causing runtime failures later.
- Solution: We check and create ATAs up front (or derive addresses as fallback), embedding addresses in the swap payload so the backend can route tokens correctly.
- Amount precision
- Challenge: Converting human inputs to lamports across varied decimals caused rounding errors.
- Solution: We convert with token‑specific decimals from pool info and log the lamport math for transparency.
Note: This website is a collection of cool stuff I learnt to do on Solana, thus it’s also a showcase of my skills. If anyone wants to build anything cool on Solana contact me. You can check out my contact info directly on SolTool.dev






