Signing and Execution
Once you've built a transaction, you need to sign it and submit it to the network. For background on how transaction signing and finality work at the protocol level, see the Sui documentation on transaction lifecycle and transaction auth.
Simulating transactions
Use simulateTransaction to dry-run a transaction without executing it. This is useful for
estimating gas costs, checking return values, and validating transactions before executing.
import { SuiGrpcClient } from '@mysten/sui/grpc';
const grpcClient = new SuiGrpcClient({
network: 'mainnet',
baseUrl: 'https://fullnode.mainnet.sui.io:443',
});
const result = await grpcClient.simulateTransaction({
transaction: tx,
include: {
effects: true,
balanceChanges: true,
commandResults: true,
},
});
if (result.$kind === 'FailedTransaction') {
console.error('Simulation failed:', result.FailedTransaction.status.error?.message);
} else {
console.log('Balance changes:', result.Transaction.balanceChanges);
console.log('Command results:', result.commandResults);
}Include options
The include parameter controls what data is returned from the simulation. All fields are optional
and default to false:
| Field | Description |
|---|---|
effects | Execution effects — created, mutated, and deleted objects, gas usage |
events | Move events emitted during execution |
balanceChanges | Token balance changes for each affected address and coin type |
objectTypes | Map of object ID to type string for all changed objects |
transaction | The full transaction data (sender, commands, gas config) |
bcs | Raw BCS-encoded transaction bytes |
commandResults | BCS-encoded return values and mutated references from each command (simulation only) |
The commandResults field is unique to simulation — it's not available on executeTransaction.
Each entry contains returnValues and mutatedReferences, both as BCS-encoded Uint8Array values
that you can decode with the BCS library.
With a keypair (backend / scripts)
The simplest approach. The keypair signs the transaction and submits it to the network in one call:
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { SuiGrpcClient } from '@mysten/sui/grpc';
const keypair = Ed25519Keypair.fromSecretKey('suiprivkey1...');
const grpcClient = new SuiGrpcClient({
network: 'mainnet',
baseUrl: 'https://fullnode.mainnet.sui.io:443',
});
const result = await keypair.signAndExecuteTransaction({
transaction: tx,
client: grpcClient,
});fromSecretKey accepts a Bech32-encoded private key (suiprivkey1...) or a raw 32-byte
Uint8Array. You can also use Ed25519Keypair.deriveKeypair(mnemonic) to derive from a mnemonic
phrase.
This method automatically sets the sender to the keypair's address, builds the transaction, signs it, and executes it. The result includes the transaction data and effects by default.
Available keypair types:
Ed25519Keypairfrom@mysten/sui/keypairs/ed25519Secp256k1Keypairfrom@mysten/sui/keypairs/secp256k1Secp256r1Keypairfrom@mysten/sui/keypairs/secp256r1PasskeyKeypairfrom@mysten/sui/keypairs/passkey
With dApp Kit (React frontend)
In a React app, use the useSignAndExecuteTransaction hook. The user's connected wallet signs and
submits the transaction:
import { useSignAndExecuteTransaction } from '@mysten/dapp-kit';
import { Transaction } from '@mysten/sui/transactions';
function SendButton() {
const { mutate: signAndExecute } = useSignAndExecuteTransaction();
function handleClick() {
const tx = new Transaction();
// ... build your transaction ...
signAndExecute(
{ transaction: tx },
{
onSuccess: (result) => {
console.log('Transaction digest:', result.digest);
},
onError: (error) => {
console.error('Transaction failed:', error);
},
},
);
}
return <button onClick={handleClick}>Send</button>;
}See the dApp Kit documentation for full setup instructions.
Sign without executing
Sometimes you need the signature without immediately executing — for multi-sig, sponsored transactions, or delayed execution.
With a keypair
// Build the transaction bytes first
tx.setSender(keypair.toSuiAddress());
const bytes = await tx.build({ client: grpcClient });
// Sign the bytes
const { signature } = await keypair.signTransaction(bytes);With dApp Kit
import { useSignTransaction } from '@mysten/dapp-kit';
function SignButton() {
const { mutate: signTransaction } = useSignTransaction();
function handleClick() {
const tx = new Transaction();
// ... build your transaction ...
signTransaction(
{ transaction: tx },
{
onSuccess: ({ bytes, signature }) => {
// Send bytes + signature to your backend, sponsor, etc.
},
},
);
}
return <button onClick={handleClick}>Sign</button>;
}Manual execution
When you already have the transaction bytes and signature(s), execute directly through the client:
const result = await grpcClient.executeTransaction({
transaction: bytes, // Uint8Array
signatures: [signature], // string[]
include: {
effects: true,
events: true,
balanceChanges: true,
objectTypes: true,
},
});The include parameter controls what data is returned with the result:
| Field | Description |
|---|---|
transaction | The full transaction data (sender, commands, gas) |
effects | Execution effects (created/mutated/deleted objects) |
events | Move events emitted during execution |
balanceChanges | Token balance changes for each affected address |
objectTypes | Map of object ID to type for changed objects |
bcs | Raw BCS bytes of the transaction |
Observing results
Checking success or failure
Every transaction result is a discriminated union — always check which variant you received:
const result = await keypair.signAndExecuteTransaction({
transaction: tx,
client: grpcClient,
});
if (result.$kind === 'FailedTransaction') {
const { status } = result.FailedTransaction;
console.error('Transaction failed:', status.error?.message);
} else {
console.log('Transaction succeeded:', result.Transaction.digest);
}You can also use the shorthand:
const tx = result.Transaction ?? result.FailedTransaction;
if (!tx.status.success) {
throw new Error(`Failed: ${tx.status.error?.message}`);
}Waiting for indexing
After a transaction executes, the effects may not be immediately visible to read APIs (like
getBalance or getObject). Waiting is also required before executing a subsequent transaction
that depends on objects created or modified by the first one. Use waitForTransaction to ensure
consistency:
const result = await keypair.signAndExecuteTransaction({
transaction: tx,
client: grpcClient,
});
// Wait for the transaction to be indexed
await grpcClient.waitForTransaction({ result });
// Now reads will reflect the transaction's effects
const { balance } = await grpcClient.getBalance({ owner: myAddress });You can also wait by digest:
await grpcClient.waitForTransaction({ digest: result.Transaction.digest });Gas configuration
Every Sui transaction costs gas. The SDK handles gas automatically in most cases — it sets the gas price, estimates the budget, and selects how to pay. You only need to configure gas explicitly for special cases.
Defaults
When you don't configure gas explicitly, the SDK:
- Gas price — uses the network's reference gas price
- Gas budget — simulates the transaction and uses the result to set an appropriate budget
- Gas payment — uses address balances, falling back to coin objects when the balance is below the budget
For most transactions, the defaults work well and you don't need to change anything.
Explicit gas settings
// Override the reference gas price
tx.setGasPrice(1500);
// Override the simulated budget (in MIST)
tx.setGasBudget(50_000_000);Gas payment with coin objects
Specify exactly which coins to use for gas. These coins are merged into a single gas coin before execution:
tx.setGasPayment([
{ objectId: '0xCoin1', version: '1', digest: 'abc...' },
{ objectId: '0xCoin2', version: '2', digest: 'def...' },
]);The coins you provide must not overlap with any object inputs in your transaction.
Gas payment with address balance
To pay gas from your address balance instead of coin objects, pass an empty array:
tx.setGasPayment([]);This is particularly useful for offline building and sponsored transactions, since there are no coin object versions to look up or coordinate.
The gas coin (tx.gas)
tx.gas references the coin used for gas payment. You can split SUI from it, merge coins into it,
or transfer it to another address:
const [coin] = tx.splitCoins(tx.gas, [1_000_000_000]);
tx.transferObjects([coin], '0xRecipientAddress');tx.gas only works when gas is paid from coin objects. If gas is paid from address balances (via
setGasPayment([])), tx.gas is not available. Use coinWithBalance
instead, which works in both cases.
Sponsored transactions
In a sponsored transaction, someone other than the sender pays for gas. There are two flows depending on whether gas is paid from coin objects or address balances.
Coin-based sponsorship
The traditional flow where the sponsor provides specific gas coin objects:
// 1. User builds transaction kind bytes (no gas info)
const tx = new Transaction();
// ... add commands ...
const kindBytes = await tx.build({ client: grpcClient, onlyTransactionKind: true });
// 2. Sponsor wraps with gas info
const sponsoredTx = Transaction.fromKind(kindBytes);
sponsoredTx.setSender(userAddress);
sponsoredTx.setGasOwner(sponsorAddress);
sponsoredTx.setGasPayment(sponsorGasCoins);
// 3. Build the full transaction
const fullBytes = await sponsoredTx.build({ client: grpcClient });
// 4. Both parties sign
const { signature: userSignature } = await userKeypair.signTransaction(fullBytes);
const { signature: sponsorSignature } = await sponsorKeypair.signTransaction(fullBytes);
// 5. Execute with both signatures
const result = await grpcClient.executeTransaction({
transaction: fullBytes,
signatures: [userSignature, sponsorSignature],
});The user must wait for the sponsor to set gas coins before signing, because gas coins are part of the signed transaction data.
Address balance sponsorship
When the sponsor pays from their address balance instead of specific coins, the flow is simpler because there are no coin object references to coordinate:
// 1. User builds and signs the transaction first
const tx = new Transaction();
tx.setSender(userAddress);
tx.setGasOwner(sponsorAddress);
tx.setGasPayment([]); // empty array = use address balance for gas
// ... add commands ...
const bytes = await tx.build({ client: grpcClient });
const { signature: userSignature } = await userKeypair.signTransaction(bytes);
// 2. Sponsor signs (can happen asynchronously)
const { signature: sponsorSignature } = await sponsorKeypair.signTransaction(bytes);
// 3. Either party executes
const result = await grpcClient.executeTransaction({
transaction: bytes,
signatures: [userSignature, sponsorSignature],
});The key advantage: the sender can sign before the sponsor, enabling simpler async flows. No need to coordinate gas coin selection.
Using coinWithBalance in sponsored transactions
When building sponsored transactions, set useGasCoin: false on coinWithBalance so it doesn't try
to split the gas coin (which belongs to the sponsor):
import { coinWithBalance, Transaction } from '@mysten/sui/transactions';
const tx = new Transaction();
tx.transferObjects(
[coinWithBalance({ balance: 1_000_000_000, useGasCoin: false })],
'0xRecipientAddress',
);