Skip to main content

Simulate and Execute Transactions

@iota/grpc does not build or sign transactions. You build them with the Transaction class of @iota/iota-sdk, sign them with a keypair, and then hand the bytes and signatures to the gRPC client.

Build and sign a transaction​

tx.build() has to know every input up front. Without a JSON-RPC client to look them up, you set the gas price, budget and payment yourself, using getReferenceGasPrice and listOwnedObjects:

import { toBase58, toHex } from '@iota/bcs';
import { IotaGrpcClient } from '@iota/grpc';
import { Ed25519Keypair } from '@iota/iota-sdk/keypairs/ed25519';
import { Transaction } from '@iota/iota-sdk/transactions';

const client = new IotaGrpcClient({ network: 'testnet' });
const keypair = Ed25519Keypair.fromSecretKey(secretKey);
const sender = keypair.toIotaAddress();

const { body: referenceGasPrice } = await client.getReferenceGasPrice();
const { body: coins } = await client.listOwnedObjects(sender, {
objectType: '0x2::coin::Coin<0x2::iota::IOTA>',
pageSize: 1,
});
const gas = coins.items[0].reference!;

const tx = new Transaction();
const [coin] = tx.splitCoins(tx.gas, [1_000]);
tx.transferObjects([coin], recipient);
tx.setSender(sender);
tx.setGasPrice(referenceGasPrice);
tx.setGasBudget(50_000_000n);
tx.setGasPayment([
{
objectId: `0x${toHex(gas.objectId!.objectId)}`,
version: String(gas.version),
digest: toBase58(gas.digest!.digest),
},
]);

const bytes = await tx.build();
const { signature } = await keypair.signTransaction(bytes);

If you also have an IotaClient, tx.build({ client: iotaClient }) resolves the gas and other inputs for you.

Execute a transaction​

Pass the bytes and signatures as a SignedTransaction. The bytes can be a Uint8Array or a base64 string, and each signature is the base64 string that signTransaction returns:

import { decodeTransactionEffects, TransactionField } from '@iota/grpc';

const { body } = await client.executeTransaction(
{ transaction: bytes, signatures: [signature] },
{
readMask: [TransactionField.EFFECTS, TransactionField.CHECKPOINT],
checkpointInclusionTimeoutMs: 10_000n,
},
);

const { status } = decodeTransactionEffects(body.effects!).V1;
console.log(status.$kind, body.checkpoint);

If the node rejects the transaction, for example because a signature is invalid, the call throws a ServerError. A transaction that runs and then fails, say by running out of gas or aborting in Move, does not throw. It comes back with a Failed status in its effects, so always check status.

Wait for the checkpoint​

By default the call returns as soon as the transaction is executed. With checkpointInclusionTimeoutMs, the node also waits up to that long for the transaction to land in a checkpoint. Add TransactionField.CHECKPOINT (and TransactionField.TIMESTAMP if you want it) to the read mask to get them back.

Choose what comes back​

By default you get the digest, effects, events, and input and output objects. Use readMask with TransactionField constants to trim that down, or to add BALANCE_CHANGES and OBJECT_CHANGES.

Execute several transactions​

executeTransactions takes a list and returns one ItemResult per transaction:

const { body } = await client.executeTransactions([first, second], {
readMask: TransactionField.EFFECTS,
});

for (const result of body) {
if (result.ok) {
console.log(decodeTransactionEffects(result.value.effects!).V1.status.$kind);
} else {
console.error(result.error.message);
}
}

The node submits the transactions concurrently, so none of them can rely on another's effects. Send dependent transactions in separate calls, and give each transaction in a batch its own gas coin.

Simulate a transaction​

simulateTransaction runs a transaction against current state without committing anything, and needs no signature. If you leave the gas payment empty, the node pays with a mock coin, so the sender does not even need to own any IOTA:

import { decodeTransactionEffects } from '@iota/grpc';
import { Transaction } from '@iota/iota-sdk/transactions';

const tx = new Transaction();
tx.setSender(sender);
tx.setGasPrice(referenceGasPrice);
tx.setGasBudget(50_000_000n);
tx.setGasPayment([]);
const [coin] = tx.splitCoins(tx.gas, [1]);
tx.transferObjects([coin], sender);

const { body } = await client.simulateTransaction({ transaction: await tx.build() });

const effects = decodeTransactionEffects(body.executedTransaction!.effects!);
console.log(effects.V1.status.$kind, effects.V1.gasUsed, body.suggestedGasPrice);

body.executionResult holds each command's result when the transaction succeeds, or the execution error when it fails.

Pass skipChecks: true to relax the Move VM's checks. This helps when you are debugging a transaction the checks would otherwise reject:

await client.simulateTransaction({ transaction: bytes, skipChecks: true });

simulateTransactions takes a list and returns one result per transaction. Each one runs separately against current state, so none of them sees another's effects.