> For the complete documentation index, see [llms.txt](/llms.txt)

# Building Offline

Build transactions without a network connection



Normally `build()` queries the network to resolve object versions, resolve intents like `tx.coin()`,
fetch the gas price, estimate the gas budget, pick gas coins, and set an expiration when gas is paid
from address balance. To build without a client, provide that information yourself. See also the Sui
documentation on
[offline signing](https://docs.sui.io/guides/developer/transactions/transaction-auth/offline-signing)
for the protocol-level details.

## Building only the transaction kind [#building-only-the-transaction-kind]

If the next step only needs the inputs and commands (for example, a sponsor or a backend that fills
in gas), build with `onlyTransactionKind`. Sender, gas data, and expiration are left out:

```typescript
import { Transaction } from '@mysten/sui/transactions';

const tx = new Transaction();
tx.moveCall({
	target: '0xPackage::module::function',
	arguments: [tx.pure.u64(100)],
});

const kindBytes = await tx.build({ onlyTransactionKind: true });
```

Inputs and intents still need to be resolved. Use full object references, `tx.withdrawal()`, or
[`assumeSufficientAddressBalances`](#coin-and-balance-intents) so nothing needs a lookup.

## Building full transaction bytes [#building-full-transaction-bytes]

A full offline build needs the sender and all gas data:

| Method            | Description                                                     |
| ----------------- | --------------------------------------------------------------- |
| `setSender()`     | The address executing the transaction                           |
| `setGasPrice()`   | Reference gas price (query `getReferenceGasPrice()` beforehand) |
| `setGasBudget()`  | Maximum gas to spend (in MIST). Estimating it requires a client |
| `setGasPayment()` | Coin object references, or `[]` to pay gas from address balance |
| `setGasOwner()`   | Only for sponsored transactions. Defaults to the sender         |

### Paying gas from address balance [#paying-gas-from-address-balance]

With `setGasPayment([])`, gas is paid from the gas owner's SUI address balance. Nothing ties the
transaction to a specific object version, so it also needs a `ValidDuring` expiration for replay
protection:

```typescript
import { Transaction } from '@mysten/sui/transactions';

// Look these up before going offline
const referenceGasPrice = 1000n;
const currentEpoch = 100;
const chainIdentifier = 'Base58ChainIdentifier'; // from getChainIdentifier()

const tx = new Transaction();

// FundsWithdrawal inputs contain the amount and type, so no object lookup is needed
tx.moveCall({
	target: '0x2::balance::send_funds',
	typeArguments: ['0x2::sui::SUI'],
	arguments: [tx.withdrawal({ amount: 1_000_000_000 }), tx.pure.address('0xRecipientAddress')],
});

tx.setSender('0xSenderAddress');
tx.setGasPrice(referenceGasPrice);
tx.setGasBudget(50_000_000);
tx.setGasPayment([]);

tx.setExpiration({
	ValidDuring: {
		minEpoch: currentEpoch,
		maxEpoch: currentEpoch + 1,
		minTimestamp: null,
		maxTimestamp: null,
		chain: chainIdentifier,
		// Must be unique for each transaction in the validity window, including across restarts
		nonce: await nonceStore.next(),
	},
});

const bytes = await tx.build();
```

Two otherwise-identical transactions with the same nonce have the same digest, and the second one is
rejected as a duplicate. Use a counter that survives restarts, not a hard-coded value.

The address balance needs to cover the gas budget on top of any withdrawals. Don't add the budget to
the withdrawal amount.

`{ Epoch: n }` expiration does not provide replay protection. Use `ValidDuring`.

### Paying gas with coin objects [#paying-gas-with-coin-objects]

Gas coins need an exact version and digest. Other SUI can still come from address balance:

```typescript
import { Transaction } from '@mysten/sui/transactions';

const tx = new Transaction();
tx.moveCall({
	target: '0x2::balance::send_funds',
	typeArguments: ['0x2::sui::SUI'],
	arguments: [
		tx.balance({ balance: 1_000_000, useGasCoin: false }),
		tx.pure.address('0xRecipientAddress'),
	],
});

tx.setSender('0xSenderAddress');
tx.setGasPrice(1000);
tx.setGasBudget(50_000_000);
tx.setGasPayment([{ objectId: '0xGasCoinId', version: '3', digest: 'Base58GasCoinDigest' }]);

// assumeSufficientAddressBalances resolves tx.balance() without a client (see below)
const bytes = await tx.build({ assumeSufficientAddressBalances: true });
```

## Object inputs [#object-inputs]

### Shared and party objects [#shared-and-party-objects]

Shared objects only need `objectId` and `initialSharedVersion`, both of which are stable:

```typescript
tx.sharedObjectRef({
	objectId: '0xSharedObjectId',
	initialSharedVersion: '1',
	mutable: true,
});
```

Party objects are address-owned but consensus-versioned, with per-address permissions. They are
referenced the same way as shared objects:

```typescript
tx.sharedObjectRef({
	objectId: '0xPartyObjectId',
	initialSharedVersion: '1',
	mutable: true,
});
```

Key properties for offline building:

* **No version lookup needed**: `initialSharedVersion` is stable and set once when the object
  becomes a party object
* **Enable pipelining**: Submit multiple transactions on the same party object without waiting for
  each one to finalize
* **Cannot be used for gas**: Pay gas from address balance or with a SUI coin object

### Owned and immutable objects [#owned-and-immutable-objects]

Owned and immutable objects need the exact version and digest:

```typescript
import { Transaction } from '@mysten/sui/transactions';

const tx = new Transaction();

// Owned objects need exact version and digest
tx.transferObjects(
	[
		tx.objectRef({
			objectId: '0xOwnedObjectId',
			version: '42',
			digest: 'abc123...',
		}),
	],
	'0xRecipientAddress',
);

// Receiving objects also need exact version and digest
tx.moveCall({
	target: '0xPackage::module::receive',
	arguments: [
		tx.objectRef({
			objectId: '0xParentId',
			version: '10',
			digest: 'def456...',
		}),
		tx.receivingRef({
			objectId: '0xReceivingId',
			version: '5',
			digest: 'ghi789...',
		}),
	],
});
```

## Coin and balance intents [#coin-and-balance-intents]

`tx.coin()` and `tx.balance()` normally look up the sender's address balance and coin objects. Pass
`assumeSufficientAddressBalances` to skip the lookups and withdraw from address balance instead:

```typescript
import { Transaction } from '@mysten/sui/transactions';

const tx = new Transaction();
tx.moveCall({
	target: '0x2::balance::send_funds',
	typeArguments: ['0xPackage::module::TOKEN'],
	arguments: [
		tx.balance({ type: '0xPackage::module::TOKEN', balance: 1_000_000 }),
		tx.pure.address('0xRecipientAddress'),
	],
});

// The sender is still required, since the withdrawal comes from its address balance
tx.setSender('0xSenderAddress');

const kindBytes = await tx.build({
	onlyTransactionKind: true,
	assumeSufficientAddressBalances: true,
});
```

<Callout type="warn">
  Nothing is checked. The transaction builds, then fails at execution if the address balance doesn't
  cover it.
</Callout>

SUI intents withdraw from address balance too, whether or not the transaction also uses `tx.gas`.
This matches an online build when the address balance is sufficient. Mixing default and
`useGasCoin: false` SUI intents in one transaction is still an error.

On a full build, the option also sets an unset gas payment to `[]`, but only when nothing else needs
a client, the transaction doesn't use `tx.gas`, and a `ValidDuring` or `Validity` expiration is
already set. Sender, gas price, and gas budget still need to be provided. Once set, the empty
payment is part of the transaction, the same as calling `setGasPayment([])`.

## Serialization [#serialization]

`toJSON()` resolves async thunks and intents but does not fill in gas or object versions. Pass
`supportedIntents` to keep an intent for another system to resolve:

```typescript
// Intents are resolved, so tx.coin() needs a client or assumeSufficientAddressBalances
const json = await tx.toJSON({ assumeSufficientAddressBalances: true });
const restored = Transaction.from(json);

// Or keep tx.coin() intents for the receiver to resolve
const jsonWithIntents = await tx.toJSON({ supportedIntents: ['CoinWithBalance'] });
```

`Transaction.from()` accepts JSON strings, BCS bytes, and base64-encoded BCS.
