Read-only calls
Query smart contract state without creating transactions
Overview
Read-only function calls allow you to query data from smart contracts without creating a transaction. These calls are free, instant, and don't require wallet interaction. They're perfect for fetching contract state, checking balances, or validating conditions before transactions.
Basic read-only call
Call a read-only function to get contract data:
import { callReadOnlyFunction, cvToValue } from '@stacks/transactions';import { StacksTestnet } from '@stacks/network';async function getContractData() {const network = new StacksTestnet();const result = await callReadOnlyFunction({network,contractAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',contractName: 'my-contract',functionName: 'get-balance',functionArgs: [],senderAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',});// Convert Clarity value to JavaScriptconst balance = cvToValue(result);console.log('Balance:', balance);}
Passing function arguments
Most read-only functions require arguments. Use Clarity value builders:
import {callReadOnlyFunction,standardPrincipalCV,uintCV,stringUtf8CV,bufferCV} from '@stacks/transactions';async function checkUserPermission() {const functionArgs = [standardPrincipalCV('ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM'),stringUtf8CV('admin'),];const result = await callReadOnlyFunction({network: new StacksTestnet(),contractAddress: 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG',contractName: 'access-control',functionName: 'has-role',functionArgs,senderAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',});const hasRole = cvToValue(result); // booleanconsole.log('Has admin role:', hasRole);}
Handling complex return types
Read-only functions can return complex Clarity types:
import {callReadOnlyFunction,cvToJSON,ResponseCV,TupleCV} from '@stacks/transactions';async function getTokenInfo() {const result = await callReadOnlyFunction({network: new StacksTestnet(),contractAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',contractName: 'sip-010-token',functionName: 'get-token-info',functionArgs: [],senderAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',});// Handle response types (ok/err)if (result.type === 'response-ok') {const tokenInfo = cvToJSON(result.value);console.log('Token info:', tokenInfo);// {// name: "My Token",// symbol: "MTK",// decimals: 6,// totalSupply: "1000000000000"// }} else {console.error('Error:', cvToValue(result.value));}}
Type-safe read-only calls
Create type-safe wrappers for your contract calls:
interface TokenInfo {name: string;symbol: string;decimals: number;totalSupply: bigint;}async function getTypedTokenInfo(): Promise<TokenInfo> {const result = await callReadOnlyFunction({network: new StacksTestnet(),contractAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',contractName: 'token',functionName: 'get-token-info',functionArgs: [],senderAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',});if (result.type !== 'response-ok') {throw new Error('Failed to get token info');}const data = result.value as TupleCV;return {name: cvToValue(data.data.name),symbol: cvToValue(data.data.symbol),decimals: cvToValue(data.data.decimals),totalSupply: BigInt(cvToValue(data.data['total-supply'])),};}
Batch read-only calls
Efficiently fetch multiple values:
async function batchReadCalls() {const addresses = ['ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM','ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG','ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC',];// Parallel calls for better performanceconst balancePromises = addresses.map(address =>callReadOnlyFunction({network: new StacksTestnet(),contractAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',contractName: 'token',functionName: 'get-balance',functionArgs: [standardPrincipalCV(address)],senderAddress: address,}));const results = await Promise.all(balancePromises);return addresses.map((address, i) => ({address,balance: cvToValue(results[i]),}));}
Error handling
Handle various error scenarios:
async function safeReadOnlyCall() {try {const result = await callReadOnlyFunction({network: new StacksTestnet(),contractAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',contractName: 'my-contract',functionName: 'get-data',functionArgs: [],senderAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',});// Check for runtime errorsif (result.type === 'response-err') {const errorCode = cvToValue(result.value);throw new Error(`Contract error: ${errorCode}`);}return cvToValue(result);} catch (error: any) {if (error.message.includes('Contract not found')) {console.error('Contract does not exist');} else if (error.message.includes('Function not found')) {console.error('Function does not exist in contract');} else if (error.message.includes('Argument count mismatch')) {console.error('Wrong number of arguments');}throw error;}}
Map and list operations
Query map entries and list elements:
// Query map entryasync function getMapEntry() {const key = tupleCV({sender: standardPrincipalCV('ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM'),recipient: standardPrincipalCV('ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG'),});const result = await callReadOnlyFunction({network: new StacksTestnet(),contractAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',contractName: 'allowances',functionName: 'get-allowance',functionArgs: [key],senderAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',});const allowance = cvToValue(result);console.log('Allowance:', allowance);}// Query list lengthasync function getListLength() {const result = await callReadOnlyFunction({network: new StacksTestnet(),contractAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',contractName: 'registry',functionName: 'get-registered-count',functionArgs: [],senderAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',});return cvToValue(result);}
Contract state validation
Use read-only calls to validate state before transactions:
async function validateTransfer(sender: string,recipient: string,amount: number) {// Check sender balanceconst balanceResult = await callReadOnlyFunction({network: new StacksTestnet(),contractAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',contractName: 'token',functionName: 'get-balance',functionArgs: [standardPrincipalCV(sender)],senderAddress: sender,});const balance = cvToValue(balanceResult);if (balance < amount) {throw new Error('Insufficient balance');}// Check if recipient can receiveconst canReceiveResult = await callReadOnlyFunction({network: new StacksTestnet(),contractAddress: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',contractName: 'token',functionName: 'can-receive',functionArgs: [standardPrincipalCV(recipient)],senderAddress: sender,});if (!cvToValue(canReceiveResult)) {throw new Error('Recipient cannot receive tokens');}return true;}
React hook for read-only calls
Create a reusable hook for contract queries:
import { useState, useEffect } from 'react';import { ClarityValue } from '@stacks/transactions';export function useReadOnlyCall<T = any>(contractAddress: string,contractName: string,functionName: string,functionArgs: ClarityValue[] = []) {const [data, setData] = useState<T | null>(null);const [loading, setLoading] = useState(true);const [error, setError] = useState<Error | null>(null);useEffect(() => {async function fetchData() {try {setLoading(true);const result = await callReadOnlyFunction({network: new StacksTestnet(),contractAddress,contractName,functionName,functionArgs,senderAddress: contractAddress, // Use contract as sender});setData(cvToValue(result) as T);} catch (err) {setError(err as Error);} finally {setLoading(false);}}fetchData();}, [contractAddress, contractName, functionName]);return { data, loading, error };}// Usagefunction TokenBalance({ address }: { address: string }) {const { data: balance, loading, error } = useReadOnlyCall<number>('ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM','token','get-balance',[standardPrincipalCV(address)]);if (loading) return <div>Loading...</div>;if (error) return <div>Error: {error.message}</div>;return <div>Balance: {balance} tokens</div>;}
Caching strategies
Implement caching for frequently accessed data:
class ContractCache {private cache = new Map<string, { data: any; timestamp: number }>();private ttl = 60000; // 1 minuteprivate getCacheKey(contractAddress: string,contractName: string,functionName: string,args: any[]): string {return `${contractAddress}.${contractName}.${functionName}:${JSON.stringify(args)}`;}async readOnlyCall(options: any) {const key = this.getCacheKey(options.contractAddress,options.contractName,options.functionName,options.functionArgs);// Check cacheconst cached = this.cache.get(key);if (cached && Date.now() - cached.timestamp < this.ttl) {return cached.data;}// Fetch fresh dataconst result = await callReadOnlyFunction(options);const data = cvToValue(result);// Update cachethis.cache.set(key, { data, timestamp: Date.now() });return data;}invalidate(contractAddress: string, contractName: string) {// Clear cache for specific contractfor (const key of this.cache.keys()) {if (key.startsWith(`${contractAddress}.${contractName}`)) {this.cache.delete(key);}}}}
Best practices
- Always validate inputs: Check argument types match contract expectations
- Handle all response types: Functions can return ok, err, or other types
- Use sender address wisely: Some contracts check sender permissions
- Cache when appropriate: Reduce API calls for static data
- Batch related calls: Improve performance with parallel requests