diff --git a/packages/espressocash_app/lib/data/db/db.dart b/packages/espressocash_app/lib/data/db/db.dart index 3b2fb8cff6..a98304acf5 100644 --- a/packages/espressocash_app/lib/data/db/db.dart +++ b/packages/espressocash_app/lib/data/db/db.dart @@ -2,6 +2,7 @@ import 'package:drift/drift.dart'; import 'package:injectable/injectable.dart'; import '../../features/activities/models/transaction.dart'; +import '../../features/currency/models/currency.dart'; import '../../features/incoming_link_payments/data/ilp_repository.dart'; import '../../features/outgoing_direct_payments/data/repository.dart'; import '../../features/outgoing_link_payments/data/repository.dart'; @@ -24,7 +25,7 @@ class OutgoingTransferRows extends Table { Set> get primaryKey => {id}; } -const int latestVersion = 60; +const int latestVersion = 61; const _tables = [ OutgoingTransferRows, @@ -117,11 +118,9 @@ class MyDatabase extends _$MyDatabase { if (from < 51) { await m.addColumn(transactionRows, transactionRows.amount); } - if (from < 52) { await m.createTable(tokenBalanceRows); } - if (from < 53) { await m.addColumn(onRampOrderRows, onRampOrderRows.authToken); await m.addColumn(onRampOrderRows, onRampOrderRows.moreInfoUrl); @@ -171,6 +170,14 @@ class MyDatabase extends _$MyDatabase { if (from >= 39 && from < 60) { await m.addColumn(iLPRows, iLPRows.receiveAmount); } + if (from < 61) { + await m.addColumn(transactionRows, transactionRows.token); + + await customStatement( + 'UPDATE ${transactionRows.actualTableName} SET token = ?', + [Currency.usdc.token.address], + ); + } }, ); } @@ -306,6 +313,7 @@ class TransactionRows extends Table { TextColumn get encodedTx => text()(); IntColumn get status => intEnum()(); IntColumn get amount => integer().nullable()(); + TextColumn get token => text().nullable()(); @override Set> get primaryKey => {id}; diff --git a/packages/espressocash_app/lib/features/activities/data/transaction_repository.dart b/packages/espressocash_app/lib/features/activities/data/transaction_repository.dart index e14704cf82..638090a6a5 100644 --- a/packages/espressocash_app/lib/features/activities/data/transaction_repository.dart +++ b/packages/espressocash_app/lib/features/activities/data/transaction_repository.dart @@ -5,15 +5,19 @@ import 'package:dfunc/dfunc.dart'; import 'package:drift/drift.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:injectable/injectable.dart'; +import 'package:intl/intl.dart'; import 'package:rxdart/rxdart.dart'; import 'package:solana/encoder.dart'; import '../../../data/db/db.dart'; +import '../../../di.dart'; import '../../currency/models/amount.dart'; import '../../currency/models/currency.dart'; import '../../outgoing_direct_payments/data/repository.dart'; import '../../outgoing_link_payments/data/repository.dart'; import '../../payment_request/data/repository.dart'; +import '../../tokens/data/token_repository.dart'; +import '../../tokens/token.dart'; import '../../transaction_request/service/tr_service.dart'; import '../models/activity.dart'; import '../models/transaction.dart'; @@ -32,6 +36,38 @@ class TransactionRepository { return query.map((row) => row.id).watch().map((event) => event.toIList()); } + Stream> watchByAddress(String tokenAddress) { + final query = _db.select(_db.transactionRows) + ..where((t) => t.token.equals(tokenAddress)) + ..orderBy([(t) => OrderingTerm.desc(t.created)]); + + return query.map((row) => row.id).watch().map((event) => event.toIList()); + } + + Stream>> watchGroupedByDate(String tokenAddress) { + final query = _db.select(_db.transactionRows) + ..where((t) => t.token.equals(tokenAddress)) + ..orderBy([(t) => OrderingTerm.desc(t.created)]); + + return query.watch().asyncMap((rows) async { + final grouped = >{}; + for (final row in rows) { + final model = await row.toModel(); + final created = model.created; + if (created != null) { + final date = DateFormat('yyyy-MM-dd').format(created); + grouped.update( + date, + (list) => list.add(model), + ifAbsent: () => IList([model]), + ); + } + } + + return grouped; + }); + } + Stream> watchCount(int count) { final query = _db.select(_db.transactionRows) ..limit(count) @@ -44,7 +80,9 @@ class TransactionRepository { final query = _db.select(_db.transactionRows) ..where((tbl) => tbl.id.equals(id)); - return query.watchSingle().asyncExpand((row) => _match(row.toModel())); + return query + .watchSingle() + .asyncMap((row) => row.toModel().then((value) => _match(value).first)); } Future saveAll( @@ -147,14 +185,26 @@ class TransactionRepository { } extension TransactionRowExt on TransactionRow { - TxCommon toModel() => TxCommon( - SignedTx.decode(encodedTx), - created: created, - status: status, - amount: amount?.let( - (it) => CryptoAmount(value: it, cryptoCurrency: Currency.usdc), + Future toModel() async { + final tokenAddress = this.token; + Token? token; + + if (tokenAddress != null) { + token = await sl().getToken(tokenAddress); + } + + return TxCommon( + SignedTx.decode(encodedTx), + created: created, + status: status, + amount: amount?.let( + (it) => CryptoAmount( + value: it, + cryptoCurrency: CryptoCurrency(token: token ?? Token.unk), ), - ); + ), + ); + } } extension on TxCommon { @@ -164,6 +214,7 @@ extension on TxCommon { encodedTx: tx.encode(), status: status, amount: amount?.value, + token: amount?.cryptoCurrency.token.address, ); } diff --git a/packages/espressocash_app/lib/features/activities/services/token_activities_repository.dart b/packages/espressocash_app/lib/features/activities/services/token_activities_repository.dart new file mode 100644 index 0000000000..005c56e6b8 --- /dev/null +++ b/packages/espressocash_app/lib/features/activities/services/token_activities_repository.dart @@ -0,0 +1,46 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:injectable/injectable.dart'; + +import '../data/transaction_repository.dart'; +import '../models/transaction.dart'; + +@injectable +class TokenActivitiesRepository { + const TokenActivitiesRepository(this._repository); + + final TransactionRepository _repository; + + Stream> watchTokenActivities(String tokenAddress) => + _repository.watchGroupedByDate(tokenAddress).map(_prepareActivityGroups); + + List _prepareActivityGroups( + Map> data, + ) { + final sortedDates = data.keys.toList()..sort((a, b) => b.compareTo(a)); + + return sortedDates.map((date) { + final transactions = data[date] ?? IList(); + + final sortedTxs = transactions.sort((a, b) { + final aCreated = a.created; + final bCreated = b.created; + + return (aCreated != null && bCreated != null) + ? bCreated.compareTo(aCreated) + : 0; + }); + + return ActivityGroup(date: date, transactions: sortedTxs); + }).toList(); + } +} + +class ActivityGroup { + const ActivityGroup({ + required this.date, + required this.transactions, + }); + + final String date; + final IList transactions; +} diff --git a/packages/espressocash_app/lib/features/activities/services/tx_updater.dart b/packages/espressocash_app/lib/features/activities/services/tx_updater.dart index a4b2f12a3d..1fe0b122ea 100644 --- a/packages/espressocash_app/lib/features/activities/services/tx_updater.dart +++ b/packages/espressocash_app/lib/features/activities/services/tx_updater.dart @@ -1,19 +1,28 @@ import 'package:async/async.dart'; +import 'package:collection/collection.dart'; import 'package:dfunc/dfunc.dart'; import 'package:get_it/get_it.dart'; import 'package:injectable/injectable.dart'; +import 'package:solana/base58.dart'; import 'package:solana/dto.dart'; import 'package:solana/encoder.dart'; import 'package:solana/solana.dart'; +import '../../../utils/errors.dart'; import '../../accounts/auth_scope.dart'; import '../../accounts/models/ec_wallet.dart'; import '../../currency/models/amount.dart'; import '../../currency/models/currency.dart'; +import '../../tokens/data/token_repository.dart'; import '../../tokens/token.dart'; import '../data/transaction_repository.dart'; import '../models/transaction.dart'; +typedef TransactionUpdateResult = ({ + List txs, + bool hasGap, +}); + @Singleton(scope: authScope) class TxUpdater implements Disposable { TxUpdater(this._client, this._wallet, this._repo); @@ -24,69 +33,232 @@ class TxUpdater implements Disposable { final AsyncCache _cache = AsyncCache.ephemeral(); - Future call() => _cache.fetch( - () => tryEitherAsync((_) async { - final usdcTokenAccount = await findAssociatedTokenAddress( - owner: _wallet.publicKey, - mint: Ed25519HDPublicKey.fromBase58(Token.usdc.address), - ); + @PostConstruct() + Future call() => _cache.fetch(_updateAllTransactions); - final mostRecentTxId = await _repo.mostRecentTxId(); + Future _updateAllTransactions() async { + try { + final mostRecentTxId = await _repo.mostRecentTxId(); - const fetchLimit = 50; + final usdcTxs = await _fetchUsdcTransactions(mostRecentTxId); - final details = await _client.rpcClient.getTransactionsList( - usdcTokenAccount, - limit: fetchLimit, - until: mostRecentTxId, - encoding: Encoding.base64, - commitment: Commitment.confirmed, + final nonUsdcTxs = await _fetchNonUsdcTransactions(mostRecentTxId); + + final allTxs = [...usdcTxs.txs, ...nonUsdcTxs.txs]; + + if (allTxs.isNotEmpty) { + await _repo.saveAll( + allTxs, + clear: usdcTxs.hasGap || nonUsdcTxs.hasGap, + ); + } + } on Exception catch (exception) { + reportError(exception); + } + } + + Future _fetchUsdcTransactions( + String? mostRecentTxId, + ) async { + final usdcTokenAccount = await findAssociatedTokenAddress( + owner: _wallet.publicKey, + mint: Ed25519HDPublicKey.fromBase58(Token.usdc.address), + ); + + final details = await _client.rpcClient.getTransactionsList( + usdcTokenAccount, + limit: _usdcFetchLimit, + until: mostRecentTxId, + encoding: Encoding.base64, + commitment: Commitment.confirmed, + ); + + final txs = await Future.wait( + details.map((it) => it.toFetched(usdcTokenAccount, Token.usdc.address)), + ); + final hasGap = mostRecentTxId != null && txs.length == _usdcFetchLimit; + + return ( + txs: txs.whereNotNull().toList(), + hasGap: hasGap, + ); + } + + Future _fetchNonUsdcTransactions( + String? mostRecentTxId, + ) async { + final tokenAccounts = await _getAllTokenAccounts(_wallet.publicKey); + + final nonUsdcTokenAccounts = tokenAccounts + .where((account) => account.mintAddress != Token.usdc.address) + .toList(); + + final allAddresses = [ + _wallet.publicKey, + ...nonUsdcTokenAccounts.map((a) => a.account), + ]; + + final details = await _client.rpcClient.getTransactionListForAddresses( + allAddresses, + limit: _tokensFetchLimit, + until: mostRecentTxId, + commitment: Commitment.confirmed, + encoding: Encoding.base64, + ); + + final txs = await Future.wait( + details.map((detail) async { + final tx = SignedTx.fromBytes( + (detail.transaction as RawTransaction).data, + ); + + final tokenAccount = nonUsdcTokenAccounts.firstWhereOrNull( + (acc) => tx.compiledMessage.accountKeys.contains(acc.account), + ); + + if (tokenAccount != null) { + return detail.toFetched( + tokenAccount.account, + tokenAccount.mintAddress, ); + } + + return tx.compiledMessage.accountKeys.contains(_wallet.publicKey) + ? detail.toFetched(_wallet.publicKey, Token.sol.address) + : null; + }), + ); - if (details.isNotEmpty) { - final txs = details.map((it) => it.toFetched(usdcTokenAccount)); + final validTxs = txs.whereNotNull().toList(); - final hasGap = mostRecentTxId != null && txs.length == fetchLimit; + final uniqueTxs = validTxs.toSet().toList(); - await _repo.saveAll(txs, clear: hasGap); - } - }), + final txsByAddress = details.groupListsBy((detail) { + final tx = SignedTx.fromBytes( + (detail.transaction as RawTransaction).data, ); + return tx.compiledMessage.accountKeys.firstWhere( + allAddresses.contains, + orElse: () => _wallet.publicKey, + ); + }); + + final hasGap = + txsByAddress.values.any((txs) => txs.length >= _tokensFetchLimit); + + return ( + txs: uniqueTxs, + hasGap: mostRecentTxId != null && hasGap, + ); + } + + Future> _getAllTokenAccounts( + Ed25519HDPublicKey owner, + ) => + _client.rpcClient + .getTokenAccountsByOwner( + owner.toBase58(), + encoding: Encoding.base64, + const TokenAccountsFilter.byProgramId(TokenProgram.programId), + ) + .letAsync( + (response) => response.value.map((account) { + final data = account.account.data as BinaryAccountData?; + if (data == null) { + throw Exception('Account info or data is null'); + } + final mintAddressBytes = data.data.sublist(0, 32); + final mintAddress = base58encode(mintAddressBytes); + + return _TokenAccountInfo( + account: Ed25519HDPublicKey.fromBase58(account.pubkey), + mintAddress: mintAddress, + ); + }).toList(), + ); + @override Future onDispose() => _repo.clear(); } extension on TransactionDetails { - TxCommon toFetched(Ed25519HDPublicKey usdcTokenAddress) { + Future toFetched( + Ed25519HDPublicKey tokenAccount, + String? tokenAddress, + ) async { final rawTx = transaction as RawTransaction; final tx = SignedTx.fromBytes(rawTx.data); - final accountIndex = - tx.compiledMessage.accountKeys.indexWhere((e) => e == usdcTokenAddress); + tx.compiledMessage.accountKeys.indexWhere((e) => e == tokenAccount); - final preTokenBalance = meta?.preTokenBalances - .where((e) => e.mint == Token.usdc.address) - .where((e) => e.accountIndex == accountIndex) - .firstOrNull; + int? getBalanceDifference( + List? preBalances, + List? postBalances, + ) { + if (preBalances != null && postBalances != null) { + final preBalance = preBalances[accountIndex] as int; + final postBalance = postBalances[accountIndex] as int; + if (preBalance == 0 && postBalance == 0) return null; - final postTokenBalance = meta?.postTokenBalances - .where((e) => e.mint == Token.usdc.address) - .where((e) => e.accountIndex == accountIndex) - .firstOrNull; + return postBalance - preBalance; + } - CryptoAmount? amount; + return null; + } - if (preTokenBalance != null && postTokenBalance != null) { - final rawAmount = int.parse(postTokenBalance.uiTokenAmount.amount) - - int.parse(preTokenBalance.uiTokenAmount.amount); + int? getTokenBalanceDifference( + List? preBalances, + List? postBalances, + ) { + final preBalance = preBalances + ?.firstWhereOrNull( + (e) => e.mint == tokenAddress && e.accountIndex == accountIndex, + ) + ?.uiTokenAmount + .amount; - amount = CryptoAmount( - value: rawAmount, - cryptoCurrency: Currency.usdc, - ); + final postBalance = postBalances + ?.firstWhereOrNull( + (e) => e.mint == tokenAddress && e.accountIndex == accountIndex, + ) + ?.uiTokenAmount + .amount; + + final preReturnValue = preBalance != null ? int.parse(preBalance) : 0; + final postReturnValue = postBalance != null ? int.parse(postBalance) : 0; + + if (preReturnValue == 0 && postReturnValue == 0) return null; + + return postReturnValue - preReturnValue; } + final rawAmount = tokenAddress == Token.sol.address + ? getBalanceDifference(meta?.preBalances, meta?.postBalances) + : getTokenBalanceDifference( + meta?.preTokenBalances, + meta?.postTokenBalances, + ); + + if (rawAmount == null || rawAmount == 0) return null; + + final amount = await rawAmount.let((amount) async { + final tokenRepository = GetIt.I(); + final cryptoCurrency = tokenAddress != null + ? tokenAddress == Token.sol.address + ? const CryptoCurrency(token: Token.sol) + : CryptoCurrency( + token: + await tokenRepository.getToken(tokenAddress) ?? Token.unk, + ) + : const CryptoCurrency(token: Token.unk); + + return CryptoAmount( + value: amount, + cryptoCurrency: cryptoCurrency, + ); + }); + return TxCommon( tx, status: @@ -97,3 +269,16 @@ extension on TransactionDetails { ); } } + +class _TokenAccountInfo { + const _TokenAccountInfo({ + required this.account, + required this.mintAddress, + }); + + final Ed25519HDPublicKey account; + final String mintAddress; +} + +const _tokensFetchLimit = 15; +const _usdcFetchLimit = 50; diff --git a/packages/espressocash_app/lib/features/activities/widgets/activity_tile.dart b/packages/espressocash_app/lib/features/activities/widgets/activity_tile.dart index f23978ae53..f3b586801f 100644 --- a/packages/espressocash_app/lib/features/activities/widgets/activity_tile.dart +++ b/packages/espressocash_app/lib/features/activities/widgets/activity_tile.dart @@ -12,6 +12,7 @@ class CpActivityTile extends StatelessWidget { required this.icon, required this.status, required this.timestamp, + this.subtitle, this.incomingAmount, this.outgoingAmount, this.onTap, @@ -22,6 +23,7 @@ class CpActivityTile extends StatelessWidget { final Widget icon; final String timestamp; final CpActivityTileStatus status; + final String? subtitle; final String? incomingAmount; final String? outgoingAmount; final VoidCallback? onTap; diff --git a/packages/espressocash_app/lib/features/activities/widgets/common_tile.dart b/packages/espressocash_app/lib/features/activities/widgets/common_tile.dart index f4ce6af89d..a5d1baf803 100644 --- a/packages/espressocash_app/lib/features/activities/widgets/common_tile.dart +++ b/packages/espressocash_app/lib/features/activities/widgets/common_tile.dart @@ -3,9 +3,11 @@ import 'package:flutter/material.dart'; import '../../../../gen/assets.gen.dart'; import '../../../l10n/device_locale.dart'; +import '../../../l10n/l10n.dart'; import '../../../ui/web_view_screen.dart'; import '../../../utils/extensions.dart'; import '../../conversion_rates/widgets/extensions.dart'; +import '../../tokens/token.dart'; import '../../transactions/services/create_transaction_link.dart'; import '../models/transaction.dart'; import 'activity_tile.dart'; @@ -24,15 +26,21 @@ class CommonTile extends StatelessWidget { Widget build(BuildContext context) { final signature = txCommon.tx.id; + final isUnknown = txCommon.amount?.cryptoCurrency.token == Token.unk; + final isOutgoing = txCommon.amount?.let((e) => e.value.isNegative || e.value == 0) ?? false; - final amount = txCommon.amount - ?.let((e) => e.format(context.locale, maxDecimals: 2)) - .let((e) => e.replaceAll('-', '')); + + final amount = isUnknown + ? null + : txCommon.amount + ?.let((e) => e.format(context.locale, maxDecimals: 9)) + .let((e) => e.replaceAll('-', '')); return CpActivityTile( - title: signature.toShortAddress(), + title: isOutgoing ? context.l10n.sentDirectly : context.l10n.received, + subtitle: signature.toShortAddress(), status: switch (txCommon.status) { TxCommonStatus.success => CpActivityTileStatus.success, TxCommonStatus.failure => CpActivityTileStatus.failure, diff --git a/packages/espressocash_app/lib/features/activities/widgets/recent_activity.dart b/packages/espressocash_app/lib/features/activities/widgets/recent_activity.dart index 157b51262f..3abe50d5dd 100644 --- a/packages/espressocash_app/lib/features/activities/widgets/recent_activity.dart +++ b/packages/espressocash_app/lib/features/activities/widgets/recent_activity.dart @@ -8,7 +8,6 @@ import '../../../ui/colors.dart'; import '../../../ui/home_tile.dart'; import '../../../ui/theme.dart'; import '../data/transaction_repository.dart'; -import '../services/tx_updater.dart'; import 'transaction_item.dart'; class RecentActivityWidget extends StatefulWidget { @@ -34,7 +33,6 @@ class _RecentActivityWidgetState extends State { void initState() { super.initState(); _txs = sl().watchCount(_activityCount); - sl().call(); } @override diff --git a/packages/espressocash_app/lib/features/activities/widgets/recent_token_activity.dart b/packages/espressocash_app/lib/features/activities/widgets/recent_token_activity.dart new file mode 100644 index 0000000000..75f6482e3d --- /dev/null +++ b/packages/espressocash_app/lib/features/activities/widgets/recent_token_activity.dart @@ -0,0 +1,205 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../../../di.dart'; +import '../../../l10n/l10n.dart'; +import '../../../ui/colors.dart'; +import '../../../ui/home_tile.dart'; +import '../../../ui/theme.dart'; +import '../services/token_activities_repository.dart'; +import 'common_tile.dart'; +import 'odp_tile.dart'; +import 'off_ramp_tile.dart'; +import 'olp_tile.dart'; +import 'on_ramp_tile.dart'; +import 'outgoing_dln_tile.dart'; +import 'payment_request_tile.dart'; +import 'tr_tile.dart'; + +class RecentTokenActivityWidget extends StatefulWidget { + const RecentTokenActivityWidget({ + super.key, + required this.tokenAddress, + }); + + final String tokenAddress; + + @override + State createState() => + _RecentTokenActivityWidgetState(); +} + +class _RecentTokenActivityWidgetState extends State { + late final Stream> _groupedTxs; + + @override + void initState() { + super.initState(); + _groupedTxs = sl() + .watchTokenActivities(widget.tokenAddress); + } + + @override + Widget build(BuildContext context) => StreamBuilder>( + stream: _groupedTxs, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const CircularProgressIndicator(); + } + + final groups = snapshot.data; + + return groups == null || groups.isEmpty + ? const Center(child: _NoActivity()) + : HomeTile( + padding: + const EdgeInsets.symmetric(horizontal: 22, vertical: 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + const SizedBox(height: 16), + ...groups + .map((group) => _ActivityGroupWidget(group: group)), + const SizedBox(height: 8), + ], + ), + ); + }, + ); +} + +class _ActivityGroupWidget extends StatelessWidget { + const _ActivityGroupWidget({required this.group}); + + final ActivityGroup group; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 12.0, bottom: 9.0), + child: Text( + context.formatActivityDate(group.date), + style: dashboardSectionTitleTextStyle, + ), + ), + _Card( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: group.transactions.length * 60, + minWidth: MediaQuery.sizeOf(context).width, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: group.transactions + .map( + (tx) => tx.map( + common: (t) => CommonTile( + key: ValueKey(t.tx.id), + txCommon: t, + showIcon: false, + ), + activity: (txActivity) => txActivity.activity.map( + outgoingPaymentRequest: (p) => PaymentRequestTile( + key: ValueKey(p.id), + id: p.id, + showIcon: false, + ), + outgoingDirectPayment: (p) => ODPTile( + key: ValueKey(p.id), + activity: p, + showIcon: false, + ), + outgoingLinkPayment: (p) => OLPTile( + key: ValueKey(p.id), + activity: p, + showIcon: false, + ), + onRamp: (it) => OnRampTile( + key: ValueKey(it.id), + activity: it, + showIcon: false, + ), + offRamp: (it) => OffRampTile( + key: ValueKey(it.id), + activity: it, + showIcon: false, + ), + outgoingDlnPayment: (it) => OutgoingDlnTile( + key: ValueKey(it.id), + activity: it, + showIcon: false, + ), + transactionRequest: (it) => TrTile( + key: ValueKey(it.id), + activity: it, + showIcon: false, + ), + kyc: (it) => const SizedBox.shrink(), + ), + ), + ) + .toList(), + ), + ), + ), + const SizedBox(height: 40), + ], + ); +} + +class _Card extends StatelessWidget { + const _Card({required this.child}); + final Widget child; + + @override + Widget build(BuildContext context) => Container( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), + decoration: const ShapeDecoration( + color: CpColors.blackGreyColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(28), + ), + ), + ), + child: child, + ); +} + +class _NoActivity extends StatelessWidget { + const _NoActivity(); + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.only(top: 72.0), + child: Text( + context.l10n.noRecentActivity, + style: const TextStyle( + color: Color(0xFF484848), + fontSize: 16, + fontWeight: FontWeight.w400, + ), + ), + ); +} + +extension ActivityDateFormatting on BuildContext { + String formatActivityDate(String date) { + final parsedDate = DateTime.parse(date); + final now = DateTime.now(); + final yesterday = now.subtract(const Duration(days: 1)); + + return (parsedDate.year == now.year && + parsedDate.month == now.month && + parsedDate.day == now.day) + ? l10n.today + : (parsedDate.year == yesterday.year && + parsedDate.month == yesterday.month && + parsedDate.day == yesterday.day) + ? l10n.yesterday + : DateFormat('MMM d, yyyy').format(parsedDate); + } +} diff --git a/packages/espressocash_app/lib/features/authenticated/widgets/balance_amount.dart b/packages/espressocash_app/lib/features/authenticated/widgets/balance_amount.dart index 45608bda46..19df68c532 100644 --- a/packages/espressocash_app/lib/features/authenticated/widgets/balance_amount.dart +++ b/packages/espressocash_app/lib/features/authenticated/widgets/balance_amount.dart @@ -8,6 +8,7 @@ import '../../conversion_rates/services/token_fiat_balance_service.dart'; import '../../conversion_rates/widgets/extensions.dart'; import '../../currency/models/amount.dart'; import '../../currency/models/currency.dart'; +import '../../token_details/screens/token_details_screen.dart'; import '../../tokens/token.dart'; import '../../tokens/widgets/token_icon.dart'; @@ -26,24 +27,27 @@ class BalanceAmount extends StatelessWidget { roundInteger: amount.isZero, ); - return Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - FittedBox( - child: Text( - formattedAmount, - style: const TextStyle( - fontSize: 48, - fontWeight: FontWeight.bold, - color: Colors.white, - letterSpacing: -1, + return GestureDetector( + onTap: () => TokenDetailsScreen.push(context, token: Token.usdc), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FittedBox( + child: Text( + formattedAmount, + style: const TextStyle( + fontSize: 48, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: -1, + ), ), - ), - ).let((it) => amount.isZero ? it : Flexible(child: it)), - const SizedBox(width: 8), - const TokenIcon(token: Token.usdc, size: 30), - ], + ).let((it) => amount.isZero ? it : Flexible(child: it)), + const SizedBox(width: 8), + const TokenIcon(token: Token.usdc, size: 30), + ], + ), ); }, ); diff --git a/packages/espressocash_app/lib/features/authenticated/widgets/portfolio_widget.dart b/packages/espressocash_app/lib/features/authenticated/widgets/portfolio_widget.dart index 880feda31a..1a3cd8874b 100644 --- a/packages/espressocash_app/lib/features/authenticated/widgets/portfolio_widget.dart +++ b/packages/espressocash_app/lib/features/authenticated/widgets/portfolio_widget.dart @@ -11,6 +11,7 @@ import '../../../ui/value_stream_builder.dart'; import '../../conversion_rates/services/token_fiat_balance_service.dart'; import '../../conversion_rates/widgets/extensions.dart'; import '../../currency/models/amount.dart'; +import '../../token_details/screens/token_details_screen.dart'; import '../../tokens/widgets/token_icon.dart'; class PortfolioWidget extends StatefulWidget { @@ -146,7 +147,7 @@ class _TokenItem extends StatelessWidget { @override Widget build(BuildContext context) { final String fiatAmountText = - context.portfilioTotalAmountText(fiatAmount, _minFiatAmount); + context.portfolioTotalAmountText(fiatAmount, _minFiatAmount); return _Card( child: ListTile( @@ -158,6 +159,8 @@ class _TokenItem extends StatelessWidget { borderRadius: BorderRadius.circular(_iconSize / 2), child: TokenIcon(token: cryptoAmount.token, size: _iconSize), ), + onTap: () => + TokenDetailsScreen.push(context, token: cryptoAmount.token), title: Text( cryptoAmount.token.name, style: const TextStyle( @@ -214,7 +217,7 @@ class _Card extends StatelessWidget { } extension TotalPortfolioTextExtension on BuildContext { - String portfilioTotalAmountText(FiatAmount? fiatAmount, num minFiatAmount) { + String portfolioTotalAmountText(FiatAmount? fiatAmount, num minFiatAmount) { if (fiatAmount != null) { if (fiatAmount.value < minFiatAmount) { return r'<$0.01'; diff --git a/packages/espressocash_app/lib/features/conversion_rates/services/token_fiat_balance_service.dart b/packages/espressocash_app/lib/features/conversion_rates/services/token_fiat_balance_service.dart index 3732cea854..88f49aa377 100644 --- a/packages/espressocash_app/lib/features/conversion_rates/services/token_fiat_balance_service.dart +++ b/packages/espressocash_app/lib/features/conversion_rates/services/token_fiat_balance_service.dart @@ -92,6 +92,14 @@ class TokenFiatBalanceService { ) .distinct(); + Stream readInvestmentBalance(Token token) => + _balancesRepository.watch(token).flatMap( + (amount) => Rx.combineLatest( + [watch(amount.token).map((fiat) => (amount, fiat))], + (values) => values.map((e) => (e.$1, e.$2 ?? _zeroFiat)).first, + ), + ); + void _logTotalCryptoBalance(Amount total) => _analyticsManager.setTotalInvestmentsBalance(total.decimal); } diff --git a/packages/espressocash_app/lib/features/currency/models/amount.dart b/packages/espressocash_app/lib/features/currency/models/amount.dart index fb98660fbf..347e866a50 100644 --- a/packages/espressocash_app/lib/features/currency/models/amount.dart +++ b/packages/espressocash_app/lib/features/currency/models/amount.dart @@ -1,5 +1,8 @@ +import 'dart:ui'; + import 'package:decimal/decimal.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:intl/intl.dart'; import '../../tokens/token.dart'; import 'currency.dart'; @@ -117,3 +120,39 @@ extension FiatAmountExt on FiatAmount { FiatAmount copyWithDecimal(Decimal decimal) => copyWith(value: currency.decimalToInt(decimal)); } + +extension FormattedAmount on Amount { + String formatRate(double rate, Locale locale) { + if (rate >= 1) { + return NumberFormat.currency( + locale: locale.toString(), + symbol: '', + decimalDigits: 2, + ).format(rate); + } + + if (rate == 0.0 || rate >= 0.01) { + return rate.toStringAsFixed(2); + } + + final initialString = rate.toStringAsFixed(10); + final decimalPointIndex = initialString.indexOf('.'); + int significantCount = 0; + bool foundFirstNonZero = false; + + for (int i = decimalPointIndex + 1; i < initialString.length; i++) { + if (initialString[i] != '0') { + foundFirstNonZero = true; + significantCount++; + } else if (foundFirstNonZero) { + significantCount++; + } + + if (significantCount == 2) { + return initialString.substring(0, i + 1); + } + } + + return initialString; + } +} diff --git a/packages/espressocash_app/lib/features/token_details/screens/token_details_screen.dart b/packages/espressocash_app/lib/features/token_details/screens/token_details_screen.dart new file mode 100644 index 0000000000..c602fef0cb --- /dev/null +++ b/packages/espressocash_app/lib/features/token_details/screens/token_details_screen.dart @@ -0,0 +1,267 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../../di.dart'; +import '../../../l10n/device_locale.dart'; +import '../../../l10n/l10n.dart'; +import '../../../ui/button.dart'; +import '../../../ui/colors.dart'; +import '../../../ui/value_stream_builder.dart'; +import '../../activities/services/tx_updater.dart'; +import '../../activities/widgets/recent_token_activity.dart'; +import '../../conversion_rates/data/repository.dart'; +import '../../conversion_rates/services/token_fiat_balance_service.dart'; +import '../../conversion_rates/widgets/extensions.dart'; +import '../../currency/models/amount.dart'; +import '../../currency/models/currency.dart'; +import '../../ramp/widgets/ramp_buttons.dart'; +import '../../tokens/token.dart'; +import '../widgets/token_app_bar.dart'; +import '../widgets/token_info.dart'; + +class TokenDetailsScreen extends StatelessWidget { + const TokenDetailsScreen({super.key, required this.token}); + + final Token token; + + static void push(BuildContext context, {required Token token}) => + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => TokenDetailsScreen(token: token), + ), + ); + + @override + Widget build(BuildContext context) => Provider.value( + value: token, + child: Scaffold( + backgroundColor: CpColors.darkSandColor, + body: SafeArea( + bottom: false, + child: NestedScrollView( + headerSliverBuilder: (context, _) => [TokenAppBar(token: token)], + body: _TokenDetailsBody(token), + ), + ), + ), + ); +} + +class _TokenDetailsBody extends StatelessWidget { + const _TokenDetailsBody(this.token); + + final Token token; + + @override + Widget build(BuildContext context) => DecoratedBox( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + CpColors.darkSandColor, + CpColors.deepGreyColor, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: [0.55, 0.56], + ), + ), + child: LayoutBuilder( + builder: ( + BuildContext context, + BoxConstraints viewportConstraints, + ) => + RefreshIndicator( + onRefresh: () => sl().call(), + color: CpColors.primaryColor, + backgroundColor: Colors.white, + child: SingleChildScrollView( + physics: const BouncingScrollPhysics( + decelerationRate: ScrollDecelerationRate.fast, + parent: ClampingScrollPhysics(), + ), + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: viewportConstraints.maxHeight), + child: IntrinsicHeight( + child: Column( + children: [ + const SizedBox(height: 4), + const _TokenHeader(), + const SizedBox(height: 24), + if (token.isUsdcToken) const _RampButtons(), + const SizedBox(height: 24), + Expanded( + child: DecoratedBox( + decoration: const BoxDecoration( + color: CpColors.deepGreyColor, + borderRadius: + BorderRadius.vertical(top: Radius.circular(31)), + ), + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 41), + child: Column( + children: [ + TokenInfo(tokenAddress: token.address), + RecentTokenActivityWidget( + tokenAddress: token.address, + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); +} + +class _TokenHeader extends StatelessWidget { + const _TokenHeader(); + + @override + Widget build(BuildContext context) { + final token = context.watch(); + final rate = sl().readRate( + CryptoCurrency(token: token), + to: defaultFiatCurrency, + ) ?? + Decimal.zero; + + return ValueStreamBuilder( + create: () => ( + sl().readInvestmentBalance(token), + ( + Amount.zero(currency: Currency.usdc) as CryptoAmount, + Amount.zero(currency: Currency.usd) as FiatAmount + ) + ), + builder: (context, value) { + final crypto = value.$1; + final fiat = value.$2; + final fiatRate = + Amount.fromDecimal(value: rate, currency: Currency.usd); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + Text.rich( + TextSpan( + text: '${context.l10n.balance} ', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w400, + ), + children: [ + TextSpan( + text: fiat?.format(context.locale), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + FittedBox( + child: Text( + context.formatWithMinAmount(crypto), + maxLines: 1, + style: const TextStyle( + fontSize: 59, + fontWeight: FontWeight.w700, + ), + ), + ), + Text.rich( + TextSpan( + text: '${context.l10n.price} ', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w400, + ), + children: [ + TextSpan( + text: + '\$${fiatRate.formatRate(rate.toDouble(), context.locale)}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } +} + +class _RampButtons extends StatelessWidget { + const _RampButtons(); + + @override + Widget build(BuildContext context) => const Padding( + padding: EdgeInsets.symmetric(horizontal: 40), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + AddCashButton(), + SizedBox(width: 16), + CashOutButton(), + ], + ), + ); +} + +// ignore: unused_element, won't be available in first release +class _SwapButton extends StatelessWidget { + const _SwapButton(); + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CpButton( + text: 'Swap', + minWidth: 106, + size: CpButtonSize.big, + onPressed: () {}, + ), + const SizedBox(width: 14), + CpButton( + text: 'Send', + minWidth: 106, + size: CpButtonSize.big, + onPressed: () {}, + ), + ], + ), + ); +} + +extension CryptoAmountFormatting on BuildContext { + String formatWithMinAmount(CryptoAmount cryptoAmount) => + cryptoAmount.decimal < Decimal.parse(_minCryptoAmount.toString()) + ? '<${Amount.fromDecimal( + value: Decimal.parse(_minCryptoAmount.toString()), + currency: cryptoAmount.currency, + ).format(locale)}' + : cryptoAmount.format(locale, maxDecimals: 4); +} + +const double _minCryptoAmount = 0.0001; diff --git a/packages/espressocash_app/lib/features/token_details/widgets/token_app_bar.dart b/packages/espressocash_app/lib/features/token_details/widgets/token_app_bar.dart new file mode 100644 index 0000000000..b85a534659 --- /dev/null +++ b/packages/espressocash_app/lib/features/token_details/widgets/token_app_bar.dart @@ -0,0 +1,118 @@ +import 'dart:math'; + +import 'package:dfunc/dfunc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../../../ui/back_button.dart'; +import '../../../ui/colors.dart'; +import '../../tokens/token.dart'; +import '../../tokens/widgets/token_icon.dart'; + +class TokenAppBar extends StatelessWidget { + const TokenAppBar({super.key, required this.token}); + + final Token token; + + @override + Widget build(BuildContext context) => SliverPersistentHeader( + pinned: true, + delegate: _TokenAppBarDelegate(token), + ); +} + +class _TokenAppBarDelegate extends SliverPersistentHeaderDelegate { + const _TokenAppBarDelegate(this.token); + + final Token token; + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + /// Scroll ratio, should vary between [0,1] from expanded to collapsed. + final ratio = (shrinkOffset / maxExtent) + .let(Curves.ease.transform) + .let((it) => 1 - it); + final iconSize = max(_tokenSize * ratio, 24.0); + + return Material( + color: CpColors.darkSandColor, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Stack( + children: [ + _buildIcon(ratio, iconSize), + _buildText(ratio, iconSize), + const _BackButton(), + ], + ), + ), + ); + } + + Widget _buildIcon(double ratio, double iconSize) => Positioned( + top: (iconSize * ratio) - iconSize + 8, + left: 0, + right: 0, + child: Opacity( + opacity: ratio, + child: TokenIcon(token: token, size: iconSize), + ), + ); + + Widget _buildText(double ratio, double iconSize) => Positioned.fill( + top: iconSize * (ratio * 1.15) + 4, + left: _buttonSize, + right: _buttonSize, + child: Center( + child: FittedBox( + child: Text( + '${token.name} (${token.symbol})', + maxLines: 1, + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 17, + ), + ), + ), + ), + ); + + @override + double get maxExtent => _tokenSize + (_minExtent - 20); + + @override + double get minExtent => _minExtent; + + @override + bool shouldRebuild(covariant _TokenAppBarDelegate oldDelegate) => + oldDelegate.token != token; + + @override + FloatingHeaderSnapConfiguration get snapConfiguration => + FloatingHeaderSnapConfiguration( + curve: Curves.easeIn, + duration: const Duration(milliseconds: 200), + ); +} + +class _BackButton extends StatelessWidget { + const _BackButton(); + + @override + Widget build(BuildContext context) => const Align( + alignment: Alignment.topLeft, + child: SizedBox( + height: _minExtent, + child: CpBackButton(), + ), + ); +} + +const double _tokenSize = 68; +const double _buttonSize = 48; +const double _minExtent = kToolbarHeight; diff --git a/packages/espressocash_app/lib/features/token_details/widgets/token_info.dart b/packages/espressocash_app/lib/features/token_details/widgets/token_info.dart new file mode 100644 index 0000000000..75eb6ba97d --- /dev/null +++ b/packages/espressocash_app/lib/features/token_details/widgets/token_info.dart @@ -0,0 +1,87 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../../di.dart'; +import '../../../l10n/device_locale.dart'; +import '../../../l10n/l10n.dart'; +import '../../../utils/extensions.dart'; +import '../../conversion_rates/data/repository.dart'; +import '../../currency/models/amount.dart'; +import '../../currency/models/currency.dart'; +import '../../tokens/token.dart'; +import 'token_item.dart'; + +class TokenInfo extends StatelessWidget { + const TokenInfo({super.key, required this.tokenAddress}); + + final String tokenAddress; + + @override + Widget build(BuildContext context) { + final token = Provider.of(context); + + final rate = sl().readRate( + CryptoCurrency(token: token), + to: defaultFiatCurrency, + ) ?? + Decimal.zero; + + final fiatRate = Amount.fromDecimal(value: rate, currency: Currency.usd); + + return TokenItemContainer( + title: '${context.l10n.about} ${token.name}', + content: Column( + children: [ + _InfoItem( + label: token != Token.sol ? context.l10n.token : context.l10n.coin, + value: '${token.name} (${token.symbol})', + ), + _InfoItem( + label: context.l10n.price, + value: '\$${fiatRate.formatRate(rate.toDouble(), context.locale)}', + ), + if (token != Token.sol) + _InfoItem( + label: context.l10n.mintAddress, + value: tokenAddress.toShortAddress(), + ), + ], + ), + ); + } +} + +class _InfoItem extends StatelessWidget { + const _InfoItem({ + required this.label, + required this.value, + }); + final String label; + final String value; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + Text( + value, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Color(0xff999999), + ), + ), + ], + ), + ); +} diff --git a/packages/espressocash_app/lib/features/token_details/widgets/token_item.dart b/packages/espressocash_app/lib/features/token_details/widgets/token_item.dart new file mode 100644 index 0000000000..3c6e08fb04 --- /dev/null +++ b/packages/espressocash_app/lib/features/token_details/widgets/token_item.dart @@ -0,0 +1,44 @@ +import 'package:flutter/widgets.dart'; + +import '../../../ui/colors.dart'; +import '../../../ui/theme.dart'; + +class TokenItemContainer extends StatelessWidget { + const TokenItemContainer({ + super.key, + required this.title, + required this.content, + }); + + final String title; + final Widget content; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 22), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 12.0), + child: Text( + title, + style: dashboardSectionTitleTextStyle, + ), + ), + const SizedBox(height: 11), + Container( + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 28), + decoration: const ShapeDecoration( + color: CpColors.blackGreyColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28)), + ), + ), + child: content, + ), + ], + ), + ); +} diff --git a/packages/espressocash_app/lib/features/tokens/token.dart b/packages/espressocash_app/lib/features/tokens/token.dart index 269e2d6cd9..17ef6ea066 100644 --- a/packages/espressocash_app/lib/features/tokens/token.dart +++ b/packages/espressocash_app/lib/features/tokens/token.dart @@ -150,3 +150,7 @@ class _UsdcDevToken extends SplToken { isStablecoin: true, ); } + +extension TokenUtils on Token { + bool get isUsdcToken => address == Token.usdc.address; +} diff --git a/packages/espressocash_app/lib/l10n/intl_en.arb b/packages/espressocash_app/lib/l10n/intl_en.arb index 51458fedcd..22b2b0147b 100644 --- a/packages/espressocash_app/lib/l10n/intl_en.arb +++ b/packages/espressocash_app/lib/l10n/intl_en.arb @@ -1205,5 +1205,23 @@ "pendingKycDialogMessage": "Your identity verification is currently under review and will be completed shortly. You can check the Activity page for real-time updates on your verification status.", "@pendingKycDialogMessage": {}, "continueVerification": "Continue Verification", - "@continueVerification": {} + "@continueVerification": {}, + "about": "About", + "@about": {}, + "coin": "Coin", + "@coin": {}, + "mintAddress": "Mint Address", + "@mintAddress": {}, + "token" : "Token", + "@token": {}, + "price": "Price", + "@price": {}, + "today": "Today", + "@today": {}, + "yesterday": "Yesterday", + "@yesterday": {}, + "balance": "Balance", + "@balance": {}, + "noRecentActivity": "No recent activity", + "@noRecentActivity": {} } diff --git a/packages/espressocash_app/moor_schemas/moor_schema_v61.json b/packages/espressocash_app/moor_schemas/moor_schema_v61.json new file mode 100644 index 0000000000..d57240c672 --- /dev/null +++ b/packages/espressocash_app/moor_schemas/moor_schema_v61.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.1.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"outgoing_transfer_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":1,"references":[],"type":"table","data":{"name":"payment_request_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"dynamic_link","getter_name":"dynamicLink","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"short_link","getter_name":"shortLink","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(PaymentRequestStateDto.values)","dart_type_name":"PaymentRequestStateDto"}},{"name":"transaction_id","getter_name":"transactionId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"resolved_at","getter_name":"resolvedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"recipient","getter_name":"recipient","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"amount","getter_name":"amount","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"splt_token","getter_name":"spltToken","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"reference","getter_name":"reference","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"message","getter_name":"message","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"memo","getter_name":"memo","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":2,"references":[],"type":"table","data":{"name":"o_d_p_rows","was_declared_in_moor":false,"columns":[{"name":"amount","getter_name":"amount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"receiver","getter_name":"receiver","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"reference","getter_name":"reference","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(ODPStatusDto.values)","dart_type_name":"ODPStatusDto"}},{"name":"tx","getter_name":"tx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_id","getter_name":"txId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_failure_reason","getter_name":"txFailureReason","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxFailureReason.values)","dart_type_name":"TxFailureReason"}},{"name":"slot","getter_name":"slot","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":3,"references":[],"type":"table","data":{"name":"transaction_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"encoded_tx","getter_name":"encodedTx","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxCommonStatus.values)","dart_type_name":"TxCommonStatus"}},{"name":"amount","getter_name":"amount","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":4,"references":[],"type":"table","data":{"name":"o_l_p_rows","was_declared_in_moor":false,"columns":[{"name":"amount","getter_name":"amount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(OLPStatusDto.values)","dart_type_name":"OLPStatusDto"}},{"name":"tx","getter_name":"tx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_id","getter_name":"txId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"withdraw_tx_id","getter_name":"withdrawTxId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"link","getter_name":"link","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_failure_reason","getter_name":"txFailureReason","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxFailureReason.values)","dart_type_name":"TxFailureReason"}},{"name":"cancel_tx","getter_name":"cancelTx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"cancel_tx_id","getter_name":"cancelTxId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"generated_links_at","getter_name":"generatedLinksAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"resolved_at","getter_name":"resolvedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"slot","getter_name":"slot","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":5,"references":[],"type":"table","data":{"name":"i_l_p_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx","getter_name":"tx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_id","getter_name":"txId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_failure_reason","getter_name":"txFailureReason","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxFailureReason.values)","dart_type_name":"TxFailureReason"}},{"name":"slot","getter_name":"slot","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(ILPStatusDto.values)","dart_type_name":"ILPStatusDto"}},{"name":"fee_amount","getter_name":"feeAmount","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"receive_amount","getter_name":"receiveAmount","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":6,"references":[],"type":"table","data":{"name":"on_ramp_order_rows","was_declared_in_moor":false,"columns":[{"name":"amount","getter_name":"amount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_completed","getter_name":"isCompleted","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_completed\" IN (0, 1))","default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"human_status","getter_name":"humanStatus","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"machine_status","getter_name":"machineStatus","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"partner_order_id","getter_name":"partnerOrderId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"receive_amount","getter_name":"receiveAmount","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_hash","getter_name":"txHash","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"partner","getter_name":"partner","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant('kado')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(RampPartner.values)","dart_type_name":"RampPartner"}},{"name":"status","getter_name":"status","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(OnRampOrderStatus.values)","dart_type_name":"OnRampOrderStatus"}},{"name":"bank_name","getter_name":"bankName","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bank_account","getter_name":"bankAccount","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bank_transfer_expiry","getter_name":"bankTransferExpiry","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bank_transfer_amount","getter_name":"bankTransferAmount","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"fiat_symbol","getter_name":"fiatSymbol","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"auth_token","getter_name":"authToken","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"more_info_url","getter_name":"moreInfoUrl","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"stellar_tx_hash","getter_name":"stellarTxHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"fee_amount","getter_name":"feeAmount","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"reference_number","getter_name":"referenceNumber","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bridge_amount","getter_name":"bridgeAmount","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":7,"references":[],"type":"table","data":{"name":"off_ramp_order_rows","was_declared_in_moor":false,"columns":[{"name":"amount","getter_name":"amount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(OffRampOrderStatus.values)","dart_type_name":"OffRampOrderStatus"}},{"name":"human_status","getter_name":"humanStatus","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"machine_status","getter_name":"machineStatus","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"partner_order_id","getter_name":"partnerOrderId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"transaction","getter_name":"transaction","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deposit_address","getter_name":"depositAddress","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"slot","getter_name":"slot","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"resolved_at","getter_name":"resolvedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"receive_amount","getter_name":"receiveAmount","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"fiat_symbol","getter_name":"fiatSymbol","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"partner","getter_name":"partner","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant('kado')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(RampPartner.values)","dart_type_name":"RampPartner"}},{"name":"fee_amount","getter_name":"feeAmount","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"fee_token","getter_name":"feeToken","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"auth_token","getter_name":"authToken","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"withdraw_anchor_account","getter_name":"withdrawAnchorAccount","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"withdraw_memo","getter_name":"withdrawMemo","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"withdraw_url","getter_name":"withdrawUrl","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"more_info_url","getter_name":"moreInfoUrl","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"solana_bridge_tx","getter_name":"solanaBridgeTx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"stellar_tx_hash","getter_name":"stellarTxHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bridge_amount","getter_name":"bridgeAmount","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"reference_number","getter_name":"referenceNumber","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"refund_amount","getter_name":"refundAmount","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"priority_fee","getter_name":"priorityFee","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"gas_fee","getter_name":"gasFee","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":8,"references":[],"type":"table","data":{"name":"outgoing_dln_payment_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx","getter_name":"tx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_id","getter_name":"txId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_failure_reason","getter_name":"txFailureReason","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxFailureReason.values)","dart_type_name":"TxFailureReason"}},{"name":"slot","getter_name":"slot","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"receiver_blockchain","getter_name":"receiverBlockchain","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(BlockchainDto.values)","dart_type_name":"BlockchainDto"}},{"name":"receiver_address","getter_name":"receiverAddress","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"amount","getter_name":"amount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(ODLNPaymentStatusDto.values)","dart_type_name":"ODLNPaymentStatusDto"}},{"name":"order_id","getter_name":"orderId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":9,"references":[],"type":"table","data":{"name":"transaction_request_rows","was_declared_in_moor":false,"columns":[{"name":"amount","getter_name":"amount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"label","getter_name":"label","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"transaction","getter_name":"transaction","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"slot","getter_name":"slot","moor_type":"bigInt","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(TRStatusDto.values)","dart_type_name":"TRStatusDto"}}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":10,"references":[],"type":"table","data":{"name":"token_balance_rows","was_declared_in_moor":false,"columns":[{"name":"amount","getter_name":"amount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["token"]}},{"id":11,"references":[],"type":"table","data":{"name":"conversion_rates_rows","was_declared_in_moor":false,"columns":[{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"fiat_currency","getter_name":"fiatCurrency","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rate","getter_name":"rate","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["token","fiat_currency"]}},{"id":12,"references":[],"type":"table","data":{"name":"token_rows","was_declared_in_moor":false,"columns":[{"name":"chain_id","getter_name":"chainId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"address","getter_name":"address","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"symbol","getter_name":"symbol","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"decimals","getter_name":"decimals","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"logo_u_r_i","getter_name":"logoURI","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_stablecoin","getter_name":"isStablecoin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_stablecoin\" IN (0, 1))","default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["chain_id","address"]}}]} \ No newline at end of file diff --git a/packages/espressocash_app/test/features/currency/models/amount_test.dart b/packages/espressocash_app/test/features/currency/models/amount_test.dart new file mode 100644 index 0000000000..3bbfb686ef --- /dev/null +++ b/packages/espressocash_app/test/features/currency/models/amount_test.dart @@ -0,0 +1,37 @@ +import 'package:espressocash_app/features/currency/models/amount.dart'; +import 'package:espressocash_app/features/currency/models/currency.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FormattedAmount', () { + final amount = Amount.zero(currency: Currency.sol); + const locale = Locale('en', 'US'); + + group('formatRate', () { + test('formats rates >= 1 with 2 decimal places', () { + expect(amount.formatRate(1.0, locale), '1.00'); + expect(amount.formatRate(1.23456, locale), '1.23'); + expect(amount.formatRate(123.456, locale), '123.46'); + }); + + test( + 'formats small decimals with 2 significant digits after first non-zero', + () { + expect(amount.formatRate(0.00123456, locale), '0.0012'); + expect(amount.formatRate(0.00012345, locale), '0.00012'); + expect(amount.formatRate(0.00001234, locale), '0.000012'); + }); + + test('handles trailing zeros correctly', () { + expect(amount.formatRate(0.10000, locale), '0.10'); + expect(amount.formatRate(0.01000, locale), '0.01'); + }); + + test('handles edge cases', () { + expect(amount.formatRate(0, locale), '0.00'); + expect(amount.formatRate(0.1, locale), '0.10'); + }); + }); + }); +} diff --git a/packages/solana/test/programs/compute_budget_program/program_test.dart b/packages/solana/test/programs/compute_budget_program/program_test.dart index 99aa871c88..6eaf1b9e4f 100644 --- a/packages/solana/test/programs/compute_budget_program/program_test.dart +++ b/packages/solana/test/programs/compute_budget_program/program_test.dart @@ -64,7 +64,7 @@ void main() { const micro = 1000000; expect( feeWithComputeUnitPrice! - feeWithoutComputeUnitPrice!, - defaultUnitLimit * unitPriceMicroLamports / micro, + (defaultUnitLimit * unitPriceMicroLamports) ~/ micro, ); }); }