How Nautilus Wallet Implements Transaction Building and Fee Calculation
Nautilus Wallet uses a layered, plug-in architecture in src/chains/ergo/transaction/builder.ts to construct Ergo transactions, supporting both native ERG fees and token-based Babel fees through dynamic liquidity selection.
The nautls/nautilus-wallet repository implements a sophisticated transaction building pipeline for the Ergo blockchain. This system handles everything from UTXO selection to dynamic fee calculation, including advanced token-based fee mechanisms. The architecture separates concerns between core builder logic, fee strategies, and high-level transaction factories.
Core Transaction Builder Architecture
The foundation of transaction building resides in src/chains/ergo/transaction/builder.ts, which orchestrates the TransactionBuilder class from the Ergo SDK.
Input Gathering and Context Setup
Every transaction begins by fetching the current blockchain context and available UTXOs. The getContext() helper retrieves inputs and the current block height, which initializes the builder:
const { inputs, currentHeight } = await getContext();
const unsigned = new TransactionBuilder(currentHeight).from(inputs);
This establishes the temporal validity of the transaction and loads the wallet's unspent boxes as potential inputs.
Output Construction and Change Handling
The builder constructs outputs using OutputBuilder, converting token amounts to their undecimalized integer representation:
.to(
new OutputBuilder(
sendingNanoErgs.eq(0) && isBabelFee ? BigInt(MIN_BOX_VALUE) : sendingNanoErgs.toString(),
recipientAddress
).addTokens(/* token list */)
)
.sendChangeTo(safeGetChangeAddress())
The safeGetChangeAddress() function derives a deterministic change address from the wallet's HD key pool, ensuring leftover ERG returns to a controlled address.
Selection and Change Strategies
The setSelectionAndChangeStrategy() function configures UTXO selection algorithms based on wallet type. For Ledger devices, it enforces isolateErgOnChange and limits maxTokensPerChangeBox to 1 to comply with hardware constraints. Standard wallets use higher token limits and sort by creation height.
Fee Calculation Strategies
The setFee function in builder.ts implements dual-path fee logic, handling both native ERG fees and Babel fees (token-based fees).
Native ERG Fees
For standard ERG fees, the process is straightforward:
let feeNanoErgs = undecimalize(fee.value, ERG_DECIMALS);
return builder.payFee(feeNanoErgs.toString());
The undecimalize helper from src/common/bigNumber.ts converts human-readable ERG amounts to nanoErgs (the base unit).
Babel Token Fees
Babel fees allow users to pay transaction fees in tokens rather than ERG. The implementation in setFee handles this complex conversion:
- Fetch liquidity:
fetchBabelBoxesqueries the GraphQL endpoint viagraphQLService.getBoxesto find boxes containing the fee token. - Select optimal box:
selectBestBabelBoxchooses a box with sufficient ERG balance and the best exchange rate. - Calculate ERG equivalent:
getNanoErgsPerTokenRateextracts the conversion rate, andfeeNanoErgs = tokenUnits.multipliedBy(rate).
If no suitable Babel box exists, the system throws: "There is not enough liquidity in the Babel Boxes for the selected fee asset in the selected price range."
Fee Conversion and Plugin Injection
When using Babel fees, the builder extends the transaction with BabelSwapPlugin:
builder.extend(
BabelSwapPlugin(fee.box, { tokenId: fee.tokenId, amount: tokenUnits.toString() })
);
This plugin ensures the network converts the token fee to ERG during transaction validation. The function also handles edge cases where the fee would consume the entire output, adjusting sendingNanoErgs to maintain the minimum box value (MIN_BOX_VALUE).
High-Level Transaction Factories
The src/dapps/wallet-optimization/transactionFactory.ts file provides ergonomic wrappers for common operations. The createP2PTransaction function demonstrates the complete orchestration:
export async function createP2PTransaction({
recipientAddress,
assets,
fee,
walletType
}: { recipientAddress: string; assets: TxAssetAmount[]; fee: FeeSettings; walletType: WalletType }) {
const { inputs, currentHeight } = await getContext();
const isBabelFee = fee.tokenId !== ERG_TOKEN_ID;
const sendingNanoErgs = getSendingNanoErgs(assets);
const unsigned = new TransactionBuilder(currentHeight)
.from(inputs)
.to(
new OutputBuilder(
sendingNanoErgs.eq(0) && isBabelFee ? BigInt(MIN_BOX_VALUE) : sendingNanoErgs.toString(),
recipientAddress
).addTokens(/* token list */)
)
.sendChangeTo(safeGetChangeAddress());
await setFee(unsigned, fee);
setSelectionAndChangeStrategy(unsigned, walletType);
return unsigned.build().toEIP12Object();
}
This factory handles peer-to-peer transfers, automatically managing Babel fee detection, minimum box value requirements, and wallet-specific selection strategies.
Complete Transaction Flow Example
The following example demonstrates building a token transfer with a Babel fee using the high-level factory:
import { createP2PTransaction } from '@/dapps/wallet-optimization/transactionFactory';
import { FeeSettings, WalletType } from '@/types/internal';
import { bn } from '@/common/bigNumber';
import { ERG_TOKEN_ID } from '@/constants/ergo';
// Define assets to send (e.g., 10 SIGMA tokens)
const assets = [
{
asset: { tokenId: '0f2c...abcd', metadata: { decimals: 2 } },
amount: bn(10) // 10.00 tokens
}
];
// Configure Babel fee (pay with the same token)
const fee: FeeSettings = {
tokenId: '0f2c...abcd',
value: bn(0.1), // 0.10 token fee
nanoErgsPerToken: bn(0),
assetInfo: { decimals: 2 }
};
// Build transaction
const unsignedTx = await createP2PTransaction({
recipientAddress: '9hZ7...xyz',
assets,
fee,
walletType: WalletType.Standard
});
// Result is EIP-12 compatible unsigned transaction
console.log(unsignedTx);
In this flow, setFee automatically fetches Babel boxes from src/chains/ergo/babelFees.ts, calculates the ERG equivalent using the liquidity pool rate, and injects the BabelSwapPlugin to handle on-chain conversion.
Summary
- Modular Architecture: Transaction building is split between
src/chains/ergo/transaction/builder.ts(core logic) andsrc/dapps/wallet-optimization/transactionFactory.ts(high-level factories). - Dual Fee Support: The
setFeefunction handles both native ERG fees and Babel token fees by querying liquidity boxes and converting token amounts to ERG equivalents. - Hardware Wallet Compatibility:
setSelectionAndChangeStrategyenforces constraints for Ledger devices (single-token change boxes) while optimizing for standard wallets. - EIP-12 Standard: All transactions are finalized using
.build().toEIP12Object(), ensuring compatibility with Ergo's standard unsigned transaction format.
Frequently Asked Questions
How does Nautilus Wallet handle fee calculation for token transfers?
Nautilus Wallet calculates fees using the setFee function in src/chains/ergo/transaction/builder.ts. For standard transfers, it undecimalizes the ERG amount and calls payFee. For token transfers using Babel fees, it queries available liquidity boxes via fetchBabelBoxes, selects the optimal rate with selectBestBabelBox, and converts the token fee amount to its ERG equivalent using getNanoErgsPerTokenRate.
What is the difference between standard and Ledger wallet transaction building?
The setSelectionAndChangeStrategy function configures the TransactionBuilder differently based on the WalletType. For Ledger devices, it sets isolateErgOnChange to true and limits maxTokensPerChangeBox to 1, ensuring hardware constraints are respected. Standard wallets use higher token limits per change box and sort inputs by creation height for optimal selection.
How are Babel fees converted to ERG equivalents?
When a Babel fee is detected (token ID differs from ERG_TOKEN_ID), the system fetches Babel boxes containing that token from the GraphQL endpoint. It selects the box with the best exchange rate and sufficient ERG balance using selectBestBabelBox. The conversion applies the formula feeNanoErgs = tokenUnits * getNanoErgsPerTokenRate(selectedBox), then extends the transaction with BabelSwapPlugin to execute the on-chain swap.
Where is the transaction builder logic located in the Nautilus Wallet repository?
The core transaction building logic resides in src/chains/ergo/transaction/builder.ts, which contains the setFee, safeGetChangeAddress, and setSelectionAndChangeStrategy functions. Babel fee-specific utilities are in src/chains/ergo/babelFees.ts. High-level transaction factories that orchestrate the complete flow are located in src/dapps/wallet-optimization/transactionFactory.ts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →