Skip to main content
Glama
sign_and_submit_transaction.md34.8 kB
# Sign and Submit Transaction Guide ## Overview This comprehensive guide covers transaction signing and submission in Stacks applications using `@stacks/connect`. It includes contract calls, STX transfers, and advanced transaction patterns with mandatory post-conditions. **Transaction Types Covered:** - **Contract Calls** - Calling public functions on deployed contracts - **STX Transfers** - Native STX token transfers - **Token Transfers** - SIP-010 fungible token transfers (with post-conditions) - **NFT Transfers** - SIP-009 non-fungible token transfers (with post-conditions) - **Multi-signature** - Complex transaction patterns ## Basic Contract Call ### 1. Simple Contract Call (React) ```tsx // src/components/ContractCall.tsx import React, { useState } from 'react'; import { useConnect } from '@stacks/connect-react'; import { PostConditionMode, uintCV, principalCV, stringAsciiCV, } from '@stacks/transactions'; import { StacksMainnet, StacksTestnet } from '@stacks/network'; export const ContractCall: React.FC = () => { const { doContractCall } = useConnect(); const [isLoading, setIsLoading] = useState(false); const [txId, setTxId] = useState<string | null>(null); const handleContractCall = async () => { setIsLoading(true); try { const result = await doContractCall({ network: new StacksTestnet(), // or StacksMainnet() for production contractAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM', contractName: 'my-contract', functionName: 'my-function', functionArgs: [ uintCV(100), principalCV('ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM'), stringAsciiCV('hello world'), ], postConditionMode: PostConditionMode.Deny, // ALWAYS use Deny mode onFinish: (data) => { console.log('Transaction submitted:', data.txId); setTxId(data.txId); setIsLoading(false); }, onCancel: () => { console.log('Transaction cancelled'); setIsLoading(false); }, }); } catch (error) { console.error('Transaction failed:', error); setIsLoading(false); } }; return ( <div className="contract-call"> <h3>Contract Call Example</h3> <button onClick={handleContractCall} disabled={isLoading} className="call-btn" > {isLoading ? 'Submitting...' : 'Call Contract'} </button> {txId && ( <div className="transaction-result"> <p>Transaction submitted!</p> <a href={`https://explorer.stacks.co/txid/${txId}?chain=testnet`} target="_blank" rel="noopener noreferrer" > View in Explorer </a> </div> )} </div> ); }; ``` ### 2. STX Transfer ```tsx // src/components/STXTransfer.tsx import React, { useState } from 'react'; import { useConnect } from '@stacks/connect-react'; import { PostConditionMode, makeStandardSTXPostCondition, FungibleConditionCode, uintCV, principalCV, someCV, bufferCV, } from '@stacks/transactions'; interface STXTransferProps { defaultRecipient?: string; } export const STXTransfer: React.FC<STXTransferProps> = ({ defaultRecipient = '' }) => { const { doSTXTransfer } = useConnect(); const [recipient, setRecipient] = useState(defaultRecipient); const [amount, setAmount] = useState(''); const [memo, setMemo] = useState(''); const [isLoading, setIsLoading] = useState(false); const [txId, setTxId] = useState<string | null>(null); const handleSTXTransfer = async () => { if (!recipient || !amount) { alert('Please enter recipient and amount'); return; } setIsLoading(true); const amountInMicroSTX = parseInt(amount) * 1000000; // Convert STX to microSTX try { // Create post-condition for exact STX amount const postConditions = [ makeStandardSTXPostCondition( recipient, FungibleConditionCode.Equal, amountInMicroSTX ), ]; await doSTXTransfer({ recipient, amount: amountInMicroSTX, memo: memo || undefined, postConditions, postConditionMode: PostConditionMode.Deny, onFinish: (data) => { console.log('STX transfer submitted:', data.txId); setTxId(data.txId); setIsLoading(false); // Clear form setRecipient(''); setAmount(''); setMemo(''); }, onCancel: () => { console.log('STX transfer cancelled'); setIsLoading(false); }, }); } catch (error) { console.error('STX transfer failed:', error); setIsLoading(false); } }; return ( <div className="stx-transfer"> <h3>Send STX</h3> <div className="form-group"> <label htmlFor="recipient">Recipient Address:</label> <input type="text" id="recipient" value={recipient} onChange={(e) => setRecipient(e.target.value)} placeholder="ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM" className="address-input" /> </div> <div className="form-group"> <label htmlFor="amount">Amount (STX):</label> <input type="number" id="amount" value={amount} onChange={(e) => setAmount(e.target.value)} placeholder="1.5" step="0.000001" min="0" className="amount-input" /> </div> <div className="form-group"> <label htmlFor="memo">Memo (optional):</label> <input type="text" id="memo" value={memo} onChange={(e) => setMemo(e.target.value)} placeholder="Payment for services" maxLength={34} className="memo-input" /> </div> <button onClick={handleSTXTransfer} disabled={isLoading || !recipient || !amount} className="transfer-btn" > {isLoading ? 'Sending...' : 'Send STX'} </button> {txId && ( <div className="transaction-result"> <p>STX transfer submitted!</p> <a href={`https://explorer.stacks.co/txid/${txId}?chain=testnet`} target="_blank" rel="noopener noreferrer" > View in Explorer </a> </div> )} </div> ); }; ``` ## Token Transfers with Post-Conditions ### 1. SIP-010 Fungible Token Transfer ```tsx // src/components/TokenTransfer.tsx import React, { useState, useEffect } from 'react'; import { useConnect } from '@stacks/connect-react'; import { PostConditionMode, makeStandardFungiblePostCondition, FungibleConditionCode, createAssetInfo, uintCV, principalCV, noneCV, someCV, bufferCV, } from '@stacks/transactions'; import { StacksApiService } from '../services/StacksApiService'; interface TokenTransferProps { contractAddress: string; contractName: string; network: 'mainnet' | 'testnet'; } export const TokenTransfer: React.FC<TokenTransferProps> = ({ contractAddress, contractName, network, }) => { const { doContractCall, userSession } = useConnect(); const [recipient, setRecipient] = useState(''); const [amount, setAmount] = useState(''); const [memo, setMemo] = useState(''); const [balance, setBalance] = useState<string>('0'); const [tokenInfo, setTokenInfo] = useState<any>(null); const [isLoading, setIsLoading] = useState(false); const [txId, setTxId] = useState<string | null>(null); const apiService = new StacksApiService(); useEffect(() => { loadTokenInfo(); loadBalance(); }, [contractAddress, contractName, network]); const loadTokenInfo = async () => { try { const contractId = `${contractAddress}.${contractName}`; const info = await apiService.getFungibleTokenInfo(contractId, network); setTokenInfo(info); } catch (error) { console.error('Failed to load token info:', error); } }; const loadBalance = async () => { if (!userSession?.isUserSignedIn()) return; try { const userData = userSession.loadUserData(); const userAddress = userData.profile.stxAddress[network]; const contractId = `${contractAddress}.${contractName}`; const tokenBalance = await apiService.getFungibleTokenBalance( contractId, userAddress, network ); if (tokenInfo) { const humanBalance = parseInt(tokenBalance) / Math.pow(10, tokenInfo.decimals); setBalance(humanBalance.toString()); } else { setBalance(tokenBalance); } } catch (error) { console.error('Failed to load balance:', error); } }; const handleTokenTransfer = async () => { if (!recipient || !amount || !userSession?.isUserSignedIn()) { alert('Please enter recipient and amount'); return; } setIsLoading(true); try { const userData = userSession.loadUserData(); const senderAddress = userData.profile.stxAddress[network]; // Convert amount to base units const decimals = tokenInfo?.decimals || 0; const amountInBaseUnits = Math.floor(parseFloat(amount) * Math.pow(10, decimals)); // MANDATORY: Create post-condition for exact token transfer const postConditions = [ makeStandardFungiblePostCondition( senderAddress, FungibleConditionCode.Equal, // Exact amount amountInBaseUnits, createAssetInfo(contractAddress, contractName, contractName) ), ]; const functionArgs = [ uintCV(amountInBaseUnits), principalCV(senderAddress), principalCV(recipient), memo ? someCV(bufferCV(Buffer.from(memo, 'utf8'))) : noneCV(), ]; await doContractCall({ contractAddress, contractName, functionName: 'transfer', functionArgs, postConditions, postConditionMode: PostConditionMode.Deny, // REQUIRED for security onFinish: (data) => { console.log('Token transfer submitted:', data.txId); setTxId(data.txId); setIsLoading(false); // Refresh balance setTimeout(loadBalance, 2000); // Clear form setRecipient(''); setAmount(''); setMemo(''); }, onCancel: () => { console.log('Token transfer cancelled'); setIsLoading(false); }, }); } catch (error) { console.error('Token transfer failed:', error); setIsLoading(false); } }; if (!tokenInfo) { return <div>Loading token information...</div>; } return ( <div className="token-transfer"> <h3>Send {tokenInfo.symbol} Tokens</h3> <div className="token-info"> <p><strong>Token:</strong> {tokenInfo.name} ({tokenInfo.symbol})</p> <p><strong>Your Balance:</strong> {parseFloat(balance).toLocaleString()} {tokenInfo.symbol}</p> </div> <div className="form-group"> <label htmlFor="recipient">Recipient Address:</label> <input type="text" id="recipient" value={recipient} onChange={(e) => setRecipient(e.target.value)} placeholder="ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM" className="address-input" /> </div> <div className="form-group"> <label htmlFor="amount">Amount ({tokenInfo.symbol}):</label> <input type="number" id="amount" value={amount} onChange={(e) => setAmount(e.target.value)} placeholder="10.5" step={`${1 / Math.pow(10, tokenInfo.decimals)}`} min="0" max={balance} className="amount-input" /> <small>Available: {parseFloat(balance).toLocaleString()} {tokenInfo.symbol}</small> </div> <div className="form-group"> <label htmlFor="memo">Memo (optional):</label> <input type="text" id="memo" value={memo} onChange={(e) => setMemo(e.target.value)} placeholder="Token transfer" maxLength={34} className="memo-input" /> </div> <button onClick={handleTokenTransfer} disabled={isLoading || !recipient || !amount || parseFloat(amount) > parseFloat(balance)} className="transfer-btn" > {isLoading ? 'Sending...' : `Send ${tokenInfo.symbol}`} </button> {txId && ( <div className="transaction-result"> <p>Token transfer submitted!</p> <a href={`https://explorer.stacks.co/txid/${txId}?chain=${network}`} target="_blank" rel="noopener noreferrer" > View in Explorer </a> </div> )} </div> ); }; ``` ### 2. SIP-009 NFT Transfer ```tsx // src/components/NFTTransfer.tsx import React, { useState, useEffect } from 'react'; import { useConnect } from '@stacks/connect-react'; import { PostConditionMode, makeStandardNonFungiblePostCondition, NonFungibleConditionCode, createAssetInfo, uintCV, principalCV, } from '@stacks/transactions'; import { StacksApiService } from '../services/StacksApiService'; interface NFTTransferProps { contractAddress: string; contractName: string; network: 'mainnet' | 'testnet'; } export const NFTTransfer: React.FC<NFTTransferProps> = ({ contractAddress, contractName, network, }) => { const { doContractCall, userSession } = useConnect(); const [recipient, setRecipient] = useState(''); const [tokenId, setTokenId] = useState(''); const [nftInfo, setNftInfo] = useState<any>(null); const [ownerAddress, setOwnerAddress] = useState<string | null>(null); const [isLoading, setIsLoading] = useState(false); const [txId, setTxId] = useState<string | null>(null); const apiService = new StacksApiService(); useEffect(() => { if (tokenId) { loadNFTInfo(); } }, [tokenId, contractAddress, contractName, network]); const loadNFTInfo = async () => { if (!tokenId) return; try { const contractId = `${contractAddress}.${contractName}`; const tokenIdNum = parseInt(tokenId); // Get NFT owner const owner = await apiService.getNFTOwner(contractId, tokenIdNum, network); setOwnerAddress(owner); // Get NFT metadata URI const uri = await apiService.getNFTTokenUri(contractId, tokenIdNum, network); if (uri) { // Fetch metadata if URI exists try { const response = await fetch(uri.startsWith('ipfs://') ? uri.replace('ipfs://', 'https://ipfs.io/ipfs/') : uri ); const metadata = await response.json(); setNftInfo(metadata); } catch (error) { console.warn('Failed to fetch NFT metadata:', error); setNftInfo({ name: `NFT #${tokenId}`, description: 'Metadata not available' }); } } else { setNftInfo({ name: `NFT #${tokenId}`, description: 'No metadata URI' }); } } catch (error) { console.error('Failed to load NFT info:', error); setNftInfo(null); setOwnerAddress(null); } }; const handleNFTTransfer = async () => { if (!recipient || !tokenId || !userSession?.isUserSignedIn()) { alert('Please enter recipient and token ID'); return; } const userData = userSession.loadUserData(); const senderAddress = userData.profile.stxAddress[network]; // Verify ownership if (ownerAddress !== senderAddress) { alert('You do not own this NFT'); return; } setIsLoading(true); try { const tokenIdNum = parseInt(tokenId); // MANDATORY: Create post-condition for NFT transfer const postConditions = [ makeStandardNonFungiblePostCondition( senderAddress, NonFungibleConditionCode.DoesNotOwn, // Sender will not own after transfer createAssetInfo(contractAddress, contractName, contractName), uintCV(tokenIdNum) ), ]; const functionArgs = [ uintCV(tokenIdNum), principalCV(senderAddress), principalCV(recipient), ]; await doContractCall({ contractAddress, contractName, functionName: 'transfer', functionArgs, postConditions, postConditionMode: PostConditionMode.Deny, // REQUIRED for security onFinish: (data) => { console.log('NFT transfer submitted:', data.txId); setTxId(data.txId); setIsLoading(false); // Refresh NFT info setTimeout(loadNFTInfo, 2000); // Clear form setRecipient(''); setTokenId(''); }, onCancel: () => { console.log('NFT transfer cancelled'); setIsLoading(false); }, }); } catch (error) { console.error('NFT transfer failed:', error); setIsLoading(false); } }; const canTransfer = ownerAddress && userSession?.isUserSignedIn() && ownerAddress === userSession.loadUserData().profile.stxAddress[network]; return ( <div className="nft-transfer"> <h3>Transfer NFT</h3> <div className="form-group"> <label htmlFor="tokenId">Token ID:</label> <input type="number" id="tokenId" value={tokenId} onChange={(e) => setTokenId(e.target.value)} placeholder="1" min="1" className="token-id-input" /> </div> {tokenId && nftInfo && ( <div className="nft-info"> <h4>{nftInfo.name}</h4> {nftInfo.image && ( <img src={nftInfo.image.startsWith('ipfs://') ? nftInfo.image.replace('ipfs://', 'https://ipfs.io/ipfs/') : nftInfo.image } alt={nftInfo.name} className="nft-image" /> )} <p>{nftInfo.description}</p> <p><strong>Owner:</strong> {ownerAddress || 'Unknown'}</p> {!canTransfer && ownerAddress && ( <div className="warning"> ⚠️ You do not own this NFT. Only the owner can transfer it. </div> )} </div> )} <div className="form-group"> <label htmlFor="recipient">Recipient Address:</label> <input type="text" id="recipient" value={recipient} onChange={(e) => setRecipient(e.target.value)} placeholder="ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM" className="address-input" /> </div> <button onClick={handleNFTTransfer} disabled={isLoading || !recipient || !tokenId || !canTransfer} className="transfer-btn" > {isLoading ? 'Transferring...' : 'Transfer NFT'} </button> {txId && ( <div className="transaction-result"> <p>NFT transfer submitted!</p> <a href={`https://explorer.stacks.co/txid/${txId}?chain=${network}`} target="_blank" rel="noopener noreferrer" > View in Explorer </a> </div> )} </div> ); }; ``` ## Advanced Transaction Patterns ### 1. Multi-Asset Atomic Swap ```tsx // src/components/AtomicSwap.tsx import React, { useState } from 'react'; import { useConnect } from '@stacks/connect-react'; import { PostConditionMode, makeStandardFungiblePostCondition, makeStandardSTXPostCondition, FungibleConditionCode, createAssetInfo, uintCV, principalCV, } from '@stacks/transactions'; export const AtomicSwap: React.FC = () => { const { doContractCall, userSession } = useConnect(); const [tokenAAmount, setTokenAAmount] = useState(''); const [tokenBAmount, setTokenBAmount] = useState(''); const [isLoading, setIsLoading] = useState(false); const handleAtomicSwap = async () => { if (!userSession?.isUserSignedIn()) return; setIsLoading(true); try { const userData = userSession.loadUserData(); const userAddress = userData.profile.stxAddress.mainnet; const tokenAAmountInBaseUnits = parseInt(tokenAAmount) * 1000000; // Assuming 6 decimals const tokenBAmountInBaseUnits = parseInt(tokenBAmount) * 1000000; // COMPREHENSIVE post-conditions for both sides of the swap const postConditions = [ // User sends Token A makeStandardFungiblePostCondition( userAddress, FungibleConditionCode.Equal, tokenAAmountInBaseUnits, createAssetInfo('SP123...ABC', 'token-a', 'token-a') ), // User receives Token B makeStandardFungiblePostCondition( userAddress, FungibleConditionCode.Equal, tokenBAmountInBaseUnits, createAssetInfo('SP456...DEF', 'token-b', 'token-b') ), ]; await doContractCall({ contractAddress: 'SP789...GHI', contractName: 'atomic-swap-contract', functionName: 'execute-swap', functionArgs: [ uintCV(tokenAAmountInBaseUnits), uintCV(tokenBAmountInBaseUnits), ], postConditions, postConditionMode: PostConditionMode.Deny, onFinish: (data) => { console.log('Atomic swap submitted:', data.txId); setIsLoading(false); }, onCancel: () => { setIsLoading(false); }, }); } catch (error) { console.error('Atomic swap failed:', error); setIsLoading(false); } }; return ( <div className="atomic-swap"> <h3>Atomic Token Swap</h3> <div className="swap-inputs"> <div className="form-group"> <label>Send Token A:</label> <input type="number" value={tokenAAmount} onChange={(e) => setTokenAAmount(e.target.value)} placeholder="100" /> </div> <div className="swap-arrow">⇄</div> <div className="form-group"> <label>Receive Token B:</label> <input type="number" value={tokenBAmount} onChange={(e) => setTokenBAmount(e.target.value)} placeholder="95" /> </div> </div> <button onClick={handleAtomicSwap} disabled={isLoading || !tokenAAmount || !tokenBAmount} className="swap-btn" > {isLoading ? 'Swapping...' : 'Execute Swap'} </button> </div> ); }; ``` ### 2. Batch Operations ```tsx // src/components/BatchOperations.tsx import React, { useState } from 'react'; import { useConnect } from '@stacks/connect-react'; import { PostConditionMode, makeStandardFungiblePostCondition, FungibleConditionCode, createAssetInfo, uintCV, listCV, principalCV, } from '@stacks/transactions'; interface BatchTransfer { recipient: string; amount: string; } export const BatchOperations: React.FC = () => { const { doContractCall, userSession } = useConnect(); const [transfers, setTransfers] = useState<BatchTransfer[]>([ { recipient: '', amount: '' } ]); const [isLoading, setIsLoading] = useState(false); const addTransfer = () => { setTransfers([...transfers, { recipient: '', amount: '' }]); }; const removeTransfer = (index: number) => { setTransfers(transfers.filter((_, i) => i !== index)); }; const updateTransfer = (index: number, field: keyof BatchTransfer, value: string) => { const newTransfers = [...transfers]; newTransfers[index][field] = value; setTransfers(newTransfers); }; const handleBatchTransfer = async () => { if (!userSession?.isUserSignedIn()) return; const validTransfers = transfers.filter(t => t.recipient && t.amount); if (validTransfers.length === 0) return; setIsLoading(true); try { const userData = userSession.loadUserData(); const userAddress = userData.profile.stxAddress.mainnet; // Calculate total amount for post-condition const totalAmount = validTransfers.reduce( (sum, transfer) => sum + (parseInt(transfer.amount) * 1000000), 0 ); // Create post-condition for total amount being sent const postConditions = [ makeStandardFungiblePostCondition( userAddress, FungibleConditionCode.Equal, totalAmount, createAssetInfo('SP123...ABC', 'my-token', 'my-token') ), ]; // Prepare batch transfer arguments const recipients = validTransfers.map(t => principalCV(t.recipient)); const amounts = validTransfers.map(t => uintCV(parseInt(t.amount) * 1000000)); await doContractCall({ contractAddress: 'SP123...ABC', contractName: 'batch-operations', functionName: 'batch-transfer', functionArgs: [ listCV(recipients), listCV(amounts), ], postConditions, postConditionMode: PostConditionMode.Deny, onFinish: (data) => { console.log('Batch transfer submitted:', data.txId); setIsLoading(false); // Reset form setTransfers([{ recipient: '', amount: '' }]); }, onCancel: () => { setIsLoading(false); }, }); } catch (error) { console.error('Batch transfer failed:', error); setIsLoading(false); } }; return ( <div className="batch-operations"> <h3>Batch Token Transfer</h3> {transfers.map((transfer, index) => ( <div key={index} className="transfer-row"> <input type="text" placeholder="Recipient address" value={transfer.recipient} onChange={(e) => updateTransfer(index, 'recipient', e.target.value)} /> <input type="number" placeholder="Amount" value={transfer.amount} onChange={(e) => updateTransfer(index, 'amount', e.target.value)} /> {transfers.length > 1 && ( <button onClick={() => removeTransfer(index)}>Remove</button> )} </div> ))} <div className="batch-controls"> <button onClick={addTransfer}>Add Transfer</button> <button onClick={handleBatchTransfer} disabled={isLoading || transfers.every(t => !t.recipient || !t.amount)} > {isLoading ? 'Processing...' : 'Execute Batch Transfer'} </button> </div> </div> ); }; ``` ## Transaction Status Monitoring ### 1. Transaction Status Hook ```tsx // src/hooks/useTransactionStatus.ts import { useState, useEffect } from 'react'; import { StacksApiService } from '../services/StacksApiService'; interface TransactionStatus { tx_id: string; tx_status: string; tx_result?: any; block_height?: number; burn_block_time_iso?: string; } export const useTransactionStatus = (txId: string | null, network: 'mainnet' | 'testnet') => { const [status, setStatus] = useState<TransactionStatus | null>(null); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const apiService = new StacksApiService(); useEffect(() => { if (!txId) return; const checkStatus = async () => { setLoading(true); setError(null); try { const txStatus = await apiService.getTransactionStatus(txId, network); setStatus(txStatus); // Continue polling if transaction is pending if (txStatus.tx_status === 'pending') { setTimeout(checkStatus, 5000); // Check again in 5 seconds } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to check transaction status'); } finally { setLoading(false); } }; checkStatus(); }, [txId, network]); return { status, loading, error }; }; ``` ### 2. Transaction Status Component ```tsx // src/components/TransactionStatus.tsx import React from 'react'; import { useTransactionStatus } from '../hooks/useTransactionStatus'; interface TransactionStatusProps { txId: string | null; network: 'mainnet' | 'testnet'; } export const TransactionStatus: React.FC<TransactionStatusProps> = ({ txId, network }) => { const { status, loading, error } = useTransactionStatus(txId, network); if (!txId) return null; if (loading) { return ( <div className="transaction-status loading"> <div className="spinner"></div> <span>Checking transaction status...</span> </div> ); } if (error) { return ( <div className="transaction-status error"> <span>❌ Error: {error}</span> </div> ); } if (!status) return null; const getStatusIcon = (txStatus: string) => { switch (txStatus) { case 'success': return '✅'; case 'abort_by_response': return '❌'; case 'abort_by_post_condition': return '⚠️'; case 'pending': return '⏳'; default: return '❓'; } }; const getStatusMessage = (txStatus: string) => { switch (txStatus) { case 'success': return 'Transaction confirmed'; case 'abort_by_response': return 'Transaction failed'; case 'abort_by_post_condition': return 'Post-condition failed'; case 'pending': return 'Transaction pending...'; default: return `Status: ${txStatus}`; } }; return ( <div className={`transaction-status ${status.tx_status}`}> <div className="status-header"> <span className="status-icon">{getStatusIcon(status.tx_status)}</span> <span className="status-message">{getStatusMessage(status.tx_status)}</span> </div> <div className="transaction-details"> <p><strong>Transaction ID:</strong> {status.tx_id}</p> {status.block_height && ( <p><strong>Block Height:</strong> {status.block_height}</p> )} {status.burn_block_time_iso && ( <p><strong>Confirmed:</strong> {new Date(status.burn_block_time_iso).toLocaleString()}</p> )} </div> <a href={`https://explorer.stacks.co/txid/${status.tx_id}?chain=${network}`} target="_blank" rel="noopener noreferrer" className="explorer-link" > View in Explorer </a> </div> ); }; ``` ## Error Handling & Best Practices ### 1. Comprehensive Error Handling ```tsx // src/utils/transactionErrors.ts export const getTransactionErrorMessage = (error: any): string => { if (typeof error === 'string') return error; if (error?.message) { const message = error.message.toLowerCase(); if (message.includes('post-condition')) { return 'Transaction failed: Post-condition not met. The transaction would not behave as expected.'; } if (message.includes('insufficient funds')) { return 'Transaction failed: Insufficient funds to complete the transaction.'; } if (message.includes('nonce')) { return 'Transaction failed: Invalid nonce. Please try again.'; } if (message.includes('fee')) { return 'Transaction failed: Transaction fee too low.'; } if (message.includes('unauthorized')) { return 'Transaction failed: You are not authorized to perform this action.'; } return `Transaction failed: ${error.message}`; } return 'Transaction failed: Unknown error occurred.'; }; ``` ### 2. Pre-Transaction Validation ```tsx // src/utils/transactionValidation.ts import { StacksApiService } from '../services/StacksApiService'; export interface ValidationResult { isValid: boolean; errors: string[]; } export const validateTransaction = async ( userAddress: string, recipientAddress: string, amount: number, tokenContract?: string, network: 'mainnet' | 'testnet' = 'testnet' ): Promise<ValidationResult> => { const errors: string[] = []; const apiService = new StacksApiService(); // Validate addresses if (!apiService.isValidStacksAddress(userAddress)) { errors.push('Invalid sender address'); } if (!apiService.isValidStacksAddress(recipientAddress)) { errors.push('Invalid recipient address'); } if (userAddress === recipientAddress) { errors.push('Cannot send to the same address'); } // Validate amount if (amount <= 0) { errors.push('Amount must be greater than zero'); } // Check balance try { if (tokenContract) { // Token balance check const balance = await apiService.getFungibleTokenBalance(tokenContract, userAddress, network); const tokenInfo = await apiService.getFungibleTokenInfo(tokenContract, network); const amountInBaseUnits = amount * Math.pow(10, tokenInfo.decimals); if (parseInt(balance) < amountInBaseUnits) { errors.push('Insufficient token balance'); } } else { // STX balance check const accountInfo = await apiService.getAccountInfo(userAddress, network); const amountInMicroSTX = amount * 1000000; if (parseInt(accountInfo.balance) < amountInMicroSTX) { errors.push('Insufficient STX balance'); } } } catch (error) { errors.push('Failed to check balance'); } return { isValid: errors.length === 0, errors, }; }; ``` ### 3. Security Best Practices ```tsx // Always use these security patterns: // 1. ALWAYS use PostConditionMode.Deny postConditionMode: PostConditionMode.Deny // 2. ALWAYS include post-conditions for asset transfers const postConditions = [ makeStandardFungiblePostCondition( senderAddress, FungibleConditionCode.Equal, // Use Equal for exact amounts amount, assetInfo ), ]; // 3. Validate all inputs before transaction const validation = await validateTransaction(sender, recipient, amount); if (!validation.isValid) { alert(`Invalid transaction: ${validation.errors.join(', ')}`); return; } // 4. Handle all error cases try { await doContractCall({...}); } catch (error) { const errorMessage = getTransactionErrorMessage(error); console.error('Transaction failed:', errorMessage); // Show user-friendly error message } // 5. Monitor transaction status const { status } = useTransactionStatus(txId, network); ``` This comprehensive guide provides everything needed to implement secure transaction signing and submission in Stacks applications with proper post-condition handling and error management.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/exponentlabshq/stacks-clarity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server