Interacting with smart contracts
Build advanced dApp functionality with complex contract interactions, security constraints, and token operations through wallet integration
Smart contract interaction is where your dApp comes alive. Users can call contract functions, transfer tokens, and execute complex blockchain operations directly from your frontend.
This guide takes you beyond basic wallet integration to build sophisticated contract interactions with security constraints, token operations, and advanced transaction patterns.
What you'll learn
You'll master advanced contract interaction patterns:
- Secure contract calls with post conditions
- SIP-010 fungible token operations
- SIP-009 NFT transfers and marketplace interactions
- Multi-step transaction workflows
- Message signing for authentication and verification
- Complex Clarity value construction
- Wallet compatibility strategies
Prerequisites
Before diving in, make sure you have:
- Completed Wallet Integration guide
- A connected Stacks wallet with testnet STX
- Node.js and npm installed
- Basic understanding of Clarity smart contracts
Step 1: Set up advanced contract interactions
Install the required dependencies and set up your environment:
npm install @stacks/connect@latest @stacks/transactions
Create a contract interaction utility with enhanced error handling:
import { request } from '@stacks/connect';import { Cl, cvToValue, hexToCV } from '@stacks/transactions';// Enhanced contract interaction with retry logicexport class ContractInteraction {private maxRetries = 3;private retryDelay = 1000;async callContractFunction(contractAddress: string,contractName: string,functionName: string,functionArgs: any[],options: {postConditions?: any[];network?: 'mainnet' | 'testnet';fee?: string;} = {}) {const { postConditions = [], network = 'testnet', fee } = options;for (let attempt = 1; attempt <= this.maxRetries; attempt++) {try {const response = await request('stx_callContract', {contract: `${contractAddress}.${contractName}`,functionName,functionArgs,postConditions,network,...(fee && { fee }),});return {success: true,txid: response.txid,attempt,};} catch (error) {if (attempt === this.maxRetries) {throw new Error(`Contract call failed after ${this.maxRetries} attempts: ${error.message}`);}await this.delay(this.retryDelay * attempt);}}}private delay(ms: number): Promise<void> {return new Promise(resolve => setTimeout(resolve, ms));}}
Step 2: Implement post conditions for security
Post conditions protect users from unexpected contract behavior. They specify what should happen to balances and tokens during transaction execution.
STX transfer post conditions
import {createSTXPostCondition,FungibleConditionCode,PostConditionMode} from '@stacks/transactions';// Protect STX transfers with post conditionsasync function transferSTXWithProtection() {const userAddress = getUserAddress(); // From your wallet integrationconst recipientAddress = 'SP2MF04VAGYHGAZWGTEDW5VYCPDWWSY08Z1QFNDSN';const amountMicroSTX = '1000000'; // 1 STX// Create post condition: sender sends exactly 1 STXconst postCondition = createSTXPostCondition(userAddress,FungibleConditionCode.Equal,amountMicroSTX);try {const response = await request('stx_transferStx', {amount: amountMicroSTX,recipient: recipientAddress,network: 'testnet',postConditions: [postCondition],});console.log('Protected STX transfer successful:', response.txid);return response;} catch (error) {console.error('STX transfer failed:', error);throw error;}}
Token contract post conditions
// Protect token contract interactionsasync function callContractWithTokenProtection() {const userAddress = getUserAddress();const contractAddress = 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM';const contractName = 'my-token';// Protect against unexpected token transfersconst tokenPostCondition = createFungiblePostCondition(userAddress,FungibleConditionCode.LessEqual,'100000000', // Max 100 tokens (assuming 6 decimals)createAssetInfo(contractAddress, contractName, 'my-token'));const contractInteraction = new ContractInteraction();try {const result = await contractInteraction.callContractFunction(contractAddress,contractName,'transfer-tokens',[Cl.uint(50000000), // 50 tokensCl.principal(userAddress),Cl.principal('SP2MF04VAGYHGAZWGTEDW5VYCPDWWSY08Z1QFNDSN'),Cl.none(), // memo],{postConditions: [tokenPostCondition],network: 'testnet',});console.log('Protected token transfer:', result.txid);return result;} catch (error) {console.error('Token transfer with protection failed:', error);throw error;}}
Step 3: Handle SIP-010 fungible tokens
SIP-010 defines the standard for fungible tokens on Stacks. Here's how to interact with them securely:
// SIP-010 token operationsexport class SIP010TokenHandler {constructor(private contractAddress: string,private contractName: string,private tokenName: string,private decimals: number = 6) {}// Get token balanceasync getBalance(address: string): Promise<number> {try {const response = await fetch(`https://api.testnet.hiro.so/v2/contracts/call-read/${this.contractAddress}/${this.contractName}/get-balance`,{method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({sender: address,arguments: [Cl.principal(address).serialize()],}),});const data = await response.json();const balanceCV = hexToCV(data.result);const balance = cvToValue(balanceCV, true);return Number(balance.value) / Math.pow(10, this.decimals);} catch (error) {console.error('Failed to get token balance:', error);return 0;}}// Transfer tokens with post conditionsasync transfer(recipient: string,amount: number,memo?: string): Promise<string> {const userAddress = getUserAddress();const amountMicroTokens = Math.floor(amount * Math.pow(10, this.decimals));// Create comprehensive post conditionsconst postConditions = [// Sender loses tokenscreateFungiblePostCondition(userAddress,FungibleConditionCode.Equal,amountMicroTokens.toString(),createAssetInfo(this.contractAddress, this.contractName, this.tokenName)),// Recipient gains tokenscreateFungiblePostCondition(recipient,FungibleConditionCode.Equal,amountMicroTokens.toString(),createAssetInfo(this.contractAddress, this.contractName, this.tokenName)),];const contractInteraction = new ContractInteraction();const result = await contractInteraction.callContractFunction(this.contractAddress,this.contractName,'transfer',[Cl.uint(amountMicroTokens),Cl.principal(userAddress),Cl.principal(recipient),memo ? Cl.some(Cl.stringUtf8(memo)) : Cl.none(),],{postConditions,network: 'testnet',});return result.txid;}// Approve spending allowanceasync approve(spender: string, amount: number): Promise<string> {const userAddress = getUserAddress();const amountMicroTokens = Math.floor(amount * Math.pow(10, this.decimals));const contractInteraction = new ContractInteraction();const result = await contractInteraction.callContractFunction(this.contractAddress,this.contractName,'approve',[Cl.principal(spender),Cl.uint(amountMicroTokens),],{network: 'testnet',});return result.txid;}}
Usage example:
// Initialize token handlerconst myToken = new SIP010TokenHandler('ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM','my-fungible-token','MFT',8 // 8 decimal places);// Check balance and transferasync function handleTokenTransfer() {const userAddress = getUserAddress();const balance = await myToken.getBalance(userAddress);console.log(`Current balance: ${balance} MFT`);if (balance >= 10) {const txid = await myToken.transfer('SP2MF04VAGYHGAZWGTEDW5VYCPDWWSY08Z1QFNDSN',10,'Payment for services');console.log(`Transfer submitted: ${txid}`);} else {console.log('Insufficient balance for transfer');}}
Step 4: Work with SIP-009 NFTs
NFTs require different handling patterns. Here's a comprehensive NFT interaction system:
// SIP-009 NFT operationsexport class SIP009NFTHandler {constructor(private contractAddress: string,private contractName: string) {}// Get NFT ownerasync getOwner(tokenId: number): Promise<string | null> {try {const response = await fetch(`https://api.testnet.hiro.so/v2/contracts/call-read/${this.contractAddress}/${this.contractName}/get-owner`,{method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({sender: this.contractAddress,arguments: [Cl.uint(tokenId).serialize()],}),});const data = await response.json();const ownerCV = hexToCV(data.result);const owner = cvToValue(ownerCV, true);return owner.value?.value || null;} catch (error) {console.error('Failed to get NFT owner:', error);return null;}}// Transfer NFT with post conditionsasync transfer(tokenId: number, recipient: string): Promise<string> {const userAddress = getUserAddress();// Verify ownership before transferconst currentOwner = await this.getOwner(tokenId);if (currentOwner !== userAddress) {throw new Error(`You don't own NFT ${tokenId}`);}// Create NFT post conditionconst nftPostCondition = createNonFungiblePostCondition(userAddress,NonFungibleConditionCode.DoesNotOwn,createAssetInfo(this.contractAddress, this.contractName, 'nft-token'),Cl.uint(tokenId));const contractInteraction = new ContractInteraction();const result = await contractInteraction.callContractFunction(this.contractAddress,this.contractName,'transfer',[Cl.uint(tokenId),Cl.principal(userAddress),Cl.principal(recipient),],{postConditions: [nftPostCondition],network: 'testnet',});return result.txid;}// List NFT for sale (marketplace pattern)async listForSale(tokenId: number,priceInSTX: number,marketplaceContract: string): Promise<string> {const userAddress = getUserAddress();const priceInMicroSTX = Math.floor(priceInSTX * 1000000);// Create post conditions for listingconst postConditions = [// NFT goes to marketplace escrowcreateNonFungiblePostCondition(userAddress,NonFungibleConditionCode.DoesNotOwn,createAssetInfo(this.contractAddress, this.contractName, 'nft-token'),Cl.uint(tokenId)),];const contractInteraction = new ContractInteraction();const [marketplaceAddress, marketplaceName] = marketplaceContract.split('.');const result = await contractInteraction.callContractFunction(marketplaceAddress,marketplaceName,'list-nft',[Cl.contractPrincipal(this.contractAddress, this.contractName),Cl.uint(tokenId),Cl.uint(priceInMicroSTX),],{postConditions,network: 'testnet',});return result.txid;}}
Step 5: Build complex multi-step workflows
Many dApps require multi-step operations. Here's how to build them:
// Multi-step transaction workflowexport class MultiStepWorkflow {private steps: Array<{name: string;execute: () => Promise<string>;verify?: (txid: string) => Promise<boolean>;}> = [];addStep(name: string,execute: () => Promise<string>,verify?: (txid: string) => Promise<boolean>) {this.steps.push({ name, execute, verify });return this;}async executeWorkflow(): Promise<{success: boolean;completedSteps: number;results: Array<{ step: string; txid: string; error?: string }>;}> {const results = [];let completedSteps = 0;for (const [index, step] of this.steps.entries()) {try {console.log(`Executing step ${index + 1}: ${step.name}`);const txid = await step.execute();console.log(`Step ${index + 1} completed: ${txid}`);// Optional verificationif (step.verify) {const verified = await step.verify(txid);if (!verified) {throw new Error(`Step ${step.name} verification failed`);}}results.push({ step: step.name, txid });completedSteps++;// Wait between steps to avoid nonce issuesif (index < this.steps.length - 1) {await this.delay(2000);}} catch (error) {console.error(`Step ${index + 1} failed:`, error);results.push({step: step.name,txid: '',error: error.message});return {success: false,completedSteps,results,};}}return {success: true,completedSteps,results,};}private delay(ms: number): Promise<void> {return new Promise(resolve => setTimeout(resolve, ms));}}
Example: NFT marketplace purchase workflow:
// Complex marketplace purchaseasync function purchaseNFTWorkflow(nftContract: string,tokenId: number,priceInSTX: number,marketplaceContract: string) {const workflow = new MultiStepWorkflow();const userAddress = getUserAddress();const priceInMicroSTX = Math.floor(priceInSTX * 1000000);// Step 1: Approve STX spendingworkflow.addStep('Approve STX for marketplace',async () => {const [marketplaceAddress, marketplaceName] = marketplaceContract.split('.');return await request('stx_callContract', {contract: `${marketplaceAddress}.${marketplaceName}`,functionName: 'approve-stx',functionArgs: [Cl.uint(priceInMicroSTX)],network: 'testnet',}).then(r => r.txid);});// Step 2: Purchase NFTworkflow.addStep('Purchase NFT',async () => {const [nftAddress, nftName] = nftContract.split('.');const [marketplaceAddress, marketplaceName] = marketplaceContract.split('.');// Complex post conditions for marketplace purchaseconst postConditions = [// User pays STXcreateSTXPostCondition(userAddress,FungibleConditionCode.Equal,priceInMicroSTX.toString()),// User receives NFTcreateNonFungiblePostCondition(userAddress,NonFungibleConditionCode.Owns,createAssetInfo(nftAddress, nftName, 'nft-token'),Cl.uint(tokenId)),];return await request('stx_callContract', {contract: marketplaceContract,functionName: 'purchase-nft',functionArgs: [Cl.contractPrincipal(nftAddress, nftName),Cl.uint(tokenId),],postConditions,network: 'testnet',}).then(r => r.txid);});const result = await workflow.executeWorkflow();if (result.success) {console.log('NFT purchase completed successfully!');console.log('Transaction IDs:', result.results.map(r => r.txid));} else {console.error('NFT purchase failed:', result.results);}return result;}
Step 6: Handle wallet compatibility
Different wallets have varying support for advanced features. Here's how to handle compatibility:
// Wallet compatibility handlerexport class WalletCompatibilityHandler {private walletCapabilities = {'LeatherProvider': {stx_callContract: true,stx_deployContract: true,stx_transferSip10Ft: true,stx_transferSip9Nft: true,postConditions: 'hex-encoded', // Requires hex-encoded post conditions},'xverse': {stx_callContract: true,stx_deployContract: true,stx_transferSip10Ft: false,stx_transferSip9Nft: false,postConditions: 'none', // No post-condition support},};async detectWallet(): Promise<string | null> {// Detect active wallet providerif (window.LeatherProvider) return 'LeatherProvider';if (window.BitcoinProvider) return 'xverse';return null;}async callContractWithCompatibility(contract: string,functionName: string,functionArgs: any[],postConditions: any[] = []) {const walletType = await this.detectWallet();if (!walletType) {throw new Error('No compatible wallet detected');}const capabilities = this.walletCapabilities[walletType];if (!capabilities.stx_callContract) {throw new Error(`${walletType} doesn't support contract calls`);}// Adjust post conditions based on walletlet adjustedPostConditions = postConditions;if (capabilities.postConditions === 'none') {console.warn('Wallet doesn\'t support post conditions - transaction is less secure');adjustedPostConditions = [];} else if (capabilities.postConditions === 'hex-encoded') {// Convert post conditions to hex if neededadjustedPostConditions = postConditions.map(pc => ({...pc,// Wallet-specific post condition formatting}));}try {const response = await request('stx_callContract', {contract,functionName,functionArgs,postConditions: adjustedPostConditions,network: 'testnet',});return response;} catch (error) {// Retry without post conditions if they caused the failureif (postConditions.length > 0 && error.message.includes('post')) {console.warn('Retrying without post conditions due to wallet incompatibility');return await request('stx_callContract', {contract,functionName,functionArgs,network: 'testnet',});}throw error;}}}
Step 7: Message signing for authentication
Message signing enables user authentication and data verification without blockchain transactions. It's essential for login systems, data integrity verification, and off-chain authentication.
Simple message signing
// Simple message signing for authenticationexport class MessageSigner {async signAuthenticationMessage(challenge: string): Promise<{signature: string;publicKey: string;message: string;}> {const message = `Authentication Challenge: ${challenge}\nTimestamp: ${Date.now()}`;try {const response = await request('stx_signMessage', {message,});return {signature: response.signature,publicKey: response.publicKey,message,};} catch (error) {console.error('Message signing failed:', error);throw error;}}// Sign arbitrary data for verificationasync signDataPayload(data: any): Promise<{signature: string;publicKey: string;payload: string;}> {const payload = JSON.stringify(data, null, 2);const message = `Data Verification:\n${payload}`;const response = await request('stx_signMessage', {message,});return {signature: response.signature,publicKey: response.publicKey,payload,};}}
Structured message signing (SIP-018)
Structured messages provide type safety and domain separation for complex signing scenarios:
// SIP-018 structured message signingexport class StructuredMessageSigner {constructor(private appDomain: string = 'myapp.com',private appVersion: string = '1.0.0') {}// Sign voting or governance decisionsasync signVotingMessage(proposalId: string,choice: 'yes' | 'no' | 'abstain',userAddress: string): Promise<{ signature: string; publicKey: string }> {const clarityMessage = Cl.tuple({proposal: Cl.stringAscii(proposalId),choice: Cl.stringAscii(choice),voter: Cl.principal(userAddress),timestamp: Cl.uint(Math.floor(Date.now() / 1000)),});const clarityDomain = Cl.tuple({name: Cl.stringAscii(this.appDomain),version: Cl.stringAscii(this.appVersion),'chain-id': Cl.uint(1), // 1 for mainnet, 2147483648 for testnet});const response = await request('stx_signStructuredMessage', {message: clarityMessage,domain: clarityDomain,});return {signature: response.signature,publicKey: response.publicKey,};}// Sign marketplace ordersasync signMarketplaceOrder(order: {nftContract: string;tokenId: number;price: number;seller: string;expiry: number;}): Promise<{ signature: string; publicKey: string }> {const clarityMessage = Cl.tuple({'nft-contract': Cl.stringAscii(order.nftContract),'token-id': Cl.uint(order.tokenId),'price': Cl.uint(order.price * 1000000), // Convert to microSTX'seller': Cl.principal(order.seller),'expiry': Cl.uint(order.expiry),});const clarityDomain = Cl.tuple({name: Cl.stringAscii('NFT Marketplace'),version: Cl.stringAscii('2.0.0'),'chain-id': Cl.uint(1),});const response = await request('stx_signStructuredMessage', {message: clarityMessage,domain: clarityDomain,});return {signature: response.signature,publicKey: response.publicKey,};}// Sign complex DeFi operationsasync signDeFiAction(action: {type: 'swap' | 'stake' | 'unstake';amount: number;tokenA?: string;tokenB?: string;slippage?: number;}): Promise<{ signature: string; publicKey: string }> {const messageData: any = {'action-type': Cl.stringAscii(action.type),'amount': Cl.uint(action.amount),'timestamp': Cl.uint(Math.floor(Date.now() / 1000)),};if (action.tokenA) {messageData['token-a'] = Cl.stringAscii(action.tokenA);}if (action.tokenB) {messageData['token-b'] = Cl.stringAscii(action.tokenB);}if (action.slippage) {messageData['slippage'] = Cl.uint(Math.floor(action.slippage * 100)); // Basis points}const clarityMessage = Cl.tuple(messageData);const clarityDomain = Cl.tuple({name: Cl.stringAscii('DeFi Protocol'),version: Cl.stringAscii('1.2.0'),'chain-id': Cl.uint(1),});const response = await request('stx_signStructuredMessage', {message: clarityMessage,domain: clarityDomain,});return {signature: response.signature,publicKey: response.publicKey,};}}
Message signing with wallet compatibility
Handle wallet-specific requirements for message signing:
// Message signing with wallet compatibilityexport class CompatibleMessageSigner {async signMessageWithCompatibility(message: string,structured: boolean = false,domain?: any): Promise<{ signature: string; publicKey: string }> {const walletType = await this.detectWallet();if (structured && domain) {return await this.signStructuredWithCompatibility(message, domain, walletType);} else {return await this.signSimpleWithCompatibility(message, walletType);}}private async signSimpleWithCompatibility(message: string,walletType: string): Promise<{ signature: string; publicKey: string }> {try {if (walletType === 'xverse') {// Xverse requires publicKey parameterconst response = await request('stx_signMessage', {message,publicKey: true, // Non-standard parameter for Xverse});return {signature: response.signature,publicKey: response.publicKey,};} else {// Leather and other walletsconst response = await request('stx_signMessage', {message,});return {signature: response.signature,publicKey: response.publicKey,};}} catch (error) {console.error('Message signing compatibility error:', error);throw new Error(`Message signing failed for ${walletType}: ${error.message}`);}}private async signStructuredWithCompatibility(message: any,domain: any,walletType: string): Promise<{ signature: string; publicKey: string }> {try {// Both Leather and Xverse require hex-encoded Clarity valuesconst response = await request('stx_signStructuredMessage', {message, // Should be properly constructed Clarity tupledomain, // Should be properly constructed Clarity tuple});return {signature: response.signature,publicKey: response.publicKey,};} catch (error) {console.error('Structured message signing compatibility error:', error);throw new Error(`Structured signing failed for ${walletType}: ${error.message}`);}}private async detectWallet(): Promise<string> {if (window.LeatherProvider) return 'leather';if (window.BitcoinProvider) return 'xverse';return 'unknown';}}
Authentication workflow with message signing
Build complete authentication systems using message signing:
// Complete authentication workflowexport class AuthenticationSystem {private messageSigner = new MessageSigner();private compatibleSigner = new CompatibleMessageSigner();// Generate authentication challengegenerateChallenge(): string {const timestamp = Date.now();const random = Math.random().toString(36).substring(2, 15);return `${timestamp}-${random}`;}// Authenticate user with message signingasync authenticateUser(challenge: string): Promise<{authenticated: boolean;userAddress: string;signature: string;challenge: string;}> {try {const userAddress = getUserAddress();if (!userAddress) {throw new Error('No wallet connected');}const authMessage = `Login to MyApp\nChallenge: ${challenge}\nAddress: ${userAddress}`;const { signature, publicKey } = await this.compatibleSigner.signMessageWithCompatibility(authMessage);// Verify signature matches connected walletconst derivedAddress = this.deriveAddressFromPublicKey(publicKey);const authenticated = derivedAddress === userAddress;return {authenticated,userAddress,signature,challenge,};} catch (error) {console.error('Authentication failed:', error);return {authenticated: false,userAddress: '',signature: '',challenge,};}}// Verify signed message on the backendasync verifySignature(message: string,signature: string,publicKey: string,expectedAddress: string): Promise<boolean> {try {// This would typically be done on your backendconst derivedAddress = this.deriveAddressFromPublicKey(publicKey);if (derivedAddress !== expectedAddress) {return false;}// Additional signature verification logic would go here// Using a library like @stacks/encryption or similarreturn true; // Simplified for example} catch (error) {console.error('Signature verification failed:', error);return false;}}private deriveAddressFromPublicKey(publicKey: string): string {// Simplified - use proper address derivation in production// This would use @stacks/transactions utilitiesreturn 'SP' + publicKey.slice(-38); // Placeholder}}
Verify your implementation
Test your smart contract interactions and message signing across different scenarios:
1. Test token operations
// Test SIP-010 token workflowasync function testTokenOperations() {const token = new SIP010TokenHandler('ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM','test-token','TEST',6);const userAddress = getUserAddress();// Check balanceconst balance = await token.getBalance(userAddress);console.log(`Token balance: ${balance} TEST`);// Transfer tokensif (balance > 0) {const txid = await token.transfer('SP2MF04VAGYHGAZWGTEDW5VYCPDWWSY08Z1QFNDSN',1,'Test transfer');console.log(`Transfer transaction: ${txid}`);}}
2. Test NFT operations
// Test SIP-009 NFT workflowasync function testNFTOperations() {const nft = new SIP009NFTHandler('ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM','test-nft');// Check NFT ownershipconst owner = await nft.getOwner(1);console.log(`NFT #1 owner: ${owner}`);// Transfer if ownedif (owner === getUserAddress()) {const txid = await nft.transfer(1, 'SP2MF04VAGYHGAZWGTEDW5VYCPDWWSY08Z1QFNDSN');console.log(`NFT transfer transaction: ${txid}`);}}
3. Test wallet compatibility
// Test wallet compatibilityasync function testWalletCompatibility() {const compatibility = new WalletCompatibilityHandler();const walletType = await compatibility.detectWallet();console.log(`Detected wallet: ${walletType}`);try {const result = await compatibility.callContractWithCompatibility('ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.test-contract','test-function',[Cl.uint(42)],[] // Post conditions);console.log('Compatible contract call successful:', result.txid);} catch (error) {console.error('Compatibility test failed:', error);}}
4. Test message signing
// Test message signing functionalityasync function testMessageSigning() {const messageSigner = new MessageSigner();const structuredSigner = new StructuredMessageSigner('testapp.com', '1.0.0');const authSystem = new AuthenticationSystem();// Test simple message signingtry {const challenge = authSystem.generateChallenge();const authResult = await messageSigner.signAuthenticationMessage(challenge);console.log('Simple message signing successful:', authResult.signature);} catch (error) {console.error('Simple message signing failed:', error);}// Test structured message signingtry {const userAddress = getUserAddress();const votingResult = await structuredSigner.signVotingMessage('proposal-001','yes',userAddress);console.log('Structured message signing successful:', votingResult.signature);} catch (error) {console.error('Structured message signing failed:', error);}// Test authentication workflowtry {const challenge = authSystem.generateChallenge();const authResult = await authSystem.authenticateUser(challenge);if (authResult.authenticated) {console.log('Authentication successful for:', authResult.userAddress);} else {console.log('Authentication failed');}} catch (error) {console.error('Authentication test failed:', error);}}
5. Test cross-wallet message signing
// Test message signing across different walletsasync function testCrossWalletSigning() {const compatibleSigner = new CompatibleMessageSigner();const walletType = await compatibleSigner.detectWallet();console.log(`Testing message signing with ${walletType} wallet`);// Test simple message with wallet compatibilitytry {const result = await compatibleSigner.signMessageWithCompatibility('Cross-wallet compatibility test message');console.log(`${walletType} simple signing successful:`, result.signature);} catch (error) {console.error(`${walletType} simple signing failed:`, error);}// Test structured message with wallet compatibilitytry {const clarityMessage = Cl.tuple({action: Cl.stringAscii('test'),timestamp: Cl.uint(Math.floor(Date.now() / 1000)),});const clarityDomain = Cl.tuple({name: Cl.stringAscii('Test App'),version: Cl.stringAscii('1.0.0'),'chain-id': Cl.uint(1),});const result = await request('stx_signStructuredMessage', {message: clarityMessage,domain: clarityDomain,});console.log(`${walletType} structured signing successful:`, result.signature);} catch (error) {console.error(`${walletType} structured signing failed:`, error);}}
Deploy and try it out
Create a complete smart contract interaction interface:
<!DOCTYPE html><html><head><title>Advanced Contract Interactions</title><style>.section { margin: 20px; padding: 20px; border: 1px solid #ccc; }.success { color: green; }.error { color: red; }button { margin: 5px; padding: 10px; }</style></head><body><div class="section"><h2>Token Operations</h2><button onclick="checkTokenBalance()">Check Balance</button><button onclick="transferTokens()">Transfer Tokens</button><div id="token-status"></div></div><div class="section"><h2>NFT Operations</h2><button onclick="checkNFTOwnership()">Check NFT Ownership</button><button onclick="transferNFT()">Transfer NFT</button><button onclick="listNFTForSale()">List for Sale</button><div id="nft-status"></div></div><div class="section"><h2>Complex Workflows</h2><button onclick="runMarketplacePurchase()">Purchase NFT</button><div id="workflow-status"></div></div><div class="section"><h2>Message Signing</h2><button onclick="signSimpleMessage()">Sign Simple Message</button><button onclick="signVotingMessage()">Sign Voting Message</button><button onclick="authenticateUser()">Authenticate User</button><button onclick="signMarketplaceOrder()">Sign Order</button><div id="signing-status"></div></div><script type="module">// Your implementation code hereimport { /* all your classes */ } from './contract-interactions.js';// Implement button handlerswindow.checkTokenBalance = async function() {// Implementation};// More handlers...</script></body></html>
Troubleshooting
Post conditions failing
- Verify exact amounts and addresses in post conditions
- Check wallet compatibility - some wallets don't support post conditions
- Use
PostConditionMode.Allow
for testing,Deny
for production
Token operations not working
- Confirm contract implements SIP-010 or SIP-009 standards correctly
- Check token contract address and name spelling
- Verify user has sufficient balance for transfers
Multi-step workflows failing
- Add longer delays between steps for slow networks
- Implement transaction status checking before next step
- Handle nonce conflicts with proper sequencing
Message signing failures
- Leather doesn't support non-standard
publicKey
parameter - omit it - Xverse requires non-standard
publicKey
parameter for simple messages - Both wallets require hex-encoded Clarity values for structured messages
- Verify message format matches wallet expectations
Structured message signing errors
- Ensure Clarity tuples are properly constructed with
Cl
helpers - Domain separation requires consistent naming across app versions
- Chain ID must match network: 1 for mainnet, 2147483648 for testnet
- Complex nested structures may not be supported by all wallets
Authentication workflow issues
- Generate unique challenges to prevent replay attacks
- Verify signature on backend before trusting authentication
- Handle wallet connection changes during auth process
- Implement proper session management and timeout handling
Wallet compatibility issues
- Reference the wallet support table for feature availability
- Implement fallback methods for unsupported features
- Test with multiple wallet types during development
- Handle wallet-specific parameter requirements for message signing
Next steps
You've mastered advanced smart contract interactions. Continue building:
- Working with APIs to add backend integration
- Creating Predicates for event-driven features
- Building an Indexer for custom data processing
Ready to deploy? Check out Deploying Your Application for production deployment strategies.