Common patterns
Reusable post-condition patterns for typical blockchain operations
Overview
This reference provides battle-tested post-condition patterns for common Stacks operations. Copy and adapt these patterns to ensure your transactions are protected against unexpected behavior.
Token transfer patterns
Simple token transfer
Ensure exact token amounts are transferred:
function createTokenTransferConditions(sender: string,recipient: string,amount: number,tokenInfo: AssetInfo): PostCondition[] {return [// Sender sends exactly the amountmakeStandardFungiblePostCondition(sender,FungibleConditionCode.Equal,amount,tokenInfo),// Optional: Verify recipient receivesmakeStandardFungiblePostCondition(recipient,FungibleConditionCode.Equal,-amount, // Negative for receivingtokenInfo),];}
Transfer with fees
Handle transfers where contracts take fees:
function createTransferWithFeeConditions(sender: string,recipient: string,amount: number,feeAmount: number,tokenInfo: AssetInfo,feeContract: string,feeContractName: string): PostCondition[] {const totalAmount = amount + feeAmount;return [// Sender pays total (amount + fee)makeStandardFungiblePostCondition(sender,FungibleConditionCode.Equal,totalAmount,tokenInfo),// Recipient receives amount (not fee)makeStandardFungiblePostCondition(recipient,FungibleConditionCode.Equal,-amount,tokenInfo),// Contract receives exactly the feemakeContractFungiblePostCondition(feeContract,feeContractName,FungibleConditionCode.Equal,-feeAmount,tokenInfo),];}
DEX and swap patterns
Token swap with slippage
Protect against excessive slippage in swaps:
interface SwapParams {user: string;dexContract: string;dexName: string;tokenIn: AssetInfo;tokenOut: AssetInfo;amountIn: number;minAmountOut: number;}function createSwapConditions(params: SwapParams): PostCondition[] {return [// User sends exact amount of tokenInmakeStandardFungiblePostCondition(params.user,FungibleConditionCode.Equal,params.amountIn,params.tokenIn),// User receives at least minAmountOut of tokenOutmakeStandardFungiblePostCondition(params.user,FungibleConditionCode.GreaterEqual,-params.minAmountOut,params.tokenOut),// DEX receives tokenInmakeContractFungiblePostCondition(params.dexContract,params.dexName,FungibleConditionCode.Equal,-params.amountIn,params.tokenIn),// DEX sends at least minAmountOut of tokenOutmakeContractFungiblePostCondition(params.dexContract,params.dexName,FungibleConditionCode.GreaterEqual,params.minAmountOut,params.tokenOut),];}
Liquidity provision
Add liquidity with balanced conditions:
function createAddLiquidityConditions(user: string,poolContract: string,poolName: string,tokenA: AssetInfo,tokenB: AssetInfo,amountA: number,amountB: number,minLPTokens: number,lpToken: AssetInfo): PostCondition[] {return [// User provides tokenAmakeStandardFungiblePostCondition(user,FungibleConditionCode.Equal,amountA,tokenA),// User provides tokenBmakeStandardFungiblePostCondition(user,FungibleConditionCode.Equal,amountB,tokenB),// User receives at least minimum LP tokensmakeStandardFungiblePostCondition(user,FungibleConditionCode.GreaterEqual,-minLPTokens,lpToken),// Pool receives exact amountsmakeContractFungiblePostCondition(poolContract,poolName,FungibleConditionCode.Equal,-amountA,tokenA),makeContractFungiblePostCondition(poolContract,poolName,FungibleConditionCode.Equal,-amountB,tokenB),];}
NFT patterns
NFT sale
Ensure safe NFT marketplace transactions:
function createNFTSaleConditions(seller: string,buyer: string,marketContract: string,marketName: string,nftAsset: AssetInfo,nftId: BufferCV,price: number,marketFee: number): PostCondition[] {return [// Seller transfers NFTmakeStandardNonFungiblePostCondition(seller,NonFungibleConditionCode.Sends,nftAsset,nftId),// Buyer pays total pricemakeStandardSTXPostCondition(buyer,FungibleConditionCode.Equal,price),// Seller receives price minus feemakeStandardSTXPostCondition(seller,FungibleConditionCode.Equal,-(price - marketFee)),// Marketplace receives feemakeContractSTXPostCondition(marketContract,marketName,FungibleConditionCode.Equal,-marketFee),];}
NFT auction settlement
Complex conditions for auction completion:
function createAuctionSettlementConditions(seller: string,winner: string,auctionContract: string,auctionName: string,nftAsset: AssetInfo,nftId: BufferCV,winningBid: number,platformFee: number,royalty: number,royaltyRecipient: string): PostCondition[] {const sellerReceives = winningBid - platformFee - royalty;return [// NFT goes from seller to winnermakeStandardNonFungiblePostCondition(seller,NonFungibleConditionCode.Sends,nftAsset,nftId),// Winner pays winning bidmakeStandardSTXPostCondition(winner,FungibleConditionCode.Equal,winningBid),// Seller receives bid minus feesmakeStandardSTXPostCondition(seller,FungibleConditionCode.Equal,-sellerReceives),// Platform receives feemakeContractSTXPostCondition(auctionContract,auctionName,FungibleConditionCode.Equal,-platformFee),// Royalty recipient receives royaltymakeStandardSTXPostCondition(royaltyRecipient,FungibleConditionCode.Equal,-royalty),];}
Staking patterns
Token staking
Conditions for staking tokens:
function createStakingConditions(staker: string,stakingContract: string,stakingContractName: string,stakeToken: AssetInfo,rewardToken: AssetInfo,stakeAmount: number,immediateReward?: number): PostCondition[] {const conditions = [// User stakes tokensmakeStandardFungiblePostCondition(staker,FungibleConditionCode.Equal,stakeAmount,stakeToken),// Contract receives stakemakeContractFungiblePostCondition(stakingContract,stakingContractName,FungibleConditionCode.Equal,-stakeAmount,stakeToken),];// If immediate rewards are givenif (immediateReward && immediateReward > 0) {conditions.push(makeContractFungiblePostCondition(stakingContract,stakingContractName,FungibleConditionCode.Equal,immediateReward,rewardToken),makeStandardFungiblePostCondition(staker,FungibleConditionCode.Equal,-immediateReward,rewardToken));}return conditions;}
Unstaking with rewards
Handle unstaking with accumulated rewards:
function createUnstakingConditions(staker: string,stakingContract: string,stakingContractName: string,stakeToken: AssetInfo,rewardToken: AssetInfo,unstakeAmount: number,minRewards: number): PostCondition[] {return [// User receives staked tokens backmakeStandardFungiblePostCondition(staker,FungibleConditionCode.Equal,-unstakeAmount,stakeToken),// Contract returns staked tokensmakeContractFungiblePostCondition(stakingContract,stakingContractName,FungibleConditionCode.Equal,unstakeAmount,stakeToken),// User receives at least minimum rewardsmakeStandardFungiblePostCondition(staker,FungibleConditionCode.GreaterEqual,-minRewards,rewardToken),// Contract sends rewardsmakeContractFungiblePostCondition(stakingContract,stakingContractName,FungibleConditionCode.GreaterEqual,minRewards,rewardToken),];}
Governance patterns
Voting with token lock
Ensure tokens are locked during voting:
function createVotingConditions(voter: string,governanceContract: string,governanceName: string,govToken: AssetInfo,voteAmount: number): PostCondition[] {return [// Voter locks tokens for votingmakeStandardFungiblePostCondition(voter,FungibleConditionCode.Equal,voteAmount,govToken),// Governance contract receives tokensmakeContractFungiblePostCondition(governanceContract,governanceName,FungibleConditionCode.Equal,-voteAmount,govToken),];}
Multi-asset patterns
Batch operations
Handle multiple assets in one transaction:
function createBatchTransferConditions(sender: string,transfers: Array<{recipient: string;amount: number;asset: AssetInfo;}>): PostCondition[] {const conditions: PostCondition[] = [];// Group by asset to calculate total per assetconst assetTotals = new Map<string, number>();transfers.forEach(transfer => {const assetKey = `${transfer.asset.contractAddress}.${transfer.asset.contractName}::${transfer.asset.assetName}`;const current = assetTotals.get(assetKey) || 0;assetTotals.set(assetKey, current + transfer.amount);// Add recipient conditionconditions.push(makeStandardFungiblePostCondition(transfer.recipient,FungibleConditionCode.Equal,-transfer.amount,transfer.asset));});// Add sender conditions for each assetassetTotals.forEach((total, assetKey) => {const [contractPart, assetName] = assetKey.split('::');const [contractAddress, contractName] = contractPart.split('.');const asset = createAssetInfo(contractAddress, contractName, assetName);conditions.push(makeStandardFungiblePostCondition(sender,FungibleConditionCode.Equal,total,asset));});return conditions;}
Utility functions
Post-condition builder
Flexible builder for complex scenarios:
class PostConditionPatternBuilder {private patterns: Map<string, (params: any) => PostCondition[]> = new Map();constructor() {// Register common patternsthis.patterns.set('token-transfer', createTokenTransferConditions);this.patterns.set('token-swap', createSwapConditions);this.patterns.set('nft-sale', createNFTSaleConditions);this.patterns.set('staking', createStakingConditions);}register(name: string, pattern: (params: any) => PostCondition[]) {this.patterns.set(name, pattern);}build(patternName: string, params: any): PostCondition[] {const pattern = this.patterns.get(patternName);if (!pattern) {throw new Error(`Unknown pattern: ${patternName}`);}return pattern(params);}combine(...conditionSets: PostCondition[][]): PostCondition[] {return conditionSets.flat();}}// Usageconst builder = new PostConditionPatternBuilder();const swapConditions = builder.build('token-swap', {user: userAddress,dexContract,dexName: 'amm-v1',tokenIn: tokenAInfo,tokenOut: tokenBInfo,amountIn: 1000,minAmountOut: 950,});
Condition validator
Validate conditions before transaction:
function validatePostConditions(conditions: PostCondition[],expectedTransfers: Array<{from: string;to?: string;amount: number;asset: 'STX' | AssetInfo;}>): boolean {// Verify all expected transfers have conditionsfor (const transfer of expectedTransfers) {const hasCondition = conditions.some(condition => {if (transfer.asset === 'STX' && condition.conditionType === ConditionType.STX) {return condition.principal === transfer.from;}// Additional validation logicreturn false;});if (!hasCondition) {console.warn(`Missing condition for transfer from ${transfer.from}`);return false;}}return true;}
Testing patterns
Test your post-conditions:
import { describe, it, expect } from 'vitest';describe('Post-condition patterns', () => {it('should create correct swap conditions', () => {const conditions = createSwapConditions({user: 'ST1TEST...',dexContract: 'ST2DEX...',dexName: 'amm-v1',tokenIn: tokenAInfo,tokenOut: tokenBInfo,amountIn: 1000,minAmountOut: 950,});expect(conditions).toHaveLength(4);expect(conditions[0].conditionCode).toBe(FungibleConditionCode.Equal);expect(conditions[1].conditionCode).toBe(FungibleConditionCode.GreaterEqual);});});
Best practices
- Use specific patterns: Don't reinvent the wheel
- Test edge cases: Ensure patterns handle all scenarios
- Document parameters: Make patterns self-explanatory
- Version patterns: Update carefully to avoid breaking changes
- Combine wisely: Some patterns can be combined, others shouldn't