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:

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 logic
export 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 conditions
async function transferSTXWithProtection() {
const userAddress = getUserAddress(); // From your wallet integration
const recipientAddress = 'SP2MF04VAGYHGAZWGTEDW5VYCPDWWSY08Z1QFNDSN';
const amountMicroSTX = '1000000'; // 1 STX
// Create post condition: sender sends exactly 1 STX
const 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 interactions
async function callContractWithTokenProtection() {
const userAddress = getUserAddress();
const contractAddress = 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM';
const contractName = 'my-token';
// Protect against unexpected token transfers
const 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 tokens
Cl.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 operations
export class SIP010TokenHandler {
constructor(
private contractAddress: string,
private contractName: string,
private tokenName: string,
private decimals: number = 6
) {}
// Get token balance
async 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 conditions
async 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 conditions
const postConditions = [
// Sender loses tokens
createFungiblePostCondition(
userAddress,
FungibleConditionCode.Equal,
amountMicroTokens.toString(),
createAssetInfo(this.contractAddress, this.contractName, this.tokenName)
),
// Recipient gains tokens
createFungiblePostCondition(
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 allowance
async 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 handler
const myToken = new SIP010TokenHandler(
'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
'my-fungible-token',
'MFT',
8 // 8 decimal places
);
// Check balance and transfer
async 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 operations
export class SIP009NFTHandler {
constructor(
private contractAddress: string,
private contractName: string
) {}
// Get NFT owner
async 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 conditions
async transfer(tokenId: number, recipient: string): Promise<string> {
const userAddress = getUserAddress();
// Verify ownership before transfer
const currentOwner = await this.getOwner(tokenId);
if (currentOwner !== userAddress) {
throw new Error(`You don't own NFT ${tokenId}`);
}
// Create NFT post condition
const 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 listing
const postConditions = [
// NFT goes to marketplace escrow
createNonFungiblePostCondition(
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 workflow
export 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 verification
if (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 issues
if (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 purchase
async 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 spending
workflow.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 NFT
workflow.addStep(
'Purchase NFT',
async () => {
const [nftAddress, nftName] = nftContract.split('.');
const [marketplaceAddress, marketplaceName] = marketplaceContract.split('.');
// Complex post conditions for marketplace purchase
const postConditions = [
// User pays STX
createSTXPostCondition(
userAddress,
FungibleConditionCode.Equal,
priceInMicroSTX.toString()
),
// User receives NFT
createNonFungiblePostCondition(
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 handler
export 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 provider
if (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 wallet
let 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 needed
adjustedPostConditions = 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 failure
if (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 authentication
export 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 verification
async 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 signing
export class StructuredMessageSigner {
constructor(
private appDomain: string = 'myapp.com',
private appVersion: string = '1.0.0'
) {}
// Sign voting or governance decisions
async 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 orders
async 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 operations
async 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 compatibility
export 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 parameter
const response = await request('stx_signMessage', {
message,
publicKey: true, // Non-standard parameter for Xverse
});
return {
signature: response.signature,
publicKey: response.publicKey,
};
} else {
// Leather and other wallets
const 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 values
const response = await request('stx_signStructuredMessage', {
message, // Should be properly constructed Clarity tuple
domain, // 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 workflow
export class AuthenticationSystem {
private messageSigner = new MessageSigner();
private compatibleSigner = new CompatibleMessageSigner();
// Generate authentication challenge
generateChallenge(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 15);
return `${timestamp}-${random}`;
}
// Authenticate user with message signing
async 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 wallet
const 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 backend
async verifySignature(
message: string,
signature: string,
publicKey: string,
expectedAddress: string
): Promise<boolean> {
try {
// This would typically be done on your backend
const derivedAddress = this.deriveAddressFromPublicKey(publicKey);
if (derivedAddress !== expectedAddress) {
return false;
}
// Additional signature verification logic would go here
// Using a library like @stacks/encryption or similar
return 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 utilities
return '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 workflow
async function testTokenOperations() {
const token = new SIP010TokenHandler(
'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
'test-token',
'TEST',
6
);
const userAddress = getUserAddress();
// Check balance
const balance = await token.getBalance(userAddress);
console.log(`Token balance: ${balance} TEST`);
// Transfer tokens
if (balance > 0) {
const txid = await token.transfer(
'SP2MF04VAGYHGAZWGTEDW5VYCPDWWSY08Z1QFNDSN',
1,
'Test transfer'
);
console.log(`Transfer transaction: ${txid}`);
}
}

2. Test NFT operations

// Test SIP-009 NFT workflow
async function testNFTOperations() {
const nft = new SIP009NFTHandler(
'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
'test-nft'
);
// Check NFT ownership
const owner = await nft.getOwner(1);
console.log(`NFT #1 owner: ${owner}`);
// Transfer if owned
if (owner === getUserAddress()) {
const txid = await nft.transfer(1, 'SP2MF04VAGYHGAZWGTEDW5VYCPDWWSY08Z1QFNDSN');
console.log(`NFT transfer transaction: ${txid}`);
}
}

3. Test wallet compatibility

// Test wallet compatibility
async 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 functionality
async function testMessageSigning() {
const messageSigner = new MessageSigner();
const structuredSigner = new StructuredMessageSigner('testapp.com', '1.0.0');
const authSystem = new AuthenticationSystem();
// Test simple message signing
try {
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 signing
try {
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 workflow
try {
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 wallets
async function testCrossWalletSigning() {
const compatibleSigner = new CompatibleMessageSigner();
const walletType = await compatibleSigner.detectWallet();
console.log(`Testing message signing with ${walletType} wallet`);
// Test simple message with wallet compatibility
try {
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 compatibility
try {
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 here
import { /* all your classes */ } from './contract-interactions.js';
// Implement button handlers
window.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:

Ready to deploy? Check out Deploying Your Application for production deployment strategies.