Skip to main content

Transactions

abidock_mvx handles EGLD, ESDT tokens, NFTs, and multi-transfers.

Transaction Types

TypeDescription
EGLD TransferNative token transfers
ESDT TransferFungible token transfers
NFT TransferNon-fungible token transfers
Multi TransferMultiple tokens in one transaction
Smart ContractContract interactions

Basic Transaction Structure

Every transaction requires typed parameters:

import 'dart:typed_data';

final senderAddress = Address.fromBech32('erd1...sender...');
final receiverAddress = Address.fromBech32('erd1...receiver...');

final tx = Transaction(
sender: senderAddress, // Who sends (Address)
receiver: receiverAddress, // Who receives (Address)
value: Balance.fromEgld(0.1), // EGLD amount (Balance)
nonce: Nonce(5), // Sender's next nonce (Nonce)
gasLimit: GasLimit(50000), // Maximum gas (GasLimit)
gasPrice: GasPrice(1000000000), // Gas price (GasPrice)
chainId: ChainId('D'), // Network (ChainId)
version: TransactionVersion(1), // Transaction version
data: Uint8List(0), // Optional data field (Uint8List)
);

Transaction Flow

┌─────────────┐
│ 1. Create │ Build transaction
└──────┬──────┘

┌──────▼──────┐
│ 2. Sign │ Cryptographic signature
└──────┬──────┘

┌──────▼──────┐
│ 3. Send │ Submit to network
└──────┬──────┘

┌──────▼──────┐
│ 4. Wait │ Monitor status
└──────┬──────┘

┌──────▼──────┐
│ 5. Verify │ Check results
└─────────────┘

Quick Example

import 'package:abidock_mvx/abidock_mvx.dart';
import 'dart:typed_data';

void main() async {
final provider = GatewayNetworkProvider.devnet();
final account = await Account.fromMnemonic('your mnemonic...');

// Get network config and account info
final config = await provider.getNetworkConfig();
final accountInfo = await provider.getAccount(account.address);

// Create EGLD transfer
final tx = Transaction(
sender: account.address,
receiver: Address.fromBech32('erd1...recipient...'),
value: Balance.fromEgld(0.1),
nonce: accountInfo.nonce,
gasLimit: GasLimit(50000),
gasPrice: GasPrice(1000000000),
chainId: ChainId(config.chainId),
version: TransactionVersion(1),
data: Uint8List(0),
);

// Sign
final signature = await account.signTransaction(tx);
final signed = tx.copyWith(
newSignature: Signature.fromUint8List(signature),
);

// Send
final hash = await provider.sendTransaction(signed);
print('Transaction: $hash');

// Wait for completion
final watcher = TransactionWatcher(networkProvider: provider);
final result = await watcher.awaitCompleted(hash);

print('Status: ${result.status.status}');
}

Gas Configuration

Every gas limit the factories compute has two parts:

gasLimit = dataMovementGas + executionGas
dataMovementGas = minGasLimit + gasLimitPerByte * data.length

minGasLimit is 50,000 and gasLimitPerByte is 1,500 by default; both come from /network/config (config.minGasLimit, config.gasPerDataByte). The execution term is what the endpoint itself costs.

Standard Gas Values

OperationExecution gasTotal (with data-movement term)
EGLD Transfer (no data)050,000
ESDT Transfer300,000350,000 + 1,500 x data bytes
NFT / SFT Transfer1,000,0001,050,000 + 1,500 x data bytes
Multi Transfer (N tokens)800,000 + 200,000 x N850,000 + 200,000 x N + 1,500 x data bytes
Contract callEndpoint-specificEndpoint cost + data-movement term

Gas Price

Default gas price is usually 1000000000. Get from network config -- note that minGasPrice is a plain int, so wrap it before handing it to a transaction:

final config = await provider.getNetworkConfig();
final gasPrice = GasPrice(config.minGasPrice);

Transaction Status

TransactionStatus wraps the raw status string the node, Gateway or Proxy reports. Several spellings mean the same thing, so prefer the predicates over string comparison:

StatusMeaningPredicate
pendingIn mempool, not yet processedisPending
receivedAccepted into the mempoolisPending
executedExecuted in a blockisExecuted / isSuccessful
successExecuted successfullyisExecuted / isSuccessful
successfulExecuted successfully (Gateway spelling)isExecuted / isSuccessful
failExecution failed (short spelling)isFailed
failedExecution failedisFailed
unsuccessfulExecution failed (Gateway spelling)isFailed
invalidRejected before executionisInvalid, also isFailed
not-executable-in-blockProposed in a block but absent from its execution resultisNotExecutableInBlock
reward-revertedReward transaction was reverted-
unknownNo observer could report a status-

Two aggregate predicates matter when you are waiting on a transaction:

  • isCompleted -- executed with an outcome, success or failure.
  • isFinal -- broader: isCompleted plus not-executable-in-block, i.e. the status will not change again. This is what TransactionWatcher.awaitCompleted waits for.
final watcher = TransactionWatcher(networkProvider: provider);
final tx = await watcher.awaitCompleted(hash);

// Use convenience getters for status checks
if (tx.isSuccessful) {
print('Transaction succeeded!');
} else if (tx.hasFailed) {
print('Transaction failed: ${tx.status.status}');
// Check logs for error details
if (tx.logs != null) {
for (final event in tx.logs!.events) {
if (event.identifier == 'signalError') {
print('Error: ${event.topics}');
}
}
}
} else {
print('Status: ${tx.status.status}');
}

Error Handling

try {
// Validate balance before sending (manual check)
final account = await provider.getAccount(tx.sender);
final totalCost = tx.value.value + tx.gasLimit.toBigInt * tx.gasPrice.toBigInt;

if (account.balance.value < totalCost) {
print('Not enough balance for transaction + gas');
return;
}

// Validate nonce (manual check)
if (tx.nonce != account.nonce) {
print('Invalid nonce: expected ${account.nonce.value}, got ${tx.nonce.value}');
return;
}

final hash = await provider.sendTransaction(signedTx);
// ...
} on NetworkException catch (e) {
print('Network error: ${e.message}');
if (e.statusCode == 404) {
print('Transaction not found');
}
} on TransactionException catch (e) {
print('Transaction error: ${e.message}');
}

Next Steps