> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metalntwx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Integrate a contract

> Read contract state, submit authorized writes, and process finalized events

> [!NOTE]
>
> **What you'll be able to do**
>
> * Read contract data through its ABI.
> * Submit transactions that call contract functions.
> * Process finalized contract events.

An application uses a contract's address and ABI to read its state and call its
functions. This guide connects to `RecordBook`, whose owner can update stored
values. The same steps apply to other contracts using their ABIs, caller
permissions and completion checks.

## Obtain the deployment inputs

Request a deployment record containing the chain ID, address, ABI, source and
compiler version, and administrator configuration. Configure the application
with the record for the network it will use.

Require nonempty code at the supplied address. Compare the source and build
artifact with the deployer's verification record to check that the ABI describes
the deployed contract.

```typescript theme={null}
import { getAddress } from "viem"

const contractAddress = getAddress(config.contractAddress)
if (await client.getChainId() !== config.chainId) throw new Error("Wrong network")
const code = await client.getCode({ address: contractAddress })
if (!code || code === "0x") throw new Error("Contract is not deployed")
```

Here `config` is the application's environment configuration, containing the
`contractAddress` and numeric `chainId` from that deployment record.

## Use the record contract

`RecordBook` stores a value and revision for each `bytes32` key. Its owner can
update a record by supplying its current revision. Each update increments the
revision, so repeating the same call fails with `StaleRevision`.

Use an existing `RecordBook` deployment, or deploy the contract below with the
project setup and compiler settings in
[Deploy an application contract](https://docs.metalntwx.com/build/contracts/deploy-an-application-contract.md),
using `src/RecordBook.sol` as the source. Follow the deployment guide's build and deployment
steps, changing the `forge create` target to `src/RecordBook.sol:RecordBook` and
omitting `--constructor-args`. Use the checks at the end of this page to test
`RecordBook`. Save its address, creation receipt, source and ABI, and verify its
owner, initial records and deployment finality before integrating it.

```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.31;

contract RecordBook {
    address public immutable owner;
    struct Record { bytes32 value; uint256 revision; }
    mapping(bytes32 => Record) public records;
    event RecordUpdated(bytes32 indexed key, bytes32 value, uint256 revision);
    error Unauthorized();
    error StaleRevision();

    constructor() { owner = msg.sender; }

    function set(bytes32 key, bytes32 value, uint256 expectedRevision) external {
        if (msg.sender != owner) revert Unauthorized();
        Record storage record = records[key];
        if (record.revision != expectedRevision) revert StaleRevision();
        record.value = value;
        record.revision += 1;
        emit RecordUpdated(key, value, record.revision);
    }
}
```

Keys and values are public onchain, so use test identifiers that contain no
personal information. An untouched key reads as a zero value and revision zero.
When integrating a contract that collects tokens, also follow the
[spending approval procedure](https://docs.metalntwx.com/reference/assets-and-payments/mip20.md#approve-a-spender).

## Read at a specific block

The ABI below describes the record reads, updates and events used by the application:

```typescript theme={null}
import { parseAbi, type Hex } from "viem"

const abi = parseAbi([
  "function owner() view returns (address)",
  "function records(bytes32 key) view returns (bytes32 value, uint256 revision)",
  "function set(bytes32 key, bytes32 value, uint256 expectedRevision)",
  "event RecordUpdated(bytes32 indexed key, bytes32 value, uint256 revision)",
  "error Unauthorized()",
  "error StaleRevision()"
])
```

Supply `key` and `nextValue` as application-chosen 32-byte hexadecimal values.
Read `consensus_status` using the public finality procedure in
[Verify and reconcile payments](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md#verify-a-finalized-transfer).
Use its finalized height as `blockNumber` for both reads so the record and owner
come from the same block:

```typescript theme={null}
const [currentValue, revision] = await client.readContract({
  address: contractAddress, abi, functionName: "records", args: [key], blockNumber
})
const owner = await client.readContract({
  address: contractAddress, abi, functionName: "owner", blockNumber
})
```

Save the intended `key`, `nextValue` and returned `revision` with an application
operation ID before signing. These fields identify the update you will verify
after submission.

## Submit an authorized update

The signing account for `RecordBook.set` must have the address stored in `owner`.

Set `signerAddress` to the signing account's address and simulate `set` with the
saved arguments. Simulation checks whether the call succeeds against the
current state. The revision check also runs during execution, so another update
before inclusion can cause this transaction to revert.

```typescript theme={null}
await client.simulateContract({
  account: signerAddress,
  address: contractAddress,
  abi,
  functionName: "set",
  args: [key, nextValue, revision]
})
```

Estimate gas without fee fields, apply the headroom from
[Estimate and record fees](https://docs.metalntwx.com/build/prepare/estimate-and-record-fees.md), and
check that the signing account's balance in the gas token covers the maximum
gas cost. Follow the
[procedure for saving outgoing attempts](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md#save-outgoing-attempts-before-submission)
to reserve the signing account and save the intended call. Use the wallet client,
selected `gasToken`, and estimated `gasLimit` and `maxFeePerGas` to sign:

```typescript theme={null}
import { signMip20Contract } from "@metalntwx/metal-ts"

const signed = await signMip20Contract({
  publicClient: client,
  walletClient,
  address: contractAddress,
  abi,
  functionName: "set",
  args: [key, nextValue, revision],
  gasToken,
  gasLimit,
  gasFallback: gasLimit,
  maxFeePerGas
})
```

Save `signed.txHash` and `signed.raw` in the attempt record before calling
`signed.send()`, then check that its returned hash matches `signed.txHash`.
The SDK uses a Metal `0x7f` envelope with zero native value. See
[TypeScript SDK](https://docs.metalntwx.com/reference/typescript-sdk.md#signmip20contract) for its signing and
fee behavior.

## Verify execution, the event, and state

For direct contract calls, fetch the transaction by hash and compare its `from`,
`to`, and `input` with the saved signing account, contract address, and
ABI-encoded function call. When another party supplies the hash, use the
application's own saved instruction for this comparison.

Read the receipt by the same hash. Check that its `from` and `to` match the
transaction, then check the execution result. Wait until
`consensus_status.finalized.number` reaches the receipt's block number, then
verify that the canonical block hash matches the receipt. Record a finalized
revert as a failed attempt. For successful execution, verify the event and state
before updating the application record.

Decode only logs from the intended contract and require the exact update:

```typescript theme={null}
import { parseEventLogs } from "viem"

const updates = parseEventLogs({
  abi,
  eventName: "RecordUpdated",
  logs: receipt.logs.filter((log) => log.address.toLowerCase() === contractAddress.toLowerCase()),
  strict: true
})
const matching = updates.filter((log) =>
  log.args.key === key && log.args.value === nextValue && log.args.revision === revision + 1n
)
if (matching.length !== 1 || matching[0].logIndex === null) {
  throw new Error("Expected one record update")
}
const after = await client.readContract({
  address: contractAddress, abi, functionName: "records", args: [key],
  blockNumber: receipt.blockNumber
})
if (after[1] < revision + 1n || (after[1] === revision + 1n && after[0] !== nextValue)) {
  throw new Error("State disagrees with the update")
}
```

An `eth_call` at the receipt's block returns the state at the end of that block,
which may include later updates to the same record. The matching event verifies
your update, while the state read must show its revision or a later one. Process
any later revisions through their events before displaying the current value.

## Process events durably

Identify each event by `(chain ID, transaction hash, log index)`. Save the
verified event, application record update and cursor in one database
transaction. The cursor records the last processed block's number and hash.
Follow the storage and uniqueness rules in
[Verify and reconcile payments](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md), adapting
the expected fields and event decoder to `RecordUpdated`.

Discover logs using the contract address, `RecordUpdated` ABI, and bounded block
ranges from the saved cursor plus one through the finalized height. Process
blocks in order and events by transaction index and log index. Apply each
record's events in revision order, retrieving any missing revisions before
applying later values. Skip events already recorded in the database.

## Recover and test

After a lost response, use the saved hash and bytes to
[recover the same submission](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md#recover-an-uncertain-submission).
Resolve that attempt before deciding whether another update is needed. For a
finalized `StaleRevision` revert, read the newer record and decide whether the
intended change still applies before preparing a new transaction.

Test the integration with these scenarios:

| Scenario | Expected observation |
| - | - |
| Read an untouched key | Zero value and revision zero |
| Owner sets a record | Exact event and finalized state at revision one |
| Another address simulates `set` | `Unauthorized`, with unchanged state |
| Owner reuses the old expected revision | `StaleRevision`, with unchanged state |
| Process the same event twice | One application update |
| Stop processing, then restart | Catch up from the committed cursor in revision order |
| Discard submission response | Resolve the saved attempt without incrementing the revision twice |

Retain the contract deployment record, dependency lockfile, signed attempt,
receipt, matching event, block hashes, and database observations for verification.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.