← Code knowledge base
Reviewed Flutter implementation · 2026-10-28

exchange_models.dart

mobile_flutter/lib/features/wallets/domain/exchange_models.dart

Immutable source snapshot based on revision ce45c9c3bbde. Original line numbers are retained. This is implementation evidence; check the public API catalogue before adapting it.

The mobile /api/v1/mobile routes belong to the example backend. Company credentials stay on that backend. Some sections outside the documented product scope may be omitted.

Lines 1–386

import 'exchange_fees_and_limits.dart';export 'exchange_fees_and_limits.dart'; class BoomFiExchangeOverview {  const BoomFiExchangeOverview({    required this.accountId,    required this.accountName,    required this.accountEnabled,    required this.accountState,    required this.balances,    required this.settlementAccounts,    required this.subAccounts,    required this.fiatFundingCurrencies,    this.feesAndLimits,    this.fiatFundingFeesAndLimits = const {},    this.deposits = const [],    this.payments = const [],    this.restrictedTradingCurrencies = const [],  });   factory BoomFiExchangeOverview.fromJson(Map<String, dynamic> json) {    final account = _map(_value(json, 'account', 'Account'));    final balanceEnvelope = _map(_value(json, 'balances', 'Balances'));    return BoomFiExchangeOverview(      feesAndLimits: _fees(account),      fiatFundingFeesAndLimits: _map(json['fiat_funding_fees_and_limits'] ?? json['fiatFundingFeesAndLimits'] ?? json['FiatFundingFeesAndLimits'])          .map((key, value) => MapEntry(key.toUpperCase(), BoomFiFeesAndLimits.fromJson(_map(value)))),      restrictedTradingCurrencies: _list(_value(json, 'restricted_trading_currencies', 'RestrictedTradingCurrencies') ?? json['restrictedTradingCurrencies']).map((v) => v.toString().trim().toUpperCase()).toList(),      payments: _list(_value(json, 'payments', 'Payments')).map((item) => BoomFiDeposit.fromJson(_map(item))).toList(),      deposits: _list(_value(json, 'deposits', 'Deposits'))          .map((item) => BoomFiDeposit.fromJson(_map(item)))          .toList(),      accountId: _integer(_value(account, 'id', 'Id')),      accountName: _text(_value(account, 'name', 'Name')),      accountEnabled: _boolean(_value(account, 'enabled', 'Enabled')),      accountState: _text(_value(account, 'state', 'State')),      balances: _list(_value(balanceEnvelope, 'balances', 'Balances'))          .map((item) => BoomFiExchangeBalance.fromJson(_map(item)))          .where((item) => item.currency.isNotEmpty)          .toList(),      settlementAccounts:          _list(_value(json, 'settlement_accounts', 'SettlementAccounts'))              .map(_map)              .toList(),      subAccounts: _list(_value(account, 'sub_accounts', 'SubAccounts'))          .map(_map)          .toList(),      fiatFundingCurrencies: _list(              _value(json, 'fiat_funding_currencies', 'FiatFundingCurrencies'))          .map((item) => item.toString().trim().toUpperCase())          .where((item) => item.isNotEmpty)          .toList(),    );  }   final BoomFiFeesAndLimits? feesAndLimits;  final Map<String, BoomFiFeesAndLimits> fiatFundingFeesAndLimits;   BoomFiFeesAndLimits? feesFor(BoomFiExchangeBalance balance) {    if (balance.accountId <= 0) return null;    if (balance.feesAndLimits != null) return balance.feesAndLimits;    if (balance.accountId == accountId) return feesAndLimits;    BoomFiFeesAndLimits? find(List<Map<String, dynamic>> accounts) {      for (final account in accounts) {        final chain = _map(_value(account, 'chain', 'Chain'));        final chainId = _integer(_value(account, 'chain_id', 'ChainId') ??            _value(chain, 'id', 'Id'));        if (_integer(_value(account, 'id', 'Id')) == balance.accountId &&            chainId == balance.chainId) {          return _fees(account);        }        final nested = find(_list(_value(account, 'sub_accounts', 'SubAccounts')).map(_map).toList());        if (nested != null) return nested;      }      return null;    }    return find(subAccounts);  }   final List<String> restrictedTradingCurrencies;  bool canTrade(String currency) => !restrictedTradingCurrencies.contains(currency.trim().toUpperCase());   final List<BoomFiDeposit> deposits, payments;  final int accountId;  final String accountName;  final bool accountEnabled;  final String accountState;  final List<BoomFiExchangeBalance> balances;  final List<Map<String, dynamic>> settlementAccounts;  final List<Map<String, dynamic>> subAccounts;  final List<String> fiatFundingCurrencies;   /// The network rows are routes into the same currency balance. Never sum  /// those repeated amounts. Keep the original rows for account/route selection.  List<BoomFiExchangeBalance> get displayBalances {    final currencies = <String, BoomFiExchangeBalance>{};    for (final balance in balances) {      final currency = balance.currency.trim().toUpperCase();      if (currency.isEmpty) continue;      final current = currencies[currency];      // Account discovery can add empty route placeholders after provider rows.      if (current == null || (current.amount == 0 && balance.amount > 0)) {        currencies[currency] = balance;      }    }    return currencies.values.toList(growable: false);  }   bool get hasProviderData =>      accountId > 0 ||      balances.isNotEmpty ||      settlementAccounts.isNotEmpty ||      subAccounts.isNotEmpty;   bool get isReady =>      hasProviderData &&      (accountEnabled ||          const {'ready', 'active', 'enabled'}              .contains(accountState.trim().toLowerCase()) ||          balances.isNotEmpty);} class BoomFiExchangeBalance {  const BoomFiExchangeBalance({    required this.accountId,    required this.currency,    required this.amount,    required this.pendingAmount,    required this.chainId,    required this.chainName,    required this.tokenAddress,    this.feesAndLimits,  });   factory BoomFiExchangeBalance.fromJson(Map<String, dynamic> json) {    final chain = _map(_value(json, 'chain', 'Chain'));    return BoomFiExchangeBalance(      feesAndLimits: _fees(json),      accountId: _integer(_value(json, 'account_id', 'AccountId')),      currency: _text(_value(json, 'currency', 'Currency')).toUpperCase(),      amount: _decimal(_value(json, 'amount', 'Amount')),      pendingAmount: _decimal(_value(json, 'pending_amount', 'PendingAmount')),      chainId: _integer(_value(chain, 'id', 'Id')),      chainName: _text(_value(chain, 'name', 'Name')),      tokenAddress: _text(_value(json, 'token_address', 'TokenAddress')),    );  }   final int accountId;  final String currency;  final double amount;  final double pendingAmount;  final int chainId;  final String chainName;  final String tokenAddress;  final BoomFiFeesAndLimits? feesAndLimits;   bool get isCrypto => chainId != 0;  String get key => '$accountId:$currency:$chainId';} class BoomFiTransfer {  const BoomFiTransfer({    required this.id,    required this.status,    required this.direction,    required this.cryptoCurrency,    required this.cryptoAmount,    required this.fiatCurrency,    required this.quoteDecisionStatus,    required this.completionMessage,    this.details = const {},  });   factory BoomFiTransfer.fromJson(Map<String, dynamic> json) => BoomFiTransfer(        details: Map<String, dynamic>.unmodifiable(json),        id: _integer(_value(json, 'id', 'Id')),        status: _text(_value(json, 'status', 'Status')),        direction:            _text(_value(json, 'transferDirection', 'TransferDirection')),        cryptoCurrency: _text(_value(json, 'cryptoCurrency', 'CryptoCurrency')),        cryptoAmount: _text(_value(json, 'cryptoAmount', 'CryptoAmount')),        fiatCurrency: _text(_value(json, 'fiatCurrency', 'FiatCurrency')),        quoteDecisionStatus:            _text(_value(json, 'quoteDecisionStatus', 'QuoteDecisionStatus')),        completionMessage:            _text(_value(json, 'completionMessage', 'CompletionMessage')),      );   final int id;  final String status;  final String direction;  final String cryptoCurrency;  final String cryptoAmount;  final String fiatCurrency;  final String quoteDecisionStatus;  final String completionMessage;  final Map<String, dynamic> details;  String detail(String key) =>      (details[key] ?? details['${key[0].toUpperCase()}${key.substring(1)}'])          ?.toString() ??      '';  bool get canKeepFunds =>      needsFundsDecision && direction == 'interlace_to_equals';  bool get isTerminal => const {        'completed',        'direct_exchange_completed',        'returned',        'cancelled',        'canceled',        'failed',        'funds_held'      }.contains(status);   String get proceedsCurrency => direction == 'equals_to_interlace' ? cryptoCurrency : fiatCurrency;  bool get hasVerifiedProceeds => const {    'payout_ready', 'payout_submitting', 'payout_submitted', 'payout_failed',    'payout_submission_unknown', 'completed', 'withdrawal_ready',    'withdrawal_submitting', 'direct_exchange_completed'  }.contains(status);  String get proceedsLabel => hasVerifiedProceeds ? 'Settled proceeds' : 'Expected proceeds';   bool get isBankExchange => direction == 'exchange_to_bank';  bool get deliversToCard => direction == 'exchange_to_crypto' && detail('destinationCryptoAddress').isNotEmpty;  bool get conversionSettled => detail('fiatSettledAt').isNotEmpty ||      const {'completed', 'settled', 'success', 'succeeded'}.contains(detail('boomFiSettlementStatus').toLowerCase());  bool get awaitingSettlementDetails => isBankExchange && conversionSettled &&      status == 'exchange_submitted' && detail('toAmount').isEmpty;  // Equals funding marks failures before submission as failed; uncertain  // submissions use source_transfer_review_required and must remain visible.  bool get failedBeforeFunding => direction == 'equals_to_interlace' &&      status == 'failed' && detail('sourceTransferId').isEmpty &&      detail('boomFiOrderId').isEmpty && !conversionSettled;  bool get showInProgress {    if (failedBeforeFunding) return false;    if (const {'completed', 'direct_exchange_completed', 'settled', 'returned',      'cancelled', 'canceled', 'funds_held', 'exchange_quoted', 'direct_exchange_quoted'}.contains(status)) { return false; }    // A quote that failed before acceptance never moved money. Retain failures    // after submission/settlement so held funds remain visible.    if (isBankExchange && status == 'exchange_failed' &&        detail('boomFiOrderId').isEmpty && !conversionSettled) { return false; }    return true;  }  String get flowLabel => switch (direction) {    'exchange_to_bank' => '$cryptoCurrency → $fiatCurrency → Bank account',    'exchange_to_crypto' => '$cryptoCurrency → $fiatCurrency · ${deliversToCard ? 'Crypto card account' : 'Exchange balance'}',    'equals_to_interlace' => 'Fiat account → Crypto card',    'interlace_to_equals' => 'Crypto card → Fiat account',    _ => 'Exchange transfer',  };  List<String> get progressLabels => switch (direction) {    'exchange_to_bank' => ['Exchange\nbalance', 'Convert to\n$fiatCurrency', 'Withdraw to\nbank'],    'exchange_to_crypto' => ['Exchange\nbalance', 'Convert to\n$fiatCurrency', deliversToCard ? 'Crypto card\naccount' : 'Exchange\nbalance'],    'equals_to_interlace' => ['Fiat\naccount', 'Received by\nExchange', 'Converting', 'Crypto\ncard'],    _ => ['Crypto\ncard', 'Received by\nExchange', 'Converting', 'Fiat\naccount'],  };  int get progressStage {    if (failedBeforeFunding) return 0;    if (const {'completed','direct_exchange_completed','settled'}.contains(status)) return progressLabels.length;    if (isBankExchange) {      if (conversionSettled || const {'withdrawal_ready','withdrawal_submitting','payout_submitted'}.contains(status)) return 2;      return const {'exchange_accepting','exchange_submitted'}.contains(status) ? 1 : 0;    }    if (direction == 'exchange_to_crypto') {      if (status.startsWith('payout') || conversionSettled) return 2;      return status == 'direct_exchange_submitted' ? 1 : 0;    }    if (status.contains('payout') || status.contains('credit') || status.contains('loading')) return 3;    if (status.contains('convert') || status.contains('quote') || status == 'submitted') return 2;    return 1;  }  String? get progressStatusLabel {    if (failedBeforeFunding) return 'Not sent';    if (awaitingSettlementDetails) return 'Awaiting provider details';    if (!isBankExchange) return null;    return switch (status) {      'exchange_quoted' => 'Awaiting confirmation',      'exchange_preparing' || 'exchange_accepting' => 'Confirming exchange',      'exchange_submitted' => conversionSettled ? 'Withdrawal pending' : 'Converting',      'withdrawal_ready' => 'Withdrawal queued',      'withdrawal_submitting' => 'Confirming withdrawal',      'payout_submitted' => 'Bank withdrawal processing',      'exchange_failed' => 'Exchange not completed',      'completed' => 'Completed',      _ => null,    };  }   bool get needsQuoteDecision => status == 'awaiting_quote_approval';  bool get needsFundsDecision => status == 'awaiting_funds_decision';} Map<String, dynamic> _map(Object? value) {  if (value is Map<String, dynamic>) return value;  if (value is Map) {    return value.map((key, item) => MapEntry(key.toString(), item));  }  return const {};} List<Object?> _list(Object? value) => value is List ? value : const []; Object? _value(Map<String, dynamic> json, String first, String second) =>    json[first] ?? json[second] ?? json['${second[0].toLowerCase()}${second.substring(1)}']; String _text(Object? value) => value?.toString().trim() ?? ''; int _integer(Object? value) =>    value is num ? value.toInt() : int.tryParse(_text(value)) ?? 0; double _decimal(Object? value) =>    value is num ? value.toDouble() : double.tryParse(_text(value)) ?? 0; bool _boolean(Object? value) =>    value == true || _text(value).toLowerCase() == 'true'; class BoomFiDeposit {  const BoomFiDeposit(      {required this.id,      required this.paymentId,      required this.amount,      required this.currency,      required this.chainId,      required this.status,      required this.providerStatus,      required this.transactionHash,      this.createdAt,      this.updatedAt,      this.actionUrl,      this.settlementTransactionHash = '',      this.paymentMethod = 'Deposit'});  factory BoomFiDeposit.fromJson(Map<String, dynamic> json) {    Object? value(String key) =>        json[key] ?? json['${key[0].toUpperCase()}${key.substring(1)}'];    return BoomFiDeposit(        id: _integer(value('id')),        paymentId: _text(value('paymentId')),        amount: _text(value('amount')),        currency: _text(value('currency')),        chainId: _integer(value('chainId')),        status: _text(value('status')),        providerStatus: _text(value('providerStatus')),        paymentMethod: _text(value('paymentMethod')).isEmpty ? 'Deposit' : _text(value('paymentMethod')),        transactionHash: _text(value('transactionHash')),        settlementTransactionHash: _text(value('settlementTransactionHash')),        createdAt: DateTime.tryParse(_text(value('createdAt'))),        updatedAt: DateTime.tryParse(_text(value('updatedAt'))),        actionUrl: _text(value('actionUrl')));  }  final int id, chainId;  final String paymentId,      amount,      currency,      status,      providerStatus,      transactionHash;  final DateTime? createdAt, updatedAt;  final String? actionUrl;  final String settlementTransactionHash, paymentMethod;  bool get isDeposit => paymentMethod == 'Deposit';  bool get requiresAction => status == 'requires_action';  String get statusLabel => switch (status) {        'requires_action' => 'Verification required',        'completed' => 'Completed',        'failed' => 'Failed',        'cancelled' => 'Cancelled',        _ => 'Processing',      };  String? get verificationUrl {    if (!requiresAction) return null;    final uri = Uri.tryParse(actionUrl ?? '');    if (uri == null ||        uri.scheme != 'https' ||        uri.userInfo.isNotEmpty ||        (uri.hasPort && uri.port != 443) ||        !const {'www.zbx.com', 'zbx.com'}.contains(uri.host.toLowerCase())) {      return null;    }    return uri.toString();  }} BoomFiFeesAndLimits? _fees(Map<String, dynamic> json) {  final raw = json['fees_and_limits'] ?? json['feesAndLimits'] ?? json['FeesAndLimits'];  return raw is Map ? BoomFiFeesAndLimits.fromJson(_map(raw)) : null;}
HOPPA DEVELOPER ASSISTANT

What are you building?

From a question to an implementation.

Get a step-by-step flow, example code and the docs behind it. Choose a starting point or describe your feature.

Answers use the public API guide and example app code. Check the linked reference before shipping. Explore code sources ↗

Your question and recent conversation are sent to our AI provider. Don’t include API keys, personal data or card details.