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
| 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:
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 oldstatusoption 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.