Frontend & SDK Integration
Writing Data to Intelligent Contracts

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:

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 or use Transaction Kit rather than simulating before every click.

Write parameters

FieldMeaning
addressDeployed Intelligent Contract address.
functionNamePublic write method name.
args / kwargsPositional or named calldata arguments.
valueOptional GEN intentionally sent to a payable contract method. This is separate from the fee deposit.
feesEstimated distribution plus feeValue on a fee-charging deployment.
validUntilOptional 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:

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:

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:

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);
}
⚠️

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