Introducing the Transaction Builder

Introducing the Transaction Builder

Today we're excited to announce a new feature in our TypeScript SDK, the Transaction Builder; a method to granularly control every aspect of the transaction process, by specifying overrides and config for each step, like so:

// Prepare a transaction, but don't send it
const tx = await contract.erc721.claim.prepare(1);

// Some example use cases for the transaction
const gasCost = await tx.estimateGasCost(); // Estimate the gas cost
const simulatedTx = await tx.simulate(); // Simulate the transaction
const signedTx = await tx.sign(); // Sign the transaction for later use

Let's dive into it!

Background

When using the TypeScript SDK, the preprocessing of information required to initiate a new transaction on the blockchain is performed out of the box by default.

When you use the contract.call function, or initiate a transaction with any of the extension interfaces, every step of the transaction process occurs. From preparing and building the transaction, all the way to waiting until that transaction has been mined and the data is available to be read from the blockchain.

For most use cases, this is a great user experience, as you can await the result of your transaction and get the data back in one line of code:

const txResult = await contract.erc721.mint({
  name: "Cool NFT #1",
});

Under the hood, this function performs the following:

  1. Uploads and pins the metadata to IPFS.
  2. Builds the mintTo transaction on the smart contract, providing the connected wallet address and the URI from the previous step as arguments.
  3. Signs the transaction from the connected wallet.
  4. Submits the transaction to the blockchain, and waits for it to be executed.
  5. Reads the events emitted by the smart contract, gets the data of the minted NFT, and returns it in an object for you to use.

The Transaction Builder and .prepare

As part of version 3.10 of our TypeScript SDK, we introduced the new Transaction Builder enabling the .prepare() syntax on all transactions in the SDK.

This enables you to configure every step of the process from building the transaction to getting the data back from the mined transaction.

This allows you to perform powerful actions on a transaction before sending it to the mempool to be included on the blockchain, let's look at an example of a Claim operation on an NFT Drop contract.

First, we "prepare" the transaction with the arguments we want to use:

// Prepare a transaction where we claim one NFT from the drop
const tx = await contract.erc721.claim.prepare(1);

Now we're ready to configure the transaction. Let's look at some examples.

Estimate Gas Costs

Before the user claims, we could estimate what amount of gas is required to perform this operation. With this information, we could:

  • Show the user how much we estimate the transaction to cost on the UI.
  • Provide the user's wallet with some funds to cover the cost of their transaction.
  • Warn the user if a gas cost is currently high.
const gasCost = await tx.estimateGasCost();

Simulate The Transaction

To give the user peace of mind when interacting with your dApp, you could provide an understanding of what will happen after the transaction occurs, enabling:

  • Information on the "before and after" state effect of the prepared transaction.
  • Proof that it is not a malicious transaction.
const simulatedTx = await tx.simulate();

Gasless Configuration

For a powerful user experience, using this new prepare method you can specify which individual transactions you want to cover the gas fees for.

  • Cover the gas fees when specific criteria is met (e.g. this user owns an NFT).
  • Cover the gas fees only for specific functions (e.g. only for minting).
tx.setGaslessOptions({
  openzeppelin: {
    relayerUrl: "xxx",
    useEOAForwarder: true,
    relayerForwarderAddress: "0x...",
  },
});

Sign it for later

You could even have user's sign the message saying they agree to this transaction, and save these signatures to be used to execute the transaction at a later time.

  • Save signed transactions in an off-chain database.
  • Perform the transactions action after X amount of users have signed.
const signedTx = await tx.sign();

Override Anything!

Sometimes you want full control of the transaction behaviour. The prepare method provides the ability to override absolutely anything about the transaction:

tx.updateOverrides({
  accessList: [], // The AccessList to include; only available for EIP-2930 and EIP-1559 transactions.
  blockTag: "latest", // A BlockTag specifies a specific block location in the Blockchain.
  ccipReadEnabled: false, // https://eips.ethereum.org/EIPS/eip-3668#use-of-ccip-read-for-transactions
  customData: {}, // The transaction data.
  from: "0x...", // The address this transaction is from.
  gasLimit: 100000, // The maximum amount of gas this transaction is permitted to use.
  gasPrice: 100000, // The price (in wei) per unit of gas this transaction will pay.
  maxFeePerGas: 100000, // The maximum price (in wei) per unit of gas this transaction will pay
  maxPriorityFeePerGas: 0, // The price (in wei) per unit of gas this transaction will allow in addition to the block's base fee
  nonce: 0, // The nonce used as part of the proof-of-work to mine this block.
  type: 0, // The EIP-2718 type of this transaction envelope, or undefined for to use the network default
  value: ethers.utils.parseEther("0.1"), // send 0.1 ether with the contract call
});

Wrapping Up

That's it! The transaction builder is a powerful way to configure exactly how you want your transactions to perform before sending them to the blockchain!

We built this feature based on your feedback that you wanted more fine-grained control over the transactions. So a big thank you to our community!

If you have any questions, jump into the thirdweb Discord and join 32,000+ other builders! Please let us know if you have any feature requests or guide requests here.