Transaction building
Create and structure Stacks transactions for any use case
Overview
Transactions are the fundamental way to interact with the Stacks blockchain. Every action—from transferring STX to calling smart contracts—requires building and broadcasting a transaction. Stacks.js provides a comprehensive transaction builder API that handles encoding, signing, and fee estimation.
Transaction anatomy
Every Stacks transaction contains these core components:
interface StacksTransaction {version: TransactionVersion; // Mainnet or testnetchainId: ChainID; // Network chain IDauth: Authorization; // Signature(s)anchorMode: AnchorMode; // Block anchoring strategypostConditionMode: PostConditionMode; // Strict or allowpostConditions: PostCondition[]; // Security constraintspayload: TransactionPayload; // The actual operation}
Building your first transaction
Create a simple STX transfer transaction:
import {makeSTXTokenTransfer,broadcastTransaction,AnchorMode,FungibleConditionCode,makeStandardSTXPostCondition} from '@stacks/transactions';import { StacksTestnet } from '@stacks/network';async function buildAndSendTransaction() {const network = new StacksTestnet();// Build the transactionconst txOptions = {recipient: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',amount: 1000000, // 1 STX in microSTXsenderKey: 'your-private-key-here', // Only for programmatic signingnetwork,memo: 'First transaction!',fee: 200, // or estimate dynamicallynonce: 0, // or fetch from APIanchorMode: AnchorMode.Any,};const transaction = await makeSTXTokenTransfer(txOptions);// Broadcast to networkconst broadcastResponse = await broadcastTransaction(transaction, network);console.log('Transaction ID:', broadcastResponse.txid);}
Transaction options
Common options available for all transaction types:
interface BaseTxOptions {network: StacksNetwork; // Target networkanchorMode: AnchorMode; // Block anchoring modefee?: number; // Transaction fee in microSTXnonce?: number; // Account noncepostConditions?: PostCondition[]; // Security constraintspostConditionMode?: PostConditionMode; // Strict or allowsponsored?: boolean; // Is transaction sponsored}
Anchor modes
Choose how your transaction is anchored to Bitcoin blocks:
enum AnchorMode {OnChainOnly = 0x01, // Must be included in anchored blockOffChainOnly = 0x02, // Can be included in microblocksAny = 0x03, // Either anchored or microblocks}
Most transactions should use AnchorMode.Any
for flexibility.
Fee estimation
Estimate appropriate fees for your transaction:
import { estimateTransactionFee } from '@stacks/transactions';async function estimateFees() {// Build transaction without broadcastingconst transaction = await makeSTXTokenTransfer({recipient: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',amount: 1000000,senderKey: 'temp-key-for-estimation',network: new StacksTestnet(),anchorMode: AnchorMode.Any,});// Estimate feeconst feeEstimate = await estimateTransactionFee(transaction);console.log(`Estimated fee: ${feeEstimate} microSTX`);// Rebuild with estimated feeconst finalTx = await makeSTXTokenTransfer({recipient: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',amount: 1000000,senderKey: 'your-private-key',network: new StacksTestnet(),fee: Math.ceil(feeEstimate * 1.1), // Add 10% bufferanchorMode: AnchorMode.Any,});}
Nonce management
Handle nonces correctly to avoid transaction conflicts:
import { getNonce } from '@stacks/transactions';async function getNextNonce(address: string, network: StacksNetwork) {try {// Get current nonce from APIconst nonceInfo = await getNonce(address, network);return nonceInfo.possible_next_nonce;} catch (error) {console.error('Failed to fetch nonce:', error);// Start from 0 if account has no transactionsreturn 0;}}// Use in transactionconst nonce = await getNextNonce(senderAddress, network);const tx = await makeSTXTokenTransfer({// ... other optionsnonce,});
Sponsored transactions
Create transactions that will be paid for by a sponsor:
import { sponsorTransaction } from '@stacks/transactions';async function createSponsoredTx() {// User creates the transaction (without paying fees)const unsignedTx = await makeSTXTokenTransfer({recipient: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',amount: 1000000,senderKey: userPrivateKey,network,fee: 0, // User doesn't paysponsored: true,anchorMode: AnchorMode.Any,});// Sponsor signs and sets feeconst sponsoredTx = await sponsorTransaction({transaction: unsignedTx,sponsorPrivateKey: sponsorKey,fee: 1000,});// Broadcast sponsored transactionconst result = await broadcastTransaction(sponsoredTx, network);}
Multi-signature transactions
Build transactions requiring multiple signatures:
import {makeUnsignedSTXTokenTransfer,createMultiSigSpendingCondition,pubKeyfromPrivKey,publicKeyToAddress,AddressVersion} from '@stacks/transactions';async function createMultiSigTransaction() {// Define signersconst privKeyA = 'private-key-a';const privKeyB = 'private-key-b';const privKeyC = 'private-key-c';const pubKeyA = pubKeyfromPrivKey(privKeyA);const pubKeyB = pubKeyfromPrivKey(privKeyB);const pubKeyC = pubKeyfromPrivKey(privKeyC);// Create 2-of-3 multisigconst spending = createMultiSigSpendingCondition(2, // signatures required[pubKeyA, pubKeyB, pubKeyC] // all public keys);// Derive multisig addressconst multisigAddress = publicKeyToAddress(AddressVersion.TestnetMultiSig,spending.signer);// Create unsigned transactionconst unsignedTx = await makeUnsignedSTXTokenTransfer({recipient: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',amount: 1000000,network,anchorMode: AnchorMode.Any,publicKey: multisigAddress, // Use multisig address});// Sign with required number of keys// Implementation depends on your signing flow}
Transaction serialization
Serialize and deserialize transactions for storage or transmission:
import {serializeTransaction,deserializeTransaction,BufferReader} from '@stacks/transactions';// Serialize for storageconst serializedTx = serializeTransaction(transaction);const txHex = serializedTx.toString('hex');// Store or transmit txHex...// Deserialize laterconst txBuffer = Buffer.from(txHex, 'hex');const bufferReader = new BufferReader(txBuffer);const deserializedTx = deserializeTransaction(bufferReader);
Advanced transaction building
Create custom transactions with fine-grained control:
import {makeUnsignedSTXTokenTransfer,createSingleSigSpendingCondition,createTransactionAuth,pubKeyfromPrivKey,TransactionSigner} from '@stacks/transactions';async function buildCustomTransaction() {const privateKey = 'your-private-key';const publicKey = pubKeyfromPrivKey(privateKey);// Create custom authconst spendingCondition = createSingleSigSpendingCondition(0, // nonce200, // feepublicKey);const auth = createTransactionAuth(spendingCondition);// Build unsigned transactionconst unsignedTx = await makeUnsignedSTXTokenTransfer({recipient: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',amount: 1000000,network: new StacksTestnet(),auth,anchorMode: AnchorMode.Any,});// Sign transactionconst signer = new TransactionSigner(unsignedTx);signer.signOrigin(privateKey);const signedTx = signer.transaction;}
Error handling
Implement robust error handling for transactions:
async function safeTransactionBroadcast(transaction: StacksTransaction) {try {const result = await broadcastTransaction(transaction, network);if (result.error) {if (result.reason === 'ConflictingNonceInMempool') {console.error('Nonce conflict - transaction already pending');// Retry with higher nonce} else if (result.reason === 'BadNonce') {console.error('Invalid nonce - refresh and retry');// Fetch fresh nonce} else if (result.reason === 'NotEnoughFunds') {console.error('Insufficient balance');// Check balance before retry}throw new Error(result.reason);}return result;} catch (error) {console.error('Broadcast failed:', error);throw error;}}
Transaction monitoring
Track transaction confirmation status:
async function waitForConfirmation(txId: string, network: StacksNetwork) {const pollingInterval = 10000; // 10 secondsconst maxAttempts = 30; // 5 minutes timeoutfor (let i = 0; i < maxAttempts; i++) {const response = await fetch(`${network.coreApiUrl}/extended/v1/tx/${txId}`);const txInfo = await response.json();if (txInfo.tx_status === 'success') {console.log('Transaction confirmed!');return txInfo;} else if (txInfo.tx_status === 'abort_by_response') {throw new Error('Transaction failed: ' + txInfo.tx_result);}// Wait before next checkawait new Promise(resolve => setTimeout(resolve, pollingInterval));}throw new Error('Transaction confirmation timeout');}
Best practices
- Always estimate fees: Use dynamic fee estimation for better inclusion rates
- Handle nonces carefully: Fetch current nonce to avoid conflicts
- Include post-conditions: Add security constraints when appropriate
- Monitor confirmations: Track transaction status after broadcasting
- Implement retry logic: Handle temporary failures gracefully
Common patterns
Batch transaction builder
class TransactionBatch {private transactions: StacksTransaction[] = [];async addSTXTransfer(recipient: string, amount: number) {const tx = await makeSTXTokenTransfer({recipient,amount,// ... other options});this.transactions.push(tx);}async broadcastAll() {const results = [];for (const tx of this.transactions) {try {const result = await broadcastTransaction(tx, network);results.push({ success: true, result });} catch (error) {results.push({ success: false, error });}}return results;}}