Solana CookbookTransactions
Offline Transactions
Sign Transaction
To create an offline transaction, you have to sign the transaction and then anyone can broadcast it on the network.
sign-transaction.ts
import {clusterApiUrl,Connection,Keypair,Transaction,SystemProgram,LAMPORTS_PER_SOL,Message,} from "@solana/web3.js";import * as nacl from "tweetnacl";import * as bs58 from "bs58";// To complete an offline transaction, I will separate them into four steps// 1. Create Transaction// 2. Sign Transaction// 3. Recover Transaction// 4. Send Transaction(async () => {// create connectionconst connection = new Connection(clusterApiUrl("devnet"), "confirmed");// create an example tx, alice transfer to bob and feePayer is `feePayer`// alice and feePayer are signer in this txconst feePayer = Keypair.generate();await connection.confirmTransaction(await connection.requestAirdrop(feePayer.publicKey, LAMPORTS_PER_SOL),);const alice = Keypair.generate();await connection.confirmTransaction(await connection.requestAirdrop(alice.publicKey, LAMPORTS_PER_SOL),);const bob = Keypair.generate();// 1. Create Transactionlet tx = new Transaction().add(SystemProgram.transfer({fromPubkey: alice.publicKey,toPubkey: bob.publicKey,lamports: 0.1 * LAMPORTS_PER_SOL,}),);tx.recentBlockhash = (await connection.getRecentBlockhash()).blockhash;tx.feePayer = feePayer.publicKey;let realDataNeedToSign = tx.serializeMessage(); // the real data singer need to sign.// 2. Sign Transaction// use any lib you like, the main idea is to use ed25519 to sign it.// the return signature should be 64 bytes.let feePayerSignature = nacl.sign.detached(realDataNeedToSign,feePayer.secretKey,);let aliceSignature = nacl.sign.detached(realDataNeedToSign, alice.secretKey);// 3. Recover Transaction// you can verify signatures before you recovering the transactionlet verifyFeePayerSignatureResult = nacl.sign.detached.verify(realDataNeedToSign,feePayerSignature,feePayer.publicKey.toBytes(), // you should use the raw pubkey (32 bytes) to verify);console.log(`verify feePayer signature: ${verifyFeePayerSignatureResult}`);let verifyAliceSignatureResult = nacl.sign.detached.verify(realDataNeedToSign,aliceSignature,alice.publicKey.toBytes(),);console.log(`verify alice signature: ${verifyAliceSignatureResult}`);// there are two ways you can recover the tx// 3.a Recover Transaction (use populate then addSignature){let recoverTx = Transaction.populate(Message.from(realDataNeedToSign));recoverTx.addSignature(feePayer.publicKey, Buffer.from(feePayerSignature));recoverTx.addSignature(alice.publicKey, Buffer.from(aliceSignature));// 4. Send transactionconsole.log(`txhash: ${await connection.sendRawTransaction(recoverTx.serialize())}`,);}// or// 3.b. Recover Transaction (use populate with signature){let recoverTx = Transaction.populate(Message.from(realDataNeedToSign), [bs58.encode(feePayerSignature),bs58.encode(aliceSignature),]);// 4. Send transactionconsole.log(`txhash: ${await connection.sendRawTransaction(recoverTx.serialize())}`,);}// if this process takes too long, your recent blockhash will expire (after 150 blocks).// you can use `durable nonce` to get rid of it.})();
Partial Sign Transaction
When a transaction requires multiple signatures, you can partially sign it. The other signers can then sign and broadcast it on the network.
Some examples of when this is useful:
- Send an SPL token in return for payment
- Sign a transaction so that you can later verify its authenticity
- Call custom programs in a transaction that require your signature
In this example Bob sends Alice an SPL token in return for her payment:
partial-sign-transaction.ts
import {createTransferCheckedInstruction,getAssociatedTokenAddress,getMint,getOrCreateAssociatedTokenAccount,} from "@solana/spl-token";import {clusterApiUrl,Connection,Keypair,LAMPORTS_PER_SOL,PublicKey,SystemProgram,Transaction,} from "@solana/web3.js";import base58 from "bs58";/* The transaction:* - sends 0.01 SOL from Alice to Bob* - sends 1 token from Bob to Alice* - is partially signed by Bob, so Alice can approve + send it*/(async () => {const connection = new Connection(clusterApiUrl("devnet"), "confirmed");const alicePublicKey = new PublicKey("5YNmS1R9nNSCDzb5a7mMJ1dwK9uHeAAF4CmPEwKgVWr8",);const bobKeypair = Keypair.fromSecretKey(base58.decode("4NMwxzmYj2uvHuq8xoqhY8RXg63KSVJM1DXkpbmkUY7YQWuoyQgFnnzn6yo3CMnqZasnNPNuAT2TLwQsCaKkUddp",),);const tokenAddress = new PublicKey("Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr",);const bobTokenAddress = await getAssociatedTokenAddress(tokenAddress,bobKeypair.publicKey,);// Alice may not have a token account, so Bob creates one if notconst aliceTokenAccount = await getOrCreateAssociatedTokenAccount(connection,bobKeypair, // Bob pays the fee to create ittokenAddress, // which token the account is foralicePublicKey, // who the token account is for);// Get the details about the token mintconst tokenMint = await getMint(connection, tokenAddress);// Get a recent blockhash to include in the transactionconst { blockhash } = await connection.getLatestBlockhash("finalized");const transaction = new Transaction({recentBlockhash: blockhash,// Alice pays the transaction feefeePayer: alicePublicKey,});// Transfer 0.01 SOL from Alice -> Bobtransaction.add(SystemProgram.transfer({fromPubkey: alicePublicKey,toPubkey: bobKeypair.publicKey,lamports: 0.01 * LAMPORTS_PER_SOL,}),);// Transfer 1 token from Bob -> Alicetransaction.add(createTransferCheckedInstruction(bobTokenAddress, // sourcetokenAddress, // mintaliceTokenAccount.address, // destinationbobKeypair.publicKey, // owner of source account1 * 10 ** tokenMint.decimals, // amount to transfertokenMint.decimals, // decimals of token),);// Partial sign as Bobtransaction.partialSign(bobKeypair);// Serialize the transaction and convert to base64 to return itconst serializedTransaction = transaction.serialize({// We will need Alice to deserialize and sign the transactionrequireAllSignatures: false,});const transactionBase64 = serializedTransaction.toString("base64");return transactionBase64;// The caller of this can convert it back to a transaction object:const recoveredTransaction = Transaction.from(Buffer.from(transactionBase64, "base64"),);})();
Durable Nonce
recentBlockhash
is an important value for a transaction. Your transaction
will
be rejected if you use an expired blockhash (older than 150 blocks). Instead of
a recent blockhash, you can use a durable nonce, which never expires. To use a
durable nonce, your transaction must:
- use a
nonce
stored innonce account
as a recent blockhash - put
nonce advance
operation in the first instruction
Create Nonce Account
create-nonce-account.ts
import {clusterApiUrl,Connection,Keypair,Transaction,NONCE_ACCOUNT_LENGTH,SystemProgram,LAMPORTS_PER_SOL,} from "@solana/web3.js";(async () => {// Setup our connection and walletconst connection = new Connection(clusterApiUrl("devnet"), "confirmed");const feePayer = Keypair.generate();// Fund our wallet with 1 SOLconst airdropSignature = await connection.requestAirdrop(feePayer.publicKey,LAMPORTS_PER_SOL,);await connection.confirmTransaction(airdropSignature);// you can use any keypair as nonce account authority,// this uses the default Solana keypair file (id.json) as the nonce account authorityconst nonceAccountAuth = await getKeypairFromFile();let nonceAccount = Keypair.generate();console.log(`nonce account: ${nonceAccount.publicKey.toBase58()}`);let tx = new Transaction().add(// create nonce accountSystemProgram.createAccount({fromPubkey: feePayer.publicKey,newAccountPubkey: nonceAccount.publicKey,lamports:await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH,),space: NONCE_ACCOUNT_LENGTH,programId: SystemProgram.programId,}),// init nonce accountSystemProgram.nonceInitialize({noncePubkey: nonceAccount.publicKey, // nonce account pubkeyauthorizedPubkey: nonceAccountAuth.publicKey, // nonce account authority (for advance and close)}),);console.log(`txhash: ${await sendAndConfirmTransaction(connection, tx, [feePayer, nonceAccount])}`,);})();
Get Nonce Account
get-nonce-account.ts
import {clusterApiUrl,Connection,PublicKey,Keypair,NonceAccount,} from "@solana/web3.js";(async () => {const connection = new Connection(clusterApiUrl("devnet"), "confirmed");const nonceAccountPubkey = new PublicKey("7H18z3v3rZEoKiwY3kh8DLn9eFT6nFCQ2m4kiC7RZ3a4",);let accountInfo = await connection.getAccountInfo(nonceAccountPubkey);let nonceAccount = NonceAccount.fromAccountData(accountInfo.data);console.log(`nonce: ${nonceAccount.nonce}`);console.log(`authority: ${nonceAccount.authorizedPubkey.toBase58()}`);console.log(`fee calculator: ${JSON.stringify(nonceAccount.feeCalculator)}`);})();
Use Nonce Account
use-nonce-account.ts
import {clusterApiUrl,Connection,PublicKey,Keypair,Transaction,SystemProgram,NonceAccount,LAMPORTS_PER_SOL,} from "@solana/web3.js";import * as bs58 from "bs58";import { getKeypairFromFile } from "@solana-developers/helpers";(async () => {// Setup our connection and walletconst connection = new Connection(clusterApiUrl("devnet"), "confirmed");const feePayer = Keypair.generate();// Fund our wallet with 1 SOLconst airdropSignature = await connection.requestAirdrop(feePayer.publicKey,LAMPORTS_PER_SOL,);await connection.confirmTransaction(airdropSignature);// you can use any keypair as nonce account authority,// but nonceAccountAuth must be the same as the one used in nonce account creation// load default solana keypair for nonce account authorityconst nonceAccountAuth = await getKeypairFromFile();const nonceAccountPubkey = new PublicKey("7H18z3v3rZEoKiwY3kh8DLn9eFT6nFCQ2m4kiC7RZ3a4",);let nonceAccountInfo = await connection.getAccountInfo(nonceAccountPubkey);let nonceAccount = NonceAccount.fromAccountData(nonceAccountInfo.data);let tx = new Transaction().add(// nonce advance must be the first instructionSystemProgram.nonceAdvance({noncePubkey: nonceAccountPubkey,authorizedPubkey: nonceAccountAuth.publicKey,}),// after that, you do what you really want to do, here we append a transfer instruction as an example.SystemProgram.transfer({fromPubkey: feePayer.publicKey,toPubkey: nonceAccountAuth.publicKey,lamports: 1,}),);// assign `nonce` as recentBlockhashtx.recentBlockhash = nonceAccount.nonce;tx.feePayer = feePayer.publicKey;tx.sign(feePayer,nonceAccountAuth,); /* fee payer + nonce account authority + ... */console.log(`txhash: ${await connection.sendRawTransaction(tx.serialize())}`);})();