Introduce `Money` class and refactor currency handling (#3157)

* feat: add `Money` class for precise currency handling and amount parsing - Introduced `Money` class for handling money values with fixed-point precision. - Added parsing and comparison methods to `CryptoCurrency` and `FiatCurrency` interfaces. - Implemented `smartAmountSanitizer` for format normalization. Includes extensive unit tests for validation. * refactor: optimize Money class with new factories and string formatting improvements * refactor: unify balance handling with `Money` refactor * refactor: update `ElectrumBalance` and transaction logic to use `Money` for currency precision * refactor: standardize balance handling with `Money` across `Monero`, `Wownero`, and `Zano` wallets * refactor: migrate EVM balance and transaction handling to `Money` for consistent currency precision * refactor: migrate Decred balance and transaction handling to `Money` for consistent currency precision * refactor: migrate nano balance and transaction handling to `Money` for consistent currency precision * refactor: migrate Solana balance and transaction logic to `Money` for precise currency handling * refactor: migrate Tron balance and transaction handling to `Money` for consistent currency precision * refactor: migrate Zcash balance and transaction handling to `Money` for consistent currency precision * refactor: adjust lib to handle money * refactor: remove legacy Decred amount formatting and migrate to `Money` * refactor: migrate pending transaction amount and fee to `Money` * refactor: migrate dEuro savings and transaction logic to `Money` for improved precision and consistency * refactor: migrate Bitcoin and lightning transaction logic to `Money` for consistent currency precision * refactor: migrate Wownero, EVM, Zano, and Zcash transaction logic to `Money` for consistent and precise currency handling * refactor: standardize balance handling across wallets, migrate to `Money.zero` and refine `Balance` class for consistency * refactor: migrate Solana transaction and wallet logic to `Money` for consistent and precise currency handling * refactor: improve fiat balance and formatting for consistency on dashboard * fix: fiat input mode switching and standardize crypto amount display logic * fix: cw_evm generator * refactor: migrate exchange amount handling to `Money` for improved precision and consistency * test: remove redundant and commented-out tests, tidy test cases, and apply minor formatting adjustments * fix using Money wrapper in BridgeViewModel and USDT0Service * refactor: streamline `AmountConverter` and `LightningWallet`, add `cw_core` tests to GitHub workflow, and remove unused/deprecated logic * refactor: enhance `changeReceiveAmount` with `isCanonical` support, improve null handling, and refine amount parsing logic * minor fix * try/catch the lightning log file error * feat: add copy-to-clipboard functionality for transaction amounts, fix typo in trailing widget method name (#3219) * Cw 1234 disallow sp and mweb addresses as refund for swaps (#3187) * disallow SP and MWEB for swaps * Simplify address selection in addressForExchange Refactor addressForExchange method to simplify address selection logic. * validate refund and receive address types * localize address validation messages * minor ui fix [skip ci] * minor fix [skip ci] * Revert "localize address validation messages" * fix translations --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * refactor: replace manual amount sanitization with `sanitized()` extension across the codebase, add unit tests for `AmountSanitizer` * fix: correct `sanitized()` test expectations for decimal handling in `amount_sanitizer_test.dart` * refactor: fee display logic and append currency symbol in `send_confirm_sheet.dart` * refactor: simplify `ZcashBalance` class by replacing custom fields with inherited properties, update references accordingly * refactor: replace hardcoded USDT contract address with `DefaultTronTokens` lookup, improve maintainability * refactor: replace `currency` with `inputAmount.currency` in Solana transaction signing methods to avoid inconsistency * fix: handle null `fee` in `electrum_transaction_info.dart` to prevent potential runtime errors * refactor: replace raw fee and amount handling with `Money` object for improved consistency and maintainability * fix: limit fiat balance display to 2 decimal places in `balance_view_model.dart` * refactor: replace raw `amount` and `fee` types with `Money` in Solana transaction methods for consistency and precision * refactor: replace raw `amount` and `fee` handling with `Money` and improve balance calculation consistency across view models * refactor: replace raw balance strings with `Money` and simplify `UnconfirmedBalanceModal` widget logic * refactor: replace `print` with `printV` for better logging, add equality and hashCode overrides to `ERC20Currency` for object comparison * feat: prepare deuro savings migration * feat: add V1 savings migration support and improve formatting consistency in DEuro view model * feat: add V1 savings withdrawal support and update associated UI components and logic * refactor: replace `Image.asset` with `CakeImageWidget`, update balance string handling, and streamline address validation logic * refactor: use `Money.tryParse` with fallback to `Money.zero` in `NanoBalance.fromRawString` for safer balance parsing * refactor: replace `ERC20Currency` with `Erc20Token` for consistency and remove unused `evm_erc20_currency.dart` file * refactor: add computed `hasDepositAmount` for deposit null-checks, optimize balance handling in `zano_wallet`, update Breez SDK to v0.14.0, and implement equality/hashCode for `Currency` * refactor: rename `amountSats` to `amount` in `PrepareLnurlPayRequest` for consistency * revert: downgrade Breez SDK to v0.11.0 in `pubspec.yaml` and update relevant references in `pubspec.lock` * fix: use `cryptoAmount.copyWith` for currency consistency and replace `Image.asset` with `CakeImageWidget` in swap address modal * fix: ensure currency consistency by using `cryptoAmount.copyWith` in Solana and EVM wallet transactions * refactor: simplify CSV row generation logic by removing `_splitAmountCurrency` and streamlining amount/fee handling * fix: LateInitializationError Field 'sdk' has not been initialized * fix: use `Money.tryParse` for safer parsing of deposit and receive amounts * chore: remove debug print statement * refactor: update transaction processing to consistently use `Money` for amounts and fees * refactor: improve currency and amount handling, simplify logic, and enhance code readability in Send and Swap pages * refactor: integrate `DecimalInputFormatter` for consistent decimal input across Send and Swap pages, simplify input handling logic * fix: "fiatMode" per output on sendPage * refactor: make transaction_details_height applicable more readable * fix: use correct base unit for deposit * refactor: use Money in estimateFakeSendAllTxAmount method * fix decred frozen balance minor fixes * fix decred frozen balance minor fixes * fix: automatically switch to lightning if lnurl or invoice detected * fix: getBolt11Amount test cases * fix: standardise crypto amount formatting and display * fix: use base unit for Tron transaction fee parsing * refactor: hardcode fractional digits to 8 in `AmountParsingProxy` display methods * refactor: ensure consistent 8-decimal precision for fee formatting in `PendingTransaction` * fix: use `AmountParsingProxy` for available balance display in exchange view model * fix: improve amount formatting and layout in swap and send confirmation sheets * chore: remove redundant decimal truncation in balance view model and send confirmation sheet * fix: allow for editing text field if it is filled with "ALL" * fix: allow for editing text field if it is filled with "ALL" * fix: update currency detection logic for lightning mode * avoid potential infinite loop [skip ci] * fix: getDisplayCryptoAmount * fix: update estimated fee calculation * chore: simplify fee calculation logic [skip-ci] * fix value display in hidden mode * fix stale state in send confirm sheet * fix decimals for mweb * fix available balance display for sol/trx on swap * restore proper hint text * fix swap receive amount calculation for xno --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> Co-authored-by: Serhii <17529954+serhii-bor@users.noreply.github.com> Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>

Konstantin Ullrich committed Jun 30, 2026 at 01:45 UTC 75f3fe735b1ad42f075e1845450060148d2514e4
178 files changed +4235 -3557
.github/workflows/pr_test_build_linux.yml
+3
@@ -227,6 +227,9 @@ jobs:
227 xmessage -timeout 30 "restore_wallet_through_seeds_flow_test" &
228 rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet
229 exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/restore_wallet_through_seeds_flow_test.dart
230 + - name: Test [cw_core]
231 + timeout-minutes: 15
232 + run: cd cw_core && flutter test
233 - name: Test [cw_monero]
234 timeout-minutes: 15
235 run: cd cw_monero && flutter test
cw_bitcoin/lib/bitcoin_wallet.dart
+20 -19
@@ -24,10 +24,10 @@ import 'package:cw_bitcoin/psbt/signer.dart';
24 import 'package:cw_bitcoin/psbt/transaction_builder.dart';
25 import 'package:cw_bitcoin/psbt/v0_deserialize.dart';
26 import 'package:cw_bitcoin/psbt/v0_finalizer.dart';
27 +import 'package:cw_core/amount/money.dart';
28 import 'package:cw_core/crypto_currency.dart';
29 import 'package:cw_core/encryption_file_utils.dart';
30 import 'package:cw_core/output_info.dart';
30 -import 'package:cw_core/parse_fixed.dart';
31 import 'package:cw_core/payjoin_session.dart';
32 import 'package:cw_core/pending_transaction.dart';
33 import 'package:cw_core/unspent_coin_type.dart';
@@ -347,13 +347,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
347
348 final lBalance = await lightningWallet!.getBalance();
349
350 - this.balance[CryptoCurrency.btcln] =
351 - ElectrumBalance(confirmed: lBalance.toInt(), unconfirmed: 0, frozen: 0);
350 + this.balance[CryptoCurrency.btcln] = ElectrumBalance(
351 + confirmed: lBalance,
352 + unconfirmed: Money.zero(CryptoCurrency.btcln),
353 + frozen: Money.zero(CryptoCurrency.btcln));
354
355 return ElectrumBalance(
356 confirmed: balance.confirmed,
357 unconfirmed: balance.unconfirmed,
356 - frozen: balance.frozen.toInt(),
358 + frozen: balance.frozen ?? Money.zero(currency),
359 );
360 }
361
@@ -493,19 +495,19 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
495 final isLNCompatible = await lightningWallet?.isCompatible(lnAddr);
496 if ((credentials.coinTypeToSpendFrom == UnspentCoinType.lightning && lightningWallet != null) ||
497 isLNCompatible == true) {
496 - BigInt amount;
498 + Money amount;
499 if (credentials.outputs.first.sendAll) {
500 amount = await lightningWallet!.getBalance();
501 } else {
500 - amount = parseFixed(
501 - credentials.outputs.first.cryptoAmount?.isNotEmpty == true
502 - ? credentials.outputs.first.cryptoAmount!
503 - : "0",
504 - 8);
502 + amount = credentials.outputs.first.cryptoAmount;
503 }
504
507 - return lightningWallet!.createTransaction(lnAddr, amount > BigInt.zero ? amount : null,
508 - credentials.priority, credentials.outputs.first.sendAll);
505 +
506 + return lightningWallet!.createTransaction(
507 + lnAddr,
508 + amount.amount > BigInt.zero ? amount.amount : null,
509 + credentials.priority,
510 + credentials.outputs.first.sendAll,);
511 }
512
513 final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction;
@@ -524,7 +526,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
526 ))
527 .toList(),
528 cwOutputs: credentials.outputs,
527 - fee: BigInt.from(tx.fee),
529 + fee: tx.fee.amount,
530 network: network,
531 memo: credentials.outputs.first.memo,
532 outputOrdering: BitcoinOrdering.none,
@@ -543,8 +545,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
545 tx.commitOverride = () async {
546 final sender =
547 await payjoinManager.initSender(payjoinUri!, originalPsbt, int.parse(tx.feeRate));
546 - payjoinManager.spawnNewSender(
547 - sender: sender, pjUrl: payjoinUri, amount: BigInt.from(tx.amount));
548 + payjoinManager.spawnNewSender(sender: sender, pjUrl: payjoinUri, amount: tx.amount.amount);
549 };
550
551 return tx;
@@ -564,8 +565,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
565 btcTx,
566 type,
567 electrumClient: electrumClient,
567 - amount: 0,
568 - fee: 0,
568 + amount: Money.zero(currency),
569 + fee: Money.zero(currency),
570 feeRate: "",
571 network: network,
572 hasChange: true,
@@ -622,8 +623,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
623 btcTx,
624 type,
625 electrumClient: electrumClient,
625 - amount: 0,
626 - fee: 0,
626 + amount: Money.zero(currency),
627 + fee: Money.zero(currency),
628 feeRate: "",
629 network: network,
630 hasChange: true,
cw_bitcoin/lib/bitcoin_wallet_addresses.dart
+18 -5
@@ -153,11 +153,24 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
153
154 @override
155 String get addressForExchange {
156 - if (addressPageType == LightningAddressType.p2l) {
157 - final addresses = receiveAddresses
158 - .where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed);
159 - return addresses.first.address;
156 +
157 + final current = getFreshAddress();
158 + final availableReceiveAddresses = receiveAddresses.where((element) =>
159 + !element.isUsed &&
160 + !element.isHidden &&
161 + !hiddenAddresses.contains(element.address));
162 +
163 + final bool isSilentPaymentsPage = addressPageType == SilentPaymentsAddresType.p2sp;
164 + final bool isLightningPage = addressPageType == LightningAddressType.p2l;
165 +
166 + if (isSilentPaymentsPage || isLightningPage) {
167 + final segwit = availableReceiveAddresses
168 + .where((e) => e.type == SegwitAddresType.p2wpkh)
169 + .map((e) => e.address)
170 + .toList();
171 +
172 + return segwit.isNotEmpty ? segwit.first : current;
173 }
161 - return address;
174 + return current;
175 }
176 }
cw_bitcoin/lib/electrum_balance.dart
+32 -29
@@ -1,49 +1,52 @@
1 import 'dart:convert';
2
3 +import 'package:cw_core/amount/money.dart';
4 import 'package:cw_core/balance.dart';
5 +import 'package:cw_core/currency.dart';
6
7 class ElectrumBalance extends Balance {
8 ElectrumBalance({
9 required this.confirmed,
10 required this.unconfirmed,
9 - required int frozen,
10 - this.secondConfirmed = 0,
11 - this.secondUnconfirmed = 0,
12 - }) : this._frozen = frozen, super.fromInt(confirmed, unconfirmed,
13 - secondAvailable: secondConfirmed, secondAdditional: secondUnconfirmed, frozen: frozen);
11 + required this.frozen,
12 + this.secondConfirmed,
13 + this.secondUnconfirmed,
14 + }) : super(confirmed, unconfirmed,
15 + secondAvailable: secondConfirmed, secondUnavailable: secondUnconfirmed);
16
15 - static ElectrumBalance? fromJSON(String? jsonSource) {
17 + Money confirmed;
18 + Money unconfirmed;
19 + Money? secondConfirmed;
20 + Money? secondUnconfirmed;
21 +
22 + @override
23 + Money get available => (confirmed + unconfirmed) - frozen;
24 +
25 + @override
26 + Money get unavailable => unconfirmed;
27 +
28 + @override
29 + Money frozen;
30 +
31 + static ElectrumBalance? fromJSON(String? jsonSource, Currency currency) {
32 if (jsonSource == null) return null;
33
34 final decoded = json.decode(jsonSource) as Map;
35
36 return ElectrumBalance(
21 - confirmed: decoded['confirmed'] as int? ?? 0,
22 - unconfirmed: decoded['unconfirmed'] as int? ?? 0,
23 - frozen: decoded['frozen'] as int? ?? 0,
24 - secondConfirmed: decoded['secondConfirmed'] as int? ?? 0,
25 - secondUnconfirmed: decoded['secondUnconfirmed'] as int? ?? 0,
37 + confirmed: Money.fromInt(decoded['confirmed'] as int? ?? 0, currency),
38 + unconfirmed: Money.fromInt(decoded['unconfirmed'] as int? ?? 0, currency),
39 + frozen: Money.fromInt(decoded['frozen'] as int? ?? 0, currency),
40 + secondConfirmed: Money.fromInt(decoded['secondConfirmed'] as int? ?? 0, currency),
41 + secondUnconfirmed: Money.fromInt(decoded['secondUnconfirmed'] as int? ?? 0, currency),
42 );
43 }
44
29 - int confirmed;
30 - int unconfirmed;
31 - final int _frozen;
32 -
33 - @override
34 - BigInt get frozen => BigInt.from(_frozen);
35 -
36 - int secondConfirmed = 0;
37 - int secondUnconfirmed = 0;
38 -
39 - @override
40 - BigInt get fullAvailableBalance => BigInt.from((confirmed + unconfirmed) + secondConfirmed - _frozen);
41 -
45 String toJSON() => json.encode({
43 - 'confirmed': confirmed,
44 - 'unconfirmed': unconfirmed,
45 - 'frozen': _frozen,
46 - 'secondConfirmed': secondConfirmed,
47 - 'secondUnconfirmed': secondUnconfirmed,
46 + 'confirmed': confirmed.amount.toInt(),
47 + 'unconfirmed': unavailable.amount.toInt(),
48 + 'frozen': frozen.amount.toInt(),
49 + 'secondConfirmed': secondAvailable?.amount.toInt() ?? 0,
50 + 'secondUnconfirmed': secondUnavailable?.amount.toInt() ?? 0,
51 });
52 }
cw_bitcoin/lib/electrum_transaction_info.dart
+17 -29
@@ -5,10 +5,11 @@ import 'package:cw_bitcoin/address_from_output.dart';
5 import 'package:cw_bitcoin/bitcoin_address_record.dart';
6 import 'package:cw_bitcoin/bitcoin_amount_format.dart';
7 import 'package:cw_bitcoin/bitcoin_unspent.dart';
8 +import 'package:cw_core/amount/money.dart';
9 +import 'package:cw_core/crypto_currency.dart';
10 import 'package:cw_core/currency_for_wallet_type.dart';
11 import 'package:cw_core/transaction_direction.dart';
12 import 'package:cw_core/transaction_info.dart';
11 -import 'package:cw_core/format_amount.dart';
13 import 'package:cw_core/wallet_type.dart';
14 import 'package:hex/hex.dart';
15
@@ -31,8 +32,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
32 this.type, {
33 required String id,
34 int? height,
34 - required int amount,
35 - int? fee,
35 + required Money amount,
36 + Money? fee,
37 List<String>? inputAddresses,
38 List<String>? outputAddresses,
39 required TransactionDirection direction,
@@ -106,9 +107,9 @@ class ElectrumTransactionInfo extends TransactionInfo {
107 height: height,
108 isPending: false,
109 isReplaced: false,
109 - fee: fee,
110 + fee: Money.fromInt(fee, walletTypeToCryptoCurrency(type)),
111 direction: direction,
111 - amount: amount,
112 + amount: Money.fromInt(amount, walletTypeToCryptoCurrency(type)),
113 date: date,
114 confirmations: confirmations);
115 }
@@ -193,9 +194,9 @@ class ElectrumTransactionInfo extends TransactionInfo {
194 isReplaced: false,
195 inputAddresses: inputAddresses,
196 outputAddresses: outputAddresses,
196 - fee: fee,
197 + fee: Money.fromInt(fee, walletTypeToCryptoCurrency(type)),
198 direction: direction,
198 - amount: amount,
199 + amount: Money.fromInt(amount, walletTypeToCryptoCurrency(type)),
200 date: date,
201 isHogEx: isHogEx,
202 confirmations: bundle.confirmations);
@@ -205,13 +206,17 @@ class ElectrumTransactionInfo extends TransactionInfo {
206 final inputAddresses = data['inputAddresses'] as List<dynamic>? ?? [];
207 final outputAddresses = data['outputAddresses'] as List<dynamic>? ?? [];
208 final unspents = data['unspents'] as List<dynamic>? ?? [];
209 + final additionalInfo = data['additionalInfo'] as Map<String, dynamic>?;
210 + final isLightning = (additionalInfo?['isLightning'] as bool?) == true;
211 +
212 + final currency = isLightning ? CryptoCurrency.btcln : walletTypeToCryptoCurrency(type);
213
214 return ElectrumTransactionInfo(
215 type,
216 id: data['id'] as String,
217 height: data['height'] as int?,
213 - amount: data['amount'] as int,
214 - fee: data['fee'] as int,
218 + amount: Money.fromInt(data['amount'] as int, currency),
219 + fee: data['fee'] != null ? Money.fromInt(data['fee'] as int, currency) : null,
220 direction: parseTransactionDirectionFromInt(data['direction'] as int),
221 date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
222 isPending: data['isPending'] as bool,
@@ -227,29 +232,12 @@ class ElectrumTransactionInfo extends TransactionInfo {
232 BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map<String, dynamic>))
233 .toList(),
234 isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false,
230 - additionalInfo: data['additionalInfo'] as Map<String, dynamic>?,
235 + additionalInfo: additionalInfo,
236 );
237 }
238
239 final WalletType type;
240
236 - String? _fiatAmount;
237 -
238 - @override
239 - String amountFormatted() =>
240 - '${walletTypeToCryptoCurrency(type).formatAmount(BigInt.from(amount))} ${walletTypeToCryptoCurrency(type).title}';
241 -
242 - @override
243 - String? feeFormatted() => fee != null
244 - ? '${walletTypeToCryptoCurrency(type).formatAmount(BigInt.from(fee!))} ${walletTypeToCryptoCurrency(type).title}'
245 - : '';
246 -
247 - @override
248 - String fiatAmount() => _fiatAmount ?? '';
249 -
250 - @override
251 - void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
252 -
241 ElectrumTransactionInfo updated(ElectrumTransactionInfo info) {
242 return ElectrumTransactionInfo(info.type,
243 id: id,
@@ -270,13 +258,13 @@ class ElectrumTransactionInfo extends TransactionInfo {
258 final m = <String, dynamic>{};
259 m['id'] = id;
260 m['height'] = height;
273 - m['amount'] = amount;
261 + m['amount'] = amount.amount.toInt();
262 m['direction'] = direction.index;
263 m['date'] = date.millisecondsSinceEpoch;
264 m['isPending'] = isPending;
265 m['isReplaced'] = isReplaced;
266 m['confirmations'] = confirmations;
279 - m['fee'] = fee;
267 + m['fee'] = fee?.amount.toInt();
268 m['to'] = to;
269 m['unspents'] = unspents?.map((e) => e.toJson()).toList() ?? [];
270 m['inputAddresses'] = inputAddresses;
cw_bitcoin/lib/electrum_wallet.dart
+91 -86
@@ -19,6 +19,7 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart';
19 import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
20 import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
21 import 'package:cw_bitcoin/bitcoin_unspent.dart';
22 +import 'package:cw_bitcoin/bitcoin_wallet.dart';
23 import 'package:cw_bitcoin/bitcoin_wallet_keys.dart';
24 import 'package:cw_bitcoin/electrum.dart' as electrum;
25 import 'package:cw_bitcoin/electrum_balance.dart';
@@ -27,20 +28,26 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart';
28 import 'package:cw_bitcoin/electrum_transaction_info.dart';
29 import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
30 import 'package:cw_bitcoin/exceptions.dart';
31 +import 'package:cw_bitcoin/litecoin_wallet.dart';
32 import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
33 import 'package:cw_bitcoin/utils.dart';
34 +import 'package:cw_core/amount/money.dart';
35 import 'package:cw_core/crypto_currency.dart';
36 import 'package:cw_core/encryption_file_utils.dart';
37 import 'package:cw_core/get_height_by_date.dart';
38 +import 'package:cw_core/hardware/hardware_wallet_service.dart';
39 import 'package:cw_core/node.dart';
40 import 'package:cw_core/output_info.dart';
41 import 'package:cw_core/pathForWallet.dart';
42 import 'package:cw_core/pending_transaction.dart';
43 +import 'package:cw_core/root_dir.dart';
44 import 'package:cw_core/sync_status.dart';
45 import 'package:cw_core/transaction_direction.dart';
46 import 'package:cw_core/transaction_priority.dart';
47 import 'package:cw_core/unspent_coin_type.dart';
48 import 'package:cw_core/unspent_coins_info.dart';
49 +import 'package:cw_core/utils/print_verbose.dart';
50 +import 'package:cw_core/utils/proxy_wrapper.dart';
51 import 'package:cw_core/utils/socket_health_logger.dart';
52 import 'package:cw_core/utils/tor/abstract.dart';
53 import 'package:cw_core/wallet_base.dart';
@@ -52,6 +59,7 @@ import 'package:hex/hex.dart';
59 import 'package:hive/hive.dart';
60 import 'package:mobx/mobx.dart';
61 import 'package:rxdart/subjects.dart';
62 +import 'package:shared_preferences/shared_preferences.dart';
63 import 'package:sp_scanner/sp_scanner.dart';
64
65 part 'electrum_wallet.g.dart';
@@ -94,9 +102,9 @@ abstract class ElectrumWalletBase
102 ? {
103 currency: initialBalance ??
104 ElectrumBalance(
97 - confirmed: 0,
98 - unconfirmed: 0,
99 - frozen: 0,
105 + confirmed: Money.zero(currency),
106 + unconfirmed: Money.zero(currency),
107 + frozen: Money.zero(currency),
108 )
109 }
110 : {}),
@@ -311,7 +319,7 @@ abstract class ElectrumWalletBase
319 bool get useLightning => _useLightning && LightningWallet.isAvailable;
320
321 set useLightning(bool val) => _useLightning = val && LightningWallet.isAvailable;
314 -
322 +
323 @observable
324 bool _useLightning;
325
@@ -597,13 +605,13 @@ abstract class ElectrumWalletBase
605 : newUnspents[0].value;
606
607 if (existingTxInfo.direction == TransactionDirection.incoming) {
600 - existingTxInfo.amount += newAmount;
608 + existingTxInfo.amount += Money.fromInt(newAmount, currency);
609 }
610
611 // Updates existing TX
612 transactionHistory.addOne(existingTxInfo);
613 // Update balance record
606 - balance[currency]!.confirmed += newAmount;
614 + balance[currency]!.confirmed += Money.fromInt(newAmount, currency);
615 }
616 } else {
617 // else: First time seeing this TX after scanning
@@ -865,9 +873,9 @@ abstract class ElectrumWalletBase
873 }
874 }
875
868 - int get networkDustAmount => 546;
876 + BigInt get networkDustAmount => BigInt.from(546);
877
870 - bool _isBelowDust(int amount) => amount <= networkDustAmount && network != BitcoinNetwork.testnet;
878 + bool _isBelowDust(BigInt amount) => amount <= networkDustAmount && network != BitcoinNetwork.testnet;
879
880 UtxoDetails _createUTXOS({
881 required bool sendAll,
@@ -1031,9 +1039,9 @@ abstract class ElectrumWalletBase
1039 }
1040
1041 // Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change
1034 - int amount = utxoDetails.allInputsAmount - fee;
1042 + final amount = BigInt.from(utxoDetails.allInputsAmount - fee);
1043
1036 - if (amount <= 0) {
1044 + if (amount <= BigInt.zero) {
1045 throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee);
1046 }
1047
@@ -1045,7 +1053,7 @@ abstract class ElectrumWalletBase
1053 if (outputs.length == 1) {
1054 outputs[0] = BitcoinOutput(
1055 address: outputs.last.address,
1048 - value: BigInt.from(amount),
1056 + value: amount,
1057 isSilentPayment: hasSilentPayment,
1058 );
1059 }
@@ -1054,8 +1062,8 @@ abstract class ElectrumWalletBase
1062 utxos: utxoDetails.utxos,
1063 inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
1064 publicKeys: utxoDetails.publicKeys,
1057 - fee: fee,
1058 - amount: amount,
1065 + fee: Money.fromInt(fee, currency),
1066 + amount: Money(amount, currency),
1067 isSendAll: true,
1068 hasChange: false,
1069 memo: memo,
@@ -1065,7 +1073,7 @@ abstract class ElectrumWalletBase
1073 }
1074
1075 Future<EstimatedTxResult> estimateTxForAmount(
1068 - int credentialsAmount,
1076 + Money credentialsAmount,
1077 List<BitcoinOutput> outputs,
1078 List<BitcoinOutput> updatedOutputs,
1079 int feeRate, {
@@ -1076,7 +1084,7 @@ abstract class ElectrumWalletBase
1084 UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
1085 }) async {
1086 // Attempting to send less than the dust limit
1079 - if (_isBelowDust(credentialsAmount)) {
1087 + if (_isBelowDust(credentialsAmount.amount)) {
1088 throw BitcoinTransactionNoDustException();
1089 }
1090
@@ -1122,7 +1130,7 @@ abstract class ElectrumWalletBase
1130
1131 final utxoDetails = _createUTXOS(
1132 sendAll: false,
1125 - credentialsAmount: credentialsAmount,
1133 + credentialsAmount: credentialsAmount.amount.toInt(),
1134 inputsCount: inputsCount,
1135 paysToSilentPayment: hasSilentPayment,
1136 coinTypeToSpendFrom: coinTypeToSpendFrom,
@@ -1133,8 +1141,7 @@ abstract class ElectrumWalletBase
1141 utxoDetails.utxos.length ==
1142 utxoDetails.availableInputs.length - utxoDetails.unconfirmedCoins.length;
1143
1136 - // How much is being spent - how much is being sent
1137 - int amountLeftForChangeAndFee = utxoDetails.allInputsAmount - credentialsAmount;
1144 + final amountLeftForChangeAndFee = utxoDetails.allInputsAmount - credentialsAmount.amount.toInt();
1145
1146 if (amountLeftForChangeAndFee <= 0) {
1147 if (!spendingAllCoins) {
@@ -1213,9 +1220,9 @@ abstract class ElectrumWalletBase
1220 throw BitcoinTransactionNoFeeException();
1221 }
1222
1216 - int amount = credentialsAmount;
1223 + var amount = credentialsAmount;
1224 final lastOutput = updatedOutputs.last;
1218 - final amountLeftForChange = amountLeftForChangeAndFee - fee;
1225 + final amountLeftForChange = BigInt.from(amountLeftForChangeAndFee - fee);
1226
1227 if (_isBelowDust(amountLeftForChange)) {
1228 // If has change that is lower than dust, will end up with tx rejected by network rules
@@ -1226,7 +1233,7 @@ abstract class ElectrumWalletBase
1233 // If the computed change is negative or below dust:
1234 // - negative: try a no-change tx (recalculate fee without change)
1235 // - non-negative but dust: drop change and add remainder to fee
1229 - if (amountLeftForChange < 0) {
1236 + if (amountLeftForChange < BigInt.zero) {
1237 final tempNoChange = outputs.map((o) => o).toList();
1238 final feeNoChange = await calcFee(
1239 utxos: utxoDetails.utxos,
@@ -1237,7 +1244,7 @@ abstract class ElectrumWalletBase
1244 inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
1245 vinOutpoints: utxoDetails.vinOutpoints,
1246 );
1240 - final leftover = utxoDetails.allInputsAmount - credentialsAmount - feeNoChange;
1247 + final leftover = utxoDetails.allInputsAmount - credentialsAmount.amount.toInt() - feeNoChange;
1248
1249 if (leftover >= 0) {
1250 final finalFee = feeNoChange + leftover; // absorb tiny remainder
@@ -1245,7 +1252,7 @@ abstract class ElectrumWalletBase
1252 utxos: utxoDetails.utxos,
1253 inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
1254 publicKeys: utxoDetails.publicKeys,
1248 - fee: finalFee,
1255 + fee: Money.fromInt(finalFee, currency),
1256 amount: amount,
1257 hasChange: false,
1258 isSendAll: spendingAllCoins,
@@ -1274,13 +1281,13 @@ abstract class ElectrumWalletBase
1281
1282 // if the amount left for change is less than dust, but not less than 0
1283 // then add it to the fees
1277 - fee += amountLeftForChange;
1284 + fee += amountLeftForChange.toInt();
1285
1286 return EstimatedTxResult(
1287 utxos: utxoDetails.utxos,
1288 inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
1289 publicKeys: utxoDetails.publicKeys,
1283 - fee: fee,
1290 + fee: Money.fromInt(fee, currency),
1291 amount: amount,
1292 hasChange: false,
1293 isSendAll: spendingAllCoins,
@@ -1292,13 +1299,13 @@ abstract class ElectrumWalletBase
1299 // Here, lastOutput already is change, return the amount left without the fee to the user's address.
1300 updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput(
1301 address: lastOutput.address,
1295 - value: BigInt.from(amountLeftForChange),
1302 + value: amountLeftForChange,
1303 isSilentPayment: lastOutput.isSilentPayment,
1304 isChange: true,
1305 );
1306 outputs[outputs.length - 1] = BitcoinOutput(
1307 address: lastOutput.address,
1301 - value: BigInt.from(amountLeftForChange),
1308 + value: amountLeftForChange,
1309 isSilentPayment: lastOutput.isSilentPayment,
1310 isChange: true,
1311 );
@@ -1307,7 +1314,7 @@ abstract class ElectrumWalletBase
1314 utxos: utxoDetails.utxos,
1315 inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
1316 publicKeys: utxoDetails.publicKeys,
1310 - fee: fee,
1317 + fee: Money.fromInt(fee, currency),
1318 amount: amount,
1319 hasChange: true,
1320 isSendAll: spendingAllCoins,
@@ -1318,7 +1325,7 @@ abstract class ElectrumWalletBase
1325 }
1326 }
1327
1321 - Future<int> _maxSpendableNoChangeAmount({
1328 + Future<Money> _maxSpendableNoChangeAmount({
1329 required BitcoinOutput initialOutput,
1330 required int feeRate,
1331 String? memo,
@@ -1350,7 +1357,7 @@ abstract class ElectrumWalletBase
1357 );
1358
1359 final maxSpendable = utxoDetailsAll.allInputsAmount - feeNoChange;
1353 - return maxSpendable > 0 ? maxSpendable : 0;
1360 + return Money.fromInt(maxSpendable > 0 ? maxSpendable : 0, currency);
1361 }
1362
1363 Future<int> calcFee({
@@ -1397,13 +1404,13 @@ abstract class ElectrumWalletBase
1404 final memo = transactionCredentials.outputs.first.memo;
1405 final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom;
1406
1400 - int credentialsAmount = 0;
1401 - bool hasSilentPayment = false;
1407 + var credentialsAmount = Money.zero(currency);
1408 + var hasSilentPayment = false;
1409
1410 for (final out in transactionCredentials.outputs) {
1404 - final outputAmount = out.formattedCryptoAmount!;
1411 + final outputAmount = out.cryptoAmount;
1412
1406 - if (!sendAll && _isBelowDust(outputAmount)) {
1413 + if (!sendAll && _isBelowDust(outputAmount.amount)) {
1414 throw BitcoinTransactionNoDustException();
1415 }
1416
@@ -1427,13 +1434,13 @@ abstract class ElectrumWalletBase
1434 // The value will be changed after estimating the Tx size and deducting the fee from the total to be sent
1435 outputs.add(BitcoinOutput(
1436 address: address,
1430 - value: BigInt.from(0),
1437 + value: BigInt.zero,
1438 isSilentPayment: isSilentPayment,
1439 ));
1440 } else {
1441 outputs.add(BitcoinOutput(
1442 address: address,
1436 - value: BigInt.from(outputAmount),
1443 + value: outputAmount.amount,
1444 isSilentPayment: isSilentPayment,
1445 ));
1446 }
@@ -1486,7 +1493,7 @@ abstract class ElectrumWalletBase
1493 utxos: estimatedTx.utxos,
1494 outputs: updatedOutputs,
1495 publicKeys: estimatedTx.publicKeys,
1489 - fee: BigInt.from(estimatedTx.fee),
1496 + fee: estimatedTx.fee.amount,
1497 network: network,
1498 memo: estimatedTx.memo,
1499 outputOrdering: BitcoinOrdering.none,
@@ -1504,7 +1511,8 @@ abstract class ElectrumWalletBase
1511 network: network,
1512 hasChange: estimatedTx.hasChange,
1513 isSendAll: estimatedTx.isSendAll,
1507 - hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot,
1514 + hasTaprootInputs: false,
1515 + // ToDo: (Konsti) Support Taproot,
1516 isViewOnly: false,
1517 )..addListener((transaction) async {
1518 transactionHistory.addOne(transaction);
@@ -1518,7 +1526,7 @@ abstract class ElectrumWalletBase
1526 txb = ForkedTransactionBuilder(
1527 utxos: estimatedTx.utxos,
1528 outputs: updatedOutputs,
1521 - fee: BigInt.from(estimatedTx.fee),
1529 + fee: estimatedTx.fee.amount,
1530 network: network,
1531 memo: estimatedTx.memo,
1532 outputOrdering: BitcoinOrdering.none,
@@ -1528,7 +1536,7 @@ abstract class ElectrumWalletBase
1536 txb = BitcoinTransactionBuilder(
1537 utxos: estimatedTx.utxos,
1538 outputs: updatedOutputs,
1531 - fee: BigInt.from(estimatedTx.fee),
1539 + fee: estimatedTx.fee.amount,
1540 network: network,
1541 memo: estimatedTx.memo,
1542 outputOrdering: BitcoinOrdering.none,
@@ -2115,12 +2123,12 @@ abstract class ElectrumWalletBase
2123 allInputsAmount += outTransaction.amount.toInt();
2124 }
2125
2118 - int totalOutAmount = bundle.originalTransaction.outputs
2126 + final totalOutAmount = bundle.originalTransaction.outputs
2127 .fold<int>(0, (previousValue, element) => previousValue + element.amount.toInt());
2128 var currentFee = allInputsAmount - totalOutAmount;
2129
2122 - int remainingFee = (newFee - currentFee > 0) ? newFee - currentFee : newFee;
2123 - return totalBalance - receiverAmount - remainingFee >= networkDustAmount;
2130 + final remainingFee = (newFee - currentFee > 0) ? newFee - currentFee : newFee;
2131 + return totalBalance - receiverAmount - remainingFee >= networkDustAmount.toInt();
2132 }
2133
2134 Future<PendingBitcoinTransaction> replaceByFee(String hash, int newFee) async {
@@ -2195,14 +2203,14 @@ abstract class ElectrumWalletBase
2203 int totalOutAmount =
2204 outputs.fold<int>(0, (previousValue, output) => previousValue + output.value.toInt());
2205 int currentFee = allInputsAmount - totalOutAmount;
2198 - int remainingFee = newFee - currentFee;
2206 + var remainingFee = BigInt.from(newFee - currentFee);
2207
2200 - if (remainingFee <= 0) {
2208 + if (remainingFee <= BigInt.zero) {
2209 throw Exception("New fee must be higher than the current fee.");
2210 }
2211
2212 // Deduct fee from change outputs first, if possible
2205 - if (remainingFee > 0) {
2213 + if (remainingFee > BigInt.zero) {
2214 final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
2215 for (int i = outputs.length - 1; i >= 0; i--) {
2216 final output = outputs[i];
@@ -2210,23 +2218,23 @@ abstract class ElectrumWalletBase
2218 .any((element) => element.address == output.address.toAddress(network));
2219
2220 if (isChange) {
2213 - int outputAmount = output.value.toInt();
2221 + final outputAmount = output.value;
2222 if (outputAmount > networkDustAmount) {
2215 - int deduction = (outputAmount - networkDustAmount >= remainingFee)
2223 + final deduction = (outputAmount - networkDustAmount >= remainingFee)
2224 ? remainingFee
2225 : outputAmount - networkDustAmount;
2226 outputs[i] = BitcoinOutput(
2219 - address: output.address, value: BigInt.from(outputAmount - deduction));
2227 + address: output.address, value: outputAmount - deduction);
2228 remainingFee -= deduction;
2229
2222 - if (remainingFee <= 0) break;
2230 + if (remainingFee <= BigInt.zero) break;
2231 }
2232 }
2233 }
2234 }
2235
2236 // If still not enough, add UTXOs until the fee is covered
2229 - if (remainingFee > 0) {
2237 + if (remainingFee > BigInt.zero) {
2238 final unusedUtxos = unspentCoins
2239 .where((utxo) => utxo.isSending && !utxo.isFrozen && utxo.confirmations! > 0)
2240 .toList();
@@ -2254,52 +2262,51 @@ abstract class ElectrumWalletBase
2262 ));
2263
2264 allInputsAmount += utxo.value;
2257 - remainingFee -= utxo.value;
2265 + remainingFee -= BigInt.from(utxo.value);
2266
2259 - if (remainingFee < 0) {
2267 + if (remainingFee < BigInt.zero) {
2268 final changeOutput = outputs.firstWhereOrNull((output) => walletAddresses.allAddresses
2269 .any((addr) => addr.address == output.address.toAddress(network)));
2270 if (changeOutput != null) {
2263 - final newValue = changeOutput.value.toInt() + (-remainingFee);
2271 + final newValue = changeOutput.value + (-remainingFee);
2272 outputs[outputs.indexOf(changeOutput)] =
2265 - BitcoinOutput(address: changeOutput.address, value: BigInt.from(newValue));
2273 + BitcoinOutput(address: changeOutput.address, value: newValue);
2274 } else {
2275 final changeAddress = await walletAddresses.getChangeAddress();
2276 outputs.add(BitcoinOutput(
2277 address: RegexUtils.addressTypeFromStr(changeAddress.address, network),
2270 - value: BigInt.from(-remainingFee)));
2278 + value: -remainingFee));
2279 }
2280
2273 - remainingFee = 0;
2281 + remainingFee = BigInt.zero;
2282 break;
2283 }
2284
2277 - if (remainingFee <= 0) break;
2285 + if (remainingFee <= BigInt.zero) break;
2286 }
2287 }
2288
2289 // Deduct from the receiver's output if remaining fee is still greater than 0
2282 - if (remainingFee > 0) {
2290 + if (remainingFee > BigInt.zero) {
2291 for (int i = 0; i < outputs.length; i++) {
2292 final output = outputs[i];
2285 - int outputAmount = output.value.toInt();
2293 + final outputAmount = output.value;
2294
2295 if (outputAmount > networkDustAmount) {
2288 - int deduction = (outputAmount - networkDustAmount >= remainingFee)
2296 + final deduction = (outputAmount - networkDustAmount >= remainingFee)
2297 ? remainingFee
2298 : outputAmount - networkDustAmount;
2299
2292 - outputs[i] = BitcoinOutput(
2293 - address: output.address, value: BigInt.from(outputAmount - deduction));
2300 + outputs[i] = BitcoinOutput(address: output.address, value: outputAmount - deduction);
2301 remainingFee -= deduction;
2302
2296 - if (remainingFee <= 0) break;
2303 + if (remainingFee <= BigInt.zero) break;
2304 }
2305 }
2306 }
2307
2308 // Final check if the remaining fee couldn't be deducted
2302 - if (remainingFee > 0) {
2309 + if (remainingFee > BigInt.zero) {
2310 throw Exception("Not enough funds to cover the fee.");
2311 }
2312
@@ -2344,8 +2351,8 @@ abstract class ElectrumWalletBase
2351 transaction,
2352 type,
2353 electrumClient: electrumClient,
2347 - amount: sendingAmount,
2348 - fee: newFee,
2354 + amount: Money.fromInt(sendingAmount, currency),
2355 + fee: Money.fromInt(newFee, currency),
2356 network: network,
2357 hasChange: changeOutputs.isNotEmpty,
2358 feeRate: newFee.toString(),
@@ -2586,16 +2593,15 @@ abstract class ElectrumWalletBase
2593
2594 final history = await electrumClient.getHistory(addressRecord.getScriptHash(network));
2595
2589 -
2596 if (history.isNotEmpty) {
2597 addressRecord.setAsUsed();
2598 walletAddresses.clearLockIfMatches(addressRecord.type, addressRecord.address);
2599
2594 - if(this is BitcoinWallet) {
2600 + if (this is BitcoinWallet) {
2601 //removes transactions no longer returned by the api, presumed replaced/invalid.
2602 transactionHistory.transactions.removeWhere(
2597 - (hash, tx) =>
2598 - tx.outputAddresses != null &&
2603 + (hash, tx) =>
2604 + tx.outputAddresses != null &&
2605 tx.outputAddresses!.contains(addressRecord.address) &&
2606 !history.any((newTransaction) => newTransaction['tx_hash'] == hash),
2607 );
@@ -3481,7 +3487,12 @@ abstract class ElectrumWalletBase
3487 // if we got null balance responses from the server, set our connection status to lost and return our last known balance:
3488 printV("got null balance responses from the server, setting connection status to lost");
3489 syncStatus = LostConnectionSyncStatus();
3484 - return balance[currency] ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
3490 + return balance[currency] ??
3491 + ElectrumBalance(
3492 + confirmed: Money.zero(currency),
3493 + unconfirmed: Money.zero(currency),
3494 + frozen: Money.zero(currency),
3495 + );
3496 }
3497
3498 for (var i = 0; i < balances.length; i++) {
@@ -3500,9 +3511,9 @@ abstract class ElectrumWalletBase
3511 }
3512
3513 return ElectrumBalance(
3503 - confirmed: totalConfirmed,
3504 - unconfirmed: totalUnconfirmed,
3505 - frozen: totalFrozen,
3514 + confirmed: Money.fromInt(totalConfirmed, currency),
3515 + unconfirmed: Money.fromInt(totalUnconfirmed, currency),
3516 + frozen: Money.fromInt(totalFrozen, currency),
3517 );
3518 }
3519
@@ -3820,12 +3831,6 @@ abstract class ElectrumWalletBase
3831 }
3832 }
3833
3823 - @override
3824 - String formatCryptoAmount(String amount) {
3825 - final amountBigInt = BigInt.parse(amount);
3826 - return currency.formatAmount(amountBigInt);
3827 - }
3828 -
3834 /// Checks the health of the socket connection
3835 /// and triggers a full reconnection if needed
3836 @override
@@ -4286,8 +4291,8 @@ Future<void> _handleScanSilentPayments(ScanData scanData) async {
4291 WalletType.bitcoin,
4292 id: txid,
4293 height: tweakHeight,
4289 - amount: 0,
4290 - fee: 0,
4294 + amount: Money.zero(CryptoCurrency.btc),
4295 + fee: Money.zero(CryptoCurrency.btc),
4296 direction: TransactionDirection.incoming,
4297 isReplaced: false,
4298 date: scanData.network == BitcoinNetwork.mainnet
@@ -4353,7 +4358,7 @@ Future<void> _handleScanSilentPayments(ScanData scanData) async {
4358 txInfo.unspents!.add(unspent);
4359 }
4360
4356 - txInfo.amount += unspent.value;
4361 + txInfo.amount += Money.fromInt(unspent.value, txInfo.amount.currency);
4362 });
4363 });
4364 });
@@ -4423,8 +4428,8 @@ class EstimatedTxResult {
4428 final List<UtxoWithAddress> utxos;
4429 final List<ECPrivateInfo> inputPrivKeyInfos;
4430 final Map<String, PublicKeyWithDerivationPath> publicKeys; // PubKey to derivationPath
4426 - final int fee;
4427 - final int amount;
4431 + final Money fee;
4432 + final Money amount;
4433 final bool spendsSilentPayment;
4434
4435 // final bool sendsToSilentPayment;
cw_bitcoin/lib/electrum_wallet_addresses.dart
+20 -3
@@ -170,9 +170,22 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
170 @computed
171 List<BitcoinAddressRecord> get allAddresses => _addresses;
172
173 + @observable
174 + bool addressRefreshToggle = false;
175 +
176 + @action
177 + String getFreshAddress() {
178 + addressRefreshToggle = !addressRefreshToggle;
179 + return address;
180 + }
181 +
182 + @override
183 + String get addressForExchange => getFreshAddress();
184 +
185 @override
186 @computed
187 String get address {
188 + final _ = addressRefreshToggle;
189 if (addressPageType == SilentPaymentsAddresType.p2sp) {
190 if (activeSilentAddress != null) {
191 return activeSilentAddress!;
@@ -198,7 +211,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
211 ];
212
213 final typeMatchingReceiveAddressesAll =
201 - typeMatchingAddressesAll.where((addr) => !addr.isUsed).toList();
214 + typeMatchingAddressesAll.where((addr) => !addr.isUsed && !hiddenAddresses.contains(addr.address)).toList();
215 final typeMatchingReceiveAddresses = <BitcoinAddressRecord>[
216 ...typeMatchingReceiveAddressesAll.where((a) => !a.isLegacyDerivation),
217 ...typeMatchingReceiveAddressesAll.where((a) => a.isLegacyDerivation),
@@ -221,10 +234,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
234 }
235
236 final locked = lockedReceiveAddressByType[addressPageType];
224 - if (locked != null) return locked;
237 + if (locked != null && !hiddenAddresses.contains(locked)) return locked;
238
239 final prev = previousAddressRecord;
227 - if (prev != null && prev.type == addressPageType && !prev.isUsed && !prev.isLegacyDerivation) {
240 + if (prev != null &&
241 + prev.type == addressPageType &&
242 + !prev.isUsed &&
243 + !prev.isLegacyDerivation &&
244 + !hiddenAddresses.contains(prev.address)) {
245 return prev.address;
246 }
247
cw_bitcoin/lib/electrum_wallet_snapshot.dart
+11 -3
@@ -2,6 +2,8 @@ import 'dart:convert';
2 import 'package:bitcoin_base/bitcoin_base.dart';
3 import 'package:cw_bitcoin/bitcoin_address_record.dart';
4 import 'package:cw_bitcoin/electrum_balance.dart';
5 +import 'package:cw_core/amount/money.dart';
6 +import 'package:cw_core/currency_for_wallet_type.dart';
7 import 'package:cw_core/encryption_file_utils.dart';
8 import 'package:cw_bitcoin/electrum_derivations.dart';
9 import 'package:cw_core/pathForWallet.dart';
@@ -92,9 +94,15 @@ class ElectrumWalletSnapshot {
94 final useLightning = data['useLightning'] as bool? ?? true;
95 final cachedLightningAddress = data['cachedLightningAddress'] as String?;
96
95 - final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ??
96 - ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
97 - final lightningBalance = ElectrumBalance.fromJSON(data['lightningBalance'] as String?);
97 + final currency = walletTypeToCryptoCurrency(type);
98 + final balance = ElectrumBalance.fromJSON(data['balance'] as String?, currency) ??
99 + ElectrumBalance(
100 + confirmed: Money.zero(currency),
101 + unconfirmed: Money.zero(currency),
102 + frozen: Money.zero(currency),
103 + );
104 + final lightningBalance =
105 + ElectrumBalance.fromJSON(data['lightningBalance'] as String?, currency);
106
107 var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
108 var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
cw_bitcoin/lib/lightning/lightning_wallet.dart
+53 -27
@@ -6,6 +6,9 @@ import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart';
6 import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
7 import 'package:cw_bitcoin/electrum_transaction_info.dart';
8 import 'package:cw_bitcoin/lightning/pending_lightning_transaction.dart';
9 +import 'package:cw_core/amount/money.dart';
10 +import 'package:cw_core/crypto_currency.dart';
11 +import 'package:cw_core/currency.dart';
12 import 'package:cw_core/transaction_direction.dart';
13 import 'package:cw_core/utils/print_verbose.dart';
14 import 'package:cw_core/wallet_type.dart';
@@ -20,7 +23,7 @@ class LightningWallet {
23 final String apiKey;
24 final String lnurlDomain;
25 final Network network;
23 - late BreezSdk sdk;
26 + BreezSdk? _sdk;
27
28 String? cachedAddress;
29
@@ -38,6 +41,10 @@ class LightningWallet {
41
42 static bool get isAvailable => Platform.isIOS || Platform.isAndroid || Platform.isMacOS;
43
44 + Currency get currency => CryptoCurrency.btcln;
45 +
46 + BreezSdk get sdk => _sdk!;
47 +
48 StreamSubscription<SdkEvent>? _eventSubscription;
49 Stream<SdkEvent>? _eventStream;
50
@@ -47,9 +54,25 @@ class LightningWallet {
54
55 void _subscribeToLogStream(File logFile) {
56 _logSubscription = _logStream?.listen((logEntry) {
50 - logFile.writeAsStringSync("[${logEntry.level}] ${logEntry.line}\n", mode: FileMode.append);
57 + try {
58 + // Check if file exists before writing (optional, but safer)
59 + if (!logFile.existsSync()) {
60 + logFile.createSync(recursive: true);
61 + }
62 + logFile.writeAsStringSync("[${logEntry.level}] ${logEntry.line}\n", mode: FileMode.append);
63 + } catch (e) {
64 + // Silently fail or use printV(e) so it doesn't crash the app
65 + printV("Failed to write to log: $e");
66 + }
67 }, onError: (e) {
52 - logFile.writeAsStringSync("[ERROR] $e\n", mode: FileMode.append);
68 + try {
69 + if (!logFile.existsSync()) {
70 + logFile.createSync(recursive: true);
71 + }
72 + logFile.writeAsStringSync("[ERROR] $e\n", mode: FileMode.append);
73 + } catch (err) {
74 + printV("Failed to write error to log: $err");
75 + }
76 });
77 }
78
@@ -76,7 +99,7 @@ class LightningWallet {
99 storageDir: "$appPath/.breez/",
100 );
101
79 - sdk = await connect(request: connectRequest);
102 + _sdk = await connect(request: connectRequest);
103
104 _eventStream ??= sdk.addEventListener().asBroadcastStream();
105 _logStream ??= initLogging().asBroadcastStream();
@@ -100,7 +123,7 @@ class LightningWallet {
123
124 Future<void> close() async {
125 _eventSubscription?.cancel();
103 - await sdk.disconnect();
126 + await _sdk?.disconnect();
127 _logSubscription?.cancel();
128 }
129
@@ -126,13 +149,14 @@ class LightningWallet {
149 request: ReceivePaymentRequest(paymentMethod: ReceivePaymentMethod.bitcoinAddress())))
150 .paymentRequest;
151
129 - Future<BigInt> getBalance() async {
152 + Future<Money> getBalance() async {
153 try {
131 - return (await sdk.getInfo(request: GetInfoRequest(ensureSynced: true))).balanceSats;
154 + return Money((await sdk.getInfo(request: GetInfoRequest(ensureSynced: true))).balanceSats,
155 + CryptoCurrency.btcln);
156 } on SdkError_Generic catch (_) {
157 } on SdkError_NetworkError catch (_) {}
158
135 - return BigInt.zero;
159 + return Money.zero(CryptoCurrency.btcln);
160 }
161
162 Future<String> registerAddress(String username) async {
@@ -188,11 +212,16 @@ class LightningWallet {
212 final lightningFeeSats = paymentMethod.lightningFeeSats;
213 final sparkTransferFeeSats = paymentMethod.sparkTransferFeeSats;
214
215 + final baseAmount = request.amount ?? amountSats;
216 + final amount = baseAmount != null
217 + ? Money(baseAmount, currency)
218 + : Money(paymentMethod.invoiceDetails.amountMsat ?? BigInt.zero, currency) /
219 + BigInt.from(1000);
220 +
221 return PendingLightningTransaction(
222 id: paymentMethod.invoiceDetails.paymentHash,
193 - amount: request.amount?.toInt() ?? amountSats?.toInt() ??
194 - ((paymentMethod.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(),
195 - fee: lightningFeeSats.toInt() + (sparkTransferFeeSats?.toInt() ?? 0),
223 + amount: amount,
224 + fee: Money(lightningFeeSats + (sparkTransferFeeSats ?? BigInt.zero), currency),
225 commitOverride: () async {
226 try {
227 final res = await sdk.sendPayment(
@@ -230,12 +259,10 @@ class LightningWallet {
259
260 final prepareResponse = await sdk.prepareLnurlPay(request: request);
261
233 - final feeSats = prepareResponse.feeSats;
234 -
262 return PendingLightningTransaction(
263 id: prepareResponse.invoiceDetails.paymentHash,
237 - amount: prepareResponse.amountSats.toInt(),
238 - fee: feeSats.toInt(),
264 + amount: Money(prepareResponse.amountSats, currency),
265 + fee: Money(prepareResponse.feeSats, currency),
266 commitOverride: () async {
267 final res =
268 await sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse));
@@ -256,27 +283,26 @@ class LightningWallet {
283 final feeQuote = paymentMethod.feeQuote;
284
285 OnchainConfirmationSpeed onchainConfirmationSpeed;
259 - int fee;
286 + BigInt fee;
287 switch (priority) {
288 case BitcoinTransactionPriority.fast:
262 - fee = (feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat).toInt();
289 + fee = feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat;
290 onchainConfirmationSpeed = OnchainConfirmationSpeed.fast;
291 break;
292 case BitcoinTransactionPriority.medium:
266 - fee =
267 - (feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat).toInt();
293 + fee = feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat;
294 onchainConfirmationSpeed = OnchainConfirmationSpeed.medium;
295 break;
296 case BitcoinTransactionPriority.slow:
297 default:
272 - fee = (feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat).toInt();
298 + fee = feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat;
299 onchainConfirmationSpeed = OnchainConfirmationSpeed.slow;
300 }
301
302 return PendingLightningTransaction(
303 id: "", // ToDo: Find out where to get it
278 - amount: prepareResponse.amount.toInt(),
279 - fee: fee,
304 + amount: Money(prepareResponse.amount, currency),
305 + fee: Money(fee, currency),
306 commitOverride: () async {
307 final options =
308 SendPaymentOptions.bitcoinAddress(confirmationSpeed: onchainConfirmationSpeed);
@@ -404,7 +430,7 @@ class LightningWallet {
430 }
431
432 ElectrumTransactionInfo _getElectrumTransactionInfoFromPayment(Payment payment) {
407 - TransactionDirection direction = TransactionDirection.outgoing;
433 + var direction = TransactionDirection.outgoing;
434
435 if (payment.paymentType == PaymentType.receive) {
436 direction = TransactionDirection.incoming;
@@ -416,10 +442,10 @@ class LightningWallet {
442 return ElectrumTransactionInfo(
443 WalletType.bitcoin,
444 id: payment.id,
419 - amount: payment.amount.toInt(),
445 + amount: Money(payment.amount, currency),
446 direction: direction,
447 isPending: payment.status == PaymentStatus.pending,
422 - fee: payment.fees.toInt(),
448 + fee: Money(payment.fees, currency),
449 date: DateTime.fromMillisecondsSinceEpoch(payment.timestamp.toInt() * 1000),
450 confirmations: payment.status == PaymentStatus.pending ? 0 : 10,
451 additionalInfo: {"isLightning": true},
@@ -430,10 +456,10 @@ class LightningWallet {
456 return ElectrumTransactionInfo(
457 WalletType.bitcoin,
458 id: deposit.txid,
433 - amount: deposit.amountSats.toInt(),
459 + amount: Money(deposit.amountSats, currency),
460 direction: TransactionDirection.incoming,
461 isPending: true,
436 - fee: 0,
462 + fee: Money.zero(currency),
463 date: DateTime.now(),
464 confirmations: 0,
465 additionalInfo: {"isLightning": true, "isSparkDeposit": true},
cw_bitcoin/lib/lightning/pending_lightning_transaction.dart
+6 -7
@@ -1,4 +1,4 @@
1 -import 'package:cw_bitcoin/bitcoin_amount_format.dart';
1 +import 'package:cw_core/amount/money.dart';
2 import 'package:cw_core/pending_transaction.dart';
3
4 class PendingLightningTransaction with PendingTransaction {
@@ -10,8 +10,7 @@ class PendingLightningTransaction with PendingTransaction {
10 required this.commitOverride,
11 });
12
13 - final int amount;
14 - final int fee;
13 +
14 final bool isSendAll;
15 Future<String> Function() commitOverride;
16 final List<void Function()> _listeners =[];
@@ -20,16 +19,16 @@ class PendingLightningTransaction with PendingTransaction {
19 String id;
20
21 @override
23 - String get hex => "";
22 + final Money amount;
23
24 @override
26 - String get amountFormatted => bitcoinAmountToString(amount: amount);
25 + final Money fee;
26
27 @override
29 - String get feeFormatted => "$feeFormattedValue BTC";
28 + String get hex => "";
29
30 @override
32 - String get feeFormattedValue => bitcoinAmountToString(amount: fee);
31 + String get amountFormatted => amount.toString();
32
33 @override
34 int? get outputCount => 1;
cw_bitcoin/lib/litecoin_wallet.dart
+23 -36
@@ -5,6 +5,7 @@ import 'package:collection/collection.dart';
5 import 'package:crypto/crypto.dart';
6 import 'package:cw_bitcoin/address_from_output.dart';
7 import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
8 +import 'package:cw_core/amount/money.dart';
9 import 'package:cw_core/cake_hive.dart';
10 import 'package:cw_core/mweb_utxo.dart';
11 import 'package:cw_core/unspent_coin_type.dart';
@@ -17,12 +18,9 @@ import 'package:bitcoin_base/bitcoin_base.dart';
18 import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart';
19 import 'package:blockchain_utils/blockchain_utils.dart';
20 import 'package:blockchain_utils/signer/ecdsa_signing_key.dart';
20 -import 'package:collection/collection.dart';
21 import 'package:convert/convert.dart' as convert;
22 -import 'package:crypto/crypto.dart';
22 import 'package:cw_bitcoin/bitcoin_address_record.dart';
23 import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
25 -import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
24 import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
25 import 'package:cw_bitcoin/bitcoin_unspent.dart';
26 import 'package:cw_bitcoin/electrum_balance.dart';
@@ -35,25 +33,18 @@ import 'package:cw_bitcoin/litecoin_wallet_addresses.dart';
33 import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
34 import 'package:cw_bitcoin/psbt/transaction_builder.dart';
35 import 'package:cw_bitcoin/utils.dart';
38 -import 'package:cw_core/cake_hive.dart';
36 import 'package:cw_core/crypto_currency.dart';
37 import 'package:cw_core/encryption_file_utils.dart';
41 -import 'package:cw_core/mweb_utxo.dart';
42 -import 'package:cw_core/node.dart';
38 import 'package:cw_core/output_info.dart';
39 import 'package:cw_core/pending_transaction.dart';
40 import 'package:cw_core/sync_status.dart';
41 import 'package:cw_core/transaction_direction.dart';
42 import 'package:cw_core/transaction_priority.dart';
48 -import 'package:cw_core/unspent_coin_type.dart';
43 import 'package:cw_core/unspent_coins_info.dart';
50 -import 'package:cw_core/utils/print_verbose.dart';
44 import 'package:cw_core/wallet_info.dart';
45 import 'package:cw_core/wallet_keys_file.dart';
46 import 'package:cw_core/wallet_type.dart';
47 import 'package:cw_mweb/cw_mweb.dart';
55 -import 'package:cw_mweb/mwebd.pbgrpc.dart';
56 -import 'package:fixnum/fixnum.dart';
48 import 'package:flutter/foundation.dart';
49 import 'package:grpc/grpc.dart';
50 import 'package:hive/hive.dart';
@@ -587,8 +578,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
578 WalletType.litecoin,
579 id: utxo.outputId,
580 height: utxo.height,
590 - amount: utxo.value.toInt(),
591 - fee: 0,
581 + amount: Money.fromInt(utxo.value, currency),
582 + fee: Money.zero(currency),
583 direction: TransactionDirection.incoming,
584 isPending: utxo.height == 0,
585 date: date,
@@ -734,9 +725,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
725
726 Future<void> checkMwebUtxosSpent() async {
727 printV("checkMwebUtxosSpent() called!");
737 - if (!mwebEnabled) {
738 - return;
739 - }
728 + if (!mwebEnabled) return;
729
730 final pendingOutgoingTransactions = transactionHistory.transactions.values
731 .where((tx) => tx.direction == TransactionDirection.outgoing && tx.isPending);
@@ -763,8 +752,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
752 final height = await electrumClient.getCurrentBlockChainTip();
753 if (height == null || status.blockHeaderHeight != height) return;
754 if (status.mwebUtxosHeight != height) return; // we aren't synced
766 - int amount = 0;
767 - Set<String> inputAddresses = {};
755 + var amount = 0;
756 + var inputAddresses = <String>{};
757 var output = convert.AccumulatorSink<Digest>();
758 var input = sha256.startChunkedConversion(output);
759
@@ -777,8 +766,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
766 if (!inputAddresses.contains(utxo.address)) {
767 addressRecord.txCount++;
768 }
780 - addressRecord.balance -= utxo.value.toInt();
781 - amount += utxo.value.toInt();
769 + addressRecord.balance -= utxo.value;
770 + amount += utxo.value;
771 inputAddresses.add(utxo.address);
772 input.add(hex.decode(outputId));
773 }
@@ -790,8 +779,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
779 WalletType.litecoin,
780 id: digest.toString(),
781 height: height,
793 - amount: amount,
794 - fee: 0,
782 + amount: Money.fromInt(amount, currency),
783 + fee: Money.zero(currency),
784 direction: TransactionDirection.outgoing,
785 isPending: false,
786 date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000),
@@ -921,10 +910,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
910 // update unspent balances:
911 await updateUnspent();
912
924 - int confirmed = balance.confirmed;
925 - int unconfirmed = balance.unconfirmed;
926 - int confirmedMweb = 0;
927 - int unconfirmedMweb = 0;
913 + var confirmedMweb = 0;
914 + var unconfirmedMweb = 0;
915 try {
916 mwebUtxosBox.values.forEach((utxo) {
917 bool isConfirmed = utxo.height > 0;
@@ -933,20 +920,20 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
920 "utxo: ${isConfirmed ? "confirmed" : "unconfirmed"} ${utxo.spent ? "spent" : "unspent"} ${utxo.outputId} ${utxo.height} ${utxo.value}");
921
922 if (isConfirmed) {
936 - confirmedMweb += utxo.value.toInt();
923 + confirmedMweb += utxo.value;
924 }
925
926 if (isConfirmed && utxo.spent) {
940 - unconfirmedMweb -= utxo.value.toInt();
927 + unconfirmedMweb -= utxo.value;
928 }
929
930 if (!isConfirmed && !utxo.spent) {
944 - unconfirmedMweb += utxo.value.toInt();
931 + unconfirmedMweb += utxo.value;
932 }
933 });
934 } catch (_) {}
935
949 - for (var addressRecord in walletAddresses.allAddresses) {
936 + for (final addressRecord in walletAddresses.allAddresses) {
937 addressRecord.balance = 0;
938 addressRecord.txCount = 0;
939 }
@@ -990,11 +977,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
977 }
978
979 return ElectrumBalance(
993 - confirmed: confirmed,
994 - unconfirmed: unconfirmed,
995 - frozen: balance.frozen.toInt(),
996 - secondConfirmed: confirmedMweb,
997 - secondUnconfirmed: unconfirmedMweb,
980 + confirmed: balance.confirmed,
981 + unconfirmed: balance.unconfirmed,
982 + frozen: balance.frozen,
983 + secondConfirmed: Money.fromInt(confirmedMweb, currency),
984 + secondUnconfirmed: Money.fromInt(unconfirmedMweb, currency),
985 );
986 }
987
@@ -1371,8 +1358,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
1358 btcTx,
1359 type,
1360 electrumClient: electrumClient,
1374 - amount: 0,
1375 - fee: resp.fee.toInt(),
1361 + amount: Money.zero(currency),
1362 + fee: Money.fromInt(resp.fee.toInt(), currency),
1363 feeRate: "",
1364 network: network,
1365 hasChange: resp.recipient.length > 1,
cw_bitcoin/lib/litecoin_wallet_addresses.dart
+14 -6
@@ -228,13 +228,21 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
228 @override
229 String get addressForExchange {
230 // don't use mweb addresses for exchange refund address:
231 - try {
232 - final addresses = receiveAddresses
233 - .where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed);
234 - return addresses.first.address;
235 - } catch (_) {
236 - return receiveAddresses.first.address;
231 +
232 + final current = getFreshAddress();
233 + final bool isMweb = receiveAddresses
234 + .any((e) => e.address == current && e.type == SegwitAddresType.mweb);
235 +
236 + if (isMweb) {
237 + final segwit = receiveAddresses
238 + .where((e) => e.type == SegwitAddresType.p2wpkh && !e.isUsed && !e.isHidden && !hiddenAddresses.contains(e.address))
239 + .map((e) => e.address)
240 + .toList();
241 +
242 + return segwit.isNotEmpty ? segwit.first : current;
243 }
244 +
245 + return current;
246 }
247
248 @override
cw_bitcoin/lib/pending_bitcoin_transaction.dart
+26 -49
@@ -1,7 +1,6 @@
1 -import 'dart:typed_data';
2 -
1 import 'package:bbqrdart/bbqrdart.dart';
2 import 'package:cw_bitcoin/electrum_wallet.dart';
3 +import 'package:cw_core/amount/money.dart';
4 import 'package:flutter/foundation.dart';
5 import 'package:grpc/grpc.dart';
6 import 'package:cw_bitcoin/exceptions.dart';
@@ -9,7 +8,6 @@ import 'package:bitcoin_base/bitcoin_base.dart';
8 import 'package:blockchain_utils/blockchain_utils.dart';
9 import 'package:cw_core/pending_transaction.dart';
10 import 'package:cw_bitcoin/electrum.dart';
12 -import 'package:cw_bitcoin/bitcoin_amount_format.dart';
11 import 'package:cw_bitcoin/electrum_transaction_info.dart';
12 import 'package:cw_core/transaction_direction.dart';
13 import 'package:cw_core/wallet_type.dart';
@@ -42,8 +40,6 @@ class PendingBitcoinTransaction with PendingTransaction {
40 final WalletType type;
41 final BtcTransaction _tx;
42 final ElectrumClient electrumClient;
45 - final int amount;
46 - final int fee;
43 final String feeRate;
44 final BasedUtxoNetwork? network;
45 final bool isSendAll;
@@ -62,36 +58,19 @@ class PendingBitcoinTransaction with PendingTransaction {
58 Uint8List? unsignedPsbt;
59
60 @override
65 - String get id => idOverride ?? _tx.txId();
61 + final Money amount;
62
63 @override
68 - String get hex => hexOverride ?? _tx.serialize();
64 + final Money fee;
65
66 @override
71 - String get amountFormatted => bitcoinAmountToString(amount: amount);
67 + String get id => idOverride ?? _tx.txId();
68
69 @override
74 - String get feeFormatted => "$feeFormattedValue ${_feeCurrency}";
75 -
76 - String get _feeCurrency {
77 - switch(type) {
78 - case WalletType.bitcoin:
79 - return "BTC";
80 - case WalletType.litecoin:
81 - return "LTC";
82 - case WalletType.bitcoinCash:
83 - return "BCH";
84 - case WalletType.dogecoin:
85 - return "DOGE";
86 - default:
87 - return type.name;
88 - }
89 -
90 - }
91 -
70 + String get hex => hexOverride ?? _tx.serialize();
71
72 @override
94 - String get feeFormattedValue => bitcoinAmountToString(amount: fee);
73 + String get amountFormatted => amount.toString();
74
75 @override
76 int? get outputCount => _tx.outputs.length;
@@ -155,14 +134,12 @@ class PendingBitcoinTransaction with PendingTransaction {
134
135 Future<void> _ltcCommit() async {
136 try {
158 - final resp = await CwMweb.broadcast(
159 - BroadcastRequest(rawTx: BytesUtils.fromHexString(hex)));
137 + final resp = await CwMweb.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex)));
138 idOverride = resp.txid;
139 } on GrpcError catch (e) {
140 throw BitcoinTransactionCommitFailed(errorMessage: e.message);
141 } catch (e) {
164 - throw BitcoinTransactionCommitFailed(
165 - errorMessage: "Unknown error: ${e.toString()}");
142 + throw BitcoinTransactionCommitFailed(errorMessage: "Unknown error: ${e.toString()}");
143 }
144 }
145
@@ -181,22 +158,23 @@ class PendingBitcoinTransaction with PendingTransaction {
158 _listeners.forEach((listener) => listener(transactionInfo()));
159 }
160
184 - void addListener(
185 - void Function(ElectrumTransactionInfo transaction) listener) =>
161 + void addListener(void Function(ElectrumTransactionInfo transaction) listener) =>
162 _listeners.add(listener);
163
188 - ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,
189 - id: id,
190 - height: 0,
191 - amount: amount,
192 - direction: TransactionDirection.outgoing,
193 - date: DateTime.now(),
194 - isPending: true,
195 - isReplaced: false,
196 - confirmations: 0,
197 - inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
198 - outputAddresses: outputAddresses,
199 - fee: fee);
164 + ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(
165 + type,
166 + id: id,
167 + height: 0,
168 + amount: amount,
169 + direction: TransactionDirection.outgoing,
170 + date: DateTime.now(),
171 + isPending: true,
172 + isReplaced: false,
173 + confirmations: 0,
174 + inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
175 + outputAddresses: outputAddresses,
176 + fee: fee,
177 + );
178
179 @override
180 bool shouldCommitUR() => isViewOnly;
@@ -207,8 +185,8 @@ class PendingBitcoinTransaction with PendingTransaction {
185 var cborEncoder = CBOREncoder();
186 cborEncoder.encodeBytes(sourceBytes);
187 var ur = UR("psbt", cborEncoder.getBytes());
210 - var urLegacy = UR("crypto-psbt", cborEncoder.getBytes());
211 - // var ur = UR("psbt", Uint8List.fromList(List.generate(64*1024, (int x) => x % 256)));
188 + var urLegacy = UR("crypto-psbt", cborEncoder.getBytes());
189 + // var ur = UR("psbt", Uint8List.fromList(List.generate(64*1024, (int x) => x % 256)));
190 var encoded = UREncoder(ur, 120);
191 var encodedLegacy = UREncoder(urLegacy, 120);
192 List<String> values = [];
@@ -230,8 +208,7 @@ class PendingBitcoinTransaction with PendingTransaction {
208 return Future.value({
209 "Cupcake bcur": values.join("\n"),
210 "ColdCard bbqr": bbqr.join("\n"),
233 - if (kDebugMode)
234 - "SeedSigner bcur legacy": valuesLegacy.join("\n"),
211 + if (kDebugMode) "SeedSigner bcur legacy": valuesLegacy.join("\n"),
212 });
213 }
214 }
cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart deleted
-95
@@ -1,95 +0,0 @@
1 -import 'package:cw_bitcoin/exceptions.dart';
2 -import 'package:bitbox/bitbox.dart' as bitbox;
3 -import 'package:cw_core/pending_transaction.dart';
4 -import 'package:cw_bitcoin/electrum.dart';
5 -import 'package:cw_bitcoin/bitcoin_amount_format.dart';
6 -import 'package:cw_bitcoin/electrum_transaction_info.dart';
7 -import 'package:cw_core/transaction_direction.dart';
8 -import 'package:cw_core/wallet_type.dart';
9 -
10 -class PendingBitcoinCashTransaction with PendingTransaction {
11 - PendingBitcoinCashTransaction(this._tx, this.type,
12 - {required this.electrumClient,
13 - required this.amount,
14 - required this.fee,
15 - required this.hasChange,
16 - required this.isSendAll})
17 - : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
18 -
19 - final WalletType type;
20 - final bitbox.Transaction _tx;
21 - final ElectrumClient electrumClient;
22 - final int amount;
23 - final int fee;
24 - final bool hasChange;
25 - final bool isSendAll;
26 -
27 - @override
28 - String get id => _tx.getId();
29 -
30 - @override
31 - String get hex => _tx.toHex();
32 -
33 - @override
34 - String get amountFormatted => bitcoinAmountToString(amount: amount);
35 -
36 - @override
37 - String get feeFormatted => "$feeFormattedValue BCH";
38 -
39 - @override
40 - String get feeFormattedValue => bitcoinAmountToString(amount: fee);
41 -
42 - final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
43 -
44 - @override
45 - Future<void> commit() async {
46 - int? callId;
47 -
48 - final result = await electrumClient.broadcastTransaction(
49 - transactionRaw: hex, idCallback: (id) => callId = id);
50 -
51 - if (result.isEmpty) {
52 - if (callId != null) {
53 - final error = electrumClient.getErrorMessage(callId!);
54 -
55 - if (error.contains("dust")) {
56 - if (hasChange) {
57 - throw BitcoinTransactionCommitFailedDustChange();
58 - } else if (!isSendAll) {
59 - throw BitcoinTransactionCommitFailedDustOutput();
60 - } else {
61 - throw BitcoinTransactionCommitFailedDustOutputSendAll();
62 - }
63 - }
64 -
65 - if (error.contains("bad-txns-vout-negative")) {
66 - throw BitcoinTransactionCommitFailedVoutNegative();
67 - }
68 - throw BitcoinTransactionCommitFailed(errorMessage: error);
69 - }
70 -
71 - throw BitcoinTransactionCommitFailed();
72 - }
73 -
74 - _listeners.forEach((listener) => listener(transactionInfo()));
75 - }
76 -
77 - void addListener(void Function(ElectrumTransactionInfo transaction) listener) =>
78 - _listeners.add(listener);
79 -
80 - ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,
81 - id: id,
82 - height: 0,
83 - amount: amount,
84 - direction: TransactionDirection.outgoing,
85 - date: DateTime.now(),
86 - isPending: true,
87 - confirmations: 0,
88 - fee: fee,
89 - isReplaced: false,
90 - );
91 - @override
92 - Future<Map<String, String>> commitUR() {
93 - throw UnimplementedError();
94 - }
95 -}
cw_core/lib/amount/amount_sanitizer.dart new
+16
@@ -0,0 +1,16 @@
1 +String _smartAmountSanitizer(String amount) {
2 + if (amount.contains(RegExp(r'^(?=.*[.])(?=.*,).*$'))) {
3 + if (amount.indexOf(".") > amount.indexOf(",")) {
4 + amount = amount.replaceAll(",", "");
5 + } else {
6 + amount = amount.replaceAll(".", "");
7 + }
8 + }
9 +
10 + return amount.replaceAll(",", ".");
11 +}
12 +
13 +
14 +extension SanitizerUserAmounts on String {
15 + String sanitized() => _smartAmountSanitizer(this);
16 +}
cw_core/lib/amount/money.dart new
+211
@@ -0,0 +1,211 @@
1 +import 'package:cw_core/crypto_amount_format.dart';
2 +import 'package:cw_core/crypto_currency.dart';
3 +import 'package:cw_core/currency.dart';
4 +import 'package:cw_core/format_fixed.dart';
5 +import 'package:cw_core/parse_fixed.dart';
6 +
7 +class Money implements Comparable<Money> {
8 + final BigInt amount;
9 + final Currency currency;
10 +
11 + const Money(this.amount, this.currency);
12 +
13 + factory Money.zero(Currency currency) => Money(BigInt.zero, currency);
14 +
15 + factory Money.fromInt(int amount, Currency currency) => Money(BigInt.from(amount), currency);
16 +
17 + /// Parse the [source] and turn it into [Money]
18 + ///
19 + /// Throws a [FormatException] if the [source] is not a valid decimal or
20 + /// not in canonical representation or if it is a decimal when [isBaseUnit]
21 + factory Money.parse(source, Currency currency, {bool isBaseUnit = false}) {
22 + final amount = isBaseUnit
23 + ? BigInt.parse(source.toString())
24 + : parseFixed(source.toString(), currency.decimals);
25 +
26 + return Money(amount, currency);
27 + }
28 +
29 + /// Parse the [source] and turn it into [Money] if possible
30 + ///
31 + /// As [parse] except that this method returns `null` if the input is not
32 + /// valid or if it is a decimal when [isBaseUnit]
33 + static Money? tryParse(source, Currency currency, {bool isBaseUnit = false}) {
34 + final amount = isBaseUnit
35 + ? BigInt.tryParse(source.toString())
36 + : tryParseFixed(source.toString(), currency.decimals);
37 +
38 + return amount != null ? Money(amount, currency) : null;
39 + }
40 +
41 + /// Returns the sign of this [BigInt] amount.
42 + /// Returns 0 for zero, -1 for values less than zero and +1 for values
43 + /// greater than zero.
44 + int get sign => amount.sign;
45 +
46 + /// Returns `true` when amount of this money is zero.
47 + bool get isZero => amount == BigInt.zero;
48 +
49 + /// Returns `true` when amount of this money is negative.
50 + bool get isNegative => amount.isNegative;
51 +
52 + /// Compares this to [other].
53 + ///
54 + /// [other] has to be in same currency, [ArgumentError] will be thrown
55 + /// otherwise.
56 + @override
57 + int compareTo(Money other) {
58 + _assertSameCurrency(other);
59 +
60 + return amount.compareTo(other.amount);
61 + }
62 +
63 + /// Returns `true` if [other] is the same amount of money in
64 + /// the same currency.
65 + @override
66 + bool operator ==(Object other) =>
67 + identical(this, other) ||
68 + (other is Money && other.currency == currency && other.amount == amount);
69 +
70 + /// Returns `true` when this money is less than [other].
71 + ///
72 + /// Both operands have to be in same currency, [ArgumentError] will be thrown
73 + /// otherwise.
74 + bool operator <(Money other) {
75 + _assertSameCurrency(other, "Cannot compare money in different currencies.");
76 +
77 + return amount < other.amount;
78 + }
79 +
80 + /// Returns `true` when this money is less than or equal to [other].
81 + ///
82 + /// Both operands have to be in same currency, [ArgumentError] will be thrown
83 + /// otherwise.
84 + bool operator <=(Money other) {
85 + _assertSameCurrency(other, "Cannot compare money in different currencies.");
86 +
87 + return amount <= other.amount;
88 + }
89 +
90 + /// Returns `true` when this money is greater than [other].
91 + ///
92 + /// Both operands have to be in same currency, [ArgumentError] will be thrown
93 + /// otherwise.
94 + bool operator >(Money other) {
95 + _assertSameCurrency(other, "Cannot compare money in different currencies.");
96 +
97 + return amount > other.amount;
98 + }
99 +
100 + /// Returns `true` when this money is greater than or equal to [other].
101 + ///
102 + /// Both operands have to be in same currency, [ArgumentError] will be thrown
103 + /// otherwise.
104 + bool operator >=(Money other) {
105 + _assertSameCurrency(other, "Cannot compare money in different currencies.");
106 +
107 + return amount >= other.amount;
108 + }
109 +
110 + /// Adds the amount of [other] to this amount.
111 + ///
112 + /// Both operands must be in same currency, [ArgumentError] will be thrown
113 + /// otherwise.
114 + Money operator +(Money other) {
115 + _assertSameCurrency(other);
116 +
117 + return _withAmount(amount + other.amount);
118 + }
119 +
120 + /// unary minus operator.
121 + Money operator -() => _withAmount(-amount);
122 +
123 + /// Subtracts the amount of [other] from this amount.
124 + ///
125 + /// Both operands must be in same currency, [ArgumentError] will be thrown
126 + /// otherwise.
127 + Money operator -(Money other) {
128 + _assertSameCurrency(other);
129 +
130 + return _withAmount(amount - other.amount);
131 + }
132 +
133 + /// Returns [Money] multiplied by [other].
134 + ///
135 + /// The result is again [Money].
136 + Money operator *(BigInt other) => _withAmount(amount * other);
137 +
138 + /// Returns [Money] divided by [other].
139 + ///
140 + /// The result is again [Money].
141 + Money operator /(BigInt other) {
142 + if (other == BigInt.zero) throw Exception('Division by zero.');
143 +
144 + final neg = (amount.isNegative) ^ (other.isNegative);
145 + final A = amount.abs();
146 + final B = other.abs();
147 +
148 + final q = A ~/ B;
149 + final r = A % B;
150 +
151 + // Compare 2*r with B to detect half/up/down
152 + final twiceR = r << 1;
153 + final mag = (twiceR >= B) ? q + BigInt.one : q;
154 +
155 + return _withAmount(neg ? -mag : mag);
156 + }
157 +
158 + /// Creates a copy of this [Money] object with optional new values
159 + /// for [amount], [currency].
160 + ///
161 + /// If just [currency] is provided and the decimals missmatch
162 + /// the amount will be transformed to keep its canonical representation
163 + Money copyWith({BigInt? amount, Currency? currency}) {
164 + if (currency != null && amount == null && currency.decimals != this.currency.decimals) {
165 + return Money(
166 + _transformAmount(this.amount, this.currency.decimals, currency.decimals), currency);
167 + }
168 +
169 + return Money(amount ?? this.amount, currency ?? this.currency);
170 + }
171 +
172 + /// Creates new instance with the same currency and given [amount].
173 + Money _withAmount(BigInt amount) => Money(amount, currency);
174 +
175 + void _assertSameCurrency(Money other, [String? message]) {
176 + if (currency != other.currency)
177 + throw ArgumentError(message ?? "Cannot operate with money values in different currencies.");
178 + }
179 +
180 + BigInt _transformAmount(BigInt source, int sourceDecimals, int targetDecimals) {
181 + if (sourceDecimals == targetDecimals) return source;
182 +
183 + if (sourceDecimals > targetDecimals) {
184 + return parseFixed(
185 + formatFixed(source, sourceDecimals).withMaxDecimals(targetDecimals), targetDecimals);
186 + } else {
187 + return parseFixed(formatFixed(source, sourceDecimals), targetDecimals);
188 + }
189 + }
190 +
191 + @override
192 + int get hashCode => amount.hashCode ^ currency.hashCode;
193 +
194 + @override
195 + String toString() => formatFixed(amount, currency.decimals);
196 +
197 + String toStringWithSymbol(
198 + {int? fractionalDigits, bool trimZeros = true, bool useBaseUnit = false}) =>
199 + "${toStringWithPrecision(fractionalDigits: fractionalDigits, trimZeros: trimZeros, useBaseUnit: useBaseUnit)} ${_getSymbol(useBaseUnit)}";
200 +
201 + String toStringWithPrecision(
202 + {int? fractionalDigits, bool trimZeros = true, bool useBaseUnit = false}) =>
203 + formatFixed(amount, useBaseUnit ? 0 : currency.decimals,
204 + fractionalDigits: fractionalDigits, trimZeros: trimZeros);
205 +
206 + // To Override the symbol with the ticker of the base unit
207 + String _getSymbol(bool useBaseUnit) {
208 + if (useBaseUnit && [CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) return "sats";
209 + return currency.symbol;
210 + }
211 +}
cw_core/lib/amount_converter.dart
-74
@@ -1,78 +1,4 @@
1 -import 'package:decimal/decimal.dart';
2 -import 'package:decimal/intl.dart';
3 -import 'package:intl/intl.dart';
4 -import 'package:cw_core/crypto_currency.dart';
5 -
1 class AmountConverter {
7 - static const _moneroAmountLength = 12;
8 - static const _moneroAmountDivider = 1000000000000;
9 - static const _wowneroAmountLength = 11;
10 - static const _wowneroAmountDivider = 100000000000;
11 - static const _bitcoinAmountDivider = 100000000;
12 - static const _bitcoinAmountLength = 8;
13 - static final _bitcoinAmountFormat = NumberFormat()
14 - ..maximumFractionDigits = _bitcoinAmountLength
15 - ..minimumFractionDigits = 1;
16 - static final _moneroAmountFormat = NumberFormat()
17 - ..maximumFractionDigits = _moneroAmountLength
18 - ..minimumFractionDigits = 1;
19 - static final _wowneroAmountFormat = NumberFormat()
20 - ..maximumFractionDigits = _wowneroAmountLength
21 - ..minimumFractionDigits = 1;
22 -
23 - static String amountIntToString(CryptoCurrency cryptoCurrency, int amount) {
24 - switch (cryptoCurrency) {
25 - case CryptoCurrency.xmr:
26 - return _moneroAmountToString(amount);
27 - case CryptoCurrency.wow:
28 - return _wowneroAmountToString(amount);
29 - case CryptoCurrency.btc:
30 - case CryptoCurrency.bch:
31 - case CryptoCurrency.ltc:
32 - case CryptoCurrency.dcr:
33 - case CryptoCurrency.doge:
34 - return _bitcoinAmountToString(amount);
35 - case CryptoCurrency.xhv:
36 - case CryptoCurrency.xag:
37 - case CryptoCurrency.xau:
38 - case CryptoCurrency.xaud:
39 - case CryptoCurrency.xbtc:
40 - case CryptoCurrency.xcad:
41 - case CryptoCurrency.xchf:
42 - case CryptoCurrency.xcny:
43 - case CryptoCurrency.xeur:
44 - case CryptoCurrency.xgbp:
45 - case CryptoCurrency.xjpy:
46 - case CryptoCurrency.xnok:
47 - case CryptoCurrency.xnzd:
48 - case CryptoCurrency.xusd:
49 - return _moneroAmountToString(amount);
50 - case CryptoCurrency.zano:
51 - return _moneroAmountToStringUsingDecimals(amount);
52 - default:
53 - return '';
54 - }
55 - }
56 -
57 - static double cryptoAmountToDouble({required num amount, required num divider}) =>
58 - amount / divider;
59 -
60 - static String _moneroAmountToString(int amount) => _moneroAmountFormat
61 - .format(cryptoAmountToDouble(amount: amount, divider: _moneroAmountDivider));
62 -
63 - static String _bitcoinAmountToString(int amount) => _bitcoinAmountFormat
64 - .format(cryptoAmountToDouble(amount: amount, divider: _bitcoinAmountDivider));
65 -
66 - static String _wowneroAmountToString(int amount) => _wowneroAmountFormat
67 - .format(cryptoAmountToDouble(amount: amount, divider: _wowneroAmountDivider));
68 -
69 - static Decimal cryptoAmountToDecimal({required int amount, required int divider}) =>
70 - (Decimal.fromInt(amount) / Decimal.fromInt(divider)).toDecimal();
71 -
72 - static String _moneroAmountToStringUsingDecimals(int amount) => _moneroAmountFormat.format(
73 - DecimalIntl(cryptoAmountToDecimal(amount: amount, divider: _moneroAmountDivider)));
74 -
75 -
2 static String toBaseUnits(String amount, int decimals) {
3 amount = amount.trim();
4 if (amount.isEmpty) return '0';
cw_core/lib/balance.dart
+14 -20
@@ -1,26 +1,20 @@
1 +import 'package:cw_core/amount/money.dart';
2 +
3 /// Balance Data class with all amounts in the lowest possible currency (e.g. satoshis or wei)
4 abstract class Balance {
3 - const Balance(this.available, this.additional, {this.secondAvailable, this.secondAdditional, this.frozen});
4 -
5 - Balance.fromInt(int available, int additional, {int? secondAvailable, int? secondAdditional, int? frozen}) :
6 - available = BigInt.from(available),
7 - additional = BigInt.from(additional),
8 - secondAvailable = secondAvailable == null ? null : BigInt.from(secondAvailable),
9 - secondAdditional = secondAdditional == null ? null : BigInt.from(secondAdditional),
10 - frozen = frozen == null ? null : BigInt.from(frozen) {}
11 -
12 -
13 -
14 - final BigInt available;
15 - final BigInt additional;
16 -
17 - final BigInt? secondAvailable;
18 - final BigInt? secondAdditional;
5 + const Balance(
6 + this.available,
7 + this.unavailable, {
8 + this.secondAvailable,
9 + this.secondUnavailable,
10 + this.frozen,
11 + });
12
20 - final BigInt? frozen;
13 + final Money available;
14 + final Money unavailable;
15
22 - BigInt get fullAvailableBalance => available;
16 + final Money? secondAvailable;
17 + final Money? secondUnavailable;
18
24 - @deprecated
25 - String get formattedUnAvailableBalance => '';
19 + final Money? frozen;
20 }
cw_core/lib/crypto_currency.dart
+25 -13
@@ -1,9 +1,9 @@
1 +import 'package:cw_core/amount/money.dart';
2 import 'package:cw_core/currency.dart';
3 import 'package:cw_core/currency_groups.dart';
4 import 'package:cw_core/enumerable_item.dart';
5 import 'package:collection/collection.dart';
6 import 'package:cw_core/format_fixed.dart';
6 -import 'package:cw_core/parse_fixed.dart';
7
8 class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implements Currency {
9 const CryptoCurrency({
@@ -21,18 +21,30 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
21 this.groups = const {},
22 }) : super(title: title, raw: raw);
23
24 - final String name;
25 - final String? tag;
26 - final String? fullName;
27 - final String? iconPath;
24 final String? flatIconPath;
25 final String? chainIconPath;
30 - @override
31 - final int decimals;
26 final bool enabled;
27 final bool isPotentialScam;
28 final Set<String> groups;
29
30 + @override
31 + final String name;
32 +
33 + @override
34 + final String? fullName;
35 +
36 + @override
37 + final String? tag;
38 +
39 + @override
40 + final String? iconPath;
41 +
42 + @override
43 + final int decimals;
44 +
45 + @override
46 + String get symbol => title;
47 +
48 set enabled(bool value) => this.enabled = value;
49
50 static const all = [
@@ -432,13 +444,13 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
444
445 bool titleAndTagEqual(CryptoCurrency other) => title == other.title && tag == other.tag;
446
447 + @override
448 + Money parseAmount(String value) => Money.parse(value, this);
449 +
450 + @override
451 + Money? tryParseAmount(String value) => Money.tryParse(value, this);
452 +
453 /// Format the raw amount into its decimal representation eg. turn Sats into Bitcoin
454 String formatAmount(BigInt amount, {int? fractionalDigits, bool trimZeros = true}) =>
455 formatFixed(amount, decimals, fractionalDigits: fractionalDigits, trimZeros: trimZeros);
438 -
439 - /// Parse the [value] and turn it into the smallest denomination eg. turn Bitcoin into Sats
440 - BigInt parseAmount(String value) => parseFixed(value, decimals);
441 -
442 - /// Try parsing the [value] and turn it into the smallest denomination eg. turn Bitcoin into Sats
443 - BigInt? tryParseAmount(String value) => tryParseFixed(value, decimals);
456 }
cw_core/lib/currency.dart
+17 -1
@@ -1,7 +1,23 @@
1 +import 'package:cw_core/amount/money.dart';
2 +
3 abstract class Currency {
4 + String get symbol;
5 String get name;
3 - String? get tag;
6 String? get fullName;
7 String? get iconPath;
8 int get decimals;
9 + String? get tag;
10 +
11 + /// Parse the [value] and turn it into [Money]
12 + Money parseAmount(String value);
13 +
14 + /// Try parsing the [value] and turn it into [Money]
15 + Money? tryParseAmount(String value);
16 +
17 + @override
18 + bool operator ==(Object other) =>
19 + other is Currency && other.decimals == decimals && other.symbol == symbol && other.tag == tag;
20 +
21 + @override
22 + int get hashCode => decimals.hashCode ^ symbol.hashCode ^ tag.hashCode;
23 }
cw_core/lib/enumerable_item.dart
-2
@@ -1,5 +1,3 @@
1 -import 'package:flutter/foundation.dart';
2 -
1 abstract class EnumerableItem<T> {
2 const EnumerableItem({required this.title, required this.raw});
3
cw_core/lib/enumerate.dart
+1 -1
@@ -2,7 +2,7 @@ abstract class Enumerate {
2 String get value;
3
4 @override
5 - operator ==(other) {
5 + bool operator ==(other) {
6 if (identical(other, this)) return true;
7 if (other is! Enumerate) return false;
8 return other.runtimeType == runtimeType && value == other.value;
cw_core/lib/exceptions.dart
+1 -7
@@ -26,9 +26,7 @@ class TransactionCommitFailed implements Exception {
26 TransactionCommitFailed({this.errorMessage});
27
28 @override
29 - String toString() {
30 - return errorMessage ?? "unknown error";
31 - }
29 + String toString() => errorMessage ?? "unknown error";
30 }
31
32 class TransactionCommitFailedDustChange implements Exception {}
@@ -57,10 +55,6 @@ class SignSPLTokenTransactionRentException implements Exception {}
55
56 class NoAssociatedTokenAccountException implements Exception {}
57
60 -
61 -/// ==============================================================================
62 -/// ==============================================================================
63 -
58 class RestoreFromSeedException implements Exception {
59 final String message;
60
cw_core/lib/lnurl.dart
+6 -4
@@ -1,6 +1,8 @@
1 import 'dart:convert';
2
3 import 'package:bech32/bech32.dart';
4 +import 'package:cw_core/amount/money.dart';
5 +import 'package:cw_core/crypto_currency.dart';
6 import 'package:cw_core/utils/proxy_wrapper.dart';
7
8 const _BOLT_PREFIXES = ["lnbcrt", "lntbs", "lnbc", "lntb"];
@@ -61,14 +63,14 @@ int? _getAmountBolt11Msat(String invoice) {
63 }
64 }
65
64 -int? getBolt11Amount(String invoice) {
66 +Money? getBolt11Amount(String invoice) {
67 final msat = _getAmountBolt11Msat(invoice);
68 if (msat == null || msat % 1000 != 0) return null;
67 - return msat ~/ 1000;
69 + return Money.fromInt(msat ~/ 1000, CryptoCurrency.btcln);
70 }
71
72 class LNURL {
71 - static Future<int?> getPayRequestAmount(String lnurl) async {
73 + static Future<Money?> getPayRequestAmount(String lnurl) async {
74 final url = decode(lnurl);
75 final response = await ProxyWrapper().get(clearnetUri: url);
76
@@ -88,7 +90,7 @@ class LNURL {
90 return null;
91
92 if (msat == null || msat % 1000 != 0) return null;
91 - return msat ~/ 1000;
93 + return Money.fromInt(msat ~/ 1000, CryptoCurrency.btcln);
94 } catch (_) {
95 return null;
96 }
cw_core/lib/monero_balance.dart deleted
-17
@@ -1,17 +0,0 @@
1 -import 'package:cw_core/balance.dart';
2 -import 'package:cw_core/monero_amount_format.dart';
3 -
4 -class MoneroBalance extends Balance {
5 - MoneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0})
6 - : formattedFrozenBalance = moneroAmountToString(amount: frozenBalance),
7 - super.fromInt(unlockedBalance, fullBalance - unlockedBalance, frozen: frozenBalance);
8 -
9 - final int fullBalance;
10 - final int unlockedBalance;
11 - final int frozenBalance;
12 - final String formattedFrozenBalance;
13 -
14 - @override
15 - String get formattedUnAvailableBalance =>
16 - formattedFrozenBalance == '0.0' ? '' : formattedFrozenBalance;
17 -}
cw_core/lib/output_info.dart
+19 -18
@@ -1,24 +1,25 @@
1 +import 'package:cw_core/amount/money.dart';
2 +
3 class OutputInfo {
2 - const OutputInfo(
3 - {required this.address,
4 - required this.sendAll,
5 - required this.isParsedAddress,
6 - this.cryptoAmount,
7 - this.formattedCryptoAmount,
4 + const OutputInfo({
5 + required this.address,
6 + required this.sendAll,
7 + required this.isParsedAddress,
8 + required this.cryptoAmount,
9 this.fiatAmount,
10 this.note,
11 this.extractedAddress,
12 this.memo,
12 - this.extra = const {}});
13 + this.extra = const {},
14 + });
15
14 - final String? fiatAmount;
15 - final String? cryptoAmount;
16 - final String address;
17 - final String? note;
18 - final String? extractedAddress;
19 - final bool sendAll;
20 - final bool isParsedAddress;
21 - final int? formattedCryptoAmount;
22 - final String? memo;
23 - final Map<String, dynamic> extra;
24 -}
\ No newline at end of file
16 + final String? fiatAmount;
17 + final Money cryptoAmount;
18 + final String address;
19 + final String? note;
20 + final String? extractedAddress;
21 + final bool sendAll;
22 + final bool isParsedAddress;
23 + final String? memo;
24 + final Map<String, dynamic> extra;
25 +}
cw_core/lib/parse_fixed.dart
+1 -1
@@ -45,7 +45,7 @@ BigInt parseFixed(String value, int decimals) {
45 var fraction = (comps.length == 2 ? comps[1] : "0").padRight(decimals, "0");
46
47 if (fraction.length > multiplier.length - 1) {
48 - throw FormatException("fractional component exceeds decimals, underflow, parseFixed");
48 + throw FormatException("fractional component(${fraction.length}) exceeds decimals(${decimals}), underflow, parseFixed");
49 }
50
51 final wholeValue = BigInt.parse(whole);
cw_core/lib/pending_transaction.dart
+8 -9
@@ -1,3 +1,5 @@
1 +import 'package:cw_core/amount/money.dart';
2 +
3 class PendingChange {
4 final String address;
5 final BigInt amount;
@@ -7,22 +9,19 @@ class PendingChange {
9
10 mixin PendingTransaction {
11 String get id;
12 +
13 + Money get amount;
14 + Money get fee;
15 +
16 String get amountFormatted;
11 - String get feeFormatted;
12 - String get feeFormattedValue;
17 + String get feeFormatted => fee.toStringWithSymbol(fractionalDigits: 8);
18 + String get feeFormattedValue => fee.toStringWithPrecision(fractionalDigits: 8);
19 String? feeRate;
20 String get hex;
21 String? get evmTxHashFromRawHex => null;
22 int? get outputCount => null;
23 PendingChange? change;
24
19 - String get roundedAmountFormatted {
20 - if (amountFormatted.split(".").last.length <= 8) {
21 - return amountFormatted;
22 - }
23 - return double.parse(amountFormatted).toStringAsFixed(8);
24 - }
25 -
25 bool shouldCommitUR() => false;
26
27 Future<void> commit();
cw_core/lib/transaction_info.dart
+8 -6
@@ -1,20 +1,18 @@
1 +import 'package:cw_core/amount/money.dart';
2 +import 'package:cw_core/format_amount.dart';
3 import 'package:cw_core/transaction_direction.dart';
4 import 'package:cw_core/keyable.dart';
5
6 abstract class TransactionInfo extends Object with Keyable {
7 late String id;
8 late String txHash = id;
7 - late int amount;
8 - int? fee;
9 + late Money amount;
10 + Money? fee;
11 late TransactionDirection direction;
12 late bool isPending;
13 late DateTime date;
14 int? height;
15 int confirmations = 0;
14 - String amountFormatted();
15 - String fiatAmount();
16 - String? feeFormatted();
17 - void changeFiatAmount(String amount);
16 String? to;
17 String? from;
18 String? evmSignatureName;
@@ -26,4 +24,8 @@ abstract class TransactionInfo extends Object with Keyable {
24 dynamic get keyIndex => id;
25
26 Map<String, dynamic> additionalInfo = {};
27 +
28 + String? _fiatAmount;
29 + String fiatAmount() => _fiatAmount ?? '';
30 + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
31 }
cw_core/lib/wallet_base.dart
-2
@@ -43,8 +43,6 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
43
44 ObservableMap<CryptoCurrency, BalanceType> get balance;
45
46 - String formatCryptoAmount(String amount) => amount;
47 -
46 SyncStatus get syncStatus;
47
48 set syncStatus(SyncStatus status);
cw_core/lib/wownero_balance.dart deleted
-17
@@ -1,17 +0,0 @@
1 -import 'package:cw_core/balance.dart';
2 -import 'package:cw_core/wownero_amount_format.dart';
3 -
4 -class WowneroBalance extends Balance {
5 - WowneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0})
6 - : formattedFrozenBalance = wowneroAmountToString(amount: frozenBalance),
7 - super.fromInt(unlockedBalance, fullBalance - unlockedBalance, frozen: frozenBalance);
8 -
9 - final int fullBalance;
10 - final int unlockedBalance;
11 - final int frozenBalance;
12 - final String formattedFrozenBalance;
13 -
14 - @override
15 - String get formattedUnAvailableBalance =>
16 - formattedFrozenBalance == '0.0' ? '' : formattedFrozenBalance;
17 -}
cw_core/test/amount/amount_sanitizer_test.dart new
+31
@@ -0,0 +1,31 @@
1 +import 'package:cw_core/amount/amount_sanitizer.dart';
2 +import 'package:flutter_test/flutter_test.dart';
3 +
4 +void main() {
5 + group('smartAmountSanitizer', () {
6 + test('sanitize decimal dot', (){
7 + final amount = "1,000.00".sanitized();
8 + expect(amount, "1000.00");
9 + });
10 +
11 + test('sanitize decimal comma', (){
12 + final amount = "1.000,00".sanitized();
13 + expect(amount, "1000.00");
14 + });
15 +
16 + test('general case', (){
17 + final amount = "1000.00".sanitized();
18 + expect(amount, "1000.00");
19 + });
20 +
21 + test('no decimals', (){
22 + final amount = "1,000".sanitized();
23 + expect(amount, "1.000");
24 + });
25 +
26 + test('no decimals', (){
27 + final amount = "1.000".sanitized();
28 + expect(amount, "1.000");
29 + });
30 + });
31 +}
cw_core/test/amount/money_test.dart new
+351
@@ -0,0 +1,351 @@
1 +import "package:cw_core/amount/money.dart";
2 +import "package:cw_core/crypto_currency.dart";
3 +import "package:flutter_test/flutter_test.dart";
4 +
5 +void main() {
6 + group("Money", () {
7 + test("parse", () {
8 + var money = Money.parse("1", CryptoCurrency.btc);
9 + expect(money.amount, BigInt.from(100000000));
10 +
11 + money = Money.parse("-1", CryptoCurrency.btc);
12 + expect(money.amount, BigInt.from(-100000000));
13 +
14 + money = Money.parse("1.11", CryptoCurrency.btc);
15 + expect(money.amount, BigInt.from(111000000));
16 +
17 + money = Money.parse("-1.11", CryptoCurrency.btc);
18 + expect(money.amount, BigInt.from(-111000000));
19 +
20 + money = Money.parse("-.11", CryptoCurrency.btc);
21 + expect(money.amount, BigInt.from(-11000000));
22 +
23 + money = Money.parse(".11", CryptoCurrency.btc);
24 + expect(money.amount, BigInt.from(11000000));
25 +
26 + // Invalid representation
27 + expect(() => Money.parse("1,11", CryptoCurrency.btc), throwsFormatException);
28 +
29 + // To many decimals
30 + expect(() => Money.parse("-1.000000000000000", CryptoCurrency.btc), throwsFormatException);
31 +
32 + money = Money.parse("1", CryptoCurrency.btc, isBaseUnit: true);
33 + expect(money.amount, BigInt.from(1));
34 +
35 + money = Money.parse("0", CryptoCurrency.btc, isBaseUnit: true);
36 + expect(money.amount, BigInt.from(0));
37 +
38 + money = Money.parse("-1", CryptoCurrency.btc, isBaseUnit: true);
39 + expect(money.amount, BigInt.from(-1));
40 +
41 + // canonical representation when expecting base units
42 + expect(() => Money.parse("1.0", CryptoCurrency.btc, isBaseUnit: true), throwsFormatException);
43 + });
44 +
45 + test("tryParse", () {
46 + var money = Money.tryParse("1", CryptoCurrency.btc);
47 + expect(money?.amount, BigInt.from(100000000));
48 +
49 + money = Money.tryParse("-1", CryptoCurrency.btc);
50 + expect(money?.amount, BigInt.from(-100000000));
51 +
52 + money = Money.tryParse("1.11", CryptoCurrency.btc);
53 + expect(money?.amount, BigInt.from(111000000));
54 +
55 + money = Money.tryParse("-1.11", CryptoCurrency.btc);
56 + expect(money?.amount, BigInt.from(-111000000));
57 +
58 + money = Money.tryParse("-.11", CryptoCurrency.btc);
59 + expect(money?.amount, BigInt.from(-11000000));
60 +
61 + money = Money.tryParse(".11", CryptoCurrency.btc);
62 + expect(money?.amount, BigInt.from(11000000));
63 +
64 + // Invalid representation
65 + money = Money.tryParse("1,11", CryptoCurrency.btc);
66 + expect(money?.amount, isNull);
67 +
68 + // To many decimals
69 + money = Money.tryParse("-1.000000000000000", CryptoCurrency.btc);
70 + expect(money?.amount, isNull);
71 +
72 + money = Money.tryParse("1", CryptoCurrency.btc, isBaseUnit: true);
73 + expect(money?.amount, BigInt.from(1));
74 +
75 + money = Money.tryParse("0", CryptoCurrency.btc, isBaseUnit: true);
76 + expect(money?.amount, BigInt.from(0));
77 +
78 + money = Money.tryParse("-1", CryptoCurrency.btc, isBaseUnit: true);
79 + expect(money?.amount, BigInt.from(-1));
80 +
81 + // canonical representation when expecting base units
82 + money = Money.tryParse("1.0", CryptoCurrency.btc, isBaseUnit: true);
83 + expect(money?.amount, isNull);
84 + });
85 +
86 + test("sign", () {
87 + expect(Money(BigInt.from(0), CryptoCurrency.btc).sign, 0);
88 +
89 + expect(Money(BigInt.from(1), CryptoCurrency.btc).sign, 1);
90 +
91 + expect(Money(BigInt.from(10), CryptoCurrency.btc).sign, 1);
92 +
93 + expect(Money(BigInt.from(-1), CryptoCurrency.btc).sign, -1);
94 +
95 + expect(Money(BigInt.from(-10), CryptoCurrency.btc).sign, -1);
96 + });
97 +
98 + test("isZero", () {
99 + expect(Money(BigInt.from(0), CryptoCurrency.btc).isZero, isTrue);
100 +
101 + expect(Money(BigInt.from(1), CryptoCurrency.btc).isZero, isFalse);
102 +
103 + expect(Money(BigInt.from(-1), CryptoCurrency.btc).isZero, isFalse);
104 + });
105 +
106 + test("isNegative", () {
107 + expect(Money(BigInt.from(0), CryptoCurrency.btc).isNegative, isFalse);
108 +
109 + expect(Money(BigInt.from(1), CryptoCurrency.btc).isNegative, isFalse);
110 +
111 + expect(Money(BigInt.from(-1), CryptoCurrency.btc).isNegative, isTrue);
112 + });
113 +
114 + group("Comparison", () {
115 + final fourBitcoin = Money(BigInt.from(400000000), CryptoCurrency.btc);
116 + final fiveBitcoin = Money(BigInt.from(500000000), CryptoCurrency.btc);
117 + final sixBitcoin = Money(BigInt.from(600000000), CryptoCurrency.btc);
118 +
119 + final fiveMonero = Money.parse("5", CryptoCurrency.xmr);
120 +
121 + test("==", () {
122 + expect(fiveBitcoin, equals(Money(BigInt.from(500000000), CryptoCurrency.btc)));
123 + expect(fiveBitcoin, isNot(equals(fourBitcoin)));
124 + expect(fiveBitcoin, isNot(equals(sixBitcoin)));
125 + expect(fiveBitcoin, isNot(equals(fiveMonero)));
126 + // intentional check of incompatible types.
127 + // ignore: unrelated_type_equality_checks
128 + expect(fiveBitcoin == "Not money", equals(false));
129 + });
130 +
131 + test("<", () {
132 + expect(fiveBitcoin < sixBitcoin, isTrue);
133 + expect(fiveBitcoin < fiveBitcoin, isFalse);
134 + expect(fiveBitcoin < fourBitcoin, isFalse);
135 +
136 + // Cannot compare money in different currencies:
137 + expect(() => fiveBitcoin < fiveMonero, throwsArgumentError);
138 + });
139 +
140 + test("<=", () {
141 + expect(fiveBitcoin <= sixBitcoin, isTrue);
142 + expect(fiveBitcoin <= fiveBitcoin, isTrue);
143 + expect(fiveBitcoin <= fourBitcoin, isFalse);
144 +
145 + // Cannot compare money in different currencies:
146 + expect(() => fiveBitcoin <= fiveMonero, throwsArgumentError);
147 + });
148 +
149 + test(">", () {
150 + expect(fiveBitcoin > fourBitcoin, isTrue);
151 + expect(fiveBitcoin > fiveBitcoin, isFalse);
152 + expect(fiveBitcoin > sixBitcoin, isFalse);
153 +
154 + // Cannot compare money in different currencies:
155 + expect(() => fiveBitcoin > fiveMonero, throwsArgumentError);
156 + });
157 +
158 + test(">=", () {
159 + expect(fiveBitcoin >= fourBitcoin, isTrue);
160 + expect(fiveBitcoin >= fiveBitcoin, isTrue);
161 + expect(fiveBitcoin >= sixBitcoin, isFalse);
162 +
163 + // Cannot compare money in different currencies:
164 + expect(() => fiveBitcoin >= fiveMonero, throwsArgumentError);
165 + });
166 +
167 + test("conformance to Comparable", () {
168 + expect(fiveBitcoin, isA<Comparable<Money>>());
169 +
170 + expect(fiveBitcoin.compareTo(fiveBitcoin), isZero);
171 + expect(fiveBitcoin.compareTo(fourBitcoin), isPositive);
172 + expect(fiveBitcoin.compareTo(sixBitcoin), isNegative);
173 + expect(() => fiveBitcoin.compareTo(fiveMonero), throwsArgumentError);
174 + });
175 + });
176 +
177 + group("Math", () {
178 + test("+", () {
179 + final oneBitcoin = Money.parse("1", CryptoCurrency.btc);
180 + final twoBitcoin = Money.parse("2", CryptoCurrency.btc);
181 + final threeBitcoin = Money.parse("3", CryptoCurrency.btc);
182 +
183 + expect(oneBitcoin + twoBitcoin, equals(threeBitcoin));
184 + });
185 +
186 + test("cannot add different currencies", () {
187 + final oneBitcoin = Money(BigInt.one, CryptoCurrency.btc);
188 + final oneMonero = Money(BigInt.one, CryptoCurrency.xmr);
189 +
190 + expect(() => oneBitcoin + oneMonero, throwsArgumentError);
191 + });
192 +
193 + test("invert money", () {
194 + final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc);
195 + final negativeOneSatoshi = Money(BigInt.from(-1), CryptoCurrency.btc);
196 +
197 + expect(-oneSatoshi, equals(negativeOneSatoshi));
198 + expect(-negativeOneSatoshi, equals(oneSatoshi));
199 + });
200 +
201 + test("-", () {
202 + final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc);
203 + final twoSatoshi = Money(BigInt.two, CryptoCurrency.btc);
204 + final threeSatoshi = Money(BigInt.from(3), CryptoCurrency.btc);
205 +
206 + expect(threeSatoshi - oneSatoshi, equals(twoSatoshi));
207 + });
208 +
209 + test("cannot subtract different currencies", () {
210 + final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc);
211 + final oneMonero = Money(BigInt.one, CryptoCurrency.xmr);
212 +
213 + expect(() => oneSatoshi - oneMonero, throwsArgumentError);
214 + });
215 +
216 + test("*", () {
217 + final zeroSatoshi = Money(BigInt.zero, CryptoCurrency.btc);
218 + final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc);
219 + final twoSatoshi = Money(BigInt.two, CryptoCurrency.btc);
220 +
221 + expect(oneSatoshi * BigInt.zero, equals(zeroSatoshi));
222 + expect(oneSatoshi * BigInt.two, equals(twoSatoshi));
223 + expect(oneSatoshi * BigInt.from(-2), equals(-twoSatoshi));
224 + });
225 +
226 + group("/", () {
227 + final threeBI = BigInt.from(3);
228 + final fourBI = BigInt.from(4);
229 +
230 + final negThreeBI = BigInt.from(-3);
231 + final negFourBI = BigInt.from(-4);
232 +
233 + test("rounds down when fractional part is < 0.5", () {
234 + expect((Money(BigInt.from(10), CryptoCurrency.btc) / threeBI).amount, threeBI);
235 + expect((Money(BigInt.from(-10), CryptoCurrency.btc) / threeBI).amount, negThreeBI);
236 + expect((Money(BigInt.from(10), CryptoCurrency.btc) / negThreeBI).amount, negThreeBI);
237 + expect((Money(BigInt.from(-10), CryptoCurrency.btc) / negThreeBI).amount, threeBI);
238 + });
239 +
240 + test("rounds up when fractional part is > 0.5", () {
241 + expect((Money(BigInt.from(11), CryptoCurrency.btc) / threeBI).amount, fourBI); // 3.66...
242 + expect((Money(BigInt.from(-11), CryptoCurrency.btc) / threeBI).amount, negFourBI);
243 + expect((Money(BigInt.from(11), CryptoCurrency.btc) / negThreeBI).amount, negFourBI);
244 + expect((Money(BigInt.from(-11), CryptoCurrency.btc) / negThreeBI).amount, fourBI);
245 + });
246 +
247 + test("rounds AWAY from zero when fractional part is exactly 0.5 (Ties)", () {
248 + // 5 / 2 = 2.5 -> 3
249 + expect((Money(BigInt.from(5), CryptoCurrency.btc) / BigInt.two).amount, threeBI);
250 + // -5 / 2 = -2.5 -> -3
251 + expect((Money(BigInt.from(-5), CryptoCurrency.btc) / BigInt.two).amount, negThreeBI);
252 + // 5 / -2 = -2.5 -> -3
253 + expect((Money(BigInt.from(5), CryptoCurrency.btc) / -BigInt.two).amount, negThreeBI);
254 + // -5 / -2 = 2.5 -> 3
255 + expect((Money(BigInt.from(-5), CryptoCurrency.btc) / -BigInt.two).amount, threeBI);
256 + });
257 +
258 + test("handles zero numerator correctly", () {
259 + expect((Money(BigInt.zero, CryptoCurrency.btc) / BigInt.from(5)).amount, BigInt.zero);
260 + expect((Money(BigInt.zero, CryptoCurrency.btc) / BigInt.from(-5)).amount, BigInt.zero);
261 + });
262 +
263 + test("throws Exception on division by zero", () {
264 + expect(
265 + () => Money(BigInt.from(5), CryptoCurrency.btc) / BigInt.zero,
266 + throwsA(isA<Exception>()),
267 + );
268 + });
269 +
270 + test("handles very large BigInts without precision loss", () {
271 + // A number that would lose precision if converted to a standard double
272 + final hugeA = BigInt.parse("10000000000000000000000000000000000005");
273 + final hugeB = BigInt.parse("10");
274 + // 1000...005 / 10 = 1000...000.5 -> Should round up to 1000...001
275 + final expected = BigInt.parse("1000000000000000000000000000000000001");
276 +
277 + expect((Money(hugeA, CryptoCurrency.btc) / hugeB).amount, expected);
278 + });
279 +
280 + test("handles exact division perfectly", () {
281 + expect((Money(BigInt.from(100), CryptoCurrency.btc) / BigInt.from(20)).amount,
282 + BigInt.from(5));
283 + expect((Money(BigInt.from(-100), CryptoCurrency.btc) / BigInt.from(20)).amount,
284 + BigInt.from(-5));
285 + });
286 + });
287 + });
288 +
289 + group("copyWith", () {
290 + final money = Money.parse("1", CryptoCurrency.btc);
291 +
292 + test("currency", () {
293 + final copy = money.copyWith(currency: CryptoCurrency.eth);
294 + expect(copy.amount, equals(BigInt.parse("1000000000000000000")));
295 + expect(copy.currency.decimals, equals(18));
296 + expect(copy.toString(), "1");
297 + });
298 +
299 + test("currency nano", () {
300 + final copy = money.copyWith(currency: CryptoCurrency.nano);
301 + expect(copy.amount, equals(BigInt.parse("1000000000000000000000000000000")));
302 + expect(copy.currency.decimals, equals(30));
303 + expect(copy.toString(), "1");
304 + });
305 +
306 + test("amount", () {
307 + final copy = money.copyWith(amount: BigInt.one);
308 + expect(copy.amount, BigInt.one);
309 + expect(copy.currency.decimals, 8);
310 + });
311 +
312 + test("amount and currency", () {
313 + final copy = money.copyWith(amount: BigInt.one);
314 + expect(copy.amount, BigInt.one);
315 + expect(copy.currency.decimals, 8);
316 + });
317 + });
318 +
319 + group("toStringWithPrecision", (){
320 + test("with fractionalDigits and padded with zeros", () {
321 + final money = Money.parse("1", CryptoCurrency.btc);
322 + expect(money.amount, equals(BigInt.parse("100000000")));
323 + expect(money.currency.decimals, equals(8));
324 + expect(money.toStringWithPrecision(fractionalDigits: 5, trimZeros: false), "1.00000");
325 + });
326 +
327 + test("using base unit sats", () {
328 + final money = Money.parse("1", CryptoCurrency.btc);
329 + expect(money.amount, equals(BigInt.parse("100000000")));
330 + expect(money.currency.decimals, equals(8));
331 + expect(money.toStringWithPrecision(useBaseUnit: true), "100000000");
332 + });
333 + });
334 +
335 + group("toStringWithSymbol", (){
336 + test("with fractionalDigits and padded with zeros", () {
337 + final money = Money.parse("1", CryptoCurrency.btc);
338 + expect(money.amount, equals(BigInt.parse("100000000")));
339 + expect(money.currency.decimals, equals(8));
340 + expect(money.toStringWithSymbol(fractionalDigits: 5, trimZeros: false), "1.00000 BTC");
341 + });
342 +
343 + test("using base unit sats", () {
344 + final money = Money.parse("1", CryptoCurrency.btc);
345 + expect(money.amount, equals(BigInt.parse("100000000")));
346 + expect(money.currency.decimals, equals(8));
347 + expect(money.toStringWithSymbol(useBaseUnit: true), "100000000 sats");
348 + });
349 + });
350 + });
351 +}
cw_core/test/lnurl_test.dart
+14 -11
@@ -1,3 +1,5 @@
1 +import 'package:cw_core/amount/money.dart';
2 +import 'package:cw_core/crypto_currency.dart';
3 import 'package:cw_core/lnurl.dart';
4 import 'package:cw_core/utils/proxy_wrapper.dart';
5 import 'package:cw_core/utils/tor/abstract.dart';
@@ -11,13 +13,14 @@ void main() {
13 expect(content, Uri.parse("https://cake.cash/.well-known/lnurlp/konsti"));
14 });
15
14 - test('get amount', () async {
15 - CakeTor.instance = await CakeTorInstance.getInstance();
16 -
17 - final content = await LNURL.getPayRequestAmount(
18 - "LNURL1DP68GURN8GHJ7CTSWQHXY6T5WFJHZAT9WD6ZU6T09AUZ7URJDAU8JTMKXYHKCM308A5N6MNHVVNXJEPAXGMNWWPEX3JRGDTPXVNXZ0FCX5CRQVPSXQNX602VFE24YNP9XGC8W6T5DQJNYVRPD4HH2MN5GTRNLZ");
19 - expect(content, 8500);
20 - });
16 + // This tests includes a rest request to an URL that expired and due to the lack of an alternative URL we ignore this test
17 + // test('get amount', () async {
18 + // CakeTor.instance = await CakeTorInstance.getInstance();
19 + //
20 + // final content = await LNURL.getPayRequestAmount(
21 + // "LNURL1DP68GURN8GHJ7CTSWQHXY6T5WFJHZAT9WD6ZU6T09AUZ7URJDAU8JTMKXYHKCM308A5N6MNHVVNXJEPAXGMNWWPEX3JRGDTPXVNXZ0FCX5CRQVPSXQNX602VFE24YNP9XGC8W6T5DQJNYVRPD4HH2MN5GTRNLZ");
22 + // expect(content, Money.fromInt(8500, CryptoCurrency.btcln));
23 + // });
24
25 test('get no amount', () async {
26 CakeTor.instance = await CakeTorInstance.getInstance();
@@ -64,22 +67,22 @@ void main() {
67
68 test("should get the amount 1 BTC encoded as bitcoin", () {
69 final invoice = "lnbc11p5exaxspp5u6e6lx90rw2vqr2e5axt8ur3v98qnmqfsmh8r0l0mcm7ry70vyvsdqqcqzzsxqrrsssp5eqydmrufk39dwhuxzajwx4xyffn0mlcu3qa8vjwry6qev5gsepkq9qxpqysgqt0zldve9uxt3txjt8wp0gev52lquljgugy26nneue85pjwk5yak4hs2d8533tus07pq5ka6zrnd0ruaeepwres8p7ws5mx63xlwff5sqlxcy8a";
67 - expect(getBolt11Amount(invoice), 100000000);
70 + expect(getBolt11Amount(invoice), Money.fromInt(100000000, CryptoCurrency.btcln));
71 });
72
73 test("should get the amount 0,1 BTC encoded as millibitcoin", () {
74 final invoice = "lnbc100m1p5exagmpp5n4c5tml3h8h7k4gthc9papkyeymceqqvcldkng4nvxwzj7nu9jnsdqqcqzzsxqrrsssp58kgzrh7y3pevantduc4yg33aa6r79ed6xgmhr0dy40zc8pxrz5ms9qxpqysgqcq27km3v3wcz6pntmsfnz259zy6tff0uwjz9kamdrvmxmdv09gp44mh0ygn7gweqf4zdev4x8e67p5kcvahethchdkkjexhtk7emuqcpasz0lp";
72 - expect(getBolt11Amount(invoice), 10000000);
75 + expect(getBolt11Amount(invoice), Money.fromInt(10000000, CryptoCurrency.btcln));
76 });
77
78 test("should get the amount 1000 SATS encoded as microbitcoin", () {
79 final invoice = "lnbc10u1p5exfgvpp5n3s9dsw9ddax5c3h8437ya7y4g582uyufyna626yr9vqdfjrhwzqdqqcqzzsxqrrsssp5mmkyp5h35fpaxm4063f3kc7d4nmgd4gd8fv8086wxd784m76kmpq9qxpqysgqye9r0vahpjy9l2je6vakxzk3cjnsyx76r29c4amkz4cmapq5tqt5l7rl0emkd003jjvz2d8jqrw6wc4cduvapuyeseh7a855l82wutcpeq30kg";
77 - expect(getBolt11Amount(invoice), 1000);
80 + expect(getBolt11Amount(invoice), Money.fromInt(1000, CryptoCurrency.btcln));
81 });
82
83 test("should get the amount 1 SAT encoded as nanobitcoin", () {
84 final invoice = "lnbc10n1p5exazppp5szh273r6nzvwydzsdlg0ws0vhxwnc6nqtqt4sjt0vu6x6l8t5csqdqqcqzzsxqrrsssp5nkup6uht7xlfqaj8lxpe5kx0ejum9p4pvv3nlnndr9ykzge3393s9qxpqysgqufh05a7qs0ezeckvmh7tjkr9ngng96nq274twg20z45ysp7afpdrttm2ky73cpjwpjqsmtzzd79w6lyvszgmkanwn8mj0jcfunrwv3qprg3z3j";
82 - expect(getBolt11Amount(invoice), 1);
85 + expect(getBolt11Amount(invoice), Money.fromInt(1, CryptoCurrency.btcln));
86 });
87 });
88 }
cw_decred/lib/amount_format.dart deleted
-26
@@ -1,26 +0,0 @@
1 -import 'package:intl/intl.dart';
2 -import 'package:cw_core/crypto_amount_format.dart';
3 -
4 -const decredAmountLength = 8;
5 -const decredAmountDivider = 100000000;
6 -final decredAmountFormat = NumberFormat()
7 - ..maximumFractionDigits = decredAmountLength
8 - ..minimumFractionDigits = 1;
9 -
10 -String decredAmountToString({required int amount}) =>
11 - decredAmountFormat.format(cryptoAmountToDouble(amount: amount, divider: decredAmountDivider));
12 -
13 -double decredAmountToDouble({required int amount}) =>
14 - cryptoAmountToDouble(amount: amount, divider: decredAmountDivider);
15 -
16 -int stringDoubleToDecredAmount(String amount) {
17 - int result = 0;
18 -
19 - try {
20 - result = (double.parse(amount) * decredAmountDivider).round();
21 - } catch (e) {
22 - result = 0;
23 - }
24 -
25 - return result;
26 -}
cw_decred/lib/balance.dart
+9 -19
@@ -1,26 +1,16 @@
1 -import 'package:cw_decred/amount_format.dart';
1 +import 'package:cw_core/amount/money.dart';
2 import 'package:cw_core/balance.dart';
3 +import 'package:cw_core/crypto_currency.dart';
4
5 class DecredBalance extends Balance {
5 - DecredBalance({required this.confirmed, required this.unconfirmed, required int frozen})
6 - : _frozen = frozen, super.fromInt(confirmed, unconfirmed);
6 + DecredBalance({required Money confirmed, required Money unconfirmed, required Money frozen})
7 + : super(confirmed, unconfirmed, frozen: frozen);
8
8 - factory DecredBalance.zero() => DecredBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
9 -
10 - final int confirmed;
11 - final int unconfirmed;
12 - final int _frozen;
13 - BigInt get frozen => BigInt.from(_frozen);
14 -
15 - @override
16 - String get formattedAvailableBalance => decredAmountToString(amount: confirmed - _frozen);
17 -
18 - @override
19 - String get formattedAdditionalBalance => decredAmountToString(amount: unconfirmed);
9 + factory DecredBalance.zero() => DecredBalance(
10 + confirmed: Money.zero(CryptoCurrency.dcr),
11 + unconfirmed: Money.zero(CryptoCurrency.dcr),
12 + frozen: Money.zero(CryptoCurrency.dcr));
13
14 @override
22 - String get formattedUnAvailableBalance {
23 - final frozenFormatted = decredAmountToString(amount: _frozen);
24 - return frozenFormatted == '0.0' ? '' : frozenFormatted;
25 - }
15 + Money get available => super.available - (frozen ?? Money.zero(CryptoCurrency.dcr));
16 }
cw_decred/lib/pending_transaction.dart
+16 -25
@@ -1,42 +1,33 @@
1 +import 'package:cw_core/amount/money.dart';
2 import 'package:cw_core/pending_transaction.dart';
2 -import 'package:cw_decred/amount_format.dart';
3
4 class DecredPendingTransaction with PendingTransaction {
5 - DecredPendingTransaction(
6 - {required this.txid,
7 - required this.amount,
8 - required this.fee,
9 - required this.rawHex,
10 - required this.send});
11 -
12 - final int amount;
13 - final int fee;
14 - final String txid;
5 + DecredPendingTransaction({
6 + required this.txId,
7 + required this.amount,
8 + required this.fee,
9 + required this.rawHex,
10 + required this.send,
11 + });
12 +
13 + final Money amount;
14 + final Money fee;
15 + final String txId;
16 final String rawHex;
17 final Future<void> Function() send;
18
19 @override
19 - String get id => txid;
20 -
21 - @override
22 - String get amountFormatted => decredAmountToString(amount: amount);
23 -
24 - @override
25 - String get feeFormatted => "$feeFormattedValue DCR";
20 + String get id => txId;
21
22 @override
28 - String get feeFormattedValue => decredAmountToString(amount: fee);
23 + String get amountFormatted => amount.toString();
24
25 @override
26 String get hex => rawHex;
27
28 @override
34 - Future<void> commit() async {
35 - return send();
36 - }
29 + Future<void> commit() => send();
30
31 @override
39 - Future<Map<String, String>> commitUR() {
40 - throw UnimplementedError();
41 - }
32 + Future<Map<String, String>> commitUR() => throw UnimplementedError();
33 }
cw_decred/lib/transaction_info.dart
+3 -22
@@ -1,15 +1,12 @@
1 -import 'package:cw_core/currency_for_wallet_type.dart';
1 +import 'package:cw_core/amount/money.dart';
2 import 'package:cw_core/transaction_direction.dart';
3 import 'package:cw_core/transaction_info.dart';
4 -import 'package:cw_core/format_amount.dart';
5 -import 'package:cw_core/wallet_type.dart';
6 -import 'package:cw_decred/amount_format.dart';
4
5 class DecredTransactionInfo extends TransactionInfo {
6 DecredTransactionInfo({
7 required String id,
11 - required int amount,
12 - required int fee,
8 + required Money amount,
9 + required Money fee,
10 required TransactionDirection direction,
11 required bool isPending,
12 required DateTime date,
@@ -27,20 +24,4 @@ class DecredTransactionInfo extends TransactionInfo {
24 this.confirmations = confirmations;
25 this.to = to;
26 }
30 -
31 - String? _fiatAmount;
32 -
33 - @override
34 - String amountFormatted() =>
35 - '${formatAmount(decredAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(WalletType.decred).title}';
36 -
37 - @override
38 - String? feeFormatted() =>
39 - '${formatAmount(decredAmountToString(amount: fee ?? 0))} ${walletTypeToCryptoCurrency(WalletType.decred).title}';
40 -
41 - @override
42 - String fiatAmount() => _fiatAmount ?? '';
43 -
44 - @override
45 - void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
27 }
cw_decred/lib/wallet.dart
+33 -38
@@ -1,13 +1,13 @@
1 import 'dart:async';
2 import 'dart:convert';
3 import 'dart:io';
4 +import 'package:cw_core/amount/money.dart';
5 import 'package:path/path.dart' as p;
6 import 'package:cw_core/exceptions.dart';
7 import 'package:cw_core/transaction_direction.dart';
8 import 'package:cw_core/utils/print_verbose.dart';
9 import 'package:cw_core/pathForWallet.dart';
10 import 'package:cw_core/wallet_type.dart';
10 -import 'package:cw_decred/amount_format.dart';
11 import 'package:cw_decred/pending_transaction.dart';
12 import 'package:cw_decred/transaction_credentials.dart';
13 import 'package:flutter/foundation.dart';
@@ -121,12 +121,7 @@ abstract class DecredWalletBase
121 @override
122 bool isTestnet;
123
124 - String get pubkey {
125 - return _pubkey;
126 - }
127 -
128 - @override
129 - String formatCryptoAmount(String amount) => decredAmountToString(amount: int.parse(amount));
124 + String get pubkey => _pubkey;
125
126 Future<void> init() async {
127 final getSeed = () async {
@@ -354,13 +349,12 @@ abstract class DecredWalletBase
349 Future<PendingTransaction> createTransaction(Object credentials) async {
350 if (watchingOnly) {
351 return DecredPendingTransaction(
357 - txid: "",
358 - amount: 0,
359 - fee: 0,
360 - rawHex: "",
361 - send: () async {
362 - throw "unable to send with watching only wallet";
363 - });
352 + txId: "",
353 + amount: Money.zero(currency),
354 + fee: Money.zero(currency),
355 + rawHex: "",
356 + send: () async => throw "unable to send with watching only wallet",
357 + );
358 }
359 var totalIn = 0;
360 final ignoreInputs = [];
@@ -380,14 +374,12 @@ abstract class DecredWalletBase
374 for (final out in creds.outputs) {
375 var amt = 0;
376 if (out.sendAll) {
383 - if (creds.outputs.length != 1) {
384 - throw "can only send all to one output";
385 - }
377 + if (creds.outputs.length != 1) throw "can only send all to one output";
378 +
379 sendAll = true;
380 totalAmt = totalIn;
388 - } else if (out.cryptoAmount != null) {
389 - final coins = double.parse(out.cryptoAmount!);
390 - amt = (coins * 1e8).round();
381 + } else {
382 + amt = out.cryptoAmount.amount.toInt();
383 }
384 totalAmt += amt;
385 final o = {
@@ -416,6 +408,7 @@ abstract class DecredWalletBase
408 final res = await _libwallet.createSignedTransaction(walletInfo.name, jsonEncode(signReq));
409 final decoded = json.decode(res);
410 final signedHex = decoded["signedhex"];
411 +
412 final send = () async {
413 await _libwallet.sendRawTransaction(walletInfo.name, signedHex);
414 await updateBalance();
@@ -424,8 +417,14 @@ abstract class DecredWalletBase
417 if (sendAll) {
418 totalAmt = (totalAmt - fee).round();
419 }
420 +
421 return DecredPendingTransaction(
428 - txid: decoded["txid"] ?? "", amount: totalAmt, fee: fee, rawHex: signedHex, send: send);
422 + txId: decoded["txid"] ?? "",
423 + amount: Money.fromInt(totalAmt, currency),
424 + fee: Money.fromInt(fee, currency),
425 + rawHex: signedHex,
426 + send: send,
427 + );
428 }
429
430 int feeRate(TransactionPriority priority) {
@@ -477,15 +476,14 @@ abstract class DecredWalletBase
476 }
477
478 @override
480 - Future<Map<String, DecredTransactionInfo>> fetchTransactions() async {
481 - return this.fetchFiveTransactions(0);
482 - }
479 + Future<Map<String, DecredTransactionInfo>> fetchTransactions() => fetchFiveTransactions(0);
480
481 Future<Map<String, DecredTransactionInfo>> fetchFiveTransactions(int from) async {
482 try {
483 final res = await _libwallet.listTransactions(walletInfo.name, from.toString(), "5");
484 final decoded = json.decode(res);
488 - var txs = <String, DecredTransactionInfo>{};
485 + final txs = <String, DecredTransactionInfo>{};
486 +
487 for (final d in decoded) {
488 final txid = uniqueTxID(d["txid"] ?? "", d["vout"] ?? 0);
489 var direction = TransactionDirection.outgoing;
@@ -498,19 +496,18 @@ abstract class DecredWalletBase
496 final fee = (feeDouble * 1e8).round().abs();
497 final confs = d["confirmations"] ?? 0;
498 final sendTime = d["time"] ?? 0;
501 - final height = d["height"] ?? 0;
502 - final txInfo = DecredTransactionInfo(
499 +
500 + txs[txid] = DecredTransactionInfo(
501 id: txid,
504 - amount: amount,
505 - fee: fee,
502 + amount: Money.fromInt(amount, currency),
503 + fee: Money.fromInt(fee, currency),
504 direction: direction,
505 isPending: confs == 0,
506 date: DateTime.fromMillisecondsSinceEpoch(sendTime * 1000, isUtc: false),
509 - height: height,
507 + height: d["height"] ?? 0,
508 confirmations: confs,
509 to: d["address"] ?? "",
510 );
513 - txs[txid] = txInfo;
511 }
512 return txs;
513 } catch (e) {
@@ -520,9 +517,7 @@ abstract class DecredWalletBase
517 }
518
519 // uniqueTxID combines the tx id and vout to create a unique id.
523 - String uniqueTxID(String id, int vout) {
524 - return id + ":" + vout.toString();
525 - }
520 + String uniqueTxID(String id, int vout) => "$id:$vout";
521
522 @override
523 Future<void> save() async {}
@@ -587,9 +582,9 @@ abstract class DecredWalletBase
582 });
583
584 balance[CryptoCurrency.dcr] = DecredBalance(
590 - confirmed: balanceMap["confirmed"] ?? 0,
591 - unconfirmed: balanceMap["unconfirmed"] ?? 0,
592 - frozen: totalFrozen,
585 + confirmed: Money.fromInt(balanceMap["confirmed"] ?? 0, currency),
586 + unconfirmed: Money.fromInt(balanceMap["unconfirmed"] ?? 0, currency),
587 + frozen: Money.fromInt(totalFrozen, currency),
588 );
589 }
590
@@ -648,7 +643,7 @@ abstract class DecredWalletBase
643 try {
644 final res = await _libwallet.listUnspents(walletInfo.name);
645 final decoded = json.decode(res);
651 - var unspents = <Unspent>[];
646 + final unspents = <Unspent>[];
647 for (final d in decoded) {
648 final spendable = d["spendable"] ?? false;
649 if (!spendable) {
cw_decred/lib/wallet_service.dart
-1
@@ -10,7 +10,6 @@ import 'package:cw_core/wallet_info.dart';
10 import 'package:cw_core/wallet_type.dart';
11 import 'package:path/path.dart';
12 import 'package:hive/hive.dart';
13 -import 'package:collection/collection.dart';
13 import 'package:cw_core/unspent_coins_info.dart';
14
15 class DecredWalletService extends WalletService<
cw_dogecoin/lib/src/dogecoin_wallet.dart
+1 -1
@@ -68,7 +68,7 @@ abstract class DogeCoinWalletBase extends ElectrumWallet with Store {
68 }
69
70 @override
71 - int get networkDustAmount => 100000000; // 1 DOGE = 1e8 koinu
71 + BigInt get networkDustAmount => BigInt.from(100000000); // 1 DOGE = 1e8 koinu
72
73 static int estimatedDogeCoinTransactionSize(int inputsCount, int outputsCounts) =>
74 inputsCount * 180 + outputsCounts * 34 + 10;
cw_evm/lib/clients/evm_chain_client.dart
+22 -29
@@ -2,6 +2,7 @@ import 'dart:async';
2 import 'dart:convert';
3 import 'dart:developer';
4
5 +import 'package:cw_core/amount/money.dart';
6 import 'package:cw_core/crypto_currency.dart';
7 import 'package:cw_core/erc20_token.dart';
8 import 'package:cw_core/node.dart';
@@ -298,14 +299,13 @@ class EVMChainClient {
299 Future<PendingEVMChainTransaction> signTransaction({
300 required Credentials privateKey,
301 required String toAddress,
301 - required BigInt amount,
302 - required BigInt gasFee,
302 + required Money amount,
303 + required Money gasFee,
304 required int estimatedGasUnits,
305 required int maxFeePerGas,
306 required EVMChainTransactionPriority? priority,
307 required CryptoCurrency currency,
308 required String feeCurrency,
308 - required int exponent,
309 String? contractAddress,
310 String? data,
311 int? gasPrice,
@@ -318,11 +318,13 @@ class EVMChainClient {
318 currency == CryptoCurrency.bnb ||
319 contractAddress != null);
320
321 - bool isNativeToken = currency == CryptoCurrency.eth ||
322 - currency == CryptoCurrency.maticpoly ||
323 - currency == CryptoCurrency.baseEth ||
324 - currency == CryptoCurrency.arbEth ||
325 - currency == CryptoCurrency.bnb;
321 + final isNativeToken = [
322 + CryptoCurrency.eth,
323 + CryptoCurrency.maticpoly,
324 + CryptoCurrency.baseEth,
325 + CryptoCurrency.arbEth,
326 + CryptoCurrency.bnb
327 + ].contains(currency);
328
329 // Get nonce with "pending" block tag to include pending transactions
330 // This prevents "Nonce too low" errors when sending multiple transactions quickly
@@ -336,7 +338,7 @@ class EVMChainClient {
338 to: EthereumAddress.fromHex(toAddress),
339 maxPriorityFeePerGas:
340 priority != null ? EtherAmount.fromInt(EtherUnit.gwei, priority.tip) : null,
339 - amount: isNativeToken ? EtherAmount.inWei(amount) : EtherAmount.zero(),
341 + amount: isNativeToken ? EtherAmount.inWei(amount.amount) : EtherAmount.zero(),
342 data: data != null ? hexToBytes(data) : null,
343 maxGas: estimatedGasUnits,
344 maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas),
@@ -359,7 +361,7 @@ class EVMChainClient {
361
362 signedTransaction = await erc20.transfer(
363 EthereumAddress.fromHex(toAddress),
362 - amount,
364 + amount.amount,
365 credentials: privateKey,
366 transaction: transaction,
367 );
@@ -370,24 +372,20 @@ class EVMChainClient {
372
373 return PendingEVMChainTransaction(
374 signedTransaction: prepareSignedTransactionForSending(signedTransaction),
373 - amount: amount.toString(),
375 + amount: amount,
376 fee: gasFee,
375 - feeCurrency: feeCurrency,
377 sendTransaction: _sendTransaction,
377 - exponent: exponent,
378 );
379 }
380
381 Future<PendingEVMChainTransaction> signApprovalTransaction({
382 required Credentials privateKey,
383 required String spender,
384 - required BigInt amount,
385 - required BigInt gasFee,
386 - required String feeCurrency,
384 + required Money amount,
385 + required Money gasFee,
386 required int estimatedGasUnits,
387 required int maxFeePerGas,
388 required EVMChainTransactionPriority? priority,
390 - required int exponent,
389 required String contractAddress,
390 int? gasPrice,
391 bool useBlinkProtection = true,
@@ -417,20 +415,18 @@ class EVMChainClient {
415
416 final signedTransaction = await erc20.approve(
417 EthereumAddress.fromHex(spender),
420 - amount,
418 + amount.amount,
419 credentials: privateKey,
420 transaction: transaction,
421 );
422
423 return PendingEVMChainTransaction(
424 signedTransaction: prepareSignedTransactionForSending(signedTransaction),
427 - amount: amount.toString(),
425 + amount: amount,
426 fee: gasFee,
429 - feeCurrency: feeCurrency,
427 sendTransaction: () =>
428 sendTransaction(signedTransaction, useBlinkProtection: useBlinkProtection),
432 - exponent: exponent,
433 - isInfiniteApproval: amount.toRadixString(16) ==
429 + isInfiniteApproval: amount.amount.toRadixString(16) ==
430 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
431 );
432 }
@@ -524,20 +520,17 @@ class EVMChainClient {
520 */
521 }
522
527 - Future<EVMChainERC20Balance> fetchERC20Balances(
528 - EthereumAddress userAddress, String contractAddress) async {
523 + Future<EVMChainERC20Balance> fetchERC20Balances(EthereumAddress userAddress, Erc20Token token) async {
524 try {
530 - final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
525 + final erc20 = ERC20(address: EthereumAddress.fromHex(token.contractAddress), client: _client!);
526 final balance = await erc20.balanceOf(userAddress);
527
533 - int exponent = (await erc20.decimals()).toInt();
534 -
535 - return EVMChainERC20Balance(balance, exponent: exponent);
528 + return EVMChainERC20Balance(Money(balance, token));
529 } on RangeError catch (_) {
530 throw Exception('Invalid token contract for this network.');
531 } catch (e) {
532 if (e.toString().contains("hostUnreachable")) {
540 - return EVMChainERC20Balance(BigInt.zero);
533 + return EVMChainERC20Balance(Money.zero(token));
534 }
535 throw Exception('Could not fetch balances: ${e.toString()}');
536 }
cw_evm/lib/deuro/deuro_savings.dart
+96 -52
@@ -1,5 +1,7 @@
1 +import 'package:cw_core/amount/money.dart';
2 import 'package:cw_core/crypto_currency.dart';
2 -import 'package:cw_evm/deuro/deuro_savings_contract.dart';
3 +import 'package:cw_evm/deuro/deuro_savings_gateway_contract.dart' as v1;
4 +import 'package:cw_evm/deuro/deuro_savings_contract.dart' as v2;
5 import 'package:cw_evm/evm_chain_wallet.dart';
6 import 'package:cw_evm/contract/erc20.dart';
7 import 'package:cw_evm/evm_chain_exceptions.dart';
@@ -9,21 +11,27 @@ import 'package:web3dart/crypto.dart';
11 import 'package:web3dart/web3dart.dart';
12
13 const String savingsGatewayAddress = "0x073493d73258C4BEb6542e8dd3e1b2891C972303";
14 +const String savingsV2Address = "0x760233b90e45d186A9A98E911B115F7F4B90d3D9";
15
16 const String dEuroAddress = "0xbA3f535bbCcCcA2A154b573Ca6c5A49BAAE0a3ea";
17 const String frontendCode = "0x00000000000000000000000000000000000000000043616b652057616c6c6574";
18
19 class DEuro {
17 - final SavingsGateway _savingsGateway;
20 + final v2.Savings _savings;
21 final ERC20 _dEuro;
22 final EVMChainWallet _wallet;
23
24 DEuro(EVMChainWallet wallet)
25 : _wallet = wallet,
23 - _savingsGateway = _getSavingsGateway(wallet.getWeb3Client()!),
26 + _savings = _getSavings(wallet.getWeb3Client()!),
27 _dEuro = _getDEuroToken(wallet.getWeb3Client()!);
28
26 - static SavingsGateway _getSavingsGateway(Web3Client client) => SavingsGateway(
29 + static v2.Savings _getSavings(Web3Client client) => v2.Savings(
30 + address: EthereumAddress.fromHex(savingsV2Address),
31 + client: client,
32 + );
33 +
34 + static v1.SavingsGateway _getSavingsGateway(Web3Client client) => v1.SavingsGateway(
35 address: EthereumAddress.fromHex(savingsGatewayAddress),
36 client: client,
37 );
@@ -35,26 +43,31 @@ class DEuro {
43
44 EthereumAddress get _address => EthereumAddress.fromHex(_wallet.walletAddresses.primaryAddress);
45
38 - Future<BigInt> get savingsBalance async =>
39 - (await _savingsGateway.savings(accountOwner: _address)).saved;
46 + Future<Money> get savingsBalance async =>
47 + Money((await _savings.savings(accountOwner: _address)).saved, CryptoCurrency.deuro);
48 +
49 + Future<Money> get savingsBalanceV1 async => Money(
50 + (await _getSavingsGateway(_wallet.getWeb3Client()!).savings(accountOwner: _address)).saved,
51 + CryptoCurrency.deuro);
52
41 - Future<BigInt> get accruedInterest => _savingsGateway.accruedInterest(accountOwner: _address);
53 + Future<Money> get accruedInterest async =>
54 + Money(await _savings.accruedInterest(accountOwner: _address), CryptoCurrency.deuro);
55
43 - Future<BigInt> get interestRate => _savingsGateway.currentRatePPM();
56 + Future<BigInt> get interestRate => _savings.currentRatePPM();
57
45 - Future<BigInt> get approvedBalance => _dEuro.allowance(_address, _savingsGateway.self.address);
58 + Future<BigInt> get approvedBalance => _dEuro.allowance(_address, _savings.self.address);
59
60 Future<void> _checkEthBalanceForGasFees(EVMChainTransactionPriority priority) async {
61 final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address);
62 final currentBalance = ethBalance.getInWei;
63 + final savings = _getSavingsGateway(_wallet.getWeb3Client()!);
64
65 final gasFeesModel = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
52 - amount: BigInt.zero,
53 - contractAddress: _savingsGateway.self.address.hexEip55,
54 - receivingAddressHex: _savingsGateway.self.address.hexEip55,
66 + amount: Money.zero(_wallet.currency),
67 + contractAddress: savings.self.address.hexEip55,
68 + receivingAddressHex: savings.self.address.hexEip55,
69 priority: priority,
56 - data: _savingsGateway.self.abi.functions[17]
57 - .encodeCall([BigInt.zero, hexToBytes(frontendCode)]),
70 + data: savings.self.abi.functions[17].encodeCall([BigInt.zero, hexToBytes(frontendCode)]),
71 );
72
73 final estimatedGasFee = BigInt.from(gasFeesModel.estimatedGasFee);
@@ -73,28 +86,26 @@ class DEuro {
86 try {
87 await _checkEthBalanceForGasFees(priority);
88
76 - final signedTransaction = await _savingsGateway.save(
77 - (amount: amount, frontendCode: hexToBytes(frontendCode)),
89 + final signedTransaction = await _savings.save(
90 + (amount: amount, compound: true),
91 credentials: _wallet.evmChainPrivateKey,
92 );
93
94 final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
82 - amount: amount,
83 - contractAddress: _savingsGateway.self.address.hexEip55,
84 - receivingAddressHex: _savingsGateway.self.address.hexEip55,
95 + amount: Money.zero(_wallet.currency),
96 + contractAddress: _savings.self.address.hexEip55,
97 + receivingAddressHex: _savings.self.address.hexEip55,
98 priority: priority,
86 - data: _savingsGateway.self.abi.functions[17].encodeCall([amount, hexToBytes(frontendCode)]),
99 + data: _savings.self.abi.functions[18].encodeCall([amount, true]),
100 );
101
89 - final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
102 + sendTransaction() => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
103
104 return PendingEVMChainTransaction(
92 - feeCurrency: "ETH",
105 sendTransaction: sendTransaction,
106 signedTransaction: signedTransaction,
95 - fee: BigInt.from(fee.estimatedGasFee),
96 - amount: amount.toString(),
97 - exponent: 18,
107 + fee: Money(BigInt.from(fee.estimatedGasFee), CryptoCurrency.eth),
108 + amount: Money(amount, CryptoCurrency.deuro),
109 );
110 } catch (e) {
111 if (e.toString().contains('insufficient funds for gas')) {
@@ -110,28 +121,65 @@ class DEuro {
121 try {
122 await _checkEthBalanceForGasFees(priority);
123
113 - final signedTransaction = await _savingsGateway.withdraw(
124 + final signedTransaction = await _savings.withdraw(
125 + (target: _address, amount: amount),
126 + credentials: _wallet.evmChainPrivateKey,
127 + );
128 +
129 + final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
130 + amount: Money.zero(_wallet.currency),
131 + contractAddress: _savings.self.address.hexEip55,
132 + receivingAddressHex: _savings.self.address.hexEip55,
133 + priority: priority,
134 + data: _savings.self.abi.functions[23].encodeCall([_address, amount]),
135 + );
136 +
137 + sendTransaction() => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
138 +
139 + return PendingEVMChainTransaction(
140 + sendTransaction: sendTransaction,
141 + signedTransaction: signedTransaction,
142 + fee: Money(BigInt.from(fee.estimatedGasFee), CryptoCurrency.eth),
143 + amount: Money(amount, CryptoCurrency.deuro),
144 + );
145 + } catch (e) {
146 + if (e.toString().contains('insufficient funds for gas')) {
147 + final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address);
148 + throw InsufficientGasFeeException(currentBalance: ethBalance.getInWei);
149 + }
150 + rethrow;
151 + }
152 + }
153 +
154 + Future<PendingEVMChainTransaction> withdrawSavingsV1(EVMChainTransactionPriority priority) async {
155 + try {
156 + await _checkEthBalanceForGasFees(priority);
157 + final withdrawAmount = await savingsBalanceV1;
158 +
159 + // Withdraw at least a million to overflow and close the savings position
160 + final amount = BigInt.parse("1000000000000000000000000");
161 + final savings = _getSavingsGateway(_wallet.getWeb3Client()!);
162 + final signedTransaction = await savings.withdraw(
163 (target: _address, amount: amount, frontendCode: hexToBytes(frontendCode)),
164 credentials: _wallet.evmChainPrivateKey,
165 );
166
167 final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
119 - amount: amount,
120 - contractAddress: _savingsGateway.self.address.hexEip55,
121 - receivingAddressHex: _savingsGateway.self.address.hexEip55,
168 + amount: Money.zero(_wallet.currency),
169 + contractAddress: savings.self.address.hexEip55,
170 + receivingAddressHex: savings.self.address.hexEip55,
171 priority: priority,
123 - data: _savingsGateway.self.abi.functions[24].encodeCall([_address, amount, hexToBytes(frontendCode)]),
172 + data: savings.self.abi.functions[24].encodeCall([_address, amount, hexToBytes(frontendCode)]),
173 );
174
126 - final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
175 + sendTransaction() => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
176
177 return PendingEVMChainTransaction(
129 - sendTransaction: sendTransaction,
130 - signedTransaction: signedTransaction,
131 - fee: BigInt.from(fee.estimatedGasFee),
132 - amount: amount.toString(),
133 - feeCurrency: "ETH",
134 - exponent: 18);
178 + sendTransaction: sendTransaction,
179 + signedTransaction: signedTransaction,
180 + fee: Money(BigInt.from(fee.estimatedGasFee), CryptoCurrency.eth),
181 + amount: withdrawAmount,
182 + );
183 } catch (e) {
184 if (e.toString().contains('insufficient funds for gas')) {
185 final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address);
@@ -145,27 +193,25 @@ class DEuro {
193 try {
194 await _checkEthBalanceForGasFees(priority);
195
148 - final signedTransaction = await _savingsGateway.refreshBalance((owner: _address),
196 + final signedTransaction = await _savings.refreshBalance((owner: _address),
197 credentials: _wallet.evmChainPrivateKey,
198 );
199
200 final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
153 - amount: BigInt.zero,
154 - contractAddress: _savingsGateway.self.address.hexEip55,
155 - receivingAddressHex: _savingsGateway.self.address.hexEip55,
201 + amount: Money.zero(_wallet.currency),
202 + contractAddress: _savings.self.address.hexEip55,
203 + receivingAddressHex: _savings.self.address.hexEip55,
204 priority: priority,
157 - data: _savingsGateway.self.abi.functions[15].encodeCall([_address]),
205 + data: _savings.self.abi.functions[16].encodeCall([_address]),
206 );
207
160 - final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
208 + sendTransaction() => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
209
210 return PendingEVMChainTransaction(
163 - feeCurrency: "ETH",
211 sendTransaction: sendTransaction,
212 signedTransaction: signedTransaction,
166 - fee: BigInt.from(fee.estimatedGasFee),
167 - amount: 0.toString(),
168 - exponent: 18,
213 + fee: Money(BigInt.from(fee.estimatedGasFee), CryptoCurrency.eth),
214 + amount: Money.zero(CryptoCurrency.deuro),
215 );
216 } catch (e) {
217 if (e.toString().contains('insufficient funds for gas')) {
@@ -182,14 +228,12 @@ class DEuro {
228 await _checkEthBalanceForGasFees(priority);
229
230 return (await _wallet.createApprovalTransaction(
185 - BigInt.parse(
231 + Money(BigInt.parse(
232 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
233 radix: 16,
188 - ),
189 - _savingsGateway.self.address.hexEip55,
190 - CryptoCurrency.deuro,
234 + ), CryptoCurrency.deuro),
235 + _savings.self.address.hexEip55,
236 priority,
192 - "ETH",
237 )) as PendingEVMChainTransaction;
238 } catch (e) {
239 if (e.toString().contains('insufficient funds for gas')) {
cw_evm/lib/deuro/deuro_savings_contract.dart
+216 -110
@@ -1,51 +1,38 @@
1 +// @dart=3.0
2 +// coverage:ignore-file
3 +// GENERATED CODE - DO NOT MODIFY BY HAND
4 // ignore_for_file: type=lint
5 // ignore_for_file: unused_local_variable, unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
6 // ignore_for_file: no_leading_underscores_for_library_prefixes
4 -import 'package:web3dart/web3dart.dart' as _i1;
7 import 'dart:typed_data' as _i2;
8
9 +import 'package:web3dart/web3dart.dart' as _i1;
10 +
11 final _contractAbi = _i1.ContractAbi.fromJson(
8 - '[{"inputs":[{"internalType":"contract IDecentralizedEURO","name":"deuro_","type":"address"},{"internalType":"uint24","name":"initialRatePPM","type":"uint24"},{"internalType":"address","name":"gateway_","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"ChangeNotReady","type":"error"},{"inputs":[],"name":"ModuleDisabled","type":"error"},{"inputs":[],"name":"NoPendingChange","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"interest","type":"uint256"}],"name":"InterestCollected","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint24","name":"newRate","type":"uint24"}],"name":"RateChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"who","type":"address"},{"indexed":false,"internalType":"uint24","name":"nextRate","type":"uint24"},{"indexed":false,"internalType":"uint40","name":"nextChange","type":"uint40"}],"name":"RateProposed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint192","name":"amount","type":"uint192"}],"name":"Saved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint192","name":"amount","type":"uint192"}],"name":"Withdrawn","type":"event"},{"inputs":[],"name":"GATEWAY","outputs":[{"internalType":"contract IFrontendGateway","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"accountOwner","type":"address"}],"name":"accruedInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"accountOwner","type":"address"},{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"accruedInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint192","name":"targetAmount","type":"uint192"},{"internalType":"bytes32","name":"frontendCode","type":"bytes32"}],"name":"adjust","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint192","name":"targetAmount","type":"uint192"}],"name":"adjust","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"applyChange","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"uint192","name":"saved","type":"uint192"},{"internalType":"uint64","name":"ticks","type":"uint64"}],"internalType":"struct Savings.Account","name":"account","type":"tuple"},{"internalType":"uint64","name":"ticks","type":"uint64"}],"name":"calculateInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"currentRatePPM","outputs":[{"internalType":"uint24","name":"","type":"uint24"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"currentTicks","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"deuro","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"equity","outputs":[{"internalType":"contract IReserve","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nextChange","outputs":[{"internalType":"uint40","name":"","type":"uint40"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nextRatePPM","outputs":[{"internalType":"uint24","name":"","type":"uint24"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint24","name":"newRatePPM_","type":"uint24"},{"internalType":"address[]","name":"helpers","type":"address[]"}],"name":"proposeChange","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"refreshBalance","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"refreshMyBalance","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint192","name":"amount","type":"uint192"},{"internalType":"bytes32","name":"frontendCode","type":"bytes32"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"},{"internalType":"bytes32","name":"frontendCode","type":"bytes32"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"savings","outputs":[{"internalType":"uint192","name":"saved","type":"uint192"},{"internalType":"uint64","name":"ticks","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"ticks","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"withdraw","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"},{"internalType":"bytes32","name":"frontendCode","type":"bytes32"}],"name":"withdraw","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"}]',
9 - 'SavingsGateway',
12 + '[{"inputs":[{"internalType":"contract IDecentralizedEURO","name":"deuro_","type":"address"},{"internalType":"uint24","name":"initialRatePPM","type":"uint24"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"ChangeNotReady","type":"error"},{"inputs":[],"name":"ModuleDisabled","type":"error"},{"inputs":[],"name":"NoPendingChange","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint192","name":"amount","type":"uint192"}],"name":"InterestClaimed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"interest","type":"uint256"},{"indexed":false,"internalType":"bool","name":"compounded","type":"bool"}],"name":"InterestCollected","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint24","name":"newRate","type":"uint24"}],"name":"RateChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"who","type":"address"},{"indexed":false,"internalType":"uint24","name":"nextRate","type":"uint24"},{"indexed":false,"internalType":"uint40","name":"nextChange","type":"uint40"}],"name":"RateProposed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint192","name":"amount","type":"uint192"}],"name":"Saved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint192","name":"amount","type":"uint192"}],"name":"Withdrawn","type":"event"},{"inputs":[{"internalType":"address","name":"accountOwner","type":"address"}],"name":"accruedInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"accountOwner","type":"address"},{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"accruedInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint192","name":"targetAmount","type":"uint192"}],"name":"adjust","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"applyChange","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"uint192","name":"saved","type":"uint192"},{"internalType":"uint64","name":"ticks","type":"uint64"}],"internalType":"struct Savings.Account","name":"account","type":"tuple"},{"internalType":"uint64","name":"ticks","type":"uint64"}],"name":"calculateInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"}],"name":"claimInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"claimableInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"currentRatePPM","outputs":[{"internalType":"uint24","name":"","type":"uint24"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"currentTicks","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"deuro","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"equity","outputs":[{"internalType":"contract IReserve","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nextChange","outputs":[{"internalType":"uint40","name":"","type":"uint40"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nextRatePPM","outputs":[{"internalType":"uint24","name":"","type":"uint24"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"nonCompounding","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint24","name":"newRatePPM_","type":"uint24"},{"internalType":"address[]","name":"helpers","type":"address[]"}],"name":"proposeChange","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"refreshBalance","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"refreshMyBalance","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint192","name":"amount","type":"uint192"},{"internalType":"bool","name":"compound","type":"bool"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"savings","outputs":[{"internalType":"uint192","name":"saved","type":"uint192"},{"internalType":"uint64","name":"ticks","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"ticks","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"withdraw","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"}]',
13 + 'Savings',
14 );
15
12 -class SavingsGateway extends _i1.GeneratedContract {
13 - SavingsGateway({
16 +class Savings extends _i1.GeneratedContract {
17 + Savings({
18 required _i1.EthereumAddress address,
19 required _i1.Web3Client client,
20 int? chainId,
21 }) : super(
18 - _i1.DeployedContract(
19 - _contractAbi,
20 - address,
21 - ),
22 - client,
23 - chainId,
24 - );
22 + _i1.DeployedContract(
23 + _contractAbi,
24 + address,
25 + ),
26 + client,
27 + chainId,
28 + );
29
30 /// The optional [atBlock] parameter can be used to view historical data. When
31 /// set, the function will be evaluated in the specified block. By default, the
32 /// latest on-chain block will be used.
29 - Future<_i1.EthereumAddress> GATEWAY({_i1.BlockNum? atBlock}) async {
33 + Future<BigInt> accruedInterest(
34 + {required _i1.EthereumAddress accountOwner, _i1.BlockNum? atBlock}) async {
35 final function = self.abi.functions[1];
31 - assert(checkSignature(function, '338c5371'));
32 - final params = [];
33 - final response = await read(
34 - function,
35 - params,
36 - atBlock,
37 - );
38 - return (response[0] as _i1.EthereumAddress);
39 - }
40 -
41 - /// The optional [atBlock] parameter can be used to view historical data. When
42 - /// set, the function will be evaluated in the specified block. By default, the
43 - /// latest on-chain block will be used.
44 - Future<BigInt> accruedInterest({
45 - required _i1.EthereumAddress accountOwner,
46 - _i1.BlockNum? atBlock,
47 - }) async {
48 - final function = self.abi.functions[2];
36 assert(checkSignature(function, '77267ec3'));
37 final params = [accountOwner];
38 final response = await read(
@@ -60,10 +47,10 @@ class SavingsGateway extends _i1.GeneratedContract {
47 /// set, the function will be evaluated in the specified block. By default, the
48 /// latest on-chain block will be used.
49 Future<BigInt> accruedInterest$2(
63 - ({_i1.EthereumAddress accountOwner, BigInt timestamp}) args, {
64 - _i1.BlockNum? atBlock,
65 - }) async {
66 - final function = self.abi.functions[3];
50 + ({_i1.EthereumAddress accountOwner, BigInt timestamp}) args, {
51 + _i1.BlockNum? atBlock,
52 + }) async {
53 + final function = self.abi.functions[2];
54 assert(checkSignature(function, 'a696399d'));
55 final params = [
56 args.accountOwner,
@@ -81,16 +68,31 @@ class SavingsGateway extends _i1.GeneratedContract {
68 /// like the gas price, nonce and max gas. The `data` and `to` fields will be
69 /// set by the contract.
70 Future<String> adjust(
84 - ({BigInt targetAmount, _i2.Uint8List frontendCode}) args, {
85 - required _i1.Credentials credentials,
86 - _i1.Transaction? transaction,
87 - }) async {
71 + ({BigInt targetAmount}) args, {
72 + required _i1.Credentials credentials,
73 + _i1.Transaction? transaction,
74 + }) async {
75 + final function = self.abi.functions[3];
76 + assert(checkSignature(function, 'bdc65152'));
77 + final params = [args.targetAmount];
78 + return write(
79 + credentials,
80 + transaction,
81 + function,
82 + params,
83 + );
84 + }
85 +
86 + /// The optional [transaction] parameter can be used to override parameters
87 + /// like the gas price, nonce and max gas. The `data` and `to` fields will be
88 + /// set by the contract.
89 + Future<String> applyChange({
90 + required _i1.Credentials credentials,
91 + _i1.Transaction? transaction,
92 + }) async {
93 final function = self.abi.functions[4];
89 - assert(checkSignature(function, '753ef93c'));
90 - final params = [
91 - args.targetAmount,
92 - args.frontendCode,
93 - ];
94 + assert(checkSignature(function, '4e65037e'));
95 + final params = [];
96 return write(
97 credentials,
98 transaction,
@@ -103,10 +105,10 @@ class SavingsGateway extends _i1.GeneratedContract {
105 /// set, the function will be evaluated in the specified block. By default, the
106 /// latest on-chain block will be used.
107 Future<BigInt> calculateInterest(
106 - ({dynamic account, BigInt ticks}) args, {
107 - _i1.BlockNum? atBlock,
108 - }) async {
109 - final function = self.abi.functions[7];
108 + ({dynamic account, BigInt ticks}) args, {
109 + _i1.BlockNum? atBlock,
110 + }) async {
111 + final function = self.abi.functions[5];
112 assert(checkSignature(function, '7915ce20'));
113 final params = [
114 args.account,
@@ -120,6 +122,43 @@ class SavingsGateway extends _i1.GeneratedContract {
122 return (response[0] as BigInt);
123 }
124
125 + /// The optional [transaction] parameter can be used to override parameters
126 + /// like the gas price, nonce and max gas. The `data` and `to` fields will be
127 + /// set by the contract.
128 + Future<String> claimInterest(
129 + ({_i1.EthereumAddress target}) args, {
130 + required _i1.Credentials credentials,
131 + _i1.Transaction? transaction,
132 + }) async {
133 + final function = self.abi.functions[6];
134 + assert(checkSignature(function, '88d742c2'));
135 + final params = [args.target];
136 + return write(
137 + credentials,
138 + transaction,
139 + function,
140 + params,
141 + );
142 + }
143 +
144 + /// The optional [atBlock] parameter can be used to view historical data. When
145 + /// set, the function will be evaluated in the specified block. By default, the
146 + /// latest on-chain block will be used.
147 + Future<BigInt> claimableInterest(
148 + ({_i1.EthereumAddress $param7}) args, {
149 + _i1.BlockNum? atBlock,
150 + }) async {
151 + final function = self.abi.functions[7];
152 + assert(checkSignature(function, '4f24d20a'));
153 + final params = [args.$param7];
154 + final response = await read(
155 + function,
156 + params,
157 + atBlock,
158 + );
159 + return (response[0] as BigInt);
160 + }
161 +
162 /// The optional [atBlock] parameter can be used to view historical data. When
163 /// set, the function will be evaluated in the specified block. By default, the
164 /// latest on-chain block will be used.
@@ -210,18 +249,39 @@ class SavingsGateway extends _i1.GeneratedContract {
249 return (response[0] as BigInt);
250 }
251
252 + /// The optional [atBlock] parameter can be used to view historical data. When
253 + /// set, the function will be evaluated in the specified block. By default, the
254 + /// latest on-chain block will be used.
255 + Future<bool> nonCompounding(
256 + ({_i1.EthereumAddress $param8}) args, {
257 + _i1.BlockNum? atBlock,
258 + }) async {
259 + final function = self.abi.functions[14];
260 + assert(checkSignature(function, 'cdb95c3b'));
261 + final params = [args.$param8];
262 + final response = await read(
263 + function,
264 + params,
265 + atBlock,
266 + );
267 + return (response[0] as bool);
268 + }
269 +
270 /// The optional [transaction] parameter can be used to override parameters
271 /// like the gas price, nonce and max gas. The `data` and `to` fields will be
272 /// set by the contract.
216 - Future<_i2.Uint8List> refreshBalance(
217 - ({_i1.EthereumAddress owner}) args, {
218 - required _i1.Credentials credentials,
219 - _i1.Transaction? transaction,
220 - }) async {
273 + Future<String> proposeChange(
274 + ({BigInt newRatePPM_, List<_i1.EthereumAddress> helpers}) args, {
275 + required _i1.Credentials credentials,
276 + _i1.Transaction? transaction,
277 + }) async {
278 final function = self.abi.functions[15];
222 - assert(checkSignature(function, 'b77cd1c7'));
223 - final params = [args.owner];
224 - return writeRaw(
279 + assert(checkSignature(function, '1de252e4'));
280 + final params = [
281 + args.newRatePPM_,
282 + args.helpers,
283 + ];
284 + return write(
285 credentials,
286 transaction,
287 function,
@@ -232,13 +292,14 @@ class SavingsGateway extends _i1.GeneratedContract {
292 /// The optional [transaction] parameter can be used to override parameters
293 /// like the gas price, nonce and max gas. The `data` and `to` fields will be
294 /// set by the contract.
235 - Future<_i2.Uint8List> refreshMyBalance({
295 + Future<_i2.Uint8List> refreshBalance(
296 + ({_i1.EthereumAddress owner}) args, {
297 required _i1.Credentials credentials,
298 _i1.Transaction? transaction,
299 }) async {
300 final function = self.abi.functions[16];
240 - assert(checkSignature(function, '85bd12d1'));
241 - final params = [];
301 + assert(checkSignature(function, 'b77cd1c7'));
302 + final params = [args.owner];
303 return writeRaw(
304 credentials,
305 transaction,
@@ -250,17 +311,13 @@ class SavingsGateway extends _i1.GeneratedContract {
311 /// The optional [transaction] parameter can be used to override parameters
312 /// like the gas price, nonce and max gas. The `data` and `to` fields will be
313 /// set by the contract.
253 - Future<_i2.Uint8List> save(
254 - ({BigInt amount, _i2.Uint8List frontendCode}) args, {
255 - required _i1.Credentials credentials,
256 - _i1.Transaction? transaction,
257 - }) async {
314 + Future<_i2.Uint8List> refreshMyBalance({
315 + required _i1.Credentials credentials,
316 + _i1.Transaction? transaction,
317 + }) async {
318 final function = self.abi.functions[17];
259 - assert(checkSignature(function, '9e2363dc'));
260 - final params = [
261 - args.amount,
262 - args.frontendCode,
263 - ];
319 + assert(checkSignature(function, '85bd12d1'));
320 + final params = [];
321 return writeRaw(
322 credentials,
323 transaction,
@@ -272,21 +329,16 @@ class SavingsGateway extends _i1.GeneratedContract {
329 /// The optional [transaction] parameter can be used to override parameters
330 /// like the gas price, nonce and max gas. The `data` and `to` fields will be
331 /// set by the contract.
275 - Future<_i2.Uint8List> saveTo(
276 - ({
277 - _i1.EthereumAddress owner,
278 - BigInt amount,
279 - _i2.Uint8List frontendCode
280 - }) args, {
281 - required _i1.Credentials credentials,
282 - _i1.Transaction? transaction,
283 - }) async {
284 - final function = self.abi.functions[19];
285 - assert(checkSignature(function, 'cbcf9676'));
332 + Future<_i2.Uint8List> save(
333 + ({BigInt amount, bool compound}) args, {
334 + required _i1.Credentials credentials,
335 + _i1.Transaction? transaction,
336 + }) async {
337 + final function = self.abi.functions[18];
338 + assert(checkSignature(function, 'a776f78c'));
339 final params = [
287 - args.owner,
340 args.amount,
289 - args.frontendCode,
341 + args.compound,
342 ];
343 return writeRaw(
344 credentials,
@@ -299,7 +351,7 @@ class SavingsGateway extends _i1.GeneratedContract {
351 /// The optional [atBlock] parameter can be used to view historical data. When
352 /// set, the function will be evaluated in the specified block. By default, the
353 /// latest on-chain block will be used.
302 - Future<Savings> savings({
354 + Future<SavingsData> savings({
355 required _i1.EthereumAddress accountOwner,
356 _i1.BlockNum? atBlock,
357 }) async {
@@ -311,27 +363,40 @@ class SavingsGateway extends _i1.GeneratedContract {
363 params,
364 atBlock,
365 );
314 - return Savings(response);
366 + return SavingsData(response);
367 + }
368 +
369 + /// The optional [atBlock] parameter can be used to view historical data. When
370 + /// set, the function will be evaluated in the specified block. By default, the
371 + /// latest on-chain block will be used.
372 + Future<BigInt> ticks(
373 + ({BigInt timestamp}) args, {
374 + _i1.BlockNum? atBlock,
375 + }) async {
376 + final function = self.abi.functions[22];
377 + assert(checkSignature(function, '534cb30d'));
378 + final params = [args.timestamp];
379 + final response = await read(
380 + function,
381 + params,
382 + atBlock,
383 + );
384 + return (response[0] as BigInt);
385 }
386
387 /// The optional [transaction] parameter can be used to override parameters
388 /// like the gas price, nonce and max gas. The `data` and `to` fields will be
389 /// set by the contract.
390 Future<_i2.Uint8List> withdraw(
321 - ({
322 - _i1.EthereumAddress target,
323 - BigInt amount,
324 - _i2.Uint8List frontendCode
325 - }) args, {
391 + ({_i1.EthereumAddress target, BigInt amount}) args, {
392 required _i1.Credentials credentials,
393 _i1.Transaction? transaction,
394 }) async {
329 - final function = self.abi.functions[24];
330 - assert(checkSignature(function, '829a0476'));
395 + final function = self.abi.functions[23];
396 + assert(checkSignature(function, '1791dc5e'));
397 final params = [
398 args.target,
399 args.amount,
334 - args.frontendCode,
400 ];
401 return writeRaw(
402 credentials,
@@ -341,6 +406,30 @@ class SavingsGateway extends _i1.GeneratedContract {
406 );
407 }
408
409 + /// Returns a live stream of all InterestClaimed events emitted by this contract.
410 + Stream<InterestClaimed> interestClaimedEvents({
411 + _i1.BlockNum? fromBlock,
412 + _i1.BlockNum? toBlock,
413 + }) {
414 + final event = self.event('InterestClaimed');
415 + final filter = _i1.FilterOptions.events(
416 + contract: self,
417 + event: event,
418 + fromBlock: fromBlock,
419 + toBlock: toBlock,
420 + );
421 + return client.events(filter).map((_i1.FilterEvent result) {
422 + final decoded = event.decodeResults(
423 + result.topics!,
424 + result.data!,
425 + );
426 + return InterestClaimed(
427 + decoded,
428 + result,
429 + );
430 + });
431 + }
432 +
433 /// Returns a live stream of all InterestCollected events emitted by this contract.
434 Stream<InterestCollected> interestCollectedEvents({
435 _i1.BlockNum? fromBlock,
@@ -462,8 +551,8 @@ class SavingsGateway extends _i1.GeneratedContract {
551 }
552 }
553
465 -class Savings {
466 - Savings(List<dynamic> response)
554 +class SavingsData {
555 + SavingsData(List<dynamic> response)
556 : saved = (response[0] as BigInt),
557 ticks = (response[1] as BigInt);
558
@@ -472,25 +561,42 @@ class Savings {
561 final BigInt ticks;
562 }
563
564 +class InterestClaimed {
565 + InterestClaimed(
566 + List<dynamic> response,
567 + this.event,
568 + ) : account = (response[0] as _i1.EthereumAddress),
569 + amount = (response[1] as BigInt);
570 +
571 + final _i1.EthereumAddress account;
572 +
573 + final BigInt amount;
574 +
575 + final _i1.FilterEvent event;
576 +}
577 +
578 class InterestCollected {
579 InterestCollected(
477 - List<dynamic> response,
478 - this.event,
479 - ) : account = (response[0] as _i1.EthereumAddress),
480 - interest = (response[1] as BigInt);
580 + List<dynamic> response,
581 + this.event,
582 + ) : account = (response[0] as _i1.EthereumAddress),
583 + interest = (response[1] as BigInt),
584 + compounded = (response[2] as bool);
585
586 final _i1.EthereumAddress account;
587
588 final BigInt interest;
589
590 + final bool compounded;
591 +
592 final _i1.FilterEvent event;
593 }
594
595 class RateChanged {
596 RateChanged(
491 - List<dynamic> response,
492 - this.event,
493 - ) : newRate = (response[0] as BigInt);
597 + List<dynamic> response,
598 + this.event,
599 + ) : newRate = (response[0] as BigInt);
600
601 final BigInt newRate;
602
@@ -499,9 +605,9 @@ class RateChanged {
605
606 class RateProposed {
607 RateProposed(
502 - List<dynamic> response,
503 - this.event,
504 - ) : who = (response[0] as _i1.EthereumAddress),
608 + List<dynamic> response,
609 + this.event,
610 + ) : who = (response[0] as _i1.EthereumAddress),
611 nextRate = (response[1] as BigInt),
612 nextChange = (response[2] as BigInt);
613
@@ -516,9 +622,9 @@ class RateProposed {
622
623 class Saved {
624 Saved(
519 - List<dynamic> response,
520 - this.event,
521 - ) : account = (response[0] as _i1.EthereumAddress),
625 + List<dynamic> response,
626 + this.event,
627 + ) : account = (response[0] as _i1.EthereumAddress),
628 amount = (response[1] as BigInt);
629
630 final _i1.EthereumAddress account;
@@ -530,9 +636,9 @@ class Saved {
636
637 class Withdrawn {
638 Withdrawn(
533 - List<dynamic> response,
534 - this.event,
535 - ) : account = (response[0] as _i1.EthereumAddress),
639 + List<dynamic> response,
640 + this.event,
641 + ) : account = (response[0] as _i1.EthereumAddress),
642 amount = (response[1] as BigInt);
643
644 final _i1.EthereumAddress account;
cw_evm/lib/deuro/deuro_savings_gateway_contract.dart new
+543
@@ -0,0 +1,543 @@
1 +// ignore_for_file: type=lint
2 +// ignore_for_file: unused_local_variable, unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
3 +// ignore_for_file: no_leading_underscores_for_library_prefixes
4 +import 'package:web3dart/web3dart.dart' as _i1;
5 +import 'dart:typed_data' as _i2;
6 +
7 +final _contractAbi = _i1.ContractAbi.fromJson(
8 + '[{"inputs":[{"internalType":"contract IDecentralizedEURO","name":"deuro_","type":"address"},{"internalType":"uint24","name":"initialRatePPM","type":"uint24"},{"internalType":"address","name":"gateway_","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"ChangeNotReady","type":"error"},{"inputs":[],"name":"ModuleDisabled","type":"error"},{"inputs":[],"name":"NoPendingChange","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"interest","type":"uint256"}],"name":"InterestCollected","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint24","name":"newRate","type":"uint24"}],"name":"RateChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"who","type":"address"},{"indexed":false,"internalType":"uint24","name":"nextRate","type":"uint24"},{"indexed":false,"internalType":"uint40","name":"nextChange","type":"uint40"}],"name":"RateProposed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint192","name":"amount","type":"uint192"}],"name":"Saved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint192","name":"amount","type":"uint192"}],"name":"Withdrawn","type":"event"},{"inputs":[],"name":"GATEWAY","outputs":[{"internalType":"contract IFrontendGateway","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"accountOwner","type":"address"}],"name":"accruedInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"accountOwner","type":"address"},{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"accruedInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint192","name":"targetAmount","type":"uint192"},{"internalType":"bytes32","name":"frontendCode","type":"bytes32"}],"name":"adjust","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint192","name":"targetAmount","type":"uint192"}],"name":"adjust","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"applyChange","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"uint192","name":"saved","type":"uint192"},{"internalType":"uint64","name":"ticks","type":"uint64"}],"internalType":"struct Savings.Account","name":"account","type":"tuple"},{"internalType":"uint64","name":"ticks","type":"uint64"}],"name":"calculateInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"currentRatePPM","outputs":[{"internalType":"uint24","name":"","type":"uint24"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"currentTicks","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"deuro","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"equity","outputs":[{"internalType":"contract IReserve","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nextChange","outputs":[{"internalType":"uint40","name":"","type":"uint40"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nextRatePPM","outputs":[{"internalType":"uint24","name":"","type":"uint24"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint24","name":"newRatePPM_","type":"uint24"},{"internalType":"address[]","name":"helpers","type":"address[]"}],"name":"proposeChange","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"refreshBalance","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"refreshMyBalance","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint192","name":"amount","type":"uint192"},{"internalType":"bytes32","name":"frontendCode","type":"bytes32"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"},{"internalType":"bytes32","name":"frontendCode","type":"bytes32"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"savings","outputs":[{"internalType":"uint192","name":"saved","type":"uint192"},{"internalType":"uint64","name":"ticks","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"ticks","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"withdraw","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"},{"internalType":"bytes32","name":"frontendCode","type":"bytes32"}],"name":"withdraw","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"}]',
9 + 'SavingsGateway',
10 +);
11 +
12 +class SavingsGateway extends _i1.GeneratedContract {
13 + SavingsGateway({
14 + required _i1.EthereumAddress address,
15 + required _i1.Web3Client client,
16 + int? chainId,
17 + }) : super(
18 + _i1.DeployedContract(
19 + _contractAbi,
20 + address,
21 + ),
22 + client,
23 + chainId,
24 + );
25 +
26 + /// The optional [atBlock] parameter can be used to view historical data. When
27 + /// set, the function will be evaluated in the specified block. By default, the
28 + /// latest on-chain block will be used.
29 + Future<_i1.EthereumAddress> GATEWAY({_i1.BlockNum? atBlock}) async {
30 + final function = self.abi.functions[1];
31 + assert(checkSignature(function, '338c5371'));
32 + final params = [];
33 + final response = await read(
34 + function,
35 + params,
36 + atBlock,
37 + );
38 + return (response[0] as _i1.EthereumAddress);
39 + }
40 +
41 + /// The optional [atBlock] parameter can be used to view historical data. When
42 + /// set, the function will be evaluated in the specified block. By default, the
43 + /// latest on-chain block will be used.
44 + Future<BigInt> accruedInterest({
45 + required _i1.EthereumAddress accountOwner,
46 + _i1.BlockNum? atBlock,
47 + }) async {
48 + final function = self.abi.functions[2];
49 + assert(checkSignature(function, '77267ec3'));
50 + final params = [accountOwner];
51 + final response = await read(
52 + function,
53 + params,
54 + atBlock,
55 + );
56 + return (response[0] as BigInt);
57 + }
58 +
59 + /// The optional [atBlock] parameter can be used to view historical data. When
60 + /// set, the function will be evaluated in the specified block. By default, the
61 + /// latest on-chain block will be used.
62 + Future<BigInt> accruedInterest$2(
63 + ({_i1.EthereumAddress accountOwner, BigInt timestamp}) args, {
64 + _i1.BlockNum? atBlock,
65 + }) async {
66 + final function = self.abi.functions[3];
67 + assert(checkSignature(function, 'a696399d'));
68 + final params = [
69 + args.accountOwner,
70 + args.timestamp,
71 + ];
72 + final response = await read(
73 + function,
74 + params,
75 + atBlock,
76 + );
77 + return (response[0] as BigInt);
78 + }
79 +
80 + /// The optional [transaction] parameter can be used to override parameters
81 + /// like the gas price, nonce and max gas. The `data` and `to` fields will be
82 + /// set by the contract.
83 + Future<String> adjust(
84 + ({BigInt targetAmount, _i2.Uint8List frontendCode}) args, {
85 + required _i1.Credentials credentials,
86 + _i1.Transaction? transaction,
87 + }) async {
88 + final function = self.abi.functions[4];
89 + assert(checkSignature(function, '753ef93c'));
90 + final params = [
91 + args.targetAmount,
92 + args.frontendCode,
93 + ];
94 + return write(
95 + credentials,
96 + transaction,
97 + function,
98 + params,
99 + );
100 + }
101 +
102 + /// The optional [atBlock] parameter can be used to view historical data. When
103 + /// set, the function will be evaluated in the specified block. By default, the
104 + /// latest on-chain block will be used.
105 + Future<BigInt> calculateInterest(
106 + ({dynamic account, BigInt ticks}) args, {
107 + _i1.BlockNum? atBlock,
108 + }) async {
109 + final function = self.abi.functions[7];
110 + assert(checkSignature(function, '7915ce20'));
111 + final params = [
112 + args.account,
113 + args.ticks,
114 + ];
115 + final response = await read(
116 + function,
117 + params,
118 + atBlock,
119 + );
120 + return (response[0] as BigInt);
121 + }
122 +
123 + /// The optional [atBlock] parameter can be used to view historical data. When
124 + /// set, the function will be evaluated in the specified block. By default, the
125 + /// latest on-chain block will be used.
126 + Future<BigInt> currentRatePPM({_i1.BlockNum? atBlock}) async {
127 + final function = self.abi.functions[8];
128 + assert(checkSignature(function, '06a7b376'));
129 + final params = [];
130 + final response = await read(
131 + function,
132 + params,
133 + atBlock,
134 + );
135 + return (response[0] as BigInt);
136 + }
137 +
138 + /// The optional [atBlock] parameter can be used to view historical data. When
139 + /// set, the function will be evaluated in the specified block. By default, the
140 + /// latest on-chain block will be used.
141 + Future<BigInt> currentTicks({_i1.BlockNum? atBlock}) async {
142 + final function = self.abi.functions[9];
143 + assert(checkSignature(function, 'b079f163'));
144 + final params = [];
145 + final response = await read(
146 + function,
147 + params,
148 + atBlock,
149 + );
150 + return (response[0] as BigInt);
151 + }
152 +
153 + /// The optional [atBlock] parameter can be used to view historical data. When
154 + /// set, the function will be evaluated in the specified block. By default, the
155 + /// latest on-chain block will be used.
156 + Future<_i1.EthereumAddress> deuro({_i1.BlockNum? atBlock}) async {
157 + final function = self.abi.functions[10];
158 + assert(checkSignature(function, '82b8eaf5'));
159 + final params = [];
160 + final response = await read(
161 + function,
162 + params,
163 + atBlock,
164 + );
165 + return (response[0] as _i1.EthereumAddress);
166 + }
167 +
168 + /// The optional [atBlock] parameter can be used to view historical data. When
169 + /// set, the function will be evaluated in the specified block. By default, the
170 + /// latest on-chain block will be used.
171 + Future<_i1.EthereumAddress> equity({_i1.BlockNum? atBlock}) async {
172 + final function = self.abi.functions[11];
173 + assert(checkSignature(function, '91a0ac6a'));
174 + final params = [];
175 + final response = await read(
176 + function,
177 + params,
178 + atBlock,
179 + );
180 + return (response[0] as _i1.EthereumAddress);
181 + }
182 +
183 + /// The optional [atBlock] parameter can be used to view historical data. When
184 + /// set, the function will be evaluated in the specified block. By default, the
185 + /// latest on-chain block will be used.
186 + Future<BigInt> nextChange({_i1.BlockNum? atBlock}) async {
187 + final function = self.abi.functions[12];
188 + assert(checkSignature(function, 'b6f83c17'));
189 + final params = [];
190 + final response = await read(
191 + function,
192 + params,
193 + atBlock,
194 + );
195 + return (response[0] as BigInt);
196 + }
197 +
198 + /// The optional [atBlock] parameter can be used to view historical data. When
199 + /// set, the function will be evaluated in the specified block. By default, the
200 + /// latest on-chain block will be used.
201 + Future<BigInt> nextRatePPM({_i1.BlockNum? atBlock}) async {
202 + final function = self.abi.functions[13];
203 + assert(checkSignature(function, '2e4b20ab'));
204 + final params = [];
205 + final response = await read(
206 + function,
207 + params,
208 + atBlock,
209 + );
210 + return (response[0] as BigInt);
211 + }
212 +
213 + /// The optional [transaction] parameter can be used to override parameters
214 + /// like the gas price, nonce and max gas. The `data` and `to` fields will be
215 + /// set by the contract.
216 + Future<_i2.Uint8List> refreshBalance(
217 + ({_i1.EthereumAddress owner}) args, {
218 + required _i1.Credentials credentials,
219 + _i1.Transaction? transaction,
220 + }) async {
221 + final function = self.abi.functions[15];
222 + assert(checkSignature(function, 'b77cd1c7'));
223 + final params = [args.owner];
224 + return writeRaw(
225 + credentials,
226 + transaction,
227 + function,
228 + params,
229 + );
230 + }
231 +
232 + /// The optional [transaction] parameter can be used to override parameters
233 + /// like the gas price, nonce and max gas. The `data` and `to` fields will be
234 + /// set by the contract.
235 + Future<_i2.Uint8List> refreshMyBalance({
236 + required _i1.Credentials credentials,
237 + _i1.Transaction? transaction,
238 + }) async {
239 + final function = self.abi.functions[16];
240 + assert(checkSignature(function, '85bd12d1'));
241 + final params = [];
242 + return writeRaw(
243 + credentials,
244 + transaction,
245 + function,
246 + params,
247 + );
248 + }
249 +
250 + /// The optional [transaction] parameter can be used to override parameters
251 + /// like the gas price, nonce and max gas. The `data` and `to` fields will be
252 + /// set by the contract.
253 + Future<_i2.Uint8List> save(
254 + ({BigInt amount, _i2.Uint8List frontendCode}) args, {
255 + required _i1.Credentials credentials,
256 + _i1.Transaction? transaction,
257 + }) async {
258 + final function = self.abi.functions[17];
259 + assert(checkSignature(function, '9e2363dc'));
260 + final params = [
261 + args.amount,
262 + args.frontendCode,
263 + ];
264 + return writeRaw(
265 + credentials,
266 + transaction,
267 + function,
268 + params,
269 + );
270 + }
271 +
272 + /// The optional [transaction] parameter can be used to override parameters
273 + /// like the gas price, nonce and max gas. The `data` and `to` fields will be
274 + /// set by the contract.
275 + Future<_i2.Uint8List> saveTo(
276 + ({
277 + _i1.EthereumAddress owner,
278 + BigInt amount,
279 + _i2.Uint8List frontendCode
280 + }) args, {
281 + required _i1.Credentials credentials,
282 + _i1.Transaction? transaction,
283 + }) async {
284 + final function = self.abi.functions[19];
285 + assert(checkSignature(function, 'cbcf9676'));
286 + final params = [
287 + args.owner,
288 + args.amount,
289 + args.frontendCode,
290 + ];
291 + return writeRaw(
292 + credentials,
293 + transaction,
294 + function,
295 + params,
296 + );
297 + }
298 +
299 + /// The optional [atBlock] parameter can be used to view historical data. When
300 + /// set, the function will be evaluated in the specified block. By default, the
301 + /// latest on-chain block will be used.
302 + Future<Savings> savings({
303 + required _i1.EthereumAddress accountOwner,
304 + _i1.BlockNum? atBlock,
305 + }) async {
306 + final function = self.abi.functions[21];
307 + assert(checkSignature(function, '1f7cdd5f'));
308 + final params = [accountOwner];
309 + final response = await read(
310 + function,
311 + params,
312 + atBlock,
313 + );
314 + return Savings(response);
315 + }
316 +
317 + /// The optional [transaction] parameter can be used to override parameters
318 + /// like the gas price, nonce and max gas. The `data` and `to` fields will be
319 + /// set by the contract.
320 + Future<_i2.Uint8List> withdraw(
321 + ({
322 + _i1.EthereumAddress target,
323 + BigInt amount,
324 + _i2.Uint8List frontendCode
325 + }) args, {
326 + required _i1.Credentials credentials,
327 + _i1.Transaction? transaction,
328 + }) async {
329 + final function = self.abi.functions[24];
330 + assert(checkSignature(function, '829a0476'));
331 + final params = [
332 + args.target,
333 + args.amount,
334 + args.frontendCode,
335 + ];
336 + return writeRaw(
337 + credentials,
338 + transaction,
339 + function,
340 + params,
341 + );
342 + }
343 +
344 + /// Returns a live stream of all InterestCollected events emitted by this contract.
345 + Stream<InterestCollected> interestCollectedEvents({
346 + _i1.BlockNum? fromBlock,
347 + _i1.BlockNum? toBlock,
348 + }) {
349 + final event = self.event('InterestCollected');
350 + final filter = _i1.FilterOptions.events(
351 + contract: self,
352 + event: event,
353 + fromBlock: fromBlock,
354 + toBlock: toBlock,
355 + );
356 + return client.events(filter).map((_i1.FilterEvent result) {
357 + final decoded = event.decodeResults(
358 + result.topics!,
359 + result.data!,
360 + );
361 + return InterestCollected(
362 + decoded,
363 + result,
364 + );
365 + });
366 + }
367 +
368 + /// Returns a live stream of all RateChanged events emitted by this contract.
369 + Stream<RateChanged> rateChangedEvents({
370 + _i1.BlockNum? fromBlock,
371 + _i1.BlockNum? toBlock,
372 + }) {
373 + final event = self.event('RateChanged');
374 + final filter = _i1.FilterOptions.events(
375 + contract: self,
376 + event: event,
377 + fromBlock: fromBlock,
378 + toBlock: toBlock,
379 + );
380 + return client.events(filter).map((_i1.FilterEvent result) {
381 + final decoded = event.decodeResults(
382 + result.topics!,
383 + result.data!,
384 + );
385 + return RateChanged(
386 + decoded,
387 + result,
388 + );
389 + });
390 + }
391 +
392 + /// Returns a live stream of all RateProposed events emitted by this contract.
393 + Stream<RateProposed> rateProposedEvents({
394 + _i1.BlockNum? fromBlock,
395 + _i1.BlockNum? toBlock,
396 + }) {
397 + final event = self.event('RateProposed');
398 + final filter = _i1.FilterOptions.events(
399 + contract: self,
400 + event: event,
401 + fromBlock: fromBlock,
402 + toBlock: toBlock,
403 + );
404 + return client.events(filter).map((_i1.FilterEvent result) {
405 + final decoded = event.decodeResults(
406 + result.topics!,
407 + result.data!,
408 + );
409 + return RateProposed(
410 + decoded,
411 + result,
412 + );
413 + });
414 + }
415 +
416 + /// Returns a live stream of all Saved events emitted by this contract.
417 + Stream<Saved> savedEvents({
418 + _i1.BlockNum? fromBlock,
419 + _i1.BlockNum? toBlock,
420 + }) {
421 + final event = self.event('Saved');
422 + final filter = _i1.FilterOptions.events(
423 + contract: self,
424 + event: event,
425 + fromBlock: fromBlock,
426 + toBlock: toBlock,
427 + );
428 + return client.events(filter).map((_i1.FilterEvent result) {
429 + final decoded = event.decodeResults(
430 + result.topics!,
431 + result.data!,
432 + );
433 + return Saved(
434 + decoded,
435 + result,
436 + );
437 + });
438 + }
439 +
440 + /// Returns a live stream of all Withdrawn events emitted by this contract.
441 + Stream<Withdrawn> withdrawnEvents({
442 + _i1.BlockNum? fromBlock,
443 + _i1.BlockNum? toBlock,
444 + }) {
445 + final event = self.event('Withdrawn');
446 + final filter = _i1.FilterOptions.events(
447 + contract: self,
448 + event: event,
449 + fromBlock: fromBlock,
450 + toBlock: toBlock,
451 + );
452 + return client.events(filter).map((_i1.FilterEvent result) {
453 + final decoded = event.decodeResults(
454 + result.topics!,
455 + result.data!,
456 + );
457 + return Withdrawn(
458 + decoded,
459 + result,
460 + );
461 + });
462 + }
463 +}
464 +
465 +class Savings {
466 + Savings(List<dynamic> response)
467 + : saved = (response[0] as BigInt),
468 + ticks = (response[1] as BigInt);
469 +
470 + final BigInt saved;
471 +
472 + final BigInt ticks;
473 +}
474 +
475 +class InterestCollected {
476 + InterestCollected(
477 + List<dynamic> response,
478 + this.event,
479 + ) : account = (response[0] as _i1.EthereumAddress),
480 + interest = (response[1] as BigInt);
481 +
482 + final _i1.EthereumAddress account;
483 +
484 + final BigInt interest;
485 +
486 + final _i1.FilterEvent event;
487 +}
488 +
489 +class RateChanged {
490 + RateChanged(
491 + List<dynamic> response,
492 + this.event,
493 + ) : newRate = (response[0] as BigInt);
494 +
495 + final BigInt newRate;
496 +
497 + final _i1.FilterEvent event;
498 +}
499 +
500 +class RateProposed {
501 + RateProposed(
502 + List<dynamic> response,
503 + this.event,
504 + ) : who = (response[0] as _i1.EthereumAddress),
505 + nextRate = (response[1] as BigInt),
506 + nextChange = (response[2] as BigInt);
507 +
508 + final _i1.EthereumAddress who;
509 +
510 + final BigInt nextRate;
511 +
512 + final BigInt nextChange;
513 +
514 + final _i1.FilterEvent event;
515 +}
516 +
517 +class Saved {
518 + Saved(
519 + List<dynamic> response,
520 + this.event,
521 + ) : account = (response[0] as _i1.EthereumAddress),
522 + amount = (response[1] as BigInt);
523 +
524 + final _i1.EthereumAddress account;
525 +
526 + final BigInt amount;
527 +
528 + final _i1.FilterEvent event;
529 +}
530 +
531 +class Withdrawn {
532 + Withdrawn(
533 + List<dynamic> response,
534 + this.event,
535 + ) : account = (response[0] as _i1.EthereumAddress),
536 + amount = (response[1] as BigInt);
537 +
538 + final _i1.EthereumAddress account;
539 +
540 + final BigInt amount;
541 +
542 + final _i1.FilterEvent event;
543 +}
cw_evm/lib/evm_chain_transaction_info.dart
+23 -32
@@ -1,18 +1,19 @@
1 // ignore_for_file: overridden_fields, annotate_overrides
2 -import 'dart:math';
3 -
2 +import 'package:cw_core/amount/money.dart';
3 +import 'package:cw_core/crypto_currency.dart';
4 +import 'package:cw_core/erc20_token.dart';
5 import 'package:cw_core/format_amount.dart';
5 -import 'package:cw_core/format_fixed.dart';
6 import 'package:cw_core/transaction_direction.dart';
7 import 'package:cw_core/transaction_info.dart';
8 +import 'package:cw_evm/evm_chain_registry.dart';
9 import 'package:cw_evm/utils/evm_chain_utils.dart';
10
11 class EVMChainTransactionInfo extends TransactionInfo {
12 EVMChainTransactionInfo({
13 required this.id,
14 required this.height,
14 - required this.ethAmount,
15 - required this.ethFee,
15 + required this.amount,
16 + required this.fee,
17 required this.tokenSymbol,
18 this.exponent = 18,
19 required this.direction,
@@ -24,19 +25,16 @@ class EVMChainTransactionInfo extends TransactionInfo {
25 this.evmSignatureName,
26 this.contractAddress,
27 required this.chainId,
27 - }) : amount = ethAmount.toInt(),
28 - fee = ethFee.toInt();
28 + });
29
30 final String id;
31 final int height;
32 - final int amount;
33 - final BigInt ethAmount;
32 + final Money amount;
33 final int exponent;
34 final TransactionDirection direction;
35 final DateTime date;
36 final bool isPending;
38 - final int fee;
39 - final BigInt ethFee;
37 + final Money fee;
38 final int confirmations;
39 final String tokenSymbol;
40 String? _fiatAmount;
@@ -49,39 +47,32 @@ class EVMChainTransactionInfo extends TransactionInfo {
47 /// Get fee currency symbol based on wallet type
48 String get feeCurrency => EVMChainUtils.getFeeCurrency(chainId);
49
52 - String get _displayTokenSymbol {
53 - if (chainId == 56 && tokenSymbol.toUpperCase() == 'BSC-USD') return 'USDT';
54 -
55 - return tokenSymbol;
56 - }
57 -
58 - @override
59 - String amountFormatted() {
60 - final amount = formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString());
61 - return '${amount.substring(0, min(10, amount.length))} $_displayTokenSymbol';
62 - }
63 -
50 @override
51 String fiatAmount() => _fiatAmount ?? '';
52
53 @override
54 void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
55
70 - @override
71 - String feeFormatted() => '${formatFixed(ethFee, 18)} $feeCurrency';
72 -
56 factory EVMChainTransactionInfo.fromJson(Map<String, dynamic> data, int chainId) {
57 + final decimals = data['exponent'] as int? ?? 18;
58 + final tokenSymbol = data['tokenSymbol'] as String;
59 + final currency =
60 + Erc20Token(name: '', symbol: tokenSymbol, contractAddress: '', decimal: decimals);
61 +
62 + final feeCurrency =
63 + EvmChainRegistry().getChainConfig(chainId)?.nativeCurrency ?? CryptoCurrency.eth;
64 +
65 return EVMChainTransactionInfo(
66 id: data['id'] as String,
67 height: data['height'] as int,
77 - ethAmount: BigInt.parse(data['amount'] as String),
78 - exponent: data['exponent'] as int? ?? 18,
79 - ethFee: BigInt.parse(data['fee'] as String),
68 + amount: Money(BigInt.parse(data['amount'] as String), currency),
69 + exponent: decimals,
70 + fee: Money(BigInt.parse(data['fee'] as String), feeCurrency),
71 direction: TransactionDirection.values[data['direction'] as int],
72 date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
73 isPending: data['isPending'] as bool? ?? false,
74 confirmations: data['confirmations'] as int,
84 - tokenSymbol: data['tokenSymbol'] as String,
75 + tokenSymbol: tokenSymbol,
76 to: data['to'] as String?,
77 from: data['from'] as String?,
78 evmSignatureName: data['evmSignatureName'] as String?,
@@ -93,9 +84,9 @@ class EVMChainTransactionInfo extends TransactionInfo {
84 Map<String, dynamic> toJson() => {
85 'id': id,
86 'height': height,
96 - 'amount': ethAmount.toString(),
87 + 'amount': amount.amount.toString(),
88 'exponent': exponent,
98 - 'fee': ethFee.toString(),
89 + 'fee': fee.amount.toString(),
90 'direction': direction.index,
91 'date': date.millisecondsSinceEpoch,
92 'isPending': isPending,
cw_evm/lib/evm_chain_wallet.dart
+74 -94
@@ -5,6 +5,7 @@ import 'dart:typed_data';
5
6 import 'package:bip32/bip32.dart' as bip32;
7 import 'package:bip39/bip39.dart' as bip39;
8 +import 'package:cw_core/amount/money.dart';
9 import 'package:cw_core/cake_hive.dart';
10 import 'package:cw_core/crypto_currency.dart';
11 import 'package:cw_core/encryption_file_utils.dart';
@@ -28,7 +29,6 @@ import 'package:cw_evm/evm_chain_default_tokens.dart';
29 import 'package:cw_evm/evm_chain_exceptions.dart';
30 import 'package:cw_evm/evm_chain_registry.dart';
31 import 'package:cw_evm/evm_chain_transaction_credentials.dart';
31 -import 'package:cw_evm/utils/evm_chain_formatter.dart';
32 import 'package:cw_evm/evm_chain_transaction_history.dart';
33 import 'package:cw_evm/evm_chain_transaction_model.dart';
34 import 'package:cw_evm/evm_chain_transaction_priority.dart';
@@ -103,7 +103,7 @@ abstract class EVMChainWalletBase
103 walletInfo, initialChainId ?? _getInitialChainId(walletInfo.type)),
104 balance = ObservableMap<CryptoCurrency, EVMChainERC20Balance>.of(
105 {
106 - nativeCurrency: initialBalance ?? EVMChainERC20Balance(BigInt.zero),
106 + nativeCurrency: initialBalance ?? EVMChainERC20Balance(Money.zero(nativeCurrency)),
107 },
108 ),
109 super(walletInfo, derivationInfo) {
@@ -411,20 +411,29 @@ abstract class EVMChainWalletBase
411 EVMChainTransactionModel transactionModel,
412 String address,
413 ) {
414 + final decimals = transactionModel.tokenDecimal ?? 18;
415 + final tokenSymbol = transactionModel.tokenSymbol ??
416 + EVMChainUtils.getDefaultTokenSymbol(transactionModel.chainId);
417 +
418 + final amountCurrency = Erc20Token(
419 + name: '',
420 + contractAddress: transactionModel.contractAddress,
421 + decimal: decimals,
422 + symbol: tokenSymbol,
423 + );
424 return EVMChainTransactionInfo(
425 id: transactionModel.hash,
426 height: transactionModel.blockNumber,
417 - ethAmount: transactionModel.amount,
427 + amount: Money(transactionModel.amount, amountCurrency),
428 direction: transactionModel.from == address
429 ? TransactionDirection.outgoing
430 : TransactionDirection.incoming,
431 isPending: false,
432 date: transactionModel.date,
433 confirmations: transactionModel.confirmations,
424 - ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice,
434 + fee: Money(BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice, currency),
435 exponent: transactionModel.tokenDecimal ?? 18,
426 - tokenSymbol: transactionModel.tokenSymbol ??
427 - EVMChainUtils.getDefaultTokenSymbol(transactionModel.chainId),
436 + tokenSymbol: tokenSymbol,
437 to: transactionModel.to,
438 from: transactionModel.from,
439 evmSignatureName: transactionModel.evmSignatureName,
@@ -473,11 +482,10 @@ abstract class EVMChainWalletBase
482 @override
483 Future<bool> checkNodeHealth() async {
484 try {
476 - // Check native balance
485 await _client.getBalance(_evmChainPrivateKey.address);
486
479 - // Check USDC token balance
480 - String usdcContractAddress = _getUSDCContractAddress();
487 + final usdcContractAddress = Erc20Token(
488 + name: "USDC", symbol: "USDC", contractAddress: _getUSDCContractAddress(), decimal: 6);
489
490 await _client.fetchERC20Balances(_evmChainPrivateKey.address, usdcContractAddress);
491
@@ -531,7 +539,7 @@ abstract class EVMChainWalletBase
539
540 // Ensure balance is initialized for current currency (in case currency changed)
541 if (!balance.containsKey(currency)) {
534 - balance[currency] = EVMChainERC20Balance(BigInt.zero);
542 + balance[currency] = EVMChainERC20Balance(Money.zero(currency));
543 }
544
545 await save();
@@ -757,7 +765,7 @@ abstract class EVMChainWalletBase
765 /// - The addressHex for the receiving wallet,
766 /// - A contract address which would be essential in determining if to calculate the estimate for ERC20 or native ETH
767 Future<GasParamsHandler> calculateActualEstimatedFeeForCreateTransaction({
760 - required amount,
768 + required Money amount,
769 required String? contractAddress,
770 required String receivingAddressHex,
771 required TransactionPriority? priority,
@@ -774,7 +782,6 @@ abstract class EVMChainWalletBase
782 final gasBaseFee = await _client.getGasBaseFee();
783 final gasPrice = await _client.getGasUnitPrice();
784
777 - // Validate that we got valid gas price
785 if (gasPrice <= 0) {
786 printV('Invalid gas price received: $gasPrice');
787 throw EVMChainTransactionFeesException('Failed to retrieve gas price from node');
@@ -791,7 +798,7 @@ abstract class EVMChainWalletBase
798 final estimatedGas = await _client.getEstimatedGasUnitsForTransaction(
799 contractAddress: contractAddress,
800 senderAddress: _evmChainPrivateKey.address,
794 - value: EtherAmount.fromBigInt(EtherUnit.wei, amount!),
801 + value: EtherAmount.fromBigInt(EtherUnit.wei, amount.amount),
802 gasPrice: EtherAmount.fromInt(EtherUnit.wei, adjustedGasPrice),
803 toAddress: EthereumAddress.fromHex(receivingAddressHex),
804 maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas),
@@ -932,15 +939,15 @@ abstract class EVMChainWalletBase
939 'Currency ${_credentials.currency.title} ${_credentials.currency.tag} is not accessible in the wallet, try to enable it first.'));
940
941 final currencyBalance = balance[transactionCurrency]!;
935 - BigInt totalAmount = BigInt.zero;
936 - BigInt estimatedFeesForTransaction = BigInt.zero;
937 - int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
938 - String? contractAddress;
939 - int estimatedGasUnitsForTransaction = 0;
940 - int maxFeePerGasForTransaction = 0;
941 - String toAddress = _credentials.outputs.first.isParsedAddress
942 + final toAddress = _credentials.outputs.first.isParsedAddress
943 ? _credentials.outputs.first.extractedAddress!
944 : _credentials.outputs.first.address;
945 + var totalAmount = Money.zero(transactionCurrency);
946 + var estimatedFeesForTransaction = Money.zero(currency);
947 + var estimatedGasUnitsForTransaction = 0;
948 + var maxFeePerGasForTransaction = 0;
949 +
950 + String? contractAddress;
951
952 if (transactionCurrency is Erc20Token) {
953 contractAddress = transactionCurrency.contractAddress;
@@ -948,23 +955,12 @@ abstract class EVMChainWalletBase
955
956 // so far this can not be made with Ethereum as Ethereum does not support multiple recipients
957 if (hasMultiDestination) {
951 - if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
958 + if (outputs.any((item) => item.sendAll || item.cryptoAmount.amount <= BigInt.zero)) {
959 throw EVMChainTransactionCreationException(transactionCurrency);
960 }
961
955 - totalAmount = outputs.fold<BigInt>(
956 - BigInt.zero,
957 - (acc, output) {
958 - if (output.cryptoAmount != null && output.cryptoAmount!.isNotEmpty) {
959 - return acc +
960 - EVMChainFormatter.parseEVMChainAmountToBigInt(
961 - output.cryptoAmount!,
962 - decimals: exponent,
963 - );
964 - }
965 - return acc;
966 - },
967 - );
962 + totalAmount = outputs.fold<Money>(Money.zero(transactionCurrency),
963 + (acc, output) => acc + output.cryptoAmount.copyWith(currency: transactionCurrency));
964
965 final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction(
966 amount: totalAmount,
@@ -973,28 +969,22 @@ abstract class EVMChainWalletBase
969 contractAddress: contractAddress,
970 );
971
976 - estimatedFeesForTransaction = BigInt.from(gasFeesModel.estimatedGasFee);
972 + estimatedFeesForTransaction =
973 + estimatedFeesForTransaction.copyWith(amount: BigInt.from(gasFeesModel.estimatedGasFee));
974 estimatedGasUnitsForTransaction = gasFeesModel.estimatedGasUnits;
975 maxFeePerGasForTransaction = gasFeesModel.maxFeePerGas;
976
980 - if (currencyBalance.balance < totalAmount) {
977 + if (currencyBalance.available < totalAmount) {
978 throw EVMChainTransactionCreationException(transactionCurrency);
979 }
980 } else {
981 final output = outputs.first;
982 if (!output.sendAll) {
986 - if (output.cryptoAmount != null && output.cryptoAmount!.isNotEmpty) {
987 - totalAmount = EVMChainFormatter.parseEVMChainAmountToBigInt(
988 - output.cryptoAmount!,
989 - decimals: exponent,
990 - );
991 - } else {
992 - totalAmount = BigInt.zero;
993 - }
983 + totalAmount = output.cryptoAmount.copyWith(currency: transactionCurrency);
984 }
985
986 if (output.sendAll && transactionCurrency is Erc20Token) {
997 - totalAmount = currencyBalance.balance;
987 + totalAmount = currencyBalance.available;
988 }
989
990 final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction(
@@ -1004,7 +994,8 @@ abstract class EVMChainWalletBase
994 contractAddress: contractAddress,
995 );
996
1007 - estimatedFeesForTransaction = BigInt.from(gasFeesModel.estimatedGasFee);
997 + estimatedFeesForTransaction =
998 + estimatedFeesForTransaction.copyWith(amount: BigInt.from(gasFeesModel.estimatedGasFee));
999 estimatedGasUnitsForTransaction = gasFeesModel.estimatedGasUnits;
1000 maxFeePerGasForTransaction = gasFeesModel.maxFeePerGas;
1001
@@ -1012,30 +1003,30 @@ abstract class EVMChainWalletBase
1003 if (selectedChainId == 8453) {
1004 // Applying a small buffer to account for gas price fluctuations
1005 // 10% or minimum 10,000 wei, whichever is higher
1015 - final refinedGasFee = estimatedFeesForTransaction;
1006 + final refinedGasFee = estimatedFeesForTransaction.amount;
1007 final gasBufferPercent = refinedGasFee * BigInt.from(110) ~/ BigInt.from(100);
1008 final gasBufferMin = refinedGasFee + BigInt.from(10000);
1009 final gasBuffer = gasBufferPercent > gasBufferMin ? gasBufferPercent : gasBufferMin;
1010
1011 // Using the buffered fee for the final amount
1021 - totalAmount = (currencyBalance.balance - gasBuffer);
1022 - estimatedFeesForTransaction = gasBuffer;
1012 + totalAmount = totalAmount.copyWith(amount: currencyBalance.available.amount - gasBuffer);
1013 + estimatedFeesForTransaction = estimatedFeesForTransaction.copyWith(amount: gasBuffer);
1014 } else {
1015 // Calculating the final amount with the estimated gas fee
1025 - totalAmount = (currencyBalance.balance - estimatedFeesForTransaction);
1016 + totalAmount = currencyBalance.available - estimatedFeesForTransaction;
1017 }
1018 }
1019
1020 // check the fees on the base currency
1030 - if (estimatedFeesForTransaction > balance[currency]!.balance) {
1021 + if (estimatedFeesForTransaction > balance[currency]!.available) {
1022 throw EVMChainTransactionFeesException.fromCurrency(currency.title);
1023 }
1024
1034 - if (currencyBalance.balance < totalAmount) {
1025 + if (currencyBalance.available < totalAmount) {
1026 throw EVMChainTransactionCreationException(transactionCurrency);
1027 }
1028 if (transactionCurrency is! Erc20Token &&
1038 - totalAmount + estimatedFeesForTransaction > currencyBalance.balance) {
1029 + totalAmount + estimatedFeesForTransaction > currencyBalance.available) {
1030 throw EVMChainTransactionFeesException.fromCurrency(currency.title);
1031 }
1032 }
@@ -1056,7 +1047,6 @@ abstract class EVMChainWalletBase
1047 currency: transactionCurrency,
1048 feeCurrency: EVMChainUtils.getFeeCurrency(selectedChainId),
1049 maxFeePerGas: maxFeePerGasForTransaction,
1059 - exponent: exponent,
1050 contractAddress:
1051 transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null,
1052 data: hexOpReturnMemo,
@@ -1070,7 +1060,7 @@ abstract class EVMChainWalletBase
1060 Future<PendingTransaction> createCallDataTransaction(
1061 String to,
1062 String dataHex,
1073 - BigInt valueWei,
1063 + Money valueWei,
1064 EVMChainTransactionPriority? priority,
1065 String? sourceTokenAddress,
1066 BigInt? sourceTokenAmount, {
@@ -1101,37 +1091,36 @@ abstract class EVMChainWalletBase
1091 gas = GasParamsHandler.zero();
1092 }
1093
1104 - // Validate NATIVE Balance (for Gas + Value)
1105 - final nativeBal = balance[nativeCurrency]?.balance ?? BigInt.zero;
1106 - final requiredNative = valueWei + BigInt.from(gas.estimatedGasFee);
1094 + final nativeBal = balance[nativeCurrency]?.available ?? Money.zero(nativeCurrency);
1095 + var requiredNative = Money.fromInt(gas.estimatedGasFee, nativeCurrency);
1096 +
1097 + if (valueWei.currency == nativeCurrency) {
1098 + requiredNative += valueWei;
1099 + }
1100
1101 if (requiredNative > nativeBal) {
1102 throw Exception('Not enough ${nativeCurrency.title} to cover value and fees.');
1103 }
1104
1112 - // Validate ERC20 Balance (Only if source is NOT native)
1105 final cleanAddress = sourceTokenAddress?.toLowerCase() ?? '';
1106
1115 - // Check for both 0xeeee... AND 0x0000... (Zero Address)
1116 - bool isNativeSource = sourceTokenAddress == null ||
1107 + final isNativeSource = sourceTokenAddress == null ||
1108 cleanAddress == '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' ||
1109 cleanAddress == '0x0000000000000000000000000000000000000000';
1110
1111 if (!isNativeSource && sourceTokenAmount != null && sourceTokenAmount > BigInt.zero) {
1121 - // Filter list to find match.
1112 final matchingTokens = balance.keys
1113 .where((k) => k is Erc20Token && k.contractAddress.toLowerCase() == cleanAddress);
1114
1115 if (matchingTokens.isEmpty) {
1126 - // Token is not in the wallet balance map -> Balance is 0
1116 throw Exception('Insufficient token balance (Token not found in wallet).');
1117 }
1118
1119 final tokenKey = matchingTokens.first;
1131 - final tokenBalance = balance[tokenKey]?.balance ?? BigInt.zero;
1120 + final tokenBalance = balance[tokenKey]?.available ?? Money.zero(tokenKey);
1121
1133 - if (tokenBalance < sourceTokenAmount) {
1134 - throw Exception('Insufficient ${tokenKey.title} balance to cover the transaction amount.');
1122 + if (tokenBalance < Money(sourceTokenAmount, tokenKey)) {
1123 + throw Exception('Insufficient ${tokenKey.symbol} balance to cover the transaction amount.');
1124 }
1125 }
1126
@@ -1144,13 +1133,12 @@ abstract class EVMChainWalletBase
1133 privateKey: _evmChainPrivateKey,
1134 toAddress: to,
1135 amount: valueWei,
1147 - gasFee: BigInt.from(gas.estimatedGasFee),
1136 + gasFee: Money.fromInt(gas.estimatedGasFee, currency),
1137 estimatedGasUnits: gasUnits,
1138 maxFeePerGas: gas.maxFeePerGas,
1139 priority: priority,
1140 currency: nativeCurrency,
1141 feeCurrency: nativeCurrency.title,
1153 - exponent: 18,
1142 contractAddress: null,
1143 data: dataHex,
1144 gasPrice: gas.gasPrice,
@@ -1161,23 +1149,23 @@ abstract class EVMChainWalletBase
1149 }
1150 }
1151
1164 - Future<PendingTransaction> createApprovalTransaction(BigInt amount, String spender,
1165 - CryptoCurrency token, EVMChainTransactionPriority? priority, String feeCurrency,
1152 + Future<PendingTransaction> createApprovalTransaction(
1153 + Money amount, String spender, EVMChainTransactionPriority? priority,
1154 {bool useBlinkProtection = true}) async {
1167 - final CryptoCurrency transactionCurrency =
1168 - balance.keys.firstWhere((element) => element.title == token.title);
1155 + final transactionCurrency =
1156 + balance.keys.firstWhere((element) => element.symbol == amount.currency.symbol);
1157 assert(transactionCurrency is Erc20Token);
1158
1159 final data = _client.getEncodedDataForApprovalTransaction(
1160 contractAddress: EthereumAddress.fromHex((transactionCurrency as Erc20Token).contractAddress),
1173 - value: EtherAmount.fromBigInt(EtherUnit.wei, amount),
1161 + value: EtherAmount.fromBigInt(EtherUnit.wei, amount.amount),
1162 toAddress: EthereumAddress.fromHex(spender),
1163 );
1164
1177 - final tokenContract = (transactionCurrency as Erc20Token).contractAddress;
1165 + final tokenContract = transactionCurrency.contractAddress;
1166
1167 final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction(
1180 - amount: BigInt.zero,
1168 + amount: Money.zero(currency),
1169 receivingAddressHex: tokenContract,
1170 priority: priority,
1171 contractAddress: tokenContract,
@@ -1193,11 +1181,9 @@ abstract class EVMChainWalletBase
1181 spender: spender,
1182 amount: amount,
1183 priority: priority,
1196 - gasFee: BigInt.from(gasFeesModel.estimatedGasFee),
1184 + gasFee: Money.fromInt(gasFeesModel.estimatedGasFee, currency),
1185 maxFeePerGas: gasFeesModel.maxFeePerGas,
1198 - feeCurrency: feeCurrency,
1186 estimatedGasUnits: safeGasUnits,
1200 - exponent: transactionCurrency.decimal,
1187 contractAddress: tokenContract,
1188 gasPrice: gasFeesModel.gasPrice,
1189 useBlinkProtection: useBlinkProtection,
@@ -1354,7 +1340,8 @@ abstract class EVMChainWalletBase
1340 String toJSON() => json.encode({
1341 'mnemonic': _mnemonic,
1342 'private_key': privateKey,
1357 - 'balance': balance[currency]?.toJSON() ?? EVMChainERC20Balance(BigInt.zero).toJSON(),
1343 + 'balance':
1344 + balance[currency]?.toJSON() ?? EVMChainERC20Balance(Money.zero(currency)).toJSON(),
1345 'passphrase': passphrase,
1346 'selected_chain_id': selectedChainId,
1347 });
@@ -1370,9 +1357,9 @@ abstract class EVMChainWalletBase
1357 try {
1358 final balance = await _client.getBalance(_evmChainPrivateKey.address);
1359
1373 - return EVMChainERC20Balance(balance.getInWei);
1360 + return EVMChainERC20Balance(Money(balance.getInWei, currency));
1361 } catch (_) {
1375 - return balance[currency] ?? EVMChainERC20Balance(BigInt.zero);
1362 + return balance[currency] ?? EVMChainERC20Balance(Money.zero(currency));
1363 }
1364 }
1365
@@ -1429,10 +1416,7 @@ abstract class EVMChainWalletBase
1416
1417 try {
1418 if (token.enabled) {
1432 - balance[token] = await _client.fetchERC20Balances(
1433 - _evmChainPrivateKey.address,
1434 - token.contractAddress,
1435 - );
1419 + balance[token] = await _client.fetchERC20Balances(_evmChainPrivateKey.address, token);
1420 } else {
1421 balance.remove(token);
1422 }
@@ -1536,12 +1520,7 @@ abstract class EVMChainWalletBase
1520
1521 if (newToken.enabled) {
1522 try {
1539 - final erc20Balance = await _client.fetchERC20Balances(
1540 - _evmChainPrivateKey.address,
1541 - newToken.contractAddress,
1542 - );
1543 -
1544 - balance[newToken] = erc20Balance;
1523 + balance[newToken] = await _client.fetchERC20Balances(_evmChainPrivateKey.address, newToken);
1524
1525 await evmChainErc20TokensBox.put(newToken.contractAddress, newToken);
1526 } on Exception catch (_) {
@@ -1611,9 +1590,6 @@ abstract class EVMChainWalletBase
1590 if (!hasKeysFile) rethrow;
1591 }
1592
1614 - final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String?) ??
1615 - EVMChainERC20Balance(BigInt.zero);
1616 -
1593 final WalletKeysData keysData;
1594 // Migrate wallet from the old scheme to the new .keys file scheme
1595 if (!hasKeysFile) {
@@ -1658,6 +1634,10 @@ abstract class EVMChainWalletBase
1634 // Use saved chainId if available, otherwise use the computed chainId
1635 final initialChainIdForWallet = savedChainId ?? chainId;
1636
1637 + final balance =
1638 + EVMChainERC20Balance.fromJSON(data?['balance'] as String?, chainConfig.nativeCurrency) ??
1639 + EVMChainERC20Balance(Money.zero(chainConfig.nativeCurrency));
1640 +
1641 return EVMChainWallet(
1642 walletInfo: walletInfo,
1643 derivationInfo: await walletInfo.getDerivationInfo(),
cw_evm/lib/evm_erc20_balance.dart
+10 -20
@@ -1,33 +1,23 @@
1 -import 'dart:convert';
1 +import "dart:convert";
2
3 -import 'package:cw_core/balance.dart';
3 +import "package:cw_core/amount/money.dart";
4 +import "package:cw_core/balance.dart";
5 +import "package:cw_core/currency.dart";
6
7 class EVMChainERC20Balance extends Balance {
6 - EVMChainERC20Balance(this.balance, {this.exponent = 18})
7 - : super(balance, balance);
8 + EVMChainERC20Balance(Money balance) : super(balance, Money.zero(balance.currency));
9
9 - final BigInt balance;
10 - final int exponent;
10 + String toJSON() => json.encode({ "balanceInWei": available.amount.toString() });
11
12 - String toJSON() => json.encode({
13 - 'balanceInWei': balance.toString(),
14 - 'exponent': exponent,
15 - });
16 -
17 - static EVMChainERC20Balance? fromJSON(String? jsonSource) {
18 - if (jsonSource == null) {
19 - return null;
20 - }
12 + static EVMChainERC20Balance? fromJSON(String? jsonSource, Currency currency) {
13 + if (jsonSource == null) return null;
14
15 final decoded = json.decode(jsonSource) as Map;
16
17 try {
25 - return EVMChainERC20Balance(
26 - BigInt.parse(decoded['balanceInWei']),
27 - exponent: decoded['exponent'],
28 - );
18 + return EVMChainERC20Balance(Money(BigInt.parse(decoded["balanceInWei"]), currency));
19 } catch (e) {
30 - return EVMChainERC20Balance(BigInt.zero);
20 + return EVMChainERC20Balance(Money.zero(currency));
21 }
22 }
23 }
cw_evm/lib/pending_evm_chain_transaction.dart
+11 -13
@@ -1,6 +1,6 @@
1 import 'dart:typed_data';
2
3 -import 'package:cw_core/format_fixed.dart';
3 +import 'package:cw_core/amount/money.dart';
4 import 'package:cw_core/pending_transaction.dart';
5 import 'package:web3dart/crypto.dart';
6 import 'package:hex/hex.dart' as Hex;
@@ -8,36 +8,36 @@ import 'package:hex/hex.dart' as Hex;
8 class PendingEVMChainTransaction with PendingTransaction {
9 final Function sendTransaction;
10 final Uint8List signedTransaction;
11 - final BigInt fee;
12 - final String feeCurrency;
13 - final String amount;
14 - final int exponent;
11 final bool isInfiniteApproval;
12
13 PendingEVMChainTransaction({
14 required this.sendTransaction,
15 required this.signedTransaction,
16 required this.fee,
21 - required this.feeCurrency,
17 required this.amount,
23 - required this.exponent,
18 this.isInfiniteApproval = false,
19 });
20
21 @override
22 String get amountFormatted {
23 if (isInfiniteApproval) return "∞";
30 - return formatFixed(BigInt.parse(amount), exponent);
24 + return amount.toString();
25 }
26
27 + @override
28 + final Money amount;
29 +
30 + @override
31 + final Money fee;
32 +
33 @override
34 Future<void> commit() async => await sendTransaction();
35
36 @override
37 - String get feeFormatted => "$feeFormattedValue $feeCurrency";
37 + String get feeFormatted => fee.toStringWithSymbol(fractionalDigits: 10);
38
39 @override
40 - String get feeFormattedValue => formatFixed(fee, 18, fractionalDigits: 10);
40 + String get feeFormattedValue => fee.toStringWithPrecision(fractionalDigits: 10);
41
42 @override
43 String get hex => bytesToHex(signedTransaction, include0x: true);
@@ -61,7 +61,5 @@ class PendingEVMChainTransaction with PendingTransaction {
61 }
62
63 @override
64 - Future<Map<String, String>> commitUR() {
65 - throw UnimplementedError();
66 - }
64 + Future<Map<String, String>> commitUR() => throw UnimplementedError();
65 }
cw_evm/lib/usdt0/usdt0_service.dart
+3 -4
@@ -1,3 +1,4 @@
1 +import 'package:cw_core/amount/money.dart';
2 import 'package:cw_core/erc20_token.dart';
3 import 'package:cw_core/pending_transaction.dart';
4 import 'package:cw_evm/contract/erc20.dart';
@@ -80,11 +81,9 @@ class USDT0Service {
81 );
82
83 final pendingApproval = await wallet.createApprovalTransaction(
83 - maxUint,
84 + Money(maxUint,token),
85 adapter,
85 - token,
86 priority,
87 - EVMChainUtils.getFeeCurrency(wallet.selectedChainId),
87 useBlinkProtection: useBlinkProtection,
88 );
89 await pendingApproval.commit();
@@ -116,7 +115,7 @@ class USDT0Service {
115 return wallet.createCallDataTransaction(
116 oftAddress,
117 encoded.dataHex,
119 - encoded.valueWei,
118 + Money(encoded.valueWei, token),
119 priority,
120 token.contractAddress,
121 amount,
cw_evm/lib/utils/evm_chain_formatter.dart
-34
@@ -11,40 +11,6 @@ class EVMChainFormatter {
11 }
12 }
13
14 - /// Parse EVM chain amount to BigInt to avoid integer overflow for large amounts
15 - /// [decimals] defaults to 18 for native ETH/POL, but should be set to token decimals for ERC20 tokens
16 - static BigInt parseEVMChainAmountToBigInt(String amount, {int decimals = evmDecimals}) {
17 - try {
18 - String cleanAmount = amount.replaceAll(',', '.');
19 -
20 - bool isNegative = cleanAmount.startsWith('-');
21 - if (isNegative) {
22 - cleanAmount = cleanAmount.substring(1);
23 - }
24 -
25 - final parts = cleanAmount.split('.');
26 - String whole = parts[0].isEmpty ? '0' : parts[0];
27 - String fraction = parts.length > 1 ? parts[1] : '';
28 -
29 - // 3. Strict Truncation Logic
30 - // If fraction is longer than decimals, strictly cut it off.
31 - // If shorter, pad it with zeros.
32 - if (fraction.length > decimals) {
33 - fraction = fraction.substring(0, decimals);
34 - } else {
35 - fraction = fraction.padRight(decimals, '0');
36 - }
37 -
38 - final wholeBigInt = BigInt.parse(whole);
39 - final fractionBigInt = BigInt.parse(fraction);
40 - final multiplier = BigInt.from(10).pow(decimals);
41 -
42 - return (wholeBigInt * multiplier) + fractionBigInt;
43 - } catch (_) {
44 - return BigInt.zero;
45 - }
46 - }
47 -
14 static String truncateDecimals(String amount, int decimals) {
15 final parts = amount.split(".");
16
cw_monero/lib/api/wallet.dart
+12 -15
@@ -4,6 +4,8 @@ import 'dart:ffi';
4 import 'dart:isolate';
5 import 'dart:math';
6
7 +import 'package:cw_core/amount/money.dart';
8 +import 'package:cw_core/crypto_currency.dart';
9 import 'package:cw_core/utils/print_verbose.dart';
10 import 'package:cw_monero/api/account_list.dart';
11 import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart';
@@ -118,9 +120,7 @@ String getSeedLegacy(String? language) {
120 return legacy;
121 }
122
121 -String getPassphrase() {
122 - return currentWallet?.getCacheAttribute(key: "cakewallet.passphrase") ?? "";
123 -}
123 +String getPassphrase() => currentWallet?.getCacheAttribute(key: "cakewallet.passphrase") ?? "";
124
125 Map<int, Map<int, Map<int, String>>> addressCache = {};
126
@@ -146,12 +146,11 @@ String getAddress({int accountIndex = 0, int addressIndex = 0}) {
146 return addressCache[currentWallet!.ffiAddress()]![accountIndex]![addressIndex]!;
147 }
148
149 -int getFullBalance({int accountIndex = 0}) {
150 - return currentWallet?.balance(accountIndex: accountIndex) ?? 0;
151 -}
149 +Money getFullBalance({int accountIndex = 0}) =>
150 + Money.fromInt(currentWallet?.balance(accountIndex: accountIndex) ?? 0, CryptoCurrency.xmr);
151
153 -int getUnlockedBalance({int accountIndex = 0}) =>
154 - currentWallet?.unlockedBalance(accountIndex: accountIndex) ?? 0;
152 +Money getUnlockedBalance({int accountIndex = 0}) => Money.fromInt(
153 + currentWallet?.unlockedBalance(accountIndex: accountIndex) ?? 0, CryptoCurrency.xmr);
154
155 int getCurrentHeight() => currentWallet?.blockChainHeight() ?? 0;
156
@@ -417,13 +416,11 @@ Future setTrustedDaemon(bool trusted) async =>
416
417 Future<bool> trustedDaemon() async => currentWallet!.trustedDaemon();
418
420 -String signMessage(String message, {String address = ""}) {
421 - return currentWallet!.signMessage(message: message, address: address);
422 -}
419 +String signMessage(String message, {String address = ""}) =>
420 + currentWallet!.signMessage(message: message, address: address);
421
424 -bool verifyMessage(String message, String address, String signature) {
425 - return currentWallet!.verifySignedMessage(message: message, address: address, signature: signature);
426 -}
422 +bool verifyMessage(String message, String address, String signature) =>
423 + currentWallet!.verifySignedMessage(message: message, address: address, signature: signature);
424
425 Map<String, List<int>> debugCallLength() => monero.debugCallLength;
426
@@ -438,4 +435,4 @@ Map<String, dynamic> getWalletCacheDebug() {
435 "error": e.toString(),
436 };
437 }
441 -}
\ No newline at end of file
438 +}
cw_monero/lib/monero_balance.dart new
+17
@@ -0,0 +1,17 @@
1 +import 'package:cw_core/amount/money.dart';
2 +import 'package:cw_core/balance.dart';
3 +import 'package:cw_core/crypto_currency.dart';
4 +
5 +class MoneroBalance extends Balance {
6 + MoneroBalance({
7 + required this.fullBalance,
8 + required Money unlockedBalance,
9 + Money? frozen,
10 + }) : super(
11 + unlockedBalance,
12 + fullBalance - unlockedBalance,
13 + frozen: frozen ?? Money.zero(CryptoCurrency.xmr),
14 + );
15 +
16 + final Money fullBalance;
17 +}
cw_monero/lib/monero_transaction_info.dart
+3 -17
@@ -1,5 +1,4 @@
1 -import 'package:cw_core/crypto_currency.dart';
2 -import 'package:cw_core/format_amount.dart';
1 +import 'package:cw_core/amount/money.dart';
2 import 'package:cw_core/transaction_direction.dart';
3 import 'package:cw_core/transaction_info.dart';
4
@@ -15,23 +14,10 @@ class MoneroTransactionInfo extends TransactionInfo {
14 final DateTime date;
15 final int accountIndex;
16 final bool isPending;
18 - final int amount;
19 - final int fee;
17 + final Money amount;
18 + final Money fee;
19 final int addressIndex;
20 final int confirmations;
21 String? recipientAddress;
22 String? key;
24 - String? _fiatAmount;
25 -
26 - @override
27 - String amountFormatted() => '${CryptoCurrency.xmr.formatAmount(BigInt.from(amount))} XMR';
28 -
29 - @override
30 - String fiatAmount() => _fiatAmount ?? '';
31 -
32 - @override
33 - void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
34 -
35 - @override
36 - String feeFormatted() => '${CryptoCurrency.xmr.formatAmount(BigInt.from(fee))} XMR';
23 }
cw_monero/lib/monero_wallet.dart
+28 -47
@@ -3,12 +3,11 @@ import 'dart:ffi';
3 import 'dart:io';
4 import 'dart:isolate';
5
6 -import 'package:cw_core/monero_amount_format.dart';
6 +import 'package:cw_core/amount/money.dart';
7 import 'package:cw_core/pathForWallet.dart';
8 import 'package:cw_core/transaction_priority.dart';
9 import 'package:cw_core/account.dart';
10 import 'package:cw_core/crypto_currency.dart';
11 -import 'package:cw_core/monero_balance.dart';
11 import 'package:cw_core/monero_transaction_priority.dart';
12 import 'package:cw_core/monero_wallet_keys.dart';
13 import 'package:cw_core/monero_wallet_utils.dart';
@@ -30,6 +29,7 @@ import 'package:cw_monero/api/wallet.dart' as monero_wallet;
29 import 'package:cw_monero/api/wallet_manager.dart';
30 import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart';
31 import 'package:cw_monero/ledger.dart';
32 +import 'package:cw_monero/monero_balance.dart';
33 import 'package:cw_monero/monero_transaction_creation_credentials.dart';
34 import 'package:cw_monero/monero_transaction_history.dart';
35 import 'package:cw_monero/monero_transaction_info.dart';
@@ -375,15 +375,15 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
375 };
376 }
377
378 - bool needExportOutputs(int amount) {
378 + bool needExportOutputs(Money amount) {
379 if (int.tryParse(currentWallet!.secretSpendKey()) != 0) {
380 return false;
381 }
382 // viewOnlyBalance - balance that we can spend
383 // TODO(mrcyjanek): remove hasUnknownKeyImages when we cleanup coin control
384 - return (currentWallet!.viewOnlyBalance(
385 - accountIndex: walletAddresses.account!.id) < amount) ||
386 - currentWallet!.hasUnknownKeyImages();
384 + return (currentWallet!.viewOnlyBalance(accountIndex: walletAddresses.account!.id) <
385 + amount.amount.toInt()) ||
386 + currentWallet!.hasUnknownKeyImages();
387 }
388
389 MoneroTrezorService? trezorService;
@@ -444,30 +444,22 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
444 }
445
446 if (hasMultiDestination) {
447 - if (outputs.any(
448 - (item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
449 - throw MoneroTransactionCreationException(
450 - 'You do not have enough XMR to send this amount.');
447 + if (outputs.any((item) => item.sendAll || item.cryptoAmount.amount <= BigInt.zero)) {
448 + throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.');
449 }
450
453 - final int totalAmount = outputs.fold(
454 - 0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
451 + final totalAmount = outputs.fold(0, (acc, value) => acc + value.cryptoAmount.amount.toInt());
452
456 - if (unlockedBalance < totalAmount) {
457 - throw MoneroTransactionCreationException(
458 - 'You do not have enough XMR to send this amount.');
453 + if (unlockedBalance < Money.fromInt(totalAmount, CryptoCurrency.xmr)) {
454 + throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.');
455 }
456
461 - if (inputs.isEmpty) MoneroTransactionCreationException(
462 - 'No inputs selected');
457 + if (inputs.isEmpty) MoneroTransactionCreationException('No inputs selected');
458
459 final moneroOutputs = outputs.map((output) {
465 - final outputAddress =
466 - output.isParsedAddress ? output.extractedAddress : output.address;
460 + final outputAddress = output.isParsedAddress ? output.extractedAddress : output.address;
461
468 - return MoneroOutput(
469 - address: outputAddress!,
470 - amount: output.cryptoAmount!.replaceAll(',', '.'));
462 + return MoneroOutput(address: outputAddress!, amount: output.cryptoAmount.toString());
463 }).toList();
464
465 pendingTransactionDescription =
@@ -481,8 +473,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
473 final output = outputs.first;
474 final address =
475 output.isParsedAddress ? output.extractedAddress : output.address;
484 - final amount =
485 - output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.');
476 + final amount = output.sendAll ? null : output.cryptoAmount.toString();
477
478 // if ((formattedAmount != null && unlockedBalance < formattedAmount) ||
479 // (formattedAmount == null && unlockedBalance <= 0)) {
@@ -492,8 +483,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
483 // 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.');
484 // }
485
495 - if (inputs.isEmpty) MoneroTransactionCreationException(
496 - 'No inputs selected');
486 + if (inputs.isEmpty) MoneroTransactionCreationException('No inputs selected');
487 pendingTransactionDescription =
488 await transaction_history.createTransactionSync(
489 address: address!,
@@ -835,10 +825,10 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
825 : TransactionDirection.incoming,
826 row.timeStamp,
827 row.isPending,
838 - row.amount,
828 + Money.fromInt(row.amount, currency),
829 row.accountIndex,
830 0,
841 - row.fee,
831 + Money.fromInt(row.fee,currency),
832 row.confirmations,
833 )..additionalInfo = <String, dynamic>{
834 'key': row.key,
@@ -893,8 +883,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
883 }
884
885 int _getHeightDistance(DateTime date) {
896 - final distance =
897 - DateTime.now().difference(date).inSeconds;
886 + final distance = DateTime.now().difference(date).inSeconds;
887 final daysTmp = (distance / 86400).round();
888 final days = daysTmp < 1 ? 1 : daysTmp;
889
@@ -915,31 +904,28 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
904
905 void _askForUpdateBalance() {
906 final unlockedBalance = _getUnlockedBalance();
918 - final fullBalance = monero_wallet.getFullBalance(
919 - accountIndex: walletAddresses.account!.id);
907 + final fullBalance = monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id);
908 final frozenBalance = _getFrozenBalance();
909 if (balance[currency]!.fullBalance != fullBalance ||
922 - balance[currency]!.unlockedBalance != unlockedBalance ||
923 - balance[currency]!.frozenBalance != frozenBalance) {
910 + balance[currency]!.available != unlockedBalance ||
911 + balance[currency]!.frozen != frozenBalance) {
912 balance[currency] = MoneroBalance(
925 - fullBalance: fullBalance,
926 - unlockedBalance: unlockedBalance,
927 - frozenBalance: frozenBalance);
913 + fullBalance: fullBalance, unlockedBalance: unlockedBalance, frozen: frozenBalance);
914 }
915 }
916
931 - int _getUnlockedBalance() => monero_wallet.getUnlockedBalance(
932 - accountIndex: walletAddresses.account!.id);
917 + Money _getUnlockedBalance() =>
918 + monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id);
919
934 - int _getFrozenBalance() {
920 + Money _getFrozenBalance() {
921 var frozenBalance = 0;
922
937 - for (var coin in unspentCoinsInfo.values.where((element) =>
923 + for (final coin in unspentCoinsInfo.values.where((element) =>
924 element.walletId == id && element.accountIndex == walletAddresses.account!.id)) {
925 if (coin.isFrozen && !coin.isSending) frozenBalance += coin.value;
926 }
927
942 - return frozenBalance;
928 + return Money.fromInt(frozenBalance, CryptoCurrency.xmr);
929 }
930
931 void _onNewBlock(int height, int blocksLeft, double ptc) async {
@@ -1013,9 +999,4 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
999 Future<void> setLedgerConnection(LedgerConnection connection) async {
1000 await enableLedgerExchange(connection);
1001 }
1016 -
1017 - @override
1018 - String formatCryptoAmount(String amount) {
1019 - return moneroAmountToString(amount: int.parse(amount));
1020 - }
1002 }
cw_monero/lib/pending_monero_transaction.dart
+9 -15
@@ -1,16 +1,14 @@
1 import 'dart:async';
2 import 'dart:ffi';
3
4 +import 'package:cw_core/amount/money.dart';
5 import 'package:cw_core/utils/print_verbose.dart';
6 import 'package:cw_core/wallet_info.dart';
6 -import 'package:cw_monero/api/account_list.dart';
7 -import 'package:cw_monero/api/structs/pending_transaction.dart';
8 -import 'package:cw_monero/api/transaction_history.dart'
9 - as monero_transaction_history;
7 import 'package:cw_core/crypto_currency.dart';
11 -import 'package:cw_core/amount_converter.dart';
12 -
8 import 'package:cw_core/pending_transaction.dart';
9 +import 'package:cw_monero/api/account_list.dart';
10 +import 'package:cw_monero/api/structs/pending_transaction.dart';
11 +import 'package:cw_monero/api/transaction_history.dart' as monero_transaction_history;
12 import 'package:cw_monero/api/wallet.dart';
13 import 'package:cw_monero/monero_wallet.dart';
14 import 'package:monero/monero.dart' as monero;
@@ -29,22 +27,18 @@ class PendingMoneroTransaction with PendingTransaction {
27 final PendingTransactionDescription pendingTransactionDescription;
28 final MoneroWalletBase wallet;
29
32 - @override
33 - String get id => pendingTransactionDescription.hash;
30 + Money get amount => Money.fromInt(pendingTransactionDescription.amount, CryptoCurrency.xmr);
31
35 - @override
36 - String get hex => pendingTransactionDescription.hex;
32 + Money get fee => Money.fromInt(pendingTransactionDescription.fee, CryptoCurrency.xmr);
33
34 @override
39 - String get amountFormatted => AmountConverter.amountIntToString(
40 - CryptoCurrency.xmr, pendingTransactionDescription.amount);
35 + String get id => pendingTransactionDescription.hash;
36
37 @override
43 - String get feeFormatted => "$feeFormattedValue XMR";
38 + String get hex => pendingTransactionDescription.hex;
39
40 @override
46 - String get feeFormattedValue => AmountConverter.amountIntToString(
47 - CryptoCurrency.xmr, pendingTransactionDescription.fee);
41 + String get amountFormatted => amount.toString();
42
43 @override
44 bool shouldCommitUR() => isViewOnly && wallet.hardwareWalletType != HardwareWalletType.trezor;
cw_nano/lib/banano_balance.dart deleted
-35
@@ -1,35 +0,0 @@
1 -import 'package:cw_core/balance.dart';
2 -import 'package:nanoutil/nanoutil.dart';
3 -
4 -BigInt stringAmountToBigIntBanano(String amount) {
5 - return BigInt.parse(NanoAmounts.getAmountAsRaw(amount, NanoAmounts.rawPerBanano));
6 -}
7 -
8 -class BananoBalance extends Balance {
9 - final BigInt currentBalance;
10 - final BigInt receivableBalance;
11 -
12 - BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(BigInt.zero, BigInt.zero);
13 -
14 - BananoBalance.fromFormattedString(
15 - {required String formattedCurrentBalance, required String formattedReceivableBalance})
16 - : currentBalance = stringAmountToBigIntBanano(formattedCurrentBalance),
17 - receivableBalance = stringAmountToBigIntBanano(formattedReceivableBalance),
18 - super(BigInt.zero, BigInt.zero);
19 -
20 - BananoBalance.fromRawString(
21 - {required String currentBalance, required String receivableBalance})
22 - : currentBalance = BigInt.parse(currentBalance),
23 - receivableBalance = BigInt.parse(receivableBalance),
24 - super(BigInt.zero, BigInt.zero);
25 -
26 - @override
27 - String get formattedAvailableBalance {
28 - return NanoAmounts.getRawAsUsableString(currentBalance.toString(), NanoAmounts.rawPerBanano);
29 - }
30 -
31 - @override
32 - String get formattedAdditionalBalance {
33 - return NanoAmounts.getRawAsUsableString(receivableBalance.toString(), NanoAmounts.rawPerBanano);
34 - }
35 -}
cw_nano/lib/nano_balance.dart
+16 -30
@@ -1,36 +1,22 @@
1 +import 'package:cw_core/amount/money.dart';
2 import 'package:cw_core/balance.dart';
2 -import 'package:cw_core/utils/print_verbose.dart';
3 -import 'package:nanoutil/nanoutil.dart';
4 -
5 -BigInt stringAmountToBigIntNano(String amount) {
6 - return BigInt.parse(NanoAmounts.getAmountAsRaw(amount, NanoAmounts.rawPerNano));
7 -}
3 +import 'package:cw_core/crypto_currency.dart';
4
5 class NanoBalance extends Balance {
10 - final BigInt currentBalance;
11 - final BigInt receivableBalance;
12 -
13 - NanoBalance({required this.currentBalance, required this.receivableBalance}) : super(currentBalance, receivableBalance);
14 -
15 - NanoBalance.fromFormattedString(
16 - {required String formattedCurrentBalance, required String formattedReceivableBalance})
17 - : currentBalance = stringAmountToBigIntNano(formattedCurrentBalance),
18 - receivableBalance = stringAmountToBigIntNano(formattedReceivableBalance),
19 - super(stringAmountToBigIntNano(formattedCurrentBalance), stringAmountToBigIntNano(formattedReceivableBalance));
20 -
21 - NanoBalance.fromRawString(
22 - {required String currentBalance, required String receivableBalance})
23 - : currentBalance = BigInt.parse(currentBalance),
24 - receivableBalance = BigInt.parse(receivableBalance),
25 - super(BigInt.parse(currentBalance), BigInt.parse(receivableBalance));
6 + final Money currentBalance;
7 + final Money receivableBalance;
8
27 - @override
28 - String get formattedAvailableBalance {
29 - return NanoAmounts.getRawAsUsableString(currentBalance.toString(), NanoAmounts.rawPerNano);
30 - }
9 + NanoBalance({required this.currentBalance, required this.receivableBalance})
10 + : super(currentBalance, receivableBalance);
11
32 - @override
33 - String get formattedAdditionalBalance {
34 - return NanoAmounts.getRawAsUsableString(receivableBalance.toString(), NanoAmounts.rawPerNano);
35 - }
12 + factory NanoBalance.fromRawString({
13 + required String currentBalance,
14 + required String receivableBalance,
15 + }) =>
16 + NanoBalance(
17 + currentBalance:
18 + Money.tryParse(currentBalance, CryptoCurrency.nano) ?? Money.zero(CryptoCurrency.nano),
19 + receivableBalance: Money.tryParse(receivableBalance, CryptoCurrency.nano) ??
20 + Money.zero(CryptoCurrency.nano),
21 + );
22 }
cw_nano/lib/nano_client.dart
+7 -5
@@ -1,6 +1,8 @@
1 import 'dart:async';
2 import 'dart:convert';
3
4 +import 'package:cw_core/amount/money.dart';
5 +import 'package:cw_core/crypto_currency.dart';
6 import 'package:cw_core/nano_account_info_response.dart';
7 import 'package:cw_core/utils/print_verbose.dart';
8 import 'package:cw_core/utils/proxy_wrapper.dart';
@@ -85,10 +87,10 @@ class NanoClient {
87 throw Exception(
88 "Error while trying to get balance! ${data["error"] != null ? data["error"] : ""}");
89 }
88 - final String currentBalance = data["balance"] as String;
89 - final String receivableBalance = data["receivable"] as String;
90 - final BigInt cur = BigInt.parse(currentBalance);
91 - final BigInt rec = BigInt.parse(receivableBalance);
90 + final currentBalance = data["balance"] as String;
91 + final receivableBalance = data["receivable"] as String;
92 + final cur = Money(BigInt.parse(currentBalance), CryptoCurrency.nano);
93 + final rec = Money(BigInt.parse(receivableBalance), CryptoCurrency.nano);
94 return NanoBalance(currentBalance: cur, receivableBalance: rec);
95 }
96
@@ -258,7 +260,7 @@ class NanoClient {
260
261 // first get the current account balance:
262 if (balanceAfterTx == null) {
261 - final BigInt currentBalance = (await getBalance(publicAddress)).currentBalance;
263 + final BigInt currentBalance = (await getBalance(publicAddress)).currentBalance.amount;
264 final BigInt txAmount = BigInt.parse(amountRaw);
265 balanceAfterTx = currentBalance - txAmount;
266 }
cw_nano/lib/nano_transaction_info.dart
+7 -18
@@ -1,13 +1,14 @@
1 +import 'package:cw_core/amount/money.dart';
2 +import 'package:cw_core/crypto_currency.dart';
3 import 'package:cw_core/format_amount.dart';
4 import 'package:cw_core/transaction_direction.dart';
5 import 'package:cw_core/transaction_info.dart';
4 -import 'package:nanoutil/nanoutil.dart';
6
7 class NanoTransactionInfo extends TransactionInfo {
8 NanoTransactionInfo({
9 required this.id,
10 required this.height,
10 - required this.amountRaw,
11 + required Money amountRaw,
12 this.tokenSymbol = "XNO",
13 required this.direction,
14 required this.confirmed,
@@ -15,12 +16,11 @@ class NanoTransactionInfo extends TransactionInfo {
16 required this.confirmations,
17 required this.to,
18 required this.from,
18 - }) : this.amount = amountRaw.toInt();
19 + }) : this.amount = amountRaw;
20
21 final String id;
22 final int height;
22 - final int amount;
23 - final BigInt amountRaw;
23 + final Money amount;
24 final TransactionDirection direction;
25 final DateTime date;
26 final bool confirmed;
@@ -32,28 +32,17 @@ class NanoTransactionInfo extends TransactionInfo {
32
33 bool get isPending => !this.confirmed;
34
35 - @override
36 - String amountFormatted() {
37 - final String amt =
38 - NanoAmounts.getRawAsUsableString(amountRaw.toString(), NanoAmounts.rawPerNano);
39 - final String acc = NanoAmounts.getRawAccuracy(amountRaw.toString(), NanoAmounts.rawPerNano);
40 - return "$acc$amt $tokenSymbol";
41 - }
42 -
35 @override
36 String fiatAmount() => _fiatAmount ?? '';
37
38 @override
39 void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
40
49 - @override
50 - String feeFormatted() => "0 XNO";
51 -
41 factory NanoTransactionInfo.fromJson(Map<String, dynamic> data) {
42 return NanoTransactionInfo(
43 id: data['id'] as String,
44 height: data['height'] as int,
56 - amountRaw: BigInt.parse(data['amountRaw'] as String),
45 + amountRaw: Money(BigInt.parse(data['amountRaw'] as String), CryptoCurrency.nano),
46 direction: parseTransactionDirectionFromInt(data['direction'] as int),
47 date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
48 confirmed: data['confirmed'] as bool,
@@ -67,7 +56,7 @@ class NanoTransactionInfo extends TransactionInfo {
56 Map<String, dynamic> toJson() => {
57 'id': id,
58 'height': height,
70 - 'amountRaw': amountRaw.toString(),
59 + 'amountRaw': amount.amount.toString(),
60 'direction': direction.index,
61 'date': date.millisecondsSinceEpoch,
62 'confirmed': confirmed,
cw_nano/lib/nano_wallet.dart
+15 -13
@@ -3,6 +3,7 @@ import 'dart:convert';
3 import 'dart:io';
4
5 import 'package:bip39/bip39.dart' as bip39;
6 +import 'package:cw_core/amount/money.dart';
7 import 'package:cw_core/cake_hive.dart';
8 import 'package:cw_core/crypto_currency.dart';
9 import 'package:cw_core/encryption_file_utils.dart';
@@ -55,7 +56,10 @@ abstract class NanoWalletBase
56 walletAddresses = NanoWalletAddresses(walletInfo),
57 balance = ObservableMap<CryptoCurrency, NanoBalance>.of({
58 CryptoCurrency.nano: initialBalance ??
58 - NanoBalance(currentBalance: BigInt.zero, receivableBalance: BigInt.zero)
59 + NanoBalance(
60 + currentBalance: Money.zero(CryptoCurrency.nano),
61 + receivableBalance: Money.zero(CryptoCurrency.nano),
62 + )
63 }),
64 super(walletInfo, derivationInfo) {
65 this.walletInfo = walletInfo;
@@ -191,7 +195,7 @@ abstract class NanoWalletBase
195
196 BigInt runningAmount = BigInt.zero;
197 await _updateBalance();
194 - BigInt runningBalance = balance[currency]?.currentBalance ?? BigInt.zero;
198 + BigInt runningBalance = balance[currency]?.currentBalance.amount ?? BigInt.zero;
199
200 final List<Map<String, String>> blocks = [];
201 String? previousHash;
@@ -199,14 +203,12 @@ abstract class NanoWalletBase
203 for (var txOut in credentials.outputs) {
204 late BigInt amt;
205 if (txOut.sendAll) {
202 - amt = balance[currency]?.currentBalance ?? BigInt.zero;
206 + amt = balance[currency]?.currentBalance.amount ?? BigInt.zero;
207 } else {
204 - amt = BigInt.tryParse(NanoAmounts.getAmountAsRaw(
205 - txOut.cryptoAmount?.replaceAll(',', '.') ?? "0", NanoAmounts.rawPerNano)) ??
206 - BigInt.zero;
208 + amt = txOut.cryptoAmount.amount;
209 }
210
209 - if (balance[currency]?.currentBalance != null && amt > balance[currency]!.currentBalance) {
211 + if (balance[currency]?.currentBalance != null && amt > balance[currency]!.currentBalance.amount) {
212 throw Exception("Trying to send more than entire balance!");
213 }
214
@@ -233,7 +235,7 @@ abstract class NanoWalletBase
235 }
236
237 try {
236 - if (runningAmount > balance[currency]!.currentBalance || runningBalance < BigInt.zero) {
238 + if (runningAmount > balance[currency]!.currentBalance.amount || runningBalance < BigInt.zero) {
239 throw Exception(("Trying to send more than entire balance!"));
240 }
241 } catch (e) {
@@ -241,7 +243,7 @@ abstract class NanoWalletBase
243 }
244
245 return PendingNanoTransaction(
244 - amount: runningAmount,
246 + amount: Money(runningAmount, currency),
247 id: "",
248 nanoClient: _client,
249 blocks: blocks,
@@ -294,7 +296,7 @@ abstract class NanoWalletBase
296 final bool isSend = transactionModel.type == "send";
297 result[transactionModel.hash] = NanoTransactionInfo(
298 id: transactionModel.hash,
297 - amountRaw: transactionModel.amount,
299 + amountRaw: Money(transactionModel.amount, currency),
300 height: transactionModel.height,
301 direction: isSend ? TransactionDirection.outgoing : TransactionDirection.incoming,
302 confirmed: transactionModel.confirmed,
@@ -360,7 +362,7 @@ abstract class NanoWalletBase
362 // get our balance:
363 await _updateBalance();
364 // if we have anything to receive, process it:
363 - if (balance[currency]!.receivableBalance > BigInt.zero) {
365 + if (balance[currency]!.receivableBalance.amount > BigInt.zero) {
366 await _receiveAll();
367 }
368 });
@@ -465,8 +467,8 @@ abstract class NanoWalletBase
467 // if we don't have a balance, we should at least create one, since it's a late binding
468 // otherwise, it's better to just leave it as whatever it was before:
469 if (balance[currency] == null) {
468 - balance[currency] =
469 - NanoBalance(currentBalance: BigInt.zero, receivableBalance: BigInt.zero);
470 + balance[currency] = NanoBalance(
471 + currentBalance: Money.zero(currency), receivableBalance: Money.zero(currency));
472 }
473 }
474 // don't save unnecessarily:
cw_nano/lib/pending_nano_transaction.dart
+7 -18
@@ -1,6 +1,7 @@
1 +import 'package:cw_core/amount/money.dart';
2 +import 'package:cw_core/crypto_currency.dart';
3 import 'package:cw_core/pending_transaction.dart';
4 import 'package:cw_nano/nano_client.dart';
3 -import 'package:nanoutil/nanoutil.dart';
5
6 class PendingNanoTransaction with PendingTransaction {
7 PendingNanoTransaction({
@@ -11,38 +12,26 @@ class PendingNanoTransaction with PendingTransaction {
12 });
13
14 final NanoClient nanoClient;
14 - final BigInt amount;
15 final String id;
16 final List<Map<String, String>> blocks;
17 String hex = "unused";
18
19 @override
20 - String get amountFormatted {
21 - final String amt = NanoAmounts.getRawAsUsableString(amount.toString(), NanoAmounts.rawPerNano);
22 - return amt;
23 - }
24 -
25 - String get accurateAmountFormatted {
26 - final String amt = NanoAmounts.getRawAsUsableString(amount.toString(), NanoAmounts.rawPerNano);
27 - final String acc = NanoAmounts.getRawAccuracy(amount.toString(), NanoAmounts.rawPerNano);
28 - return "$acc$amt";
29 - }
20 + final Money amount;
21
22 @override
32 - String get feeFormatted => "$feeFormattedValue XNO";
23 + String get amountFormatted => amount.toString();
24
25 @override
35 - String get feeFormattedValue => "0";
26 + Money get fee => Money.zero(CryptoCurrency.nano);
27
28 @override
29 Future<void> commit() async {
39 - for (var block in blocks) {
30 + for (final block in blocks) {
31 await nanoClient.processBlock(block, "send");
32 }
33 }
34
35 @override
45 - Future<Map<String, String>> commitUR() {
46 - throw UnimplementedError();
47 - }
36 + Future<Map<String, String>> commitUR() => throw UnimplementedError();
37 }
cw_solana/lib/default_spl_tokens.dart
+2
@@ -765,4 +765,6 @@ class DefaultSPLTokens {
765
766 return SPLToken.copyWith(token, icon: iconPath, tag: 'SOL');
767 }).toList();
768 +
769 + SPLToken get usdc => _defaultTokens.where((e) => e.symbol == "USDC").first;
770 }
cw_solana/lib/pending_solana_transaction.dart
+7 -8
@@ -1,11 +1,10 @@
1 +import 'package:cw_core/amount/money.dart';
2 import 'package:cw_core/pending_transaction.dart';
3
4 class PendingSolanaTransaction with PendingTransaction {
4 - final double amount;
5 final String serializedTransaction;
6 final String destinationAddress;
7 final Function sendTransaction;
8 - final double fee;
8 String? _sig;
9
10 PendingSolanaTransaction({
@@ -16,6 +15,12 @@ class PendingSolanaTransaction with PendingTransaction {
15 required this.sendTransaction,
16 });
17
18 + @override
19 + final Money amount;
20 +
21 + @override
22 + final Money fee;
23 +
24 @override
25 String get amountFormatted {
26 String stringifiedAmount = amount.toString();
@@ -32,12 +37,6 @@ class PendingSolanaTransaction with PendingTransaction {
37 _sig = await sendTransaction();
38 }
39
35 - @override
36 - String get feeFormatted => "$feeFormattedValue SOL";
37 -
38 - @override
39 - String get feeFormattedValue => fee.toString();
40 -
40 @override
41 String get hex => serializedTransaction;
42
cw_solana/lib/solana_balance.dart
+11 -30
@@ -1,44 +1,25 @@
1 -import 'dart:convert';
1 +import "dart:convert";
2
3 -import 'package:cw_core/balance.dart';
3 +import "package:cw_core/amount/money.dart";
4 +import "package:cw_core/balance.dart";
5 +import "package:cw_core/currency.dart";
6
7 class SolanaBalance extends Balance {
6 - SolanaBalance(this.balance, bool isToken) : super(
7 - BigInt.from(int.tryParse(balance.toStringAsFixed(isToken ? 6 : 9).replaceFirst(".", "")) ?? 0),
8 - BigInt.from(int.tryParse(balance.toStringAsFixed(isToken ? 6 : 9).replaceFirst(".", "")) ?? 0));
8 + SolanaBalance(Money balance) : super(balance, balance.copyWith(amount: BigInt.zero));
9
10 - // Using raw amount from RPC to avoid decimals mismatch for SPL tokens.
11 - SolanaBalance.forToken(BigInt rawAmount, double uiAmount)
12 - : balance = uiAmount,
13 - super(rawAmount, rawAmount);
10 + factory SolanaBalance.zero(Currency currency) => SolanaBalance(Money.zero(currency));
11
15 - final double balance;
16 -
17 - String get formattedAdditionalBalance => _balanceFormatted();
18 -
19 - String get formattedAvailableBalance => _balanceFormatted();
20 -
21 - String _balanceFormatted() {
22 - String stringBalance = balance.toString();
23 - if (stringBalance.toString().length >= 12) {
24 - stringBalance = stringBalance.substring(0, 12);
25 - }
26 - return stringBalance;
27 - }
28 -
29 - static SolanaBalance? fromJSON(String? jsonSource, bool isToken) {
30 - if (jsonSource == null) {
31 - return null;
32 - }
12 + static SolanaBalance? fromJSON(String? jsonSource, Currency currency) {
13 + if (jsonSource == null) return null;
14
15 final decoded = json.decode(jsonSource) as Map;
16
17 try {
37 - return SolanaBalance(decoded['balance'], isToken);
18 + return SolanaBalance(currency.parseAmount(decoded["balance"]));
19 } catch (e) {
39 - return SolanaBalance(0.0, isToken);
20 + return SolanaBalance(Money.zero(currency));
21 }
22 }
23
43 - String toJSON() => json.encode({'balance': balance.toString()});
24 + String toJSON() => json.encode({"balance": available.toString()});
25 }
cw_solana/lib/solana_client.dart
+102 -185
@@ -1,9 +1,10 @@
1 import 'dart:async';
2 import 'dart:convert';
3 -import 'dart:math' as math;
3
4 import 'package:blockchain_utils/blockchain_utils.dart';
5 +import 'package:cw_core/amount/money.dart';
6 import 'package:cw_core/crypto_currency.dart';
7 +import 'package:cw_core/currency.dart';
8 import 'package:cw_core/node.dart';
9 import 'package:cw_core/utils/proxy_wrapper.dart';
10 import 'package:cw_core/solana_rpc_http_service.dart';
@@ -32,7 +33,7 @@ class TransactionFetchResult {
33
34 class SolanaWalletClient {
35 // Minimum amount in SOL to consider a transaction valid (to filter spam)
35 - static const double minValidAmount = 0.00000003;
36 + static Money minValidAmount = Money.parse("0.00000003", CryptoCurrency.sol);
37 late final client = ProxyWrapper().getHttpIOClient();
38 SolanaRPC? _provider;
39
@@ -61,24 +62,17 @@ class SolanaWalletClient {
62 }
63 }
64
64 - Future<double> getBalance(String walletAddress, {bool throwOnError = false}) async {
65 + Future<Money> getBalance(String walletAddress, {bool throwOnError = false}) async {
66 try {
67 final balance = await _provider!.requestWithContext(
67 - SolanaRPCGetBalance(
68 - account: SolAddress(walletAddress),
69 - ),
68 + SolanaRPCGetBalance(account: SolAddress(walletAddress)),
69 );
71 -
72 - final balInLamp = balance.result.toDouble();
73 -
74 - final solBalance = balInLamp / SolanaUtils.lamportsPerSol;
75 -
76 - return solBalance;
70 + return Money(balance.result, CryptoCurrency.sol);
71 } catch (_) {
72 if (throwOnError) {
73 rethrow;
74 }
81 - return 0.0;
75 + return Money.zero(CryptoCurrency.sol);
76 }
77 }
78
@@ -100,11 +94,11 @@ class SolanaWalletClient {
94 }
95 }
96
103 - Future<SolanaBalance?> getSplTokenBalance(String mintAddress, String walletAddress,
97 + Future<SolanaBalance?> getSplTokenBalance(SPLToken token, String walletAddress,
98 {bool throwOnError = false}) async {
99 try {
100 // Fetch the token accounts (a token can have multiple accounts for various uses)
107 - final tokenAccounts = await getSPLTokenAccounts(mintAddress, walletAddress);
101 + final tokenAccounts = await getSPLTokenAccounts(token.mintAddress, walletAddress);
102
103 // Handle scenario where there is no token account
104 if (tokenAccounts == null || tokenAccounts.isEmpty) {
@@ -112,8 +106,7 @@ class SolanaWalletClient {
106 }
107
108 // Sum raw amounts and ui amounts across all token accounts
115 - BigInt totalRaw = BigInt.zero;
116 - double totalUi = 0.0;
109 + var totalRaw = BigInt.zero;
110
111 for (var tokenAccount in tokenAccounts) {
112 final tokenAmountResult = await _provider!.request(
@@ -122,56 +115,43 @@ class SolanaWalletClient {
115
116 final raw = BigInt.tryParse(tokenAmountResult.amount) ?? BigInt.zero;
117 totalRaw += raw;
125 -
126 - final ui = tokenAmountResult.uiAmount ??
127 - (double.tryParse(tokenAmountResult.uiAmountString ?? '0') ?? 0.0);
128 - totalUi += ui;
118 }
119
131 - return SolanaBalance.forToken(totalRaw, totalUi);
120 + return SolanaBalance(Money(totalRaw, token));
121 } catch (_) {
133 - if (throwOnError) {
134 - rethrow;
135 - }
122 + if (throwOnError) rethrow;
123 +
124 return null;
125 }
126 }
127
140 - Future<double> getFeeForMessage(String message, Commitment commitment) async {
128 + Future<Money> getFeeForMessage(String message, Commitment commitment) async {
129 try {
130 final feeForMessage = await _provider!.request(
143 - SolanaRPCGetFeeForMessage(
144 - encodedMessage: message,
145 - commitment: commitment,
146 - ),
131 + SolanaRPCGetFeeForMessage(encodedMessage: message, commitment: commitment),
132 );
133
149 - final fee = (feeForMessage?.toDouble() ?? 0.0) / SolanaUtils.lamportsPerSol;
150 - return fee;
134 + return Money(feeForMessage ?? BigInt.zero, CryptoCurrency.sol);
135 } catch (_) {
152 - return 0.0;
136 + return Money.zero(CryptoCurrency.sol);
137 }
138 }
139
156 - Future<double> getEstimatedFee(SolanaPublicKey publicKey, Commitment commitment) async {
140 + Future<Money> getEstimatedFee(SolanaPublicKey publicKey, Commitment commitment) async {
141 final message = await _getMessageForNativeTransaction(
142 publicKey: publicKey,
143 destinationAddress: publicKey.toAddress().address,
160 - lamports: SolanaUtils.lamportsPerSol,
144 + lamports: Money(BigInt.from(1000000000), CryptoCurrency.sol),
145 commitment: commitment,
146 );
147
164 - final estimatedFee = await _getFeeFromCompiledMessage(
165 - message,
166 - commitment,
167 - );
168 - return estimatedFee;
148 + return _getFeeFromCompiledMessage(message, commitment);
149 }
150
151 Future<List<SolanaTransactionModel>?> parseTransaction({
152 VersionedTransactionResponse? txResponse,
153 required String walletAddress,
174 - String? splTokenSymbol,
154 + SPLToken? splToken,
155 }) async {
156 if (txResponse == null) return null;
157
@@ -182,8 +162,7 @@ class SolanaWalletClient {
162
163 if (meta == null || transaction == null) return null;
164
185 - final int fee = meta.fee;
186 - final feeInSol = fee / SolanaUtils.lamportsPerSol;
165 + final fee = meta.fee;
166
167 final message = transaction.message;
168 final instructions = message.compiledInstructions;
@@ -201,12 +180,10 @@ class SolanaWalletClient {
180 message: message,
181 meta: meta,
182 fee: fee,
204 - feeInSol: feeInSol,
183 walletAddress: walletAddress,
184 signature: signature,
185 blockTime: blockTime,
186 instructions: instructions,
209 - splTokenSymbol: splTokenSymbol,
187 );
188
189 if (swapTransactions.isNotEmpty) return swapTransactions;
@@ -230,7 +207,6 @@ class SolanaWalletClient {
207 message: message,
208 meta: meta,
209 fee: fee,
233 - feeInSol: feeInSol,
210 feePayerIndex: feePayerIndex,
211 walletAddress: walletAddress,
212 signature: signature,
@@ -248,12 +224,11 @@ class SolanaWalletClient {
224 message: message,
225 meta: meta,
226 fee: fee,
251 - feeInSol: feeInSol,
227 instruction: instruction,
228 walletAddress: walletAddress,
229 signature: signature,
230 blockTime: blockTime,
256 - splTokenSymbol: splTokenSymbol,
231 + splToken: splToken,
232 );
233
234 if (transactionModel != null) {
@@ -401,12 +376,10 @@ class SolanaWalletClient {
376 required VersionedMessage message,
377 required ConfirmedTransactionMeta meta,
378 required int fee,
404 - required double feeInSol,
379 required String walletAddress,
380 required String signature,
381 required BigInt? blockTime,
382 required List<CompiledInstruction> instructions,
409 - String? splTokenSymbol,
383 }) async {
384 final List<SolanaTransactionModel> swapTransactions = [];
385
@@ -452,7 +425,7 @@ class SolanaWalletClient {
425
426 // Parse outgoing side (what was sent)
427 double outgoingAmount = 0.0;
455 - String outgoingTokenSymbol = '';
428 + Currency outgoingToken = CryptoCurrency.sol;
429 String? outgoingMintAddress;
430 String? outgoingFrom;
431 String? outgoingTo;
@@ -475,7 +448,7 @@ class SolanaWalletClient {
448 if (balanceChange > BigInt.zero) {
449 // The wallet sent SOL
450 outgoingAmount = balanceChange.toDouble() / SolanaUtils.lamportsPerSol;
478 - outgoingTokenSymbol = 'SOL';
451 + outgoingToken = CryptoCurrency.sol;
452 outgoingMintAddress = null;
453 outgoingFrom = walletAddress;
454 // We find the intermediate account or swap program account
@@ -519,7 +492,7 @@ class SolanaWalletClient {
492 outgoingAmount = diff.toDouble();
493 outgoingMintAddress = mint;
494 final token = await getTokenInfo(mint);
522 - outgoingTokenSymbol = token?.symbol ?? 'TOKEN';
495 + outgoingToken = token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6);
496 outgoingFrom = walletAddress;
497 // We find the intermediate account
498 if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) {
@@ -541,7 +514,7 @@ class SolanaWalletClient {
514
515 // Parse incoming side (what was received)
516 double incomingAmount = 0.0;
544 - String incomingTokenSymbol = '';
517 + Currency incomingToken = CryptoCurrency.sol;
518 String? incomingMintAddress;
519 String? incomingFrom;
520 String? incomingTo;
@@ -563,7 +536,7 @@ class SolanaWalletClient {
536 if (balanceChange > BigInt.zero) {
537 // The wallet received SOL
538 incomingAmount = balanceChange.toDouble() / SolanaUtils.lamportsPerSol;
566 - incomingTokenSymbol = 'SOL';
539 + incomingToken = CryptoCurrency.sol;
540 incomingMintAddress = null;
541 incomingTo = walletAddress;
542 // We find the intermediate account
@@ -650,7 +623,9 @@ class SolanaWalletClient {
623 incomingAmount = diff.toDouble();
624 incomingMintAddress = mint;
625 final token = await getTokenInfo(mint);
653 - incomingTokenSymbol = token?.symbol ?? 'TOKEN';
626 + printV(token?.symbol);
627 + printV(token?.decimals);
628 + incomingToken = token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6);
629 incomingTo = walletAddress;
630 // We find the intermediate account
631 if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) {
@@ -679,13 +654,12 @@ class SolanaWalletClient {
654 from: outgoingFrom,
655 to: outgoingTo,
656 id: outgoingId,
682 - amount: outgoingAmount,
657 + amount: Money.parse(outgoingAmount.toStringAsFixed(outgoingToken.decimals), outgoingToken),
658 programId: outgoingMintAddress == null
659 ? SystemProgramConst.programId.address
660 : SPLTokenProgramConst.tokenProgramId.address,
661 blockTimeInInt: blockTime?.toInt() ?? 0,
687 - tokenSymbol: outgoingTokenSymbol,
688 - fee: feeInSol,
662 + fee: Money.fromInt(fee, CryptoCurrency.sol),
663 ));
664 }
665
@@ -698,13 +672,12 @@ class SolanaWalletClient {
672 from: incomingFrom,
673 to: incomingTo,
674 id: incomingId,
701 - amount: incomingAmount,
675 + amount: Money.parse(incomingAmount.toStringAsFixed(incomingToken.decimals), incomingToken),
676 programId: incomingMintAddress == null
677 ? SystemProgramConst.programId.address
678 : SPLTokenProgramConst.tokenProgramId.address,
679 blockTimeInInt: blockTime?.toInt() ?? 0,
706 - tokenSymbol: incomingTokenSymbol,
707 - fee: 0.0, // Fee only charged on outgoing side
680 + fee: Money.zero(CryptoCurrency.sol), // Fee only charged on outgoing side
681 ));
682 }
683
@@ -715,7 +688,6 @@ class SolanaWalletClient {
688 required VersionedMessage message,
689 required ConfirmedTransactionMeta meta,
690 required int fee,
718 - required double feeInSol,
691 required int feePayerIndex,
692 required String walletAddress,
693 required String signature,
@@ -754,10 +726,9 @@ class SolanaWalletClient {
726 final netChange = walletPaidFee ? walletChange - BigInt.from(fee) : walletChange;
727
728 final isOutgoing = netChange > BigInt.zero;
757 - final amountLamports = netChange.abs();
758 - final amountInSol = amountLamports.toDouble() / SolanaUtils.lamportsPerSol;
729 + final amountLamports = Money(netChange.abs(), CryptoCurrency.sol);
730
760 - if (amountInSol < minValidAmount) return null;
731 + if (amountLamports < minValidAmount) return null;
732
733 // Find the most likely receiver, the account that has the largest opposite balance change.
734 String? receiver;
@@ -783,11 +754,10 @@ class SolanaWalletClient {
754 from: isOutgoing ? walletAddress : receiver,
755 to: isOutgoing ? receiver : walletAddress,
756 id: signature,
786 - amount: amountInSol,
757 + amount: amountLamports,
758 programId: SystemProgramConst.programId.address,
788 - tokenSymbol: 'SOL',
759 blockTimeInInt: blockTime?.toInt() ?? 0,
790 - fee: feeInSol,
760 + fee: Money.fromInt(fee, CryptoCurrency.sol),
761 );
762 }
763
@@ -795,12 +765,11 @@ class SolanaWalletClient {
765 required VersionedMessage message,
766 required ConfirmedTransactionMeta meta,
767 required int fee,
798 - required double feeInSol,
768 required CompiledInstruction instruction,
769 required String walletAddress,
770 required String signature,
771 required BigInt? blockTime,
803 - String? splTokenSymbol,
772 + SPLToken? splToken,
773 }) async {
774 final preTokenBalances = meta.preTokenBalances;
775 final postTokenBalances = meta.postTokenBalances;
@@ -891,11 +860,8 @@ class SolanaWalletClient {
860 final sender = senderOwner ?? accountKeys[sourceAccountIndex].address;
861 final receiver = receiverOwner ?? accountKeys[destinationAccountIndex].address;
862
894 - String? tokenSymbol = splTokenSymbol;
895 -
896 - if (tokenSymbol == null && mintAddress != null) {
897 - final token = await getTokenInfo(mintAddress);
898 - tokenSymbol = token?.symbol;
863 + if (splToken == null && mintAddress != null) {
864 + splToken = await getTokenInfo(mintAddress);
865 }
866
867 return SolanaTransactionModel(
@@ -903,20 +869,21 @@ class SolanaWalletClient {
869 from: sender,
870 to: receiver,
871 id: signature,
906 - amount: amount,
872 + amount: Money.parse(amount.toStringAsFixed((splToken ?? CryptoCurrency.sol).decimals),
873 + splToken ?? CryptoCurrency.sol),
874 programId: SPLTokenProgramConst.tokenProgramId.address,
875 blockTimeInInt: blockTime?.toInt() ?? 0,
909 - tokenSymbol: tokenSymbol ?? '',
910 - fee: feeInSol,
876 + fee: Money.fromInt(fee, CryptoCurrency.sol),
877 );
878 }
879
880 /// Fetches a specific transaction by signature and parses it
915 - /// It returns a TransactionFetchResult object containing both transactions and token mints extracted from the transaction or null if the transaction is not found or cannot be parsed
881 + /// It returns a TransactionFetchResult object containing both transactions and token mints
882 + /// extracted from the transaction or null if the transaction is not found or cannot be parsed
883 Future<TransactionFetchResult?> fetchTransactionBySignature({
884 required String signature,
885 required String walletAddress,
919 - String? splTokenSymbol,
886 + SPLToken? splToken,
887 }) async {
888 try {
889 final txResponse = await _provider!.request(
@@ -936,7 +903,7 @@ class SolanaWalletClient {
903 final parsed = await parseTransaction(
904 txResponse: versionedResponse,
905 walletAddress: walletAddress,
939 - splTokenSymbol: splTokenSymbol,
906 + splToken: splToken,
907 );
908
909 if (parsed == null) return null;
@@ -985,8 +952,7 @@ class SolanaWalletClient {
952 /// Load the Address's transactions into the account
953 Future<List<SolanaTransactionModel>> fetchTransactions(
954 SolAddress address, {
988 - String? splTokenSymbol,
989 - int? splTokenDecimal,
955 + SPLToken? splToken,
956 Commitment? commitment,
957 SolAddress? walletAddress,
958 required void Function(List<SolanaTransactionModel>) onUpdate,
@@ -1025,7 +991,7 @@ class SolanaWalletClient {
991
992 final parsedTransactionsFutures = versionedBatchResponses.map((tx) => parseTransaction(
993 txResponse: tx,
1028 - splTokenSymbol: splTokenSymbol,
994 + splToken: splToken,
995 walletAddress: walletAddress?.address ?? address.address,
996 ));
997
@@ -1057,8 +1023,7 @@ class SolanaWalletClient {
1023
1024 Future<List<SolanaTransactionModel>> getSPLTokenTransfers({
1025 required String mintAddress,
1060 - required String splTokenSymbol,
1061 - required int splTokenDecimal,
1026 + required SPLToken splToken,
1027 required SolanaPrivateKey privateKey,
1028 required void Function(List<SolanaTransactionModel>) onUpdate,
1029 }) async {
@@ -1077,23 +1042,19 @@ class SolanaWalletClient {
1042
1043 if (associatedTokenAccount == null) return [];
1044
1080 - final accountPublicKey = associatedTokenAccount.address;
1081 -
1082 - final tokenTransactions = await fetchTransactions(
1083 - accountPublicKey,
1084 - splTokenSymbol: splTokenSymbol,
1085 - splTokenDecimal: splTokenDecimal,
1045 + return fetchTransactions(
1046 + associatedTokenAccount.address,
1047 + splToken: splToken,
1048 walletAddress: ownerWalletAddress,
1049 onUpdate: onUpdate,
1050 );
1089 -
1090 - return tokenTransactions;
1051 }
1052
1053 final Map<String, SPLToken?> tokenInfoCache = {};
1054
1055 Future<SPLToken?> getTokenInfo(String mintAddress) async {
1056 if (tokenInfoCache.containsKey(mintAddress)) {
1057 + printV("Cached");
1058 return tokenInfoCache[mintAddress];
1059 } else {
1060 final token = await fetchSPLTokenInfo(mintAddress);
@@ -1119,16 +1080,15 @@ class SolanaWalletClient {
1080 },
1081 );
1082
1083 + if (response.statusCode != 200) return null;
1084 final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>;
1085
1124 - final symbol = (decodedResponse['symbol'] ?? '') as String;
1125 -
1086 + final symbol = decodedResponse['symbol'] ?? '';
1087 final name = decodedResponse['name'] ?? '';
1088 final decimal = decodedResponse['decimals'] ?? '0';
1089 final iconPath = decodedResponse['logo'] ?? '';
1090
1130 - String filteredTokenSymbol =
1131 - symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', '');
1091 + final filteredTokenSymbol = symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', '');
1092
1093 return SPLToken(
1094 name: name,
@@ -1182,20 +1142,18 @@ class SolanaWalletClient {
1142 SolanaRPC? get getSolanaProvider => _provider;
1143
1144 Future<PendingSolanaTransaction> signSolanaTransaction({
1185 - required String tokenTitle,
1186 - required int tokenDecimals,
1187 - required double inputAmount,
1145 + required Money inputAmount,
1146 required String destinationAddress,
1147 required SolanaPrivateKey ownerPrivateKey,
1148 required bool isSendAll,
1191 - required double solBalance,
1149 + required Money solBalance,
1150 String? tokenMint,
1151 List<String> references = const [],
1152 }) async {
1153 const commitment = Commitment.confirmed;
1154
1197 - if (tokenTitle == CryptoCurrency.sol.title) {
1198 - final pendingNativeTokenTransaction = await _signNativeTokenTransaction(
1155 + if (inputAmount.currency == CryptoCurrency.sol) {
1156 + return _signNativeTokenTransaction(
1157 inputAmount: inputAmount,
1158 destinationAddress: destinationAddress,
1159 ownerPrivateKey: ownerPrivateKey,
@@ -1203,10 +1161,9 @@ class SolanaWalletClient {
1161 isSendAll: isSendAll,
1162 solBalance: solBalance,
1163 );
1206 - return pendingNativeTokenTransaction;
1164 } else {
1208 - final pendingSPLTokenTransaction = _signSPLTokenTransaction(
1209 - tokenDecimals: tokenDecimals,
1165 + return _signSPLTokenTransaction(
1166 + tokenDecimals: inputAmount.currency.decimals,
1167 tokenMint: tokenMint!,
1168 inputAmount: inputAmount,
1169 ownerPrivateKey: ownerPrivateKey,
@@ -1214,7 +1171,6 @@ class SolanaWalletClient {
1171 commitment: commitment,
1172 solBalance: solBalance,
1173 );
1217 - return pendingSPLTokenTransaction;
1174 }
1175 }
1176
@@ -1229,25 +1185,24 @@ class SolanaWalletClient {
1185 Future<Message> _getMessageForNativeTransaction({
1186 required SolanaPublicKey publicKey,
1187 required String destinationAddress,
1232 - required int lamports,
1188 + required Money lamports,
1189 required Commitment commitment,
1190 }) async {
1191 final instructions = [
1192 SystemProgram.transfer(
1193 from: publicKey.toAddress(),
1238 - layout: SystemTransferLayout(lamports: BigInt.from(lamports)),
1194 + layout: SystemTransferLayout(lamports: lamports.amount),
1195 to: SolAddress(destinationAddress),
1196 ),
1197 ];
1198
1199 final latestBlockhash = await _getLatestBlockhash(commitment);
1200
1245 - final message = Message.compile(
1201 + return Message.compile(
1202 transactionInstructions: instructions,
1203 payer: publicKey.toAddress(),
1204 recentBlockhash: latestBlockhash,
1205 );
1250 - return message;
1206 }
1207
1208 Future<Message> _getMessageForSPLTokenTransaction({
@@ -1256,14 +1211,14 @@ class SolanaWalletClient {
1211 required int tokenDecimals,
1212 required SolAddress mintAddress,
1213 required SolAddress sourceAccount,
1259 - required int amount,
1214 + required Money amount,
1215 required Commitment commitment,
1216 required SolAddress tokenProgramId,
1217 }) async {
1218 final instructions = [
1219 SPLTokenProgram.transferChecked(
1220 layout: SPLTokenTransferCheckedLayout(
1266 - amount: BigInt.from(amount),
1221 + amount: amount.amount,
1222 decimals: tokenDecimals,
1223 ),
1224 mint: mintAddress,
@@ -1275,61 +1230,46 @@ class SolanaWalletClient {
1230
1231 final latestBlockhash = await _getLatestBlockhash(commitment);
1232
1278 - final message = Message.compile(
1233 + return Message.compile(
1234 transactionInstructions: instructions,
1235 payer: ownerAddress,
1236 recentBlockhash: latestBlockhash,
1237 );
1283 - return message;
1238 }
1239
1286 - Future<double> _getFeeFromCompiledMessage(Message message, Commitment commitment) async {
1240 + Future<Money> _getFeeFromCompiledMessage(Message message, Commitment commitment) {
1241 final base64Message = base64Encode(message.serialize());
1288 -
1289 - final fee = await getFeeForMessage(base64Message, commitment);
1290 -
1291 - return fee;
1242 + return getFeeForMessage(base64Message, commitment);
1243 }
1244
1245 Future<bool> hasSufficientFundsLeftForRent({
1295 - required double inputAmount,
1296 - required double solBalance,
1297 - required double fee,
1246 + required Money inputAmount,
1247 + required Money solBalance,
1248 + required Money fee,
1249 }) async {
1250 final rent = await _provider!.request(
1300 - SolanaRPCGetMinimumBalanceForRentExemption(
1301 - size: SolanaTokenAccountUtils.accountSize,
1302 - ),
1251 + SolanaRPCGetMinimumBalanceForRentExemption(size: SolanaTokenAccountUtils.accountSize),
1252 );
1253
1305 - final rentInSol = (rent.toDouble() / SolanaUtils.lamportsPerSol).toDouble();
1306 -
1307 - final remnant = solBalance - (inputAmount + fee);
1308 -
1309 - if (remnant > rentInSol) return true;
1310 -
1311 - return false;
1254 + return (solBalance - (inputAmount + fee)) > Money(rent, CryptoCurrency.sol);
1255 }
1256
1257 Future<PendingSolanaTransaction> _signNativeTokenTransaction({
1315 - required double inputAmount,
1258 + required Money inputAmount,
1259 required String destinationAddress,
1260 required SolanaPrivateKey ownerPrivateKey,
1261 required Commitment commitment,
1262 required bool isSendAll,
1320 - required double solBalance,
1263 + required Money solBalance,
1264 }) async {
1322 - // Convert SOL to lamport
1323 - int lamports = (inputAmount * SolanaUtils.lamportsPerSol).toInt();
1324 -
1325 - Message message = await _getMessageForNativeTransaction(
1265 + final message = await _getMessageForNativeTransaction(
1266 publicKey: ownerPrivateKey.publicKey(),
1267 destinationAddress: destinationAddress,
1328 - lamports: lamports,
1268 + lamports: inputAmount,
1269 commitment: commitment,
1270 );
1271
1332 - SolAddress latestBlockhash = await _getLatestBlockhash(commitment);
1272 + final latestBlockhash = await _getLatestBlockhash(commitment);
1273
1274 final fee = await _getFeeFromCompiledMessage(
1275 message,
@@ -1337,7 +1277,7 @@ class SolanaWalletClient {
1277 );
1278
1279 if (!isSendAll) {
1340 - bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent(
1280 + final hasSufficientFundsLeft = await hasSufficientFundsLeftForRent(
1281 inputAmount: inputAmount,
1282 fee: fee,
1283 solBalance: solBalance,
@@ -1350,8 +1290,7 @@ class SolanaWalletClient {
1290
1291 String serializedTransaction;
1292 if (isSendAll) {
1353 - final feeInLamports = (fee * SolanaUtils.lamportsPerSol).toInt();
1354 - final updatedLamports = lamports - feeInLamports;
1293 + final updatedLamports = inputAmount - fee;
1294
1295 final transaction = _constructNativeTransaction(
1296 ownerPrivateKey: ownerPrivateKey,
@@ -1369,7 +1308,7 @@ class SolanaWalletClient {
1308 ownerPrivateKey: ownerPrivateKey,
1309 destinationAddress: destinationAddress,
1310 latestBlockhash: latestBlockhash,
1372 - lamports: lamports,
1311 + lamports: inputAmount,
1312 );
1313
1314 serializedTransaction = await _signTransactionInternal(
@@ -1383,33 +1322,29 @@ class SolanaWalletClient {
1322 commitment: commitment,
1323 );
1324
1386 - final pendingTransaction = PendingSolanaTransaction(
1325 + return PendingSolanaTransaction(
1326 amount: inputAmount,
1327 serializedTransaction: serializedTransaction,
1328 destinationAddress: destinationAddress,
1329 sendTransaction: sendTx,
1330 fee: fee,
1331 );
1393 -
1394 - return pendingTransaction;
1332 }
1333
1334 SolanaTransaction _constructNativeTransaction({
1335 required SolanaPrivateKey ownerPrivateKey,
1336 required String destinationAddress,
1337 required SolAddress latestBlockhash,
1401 - required int lamports,
1338 + required Money lamports,
1339 }) {
1340 final owner = ownerPrivateKey.publicKey().toAddress();
1341
1405 - /// Create a transfer instruction to move funds from the owner to the receiver.
1342 final transferInstruction = SystemProgram.transfer(
1343 from: owner,
1408 - layout: SystemTransferLayout(lamports: BigInt.from(lamports)),
1344 + layout: SystemTransferLayout(lamports: lamports.amount),
1345 to: SolAddress(destinationAddress),
1346 );
1347
1412 - /// Construct a Solana transaction with the transfer instruction.
1348 return SolanaTransaction(
1349 instructions: [transferInstruction],
1350 recentBlockhash: latestBlockhash,
@@ -1609,17 +1544,13 @@ class SolanaWalletClient {
1544 Future<PendingSolanaTransaction> _signSPLTokenTransaction({
1545 required int tokenDecimals,
1546 required String tokenMint,
1612 - required double inputAmount,
1547 + required Money inputAmount,
1548 required String destinationAddress,
1549 required SolanaPrivateKey ownerPrivateKey,
1550 required Commitment commitment,
1616 - required double solBalance,
1551 + required Money solBalance,
1552 }) async {
1553 final mintAddress = SolAddress(tokenMint);
1619 -
1620 - // Input by the user
1621 - final amount = (inputAmount * math.pow(10, tokenDecimals)).toInt();
1622 -
1554 final tokenProgramId = await _getTokenProgramId(mintAddress);
1555
1556 ProgramDerivedAddress? associatedSenderAccount;
@@ -1751,7 +1682,7 @@ class SolanaWalletClient {
1682 destination: associatedRecipientAccount.address,
1683 mint: mintAddress,
1684 owner: ownerPrivateKey.publicKey().toAddress(),
1754 - amount: BigInt.from(amount),
1685 + amount: inputAmount.amount,
1686 decimals: tokenDecimals,
1687 );
1688
@@ -1769,75 +1700,61 @@ class SolanaWalletClient {
1700 mintAddress: mintAddress,
1701 destinationAddress: associatedRecipientAccount.address,
1702 sourceAccount: associatedSenderAccount.address,
1772 - amount: amount,
1703 + amount: inputAmount,
1704 commitment: commitment,
1705 tokenProgramId: tokenProgramId,
1706 );
1707
1708 final fee = await _getFeeFromCompiledMessage(message, commitment);
1709
1779 - bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent(
1780 - inputAmount: 0,
1710 + final hasSufficientFundsLeft = await hasSufficientFundsLeftForRent(
1711 + inputAmount: Money.zero(CryptoCurrency.sol),
1712 fee: fee,
1713 solBalance: solBalance,
1714 );
1715
1785 - if (!hasSufficientFundsLeft) {
1786 - throw SolanaSignSPLTokenTransactionRentException();
1787 - }
1716 + if (!hasSufficientFundsLeft) throw SolanaSignSPLTokenTransactionRentException();
1717
1718 final serializedTransaction = await _signTransactionInternal(
1719 ownerPrivateKey: ownerPrivateKey,
1720 transaction: transaction,
1721 );
1722
1794 - sendTx() async => await sendTransaction(
1723 + sendTx() => sendTransaction(
1724 serializedTransaction: serializedTransaction,
1725 commitment: commitment,
1726 );
1727
1799 - final pendingTransaction = PendingSolanaTransaction(
1728 + return PendingSolanaTransaction(
1729 amount: inputAmount,
1730 serializedTransaction: serializedTransaction,
1731 destinationAddress: destinationAddress,
1732 sendTransaction: sendTx,
1733 fee: fee,
1734 );
1806 - return pendingTransaction;
1735 }
1736
1737 Future<String> _signTransactionInternal({
1738 required SolanaPrivateKey ownerPrivateKey,
1739 required SolanaTransaction transaction,
1740 }) async {
1813 - /// Sign the transaction with the owner's private key.
1741 final ownerSignature = ownerPrivateKey.sign(transaction.serializeMessage());
1742
1743 transaction.addSignature(ownerPrivateKey.publicKey().toAddress(), ownerSignature);
1744
1818 - /// Serialize the transaction.
1819 - final serializedTransaction = transaction.serializeString();
1820 -
1821 - return serializedTransaction;
1745 + return transaction.serializeString();
1746 }
1747
1748 Future<String> sendTransaction({
1749 required String serializedTransaction,
1826 - required Commitment commitment,
1827 - }) async {
1828 - try {
1829 - /// Send the transaction to the Solana network.
1830 - final signature = await _provider!.request(
1750 + required Commitment commitment
1751 + }) =>
1752 + _provider!.request(
1753 SolanaRPCSendTransaction(
1754 encodedTransaction: serializedTransaction,
1755 commitment: commitment,
1756 ),
1757 );
1836 - return signature;
1837 - } catch (e) {
1838 - throw Exception(e);
1839 - }
1840 - }
1758
1759 Future<String?> getIconImageFromTokenUri(String uri) async {
1760 if (uri.isEmpty || uri == '…') return null;
cw_solana/lib/solana_transaction_info.dart
+27 -38
@@ -1,80 +1,69 @@
1 -import 'package:cw_core/format_amount.dart';
1 +import 'package:cw_core/amount/money.dart';
2 +import 'package:cw_core/crypto_currency.dart';
3 import 'package:cw_core/transaction_direction.dart';
4 import 'package:cw_core/transaction_info.dart';
5
6 class SolanaTransactionInfo extends TransactionInfo {
7 SolanaTransactionInfo({
8 required this.id,
8 - required this.blockTime,
9 + required this.date,
10 required this.to,
11 required this.from,
12 required this.direction,
12 - required this.solAmount,
13 - this.tokenSymbol = "SOL",
13 + required this.amount,
14 required this.isPending,
15 - required this.txFee,
16 - }) : amount = solAmount.toInt();
15 + required this.fee,
16 + });
17
18 + @override
19 final String id;
20 + @override
21 final String? to;
22 + @override
23 final String? from;
21 - final int amount;
22 - final bool isPending;
23 - final double solAmount;
24 - final String tokenSymbol;
25 - final DateTime blockTime;
26 - final double txFee;
27 - final TransactionDirection direction;
28 -
29 - String? _fiatAmount;
24
25 @override
26 String get txHash => id.replaceFirst(RegExp(r'_(outgoing|incoming)$'), '');
27
28 @override
35 - DateTime get date => blockTime;
36 -
29 + final Money amount;
30 @override
38 - String amountFormatted() {
39 - String stringBalance = solAmount.toString();
40 - if (stringBalance.toString().length >= 12) {
41 - stringBalance = stringBalance.substring(0, 12);
42 - }
43 - return '$stringBalance $tokenSymbol';
44 - }
45 -
31 + final bool isPending;
32 @override
47 - String fiatAmount() => _fiatAmount ?? '';
48 -
33 + final Money fee;
34 @override
50 - void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
51 -
35 + final TransactionDirection direction;
36 @override
53 - String feeFormatted() => '${txFee.toString()} SOL';
37 + final DateTime date;
38
39 factory SolanaTransactionInfo.fromJson(Map<String, dynamic> data) {
40 + final symbol = data['tokenSymbol'] as String? ?? "SOL";
41 + final decimals = data['tokenDecimals'] as int? ?? 6;
42 +
43 + final currency = CryptoCurrency(name: symbol, title: symbol, decimals: decimals);
44 +
45 return SolanaTransactionInfo(
46 id: data['id'] as String,
58 - solAmount: data['solAmount'],
47 + amount: Money.parse(data['solAmount'].toString(), currency),
48 direction: parseTransactionDirectionFromInt(data['direction'] as int),
60 - blockTime: DateTime.fromMillisecondsSinceEpoch(data['blockTime'] as int),
49 + date: DateTime.fromMillisecondsSinceEpoch(data['blockTime'] as int),
50 isPending: data['isPending'] as bool,
62 - tokenSymbol: data['tokenSymbol'] as String,
51 to: data['to'],
52 from: data['from'],
65 - txFee: data['txFee'],
53 + fee: Money.parse(data['txFee'], CryptoCurrency.sol),
54 );
55 }
56
57 Map<String, dynamic> toJson() => {
58 'id': id,
71 - 'solAmount': solAmount,
59 + 'solAmount': amount.toString(),
60 'direction': direction.index,
73 - 'blockTime': blockTime.millisecondsSinceEpoch,
61 + 'blockTime': date.millisecondsSinceEpoch,
62 'isPending': isPending,
75 - 'tokenSymbol': tokenSymbol,
63 + 'tokenSymbol': amount.currency.symbol,
64 + 'tokenDecimals': amount.currency.decimals,
65 'to': to,
66 'from': from,
78 - 'txFee': txFee,
67 + 'txFee': fee.toString(),
68 };
69 }
cw_solana/lib/solana_transaction_model.dart
+4 -20
@@ -1,3 +1,5 @@
1 +import 'package:cw_core/amount/money.dart';
2 +
3 class SolanaTransactionModel {
4 final String id;
5
@@ -5,22 +7,16 @@ class SolanaTransactionModel {
7
8 final String to;
9
8 - final double amount;
10 + final Money amount;
11
10 - // If this is an outgoing transaction
12 final bool isOutgoingTx;
13
14 // The Program ID of this transaction, e.g, System Program, Token Program...
15 final String programId;
16
16 - // The DateTime from the UNIX timestamp of the block where the transaction was included
17 final DateTime blockTime;
18
19 - // The Transaction fee
20 - final double fee;
21 -
22 - // The token symbol
23 - final String tokenSymbol;
19 + final Money fee;
20
21 SolanaTransactionModel({
22 required this.id,
@@ -30,18 +26,6 @@ class SolanaTransactionModel {
26 required this.programId,
27 required int blockTimeInInt,
28 this.isOutgoingTx = false,
33 - required this.tokenSymbol,
29 required this.fee,
30 }) : blockTime = DateTime.fromMillisecondsSinceEpoch(blockTimeInInt * 1000);
36 -
37 - factory SolanaTransactionModel.fromJson(Map<String, dynamic> json) => SolanaTransactionModel(
38 - id: json['id'],
39 - blockTimeInInt: int.parse(json["timeStamp"]) * 1000,
40 - from: json["from"],
41 - to: json["to"],
42 - amount: double.parse(json["value"]),
43 - programId: json["programId"],
44 - fee: json['fee'],
45 - tokenSymbol: json['tokenSymbol'],
46 - );
31 }
cw_solana/lib/solana_wallet.dart
+26 -45
@@ -2,6 +2,7 @@ import 'dart:async';
2 import 'dart:convert';
3 import 'dart:io';
4
5 +import 'package:cw_core/amount/money.dart';
6 import 'package:cw_core/cake_hive.dart';
7 import 'package:cw_core/crypto_currency.dart';
8 import 'package:cw_core/encryption_file_utils.dart';
@@ -57,7 +58,7 @@ abstract class SolanaWalletBase
58 _client = SolanaWalletClient(),
59 walletAddresses = SolanaWalletAddresses(walletInfo),
60 balance = ObservableMap<CryptoCurrency, SolanaBalance>.of(
60 - {CryptoCurrency.sol: initialBalance ?? SolanaBalance(BigInt.zero.toDouble(), false)}),
61 + {CryptoCurrency.sol: initialBalance ?? SolanaBalance.zero(CryptoCurrency.sol)}),
62 super(walletInfo, derivationInfo) {
63 this.walletInfo = walletInfo;
64 transactionHistory = SolanaTransactionHistory(
@@ -83,7 +84,7 @@ abstract class SolanaWalletBase
84 SolanaWalletClient get client => _client;
85
86 @observable
86 - double? estimatedFee;
87 + Money? estimatedFee;
88
89 Timer? _transactionsUpdateTimer;
90
@@ -214,9 +215,8 @@ abstract class SolanaWalletBase
215 Future<void> _getEstimatedFees() async {
216 try {
217 estimatedFee = await _client.getEstimatedFee(_solanaPublicKey, Commitment.confirmed);
217 - printV(estimatedFee.toString());
218 } catch (e) {
219 - estimatedFee = 0.0;
219 + estimatedFee = Money.zero(currency);
220 }
221 }
222
@@ -237,27 +237,16 @@ abstract class SolanaWalletBase
237 orElse: () => throw Exception(
238 'Currency ${credentials.currency.title} ${credentials.currency.tag} is not accessible in the wallet, try to enable it first.'));
239
240 - final walletBalanceForCurrency = balance[transactionCurrency]!.balance;
240 + final walletBalanceForCurrency = balance[transactionCurrency]!.available;
241
242 - final solBalance = balance[CryptoCurrency.sol]!.balance;
242 + final solBalance = balance[CryptoCurrency.sol]!.available;
243
244 - double totalAmount = 0.0;
245 -
246 - bool isSendAll = false;
244 + var totalAmount = Money.zero(transactionCurrency);
245 + var isSendAll = false;
246
247 if (hasMultiDestination) {
249 - if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
250 - throw SolanaTransactionWrongBalanceException(transactionCurrency);
251 - }
252 -
253 - final totalAmountFromCredentials =
254 - outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
255 -
256 - totalAmount = totalAmountFromCredentials.toDouble();
257 -
258 - if (walletBalanceForCurrency < totalAmount) {
259 - throw SolanaTransactionWrongBalanceException(transactionCurrency);
260 - }
248 + // Solana doesn't have multi destination right now
249 + throw SolanaTransactionCreationException(transactionCurrency);
250 } else {
251 final output = outputs.first;
252
@@ -266,9 +255,7 @@ abstract class SolanaWalletBase
255 if (isSendAll) {
256 totalAmount = walletBalanceForCurrency;
257 } else {
269 - final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
270 -
271 - totalAmount = totalOriginalAmount;
258 + totalAmount = output.cryptoAmount.copyWith(currency: transactionCurrency);
259 }
260
261 if (walletBalanceForCurrency < totalAmount) {
@@ -282,20 +269,16 @@ abstract class SolanaWalletBase
269 tokenMint = (transactionCurrency as SPLToken).mintAddress;
270 }
271
285 - final pendingSolanaTransaction = await _client.signSolanaTransaction(
272 + return _client.signSolanaTransaction(
273 tokenMint: tokenMint,
287 - tokenTitle: transactionCurrency.title,
274 inputAmount: totalAmount,
275 ownerPrivateKey: _solanaPrivateKey,
290 - tokenDecimals: transactionCurrency.decimals,
276 destinationAddress: solCredentials.outputs.first.isParsedAddress
277 ? solCredentials.outputs.first.extractedAddress!
278 : solCredentials.outputs.first.address,
279 isSendAll: isSendAll,
280 solBalance: solBalance,
281 );
297 -
298 - return pendingSolanaTransaction;
282 }
283
284 @override
@@ -387,8 +370,7 @@ abstract class SolanaWalletBase
370 try {
371 return await _client.getSPLTokenTransfers(
372 mintAddress: token.mintAddress,
390 - splTokenSymbol: token.symbol,
391 - splTokenDecimal: token.decimal,
373 + splToken: token,
374 privateKey: _solanaPrivateKey,
375 onUpdate: updateTransactions,
376 );
@@ -414,14 +396,13 @@ abstract class SolanaWalletBase
396 id: transactionModel.id,
397 to: transactionModel.to,
398 from: transactionModel.from,
417 - blockTime: transactionModel.blockTime,
399 + date: transactionModel.blockTime,
400 direction: transactionModel.isOutgoingTx
401 ? TransactionDirection.outgoing
402 : TransactionDirection.incoming,
421 - solAmount: transactionModel.amount,
403 + amount: transactionModel.amount,
404 isPending: false,
423 - txFee: transactionModel.fee,
424 - tokenSymbol: transactionModel.tokenSymbol,
405 + fee: transactionModel.fee,
406 );
407 }
408
@@ -497,7 +478,8 @@ abstract class SolanaWalletBase
478 if (!hasKeysFile) rethrow;
479 }
480
500 - final balance = SolanaBalance.fromJSON(data?['balance'] as String?, false) ?? SolanaBalance(0.0, false);
481 + final balance = SolanaBalance.fromJSON(data?['balance'] as String?, CryptoCurrency.sol) ??
482 + SolanaBalance.zero(CryptoCurrency.sol);
483
484 final WalletKeysData keysData;
485 // Migrate wallet from the old scheme to then new .keys file scheme
@@ -545,7 +527,7 @@ abstract class SolanaWalletBase
527 Future<SolanaBalance> _fetchSOLBalance() async {
528 final balance = await _client.getBalance(solanaAddress);
529
548 - return SolanaBalance(balance, false);
530 + return SolanaBalance(balance);
531 }
532
533 /// Internal helper to update SPL token balances.
@@ -577,7 +559,7 @@ abstract class SolanaWalletBase
559
560 final results = await Future.wait(batch.map((token) async {
561 try {
580 - final fetched = await _client.getSplTokenBalance(token.mintAddress, solanaAddress);
562 + final fetched = await _client.getSplTokenBalance(token, solanaAddress);
563 return MapEntry(token, fetched);
564 } catch (e) {
565 printV('Error fetching spl token (${token.symbol}) balance ${e.toString()}');
@@ -588,8 +570,7 @@ abstract class SolanaWalletBase
570 for (final entry in results) {
571 final token = entry.key;
572 final fetchedBalance = entry.value;
591 - final currentBalance = balance[token] ??
592 - SolanaBalance.forToken(BigInt.zero, 0.0);
573 + final currentBalance = balance[token] ?? SolanaBalance.zero(token);
574 balance[token] = fetchedBalance ?? currentBalance;
575 }
576 }
@@ -605,7 +586,7 @@ abstract class SolanaWalletBase
586 await _client.getBalance(solanaAddress, throwOnError: true);
587
588 // Check USDC token balance
608 - const usdcMintAddress = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
589 + final usdcMintAddress = DefaultSPLTokens().usdc;
590 await _client.getSplTokenBalance(usdcMintAddress, solanaAddress, throwOnError: true);
591
592 return true;
@@ -694,9 +675,8 @@ abstract class SolanaWalletBase
675 await splTokensBox.put(token.mintAddress, token);
676
677 if (token.enabled) {
697 - final tokenBalance = await _client.getSplTokenBalance(token.mintAddress, solanaAddress) ??
698 - balance[token] ??
699 - SolanaBalance.forToken(BigInt.zero, 0.0);
678 + final tokenBalance = await _client.getSplTokenBalance(token, solanaAddress) ??
679 + balance[token] ?? SolanaBalance.zero(token);
680
681 balance[token] = tokenBalance;
682 } else {
@@ -715,7 +695,8 @@ abstract class SolanaWalletBase
695 }
696
697 Future<void> _removeTokenTransactionsInHistory(SPLToken token) async {
718 - transactionHistory.transactions.removeWhere((key, value) => value.tokenSymbol == token.title);
698 + transactionHistory.transactions
699 + .removeWhere((key, value) => value.amount.currency.symbol == token.symbol);
700 await transactionHistory.save();
701 }
702
cw_tron/lib/default_tron_tokens.dart
+2
@@ -104,4 +104,6 @@ class DefaultTronTokens {
104
105 return TronToken.copyWith(token, icon: iconPath, tag: 'TRX');
106 }).toList();
107 +
108 + TronToken get usdt => _defaultTokens.where((e) => e.symbol == "USDT").first;
109 }
cw_tron/lib/pending_tron_transaction.dart
+5 -8
@@ -1,13 +1,10 @@
1 -
2 -
1 +import 'package:cw_core/amount/money.dart';
2 import 'package:cw_core/pending_transaction.dart';
3 import 'package:web3dart/crypto.dart';
4
5 class PendingTronTransaction with PendingTransaction {
6 final Function sendTransaction;
7 final List<int> signedTransaction;
9 - final String fee;
10 - final String amount;
8
9 PendingTronTransaction({
10 required this.sendTransaction,
@@ -18,16 +15,16 @@ class PendingTronTransaction with PendingTransaction {
15 });
16
17 @override
21 - String get amountFormatted => amount;
18 + final Money amount;
19
20 @override
24 - Future<void> commit() async => await sendTransaction();
21 + final Money fee;
22
23 @override
27 - String get feeFormatted => "$feeFormattedValue TRX";
24 + String get amountFormatted => amount.toString();
25
26 @override
30 - String get feeFormattedValue => fee;
27 + Future<void> commit() async => await sendTransaction();
28
29 @override
30 String get hex => bytesToHex(signedTransaction);
cw_tron/lib/tron_balance.dart
+8 -8
@@ -1,25 +1,25 @@
1 import 'dart:convert';
2
3 +import 'package:cw_core/amount/money.dart';
4 import 'package:cw_core/balance.dart';
5 +import 'package:cw_core/currency.dart';
6
7 class TronBalance extends Balance {
8 TronBalance(this.balance) : super(balance, balance);
9
8 - final BigInt balance;
10 + final Money balance;
11
10 - String toJSON() => json.encode({ 'balance': balance.toString() });
12 + String toJSON() => json.encode({ 'balance': balance.amount.toString() });
13
12 - static TronBalance? fromJSON(String? jsonSource) {
13 - if (jsonSource == null) {
14 - return null;
15 - }
14 + static TronBalance? fromJSON(String? jsonSource, Currency currency) {
15 + if (jsonSource == null) return null;
16
17 final decoded = json.decode(jsonSource) as Map;
18
19 try {
20 - return TronBalance(BigInt.parse(decoded['balance']));
20 + return TronBalance(Money(BigInt.parse(decoded['balance']), currency));
21 } catch (e) {
22 - return TronBalance(BigInt.zero);
22 + return TronBalance(Money.zero(currency));
23 }
24 }
25 }
cw_tron/lib/tron_client.dart
+32 -64
@@ -3,9 +3,12 @@ import 'dart:convert';
3 import 'dart:developer';
4
5 import 'package:blockchain_utils/blockchain_utils.dart';
6 +import 'package:cw_core/amount/money.dart';
7 import 'package:cw_core/crypto_currency.dart';
8 +import 'package:cw_core/currency.dart';
9 import 'package:cw_core/node.dart';
10 import 'package:cw_core/utils/proxy_wrapper.dart';
11 +import 'package:cw_tron/default_tron_tokens.dart';
12 import 'package:cw_tron/pending_tron_transaction.dart';
13 import 'package:cw_tron/tron_abi.dart';
14 import 'package:cw_tron/tron_balance.dart';
@@ -234,26 +237,18 @@ class TronClient {
237 }
238
239 Future<int> getTRCEstimatedFee(TronAddress ownerAddress) async {
237 - String contractAddress = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';
238 - String constantAmount =
239 - '0'; // We're using 0 as the base amount here as we get an error when balance is zero i.e for new wallets.
240 + final usdtContractAddress = DefaultTronTokens().usdt.contractAddress;
241 final contract = ContractABI.fromJson(trc20Abi, isTron: true);
241 -
242 final function = contract.functionFromName("transfer");
243
244 - /// address /// amount
245 - final transferparams = [
246 - ownerAddress,
247 - TronHelper.toSun(constantAmount),
248 - ];
249 -
250 - final contractAddr = TronAddress(contractAddress);
244 + // We're using 0 as the base amount here as we get an error when balance is zero i.e for new wallets.
245 + final transferParams = [ownerAddress, BigInt.zero];
246
247 final request = await _provider!.request(
248 TronRequestTriggerConstantContract(
249 ownerAddress: ownerAddress,
255 - contractAddress: contractAddr,
256 - data: function.encodeHex(transferparams),
250 + contractAddress: TronAddress(usdtContractAddress),
251 + data: function.encodeHex(transferParams),
252 ),
253 );
254
@@ -261,21 +256,19 @@ class TronClient {
256 log("Tron TRC20 error: ${request.error} \n ${request.respose}");
257 }
258
264 - final feeLimit = await getFeeLimit(
259 + return getFeeLimit(
260 request.transactionRaw!,
261 ownerAddress,
262 ownerAddress,
263 energyUsed: request.energyUsed ?? 0,
264 isEstimatedFeeFlow: true,
265 );
271 - return feeLimit;
266 }
267
268 Future<PendingTronTransaction> signTransaction({
269 required TronPrivateKey ownerPrivKey,
270 required String toAddress,
277 - required String amount,
278 - required CryptoCurrency currency,
271 + required Money amount,
272 required BigInt tronBalance,
273 required bool sendAll,
274 }) async {
@@ -285,9 +278,9 @@ class TronClient {
278 // Define the receiving Tron address for the transaction.
279 final receiverAddress = TronAddress(toAddress);
280
288 - bool isNativeTransaction = currency == CryptoCurrency.trx;
281 + final isNativeTransaction = amount.currency == CryptoCurrency.trx;
282
290 - String totalAmount;
283 + Money totalAmount;
284 TransactionRaw rawTransaction;
285 if (isNativeTransaction) {
286 if (sendAll) {
@@ -300,12 +293,10 @@ class TronClient {
293 if (availableBandWidth >= 269) {
294 totalAmount = amount;
295 } else {
303 - final amountInSun = TronHelper.toSun(amount).toInt();
304 -
296 // 5000 added here is a buffer since we're working with "estimated" value of the fee.
306 - final result = amountInSun - (_nativeTxEstimatedFee + 5000);
297 + final result = amount.amount - BigInt.from(_nativeTxEstimatedFee + 5000);
298
308 - totalAmount = TronHelper.fromSun(BigInt.from(result));
299 + totalAmount = amount.copyWith(amount: result);
300 }
301 } else {
302 totalAmount = amount;
@@ -318,7 +309,7 @@ class TronClient {
309 sendAll,
310 );
311 } else {
321 - final tokenAddress = (currency as TronToken).contractAddress;
312 + final tokenAddress = (amount.currency as TronToken).contractAddress;
313 totalAmount = amount;
314 rawTransaction = await _signTrcTokenTransaction(
315 ownerAddress,
@@ -339,7 +330,7 @@ class TronClient {
330 return PendingTronTransaction(
331 signedTransaction: signature,
332 amount: totalAmount,
342 - fee: TronHelper.fromSun(rawTransaction.feeLimit ?? BigInt.zero),
333 + fee: Money(rawTransaction.feeLimit ?? BigInt.zero, CryptoCurrency.trx),
334 sendTransaction: sendTx,
335 id: rawTransaction.txID
336 );
@@ -348,7 +339,7 @@ class TronClient {
339 Future<TransactionRaw> _signNativeTransaction(
340 TronAddress ownerAddress,
341 TronAddress receiverAddress,
351 - String amount,
342 + Money amount,
343 BigInt tronBalance,
344 bool sendAll,
345 ) async {
@@ -359,7 +350,7 @@ class TronClient {
350 final block = await _provider!.request(TronRequestGetNowBlock());
351 // Create the transfer contract
352 final contract = TransferContract(
362 - amount: TronHelper.toSun(amount),
353 + amount: amount.amount,
354 ownerAddress: ownerAddress,
355 toAddress: receiverAddress,
356 );
@@ -374,8 +365,7 @@ class TronClient {
365 // Set the transaction expiration time (maximum 24 hours)
366 final expireTime = DateTime.now().add(const Duration(minutes: 30));
367
377 - // Create a raw transaction
378 - TransactionRaw rawTransaction = TransactionRaw(
368 + final rawTransaction = TransactionRaw(
369 refBlockBytes: block.blockHeader.rawData.refBlockBytes,
370 refBlockHash: block.blockHeader.rawData.refBlockHash,
371 expiration: BigInt.from(expireTime.millisecondsSinceEpoch),
@@ -394,45 +384,31 @@ class TronClient {
384 );
385 }
386
397 - rawTransaction = rawTransaction.copyWith(
398 - feeLimit: BigInt.from(feeLimitToUse),
399 - );
400 -
401 - return rawTransaction;
387 + return rawTransaction.copyWith(feeLimit: BigInt.from(feeLimitToUse));
388 }
389
390 Future<TransactionRaw> _signTrcTokenTransaction(
391 TronAddress ownerAddress,
392 TronAddress receiverAddress,
407 - String amount,
393 + Money amount,
394 String contractAddress,
395 BigInt tronBalance,
396 ) async {
397 final contract = ContractABI.fromJson(trc20Abi, isTron: true);
398
399 final function = contract.functionFromName("transfer");
414 -
415 - /// address /// amount
416 - final transferparams = [
417 - receiverAddress,
418 - TronHelper.toSun(amount),
419 - ];
420 -
421 - final contractAddr = TronAddress(contractAddress);
422 -
400 + final transferParams = [receiverAddress, amount.amount];
401 final request = await _provider!.request(
402 TronRequestTriggerConstantContract(
403 ownerAddress: ownerAddress,
426 - contractAddress: contractAddr,
427 - data: function.encodeHex(transferparams),
404 + contractAddress: TronAddress(contractAddress),
405 + data: function.encodeHex(transferParams),
406 ),
407 );
408
409 if (!request.isSuccess) {
410 log("Tron TRC20 error: ${request.error} \n ${request.respose}");
433 - throw Exception(
434 - 'An error occurred while creating the transfer request. Please try again.',
435 - );
411 + throw Exception('An error occurred while creating the transfer request. Please try again.');
412 }
413
414 final feeLimit = await getFeeLimit(
@@ -442,20 +418,14 @@ class TronClient {
418 energyUsed: request.energyUsed ?? 0,
419 );
420
445 - final tronBalanceInt = tronBalance.toInt();
446 -
447 - if (feeLimit > tronBalanceInt) {
421 + if (feeLimit > tronBalance.toInt()) {
422 final feeInTrx = TronHelper.fromSun(BigInt.parse(feeLimit.toString()));
423 throw Exception(
424 'You don\'t have enough TRX to cover the transaction fee for this transaction. Please top up. Transaction fee: $feeInTrx TRX',
425 );
426 }
427
454 - final rawTransaction = request.transactionRaw!.copyWith(
455 - feeLimit: BigInt.from(feeLimit),
456 - );
457 -
458 - return rawTransaction;
428 + return request.transactionRaw!.copyWith(feeLimit: BigInt.from(feeLimit));
429 }
430
431 Future<String> sendTransaction({
@@ -480,20 +450,18 @@ class TronClient {
450 }
451 }
452
483 - Future<TronBalance> fetchTronTokenBalances(String userAddress, String contractAddress, {bool throwOnError = false}) async {
453 + Future<TronBalance> fetchTronTokenBalances(String userAddress, String contractAddress,
454 + {bool throwOnError = false, required Currency currency}) async {
455 try {
456 final ownerAddress = TronAddress(userAddress);
457
487 - final tokenAddress = TronAddress(contractAddress);
488 -
458 final contract = ContractABI.fromJson(trc20Abi, isTron: true);
490 -
459 final function = contract.functionFromName("balanceOf");
460
461 final request = await _provider!.request(
462 TronRequestTriggerConstantContract.fromMethod(
463 ownerAddress: ownerAddress,
496 - contractAddress: tokenAddress,
464 + contractAddress: TronAddress(contractAddress),
465 function: function,
466 params: [ownerAddress],
467 ),
@@ -501,12 +469,12 @@ class TronClient {
469
470 final outputResult = request.outputResult?.first ?? BigInt.zero;
471
504 - return TronBalance(outputResult);
472 + return TronBalance(Money(outputResult, currency));
473 } catch (_) {
474 if (throwOnError) {
475 rethrow;
476 }
509 - return TronBalance(BigInt.zero);
477 + return TronBalance(Money.zero(currency));
478 }
479 }
480
cw_tron/lib/tron_transaction_info.dart
+33 -40
@@ -1,41 +1,55 @@
1 -import 'package:cw_core/format_amount.dart';
1 +import 'package:cw_core/amount/money.dart';
2 +import 'package:cw_core/crypto_currency.dart';
3 import 'package:cw_core/transaction_direction.dart';
4 import 'package:cw_core/transaction_info.dart';
4 -import 'package:on_chain/on_chain.dart' as onchain;
5 import 'package:on_chain/tron/tron.dart';
6
7 class TronTransactionInfo extends TransactionInfo {
8 TronTransactionInfo({
9 required this.id,
10 - required this.tronAmount,
11 - required this.txFee,
10 + required this.amount,
11 + required this.fee,
12 required this.direction,
13 required this.blockTime,
14 required this.to,
15 required this.from,
16 required this.isPending,
17 - this.tokenSymbol = 'TRX',
18 - }) : amount = tronAmount.toInt();
17 + });
18
19 + @override
20 final String id;
21 +
22 + @override
23 final String? to;
24 +
25 + @override
26 final String? from;
23 - final int amount;
24 - final BigInt tronAmount;
25 - final String tokenSymbol;
26 - final DateTime blockTime;
27 +
28 + @override
29 + final Money amount;
30 +
31 + @override
32 + final Money? fee;
33 +
34 + @override
35 final bool isPending;
28 - final int? txFee;
36 +
37 + @override
38 final TransactionDirection direction;
39
40 + final DateTime blockTime;
41 +
42 factory TronTransactionInfo.fromJson(Map<String, dynamic> data) {
43 + final tokenSymbol = data['tokenSymbol'] as String;
44 + final decimals = data['decimals'] as int? ?? CryptoCurrency.trx.decimals;
45 + final currency = CryptoCurrency(name: tokenSymbol, title: tokenSymbol, decimals: decimals);
46 +
47 return TronTransactionInfo(
48 id: data['id'] as String,
34 - tronAmount: BigInt.parse(data['tronAmount']),
35 - txFee: data['txFee'],
49 + amount: Money(BigInt.parse(data['tronAmount']), currency),
50 + fee: Money.tryParse(data['txFee']?.toString() ?? '0', CryptoCurrency.trx, isBaseUnit: true),
51 direction: parseTransactionDirectionFromInt(data['direction'] as int),
52 blockTime: DateTime.fromMillisecondsSinceEpoch(data['blockTime'] as int),
38 - tokenSymbol: data['tokenSymbol'] as String,
53 to: data['to'],
54 from: data['from'],
55 isPending: data['isPending'],
@@ -44,41 +58,20 @@ class TronTransactionInfo extends TransactionInfo {
58
59 Map<String, dynamic> toJson() => {
60 'id': id,
47 - 'tronAmount': tronAmount.toString(),
48 - 'txFee': txFee,
61 + 'tronAmount': amount.amount.toString(),
62 + 'txFee': fee?.amount.toString(),
63 'direction': direction.index,
64 'blockTime': blockTime.millisecondsSinceEpoch,
51 - 'tokenSymbol': tokenSymbol,
65 'to': to,
66 'from': from,
67 'isPending': isPending,
68 + 'tokenSymbol': amount.currency.symbol,
69 + 'decimals': amount.currency.decimals
70 };
71
72 @override
73 DateTime get date => blockTime;
74
60 - String? _fiatAmount;
61 -
62 - @override
63 - String amountFormatted() {
64 - String formattedAmount = _rawAmountAsString(tronAmount);
65 -
66 - return '$formattedAmount $tokenSymbol';
67 - }
68 -
69 - @override
70 - String fiatAmount() => _fiatAmount ?? '';
71 -
72 - @override
73 - void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
74 -
75 - @override
76 - String feeFormatted() {
77 - final formattedFee = onchain.TronHelper.fromSun(BigInt.from(txFee ?? 0));
78 -
79 - return '$formattedFee TRX';
80 - }
81 -
75 String _rawAmountAsString(BigInt amount) {
76 String formattedAmount = TronHelper.fromSun(amount);
77
@@ -89,5 +82,5 @@ class TronTransactionInfo extends TransactionInfo {
82 return formattedAmount;
83 }
84
92 - String rawTronAmount() => _rawAmountAsString(tronAmount);
85 + String rawTronAmount() => _rawAmountAsString(amount.amount);
86 }
cw_tron/lib/tron_transaction_model.dart
+7
@@ -1,4 +1,5 @@
1 import 'package:blockchain_utils/hex/hex.dart';
2 +import 'package:cw_core/crypto_currency.dart';
3 import 'package:on_chain/on_chain.dart';
4
5 class TronTRC20TransactionModel extends TronTransactionModel {
@@ -6,6 +7,11 @@ class TronTRC20TransactionModel extends TronTransactionModel {
7
8 String? tokenSymbol;
9
10 + int? decimals;
11 +
12 + CryptoCurrency get currency => CryptoCurrency(
13 + name: tokenSymbol ?? "TRX", title: tokenSymbol ?? "TRX", decimals: decimals ?? 6);
14 +
15 int? timestamp;
16
17 @override
@@ -40,6 +46,7 @@ class TronTRC20TransactionModel extends TronTransactionModel {
46 TronTRC20TransactionModel.fromJson(Map<String, dynamic> json) {
47 transactionId = json['transaction_id'];
48 tokenSymbol = json['token_info'] != null ? json['token_info']['symbol'] : null;
49 + decimals = json['token_info'] != null ? json['token_info']['decimals'] : null;
50 timestamp = json['block_timestamp'];
51 from = json['from'];
52 to = json['to'];
cw_tron/lib/tron_wallet.dart
+44 -44
@@ -5,6 +5,7 @@ import 'dart:io';
5
6 import 'package:bip39/bip39.dart' as bip39;
7 import 'package:blockchain_utils/blockchain_utils.dart';
8 +import 'package:cw_core/amount/money.dart';
9 import 'package:cw_core/cake_hive.dart';
10 import 'package:cw_core/crypto_currency.dart';
11 import 'package:cw_core/encryption_file_utils.dart';
@@ -56,7 +57,7 @@ abstract class TronWalletBase
57 _client = TronClient(),
58 walletAddresses = TronWalletAddresses(walletInfo),
59 balance = ObservableMap<CryptoCurrency, TronBalance>.of(
59 - {CryptoCurrency.trx: initialBalance ?? TronBalance(BigInt.zero)},
60 + {CryptoCurrency.trx: initialBalance ?? TronBalance(Money.zero(CryptoCurrency.trx))},
61 ),
62 super(walletInfo, derivationInfo) {
63 this.walletInfo = walletInfo;
@@ -93,10 +94,10 @@ abstract class TronWalletBase
94 WalletAddresses walletAddresses;
95
96 @observable
96 - String? nativeTxEstimatedFee;
97 + Money? nativeTxEstimatedFee;
98
99 @observable
99 - String? trc20EstimatedFee;
100 + Money? trc20EstimatedFee;
101
102 @override
103 @observable
@@ -145,7 +146,8 @@ abstract class TronWalletBase
146 if (!hasKeysFile) rethrow;
147 }
148
148 - final balance = TronBalance.fromJSON(data?['balance'] as String?) ?? TronBalance(BigInt.zero);
149 + final balance = TronBalance.fromJSON(data?['balance'] as String?, CryptoCurrency.trx) ??
150 + TronBalance(Money.zero(CryptoCurrency.trx));
151
152 final WalletKeysData keysData;
153 // Migrate wallet from the old scheme to then new .keys file scheme
@@ -253,10 +255,10 @@ abstract class TronWalletBase
255
256 Future<void> _getEstimatedFees() async {
257 final nativeFee = await _getNativeTxFee();
256 - nativeTxEstimatedFee = TronHelper.fromSun(BigInt.from(nativeFee));
258 + nativeTxEstimatedFee = Money.fromInt(nativeFee, currency);
259
260 final trc20Fee = await _getTrc20TxFee();
259 - trc20EstimatedFee = TronHelper.fromSun(BigInt.from(trc20Fee));
261 + trc20EstimatedFee = Money.fromInt(trc20Fee, currency);
262
263 log('Native Estimated Fee: $nativeTxEstimatedFee');
264 log('TRC20 Estimated Fee: $trc20EstimatedFee');
@@ -321,21 +323,11 @@ abstract class TronWalletBase
323
324 final walletBalanceForCurrency = balance[transactionCurrency]!.balance;
325
324 - BigInt totalAmount = BigInt.zero;
325 - bool shouldSendAll = false;
326 + var totalAmount = Money.zero(transactionCurrency);
327 + var shouldSendAll = false;
328 if (hasMultiDestination) {
327 - if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
329 + // Tron does not have multi Destination right now
330 throw TronTransactionCreationException(transactionCurrency);
329 - }
330 -
331 - final totalAmountFromCredentials =
332 - outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
333 -
334 - totalAmount = BigInt.from(totalAmountFromCredentials);
335 -
336 - if (walletBalanceForCurrency < totalAmount) {
337 - throw TronTransactionCreationException(transactionCurrency);
338 - }
331 } else {
332 final output = outputs.first;
333
@@ -344,25 +336,23 @@ abstract class TronWalletBase
336 if (shouldSendAll) {
337 totalAmount = walletBalanceForCurrency;
338 } else {
347 - final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
348 - totalAmount = TronHelper.toSun(totalOriginalAmount.toString());
339 + totalAmount = output.cryptoAmount.copyWith(currency: transactionCurrency);
340 }
341
351 - if (walletBalanceForCurrency < totalAmount || totalAmount < BigInt.zero) {
342 + if (walletBalanceForCurrency < totalAmount || totalAmount < Money.zero(transactionCurrency)) {
343 throw TronTransactionCreationException(transactionCurrency);
344 }
345 }
346
356 - final tronBalance = balance[CryptoCurrency.trx]?.balance ?? BigInt.zero;
347 + final tronBalance = balance[CryptoCurrency.trx]?.balance ?? Money.zero(CryptoCurrency.trx);
348
349 final pendingTransaction = await _client.signTransaction(
350 ownerPrivKey: _tronPrivateKey,
351 toAddress: tronCredentials.outputs.first.isParsedAddress
352 ? tronCredentials.outputs.first.extractedAddress!
353 : tronCredentials.outputs.first.address,
363 - amount: TronHelper.fromSun(totalAmount),
364 - currency: transactionCurrency,
365 - tronBalance: tronBalance,
354 + amount: totalAmount,
355 + tronBalance: tronBalance.amount,
356 sendAll: shouldSendAll,
357 );
358
@@ -394,33 +384,41 @@ abstract class TronWalletBase
384 continue;
385 }
386
397 - // Filter out spam transaactions that involve receiving TRC10 assets transaction, we deal with TRX and TRC20 transactions
387 + // Filter out spam transactions that involve receiving TRC10 assets transaction, we deal with TRX and TRC20 transactions
388 if (transactionModel.contracts?.first.type == "TransferAssetContract") {
389 continue;
390 }
391
402 - String? tokenSymbol;
392 + var txCurrency = currency;
393 if (transactionModel.contractAddress != null) {
394 final tokenAddress = TronAddress(transactionModel.contractAddress!);
395
406 - tokenSymbol = (await _client.getTokenDetail(
396 + final tokenSymbol = (await _client.getTokenDetail(
397 contract,
398 "symbol",
399 ownerAddress,
400 tokenAddress,
401 ) as String?) ??
402 '';
403 +
404 + final decimals = (await _client.getTokenDetail(
405 + contract,
406 + "decimals",
407 + ownerAddress,
408 + tokenAddress,
409 + ) as BigInt?)?.toInt() ?? txCurrency.decimals;
410 +
411 + txCurrency = CryptoCurrency(name: tokenSymbol, title: tokenSymbol, decimals: decimals);
412 }
413
414 result[transactionModel.hash] = TronTransactionInfo(
415 id: transactionModel.hash,
417 - tronAmount: transactionModel.amount ?? BigInt.zero,
416 + amount: Money(transactionModel.amount ?? BigInt.zero, txCurrency),
417 direction: TronAddress(transactionModel.from!, visible: false).toAddress() == address
418 ? TransactionDirection.outgoing
419 : TransactionDirection.incoming,
420 blockTime: transactionModel.date,
422 - txFee: transactionModel.fee,
423 - tokenSymbol: tokenSymbol ?? "TRX",
421 + fee: transactionModel.fee != null ? Money.fromInt(transactionModel.fee!, currency) : null,
422 to: transactionModel.to,
423 from: transactionModel.from,
424 isPending: false,
@@ -441,20 +439,19 @@ abstract class TronWalletBase
439
440 final Map<String, TronTransactionInfo> result = {};
441
444 - for (var transactionModel in transactions) {
442 + for (final transactionModel in transactions) {
443 if (transactionHistory.transactions.containsKey(transactionModel.hash)) {
444 continue;
445 }
446
447 result[transactionModel.hash] = TronTransactionInfo(
448 id: transactionModel.hash,
451 - tronAmount: transactionModel.amount ?? BigInt.zero,
449 + amount: Money(transactionModel.amount ?? BigInt.zero, transactionModel.currency),
450 direction: transactionModel.from! == address
451 ? TransactionDirection.outgoing
452 : TransactionDirection.incoming,
453 blockTime: transactionModel.date,
456 - txFee: transactionModel.fee,
457 - tokenSymbol: transactionModel.tokenSymbol ?? "TRX",
454 + fee: transactionModel.fee != null ? Money.fromInt(transactionModel.fee!, currency) : null,
455 to: transactionModel.to,
456 from: transactionModel.from,
457 isPending: false,
@@ -514,7 +511,7 @@ abstract class TronWalletBase
511
512 Future<TronBalance> _fetchTronBalance() async {
513 final balance = await _client.getBalance(_tronPublicKey.toAddress());
517 - return TronBalance(balance);
514 + return TronBalance(Money(balance, CryptoCurrency.trx));
515 }
516
517 Future<void> _fetchTronTokenBalances() async {
@@ -524,6 +521,7 @@ abstract class TronWalletBase
521 balance[token] = await _client.fetchTronTokenBalances(
522 _tronAddress,
523 token.contractAddress,
524 + currency: token,
525 );
526 } else {
527 balance.remove(token);
@@ -542,8 +540,9 @@ abstract class TronWalletBase
540 await _client.getBalance(_tronPublicKey.toAddress(), throwOnError: true);
541
542 // Check USDT token balance
545 - const usdtContractAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
546 - await _client.fetchTronTokenBalances(_tronAddress, usdtContractAddress, throwOnError: true);
543 + final usdtContractAddress = DefaultTronTokens().usdt.contractAddress;
544 + await _client.fetchTronTokenBalances(_tronAddress, usdtContractAddress,
545 + throwOnError: true, currency: CryptoCurrency.usdttrc20);
546
547 return true;
548 } catch (e) {
@@ -582,6 +581,7 @@ abstract class TronWalletBase
581 balance[newToken] = await _client.fetchTronTokenBalances(
582 _tronAddress,
583 newToken.contractAddress,
584 + currency: token
585 );
586 } else {
587 balance.remove(newToken);
@@ -599,7 +599,8 @@ abstract class TronWalletBase
599 }
600
601 Future<void> _removeTokenTransactionsInHistory(TronToken token) async {
602 - transactionHistory.transactions.removeWhere((key, value) => value.tokenSymbol == token.title);
602 + transactionHistory.transactions
603 + .removeWhere((key, value) => value.amount.currency.symbol == token.title);
604 await transactionHistory.save();
605 }
606
@@ -649,10 +650,9 @@ abstract class TronWalletBase
650
651 @override
652 Future<bool> verifyMessage(String message, String signature, {String? address}) async {
652 - if (address == null) {
653 - return false;
654 - }
655 - TronPublicKey pubKey = TronPublicKey.fromPersonalSignature(ascii.encode(message), signature)!;
653 + if (address == null) return false;
654 +
655 + final pubKey = TronPublicKey.fromPersonalSignature(ascii.encode(message), signature)!;
656 return pubKey.toAddress().toString() == address;
657 }
658
cw_wownero/lib/pending_wownero_transaction.dart
+7 -12
@@ -1,8 +1,7 @@
1 +import 'package:cw_core/amount/money.dart';
2 import 'package:cw_wownero/api/structs/pending_transaction.dart';
2 -import 'package:cw_wownero/api/transaction_history.dart'
3 - as wownero_transaction_history;
3 +import 'package:cw_wownero/api/transaction_history.dart' as wownero_transaction_history;
4 import 'package:cw_core/crypto_currency.dart';
5 -import 'package:cw_core/amount_converter.dart';
5
6 import 'package:cw_core/pending_transaction.dart';
7
@@ -19,6 +18,10 @@ class PendingWowneroTransaction with PendingTransaction {
18
19 final PendingTransactionDescription pendingTransactionDescription;
20
21 + Money get amount => Money.fromInt(pendingTransactionDescription.amount, CryptoCurrency.wow);
22 +
23 + Money get fee => Money.fromInt(pendingTransactionDescription.fee, CryptoCurrency.wow);
24 +
25 @override
26 String get id => pendingTransactionDescription.hash;
27
@@ -28,15 +31,7 @@ class PendingWowneroTransaction with PendingTransaction {
31 String get txKey => pendingTransactionDescription.txKey;
32
33 @override
31 - String get amountFormatted =>
32 - AmountConverter.amountIntToString(CryptoCurrency.wow, pendingTransactionDescription.amount);
33 -
34 - @override
35 - String get feeFormatted => "$feeFormattedValue WOW";
36 -
37 - @override
38 - String get feeFormattedValue =>
39 - AmountConverter.amountIntToString(CryptoCurrency.wow, pendingTransactionDescription.fee);
34 + String get amountFormatted => amount.toString();
35
36 @override
37 Future<void> commit() async {
cw_wownero/lib/wownero_balance.dart new
+17
@@ -0,0 +1,17 @@
1 +import 'package:cw_core/amount/money.dart';
2 +import 'package:cw_core/balance.dart';
3 +import 'package:cw_core/crypto_currency.dart';
4 +
5 +class WowneroBalance extends Balance {
6 + WowneroBalance({
7 + required this.fullBalance,
8 + required Money unlockedBalance,
9 + Money? frozen,
10 + }) : super(
11 + unlockedBalance,
12 + fullBalance - unlockedBalance,
13 + frozen: frozen ?? Money.zero(CryptoCurrency.wow),
14 + );
15 +
16 + final Money fullBalance;
17 +}
cw_wownero/lib/wownero_transaction_info.dart
+12 -16
@@ -1,3 +1,5 @@
1 +import 'package:cw_core/amount/money.dart';
2 +import 'package:cw_core/crypto_currency.dart';
3 import 'package:cw_core/transaction_info.dart';
4 import 'package:cw_core/wownero_amount_format.dart';
5 import 'package:cw_core/parseBoolFromString.dart';
@@ -21,18 +23,18 @@ class WowneroTransactionInfo extends TransactionInfo {
23 date = DateTime.fromMillisecondsSinceEpoch(
24 (int.tryParse(map['timestamp'] as String? ?? '') ?? 0) * 1000),
25 isPending = parseBoolFromString(map['isPending'] as String),
24 - amount = map['amount'] as int,
26 + amount = Money.fromInt(map['amount'] as int, CryptoCurrency.wow),
27 accountIndex = int.parse(map['accountIndex'] as String),
28 addressIndex = map['addressIndex'] as int,
29 confirmations = map['confirmations'] as int,
30 key = getTxKey((map['hash'] ?? '') as String),
29 - fee = map['fee'] as int? ?? 0 {
30 - additionalInfo = <String, dynamic>{
31 - 'key': key,
32 - 'accountIndex': accountIndex,
33 - 'addressIndex': addressIndex
34 - };
35 - }
31 + fee = Money.fromInt(map['fee'] as int? ?? 0, CryptoCurrency.wow) {
32 + additionalInfo = <String, dynamic>{
33 + 'key': key,
34 + 'accountIndex': accountIndex,
35 + 'addressIndex': addressIndex
36 + };
37 + }
38
39 final String id;
40 final String txHash;
@@ -41,23 +43,17 @@ class WowneroTransactionInfo extends TransactionInfo {
43 final DateTime date;
44 final int accountIndex;
45 final bool isPending;
44 - final int amount;
45 - final int fee;
46 + final Money amount;
47 + final Money fee;
48 final int addressIndex;
49 final int confirmations;
50 String? recipientAddress;
51 String? key;
52 String? _fiatAmount;
53
52 - @override
53 - String amountFormatted() => '${formatAmount(wowneroAmountToString(amount: amount))} WOW';
54 -
54 @override
55 String fiatAmount() => _fiatAmount ?? '';
56
57 @override
58 void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
60 -
61 - @override
62 - String feeFormatted() => '${formatAmount(wowneroAmountToString(amount: fee))} WOW';
59 }
cw_wownero/lib/wownero_wallet.dart
+37 -26
@@ -4,6 +4,7 @@ import 'dart:io';
4 import 'dart:isolate';
5
6 import 'package:cw_core/account.dart';
7 +import 'package:cw_core/amount/money.dart';
8 import 'package:cw_core/crypto_currency.dart';
9 import 'package:cw_core/monero_transaction_priority.dart';
10 import 'package:cw_core/monero_wallet_keys.dart';
@@ -20,7 +21,6 @@ import 'package:cw_core/utils/proxy_wrapper.dart';
21 import 'package:cw_core/wallet_base.dart';
22 import 'package:cw_core/wallet_info.dart';
23 import 'package:cw_core/wownero_amount_format.dart';
23 -import 'package:cw_core/wownero_balance.dart';
24 import 'package:cw_wownero/api/account_list.dart';
25 import 'package:cw_wownero/api/coins_info.dart';
26 import 'package:cw_wownero/api/structs/pending_transaction.dart';
@@ -31,6 +31,7 @@ import 'package:cw_wownero/api/wownero_output.dart';
31 import 'package:cw_wownero/exceptions/wownero_transaction_creation_exception.dart';
32 import 'package:cw_wownero/exceptions/wownero_transaction_no_inputs_exception.dart';
33 import 'package:cw_wownero/pending_wownero_transaction.dart';
34 +import 'package:cw_wownero/wownero_balance.dart';
35 import 'package:cw_wownero/wownero_transaction_creation_credentials.dart';
36 import 'package:cw_wownero/wownero_transaction_history.dart';
37 import 'package:cw_wownero/wownero_transaction_info.dart';
@@ -56,8 +57,11 @@ abstract class WowneroWalletBase
57 {required WalletInfo walletInfo, required DerivationInfo derivationInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo, required String password})
58 : balance = ObservableMap<CryptoCurrency, WowneroBalance>.of({
59 CryptoCurrency.wow: WowneroBalance(
59 - fullBalance: wownero_wallet.getFullBalance(accountIndex: 0),
60 - unlockedBalance: wownero_wallet.getFullBalance(accountIndex: 0))
60 + fullBalance:
61 + Money.fromInt(wownero_wallet.getFullBalance(accountIndex: 0), CryptoCurrency.wow),
62 + unlockedBalance:
63 + Money.fromInt(wownero_wallet.getFullBalance(accountIndex: 0), CryptoCurrency.wow),
64 + )
65 }),
66 _isTransactionUpdating = false,
67 _hasSyncAfterStartup = false,
@@ -75,8 +79,11 @@ abstract class WowneroWalletBase
79
80 balance = ObservableMap<CryptoCurrency, WowneroBalance>.of(<CryptoCurrency, WowneroBalance>{
81 currency: WowneroBalance(
78 - fullBalance: wownero_wallet.getFullBalance(accountIndex: account.id),
79 - unlockedBalance: wownero_wallet.getUnlockedBalance(accountIndex: account.id))
82 + fullBalance: Money.fromInt(
83 + wownero_wallet.getFullBalance(accountIndex: account.id), CryptoCurrency.wow),
84 + unlockedBalance: Money.fromInt(
85 + wownero_wallet.getUnlockedBalance(accountIndex: account.id), CryptoCurrency.wow),
86 + )
87 });
88 _updateSubAddress(isEnabledAutoGenerateSubaddress, account: account);
89 _askForUpdateTransactionHistory();
@@ -152,9 +159,13 @@ abstract class WowneroWalletBase
159 await walletAddresses.init();
160 balance = ObservableMap<CryptoCurrency, WowneroBalance>.of(<CryptoCurrency, WowneroBalance>{
161 currency: WowneroBalance(
155 - fullBalance: wownero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id),
156 - unlockedBalance:
157 - wownero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id))
162 + fullBalance: Money.fromInt(
163 + wownero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id),
164 + CryptoCurrency.wow),
165 + unlockedBalance: Money.fromInt(
166 + wownero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id),
167 + CryptoCurrency.wow),
168 + )
169 });
170 _setListeners();
171 await updateTransactions();
@@ -290,13 +301,12 @@ abstract class WowneroWalletBase
301 final spendAllCoins = inputs.length == unspentCoins.length;
302
303 if (hasMultiDestination) {
293 - if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
304 + if (outputs.any((item) => item.sendAll || item.cryptoAmount.amount <= BigInt.zero)) {
305 throw WowneroTransactionCreationException(
306 'You do not have enough WOW to send this amount.');
307 }
308
298 - final int totalAmount =
299 - outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
309 + final totalAmount = outputs.fold(0, (acc, value) => acc + value.cryptoAmount.amount.toInt());
310
311 final estimatedFee = calculateEstimatedFee(_credentials.priority, totalAmount);
312 if (unlockedBalance < totalAmount) {
@@ -311,8 +321,7 @@ abstract class WowneroWalletBase
321 final wowneroOutputs = outputs.map((output) {
322 final outputAddress = output.isParsedAddress ? output.extractedAddress : output.address;
323
314 - return WowneroOutput(
315 - address: outputAddress!, amount: output.cryptoAmount!.replaceAll(',', '.'));
324 + return WowneroOutput(address: outputAddress!, amount: output.cryptoAmount.toString());
325 }).toList();
326
327 pendingTransactionDescription = await transaction_history.createTransactionMultDest(
@@ -323,8 +332,8 @@ abstract class WowneroWalletBase
332 } else {
333 final output = outputs.first;
334 final address = output.isParsedAddress ? output.extractedAddress : output.address;
326 - final amount = output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.');
327 - final formattedAmount = output.sendAll ? null : output.formattedCryptoAmount;
335 + final amount = output.sendAll ? null : output.cryptoAmount.toString();
336 + final formattedAmount = output.sendAll ? null : output.cryptoAmount.amount.toInt();
337
338 if ((formattedAmount != null && unlockedBalance < formattedAmount) ||
339 (formattedAmount == null && unlockedBalance <= 0)) {
@@ -635,10 +644,10 @@ abstract class WowneroWalletBase
644 row.isSpend ? TransactionDirection.outgoing : TransactionDirection.incoming,
645 row.timeStamp,
646 row.isPending,
638 - row.amount,
647 + Money.fromInt(row.amount, CryptoCurrency.wow),
648 row.accountIndex,
649 0,
641 - row.fee,
650 + Money.fromInt(row.fee, CryptoCurrency.wow),
651 row.confirmations,
652 )..additionalInfo = <String, dynamic>{
653 'key': row.key,
@@ -706,29 +715,31 @@ abstract class WowneroWalletBase
715 final frozenBalance = _getFrozenBalance();
716
717 if (balance[currency]!.fullBalance != fullBalance ||
709 - balance[currency]!.unlockedBalance != unlockedBalance ||
710 - balance[currency]!.frozenBalance != frozenBalance) {
718 + balance[currency]!.available != unlockedBalance ||
719 + balance[currency]!.frozen != frozenBalance) {
720 balance[currency] = WowneroBalance(
712 - fullBalance: fullBalance, unlockedBalance: unlockedBalance, frozenBalance: frozenBalance);
721 + fullBalance: fullBalance, unlockedBalance: unlockedBalance, frozen: frozenBalance);
722 }
723 }
724
725 Future<void> _askForUpdateTransactionHistory() async => await updateTransactions();
726
718 - int _getFullBalance() => wownero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id);
727 + Money _getFullBalance() => Money.fromInt(
728 + wownero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id), CryptoCurrency.wow);
729
720 - int _getUnlockedBalance() =>
721 - wownero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id);
730 + Money _getUnlockedBalance() => Money.fromInt(
731 + wownero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id),
732 + CryptoCurrency.wow);
733
723 - int _getFrozenBalance() {
734 + Money _getFrozenBalance() {
735 var frozenBalance = 0;
736
726 - for (var coin in unspentCoinsInfo.values.where((element) =>
737 + for (final coin in unspentCoinsInfo.values.where((element) =>
738 element.walletId == id && element.accountIndex == walletAddresses.account!.id)) {
739 if (coin.isFrozen) frozenBalance += coin.value;
740 }
741
731 - return frozenBalance;
742 + return Money.fromInt(frozenBalance, CryptoCurrency.wow);
743 }
744
745 void _onNewBlock(int height, int blocksLeft, double ptc) async {
cw_zano/lib/api/model/transfer.dart
+5 -2
@@ -1,3 +1,5 @@
1 +import 'package:cw_core/amount/money.dart';
2 +import 'package:cw_core/crypto_currency.dart';
3 import 'package:cw_core/utils/print_verbose.dart';
4 import 'package:cw_core/zano_asset.dart';
5 import 'package:cw_zano/api/model/employed_entries.dart';
@@ -113,7 +115,8 @@ class Transfer {
115 confirmations: currentDaemonHeight - transfer.height,
116 isIncome: single.isIncome,
117 assetId: single.assetId,
116 - amount: single.amount,
118 + amount: Money(single.amount,
119 + asset ?? CryptoCurrency(title: ticker, name: ticker, decimals: decimalPoint)),
120 tokenSymbol: isSimple ? ticker : '*${ticker}',
121 decimalPoint: decimalPoint,
122 );
@@ -124,7 +127,7 @@ class Transfer {
127 confirmations: currentDaemonHeight - transfer.height,
128 isIncome: single.isIncome,
129 assetId: single.assetId,
127 - amount: amount,
130 + amount: Money(amount, CryptoCurrency.zano),
131 tokenSymbol: isSimple ? 'ZANO' : '*ZANO',
132 );
133 },
cw_zano/lib/model/pending_zano_transaction.dart
+8 -16
@@ -1,7 +1,7 @@
1 +import 'package:cw_core/amount/money.dart';
2 import 'package:cw_core/pending_transaction.dart';
3 import 'package:cw_zano/api/model/destination.dart';
4 import 'package:cw_zano/api/model/transfer_result.dart';
4 -import 'package:cw_zano/zano_formatter.dart';
5 import 'package:cw_zano/zano_wallet.dart';
6
7 class PendingZanoTransaction with PendingTransaction {
@@ -11,45 +11,37 @@ class PendingZanoTransaction with PendingTransaction {
11 required this.fee,
12 required this.comment,
13 required this.assetId,
14 - required this.ticker,
15 - this.decimalPoint = ZanoFormatter.defaultDecimalPoint,
14 required this.amount,
15 });
16
17 final ZanoWalletBase zanoWallet;
18 final List<Destination> destinations;
21 - final BigInt fee;
19 final String comment;
20 final String assetId;
24 - final String ticker;
25 - final int decimalPoint;
26 - final BigInt amount;
21
22 @override
29 - String get id => transferResult?.txHash ?? '';
23 + final Money amount;
24
25 @override
32 - String get hex => '';
26 + final Money fee;
27
28 @override
35 - String get amountFormatted => ZanoFormatter.bigIntAmountToString(amount, decimalPoint);
29 + String get id => transferResult?.txHash ?? '';
30
31 @override
38 - String get feeFormatted => "$feeFormattedValue ZANO";
32 + String get hex => '';
33
34 @override
41 - String get feeFormattedValue => ZanoFormatter.bigIntAmountToString(fee);
35 + String get amountFormatted => amount.toString();
36
37 TransferResult? transferResult;
38
39 @override
40 Future<void> commit() async {
47 - transferResult = await zanoWallet.transfer(destinations, fee, comment);
41 + transferResult = await zanoWallet.transfer(destinations, fee.amount, comment);
42 zanoWallet.fetchTransactions();
43 }
44
45 @override
52 - Future<Map<String, String>> commitUR() {
53 - throw UnimplementedError();
54 - }
46 + Future<Map<String, String>> commitUR() => throw UnimplementedError();
47 }
cw_zano/lib/model/zano_balance.dart
+9 -11
@@ -1,17 +1,15 @@
1 +import 'package:cw_core/amount/money.dart';
2 import 'package:cw_core/balance.dart';
2 -import 'package:cw_zano/zano_formatter.dart';
3 +import 'package:cw_core/currency.dart';
4
5 class ZanoBalance extends Balance {
5 - final BigInt total;
6 - final BigInt unlocked;
7 - final int decimalPoint;
8 - ZanoBalance({required this.total, required this.unlocked, this.decimalPoint = ZanoFormatter.defaultDecimalPoint}) : super(unlocked, (total - unlocked));
6 + final Money total;
7 + final Money unlocked;
8
10 - ZanoBalance.empty({this.decimalPoint = ZanoFormatter.defaultDecimalPoint}): total = BigInt.zero, unlocked = BigInt.zero, super(BigInt.zero, BigInt.zero);
9 + ZanoBalance({required this.total, required this.unlocked}) : super(unlocked, (total - unlocked));
10
12 - @override
13 - String get formattedAdditionalBalance => ZanoFormatter.bigIntAmountToString(total - unlocked, decimalPoint);
14 -
15 - @override
16 - String get formattedAvailableBalance => ZanoFormatter.bigIntAmountToString(unlocked, decimalPoint);
11 + ZanoBalance.empty(Currency currency)
12 + : total = Money.zero(currency),
13 + unlocked = Money.zero(currency),
14 + super(Money.zero(currency), Money.zero(currency));
15 }
cw_zano/lib/model/zano_transaction_info.dart
+9 -26
@@ -1,4 +1,5 @@
1 -import 'package:cw_core/format_amount.dart';
1 +import 'package:cw_core/amount/money.dart';
2 +import 'package:cw_core/crypto_currency.dart';
3 import 'package:cw_core/transaction_direction.dart';
4 import 'package:cw_core/transaction_info.dart';
5 import 'package:cw_zano/api/model/transfer.dart';
@@ -11,13 +12,13 @@ class ZanoTransactionInfo extends TransactionInfo {
12 required this.direction,
13 required this.date,
14 required this.isPending,
14 - required this.zanoAmount,
15 + required this.amount,
16 required this.fee,
17 required this.confirmations,
18 required this.tokenSymbol,
19 required this.decimalPoint,
20 required String assetId,
20 - }) : amount = zanoAmount.isValidInt ? zanoAmount.toInt() : 0 {
21 + }) {
22 additionalInfo['assetId'] = assetId;
23 }
24
@@ -25,16 +26,15 @@ class ZanoTransactionInfo extends TransactionInfo {
26 {required int confirmations,
27 required bool isIncome,
28 required String assetId,
28 - required BigInt amount,
29 + required Money amount,
30 this.tokenSymbol = 'ZANO',
31 this.decimalPoint = ZanoFormatter.defaultDecimalPoint})
32 : id = transfer.txHash,
33 height = transfer.height,
34 direction = isIncome ? TransactionDirection.incoming : TransactionDirection.outgoing,
35 date = DateTime.fromMillisecondsSinceEpoch(transfer.timestamp * 1000),
35 - zanoAmount = amount,
36 - amount = amount.isValidInt ? amount.toInt() : 0,
37 - fee = transfer.fee,
36 + amount = amount,
37 + fee = Money.fromInt(transfer.fee, CryptoCurrency.zano),
38 confirmations = confirmations,
39 isPending = confirmations < 10,
40 recipientAddress = transfer.remoteAddresses.isNotEmpty
@@ -54,28 +54,11 @@ class ZanoTransactionInfo extends TransactionInfo {
54 final TransactionDirection direction;
55 final DateTime date;
56 final bool isPending;
57 - final BigInt zanoAmount;
58 - final int amount;
59 - final int fee;
57 + final Money amount;
58 + final Money fee;
59 final int confirmations;
60 final int decimalPoint;
61 late String recipientAddress;
62 final String tokenSymbol;
64 - String? _fiatAmount;
63 String? key;
66 -
67 - @override
68 - String amountFormatted() =>
69 - '${formatAmount(ZanoFormatter.bigIntAmountToString(zanoAmount, decimalPoint))} $tokenSymbol';
70 -
71 - @override
72 - String fiatAmount() => _fiatAmount ?? '';
73 -
74 - @override
75 - void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
76 -
77 - @override
78 - String feeFormatted() => '${formatAmount(ZanoFormatter.intAmountToString(fee))} $feeCurrency';
79 -
80 - String get feeCurrency => 'ZANO';
64 }
cw_zano/lib/zano_wallet.dart
+38 -33
@@ -3,6 +3,7 @@ import 'dart:core';
3 import 'dart:io';
4 import 'dart:math';
5
6 +import 'package:cw_core/amount/money.dart';
7 import 'package:cw_core/cake_hive.dart';
8 import 'package:cw_core/crypto_currency.dart';
9 import 'package:cw_core/node.dart';
@@ -26,7 +27,6 @@ import 'package:cw_zano/model/zano_transaction_creation_exception.dart';
27 import 'package:cw_zano/model/zano_transaction_credentials.dart';
28 import 'package:cw_zano/model/zano_transaction_info.dart';
29 import 'package:cw_zano/model/zano_wallet_keys.dart';
29 -import 'package:cw_zano/zano_formatter.dart';
30 import 'package:cw_zano/zano_transaction_history.dart';
31 import 'package:cw_zano/zano_wallet_addresses.dart';
32 import 'package:cw_zano/zano_wallet_api.dart';
@@ -106,7 +106,7 @@ abstract class ZanoWalletBase
106 static final int _txChunkSize = (pow(2, 32) - 1).toInt();
107
108 ZanoWalletBase(WalletInfo walletInfo, DerivationInfo derivationInfo, String password)
109 - : balance = ObservableMap.of({CryptoCurrency.zano: ZanoBalance.empty()}),
109 + : balance = ObservableMap.of({CryptoCurrency.zano: ZanoBalance.empty(CryptoCurrency.zano)}),
110 _isTransactionUpdating = false,
111 _hasSyncAfterStartup = false,
112 walletAddresses = ZanoWalletAddresses(walletInfo),
@@ -199,8 +199,8 @@ abstract class ZanoWalletBase
199 for (final item in result.wi.balances) {
200 if (item.assetInfo.assetId == zanoAssetId) {
201 balance[CryptoCurrency.zano] = ZanoBalance(
202 - total: item.total,
203 - unlocked: item.unlocked,
202 + total: Money(item.total, CryptoCurrency.zano),
203 + unlocked: Money(item.unlocked, CryptoCurrency.zano),
204 );
205 }
206 }
@@ -232,24 +232,25 @@ abstract class ZanoWalletBase
232 final isZano = credentials.currency == CryptoCurrency.zano;
233 final outputs = credentials.outputs;
234 final hasMultiDestination = outputs.length > 1;
235 - final unlockedBalanceZano = balance[CryptoCurrency.zano]?.unlocked ?? BigInt.zero;
236 - final unlockedBalanceCurrency = balance[credentials.currency]?.unlocked ?? BigInt.zero;
237 - final fee = BigInt.from(calculateEstimatedFee(credentials.priority));
238 - late BigInt totalAmount;
235 + final unlockedBalanceZano = balance[CryptoCurrency.zano]?.unlocked ?? Money.zero(CryptoCurrency.zano);
236 + final unlockedBalanceCurrency = balance[credentials.currency]?.unlocked ?? Money.zero(credentials.currency);
237 + final fee = Money(BigInt.from(calculateEstimatedFee(credentials.priority)), CryptoCurrency.zano);
238 +
239 + var totalAmount = Money.zero(credentials.currency);
240 void checkForEnoughBalances() {
241 if (isZano) {
242 if (totalAmount + fee > unlockedBalanceZano) {
243 throw ZanoTransactionCreationException(
243 - "You don't have enough coins (required: ${ZanoFormatter.bigIntAmountToString(totalAmount + fee)} ZANO, unlocked ${ZanoFormatter.bigIntAmountToString(unlockedBalanceZano)} ZANO).");
244 + "You don't have enough coins (required: ${(totalAmount + fee).toStringWithSymbol()}, unlocked ${unlockedBalanceZano.toStringWithSymbol()}).");
245 }
246 } else {
247 if (fee > unlockedBalanceZano) {
248 throw ZanoTransactionCreationException(
248 - "You don't have enough coins (required: ${ZanoFormatter.bigIntAmountToString(fee)} ZANO, unlocked ${ZanoFormatter.bigIntAmountToString(unlockedBalanceZano)} ZANO).");
249 + "You don't have enough coins (required: ${fee.toStringWithSymbol()}, unlocked ${unlockedBalanceZano.toStringWithSymbol()}).");
250 }
251 if (totalAmount > unlockedBalanceCurrency) {
252 throw ZanoTransactionCreationException(
252 - "You don't have enough coins (required: ${ZanoFormatter.bigIntAmountToString(totalAmount, credentials.currency.decimals)} ${credentials.currency.title}, unlocked ${ZanoFormatter.bigIntAmountToString(unlockedBalanceCurrency, credentials.currency.decimals)} ${credentials.currency.title}).");
253 + "You don't have enough coins (required: ${totalAmount.toStringWithSymbol()}, unlocked ${unlockedBalanceCurrency.toStringWithSymbol()}).");
254 }
255 }
256 }
@@ -257,15 +258,15 @@ abstract class ZanoWalletBase
258 final assetId = isZano ? zanoAssetId : (credentials.currency as ZanoAsset).assetId;
259 late List<Destination> destinations;
260 if (hasMultiDestination) {
260 - if (outputs.any((output) => output.sendAll || (output.formattedCryptoAmount ?? 0) <= 0)) {
261 + if (outputs.any((output) => output.sendAll || output.cryptoAmount.amount <= BigInt.zero)) {
262 throw ZanoTransactionCreationException("You don't have enough coins.");
263 }
263 - totalAmount = outputs.fold(
264 - BigInt.zero, (acc, value) => acc + BigInt.from(value.formattedCryptoAmount ?? 0));
264 + totalAmount =
265 + outputs.fold(Money.zero(credentials.currency), (acc, value) => acc + value.cryptoAmount);
266 checkForEnoughBalances();
267 destinations = outputs
268 .map((output) => Destination(
268 - amount: BigInt.from(output.formattedCryptoAmount ?? 0),
269 + amount: output.cryptoAmount.amount,
270 address: output.isParsedAddress ? output.extractedAddress! : output.address,
271 assetId: assetId,
272 ))
@@ -279,12 +280,12 @@ abstract class ZanoWalletBase
280 totalAmount = unlockedBalanceCurrency;
281 }
282 } else {
282 - totalAmount = BigInt.from(output.formattedCryptoAmount!);
283 + totalAmount = output.cryptoAmount;
284 }
285 checkForEnoughBalances();
286 destinations = [
287 Destination(
287 - amount: totalAmount,
288 + amount: totalAmount.amount,
289 address: output.isParsedAddress ? output.extractedAddress! : output.address,
290 assetId: assetId,
291 )
@@ -296,8 +297,6 @@ abstract class ZanoWalletBase
297 fee: fee,
298 comment: outputs.first.note ?? '',
299 assetId: assetId,
299 - ticker: credentials.currency.title,
300 - decimalPoint: credentials.currency.decimals,
300 amount: totalAmount,
301 );
302 }
@@ -414,7 +413,7 @@ abstract class ZanoWalletBase
413 Future<bool> checkNodeHealth() async {
414 try {
415 final status = await getWalletStatus();
417 -
416 +
417 return status.isDaemonConnected;
418 } catch (_) {
419 return false;
@@ -453,7 +452,7 @@ abstract class ZanoWalletBase
452 enabled: true,
453 );
454 zanoAssets[asset.assetId] = asset;
456 - balance[asset] = ZanoBalance.empty(decimalPoint: asset.decimalPoint);
455 + balance[asset] = ZanoBalance.empty(asset);
456 return asset;
457 }
458
@@ -555,32 +554,38 @@ abstract class ZanoWalletBase
554 // 2. set whitelists available in balances as 'enabled' ('disabled' by default)
555 for (final b in walletInfo.wi.balances) {
556 if (b.assetId == zanoAssetId) {
558 - balance[CryptoCurrency.zano] = ZanoBalance(total: b.total, unlocked: b.unlocked);
557 + balance[CryptoCurrency.zano] = ZanoBalance(
558 + total: Money(b.total, CryptoCurrency.zano),
559 + unlocked: Money(b.unlocked, CryptoCurrency.zano),
560 + );
561 } else {
562 final asset = zanoAssets[b.assetId];
563 if (asset == null) {
564 printV('balance for an unknown asset ${b.assetInfo.assetId}');
565 continue;
566 }
565 - if (balance.keys.any(
566 - (element) => element is ZanoAsset && element.assetId == b.assetInfo.assetId)) {
567 - balance[balance.keys.firstWhere((element) =>
568 - element is ZanoAsset && element.assetId == b.assetInfo.assetId)] =
569 - ZanoBalance(
570 - total: b.total, unlocked: b.unlocked, decimalPoint: asset.decimalPoint);
567 +
568 + final assetBalanceKey =
569 + balance.keys.where((e) => e is ZanoAsset && e.assetId == asset.assetId).firstOrNull;
570 + if (assetBalanceKey != null) {
571 + balance[assetBalanceKey] = ZanoBalance(
572 + total: Money(b.total, assetBalanceKey),
573 + unlocked: Money(b.unlocked, assetBalanceKey),
574 + );
575 } else {
576 balance[asset] = ZanoBalance(
573 - total: b.total, unlocked: b.unlocked, decimalPoint: asset.decimalPoint);
577 + total: Money(b.total, asset),
578 + unlocked: Money(b.unlocked, asset),
579 + );
580 }
581 }
582 }
583 await updateTransactions();
584 // removing balances for assets missing in wallet info balances
585 balance.removeWhere(
580 - (key, _) =>
581 - key != CryptoCurrency.zano &&
582 - !walletInfo.wi.balances
583 - .any((element) => element.assetId == (key as ZanoAsset).assetId),
586 + (key, _) =>
587 + key != CryptoCurrency.zano &&
588 + !walletInfo.wi.balances.any((element) => element.assetId == (key as ZanoAsset).assetId),
589 );
590 }
591 }
cw_zcash/lib/src/pending_zcash_transaction.dart
+15 -23
@@ -1,11 +1,11 @@
1 import 'dart:convert';
2 import 'dart:math';
3
4 +import 'package:cw_core/amount/money.dart';
5 +import 'package:cw_core/crypto_currency.dart';
6 import 'package:cw_core/output_info.dart';
7 import 'package:cw_core/pending_transaction.dart';
8 import 'package:cw_zcash/cw_zcash.dart';
7 -import 'package:cw_core/currency_for_wallet_type.dart';
8 -import 'package:cw_core/wallet_type.dart';
9 import 'package:cw_zcash/src/zcash_taddress_rotation.dart';
10 import 'package:warp_api/data_fb_generated.dart';
11 import 'package:warp_api/warp_api.dart';
@@ -23,7 +23,7 @@ class PendingZcashTransaction with PendingTransaction {
23 final ZcashTransactionCredentials credentials;
24 final String txPlan;
25 String? _txId;
26 - final int availableBalance;
26 + final Money availableBalance;
27
28 @override
29 String get id => _txId ?? '';
@@ -32,31 +32,24 @@ class PendingZcashTransaction with PendingTransaction {
32 String get hex => '';
33
34 @override
35 - String get amountFormatted {
36 - return walletTypeToCryptoCurrency(WalletType.zcash).formatAmount(BigInt.from(totalAmount));
37 - }
35 + Money get amount => Money.fromInt(_totalAmount, CryptoCurrency.zec);
36 +
37 + @override
38 + String get amountFormatted => amount.toString();
39
39 - int get totalAmount {
40 + int get _totalAmount {
41 final isAll = credentials.outputs.fold<bool>(false, (final a, final b) => a || (b.sendAll));
42 if (isAll) {
42 - return availableBalance;
43 + return availableBalance.amount.toInt();
44 }
45 return credentials.outputs.fold<int>(
46 0,
46 - (final a, final b) => a + (b.formattedCryptoAmount ?? 0),
47 + (final a, final b) => a + b.cryptoAmount.amount.toInt(),
48 );
49 }
50
51 @override
51 - String get feeFormatted =>
52 - '$feeFormattedValue ${walletTypeToCryptoCurrency(WalletType.zcash).title}';
53 -
54 - @override
55 - late String feeFormattedValue = walletTypeToCryptoCurrency(
56 - WalletType.zcash,
57 - ).formatAmount(BigInt.from(fee));
58 -
59 - int fee;
52 + final Money fee;
53
54 @override
55 Future<void> commit() async {
@@ -73,7 +66,7 @@ class PendingZcashTransaction with PendingTransaction {
66 txId: _txId,
67 height: 0,
68 timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
76 - value: -totalAmount,
69 + value: -_totalAmount,
70 ).pack,
71 ),
72 ),
@@ -83,7 +76,8 @@ class PendingZcashTransaction with PendingTransaction {
76 _txId ?? '',
77 credentials.outputs.reduce((final o1, final o2) {
78 return OutputInfo(
86 - address: o1.address + "," + o2.address,
79 + cryptoAmount: Money.zero(CryptoCurrency.zec),
80 + address: "${o1.address},${o2.address}",
81 sendAll: false,
82 isParsedAddress: false,
83 );
@@ -94,9 +88,7 @@ class PendingZcashTransaction with PendingTransaction {
88 }
89
90 @override
97 - Future<Map<String, String>> commitUR() {
98 - throw UnimplementedError('UR not supported for Zcash');
99 - }
91 + Future<Map<String, String>> commitUR() => throw UnimplementedError('UR not supported for Zcash');
92
93 @override
94 bool shouldCommitUR() => false;
cw_zcash/lib/src/zcash_balance.dart
+7 -26
@@ -1,32 +1,13 @@
1 +import 'package:cw_core/amount/money.dart';
2 import 'package:cw_core/balance.dart';
3 import 'package:cw_core/crypto_currency.dart';
4
5 class ZcashBalance extends Balance {
5 - ZcashBalance({required this.confirmed, required this.unconfirmed, required final int frozen})
6 - : _frozen = frozen,
7 - super.fromInt(confirmed, unconfirmed);
6 + ZcashBalance(super.available, super.unavailable, {required super.frozen});
7
9 - factory ZcashBalance.zero() => ZcashBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
10 -
11 - final int confirmed;
12 - final int unconfirmed;
13 - final int _frozen;
14 - BigInt get frozen => BigInt.from(_frozen);
15 -
16 - @override
17 - String get formattedAvailableBalance {
18 - return CryptoCurrency.zec.formatAmount(BigInt.from(confirmed));
19 - }
20 -
21 - @override
22 - String get formattedAdditionalBalance {
23 - if (unconfirmed == 0) return '0.0';
24 - return CryptoCurrency.zec.formatAmount(BigInt.from(unconfirmed));
25 - }
26 -
27 - @override
28 - String get formattedUnAvailableBalance {
29 - if (frozen == 0) return '';
30 - return CryptoCurrency.zec.formatAmount(BigInt.from(_frozen));
31 - }
8 + factory ZcashBalance.zero() => ZcashBalance(
9 + Money.zero(CryptoCurrency.zec),
10 + Money.zero(CryptoCurrency.zec),
11 + frozen: Money.zero(CryptoCurrency.zec),
12 + );
13 }
cw_zcash/lib/src/zcash_transaction_info.dart
+3 -13
@@ -1,7 +1,7 @@
1 import 'dart:convert';
2 import 'dart:io';
3
4 -import 'package:cw_core/currency_for_wallet_type.dart';
4 +import 'package:cw_core/amount/money.dart';
5 import 'package:cw_core/pathForWallet.dart';
6 import 'package:cw_core/transaction_direction.dart';
7 import 'package:cw_core/transaction_info.dart';
@@ -14,8 +14,8 @@ import 'package:path/path.dart' as p;
14 class ZcashTransactionInfo extends TransactionInfo {
15 ZcashTransactionInfo({
16 required final String id,
17 - required final int amount,
18 - required final int fee,
17 + required final Money amount,
18 + required final Money fee,
19 required final TransactionDirection direction,
20 required final bool isPending,
21 required final DateTime date,
@@ -54,16 +54,6 @@ class ZcashTransactionInfo extends TransactionInfo {
54
55 String? _fiatAmount;
56
57 - @override
58 - String amountFormatted() =>
59 - '${walletTypeToCryptoCurrency(WalletType.zcash).formatAmount(BigInt.from(amount))} ${walletTypeToCryptoCurrency(WalletType.zcash).title}';
60 -
61 - @override
62 - String? feeFormatted() {
63 - if (fee == null || fee == 0) return null;
64 - return '${walletTypeToCryptoCurrency(WalletType.zcash).formatAmount(BigInt.from(fee!))} ${walletTypeToCryptoCurrency(WalletType.zcash).title}';
65 - }
66 -
57 @override
58 String fiatAmount() => _fiatAmount ?? '';
59
cw_zcash/lib/src/zcash_wallet.dart
+19 -30
@@ -2,6 +2,7 @@ import 'dart:async';
2 import 'dart:convert';
3 import 'dart:io';
4
5 +import 'package:cw_core/amount/money.dart';
6 import 'package:cw_core/crypto_currency.dart';
7 import 'package:cw_core/monero_transaction_priority.dart';
8 import 'package:cw_core/node.dart';
@@ -56,7 +57,7 @@ abstract class ZcashWalletBase
57
58 @override
59 ObservableMap<CryptoCurrency, ZcashBalance> balance = ObservableMap.of({
59 - CryptoCurrency.zec: ZcashBalance(confirmed: 0, unconfirmed: 0, frozen: 0),
60 + CryptoCurrency.zec: ZcashBalance.zero(),
61 });
62
63 static int internalCalculateEstimatedFee(final TransactionPriority priority, final int? amount) {
@@ -135,33 +136,21 @@ abstract class ZcashWalletBase
136 await updateBalance();
137
138 final zcashBalance = balance[CryptoCurrency.zec];
138 - final availableBalance = zcashBalance?.confirmed ?? 0;
139 + final availableBalance = zcashBalance?.available ?? Money.zero(currency);
140
141 final recipients = <Recipient>[];
141 - int totalAmount = 0;
142 + var totalAmount = Money.zero(currency);
143
144 for (final output in creds.outputs) {
144 - int amount;
145 + Money amount;
146 if (output.sendAll) {
147 amount = availableBalance;
148 } else {
148 - amount = output.formattedCryptoAmount ?? 0;
149 -
150 - if (amount == 0 && output.cryptoAmount != null && output.cryptoAmount!.isNotEmpty) {
151 - try {
152 - final parsedAmount = CryptoCurrency.zec.parseAmount(
153 - output.cryptoAmount!.replaceAll(',', '.'),
154 - );
155 - amount = parsedAmount.toInt();
156 - printV("Parsed amount from cryptoAmount '${output.cryptoAmount}': $amount");
157 - } catch (e) {
158 - printV("Failed to parse cryptoAmount '${output.cryptoAmount}': $e");
159 - }
160 - }
149 + amount = output.cryptoAmount;
150
162 - if (amount <= 0) {
151 + if (amount <= Money.zero(currency)) {
152 throw Exception(
164 - 'Invalid amount for output. Amount: ${output.cryptoAmount}, Formatted: ${output.formattedCryptoAmount}',
153 + 'Invalid amount for output. Amount: ${output.cryptoAmount}, Formatted: ${output.cryptoAmount}',
154 );
155 }
156 }
@@ -200,7 +189,7 @@ abstract class ZcashWalletBase
189 final builder = RecipientObjectBuilder(
190 address: address,
191 pools: recipientPools,
203 - amount: amount,
192 + amount: amount.amount.toInt(),
193 feeIncluded: output.sendAll,
194 replyTo: false,
195 memo: memo.isNotEmpty ? memo : null,
@@ -240,7 +229,7 @@ abstract class ZcashWalletBase
229 zcashWallet: this as ZcashWallet,
230 credentials: creds,
231 txPlan: txPlan,
243 - fee: internalCalculateEstimatedFee(creds.priority, null),
232 + fee: Money.fromInt(internalCalculateEstimatedFee(creds.priority, null), currency),
233 availableBalance: availableBalance,
234 );
235 }
@@ -307,8 +296,8 @@ abstract class ZcashWalletBase
296
297 final txInfo = ZcashTransactionInfo(
298 id: txId.trim().replaceAll('"', ''),
310 - amount: tx.value.abs(),
311 - fee: 0,
299 + amount: Money.fromInt(tx.value.abs(), currency),
300 + fee: Money.zero(currency),
301 direction: direction,
302 isPending: tx.height == 0,
303 date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
@@ -807,9 +796,9 @@ abstract class ZcashWalletBase
796 unawaited(_autoShield());
797
798 balance[CryptoCurrency.zec] = ZcashBalance(
810 - confirmed: confirmedSpendable,
811 - unconfirmed: spendable - confirmedSpendable,
812 - frozen: 0,
799 + Money.fromInt(confirmedSpendable, currency),
800 + Money.fromInt(spendable - confirmedSpendable, currency),
801 + frozen: Money.zero(currency),
802 );
803 } catch (e, stackTrace) {
804 printV("Balance update error: $e");
@@ -1158,13 +1147,13 @@ abstract class ZcashWalletBase
1147 }
1148 _didRunRescanInternalChange = true;
1149 final bal =
1161 - balance[CryptoCurrency.zec]!.confirmed +
1162 - balance[CryptoCurrency.zec]!.unconfirmed +
1163 - balance[CryptoCurrency.zec]!.frozen.toInt();
1150 + balance[CryptoCurrency.zec]!.available +
1151 + balance[CryptoCurrency.zec]!.unavailable +
1152 + (balance[CryptoCurrency.zec]!.frozen ?? Money.zero(CryptoCurrency.zec));
1153 final osCacheDir = await getApplicationCacheDirectory();
1154 final cacheDir = osCacheDir.createTempSync("zkool-import");
1155 zkoolSweep = ZkoolSweep(
1167 - currentBalance: bal,
1156 + currentBalance: bal.amount.toInt(),
1157 cacheDir: cacheDir.path,
1158 seed: seed ?? '',
1159 passphrase: password,
lib/bitcoin/cw_bitcoin.dart
+3 -8
@@ -156,13 +156,12 @@ class CWBitcoin extends Bitcoin {
156 outputs
157 .map((out) => OutputInfo(
158 fiatAmount: out.fiatAmount,
159 - cryptoAmount: out.cryptoAmount,
159 + cryptoAmount: out.cryptoAmountMoney,
160 address: out.address,
161 note: out.note,
162 sendAll: out.sendAll,
163 extractedAddress: out.extractedAddress,
164 isParsedAddress: out.isParsedAddress,
165 - formattedCryptoAmount: out.formattedCryptoAmount,
165 memo: out.memo.isNotEmpty ? out.memo : null,
166 extra: out.extra,
167 ))
@@ -191,7 +190,7 @@ class CWBitcoin extends Bitcoin {
190 }
191
192 @override
194 - Future<int> estimateFakeSendAllTxAmount(Object wallet, TransactionPriority priority,
193 + Future<Money> estimateFakeSendAllTxAmount(WalletBase wallet, TransactionPriority priority,
194 {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) async {
195 try {
196 final sk = ECPrivate.random();
@@ -233,7 +232,7 @@ class CWBitcoin extends Bitcoin {
232
233 return estimatedTx.amount;
234 } catch (_) {
236 - return 0;
235 + return Money.zero(wallet.currency);
236 }
237 }
238
@@ -247,10 +246,6 @@ class CWBitcoin extends Bitcoin {
246 String formatterBitcoinAmountToString({required int amount}) =>
247 bitcoinAmountToString(amount: amount);
248
250 - @override
251 - double formatterBitcoinAmountToDouble({required int amount}) =>
252 - bitcoinAmountToDouble(amount: amount);
253 -
249 @override
250 int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount);
251
lib/cake_pay/src/cards/cake_pay_buy_card_page.dart
+6 -12
@@ -22,7 +22,6 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.da
22 import 'package:cake_wallet/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart';
23 import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
24 import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
25 -import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
25 import 'package:cake_wallet/src/widgets/primary_button.dart';
26 import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
27 import 'package:cake_wallet/typography.dart';
@@ -36,7 +35,6 @@ import 'package:cw_core/unspent_coin_type.dart';
35 import 'package:cw_core/wallet_type.dart';
36 import 'package:flutter/material.dart';
37 import 'package:flutter_mobx/flutter_mobx.dart';
39 -import 'package:keyboard_actions/keyboard_actions.dart';
38 import 'package:mobx/mobx.dart';
39
40 class CakePayBuyCardPage extends BasePage {
@@ -612,16 +610,13 @@ class CakePayBuyCardPage extends BasePage {
610 titleIconPath: _sendViewModel.selectedCryptoCurrency.iconPath,
611 currency: _sendViewModel.selectedCryptoCurrency,
612 amount: S.of(bottomSheetContext).send_amount,
615 - amountValue: _sendViewModel.amountParsingProxy.getDisplayCryptoAmount(
616 - _sendViewModel.pendingTransaction!.amountFormatted,
617 - _sendViewModel.selectedCryptoCurrency),
613 + amountValue: _sendViewModel.amountParsingProxy.asDisplayStringWithSymbol(
614 + _sendViewModel.pendingTransaction!.amount),
615 quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
616 fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted,
617 fee: S.of(bottomSheetContext).send_fee,
621 - feeValue: "${_sendViewModel.amountParsingProxy.getDisplayCryptoAmount(
622 - _sendViewModel.pendingTransaction!.feeFormattedValue,
623 - _sendViewModel.selectedCryptoCurrency)} ${_sendViewModel.amountParsingProxy
624 - .getCryptoSymbol(_sendViewModel.wallet.currency)}",
618 + feeValue: _sendViewModel.amountParsingProxy.asDisplayStringWithSymbol(
619 + _sendViewModel.pendingTransaction!.fee),
620 feeFiatAmount: _sendViewModel.pendingTransactionFeeFiatAmountFormatted,
621 outputs: displayingOutputs,
622 footerType: FooterType.slideActionButton,
@@ -699,13 +694,12 @@ class CakePayBuyCardPage extends BasePage {
694 child: Icon(Icons.check, size: 16, color: Colors.white),
695 ),
696 output: displayingOutputs.first,
702 - currency: _sendViewModel.selectedCryptoCurrency,
697 amount: S.of(sheetCtx).send_amount,
704 - amountValue: _sendViewModel.pendingTransaction!.amountFormatted,
698 + amountValue: _sendViewModel.pendingTransaction!.amount.toStringWithSymbol(),
699 quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
700 fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted,
701 fee: S.of(sheetCtx).send_fee,
708 - feeValue: _sendViewModel.pendingTransaction!.feeFormatted,
702 + feeValue: _sendViewModel.pendingTransaction!.fee.toStringWithSymbol(),
703 feeFiatAmount: _sendViewModel.pendingTransactionFeeFiatAmountFormatted,
704 paymentId: 'Order ID',
705 paymentIdValue: order?.orderId ?? '',
lib/core/amount_parsing_proxy.dart
+36 -50
@@ -1,7 +1,9 @@
1 import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart';
2 import 'package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart';
3 +import 'package:cw_core/amount/money.dart';
4 import 'package:cw_core/crypto_amount_format.dart';
5 import 'package:cw_core/crypto_currency.dart';
6 +import 'package:cw_core/currency.dart';
7 import 'package:cw_core/utils/print_verbose.dart';
8
9 class AmountParsingProxy {
@@ -20,70 +22,54 @@ class AmountParsingProxy {
22
23 /// [getCryptoOutputAmount] turns the input [amount] into the preferred representation of [cryptoCurrency]
24 String getDisplayCryptoAmount(String amount, CryptoCurrency cryptoCurrency) {
23 -
25 try {
26 if (useSatoshi(cryptoCurrency) && amount.isNotEmpty) {
26 - return cryptoCurrency.parseAmount(amount.withMaxDecimals(cryptoCurrency.decimals)).toString();
27 + return cryptoCurrency
28 + .parseAmount(amount.withMaxDecimals(cryptoCurrency.decimals))
29 + .toStringWithPrecision(useBaseUnit: true);
30 }
31
32 return amount.withMaxDecimals(cryptoCurrency.decimals);
30 -
31 - } catch(_) {
32 - printV("failed to parse amount $amount for currency ${cryptoCurrency.title}, falling back to showing unparsed");
33 + } catch (_) {
34 + printV(
35 + "failed to parse amount $amount for currency ${cryptoCurrency.title}, falling back to showing unparsed");
36 return amount;
37 }
35 -
36 - }
37 -
38 - /// [getCryptoStringRepresentation] turns the input [amount] into the preferred representation of [cryptoCurrency]
39 - String getDisplayCryptoString(int amount, CryptoCurrency cryptoCurrency) {
40 - if (useSatoshi(cryptoCurrency)) {
41 - return "$amount";
42 - }
43 -
44 - return cryptoCurrency.formatAmount(BigInt.from(amount));
45 - }
46 -
47 - String getDisplayCryptoStringFromBigInt(BigInt amount, CryptoCurrency cryptoCurrency) {
48 - if (useSatoshi(cryptoCurrency)) {
49 - return "$amount";
50 - }
51 -
52 - return cryptoCurrency.formatAmount(amount);
53 - }
54 -
55 - /// [getCryptoStringFromDouble] turns the input [amount] into the preferred representation of [cryptoCurrency] and
56 - String getDisplayCryptoStringFromDouble(double amount, CryptoCurrency cryptoCurrency) {
57 - if (useSatoshi(cryptoCurrency)) {
58 - return "$amount";
59 - }
60 -
61 - return cryptoCurrency.formatAmount(BigInt.from(amount));
38 }
39
64 - /// [parseCryptoString] turns the input [string] into a `BigInt` presentation of the [cryptoCurrency]
65 - BigInt parseCryptoString(String amount, CryptoCurrency cryptoCurrency) {
66 - if (useSatoshi(cryptoCurrency)) {
67 - return BigInt.parse(amount);
68 - }
69 -
70 - return cryptoCurrency.parseAmount(amount);
71 - }
72 -
73 - /// [tryParseCryptoString] tries to turn the display representation [string] into a `BigInt` presentation of the [cryptoCurrency]
74 - BigInt? tryParseCryptoString(String amount, CryptoCurrency cryptoCurrency) {
75 - if (useSatoshi(cryptoCurrency)) {
76 - return BigInt.tryParse(amount);
77 - }
78 -
79 - return cryptoCurrency.tryParseAmount(amount);
80 - }
40 + String getDisplayCryptoString(int amount, CryptoCurrency cryptoCurrency) =>
41 + asDisplayString(Money.fromInt(amount, cryptoCurrency));
42 +
43 + /// [getDisplayCryptoString] turns the input [amount] into the preferred representation of [cryptoCurrency]
44 + ///
45 + /// if [displayMode] is [BitcoinAmountDisplayMode.satoshi] it returns 1 BTC as 100000000
46 + /// if [displayMode] is [BitcoinAmountDisplayMode.bitcoin] it returns 1 BTC as 1
47 + /// hardcoding fractionalDigits to 8 as it's a display string, so no need to show more than that
48 + String asDisplayString(Money amount) =>
49 + amount.toStringWithPrecision(useBaseUnit: useSatoshi(amount.currency), fractionalDigits: 8);
50 +
51 + /// [asDisplayStringWithSymbol] turns the input [amount] into the preferred representation of
52 + /// [cryptoCurrency] following the symbol of the unit
53 + ///
54 + /// if [displayMode] is [BitcoinAmountDisplayMode.satoshi] it returns 1 BTC as 100000000 sats
55 + /// if [displayMode] is [BitcoinAmountDisplayMode.bitcoin] it returns 1 BTC as 1 BTC
56 + /// hardcoding fractionalDigits to 8 as it's a display string, so no need to show more than that
57 + String asDisplayStringWithSymbol(Money amount) =>
58 + amount.toStringWithSymbol(useBaseUnit: useSatoshi(amount.currency), fractionalDigits: 8);
59 +
60 + /// [parseCryptoString] turns the the display representation [string] into `Money` with [cryptoCurrency]
61 + Money parseCryptoString(String amount, CryptoCurrency cryptoCurrency) =>
62 + Money.parse(amount, cryptoCurrency, isBaseUnit: useSatoshi(cryptoCurrency));
63 +
64 + /// [tryParseCryptoString] tries to turn the display representation [string] into `Money` with [cryptoCurrency]
65 + Money? tryParseCryptoString(String amount, CryptoCurrency cryptoCurrency) =>
66 + Money.tryParse(amount, cryptoCurrency, isBaseUnit: useSatoshi(cryptoCurrency));
67
68 /// [getCryptoSymbol] returns the correct Symbol related to the presentation
69 String getCryptoSymbol(CryptoCurrency cryptoCurrency) =>
70 useSatoshi(cryptoCurrency) ? "sats" : cryptoCurrency.title.safeSubString(0, 8);
71
86 - bool useSatoshi(CryptoCurrency cryptoCurrency) =>
72 + bool useSatoshi(Currency cryptoCurrency) =>
73 ([CryptoCurrency.btc, CryptoCurrency.btcln].contains(cryptoCurrency) &&
74 displayMode == BitcoinAmountDisplayMode.satoshi) ||
75 (CryptoCurrency.btcln == cryptoCurrency &&
lib/core/background_sync.dart
+1 -1
@@ -213,7 +213,7 @@ class BackgroundSync {
213 final action = tx.direction == TransactionDirection.incoming ? "Received" : "Sent";
214 if (sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false) {
215 await showNotification(
216 - "$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amountFormatted()}");
216 + "$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amount.toString()}");
217 }
218 printV(
219 "${wallet.currency.fullName} in ${wallet.name}: TX: ${tx.date} ${tx.amount} ${tx.direction}");
lib/core/csv_export_service.dart
+4 -19
@@ -80,21 +80,15 @@ class CsvExportService {
80 ? tx.to!
81 : (tx.outputAddresses?.firstOrNull ?? ''));
82
83 - // Split "0.2769 LTC" → amount="0.2769", currency="LTC"
84 - final (amount, currency) = _splitAmountCurrency(tx.amountFormatted());
85 -
86 - // fee is stored as (inputAmount - totalOutputAmount) which is negative for incoming
87 - // transactions — only include it when the raw value is a sensible positive fee.
88 - final fee = tx.fee != null && tx.fee! > 0 ? (tx.feeFormatted() ?? '') : '';
89 - final (feeAmount, _) = fee.isNotEmpty ? _splitAmountCurrency(fee) : ('', '');
83 + final fee = tx.fee != null && !tx.fee!.isZero ? tx.fee.toString() : '';
84
85 return _row([
86 'transaction',
87 _isoDate(tx.date),
88 type,
95 - amount,
96 - currency,
97 - feeAmount,
89 + tx.amount.toString(),
90 + tx.amount.currency.symbol,
91 + fee,
92 tx.id,
93 address,
94 status,
@@ -109,15 +103,6 @@ class CsvExportService {
103 ]);
104 }
105
112 - /// Splits a formatted amount like "0.2769 LTC" into ("0.2769", "LTC").
113 - /// If there is no trailing symbol the whole string is returned as the amount.
114 - (String, String) _splitAmountCurrency(String formatted) {
115 - final trimmed = formatted.trim();
116 - final idx = trimmed.lastIndexOf(' ');
117 - if (idx == -1) return (trimmed, '');
118 - return (trimmed.substring(0, idx), trimmed.substring(idx + 1));
119 - }
120 -
106 String _tradeRow(TradeListItem item) {
107 final trade = item.trade;
108 return _row([
lib/decred/cw_decred.dart
+9 -20
@@ -19,9 +19,8 @@ class CWDecred extends Decred {
19 DecredRestoreWalletFromPubkeyCredentials(name: name, pubkey: pubkey, password: password);
20
21 @override
22 - WalletService createDecredWalletService(Box<UnspentCoinsInfo> unspentCoinSource) {
23 - return DecredWalletService(unspentCoinSource);
24 - }
22 + WalletService createDecredWalletService(Box<UnspentCoinsInfo> unspentCoinSource) =>
23 + DecredWalletService(unspentCoinSource);
24
25 @override
26 List<TransactionPriority> getTransactionPriorities() => DecredTransactionPriority.all;
@@ -39,18 +38,19 @@ class CWDecred extends Decred {
38 @override
39 Object createDecredTransactionCredentials(List<Output> outputs, TransactionPriority priority) =>
40 DecredTransactionCredentials(
42 - outputs
43 - .map((out) => OutputInfo(
41 + outputs
42 + .map((out) => OutputInfo(
43 fiatAmount: out.fiatAmount,
45 - cryptoAmount: out.cryptoAmount,
44 + cryptoAmount: out.cryptoAmountMoney,
45 address: out.address,
46 note: out.note,
47 sendAll: out.sendAll,
48 extractedAddress: out.extractedAddress,
49 isParsedAddress: out.isParsedAddress,
51 - formattedCryptoAmount: out.formattedCryptoAmount))
52 - .toList(),
53 - priority: priority as DecredTransactionPriority);
50 + ))
51 + .toList(),
52 + priority: priority as DecredTransactionPriority,
53 + );
54
55 List<WalletInfoAddressInfo> getAddressInfos(Object wallet) {
56 final decredWallet = wallet as DecredWallet;
@@ -69,17 +69,6 @@ class CWDecred extends Decred {
69 await decredWallet.walletAddresses.generateNewAddress(label);
70 }
71
72 - @override
73 - String formatterDecredAmountToString({required int amount}) =>
74 - decredAmountToString(amount: amount);
75 -
76 - @override
77 - double formatterDecredAmountToDouble({required int amount}) =>
78 - decredAmountToDouble(amount: amount);
79 -
80 - @override
81 - int formatterStringDoubleToDecredAmount(String amount) => stringDoubleToDecredAmount(amount);
82 -
72 @override
73 List<Unspent> getUnspents(Object wallet) {
74 final decredWallet = wallet as DecredWallet;
lib/entities/calculate_fiat_amount.dart
+3 -1
@@ -1,9 +1,11 @@
1 +import 'package:cw_core/amount/amount_sanitizer.dart';
2 +
3 String calculateFiatAmount({double? price, String? cryptoAmount, bool raw = false}) {
4 if (price == null || cryptoAmount == null) {
5 return '0.00';
6 }
7
6 - cryptoAmount = cryptoAmount.replaceAll(',', '.');
8 + cryptoAmount = cryptoAmount.sanitized();
9
10 final _amount = double.tryParse(cryptoAmount);
11 if (_amount == null || _amount.isNaN) return '0.00';
lib/entities/fiat_currency.dart
+15 -1
@@ -1,18 +1,26 @@
1 +import 'package:cw_core/amount/money.dart';
2 import 'package:cw_core/currency.dart';
3 import 'package:cw_core/enumerable_item.dart';
4
5 class FiatCurrency extends EnumerableItem<String> with Serializable<String> implements Currency {
6 const FiatCurrency({
6 - required String symbol,
7 + required this.symbol,
8 required this.countryCode,
9 required this.fullName,
10 this.decimals = 2,
11 }) : super(title: symbol, raw: symbol);
12
13 final String countryCode;
14 +
15 + @override
16 final String fullName;
17 +
18 + @override
19 final int decimals;
20
21 + @override
22 + final String symbol;
23 +
24 static List<FiatCurrency> get all => _all.values.toList();
25
26 static List<FiatCurrency> get currenciesAvailableToBuyWith => [
@@ -182,4 +190,10 @@ class FiatCurrency extends EnumerableItem<String> with Serializable<String> impl
190
191 @override
192 String get iconPath => "assets/images/flags/$countryCode.png";
193 +
194 + @override
195 + Money parseAmount(String value) => Money.parse(value, this);
196 +
197 + @override
198 + Money? tryParseAmount(String value) => Money.tryParse(value, this);
199 }
lib/evm/cw_evm.dart
+61 -81
@@ -105,26 +105,25 @@ class CWEVM extends EVM {
105 TransactionPriority? priority,
106 int? feeRate,
107 bool useBlinkProtection = true,
108 - }) {
109 - return EVMChainTransactionCredentials(
110 - outputs
111 - .map((out) => OutputInfo(
112 - fiatAmount: out.fiatAmount,
113 - cryptoAmount: out.cryptoAmount,
114 - address: out.address,
115 - note: out.note,
116 - sendAll: out.sendAll,
117 - extractedAddress: out.extractedAddress,
118 - isParsedAddress: out.isParsedAddress,
119 - formattedCryptoAmount: out.formattedCryptoAmount,
120 - memo: out.memo))
121 - .toList(),
122 - priority: priority as EVMChainTransactionPriority?,
123 - currency: currency,
124 - feeRate: feeRate,
125 - useBlinkProtection: useBlinkProtection,
126 - );
127 - }
108 + }) =>
109 + EVMChainTransactionCredentials(
110 + outputs
111 + .map((out) => OutputInfo(
112 + fiatAmount: out.fiatAmount,
113 + cryptoAmount: out.cryptoAmountMoney,
114 + address: out.address,
115 + note: out.note,
116 + sendAll: out.sendAll,
117 + extractedAddress: out.extractedAddress,
118 + isParsedAddress: out.isParsedAddress,
119 + memo: out.memo,
120 + ))
121 + .toList(),
122 + priority: priority as EVMChainTransactionPriority?,
123 + currency: currency,
124 + feeRate: feeRate,
125 + useBlinkProtection: useBlinkProtection,
126 + );
127
128 @override
129 Object createEVMTransactionCredentialsRaw(
@@ -144,29 +143,28 @@ class CWEVM extends EVM {
143 }
144
145 @override
147 - TransactionInfo getTransactionInfo(
148 -
149 - {
150 - required String id,
151 - required int height,
152 - required BigInt ethAmount,
153 - required BigInt ethFee,
154 - required String tokenSymbol,
155 - int exponent = 18,
156 - required TransactionDirection direction,
157 - required bool isPending,
158 - required DateTime date,
159 - required int confirmations,
160 - String? to,
161 - String? from,
162 - String? evmSignatureName,
163 - String? contractAddress,
164 - required int chainId,
165 - }) =>
166 - EVMChainTransactionInfo(id: id,
146 + TransactionInfo getTransactionInfo({
147 + required String id,
148 + required int height,
149 + required Money amount,
150 + required Money fee,
151 + required String tokenSymbol,
152 + int exponent = 18,
153 + required TransactionDirection direction,
154 + required bool isPending,
155 + required DateTime date,
156 + required int confirmations,
157 + String? to,
158 + String? from,
159 + String? evmSignatureName,
160 + String? contractAddress,
161 + required int chainId,
162 + }) =>
163 + EVMChainTransactionInfo(
164 + id: id,
165 height: height,
168 - ethAmount: ethAmount,
169 - ethFee: ethFee,
166 + amount: amount,
167 + fee: fee,
168 tokenSymbol: tokenSymbol,
169 direction: direction,
170 isPending: isPending,
@@ -179,22 +177,6 @@ class CWEVM extends EVM {
177 @override
178 int formatterEVMParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount);
179
182 - @override
183 - double formatterEVMAmountToDouble({
184 - TransactionInfo? transaction,
185 - BigInt? amount,
186 - int exponent = 18,
187 - }) {
188 - assert(transaction != null || amount != null);
189 -
190 - if (transaction != null) {
191 - transaction as EVMChainTransactionInfo;
192 - return transaction.ethAmount / BigInt.from(10).pow(transaction.exponent);
193 - } else {
194 - return (amount!) / BigInt.from(10).pow(exponent);
195 - }
196 - }
197 -
180 @override
181 List<Erc20Token> getERC20Currencies(WalletBase wallet) =>
182 (wallet as EVMChainWallet).erc20Currencies;
@@ -294,20 +276,16 @@ class CWEVM extends EVM {
276 @override
277 Future<PendingTransaction> createTokenApproval(
278 WalletBase wallet,
297 - BigInt amount,
279 + Money amount,
280 String spender,
299 - CryptoCurrency token,
281 TransactionPriority? priority, {
282 bool useBlinkProtection = true,
283 }) {
284 final evmWallet = wallet as EVMChainWallet;
304 - final feeCurrency = EVMChainUtils.getFeeCurrency(evmWallet.selectedChainId);
285 return evmWallet.createApprovalTransaction(
286 amount,
287 spender,
308 - token,
288 priority as EVMChainTransactionPriority?,
310 - feeCurrency,
289 useBlinkProtection: useBlinkProtection,
290 );
291 }
@@ -317,7 +295,7 @@ class CWEVM extends EVM {
295 WalletBase wallet,
296 String to,
297 String dataHex,
320 - BigInt valueWei,
298 + Money valueWei,
299 TransactionPriority? priority, {
300 bool useBlinkProtection = true,
301 String? sourceTokenAddress,
@@ -399,7 +377,7 @@ class CWEVM extends EVM {
377
378 // Chain-specific integrations (only for Ethereum)
379 @override
402 - Future<BigInt>? getDEuroSavingsBalance(WalletBase wallet) {
380 + Future<Money>? getDEuroSavingsBalance(WalletBase wallet) {
381 if (wallet.chainId == 1 && wallet is EVMChainWallet) {
382 return DEuro(wallet).savingsBalance;
383 }
@@ -407,7 +385,15 @@ class CWEVM extends EVM {
385 }
386
387 @override
410 - Future<BigInt>? getDEuroAccruedInterest(WalletBase wallet) {
388 + Future<Money>? getDEuroSavingsV1Balance(WalletBase wallet) {
389 + if (wallet.chainId == 1 && wallet is EVMChainWallet) {
390 + return DEuro(wallet).savingsBalanceV1;
391 + }
392 + return null;
393 + }
394 +
395 + @override
396 + Future<Money>? getDEuroAccruedInterest(WalletBase wallet) {
397 if (wallet.chainId == 1 && wallet is EVMChainWallet) {
398 return DEuro(wallet).accruedInterest;
399 }
@@ -448,6 +434,15 @@ class CWEVM extends EVM {
434 return null;
435 }
436
437 + @override
438 + Future<PendingTransaction>? withdrawDEuroSavingV1(
439 + WalletBase wallet, TransactionPriority priority) {
440 + if (wallet.chainId == 1 && wallet is EVMChainWallet) {
441 + return DEuro(wallet).withdrawSavingsV1(priority as EVMChainTransactionPriority);
442 + }
443 + return null;
444 + }
445 +
446 @override
447 Future<PendingTransaction>? reinvestDEuroInterest(
448 WalletBase wallet, TransactionPriority priority) {
@@ -465,13 +460,6 @@ class CWEVM extends EVM {
460 return null;
461 }
462
468 -
469 - @override
470 - BigInt getPendingTransactionFee(PendingTransaction tx) => (tx as PendingEVMChainTransaction).fee;
471 -
472 - @override
473 - String getPendingTransactionAmount(PendingTransaction tx) => (tx as PendingEVMChainTransaction).amount;
474 -
463 // Registry helper methods
464 static final EvmChainRegistry _registry = EvmChainRegistry();
465
@@ -532,14 +520,6 @@ class CWEVM extends EVM {
520 return _registry.getWalletTypeByChainId(chainId);
521 }
522
535 - @override
536 - BigInt? getERC20AvailableBalance(Object balance) {
537 - if (balance is EVMChainERC20Balance) {
538 - return balance.balance;
539 - }
540 - return null;
541 - }
542 -
523 @override
524 List<ChainInfo> getAllChains() {
525 final allChains = _registry.getAllChains();
lib/exchange/provider/xoswap_exchange_provider.dart
+5 -3
@@ -18,9 +18,11 @@ class XOSwapExchangeProvider extends ExchangeProvider {
18 }
19
20 void _addAppVersionHeader() async {
21 - final packageInfo = await PackageInfo.fromPlatform();
22 - final currentVersion = packageInfo.version;
23 - _headers['App-Version'] = currentVersion;
21 + try {
22 + final packageInfo = await PackageInfo.fromPlatform();
23 + final currentVersion = packageInfo.version;
24 + _headers['App-Version'] = currentVersion;
25 + } catch (_) {}
26 }
27
28 static const _apiAuthority = 'exchange.exodus.io';
lib/monero/cw_monero.dart
+9 -9
@@ -301,14 +301,14 @@ class CWMonero extends Monero {
301 MoneroTransactionCreationCredentials(
302 outputs: outputs
303 .map((out) => OutputInfo(
304 - fiatAmount: out.fiatAmount,
305 - cryptoAmount: out.cryptoAmount,
306 - address: out.address,
307 - note: out.note,
308 - sendAll: out.sendAll,
309 - extractedAddress: out.extractedAddress,
310 - isParsedAddress: out.isParsedAddress,
311 - formattedCryptoAmount: out.formattedCryptoAmount))
304 + fiatAmount: out.fiatAmount,
305 + cryptoAmount: out.cryptoAmountMoney,
306 + address: out.address,
307 + note: out.note,
308 + sendAll: out.sendAll,
309 + extractedAddress: out.extractedAddress,
310 + isParsedAddress: out.isParsedAddress,
311 + ))
312 .toList(),
313 priority: priority as MoneroTransactionPriority);
314
@@ -411,7 +411,7 @@ class CWMonero extends Monero {
411 }
412
413 @override
414 - bool needExportOutputs(Object wallet, int amount) {
414 + bool needExportOutputs(Object wallet, Money amount) {
415 final moneroWallet = wallet as MoneroWallet;
416 return moneroWallet.needExportOutputs(amount);
417 }
lib/nano/cw_nano.dart
+1 -7
@@ -150,13 +150,12 @@ class CWNano extends Nano {
150 outputs
151 .map((out) => OutputInfo(
152 fiatAmount: out.fiatAmount,
153 - cryptoAmount: out.cryptoAmount,
153 + cryptoAmount: out.cryptoAmountMoney,
154 address: out.address,
155 note: out.note,
156 sendAll: out.sendAll,
157 extractedAddress: out.extractedAddress,
158 isParsedAddress: out.isParsedAddress,
159 - formattedCryptoAmount: out.formattedCryptoAmount,
159 ))
160 .toList(),
161 );
@@ -175,11 +174,6 @@ class CWNano extends Nano {
174 return (wallet as NanoWallet).updateTransactions();
175 }
176
178 - @override
179 - BigInt getTransactionAmountRaw(TransactionInfo transactionInfo) {
180 - return (transactionInfo as NanoTransactionInfo).amountRaw;
181 - }
182 -
177 @override
178 String getRepresentative(Object wallet) {
179 return (wallet as NanoWallet).representative;
lib/new-ui/pages/send_page.dart
+52 -45
@@ -26,6 +26,8 @@ import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
26 import 'package:cake_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart';
27 import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
28 import 'package:cake_wallet/store/app_store.dart';
29 +import 'package:cw_core/amount/amount_sanitizer.dart';
30 +import 'package:cw_core/amount/money.dart';
31 import 'package:cw_core/lnurl.dart';
32 import 'package:cw_core/wallet_info.dart';
33 import "package:cw_core/wallet_type.dart";
@@ -123,7 +125,7 @@ class SendPageModes {
125 helpContent: SendPageHelpContent(
126 title: S.current.about_litecoin_privacy,
127 imagePath: "assets/new-ui/mweb_help.svg",
126 - description: S.current.mweb_help_desc_1 + "\n\n" + S.current.mweb_help_desc_2,
128 + description: "${S.current.mweb_help_desc_1}\n\n${S.current.mweb_help_desc_2}",
129 disclaimer: S.current.mweb_help_disclaimer),
130 popOnConfirmation: false);
131
@@ -134,7 +136,7 @@ class SendPageModes {
136 helpContent: SendPageHelpContent(
137 title: S.current.about_litecoin_privacy,
138 imagePath: "assets/new-ui/mweb_help.svg",
137 - description: S.current.mweb_help_desc_1 + "\n\n" + S.current.mweb_help_desc_2,
139 + description: "${S.current.mweb_help_desc_1}\n\n${S.current.mweb_help_desc_2}",
140 disclaimer: S.current.mweb_help_disclaimer),
141 popOnConfirmation: false);
142
@@ -184,12 +186,12 @@ class NewSendPage extends StatefulWidget {
186 }
187
188 class _NewSendPageState extends State<NewSendPage> {
187 - bool _fiatInputMode = false;
189 +
190 int _selectedOutput = 0;
191
190 - List<TextEditingController> _amountControllers = [];
191 - List<TextEditingController> _addressControllers = [];
192 - List<TextEditingController> _memoControllers = [];
192 + final _amountControllers = <TextEditingController>[];
193 + final _addressControllers = <TextEditingController>[];
194 + final _memoControllers = <TextEditingController>[];
195 final _formKey = GlobalKey<FormState>();
196 final _addressFocusNode = FocusNode();
197 BuildContext? loadingBottomSheetContext;
@@ -205,7 +207,7 @@ class _NewSendPageState extends State<NewSendPage> {
207
208 reaction((_) => widget.sendViewModel.outputs[_selectedOutput].sendAll, ((bool all) {
209 if (all) {
208 - _fiatInputMode = false;
210 + widget.sendViewModel.outputs[_selectedOutput].isFiatEntry = false;
211 _amountControllers[_selectedOutput].text = S.current.all;
212 }
213 }));
@@ -439,46 +441,46 @@ class _NewSendPageState extends State<NewSendPage> {
441 ? widget.sendViewModel.allAmountValidator
442 : widget.sendViewModel.amountValidator(output),
443 amountController: _amountControllers[_selectedOutput],
442 - currency: _fiatInputMode
444 + currency: output.isFiatEntry
445 ? widget.sendViewModel.fiatCurrency.title
446 : widget.sendViewModel.selectedCryptoCurrencySymbol,
445 - currencyIconPath: _fiatInputMode
447 + currencyIconPath: output.isFiatEntry
448 ? ""
449 : widget.sendViewModel.selectedCryptoCurrency
450 .iconPath ??
451 "",
450 - hasPicker: (_fiatInputMode ||
452 + hasPicker: (output.isFiatEntry ||
453 widget.sendViewModel.hasMultipleTokens),
452 - onPickerClicked: () {
453 - _presentCurrencyPicker(context);
454 - },
454 + onPickerClicked: () => _presentCurrencyPicker(context),
455 + maxDecimals: output.isFiatEntry
456 + ? widget.sendViewModel.fiatCurrency.decimals
457 + : widget.sendViewModel.useBaseUnits
458 + ? 0
459 + : widget
460 + .sendViewModel.selectedCryptoCurrency.decimals,
461 ),
462 FiatAmountBar(
457 - fiatInputMode: _fiatInputMode,
463 + fiatInputMode: output.isFiatEntry,
464 onSwitchButtonPressed: () {
459 - setState(() {
460 - if(!_fiatInputMode) {
461 - widget.sendViewModel.outputs[_selectedOutput]
462 - .cryptoAmount =
463 - _amountControllers[_selectedOutput].text;
464 - }
465 - _fiatInputMode = !_fiatInputMode;
466 - _amountControllers[_selectedOutput].text =
467 - _fiatInputMode
468 - ? output.fiatAmount
469 - : output.displayCryptoAmount;
470 - });
465 + widget.sendViewModel.outputs[_selectedOutput]
466 + .isFiatEntry = !output.isFiatEntry;
467 + _amountControllers[_selectedOutput].text =
468 + output.isFiatEntry
469 + ? output.fiatAmount
470 + : output.displayCryptoAmount;
471 },
472 fiatAmount: _wrapAmount(output.roundedFiatAmount(6), 20),
473 cryptoAmount:
474 _wrapAmount(output.roundedCryptoAmount(6), 20),
475 allAmount: widget.sendViewModel.balance,
476 - cryptoCurrency:
476 + cryptoCurrencySymbol:
477 widget.sendViewModel.selectedCryptoCurrencySymbol,
478 - fiatCurrency: widget.sendViewModel.fiatCurrency.title,
478 + fiatCurrencySymbol:
479 + widget.sendViewModel.fiatCurrency.symbol,
480 onAllButtonPressed: () async {
481 output.setSendAll(
482 await widget.sendViewModel.sendingBalance);
483 + await output.calculateEstimatedFee();
484 },
485 ),
486 ],
@@ -642,10 +644,10 @@ class _NewSendPageState extends State<NewSendPage> {
644 return;
645 }
646
645 - final amount = _amountControllers[_selectedOutput].text.replaceAll(',', '.');
647 + final amount = _amountControllers[_selectedOutput].text.sanitized();
648 final output = widget.sendViewModel.outputs[_selectedOutput];
649
648 - if (_fiatInputMode) {
650 + if (output.isFiatEntry) {
651 if (amount != output.fiatAmount) {
652 output.sendAll = false;
653 output.setFiatAmount(amount);
@@ -705,11 +707,11 @@ class _NewSendPageState extends State<NewSendPage> {
707
708 for (var i = 0; i < widget.sendViewModel.outputs.length; i++) {
709 if (i < _amountControllers.length && !widget.sendViewModel.outputs[i].sendAll) {
708 - if (_fiatInputMode) {
710 + if (widget.sendViewModel.outputs[i].isFiatEntry) {
711 widget.sendViewModel.outputs[i].setFiatAmount(_amountControllers[i].text);
712 } else {
713 final amount = widget.sendViewModel.amountParsingProxy.getCanonicalCryptoAmount(
712 - _amountControllers[i].text.replaceAll(",", "."),
714 + _amountControllers[i].text.sanitized(),
715 widget.sendViewModel.selectedCryptoCurrency);
716 widget.sendViewModel.outputs[i].setCryptoAmount(amount);
717 }
@@ -752,9 +754,9 @@ class _NewSendPageState extends State<NewSendPage> {
754 }
755
756 if (widget.sendViewModel.wallet.type == WalletType.monero) {
755 - int amount = 0;
757 + var amount = Money.zero(widget.sendViewModel.wallet.currency);
758 for (var item in widget.sendViewModel.outputs) {
757 - amount += item.formattedCryptoAmount;
759 + amount += item.cryptoAmountMoney;
760 }
761 if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) {
762 if (widget.sendViewModel.wallet.hardwareWalletType == HardwareWalletType.trezor) {
@@ -811,13 +813,11 @@ class _NewSendPageState extends State<NewSendPage> {
813 }
814
815 void _presentCurrencyPicker(BuildContext context) {
814 - if (!_fiatInputMode && !widget.sendViewModel.hasMultipleTokens) {
815 - return;
816 - }
817 -
816 final output = widget.sendViewModel.outputs[_selectedOutput];
817
820 - if (_fiatInputMode) {
818 + if (!output.isFiatEntry && !widget.sendViewModel.hasMultipleTokens) return;
819 +
820 + if (output.isFiatEntry) {
821 FiatCurrencyPickerSheet.show(
822 context: context,
823 selected: widget.sendViewModel.fiatCurrency,
@@ -836,7 +836,7 @@ class _NewSendPageState extends State<NewSendPage> {
836 amount: '${r.availableBalance} ${r.asset.title}',
837 fiat: isFiatDisabled
838 ? null
839 - : '${r.fiatAvailableBalanceRaw} ${r.fiatCurrencyTicker}',
839 + : '${r.fiatAvailableBalanceRaw} ${r.fiatCurrency?.symbol}',
840 fiatValue: isFiatDisabled ? null : double.tryParse(r.fiatAvailableBalanceRaw),
841 ),
842 };
@@ -859,10 +859,11 @@ class _NewSendPageState extends State<NewSendPage> {
859
860 void _handleLightningInvoicePaste() {
861 try {
862 - final lnAmount = CryptoCurrency.btcln.formatAmount(
863 - BigInt.from(getBolt11Amount(_addressControllers[_selectedOutput].text) ?? 0));
864 - if (lnAmount != 0) {
865 - _amountControllers[_selectedOutput].text = lnAmount;
862 + final lnAmount = getBolt11Amount(_addressControllers[_selectedOutput].text) ??
863 + Money.zero(CryptoCurrency.btcln);
864 + if (!lnAmount.isZero) {
865 + _amountControllers[_selectedOutput].text =
866 + widget.sendViewModel.amountParsingProxy.asDisplayString(lnAmount);
867 }
868 } catch (_) {}
869 }
@@ -885,6 +886,12 @@ class _NewSendPageState extends State<NewSendPage> {
886 await widget.sendViewModel.fetchTokenForContractAddress(paymentRequest.contractAddress!);
887 }
888
889 + // This automatically switches to lightning mode if you are in a bitcoin wallet
890 + if (result.addressDetectionResult?.detectedCurrency == CryptoCurrency.btcln) {
891 + widget.sendViewModel.selectedCryptoCurrency = CryptoCurrency.btcln;
892 + widget.sendViewModel.coinTypeToSpendFrom = UnspentCoinType.lightning;
893 + }
894 +
895 switch (result.type) {
896 case PaymentFlowType.singleWallet:
897 case PaymentFlowType.multipleWallets:
@@ -1216,7 +1223,7 @@ class _NewSendPageState extends State<NewSendPage> {
1223 try{
1224 _amountControllers[_selectedOutput].text = widget.sendViewModel.amountParsingProxy
1225 .getDisplayCryptoAmount(
1219 - paymentRequest.amount, widget.sendViewModel.selectedCryptoCurrency);
1226 + paymentRequest.amount, widget.sendViewModel.selectedCryptoCurrency);
1227 } catch(e) {
1228
1229 }
lib/new-ui/pages/swap_page.dart
+163 -158
@@ -8,7 +8,6 @@ import 'package:cake_wallet/exchange/exchange_trade_state.dart';
8 import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
9 import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
10 import 'package:cake_wallet/generated/i18n.dart';
11 -import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
11 import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
12 import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
13 import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
@@ -30,6 +29,7 @@ import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
29 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
30 import 'package:cake_wallet/src/widgets/primary_button.dart';
31 import 'package:cake_wallet/utils/debounce.dart';
32 +import 'package:cake_wallet/utils/decimal_input_formatter.dart';
33 import 'package:cake_wallet/utils/payment_request.dart';
34 import 'package:cake_wallet/utils/permission_handler.dart';
35 import 'package:cw_core/wallet_info.dart';
@@ -137,9 +137,8 @@ class _NewSwapPageState extends State<NewSwapPage> {
137 });
138 });
139 receiveFiatAmountController.addListener(() {
140 - if (!receiveKey.currentState!.amountFocusNode.hasFocus) {
141 - return;
142 - }
140 + if (!receiveKey.currentState!.amountFocusNode.hasFocus) return;
141 +
142 widget.exchangeViewModel.enableFixedRateMode();
143 widget.exchangeViewModel.isSendAllEnabled = false;
144 Future.delayed(Duration(milliseconds: 200)).then((_) {
@@ -168,6 +167,8 @@ class _NewSwapPageState extends State<NewSwapPage> {
167 });
168
169 reaction((_) => widget.exchangeViewModel.depositAmount, (String amount) {
170 + if (depositKey.currentState!.amountFocusNode.hasFocus) return;
171 +
172 if (widget.exchangeViewModel.isSendAllEnabled) {
173 depositAmountController.text = S.of(context).all;
174 } else if (depositAmountController.text != amount && amount != S.of(context).all) {
@@ -191,14 +192,14 @@ class _NewSwapPageState extends State<NewSwapPage> {
192 widget.exchangeViewModel, widget.exchangeViewModel.depositCurrency, depositKey));
193
194 reaction(
194 - (_) => widget.exchangeViewModel.receiveCurrency,
195 - (CryptoCurrency currency) =>
196 - _onCurrencyChange(currency, widget.exchangeViewModel, receiveKey));
195 + (_) => widget.exchangeViewModel.receiveCurrency,
196 + (currency) => _onCurrencyChange(currency, widget.exchangeViewModel, receiveKey),
197 + );
198
199 reaction(
199 - (_) => widget.exchangeViewModel.depositCurrency,
200 - (CryptoCurrency currency) =>
201 - _onCurrencyChange(currency, widget.exchangeViewModel, depositKey));
200 + (_) => widget.exchangeViewModel.depositCurrency,
201 + (currency) => _onCurrencyChange(currency, widget.exchangeViewModel, depositKey),
202 + );
203
204 reaction((_) => widget.exchangeViewModel.depositAddress, (String address) {
205 if (depositKey.currentState!.addressController.text != address) {
@@ -206,7 +207,7 @@ class _NewSwapPageState extends State<NewSwapPage> {
207 }
208 });
209
209 - reaction((_) => widget.exchangeViewModel.isDepositAddressEnabled, (bool isEnabled) {});
210 + reaction((_) => widget.exchangeViewModel.isDepositAddressEnabled, (isEnabled) {});
211
212 reaction((_) => widget.exchangeViewModel.receiveAmount, (String amount) {
213 if (receiveKey.currentState!.amountController.text != amount) {
@@ -398,14 +399,12 @@ class _NewSwapPageState extends State<NewSwapPage> {
399 if (key == depositKey && !isCurrentTypeWallet) exchangeViewModel.isSendFromExternal = true;
400 if (key == depositKey && isCurrentTypeWallet) exchangeViewModel.isSendFromExternal = false;
401
401 -
402 - key.currentState!.changeSelectedCurrency(currency);
403 -
404 - if (key == depositKey)
402 + if (key == depositKey) {
403 key.currentState!.changeAddress(
406 - address: isCurrentTypeWallet
407 - ? exchangeViewModel.wallet.walletAddresses.addressForExchange
408 - : '');
404 + address:
405 + isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.addressForExchange : '',
406 + );
407 + }
408 }
409
410 void _onWalletNameChange(ExchangeViewModel exchangeViewModel, CryptoCurrency currency,
@@ -492,7 +491,7 @@ class _NewSwapPageState extends State<NewSwapPage> {
491 child: Form(
492 key: formKey,
493 child: SingleChildScrollView(
495 - physics: ClampingScrollPhysics(),
494 + physics: const ClampingScrollPhysics(),
495 controller: ModalScrollController.of(context),
496 child: Column(
497 children: [
@@ -508,7 +507,8 @@ class _NewSwapPageState extends State<NewSwapPage> {
507 : null,
508 key: depositKey,
509 title: S.of(context).send,
511 - initialCurrency: widget.exchangeViewModel.depositCurrency,
510 + currency: widget.exchangeViewModel.depositCurrency,
511 + useBaseUnit: widget.exchangeViewModel.useDepositBaseUnit,
512 hasRefundAddress: true,
513 currencies: widget.exchangeViewModel.depositCurrencies,
514 onCurrencySelected: (currency) {
@@ -522,11 +522,13 @@ class _NewSwapPageState extends State<NewSwapPage> {
522 value != S.of(context).all
523 ? AmountValidator(
524 isAutovalidate: true,
525 - currency: widget.exchangeViewModel.isFixedRateMode
525 + currency: widget.exchangeViewModel.isFixedRateMode
526 ? widget.exchangeViewModel.receiveCurrency
527 : widget.exchangeViewModel.depositCurrency,
528 - minValue: widget.exchangeViewModel.limits.min.toString(),
529 - maxValue: widget.exchangeViewModel.limits.max.toString(),
528 + minValue:
529 + widget.exchangeViewModel.limits.min.toString(),
530 + maxValue:
531 + widget.exchangeViewModel.limits.max.toString(),
532 amountParsingProxy:
533 widget.exchangeViewModel.amountParsingProxy,
534 ).call(value)
@@ -579,8 +581,9 @@ class _NewSwapPageState extends State<NewSwapPage> {
581 onDispose: disposeBestRateSync,
582 key: receiveKey,
583 title: S.of(context).receive,
582 - initialCurrency: widget.exchangeViewModel.receiveCurrency,
584 currencies: widget.exchangeViewModel.receiveCurrencies,
585 + currency: widget.exchangeViewModel.receiveCurrency,
586 + useBaseUnit: widget.exchangeViewModel.useReceiveBaseUnit,
587 onCurrencySelected: (currency) {
588 if (currency is CryptoCurrency) {
589 widget.exchangeViewModel
@@ -685,7 +688,6 @@ class _NewSwapPageState extends State<NewSwapPage> {
688 isLoading: widget.exchangeViewModel.tradeState is TradeIsCreating,
689 ),
690 ),
688 - SizedBox()
691 ],
692 ),
693 )
@@ -701,18 +703,14 @@ class _NewSwapPageState extends State<NewSwapPage> {
703 }
704
705 bool _swapButtonDisabled() {
704 - if (widget.exchangeViewModel.selectedProviders.isEmpty) {
705 - return true;
706 - }
707 - if (widget.exchangeViewModel.receiveAddress.isEmpty) {
708 - return true;
709 - }
710 - if (widget.exchangeViewModel.status is! SyncedSyncStatus) {
711 - return true;
712 - }
713 - if (widget.exchangeViewModel.depositAmount.isEmpty) {
714 - return true;
715 - }
706 + if (widget.exchangeViewModel.selectedProviders.isEmpty) return true;
707 +
708 + if (widget.exchangeViewModel.receiveAddress.isEmpty) return true;
709 +
710 + if (widget.exchangeViewModel.status is! SyncedSyncStatus) return true;
711 +
712 + if (widget.exchangeViewModel.depositAmount.isEmpty) return true;
713 +
714 return false;
715 }
716 }
@@ -725,9 +723,7 @@ class SwapProviderPreview extends StatelessWidget {
723 @override
724 Widget build(BuildContext context) {
725 return Observer(builder: (_) {
728 - if (exchangeViewModel.depositAmount.isEmpty) {
729 - return SizedBox.shrink();
730 - }
726 + if (exchangeViewModel.depositAmount.isEmpty) return SizedBox.shrink();
727
728 final provider = exchangeViewModel.forcedProvider ?? exchangeViewModel.providerDisplay;
729 final rate = exchangeViewModel.forcedProvider == null
@@ -738,8 +734,10 @@ class SwapProviderPreview extends StatelessWidget {
734 onTap: () {
735 if (provider != null) {
736 Navigator.of(context).push(CupertinoPageRoute(
741 - builder: (context) =>
742 - Material(child: ProviderSelectorPage(exchangeViewModel: exchangeViewModel))));
737 + builder: (context) => Material(
738 + child: ProviderSelectorPage(exchangeViewModel: exchangeViewModel),
739 + ),
740 + ));
741 }
742 },
743 child: Container(
@@ -757,7 +755,7 @@ class SwapProviderPreview extends StatelessWidget {
755 spacing: 12,
756 children: [
757 if (provider != null)
760 - CakeImageWidget(imageUrl:provider.description.image,width:28,height:28),
758 + CakeImageWidget(imageUrl: provider.description.image, width: 28, height: 28),
759 if (provider == null) CupertinoActivityIndicator(),
760 Text(
761 provider?.title ?? "${S.of(context).finding_provider}...",
@@ -799,7 +797,7 @@ class SwapProviderPreview extends StatelessWidget {
797 class SwapAmountBox extends StatefulWidget {
798 SwapAmountBox({
799 Key? key,
802 - required this.initialCurrency,
800 + required this.currency,
801 required this.currencies,
802 required this.onCurrencySelected,
803 this.currencyValueValidator,
@@ -807,6 +805,7 @@ class SwapAmountBox extends StatefulWidget {
805 this.title = '',
806 this.hasRefundAddress = false,
807 this.hasAllAmount = false,
808 + this.useBaseUnit = false,
809 this.allAmount,
810 this.onPushPasteButton,
811 this.onPushAddressBookButton,
@@ -820,7 +819,8 @@ class SwapAmountBox extends StatefulWidget {
819 final Function(Currency) onCurrencySelected;
820 final String title;
821 final bool isReceiverCard;
823 - final Currency initialCurrency;
822 + final Currency currency;
823 + final bool useBaseUnit;
824 final bool hasRefundAddress;
825 final FormFieldValidator<String>? currencyValueValidator;
826 final FormFieldValidator<String>? addressTextFieldValidator;
@@ -848,10 +848,6 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
848
849 @override
850 void initState() {
851 - _selectedCurrency = widget.initialCurrency;
852 -
853 - super.initState();
854 -
851 if (widget.isReceiverCard) {
852 memoController.text = widget.exchangeViewModel.receiveAddressExtraId;
853
@@ -869,6 +865,8 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
865 }
866 });
867 }
868 +
869 + super.initState();
870 }
871
872 @override
@@ -881,18 +879,18 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
879 super.dispose();
880 }
881
884 - late Currency _selectedCurrency;
882 bool _fiatInputMode = false;
883
884 @override
885 Widget build(BuildContext context) {
889 - final currencyToShow = (_selectedCurrency is CryptoCurrency)
886 + final currencyToShow = (widget.currency is CryptoCurrency)
887 ? widget.exchangeViewModel.amountParsingProxy
891 - .getCryptoSymbol(_selectedCurrency as CryptoCurrency)
892 - : _selectedCurrency.name.toUpperCase();
888 + .getCryptoSymbol(widget.currency as CryptoCurrency)
889 + : widget.currency.name.toUpperCase();
890
894 -
895 - final chainIconPath = (_selectedCurrency is CryptoCurrency) ? _getCurrencyChainIconPath(_selectedCurrency as CryptoCurrency) : null;
891 + final chainIconPath = (widget.currency is CryptoCurrency)
892 + ? _getCurrencyChainIconPath(widget.currency as CryptoCurrency)
893 + : null;
894
895 return Column(
896 spacing: 12,
@@ -928,36 +926,48 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
926 children: [
927 Flexible(
928 child: IntrinsicWidth(
931 - child: TextFormField(
932 - keyboardType:
933 - TextInputType.numberWithOptions(signed: false, decimal: true),
934 - validator: _fiatInputMode ? null : widget.currencyValueValidator,
935 - controller:
936 - _fiatInputMode ? fiatAmountController : amountController,
937 - focusNode: amountFocusNode,
938 - style: TextStyle(
929 + child: TextFormField(
930 + keyboardType: TextInputType.numberWithOptions(
931 + signed: false,
932 + decimal: !widget.useBaseUnit,
933 + ),
934 + validator: _fiatInputMode ? null : widget.currencyValueValidator,
935 + controller:
936 + _fiatInputMode ? fiatAmountController : amountController,
937 + focusNode: amountFocusNode,
938 + style: TextStyle(
939 fontSize: 28,
940 color: Theme.of(context).colorScheme.onSurface,
941 - fontWeight: FontWeight.w400),
942 - decoration: InputDecoration(
943 - contentPadding: EdgeInsets.zero,
944 - isDense: true,
945 - hintText: "0",
946 - fillColor: Colors.transparent,
947 - hoverColor: Colors.transparent,
948 - focusedBorder: InputBorder.none,
949 - enabledBorder: InputBorder.none,
941 + fontWeight: FontWeight.w400,
942 + ),
943 + decoration: InputDecoration(
944 + contentPadding: EdgeInsets.zero,
945 + isDense: true,
946 + hintText: "0",
947 + fillColor: Colors.transparent,
948 + hoverColor: Colors.transparent,
949 + focusedBorder: InputBorder.none,
950 + enabledBorder: InputBorder.none,
951 + ),
952 + inputFormatters: <TextInputFormatter>[
953 + DecimalInputFormatter(
954 + maxDecimals:
955 + widget.useBaseUnit ? 0 : widget.currency.decimals,
956 + ),
957 + ],
958 ),
951 - )),
959 + ),
960 ),
961 if (_fiatInputMode)
962 Center(
955 - child: Text(
956 - widget.exchangeViewModel.fiat.title,
957 - style: TextStyle(
963 + child: Text(
964 + widget.exchangeViewModel.fiat.title,
965 + style: TextStyle(
966 fontSize: 16,
959 - color: Theme.of(context).colorScheme.onSurfaceVariant),
960 - )),
967 + color: Theme.of(context).colorScheme.onSurfaceVariant,
968 + ),
969 + ),
970 + ),
971 ],
972 ),
973 ),
@@ -966,17 +976,19 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
976 child: Container(
977 decoration: BoxDecoration(
978 color: Theme.of(context).colorScheme.surface,
969 - borderRadius: BorderRadius.circular(999999)),
979 + borderRadius: BorderRadius.circular(999999),
980 + ),
981 child: Padding(
982 padding: const EdgeInsets.only(top: 4.0, bottom: 4, left: 4, right: 4),
983 child: Row(
984 mainAxisAlignment: MainAxisAlignment.center,
985 children: [
975 - TokenImageWidget(
976 - imageUrl: _selectedCurrency.iconPath ?? "",
977 - size: 28,
986 + CakeImageWidget(
987 + imageUrl: widget.currency.iconPath ?? "",
988 + width: 28,
989 + height: 28,
990 ),
979 - SizedBox(width: 10),
991 + const SizedBox(width: 10),
992 Text(
993 currencyToShow,
994 textAlign: TextAlign.center,
@@ -988,12 +1000,13 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
1000 width: 12,
1001 height: 12,
1002 colorFilter: ColorFilter.mode(
991 - Theme.of(context).colorScheme.onSurfaceVariant,
992 - BlendMode.srcIn),
1003 + Theme.of(context).colorScheme.onSurfaceVariant,
1004 + BlendMode.srcIn,
1005 + ),
1006 ),
994 - SizedBox(width: 6)
1007 + const SizedBox(width: 6)
1008 ] else
996 - SizedBox(width: 10),
1009 + const SizedBox(width: 10),
1010 Container(
1011 width:16,height:16,
1012 decoration: BoxDecoration(
@@ -1003,17 +1016,20 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
1016 child: Padding(
1017 padding: const EdgeInsets.all(4.0),
1018 child: RotatedBox(
1006 - quarterTurns: 2,
1007 - child: CakeImageWidget(imageUrl:
1008 - "assets/new-ui/dropdown_arrow.svg",
1009 - width: 4,
1010 - height: 4,
1011 - colorFilter: ColorFilter.mode(
1012 - Theme.of(context).colorScheme.primary, BlendMode.srcIn),
1013 - )),
1019 + quarterTurns: 2,
1020 + child: CakeImageWidget(
1021 + imageUrl: "assets/new-ui/dropdown_arrow.svg",
1022 + width: 4,
1023 + height: 4,
1024 + colorFilter: ColorFilter.mode(
1025 + Theme.of(context).colorScheme.primary,
1026 + BlendMode.srcIn,
1027 + ),
1028 + ),
1029 + ),
1030 ),
1031 ),
1016 - SizedBox(width:4),
1032 + const SizedBox(width: 4),
1033 ],
1034 ),
1035 ),
@@ -1029,36 +1045,33 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
1045 !widget.exchangeViewModel.hasAllAmount);
1046
1047 return FiatAmountBar(
1032 - foregroundElementColor: Theme.of(context).colorScheme.surfaceContainerHigh,
1033 - textColor: Theme.of(context).colorScheme.onSurfaceVariant,
1034 - allAmountColor: hasAllAmount ? null : Colors.transparent,
1035 - allAmountTextColor:
1036 - hasAllAmount ? null : Theme.of(context).colorScheme.onSurfaceVariant,
1037 - fiatInputMode: _fiatInputMode,
1038 - allAmount: !hasAllAmount
1039 - ? widget.isReceiverCard
1040 - ? null
1041 - : widget.exchangeViewModel.balanceDisplay
1042 - : widget.exchangeViewModel.depositAvailableAmount,
1043 - onSwitchButtonPressed: () {
1044 - setState(() {
1045 - _fiatInputMode = !_fiatInputMode;
1046 - });
1047 - if (_fiatInputMode) {
1048 - updateFiatAmount();
1049 - }
1050 - },
1051 - onAllButtonPressed: widget.allAmount,
1052 - cryptoAmount: widget.isReceiverCard
1053 - ? widget.exchangeViewModel.roundedReceiveAmount(6)
1054 - : widget.exchangeViewModel.roundedDepositAmount(6),
1055 - fiatAmount: widget.isReceiverCard
1056 - ? widget.exchangeViewModel.roundedReceiveAmountFiat(6)
1057 - : widget.exchangeViewModel.roundedDepositAmountFiat(6),
1058 - cryptoCurrency: widget.isReceiverCard
1059 - ? widget.exchangeViewModel.receiveCurrency.title
1060 - : widget.exchangeViewModel.depositCurrency.title,
1061 - fiatCurrency: widget.exchangeViewModel.fiat.name);
1048 + foregroundElementColor: Theme.of(context).colorScheme.surfaceContainerHigh,
1049 + textColor: Theme.of(context).colorScheme.onSurfaceVariant,
1050 + allAmountColor: hasAllAmount ? null : Colors.transparent,
1051 + allAmountTextColor:
1052 + hasAllAmount ? null : Theme.of(context).colorScheme.onSurfaceVariant,
1053 + fiatInputMode: _fiatInputMode,
1054 + allAmount: !hasAllAmount
1055 + ? widget.isReceiverCard
1056 + ? null
1057 + : widget.exchangeViewModel.balanceDisplay
1058 + : widget.exchangeViewModel.depositAvailableAmount,
1059 + onSwitchButtonPressed: () {
1060 + setState(() => _fiatInputMode = !_fiatInputMode);
1061 + if (_fiatInputMode) {
1062 + updateFiatAmount();
1063 + }
1064 + },
1065 + onAllButtonPressed: widget.allAmount,
1066 + cryptoAmount: widget.isReceiverCard
1067 + ? widget.exchangeViewModel.roundedReceiveAmount(6)
1068 + : widget.exchangeViewModel.roundedDepositAmount(6),
1069 + fiatAmount: widget.isReceiverCard
1070 + ? widget.exchangeViewModel.roundedReceiveAmountFiat(6)
1071 + : widget.exchangeViewModel.roundedDepositAmountFiat(6),
1072 + cryptoCurrencySymbol: currencyToShow,
1073 + fiatCurrencySymbol: widget.exchangeViewModel.fiat.symbol,
1074 + );
1075 },
1076 ),
1077 Observer(
@@ -1107,22 +1120,25 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
1120 ? Theme.of(context).colorScheme.onPrimary
1121 : Theme.of(context).colorScheme.onSurface),
1122 ),
1110 - Text(addressDescription,
1111 - style: TextStyle(
1112 - fontSize: 12,
1113 - fontWeight: FontWeight.w500,
1114 - color: Theme.of(context).colorScheme.primary))
1123 + Text(
1124 + addressDescription,
1125 + style: TextStyle(
1126 + fontSize: 12,
1127 + fontWeight: FontWeight.w500,
1128 + color: Theme.of(context).colorScheme.primary),
1129 + )
1130 ],
1131 ),
1132 RotatedBox(
1133 quarterTurns: 2,
1119 - child: CakeImageWidget(imageUrl:
1120 - "assets/new-ui/dropdown_arrow.svg",
1134 + child: CakeImageWidget(
1135 + imageUrl: "assets/new-ui/dropdown_arrow.svg",
1136 colorFilter: ColorFilter.mode(
1122 - (addressEmpty && widget.isReceiverCard)
1123 - ? Theme.of(context).colorScheme.onPrimary
1124 - : Theme.of(context).colorScheme.onSurface,
1125 - BlendMode.srcIn),
1137 + (addressEmpty && widget.isReceiverCard)
1138 + ? Theme.of(context).colorScheme.onPrimary
1139 + : Theme.of(context).colorScheme.onSurface,
1140 + BlendMode.srcIn,
1141 + ),
1142 ),
1143 )
1144 ],
@@ -1146,18 +1162,14 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
1162 widget.exchangeViewModel.receiveAddress.isEmpty) ...[
1163 ModernButton.svg(
1164 svgPath: "assets/new-ui/paste.svg",
1149 - onPressed: () {
1150 - widget.onPushPasteButton?.call(context);
1151 - },
1165 + onPressed: () => widget.onPushPasteButton?.call(context),
1166 size: 36,
1167 iconSize: 20,
1168 backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
1169 ),
1170 ModernButton.svg(
1171 svgPath: "assets/new-ui/scan.svg",
1158 - onPressed: () {
1159 - _presentQRScanner(context);
1160 - },
1172 + onPressed: () => _presentQRScanner(context),
1173 size: 36,
1174 iconSize: 20,
1175 backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
@@ -1172,7 +1184,7 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
1184 final selected = widget.exchangeViewModel.receiveCurrency;
1185 final labelType = memoLabelTypeFor(selected);
1186 if (labelType == null) return const SizedBox.shrink();
1175 -
1187 +
1188 final isDestinationTag = labelType == MemoLabelType.destinationTag;
1189 final hint = isDestinationTag
1190 ? S.of(context).destination_tag_optional
@@ -1180,7 +1192,7 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
1192 final disclaimer = isDestinationTag
1193 ? S.of(context).destination_tag_swap_disclaimer
1194 : S.of(context).memo_swap_disclaimer;
1183 -
1195 +
1196 return NewSendMemoInput(
1197 memoController: memoController,
1198 maxMemoLength: isDestinationTag ? 20 : 256,
@@ -1197,10 +1209,6 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
1209 );
1210 }
1211
1200 - void changeSelectedCurrency(Currency currency) {
1201 - setState(() => _selectedCurrency = currency);
1202 - }
1203 -
1212 void changeAddress({required String address}) {
1213 setState(() => addressController.text = _normalizeAddressFormat(address));
1214 }
@@ -1343,18 +1351,15 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
1351 }
1352
1353 String? _getCurrencyChainIconPath(CryptoCurrency curr) {
1346 - try{
1347 -
1348 - if(curr.chainIconPath != null) return curr.chainIconPath!;
1349 -
1354 + try {
1355 + if (curr.chainIconPath != null) return curr.chainIconPath!;
1356
1351 - if(curr.tag != null) {
1357 + if (curr.tag != null) {
1358 final currencyFromTag = CryptoCurrency.fromString(curr.tag!);
1353 - if(currencyFromTag.chainIconPath != null) {
1354 - return currencyFromTag.chainIconPath!;
1355 - }
1359 +
1360 + if (currencyFromTag.chainIconPath != null) return currencyFromTag.chainIconPath!;
1361 }
1357 - }catch(_){}
1362 + } catch (_) {}
1363 return null;
1364 }
1365 }
lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart
+2 -3
@@ -24,7 +24,6 @@ import 'package:flutter/material.dart';
24 import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
25
26 import '../../../pages/receive_page.dart';
27 -import '../../../pages/scan_page.dart';
27 import 'coin_action_button.dart';
28
29 class CoinActionRow extends StatelessWidget {
@@ -165,11 +164,11 @@ class CoinActionRow extends StatelessWidget {
164 var unspentCoinType = UnspentCoinType.any;
165 if (SendViewModelBase.isNonZeroAmountLightningInvoice(code)) {
166 unspentCoinType = UnspentCoinType.lightning;
168 - final amount = CryptoCurrency.btcln.formatAmount(BigInt.from(getBolt11Amount(code) ?? 0));
167 + final amount = getBolt11Amount(code)?.toString() ?? "0";
168 req = PaymentRequest(code, amount, "", "", "");
169 } else if (SendViewModelBase.isLnurlInvoice(code)) {
170 unspentCoinType = UnspentCoinType.lightning;
172 - final amount = CryptoCurrency.btcln.formatAmount(BigInt.from(await LNURL.getPayRequestAmount(code) ?? 0));
171 + final amount = (await LNURL.getPayRequestAmount(code))?.toString() ?? "0";
172 req = PaymentRequest(code, amount, "", "", "");
173 } else if (OpenCryptoPayService.isOpenCryptoPayQR(code)) {
174 req = PaymentRequest(code, "", "", "", "");
lib/new-ui/widgets/coins_page/assets_history/history_section.dart
+7 -5
@@ -1,4 +1,5 @@
1 import 'package:cake_wallet/di.dart';
2 +import 'package:cake_wallet/entities/balance_display_mode.dart';
3 import 'package:cake_wallet/generated/i18n.dart';
4 import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/anonpay_history_tile.dart';
5 import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_order_tile.dart';
@@ -15,6 +16,7 @@ import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
16 import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
17 import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
18 import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
19 +import 'package:cw_core/amount/money.dart';
20 import 'package:cw_core/crypto_currency.dart';
21 import 'package:cw_core/sync_status.dart';
22 import 'package:flutter/material.dart';
@@ -111,8 +113,8 @@ class HistorySection extends StatelessWidget {
113 provider: trade.provider,
114 date:
115 DateFormat('HH:mm').format(item.trade.createdAt ?? DateTime.now()),
114 - amount: trade.amountFormatted(),
115 - receiveAmount: trade.receiveAmountFormatted(),
116 + amount: dashboardViewModel.balanceDisplayMode == BalanceDisplayMode.hiddenBalance ? "---" : trade.amountFormatted(),
117 + receiveAmount: dashboardViewModel.balanceDisplayMode == BalanceDisplayMode.hiddenBalance ? "---" : trade.receiveAmountFormatted(),
118 roundedBottom: roundedBottom,
119 roundedTop: roundedTop,
120 bottomSeparator: !roundedBottom,
@@ -132,7 +134,7 @@ class HistorySection extends StatelessWidget {
134 child: HistoryOrderTile(
135 date: DateFormat('HH:mm').format(item.order.createdAt),
136 amount: item.orderFormattedAmount,
135 - amountFiat: "USD 0.00",
137 + amountFiat: "",
138 roundedBottom: roundedBottom,
139 roundedTop: roundedTop,
140 bottomSeparator: !roundedBottom,
@@ -149,8 +151,8 @@ class HistorySection extends StatelessWidget {
151 child: PayjoinHistoryTile(
152 createdAt: DateFormat('HH:mm').format(session.inProgressSince!),
153 amount: dashboardViewModel.appStore.amountParsingProxy
152 - .getDisplayCryptoString(
153 - session.amount.toInt(), CryptoCurrency.btc),
154 + .asDisplayString(Money(
155 + session.amount, CryptoCurrency.btc)),
156 currency: item.transaction?.from ?? "BTC",
157 state: item.status,
158 isSending: session.isSenderSession,
lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart
+6 -4
@@ -47,6 +47,8 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
47
48 @override
49 Widget build(BuildContext context) {
50 + final transactionInfoAmount = widget.transactionDetailsViewModel.transactionInfo.amount;
51 +
52 return DraggableScrollableSheet(
53 expand: false,
54 initialChildSize: 0.9,
@@ -90,12 +92,12 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
92 ),
93 CopyWrapper(
94 requireLongPress: true,
93 - data: ClipboardData(text: widget.transactionDetailsViewModel.formattedCryptoAmount),
95 + data: ClipboardData(text: transactionInfoAmount.toString()),
96 builder: (context, copied)=> AnimatedSwitcher(
97 duration: Duration(milliseconds: 300),
98 child: Text(
99 key: ValueKey(copied),
98 - copied ? S.of(context).copied : widget.transactionDetailsViewModel.formattedCryptoAmount,
100 + copied ? S.of(context).copied : transactionInfoAmount.toStringWithSymbol(),
101 style: TextStyle(fontSize: 28, color: copied ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface),
102 ),
103 ),
@@ -122,7 +124,7 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
124 label: item.title,
125 trailingWidget: shouldBuildBottomWidget
126 ? null
125 - : _buildTrailingWIdget(item),
127 + : _buildTrailingWidget(item),
128 bottomWidget: shouldBuildBottomWidget
129 ? _buildBottomWidget(item)
130 : null);
@@ -203,7 +205,7 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
205 ));
206 }
207
206 - Widget _buildTrailingWIdget(TransactionDetailsListItem item) {
208 + Widget _buildTrailingWidget(TransactionDetailsListItem item) {
209 return Padding(
210 padding: const EdgeInsets.symmetric(vertical: 4.0),
211 child: switch (item.runtimeType) {
lib/new-ui/widgets/coins_page/cards/balance_card.dart
+1 -3
@@ -70,8 +70,6 @@ class BalanceCard extends StatelessWidget {
70
71 final height = width * 0.62;
72
73 -
74 -
73 return AnimatedContainer(
74 duration: designSwitchDuration,
75 width: width,
@@ -208,7 +206,7 @@ class BalanceCard extends StatelessWidget {
206 },
207 child: Text(
208 key: ValueKey(fiatFirst ? balance : fiatBalance),
211 - fiatFirst ? "$assetName $balance" : "$fiatCurrencyTitle $fiatBalance",
209 + fiatFirst ? "$assetName $balance" : fiatBalance,
210 ),
211 ),
212 ),
lib/new-ui/widgets/coins_page/cards/cards_view.dart
+7 -7
@@ -119,23 +119,22 @@ class _CardsViewState extends State<CardsView> {
119 late final String walletBalance;
120 late final String walletFiatBalance;
121 if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) {
122 - if(widget.dashboardViewModel.balanceViewModel.displayMode == BalanceDisplayMode.hiddenBalance) {
122 + if (widget.dashboardViewModel.balanceViewModel.displayMode ==
123 + BalanceDisplayMode.hiddenBalance) {
124 walletBalance = '●●●●●●';
125 walletFiatBalance = '●●●●●●';
126 } else {
127 walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0";
128 walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00";
129 }
129 - } else if(widget.dashboardViewModel.balanceViewModel.showCombinedBalance){
130 + } else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) {
131 walletBalance = "";
132 walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance;
132 -
133 - }else {
133 + } else {
134 walletBalance = walletBalanceRecord?.availableBalance ?? "0";
135 - walletFiatBalance = walletBalanceRecord?.fiatAvailableBalanceRaw ?? "0.00";
135 + walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00";
136 }
137
138 -
138 // the card designs is empty if widget gets built before it loads.
139 // should get populated before user sees anything
140 final CardDesign cardDesign;
@@ -191,7 +190,8 @@ class _CardsViewState extends State<CardsView> {
190 assetName: assetName,
191 capitalizeAssetName: _shouldCapitalizeAssetName(),
192 balance: walletBalance,
194 - fiatCurrencyTitle: walletBalanceRecord?.fiatCurrencyTicker ?? widget.dashboardViewModel.settingsStore.fiatCurrency.title,
193 + fiatCurrencyTitle: walletBalanceRecord?.fiatCurrency?.title ??
194 + widget.dashboardViewModel.settingsStore.fiatCurrency.title,
195 fiatFirst: widget.dashboardViewModel.balanceViewModel.showCombinedBalance,
196 fiatBalance: walletFiatBalance,
197 selected: _selectedIndex == visualIndex,
lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart
+5 -8
@@ -48,12 +48,10 @@ class UnconfirmedBalanceWidget extends StatelessWidget {
48 showMaterialModalBottomSheet(
49 backgroundColor: Colors.transparent,
50 context: context,
51 - builder: (context) {
52 - return UnconfirmedBalanceModal(
53 - balance: "${balance} ${currency.title}",
51 + builder: (context) => UnconfirmedBalanceModal(
52 + balance: balance.toStringWithSymbol(),
53 currencyIconPath: currency.iconPath ?? "",
55 - );
56 - });
54 + ));
55 },
56 child: Padding(
57 padding: const EdgeInsets.symmetric(horizontal: 12.0),
@@ -101,9 +99,8 @@ class UnconfirmedBalanceWidget extends StatelessWidget {
99 ),
100 ],
101 )
104 - : const SizedBox(
105 - width: double.infinity,
106 - ));
102 + : const SizedBox(width: double.infinity),
103 + );
104 });
105 }
106 }
lib/new-ui/widgets/receive_page/receive_amount_modal.dart
+12 -2
@@ -7,6 +7,7 @@ import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
7 import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
8 import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
9 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
10 +import 'package:cake_wallet/utils/decimal_input_formatter.dart';
11 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
12 import 'package:cw_core/crypto_currency.dart';
13 import 'package:flutter/material.dart';
@@ -134,8 +135,16 @@ class _ReceiveAmountModalState extends State<ReceiveAmountModal> {
135 textAlign: TextAlign.left,
136 textAlignVertical: TextAlignVertical.center,
137 controller: _amountController,
137 - keyboardType:
138 - TextInputType.numberWithOptions(signed: false, decimal: !widget.walletAddressListViewModel.useSatoshi),
138 + keyboardType: TextInputType.numberWithOptions(
139 + signed: false,
140 + decimal: widget.walletAddressListViewModel.selectedCurrencyDecimals > 0,
141 + ),
142 + inputFormatters: [
143 + DecimalInputFormatter(
144 + maxDecimals:
145 + widget.walletAddressListViewModel.selectedCurrencyDecimals,
146 + ),
147 + ],
148 decoration: InputDecoration(
149 hint: Text(
150 widget.walletAddressListViewModel.useSatoshi ? "0" : "0.00000000",
@@ -150,6 +159,7 @@ class _ReceiveAmountModalState extends State<ReceiveAmountModal> {
159 border: InputBorder.none,
160 filled: true,
161 fillColor: Colors.transparent,
162 +
163 ),
164 style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
165 ),
lib/new-ui/widgets/send_page/fiat_amount_bar.dart
+13 -15
@@ -2,7 +2,6 @@ import 'package:cake_wallet/generated/i18n.dart';
2 import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
3 import 'package:cw_core/crypto_amount_format.dart';
4 import 'package:flutter/material.dart';
5 -import 'package:flutter_mobx/flutter_mobx.dart';
5
6 class FiatAmountBar extends StatelessWidget {
7 const FiatAmountBar(
@@ -12,8 +11,8 @@ class FiatAmountBar extends StatelessWidget {
11 this.onAllButtonPressed,
12 required this.cryptoAmount,
13 required this.fiatAmount,
15 - required this.cryptoCurrency,
16 - required this.fiatCurrency,
14 + required this.cryptoCurrencySymbol,
15 + required this.fiatCurrencySymbol,
16 this.allAmount,
17 this.foregroundElementColor,
18 this.textColor, this.allAmountColor, this.allAmountTextColor});
@@ -24,8 +23,8 @@ class FiatAmountBar extends StatelessWidget {
23
24 final String cryptoAmount;
25 final String fiatAmount;
27 - final String cryptoCurrency;
28 - final String fiatCurrency;
26 + final String cryptoCurrencySymbol;
27 + final String fiatCurrencySymbol;
28 final String? allAmount;
29 final Color? foregroundElementColor;
30 final Color? textColor;
@@ -47,16 +46,15 @@ class FiatAmountBar extends StatelessWidget {
46 iconSize: 18,
47 onPressed: onSwitchButtonPressed,
48 ),
50 - Observer(
51 - builder: (_) => GestureDetector(
52 - onTap: onSwitchButtonPressed,
53 - child: Text(
54 - fiatInputMode
55 - ? "${cryptoAmount.isEmpty ? "0" : cryptoAmount.withMaxDecimals(8)} ${cryptoCurrency}"
56 - : "${fiatAmount.isEmpty ? "0" : fiatAmount} ${fiatCurrency}",
57 - style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface),
58 - ),
59 - )),
49 + GestureDetector(
50 + onTap: onSwitchButtonPressed,
51 + child: Text(
52 + fiatInputMode
53 + ? "${cryptoAmount.isEmpty ? "0" : cryptoAmount.withMaxDecimals(8)} $cryptoCurrencySymbol"
54 + : "${fiatAmount.isEmpty ? "0" : fiatAmount} $fiatCurrencySymbol",
55 + style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface),
56 + ),
57 + ),
58 ],
59 ),
60 if (allAmount != null && allAmount!.isNotEmpty)
lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart
-1
@@ -18,7 +18,6 @@ import 'package:cw_core/wallet_info.dart';
18 import 'package:cw_core/wallet_type.dart';
19 import 'package:flutter/cupertino.dart';
20 import 'package:flutter/material.dart';
21 -import 'package:flutter_svg/svg.dart';
21 import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
22
23 enum l2actions { deposit, withdraw }
lib/new-ui/widgets/send_page/send_amount_input.dart
+15 -7
@@ -1,6 +1,7 @@
1 import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
2 import 'package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart';
3 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
4 +import 'package:cake_wallet/utils/decimal_input_formatter.dart';
5 import 'package:flutter/material.dart';
6 import 'package:flutter/services.dart';
7 import 'package:flutter_mobx/flutter_mobx.dart';
@@ -9,15 +10,18 @@ class NewSendAmountInput extends StatefulWidget {
10 const NewSendAmountInput(
11 {super.key,
12 required this.currency,
13 + required this.maxDecimals,
14 required this.hasPicker,
15 required this.onPickerClicked,
16 required this.currencyIconPath,
17 required this.amountController,
16 - this.validator});
18 + this.validator,
19 + });
20
21 final String currency;
22 final String currencyIconPath;
23 final bool hasPicker;
24 + final int maxDecimals;
25 final VoidCallback onPickerClicked;
26 final TextEditingController amountController;
27 final FormFieldValidator<String>? validator;
@@ -63,14 +67,19 @@ class _NewSendAmountInputState extends State<NewSendAmountInput> {
67 children: [
68 Expanded(
69 child: TextField(
66 - keyboardType:
67 - TextInputType.numberWithOptions(signed: false, decimal: true),
70 + keyboardType: TextInputType.numberWithOptions(
71 + signed: false,
72 + decimal: widget.maxDecimals > 0,
73 + ),
74 inputFormatters: <TextInputFormatter>[
69 - FilteringTextInputFormatter.allow(RegExp(r'^\d*[.,]?\d*$'))
75 + DecimalInputFormatter(maxDecimals: widget.maxDecimals),
76 ],
71 - onChanged: state.didChange,
77 controller: widget.amountController,
73 - decoration: InputDecoration(hintText: "0", errorMaxLines: 3),
78 + decoration: InputDecoration(
79 + hintText: widget.maxDecimals > 0 ? "0" : "0.00",
80 + errorMaxLines: 3,
81 + ),
82 + onChanged: state.didChange,
83 ),
84 ),
85 FloatingIconButton(
@@ -81,7 +90,6 @@ class _NewSendAmountInputState extends State<NewSendAmountInput> {
90 widget.amountController.text = data.text!;
91 }
92 }),
84 - SizedBox.shrink()
93 ],
94 ),
95 ),
lib/new-ui/widgets/send_page/send_confirm_sheet.dart
+200 -198
@@ -17,13 +17,13 @@ import 'package:cake_wallet/utils/address_formatter.dart';
17 import 'package:cake_wallet/view_model/send/send_view_model.dart';
18 import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
19 import 'package:cake_wallet/bitcoin/bitcoin.dart';
20 +import 'package:cw_core/amount/money.dart';
21 import 'package:cw_core/cake_hive.dart';
22 import 'package:cw_core/crypto_amount_format.dart';
23 import 'package:cw_core/crypto_currency.dart';
24 import 'package:flutter/cupertino.dart';
25 import 'package:flutter/material.dart';
26 import 'package:flutter_mobx/flutter_mobx.dart';
26 -import 'package:flutter_svg/flutter_svg.dart';
27 import 'package:mobx/mobx.dart';
28
29 class SendConfirmSheet extends StatefulWidget {
@@ -173,8 +173,8 @@ class SendTransactionDetails extends StatelessWidget {
173 double sumBy<T>(List<T> list, double Function(T) picker) =>
174 list.map(picker).fold(0.0, (a, b) => a + b);
175
176 - BigInt sumByBigInt<T>(List<T> list, BigInt Function(T) picker) =>
177 - list.map(picker).fold(BigInt.zero, (a, b) => a + b);
176 + Money sumByMoney<T>(List<T> list, Money Function(T) picker, CryptoCurrency currency) =>
177 + list.map(picker).fold(Money.zero(currency), (a, b) => a + b);
178
179 String sumStr<T>(List<T> list, double Function(T) picker) =>
180 sumBy(list, picker).toString();
@@ -184,220 +184,188 @@ class SendTransactionDetails extends StatelessWidget {
184 return "${decimals == null ? str : str.withDecimals(decimals)} $unit";
185 }
186
187 -
187 Widget _buildMainContent(BuildContext context) {
189 - final transaction = sendViewModel.pendingTransaction;
188 + return Observer(builder: (context) {
189 + final transaction = sendViewModel.pendingTransaction;
190
191 - final currencySymbol = sendViewModel.amountParsingProxy.getCryptoSymbol(sendViewModel.selectedCryptoCurrency);
191 + final currencySymbol =
192 + sendViewModel.amountParsingProxy.getCryptoSymbol(sendViewModel.selectedCryptoCurrency);
193
193 - final amount = (transaction == null)
194 - ? sendViewModel.amountParsingProxy.getDisplayCryptoStringFromBigInt(
195 - sumByBigInt(sendViewModel.outputs, (o) {
194 + final amount = (transaction == null)
195 + ? sendViewModel.amountParsingProxy.asDisplayString(sumByMoney(sendViewModel.outputs, (o) {
196 + final zero = Money.zero(sendViewModel.selectedCryptoCurrency);
197 if (o.sendAll)
198 return sendViewModel.amountParsingProxy.tryParseCryptoString(
199 sendViewModel.balance, sendViewModel.selectedCryptoCurrency) ??
199 - BigInt.zero;
200 -
201 - return sendViewModel.selectedCryptoCurrency.tryParseAmount(o.cryptoAmount) ??
202 - BigInt.zero;
203 - }),
204 - sendViewModel.selectedCryptoCurrency)
205 - : sendViewModel.amountParsingProxy.getDisplayCryptoAmount(
206 - formatAmount(transaction.amountFormatted), sendViewModel.selectedCryptoCurrency).withMaxDecimals(8);
207 -
208 - final fee =
209 - "${(transaction == null) ? sendViewModel.amountParsingProxy.getDisplayCryptoStringFromBigInt(sumByBigInt(
210 - sendViewModel.outputs.where((e) => !e.sendAll).toList(),
211 - (o) {
212 - if (sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln) {
213 - return sendViewModel.amountParsingProxy.tryParseCryptoString(
214 - o.estimatedFee.replaceAll(",", ""),
215 - sendViewModel.selectedCryptoCurrency) ??
216 - BigInt.zero;
217 - }
218 - return sendViewModel.currency.tryParseAmount(o.estimatedFee.replaceAll(",", "")) ??
219 - BigInt.zero;
220 - },
221 - ), sendViewModel.currency) : sendViewModel.amountParsingProxy.getDisplayCryptoAmount(transaction.feeFormattedValue, sendViewModel.currency)} ${sendViewModel.currencySymbol}";
222 -
223 - final fiatAmount = (transaction == null)
224 - ? sumWithUnit(
225 - sendViewModel.outputs,
226 - (o) => double.tryParse(o.fiatAmount.replaceAll(",", "")) ?? 0,
227 - sendViewModel.fiatCurrency.title,
228 - decimals:2
229 - )
230 - : sendViewModel.pendingTransactionFiatAmountFormatted;
231 -
232 - final fiatFee = (transaction == null)
233 - ? sumWithUnit(
234 - sendViewModel.outputs,
235 - (o) => double.tryParse(o.estimatedFeeFiatAmount.replaceAll(",", "")) ?? 0,
236 - sendViewModel.fiatCurrency.title,
237 - decimals:2
238 - )
239 - : sendViewModel.pendingTransactionFeeFiatAmountFormatted;
240 -
241 - final showAddress = !sendViewModel.outputs.any((e) =>
242 - RegExp(AddressValidator.bolt11InvoiceMatcher).hasMatch(e.address.toLowerCase()) ||
243 - RegExp(AddressValidator.lnurlMatcher).hasMatch(e.address.toLowerCase()) ||
244 - (e.isParsedAddress &&
245 - e.parsedAddress.addresses.isNotEmpty &&
246 - RegExp(AddressValidator.lnurlMatcher)
247 - .hasMatch(e.parsedAddress.addresses.first.toLowerCase())));
248 -
249 - final outputs = sendViewModel.outputs;
250 -
251 - return SingleChildScrollView(
252 - child: Padding(
253 - padding: const EdgeInsets.symmetric(horizontal: 24.0),
254 - child: Column(
255 - crossAxisAlignment: CrossAxisAlignment.start,
256 - mainAxisAlignment: MainAxisAlignment.start,
257 - spacing: 24,
258 - children: [
259 - Column(
260 - children: [
261 - Row(
262 - mainAxisSize: MainAxisSize.max,
263 - mainAxisAlignment: MainAxisAlignment.center,
264 - spacing: 4,
265 - children: [
266 - Text(
267 - amount,
268 - style: TextStyle(
269 - fontSize: 36,
270 - fontWeight: FontWeight.w400,
271 - color: Theme.of(context).colorScheme.onSurface),
272 - ),
273 - Text(currencySymbol,
274 - style: TextStyle(
275 - fontSize: 36,
276 - fontWeight: FontWeight.w400,
277 - color: Theme.of(context).colorScheme.onSurfaceVariant))
278 - ],
279 - ),
280 - Text(
281 - fiatAmount,
282 - style: TextStyle(
283 - fontSize: 20,
284 - fontWeight: FontWeight.w500,
285 - color: Theme.of(context).colorScheme.onSurfaceVariant),
286 - ),
287 - ],
288 - ),
289 - if (outputs.length >= 1 && (outputs.first.extractedAddress.isNotEmpty || outputs.first.address.isNotEmpty) && showAddress)
200 + zero;
201 +
202 + return sendViewModel.selectedCryptoCurrency.tryParseAmount(o.cryptoAmount) ?? zero;
203 + }, sendViewModel.selectedCryptoCurrency))
204 + : sendViewModel.amountParsingProxy.asDisplayString(transaction.amount);
205 +
206 + final fee = (transaction == null)
207 + ? sendViewModel.amountParsingProxy.asDisplayString(sumByMoney(
208 + sendViewModel.outputs,
209 + (o) => o.estimatedFee,
210 + sendViewModel.currency,
211 + ))
212 + : sendViewModel.amountParsingProxy.asDisplayString(transaction.fee);
213 +
214 + final fiatAmount = (transaction == null)
215 + ? sumWithUnit(
216 + sendViewModel.outputs,
217 + (o) => double.tryParse(o.fiatAmount.replaceAll(",", "")) ?? 0,
218 + sendViewModel.fiatCurrency.title,
219 + decimals: 2)
220 + : sendViewModel.pendingTransactionFiatAmountFormatted;
221 +
222 + final fiatFee = (transaction == null)
223 + ? sumWithUnit(
224 + sendViewModel.outputs,
225 + (o) => double.tryParse(o.estimatedFeeFiatAmount.replaceAll(",", "")) ?? 0,
226 + sendViewModel.fiatCurrency.title,
227 + decimals: 2)
228 + : sendViewModel.pendingTransactionFeeFiatAmountFormatted;
229 +
230 + final showAddress = !sendViewModel.outputs.any((e) =>
231 + RegExp(AddressValidator.bolt11InvoiceMatcher).hasMatch(e.address.toLowerCase()) ||
232 + RegExp(AddressValidator.lnurlMatcher).hasMatch(e.address.toLowerCase()) ||
233 + (e.isParsedAddress &&
234 + e.parsedAddress.addresses.isNotEmpty &&
235 + RegExp(AddressValidator.lnurlMatcher)
236 + .hasMatch(e.parsedAddress.addresses.firstOrNull?.toLowerCase() ?? "")));
237 +
238 + final outputs = sendViewModel.outputs;
239 +
240 + return SingleChildScrollView(
241 + child: Padding(
242 + padding: const EdgeInsets.symmetric(horizontal: 24.0),
243 + child: Column(
244 + crossAxisAlignment: CrossAxisAlignment.start,
245 + mainAxisAlignment: MainAxisAlignment.start,
246 + spacing: 24,
247 + children: [
248 Column(
291 - crossAxisAlignment: CrossAxisAlignment.start,
292 - spacing: 12,
249 children: [
250 + Row(
251 + mainAxisSize: MainAxisSize.max,
252 + mainAxisAlignment: MainAxisAlignment.center,
253 + spacing: 4,
254 + children: [
255 + Flexible(
256 + child: Text(
257 + amount,
258 + style: TextStyle(
259 + fontSize: 36,
260 + fontWeight: FontWeight.w400,
261 + color: Theme.of(context).colorScheme.onSurface),
262 + ),
263 + ),
264 + Text(currencySymbol,
265 + style: TextStyle(
266 + fontSize: 36,
267 + fontWeight: FontWeight.w400,
268 + color: Theme.of(context).colorScheme.onSurfaceVariant))
269 + ],
270 + ),
271 Text(
295 - S.of(context).send_to,
272 + fiatAmount,
273 style: TextStyle(
297 - fontSize: 16,
274 + fontSize: 20,
275 fontWeight: FontWeight.w500,
276 color: Theme.of(context).colorScheme.onSurfaceVariant),
277 ),
301 - if (outputs.length == 1)
302 - Container(
303 - decoration: BoxDecoration(
304 - color: Theme.of(context).colorScheme.surfaceContainer,
305 - borderRadius: BorderRadius.circular(16),
306 - ),
307 - child: Padding(
308 - padding: const EdgeInsets.all(12.0),
309 - child: AddressFormatter.buildSegmentedAddress(
310 - address: outputs.first.isParsedAddress ? outputs.first.extractedAddress : outputs.first.address,
311 - evenTextStyle:
312 - TextStyle(color: Theme.of(context).colorScheme.onSurface)),
313 - ),
314 - )
315 - else
316 - AnimatedDropdown(
317 - content: Column(
318 - children: outputs
319 - .map(
320 - (item) => Column(children: [
321 - MultiSendAddressPreview(
322 - index: outputs.indexOf(item) + 1,
323 - address: item.isParsedAddress
324 - ? item.extractedAddress
325 - : item.address,
326 - amount:
327 - "${item.roundedCryptoAmount(8).withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.currency.title}",
328 - fiatAmount:
329 - "${item.fiatAmount.withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.fiatCurrency.title}",
330 - ),
331 - if(item != outputs.last)
332 - Container(width: double.infinity, height: 1, color: Theme.of(context).colorScheme.surfaceContainerHigh)
333 - ],),
334 - )
335 - .toList(),
336 - ),
337 - dropdownText: "${outputs.length} ${S.of(context).addresses}"),
278 ],
279 ),
340 - Container(
341 - decoration: BoxDecoration(
342 - color: Theme.of(context).colorScheme.surfaceContainer,
343 - borderRadius: BorderRadius.circular(16),
344 - ),
345 - child: Column(
346 - children: [
347 - Padding(
348 - padding: const EdgeInsets.all(12.0),
349 - child: Row(
350 - mainAxisAlignment: MainAxisAlignment.spaceBetween,
351 - children: [
352 - Text(S.of(context).fee,
353 - style: TextStyle(
354 - fontSize: 14,
355 - fontWeight: FontWeight.w400,
356 - color: Theme.of(context).colorScheme.onSurface)),
357 - Column(
358 - crossAxisAlignment: CrossAxisAlignment.end,
359 - children: [
360 - Text(
361 - fee.withLocalSeperator(sendViewModel.languageCode),
362 - style: TextStyle(
363 - fontSize: 14,
364 - fontWeight: FontWeight.w400,
365 - color: Theme.of(context).colorScheme.onSurfaceVariant),
366 - ),
367 - Text(fiatFee.withLocalSeperator(sendViewModel.languageCode),
368 - style: TextStyle(
369 - fontSize: 14,
370 - fontWeight: FontWeight.w400,
371 - color: Theme.of(context).colorScheme.onSurfaceVariant))
372 - ],
373 - )
374 - ],
375 - ),
376 - ),
377 - if (sendViewModel.isElectrumWallet) ...[
378 - Padding(
379 - padding: EdgeInsets.symmetric(horizontal: 12),
380 - child: Container(
381 - height: 1,
382 - color: Theme.of(context).colorScheme.surfaceContainerHigh,
383 - ),
280 + if (outputs.length >= 1 &&
281 + (outputs.first.extractedAddress.isNotEmpty || outputs.first.address.isNotEmpty) &&
282 + showAddress)
283 + Column(
284 + crossAxisAlignment: CrossAxisAlignment.start,
285 + spacing: 12,
286 + children: [
287 + Text(
288 + S.of(context).send_to,
289 + style: TextStyle(
290 + fontSize: 16,
291 + fontWeight: FontWeight.w500,
292 + color: Theme.of(context).colorScheme.onSurfaceVariant),
293 ),
294 + if (outputs.length == 1)
295 + Container(
296 + decoration: BoxDecoration(
297 + color: Theme.of(context).colorScheme.surfaceContainer,
298 + borderRadius: BorderRadius.circular(16),
299 + ),
300 + child: Padding(
301 + padding: const EdgeInsets.all(12.0),
302 + child: AddressFormatter.buildSegmentedAddress(
303 + address: outputs.first.isParsedAddress
304 + ? outputs.first.extractedAddress
305 + : outputs.first.address,
306 + evenTextStyle:
307 + TextStyle(color: Theme.of(context).colorScheme.onSurface)),
308 + ),
309 + )
310 + else
311 + AnimatedDropdown(
312 + content: Column(
313 + children: outputs
314 + .map(
315 + (item) => Column(
316 + children: [
317 + MultiSendAddressPreview(
318 + index: outputs.indexOf(item) + 1,
319 + address: item.isParsedAddress
320 + ? item.extractedAddress
321 + : item.address,
322 + amount:
323 + "${item.roundedCryptoAmount(8).withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.currency.title}",
324 + fiatAmount:
325 + "${item.fiatAmount.withDecimals(2).withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.fiatCurrency.title}",
326 + ),
327 + if (item != outputs.last)
328 + Container(
329 + width: double.infinity,
330 + height: 1,
331 + color:
332 + Theme.of(context).colorScheme.surfaceContainerHigh)
333 + ],
334 + ),
335 + )
336 + .toList(),
337 + ),
338 + dropdownText: "${outputs.length} ${S.of(context).addresses}"),
339 + ],
340 + ),
341 + Container(
342 + decoration: BoxDecoration(
343 + color: Theme.of(context).colorScheme.surfaceContainer,
344 + borderRadius: BorderRadius.circular(16),
345 + ),
346 + child: Column(
347 + children: [
348 Padding(
349 padding: const EdgeInsets.all(12.0),
350 child: Row(
351 mainAxisAlignment: MainAxisAlignment.spaceBetween,
352 children: [
390 - Text(S.of(context).network,
353 + Text(S.of(context).fee,
354 style: TextStyle(
355 fontSize: 14,
356 fontWeight: FontWeight.w400,
357 color: Theme.of(context).colorScheme.onSurface)),
358 Column(
359 + crossAxisAlignment: CrossAxisAlignment.end,
360 children: [
361 Text(
398 - sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln
399 - ? "Lightning"
400 - : bitcoin!.getNetworkName(sendViewModel.wallet),
362 + "${fee.withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.currencySymbol}",
363 + style: TextStyle(
364 + fontSize: 14,
365 + fontWeight: FontWeight.w400,
366 + color: Theme.of(context).colorScheme.onSurfaceVariant),
367 + ),
368 + Text(fiatFee.withLocalSeperator(sendViewModel.languageCode),
369 style: TextStyle(
370 fontSize: 14,
371 fontWeight: FontWeight.w400,
@@ -406,18 +374,52 @@ class SendTransactionDetails extends StatelessWidget {
374 )
375 ],
376 ),
409 - )
377 + ),
378 + if (sendViewModel.isElectrumWallet) ...[
379 + Padding(
380 + padding: EdgeInsets.symmetric(horizontal: 12),
381 + child: Container(
382 + height: 1,
383 + color: Theme.of(context).colorScheme.surfaceContainerHigh,
384 + ),
385 + ),
386 + Padding(
387 + padding: const EdgeInsets.all(12.0),
388 + child: Row(
389 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
390 + children: [
391 + Text(S.of(context).network,
392 + style: TextStyle(
393 + fontSize: 14,
394 + fontWeight: FontWeight.w400,
395 + color: Theme.of(context).colorScheme.onSurface)),
396 + Column(
397 + children: [
398 + Text(
399 + sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln
400 + ? "Lightning"
401 + : bitcoin!.getNetworkName(sendViewModel.wallet),
402 + style: TextStyle(
403 + fontSize: 14,
404 + fontWeight: FontWeight.w400,
405 + color: Theme.of(context).colorScheme.onSurfaceVariant))
406 + ],
407 + )
408 + ],
409 + ),
410 + )
411 + ],
412 ],
411 - ],
413 + ),
414 ),
413 - ),
414 - SendConfirmBottomWidget(sendViewModel: sendViewModel),
415 - if(Platform.isAndroid) // spacing between bottom widget and system navbar
416 - SizedBox(),
417 - ],
415 + SendConfirmBottomWidget(sendViewModel: sendViewModel),
416 + if (Platform.isAndroid) // spacing between bottom widget and system navbar
417 + SizedBox(),
418 + ],
419 + ),
420 ),
419 - ),
420 - );
421 + );
422 + });
423 }
424
425 String formatAmount(String amount) {
lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart
+21 -15
@@ -14,6 +14,8 @@ import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
14 import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart';
15 import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
16 import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
17 +import 'package:cw_core/amount/money.dart';
18 +import 'package:cw_core/crypto_amount_format.dart';
19 import 'package:cw_core/crypto_currency.dart';
20 import 'package:cw_core/currencies_with_memo.dart';
21 import 'package:cw_core/currency_for_wallet_type.dart';
@@ -159,14 +161,16 @@ class SwapTransactionDetails extends StatelessWidget {
161 builder: (_) => NewListSections(showHeader: true, sections: {
162 S.of(context).send: [
163 ListItemRegularRow(
162 - showArrow: false,
163 - keyValue: "send value",
164 - label: exchangeViewModel.depositCurrency.fullName ?? "",
165 - iconPath: exchangeViewModel.depositCurrency.iconPath ?? "",
166 - badgeIconPath: _resolveChainBadgePath(exchangeViewModel.depositCurrency),
167 - trailingText: exchangeTradeViewModel.trade.amountFormatted() +
168 - " " +
169 - (exchangeViewModel.depositCurrency.title)),
164 + showArrow: false,
165 + keyValue: "send value",
166 + label: exchangeViewModel.depositCurrency.fullName ?? "",
167 + iconPath: exchangeViewModel.depositCurrency.iconPath ?? "",
168 + badgeIconPath: _resolveChainBadgePath(exchangeViewModel.depositCurrency),
169 + trailingText: exchangeViewModel.amountParsingProxy
170 + .asDisplayStringWithSymbol(exchangeViewModel.depositCurrency
171 + .tryParseAmount(exchangeTradeViewModel.trade.amount) ??
172 + Money.zero(exchangeViewModel.depositCurrency)),
173 + ),
174 if (exchangeTradeViewModel.sendViewModel.pendingTransaction != null)
175 ListItemRegularRow(
176 showArrow: false,
@@ -184,13 +188,15 @@ class SwapTransactionDetails extends StatelessWidget {
188 ],
189 S.of(context).receive: [
190 ListItemRegularRow(
187 - showArrow: false,
188 - keyValue: "receive value",
189 - label: exchangeViewModel.receiveCurrency.fullName ?? "",
190 - iconPath: exchangeViewModel.receiveCurrency.iconPath ?? "",
191 - badgeIconPath: _resolveChainBadgePath(exchangeViewModel.receiveCurrency),
192 - trailingText:
193 - (receiveAmount) + " " + (exchangeViewModel.receiveCurrency.title)),
191 + showArrow: false,
192 + keyValue: "receive value",
193 + label: exchangeViewModel.receiveCurrency.fullName ?? "",
194 + iconPath: exchangeViewModel.receiveCurrency.iconPath ?? "",
195 + badgeIconPath: _resolveChainBadgePath(exchangeViewModel.receiveCurrency),
196 + trailingText: (receiveAmount.withMaxDecimals(8)) +
197 + " " +
198 + (exchangeViewModel.receiveCurrency.title),
199 + ),
200 ListItemRegularRow(
201 keyValue: "receiver",
202 label: S.of(context).to,
lib/new-ui/widgets/swap_page/swap_limit_popup.dart
+3 -1
@@ -20,7 +20,9 @@ class SwapLimitPopup extends StatelessWidget {
20 child: Container(
21 width: double.infinity,
22 child: Observer(builder: (_) {
23 - final double? amount = double.tryParse(exchangeViewModel.depositAmountCanonical);
23 + final amount = exchangeViewModel.hasDepositAmount
24 + ? double.tryParse(exchangeViewModel.depositAmountCanonical)
25 + : null;
26 final max = exchangeViewModel.limits.max ?? double.infinity;
27 final min = exchangeViewModel.limits.min ?? 0;
28 final tooLarge = amount != null && max != 0 && amount > max;
lib/new-ui/widgets/swap_page/swap_modal_header.dart
+7 -2
@@ -1,4 +1,5 @@
1 import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
3 import 'package:flutter/material.dart';
4
5 class SwapModalHeader extends StatelessWidget {
@@ -17,8 +18,12 @@ class SwapModalHeader extends StatelessWidget {
18 width: 36,
19 child: Stack(
20 children: [
20 - Image.asset(fromIconPath, width: 24, height: 24),
21 - Positioned(top: 12, left: 12, child: Image.asset(toIconPath, width: 24, height: 24)),
21 + CakeImageWidget(imageUrl: fromIconPath, width: 24, height: 24),
22 + Positioned(
23 + top: 12,
24 + left: 12,
25 + child: CakeImageWidget(imageUrl: toIconPath, width: 24, height: 24),
26 + ),
27 ],
28 ),
29 ),
lib/solana/cw_solana.dart
+27 -41
@@ -53,6 +53,8 @@ class CWSolana extends Solana {
53 @override
54 String getPublicKey(WalletBase wallet) =>
55 (wallet as SolanaWallet).solanaPublicKey.toAddress().address;
56 +
57 + @override
58 Object createSolanaTransactionCredentials(
59 List<Output> outputs, {
60 required CryptoCurrency currency,
@@ -60,14 +62,14 @@ class CWSolana extends Solana {
62 SolanaTransactionCredentials(
63 outputs
64 .map((out) => OutputInfo(
63 - fiatAmount: out.fiatAmount,
64 - cryptoAmount: out.cryptoAmount,
65 - address: out.address,
66 - note: out.note,
67 - sendAll: out.sendAll,
68 - extractedAddress: out.extractedAddress,
69 - isParsedAddress: out.isParsedAddress,
70 - formattedCryptoAmount: out.formattedCryptoAmount))
65 + fiatAmount: out.fiatAmount,
66 + cryptoAmount: out.cryptoAmountMoney,
67 + address: out.address,
68 + note: out.note,
69 + sendAll: out.sendAll,
70 + extractedAddress: out.extractedAddress,
71 + isParsedAddress: out.isParsedAddress,
72 + ))
73 .toList(),
74 currency: currency,
75 );
@@ -116,23 +118,15 @@ class CWSolana extends Solana {
118
119 @override
120 CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) {
119 - transaction as SolanaTransactionInfo;
120 - if (transaction.tokenSymbol == CryptoCurrency.sol.title) {
121 + if (transaction.amount.currency.symbol == CryptoCurrency.sol.symbol) {
122 return CryptoCurrency.sol;
123 }
124
124 - wallet as SolanaWallet;
125 -
126 - return wallet.splTokenCurrencies.firstWhere(
127 - (element) => transaction.tokenSymbol == element.symbol,
125 + return (wallet as SolanaWallet).splTokenCurrencies.firstWhere(
126 + (element) => transaction.amount.currency.symbol == element.symbol,
127 );
128 }
129
131 - @override
132 - double getTransactionAmountRaw(TransactionInfo transactionInfo) {
133 - return (transactionInfo as SolanaTransactionInfo).solAmount.toDouble();
134 - }
135 -
130 @override
131 String getTokenAddress(CryptoCurrency asset) {
132 // If it's already an SPLToken, use its mint address
@@ -168,9 +162,7 @@ class CWSolana extends Solana {
162 }
163
164 @override
171 - double? getEstimateFees(WalletBase wallet) {
172 - return (wallet as SolanaWallet).estimatedFee;
173 - }
165 + Money? getEstimateFees(WalletBase wallet) => (wallet as SolanaWallet).estimatedFee;
166
167 @override
168 List<SPLToken> getDefaultSPLTokens() => DefaultSPLTokens().initialSPLTokens;
@@ -197,8 +189,8 @@ class CWSolana extends Solana {
189 String base64Transaction,
190 String requestId,
191 String destinationAddress,
200 - double amount,
201 - double fee,
192 + Money amount,
193 + Money fee,
194 ) async {
195 final solanaWallet = wallet as SolanaWallet;
196 final privateKey = solanaWallet.solanaPrivateKey;
@@ -418,24 +410,18 @@ class CWSolana extends Solana {
410 required String from,
411 String? tokenSymbol,
412 required TransactionDirection direction,
421 - required double solAmount,
413 + required Money amount,
414 required bool isPending,
423 - required double txFee,
415 + required Money fee,
416 }) =>
417 SolanaTransactionInfo(
426 - id: id,
427 - blockTime: blockTime,
428 - to: to,
429 - from: from,
430 - tokenSymbol: tokenSymbol ?? "SOL",
431 - direction: direction,
432 - solAmount: solAmount,
433 - isPending: isPending,
434 - txFee: txFee);
435 -
436 - @override
437 - double getPendingTransactionAmount(PendingTransaction tx) => (tx as PendingSolanaTransaction).amount;
438 -
439 - @override
440 - double getPendingTransactionFee(PendingTransaction tx) => (tx as PendingSolanaTransaction).fee;
418 + id: id,
419 + date: blockTime,
420 + to: to,
421 + from: from,
422 + direction: direction,
423 + amount: amount,
424 + isPending: isPending,
425 + fee: fee,
426 + );
427 }
lib/src/screens/exchange/exchange_page.dart
+2 -2
@@ -733,7 +733,7 @@ class ExchangePage extends BasePage {
733 exchangeViewModel.depositAddress =
734 await fetchParsedAddress(context, domain, exchangeViewModel.depositCurrency);
735 },
736 - useSatoshis: exchangeViewModel.useSatoshiDeposit,
736 + useSatoshis: exchangeViewModel.useDepositBaseUnit,
737 ),
738 );
739
@@ -782,7 +782,7 @@ class ExchangePage extends BasePage {
782 exchangeViewModel.receiveAddress =
783 await fetchParsedAddress(context, domain, exchangeViewModel.receiveCurrency);
784 },
785 - useSatoshis: exchangeViewModel.useSatoshisReceive,
785 + useSatoshis: exchangeViewModel.useReceiveBaseUnit,
786 ),
787 );
788
lib/src/screens/exchange_trade/exchange_trade_page.dart
+2 -6
@@ -1,4 +1,3 @@
1 -import 'package:cake_wallet/entities/parsed_address.dart';
1 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
2 import 'package:cake_wallet/reactions/wallet_connect.dart';
3 import 'package:cake_wallet/routes.dart';
@@ -11,7 +10,6 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_shee
10 import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
11 import 'package:cake_wallet/utils/request_review_handler.dart';
12 import 'package:cake_wallet/utils/responsive_layout_util.dart';
14 -import 'package:cake_wallet/view_model/send/output.dart';
13 import 'package:mobx/mobx.dart';
14 import 'package:flutter_mobx/flutter_mobx.dart';
15 import 'package:flutter/material.dart';
@@ -330,10 +328,8 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
328 fee: isEVMCompatibleChain(sendVM.walletType)
329 ? S.of(bottomSheetContext).send_estimated_fee
330 : S.of(bottomSheetContext).send_fee,
333 - feeValue: "${sendVM.amountParsingProxy.getDisplayCryptoAmount(
334 - sendVM.pendingTransaction!.feeFormattedValue,
335 - sendVM.selectedCryptoCurrency)} ${sendVM.amountParsingProxy
336 - .getCryptoSymbol(sendVM.wallet.currency)}",
331 + feeValue: sendVM.amountParsingProxy.asDisplayStringWithSymbol(
332 + sendVM.pendingTransaction!.fee),
333 feeFiatAmount: sendVM.pendingTransactionFeeFiatAmountFormatted,
334 outputs: sendVM.outputs,
335 onSlideActionComplete: () async {
lib/src/screens/integrations/deuro/savings_page.dart
+20 -5
@@ -58,7 +58,8 @@ class DEuroSavingsPage extends BasePage {
58 Observer(
59 builder: (_) => SavingsCard(
60 interestRate: "${_dEuroViewModel.interestRateFormated}%",
61 - savingsBalance: _dEuroViewModel.savingsBalanceFormated,
61 + savingsBalance:
62 + _dEuroViewModel.savingsBalance.toStringWithPrecision(fractionalDigits: 6),
63 fiatSavingsBalance: _dEuroViewModel.fiatSavingsBalanceFormated,
64 currency: CryptoCurrency.deuro,
65 fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat,
@@ -68,6 +69,10 @@ class DEuroSavingsPage extends BasePage {
69 onTooltipPressed: () => _onSavingsTooltipPressed(context),
70 isEnabled: _dEuroViewModel.isEnabled,
71 isLoading: _dEuroViewModel.isLoading,
72 + onWithdrawV1Pressed: () => _onWithdrawV1(context),
73 + savingsBalanceV1: _dEuroViewModel.savingsBalanceV1
74 + ?.toStringWithPrecision(fractionalDigits: 6),
75 + fiatSavingsBalanceV1: _dEuroViewModel.fiatSavingsBalanceV1Formated,
76 ),
77 ),
78 Observer(
@@ -126,6 +131,16 @@ class DEuroSavingsPage extends BasePage {
131 _editSheetIsOpen = false;
132 }
133
134 + Future<void> _onWithdrawV1(BuildContext context) async {
135 + if (_editSheetIsOpen) return;
136 + _editSheetIsOpen = true;
137 + try {
138 + await _requireHardwareWallet(context);
139 + await _dEuroViewModel.prepareSavingsV1Withdraw();
140 + } catch (_) {}
141 + _editSheetIsOpen = false;
142 + }
143 +
144 Future<void> _onSavingsRemove(BuildContext context) async {
145 if (_editSheetIsOpen) return;
146 _editSheetIsOpen = true;
@@ -215,7 +230,7 @@ class DEuroSavingsPage extends BasePage {
230 amount: S.of(bottomSheetContext).send_amount,
231 amountValue: _dEuroViewModel.actionType == DEuroActionType.reinvest
232 ? _dEuroViewModel.accruedInterestFormated
218 - : tx.amountFormatted,
233 + : tx.amount.toStringWithSymbol(),
234 fiatAmountValue: _dEuroViewModel.actionType == DEuroActionType.reinvest
235 ? _dEuroViewModel.fiatAccruedInterestFormated
236 : _dEuroViewModel.pendingTransactionFiatAmountFormatted,
@@ -251,7 +266,7 @@ class DEuroSavingsPage extends BasePage {
266 amountValue: tx.amountFormatted,
267 fiatAmountValue: "",
268 fee: S.of(bottomSheetContext).send_estimated_fee,
254 - feeValue: tx.feeFormatted,
269 + feeValue: tx.fee.toStringWithSymbol(),
270 feeFiatAmount: "",
271 outputs: [],
272 onSlideActionComplete: () {
@@ -411,8 +426,8 @@ class DEuroSavingsPage extends BasePage {
426 ? S.of(context).deuro_savings_available_to_add
427 : S.of(context).deuro_savings_available_to_remove,
428 balance: isAdding
414 - ? _dEuroViewModel.accountBalanceFormated
415 - : _dEuroViewModel.savingsBalanceFormated,
429 + ? _dEuroViewModel.accountBalance
430 + : _dEuroViewModel.savingsBalance,
431 footerType: FooterType.none,
432 maxHeight: MediaQuery.of(context).size.height * 0.8,
433 ),
lib/src/screens/integrations/deuro/widgets/savings_card_widget.dart
+86 -55
@@ -23,6 +23,9 @@ class SavingsCard extends StatelessWidget {
23 this.isLoading = false,
24 this.fiatSavingsBalance,
25 this.fiatCurrency,
26 + this.savingsBalanceV1,
27 + this.fiatSavingsBalanceV1,
28 + this.onWithdrawV1Pressed,
29 });
30
31 final bool isEnabled;
@@ -30,8 +33,11 @@ class SavingsCard extends StatelessWidget {
33 final String interestRate;
34 final String savingsBalance;
35 final String? fiatSavingsBalance;
36 + final String? savingsBalanceV1;
37 + final String? fiatSavingsBalanceV1;
38 final FiatCurrency? fiatCurrency;
39 final CryptoCurrency currency;
40 + final VoidCallback? onWithdrawV1Pressed;
41 final VoidCallback onAddSavingsPressed;
42 final VoidCallback onRemoveSavingsPressed;
43 final VoidCallback onApproveSavingsPressed;
@@ -98,46 +104,72 @@ class SavingsCard extends StatelessWidget {
104 onTooltipPressed: onTooltipPressed,
105 ),
106 ),
107 + if (savingsBalanceV1 != null)
108 + Padding(
109 + padding: const EdgeInsets.only(bottom: 20),
110 + child: getAssetBalanceRow(
111 + context,
112 + title: "${S.of(context).deuro_savings_balance} V1",
113 + amount: savingsBalanceV1!,
114 + fiatAmount: fiatSavingsBalanceV1,
115 + currency: currency,
116 + fiatCurrency: fiatCurrency,
117 + hideSymbol: true,
118 + ),
119 + ),
120 isLoading
121 ? CupertinoActivityIndicator(color: Theme.of(context).colorScheme.onSurface)
122 : Row(
123 mainAxisAlignment: MainAxisAlignment.spaceBetween,
105 - children: isEnabled
124 + children: savingsBalanceV1 != null
125 ? [
126 Expanded(
127 child: getButton(
128 context,
110 - label: S.of(context).deuro_savings_add,
111 - imagePath: 'assets/images/received.png',
112 - onPressed: onAddSavingsPressed,
129 + label: "${S.of(context).deuro_savings_remove} V1",
130 + onPressed: onWithdrawV1Pressed ?? () {},
131 backgroundColor: Theme.of(context).colorScheme.primary,
132 color: Theme.of(context).colorScheme.onPrimary,
115 - ),
116 - ),
117 - SizedBox(width: 12),
118 - Expanded(
119 - child: getButton(
120 - context,
121 - label: S.of(context).deuro_savings_remove,
122 - imagePath: 'assets/images/upload.png',
123 - onPressed: onRemoveSavingsPressed,
124 - backgroundColor: Theme.of(context).colorScheme.surface,
125 - color: Theme.of(context).colorScheme.onSecondaryContainer,
126 - ),
127 - ),
128 - ]
129 - : [
130 - Expanded(
131 - child: getButton(
132 - context,
133 - label: S.of(context).deuro_savings_approve_app,
134 - onPressed: onApproveSavingsPressed,
135 - backgroundColor: Theme.of(context).colorScheme.primary,
136 - color: Theme.of(context).colorScheme.onPrimary,
137 - icon: Icons.check,
133 + icon: Icons.arrow_downward,
134 ),
135 )
140 - ],
136 + ]
137 + : isEnabled
138 + ? [
139 + Expanded(
140 + child: getButton(
141 + context,
142 + label: S.of(context).deuro_savings_add,
143 + imagePath: 'assets/images/received.png',
144 + onPressed: onAddSavingsPressed,
145 + backgroundColor: Theme.of(context).colorScheme.primary,
146 + color: Theme.of(context).colorScheme.onPrimary,
147 + ),
148 + ),
149 + SizedBox(width: 12),
150 + Expanded(
151 + child: getButton(
152 + context,
153 + label: S.of(context).deuro_savings_remove,
154 + imagePath: 'assets/images/upload.png',
155 + onPressed: onRemoveSavingsPressed,
156 + backgroundColor: Theme.of(context).colorScheme.surface,
157 + color: Theme.of(context).colorScheme.onSecondaryContainer,
158 + ),
159 + ),
160 + ]
161 + : [
162 + Expanded(
163 + child: getButton(
164 + context,
165 + label: S.of(context).deuro_savings_approve_app,
166 + onPressed: onApproveSavingsPressed,
167 + backgroundColor: Theme.of(context).colorScheme.primary,
168 + color: Theme.of(context).colorScheme.onPrimary,
169 + icon: Icons.check,
170 + ),
171 + )
172 + ],
173 ),
174 ],
175 ),
@@ -264,33 +296,33 @@ class SavingsCard extends StatelessWidget {
296 ),
297 ],
298 ),
267 - SizedBox(
268 - child: Center(
269 - child: Column(
270 - children: [
271 - CakeImageWidget(
272 - imageUrl: currency.iconPath,
273 - height: 40,
274 - width: 40,
275 - errorWidget: Container(
276 - height: 30.0,
277 - width: 30.0,
278 - child: Center(
279 - child: Text(
280 - currency.title.substring(0, min(currency.title.length, 2)),
281 - style: Theme.of(context).textTheme.bodySmall?.copyWith(
282 - fontSize: 11,
283 - color: Theme.of(context).colorScheme.onSurfaceVariant,
284 - ),
299 + if (!hideSymbol)
300 + SizedBox(
301 + child: Center(
302 + child: Column(
303 + children: [
304 + CakeImageWidget(
305 + imageUrl: currency.iconPath,
306 + height: 40,
307 + width: 40,
308 + errorWidget: Container(
309 + height: 30.0,
310 + width: 30.0,
311 + child: Center(
312 + child: Text(
313 + currency.title.substring(0, min(currency.title.length, 2)),
314 + style: Theme.of(context).textTheme.bodySmall?.copyWith(
315 + fontSize: 11,
316 + color: Theme.of(context).colorScheme.onSurfaceVariant,
317 + ),
318 + ),
319 + ),
320 + decoration: BoxDecoration(
321 + shape: BoxShape.circle,
322 + color: Theme.of(context).colorScheme.surfaceContainer,
323 ),
286 - ),
287 - decoration: BoxDecoration(
288 - shape: BoxShape.circle,
289 - color: Theme.of(context).colorScheme.surfaceContainer,
324 ),
325 ),
292 - ),
293 - if (!hideSymbol) ...[
326 const SizedBox(height: 3),
327 Text(
328 currency.title,
@@ -301,11 +333,10 @@ class SavingsCard extends StatelessWidget {
333 height: 1,
334 ),
335 ),
304 - ]
305 - ],
336 + ],
337 + ),
338 ),
339 ),
308 - ),
340 ],
341 );
342 }
lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart
+11 -8
@@ -3,11 +3,11 @@ import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/src/screens/integrations/deuro/widgets/numpad.dart';
4 import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
5 import 'package:cake_wallet/src/widgets/primary_button.dart';
6 -import 'package:cw_core/parse_fixed.dart';
6 +import 'package:cw_core/amount/money.dart';
7 import 'package:flutter/material.dart';
8
9 class SavingsEditSheet extends BaseBottomSheet {
10 - final String? balance;
10 + final Money? balance;
11 final String? balanceTitle;
12
13 const SavingsEditSheet({
@@ -32,7 +32,7 @@ class SavingsEditSheet extends BaseBottomSheet {
32 }
33
34 class _SavingsEditBody extends StatefulWidget {
35 - final String? balance;
35 + final Money? balance;
36 final String? balanceTitle;
37
38 const _SavingsEditBody({this.balance, this.balanceTitle});
@@ -59,7 +59,7 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> {
59 final FocusNode _numpadFocusNode = FocusNode();
60
61 void _onPressedAll() => setState(() {
62 - amount = widget.balance!;
62 + amount = widget.balance!.toString();
63 isValid = _validate();
64 });
65
@@ -79,9 +79,12 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> {
79 });
80
81 bool _validate() {
82 - final amountBigInt = parseFixed(amount, 18);
83 - final balanceBigInt = parseFixed(widget.balance!, 18);
84 - return balanceBigInt >= amountBigInt && amountBigInt > BigInt.zero;
82 + try {
83 + final amountBigInt = Money.parse(amount, widget.balance!.currency);
84 + return widget.balance! >= amountBigInt && amountBigInt.sign > 0;
85 + } catch (_) {
86 + return false;
87 + }
88 }
89
90 @override
@@ -108,7 +111,7 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> {
111 Divider(),
112 AssetBalanceRow(
113 title: widget.balanceTitle!,
111 - amount: widget.balance!,
114 + amount: widget.balance!.toStringWithSymbol(fractionalDigits: 6),
115 onAllPressed: _onPressedAll,
116 ),
117 ],
lib/src/screens/receive/widgets/currency_input_field.dart
+2 -2
@@ -1,5 +1,6 @@
1 import 'package:cake_wallet/generated/i18n.dart';
2 import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
3 +import 'package:cw_core/amount/amount_sanitizer.dart';
4 import 'package:cw_core/crypto_amount_format.dart';
5 import 'package:flutter/material.dart';
6 import 'package:flutter/services.dart';
@@ -198,8 +199,7 @@ class CurrencyAmountTextField extends StatelessWidget {
199 ),
200 validator: isAmountEditable ? currencyValueValidator : null,
201 onChanged: (value) {
201 - var sanitized =
202 - value.replaceAll(',', '.').withMaxDecimals(selectedCurrencyDecimals);
202 + var sanitized = value.sanitized().withMaxDecimals(selectedCurrencyDecimals);
203
204 if (selectedCurrencyDecimals == 0) {
205 sanitized = sanitized.replaceAll('.', '');
lib/src/screens/send/send_page.dart
+3 -2
@@ -35,6 +35,7 @@ import 'package:cake_wallet/view_model/send/output.dart';
35 import 'package:cake_wallet/view_model/send/send_view_model.dart';
36 import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
37 import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
38 +import 'package:cw_core/amount/money.dart';
39 import 'package:cw_core/crypto_currency.dart';
40 import 'package:cw_core/utils/print_verbose.dart';
41 import 'package:cw_core/wallet_type.dart';
@@ -445,9 +446,9 @@ class SendPage extends BasePage {
446 }
447
448 if (sendViewModel.wallet.type == WalletType.monero) {
448 - int amount = 0;
449 + var amount = Money.zero(sendViewModel.wallet.currency);
450 for (var item in sendViewModel.outputs) {
450 - amount += item.formattedCryptoAmount;
451 + amount += item.cryptoAmountMoney;
452 }
453 if (monero!.needExportOutputs(sendViewModel.wallet, amount)) {
454 await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage,
lib/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart
+18 -63
@@ -3,7 +3,6 @@ import 'package:cake_wallet/src/widgets/primary_button.dart';
3 import 'package:cake_wallet/src/widgets/rounded_icon_button.dart';
4 import 'package:cake_wallet/utils/image_utill.dart';
5 import 'package:cake_wallet/view_model/send/output.dart';
6 -import 'package:cw_core/crypto_currency.dart';
6 import 'package:flutter/material.dart';
7 import 'package:flutter/services.dart';
8
@@ -11,7 +10,6 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
10 const CakePayTransactionSentBottomSheet({
11 super.key,
12 required this.titleText,
14 - required this.currency,
13 required this.amount,
14 required this.amountValue,
15 required this.fiatAmountValue,
@@ -28,7 +26,6 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
26
27 final String titleText;
28 final Widget? titleIconWidget;
31 - final CryptoCurrency currency;
29 final String amount;
30 final String amountValue;
31 final String fiatAmountValue;
@@ -41,32 +38,15 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
38 final String paymentId;
39 final String paymentIdValue;
40
44 - TextStyle _titleStyle(BuildContext ctx) =>
45 - Theme
46 - .of(ctx)
47 - .textTheme
48 - .bodyLarge!;
41 + TextStyle _titleStyle(BuildContext ctx) => Theme.of(ctx).textTheme.bodyLarge!;
42
50 - TextStyle _valueStyle(BuildContext ctx) =>
51 - Theme
52 - .of(ctx)
53 - .textTheme
54 - .titleMedium!
55 - .copyWith(
43 + TextStyle _valueStyle(BuildContext ctx) => Theme.of(ctx).textTheme.titleMedium!.copyWith(
44 fontSize: 18,
45 fontWeight: FontWeight.w600,
46 );
47
60 - TextStyle _subStyle(BuildContext ctx) =>
61 - Theme
62 - .of(ctx)
63 - .textTheme
64 - .labelSmall!
65 - .copyWith(
66 - color: Theme
67 - .of(ctx)
68 - .colorScheme
69 - .onSurfaceVariant,
48 + TextStyle _subStyle(BuildContext ctx) => Theme.of(ctx).textTheme.labelSmall!.copyWith(
49 + color: Theme.of(ctx).colorScheme.onSurfaceVariant,
50 );
51
52 Widget _buildHeader(BuildContext ctx) =>
@@ -78,10 +58,7 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
58 height: 5,
59 decoration: BoxDecoration(
60 borderRadius: BorderRadius.circular(4),
81 - color: Theme
82 - .of(ctx)
83 - .colorScheme
84 - .onSurface,
61 + color: Theme.of(ctx).colorScheme.onSurface,
62 ),
63 ),
64 const SizedBox(height: 20),
@@ -92,15 +69,11 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
69 const SizedBox(width: 6),
70 Text(
71 titleText,
95 - style: Theme
96 - .of(ctx)
97 - .textTheme
98 - .titleLarge!
99 - .copyWith(
100 - fontSize: 20,
101 - fontWeight: FontWeight.w600,
102 - decoration: TextDecoration.none,
103 - ),
72 + style: Theme.of(ctx).textTheme.titleLarge!.copyWith(
73 + fontSize: 20,
74 + fontWeight: FontWeight.w600,
75 + decoration: TextDecoration.none,
76 + ),
77 ),
78 ],
79 ),
@@ -116,7 +89,7 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
89 _StandardTile(
90 itemTitle: amount,
91 titleStyle: _titleStyle(context),
119 - itemValue: '$amountValue ${currency.title}',
92 + itemValue: amountValue,
93 itemValueStyle: _valueStyle(context),
94 itemSubTitle: fiatAmountValue,
95 itemSubTitleStyle: _subStyle(context),
@@ -143,9 +116,7 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
116 const SizedBox(height: 8),
117 _StandardTile(
118 itemTitle: paymentId + ': \n' + paymentIdValue,
146 - titleStyle: _titleStyle(context).copyWith(
147 - fontSize: 14
148 - ),
119 + titleStyle: _titleStyle(context).copyWith(fontSize: 14),
120 itemValue: '',
121 itemValueStyle: _valueStyle(context),
122 copyButton: true,
@@ -161,28 +132,15 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
132 Image.asset('assets/images/envelope.png'),
133 const SizedBox(height: 18),
134 Text(
164 - S
165 - .of(context)
166 - .cake_pay_card_email_delivered_message,
167 - style: Theme
168 - .of(context)
169 - .textTheme
170 - .bodyLarge,
135 + S.of(context).cake_pay_card_email_delivered_message,
136 + style: Theme.of(context).textTheme.bodyLarge,
137 textAlign: TextAlign.center,
138 ),
139 const SizedBox(height: 24),
140 PrimaryButton(
175 - text: S
176 - .of(context)
177 - .close,
178 - color: Theme
179 - .of(context)
180 - .colorScheme
181 - .primary,
182 - textColor: Theme
183 - .of(context)
184 - .colorScheme
185 - .onPrimaryContainer,
141 + text: S.of(context).close,
142 + color: Theme.of(context).colorScheme.primary,
143 + textColor: Theme.of(context).colorScheme.onPrimaryContainer,
144 onPressed: onClose,
145 ),
146 ],
@@ -194,10 +152,7 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget {
152
153 @override
154 Widget build(BuildContext context) {
197 - final maxHeight = MediaQuery
198 - .of(context)
199 - .size
200 - .height * 0.9;
155 + final maxHeight = MediaQuery.of(context).size.height * 0.9;
156
157 return ClipRRect(
158 borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart
+3 -2
@@ -7,6 +7,7 @@ import 'package:cake_wallet/utils/address_formatter.dart';
7 import 'package:cake_wallet/utils/image_utill.dart';
8 import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
9 import 'package:cake_wallet/view_model/send/output.dart';
10 +import 'package:cw_core/amount/amount_sanitizer.dart';
11 import 'package:cw_core/crypto_currency.dart';
12 import 'package:cw_core/pending_transaction.dart';
13 import 'package:cw_core/wallet_type.dart';
@@ -134,7 +135,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
135 ),
136 StandardTile(
137 itemTitle: amount,
137 - itemValue: '$amountValue ${amountParsingProxy?.getCryptoSymbol(currency) ?? currency.title}',
138 + itemValue: amountValue,
139 itemTitleTextStyle: itemTitleTextStyle,
140 itemSubTitle: fiatAmountValue,
141 itemSubTitleTextStyle: itemSubTitleTextStyle,
@@ -167,7 +168,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
168 final batchContactTitle =
169 '${index + 1}/${outputs.length} - ${contactName.isEmpty ? 'Address' : contactName}';
170 final _address = item.isParsedAddress ? item.extractedAddress : item.address;
170 - final _amount = '${item.cryptoAmount.replaceAll(',', '.')} ${amountParsingProxy?.getCryptoSymbol(currency) ?? currency.title}';
171 + final _amount = '${item.cryptoAmount.sanitized()} ${amountParsingProxy?.getCryptoSymbol(currency) ?? currency.title}';
172 return isBatchSending || (contactName.isNotEmpty && !isCakePayName)
173 ? ExpansionAddressTile(
174 contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact,
lib/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart
+2 -1
@@ -10,6 +10,7 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/swap_details_bottom_sheet.d
10 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
11 import 'package:cake_wallet/utils/address_formatter.dart';
12 import 'package:cake_wallet/utils/debounce.dart';
13 +import 'package:cw_core/amount/amount_sanitizer.dart';
14 import 'package:cw_core/currency_for_wallet_type.dart';
15 import 'package:cw_core/crypto_amount_format.dart';
16
@@ -197,7 +198,7 @@ class SwapConfirmationContentState extends State<SwapConfirmationContent> {
198 ],
199 onChanged: (value) {
200 final sanitized = value
200 - .replaceAll(',', '.')
201 + .sanitized()
202 .withMaxDecimals(widget.exchangeViewModel.receiveCurrency.decimals);
203 if (sanitized != _amountController.text) {
204 // Update text while preserving a sane cursor position to avoid auto-selection
lib/tron/cw_tron.dart
+10 -25
@@ -58,13 +58,12 @@ class CWTron extends Tron {
58 .map(
59 (out) => OutputInfo(
60 fiatAmount: out.fiatAmount,
61 - cryptoAmount: out.cryptoAmount,
61 + cryptoAmount: out.cryptoAmountMoney,
62 address: out.address,
63 note: out.note,
64 sendAll: out.sendAll,
65 extractedAddress: out.extractedAddress,
66 isParsedAddress: out.isParsedAddress,
67 - formattedCryptoAmount: out.formattedCryptoAmount,
67 ),
68 )
69 .toList(),
@@ -97,22 +96,16 @@ class CWTron extends Tron {
96 Future<TronToken?> getTronToken(WalletBase wallet, String contractAddress) async =>
97 (wallet as TronWallet).getTronToken(contractAddress);
98
100 - @override
101 - double getTransactionAmountRaw(TransactionInfo transactionInfo) {
102 - final amount = (transactionInfo as TronTransactionInfo).rawTronAmount();
103 - return double.parse(amount);
104 - }
105 -
99 @override
100 CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) {
101 transaction as TronTransactionInfo;
109 - if (transaction.tokenSymbol == CryptoCurrency.trx.title) {
102 + if (transaction.amount.currency.symbol == CryptoCurrency.trx.title) {
103 return CryptoCurrency.trx;
104 }
105
106 wallet as TronWallet;
114 - return wallet.tronTokenCurrencies.firstWhere(
115 - (element) => transaction.tokenSymbol.toLowerCase() == element.symbol.toLowerCase());
107 + return wallet.tronTokenCurrencies.firstWhere((element) =>
108 + transaction.amount.currency.symbol.toLowerCase() == element.symbol.toLowerCase());
109 }
110
111 @override
@@ -123,11 +116,11 @@ class CWTron extends Tron {
116 (wallet as TronWallet).getTronBase58AddressFromHex(hexAddress);
117
118 @override
126 - String? getTronNativeEstimatedFee(WalletBase wallet) =>
119 + Money? getTronNativeEstimatedFee(WalletBase wallet) =>
120 (wallet as TronWallet).nativeTxEstimatedFee;
121
122 @override
130 - String? getTronTRC20EstimatedFee(WalletBase wallet) => (wallet as TronWallet).trc20EstimatedFee;
123 + Money? getTronTRC20EstimatedFee(WalletBase wallet) => (wallet as TronWallet).trc20EstimatedFee;
124
125 @override
126 void updateTronGridUsageState(WalletBase wallet, bool isEnabled) {
@@ -157,9 +150,8 @@ class CWTron extends Tron {
150 @override
151 TransactionInfo getTransactionInfo({
152 required String id,
160 - required BigInt tronAmount,
161 - int? txFee,
162 - String? tokenSymbol,
153 + required Money amount,
154 + Money? fee,
155 required TransactionDirection direction,
156 required DateTime blockTime,
157 String? to,
@@ -167,18 +159,11 @@ class CWTron extends Tron {
159 required bool isPending,
160 }) =>
161 TronTransactionInfo(id: id,
170 - tronAmount: tronAmount,
171 - tokenSymbol: tokenSymbol ?? "TRX",
172 - txFee: txFee,
162 + amount: amount,
163 + fee: fee,
164 direction: direction,
165 blockTime: blockTime,
166 to: to,
167 from: from,
168 isPending: isPending);
178 -
179 - @override
180 - String getPendingTransactionAmount(PendingTransaction tx) => (tx as PendingTronTransaction).amount;
181 -
182 - @override
183 - String getPendingTransactionFee(PendingTransaction tx) => (tx as PendingTronTransaction).amount;
169 }
lib/utils/decimal_input_formatter.dart new
+19
@@ -0,0 +1,19 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:flutter/services.dart';
3 +
4 +class DecimalInputFormatter extends TextInputFormatter {
5 + const DecimalInputFormatter({this.maxDecimals = 7}) : assert(maxDecimals >= 0);
6 +
7 + final int maxDecimals;
8 +
9 + @override
10 + TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
11 + final text = newValue.text;
12 + if (text.isEmpty) return newValue;
13 +
14 + if (S.current.all.startsWith(text)) return TextEditingValue(text: "");
15 +
16 + final regex = maxDecimals == 0 ? RegExp(r'^\d*$') : RegExp('^\\d*([.,]\\d{0,$maxDecimals})?\$');
17 + return regex.hasMatch(text) ? newValue.copyWith(text: text.replaceAll(',', '.')) : oldValue;
18 + }
19 +}
lib/utils/exception_handler.dart
+1
@@ -306,6 +306,7 @@ class ExceptionHandler {
306 "the timeout of the request was reached",
307
308 "support for coin removed, your seedphrase:"
309 + "Exception: Invalid image data"
310 ];
311
312 static Future<void> _addDeviceInfo(File file) async {
lib/utils/payment_request.dart
+8 -8
@@ -1,3 +1,5 @@
1 +import 'package:cw_core/amount/amount_sanitizer.dart';
2 +import 'package:cw_core/amount/money.dart';
3 import 'package:cw_core/crypto_currency.dart';
4 import 'package:cw_core/lnurl.dart';
5 import 'package:cw_core/payment_uris.dart';
@@ -16,9 +18,8 @@ class PaymentRequest {
18 }
19
20 factory PaymentRequest.fromBolt11(String invoice) {
19 - final amountRaw = getBolt11Amount(invoice) ?? 0;
20 - final amount = CryptoCurrency.btcln.formatAmount(BigInt.from(amountRaw));
21 - return PaymentRequest(invoice, amount, '', 'lightning', null);
21 + final amount = getBolt11Amount(invoice) ?? Money.zero(CryptoCurrency.btcln);
22 + return PaymentRequest(invoice, amount.toString(), '', 'lightning', null);
23 }
24
25 factory PaymentRequest.fromUri(Uri? uri) {
@@ -39,10 +40,10 @@ class PaymentRequest {
40
41 address = uri.queryParameters['address'] ?? uri.path;
42 try {
42 - final lnAmount =
43 - CryptoCurrency.btcln.formatAmount(BigInt.from(getBolt11Amount(uri.path) ?? 0));
43 + final lnAmount = getBolt11Amount(uri.path) ?? Money.zero(CryptoCurrency.btcln);
44 +
45 if (lnAmount != 0) {
45 - amount = lnAmount;
46 + amount = lnAmount.toString();
47 }
48 } catch (_) {}
49 if (amount.isEmpty) {
@@ -116,8 +117,7 @@ class PaymentRequest {
117 static bool _isAlreadyUsableAmount(String amount) {
118 if (amount.isEmpty) return false;
119
119 - // Try to parse as double - if successful, it's already in usable format
120 - final parsed = double.tryParse(amount.replaceAll(',', '.'));
120 + final parsed = double.tryParse(amount.sanitized());
121 if (parsed == null) return false;
122
123 // Check if the amount contains a decimal point and is a reasonable number,
lib/view_model/bridge/bridge_view_model.dart
+22 -22
@@ -17,6 +17,8 @@ import 'package:cake_wallet/store/app_store.dart';
17 import 'package:cake_wallet/store/bridge_transfers_store.dart';
18 import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
19 import 'package:cake_wallet/store/settings_store.dart';
20 +import 'package:cw_core/amount/amount_sanitizer.dart';
21 +import 'package:cw_core/amount/money.dart';
22 import 'package:cw_core/crypto_amount_format.dart';
23 import 'package:cw_core/crypto_currency.dart';
24 import 'package:cw_core/erc20_token.dart';
@@ -130,18 +132,18 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
132 }
133
134 @computed
133 - String get tokenBalanceFormatted {
134 - if (selectedToken == null) return "0.00";
135 -
136 - return amountParsingProxy.getDisplayCryptoStringFromBigInt(
137 - selectedTokenBalance, selectedToken!,
138 - );
139 -}
135 + String get tokenBalanceFormatted {
136 + if (selectedToken == null) return "0.00";
137 +
138 + return amountParsingProxy.asDisplayString(
139 + Money(selectedTokenBalance, selectedToken!),
140 + );
141 + }
142
143 @computed
144 String get amountDisplayFormatted {
145 if (selectedToken == null) return "0.00";
144 -
146 +
147 return amountParsingProxy.getDisplayCryptoAmount(
148 amount.replaceAll(',', '.'),
149 selectedToken!,
@@ -174,10 +176,8 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
176 String get quoteNativeFee {
177 if (quote == null) return '—';
178
177 - return amountParsingProxy.getDisplayCryptoStringFromBigInt(
178 - quote!.nativeFee,
179 - wallet.currency,
180 - );
179 + return amountParsingProxy.asDisplayString(
180 + Money(quote!.nativeFee, wallet.currency));
181 }
182
183 @computed
@@ -218,10 +218,10 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
218 if (amountError != null) return false;
219
220 final validAmount = amountParsingProxy.tryParseCryptoString(
221 - amount.replaceAll(',', '.'),
221 + amount.sanitized(),
222 selectedToken!,
223 );
224 - return validAmount != null && validAmount > BigInt.zero;
224 + return validAmount != null && validAmount > Money.zero(selectedToken!);
225 }
226
227 @computed
@@ -231,7 +231,7 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
231 try {
232 final bal = wallet.balance[selectedToken!];
233
234 - return bal?.fullAvailableBalance ?? BigInt.zero;
234 + return bal?.available.amount ?? BigInt.zero;
235 } catch (e) {
236 return BigInt.zero;
237 }
@@ -247,7 +247,7 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
247 );
248
249 if (amountBigInt == null || amountBigInt == BigInt.zero) return null;
250 - if (amountBigInt > selectedTokenBalance) {
250 + if (amountBigInt.amount > selectedTokenBalance) {
251 return 'Insufficient balance for ${selectedToken!.title} token.';
252 }
253
@@ -281,9 +281,9 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
281 return;
282 }
283 setAmount(
284 - amountParsingProxy.getDisplayCryptoStringFromBigInt(
284 + amountParsingProxy.asDisplayString(Money(
285 selectedTokenBalance,
286 - token,
286 + token)
287 ),
288 );
289 }
@@ -382,23 +382,23 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
382 }
383
384 ({String? error, BigInt? parsedAmount}) _parseAndValidateAmount(Erc20Token token) {
385 - final amountBigInt = amountParsingProxy.tryParseCryptoString(
385 + final parsedAmount = amountParsingProxy.tryParseCryptoString(
386 amount.replaceAll(',', '.'),
387 token,
388 );
389
390 - if (amountBigInt == null || amountBigInt == BigInt.zero) {
390 + if (parsedAmount == null || parsedAmount == Money(BigInt.zero, token)) {
391 return (error: 'Invalid amount', parsedAmount: null);
392 }
393
394 - if (amountBigInt > selectedTokenBalance) {
394 + if (parsedAmount.amount > selectedTokenBalance) {
395 return (
396 error: 'Insufficient balance for ${token.title} token.',
397 parsedAmount: null,
398 );
399 }
400
401 - return (error: null, parsedAmount: amountBigInt);
401 + return (error: null, parsedAmount: parsedAmount.amount);
402 }
403
404 @action
lib/view_model/dashboard/balance_view_model.dart
+114 -90
@@ -3,6 +3,7 @@ import 'package:cake_wallet/core/utilities.dart';
3 import 'package:cake_wallet/entities/balance_display_mode.dart';
4 import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
5 import 'package:cake_wallet/entities/fiat_api_mode.dart';
6 +import 'package:cake_wallet/entities/fiat_currency.dart';
7 import 'package:cake_wallet/entities/sort_balance_types.dart';
8 import 'package:cake_wallet/generated/i18n.dart';
9 import 'package:cake_wallet/reactions/wallet_connect.dart';
@@ -10,8 +11,10 @@ import 'package:cake_wallet/evm/evm.dart';
11 import 'package:cake_wallet/solana/solana.dart';
12 import 'package:cake_wallet/tron/tron.dart';
13 import 'package:cake_wallet/zano/zano.dart';
14 +import 'package:cw_core/amount/money.dart';
15 import 'package:cw_core/crypto_amount_format.dart';
16 import 'package:cw_core/transaction_history.dart';
17 +import 'package:cw_core/utils/print_verbose.dart';
18 import 'package:cw_core/wallet_base.dart';
19 import 'package:cake_wallet/store/app_store.dart';
20 import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
@@ -28,47 +31,74 @@ part 'balance_view_model.g.dart';
31
32 class BalanceRecord {
33 const BalanceRecord({
34 + required this.raw,
35 required this.availableBalance,
36 required this.additionalBalance,
37 required this.secondAvailableBalance,
38 required this.secondAdditionalBalance,
39 required this.frozenBalance,
36 - required this.fiatAvailableBalance,
37 - required this.fiatAdditionalBalance,
38 - required this.fiatFrozenBalance,
39 - required this.fiatSecondAvailableBalance,
40 - required this.fiatSecondAdditionalBalance,
40 + required this.fiatAvailableBalanceRaw,
41 + required this.fiatAdditionalBalanceRaw,
42 + required this.fiatFrozenBalanceRaw,
43 + required this.fiatSecondAvailableBalanceRaw,
44 + required this.fiatSecondAdditionalBalanceRaw,
45 required this.asset,
46 required this.secondAsset,
47 + required this.fiatCurrency,
48 required this.formattedAssetTitle,
49 + this.languageCode,
50 });
51
46 - final String fiatAdditionalBalance;
47 - final String fiatAvailableBalance;
48 - final String fiatFrozenBalance;
52 + final Balance raw;
53 +
54 + final String fiatAvailableBalanceRaw;
55 + final String fiatAdditionalBalanceRaw;
56 + final String fiatFrozenBalanceRaw;
57 + final String fiatSecondAvailableBalanceRaw;
58 + final String fiatSecondAdditionalBalanceRaw;
59 +
60 final String additionalBalance;
61 final String availableBalance;
62 final String frozenBalance;
63 final String secondAvailableBalance;
64 final String secondAdditionalBalance;
54 - final String fiatSecondAdditionalBalance;
55 - final String fiatSecondAvailableBalance;
65 final CryptoCurrency asset;
66 final CryptoCurrency secondAsset;
67 + final FiatCurrency? fiatCurrency;
68 final String formattedAssetTitle;
69 + final String? languageCode;
70
71 String get combinedAvailableBalance =>
61 - ((double.tryParse(availableBalance) ?? 0) + (double.tryParse(secondAvailableBalance) ?? 0))
62 - .toString().withMaxDecimals(8);
72 + (raw.available + (raw.secondAvailable ?? Money.zero(raw.available.currency)))
73 + .toString()
74 + .withMaxDecimals(8);
75 +
76 + String get combinedFiatAvailableBalance => fiatCurrency != null
77 + ? "$fiatCurrency " +
78 + _withLocalSeperator(((double.tryParse(fiatAvailableBalanceRaw) ?? 0) +
79 + (double.tryParse(fiatSecondAvailableBalanceRaw) ?? 0))
80 + .toStringAsFixed(2))
81 + : "";
82 +
83 + String get fiatAvailableBalance =>
84 + fiatCurrency != null ? "$fiatCurrency ${_withLocalSeperator(fiatAvailableBalanceRaw)}" : "";
85 +
86 + String get fiatAdditionalBalance =>
87 + fiatCurrency != null ? "$fiatCurrency ${_withLocalSeperator(fiatAdditionalBalanceRaw)}" : "";
88 +
89 + String get fiatFrozenBalance =>
90 + fiatCurrency != null ? "$fiatCurrency ${_withLocalSeperator(fiatFrozenBalanceRaw)}" : "";
91 +
92 + String get fiatSecondAvailableBalance => fiatCurrency != null
93 + ? "$fiatCurrency ${_withLocalSeperator(fiatSecondAvailableBalanceRaw)}"
94 + : "";
95
64 - String get combinedFiatAvailableBalance =>
65 - ((double.tryParse(fiatAvailableBalanceRaw) ?? 0) +
66 - (double.tryParse(fiatSecondAvailableBalanceRaw) ?? 0))
67 - .toStringAsFixed(2);
96 + String get fiatSecondAdditionalBalance => fiatCurrency != null
97 + ? "$fiatCurrency ${_withLocalSeperator(fiatSecondAdditionalBalanceRaw)}"
98 + : "";
99
69 - String get fiatAvailableBalanceRaw => fiatAvailableBalance.split(" ").last;
70 - String get fiatSecondAvailableBalanceRaw => fiatSecondAvailableBalance.split(" ").last;
71 - String get fiatCurrencyTicker => fiatAvailableBalance.split(" ").first;
100 + String _withLocalSeperator(String rawAmount) =>
101 + languageCode == null ? rawAmount : rawAmount.withLocalSeperator(languageCode);
102 }
103
104 class BalanceViewModel = BalanceViewModelBase with _$BalanceViewModel;
@@ -221,12 +251,13 @@ abstract class BalanceViewModelBase with Store {
251 }
252 }
253
224 - String additionalBalance(CryptoCurrency cryptoCurrency) {
254 + Money additionalBalance(CryptoCurrency cryptoCurrency) {
255 final balance = _currencyBalance(cryptoCurrency);
256
227 - if (displayMode == BalanceDisplayMode.hiddenBalance || balance.additional == BigInt.zero) return '0.0';
257 + if (displayMode == BalanceDisplayMode.hiddenBalance || balance.unavailable.isZero)
258 + return Money.zero(cryptoCurrency);
259
229 - return cryptoCurrency.formatAmount(balance.additional);
260 + return balance.unavailable;
261 }
262
263 @computed
@@ -237,25 +268,26 @@ abstract class BalanceViewModelBase with Store {
268 if (displayMode == BalanceDisplayMode.hiddenBalance) {
269 final fiatCurrency = settingsStore.fiatCurrency;
270 return MapEntry(
240 - key,
241 - BalanceRecord(
242 - availableBalance: '●●●●●●',
243 - additionalBalance: additionalBalance(key),
244 - frozenBalance: '',
245 - secondAvailableBalance: '●●●●●●',
246 - secondAdditionalBalance: '●●●●●●',
247 - fiatAdditionalBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●',
248 - fiatAvailableBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●',
249 - fiatFrozenBalance: isFiatDisabled ? '' : '',
250 - fiatSecondAvailableBalance:
251 - isFiatDisabled ? '' : '${fiatCurrency.toString()}',
252 - fiatSecondAdditionalBalance:
253 - isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●',
254 - asset: key,
255 - secondAsset: secondAsset,
256 - formattedAssetTitle: _formatterAsset(key)));
271 + key,
272 + BalanceRecord(
273 + raw: value,
274 + availableBalance: '●●●●●●',
275 + additionalBalance: '',
276 + frozenBalance: '',
277 + secondAvailableBalance: '●●●●●●',
278 + secondAdditionalBalance: '●●●●●●',
279 + fiatAdditionalBalanceRaw: '●●●●●',
280 + fiatAvailableBalanceRaw: '●●●●●',
281 + fiatFrozenBalanceRaw: '',
282 + fiatSecondAvailableBalanceRaw: '●●●●●',
283 + fiatSecondAdditionalBalanceRaw: '●●●●●',
284 + asset: key,
285 + secondAsset: secondAsset,
286 + fiatCurrency: isFiatDisabled ? null : fiatCurrency,
287 + formattedAssetTitle: _formatterAsset(key),
288 + ),
289 + );
290 }
258 - final fiatCurrency = settingsStore.fiatCurrency;
291 final price = key.isPotentialScam
292 ? 0.0
293 : fiatConversionStore.prices[key == CryptoCurrency.btcln ? CryptoCurrency.btc : key] ?? 0;
@@ -264,48 +296,41 @@ abstract class BalanceViewModelBase with Store {
296 // throw Exception('Price is null for: $key');
297 // }
298
267 - final available = evm?.getERC20AvailableBalance(value) ??
268 - (value.fullAvailableBalance - (value.secondAvailable ?? BigInt.zero));
269 - final additional = evm?.getERC20AvailableBalance(value) ??
270 - (value.additional - (value.secondAdditional ?? BigInt.zero));
299 + final availableFiatBalance =
300 + isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.available);
301
272 - final availableFiatBalance = isFiatDisabled
273 - ? ''
274 - : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: key.formatAmount(available))}';
302 + final additionalFiatBalance =
303 + isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.unavailable);
304
276 - final additionalFiatBalance = isFiatDisabled
277 - ? ''
278 - : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: key.formatAmount(additional))}';
305 + final frozenFiatBalance =
306 + isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.frozen);
307
280 - final frozenFiatBalance = isFiatDisabled
281 - ? ''
282 - : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.frozen != null ? key.formatAmount(value.frozen!) : null)}';
283 -
284 - final secondAvailableFiatBalance = isFiatDisabled
285 - ? ''
286 - : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.secondAvailable != null ? key.formatAmount(value.secondAvailable!) : null)}';
308 + final secondAvailableFiatBalance =
309 + isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.secondAvailable);
310
311 final secondAdditionalFiatBalance = isFiatDisabled
312 ? ''
290 - : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.secondAdditional != null ? key.formatAmount(value.secondAdditional!) : null)}';
313 + : _getFiatBalance(price: price, cryptoAmount: value.secondUnavailable);
314
315 return MapEntry(
316 key,
317 BalanceRecord(
295 - availableBalance: _getFormattedCryptoAmount(key, available),
296 - fiatAvailableBalance: availableFiatBalance,
297 - additionalBalance: _getFormattedCryptoAmount(key, additional),
298 - fiatAdditionalBalance: additionalFiatBalance,
299 - frozenBalance:
300 - (value.frozen ?? BigInt.zero) > BigInt.zero ? _getFormattedCryptoAmount(key, value.frozen??BigInt.zero) : '',
301 - fiatFrozenBalance: frozenFiatBalance,
302 - secondAvailableBalance: _getFormattedCryptoAmount(secondAsset, value.secondAvailable??BigInt.zero),
303 - fiatSecondAvailableBalance: secondAvailableFiatBalance,
304 - secondAdditionalBalance: _getFormattedCryptoAmount(secondAsset, value.secondAdditional??BigInt.zero),
305 - fiatSecondAdditionalBalance: secondAdditionalFiatBalance,
318 + raw: value,
319 + availableBalance: _getFormattedCryptoAmount(value.available),
320 + fiatAvailableBalanceRaw: availableFiatBalance,
321 + additionalBalance: _getFormattedCryptoAmount(value.unavailable),
322 + fiatAdditionalBalanceRaw: additionalFiatBalance,
323 + frozenBalance: _getFormattedCryptoAmount(value.frozen),
324 + fiatFrozenBalanceRaw: frozenFiatBalance,
325 + secondAvailableBalance: _getFormattedCryptoAmount(value.secondAvailable),
326 + fiatSecondAvailableBalanceRaw: secondAvailableFiatBalance,
327 + secondAdditionalBalance: _getFormattedCryptoAmount(value.secondUnavailable),
328 + fiatSecondAdditionalBalanceRaw: secondAdditionalFiatBalance,
329 asset: key,
330 secondAsset: secondAsset,
331 + fiatCurrency: isFiatDisabled ? null : settingsStore.fiatCurrency,
332 formattedAssetTitle: _formatterAsset(key),
333 + languageCode: settingsStore.languageCode,
334 ),
335 );
336 });
@@ -316,7 +341,7 @@ abstract class BalanceViewModelBase with Store {
341
342 bool hasAdditionalBalance(CryptoCurrency currency) {
343 final isWalletTypeActivated = _hasAdditionalBalanceForWalletType(wallet.type);
319 - final isNotZeroAmount = additionalBalance(currency) != "0.0";
344 + final isNotZeroAmount = !additionalBalance(currency).isZero;
345
346 return isWalletTypeActivated && isNotZeroAmount;
347 }
@@ -324,9 +349,9 @@ abstract class BalanceViewModelBase with Store {
349 @computed
350 bool get hasSecondAdditionalBalance {
351 if (wallet.type == WalletType.litecoin && mwebEnabled) {
327 - return (wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0;
352 + return (wallet.balance[CryptoCurrency.ltc]?.secondUnavailable ?? 0) != 0;
353 } else if (wallet.type == WalletType.bitcoin) {
329 - return (wallet.balance[CryptoCurrency.btc]?.secondAdditional ?? 0) != 0;
354 + return (wallet.balance[CryptoCurrency.btc]?.secondUnavailable ?? 0) != 0;
355 }
356 return false;
357 }
@@ -351,12 +376,12 @@ abstract class BalanceViewModelBase with Store {
376 WalletType.zcash
377 ].contains(type);
378
354 - String _getFormattedCryptoAmount(CryptoCurrency cryptoCurrency, BigInt? amount) {
379 + String _getFormattedCryptoAmount(Money? amount) {
380 if (amount == null) return "";
381
382 return appStore.amountParsingProxy
358 - .getDisplayCryptoStringFromBigInt(amount, cryptoCurrency)
359 - .withMaxDecimals(8).withLocalSeperator(settingsStore.languageCode);
383 + .asDisplayString(amount)
384 + .withLocalSeperator(settingsStore.languageCode);
385 }
386
387 @computed
@@ -402,9 +427,9 @@ abstract class BalanceViewModelBase with Store {
427 switch (sortBalanceBy) {
428 case SortBalanceBy.FiatBalance:
429 final aFiatBalance = _getFiatBalance(
405 - price: fiatConversionStore.prices[a.asset] ?? 0, cryptoAmount: a.availableBalance);
430 + price: fiatConversionStore.prices[a.asset] ?? 0, cryptoAmount: a.raw.available);
431 final bFiatBalance = _getFiatBalance(
407 - price: fiatConversionStore.prices[b.asset] ?? 0, cryptoAmount: b.availableBalance);
432 + price: fiatConversionStore.prices[b.asset] ?? 0, cryptoAmount: b.raw.available);
433
434 return (double.tryParse(bFiatBalance) ?? 0)
435 .compareTo((double.tryParse(aFiatBalance)) ?? 0);
@@ -425,7 +450,8 @@ abstract class BalanceViewModelBase with Store {
450 }
451
452 if (wallet.walletInfo.favoriteTokenAddress != null) {
428 - return formattedBalances.firstWhereOrNull((item) => (getTokenAddressBasedOnWallet(item.asset) ==
453 + return formattedBalances.firstWhereOrNull((item) =>
454 + (getTokenAddressBasedOnWallet(item.asset) ==
455 wallet.walletInfo.favoriteTokenAddress)) ??
456 formattedBalances.elementAtOrNull(0);
457 }
@@ -455,8 +481,8 @@ abstract class BalanceViewModelBase with Store {
481
482 @computed
483 bool get showCombinedBalance {
458 - if(wallet.type == WalletType.bitcoin) return false;
459 - if(balances.values.length == 1) return false;
484 + if (wallet.type == WalletType.bitcoin) return false;
485 + if (balances.values.length == 1) return false;
486
487 return wallet.walletInfo.showCombinedBalance;
488 }
@@ -470,15 +496,14 @@ abstract class BalanceViewModelBase with Store {
496 double ret = 0.0;
497 for (final curr in wallet.balance.keys) {
498 final record = wallet.balance[curr]!;
473 - final available = evm?.getERC20AvailableBalance(record) ??
474 - (record.fullAvailableBalance - (record.secondAvailable ?? BigInt.zero));
499 + final available = record.available - (record.secondAvailable ?? Money.zero(curr));
500 final price = fiatConversionStore.prices[curr] ?? 0;
476 - ret += double.tryParse(calculateFiatAmount(
477 - price: price, cryptoAmount: curr.formatAmount(available).replaceAll(",", ""))
501 + printV(record.available);
502 + ret += double.tryParse(calculateFiatAmount(price: price, cryptoAmount: available.toString())
503 .replaceAll(",", "")) ??
504 0;
505 }
481 - return ret.toStringAsFixed(2);
506 + return ret.toStringAsFixed(2).withLocalSeperator(settingsStore.languageCode);
507 }
508
509 Balance _currencyBalance(CryptoCurrency cryptoCurrency) {
@@ -522,13 +547,12 @@ abstract class BalanceViewModelBase with Store {
547 }
548 }
549
525 - String _getFiatBalance({required double price, String? cryptoAmount}) {
526 - if (cryptoAmount == null || cryptoAmount.isEmpty || double.tryParse(cryptoAmount) == null) {
527 - return '0.00'.withLocalSeperator(settingsStore.languageCode);
550 + String _getFiatBalance({required double price, Money? cryptoAmount}) {
551 + if (cryptoAmount == null) {
552 + return '0.00';
553 }
554
530 - return calculateFiatAmount(price: price, cryptoAmount: cryptoAmount)
531 - .withLocalSeperator(settingsStore.languageCode);
555 + return calculateFiatAmount(price: price, cryptoAmount: cryptoAmount.toString(), raw: true);
556 }
557
558 String _formatterAsset(CryptoCurrency asset) {
lib/view_model/dashboard/transaction_list_item.dart
+11 -52
@@ -1,23 +1,17 @@
1 -import 'package:cake_wallet/decred/decred.dart';
1 import 'package:cake_wallet/entities/balance_display_mode.dart';
2 import 'package:cake_wallet/entities/fiat_currency.dart';
3 import 'package:cake_wallet/evm/evm.dart';
4 import 'package:cake_wallet/generated/i18n.dart';
6 -import 'package:cake_wallet/nano/nano.dart';
5 import 'package:cake_wallet/reactions/wallet_connect.dart';
6 import 'package:cake_wallet/solana/solana.dart';
7 import 'package:cake_wallet/store/app_store.dart';
8 import 'package:cake_wallet/tron/tron.dart';
11 -import 'package:cake_wallet/wownero/wownero.dart';
9 import 'package:cake_wallet/zano/zano.dart';
13 -import 'package:cake_wallet/zcash/zcash.dart';
10 import 'package:cw_core/crypto_amount_format.dart';
11 import 'package:cw_core/crypto_currency.dart';
12 import 'package:cw_core/transaction_direction.dart';
13 import 'package:cw_core/transaction_info.dart';
14 import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
19 -import 'package:cake_wallet/monero/monero.dart';
20 -import 'package:cake_wallet/bitcoin/bitcoin.dart';
15 import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart';
16 import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
17 import 'package:cw_core/keyable.dart';
@@ -52,17 +46,12 @@ class TransactionListItem extends ActionListItem with Keyable {
46 String get formattedCryptoAmount {
47 if (displayMode == BalanceDisplayMode.hiddenBalance) return '---';
48 if (balanceViewModel.wallet.type == WalletType.bitcoin) {
55 - final isLightning = (transaction.additionalInfo["isLightning"] as bool?) ?? false;
56 - final crypto = isLightning ? CryptoCurrency.btcln : CryptoCurrency.btc;
57 - final amount = _appStore.amountParsingProxy
58 - .getDisplayCryptoString(transaction.amount, crypto)
59 - .withMaxDecimals(8)
49 + return _appStore.amountParsingProxy
50 + .asDisplayStringWithSymbol(transaction.amount)
51 .withLocalSeperator(_appStore.settingsStore.languageCode);
61 -
62 - return '$amount ${_appStore.amountParsingProxy.getCryptoSymbol(crypto)}';
52 }
53
65 - return transaction.amountFormatted();
54 + return transaction.amount.toStringWithSymbol();
55 }
56
57 String get formattedTitle {
@@ -193,26 +182,18 @@ class TransactionListItem extends ActionListItem with Keyable {
182
183 switch (balanceViewModel.wallet.type) {
184 case WalletType.monero:
196 - amount = calculateFiatAmountRaw(
197 - cryptoAmount: monero!.formatterMoneroAmountToDouble(amount: transaction.amount),
198 - price: price,
199 - ).withLocalSeperator(_appStore.settingsStore.languageCode);
200 - break;
185 case WalletType.wownero:
202 - amount = calculateFiatAmountRaw(
203 - cryptoAmount: wownero!.formatterWowneroAmountToDouble(amount: transaction.amount),
204 - price: price,
205 - ).withLocalSeperator(_appStore.settingsStore.languageCode);
206 - break;
186 case WalletType.bitcoin:
187 case WalletType.litecoin:
188 case WalletType.bitcoinCash:
189 case WalletType.dogecoin:
190 + case WalletType.nano:
191 + case WalletType.decred:
192 + case WalletType.zcash:
193 amount = calculateFiatAmountRaw(
212 - cryptoAmount: bitcoin!.formatterBitcoinAmountToDouble(amount: transaction.amount),
194 + cryptoAmount: double.parse(transaction.amount.toString()),
195 price: price,
196 ).withLocalSeperator(_appStore.settingsStore.languageCode);
215 - break;
197 case WalletType.ethereum:
198 case WalletType.polygon:
199 case WalletType.base:
@@ -221,14 +202,7 @@ class TransactionListItem extends ActionListItem with Keyable {
202 final asset = assetOfTransaction;
203 final price = balanceViewModel.fiatConversionStore.prices[asset];
204 amount = calculateFiatAmountRaw(
224 - cryptoAmount: evm!.formatterEVMAmountToDouble(transaction: transaction),
225 - price: price,
226 - ).withLocalSeperator(_appStore.settingsStore.languageCode);
227 - break;
228 - case WalletType.nano:
229 - amount = calculateFiatAmountRaw(
230 - cryptoAmount: double.parse(nanoUtil!.getRawAsUsableString(
231 - nano!.getTransactionAmountRaw(transaction).toString(), nanoUtil!.rawPerNano)),
205 + cryptoAmount: double.parse(transaction.amount.toString()),
206 price: price,
207 ).withLocalSeperator(_appStore.settingsStore.languageCode);
208 break;
@@ -236,16 +210,15 @@ class TransactionListItem extends ActionListItem with Keyable {
210 final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction);
211 final price = balanceViewModel.fiatConversionStore.prices[asset];
212 amount = calculateFiatAmountRaw(
239 - cryptoAmount: solana!.getTransactionAmountRaw(transaction),
213 + cryptoAmount: double.parse(transaction.amount.toString()),
214 price: price,
215 ).withLocalSeperator(_appStore.settingsStore.languageCode);
216 break;
217 case WalletType.tron:
218 final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction);
219 final price = balanceViewModel.fiatConversionStore.prices[asset];
246 - final cryptoAmount = tron!.getTransactionAmountRaw(transaction);
220 amount = calculateFiatAmountRaw(
248 - cryptoAmount: cryptoAmount,
221 + cryptoAmount: double.parse(transaction.amount.toString()),
222 price: price,
223 ).withLocalSeperator(_appStore.settingsStore.languageCode);
224 break;
@@ -257,24 +230,10 @@ class TransactionListItem extends ActionListItem with Keyable {
230 }
231 final price = balanceViewModel.fiatConversionStore.prices[asset];
232 amount = calculateFiatAmountRaw(
260 - cryptoAmount: zano!.formatterIntAmountToDouble(
261 - amount: transaction.amount, currency: asset, forFee: false),
262 - price: price,
263 - ).withLocalSeperator(_appStore.settingsStore.languageCode);
264 - break;
265 - case WalletType.decred:
266 - amount = calculateFiatAmountRaw(
267 - cryptoAmount: decred!.formatterDecredAmountToDouble(amount: transaction.amount),
233 + cryptoAmount: double.parse(transaction.amount.toString()),
234 price: price,
235 ).withLocalSeperator(_appStore.settingsStore.languageCode);
236 break;
271 - case WalletType.zcash:
272 - amount = calculateFiatAmountRaw(
273 - cryptoAmount:
274 - zcash!.formatterZcashAmountToDouble(amount: BigInt.from(transaction.amount)),
275 - price: price,
276 - ).withLocalSeperator(_appStore.settingsStore.languageCode);
277 -
237 case WalletType.none:
238 case WalletType.banano:
239 case WalletType.haven:
lib/view_model/exchange/exchange_view_model.dart
+129 -92
@@ -2,10 +2,12 @@ import 'dart:async';
2 import 'dart:collection';
3 import 'dart:convert';
4 import 'dart:developer';
5 +import 'dart:math' show min;
6
7 import 'package:bitcoin_base/bitcoin_base.dart';
8 import 'package:cake_wallet/.secrets.g.dart' as secrets;
9 import 'package:cake_wallet/bitcoin/bitcoin.dart';
10 +import 'package:cake_wallet/core/address_validator.dart';
11 import 'package:cake_wallet/core/amount_parsing_proxy.dart';
12 import 'package:cake_wallet/core/create_trade_result.dart';
13 import 'package:cake_wallet/core/fiat_conversion_service.dart';
@@ -41,6 +43,8 @@ import 'package:cake_wallet/generated/i18n.dart';
43 import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart';
44 import 'package:cake_wallet/store/app_store.dart';
45 import 'package:cake_wallet/utils/exchange_provider_logger.dart';
46 +import 'package:cw_core/amount/amount_sanitizer.dart';
47 +import 'package:cw_core/amount/money.dart';
48 import "package:cw_core/wallet_info.dart";
49 import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
50 import 'package:cake_wallet/store/dashboard/trades_store.dart';
@@ -103,8 +107,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
107 ) : isSendAllEnabled = false,
108 isFixedRateMode = false,
109 isReceiveAmountEntered = false,
106 - _depositAmount = '',
107 - _receiveAmount = '',
110 + _depositAmount = null,
111 + _receiveAmount = null,
112 receiveAddress = '',
113 depositAddress = '',
114 isDepositAddressEnabled = false,
@@ -163,8 +167,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
167 });
168
169 isDepositAddressEnabled = !useSameWalletAddress(depositCurrency);
166 - _depositAmount = '';
167 - _receiveAmount = '';
170 + _depositAmount = null;
171 + _receiveAmount = null;
172 receiveAddress = '';
173 depositAddress =
174 useSameWalletAddress(depositCurrency) ? wallet.walletAddresses.addressForExchange : '';
@@ -237,12 +241,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
241 }
242 final balance = wallet.balance[depositCurrency];
243 if (balance != null) {
240 - depositAvailableAmount = _appStore.amountParsingProxy
241 - .getDisplayCryptoStringFromBigInt(balance.fullAvailableBalance, depositCurrency);
244 + depositAvailableAmount = _appStore.amountParsingProxy.asDisplayString(balance.available);
245 return false;
246 }
247 return true;
245 - });
248 + }).timeout(Duration(seconds: 20));
249 } else if (isEVMCompatibleChain(wallet.type)) {
250 final currency = depositCurrency;
251 final balanceCurrency = wallet.balance.keys.firstWhereOrNull(
@@ -253,15 +256,15 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
256 final balanceForCurrency =
257 balanceCurrency != null ? wallet.balance[balanceCurrency] : null;
258 if (depositCurrency == currency && balanceForCurrency != null) {
256 - depositAvailableAmount = _appStore.amountParsingProxy
257 - .getDisplayCryptoStringFromBigInt(
258 - balanceForCurrency.fullAvailableBalance, balanceCurrency!);
259 + depositAvailableAmount =
260 + _appStore.amountParsingProxy.asDisplayStringWithSymbol(balanceForCurrency.available);
261 }
262 } else {
263 final currency = depositCurrency;
262 - final amount = _appStore.amountParsingProxy.getDisplayCryptoString(
263 - (await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any)), depositCurrency);
264 - if(depositCurrency == currency) {
264 + final sendingBalance = Money.fromInt(
265 + await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any), currency);
266 + final amount = _appStore.amountParsingProxy.asDisplayStringWithSymbol(sendingBalance);
267 + if (depositCurrency == currency) {
268 depositAvailableAmount = amount;
269 }
270 }
@@ -289,7 +292,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
292 ].contains(wallet.type);
293
294 bool get hideAddressAfterExchange =>
292 - [WalletType.monero, WalletType.wownero, WalletType.zcash].contains(wallet.type);
295 + [WalletType.monero, WalletType.wownero, WalletType.zcash].contains(wallet.type) ||
296 + isElectrumWallet;
297
298 bool _useTorOnly;
299 final ExchangeTemplateStore _exchangeTemplateStore;
@@ -352,25 +356,29 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
356 ExchangeTradeState tradeState;
357
358 @observable
355 - String _depositAmount;
359 + Money? _depositAmount;
360 +
361 + @computed
362 + bool get hasDepositAmount => _depositAmount != null;
363
364 @computed
365 String get depositAmount =>
359 - _depositAmount == "..."? _depositAmount:amountParsingProxy.getDisplayCryptoAmount(_depositAmount, depositCurrency);
366 + _depositAmount == null ? "" : amountParsingProxy.asDisplayString(_depositAmount!);
367
368 @computed
362 - String get depositAmountCanonical => _depositAmount;
369 + String get depositAmountCanonical => _depositAmount == null ? "0.0" : _depositAmount.toString();
370
371 @observable
365 - String _receiveAmount;
372 + Money? _receiveAmount;
373
374 @computed
375 String get receiveAmount =>
369 - _receiveAmount == "..." ? _receiveAmount:amountParsingProxy.getDisplayCryptoAmount(_receiveAmount, receiveCurrency);
376 + _receiveAmount == null ? "" : amountParsingProxy.asDisplayString(_receiveAmount!);
377
378 @action
379 // only set canonical formated amounts here;
373 - void setCanonicalReceiveAmount(String value) => _receiveAmount = value;
380 + void setCanonicalReceiveAmount(String value) =>
381 + _receiveAmount = Money.tryParse(value, receiveCurrency);
382
383 @observable
384 String depositAddress;
@@ -486,7 +494,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
494 @computed
495 String? get balanceDisplay {
496 CryptoCurrency? balanceCurrency;
489 - if (isEVMCompatibleChain(wallet.type)) {
497 + if (isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana || wallet.type == WalletType.tron) {
498 balanceCurrency = wallet.balance.keys.firstWhereOrNull(
499 (c) =>
500 c.title == depositCurrency.title &&
@@ -495,10 +503,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
503 } else {
504 balanceCurrency = depositCurrency;
505 }
498 - final bal = balanceCurrency != null ? wallet.balance[balanceCurrency]?.fullAvailableBalance : null;
506 + final bal = balanceCurrency != null ? wallet.balance[balanceCurrency]?.available : null;
507 if (bal == null) return null;
500 - return amountParsingProxy.getDisplayCryptoStringFromBigInt(
501 - bal, balanceCurrency ?? depositCurrency);
508 + return amountParsingProxy.asDisplayString(bal);
509 }
510
511 //* Still open to further optimize these checks
@@ -572,10 +579,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
579 double bestRate = 0.0;
580
581 @computed
575 - bool get useSatoshiDeposit => _appStore.amountParsingProxy.useSatoshi(depositCurrency);
582 + bool get useDepositBaseUnit => _appStore.amountParsingProxy.useSatoshi(depositCurrency);
583
584 @computed
578 - bool get useSatoshisReceive => _appStore.amountParsingProxy.useSatoshi(receiveCurrency);
585 + bool get useReceiveBaseUnit => _appStore.amountParsingProxy.useSatoshi(receiveCurrency);
586
587 @computed
588 AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy;
@@ -624,12 +631,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
631 String get receiveAmountFiatFormatted {
632 var amount = '0.00';
633 try {
627 - if (_receiveAmount.isNotEmpty) {
634 + if (_receiveAmount != null) {
635 if (fiatConversionStore.prices[receiveCurrency] == null) return '';
636
637 amount = calculateFiatAmount(
638 price: fiatConversionStore.prices[receiveCurrency]!,
632 - cryptoAmount: _receiveAmount,
639 + cryptoAmount: _receiveAmount.toString(),
640 );
641 }
642 } catch (_) {
@@ -642,12 +649,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
649 String get depositAmountFiatFormatted {
650 var amount = '0.00';
651 try {
645 - if (_depositAmount.isNotEmpty) {
652 + if (_depositAmount != null) {
653 if (fiatConversionStore.prices[depositCurrency] == null) return '';
654
655 amount = calculateFiatAmount(
656 price: fiatConversionStore.prices[depositCurrency]!,
650 - cryptoAmount: _depositAmount,
657 + cryptoAmount: _depositAmount.toString(),
658 );
659 }
660 } catch (_) {
@@ -660,12 +667,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
667 String get receiveAmountFiat {
668 var amount = '';
669 try {
663 - if (_receiveAmount.isNotEmpty) {
670 + if (_receiveAmount != null) {
671 if (fiatConversionStore.prices[receiveCurrency] == null) return '';
672
673 amount = calculateFiatAmount(
674 price: fiatConversionStore.prices[receiveCurrency]!,
668 - cryptoAmount: _receiveAmount,
675 + cryptoAmount: _receiveAmount.toString(),
676 raw: true);
677 }
678 } catch (_) {
@@ -678,13 +685,14 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
685 String get depositAmountFiat {
686 var amount = '';
687 try {
681 - if (_depositAmount.isNotEmpty) {
688 + if (_depositAmount != null) {
689 if (fiatConversionStore.prices[depositCurrency] == null) return '';
690
691 amount = calculateFiatAmount(
685 - price: fiatConversionStore.prices[depositCurrency]!,
686 - cryptoAmount: _depositAmount,
687 - raw: true);
692 + price: fiatConversionStore.prices[depositCurrency]!,
693 + cryptoAmount: _depositAmount.toString(),
694 + raw: true,
695 + );
696 }
697 } catch (_) {
698 log('Error calculating deposit amount fiat formatted: $_');
@@ -771,32 +779,37 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
779 }
780
781 @action
774 - Future<void> changeReceiveAmount({required String amount}) async {
775 - amount = amountParsingProxy.getCanonicalCryptoAmount(amount, receiveCurrency);
782 + Future<void> changeReceiveAmount({required String amount, bool isCanonical = false}) async {
783 + if (amount.isEmpty) {
784 + _depositAmount = null;
785 + _receiveAmount = null;
786 + return;
787 + }
788
777 - _receiveAmount = amount;
789 + _receiveAmount = isCanonical
790 + ? Money.tryParse(amount.sanitized(), receiveCurrency)
791 + : _appStore.amountParsingProxy.tryParseCryptoString(amount.sanitized(), receiveCurrency);
792
779 - if (amount.isEmpty) {
780 - _depositAmount = '';
781 - _receiveAmount = '';
793 + if (_receiveAmount == null) {
794 + _depositAmount = null;
795 return;
796 }
797
785 - final _enteredAmount = double.tryParse(_receiveAmount.replaceAll(',', '.')) ?? 0;
798 + final _enteredAmount = double.tryParse(_receiveAmount.toString()) ?? 0;
799
800 if (bestRate == 0) {
788 - _depositAmount = "...";
801 + _depositAmount = null;
802
803 await calculateBestRate();
804 }
805
806 final amount_ = _enteredAmount / (forcedProvider == null ? bestRate : forcedProviderRate);
794 - _depositAmount = amount_.toString().withMaxDecimals(depositCurrency.decimals);
807 + _depositAmount = Money.tryParse(amount_.toStringAsFixed(depositCurrency.decimals), depositCurrency);
808 }
809
810 @action
811 void setReceiveAmountFromFiat({required String fiatAmount}) {
799 - final _enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0.0;
812 + final _enteredAmount = double.tryParse(fiatAmount.sanitized()) ?? 0.0;
813 final price = fiatConversionStore.prices[receiveCurrency];
814 if (price == null || price == 0.0) return;
815
@@ -809,7 +822,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
822
823 @action
824 void setDepositAmountFromFiat({required String fiatAmount}) {
812 - final _enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0.0;
825 + final _enteredAmount = double.tryParse(fiatAmount.sanitized()) ?? 0.0;
826 final price = fiatConversionStore.prices[depositCurrency];
827 if (price == null || price == 0.0) return;
828
@@ -822,34 +835,40 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
835
836 @action
837 Future<void> changeDepositAmount({required String amount, bool isCanonical = false}) async {
825 - _depositAmount = isCanonical ? amount.replaceAll(',', '.') : _appStore.amountParsingProxy
826 - .getCanonicalCryptoAmount(amount.replaceAll(',', '.'), depositCurrency);
827 -
838 if (amount.isEmpty) {
829 - _depositAmount = '';
830 - _receiveAmount = '';
839 + _depositAmount = null;
840 + _receiveAmount = null;
841 return;
842 }
843
844 + _depositAmount = isCanonical
845 + ? Money.tryParse(amount.sanitized(), depositCurrency)
846 + : _appStore.amountParsingProxy.tryParseCryptoString(amount.sanitized(), depositCurrency);
847 +
848 + if (_depositAmount == null) {
849 + _receiveAmount = null;
850 + return;
851 + }
852 /// For fixed-rate transactions, we don't want to recalculate receive amount
853 /// as it should remain exactly what the user set
854 if (isFixedRateMode) return;
855
838 - final _enteredAmount = double.tryParse(_depositAmount) ?? 0;
856 + final _enteredAmount = double.tryParse(_depositAmount.toString()) ?? 0;
857
858 /// in case the best rate was not calculated yet
859 if (bestRate == 0) {
842 - _receiveAmount = "...";
860 + _receiveAmount = null;
861
862 await calculateBestRate();
863 }
864
865 final amount_ = _enteredAmount * (forcedProvider == null ? bestRate : forcedProviderRate);
848 - _receiveAmount = amount_.toString().withMaxDecimals(receiveCurrency.decimals);
866 + _receiveAmount =
867 + Money.tryParse(amount_.toStringAsFixed(min(20, receiveCurrency.decimals)), receiveCurrency);
868 }
869
870 bool checkIfInputMeetsMinOrMaxCondition(String input) {
852 - final _enteredAmount = double.tryParse(input.replaceAll(',', '.')) ?? 0;
871 + final _enteredAmount = double.tryParse(input.sanitized()) ?? 0;
872 double minLimit = limits.min ?? 0;
873 double? maxLimit = limits.max;
874
@@ -866,8 +885,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
885 return;
886 }
887
869 - final amount = double.tryParse(isFixedRateMode ? _receiveAmount : _depositAmount) ??
870 - initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
888 + final amount =
889 + double.tryParse(isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString()) ??
890 + initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
891
892 forcedProviderRate = await forcedProvider!.fetchRate(
893 from: depositCurrency,
@@ -891,8 +911,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
911 bestRateProvider = null;
912 return;
913 }
894 - final amount = double.tryParse(isFixedRateMode ? _receiveAmount : _depositAmount) ??
895 - initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
914 + final amount =
915 + double.tryParse(isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString()) ??
916 + initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
917
918 final validProvidersForAmount = _tradeAvailableProviders.where((provider) {
919 if (_excludeProviderForSwapAll(provider)) return false;
@@ -1025,13 +1046,13 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1046
1047 @action
1048 Future<void> createTrade() async {
1028 - final depositAmountValue =
1029 - double.tryParse(_depositAmount.replaceAll(',', '.')) ?? 0.0;
1030 -
1031 - final receiveAmountValue =
1032 - double.tryParse(_receiveAmount.replaceAll(',', '.')) ?? 0.0;
1049 + final depositAmountValue = _depositAmount ?? Money.zero(depositCurrency);
1050 + final receiveAmountValue = _receiveAmount ?? Money.zero(receiveCurrency);
1051
1034 - if (depositAmountValue <= 0 || receiveAmountValue <= 0) {
1052 + if (depositAmountValue.isZero ||
1053 + depositAmountValue.isNegative ||
1054 + receiveAmountValue.isZero ||
1055 + receiveAmountValue.isNegative) {
1056 ExchangeProviderLogger.logError(
1057 provider: forcedProvider?.description ?? bestRateProvider?.description,
1058 function: 'createTrade',
@@ -1047,25 +1068,26 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1068 },
1069 );
1070
1050 - final invalidAmountError = depositAmountValue <= 0
1071 + final invalidAmountError = depositAmountValue.isZero || depositAmountValue.isNegative
1072 ? '$depositAmountValue is not a valid amount for depositAmount'
1073 : '$receiveAmountValue is not a valid amount for receiveAmount';
1074
1075 tradeState = TradeIsCreatedFailure(
1076 title: S.current.trade_not_created,
1056 - error: invalidAmountError,
1077 + error: invalidAmountError
1078 );
1079 return;
1080 }
1081
1082 if (isSendAllEnabled) {
1083 await calculateDepositAllAmount();
1063 - final amount = double.tryParse(_depositAmount);
1084 + final amount = double.tryParse(_depositAmount.toString());
1085
1086 if (limits.min != null && amount != null && amount < limits.min!) {
1087 tradeState = TradeIsCreatedFailure(
1088 title: S.current.trade_not_created,
1068 - error: S.current.amount_is_below_minimum_limit(limits.min!.toString()));
1089 + error: S.current.amount_is_below_minimum_limit(limits.min!.toString()),
1090 + );
1091 return;
1092 }
1093 }
@@ -1090,8 +1112,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1112
1113 if (forcedProvider != null && _excludeProviderForReceiveExtraId(forcedProvider!)) {
1114 tradeState = TradeIsCreatedFailure(
1093 - title: S.current.trade_not_created,
1094 - error: S.current.none_of_selected_providers_can_exchange);
1115 + title: S.current.trade_not_created,
1116 + error: S.current.none_of_selected_providers_can_exchange,
1117 + );
1118 return;
1119 }
1120
@@ -1183,7 +1206,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1206 if (isFixedRateMode && provider.supportsFixedRate == false) {
1207 continue;
1208 }
1186 -
1209 +
1210 if (_excludeProviderForReceiveExtraId(provider)) {
1211 continue;
1212 }
@@ -1200,13 +1223,13 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1223 bestRate = providerRate;
1224 bestRateProvider = provider;
1225
1203 - await changeDepositAmount(amount: _depositAmount, isCanonical: true);
1226 + await changeDepositAmount(amount: _depositAmount.toString(), isCanonical: true);
1227
1228 final request = TradeRequest(
1229 fromCurrency: depositCurrency,
1230 toCurrency: receiveCurrency,
1208 - fromAmount: _depositAmount.replaceAll(',', '.'),
1209 - toAmount: _receiveAmount.replaceAll(',', '.'),
1231 + fromAmount: _depositAmount.toString(),
1232 + toAmount: _receiveAmount.toString(),
1233 refundAddress: depositAddress,
1234 toAddress: receiveAddress,
1235 toAddressExtraId: receiveAddressExtraId.trim(),
@@ -1218,8 +1241,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1241 await wallet.walletAddresses.saveAddressesInBox();
1242 }
1243
1221 - var amount = isFixedRateMode ? _receiveAmount : _depositAmount;
1222 - amount = amount.replaceAll(',', '.');
1244 + var amount = isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString();
1245
1246 if (limitsState is LimitsLoadedSuccessfully) {
1247 if (double.tryParse(amount) == null) {
@@ -1266,7 +1288,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1288 }
1289
1290 tradesStore.setTrade(trade);
1269 - if (trade.provider != ExchangeProviderDescription.thorChain) await trade.save();
1291 + if (trade.provider != ExchangeProviderDescription.thorChain) await trade.save();
1292 tradeState = TradeIsCreatedSuccessfully(trade: trade);
1293
1294 /// return after the first successful trade
@@ -1310,8 +1332,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1332 void reset() {
1333 _initialPairBasedOnWallet();
1334 isReceiveAmountEntered = false;
1313 - _depositAmount = '';
1314 - _receiveAmount = '';
1335 + _depositAmount = null;
1336 + _receiveAmount = null;
1337 depositAddress =
1338 depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
1339 receiveAddress =
@@ -1347,8 +1369,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1369
1370 final amount = depositCurrency == CryptoCurrency.btcln
1371 // FIXME amount estimation is broken/impossible for ln, konsti suggested this
1350 - ? (amountParsingProxy
1351 - .parseCryptoString(depositAvailableAmount, depositCurrency).toInt() - 10)
1372 + ? (amountParsingProxy.parseCryptoString(depositAvailableAmount, depositCurrency) -
1373 + Money.fromInt(10, depositCurrency))
1374 : await bitcoin!.estimateFakeSendAllTxAmount(
1375 wallet,
1376 priority,
@@ -1359,7 +1381,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1381 : UnspentCoinType.any,
1382 );
1383
1362 - changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.from(amount)), isCanonical: true);
1384 + changeDepositAmount(amount: amount.toString(), isCanonical: true);
1385 } else if (wallet.type == WalletType.monero) {
1386 final amount = await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any);
1387
@@ -1377,7 +1399,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1399 return;
1400 }
1401
1380 - final balanceAmount = _appStore.amountParsingProxy.getDisplayCryptoStringFromBigInt(balanceForCurrency.fullAvailableBalance, balanceCurrency!);
1402 + final balanceAmount = _appStore.amountParsingProxy.asDisplayString(balanceForCurrency.available);
1403 final balanceDouble = double.tryParse(balanceAmount.replaceAll(',', '.')) ?? 0.0;
1404 if (balanceDouble <= 0) {
1405 changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.zero), isCanonical: true);
@@ -1405,7 +1427,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1427 return;
1428 }
1429
1408 - final balanceWei = balanceForCurrency.fullAvailableBalance;
1430 + final balanceWei = balanceForCurrency.available.amount;
1431 final feeWei = BigInt.parse(feeString);
1432 final amountAfterFeeWei = balanceWei > feeWei ? balanceWei - feeWei : BigInt.zero;
1433 changeDepositAmount(
@@ -1455,14 +1477,14 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1477
1478 void _onPairChange({bool clearBoth = false}) {
1479 if (clearBoth) {
1458 - _depositAmount = '';
1459 - _receiveAmount = '';
1480 + _depositAmount = null;
1481 + _receiveAmount = null;
1482 if (!hasAllAmount) isSendAllEnabled = false;
1483 } else {
1484 if (isFixedRateMode) {
1463 - _depositAmount = '';
1485 + _depositAmount = null;
1486 } else {
1465 - _receiveAmount = '';
1487 + _receiveAmount = null;
1488 }
1489 }
1490 bestRate = 0.0;
@@ -1565,6 +1587,21 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1587 }
1588 }
1589
1590 + String? _addressTypeValidation(String refundAddress, String receiveAddress) {
1591 + final isRefundAddressSP = RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(refundAddress);
1592 + if (isRefundAddressSP) return 'Silent Payment ${S.current.address_not_allowed_as_refund}';
1593 +
1594 + final isReceiveAddressSP = RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(receiveAddress);
1595 + if (isReceiveAddressSP) return 'Silent Payment ${S.current.address_not_allowed_as_receive}';
1596 +
1597 + final isRefundAddressMWEB = RegExp(AddressValidator.mWebAddressPattern).hasMatch(refundAddress);
1598 + if (isRefundAddressMWEB) return 'MWEB ${S.current.address_not_allowed_as_refund}';
1599 +
1600 + final isReceiveAddressMWEB = RegExp(AddressValidator.mWebAddressPattern).hasMatch(receiveAddress);
1601 + if (isReceiveAddressMWEB) return 'MWEB ${S.current.address_not_allowed_as_receive}';
1602 + return null;
1603 + }
1604 +
1605 void _defineIsReceiveAmountEditable() {
1606 /*if ((provider is ChangeNowExchangeProvider)
1607 &&(depositCurrency == CryptoCurrency.xmr)
@@ -1597,8 +1634,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1634
1635 @action
1636 void saveSelectedProviders() {
1600 - _depositAmount = '';
1601 - _receiveAmount = '';
1637 + _depositAmount = null;
1638 + _receiveAmount = null;
1639 isFixedRateMode = false;
1640 _defineIsReceiveAmountEditable();
1641 bestRateProvider = null;
lib/view_model/integrations/deuro_view_model.dart
+60 -42
@@ -9,6 +9,7 @@ import 'package:cake_wallet/store/settings_store.dart';
9 import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
10 import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
11 import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
12 +import 'package:cw_core/amount/money.dart';
13 import 'package:cw_core/crypto_currency.dart';
14 import 'package:cw_core/parse_fixed.dart';
15 import 'package:cw_core/pending_transaction.dart';
@@ -22,7 +23,9 @@ class DEuroViewModel = DEuroViewModelBase with _$DEuroViewModel;
23 abstract class DEuroViewModelBase with Store {
24 final AppStore _appStore;
25
25 - static BigInt get MIN_ACCRUED_INTEREST => BigInt.parse("1000000000000");
26 + static Money get MIN_ACCRUED_INTEREST => Money(BigInt.parse("1000000000000"), CryptoCurrency.deuro);
27 +
28 + static Money get ZERO => Money.zero(CryptoCurrency.deuro);
29
30 DEuroViewModelBase(
31 this._appStore, this.balanceViewModel, this._settingsStore, this._fiatConversationStore,
@@ -51,13 +54,13 @@ abstract class DEuroViewModelBase with Store {
54
55 @computed
56 String get pendingTransactionFiatAmountFormatted {
54 - final amount = transaction == null ? '0.00' : _getDEuroFiatAmount(transaction!.amountFormatted);
55 - return isFiatDisabled ? '' : '$amount ${fiat.title}';
57 + final amount = transaction == null ? "0.00" : _getDEuroFiatAmount(transaction!.amount);
58 + return isFiatDisabled ? "" : "$amount ${fiat.symbol}";
59 }
60
61 @computed
62 String get pendingTransactionFeeFiatAmountFormatted {
60 - var amount = '0.00';
63 + var amount = "0.00";
64 try {
65 if (transaction != null) {
66 final feeCurrency = CryptoCurrency.eth;
@@ -67,42 +70,43 @@ abstract class DEuroViewModelBase with Store {
70 );
71 }
72 } catch (_) {}
70 - return isFiatDisabled ? '' : '$amount ${fiat.title}';
73 + return isFiatDisabled ? "" : "$amount ${fiat.symbol}";
74 }
75
76 @computed
74 - String get accountBalanceFormated {
77 + Money get accountBalance {
78 final dEuroKey = balanceViewModel.balances.keys
79 .firstWhereOrNull((e) => e.title == CryptoCurrency.deuro.title);
77 - if (dEuroKey == null) return '0.00';
78 - return balanceViewModel.balances[dEuroKey]?.availableBalance ?? '0.00';
80 + if (dEuroKey == null) return ZERO;
81 + return wallet.balance[dEuroKey]?.available ?? ZERO;
82 }
83
84 @observable
82 - BigInt savingsBalance = BigInt.zero;
85 + Money savingsBalance = ZERO;
86
87 @computed
85 - String get savingsBalanceFormated =>
86 - evm!.formatterEVMAmountToDouble(amount: savingsBalance).toStringAsFixed(6);
88 + String get fiatSavingsBalanceFormated => _getDEuroFiatAmount(savingsBalance);
89 +
90 + @observable
91 + Money? savingsBalanceV1 = null;
92
93 @computed
89 - String get fiatSavingsBalanceFormated => _getDEuroFiatAmount(savingsBalanceFormated);
94 + String get fiatSavingsBalanceV1Formated => _getDEuroFiatAmount(savingsBalanceV1);
95
96 @observable
97 ExecutionState state = InitialExecutionState();
98
99 @observable
95 - String interestRateFormated = '0';
100 + String interestRateFormated = "0";
101
102 @observable
98 - BigInt accruedInterest = BigInt.zero;
103 + Money accruedInterest = ZERO;
104
105 @computed
101 - String get accruedInterestFormated =>
102 - evm!.formatterEVMAmountToDouble(amount: accruedInterest).toStringAsFixed(6);
106 + String get accruedInterestFormated => accruedInterest.toStringWithPrecision(fractionalDigits: 6);
107
108 @computed
105 - String get fiatAccruedInterestFormated => _getDEuroFiatAmount(accruedInterestFormated);
109 + String get fiatAccruedInterestFormated => _getDEuroFiatAmount(accruedInterest);
110
111 @observable
112 BigInt approvedTokens = BigInt.zero;
@@ -128,8 +132,12 @@ abstract class DEuroViewModelBase with Store {
132 @action
133 Future<void> reloadSavingsUserData() async {
134 approvedTokens = await evm!.getDEuroSavingsApproved(_appStore.wallet!) ?? BigInt.zero;
131 - savingsBalance = await evm!.getDEuroSavingsBalance(_appStore.wallet!) ?? BigInt.zero;
132 - accruedInterest = await evm!.getDEuroAccruedInterest(_appStore.wallet!) ?? BigInt.zero;
135 + savingsBalance = await evm!.getDEuroSavingsBalance(_appStore.wallet!) ?? ZERO;
136 + accruedInterest = await evm!.getDEuroAccruedInterest(_appStore.wallet!) ?? ZERO;
137 +
138 + final v1Balance = await evm!.getDEuroSavingsV1Balance(_appStore.wallet!) ?? ZERO;
139 + savingsBalanceV1 = v1Balance.isZero ? null : v1Balance;
140 +
141 isLoading = false;
142 }
143
@@ -151,9 +159,8 @@ abstract class DEuroViewModelBase with Store {
159 state = TransactionCommitting();
160 final priority = _appStore.settingsStore.getPriority(wallet.type, chainId: wallet.chainId)!;
161 final approval = await evm!.enableDEuroSaving(_appStore.wallet!, priority);
154 - if (approval == null) {
155 - throw Exception('DEuro saving not available');
156 - }
162 + if (approval == null) throw Exception("DEuro saving not available");
163 +
164 approvalTransaction = approval;
165 state = InitialExecutionState();
166 } catch (e) {
@@ -167,13 +174,13 @@ abstract class DEuroViewModelBase with Store {
174 state = TransactionCommitting();
175
176 if (amountRaw.isEmpty || amountRaw.trim().isEmpty) {
170 - throw Exception('Invalid amount: amount cannot be empty');
177 + throw Exception("Invalid amount: amount cannot be empty");
178 }
179
180 final amount = tryParseFixed(amountRaw, 18);
181
182 if (amount == BigInt.zero || amount == null) {
176 - throw Exception('Invalid amount: amount cannot be zero');
183 + throw Exception("Invalid amount: amount cannot be zero");
184 }
185
186 final priority = _appStore.settingsStore.getPriority(wallet.type, chainId: wallet.chainId)!;
@@ -181,9 +188,24 @@ abstract class DEuroViewModelBase with Store {
188 final tx = await (isAdding
189 ? evm!.addDEuroSaving(_appStore.wallet!, amount, priority)
190 : evm!.removeDEuroSaving(_appStore.wallet!, amount, priority));
184 - if (tx == null) {
185 - throw Exception('DEuro saving not available');
186 - }
191 + if (tx == null) throw Exception("DEuro saving not available");
192 +
193 + transaction = tx;
194 + state = InitialExecutionState();
195 + } catch (e) {
196 + state = FailureState(e.toString());
197 + }
198 + }
199 +
200 + @action
201 + Future<void> prepareSavingsV1Withdraw() async {
202 + try {
203 + state = TransactionCommitting();
204 + actionType = DEuroActionType.withdraw;
205 + final priority = _appStore.settingsStore.getPriority(wallet.type, chainId: wallet.chainId)!;
206 + final tx = await evm!.withdrawDEuroSavingV1(_appStore.wallet!, priority);
207 + if (tx == null) throw Exception("DEuro saving not available");
208 +
209 transaction = tx;
210 state = InitialExecutionState();
211 } catch (e) {
@@ -193,13 +215,13 @@ abstract class DEuroViewModelBase with Store {
215
216 Future<void> prepareCollectInterest() async {
217 if (accruedInterest < MIN_ACCRUED_INTEREST) {
196 - state = FailureState('Accrued interest is below minimum threshold');
218 + state = FailureState("Accrued interest is below minimum threshold");
219 return;
220 }
221
222 final formatted = accruedInterestFormated;
201 - if (formatted.isEmpty || formatted == '0.000000') {
202 - state = FailureState('Invalid accrued interest amount');
223 + if (formatted.isEmpty || formatted == "0.000000") {
224 + state = FailureState("Invalid accrued interest amount");
225 return;
226 }
227
@@ -212,9 +234,8 @@ abstract class DEuroViewModelBase with Store {
234 actionType = DEuroActionType.reinvest;
235 final priority = _appStore.settingsStore.getPriority(wallet.type, chainId: wallet.chainId)!;
236 final tx = await evm!.reinvestDEuroInterest(_appStore.wallet!, priority);
215 - if (tx == null) {
216 - throw Exception('DEuro saving not available');
217 - }
237 + if (tx == null) throw Exception("DEuro saving not available");
238 +
239 transaction = tx;
240 state = InitialExecutionState();
241 } catch (e) {
@@ -261,7 +282,9 @@ abstract class DEuroViewModelBase with Store {
282 state = InitialExecutionState();
283 }
284
264 - String _getDEuroFiatAmount(String amount) {
285 + String _getDEuroFiatAmount(Money? amount) {
286 + if (amount == null) return "0.00";
287 +
288 try {
289 var dEuro = CryptoCurrency.deuro;
290 final keys = _fiatConversationStore.prices.keys.toList();
@@ -270,19 +293,14 @@ abstract class DEuroViewModelBase with Store {
293 }
294 return calculateFiatAmount(
295 price: _fiatConversationStore.prices[dEuro]!,
273 - cryptoAmount: amount,
296 + cryptoAmount: amount.toStringWithPrecision(fractionalDigits: 6),
297 );
298 } catch (_) {
276 - return '0.00';
299 + return "0.00";
300 }
301 }
302 }
303
304 class NoEtherState extends ExecutionState {}
305
283 -enum DEuroActionType {
284 - deposit,
285 - withdraw,
286 - reinvest,
287 - none;
288 -}
306 +enum DEuroActionType { deposit, withdraw, reinvest, none }
lib/view_model/payjoin_details_view_model.dart
+2 -2
@@ -110,10 +110,10 @@ abstract class PayjoinDetailsViewModelBase with Store {
110 value: '${transactionInfo!.height}',
111 key: ValueKey('standard_list_item_transaction_details_height_key'),
112 ),
113 - if (transactionInfo!.feeFormatted()?.isNotEmpty ?? false)
113 + if (transactionInfo!.fee != null)
114 StandartListItem(
115 title: S.current.transaction_details_fee,
116 - value: transactionInfo!.feeFormatted()!,
116 + value: transactionInfo!.fee!.toStringWithSymbol(),
117 key: ValueKey('standard_list_item_transaction_details_fee_key'),
118 ),
119 ]);
lib/view_model/payment/payment_view_model.dart
+7 -3
@@ -93,7 +93,7 @@ abstract class PaymentViewModelBase with Store {
93 if (currentWallet != null &&
94 currentWallet.type == detectedWalletType &&
95 !isEVMCompatibleChain(detectedWalletType!)) {
96 - return PaymentFlowResult.currentWalletCompatible();
96 + return PaymentFlowResult.currentWalletCompatible(detectionResult);
97 }
98
99 if (detectedWalletType == WalletType.solana) {
@@ -229,8 +229,12 @@ class PaymentFlowResult {
229 );
230
231 /// Current wallet is compatible
232 - factory PaymentFlowResult.currentWalletCompatible() =>
233 - PaymentFlowResult._(type: PaymentFlowType.currentWalletCompatible);
232 + factory PaymentFlowResult.currentWalletCompatible(
233 + AddressDetectionResult addressDetectionResult) =>
234 + PaymentFlowResult._(
235 + type: PaymentFlowType.currentWalletCompatible,
236 + addressDetectionResult: addressDetectionResult,
237 + );
238
239 /// Single compatible wallet available
240 factory PaymentFlowResult.singleWallet(
lib/view_model/send/output.dart
+42 -91
@@ -1,6 +1,5 @@
1 import 'dart:math' show min;
2 import 'package:cake_wallet/bitcoin/bitcoin.dart';
3 -import 'package:cake_wallet/decred/decred.dart';
3 import 'package:cake_wallet/di.dart';
4 import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
5 import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart';
@@ -9,7 +8,6 @@ import 'package:cake_wallet/entities/parse_address_from_domain.dart';
8 import 'package:cake_wallet/entities/parsed_address.dart';
9 import 'package:cake_wallet/evm/evm.dart';
10 import 'package:cake_wallet/generated/i18n.dart';
12 -import 'package:cake_wallet/monero/monero.dart';
11 import 'package:cake_wallet/reactions/wallet_connect.dart';
12 import 'package:cake_wallet/solana/solana.dart';
13 import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
@@ -17,14 +15,12 @@ import 'package:cake_wallet/store/app_store.dart';
15 import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
16 import 'package:cake_wallet/store/settings_store.dart';
17 import 'package:cake_wallet/tron/tron.dart';
20 -import 'package:cake_wallet/wownero/wownero.dart';
21 -import 'package:cake_wallet/zano/zano.dart';
22 -import 'package:cake_wallet/zcash/zcash.dart';
18 +import 'package:cw_core/amount/amount_sanitizer.dart';
19 +import 'package:cw_core/amount/money.dart';
20 import 'package:cw_core/balance.dart';
21 import 'package:cw_core/crypto_amount_format.dart';
22 import 'package:cw_core/crypto_currency.dart';
23 import 'package:cw_core/currency_for_wallet_type.dart';
27 -import 'package:cw_core/format_fixed.dart';
24 import 'package:cw_core/transaction_history.dart';
25 import 'package:cw_core/transaction_info.dart';
26 import 'package:cw_core/utils/print_verbose.dart';
@@ -50,7 +46,7 @@ abstract class OutputBase with Store {
46 note = '',
47 memo = "",
48 extractedAddress = '',
53 - estimatedFee = '0.0',
49 + estimatedFee = Money.zero(cryptoCurrencyHandler()),
50 parsedAddress = ParsedAddress(addresses: []) {
51 autorun((_) {
52 final status = _wallet.syncStatus;
@@ -64,6 +60,9 @@ abstract class OutputBase with Store {
60
61 bool get useSatoshi => _appStore.amountParsingProxy.useSatoshi(cryptoCurrencyHandler());
62
63 + @observable
64 + bool isFiatEntry = false;
65 +
66 @observable
67 String fiatAmount;
68
@@ -74,8 +73,7 @@ abstract class OutputBase with Store {
73 String? displayName;
74
75 @computed
77 - String get displayCryptoAmount =>
78 - _appStore.amountParsingProxy.getDisplayCryptoAmount(cryptoAmount, cryptoCurrencyHandler());
76 + String get displayCryptoAmount => _appStore.amountParsingProxy.asDisplayString(cryptoAmountMoney);
77
78 @observable
79 String cryptoFullBalance;
@@ -106,9 +104,8 @@ abstract class OutputBase with Store {
104 String roundedCryptoAmount(int digits) => displayCryptoAmount.withMaxDecimals(digits);
105
106 String roundedFiatAmount(int digits) {
109 - if (fiatAmount.split(".").last.length <= digits) {
110 - return fiatAmount;
111 - }
107 + if (fiatAmount.split(".").last.length <= digits) return fiatAmount;
108 +
109 return double.tryParse(fiatAmount.replaceAll(",", ""))?.toStringAsPrecision(digits) ?? '0.0';
110 }
111
@@ -116,65 +113,18 @@ abstract class OutputBase with Store {
113 String? stealthAddress;
114
115 @computed
119 - int get formattedCryptoAmount {
120 - int amount = 0;
116 + Money get cryptoAmountMoney {
117 + if (cryptoAmount.isEmpty) return Money.zero(cryptoCurrencyHandler());
118
119 try {
123 - if (cryptoAmount.isNotEmpty) {
124 - final _cryptoAmount = cryptoAmount.replaceAll(',', '.');
125 - int _amount = 0;
126 - switch (walletType) {
127 - case WalletType.monero:
128 - _amount = monero!.formatterMoneroParseAmount(amount: _cryptoAmount);
129 - break;
130 - case WalletType.bitcoin:
131 - case WalletType.litecoin:
132 - case WalletType.bitcoinCash:
133 - case WalletType.dogecoin:
134 - _amount = cryptoCurrencyHandler().parseAmount(_cryptoAmount).toInt();
135 - break;
136 - case WalletType.decred:
137 - _amount = decred!.formatterStringDoubleToDecredAmount(_cryptoAmount);
138 - break;
139 - case WalletType.ethereum:
140 - case WalletType.polygon:
141 - case WalletType.base:
142 - case WalletType.arbitrum:
143 - case WalletType.bsc:
144 - _amount = evm!.formatterEVMParseAmount(_cryptoAmount);
145 - break;
146 - case WalletType.wownero:
147 - _amount = wownero!.formatterWowneroParseAmount(amount: _cryptoAmount);
148 - break;
149 - case WalletType.zano:
150 - _amount = zano!
151 - .formatterParseAmount(amount: _cryptoAmount, currency: cryptoCurrencyHandler());
152 - break;
153 - case WalletType.zcash:
154 - _amount = zcash!.formatterZcashParseAmount(_cryptoAmount);
155 - break;
156 - case WalletType.none:
157 - case WalletType.haven:
158 - case WalletType.nano:
159 - case WalletType.banano:
160 - case WalletType.solana:
161 - case WalletType.tron:
162 - break;
163 - }
164 -
165 - if (_amount > 0) {
166 - amount = _amount;
167 - }
168 - }
120 + return cryptoCurrencyHandler().parseAmount(cryptoAmount.sanitized());
121 } catch (e) {
170 - amount = 0;
122 + return Money.zero(cryptoCurrencyHandler());
123 }
172 -
173 - return amount;
124 }
125
126 @observable
177 - String estimatedFee;
127 + Money estimatedFee;
128
129 @action
130 Future<void> calculateEstimatedFee() async {
@@ -188,7 +138,7 @@ abstract class OutputBase with Store {
138 if (_settingsStore.getPriority(_wallet.type, chainId: _wallet.chainId) != null) {
139 fee = _wallet.calculateEstimatedFee(
140 _settingsStore.getPriority(_wallet.type, chainId: _wallet.chainId)!,
191 - formattedCryptoAmount,
141 + cryptoAmountMoney.amount.toInt(),
142 );
143 }
144
@@ -199,41 +149,35 @@ abstract class OutputBase with Store {
149 case WalletType.bitcoinCash:
150 case WalletType.dogecoin:
151 case WalletType.decred:
202 - estimatedFee = walletTypeToCryptoCurrency(_wallet.type).formatAmount(BigInt.from(fee));
152 + case WalletType.zano:
153 + estimatedFee = Money.fromInt(fee, walletTypeToCryptoCurrency(_wallet.type));
154 break;
155 case WalletType.bitcoin:
156 if (cryptoCurrencyHandler() == CryptoCurrency.btcln) {
206 - estimatedFee =
207 - _appStore.amountParsingProxy.getDisplayCryptoString(10, cryptoCurrencyHandler());
157 + estimatedFee = Money.fromInt(10, cryptoCurrencyHandler());
158 break;
159 }
160 if (_settingsStore.getPriority(_wallet.type) ==
161 bitcoin!.getBitcoinTransactionPriorityCustom()) {
162 fee = bitcoin!.getEstimatedFeeWithFeeRate(
213 - _wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount);
163 + _wallet, _settingsStore.customBitcoinFeeRate, cryptoAmountMoney.amount.toInt());
164 }
165
216 - estimatedFee = _appStore.amountParsingProxy.getDisplayCryptoString(fee, cryptoCurrencyHandler());
166 + estimatedFee = Money.fromInt(fee, cryptoCurrencyHandler());
167 break;
168 case WalletType.solana:
219 - estimatedFee = solana!.getEstimateFees(_wallet).toString();
220 - break;
221 - case WalletType.zano:
222 - estimatedFee = zano!
223 - .formatterIntAmountToDouble(
224 - amount: fee, currency: cryptoCurrencyHandler(), forFee: true)
225 - .toString();
169 + estimatedFee = solana!.getEstimateFees(_wallet) ?? Money.zero(CryptoCurrency.sol);
170 break;
171 case WalletType.tron:
172 if (cryptoCurrencyHandler() == CryptoCurrency.trx) {
229 - estimatedFee = tron!.getTronNativeEstimatedFee(_wallet).toString();
173 + estimatedFee = tron!.getTronNativeEstimatedFee(_wallet) ?? Money.zero(CryptoCurrency.trx);
174 } else {
231 - estimatedFee = tron!.getTronTRC20EstimatedFee(_wallet).toString();
175 + estimatedFee = tron!.getTronTRC20EstimatedFee(_wallet) ?? Money.zero(CryptoCurrency.trx);
176 }
177 break;
178
179 case WalletType.zcash:
236 - estimatedFee = zcash!.formatterZcashAmountToDouble(amount: BigInt.from(fee)).toString();
180 + estimatedFee = Money.fromInt(fee, cryptoCurrencyHandler());
181 break;
182
183 /// EVMs
@@ -242,16 +186,19 @@ abstract class OutputBase with Store {
186 case WalletType.base:
187 case WalletType.arbitrum:
188 case WalletType.bsc:
245 - final isNative = cryptoCurrencyHandler() == CryptoCurrency.eth ||
246 - cryptoCurrencyHandler() == CryptoCurrency.maticpoly ||
247 - cryptoCurrencyHandler() == CryptoCurrency.baseEth ||
248 - cryptoCurrencyHandler() == CryptoCurrency.arbEth ||
249 - cryptoCurrencyHandler() == CryptoCurrency.bnb;
250 - String? fee = isNative
189 + final isNative = [
190 + CryptoCurrency.eth,
191 + CryptoCurrency.maticpoly,
192 + CryptoCurrency.baseEth,
193 + CryptoCurrency.arbEth,
194 + CryptoCurrency.bnb
195 + ].contains(cryptoCurrencyHandler());
196 +
197 + final fee = isNative
198 ? evm!.getEVMNativeEstimatedFee(_wallet)
199 : evm!.getEVMERC20EstimatedFee(_wallet);
200
254 - estimatedFee = formatFixed(BigInt.parse(fee ?? '0.0'), 18, fractionalDigits: 12);
201 + estimatedFee = Money(BigInt.parse(fee ?? '0.0'), walletTypeToCryptoCurrency(_wallet.type));
202 break;
203
204 /// end EVMs
@@ -275,12 +222,12 @@ abstract class OutputBase with Store {
222
223 try {
224 final currency = (isEVMCompatibleChain(_wallet.type) ||
278 - [WalletType.solana, WalletType.tron].contains(_wallet.type))
225 + [WalletType.solana, WalletType.tron].contains(_wallet.type)) ||
226 + cryptoCurrencyHandler() == CryptoCurrency.btcln
227 ? _wallet.currency
228 : cryptoCurrencyHandler();
229
282 - final cryptoAmount =
283 - double.parse(_appStore.amountParsingProxy.getCanonicalCryptoAmount(estimatedFee, currency));
230 + final cryptoAmount = double.parse(estimatedFee.toString());
231
232 return calculateFiatAmountRaw(
233 price: _fiatConversationStore.prices[currency]!, cryptoAmount: cryptoAmount);
@@ -294,7 +241,7 @@ abstract class OutputBase with Store {
241
242 WalletType get walletType => _wallet.type;
243
297 - final CryptoCurrency Function() cryptoCurrencyHandler;
244 + final CryptoCurrency Function([CryptoCurrency?]) cryptoCurrencyHandler;
245 final FiatConversionStore _fiatConversationStore;
246 final AppStore _appStore;
247
@@ -401,6 +348,10 @@ abstract class OutputBase with Store {
348 if(parsedAddress.name.isNotEmpty) {
349 displayName = parsedAddress.name;
350 }
351 +
352 + if (parsedAddress.parseFrom == ParseFrom.lnurlpay) {
353 + cryptoCurrencyHandler(CryptoCurrency.btcln);
354 + }
355 extractedAddress = await extractAddressFromParsed(context, parsedAddress);
356 note = parsedAddress.description;
357 }
lib/view_model/send/send_view_model.dart
+50 -39
@@ -53,6 +53,7 @@ import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_mod
53 import 'package:cake_wallet/wownero/wownero.dart';
54 import 'package:cake_wallet/zano/zano.dart';
55 import 'package:cake_wallet/zcash/zcash.dart';
56 +import 'package:cw_core/amount/money.dart';
57 import 'package:cw_core/crypto_currency.dart';
58 import 'package:cw_core/currency_for_wallet_type.dart';
59 import 'package:cw_core/erc20_token.dart';
@@ -121,7 +122,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
122 fiatFromSettings = _appStore.settingsStore.fiatCurrency,
123 fiatCurrencies = FiatCurrency.all,
124 super(appStore: _appStore) {
124 - outputs.add(Output(wallet, _appStore, _fiatConversationStore, () => selectedCryptoCurrency));
125 + outputs.add(Output(wallet, _appStore, _fiatConversationStore, _outputCryptoCurrencyHandler));
126
127 unspentCoinsListViewModel
128 .initialSetup()
@@ -162,12 +163,19 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
163
164 bool get isEVMWallet => isEVMCompatibleChain(walletType);
165
166 + @action
167 + CryptoCurrency _outputCryptoCurrencyHandler([CryptoCurrency? override]) {
168 + if (override != null && override != selectedCryptoCurrency) selectedCryptoCurrency = override;
169 +
170 + return selectedCryptoCurrency;
171 + }
172 +
173 @action
174 void setShowAddressBookPopup(bool value) => _settingsStore.showAddressBookPopupEnabled = value;
175
176 @action
177 void addOutput() =>
170 - outputs.add(Output(wallet, _appStore, _fiatConversationStore, () => selectedCryptoCurrency));
178 + outputs.add(Output(wallet, _appStore, _fiatConversationStore, _outputCryptoCurrencyHandler));
179
180 @action
181 void removeOutput(Output output) {
@@ -310,10 +318,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
318 // Handle case where balance might not be available yet (e.g., during chain switch)
319 final balanceForCurrency = wallet.balance[selectedCryptoCurrency];
320 if (balanceForCurrency == null) {
313 - return _appStore.amountParsingProxy.getDisplayCryptoString(0, selectedCryptoCurrency);
321 + return _appStore.amountParsingProxy.asDisplayString(Money.zero(selectedCryptoCurrency));
322 }
315 - return _appStore.amountParsingProxy.getDisplayCryptoStringFromBigInt(
316 - wallet.balance[selectedCryptoCurrency]!.fullAvailableBalance, selectedCryptoCurrency);
323 + return _appStore.amountParsingProxy
324 + .asDisplayString(wallet.balance[selectedCryptoCurrency]!.available);
325 }
326
327 @action
@@ -340,7 +348,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
348 // only for electrum, monero, wownero, decred wallets atm:
349 switch (wallet.type) {
350 case WalletType.bitcoin:
343 - if (coinTypeToSpendFrom == UnspentCoinType.lightning) return balance;
351 + if (selectedCryptoCurrency == CryptoCurrency.btcln) return balance;
352 return _appStore.amountParsingProxy.getDisplayCryptoString(
353 await unspentCoinsListViewModel.getSendingBalance(coinTypeToSpendFrom),
354 walletTypeToCryptoCurrency(walletType));
@@ -420,6 +428,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
428 String get selectedCryptoCurrencySymbol =>
429 amountParsingProxy.getCryptoSymbol(selectedCryptoCurrency);
430
431 + @computed
432 + bool get useBaseUnits => amountParsingProxy.useSatoshi(selectedCryptoCurrency);
433 +
434 List<CryptoCurrency> currencies;
435
436 WalletType get walletType => wallet.type;
@@ -681,7 +692,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
692 wallet,
693 routerTo,
694 routerData,
684 - BigInt.zero,
695 + Money.zero(wallet.currency),
696 priority,
697 useBlinkProtection: canSupportBlinkProtection(selectedChainId)
698 ? _settingsStore.useBlinkProtection
@@ -804,7 +815,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
815 wallet,
816 routerTo,
817 routerData,
807 - routerValueWei,
818 + Money(routerValueWei, wallet.currency),
819 priority,
820 sourceTokenAddress: tokenContract,
821 sourceTokenAmount: requiredAmount,
@@ -842,7 +853,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
853 // Fallback to estimate if not available
854 final fee = actualFee > 0 ? actualFee : 0.0005;
855
845 - final amount = double.tryParse(trade.amount) ?? 0.0;
856 + final fromCurrency = trade.from ?? CryptoCurrency.sol;
857 + final amount = Money.tryParse(trade.amount, fromCurrency) ?? Money.zero(fromCurrency);
858
859 pendingTransaction = await solana!.signAndPrepareJupiterSwapTransaction(
860 wallet,
@@ -850,7 +862,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
862 requestId!,
863 trade.payoutAddress ?? '',
864 amount,
853 - fee,
865 + Money.tryParse(fee.toString(), CryptoCurrency.sol) ?? Money.zero(CryptoCurrency.sol),
866 );
867
868 state = ExecutedSuccessfullyState();
@@ -1104,41 +1116,41 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
1116 // instead, they rely on the tx to eventually get fetched at sync time, which can take a while
1117 if(isEVMWallet) {
1118 wallet.transactionHistory.addOne(evm!.getTransactionInfo(
1107 - id: pendingTransaction!.evmTxHashFromRawHex!,
1108 - height: 0,
1109 - // FIXME(malik) this is even more critical given all the issues we had with decimal point parsing. with money it'll be easy
1110 - ethAmount: selectedCryptoCurrency.parseAmount(outputs.first.cryptoAmount),
1111 - ethFee: CryptoCurrency.eth.parseAmount(pendingTransaction!.feeFormattedValue),
1112 - tokenSymbol: selectedCryptoCurrency.title,
1113 - direction: TransactionDirection.outgoing,
1114 - isPending: true,
1115 - date: DateTime.now(),
1116 - confirmations: 0,
1117 - chainId: wallet.chainId ?? 0));
1119 + id: pendingTransaction!.evmTxHashFromRawHex!,
1120 + height: 0,
1121 + amount: outputs.first.cryptoAmountMoney,
1122 + fee: pendingTransaction!.fee,
1123 + tokenSymbol: selectedCryptoCurrency.title,
1124 + direction: TransactionDirection.outgoing,
1125 + isPending: true,
1126 + date: DateTime.now(),
1127 + confirmations: 0,
1128 + chainId: wallet.chainId ?? 0,
1129 + ));
1130 }
1131
1132 if(walletType == WalletType.solana) {
1133 wallet.transactionHistory.addOne(solana!.getTransactionInfo(
1122 - id: pendingTransaction!.id,
1123 - blockTime: DateTime.now(),
1124 - tokenSymbol: selectedCryptoCurrency.title,
1125 - to: "",
1126 - from: "",
1127 - direction: TransactionDirection.outgoing,
1128 - solAmount: solana!.getPendingTransactionAmount(pendingTransaction!),
1129 - isPending: true,
1130 - txFee: solana!.getPendingTransactionFee(pendingTransaction!)));
1134 + id: pendingTransaction!.id,
1135 + blockTime: DateTime.now(),
1136 + to: "",
1137 + from: "",
1138 + direction: TransactionDirection.outgoing,
1139 + amount: pendingTransaction!.amount,
1140 + isPending: true,
1141 + fee: pendingTransaction!.fee,
1142 + ));
1143 }
1144
1145 if (walletType == WalletType.tron) {
1146 wallet.transactionHistory.addOne(tron!.getTransactionInfo(
1135 - id: pendingTransaction!.id,
1136 - blockTime: DateTime.now(),
1137 - tokenSymbol: selectedCryptoCurrency.title,
1138 - direction: TransactionDirection.outgoing,
1139 - tronAmount: selectedCryptoCurrency.parseAmount(outputs.first.cryptoAmount),
1140 - isPending: true,
1141 - txFee: CryptoCurrency.trx.parseAmount(outputs.first.estimatedFee).toInt()));
1147 + id: pendingTransaction!.id,
1148 + blockTime: DateTime.now(),
1149 + direction: TransactionDirection.outgoing,
1150 + amount: outputs.first.cryptoAmountMoney,
1151 + isPending: true,
1152 + fee: outputs.first.estimatedFee,
1153 + ));
1154 }
1155
1156 if (pendingTransaction!.id.isNotEmpty) {
@@ -1419,9 +1431,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
1431
1432 return await evm!.createTokenApproval(
1433 wallet,
1422 - requiredAmount,
1434 + Money(requiredAmount, erc20Token),
1435 spender,
1424 - erc20Token,
1436 priority,
1437 useBlinkProtection:
1438 canSupportBlinkProtection(selectedChainId) ? _settingsStore.useBlinkProtection : false,
lib/view_model/send/template_view_model.dart
+10 -5
@@ -23,8 +23,8 @@ abstract class TemplateViewModelBase with Store {
23 _appStore = appStore,
24 _fiatConversationStore = fiatConversationStore,
25 _currency = wallet.currency,
26 - output = Output(wallet, appStore, fiatConversationStore, () => wallet.currency) {
27 - output = Output(_wallet, _appStore, _fiatConversationStore, () => _currency);
26 + output = Output(wallet, appStore, fiatConversationStore, ([_]) => wallet.currency) {
27 + output = Output(_wallet, _appStore, _fiatConversationStore, _outputCryptoCurrencyHandler);
28 }
29
30 @observable
@@ -53,15 +53,20 @@ abstract class TemplateViewModelBase with Store {
53 output.reset();
54 }
55
56 - Template toTemplate({required String cryptoCurrency, required String fiatCurrency}) {
57 - return Template(
56 + Template toTemplate({required String cryptoCurrency, required String fiatCurrency}) => Template(
57 isCurrencySelectedRaw: isCryptoSelected,
58 nameRaw: name,
59 addressRaw: address,
60 cryptoCurrencyRaw: cryptoCurrency,
61 fiatCurrencyRaw: fiatCurrency,
62 amountRaw: output.cryptoAmount,
64 - amountFiatRaw: output.fiatAmount);
63 + amountFiatRaw: output.fiatAmount,
64 + );
65 +
66 + CryptoCurrency _outputCryptoCurrencyHandler([CryptoCurrency? override]) {
67 + if (override != null && _currency != override) changeSelectedCurrency(override);
68 +
69 + return _currency;
70 }
71
72 @action
lib/view_model/support_view_model.dart
+5 -5
@@ -56,11 +56,11 @@ abstract class SupportViewModelBase with Store {
56 icon: 'assets/images/change_now.png',
57 linkTitle: 'support@changenow.io',
58 link: 'mailto:support@changenow.io'),
59 - LinkListItem(
60 - title: 'SideShift',
61 - icon: 'assets/images/sideshift.png',
62 - linkTitle: 'help.sideshift.ai',
63 - link: 'https://help.sideshift.ai/en/'),
59 + // LinkListItem(
60 + // title: 'SideShift',
61 + // icon: 'assets/images/sideshift.png',
62 + // linkTitle: 'help.sideshift.ai',
63 + // link: 'https://help.sideshift.ai/en/'),
64 LinkListItem(
65 title: 'SimpleSwap',
66 icon: 'assets/images/simpleSwap.png',
lib/view_model/transaction_details_view_model.dart
+14 -13
@@ -79,22 +79,22 @@ class TxDetailRowDefinition {
79 title: S.current.transaction_details_date,
80 valueGetter: (vm) => DateFormatter.withCurrentLocal().format(vm.transactionInfo.date)),
81
82 -
82 TxDetailRowDefinition(
84 - keyString: "standard_list_item_transaction_details_height_key",
85 - title: S.current.transaction_details_height,
86 - valueGetter: (vm) => vm.transactionInfo.height?.toString() ?? "",
87 - applicable: (vm) => !([WalletType.solana, WalletType.tron].contains(vm.wallet.type) &&
88 - !isLightning(vm.transactionInfo))),
89 -
83 + keyString: "standard_list_item_transaction_details_height_key",
84 + title: S.current.transaction_details_height,
85 + valueGetter: (vm) => vm.transactionInfo.height?.toString() ?? "",
86 + applicable: (vm) =>
87 + ![WalletType.solana, WalletType.tron].contains(vm.wallet.type) ||
88 + !isLightning(vm.transactionInfo),
89 + ),
90
91 TxDetailRowDefinition(
92 keyString: "standard_list_item_transaction_details_fee_key",
93 title: S.current.transaction_details_fee,
94 - valueGetter: (vm) => vm.transactionInfo.feeFormatted()!,
94 + valueGetter: (vm) => vm.transactionInfo.fee!.toStringWithSymbol(),
95 applicable: (vm) =>
96 vm.wallet.type != WalletType.nano &&
97 - (vm.transactionInfo.feeFormatted() ?? "").isNotEmpty),
97 + (vm.transactionInfo.fee?.toStringWithSymbol() ?? "").isNotEmpty),
98
99
100 TxDetailRowDefinition(
@@ -496,14 +496,15 @@ abstract class TransactionDetailsViewModelBase with Store {
496 RBFListItems.add(
497 StandartListItem(
498 title: S.current.old_fee,
499 - value: tx.feeFormatted() ?? '0.0',
499 + value: tx.fee?.toStringWithSymbol() ?? '0.0',
500 key: ValueKey('standard_list_item_rbf_old_fee_key'),
501 ),
502 );
503
504 if (transactionInfo.fee != null && rawTransaction.isNotEmpty) {
505 final size = bitcoin!.getTransactionVSize(wallet, rawTransaction);
506 - final recommendedRate = (transactionInfo.fee! / size).round() + 1;
506 + final recommendedRate = (transactionInfo.fee! / BigInt.from(size)) +
507 + transactionInfo.fee!.copyWith(amount: BigInt.one);
508
509 RBFListItems.add(
510 StandartListItem(title: 'New recommended fee rate', value: '$recommendedRate sat/byte'));
@@ -609,14 +610,14 @@ abstract class TransactionDetailsViewModelBase with Store {
610 if (wallet.type == WalletType.bitcoin) {
611 final crypto = isLightning(transactionInfo) ? CryptoCurrency.btcln : CryptoCurrency.btc;
612 final amount = _appStore.amountParsingProxy
612 - .getDisplayCryptoString(transactionInfo.amount, crypto)
613 + .asDisplayString(transactionInfo.amount)
614 .withMaxDecimals(8)
615 .withLocalSeperator(_appStore.settingsStore.languageCode);
616
617 return '$amount ${_appStore.amountParsingProxy.getCryptoSymbol(crypto)}';
618 }
619
619 - return transactionInfo.amountFormatted();
620 + return transactionInfo.amount.toStringWithSymbol();
621 }
622
623 void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo, newFee);
lib/wownero/cw_wownero.dart
+8 -8
@@ -267,14 +267,14 @@ class CWWownero extends Wownero {
267 WowneroTransactionCreationCredentials(
268 outputs: outputs
269 .map((out) => OutputInfo(
270 - fiatAmount: out.fiatAmount,
271 - cryptoAmount: out.cryptoAmount,
272 - address: out.address,
273 - note: out.note,
274 - sendAll: out.sendAll,
275 - extractedAddress: out.extractedAddress,
276 - isParsedAddress: out.isParsedAddress,
277 - formattedCryptoAmount: out.formattedCryptoAmount))
270 + fiatAmount: out.fiatAmount,
271 + cryptoAmount: out.cryptoAmountMoney,
272 + address: out.address,
273 + note: out.note,
274 + sendAll: out.sendAll,
275 + extractedAddress: out.extractedAddress,
276 + isParsedAddress: out.isParsedAddress,
277 + ))
278 .toList(),
279 priority: priority as MoneroTransactionPriority);
280
lib/zano/cw_zano.dart
+19 -17
@@ -70,23 +70,25 @@ class CWZano extends Zano {
70 }
71
72 @override
73 - Object createZanoTransactionCredentials({required List<Output> outputs, required TransactionPriority priority, required CryptoCurrency currency}) {
74 - return ZanoTransactionCredentials(
75 - outputs: outputs
76 - .map((out) => OutputInfo(
77 - fiatAmount: out.fiatAmount,
78 - cryptoAmount: out.cryptoAmount,
79 - address: out.address,
80 - note: out.note,
81 - sendAll: out.sendAll,
82 - extractedAddress: out.extractedAddress,
83 - isParsedAddress: out.isParsedAddress,
84 - formattedCryptoAmount: out.formattedCryptoAmount))
85 - .toList(),
86 - priority: priority as MoneroTransactionPriority,
87 - currency: currency,
88 - );
89 - }
73 + Object createZanoTransactionCredentials(
74 + {required List<Output> outputs,
75 + required TransactionPriority priority,
76 + required CryptoCurrency currency}) =>
77 + ZanoTransactionCredentials(
78 + outputs: outputs
79 + .map((out) => OutputInfo(
80 + fiatAmount: out.fiatAmount,
81 + cryptoAmount: out.cryptoAmountMoney,
82 + address: out.address,
83 + note: out.note,
84 + sendAll: out.sendAll,
85 + extractedAddress: out.extractedAddress,
86 + isParsedAddress: out.isParsedAddress,
87 + ))
88 + .toList(),
89 + priority: priority as MoneroTransactionPriority,
90 + currency: currency,
91 + );
92
93 @override
94 double formatterIntAmountToDouble({required int amount, required CryptoCurrency currency, required bool forFee}) {
lib/zcash/cw_zcash.dart
+13 -15
@@ -43,23 +43,21 @@ class CWZcash extends Zcash {
43
44 @override
45 Object createZcashTransactionCredentials(List<Output> outputs,
46 - {required CryptoCurrency currency,
47 - int? feeRate,
48 - TransactionPriority? priority}) {
49 - final txPriority = (priority ?? getZcashTransactionPriorityAutomatic())
50 - as MoneroTransactionPriority;
46 + {required CryptoCurrency currency, int? feeRate, TransactionPriority? priority}) {
47 + final txPriority =
48 + (priority ?? getZcashTransactionPriorityAutomatic()) as MoneroTransactionPriority;
49 return ZcashTransactionCredentials(
50 outputs: outputs
51 .map((out) => OutputInfo(
54 - fiatAmount: out.fiatAmount,
55 - cryptoAmount: out.cryptoAmount,
56 - address: out.address,
57 - note: out.note,
58 - memo: out.memo,
59 - sendAll: out.sendAll,
60 - extractedAddress: out.extractedAddress,
61 - isParsedAddress: out.isParsedAddress,
62 - formattedCryptoAmount: out.formattedCryptoAmount))
52 + fiatAmount: out.fiatAmount,
53 + cryptoAmount: out.cryptoAmountMoney,
54 + address: out.address,
55 + note: out.note,
56 + memo: out.memo,
57 + sendAll: out.sendAll,
58 + extractedAddress: out.extractedAddress,
59 + isParsedAddress: out.isParsedAddress,
60 + ))
61 .toList(),
62 priority: txPriority,
63 currency: currency,
@@ -90,7 +88,7 @@ class CWZcash extends Zcash {
88
89 @override
90 int formatterZcashParseAmount(String amount) {
93 - return CryptoCurrency.zec.parseAmount(amount).toInt();
91 + return CryptoCurrency.zec.parseAmount(amount).amount.toInt();
92 }
93
94 @override
res/values/strings_ar.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "هذا العنوان من ${emoji} على Yat",
41 "address_label": "تسمية العنوان",
42 "address_label_explainer": "يمكنك الوصول إلى جميع عناوينك المستخدمة سابقًا وإدارتها بالضغط على «العناوين»",
43 + "address_not_allowed_as_receive": "العنوان غير مسموح به كعنوان استلام",
44 + "address_not_allowed_as_refund": "العنوان غير مسموح به كعنوان استرداد",
45 "address_or_alias": "العنوان أو الاسم المستعار",
46 "address_remove_contact": "إزالة جهة الاتصال",
47 "address_remove_content": "هل أنت متأكد أنك تريد إزالة جهة الاتصال المحددة؟",
res/values/strings_bg.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Този адрес е от ${emoji} в Yat",
41 "address_label": "Етикет на адреса",
42 "address_label_explainer": "Можете да получите достъп и да управлявате всички използвани преди адреси, като натиснете „Addresses“.",
43 + "address_not_allowed_as_receive": "адресът не е разрешен като адрес за получаване",
44 + "address_not_allowed_as_refund": "адресът не е разрешен като адрес за възстановяване на средства",
45 "address_or_alias": "Адрес или псевдоним",
46 "address_remove_contact": "Премахване на контакт",
47 "address_remove_content": "Сигурни ли сте, че искате да премахнете избрания контакт?",
res/values/strings_cs.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Tato adresa je z ${emoji} na Yat",
41 "address_label": "Štítek adresy",
42 "address_label_explainer": "Ke všem dříve používaným adresám můžete přistupovat a spravovat je stisknutím tlačítka Adresy",
43 + "address_not_allowed_as_receive": "adresa není povolena jako adresa pro příjem",
44 + "address_not_allowed_as_refund": "address not allowed as refund address",
45 "address_or_alias": "Adresa nebo alias",
46 "address_remove_contact": "Odebrat kontakt",
47 "address_remove_content": "Opravdu chcete odstranit vybraný kontakt?",
res/values/strings_de.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Diese Adresse stammt von ${emoji} auf Yat",
41 "address_label": "Adressbezeichnung",
42 "address_label_explainer": "Sie können auf alle Ihre zuvor verwendeten Adressen zugreifen und diese verwalten, indem Sie auf „Adressen“ tippen.",
43 + "address_not_allowed_as_receive": "Adresse als Empfangsadresse nicht zulässig",
44 + "address_not_allowed_as_refund": "Adresse als Rückerstattungsadresse nicht zulässig",
45 "address_or_alias": "Adresse oder Alias",
46 "address_remove_contact": "Kontakt entfernen",
47 "address_remove_content": "Sind Sie sicher, dass Sie den ausgewählten Kontakt entfernen möchten?",
res/values/strings_en.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "This address is from ${emoji} on Yat",
41 "address_label": "Address label",
42 "address_label_explainer": "You can access and manage all your previously used addressses by pressing Addresses",
43 + "address_not_allowed_as_receive": "address not allowed as receive address",
44 + "address_not_allowed_as_refund": "address not allowed as refund address",
45 "address_or_alias": "Address or alias",
46 "address_remove_contact": "Remove contact",
47 "address_remove_content": "Are you sure that you want to remove selected contact?",
res/values/strings_es.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Esta dirección proviene de ${emoji} en Yat",
41 "address_label": "Etiqueta de la dirección",
42 "address_label_explainer": "Puede acceder y gestionar todas las direcciones que ha usado anteriormente pulsando Direcciones",
43 + "address_not_allowed_as_receive": "dirección no permitida como dirección de recepción",
44 + "address_not_allowed_as_refund": "dirección no permitida como dirección de reembolso",
45 "address_or_alias": "Dirección o alias",
46 "address_remove_contact": "Eliminar contacto",
47 "address_remove_content": "¿Seguro que deseas eliminar el contacto seleccionado?",
res/values/strings_fa.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "این آدرس از ${emoji} در Yat است",
41 "address_label": "برچسب آدرس",
42 "address_label_explainer": "می‌توانید با زدن Addresses به همه آدرس‌هایی که قبلاً استفاده کرده‌اید دسترسی داشته باشید و آن‌ها را مدیریت کنید",
43 + "address_not_allowed_as_receive": "آدرس به عنوان آدرس دریافت مجاز نیست",
44 + "address_not_allowed_as_refund": "آدرس به عنوان آدرس بازپرداخت مجاز نیست",
45 "address_or_alias": "آدرس یا نام مستعار",
46 "address_remove_contact": "حذف مخاطب",
47 "address_remove_content": "آیا مطمئن هستید که می‌خواهید مخاطب انتخاب‌شده را حذف کنید؟",
res/values/strings_fr.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Cette adresse provient de ${emoji} sur Yat",
41 "address_label": "Libellé de l'adresse",
42 "address_label_explainer": "Vous pouvez accéder à toutes vos adresses déjà utilisées et les gérer en appuyant sur Adresses",
43 + "address_not_allowed_as_receive": "adresse non autorisée comme adresse de réception",
44 + "address_not_allowed_as_refund": "adresse non autorisée comme adresse de remboursement",
45 "address_or_alias": "Adresse ou alias",
46 "address_remove_contact": "Supprimer le contact",
47 "address_remove_content": "Êtes-vous sûr de vouloir supprimer le contact sélectionné ?",
res/values/strings_gn.arb
+2
@@ -35,6 +35,8 @@
35 "address_from_yat": "Ko dirección ou ${emoji} rehegua Yat-pe",
36 "address_label": "Kundaharape reramoĩ",
37 "address_label_explainer": "Ikatu reike ha remboguata opaite ne kundaha reipuruva’ekue eikutu hag̃ua Addresses.",
38 + "address_not_allowed_as_receive": "dirección ndojehejáiva dirección de recepción ramo",
39 + "address_not_allowed_as_refund": "dirección ndojehejáiva dirección de reembolso ramo",
40 "address_or_alias": "Kundaharape térã alias",
41 "address_remove_contact": "Emboguete hag̃ua pe contacto",
42 "address_remove_content": "¿Eime hag̃ua remboguese pe contacto ojeporavóva?",
res/values/strings_ha.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Wannan adireshi daga ${emoji} ne a Yat",
41 "address_label": "Alamar adireshi",
42 "address_label_explainer": "Za ka iya samun dama da sarrafa duk adireshoshin da ka yi amfani da su a baya ta danna Addresses",
43 + "address_not_allowed_as_receive": "adireshin da ba a yarda da adireshin karɓa ba",
44 + "address_not_allowed_as_refund": "adireshin da ba a yarda a matsayin adireshin mayar da kuɗi ba",
45 "address_or_alias": "Adireshi ko laƙabi",
46 "address_remove_contact": "Cire tuntuɓa",
47 "address_remove_content": "Kuna da tabbacin kuna so ku cire wannan lambar tuntuɓa da aka zaɓa?",
res/values/strings_hi.arb
+3 -1
@@ -40,6 +40,8 @@
40 "address_from_yat": "यह पता Yat पर ${emoji} से है",
41 "address_label": "पता लेबल",
42 "address_label_explainer": "आप \"Addresses\" दबाकर अपने पहले उपयोग किए गए सभी पतों तक पहुंच सकते हैं और उन्हें प्रबंधित कर सकते हैं।",
43 + "address_not_allowed_as_receive": "प्राप्त पते के रूप में पते की अनुमति नहीं है",
44 + "address_not_allowed_as_refund": "धनवापसी पते के रूप में पते की अनुमति नहीं है",
45 "address_or_alias": "पता या उपनाम",
46 "address_remove_contact": "संपर्क हटाएँ",
47 "address_remove_content": "क्या आप वाकई चयनित संपर्क को हटाना चाहते हैं?",
@@ -732,8 +734,8 @@
734 "payjoin_unavailable_sheet_title": "Payjoin उपलब्ध क्यों नहीं है?",
735 "payment_id": "भुगतान आईडी: ",
736 "payment_made_easy": "भुगतान करना आसान",
735 - "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।",
737 "payment_was_received": "आपका भुगतान प्राप्त हो गया।",
738 + "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।",
739 "payments": "भुगतान",
740 "pending": " (लंबित)",
741 "percentageOf": "${amount} का",
res/values/strings_hr.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Ova adresa je od ${emoji} na Yat-u",
41 "address_label": "Oznaka adrese",
42 "address_label_explainer": "Možete pristupiti svim prethodno korištenim adresama i upravljati njima pritiskom na Adrese",
43 + "address_not_allowed_as_receive": "adresa nije dopuštena kao adresa primanja",
44 + "address_not_allowed_as_refund": "adresa nije dopuštena kao adresa za povrat novca",
45 "address_or_alias": "Adresa ili alias",
46 "address_remove_contact": "Ukloni kontakt",
47 "address_remove_content": "Jeste li sigurni da želite ukloniti odabrani kontakt?",
res/values/strings_hy.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Այս հասցեն ստացվել է Yat-ում ${emoji}-ից",
41 "address_label": "Հասցեի պիտակ",
42 "address_label_explainer": "Դուք կարող եք մուտք գործել և կառավարել ձեր նախկինում օգտագործված բոլոր հասցեները՝ սեղմելով «Հասցեներ»",
43 + "address_not_allowed_as_receive": "հասցեն չի թույլատրվում որպես ստացման հասցե",
44 + "address_not_allowed_as_refund": "հասցեն չի թույլատրվում որպես վերադարձի հասցե",
45 "address_or_alias": "Հասցե կամ այլանուն",
46 "address_remove_contact": "Հեռացնել կոնտակտը",
47 "address_remove_content": "Վստա՞հ եք, որ ցանկանում եք հեռացնել ընտրված կոնտակտը:",
res/values/strings_id.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Alamat ini berasal dari ${emoji} di Yat",
41 "address_label": "Label alamat",
42 "address_label_explainer": "Anda dapat mengakses dan mengelola semua alamat yang pernah Anda gunakan dengan menekan Alamat",
43 + "address_not_allowed_as_receive": "alamat tidak diperbolehkan sebagai alamat terima",
44 + "address_not_allowed_as_refund": "alamat tidak diperbolehkan sebagai alamat pengembalian dana",
45 "address_or_alias": "Alamat atau alias",
46 "address_remove_contact": "Hapus kontak",
47 "address_remove_content": "Apakah Anda yakin ingin menghapus kontak yang dipilih?",
res/values/strings_it.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Questo indirizzo proviene da ${emoji} su Yat",
41 "address_label": "Etichetta dell'indirizzo",
42 "address_label_explainer": "Puoi accedere e gestire tutti gli indirizzi utilizzati in precedenza premendo Indirizzi",
43 + "address_not_allowed_as_receive": "indirizzo non consentito come indirizzo di ricezione",
44 + "address_not_allowed_as_refund": "indirizzo non consentito come indirizzo di rimborso",
45 "address_or_alias": "Indirizzo o alias",
46 "address_remove_contact": "Rimuovi contatto",
47 "address_remove_content": "Sei sicuro di voler rimuovere il contatto selezionato?",
res/values/strings_ja.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "このアドレスはYatの${emoji}からのものです",
41 "address_label": "アドレスラベル",
42 "address_label_explainer": "「アドレス」を押すと、これまでに使用したすべてのアドレスにアクセスして管理できます。",
43 + "address_not_allowed_as_receive": "アドレスは受信アドレスとして許可されません",
44 + "address_not_allowed_as_refund": "住所は払い戻し住所として使用できません",
45 "address_or_alias": "アドレスまたはエイリアス",
46 "address_remove_contact": "連絡先を削除",
47 "address_remove_content": "選択した連絡先を削除してもよろしいですか?",
res/values/strings_ko.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "이 주소는 Yat의 ${emoji}에서 가져온 것입니다",
41 "address_label": "주소 라벨",
42 "address_label_explainer": "\"주소\"를 눌러 이전에 사용한 모든 주소에 액세스하고 관리할 수 있습니다.",
43 + "address_not_allowed_as_receive": "주소는 수신 주소로 허용되지 않습니다.",
44 + "address_not_allowed_as_refund": "주소는 환불 주소로 허용되지 않습니다.",
45 "address_or_alias": "주소 또는 별칭",
46 "address_remove_contact": "연락처 삭제",
47 "address_remove_content": "선택한 연락처를 정말로 삭제하시겠습니까?",
res/values/strings_my.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "ဤလိပ်စာသည် Yat ပေါ်ရှိ ${emoji} မှ ဖြစ်သည်။",
41 "address_label": "လိပ်စာ အညွှန်း",
42 "address_label_explainer": "Addresses ကို နှိပ်ခြင်းဖြင့် သင် ယခင်အသုံးပြုခဲ့သည့် လိပ်စာများအားလုံးကို ဝင်ရောက်ကြည့်ရှု၍ စီမံခန့်ခွဲနိုင်ပါသည်။",
43 + "address_not_allowed_as_receive": "လက်ခံလိပ်စာအဖြစ် ခွင့်မပြုပါ။",
44 + "address_not_allowed_as_refund": "လိပ်စာကို ပြန်အမ်းငွေလိပ်စာအဖြစ် ခွင့်မပြုပါ။",
45 "address_or_alias": "လိပ်စာ သို့မဟုတ် အမည်ပြောင်",
46 "address_remove_contact": "အဆက်အသွယ်ကို ဖယ်ရှားပါ",
47 "address_remove_content": "ရွေးချယ်ထားသော အဆက်အသွယ်ကို ဖယ်ရှားလိုသည်မှာ သေချာပါသလား။",
res/values/strings_nl.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Dit adres is van ${emoji} op Yat",
41 "address_label": "Adreslabel",
42 "address_label_explainer": "Je kunt al je eerder gebruikte adressen openen en beheren door op Adressen te drukken",
43 + "address_not_allowed_as_receive": "adres niet toegestaan ​​als ontvangstadres",
44 + "address_not_allowed_as_refund": "adres niet toegestaan ​​als restitutieadres",
45 "address_or_alias": "Adres of alias",
46 "address_remove_contact": "Contact verwijderen",
47 "address_remove_content": "Weet je zeker dat je het geselecteerde contact wilt verwijderen?",
res/values/strings_pl.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Ten adres pochodzi z ${emoji} na Yat",
41 "address_label": "Etykieta adresu",
42 "address_label_explainer": "Możesz uzyskać dostęp do wszystkich wcześniej używanych adresów i zarządzać nimi, naciskając Adresy",
43 + "address_not_allowed_as_receive": "adres nie jest dozwolony jako adres odbiorczy",
44 + "address_not_allowed_as_refund": "adres nie jest dozwolony jako adres zwrotu",
45 "address_or_alias": "Adres lub alias",
46 "address_remove_contact": "Usuń kontakt",
47 "address_remove_content": "Czy na pewno chcesz usunąć wybrany kontakt?",
res/values/strings_pt.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Este endereço é do ${emoji} no Yat",
41 "address_label": "Rótulo do endereço",
42 "address_label_explainer": "Você pode acessar e gerenciar todos os seus endereços usados anteriormente pressionando Endereços",
43 + "address_not_allowed_as_receive": "endereço não permitido como endereço de recebimento",
44 + "address_not_allowed_as_refund": "endereço não permitido como endereço de reembolso",
45 "address_or_alias": "Endereço ou alias",
46 "address_remove_contact": "Remover contato",
47 "address_remove_content": "Tem certeza de que deseja remover o contato selecionado?",
res/values/strings_ru.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Этот адрес из ${emoji} на Yat",
41 "address_label": "Метка адреса",
42 "address_label_explainer": "Вы можете получить доступ ко всем ранее использованным адресам и управлять ими, нажав «Адреса».",
43 + "address_not_allowed_as_receive": "адрес не разрешен в качестве адреса получения",
44 + "address_not_allowed_as_refund": "адрес не разрешен в качестве адреса возврата",
45 "address_or_alias": "Адрес или псевдоним",
46 "address_remove_contact": "Удалить контакт",
47 "address_remove_content": "Вы уверены, что хотите удалить выбранный контакт?",
res/values/strings_th.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "ที่อยู่นี้มาจาก ${emoji} บน Yat",
41 "address_label": "ป้ายกำกับที่อยู่",
42 "address_label_explainer": "คุณสามารถเข้าถึงและจัดการที่อยู่ที่เคยใช้ทั้งหมดได้โดยกดที่ “Addresses”",
43 + "address_not_allowed_as_receive": "ไม่อนุญาตให้ใช้ที่อยู่เป็นที่อยู่รับ",
44 + "address_not_allowed_as_refund": "ที่อยู่ไม่ได้รับอนุญาตเป็นที่อยู่คืนเงิน",
45 "address_or_alias": "ที่อยู่หรือชื่อแทน",
46 "address_remove_contact": "ลบผู้ติดต่อ",
47 "address_remove_content": "คุณแน่ใจหรือไม่ว่าต้องการลบผู้ติดต่อที่เลือก?",
res/values/strings_tl.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Ang address na ito ay mula sa ${emoji} sa Yat",
41 "address_label": "Label ng address",
42 "address_label_explainer": "Maaari mong i-access at pamahalaan ang lahat ng dati mong ginamit na address sa pamamagitan ng pagpindot sa Mga Address",
43 + "address_not_allowed_as_receive": "hindi pinapayagan ang address bilang address ng pagtanggap",
44 + "address_not_allowed_as_refund": "hindi pinapayagan ang address bilang address ng refund",
45 "address_or_alias": "Address o alyas",
46 "address_remove_contact": "Alisin ang contact",
47 "address_remove_content": "Sigurado ka bang gusto mong alisin ang napiling contact?",
res/values/strings_tr.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Bu adres, Yat'taki ${emoji} üzerinden alınmıştır",
41 "address_label": "Adres etiketi",
42 "address_label_explainer": "Adresler’e basarak daha önce kullandığınız tüm adreslere erişebilir ve bunları yönetebilirsiniz.",
43 + "address_not_allowed_as_receive": "Adresin alma adresi olarak kullanılmasına izin verilmiyor",
44 + "address_not_allowed_as_refund": "adresin geri ödeme adresi olarak kullanılmasına izin verilmiyor",
45 "address_or_alias": "Adres veya takma ad",
46 "address_remove_contact": "Kişiyi kaldır",
47 "address_remove_content": "Seçili kişiyi kaldırmak istediğinizden emin misiniz?",
res/values/strings_uk.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Ця адреса з ${emoji} на Yat",
41 "address_label": "Мітка адреси",
42 "address_label_explainer": "Ви можете отримати доступ до всіх раніше використаних адрес і керувати ними, натиснувши «Адреси».",
43 + "address_not_allowed_as_receive": "адреса не допускається як адреса отримання",
44 + "address_not_allowed_as_refund": "адреса не допускається як адреса повернення коштів",
45 "address_or_alias": "Адреса або псевдонім",
46 "address_remove_contact": "Видалити контакт",
47 "address_remove_content": "Ви впевнені, що хочете видалити вибраний контакт?",
res/values/strings_ur.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "یہ پتہ Yat پر ${emoji} سے ہے۔",
41 "address_label": "ایڈریس لیبل",
42 "address_label_explainer": "آپ \"Addresses\" دباکر اپنے پہلے استعمال شدہ تمام پتوں تک رسائی حاصل کر سکتے ہیں اور ان کا نظم کر سکتے ہیں",
43 + "address_not_allowed_as_receive": "ایڈریس موصول ہونے کی اجازت نہیں ہے۔",
44 + "address_not_allowed_as_refund": "ریفنڈ ایڈریس کے طور پر ایڈریس کی اجازت نہیں ہے۔",
45 "address_or_alias": "ایڈریس یا عرف",
46 "address_remove_contact": "رابطہ حذف کریں",
47 "address_remove_content": "کیا آپ واقعی منتخب رابطہ حذف کرنا چاہتے ہیں؟",
res/values/strings_vi.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Địa chỉ này lấy từ ${emoji} trên Yat",
41 "address_label": "Nhãn địa chỉ",
42 "address_label_explainer": "Bạn có thể truy cập và quản lý tất cả các địa chỉ đã dùng trước đây của mình bằng cách nhấn Địa chỉ",
43 + "address_not_allowed_as_receive": "địa chỉ không được phép làm địa chỉ nhận",
44 + "address_not_allowed_as_refund": "địa chỉ không được phép làm địa chỉ hoàn tiền",
45 "address_or_alias": "Địa chỉ hoặc bí danh",
46 "address_remove_contact": "Xóa liên hệ",
47 "address_remove_content": "Bạn có chắc chắn muốn xóa liên hệ đã chọn không?",
res/values/strings_yo.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "Àdírẹ́sì yìí wá láti ${emoji} lórí Yat",
41 "address_label": "Aami adirẹsi",
42 "address_label_explainer": "O le wọle síi àti ṣàkóso gbogbo àwọn adirẹsi tí o ti lò tẹ́lẹ̀ nípa títẹ Adirẹsi",
43 + "address_not_allowed_as_receive": "adirẹsi ko gba ọ laaye bi gbigba adirẹsi",
44 + "address_not_allowed_as_refund": "adiresi ko gba laaye bi adirẹsi agbapada",
45 "address_or_alias": "Adirẹsi tàbí orúkọ-àpèjọ",
46 "address_remove_contact": "Yọ olùbánisọ̀rọ̀ kúrò",
47 "address_remove_content": "Ṣé ó dá ẹ lójú pé ẹ fẹ́ yọ ẹni ìbánisọ̀rọ̀ tí ẹ yàn kúrò?",
res/values/strings_zh.arb
+2
@@ -40,6 +40,8 @@
40 "address_from_yat": "此地址来自 Yat 上的 ${emoji}",
41 "address_label": "地址标签",
42 "address_label_explainer": "您可以点击“地址”来访问和管理您之前使用过的所有地址",
43 + "address_not_allowed_as_receive": "地址不允许作为接收地址",
44 + "address_not_allowed_as_refund": "地址不允许作为退款地址",
45 "address_or_alias": "地址或别名",
46 "address_remove_contact": "移除联系人",
47 "address_remove_content": "您确定要移除所选联系人吗?",
tool/configure.dart
+53 -100
@@ -103,6 +103,7 @@ import 'dart:typed_data';
103 import 'package:bitcoin_base/bitcoin_base.dart';
104 import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
105 import 'package:cake_wallet/view_model/send/output.dart';
106 +import 'package:cw_core/amount/money.dart';
107 import 'package:cw_core/hardware/hardware_account_data.dart';
108 import 'package:cw_core/hardware/hardware_wallet_service.dart';
109 import 'package:cw_core/node.dart';
@@ -213,12 +214,11 @@ abstract class Bitcoin {
214 List<ElectrumSubAddress> getSilentPaymentAddresses(Object wallet);
215 List<ElectrumSubAddress> getSilentPaymentReceivedAddresses(Object wallet);
216
216 - Future<int> estimateFakeSendAllTxAmount(Object wallet, TransactionPriority priority,
217 + Future<Money> estimateFakeSendAllTxAmount(WalletBase wallet, TransactionPriority priority,
218 {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any});
219 List<ElectrumSubAddress> getSubAddresses(Object wallet);
220
221 String formatterBitcoinAmountToString({required int amount});
221 - double formatterBitcoinAmountToDouble({required int amount});
222 int formatterStringDoubleToBitcoinAmount(String amount);
223 String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate, {int? customRate});
224
@@ -320,6 +320,8 @@ abstract class Bitcoin {
320 Future<void> generateMonero(bool hasImplementation) async {
321 final outputFile = File(moneroOutputPath);
322 const moneroCommonHeaders = """
323 +import 'package:cw_core/amount/money.dart';
324 +import 'package:cw_core/crypto_currency.dart';
325 import 'package:cw_core/unspent_transaction_output.dart';
326 import 'package:cw_core/unspent_coins_info.dart';
327 import 'package:mobx/mobx.dart';
@@ -388,30 +390,17 @@ class Subaddress {
390 }
391
392 class MoneroBalance extends Balance {
391 - MoneroBalance({required this.fullBalance, required this.unlockedBalance})
392 - : formattedFullBalance = monero!.formatterMoneroAmountToString(amount: fullBalance),
393 - formattedUnlockedBalance =
394 - monero!.formatterMoneroAmountToString(amount: unlockedBalance),
395 - super.fromInt(unlockedBalance, fullBalance);
396 -
397 - MoneroBalance.fromString(
398 - {required this.formattedFullBalance,
399 - required this.formattedUnlockedBalance})
400 - : fullBalance = monero!.formatterMoneroParseAmount(amount: formattedFullBalance),
401 - unlockedBalance = monero!.formatterMoneroParseAmount(amount: formattedUnlockedBalance),
402 - super.fromInt(monero!.formatterMoneroParseAmount(amount: formattedUnlockedBalance),
403 - monero!.formatterMoneroParseAmount(amount: formattedFullBalance));
404 -
405 - final int fullBalance;
406 - final int unlockedBalance;
407 - final String formattedFullBalance;
408 - final String formattedUnlockedBalance;
409 -
410 - @override
411 - String get formattedAvailableBalance => formattedUnlockedBalance;
412 -
413 - @override
414 - String get formattedAdditionalBalance => formattedFullBalance;
393 + MoneroBalance({
394 + required this.fullBalance,
395 + required Money unlockedBalance,
396 + Money? frozen,
397 + }) : super(
398 + unlockedBalance,
399 + fullBalance - unlockedBalance,
400 + frozen: frozen ?? Money.zero(CryptoCurrency.xmr),
401 + );
402 +
403 + final Money fullBalance;
404 }
405
406 abstract class MoneroWalletDetails {
@@ -452,7 +441,7 @@ abstract class Monero {
441
442 Map<String, String> exportOutputsUR(Object wallet);
443
455 - bool needExportOutputs(Object wallet, int amount);
444 + bool needExportOutputs(Object wallet, Money amount);
445
446 bool importKeyImagesUR(Object wallet, String ur);
447
@@ -535,6 +524,8 @@ abstract class MoneroAccountList {
524 Future<void> generateWownero(bool hasImplementation) async {
525 final outputFile = File(wowneroOutputPath);
526 const wowneroCommonHeaders = """
527 +import 'package:cw_core/amount/money.dart';
528 +import 'package:cw_core/crypto_currency.dart';
529 import 'package:cw_core/unspent_transaction_output.dart';
530 import 'package:cw_core/unspent_coins_info.dart';
531 import 'package:mobx/mobx.dart';
@@ -600,30 +591,17 @@ class Subaddress {
591 }
592
593 class WowneroBalance extends Balance {
603 - WowneroBalance({required this.fullBalance, required this.unlockedBalance})
604 - : formattedFullBalance = wownero!.formatterWowneroAmountToString(amount: fullBalance),
605 - formattedUnlockedBalance =
606 - wownero!.formatterWowneroAmountToString(amount: unlockedBalance),
607 - super.fromInt(unlockedBalance, fullBalance);
608 -
609 - WowneroBalance.fromString(
610 - {required this.formattedFullBalance,
611 - required this.formattedUnlockedBalance})
612 - : fullBalance = wownero!.formatterWowneroParseAmount(amount: formattedFullBalance),
613 - unlockedBalance = wownero!.formatterWowneroParseAmount(amount: formattedUnlockedBalance),
614 - super.fromInt(wownero!.formatterWowneroParseAmount(amount: formattedUnlockedBalance),
615 - wownero!.formatterWowneroParseAmount(amount: formattedFullBalance));
616 -
617 - final int fullBalance;
618 - final int unlockedBalance;
619 - final String formattedFullBalance;
620 - final String formattedUnlockedBalance;
621 -
622 - @override
623 - String get formattedAvailableBalance => formattedUnlockedBalance;
624 -
625 - @override
626 - String get formattedAdditionalBalance => formattedFullBalance;
594 + WowneroBalance({
595 + required this.fullBalance,
596 + required Money unlockedBalance,
597 + Money? frozen,
598 + }) : super(
599 + unlockedBalance,
600 + fullBalance - unlockedBalance,
601 + frozen: frozen ?? Money.zero(CryptoCurrency.wow),
602 + );
603 +
604 + final Money fullBalance;
605 }
606
607 abstract class WowneroWalletDetails {
@@ -860,7 +838,6 @@ abstract class Nano {
838 Object createNanoTransactionCredentials(List<Output> outputs);
839 Future<void> changeRep(Object wallet, String address);
840 Future<bool> updateTransactions(Object wallet);
863 - BigInt getTransactionAmountRaw(TransactionInfo transactionInfo);
841 String getRepresentative(Object wallet);
842 Future<List<N2Node>> getN2Reps(Object wallet);
843 bool isRepOk(Object wallet);
@@ -929,6 +906,7 @@ Future<void> generateSolana(bool hasImplementation) async {
906 const solanaCommonHeaders = """
907 import 'package:cake_wallet/view_model/send/output.dart';
908 import 'package:cake_wallet/exchange/provider/jupiter_exchange_provider.dart';
909 +import 'package:cw_core/amount/money.dart';
910 import 'package:cw_core/crypto_currency.dart';
911 import 'package:cw_core/output_info.dart';
912 import 'package:cw_core/pending_transaction.dart';
@@ -995,10 +973,9 @@ abstract class Solana {
973 Future<CryptoCurrency?> getSPLToken(WalletBase wallet, String contractAddress);
974
975 CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction);
998 - double getTransactionAmountRaw(TransactionInfo transactionInfo);
976 String getTokenAddress(CryptoCurrency asset);
977 List<int>? getValidationLength(CryptoCurrency type);
1001 - double? getEstimateFees(WalletBase wallet);
978 + Money? getEstimateFees(WalletBase wallet);
979 List<SPLToken> getDefaultSPLTokens();
980 List<String> getDefaultTokenContractAddresses();
981 List<String> getDefaultTokenSymbols();
@@ -1011,8 +988,8 @@ abstract class Solana {
988 String base64Transaction,
989 String requestId,
990 String destinationAddress,
1014 - double amount,
1015 - double fee,
991 + Money amount,
992 + Money fee,
993 );
994
995 // Fast transaction update after sending
@@ -1035,22 +1012,16 @@ abstract class Solana {
1012
1013 Future<void> discoverAndAddWalletTokens(WalletBase wallet);
1014
1038 - TransactionInfo getTransactionInfo(
1039 -{
1015 + TransactionInfo getTransactionInfo({
1016 required String id,
1017 required DateTime blockTime,
1018 required String to,
1019 required String from,
1044 - String? tokenSymbol,
1020 required TransactionDirection direction,
1046 - required double solAmount,
1021 + required Money amount,
1022 required bool isPending,
1048 - required double txFee,
1049 - }
1050 - );
1051 -
1052 - double getPendingTransactionAmount(PendingTransaction tx);
1053 - double getPendingTransactionFee(PendingTransaction tx);
1023 + required Money fee,
1024 + });
1025 }
1026
1027 class JupiterSwapFailedException implements Exception {
@@ -1093,6 +1064,7 @@ Future<void> generateTron(bool hasImplementation) async {
1064 final outputFile = File(tronOutputPath);
1065 const tronCommonHeaders = """
1066 import 'package:cake_wallet/view_model/send/output.dart';
1067 +import 'package:cw_core/amount/money.dart';
1068 import 'package:cw_core/crypto_currency.dart';
1069 import 'package:cw_core/pending_transaction.dart';
1070 import 'package:cw_core/output_info.dart';
@@ -1102,7 +1074,6 @@ import 'package:cw_core/wallet_credentials.dart';
1074 import 'package:cw_core/wallet_info.dart';
1075 import 'package:cw_core/wallet_service.dart';
1076 import 'package:cw_core/tron_token.dart';
1105 -import 'package:hive/hive.dart';
1077 import 'package:cw_core/transaction_direction.dart';
1078
1079 """;
@@ -1111,7 +1082,6 @@ import 'package:cw_evm/evm_chain_mnemonics.dart';
1082 import 'package:cw_tron/tron_transaction_credentials.dart';
1083 import 'package:cw_tron/tron_transaction_info.dart';
1084 import 'package:cw_tron/tron_wallet_creation_credentials.dart';
1114 -import 'package:cw_tron/pending_tron_transaction.dart';
1085
1086 import 'package:cw_tron/tron_client.dart';
1087 import 'package:cw_tron/tron_wallet.dart';
@@ -1139,13 +1109,12 @@ abstract class Tron {
1109 Future<void> deleteTronToken(WalletBase wallet, CryptoCurrency token);
1110 Future<CryptoCurrency?> getTronToken(WalletBase wallet, String contractAddress);
1111
1142 - double getTransactionAmountRaw(TransactionInfo transactionInfo);
1112 CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction);
1113 String getTokenAddress(CryptoCurrency asset);
1114 String getTronBase58Address(String hexAddress, WalletBase wallet);
1115
1147 - String? getTronNativeEstimatedFee(WalletBase wallet);
1148 - String? getTronTRC20EstimatedFee(WalletBase wallet);
1116 + Money? getTronNativeEstimatedFee(WalletBase wallet);
1117 + Money? getTronTRC20EstimatedFee(WalletBase wallet);
1118
1119 void updateTronGridUsageState(WalletBase wallet, bool isEnabled);
1120 List<TronToken> getDefaultTronTokens();
@@ -1154,17 +1123,14 @@ abstract class Tron {
1123 bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress);
1124 TransactionInfo getTransactionInfo({
1125 required String id,
1157 - required BigInt tronAmount,
1158 - int? txFee,
1159 - String? tokenSymbol,
1126 + required Money amount,
1127 + Money? fee,
1128 required TransactionDirection direction,
1129 required DateTime blockTime,
1130 String? to,
1131 String? from,
1132 required bool isPending,
1133 });
1166 - String getPendingTransactionFee(PendingTransaction tx);
1167 - String getPendingTransactionAmount(PendingTransaction tx);
1134 }
1135 """;
1136
@@ -1278,7 +1244,6 @@ import 'package:cw_decred/transaction_priority.dart';
1244 import 'package:cw_decred/wallet.dart';
1245 import 'package:cw_decred/wallet_service.dart';
1246 import 'package:cw_decred/wallet_creation_credentials.dart';
1281 -import 'package:cw_decred/amount_format.dart';
1247 import 'package:cw_decred/transaction_credentials.dart';
1248 import 'package:cw_decred/mnemonic.dart';
1249 """;
@@ -1305,10 +1270,6 @@ abstract class Decred {
1270 Future<void> updateAddress(Object wallet, String address, String label);
1271 Future<void> generateNewAddress(Object wallet, String label);
1272
1308 - String formatterDecredAmountToString({required int amount});
1309 - double formatterDecredAmountToDouble({required int amount});
1310 - int formatterStringDoubleToDecredAmount(String amount);
1311 -
1273 List<Unspent> getUnspents(Object wallet);
1274 void updateUnspents(Object wallet);
1275
@@ -1395,6 +1356,7 @@ Future<void> generateEVM(bool hasImplementation) async {
1356 import 'dart:math' as math;
1357 import 'package:cake_wallet/core/utilities.dart';
1358 import 'package:cake_wallet/view_model/send/output.dart';
1359 +import 'package:cw_core/amount/money.dart';
1360 import 'package:cw_core/pending_transaction.dart';
1361 import 'package:cw_core/crypto_currency.dart';
1362 import 'package:cw_core/erc20_token.dart';
@@ -1519,11 +1481,6 @@ abstract class EVM {
1481 });
1482
1483 int formatterEVMParseAmount(String amount);
1522 - double formatterEVMAmountToDouble({
1523 - TransactionInfo? transaction,
1524 - BigInt? amount,
1525 - int exponent = 18,
1526 - });
1484
1485 List<Erc20Token> getERC20Currencies(WalletBase wallet);
1486 Future<void> addErc20Token(WalletBase wallet, CryptoCurrency token);
@@ -1535,7 +1492,6 @@ abstract class EVM {
1492 void updateScanProviderUsageState(WalletBase wallet, bool isEnabled);
1493 Web3Client? getWeb3Client(WalletBase wallet);
1494 String getTokenAddress(CryptoCurrency asset);
1538 - BigInt? getERC20AvailableBalance(Object balance);
1495
1496 Future<bool> isApprovalRequired(
1497 WalletBase wallet,
@@ -1551,9 +1507,8 @@ abstract class EVM {
1507
1508 Future<PendingTransaction> createTokenApproval(
1509 WalletBase wallet,
1554 - BigInt amount,
1510 + Money amount,
1511 String spender,
1556 - CryptoCurrency token,
1512 TransactionPriority? priority,
1513 {bool useBlinkProtection = true}
1514 );
@@ -1562,7 +1517,7 @@ abstract class EVM {
1517 WalletBase wallet,
1518 String to,
1519 String dataHex,
1565 - BigInt valueWei,
1520 + Money valueWei,
1521 TransactionPriority? priority,
1522 {bool useBlinkProtection = true,
1523 String? sourceTokenAddress,
@@ -1584,10 +1539,12 @@ abstract class EVM {
1539 String? getEVMERC20EstimatedFee(WalletBase wallet);
1540
1541 // Chain-specific integrations (optional, can be null for non-Ethereum chains)
1587 - Future<BigInt>? getDEuroSavingsBalance(WalletBase wallet) => null;
1588 - Future<BigInt>? getDEuroAccruedInterest(WalletBase wallet) => null;
1542 + Future<Money>? getDEuroSavingsBalance(WalletBase wallet) => null;
1543 + Future<Money>? getDEuroSavingsV1Balance(WalletBase wallet) => null;
1544 + Future<Money>? getDEuroAccruedInterest(WalletBase wallet) => null;
1545 Future<BigInt>? getDEuroInterestRate(WalletBase wallet) => null;
1546 Future<BigInt>? getDEuroSavingsApproved(WalletBase wallet) => null;
1547 + Future<PendingTransaction>? withdrawDEuroSavingV1(WalletBase wallet, TransactionPriority priority) => null;
1548 Future<PendingTransaction>? addDEuroSaving(WalletBase wallet, BigInt amount, TransactionPriority priority) => null;
1549 Future<PendingTransaction>? removeDEuroSaving(WalletBase wallet, BigInt amount, TransactionPriority priority) => null;
1550 Future<PendingTransaction>? reinvestDEuroInterest(WalletBase wallet, TransactionPriority priority) => null;
@@ -1646,12 +1603,11 @@ abstract class EVM {
1603 TransactionPriority priority,
1604 );
1605
1649 - TransactionInfo getTransactionInfo(
1650 - {
1606 + TransactionInfo getTransactionInfo({
1607 required String id,
1608 required int height,
1653 - required BigInt ethAmount,
1654 - required BigInt ethFee,
1609 + required Money amount,
1610 + required Money fee,
1611 required String tokenSymbol,
1612 int exponent = 18,
1613 required TransactionDirection direction,
@@ -1663,10 +1619,7 @@ abstract class EVM {
1619 String? evmSignatureName,
1620 String? contractAddress,
1621 required int chainId,
1666 - }
1667 - );
1668 - BigInt getPendingTransactionFee(PendingTransaction tx);
1669 - String getPendingTransactionAmount(PendingTransaction tx);
1622 + });
1623
1624 Future<void> discoverAndAddWalletTokens(WalletBase wallet);
1625 }