---
title: "Writing to Intelligent Contracts"
description: "Estimate, submit, and verify fee-funded writes to Intelligent Contracts with GenLayerJS."
source: https://docs.genlayer.com/developers/decentralized-applications/writing-data
last_updated: 2026-09-03
---

# Writing to Intelligent Contracts

A write changes Intelligent Contract state and enters the GenLayer consensus lifecycle. On a fee-charging deployment, estimate the transaction policy first, submit its `distribution` and `feeValue`, then verify both the consensus status and execution result.

## Estimate and submit

For a development-time write, Studio can simulate the concrete call and turn its observed execution and message use into an estimate:

```typescript
import { createAccount, createClient, isSuccessful } from 'genlayer-js';
import { localnet } from 'genlayer-js/chains';

const account = createAccount();
const client = createClient({ chain: localnet, account });

const write = {
  address: contractAddress,
  functionName: 'update_storage',
  args: ['new_data'],
};

const estimate = await client.estimateTransactionFeesForWrite(write);
const txId = await client.writeContract({
  ...write,
  fees: {
    distribution: estimate.distribution,
    feeValue: estimate.feeValue,
  },
});

const transaction = await client.waitForFinalization({ hash: txId });
if (!isSuccessful(transaction)) {
  throw new Error(
    `Write failed: ${transaction.statusName} / ${transaction.txExecutionResultName}`,
  );
}
```

`estimateTransactionFeesForWrite` is useful while developing and profiling because it performs a write simulation. For an application user flow, consume a checked-in [`fee-profile.json`](./fee-profiling-and-estimation) or use [Transaction Kit](./transaction-kit-integration) rather than simulating before every click.

## Write parameters

| Field | Meaning |
|:--|:--|
| `address` | Deployed Intelligent Contract address. |
| `functionName` | Public write method name. |
| `args` / `kwargs` | Positional or named calldata arguments. |
| `value` | Optional GEN intentionally sent to a payable contract method. This is separate from the fee deposit. |
| `fees` | Estimated `distribution` plus `feeValue` on a fee-charging deployment. |
| `validUntil` | Optional latest activation time. It is not a finalization deadline. |

## Payable writes

The user value and fee deposit are different amounts. To send 5 GEN to a payable method, include `value` while keeping the estimate in `fees`:

```typescript
const call = {
  address: contractAddress,
  functionName: 'tip',
  args: [],
  value: 5n * 10n ** 18n,
};

const estimate = await client.estimateTransactionFeesForWrite(call);
const txId = await client.writeContract({
  ...call,
  fees: {
    distribution: estimate.distribution,
    feeValue: estimate.feeValue,
  },
});
```

The receiving method must be decorated with `@gl.public.write.payable`. The transaction's total wallet requirement is the user value plus the fee deposit.

## Browser wallets

Use an EIP-1193 provider for signing and connect it to the same chain definition used by the client:

```typescript
import { createClient } from 'genlayer-js';
import { studionet } from 'genlayer-js/chains';

const client = createClient({
  chain: studionet,
  account: walletAddress as `0x${string}`,
  provider: window.ethereum,
});

await client.connect('studionet');

const write = {
  address: contractAddress,
  functionName: 'create_profile',
  args: ['alice', 'Hello world'],
};
const estimate = await client.estimateTransactionFeesForWrite(write);

const txId = await client.writeContract({
  ...write,
  fees: {
    distribution: estimate.distribution,
    feeValue: estimate.feeValue,
  },
});
```

The matching v2 release candidate also supplies the Studio-dev chain definition for chain ID 61997. Do not reuse `studionet` for the preview; chain identity, RPC, and consensus addresses must move together.

## Wait for the intended lifecycle point

- `waitForDecision({ hash })` returns after a materialized decision. Use it for responsive UI that does not need final settlement yet.
- `waitForFinalization({ hash })` waits for final fee settlement and refunds. Use it for accounting and durable completion.
- `waitForTransactionReceipt({ waitUntil: 'decided' | 'finalized' })` is the generic form. Its old `status` option remains only for compatibility.

An `ACCEPTED` or `FINALIZED` status does not prove that the contract returned successfully. Always use `isSuccessful(transaction)` or require `FINISHED_WITH_RETURN` as well as the accepted/finalized status.

## Handle failures without duplicating writes

Separate failures before submission from failures after a transaction ID exists:

```typescript
let txId: `0x${string}` | undefined;

try {
  const estimate = await client.estimateTransactionFeesForWrite(write);
  txId = await client.writeContract({
    ...write,
    fees: {
      distribution: estimate.distribution,
      feeValue: estimate.feeValue,
    },
  });
} catch (error) {
  // No GenLayer transaction ID was returned. Re-estimate after fixing the
  // wallet rejection, balance, stale fee policy, or invalid input.
  throw error;
}

const transaction = await client.waitForFinalization({ hash: txId });
if (!isSuccessful(transaction)) {
  // Inspect this transaction. Do not blindly submit the same state-changing
  // operation again: the first write may already have executed.
  console.error(transaction.statusName, transaction.txExecutionResultName);
}
```

> **Warning:**
> Once `writeContract` returns a transaction ID, a timeout in your process is not evidence that submission failed. Resume tracking that ID. Blind retries can create duplicate application actions.

## Related

- [Fee Profiling and Estimation](./fee-profiling-and-estimation)
- [Fee Outcomes and Debugging](./fee-outcomes-and-debugging)
- [Transaction Kit Integration](./transaction-kit-integration)
- [Querying a Transaction](./querying-a-transaction)
