Skip to main content

Calling Contract Methods

Learn how to call methods on deployed ink! smart contracts.

Overview

After deploying a contract, you can interact with it by calling its methods. Contract methods fall into two categories:

  • 🔵 Queries - Read-only operations (covered in Querying State)
  • 🟢 Transactions - State-changing operations (this guide)

This guide focuses on transactions - methods that modify contract state.

Prerequisites

  • ✅ Contract deployed to GLIN Network (Deploy Guide)
  • ✅ Contract address and metadata
  • ✅ Account with GLIN for gas fees

Call a Contract Method

Basic Example

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

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

// 2. Load caller account
const keyring = new Keyring({ type: 'sr25519' });
const caller = keyring.addFromUri('//Alice');

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

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

// 4. Estimate gas
const gasLimit = client.api.registry.createType('WeightV2', {
refTime: 1_000_000_000,
proofSize: 500_000,
});

// 5. Call method
const tx = contract.tx.setMessage(
{ gasLimit, storageDepositLimit: null },
'Hello from TypeScript!'
);

// 6. Sign and send
const hash = await tx.signAndSend(caller);
console.log('Transaction hash:', hash.toHex());

// 7. Wait for finalization
await client.waitForFinalization(hash);
console.log('✅ Transaction finalized!');
}

callContract().catch(console.error);

Call with Return Values

Some methods return values after execution:

call-with-return.ts
import { GlinClient, Keyring } from '@glin-ai/sdk';
import { ContractPromise } from '@polkadot/api-contract';

async function incrementAndGet() {
const client = await GlinClient.connect('wss://testnet.glin.ai');
const keyring = new Keyring({ type: 'sr25519' });
const caller = keyring.addFromUri('//Alice');

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

// Call increment method
const gasLimit = client.api.registry.createType('WeightV2', {
refTime: 1_000_000_000,
proofSize: 500_000,
});

const tx = contract.tx.increment({ gasLimit, storageDepositLimit: null });

// Execute transaction
await new Promise((resolve, reject) => {
tx.signAndSend(caller, (result) => {
if (result.status.isFinalized) {
console.log('Transaction finalized');
resolve(result);
}
if (result.isError) {
reject(new Error('Transaction failed'));
}
});
});

// Query the new value
const { output } = await contract.query.get(caller.address, {
gasLimit: -1,
});

console.log('New value:', output?.toHuman());
}

Call with Multiple Arguments

// ERC20 transfer example
const tx = contract.tx.transfer(
{ gasLimit, storageDepositLimit: null },
recipientAddress, // arg 1: to
1000n * 10n ** 18n // arg 2: amount (1000 tokens)
);

await tx.signAndSend(caller);

Gas Estimation

Always estimate gas before calling contracts:

estimate-gas.ts
async function estimateGas() {
const contract = new ContractPromise(client.api, metadata, contractAddress);

// Dry run to estimate gas
const { gasRequired, result } = await contract.query.setMessage(
caller.address,
{ gasLimit: -1 }, // -1 = estimate
'New message'
);

if (result.isOk) {
console.log('Gas required:', gasRequired.toHuman());

// Add 10% buffer
const gasLimit = client.api.registry.createType('WeightV2', {
refTime: gasRequired.refTime.muln(1.1),
proofSize: gasRequired.proofSize.muln(1.1),
});

// Use estimated gas for actual call
const tx = contract.tx.setMessage({ gasLimit, storageDepositLimit: null }, 'New message');
await tx.signAndSend(caller);
}
}

Handle Transaction Results

handle-results.ts
async function callWithHandling() {
const tx = contract.tx.transfer(
{ gasLimit, storageDepositLimit: null },
recipient,
amount
);

await new Promise((resolve, reject) => {
tx.signAndSend(caller, ({ status, events, dispatchError }) => {
// Transaction is in a block
if (status.isInBlock) {
console.log(`In block: ${status.asInBlock.toHex()}`);
}

// Transaction is finalized
if (status.isFinalized) {
console.log(`Finalized: ${status.asFinalized.toHex()}`);

// Check for errors
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = client.api.registry.findMetaError(
dispatchError.asModule
);
const { docs, name, section } = decoded;

reject(new Error(`${section}.${name}: ${docs.join(' ')}`));
} else {
reject(new Error(dispatchError.toString()));
}
} else {
// Success - process events
events.forEach(({ event }) => {
if (client.api.events.system.ExtrinsicSuccess.is(event)) {
console.log('✅ Transaction succeeded');
}

if (client.api.events.contracts.ContractEmitted.is(event)) {
console.log('📊 Contract event:', event.data);
}
});

resolve(status.asFinalized);
}
}
});
});
}

Batch Calls

Execute multiple contract calls in a single transaction:

batch-calls.ts
import { GlinClient } from '@glin-ai/sdk';

async function batchCalls() {
const client = await GlinClient.connect('wss://testnet.glin.ai');

// Prepare multiple calls
const call1 = contract.tx.setMessage(
{ gasLimit, storageDepositLimit: null },
'First message'
);
const call2 = contract.tx.setMessage(
{ gasLimit, storageDepositLimit: null },
'Second message'
);

// Batch them
const batchTx = client.api.tx.utility.batch([
call1.toJSON(),
call2.toJSON()
]);

const hash = await batchTx.signAndSend(caller);
console.log('Batch transaction hash:', hash.toHex());
}

Error Handling

try {
await tx.signAndSend(caller);
} catch (error) {
if (error.message.includes('Module')) {
console.error('❌ Contract reverted:', error.message);
} else if (error.message.includes('OutOfGas')) {
console.error('❌ Out of gas - increase gas limit');
} else if (error.message.includes('InsufficientBalance')) {
console.error('❌ Insufficient balance');
} else {
console.error('❌ Transaction failed:', error.message);
}
}

Common Patterns

ERC20 Transfer

async function transferTokens(to: string, amount: bigint) {
const contract = new ContractPromise(client.api, erc20Metadata, tokenAddress);

const gasLimit = client.api.registry.createType('WeightV2', {
refTime: 2_000_000_000,
proofSize: 500_000,
});

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

await tx.signAndSend(caller);
console.log(`Transferred ${amount} tokens to ${to}`);
}

Approve & Transfer From

// 1. Approve spender
const approveTx = contract.tx.approve(
{ gasLimit, storageDepositLimit: null },
spenderAddress,
allowance
);
await approveTx.signAndSend(owner);

// 2. Spender transfers on behalf of owner
const transferFromTx = contract.tx.transferFrom(
{ gasLimit, storageDepositLimit: null },
ownerAddress,
recipientAddress,
amount
);
await transferFromTx.signAndSend(spender);

Best Practices

1. Always Estimate Gas

// ❌ Bad - hardcoded gas
const gasLimit = 1_000_000_000;

// ✅ Good - estimated gas with buffer
const { gasRequired } = await contract.query.method(/* ... */);
const gasLimit = gasRequired.muln(1.1);

2. Handle Errors Gracefully

// ❌ Bad - no error handling
await tx.signAndSend(caller);

// ✅ Good - comprehensive error handling
try {
await tx.signAndSend(caller);
} catch (error) {
// Handle specific error types
}

3. Wait for Finalization

// ❌ Bad - don't wait
const hash = await tx.signAndSend(caller);

// ✅ Good - wait for finalization
await new Promise((resolve) => {
tx.signAndSend(caller, ({ status }) => {
if (status.isFinalized) resolve();
});
});

4. Check Contract State After Calls

// Call method
await contract.tx.increment({ gasLimit, storageDepositLimit: null }).signAndSend(caller);

// Verify state changed
const { output } = await contract.query.get(caller.address, { gasLimit: -1 });
console.log('New value:', output?.toNumber());

Troubleshooting

Transaction Reverted

Symptom: Module { index: 8, error: 3 }

Common causes:

  • Contract logic rejected the call (e.g., insufficient balance in ERC20)
  • Invalid arguments
  • Precondition not met

Solution: Check contract logic and ensure all preconditions are met

Out of Gas

Symptom: OutOfGas error

Solution: Increase gas limit or optimize contract code

Insufficient Balance

Symptom: InsufficientBalance error

Solution: Fund the caller account with more GLIN

Next Steps


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