UNPKG

@mysten/sui

Version:
298 lines (228 loc) 11.5 kB
# Sui Programmable Transaction Basics > Construct programmable transaction blocks with the Transaction API This example starts by constructing a transaction to send SUI. To construct transactions, import the `Transaction` class and construct it: ```tsx const tx = new Transaction(); ``` You can then add commands to the transaction . ```tsx // create a new coin with balance 100, based on the coins used as gas payment // you can define any balance here const [coin] = tx.splitCoins(tx.gas, [100]); // transfer the split coin to a specific address tx.transferObjects([coin], '0xSomeSuiAddress'); ``` You can attach multiple commands of the same type to a transaction, as well. For example, to get a list of transfers and iterate over them to transfer coins to each of them: ```tsx interface Transfer { to: string; amount: number; } // procure a list of some Sui transfers to make const transfers: Transfer[] = getTransfers(); const tx = new Transaction(); // first, split the gas coin into multiple coins const coins = tx.splitCoins( tx.gas, transfers.map((transfer) => transfer.amount), ); // next, create a transfer command for each coin transfers.forEach((transfer, index) => { tx.transferObjects([coins[index]], transfer.to); }); ``` After you have the transaction defined, you can directly execute it with a signer using `signAndExecuteTransaction`. ```tsx const result = await client.signAndExecuteTransaction({ signer: keypair, transaction: tx }); // IMPORTANT: Always check the transaction status // Transactions can execute but still fail (e.g., insufficient gas, move errors) if (result.$kind === 'FailedTransaction') { throw new Error(`Transaction failed: ${result.FailedTransaction.status.error?.message}`); } ``` ## Observing the results of a transaction When you use `client.signAndExecuteTransaction` or `client.executeTransactionBlock`, the transaction will be finalized on the blockchain before the function resolves, but the effects of the transaction may not be immediately observable. There are 2 ways to observe the results of a transaction. Methods like `client.signAndExecuteTransaction` accept an `options` object with options like `showObjectChanges` and `showBalanceChanges` (see [the SuiJsonRpcClient docs for more details](/sui/clients/json-rpc#arguments)). These options will cause the request to contain additional details about the effects of the transaction that can be immediately displayed to the user, or used for further processing in your application. The other way effects of transactions can be observed is by querying other RPC methods like `client.getBalances` that return objects or balances owned by a specific address. These RPC calls depend on the RPC node having indexed the effects of the transaction, which may not have happened immediately after a transaction has been executed. To ensure that effects of a transaction are represented in future RPC calls, you can use the `waitForTransaction` method on the client: ```typescript const result = await client.signAndExecuteTransaction({ signer: keypair, transaction: tx }); // Check transaction status if (result.$kind === 'FailedTransaction') { throw new Error(`Transaction failed: ${result.FailedTransaction.status.error?.message}`); } await client.waitForTransaction({ result }); ``` Once `waitForTransaction` resolves, any future RPC calls will be guaranteed to reflect the effects of the transaction. ## Transactions Programmable Transactions have two key concepts: inputs and commands. Commands are steps of execution in the transaction. Each command in a Transaction takes a set of inputs, and produces results. The inputs for a transaction depend on the kind of command. Sui supports following commands: - `tx.splitCoins(coin, amounts)` - Creates new coins with the defined amounts, split from the provided coin. Returns the coins so that it can be used in subsequent transactions. - Example: `tx.splitCoins(tx.gas, [100, 200])` - `tx.mergeCoins(destinationCoin, sourceCoins)` - Merges the sourceCoins into the destinationCoin. - Example: `tx.mergeCoins(tx.object(coin1), [tx.object(coin2), tx.object(coin3)])` - `tx.transferObjects(objects, address)` - Transfers a list of objects to the specified address. - Example: `tx.transferObjects([tx.object(thing1), tx.object(thing2)], myAddress)` - `tx.moveCall({ target, arguments, typeArguments })` - Executes a Move call. Returns whatever the Sui Move call returns. - Example: `tx.moveCall({ target: '0x2::devnet_nft::mint', arguments: [tx.pure.string(name), tx.pure.string(description), tx.pure.string(image)] })` - `tx.makeMoveVec({ type, elements })` - Constructs a vector of objects that can be passed into a `moveCall`. This is required as there’s no way to define a vector as an input. - Example: `tx.makeMoveVec({ elements: [tx.object(id1), tx.object(id2)] })` - `tx.publish(modules, dependencies)` - Publishes a Move package. Returns the upgrade capability object. ## Passing inputs to a command Command inputs can be provided in a number of different ways, depending on the command, and the type of value being provided. #### JavaScript values For specific command arguments (`amounts` in `splitCoins`, and `address` in `transferObjects`) the expected type is known ahead of time, and you can directly pass raw javascript values when calling the command method. appropriate Move type automatically. ```ts // the amount to split off the gas coin is provided as a pure javascript number const [coin] = tx.splitCoins(tx.gas, [100]); // the address for the transfer is provided as a pure javascript string tx.transferObjects([coin], '0xSomeSuiAddress'); ``` #### Pure values When providing inputs that are not on chain objects, the values must be serialized as [BCS](https://sdk.mystenlabs.com/bcs), which can be done using `tx.pure` eg, `tx.pure.address(address)` or `tx.pure(bcs.vector(bcs.U8).serialize(bytes))`. `tx.pure` can be called as a function that accepts a SerializedBcs object, or as a namespace that contains functions for each of the supported types. ```ts const [coin] = tx.splitCoins(tx.gas, [tx.pure.u64(100)]); const [coin] = tx.splitCoins(tx.gas, [tx.pure(bcs.U64.serialize(100))]); tx.transferObjects([coin], tx.pure.address('0xSomeSuiAddress')); tx.transferObjects([coin], tx.pure(bcs.Address.serialize('0xSomeSuiAddress'))); ``` To pass `vector` or `option` types, you can pass use the corresponding methods on `tx.pure`, use tx.pure as a function with a type argument, or serialize the value before passing it to tx.pure using the bcs sdk: ```ts tx.moveCall({ target: '0x2::foo::bar', arguments: [ // using vector and option methods tx.pure.vector('u8', [1, 2, 3]), tx.pure.option('u8', 1), tx.pure.option('u8', null), // Using pure with type arguments tx.pure('vector<u8>', [1, 2, 3]), tx.pure('option<u8>', 1), tx.pure('option<u8>', null), tx.pure('vector<option<u8>>', [1, null, 2]), // Using bcs.serialize tx.pure(bcs.vector(bcs.U8).serialize([1, 2, 3])), tx.pure(bcs.option(bcs.U8).serialize(1)), tx.pure(bcs.option(bcs.U8).serialize(null)), tx.pure(bcs.vector(bcs.option(bcs.U8)).serialize([1, null, 2])), ], }); ``` #### Object references To use an on chain object as a transaction input, you must pass a reference to that object. This can be done by calling `tx.object` with the object id. Transaction arguments that only accept objects (like `objects` in `transferObjects`) will automatically treat any provided strings as objects ids. For methods like `moveCall` that accept both objects and other types, you must explicitly call `tx.object` to convert the id to an object reference. ```ts // Object IDs can be passed to some methods like (transferObjects) directly tx.transferObjects(['0xSomeObject'], 'OxSomeAddress'); // tx.object can be used anywhere an object is accepted tx.transferObjects([tx.object('0xSomeObject')], 'OxSomeAddress'); tx.moveCall({ target: '0x2::nft::mint', // object IDs must be wrapped in moveCall arguments arguments: [tx.object('0xSomeObject')], }); // tx.object automatically converts the object ID to receiving transaction arguments if the moveCall expects it tx.moveCall({ target: '0xSomeAddress::example::receive_object', // 0xSomeAddress::example::receive_object expects a receiving argument and has a Move definition that looks like this: // public fun receive_object<T: key>(parent_object: &mut ParentObjectType, receiving_object: Receiving<ChildObjectType>) { ... } arguments: [tx.object('0xParentObjectID'), tx.object('0xReceivingObjectID')], }); ``` When building a transaction, Sui expects all objects to be fully resolved, including the object version. The SDK automatically looks up the current version of objects for any provided object reference when building a transaction. If the object reference is used as a receiving argument to a `moveCall`, the object reference is automatically converted to a receiving transaction argument. This greatly simplifies building transactions, but requires additional RPC calls. You can optimize this process by providing a fully resolved object reference instead: ```ts // for owned or immutable objects tx.object(Inputs.ObjectRef({ digest, objectId, version })); // for shared objects tx.object(Inputs.SharedObjectRef({ objectId, initialSharedVersion, mutable })); // for receiving objects tx.object(Inputs.ReceivingRef({ digest, objectId, version })); ``` ##### Object helpers There are a handful of specific object types that can be referenced through helper methods on tx.object: ```ts tx.object.system(), tx.object.clock(), tx.object.random(), tx.object.denyList(), tx.object.option({ type: '0x123::example::Thing', // value can be an Object ID, or any other object reference, or null for `none` value: '0x456', }), ``` #### Transaction results You can also use the result of a command as an argument in a subsequent commands. Each method on the transaction builder returns a reference to the transaction result. ```tsx // split a coin object off of the gas object const [coin] = tx.splitCoins(tx.gas, [100]); // transfer the resulting coin object tx.transferObjects([coin], address); ``` When a command returns multiple results, you can access the result at a specific index either using destructuring, or array indexes. ```tsx // destructuring (preferred, as it gives you logical local names) const [nft1, nft2] = tx.moveCall({ target: '0x2::nft::mint_many' }); tx.transferObjects([nft1, nft2], address); // array indexes const mintMany = tx.moveCall({ target: '0x2::nft::mint_many' }); tx.transferObjects([mintMany[0], mintMany[1]], address); ``` ## Get transaction bytes If you need the transaction bytes, instead of signing or executing the transaction, you can use the `build` method on the transaction builder itself. **Important:** You might need to explicitly call `setSender()` on the transaction to ensure that the `sender` field is populated. This is normally done by the signer before signing the transaction, but will not be done automatically if you’re building the transaction bytes yourself. ```tsx const tx = new Transaction(); // ... add some transactions... await tx.build({ client }); ``` In most cases, building requires your SuiJsonRpcClient to fully resolve input values. If you have transaction bytes, you can also convert them back into a `Transaction` class: ```tsx const bytes = getTransactionBytesFromSomewhere(); const tx = Transaction.from(bytes); ```