TransactionBlock.build()

Summary

Calling TransactionBlock.build() on a programmable transaction block is a common method you call before testing or executing a transaction. The method makes additional requests to a Sui Full Node in order to gather the information it needs to build the programmable transaction block. This is why the method takes a SuiClient as an argument. You can also pass an unbuilt TransactionBlock into the Shinami or Mysten SDK's devInspectTransactionBlock method, in which case it calls TransactionBlock.build() on the block - generating Full Node requests from the .build() call - before sending the sui_devInspectTransactionBlock request to the Sui network with the now-built transaction block.

We made the examples in this guide so you can better:

  1. understand the TransactionBlock build process
  2. estimate your Shinami usage and billing (these requests, like all requests you make to our Node Service, count towards your Node access key's QPS limit and your daily request total).

Note: the simple examples below show 4-5 Node Service requests generated from TransactionBlock.build() plus a subsequent request with the built transaction (e.g. sui_devInspectTransactionBlock). However, the count of requests can increase above 5 for larger and different transaction blocks. You can run a test with your most common transactions and observe your own results.

The TransactionBlock.build() code

In order to build the transaction, the TransactionBlock build method uses the Full Node client you pass it to make a variety of JSON-RPC requests to get the information it needs to build the transaction. This can include, but is not necessarily limited to, fetching the current reference gas price, retrieving the structured representation of any Move calls, fetching information about any objects involved in the transaction, and checking the available gas coins of the address paying for the gas (sender or otherwise).

The exact requests your build() calls produce, and the count of each request type, will vary based on the contents of your transaction blocks.

Initial code for all examples

We'll be constructing programmable transactions using the Sui TypeScript SDK provided by Mysten. We'll also use the Shinami Clients SDK to make JSON-RPC calls to Shinami's Node Service. All code examples in the sections below start after the following code.

Replace all instances of {{name}} with the actual value for that name

import { createSuiClient} from "@shinami/clients/sui";
import { TransactionBlock } from "@mysten/sui.js/transactions";

const GAS_BUDGET = 10_000_000;
const SENDER_ADDRESS = "{{senderAddress}}";
const NODE_TESTNET_ACCESS_KEY = "{{nodeAccessKey}}";

const shinamiNodeClient = createSuiClient(NODE_TESTNET_ACCESS_KEY);

// Step 1: create and populate a TransactionBlock
let txb = new TransactionBlock();

// This is for a Move module I deployed to Testnet
txb.moveCall({
    target: "0xfa0e78030bd16672174c2d6cc4cd5d1d1423d03c28a74909b2a148eda8bcca16::clock::access",
    arguments: [txb.object('0x6')]
  });

dryRunTransactionBlock - building a TransactionData

Set sender and gas info and call TransactionBlock.build()

sui_dryRunTransactionBlock takes a TransactionData as a Base64 encoded string, which includes everything needed to run a transaction (including the sender address and a gas object). When you build the transaction block, you pass in a Sui Full Node client (here a Shinami Node Service client):

// Step 2: Set sender and gas info
txb.setSender(SENDER_ADDRESS);
txb.setGasBudget(GAS_BUDGET);
txb.setGasOwner(SENDER_ADDRESS);

// Step 3: call TransactionBlock.build() and convert the result to a Base64 string
const txBytes = await txb.build({ client: shinamiNodeClient, onlyTransactionKind: false});
const txBytesBase64 = btoa(
    txBytes
        .reduce((data, byte) => data + String.fromCharCode(byte), '')
);

In order to build the transaction, the txb.build() call above made the following requests using the client I provided it:

"sui_getNormalizedMoveFunction": 1,
"sui_getProtocolConfig": 1,
"sui_multiGetObjects": 1,
"suix_getCoins": 1,
"suix_getReferenceGasPrice": 1

I can see this in my Node Service Metrics in my Shinami Dashboard. The hover state of the stack bar graph shows me I had 5 total requests, and the individual section I was hovering over shows me that 1 of those requests was a suix_getCoins request.

Your results may be different and depend on the type and amount of operations and objects in your transaction block.

Call dryRunTransactionBlock

When I complete the above code block with the final step of running the built TransactionData through sui_dryRunTransactionBlock:

// Step 4: call dryRunTransactionBlock with the Base64 TransactionData string
await shinamiNodeClient.dryRunTransactionBlock({
    transactionBlock: txBytesBase64
});

I get only one new Node Service request: for sui_dryRunTransactionBlock itself (I'm hovering over the short bar on the right side of the graph, and the five requests in the taller bar to the left is what we just saw above with the txb.build() call):

The reason there are no additional requests is because the Mysten TypeScript SDK (which our TypeScript SDK wraps) simply makes a sui_dryRunTransactionBlock request to the Full Node without making additional calls .

devInspectTransactionBlock - building a TransactionKind

Call devInspectTransactionBlock with an unbuilt TransactionBlock

devInspectTransactionBlock can accept a TransactionBlock that's not built. If you do this, it will call TransactionBlock.build() with the input block , which will lead to additional Node Service requests made from the build process.

// Step 2. Call devInspectTransactionBlock using the TransactionBlock that's
//  populated with our Move call but has not been built()
await shinamiNodeClient.devInspectTransactionBlock({
    sender: SENDER_ADDRESS,
    transactionBlock: txb
});

In this case, I see the above code produced four requests to Shinami's Node service:

"sui_getNormalizedMoveFunction": 1,
"sui_getProtocolConfig": 1,
"sui_multiGetObjects": 1,
"sui_devInspectTransactionBlock": 1

Which I can see in my Node Service Metrics in my Shinami Dashboard by hovering over the parts of the stack bar on the Requests graph.

Your results may be different and depend on the type and amount of operations and objects in your transaction block.

Call TransactionBlock.build() without sender and gas data

Let's replace the above with first building the TransactionBlock and then calling devInspectTransactionBlock. First, we'll build the TransactionBlock (in this case a TransactionKind, without sender and gas information).

// Step 2: call TransactionBlock.build() and convert the result to a Base64 string
//  Here, we didn't set sender and gas data so we're setting `onlyTransactionKind: true`
const txBytes = await txb.build({ client: shinamiNodeClient, onlyTransactionKind: true});
const gaslessPayloadBase64 = btoa(
    txBytes
        .reduce((data, byte) => data + String.fromCharCode(byte), '')
);

From the above code, I produced three Node Service requests:

"sui_getNormalizedMoveFunction": 1,
"sui_getProtocolConfig": 1,
"sui_multiGetObjects": 1

I can see this in my Node Service Metrics in my Shinami Dashboard. The hover state of the stack bar graph shows me I had 3 total requests, and the individual section I was hovering over shows me that 1 of those requests was a suix_multiGetObjects request.

Your results may be different and depend on the type and amount of operations and objects in your transaction block.

Call devInspectTransactionBlock with a pre-built TransactionBlock

When I take the final step of running the built TransactionKind through sui_devInspectTransactionBlock

// Step 3. call sui_devInspectTransactionBlock with the Base64 TransactionKind string
await shinamiNodeClient.devInspectTransactionBlock({
    sender: SENDER_ADDRESS,
    transactionBlock: gaslessPayloadBase64
});

I see I made one new request to Shinami's Node Service - the sui_devInspectTransactionBlock request itself - which I can see in my Node Service Metrics in my Shinami Dashboard.

In other words, calling TransactionBlock.build() and then calling sui_devInspectTransactionBlock produced the same four requests as calling sui_devInspectTransactionBlock with an unbuilt transaction block (because that also calls TransactionBlock.build() before makings a sui_devInspectTransactionBlock to the Full Node).

A note on our buildGaslessTransactionBytes SDK helper function

Our SDK has a helper function which is a shorthand way for doing Steps 1 and 2 in one request.

// You could replace Steps 1 and 2 with:
let gaslessPayloadBase64 = await buildGaslessTransactionBytes({
    sui: shinamiNodeClient,
    build: async (txb) => {
        txb.moveCall({
            target: "0xfa0e78030bd16672174c2d6cc4cd5d1d1423d03c28a74909b2a148eda8bcca16::clock::access",
            arguments: [txb.object('0x6')]
        });
    }
});

Behind the scenes it does the same things we did in Steps 1 and 2:

  1. Populate the transaction block
  2. Call TransactionBlock.build() with onlyTransactionKind: true
  3. Convert the result to a Base64 string

If you've followed along with the rest of the tutorial, you won't be surprised by the result of running this code:

"sui_getNormalizedMoveFunction": 1,
"sui_getProtocolConfig": 1,
"sui_multiGetObjects": 1

Your results may be different and depend on the type and amount of operations and objects in your transaction block.