Skip to main content

Querying Contract State

Learn how to query (read) state from deployed ink! smart contracts without paying gas fees.

Overview​

Contract queries are read-only operations that:

  • ✅ Don't modify contract state
  • ✅ Don't require gas fees
  • ✅ Return results immediately
  • ✅ Don't create transactions

Use queries to read contract data before making state-changing calls.

Prerequisites​

  • ✅ Contract deployed to GLIN Network
  • ✅ Contract address and metadata
  • ✅ Connection to GLIN Network

Basic Query​

query-contract.ts
import { GlinClient } from '@glin-ai/sdk';
import { ContractPromise } from '@polkadot/api-contract';
import fs from 'fs';

async function queryContract() {
// 1. Connect to network
const client = await GlinClient.connect('wss://testnet.glin.ai');

// 2. Load contract
const contractAddress = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
const metadata = JSON.parse(
fs.readFileSync('./target/ink/metadata.json', 'utf8')
);

const contract = new ContractPromise(client.api, metadata, contractAddress);

// 3. Query contract state
const { output, result } = await contract.query.getMessage(
contractAddress, // caller address (any address works for queries)
{ gasLimit: -1 } // -1 = unlimited for queries
);

// 4. Process result
if (result.isOk && output) {
console.log('Message:', output.toHuman());
} else {
console.error('Query failed:', result.asErr);
}
}

queryContract().catch(console.error);

Query with Arguments​

query-with-args.ts
// Query ERC20 balance
const { output } = await contract.query.balanceOf(
queryAddress,
{ gasLimit: -1 },
accountAddress // argument: which account's balance?
);

const balance = output?.toHuman();
console.log(`Balance: ${balance}`);

// Query ERC20 allowance
const { output: allowanceOutput } = await contract.query.allowance(
queryAddress,
{ gasLimit: -1 },
ownerAddress, // arg 1: owner
spenderAddress // arg 2: spender
);

const allowance = allowanceOutput?.toHuman();
console.log(`Allowance: ${allowance}`);

Multiple Queries​

Query multiple values efficiently:

batch-queries.ts
async function batchQueries() {
const addresses = [
'5GrwvaEF...',
'5FHneW4...',
'5DAAnrj...'
];

// Run queries in parallel
const balances = await Promise.all(
addresses.map(async (address) => {
const { output } = await contract.query.balanceOf(
address,
{ gasLimit: -1 },
address
);
return {
address,
balance: output?.toString() || '0'
};
})
);

balances.forEach(({ address, balance }) => {
console.log(`${address}: ${balance}`);
});
}

Decode Query Results​

// Query returns a complex type
const { output } = await contract.query.getUserInfo(
queryAddress,
{ gasLimit: -1 },
userId
);

// Decode to JSON
const userInfo = output?.toJSON();
console.log('User info:', userInfo);

// Decode to human-readable format
const userInfoHuman = output?.toHuman();
console.log('User info (human):', userInfoHuman);

// Access specific fields
if (output) {
const decoded = output.toJSON();
console.log('Username:', decoded.username);
console.log('Score:', decoded.score);
console.log('Active:', decoded.isActive);
}

Query Before Transaction​

Always query before making state changes:

async function safeTransfer(to: string, amount: bigint) {
// 1. Query current balance
const { output } = await contract.query.balanceOf(
caller.address,
{ gasLimit: -1 },
caller.address
);

const balance = BigInt(output?.toString() || '0');

// 2. Check if sufficient balance
if (balance < amount) {
throw new Error(
`Insufficient balance. Have: ${balance}, Need: ${amount}`
);
}

// 3. Query recipient's current balance
const { output: recipientOutput } = await contract.query.balanceOf(
to,
{ gasLimit: -1 },
to
);

const recipientBefore = BigInt(recipientOutput?.toString() || '0');

// 4. Execute transfer
const tx = contract.tx.transfer(
{ gasLimit, storageDepositLimit: null },
to,
amount
);

await tx.signAndSend(caller);

// 5. Verify transfer succeeded
const { output: newBalance } = await contract.query.balanceOf(
to,
{ gasLimit: -1 },
to
);

const recipientAfter = BigInt(newBalance?.toString() || '0');

console.log(`✅ Transfer successful!`);
console.log(`Recipient balance: ${recipientBefore} → ${recipientAfter}`);
}

Real-Time Queries​

Query contract state at specific block heights:

// Query at latest block (default)
const { output: latest } = await contract.query.getValue(
address,
{ gasLimit: -1 }
);

// Query at specific block
const blockHash = await client.api.rpc.chain.getBlockHash(12345);
const { output: historical } = await contract.query.getValue(
address,
{ gasLimit: -1, at: blockHash }
);

console.log('Current value:', latest?.toHuman());
console.log('Value at block 12345:', historical?.toHuman());

Common Query Patterns​

ERC20 Queries​

// Get token info
const { output: name } = await contract.query.tokenName(address, { gasLimit: -1 });
const { output: symbol } = await contract.query.tokenSymbol(address, { gasLimit: -1 });
const { output: decimals } = await contract.query.tokenDecimals(address, { gasLimit: -1 });
const { output: totalSupply } = await contract.query.totalSupply(address, { gasLimit: -1 });

console.log(`Token: ${name?.toHuman()} (${symbol?.toHuman()})`);
console.log(`Decimals: ${decimals?.toNumber()}`);
console.log(`Total Supply: ${totalSupply?.toString()}`);

// Get user balance
const { output: balance } = await contract.query.balanceOf(
userAddress,
{ gasLimit: -1 },
userAddress
);

console.log(`Your balance: ${balance?.toString()}`);

// Check allowance
const { output: allowance } = await contract.query.allowance(
ownerAddress,
{ gasLimit: -1 },
ownerAddress,
spenderAddress
);

console.log(`Allowance: ${allowance?.toString()}`);

Check Contract Ownership​

const { output } = await contract.query.owner(
queryAddress,
{ gasLimit: -1 }
);

const owner = output?.toString();
const isOwner = owner === myAddress;

console.log(`Contract owner: ${owner}`);
console.log(`Am I owner? ${isOwner}`);

Error Handling​

try {
const { output, result } = await contract.query.getValue(
address,
{ gasLimit: -1 }
);

if (result.isErr) {
const error = result.asErr;

if (error.isModule) {
console.error('Contract error:', error.asModule.toHuman());
} else {
console.error('Query error:', error.toString());
}

return null;
}

return output?.toHuman();
} catch (error) {
console.error('Query failed:', error.message);
return null;
}

Best Practices​

1. Use Queries for Read Operations​

// ❌ Bad - using transaction to read data (costs gas!)
const tx = contract.tx.getValue({ gasLimit, storageDepositLimit: null });
await tx.signAndSend(caller);

// ✅ Good - using query (free!)
const { output } = await contract.query.getValue(address, { gasLimit: -1 });

2. Always Check Query Results​

// ❌ Bad - assuming query succeeds
const value = output.toNumber();

// ✅ Good - checking result first
if (result.isOk && output) {
const value = output.toNumber();
} else {
console.error('Query failed');
}

3. Batch Independent Queries​

// ❌ Bad - sequential queries
const balance1 = await contract.query.balanceOf(/* ... */);
const balance2 = await contract.query.balanceOf(/* ... */);
const balance3 = await contract.query.balanceOf(/* ... */);

// ✅ Good - parallel queries
const [balance1, balance2, balance3] = await Promise.all([
contract.query.balanceOf(/* ... */),
contract.query.balanceOf(/* ... */),
contract.query.balanceOf(/* ... */),
]);

4. Cache Query Results When Appropriate​

// Cache contract metadata (changes rarely)
const metadataCache = new Map();

async function getTokenInfo(contractAddress: string) {
if (metadataCache.has(contractAddress)) {
return metadataCache.get(contractAddress);
}

const info = {
name: await contract.query.tokenName(/* ... */),
symbol: await contract.query.tokenSymbol(/* ... */),
decimals: await contract.query.tokenDecimals(/* ... */),
};

metadataCache.set(contractAddress, info);
return info;
}

Troubleshooting​

Query Returns Unexpected Data​

Issue: Query returns null or wrong type

Solutions:

  1. Verify contract address is correct
  2. Check method name matches contract ABI
  3. Ensure arguments match expected types
  4. Verify contract is deployed and initialized

Query Fails Silently​

Issue: No error but no data returned

Solutions:

  1. Check result.isErr before using output
  2. Verify contract method is public (not private)
  3. Ensure RPC connection is stable

Next Steps​


Need help? Join our Discord or check the ink! documentation.