TransactionBuilder pattern
The most useful single piece of the post-fv toolkit is the TransactionBuilder exposed by @gen3labs/futurepass-transact (and its identical @futureverse/transact predecessor). It chains four orthogonal concerns into one fluent expression:
- What to do — a domain call (mint an NFT, transfer a token, batch several calls).
- Who pays gas — the user’s EOA, or their FuturePass.
- What asset gas is paid in — XRP (default), or any other asset via
FeeProxy. - How to sign and watch — a single
signAndSendwithonSign/onSendlifecycle hooks.
This page is the conceptual reference. The code is in the per-package READMEs (gen3labs, futureverse).
The four canonical shapes
1. Domain call, EOA pays gas in XRP
The simplest path. Build, sign, send.
const builder = await TransactionBuilder
.nft(trnApi, signer, userSession.eoa, COLLECTION_ID)
.mint({ quantity: 1, walletAddress: userSession.futurepass });
await builder.signAndSend({ onSign, onSend });2. Domain call, EOA pays gas in a non-native asset (FeeProxy)
addFeeProxy({ assetId, slippage }) wraps the inner call in feeProxy.callWithFeePreferences, swapping the chosen asset for XRP at execution time.
const builder = await TransactionBuilder
.transfer({ destinationAddress: recipient, amount: 1 })
.addFeeProxy({ assetId: ROOT_TOKEN_ID, slippage: 5 });
await builder.signAndSend({ onSign, onSend });slippage is a percentage — guards against price moves between the time you sign and the time the fee swap executes.
3. Domain call, FuturePass pays gas
addFuturePass(futurepassAddress) wraps the inner call in futurepass.proxyExtrinsic. Useful when the user holds funds on the FuturePass account rather than the EOA.
const builder = await TransactionBuilder
.nft(trnApi, signer, userSession.eoa, COLLECTION_ID)
.mint({ quantity: 1, walletAddress: userSession.futurepass })
.addFuturePass(userSession.futurepass);
await builder.signAndSend({ onSign, onSend });4. Batch several calls, FuturePass pays in a chosen asset
This is the everything-bagel: multiple calls, atomic batch, FuturePass proxy, FeeProxy on top.
const tx1 = trnApi.tx.nft.mint(COLLECTION, 1, eoa);
const tx2 = trnApi.tx.nft.mint(COLLECTION, 5, fp);
const tx3 = trnApi.tx.assetsExt.transfer(1, fp, 1, true);
const tx4 = trnApi.tx.assetsExt.transfer(2, fp, 10, true);
const builder = await TransactionBuilder
.batch(trnApi, signer, userSession.eoa)
.addExtrinsic(tx1).addExtrinsic(tx2).addExtrinsic(tx3).addExtrinsic(tx4)
.batchAll()
.addFuturePassAndFeeProxy({
futurePass: userSession.futurepass,
assetId: ROOT_TOKEN_ID,
slippage: 5,
});
await builder.signAndSend({ onSign, onSend });batchAll() produces an atomic batch (all-or-nothing); batch() is non-atomic. addFuturePassAndFeeProxy(...) is a convenience that does what addFuturePass(...) and addFeeProxy(...) would do together — the wrapping order matters and this helper picks the correct nesting.
Wrapping order (so you can debug it)
The four wrappers nest in this order:
┌─ futurepass.proxyExtrinsic ─────────────────────────┐
│ ┌─ feeProxy.callWithFeePreferences ─────────────┐ │
│ │ ┌─ utility.batchAll ──────────────────────┐ │ │
│ │ │ nft.mint(...) │ │ │
│ │ │ nft.mint(...) │ │ │
│ │ │ assetsExt.transfer(...) │ │ │
│ │ │ assetsExt.transfer(...) │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘Reading inside-out: build the domain calls, batch them, swap an asset for XRP to pay fees, dispatch as the FuturePass.
This is also the order the chain itself decodes the call envelope when you read it out of system.events — knowing it makes debugging an extrinsic that lands "in block but failed" much faster.
Lifecycle hooks
signAndSend({ onSign, onSend }) returns a result describing the included extrinsic. Hooks fire as you'd expect:
| Hook | When it fires | What you typically do |
|---|---|---|
onSign | After the user’s wallet signs the message but before it’s submitted to the node. | Show a "submitting…" spinner. |
onSend | After the node accepts the extrinsic into the pool. | Update UI to "in flight". |
| (return value) | After finalization. | Read events, surface success / error to the user. |
The return value carries the events emitted by the runtime — that’s where you read the nft.Mint / feeProxy.CallSwapped / futurepass.ProxyExecuted events that confirm each layer of the wrap actually fired.
Pre-flight checks (before users hit "Mint")
- FuturePass balance for the fee asset. If the FuturePass is paying via
FeeProxy, it needs both the asset and enough liquidity in the asset/XRP DEX pool — otherwisefeeProxy.callWithFeePreferencesaborts. Querydex.getAmountInto estimate. - Slippage. A slippage of
5(5%) is conservative; tight pools (small TVL onassetId/XRP) can move enough between sign and submit to busted a tighter setting. - Collection ownership for
nft.mint. If you’re minting on a collection the EOA does not own and haven’t added as an authorised minter, the call fails — checknft.collectionInfo(collectionId)first.
When to skip the builder and go raw
The builder’s value is the wrapping-order discipline. You don’t need it when:
- You're sending a single, vanilla extrinsic with EOA paying XRP —
api.tx.<pallet>.<call>(...).signAndSend(signer)is two lines. - You're calling an EVM precompile from Solidity / viem — the wrapping concerns don't exist there.
Related
- FuturePass — concept-level overview of the proxy account model.
futurepasspallet — runtime metadata: every call, event, error, storage item the builder ultimately produces.feeProxypallet — same for the fee-swapping layer.utilitypallet —batch,batchAll,forceBatch.@gen3labs/futurepass-transactand@futureverse/transact— package pages with the full upstream READMEs and examples.
@gen3labs/futurepass-transact and @futureverse/transact; pallet-side behaviour cross-checked against the live runtime metadata under /api/root/.