# Build an application (/docs/chain/build)



## What you will build [#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:

```sh
npm init -y
npm install viem@2.52.2
```

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

## Read and simulate [#read-and-simulate]

```js
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:

```sh
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:

```text
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 [#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 [#move-from-simulation-to-an-application]

Use the [contract reference](/docs/resources/testnet-contracts) and [network configuration](/docs/chain/networks) 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](/docs/tokenization/integration) explains session-based [preflight](/docs/resources/glossary#preflight) and durable jobs. Platform preflight can impose additional account checks beyond this direct contract simulation.
