Real Chain

Build an application

Discover testnet configuration, read a token, and simulate a transfer with viem.

View Markdown ↗

What you will build

This example connects to Real Testnet, checks the chain ID, reads a listed permissioned token, checks recipient eligibility, and simulates an ordinary transfer. It uses no private key and sends no transaction.

Use Node.js 24 and viem 2.52.2, the versions used for verification. In an empty project, run:

npm init -y
npm install viem@2.52.2

Save the following as read-and-preflight.mjs, or download the example. The default sender and recipient are deliberately unprepared addresses so you can observe a refusal before using real test wallets.

Read and simulate

import {
  BaseError, ContractFunctionRevertedError, createPublicClient,
  defineChain, getAddress, http, parseAbi, parseUnits,
} from 'viem';

const response = await fetch('https://testnet.rwa-platform.real.finance/v1/config');
if (!response.ok) throw new Error(`Configuration request failed: ${response.status}`);
const config = await response.json();
if (config.chainId !== 117711) throw new Error('Unexpected configuration chain');
const chain = defineChain({
  id: 117711, name: 'Real Testnet',
  nativeCurrency: { name: 'ASSET', symbol: 'ASSET', decimals: 18 },
  rpcUrls: { default: { http: [config.rpcUrl] } },
});
const client = createPublicClient({ chain, transport: http() });
if (await client.getChainId() !== chain.id) throw new Error('RPC chain mismatch');

// A listed test token; replace these public addresses for your own simulation.
const token = getAddress(process.argv[2] ?? '0xe157Af54109e7bc05ed868Cd859cc01908Fd1DdD');
const sender = getAddress(process.argv[3] ?? '0x0000000000000000000000000000000000000001');
const recipient = getAddress(process.argv[4] ?? '0x0000000000000000000000000000000000000002');
const blockNumber = await client.getBlockNumber();
if (!await client.getCode({ address: token, blockNumber })) throw new Error('No token code');
const tokenAbi = parseAbi([
  'function name() view returns (string)',
  'function decimals() view returns (uint8)',
  'function balanceOf(address) view returns (uint256)',
  'function identityRegistry() view returns (address)',
  'function paused() view returns (bool)',
  'function transfer(address,uint256) returns (bool)',
]);
const read = (functionName, args = []) => client.readContract({ address: token, abi: tokenAbi, functionName, args, blockNumber });
// Sequential reads also work with conservative public RPC rate limits.
const name = await read('name');
const decimals = await read('decimals');
const balance = await read('balanceOf', [sender]);
const paused = await read('paused');
const registry = await read('identityRegistry');
const recipientVerified = await client.readContract({
  address: registry, abi: parseAbi(['function isVerified(address) view returns (bool)']),
  functionName: 'isVerified', args: [recipient], blockNumber,
});
const amount = parseUnits(process.argv[5] ?? '1', decimals);
if (amount <= 0n) throw new Error('Use a positive transfer amount');
console.log({ chainId: chain.id, blockNumber: String(blockNumber), token, name, decimals,
  senderBalance: String(balance), paused, recipientVerified });
try {
  await client.simulateContract({ address: token, abi: tokenAbi, functionName: 'transfer',
    args: [recipient, amount], account: sender, blockNumber });
  console.log('Simulation succeeded. No transaction was sent.');
} catch (error) {
  const revert = error instanceof BaseError
    ? error.walk(cause => cause instanceof ContractFunctionRevertedError) : undefined;
  if (!(revert instanceof ContractFunctionRevertedError)) throw error;
  console.log(`Simulation refused: ${revert.reason ?? revert.shortMessage}`);
  console.log('No transaction was sent. Resolve balance, pause, freeze, identity, or compliance restrictions before retrying.');
}

Run it:

node read-and-preflight.mjs

To use your own token and wallets, pass public addresses and a positive human-readable token amount in this order:

node read-and-preflight.mjs TOKEN SENDER RECIPIENT AMOUNT

The optional addresses select the simulated call; they do not grant signing authority. Reads and simulation use one block number so the result describes a consistent snapshot.

Expected result

On 17 September 2026, block 4503, the default example read Acme M23 Bond, 18 decimals, an unpaused token, a zero sender balance, and an unverified recipient. Simulation reverted with Insufficient Balance. That is the expected contract refusal, not a failed connection. Other errors, such as an unreachable RPC, are rethrown rather than disguised as transfer refusals.

For a permitted transfer, the sender needs enough unfrozen balance and the recipient must satisfy identity checks. Both wallets and the token must satisfy freeze, pause, and compliance rules. A successful simulation is evidence for that block only; recheck before asking a wallet to sign.

Move from simulation to an application

Use the contract reference and network configuration for the selected environment. Keep native ASSET available for transaction gas when you later introduce wallet-signed transactions.

Show awaiting signature, submitted, confirmed, and indexed states separately. A transaction hash is not a success receipt, and the indexer may update later. Never retry a submission just because the portfolio has not refreshed.

The platform integration guide explains session-based preflight and durable jobs. Platform preflight can impose additional account checks beyond this direct contract simulation.