From 75f3fe735b1ad42f075e1845450060148d2514e4 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 30 Jun 2026 01:45:44 +0200 Subject: [PATCH] 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 * 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 Co-authored-by: Serhii <17529954+serhii-bor@users.noreply.github.com> Co-authored-by: Robert Malikowski --- .github/workflows/pr_test_build_linux.yml | 3 + cw_bitcoin/lib/bitcoin_wallet.dart | 39 +- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 23 +- cw_bitcoin/lib/electrum_balance.dart | 61 +- cw_bitcoin/lib/electrum_transaction_info.dart | 46 +- cw_bitcoin/lib/electrum_wallet.dart | 177 +++--- cw_bitcoin/lib/electrum_wallet_addresses.dart | 23 +- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 14 +- .../lib/lightning/lightning_wallet.dart | 80 ++- .../pending_lightning_transaction.dart | 19 +- cw_bitcoin/lib/litecoin_wallet.dart | 59 +- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 20 +- .../lib/pending_bitcoin_transaction.dart | 79 +-- .../src/pending_bitcoin_cash_transaction.dart | 95 --- cw_core/lib/amount/amount_sanitizer.dart | 16 + cw_core/lib/amount/money.dart | 211 +++++++ cw_core/lib/amount_converter.dart | 74 --- cw_core/lib/balance.dart | 34 +- cw_core/lib/crypto_currency.dart | 38 +- cw_core/lib/currency.dart | 18 +- cw_core/lib/enumerable_item.dart | 2 - cw_core/lib/enumerate.dart | 2 +- cw_core/lib/exceptions.dart | 8 +- cw_core/lib/lnurl.dart | 10 +- cw_core/lib/monero_balance.dart | 17 - cw_core/lib/output_info.dart | 37 +- cw_core/lib/parse_fixed.dart | 2 +- cw_core/lib/pending_transaction.dart | 17 +- cw_core/lib/transaction_info.dart | 14 +- cw_core/lib/wallet_base.dart | 2 - cw_core/lib/wownero_balance.dart | 17 - .../test/amount/amount_sanitizer_test.dart | 31 + cw_core/test/amount/money_test.dart | 351 +++++++++++ cw_core/test/lnurl_test.dart | 25 +- cw_decred/lib/amount_format.dart | 26 - cw_decred/lib/balance.dart | 28 +- cw_decred/lib/pending_transaction.dart | 39 +- cw_decred/lib/transaction_info.dart | 25 +- cw_decred/lib/wallet.dart | 71 ++- cw_decred/lib/wallet_service.dart | 1 - cw_dogecoin/lib/src/dogecoin_wallet.dart | 2 +- cw_evm/lib/clients/evm_chain_client.dart | 51 +- cw_evm/lib/deuro/deuro_savings.dart | 148 +++-- cw_evm/lib/deuro/deuro_savings_contract.dart | 338 +++++++---- .../deuro/deuro_savings_gateway_contract.dart | 543 ++++++++++++++++++ cw_evm/lib/evm_chain_transaction_info.dart | 55 +- cw_evm/lib/evm_chain_wallet.dart | 168 +++--- cw_evm/lib/evm_erc20_balance.dart | 30 +- cw_evm/lib/pending_evm_chain_transaction.dart | 24 +- cw_evm/lib/usdt0/usdt0_service.dart | 7 +- cw_evm/lib/utils/evm_chain_formatter.dart | 34 -- cw_monero/lib/api/wallet.dart | 27 +- cw_monero/lib/monero_balance.dart | 17 + cw_monero/lib/monero_transaction_info.dart | 20 +- cw_monero/lib/monero_wallet.dart | 75 +-- cw_monero/lib/pending_monero_transaction.dart | 24 +- cw_nano/lib/banano_balance.dart | 35 -- cw_nano/lib/nano_balance.dart | 46 +- cw_nano/lib/nano_client.dart | 12 +- cw_nano/lib/nano_transaction_info.dart | 25 +- cw_nano/lib/nano_wallet.dart | 28 +- cw_nano/lib/pending_nano_transaction.dart | 25 +- cw_solana/lib/default_spl_tokens.dart | 2 + cw_solana/lib/pending_solana_transaction.dart | 15 +- cw_solana/lib/solana_balance.dart | 41 +- cw_solana/lib/solana_client.dart | 287 ++++----- cw_solana/lib/solana_transaction_info.dart | 65 +-- cw_solana/lib/solana_transaction_model.dart | 24 +- cw_solana/lib/solana_wallet.dart | 71 +-- cw_tron/lib/default_tron_tokens.dart | 2 + cw_tron/lib/pending_tron_transaction.dart | 19 +- cw_tron/lib/tron_balance.dart | 16 +- cw_tron/lib/tron_client.dart | 96 ++-- cw_tron/lib/tron_transaction_info.dart | 73 ++- cw_tron/lib/tron_transaction_model.dart | 7 + cw_tron/lib/tron_wallet.dart | 88 +-- .../lib/pending_wownero_transaction.dart | 19 +- cw_wownero/lib/wownero_balance.dart | 17 + cw_wownero/lib/wownero_transaction_info.dart | 28 +- cw_wownero/lib/wownero_wallet.dart | 63 +- cw_zano/lib/api/model/transfer.dart | 7 +- .../lib/model/pending_zano_transaction.dart | 28 +- cw_zano/lib/model/zano_balance.dart | 20 +- cw_zano/lib/model/zano_transaction_info.dart | 35 +- cw_zano/lib/zano_wallet.dart | 71 +-- .../lib/src/pending_zcash_transaction.dart | 38 +- cw_zcash/lib/src/zcash_balance.dart | 33 +- cw_zcash/lib/src/zcash_transaction_info.dart | 16 +- cw_zcash/lib/src/zcash_wallet.dart | 49 +- lib/bitcoin/cw_bitcoin.dart | 11 +- .../src/cards/cake_pay_buy_card_page.dart | 18 +- lib/core/amount_parsing_proxy.dart | 78 ++- lib/core/background_sync.dart | 2 +- lib/core/csv_export_service.dart | 23 +- lib/decred/cw_decred.dart | 29 +- lib/entities/calculate_fiat_amount.dart | 4 +- lib/entities/fiat_currency.dart | 16 +- lib/evm/cw_evm.dart | 142 ++--- .../provider/xoswap_exchange_provider.dart | 8 +- lib/monero/cw_monero.dart | 18 +- lib/nano/cw_nano.dart | 8 +- lib/new-ui/pages/send_page.dart | 97 ++-- lib/new-ui/pages/swap_page.dart | 321 ++++++----- .../action_row/coin_action_row.dart | 5 +- .../assets_history/history_section.dart | 12 +- .../transaction_details_modal.dart | 10 +- .../coins_page/cards/balance_card.dart | 4 +- .../widgets/coins_page/cards/cards_view.dart | 14 +- .../unconfirmed_balance_widget.dart | 13 +- .../receive_page/receive_amount_modal.dart | 14 +- .../widgets/send_page/fiat_amount_bar.dart | 28 +- .../send_page/l2_action_wallet_selector.dart | 1 - .../widgets/send_page/send_amount_input.dart | 22 +- .../widgets/send_page/send_confirm_sheet.dart | 384 +++++++------ .../widgets/swap_page/swap_confirm_sheet.dart | 36 +- .../widgets/swap_page/swap_limit_popup.dart | 4 +- .../widgets/swap_page/swap_modal_header.dart | 9 +- lib/solana/cw_solana.dart | 68 +-- lib/src/screens/exchange/exchange_page.dart | 4 +- .../exchange_trade/exchange_trade_page.dart | 8 +- .../integrations/deuro/savings_page.dart | 25 +- .../deuro/widgets/savings_card_widget.dart | 141 +++-- .../deuro/widgets/savings_edit_sheet.dart | 19 +- .../receive/widgets/currency_input_field.dart | 4 +- lib/src/screens/send/send_page.dart | 5 +- ...ake_pay_transaction_sent_bottom_sheet.dart | 81 +-- .../confirm_sending_bottom_sheet_widget.dart | 5 +- .../swap_confirmation_bottom_sheet.dart | 3 +- lib/tron/cw_tron.dart | 35 +- lib/utils/decimal_input_formatter.dart | 19 + lib/utils/exception_handler.dart | 1 + lib/utils/payment_request.dart | 16 +- lib/view_model/bridge/bridge_view_model.dart | 44 +- .../dashboard/balance_view_model.dart | 204 ++++--- .../dashboard/transaction_list_item.dart | 63 +- .../exchange/exchange_view_model.dart | 223 ++++--- .../integrations/deuro_view_model.dart | 102 ++-- .../payjoin_details_view_model.dart | 4 +- .../payment/payment_view_model.dart | 10 +- lib/view_model/send/output.dart | 133 ++--- lib/view_model/send/send_view_model.dart | 89 +-- lib/view_model/send/template_view_model.dart | 15 +- lib/view_model/support_view_model.dart | 10 +- .../transaction_details_view_model.dart | 27 +- lib/wownero/cw_wownero.dart | 16 +- lib/zano/cw_zano.dart | 36 +- lib/zcash/cw_zcash.dart | 28 +- res/values/strings_ar.arb | 2 + res/values/strings_bg.arb | 2 + res/values/strings_cs.arb | 2 + res/values/strings_de.arb | 2 + res/values/strings_en.arb | 2 + res/values/strings_es.arb | 2 + res/values/strings_fa.arb | 2 + res/values/strings_fr.arb | 2 + res/values/strings_gn.arb | 2 + res/values/strings_ha.arb | 2 + res/values/strings_hi.arb | 4 +- res/values/strings_hr.arb | 2 + res/values/strings_hy.arb | 2 + res/values/strings_id.arb | 2 + res/values/strings_it.arb | 2 + res/values/strings_ja.arb | 2 + res/values/strings_ko.arb | 2 + res/values/strings_my.arb | 2 + res/values/strings_nl.arb | 2 + res/values/strings_pl.arb | 2 + res/values/strings_pt.arb | 2 + res/values/strings_ru.arb | 2 + res/values/strings_th.arb | 2 + res/values/strings_tl.arb | 2 + res/values/strings_tr.arb | 2 + res/values/strings_uk.arb | 2 + res/values/strings_ur.arb | 2 + res/values/strings_vi.arb | 2 + res/values/strings_yo.arb | 2 + res/values/strings_zh.arb | 2 + tool/configure.dart | 149 ++--- 178 files changed, 4238 insertions(+), 3560 deletions(-) delete mode 100644 cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart create mode 100644 cw_core/lib/amount/amount_sanitizer.dart create mode 100644 cw_core/lib/amount/money.dart delete mode 100644 cw_core/lib/monero_balance.dart delete mode 100644 cw_core/lib/wownero_balance.dart create mode 100644 cw_core/test/amount/amount_sanitizer_test.dart create mode 100644 cw_core/test/amount/money_test.dart delete mode 100644 cw_decred/lib/amount_format.dart create mode 100644 cw_evm/lib/deuro/deuro_savings_gateway_contract.dart create mode 100644 cw_monero/lib/monero_balance.dart delete mode 100644 cw_nano/lib/banano_balance.dart create mode 100644 cw_wownero/lib/wownero_balance.dart create mode 100644 lib/utils/decimal_input_formatter.dart diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index b1f74570f..8cb442447 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -227,6 +227,9 @@ jobs: xmessage -timeout 30 "restore_wallet_through_seeds_flow_test" & rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet 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 + - name: Test [cw_core] + timeout-minutes: 15 + run: cd cw_core && flutter test - name: Test [cw_monero] timeout-minutes: 15 run: cd cw_monero && flutter test diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 144c2eafd..a2b2f798f 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -24,10 +24,10 @@ import 'package:cw_bitcoin/psbt/signer.dart'; import 'package:cw_bitcoin/psbt/transaction_builder.dart'; import 'package:cw_bitcoin/psbt/v0_deserialize.dart'; import 'package:cw_bitcoin/psbt/v0_finalizer.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/output_info.dart'; -import 'package:cw_core/parse_fixed.dart'; import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coin_type.dart'; @@ -347,13 +347,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final lBalance = await lightningWallet!.getBalance(); - this.balance[CryptoCurrency.btcln] = - ElectrumBalance(confirmed: lBalance.toInt(), unconfirmed: 0, frozen: 0); + this.balance[CryptoCurrency.btcln] = ElectrumBalance( + confirmed: lBalance, + unconfirmed: Money.zero(CryptoCurrency.btcln), + frozen: Money.zero(CryptoCurrency.btcln)); return ElectrumBalance( confirmed: balance.confirmed, unconfirmed: balance.unconfirmed, - frozen: balance.frozen.toInt(), + frozen: balance.frozen ?? Money.zero(currency), ); } @@ -493,19 +495,19 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final isLNCompatible = await lightningWallet?.isCompatible(lnAddr); if ((credentials.coinTypeToSpendFrom == UnspentCoinType.lightning && lightningWallet != null) || isLNCompatible == true) { - BigInt amount; + Money amount; if (credentials.outputs.first.sendAll) { amount = await lightningWallet!.getBalance(); } else { - amount = parseFixed( - credentials.outputs.first.cryptoAmount?.isNotEmpty == true - ? credentials.outputs.first.cryptoAmount! - : "0", - 8); + amount = credentials.outputs.first.cryptoAmount; } - return lightningWallet!.createTransaction(lnAddr, amount > BigInt.zero ? amount : null, - credentials.priority, credentials.outputs.first.sendAll); + + return lightningWallet!.createTransaction( + lnAddr, + amount.amount > BigInt.zero ? amount.amount : null, + credentials.priority, + credentials.outputs.first.sendAll,); } final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction; @@ -524,7 +526,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { )) .toList(), cwOutputs: credentials.outputs, - fee: BigInt.from(tx.fee), + fee: tx.fee.amount, network: network, memo: credentials.outputs.first.memo, outputOrdering: BitcoinOrdering.none, @@ -543,8 +545,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { tx.commitOverride = () async { final sender = await payjoinManager.initSender(payjoinUri!, originalPsbt, int.parse(tx.feeRate)); - payjoinManager.spawnNewSender( - sender: sender, pjUrl: payjoinUri, amount: BigInt.from(tx.amount)); + payjoinManager.spawnNewSender(sender: sender, pjUrl: payjoinUri, amount: tx.amount.amount); }; return tx; @@ -564,8 +565,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { btcTx, type, electrumClient: electrumClient, - amount: 0, - fee: 0, + amount: Money.zero(currency), + fee: Money.zero(currency), feeRate: "", network: network, hasChange: true, @@ -622,8 +623,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { btcTx, type, electrumClient: electrumClient, - amount: 0, - fee: 0, + amount: Money.zero(currency), + fee: Money.zero(currency), feeRate: "", network: network, hasChange: true, diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index be03f26d0..6abce9689 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -153,11 +153,24 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @override String get addressForExchange { - if (addressPageType == LightningAddressType.p2l) { - final addresses = receiveAddresses - .where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed); - return addresses.first.address; + + final current = getFreshAddress(); + final availableReceiveAddresses = receiveAddresses.where((element) => + !element.isUsed && + !element.isHidden && + !hiddenAddresses.contains(element.address)); + + final bool isSilentPaymentsPage = addressPageType == SilentPaymentsAddresType.p2sp; + final bool isLightningPage = addressPageType == LightningAddressType.p2l; + + if (isSilentPaymentsPage || isLightningPage) { + final segwit = availableReceiveAddresses + .where((e) => e.type == SegwitAddresType.p2wpkh) + .map((e) => e.address) + .toList(); + + return segwit.isNotEmpty ? segwit.first : current; } - return address; + return current; } } diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index a1529f92f..02569887c 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -1,49 +1,52 @@ import 'dart:convert'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/balance.dart'; +import 'package:cw_core/currency.dart'; class ElectrumBalance extends Balance { ElectrumBalance({ required this.confirmed, required this.unconfirmed, - required int frozen, - this.secondConfirmed = 0, - this.secondUnconfirmed = 0, - }) : this._frozen = frozen, super.fromInt(confirmed, unconfirmed, - secondAvailable: secondConfirmed, secondAdditional: secondUnconfirmed, frozen: frozen); + required this.frozen, + this.secondConfirmed, + this.secondUnconfirmed, + }) : super(confirmed, unconfirmed, + secondAvailable: secondConfirmed, secondUnavailable: secondUnconfirmed); - static ElectrumBalance? fromJSON(String? jsonSource) { + Money confirmed; + Money unconfirmed; + Money? secondConfirmed; + Money? secondUnconfirmed; + + @override + Money get available => (confirmed + unconfirmed) - frozen; + + @override + Money get unavailable => unconfirmed; + + @override + Money frozen; + + static ElectrumBalance? fromJSON(String? jsonSource, Currency currency) { if (jsonSource == null) return null; final decoded = json.decode(jsonSource) as Map; return ElectrumBalance( - confirmed: decoded['confirmed'] as int? ?? 0, - unconfirmed: decoded['unconfirmed'] as int? ?? 0, - frozen: decoded['frozen'] as int? ?? 0, - secondConfirmed: decoded['secondConfirmed'] as int? ?? 0, - secondUnconfirmed: decoded['secondUnconfirmed'] as int? ?? 0, + confirmed: Money.fromInt(decoded['confirmed'] as int? ?? 0, currency), + unconfirmed: Money.fromInt(decoded['unconfirmed'] as int? ?? 0, currency), + frozen: Money.fromInt(decoded['frozen'] as int? ?? 0, currency), + secondConfirmed: Money.fromInt(decoded['secondConfirmed'] as int? ?? 0, currency), + secondUnconfirmed: Money.fromInt(decoded['secondUnconfirmed'] as int? ?? 0, currency), ); } - int confirmed; - int unconfirmed; - final int _frozen; - - @override - BigInt get frozen => BigInt.from(_frozen); - - int secondConfirmed = 0; - int secondUnconfirmed = 0; - - @override - BigInt get fullAvailableBalance => BigInt.from((confirmed + unconfirmed) + secondConfirmed - _frozen); - String toJSON() => json.encode({ - 'confirmed': confirmed, - 'unconfirmed': unconfirmed, - 'frozen': _frozen, - 'secondConfirmed': secondConfirmed, - 'secondUnconfirmed': secondUnconfirmed, + 'confirmed': confirmed.amount.toInt(), + 'unconfirmed': unavailable.amount.toInt(), + 'frozen': frozen.amount.toInt(), + 'secondConfirmed': secondAvailable?.amount.toInt() ?? 0, + 'secondUnconfirmed': secondUnavailable?.amount.toInt() ?? 0, }); } diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index cee1f9b9f..9e18d0e60 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -5,10 +5,11 @@ import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/format_amount.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hex/hex.dart'; @@ -31,8 +32,8 @@ class ElectrumTransactionInfo extends TransactionInfo { this.type, { required String id, int? height, - required int amount, - int? fee, + required Money amount, + Money? fee, List? inputAddresses, List? outputAddresses, required TransactionDirection direction, @@ -106,9 +107,9 @@ class ElectrumTransactionInfo extends TransactionInfo { height: height, isPending: false, isReplaced: false, - fee: fee, + fee: Money.fromInt(fee, walletTypeToCryptoCurrency(type)), direction: direction, - amount: amount, + amount: Money.fromInt(amount, walletTypeToCryptoCurrency(type)), date: date, confirmations: confirmations); } @@ -193,9 +194,9 @@ class ElectrumTransactionInfo extends TransactionInfo { isReplaced: false, inputAddresses: inputAddresses, outputAddresses: outputAddresses, - fee: fee, + fee: Money.fromInt(fee, walletTypeToCryptoCurrency(type)), direction: direction, - amount: amount, + amount: Money.fromInt(amount, walletTypeToCryptoCurrency(type)), date: date, isHogEx: isHogEx, confirmations: bundle.confirmations); @@ -205,13 +206,17 @@ class ElectrumTransactionInfo extends TransactionInfo { final inputAddresses = data['inputAddresses'] as List? ?? []; final outputAddresses = data['outputAddresses'] as List? ?? []; final unspents = data['unspents'] as List? ?? []; + final additionalInfo = data['additionalInfo'] as Map?; + final isLightning = (additionalInfo?['isLightning'] as bool?) == true; + + final currency = isLightning ? CryptoCurrency.btcln : walletTypeToCryptoCurrency(type); return ElectrumTransactionInfo( type, id: data['id'] as String, height: data['height'] as int?, - amount: data['amount'] as int, - fee: data['fee'] as int, + amount: Money.fromInt(data['amount'] as int, currency), + fee: data['fee'] != null ? Money.fromInt(data['fee'] as int, currency) : null, direction: parseTransactionDirectionFromInt(data['direction'] as int), date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), isPending: data['isPending'] as bool, @@ -227,29 +232,12 @@ class ElectrumTransactionInfo extends TransactionInfo { BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map)) .toList(), isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false, - additionalInfo: data['additionalInfo'] as Map?, + additionalInfo: additionalInfo, ); } final WalletType type; - String? _fiatAmount; - - @override - String amountFormatted() => - '${walletTypeToCryptoCurrency(type).formatAmount(BigInt.from(amount))} ${walletTypeToCryptoCurrency(type).title}'; - - @override - String? feeFormatted() => fee != null - ? '${walletTypeToCryptoCurrency(type).formatAmount(BigInt.from(fee!))} ${walletTypeToCryptoCurrency(type).title}' - : ''; - - @override - String fiatAmount() => _fiatAmount ?? ''; - - @override - void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); - ElectrumTransactionInfo updated(ElectrumTransactionInfo info) { return ElectrumTransactionInfo(info.type, id: id, @@ -270,13 +258,13 @@ class ElectrumTransactionInfo extends TransactionInfo { final m = {}; m['id'] = id; m['height'] = height; - m['amount'] = amount; + m['amount'] = amount.amount.toInt(); m['direction'] = direction.index; m['date'] = date.millisecondsSinceEpoch; m['isPending'] = isPending; m['isReplaced'] = isReplaced; m['confirmations'] = confirmations; - m['fee'] = fee; + m['fee'] = fee?.amount.toInt(); m['to'] = to; m['unspents'] = unspents?.map((e) => e.toJson()).toList() ?? []; m['inputAddresses'] = inputAddresses; diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 381b7b629..095d1a45e 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -19,6 +19,7 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; +import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/bitcoin_wallet_keys.dart'; import 'package:cw_bitcoin/electrum.dart' as electrum; import 'package:cw_bitcoin/electrum_balance.dart'; @@ -27,20 +28,26 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/exceptions.dart'; +import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_core/hardware/hardware_wallet_service.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/root_dir.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/socket_health_logger.dart'; import 'package:cw_core/utils/tor/abstract.dart'; import 'package:cw_core/wallet_base.dart'; @@ -52,6 +59,7 @@ import 'package:hex/hex.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:sp_scanner/sp_scanner.dart'; part 'electrum_wallet.g.dart'; @@ -94,9 +102,9 @@ abstract class ElectrumWalletBase ? { currency: initialBalance ?? ElectrumBalance( - confirmed: 0, - unconfirmed: 0, - frozen: 0, + confirmed: Money.zero(currency), + unconfirmed: Money.zero(currency), + frozen: Money.zero(currency), ) } : {}), @@ -311,7 +319,7 @@ abstract class ElectrumWalletBase bool get useLightning => _useLightning && LightningWallet.isAvailable; set useLightning(bool val) => _useLightning = val && LightningWallet.isAvailable; - + @observable bool _useLightning; @@ -597,13 +605,13 @@ abstract class ElectrumWalletBase : newUnspents[0].value; if (existingTxInfo.direction == TransactionDirection.incoming) { - existingTxInfo.amount += newAmount; + existingTxInfo.amount += Money.fromInt(newAmount, currency); } // Updates existing TX transactionHistory.addOne(existingTxInfo); // Update balance record - balance[currency]!.confirmed += newAmount; + balance[currency]!.confirmed += Money.fromInt(newAmount, currency); } } else { // else: First time seeing this TX after scanning @@ -865,9 +873,9 @@ abstract class ElectrumWalletBase } } - int get networkDustAmount => 546; + BigInt get networkDustAmount => BigInt.from(546); - bool _isBelowDust(int amount) => amount <= networkDustAmount && network != BitcoinNetwork.testnet; + bool _isBelowDust(BigInt amount) => amount <= networkDustAmount && network != BitcoinNetwork.testnet; UtxoDetails _createUTXOS({ required bool sendAll, @@ -1031,9 +1039,9 @@ abstract class ElectrumWalletBase } // 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 - int amount = utxoDetails.allInputsAmount - fee; + final amount = BigInt.from(utxoDetails.allInputsAmount - fee); - if (amount <= 0) { + if (amount <= BigInt.zero) { throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee); } @@ -1045,7 +1053,7 @@ abstract class ElectrumWalletBase if (outputs.length == 1) { outputs[0] = BitcoinOutput( address: outputs.last.address, - value: BigInt.from(amount), + value: amount, isSilentPayment: hasSilentPayment, ); } @@ -1054,8 +1062,8 @@ abstract class ElectrumWalletBase utxos: utxoDetails.utxos, inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, publicKeys: utxoDetails.publicKeys, - fee: fee, - amount: amount, + fee: Money.fromInt(fee, currency), + amount: Money(amount, currency), isSendAll: true, hasChange: false, memo: memo, @@ -1065,7 +1073,7 @@ abstract class ElectrumWalletBase } Future estimateTxForAmount( - int credentialsAmount, + Money credentialsAmount, List outputs, List updatedOutputs, int feeRate, { @@ -1076,7 +1084,7 @@ abstract class ElectrumWalletBase UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { // Attempting to send less than the dust limit - if (_isBelowDust(credentialsAmount)) { + if (_isBelowDust(credentialsAmount.amount)) { throw BitcoinTransactionNoDustException(); } @@ -1122,7 +1130,7 @@ abstract class ElectrumWalletBase final utxoDetails = _createUTXOS( sendAll: false, - credentialsAmount: credentialsAmount, + credentialsAmount: credentialsAmount.amount.toInt(), inputsCount: inputsCount, paysToSilentPayment: hasSilentPayment, coinTypeToSpendFrom: coinTypeToSpendFrom, @@ -1133,8 +1141,7 @@ abstract class ElectrumWalletBase utxoDetails.utxos.length == utxoDetails.availableInputs.length - utxoDetails.unconfirmedCoins.length; - // How much is being spent - how much is being sent - int amountLeftForChangeAndFee = utxoDetails.allInputsAmount - credentialsAmount; + final amountLeftForChangeAndFee = utxoDetails.allInputsAmount - credentialsAmount.amount.toInt(); if (amountLeftForChangeAndFee <= 0) { if (!spendingAllCoins) { @@ -1213,9 +1220,9 @@ abstract class ElectrumWalletBase throw BitcoinTransactionNoFeeException(); } - int amount = credentialsAmount; + var amount = credentialsAmount; final lastOutput = updatedOutputs.last; - final amountLeftForChange = amountLeftForChangeAndFee - fee; + final amountLeftForChange = BigInt.from(amountLeftForChangeAndFee - fee); if (_isBelowDust(amountLeftForChange)) { // If has change that is lower than dust, will end up with tx rejected by network rules @@ -1226,7 +1233,7 @@ abstract class ElectrumWalletBase // If the computed change is negative or below dust: // - negative: try a no-change tx (recalculate fee without change) // - non-negative but dust: drop change and add remainder to fee - if (amountLeftForChange < 0) { + if (amountLeftForChange < BigInt.zero) { final tempNoChange = outputs.map((o) => o).toList(); final feeNoChange = await calcFee( utxos: utxoDetails.utxos, @@ -1237,7 +1244,7 @@ abstract class ElectrumWalletBase inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, vinOutpoints: utxoDetails.vinOutpoints, ); - final leftover = utxoDetails.allInputsAmount - credentialsAmount - feeNoChange; + final leftover = utxoDetails.allInputsAmount - credentialsAmount.amount.toInt() - feeNoChange; if (leftover >= 0) { final finalFee = feeNoChange + leftover; // absorb tiny remainder @@ -1245,7 +1252,7 @@ abstract class ElectrumWalletBase utxos: utxoDetails.utxos, inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, publicKeys: utxoDetails.publicKeys, - fee: finalFee, + fee: Money.fromInt(finalFee, currency), amount: amount, hasChange: false, isSendAll: spendingAllCoins, @@ -1274,13 +1281,13 @@ abstract class ElectrumWalletBase // if the amount left for change is less than dust, but not less than 0 // then add it to the fees - fee += amountLeftForChange; + fee += amountLeftForChange.toInt(); return EstimatedTxResult( utxos: utxoDetails.utxos, inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, publicKeys: utxoDetails.publicKeys, - fee: fee, + fee: Money.fromInt(fee, currency), amount: amount, hasChange: false, isSendAll: spendingAllCoins, @@ -1292,13 +1299,13 @@ abstract class ElectrumWalletBase // Here, lastOutput already is change, return the amount left without the fee to the user's address. updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput( address: lastOutput.address, - value: BigInt.from(amountLeftForChange), + value: amountLeftForChange, isSilentPayment: lastOutput.isSilentPayment, isChange: true, ); outputs[outputs.length - 1] = BitcoinOutput( address: lastOutput.address, - value: BigInt.from(amountLeftForChange), + value: amountLeftForChange, isSilentPayment: lastOutput.isSilentPayment, isChange: true, ); @@ -1307,7 +1314,7 @@ abstract class ElectrumWalletBase utxos: utxoDetails.utxos, inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, publicKeys: utxoDetails.publicKeys, - fee: fee, + fee: Money.fromInt(fee, currency), amount: amount, hasChange: true, isSendAll: spendingAllCoins, @@ -1318,7 +1325,7 @@ abstract class ElectrumWalletBase } } - Future _maxSpendableNoChangeAmount({ + Future _maxSpendableNoChangeAmount({ required BitcoinOutput initialOutput, required int feeRate, String? memo, @@ -1350,7 +1357,7 @@ abstract class ElectrumWalletBase ); final maxSpendable = utxoDetailsAll.allInputsAmount - feeNoChange; - return maxSpendable > 0 ? maxSpendable : 0; + return Money.fromInt(maxSpendable > 0 ? maxSpendable : 0, currency); } Future calcFee({ @@ -1397,13 +1404,13 @@ abstract class ElectrumWalletBase final memo = transactionCredentials.outputs.first.memo; final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom; - int credentialsAmount = 0; - bool hasSilentPayment = false; + var credentialsAmount = Money.zero(currency); + var hasSilentPayment = false; for (final out in transactionCredentials.outputs) { - final outputAmount = out.formattedCryptoAmount!; + final outputAmount = out.cryptoAmount; - if (!sendAll && _isBelowDust(outputAmount)) { + if (!sendAll && _isBelowDust(outputAmount.amount)) { throw BitcoinTransactionNoDustException(); } @@ -1427,13 +1434,13 @@ abstract class ElectrumWalletBase // The value will be changed after estimating the Tx size and deducting the fee from the total to be sent outputs.add(BitcoinOutput( address: address, - value: BigInt.from(0), + value: BigInt.zero, isSilentPayment: isSilentPayment, )); } else { outputs.add(BitcoinOutput( address: address, - value: BigInt.from(outputAmount), + value: outputAmount.amount, isSilentPayment: isSilentPayment, )); } @@ -1486,7 +1493,7 @@ abstract class ElectrumWalletBase utxos: estimatedTx.utxos, outputs: updatedOutputs, publicKeys: estimatedTx.publicKeys, - fee: BigInt.from(estimatedTx.fee), + fee: estimatedTx.fee.amount, network: network, memo: estimatedTx.memo, outputOrdering: BitcoinOrdering.none, @@ -1504,7 +1511,8 @@ abstract class ElectrumWalletBase network: network, hasChange: estimatedTx.hasChange, isSendAll: estimatedTx.isSendAll, - hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot, + hasTaprootInputs: false, + // ToDo: (Konsti) Support Taproot, isViewOnly: false, )..addListener((transaction) async { transactionHistory.addOne(transaction); @@ -1518,7 +1526,7 @@ abstract class ElectrumWalletBase txb = ForkedTransactionBuilder( utxos: estimatedTx.utxos, outputs: updatedOutputs, - fee: BigInt.from(estimatedTx.fee), + fee: estimatedTx.fee.amount, network: network, memo: estimatedTx.memo, outputOrdering: BitcoinOrdering.none, @@ -1528,7 +1536,7 @@ abstract class ElectrumWalletBase txb = BitcoinTransactionBuilder( utxos: estimatedTx.utxos, outputs: updatedOutputs, - fee: BigInt.from(estimatedTx.fee), + fee: estimatedTx.fee.amount, network: network, memo: estimatedTx.memo, outputOrdering: BitcoinOrdering.none, @@ -2115,12 +2123,12 @@ abstract class ElectrumWalletBase allInputsAmount += outTransaction.amount.toInt(); } - int totalOutAmount = bundle.originalTransaction.outputs + final totalOutAmount = bundle.originalTransaction.outputs .fold(0, (previousValue, element) => previousValue + element.amount.toInt()); var currentFee = allInputsAmount - totalOutAmount; - int remainingFee = (newFee - currentFee > 0) ? newFee - currentFee : newFee; - return totalBalance - receiverAmount - remainingFee >= networkDustAmount; + final remainingFee = (newFee - currentFee > 0) ? newFee - currentFee : newFee; + return totalBalance - receiverAmount - remainingFee >= networkDustAmount.toInt(); } Future replaceByFee(String hash, int newFee) async { @@ -2195,14 +2203,14 @@ abstract class ElectrumWalletBase int totalOutAmount = outputs.fold(0, (previousValue, output) => previousValue + output.value.toInt()); int currentFee = allInputsAmount - totalOutAmount; - int remainingFee = newFee - currentFee; + var remainingFee = BigInt.from(newFee - currentFee); - if (remainingFee <= 0) { + if (remainingFee <= BigInt.zero) { throw Exception("New fee must be higher than the current fee."); } // Deduct fee from change outputs first, if possible - if (remainingFee > 0) { + if (remainingFee > BigInt.zero) { final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden); for (int i = outputs.length - 1; i >= 0; i--) { final output = outputs[i]; @@ -2210,23 +2218,23 @@ abstract class ElectrumWalletBase .any((element) => element.address == output.address.toAddress(network)); if (isChange) { - int outputAmount = output.value.toInt(); + final outputAmount = output.value; if (outputAmount > networkDustAmount) { - int deduction = (outputAmount - networkDustAmount >= remainingFee) + final deduction = (outputAmount - networkDustAmount >= remainingFee) ? remainingFee : outputAmount - networkDustAmount; outputs[i] = BitcoinOutput( - address: output.address, value: BigInt.from(outputAmount - deduction)); + address: output.address, value: outputAmount - deduction); remainingFee -= deduction; - if (remainingFee <= 0) break; + if (remainingFee <= BigInt.zero) break; } } } } // If still not enough, add UTXOs until the fee is covered - if (remainingFee > 0) { + if (remainingFee > BigInt.zero) { final unusedUtxos = unspentCoins .where((utxo) => utxo.isSending && !utxo.isFrozen && utxo.confirmations! > 0) .toList(); @@ -2254,52 +2262,51 @@ abstract class ElectrumWalletBase )); allInputsAmount += utxo.value; - remainingFee -= utxo.value; + remainingFee -= BigInt.from(utxo.value); - if (remainingFee < 0) { + if (remainingFee < BigInt.zero) { final changeOutput = outputs.firstWhereOrNull((output) => walletAddresses.allAddresses .any((addr) => addr.address == output.address.toAddress(network))); if (changeOutput != null) { - final newValue = changeOutput.value.toInt() + (-remainingFee); + final newValue = changeOutput.value + (-remainingFee); outputs[outputs.indexOf(changeOutput)] = - BitcoinOutput(address: changeOutput.address, value: BigInt.from(newValue)); + BitcoinOutput(address: changeOutput.address, value: newValue); } else { final changeAddress = await walletAddresses.getChangeAddress(); outputs.add(BitcoinOutput( address: RegexUtils.addressTypeFromStr(changeAddress.address, network), - value: BigInt.from(-remainingFee))); + value: -remainingFee)); } - remainingFee = 0; + remainingFee = BigInt.zero; break; } - if (remainingFee <= 0) break; + if (remainingFee <= BigInt.zero) break; } } // Deduct from the receiver's output if remaining fee is still greater than 0 - if (remainingFee > 0) { + if (remainingFee > BigInt.zero) { for (int i = 0; i < outputs.length; i++) { final output = outputs[i]; - int outputAmount = output.value.toInt(); + final outputAmount = output.value; if (outputAmount > networkDustAmount) { - int deduction = (outputAmount - networkDustAmount >= remainingFee) + final deduction = (outputAmount - networkDustAmount >= remainingFee) ? remainingFee : outputAmount - networkDustAmount; - outputs[i] = BitcoinOutput( - address: output.address, value: BigInt.from(outputAmount - deduction)); + outputs[i] = BitcoinOutput(address: output.address, value: outputAmount - deduction); remainingFee -= deduction; - if (remainingFee <= 0) break; + if (remainingFee <= BigInt.zero) break; } } } // Final check if the remaining fee couldn't be deducted - if (remainingFee > 0) { + if (remainingFee > BigInt.zero) { throw Exception("Not enough funds to cover the fee."); } @@ -2344,8 +2351,8 @@ abstract class ElectrumWalletBase transaction, type, electrumClient: electrumClient, - amount: sendingAmount, - fee: newFee, + amount: Money.fromInt(sendingAmount, currency), + fee: Money.fromInt(newFee, currency), network: network, hasChange: changeOutputs.isNotEmpty, feeRate: newFee.toString(), @@ -2586,16 +2593,15 @@ abstract class ElectrumWalletBase final history = await electrumClient.getHistory(addressRecord.getScriptHash(network)); - if (history.isNotEmpty) { addressRecord.setAsUsed(); walletAddresses.clearLockIfMatches(addressRecord.type, addressRecord.address); - if(this is BitcoinWallet) { + if (this is BitcoinWallet) { //removes transactions no longer returned by the api, presumed replaced/invalid. transactionHistory.transactions.removeWhere( - (hash, tx) => - tx.outputAddresses != null && + (hash, tx) => + tx.outputAddresses != null && tx.outputAddresses!.contains(addressRecord.address) && !history.any((newTransaction) => newTransaction['tx_hash'] == hash), ); @@ -3481,7 +3487,12 @@ abstract class ElectrumWalletBase // if we got null balance responses from the server, set our connection status to lost and return our last known balance: printV("got null balance responses from the server, setting connection status to lost"); syncStatus = LostConnectionSyncStatus(); - return balance[currency] ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); + return balance[currency] ?? + ElectrumBalance( + confirmed: Money.zero(currency), + unconfirmed: Money.zero(currency), + frozen: Money.zero(currency), + ); } for (var i = 0; i < balances.length; i++) { @@ -3500,9 +3511,9 @@ abstract class ElectrumWalletBase } return ElectrumBalance( - confirmed: totalConfirmed, - unconfirmed: totalUnconfirmed, - frozen: totalFrozen, + confirmed: Money.fromInt(totalConfirmed, currency), + unconfirmed: Money.fromInt(totalUnconfirmed, currency), + frozen: Money.fromInt(totalFrozen, currency), ); } @@ -3820,12 +3831,6 @@ abstract class ElectrumWalletBase } } - @override - String formatCryptoAmount(String amount) { - final amountBigInt = BigInt.parse(amount); - return currency.formatAmount(amountBigInt); - } - /// Checks the health of the socket connection /// and triggers a full reconnection if needed @override @@ -4286,8 +4291,8 @@ Future _handleScanSilentPayments(ScanData scanData) async { WalletType.bitcoin, id: txid, height: tweakHeight, - amount: 0, - fee: 0, + amount: Money.zero(CryptoCurrency.btc), + fee: Money.zero(CryptoCurrency.btc), direction: TransactionDirection.incoming, isReplaced: false, date: scanData.network == BitcoinNetwork.mainnet @@ -4353,7 +4358,7 @@ Future _handleScanSilentPayments(ScanData scanData) async { txInfo.unspents!.add(unspent); } - txInfo.amount += unspent.value; + txInfo.amount += Money.fromInt(unspent.value, txInfo.amount.currency); }); }); }); @@ -4423,8 +4428,8 @@ class EstimatedTxResult { final List utxos; final List inputPrivKeyInfos; final Map publicKeys; // PubKey to derivationPath - final int fee; - final int amount; + final Money fee; + final Money amount; final bool spendsSilentPayment; // final bool sendsToSilentPayment; diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index e692816b6..239fa491d 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -170,9 +170,22 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @computed List get allAddresses => _addresses; + @observable + bool addressRefreshToggle = false; + + @action + String getFreshAddress() { + addressRefreshToggle = !addressRefreshToggle; + return address; + } + + @override + String get addressForExchange => getFreshAddress(); + @override @computed String get address { + final _ = addressRefreshToggle; if (addressPageType == SilentPaymentsAddresType.p2sp) { if (activeSilentAddress != null) { return activeSilentAddress!; @@ -198,7 +211,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ]; final typeMatchingReceiveAddressesAll = - typeMatchingAddressesAll.where((addr) => !addr.isUsed).toList(); + typeMatchingAddressesAll.where((addr) => !addr.isUsed && !hiddenAddresses.contains(addr.address)).toList(); final typeMatchingReceiveAddresses = [ ...typeMatchingReceiveAddressesAll.where((a) => !a.isLegacyDerivation), ...typeMatchingReceiveAddressesAll.where((a) => a.isLegacyDerivation), @@ -221,10 +234,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } final locked = lockedReceiveAddressByType[addressPageType]; - if (locked != null) return locked; + if (locked != null && !hiddenAddresses.contains(locked)) return locked; final prev = previousAddressRecord; - if (prev != null && prev.type == addressPageType && !prev.isUsed && !prev.isLegacyDerivation) { + if (prev != null && + prev.type == addressPageType && + !prev.isUsed && + !prev.isLegacyDerivation && + !hiddenAddresses.contains(prev.address)) { return prev.address; } diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 810706374..67c822f01 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -2,6 +2,8 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_core/pathForWallet.dart'; @@ -92,9 +94,15 @@ class ElectrumWalletSnapshot { final useLightning = data['useLightning'] as bool? ?? true; final cachedLightningAddress = data['cachedLightningAddress'] as String?; - final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ?? - ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); - final lightningBalance = ElectrumBalance.fromJSON(data['lightningBalance'] as String?); + final currency = walletTypeToCryptoCurrency(type); + final balance = ElectrumBalance.fromJSON(data['balance'] as String?, currency) ?? + ElectrumBalance( + confirmed: Money.zero(currency), + unconfirmed: Money.zero(currency), + frozen: Money.zero(currency), + ); + final lightningBalance = + ElectrumBalance.fromJSON(data['lightningBalance'] as String?, currency); var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart index 31ba4b60a..d78c88d13 100644 --- a/cw_bitcoin/lib/lightning/lightning_wallet.dart +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -6,6 +6,9 @@ import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/lightning/pending_lightning_transaction.dart'; +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; @@ -20,7 +23,7 @@ class LightningWallet { final String apiKey; final String lnurlDomain; final Network network; - late BreezSdk sdk; + BreezSdk? _sdk; String? cachedAddress; @@ -38,6 +41,10 @@ class LightningWallet { static bool get isAvailable => Platform.isIOS || Platform.isAndroid || Platform.isMacOS; + Currency get currency => CryptoCurrency.btcln; + + BreezSdk get sdk => _sdk!; + StreamSubscription? _eventSubscription; Stream? _eventStream; @@ -47,9 +54,25 @@ class LightningWallet { void _subscribeToLogStream(File logFile) { _logSubscription = _logStream?.listen((logEntry) { - logFile.writeAsStringSync("[${logEntry.level}] ${logEntry.line}\n", mode: FileMode.append); + try { + // Check if file exists before writing (optional, but safer) + if (!logFile.existsSync()) { + logFile.createSync(recursive: true); + } + logFile.writeAsStringSync("[${logEntry.level}] ${logEntry.line}\n", mode: FileMode.append); + } catch (e) { + // Silently fail or use printV(e) so it doesn't crash the app + printV("Failed to write to log: $e"); + } }, onError: (e) { - logFile.writeAsStringSync("[ERROR] $e\n", mode: FileMode.append); + try { + if (!logFile.existsSync()) { + logFile.createSync(recursive: true); + } + logFile.writeAsStringSync("[ERROR] $e\n", mode: FileMode.append); + } catch (err) { + printV("Failed to write error to log: $err"); + } }); } @@ -76,7 +99,7 @@ class LightningWallet { storageDir: "$appPath/.breez/", ); - sdk = await connect(request: connectRequest); + _sdk = await connect(request: connectRequest); _eventStream ??= sdk.addEventListener().asBroadcastStream(); _logStream ??= initLogging().asBroadcastStream(); @@ -100,7 +123,7 @@ class LightningWallet { Future close() async { _eventSubscription?.cancel(); - await sdk.disconnect(); + await _sdk?.disconnect(); _logSubscription?.cancel(); } @@ -126,13 +149,14 @@ class LightningWallet { request: ReceivePaymentRequest(paymentMethod: ReceivePaymentMethod.bitcoinAddress()))) .paymentRequest; - Future getBalance() async { + Future getBalance() async { try { - return (await sdk.getInfo(request: GetInfoRequest(ensureSynced: true))).balanceSats; + return Money((await sdk.getInfo(request: GetInfoRequest(ensureSynced: true))).balanceSats, + CryptoCurrency.btcln); } on SdkError_Generic catch (_) { } on SdkError_NetworkError catch (_) {} - return BigInt.zero; + return Money.zero(CryptoCurrency.btcln); } Future registerAddress(String username) async { @@ -188,11 +212,16 @@ class LightningWallet { final lightningFeeSats = paymentMethod.lightningFeeSats; final sparkTransferFeeSats = paymentMethod.sparkTransferFeeSats; + final baseAmount = request.amount ?? amountSats; + final amount = baseAmount != null + ? Money(baseAmount, currency) + : Money(paymentMethod.invoiceDetails.amountMsat ?? BigInt.zero, currency) / + BigInt.from(1000); + return PendingLightningTransaction( id: paymentMethod.invoiceDetails.paymentHash, - amount: request.amount?.toInt() ?? amountSats?.toInt() ?? - ((paymentMethod.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(), - fee: lightningFeeSats.toInt() + (sparkTransferFeeSats?.toInt() ?? 0), + amount: amount, + fee: Money(lightningFeeSats + (sparkTransferFeeSats ?? BigInt.zero), currency), commitOverride: () async { try { final res = await sdk.sendPayment( @@ -230,12 +259,10 @@ class LightningWallet { final prepareResponse = await sdk.prepareLnurlPay(request: request); - final feeSats = prepareResponse.feeSats; - return PendingLightningTransaction( id: prepareResponse.invoiceDetails.paymentHash, - amount: prepareResponse.amountSats.toInt(), - fee: feeSats.toInt(), + amount: Money(prepareResponse.amountSats, currency), + fee: Money(prepareResponse.feeSats, currency), commitOverride: () async { final res = await sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)); @@ -256,27 +283,26 @@ class LightningWallet { final feeQuote = paymentMethod.feeQuote; OnchainConfirmationSpeed onchainConfirmationSpeed; - int fee; + BigInt fee; switch (priority) { case BitcoinTransactionPriority.fast: - fee = (feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat).toInt(); + fee = feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat; onchainConfirmationSpeed = OnchainConfirmationSpeed.fast; break; case BitcoinTransactionPriority.medium: - fee = - (feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat).toInt(); + fee = feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat; onchainConfirmationSpeed = OnchainConfirmationSpeed.medium; break; case BitcoinTransactionPriority.slow: default: - fee = (feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat).toInt(); + fee = feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat; onchainConfirmationSpeed = OnchainConfirmationSpeed.slow; } return PendingLightningTransaction( id: "", // ToDo: Find out where to get it - amount: prepareResponse.amount.toInt(), - fee: fee, + amount: Money(prepareResponse.amount, currency), + fee: Money(fee, currency), commitOverride: () async { final options = SendPaymentOptions.bitcoinAddress(confirmationSpeed: onchainConfirmationSpeed); @@ -404,7 +430,7 @@ class LightningWallet { } ElectrumTransactionInfo _getElectrumTransactionInfoFromPayment(Payment payment) { - TransactionDirection direction = TransactionDirection.outgoing; + var direction = TransactionDirection.outgoing; if (payment.paymentType == PaymentType.receive) { direction = TransactionDirection.incoming; @@ -416,10 +442,10 @@ class LightningWallet { return ElectrumTransactionInfo( WalletType.bitcoin, id: payment.id, - amount: payment.amount.toInt(), + amount: Money(payment.amount, currency), direction: direction, isPending: payment.status == PaymentStatus.pending, - fee: payment.fees.toInt(), + fee: Money(payment.fees, currency), date: DateTime.fromMillisecondsSinceEpoch(payment.timestamp.toInt() * 1000), confirmations: payment.status == PaymentStatus.pending ? 0 : 10, additionalInfo: {"isLightning": true}, @@ -430,10 +456,10 @@ class LightningWallet { return ElectrumTransactionInfo( WalletType.bitcoin, id: deposit.txid, - amount: deposit.amountSats.toInt(), + amount: Money(deposit.amountSats, currency), direction: TransactionDirection.incoming, isPending: true, - fee: 0, + fee: Money.zero(currency), date: DateTime.now(), confirmations: 0, additionalInfo: {"isLightning": true, "isSparkDeposit": true}, diff --git a/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart b/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart index d389949f9..d2bcabb1f 100644 --- a/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart +++ b/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart @@ -1,4 +1,4 @@ -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/pending_transaction.dart'; class PendingLightningTransaction with PendingTransaction { @@ -10,8 +10,7 @@ class PendingLightningTransaction with PendingTransaction { required this.commitOverride, }); - final int amount; - final int fee; + final bool isSendAll; Future Function() commitOverride; final List _listeners =[]; @@ -19,17 +18,17 @@ class PendingLightningTransaction with PendingTransaction { @override String id; + @override + final Money amount; + + @override + final Money fee; + @override String get hex => ""; @override - String get amountFormatted => bitcoinAmountToString(amount: amount); - - @override - String get feeFormatted => "$feeFormattedValue BTC"; - - @override - String get feeFormattedValue => bitcoinAmountToString(amount: fee); + String get amountFormatted => amount.toString(); @override int? get outputCount => 1; diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 3a363aa4a..51cc8a4d8 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -5,6 +5,7 @@ import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; import 'package:cw_core/unspent_coin_type.dart'; @@ -17,12 +18,9 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:blockchain_utils/signer/ecdsa_signing_key.dart'; -import 'package:collection/collection.dart'; import 'package:convert/convert.dart' as convert; -import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; -import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; @@ -35,25 +33,18 @@ import 'package:cw_bitcoin/litecoin_wallet_addresses.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/psbt/transaction_builder.dart'; import 'package:cw_bitcoin/utils.dart'; -import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; -import 'package:cw_core/mweb_utxo.dart'; -import 'package:cw_core/node.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; -import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_mweb/cw_mweb.dart'; -import 'package:cw_mweb/mwebd.pbgrpc.dart'; -import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:grpc/grpc.dart'; import 'package:hive/hive.dart'; @@ -587,8 +578,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { WalletType.litecoin, id: utxo.outputId, height: utxo.height, - amount: utxo.value.toInt(), - fee: 0, + amount: Money.fromInt(utxo.value, currency), + fee: Money.zero(currency), direction: TransactionDirection.incoming, isPending: utxo.height == 0, date: date, @@ -734,9 +725,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Future checkMwebUtxosSpent() async { printV("checkMwebUtxosSpent() called!"); - if (!mwebEnabled) { - return; - } + if (!mwebEnabled) return; final pendingOutgoingTransactions = transactionHistory.transactions.values .where((tx) => tx.direction == TransactionDirection.outgoing && tx.isPending); @@ -763,8 +752,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final height = await electrumClient.getCurrentBlockChainTip(); if (height == null || status.blockHeaderHeight != height) return; if (status.mwebUtxosHeight != height) return; // we aren't synced - int amount = 0; - Set inputAddresses = {}; + var amount = 0; + var inputAddresses = {}; var output = convert.AccumulatorSink(); var input = sha256.startChunkedConversion(output); @@ -777,8 +766,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (!inputAddresses.contains(utxo.address)) { addressRecord.txCount++; } - addressRecord.balance -= utxo.value.toInt(); - amount += utxo.value.toInt(); + addressRecord.balance -= utxo.value; + amount += utxo.value; inputAddresses.add(utxo.address); input.add(hex.decode(outputId)); } @@ -790,8 +779,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { WalletType.litecoin, id: digest.toString(), height: height, - amount: amount, - fee: 0, + amount: Money.fromInt(amount, currency), + fee: Money.zero(currency), direction: TransactionDirection.outgoing, isPending: false, date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000), @@ -921,10 +910,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { // update unspent balances: await updateUnspent(); - int confirmed = balance.confirmed; - int unconfirmed = balance.unconfirmed; - int confirmedMweb = 0; - int unconfirmedMweb = 0; + var confirmedMweb = 0; + var unconfirmedMweb = 0; try { mwebUtxosBox.values.forEach((utxo) { bool isConfirmed = utxo.height > 0; @@ -933,20 +920,20 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { "utxo: ${isConfirmed ? "confirmed" : "unconfirmed"} ${utxo.spent ? "spent" : "unspent"} ${utxo.outputId} ${utxo.height} ${utxo.value}"); if (isConfirmed) { - confirmedMweb += utxo.value.toInt(); + confirmedMweb += utxo.value; } if (isConfirmed && utxo.spent) { - unconfirmedMweb -= utxo.value.toInt(); + unconfirmedMweb -= utxo.value; } if (!isConfirmed && !utxo.spent) { - unconfirmedMweb += utxo.value.toInt(); + unconfirmedMweb += utxo.value; } }); } catch (_) {} - for (var addressRecord in walletAddresses.allAddresses) { + for (final addressRecord in walletAddresses.allAddresses) { addressRecord.balance = 0; addressRecord.txCount = 0; } @@ -990,11 +977,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } return ElectrumBalance( - confirmed: confirmed, - unconfirmed: unconfirmed, - frozen: balance.frozen.toInt(), - secondConfirmed: confirmedMweb, - secondUnconfirmed: unconfirmedMweb, + confirmed: balance.confirmed, + unconfirmed: balance.unconfirmed, + frozen: balance.frozen, + secondConfirmed: Money.fromInt(confirmedMweb, currency), + secondUnconfirmed: Money.fromInt(unconfirmedMweb, currency), ); } @@ -1371,8 +1358,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { btcTx, type, electrumClient: electrumClient, - amount: 0, - fee: resp.fee.toInt(), + amount: Money.zero(currency), + fee: Money.fromInt(resp.fee.toInt(), currency), feeRate: "", network: network, hasChange: resp.recipient.length > 1, diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index eee49591a..a4a4b7590 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -228,13 +228,21 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @override String get addressForExchange { // don't use mweb addresses for exchange refund address: - try { - final addresses = receiveAddresses - .where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed); - return addresses.first.address; - } catch (_) { - return receiveAddresses.first.address; + + final current = getFreshAddress(); + final bool isMweb = receiveAddresses + .any((e) => e.address == current && e.type == SegwitAddresType.mweb); + + if (isMweb) { + final segwit = receiveAddresses + .where((e) => e.type == SegwitAddresType.p2wpkh && !e.isUsed && !e.isHidden && !hiddenAddresses.contains(e.address)) + .map((e) => e.address) + .toList(); + + return segwit.isNotEmpty ? segwit.first : current; } + + return current; } @override diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index a9b741dc7..565234291 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,7 +1,6 @@ -import 'dart:typed_data'; - import 'package:bbqrdart/bbqrdart.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:flutter/foundation.dart'; import 'package:grpc/grpc.dart'; import 'package:cw_bitcoin/exceptions.dart'; @@ -9,7 +8,6 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_bitcoin/electrum.dart'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_type.dart'; @@ -42,8 +40,6 @@ class PendingBitcoinTransaction with PendingTransaction { final WalletType type; final BtcTransaction _tx; final ElectrumClient electrumClient; - final int amount; - final int fee; final String feeRate; final BasedUtxoNetwork? network; final bool isSendAll; @@ -61,6 +57,12 @@ class PendingBitcoinTransaction with PendingTransaction { Uint8List? unsignedPsbt; + @override + final Money amount; + + @override + final Money fee; + @override String get id => idOverride ?? _tx.txId(); @@ -68,30 +70,7 @@ class PendingBitcoinTransaction with PendingTransaction { String get hex => hexOverride ?? _tx.serialize(); @override - String get amountFormatted => bitcoinAmountToString(amount: amount); - - @override - String get feeFormatted => "$feeFormattedValue ${_feeCurrency}"; - - String get _feeCurrency { - switch(type) { - case WalletType.bitcoin: - return "BTC"; - case WalletType.litecoin: - return "LTC"; - case WalletType.bitcoinCash: - return "BCH"; - case WalletType.dogecoin: - return "DOGE"; - default: - return type.name; - } - - } - - - @override - String get feeFormattedValue => bitcoinAmountToString(amount: fee); + String get amountFormatted => amount.toString(); @override int? get outputCount => _tx.outputs.length; @@ -155,14 +134,12 @@ class PendingBitcoinTransaction with PendingTransaction { Future _ltcCommit() async { try { - final resp = await CwMweb.broadcast( - BroadcastRequest(rawTx: BytesUtils.fromHexString(hex))); + final resp = await CwMweb.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex))); idOverride = resp.txid; } on GrpcError catch (e) { throw BitcoinTransactionCommitFailed(errorMessage: e.message); } catch (e) { - throw BitcoinTransactionCommitFailed( - errorMessage: "Unknown error: ${e.toString()}"); + throw BitcoinTransactionCommitFailed(errorMessage: "Unknown error: ${e.toString()}"); } } @@ -181,22 +158,23 @@ class PendingBitcoinTransaction with PendingTransaction { _listeners.forEach((listener) => listener(transactionInfo())); } - void addListener( - void Function(ElectrumTransactionInfo transaction) listener) => + void addListener(void Function(ElectrumTransactionInfo transaction) listener) => _listeners.add(listener); - ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type, - id: id, - height: 0, - amount: amount, - direction: TransactionDirection.outgoing, - date: DateTime.now(), - isPending: true, - isReplaced: false, - confirmations: 0, - inputAddresses: _tx.inputs.map((input) => input.txId).toList(), - outputAddresses: outputAddresses, - fee: fee); + ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo( + type, + id: id, + height: 0, + amount: amount, + direction: TransactionDirection.outgoing, + date: DateTime.now(), + isPending: true, + isReplaced: false, + confirmations: 0, + inputAddresses: _tx.inputs.map((input) => input.txId).toList(), + outputAddresses: outputAddresses, + fee: fee, + ); @override bool shouldCommitUR() => isViewOnly; @@ -207,8 +185,8 @@ class PendingBitcoinTransaction with PendingTransaction { var cborEncoder = CBOREncoder(); cborEncoder.encodeBytes(sourceBytes); var ur = UR("psbt", cborEncoder.getBytes()); - var urLegacy = UR("crypto-psbt", cborEncoder.getBytes()); - // var ur = UR("psbt", Uint8List.fromList(List.generate(64*1024, (int x) => x % 256))); + var urLegacy = UR("crypto-psbt", cborEncoder.getBytes()); + // var ur = UR("psbt", Uint8List.fromList(List.generate(64*1024, (int x) => x % 256))); var encoded = UREncoder(ur, 120); var encodedLegacy = UREncoder(urLegacy, 120); List values = []; @@ -230,8 +208,7 @@ class PendingBitcoinTransaction with PendingTransaction { return Future.value({ "Cupcake bcur": values.join("\n"), "ColdCard bbqr": bbqr.join("\n"), - if (kDebugMode) - "SeedSigner bcur legacy": valuesLegacy.join("\n"), + if (kDebugMode) "SeedSigner bcur legacy": valuesLegacy.join("\n"), }); } } diff --git a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart deleted file mode 100644 index 24dc1970d..000000000 --- a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:cw_bitcoin/exceptions.dart'; -import 'package:bitbox/bitbox.dart' as bitbox; -import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_bitcoin/electrum.dart'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; -import 'package:cw_bitcoin/electrum_transaction_info.dart'; -import 'package:cw_core/transaction_direction.dart'; -import 'package:cw_core/wallet_type.dart'; - -class PendingBitcoinCashTransaction with PendingTransaction { - PendingBitcoinCashTransaction(this._tx, this.type, - {required this.electrumClient, - required this.amount, - required this.fee, - required this.hasChange, - required this.isSendAll}) - : _listeners = []; - - final WalletType type; - final bitbox.Transaction _tx; - final ElectrumClient electrumClient; - final int amount; - final int fee; - final bool hasChange; - final bool isSendAll; - - @override - String get id => _tx.getId(); - - @override - String get hex => _tx.toHex(); - - @override - String get amountFormatted => bitcoinAmountToString(amount: amount); - - @override - String get feeFormatted => "$feeFormattedValue BCH"; - - @override - String get feeFormattedValue => bitcoinAmountToString(amount: fee); - - final List _listeners; - - @override - Future commit() async { - int? callId; - - final result = await electrumClient.broadcastTransaction( - transactionRaw: hex, idCallback: (id) => callId = id); - - if (result.isEmpty) { - if (callId != null) { - final error = electrumClient.getErrorMessage(callId!); - - if (error.contains("dust")) { - if (hasChange) { - throw BitcoinTransactionCommitFailedDustChange(); - } else if (!isSendAll) { - throw BitcoinTransactionCommitFailedDustOutput(); - } else { - throw BitcoinTransactionCommitFailedDustOutputSendAll(); - } - } - - if (error.contains("bad-txns-vout-negative")) { - throw BitcoinTransactionCommitFailedVoutNegative(); - } - throw BitcoinTransactionCommitFailed(errorMessage: error); - } - - throw BitcoinTransactionCommitFailed(); - } - - _listeners.forEach((listener) => listener(transactionInfo())); - } - - void addListener(void Function(ElectrumTransactionInfo transaction) listener) => - _listeners.add(listener); - - ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type, - id: id, - height: 0, - amount: amount, - direction: TransactionDirection.outgoing, - date: DateTime.now(), - isPending: true, - confirmations: 0, - fee: fee, - isReplaced: false, - ); - @override - Future> commitUR() { - throw UnimplementedError(); - } -} diff --git a/cw_core/lib/amount/amount_sanitizer.dart b/cw_core/lib/amount/amount_sanitizer.dart new file mode 100644 index 000000000..6504d1bd9 --- /dev/null +++ b/cw_core/lib/amount/amount_sanitizer.dart @@ -0,0 +1,16 @@ +String _smartAmountSanitizer(String amount) { + if (amount.contains(RegExp(r'^(?=.*[.])(?=.*,).*$'))) { + if (amount.indexOf(".") > amount.indexOf(",")) { + amount = amount.replaceAll(",", ""); + } else { + amount = amount.replaceAll(".", ""); + } + } + + return amount.replaceAll(",", "."); +} + + +extension SanitizerUserAmounts on String { + String sanitized() => _smartAmountSanitizer(this); +} diff --git a/cw_core/lib/amount/money.dart b/cw_core/lib/amount/money.dart new file mode 100644 index 000000000..3ff5af928 --- /dev/null +++ b/cw_core/lib/amount/money.dart @@ -0,0 +1,211 @@ +import 'package:cw_core/crypto_amount_format.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; +import 'package:cw_core/format_fixed.dart'; +import 'package:cw_core/parse_fixed.dart'; + +class Money implements Comparable { + final BigInt amount; + final Currency currency; + + const Money(this.amount, this.currency); + + factory Money.zero(Currency currency) => Money(BigInt.zero, currency); + + factory Money.fromInt(int amount, Currency currency) => Money(BigInt.from(amount), currency); + + /// Parse the [source] and turn it into [Money] + /// + /// Throws a [FormatException] if the [source] is not a valid decimal or + /// not in canonical representation or if it is a decimal when [isBaseUnit] + factory Money.parse(source, Currency currency, {bool isBaseUnit = false}) { + final amount = isBaseUnit + ? BigInt.parse(source.toString()) + : parseFixed(source.toString(), currency.decimals); + + return Money(amount, currency); + } + + /// Parse the [source] and turn it into [Money] if possible + /// + /// As [parse] except that this method returns `null` if the input is not + /// valid or if it is a decimal when [isBaseUnit] + static Money? tryParse(source, Currency currency, {bool isBaseUnit = false}) { + final amount = isBaseUnit + ? BigInt.tryParse(source.toString()) + : tryParseFixed(source.toString(), currency.decimals); + + return amount != null ? Money(amount, currency) : null; + } + + /// Returns the sign of this [BigInt] amount. + /// Returns 0 for zero, -1 for values less than zero and +1 for values + /// greater than zero. + int get sign => amount.sign; + + /// Returns `true` when amount of this money is zero. + bool get isZero => amount == BigInt.zero; + + /// Returns `true` when amount of this money is negative. + bool get isNegative => amount.isNegative; + + /// Compares this to [other]. + /// + /// [other] has to be in same currency, [ArgumentError] will be thrown + /// otherwise. + @override + int compareTo(Money other) { + _assertSameCurrency(other); + + return amount.compareTo(other.amount); + } + + /// Returns `true` if [other] is the same amount of money in + /// the same currency. + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Money && other.currency == currency && other.amount == amount); + + /// Returns `true` when this money is less than [other]. + /// + /// Both operands have to be in same currency, [ArgumentError] will be thrown + /// otherwise. + bool operator <(Money other) { + _assertSameCurrency(other, "Cannot compare money in different currencies."); + + return amount < other.amount; + } + + /// Returns `true` when this money is less than or equal to [other]. + /// + /// Both operands have to be in same currency, [ArgumentError] will be thrown + /// otherwise. + bool operator <=(Money other) { + _assertSameCurrency(other, "Cannot compare money in different currencies."); + + return amount <= other.amount; + } + + /// Returns `true` when this money is greater than [other]. + /// + /// Both operands have to be in same currency, [ArgumentError] will be thrown + /// otherwise. + bool operator >(Money other) { + _assertSameCurrency(other, "Cannot compare money in different currencies."); + + return amount > other.amount; + } + + /// Returns `true` when this money is greater than or equal to [other]. + /// + /// Both operands have to be in same currency, [ArgumentError] will be thrown + /// otherwise. + bool operator >=(Money other) { + _assertSameCurrency(other, "Cannot compare money in different currencies."); + + return amount >= other.amount; + } + + /// Adds the amount of [other] to this amount. + /// + /// Both operands must be in same currency, [ArgumentError] will be thrown + /// otherwise. + Money operator +(Money other) { + _assertSameCurrency(other); + + return _withAmount(amount + other.amount); + } + + /// unary minus operator. + Money operator -() => _withAmount(-amount); + + /// Subtracts the amount of [other] from this amount. + /// + /// Both operands must be in same currency, [ArgumentError] will be thrown + /// otherwise. + Money operator -(Money other) { + _assertSameCurrency(other); + + return _withAmount(amount - other.amount); + } + + /// Returns [Money] multiplied by [other]. + /// + /// The result is again [Money]. + Money operator *(BigInt other) => _withAmount(amount * other); + + /// Returns [Money] divided by [other]. + /// + /// The result is again [Money]. + Money operator /(BigInt other) { + if (other == BigInt.zero) throw Exception('Division by zero.'); + + final neg = (amount.isNegative) ^ (other.isNegative); + final A = amount.abs(); + final B = other.abs(); + + final q = A ~/ B; + final r = A % B; + + // Compare 2*r with B to detect half/up/down + final twiceR = r << 1; + final mag = (twiceR >= B) ? q + BigInt.one : q; + + return _withAmount(neg ? -mag : mag); + } + + /// Creates a copy of this [Money] object with optional new values + /// for [amount], [currency]. + /// + /// If just [currency] is provided and the decimals missmatch + /// the amount will be transformed to keep its canonical representation + Money copyWith({BigInt? amount, Currency? currency}) { + if (currency != null && amount == null && currency.decimals != this.currency.decimals) { + return Money( + _transformAmount(this.amount, this.currency.decimals, currency.decimals), currency); + } + + return Money(amount ?? this.amount, currency ?? this.currency); + } + + /// Creates new instance with the same currency and given [amount]. + Money _withAmount(BigInt amount) => Money(amount, currency); + + void _assertSameCurrency(Money other, [String? message]) { + if (currency != other.currency) + throw ArgumentError(message ?? "Cannot operate with money values in different currencies."); + } + + BigInt _transformAmount(BigInt source, int sourceDecimals, int targetDecimals) { + if (sourceDecimals == targetDecimals) return source; + + if (sourceDecimals > targetDecimals) { + return parseFixed( + formatFixed(source, sourceDecimals).withMaxDecimals(targetDecimals), targetDecimals); + } else { + return parseFixed(formatFixed(source, sourceDecimals), targetDecimals); + } + } + + @override + int get hashCode => amount.hashCode ^ currency.hashCode; + + @override + String toString() => formatFixed(amount, currency.decimals); + + String toStringWithSymbol( + {int? fractionalDigits, bool trimZeros = true, bool useBaseUnit = false}) => + "${toStringWithPrecision(fractionalDigits: fractionalDigits, trimZeros: trimZeros, useBaseUnit: useBaseUnit)} ${_getSymbol(useBaseUnit)}"; + + String toStringWithPrecision( + {int? fractionalDigits, bool trimZeros = true, bool useBaseUnit = false}) => + formatFixed(amount, useBaseUnit ? 0 : currency.decimals, + fractionalDigits: fractionalDigits, trimZeros: trimZeros); + + // To Override the symbol with the ticker of the base unit + String _getSymbol(bool useBaseUnit) { + if (useBaseUnit && [CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) return "sats"; + return currency.symbol; + } +} diff --git a/cw_core/lib/amount_converter.dart b/cw_core/lib/amount_converter.dart index 3c48cde09..850dd9b10 100644 --- a/cw_core/lib/amount_converter.dart +++ b/cw_core/lib/amount_converter.dart @@ -1,78 +1,4 @@ -import 'package:decimal/decimal.dart'; -import 'package:decimal/intl.dart'; -import 'package:intl/intl.dart'; -import 'package:cw_core/crypto_currency.dart'; - class AmountConverter { - static const _moneroAmountLength = 12; - static const _moneroAmountDivider = 1000000000000; - static const _wowneroAmountLength = 11; - static const _wowneroAmountDivider = 100000000000; - static const _bitcoinAmountDivider = 100000000; - static const _bitcoinAmountLength = 8; - static final _bitcoinAmountFormat = NumberFormat() - ..maximumFractionDigits = _bitcoinAmountLength - ..minimumFractionDigits = 1; - static final _moneroAmountFormat = NumberFormat() - ..maximumFractionDigits = _moneroAmountLength - ..minimumFractionDigits = 1; - static final _wowneroAmountFormat = NumberFormat() - ..maximumFractionDigits = _wowneroAmountLength - ..minimumFractionDigits = 1; - - static String amountIntToString(CryptoCurrency cryptoCurrency, int amount) { - switch (cryptoCurrency) { - case CryptoCurrency.xmr: - return _moneroAmountToString(amount); - case CryptoCurrency.wow: - return _wowneroAmountToString(amount); - case CryptoCurrency.btc: - case CryptoCurrency.bch: - case CryptoCurrency.ltc: - case CryptoCurrency.dcr: - case CryptoCurrency.doge: - return _bitcoinAmountToString(amount); - case CryptoCurrency.xhv: - case CryptoCurrency.xag: - case CryptoCurrency.xau: - case CryptoCurrency.xaud: - case CryptoCurrency.xbtc: - case CryptoCurrency.xcad: - case CryptoCurrency.xchf: - case CryptoCurrency.xcny: - case CryptoCurrency.xeur: - case CryptoCurrency.xgbp: - case CryptoCurrency.xjpy: - case CryptoCurrency.xnok: - case CryptoCurrency.xnzd: - case CryptoCurrency.xusd: - return _moneroAmountToString(amount); - case CryptoCurrency.zano: - return _moneroAmountToStringUsingDecimals(amount); - default: - return ''; - } - } - - static double cryptoAmountToDouble({required num amount, required num divider}) => - amount / divider; - - static String _moneroAmountToString(int amount) => _moneroAmountFormat - .format(cryptoAmountToDouble(amount: amount, divider: _moneroAmountDivider)); - - static String _bitcoinAmountToString(int amount) => _bitcoinAmountFormat - .format(cryptoAmountToDouble(amount: amount, divider: _bitcoinAmountDivider)); - - static String _wowneroAmountToString(int amount) => _wowneroAmountFormat - .format(cryptoAmountToDouble(amount: amount, divider: _wowneroAmountDivider)); - - static Decimal cryptoAmountToDecimal({required int amount, required int divider}) => - (Decimal.fromInt(amount) / Decimal.fromInt(divider)).toDecimal(); - - static String _moneroAmountToStringUsingDecimals(int amount) => _moneroAmountFormat.format( - DecimalIntl(cryptoAmountToDecimal(amount: amount, divider: _moneroAmountDivider))); - - static String toBaseUnits(String amount, int decimals) { amount = amount.trim(); if (amount.isEmpty) return '0'; diff --git a/cw_core/lib/balance.dart b/cw_core/lib/balance.dart index 8c7820dba..4e400b93e 100644 --- a/cw_core/lib/balance.dart +++ b/cw_core/lib/balance.dart @@ -1,26 +1,20 @@ +import 'package:cw_core/amount/money.dart'; + /// Balance Data class with all amounts in the lowest possible currency (e.g. satoshis or wei) abstract class Balance { - const Balance(this.available, this.additional, {this.secondAvailable, this.secondAdditional, this.frozen}); + const Balance( + this.available, + this.unavailable, { + this.secondAvailable, + this.secondUnavailable, + this.frozen, + }); - Balance.fromInt(int available, int additional, {int? secondAvailable, int? secondAdditional, int? frozen}) : - available = BigInt.from(available), - additional = BigInt.from(additional), - secondAvailable = secondAvailable == null ? null : BigInt.from(secondAvailable), - secondAdditional = secondAdditional == null ? null : BigInt.from(secondAdditional), - frozen = frozen == null ? null : BigInt.from(frozen) {} + final Money available; + final Money unavailable; + final Money? secondAvailable; + final Money? secondUnavailable; - - final BigInt available; - final BigInt additional; - - final BigInt? secondAvailable; - final BigInt? secondAdditional; - - final BigInt? frozen; - - BigInt get fullAvailableBalance => available; - - @deprecated - String get formattedUnAvailableBalance => ''; + final Money? frozen; } diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 0baeabcec..cdf8283f4 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -1,9 +1,9 @@ +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/currency.dart'; import 'package:cw_core/currency_groups.dart'; import 'package:cw_core/enumerable_item.dart'; import 'package:collection/collection.dart'; import 'package:cw_core/format_fixed.dart'; -import 'package:cw_core/parse_fixed.dart'; class CryptoCurrency extends EnumerableItem with Serializable implements Currency { const CryptoCurrency({ @@ -21,18 +21,30 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen this.groups = const {}, }) : super(title: title, raw: raw); - final String name; - final String? tag; - final String? fullName; - final String? iconPath; final String? flatIconPath; final String? chainIconPath; - @override - final int decimals; final bool enabled; final bool isPotentialScam; final Set groups; + @override + final String name; + + @override + final String? fullName; + + @override + final String? tag; + + @override + final String? iconPath; + + @override + final int decimals; + + @override + String get symbol => title; + set enabled(bool value) => this.enabled = value; static const all = [ @@ -432,13 +444,13 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen bool titleAndTagEqual(CryptoCurrency other) => title == other.title && tag == other.tag; + @override + Money parseAmount(String value) => Money.parse(value, this); + + @override + Money? tryParseAmount(String value) => Money.tryParse(value, this); + /// Format the raw amount into its decimal representation eg. turn Sats into Bitcoin String formatAmount(BigInt amount, {int? fractionalDigits, bool trimZeros = true}) => formatFixed(amount, decimals, fractionalDigits: fractionalDigits, trimZeros: trimZeros); - - /// Parse the [value] and turn it into the smallest denomination eg. turn Bitcoin into Sats - BigInt parseAmount(String value) => parseFixed(value, decimals); - - /// Try parsing the [value] and turn it into the smallest denomination eg. turn Bitcoin into Sats - BigInt? tryParseAmount(String value) => tryParseFixed(value, decimals); } diff --git a/cw_core/lib/currency.dart b/cw_core/lib/currency.dart index f43455a3e..75fa854b7 100644 --- a/cw_core/lib/currency.dart +++ b/cw_core/lib/currency.dart @@ -1,7 +1,23 @@ +import 'package:cw_core/amount/money.dart'; + abstract class Currency { + String get symbol; String get name; - String? get tag; String? get fullName; String? get iconPath; int get decimals; + String? get tag; + + /// Parse the [value] and turn it into [Money] + Money parseAmount(String value); + + /// Try parsing the [value] and turn it into [Money] + Money? tryParseAmount(String value); + + @override + bool operator ==(Object other) => + other is Currency && other.decimals == decimals && other.symbol == symbol && other.tag == tag; + + @override + int get hashCode => decimals.hashCode ^ symbol.hashCode ^ tag.hashCode; } diff --git a/cw_core/lib/enumerable_item.dart b/cw_core/lib/enumerable_item.dart index e7a863909..3eac165be 100644 --- a/cw_core/lib/enumerable_item.dart +++ b/cw_core/lib/enumerable_item.dart @@ -1,5 +1,3 @@ -import 'package:flutter/foundation.dart'; - abstract class EnumerableItem { const EnumerableItem({required this.title, required this.raw}); diff --git a/cw_core/lib/enumerate.dart b/cw_core/lib/enumerate.dart index d92347e91..70fc2e7a6 100644 --- a/cw_core/lib/enumerate.dart +++ b/cw_core/lib/enumerate.dart @@ -2,7 +2,7 @@ abstract class Enumerate { String get value; @override - operator ==(other) { + bool operator ==(other) { if (identical(other, this)) return true; if (other is! Enumerate) return false; return other.runtimeType == runtimeType && value == other.value; diff --git a/cw_core/lib/exceptions.dart b/cw_core/lib/exceptions.dart index 35999b78b..b6155529c 100644 --- a/cw_core/lib/exceptions.dart +++ b/cw_core/lib/exceptions.dart @@ -26,9 +26,7 @@ class TransactionCommitFailed implements Exception { TransactionCommitFailed({this.errorMessage}); @override - String toString() { - return errorMessage ?? "unknown error"; - } + String toString() => errorMessage ?? "unknown error"; } class TransactionCommitFailedDustChange implements Exception {} @@ -57,10 +55,6 @@ class SignSPLTokenTransactionRentException implements Exception {} class NoAssociatedTokenAccountException implements Exception {} - -/// ============================================================================== -/// ============================================================================== - class RestoreFromSeedException implements Exception { final String message; diff --git a/cw_core/lib/lnurl.dart b/cw_core/lib/lnurl.dart index 03be284d5..7676daf7c 100644 --- a/cw_core/lib/lnurl.dart +++ b/cw_core/lib/lnurl.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'package:bech32/bech32.dart'; +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; const _BOLT_PREFIXES = ["lnbcrt", "lntbs", "lnbc", "lntb"]; @@ -61,14 +63,14 @@ int? _getAmountBolt11Msat(String invoice) { } } -int? getBolt11Amount(String invoice) { +Money? getBolt11Amount(String invoice) { final msat = _getAmountBolt11Msat(invoice); if (msat == null || msat % 1000 != 0) return null; - return msat ~/ 1000; + return Money.fromInt(msat ~/ 1000, CryptoCurrency.btcln); } class LNURL { - static Future getPayRequestAmount(String lnurl) async { + static Future getPayRequestAmount(String lnurl) async { final url = decode(lnurl); final response = await ProxyWrapper().get(clearnetUri: url); @@ -88,7 +90,7 @@ class LNURL { return null; if (msat == null || msat % 1000 != 0) return null; - return msat ~/ 1000; + return Money.fromInt(msat ~/ 1000, CryptoCurrency.btcln); } catch (_) { return null; } diff --git a/cw_core/lib/monero_balance.dart b/cw_core/lib/monero_balance.dart deleted file mode 100644 index 3a85d3cb9..000000000 --- a/cw_core/lib/monero_balance.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:cw_core/balance.dart'; -import 'package:cw_core/monero_amount_format.dart'; - -class MoneroBalance extends Balance { - MoneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0}) - : formattedFrozenBalance = moneroAmountToString(amount: frozenBalance), - super.fromInt(unlockedBalance, fullBalance - unlockedBalance, frozen: frozenBalance); - - final int fullBalance; - final int unlockedBalance; - final int frozenBalance; - final String formattedFrozenBalance; - - @override - String get formattedUnAvailableBalance => - formattedFrozenBalance == '0.0' ? '' : formattedFrozenBalance; -} diff --git a/cw_core/lib/output_info.dart b/cw_core/lib/output_info.dart index 1db1c2ed6..ef53099f9 100644 --- a/cw_core/lib/output_info.dart +++ b/cw_core/lib/output_info.dart @@ -1,24 +1,25 @@ +import 'package:cw_core/amount/money.dart'; + class OutputInfo { - const OutputInfo( - {required this.address, - required this.sendAll, - required this.isParsedAddress, - this.cryptoAmount, - this.formattedCryptoAmount, + const OutputInfo({ + required this.address, + required this.sendAll, + required this.isParsedAddress, + required this.cryptoAmount, this.fiatAmount, this.note, this.extractedAddress, this.memo, - this.extra = const {}}); + this.extra = const {}, + }); - final String? fiatAmount; - final String? cryptoAmount; - final String address; - final String? note; - final String? extractedAddress; - final bool sendAll; - final bool isParsedAddress; - final int? formattedCryptoAmount; - final String? memo; - final Map extra; -} \ No newline at end of file + final String? fiatAmount; + final Money cryptoAmount; + final String address; + final String? note; + final String? extractedAddress; + final bool sendAll; + final bool isParsedAddress; + final String? memo; + final Map extra; +} diff --git a/cw_core/lib/parse_fixed.dart b/cw_core/lib/parse_fixed.dart index 695bad60f..fede4df09 100644 --- a/cw_core/lib/parse_fixed.dart +++ b/cw_core/lib/parse_fixed.dart @@ -45,7 +45,7 @@ BigInt parseFixed(String value, int decimals) { var fraction = (comps.length == 2 ? comps[1] : "0").padRight(decimals, "0"); if (fraction.length > multiplier.length - 1) { - throw FormatException("fractional component exceeds decimals, underflow, parseFixed"); + throw FormatException("fractional component(${fraction.length}) exceeds decimals(${decimals}), underflow, parseFixed"); } final wholeValue = BigInt.parse(whole); diff --git a/cw_core/lib/pending_transaction.dart b/cw_core/lib/pending_transaction.dart index 89bd75922..3d8912fa3 100644 --- a/cw_core/lib/pending_transaction.dart +++ b/cw_core/lib/pending_transaction.dart @@ -1,3 +1,5 @@ +import 'package:cw_core/amount/money.dart'; + class PendingChange { final String address; final BigInt amount; @@ -7,22 +9,19 @@ class PendingChange { mixin PendingTransaction { String get id; + + Money get amount; + Money get fee; + String get amountFormatted; - String get feeFormatted; - String get feeFormattedValue; + String get feeFormatted => fee.toStringWithSymbol(fractionalDigits: 8); + String get feeFormattedValue => fee.toStringWithPrecision(fractionalDigits: 8); String? feeRate; String get hex; String? get evmTxHashFromRawHex => null; int? get outputCount => null; PendingChange? change; - String get roundedAmountFormatted { - if (amountFormatted.split(".").last.length <= 8) { - return amountFormatted; - } - return double.parse(amountFormatted).toStringAsFixed(8); - } - bool shouldCommitUR() => false; Future commit(); diff --git a/cw_core/lib/transaction_info.dart b/cw_core/lib/transaction_info.dart index e3453747a..a8743e0f8 100644 --- a/cw_core/lib/transaction_info.dart +++ b/cw_core/lib/transaction_info.dart @@ -1,20 +1,18 @@ +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/format_amount.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/keyable.dart'; abstract class TransactionInfo extends Object with Keyable { late String id; late String txHash = id; - late int amount; - int? fee; + late Money amount; + Money? fee; late TransactionDirection direction; late bool isPending; late DateTime date; int? height; int confirmations = 0; - String amountFormatted(); - String fiatAmount(); - String? feeFormatted(); - void changeFiatAmount(String amount); String? to; String? from; String? evmSignatureName; @@ -26,4 +24,8 @@ abstract class TransactionInfo extends Object with Keyable { dynamic get keyIndex => id; Map additionalInfo = {}; + + String? _fiatAmount; + String fiatAmount() => _fiatAmount ?? ''; + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); } diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index c7ed4eccb..917d86ef4 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -43,8 +43,6 @@ abstract class WalletBase get balance; - String formatCryptoAmount(String amount) => amount; - SyncStatus get syncStatus; set syncStatus(SyncStatus status); diff --git a/cw_core/lib/wownero_balance.dart b/cw_core/lib/wownero_balance.dart deleted file mode 100644 index de6518023..000000000 --- a/cw_core/lib/wownero_balance.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:cw_core/balance.dart'; -import 'package:cw_core/wownero_amount_format.dart'; - -class WowneroBalance extends Balance { - WowneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0}) - : formattedFrozenBalance = wowneroAmountToString(amount: frozenBalance), - super.fromInt(unlockedBalance, fullBalance - unlockedBalance, frozen: frozenBalance); - - final int fullBalance; - final int unlockedBalance; - final int frozenBalance; - final String formattedFrozenBalance; - - @override - String get formattedUnAvailableBalance => - formattedFrozenBalance == '0.0' ? '' : formattedFrozenBalance; -} diff --git a/cw_core/test/amount/amount_sanitizer_test.dart b/cw_core/test/amount/amount_sanitizer_test.dart new file mode 100644 index 000000000..f6b5b5bed --- /dev/null +++ b/cw_core/test/amount/amount_sanitizer_test.dart @@ -0,0 +1,31 @@ +import 'package:cw_core/amount/amount_sanitizer.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('smartAmountSanitizer', () { + test('sanitize decimal dot', (){ + final amount = "1,000.00".sanitized(); + expect(amount, "1000.00"); + }); + + test('sanitize decimal comma', (){ + final amount = "1.000,00".sanitized(); + expect(amount, "1000.00"); + }); + + test('general case', (){ + final amount = "1000.00".sanitized(); + expect(amount, "1000.00"); + }); + + test('no decimals', (){ + final amount = "1,000".sanitized(); + expect(amount, "1.000"); + }); + + test('no decimals', (){ + final amount = "1.000".sanitized(); + expect(amount, "1.000"); + }); + }); +} diff --git a/cw_core/test/amount/money_test.dart b/cw_core/test/amount/money_test.dart new file mode 100644 index 000000000..ddb13bc3b --- /dev/null +++ b/cw_core/test/amount/money_test.dart @@ -0,0 +1,351 @@ +import "package:cw_core/amount/money.dart"; +import "package:cw_core/crypto_currency.dart"; +import "package:flutter_test/flutter_test.dart"; + +void main() { + group("Money", () { + test("parse", () { + var money = Money.parse("1", CryptoCurrency.btc); + expect(money.amount, BigInt.from(100000000)); + + money = Money.parse("-1", CryptoCurrency.btc); + expect(money.amount, BigInt.from(-100000000)); + + money = Money.parse("1.11", CryptoCurrency.btc); + expect(money.amount, BigInt.from(111000000)); + + money = Money.parse("-1.11", CryptoCurrency.btc); + expect(money.amount, BigInt.from(-111000000)); + + money = Money.parse("-.11", CryptoCurrency.btc); + expect(money.amount, BigInt.from(-11000000)); + + money = Money.parse(".11", CryptoCurrency.btc); + expect(money.amount, BigInt.from(11000000)); + + // Invalid representation + expect(() => Money.parse("1,11", CryptoCurrency.btc), throwsFormatException); + + // To many decimals + expect(() => Money.parse("-1.000000000000000", CryptoCurrency.btc), throwsFormatException); + + money = Money.parse("1", CryptoCurrency.btc, isBaseUnit: true); + expect(money.amount, BigInt.from(1)); + + money = Money.parse("0", CryptoCurrency.btc, isBaseUnit: true); + expect(money.amount, BigInt.from(0)); + + money = Money.parse("-1", CryptoCurrency.btc, isBaseUnit: true); + expect(money.amount, BigInt.from(-1)); + + // canonical representation when expecting base units + expect(() => Money.parse("1.0", CryptoCurrency.btc, isBaseUnit: true), throwsFormatException); + }); + + test("tryParse", () { + var money = Money.tryParse("1", CryptoCurrency.btc); + expect(money?.amount, BigInt.from(100000000)); + + money = Money.tryParse("-1", CryptoCurrency.btc); + expect(money?.amount, BigInt.from(-100000000)); + + money = Money.tryParse("1.11", CryptoCurrency.btc); + expect(money?.amount, BigInt.from(111000000)); + + money = Money.tryParse("-1.11", CryptoCurrency.btc); + expect(money?.amount, BigInt.from(-111000000)); + + money = Money.tryParse("-.11", CryptoCurrency.btc); + expect(money?.amount, BigInt.from(-11000000)); + + money = Money.tryParse(".11", CryptoCurrency.btc); + expect(money?.amount, BigInt.from(11000000)); + + // Invalid representation + money = Money.tryParse("1,11", CryptoCurrency.btc); + expect(money?.amount, isNull); + + // To many decimals + money = Money.tryParse("-1.000000000000000", CryptoCurrency.btc); + expect(money?.amount, isNull); + + money = Money.tryParse("1", CryptoCurrency.btc, isBaseUnit: true); + expect(money?.amount, BigInt.from(1)); + + money = Money.tryParse("0", CryptoCurrency.btc, isBaseUnit: true); + expect(money?.amount, BigInt.from(0)); + + money = Money.tryParse("-1", CryptoCurrency.btc, isBaseUnit: true); + expect(money?.amount, BigInt.from(-1)); + + // canonical representation when expecting base units + money = Money.tryParse("1.0", CryptoCurrency.btc, isBaseUnit: true); + expect(money?.amount, isNull); + }); + + test("sign", () { + expect(Money(BigInt.from(0), CryptoCurrency.btc).sign, 0); + + expect(Money(BigInt.from(1), CryptoCurrency.btc).sign, 1); + + expect(Money(BigInt.from(10), CryptoCurrency.btc).sign, 1); + + expect(Money(BigInt.from(-1), CryptoCurrency.btc).sign, -1); + + expect(Money(BigInt.from(-10), CryptoCurrency.btc).sign, -1); + }); + + test("isZero", () { + expect(Money(BigInt.from(0), CryptoCurrency.btc).isZero, isTrue); + + expect(Money(BigInt.from(1), CryptoCurrency.btc).isZero, isFalse); + + expect(Money(BigInt.from(-1), CryptoCurrency.btc).isZero, isFalse); + }); + + test("isNegative", () { + expect(Money(BigInt.from(0), CryptoCurrency.btc).isNegative, isFalse); + + expect(Money(BigInt.from(1), CryptoCurrency.btc).isNegative, isFalse); + + expect(Money(BigInt.from(-1), CryptoCurrency.btc).isNegative, isTrue); + }); + + group("Comparison", () { + final fourBitcoin = Money(BigInt.from(400000000), CryptoCurrency.btc); + final fiveBitcoin = Money(BigInt.from(500000000), CryptoCurrency.btc); + final sixBitcoin = Money(BigInt.from(600000000), CryptoCurrency.btc); + + final fiveMonero = Money.parse("5", CryptoCurrency.xmr); + + test("==", () { + expect(fiveBitcoin, equals(Money(BigInt.from(500000000), CryptoCurrency.btc))); + expect(fiveBitcoin, isNot(equals(fourBitcoin))); + expect(fiveBitcoin, isNot(equals(sixBitcoin))); + expect(fiveBitcoin, isNot(equals(fiveMonero))); + // intentional check of incompatible types. + // ignore: unrelated_type_equality_checks + expect(fiveBitcoin == "Not money", equals(false)); + }); + + test("<", () { + expect(fiveBitcoin < sixBitcoin, isTrue); + expect(fiveBitcoin < fiveBitcoin, isFalse); + expect(fiveBitcoin < fourBitcoin, isFalse); + + // Cannot compare money in different currencies: + expect(() => fiveBitcoin < fiveMonero, throwsArgumentError); + }); + + test("<=", () { + expect(fiveBitcoin <= sixBitcoin, isTrue); + expect(fiveBitcoin <= fiveBitcoin, isTrue); + expect(fiveBitcoin <= fourBitcoin, isFalse); + + // Cannot compare money in different currencies: + expect(() => fiveBitcoin <= fiveMonero, throwsArgumentError); + }); + + test(">", () { + expect(fiveBitcoin > fourBitcoin, isTrue); + expect(fiveBitcoin > fiveBitcoin, isFalse); + expect(fiveBitcoin > sixBitcoin, isFalse); + + // Cannot compare money in different currencies: + expect(() => fiveBitcoin > fiveMonero, throwsArgumentError); + }); + + test(">=", () { + expect(fiveBitcoin >= fourBitcoin, isTrue); + expect(fiveBitcoin >= fiveBitcoin, isTrue); + expect(fiveBitcoin >= sixBitcoin, isFalse); + + // Cannot compare money in different currencies: + expect(() => fiveBitcoin >= fiveMonero, throwsArgumentError); + }); + + test("conformance to Comparable", () { + expect(fiveBitcoin, isA>()); + + expect(fiveBitcoin.compareTo(fiveBitcoin), isZero); + expect(fiveBitcoin.compareTo(fourBitcoin), isPositive); + expect(fiveBitcoin.compareTo(sixBitcoin), isNegative); + expect(() => fiveBitcoin.compareTo(fiveMonero), throwsArgumentError); + }); + }); + + group("Math", () { + test("+", () { + final oneBitcoin = Money.parse("1", CryptoCurrency.btc); + final twoBitcoin = Money.parse("2", CryptoCurrency.btc); + final threeBitcoin = Money.parse("3", CryptoCurrency.btc); + + expect(oneBitcoin + twoBitcoin, equals(threeBitcoin)); + }); + + test("cannot add different currencies", () { + final oneBitcoin = Money(BigInt.one, CryptoCurrency.btc); + final oneMonero = Money(BigInt.one, CryptoCurrency.xmr); + + expect(() => oneBitcoin + oneMonero, throwsArgumentError); + }); + + test("invert money", () { + final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc); + final negativeOneSatoshi = Money(BigInt.from(-1), CryptoCurrency.btc); + + expect(-oneSatoshi, equals(negativeOneSatoshi)); + expect(-negativeOneSatoshi, equals(oneSatoshi)); + }); + + test("-", () { + final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc); + final twoSatoshi = Money(BigInt.two, CryptoCurrency.btc); + final threeSatoshi = Money(BigInt.from(3), CryptoCurrency.btc); + + expect(threeSatoshi - oneSatoshi, equals(twoSatoshi)); + }); + + test("cannot subtract different currencies", () { + final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc); + final oneMonero = Money(BigInt.one, CryptoCurrency.xmr); + + expect(() => oneSatoshi - oneMonero, throwsArgumentError); + }); + + test("*", () { + final zeroSatoshi = Money(BigInt.zero, CryptoCurrency.btc); + final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc); + final twoSatoshi = Money(BigInt.two, CryptoCurrency.btc); + + expect(oneSatoshi * BigInt.zero, equals(zeroSatoshi)); + expect(oneSatoshi * BigInt.two, equals(twoSatoshi)); + expect(oneSatoshi * BigInt.from(-2), equals(-twoSatoshi)); + }); + + group("/", () { + final threeBI = BigInt.from(3); + final fourBI = BigInt.from(4); + + final negThreeBI = BigInt.from(-3); + final negFourBI = BigInt.from(-4); + + test("rounds down when fractional part is < 0.5", () { + expect((Money(BigInt.from(10), CryptoCurrency.btc) / threeBI).amount, threeBI); + expect((Money(BigInt.from(-10), CryptoCurrency.btc) / threeBI).amount, negThreeBI); + expect((Money(BigInt.from(10), CryptoCurrency.btc) / negThreeBI).amount, negThreeBI); + expect((Money(BigInt.from(-10), CryptoCurrency.btc) / negThreeBI).amount, threeBI); + }); + + test("rounds up when fractional part is > 0.5", () { + expect((Money(BigInt.from(11), CryptoCurrency.btc) / threeBI).amount, fourBI); // 3.66... + expect((Money(BigInt.from(-11), CryptoCurrency.btc) / threeBI).amount, negFourBI); + expect((Money(BigInt.from(11), CryptoCurrency.btc) / negThreeBI).amount, negFourBI); + expect((Money(BigInt.from(-11), CryptoCurrency.btc) / negThreeBI).amount, fourBI); + }); + + test("rounds AWAY from zero when fractional part is exactly 0.5 (Ties)", () { + // 5 / 2 = 2.5 -> 3 + expect((Money(BigInt.from(5), CryptoCurrency.btc) / BigInt.two).amount, threeBI); + // -5 / 2 = -2.5 -> -3 + expect((Money(BigInt.from(-5), CryptoCurrency.btc) / BigInt.two).amount, negThreeBI); + // 5 / -2 = -2.5 -> -3 + expect((Money(BigInt.from(5), CryptoCurrency.btc) / -BigInt.two).amount, negThreeBI); + // -5 / -2 = 2.5 -> 3 + expect((Money(BigInt.from(-5), CryptoCurrency.btc) / -BigInt.two).amount, threeBI); + }); + + test("handles zero numerator correctly", () { + expect((Money(BigInt.zero, CryptoCurrency.btc) / BigInt.from(5)).amount, BigInt.zero); + expect((Money(BigInt.zero, CryptoCurrency.btc) / BigInt.from(-5)).amount, BigInt.zero); + }); + + test("throws Exception on division by zero", () { + expect( + () => Money(BigInt.from(5), CryptoCurrency.btc) / BigInt.zero, + throwsA(isA()), + ); + }); + + test("handles very large BigInts without precision loss", () { + // A number that would lose precision if converted to a standard double + final hugeA = BigInt.parse("10000000000000000000000000000000000005"); + final hugeB = BigInt.parse("10"); + // 1000...005 / 10 = 1000...000.5 -> Should round up to 1000...001 + final expected = BigInt.parse("1000000000000000000000000000000000001"); + + expect((Money(hugeA, CryptoCurrency.btc) / hugeB).amount, expected); + }); + + test("handles exact division perfectly", () { + expect((Money(BigInt.from(100), CryptoCurrency.btc) / BigInt.from(20)).amount, + BigInt.from(5)); + expect((Money(BigInt.from(-100), CryptoCurrency.btc) / BigInt.from(20)).amount, + BigInt.from(-5)); + }); + }); + }); + + group("copyWith", () { + final money = Money.parse("1", CryptoCurrency.btc); + + test("currency", () { + final copy = money.copyWith(currency: CryptoCurrency.eth); + expect(copy.amount, equals(BigInt.parse("1000000000000000000"))); + expect(copy.currency.decimals, equals(18)); + expect(copy.toString(), "1"); + }); + + test("currency nano", () { + final copy = money.copyWith(currency: CryptoCurrency.nano); + expect(copy.amount, equals(BigInt.parse("1000000000000000000000000000000"))); + expect(copy.currency.decimals, equals(30)); + expect(copy.toString(), "1"); + }); + + test("amount", () { + final copy = money.copyWith(amount: BigInt.one); + expect(copy.amount, BigInt.one); + expect(copy.currency.decimals, 8); + }); + + test("amount and currency", () { + final copy = money.copyWith(amount: BigInt.one); + expect(copy.amount, BigInt.one); + expect(copy.currency.decimals, 8); + }); + }); + + group("toStringWithPrecision", (){ + test("with fractionalDigits and padded with zeros", () { + final money = Money.parse("1", CryptoCurrency.btc); + expect(money.amount, equals(BigInt.parse("100000000"))); + expect(money.currency.decimals, equals(8)); + expect(money.toStringWithPrecision(fractionalDigits: 5, trimZeros: false), "1.00000"); + }); + + test("using base unit sats", () { + final money = Money.parse("1", CryptoCurrency.btc); + expect(money.amount, equals(BigInt.parse("100000000"))); + expect(money.currency.decimals, equals(8)); + expect(money.toStringWithPrecision(useBaseUnit: true), "100000000"); + }); + }); + + group("toStringWithSymbol", (){ + test("with fractionalDigits and padded with zeros", () { + final money = Money.parse("1", CryptoCurrency.btc); + expect(money.amount, equals(BigInt.parse("100000000"))); + expect(money.currency.decimals, equals(8)); + expect(money.toStringWithSymbol(fractionalDigits: 5, trimZeros: false), "1.00000 BTC"); + }); + + test("using base unit sats", () { + final money = Money.parse("1", CryptoCurrency.btc); + expect(money.amount, equals(BigInt.parse("100000000"))); + expect(money.currency.decimals, equals(8)); + expect(money.toStringWithSymbol(useBaseUnit: true), "100000000 sats"); + }); + }); + }); +} diff --git a/cw_core/test/lnurl_test.dart b/cw_core/test/lnurl_test.dart index 1df503b9e..f9120d881 100644 --- a/cw_core/test/lnurl_test.dart +++ b/cw_core/test/lnurl_test.dart @@ -1,3 +1,5 @@ +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/lnurl.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/tor/abstract.dart'; @@ -11,13 +13,14 @@ void main() { expect(content, Uri.parse("https://cake.cash/.well-known/lnurlp/konsti")); }); - test('get amount', () async { - CakeTor.instance = await CakeTorInstance.getInstance(); - - final content = await LNURL.getPayRequestAmount( - "LNURL1DP68GURN8GHJ7CTSWQHXY6T5WFJHZAT9WD6ZU6T09AUZ7URJDAU8JTMKXYHKCM308A5N6MNHVVNXJEPAXGMNWWPEX3JRGDTPXVNXZ0FCX5CRQVPSXQNX602VFE24YNP9XGC8W6T5DQJNYVRPD4HH2MN5GTRNLZ"); - expect(content, 8500); - }); + // This tests includes a rest request to an URL that expired and due to the lack of an alternative URL we ignore this test + // test('get amount', () async { + // CakeTor.instance = await CakeTorInstance.getInstance(); + // + // final content = await LNURL.getPayRequestAmount( + // "LNURL1DP68GURN8GHJ7CTSWQHXY6T5WFJHZAT9WD6ZU6T09AUZ7URJDAU8JTMKXYHKCM308A5N6MNHVVNXJEPAXGMNWWPEX3JRGDTPXVNXZ0FCX5CRQVPSXQNX602VFE24YNP9XGC8W6T5DQJNYVRPD4HH2MN5GTRNLZ"); + // expect(content, Money.fromInt(8500, CryptoCurrency.btcln)); + // }); test('get no amount', () async { CakeTor.instance = await CakeTorInstance.getInstance(); @@ -64,22 +67,22 @@ void main() { test("should get the amount 1 BTC encoded as bitcoin", () { final invoice = "lnbc11p5exaxspp5u6e6lx90rw2vqr2e5axt8ur3v98qnmqfsmh8r0l0mcm7ry70vyvsdqqcqzzsxqrrsssp5eqydmrufk39dwhuxzajwx4xyffn0mlcu3qa8vjwry6qev5gsepkq9qxpqysgqt0zldve9uxt3txjt8wp0gev52lquljgugy26nneue85pjwk5yak4hs2d8533tus07pq5ka6zrnd0ruaeepwres8p7ws5mx63xlwff5sqlxcy8a"; - expect(getBolt11Amount(invoice), 100000000); + expect(getBolt11Amount(invoice), Money.fromInt(100000000, CryptoCurrency.btcln)); }); test("should get the amount 0,1 BTC encoded as millibitcoin", () { final invoice = "lnbc100m1p5exagmpp5n4c5tml3h8h7k4gthc9papkyeymceqqvcldkng4nvxwzj7nu9jnsdqqcqzzsxqrrsssp58kgzrh7y3pevantduc4yg33aa6r79ed6xgmhr0dy40zc8pxrz5ms9qxpqysgqcq27km3v3wcz6pntmsfnz259zy6tff0uwjz9kamdrvmxmdv09gp44mh0ygn7gweqf4zdev4x8e67p5kcvahethchdkkjexhtk7emuqcpasz0lp"; - expect(getBolt11Amount(invoice), 10000000); + expect(getBolt11Amount(invoice), Money.fromInt(10000000, CryptoCurrency.btcln)); }); test("should get the amount 1000 SATS encoded as microbitcoin", () { final invoice = "lnbc10u1p5exfgvpp5n3s9dsw9ddax5c3h8437ya7y4g582uyufyna626yr9vqdfjrhwzqdqqcqzzsxqrrsssp5mmkyp5h35fpaxm4063f3kc7d4nmgd4gd8fv8086wxd784m76kmpq9qxpqysgqye9r0vahpjy9l2je6vakxzk3cjnsyx76r29c4amkz4cmapq5tqt5l7rl0emkd003jjvz2d8jqrw6wc4cduvapuyeseh7a855l82wutcpeq30kg"; - expect(getBolt11Amount(invoice), 1000); + expect(getBolt11Amount(invoice), Money.fromInt(1000, CryptoCurrency.btcln)); }); test("should get the amount 1 SAT encoded as nanobitcoin", () { final invoice = "lnbc10n1p5exazppp5szh273r6nzvwydzsdlg0ws0vhxwnc6nqtqt4sjt0vu6x6l8t5csqdqqcqzzsxqrrsssp5nkup6uht7xlfqaj8lxpe5kx0ejum9p4pvv3nlnndr9ykzge3393s9qxpqysgqufh05a7qs0ezeckvmh7tjkr9ngng96nq274twg20z45ysp7afpdrttm2ky73cpjwpjqsmtzzd79w6lyvszgmkanwn8mj0jcfunrwv3qprg3z3j"; - expect(getBolt11Amount(invoice), 1); + expect(getBolt11Amount(invoice), Money.fromInt(1, CryptoCurrency.btcln)); }); }); } diff --git a/cw_decred/lib/amount_format.dart b/cw_decred/lib/amount_format.dart deleted file mode 100644 index c09f76b3b..000000000 --- a/cw_decred/lib/amount_format.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:intl/intl.dart'; -import 'package:cw_core/crypto_amount_format.dart'; - -const decredAmountLength = 8; -const decredAmountDivider = 100000000; -final decredAmountFormat = NumberFormat() - ..maximumFractionDigits = decredAmountLength - ..minimumFractionDigits = 1; - -String decredAmountToString({required int amount}) => - decredAmountFormat.format(cryptoAmountToDouble(amount: amount, divider: decredAmountDivider)); - -double decredAmountToDouble({required int amount}) => - cryptoAmountToDouble(amount: amount, divider: decredAmountDivider); - -int stringDoubleToDecredAmount(String amount) { - int result = 0; - - try { - result = (double.parse(amount) * decredAmountDivider).round(); - } catch (e) { - result = 0; - } - - return result; -} diff --git a/cw_decred/lib/balance.dart b/cw_decred/lib/balance.dart index 97889c9af..b283cb4ba 100644 --- a/cw_decred/lib/balance.dart +++ b/cw_decred/lib/balance.dart @@ -1,26 +1,16 @@ -import 'package:cw_decred/amount_format.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/balance.dart'; +import 'package:cw_core/crypto_currency.dart'; class DecredBalance extends Balance { - DecredBalance({required this.confirmed, required this.unconfirmed, required int frozen}) - : _frozen = frozen, super.fromInt(confirmed, unconfirmed); + DecredBalance({required Money confirmed, required Money unconfirmed, required Money frozen}) + : super(confirmed, unconfirmed, frozen: frozen); - factory DecredBalance.zero() => DecredBalance(confirmed: 0, unconfirmed: 0, frozen: 0); - - final int confirmed; - final int unconfirmed; - final int _frozen; - BigInt get frozen => BigInt.from(_frozen); + factory DecredBalance.zero() => DecredBalance( + confirmed: Money.zero(CryptoCurrency.dcr), + unconfirmed: Money.zero(CryptoCurrency.dcr), + frozen: Money.zero(CryptoCurrency.dcr)); @override - String get formattedAvailableBalance => decredAmountToString(amount: confirmed - _frozen); - - @override - String get formattedAdditionalBalance => decredAmountToString(amount: unconfirmed); - - @override - String get formattedUnAvailableBalance { - final frozenFormatted = decredAmountToString(amount: _frozen); - return frozenFormatted == '0.0' ? '' : frozenFormatted; - } + Money get available => super.available - (frozen ?? Money.zero(CryptoCurrency.dcr)); } diff --git a/cw_decred/lib/pending_transaction.dart b/cw_decred/lib/pending_transaction.dart index 23c76920c..3f58ee8b2 100644 --- a/cw_decred/lib/pending_transaction.dart +++ b/cw_decred/lib/pending_transaction.dart @@ -1,42 +1,33 @@ +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_decred/amount_format.dart'; class DecredPendingTransaction with PendingTransaction { - DecredPendingTransaction( - {required this.txid, - required this.amount, - required this.fee, - required this.rawHex, - required this.send}); + DecredPendingTransaction({ + required this.txId, + required this.amount, + required this.fee, + required this.rawHex, + required this.send, + }); - final int amount; - final int fee; - final String txid; + final Money amount; + final Money fee; + final String txId; final String rawHex; final Future Function() send; @override - String get id => txid; + String get id => txId; @override - String get amountFormatted => decredAmountToString(amount: amount); - - @override - String get feeFormatted => "$feeFormattedValue DCR"; - - @override - String get feeFormattedValue => decredAmountToString(amount: fee); + String get amountFormatted => amount.toString(); @override String get hex => rawHex; @override - Future commit() async { - return send(); - } + Future commit() => send(); @override - Future> commitUR() { - throw UnimplementedError(); - } + Future> commitUR() => throw UnimplementedError(); } diff --git a/cw_decred/lib/transaction_info.dart b/cw_decred/lib/transaction_info.dart index 18913175a..6a31c04d9 100644 --- a/cw_decred/lib/transaction_info.dart +++ b/cw_decred/lib/transaction_info.dart @@ -1,15 +1,12 @@ -import 'package:cw_core/currency_for_wallet_type.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/format_amount.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cw_decred/amount_format.dart'; class DecredTransactionInfo extends TransactionInfo { DecredTransactionInfo({ required String id, - required int amount, - required int fee, + required Money amount, + required Money fee, required TransactionDirection direction, required bool isPending, required DateTime date, @@ -27,20 +24,4 @@ class DecredTransactionInfo extends TransactionInfo { this.confirmations = confirmations; this.to = to; } - - String? _fiatAmount; - - @override - String amountFormatted() => - '${formatAmount(decredAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(WalletType.decred).title}'; - - @override - String? feeFormatted() => - '${formatAmount(decredAmountToString(amount: fee ?? 0))} ${walletTypeToCryptoCurrency(WalletType.decred).title}'; - - @override - String fiatAmount() => _fiatAmount ?? ''; - - @override - void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); } diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index 97c118331..a41dcb8f6 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -1,13 +1,13 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:cw_core/amount/money.dart'; import 'package:path/path.dart' as p; import 'package:cw_core/exceptions.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_decred/amount_format.dart'; import 'package:cw_decred/pending_transaction.dart'; import 'package:cw_decred/transaction_credentials.dart'; import 'package:flutter/foundation.dart'; @@ -121,12 +121,7 @@ abstract class DecredWalletBase @override bool isTestnet; - String get pubkey { - return _pubkey; - } - - @override - String formatCryptoAmount(String amount) => decredAmountToString(amount: int.parse(amount)); + String get pubkey => _pubkey; Future init() async { final getSeed = () async { @@ -354,13 +349,12 @@ abstract class DecredWalletBase Future createTransaction(Object credentials) async { if (watchingOnly) { return DecredPendingTransaction( - txid: "", - amount: 0, - fee: 0, - rawHex: "", - send: () async { - throw "unable to send with watching only wallet"; - }); + txId: "", + amount: Money.zero(currency), + fee: Money.zero(currency), + rawHex: "", + send: () async => throw "unable to send with watching only wallet", + ); } var totalIn = 0; final ignoreInputs = []; @@ -380,14 +374,12 @@ abstract class DecredWalletBase for (final out in creds.outputs) { var amt = 0; if (out.sendAll) { - if (creds.outputs.length != 1) { - throw "can only send all to one output"; - } + if (creds.outputs.length != 1) throw "can only send all to one output"; + sendAll = true; totalAmt = totalIn; - } else if (out.cryptoAmount != null) { - final coins = double.parse(out.cryptoAmount!); - amt = (coins * 1e8).round(); + } else { + amt = out.cryptoAmount.amount.toInt(); } totalAmt += amt; final o = { @@ -416,6 +408,7 @@ abstract class DecredWalletBase final res = await _libwallet.createSignedTransaction(walletInfo.name, jsonEncode(signReq)); final decoded = json.decode(res); final signedHex = decoded["signedhex"]; + final send = () async { await _libwallet.sendRawTransaction(walletInfo.name, signedHex); await updateBalance(); @@ -424,8 +417,14 @@ abstract class DecredWalletBase if (sendAll) { totalAmt = (totalAmt - fee).round(); } + return DecredPendingTransaction( - txid: decoded["txid"] ?? "", amount: totalAmt, fee: fee, rawHex: signedHex, send: send); + txId: decoded["txid"] ?? "", + amount: Money.fromInt(totalAmt, currency), + fee: Money.fromInt(fee, currency), + rawHex: signedHex, + send: send, + ); } int feeRate(TransactionPriority priority) { @@ -477,15 +476,14 @@ abstract class DecredWalletBase } @override - Future> fetchTransactions() async { - return this.fetchFiveTransactions(0); - } + Future> fetchTransactions() => fetchFiveTransactions(0); Future> fetchFiveTransactions(int from) async { try { final res = await _libwallet.listTransactions(walletInfo.name, from.toString(), "5"); final decoded = json.decode(res); - var txs = {}; + final txs = {}; + for (final d in decoded) { final txid = uniqueTxID(d["txid"] ?? "", d["vout"] ?? 0); var direction = TransactionDirection.outgoing; @@ -498,19 +496,18 @@ abstract class DecredWalletBase final fee = (feeDouble * 1e8).round().abs(); final confs = d["confirmations"] ?? 0; final sendTime = d["time"] ?? 0; - final height = d["height"] ?? 0; - final txInfo = DecredTransactionInfo( + + txs[txid] = DecredTransactionInfo( id: txid, - amount: amount, - fee: fee, + amount: Money.fromInt(amount, currency), + fee: Money.fromInt(fee, currency), direction: direction, isPending: confs == 0, date: DateTime.fromMillisecondsSinceEpoch(sendTime * 1000, isUtc: false), - height: height, + height: d["height"] ?? 0, confirmations: confs, to: d["address"] ?? "", ); - txs[txid] = txInfo; } return txs; } catch (e) { @@ -520,9 +517,7 @@ abstract class DecredWalletBase } // uniqueTxID combines the tx id and vout to create a unique id. - String uniqueTxID(String id, int vout) { - return id + ":" + vout.toString(); - } + String uniqueTxID(String id, int vout) => "$id:$vout"; @override Future save() async {} @@ -587,9 +582,9 @@ abstract class DecredWalletBase }); balance[CryptoCurrency.dcr] = DecredBalance( - confirmed: balanceMap["confirmed"] ?? 0, - unconfirmed: balanceMap["unconfirmed"] ?? 0, - frozen: totalFrozen, + confirmed: Money.fromInt(balanceMap["confirmed"] ?? 0, currency), + unconfirmed: Money.fromInt(balanceMap["unconfirmed"] ?? 0, currency), + frozen: Money.fromInt(totalFrozen, currency), ); } @@ -648,7 +643,7 @@ abstract class DecredWalletBase try { final res = await _libwallet.listUnspents(walletInfo.name); final decoded = json.decode(res); - var unspents = []; + final unspents = []; for (final d in decoded) { final spendable = d["spendable"] ?? false; if (!spendable) { diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart index afcf6d5db..44e6d6886 100644 --- a/cw_decred/lib/wallet_service.dart +++ b/cw_decred/lib/wallet_service.dart @@ -10,7 +10,6 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:path/path.dart'; import 'package:hive/hive.dart'; -import 'package:collection/collection.dart'; import 'package:cw_core/unspent_coins_info.dart'; class DecredWalletService extends WalletService< diff --git a/cw_dogecoin/lib/src/dogecoin_wallet.dart b/cw_dogecoin/lib/src/dogecoin_wallet.dart index c9db30fa7..610cd592e 100644 --- a/cw_dogecoin/lib/src/dogecoin_wallet.dart +++ b/cw_dogecoin/lib/src/dogecoin_wallet.dart @@ -68,7 +68,7 @@ abstract class DogeCoinWalletBase extends ElectrumWallet with Store { } @override - int get networkDustAmount => 100000000; // 1 DOGE = 1e8 koinu + BigInt get networkDustAmount => BigInt.from(100000000); // 1 DOGE = 1e8 koinu static int estimatedDogeCoinTransactionSize(int inputsCount, int outputsCounts) => inputsCount * 180 + outputsCounts * 34 + 10; diff --git a/cw_evm/lib/clients/evm_chain_client.dart b/cw_evm/lib/clients/evm_chain_client.dart index 48e1f5fa7..2751e94bb 100644 --- a/cw_evm/lib/clients/evm_chain_client.dart +++ b/cw_evm/lib/clients/evm_chain_client.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/node.dart'; @@ -298,14 +299,13 @@ class EVMChainClient { Future signTransaction({ required Credentials privateKey, required String toAddress, - required BigInt amount, - required BigInt gasFee, + required Money amount, + required Money gasFee, required int estimatedGasUnits, required int maxFeePerGas, required EVMChainTransactionPriority? priority, required CryptoCurrency currency, required String feeCurrency, - required int exponent, String? contractAddress, String? data, int? gasPrice, @@ -318,11 +318,13 @@ class EVMChainClient { currency == CryptoCurrency.bnb || contractAddress != null); - bool isNativeToken = currency == CryptoCurrency.eth || - currency == CryptoCurrency.maticpoly || - currency == CryptoCurrency.baseEth || - currency == CryptoCurrency.arbEth || - currency == CryptoCurrency.bnb; + final isNativeToken = [ + CryptoCurrency.eth, + CryptoCurrency.maticpoly, + CryptoCurrency.baseEth, + CryptoCurrency.arbEth, + CryptoCurrency.bnb + ].contains(currency); // Get nonce with "pending" block tag to include pending transactions // This prevents "Nonce too low" errors when sending multiple transactions quickly @@ -336,7 +338,7 @@ class EVMChainClient { to: EthereumAddress.fromHex(toAddress), maxPriorityFeePerGas: priority != null ? EtherAmount.fromInt(EtherUnit.gwei, priority.tip) : null, - amount: isNativeToken ? EtherAmount.inWei(amount) : EtherAmount.zero(), + amount: isNativeToken ? EtherAmount.inWei(amount.amount) : EtherAmount.zero(), data: data != null ? hexToBytes(data) : null, maxGas: estimatedGasUnits, maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), @@ -359,7 +361,7 @@ class EVMChainClient { signedTransaction = await erc20.transfer( EthereumAddress.fromHex(toAddress), - amount, + amount.amount, credentials: privateKey, transaction: transaction, ); @@ -370,24 +372,20 @@ class EVMChainClient { return PendingEVMChainTransaction( signedTransaction: prepareSignedTransactionForSending(signedTransaction), - amount: amount.toString(), + amount: amount, fee: gasFee, - feeCurrency: feeCurrency, sendTransaction: _sendTransaction, - exponent: exponent, ); } Future signApprovalTransaction({ required Credentials privateKey, required String spender, - required BigInt amount, - required BigInt gasFee, - required String feeCurrency, + required Money amount, + required Money gasFee, required int estimatedGasUnits, required int maxFeePerGas, required EVMChainTransactionPriority? priority, - required int exponent, required String contractAddress, int? gasPrice, bool useBlinkProtection = true, @@ -417,20 +415,18 @@ class EVMChainClient { final signedTransaction = await erc20.approve( EthereumAddress.fromHex(spender), - amount, + amount.amount, credentials: privateKey, transaction: transaction, ); return PendingEVMChainTransaction( signedTransaction: prepareSignedTransactionForSending(signedTransaction), - amount: amount.toString(), + amount: amount, fee: gasFee, - feeCurrency: feeCurrency, sendTransaction: () => sendTransaction(signedTransaction, useBlinkProtection: useBlinkProtection), - exponent: exponent, - isInfiniteApproval: amount.toRadixString(16) == + isInfiniteApproval: amount.amount.toRadixString(16) == 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', ); } @@ -524,20 +520,17 @@ class EVMChainClient { */ } - Future fetchERC20Balances( - EthereumAddress userAddress, String contractAddress) async { + Future fetchERC20Balances(EthereumAddress userAddress, Erc20Token token) async { try { - final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final erc20 = ERC20(address: EthereumAddress.fromHex(token.contractAddress), client: _client!); final balance = await erc20.balanceOf(userAddress); - int exponent = (await erc20.decimals()).toInt(); - - return EVMChainERC20Balance(balance, exponent: exponent); + return EVMChainERC20Balance(Money(balance, token)); } on RangeError catch (_) { throw Exception('Invalid token contract for this network.'); } catch (e) { if (e.toString().contains("hostUnreachable")) { - return EVMChainERC20Balance(BigInt.zero); + return EVMChainERC20Balance(Money.zero(token)); } throw Exception('Could not fetch balances: ${e.toString()}'); } diff --git a/cw_evm/lib/deuro/deuro_savings.dart b/cw_evm/lib/deuro/deuro_savings.dart index e35020c72..478439f43 100644 --- a/cw_evm/lib/deuro/deuro_savings.dart +++ b/cw_evm/lib/deuro/deuro_savings.dart @@ -1,5 +1,7 @@ +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_evm/deuro/deuro_savings_contract.dart'; +import 'package:cw_evm/deuro/deuro_savings_gateway_contract.dart' as v1; +import 'package:cw_evm/deuro/deuro_savings_contract.dart' as v2; import 'package:cw_evm/evm_chain_wallet.dart'; import 'package:cw_evm/contract/erc20.dart'; import 'package:cw_evm/evm_chain_exceptions.dart'; @@ -9,21 +11,27 @@ import 'package:web3dart/crypto.dart'; import 'package:web3dart/web3dart.dart'; const String savingsGatewayAddress = "0x073493d73258C4BEb6542e8dd3e1b2891C972303"; +const String savingsV2Address = "0x760233b90e45d186A9A98E911B115F7F4B90d3D9"; const String dEuroAddress = "0xbA3f535bbCcCcA2A154b573Ca6c5A49BAAE0a3ea"; const String frontendCode = "0x00000000000000000000000000000000000000000043616b652057616c6c6574"; class DEuro { - final SavingsGateway _savingsGateway; + final v2.Savings _savings; final ERC20 _dEuro; final EVMChainWallet _wallet; DEuro(EVMChainWallet wallet) : _wallet = wallet, - _savingsGateway = _getSavingsGateway(wallet.getWeb3Client()!), + _savings = _getSavings(wallet.getWeb3Client()!), _dEuro = _getDEuroToken(wallet.getWeb3Client()!); - static SavingsGateway _getSavingsGateway(Web3Client client) => SavingsGateway( + static v2.Savings _getSavings(Web3Client client) => v2.Savings( + address: EthereumAddress.fromHex(savingsV2Address), + client: client, + ); + + static v1.SavingsGateway _getSavingsGateway(Web3Client client) => v1.SavingsGateway( address: EthereumAddress.fromHex(savingsGatewayAddress), client: client, ); @@ -35,26 +43,31 @@ class DEuro { EthereumAddress get _address => EthereumAddress.fromHex(_wallet.walletAddresses.primaryAddress); - Future get savingsBalance async => - (await _savingsGateway.savings(accountOwner: _address)).saved; + Future get savingsBalance async => + Money((await _savings.savings(accountOwner: _address)).saved, CryptoCurrency.deuro); - Future get accruedInterest => _savingsGateway.accruedInterest(accountOwner: _address); + Future get savingsBalanceV1 async => Money( + (await _getSavingsGateway(_wallet.getWeb3Client()!).savings(accountOwner: _address)).saved, + CryptoCurrency.deuro); - Future get interestRate => _savingsGateway.currentRatePPM(); + Future get accruedInterest async => + Money(await _savings.accruedInterest(accountOwner: _address), CryptoCurrency.deuro); - Future get approvedBalance => _dEuro.allowance(_address, _savingsGateway.self.address); + Future get interestRate => _savings.currentRatePPM(); + + Future get approvedBalance => _dEuro.allowance(_address, _savings.self.address); Future _checkEthBalanceForGasFees(EVMChainTransactionPriority priority) async { final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address); final currentBalance = ethBalance.getInWei; + final savings = _getSavingsGateway(_wallet.getWeb3Client()!); final gasFeesModel = await _wallet.calculateActualEstimatedFeeForCreateTransaction( - amount: BigInt.zero, - contractAddress: _savingsGateway.self.address.hexEip55, - receivingAddressHex: _savingsGateway.self.address.hexEip55, + amount: Money.zero(_wallet.currency), + contractAddress: savings.self.address.hexEip55, + receivingAddressHex: savings.self.address.hexEip55, priority: priority, - data: _savingsGateway.self.abi.functions[17] - .encodeCall([BigInt.zero, hexToBytes(frontendCode)]), + data: savings.self.abi.functions[17].encodeCall([BigInt.zero, hexToBytes(frontendCode)]), ); final estimatedGasFee = BigInt.from(gasFeesModel.estimatedGasFee); @@ -73,28 +86,26 @@ class DEuro { try { await _checkEthBalanceForGasFees(priority); - final signedTransaction = await _savingsGateway.save( - (amount: amount, frontendCode: hexToBytes(frontendCode)), + final signedTransaction = await _savings.save( + (amount: amount, compound: true), credentials: _wallet.evmChainPrivateKey, ); final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction( - amount: amount, - contractAddress: _savingsGateway.self.address.hexEip55, - receivingAddressHex: _savingsGateway.self.address.hexEip55, + amount: Money.zero(_wallet.currency), + contractAddress: _savings.self.address.hexEip55, + receivingAddressHex: _savings.self.address.hexEip55, priority: priority, - data: _savingsGateway.self.abi.functions[17].encodeCall([amount, hexToBytes(frontendCode)]), + data: _savings.self.abi.functions[18].encodeCall([amount, true]), ); - final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); + sendTransaction() => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); return PendingEVMChainTransaction( - feeCurrency: "ETH", sendTransaction: sendTransaction, signedTransaction: signedTransaction, - fee: BigInt.from(fee.estimatedGasFee), - amount: amount.toString(), - exponent: 18, + fee: Money(BigInt.from(fee.estimatedGasFee), CryptoCurrency.eth), + amount: Money(amount, CryptoCurrency.deuro), ); } catch (e) { if (e.toString().contains('insufficient funds for gas')) { @@ -110,28 +121,65 @@ class DEuro { try { await _checkEthBalanceForGasFees(priority); - final signedTransaction = await _savingsGateway.withdraw( + final signedTransaction = await _savings.withdraw( + (target: _address, amount: amount), + credentials: _wallet.evmChainPrivateKey, + ); + + final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction( + amount: Money.zero(_wallet.currency), + contractAddress: _savings.self.address.hexEip55, + receivingAddressHex: _savings.self.address.hexEip55, + priority: priority, + data: _savings.self.abi.functions[23].encodeCall([_address, amount]), + ); + + sendTransaction() => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); + + return PendingEVMChainTransaction( + sendTransaction: sendTransaction, + signedTransaction: signedTransaction, + fee: Money(BigInt.from(fee.estimatedGasFee), CryptoCurrency.eth), + amount: Money(amount, CryptoCurrency.deuro), + ); + } catch (e) { + if (e.toString().contains('insufficient funds for gas')) { + final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address); + throw InsufficientGasFeeException(currentBalance: ethBalance.getInWei); + } + rethrow; + } + } + + Future withdrawSavingsV1(EVMChainTransactionPriority priority) async { + try { + await _checkEthBalanceForGasFees(priority); + final withdrawAmount = await savingsBalanceV1; + + // Withdraw at least a million to overflow and close the savings position + final amount = BigInt.parse("1000000000000000000000000"); + final savings = _getSavingsGateway(_wallet.getWeb3Client()!); + final signedTransaction = await savings.withdraw( (target: _address, amount: amount, frontendCode: hexToBytes(frontendCode)), credentials: _wallet.evmChainPrivateKey, ); final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction( - amount: amount, - contractAddress: _savingsGateway.self.address.hexEip55, - receivingAddressHex: _savingsGateway.self.address.hexEip55, + amount: Money.zero(_wallet.currency), + contractAddress: savings.self.address.hexEip55, + receivingAddressHex: savings.self.address.hexEip55, priority: priority, - data: _savingsGateway.self.abi.functions[24].encodeCall([_address, amount, hexToBytes(frontendCode)]), + data: savings.self.abi.functions[24].encodeCall([_address, amount, hexToBytes(frontendCode)]), ); - final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); + sendTransaction() => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); return PendingEVMChainTransaction( - sendTransaction: sendTransaction, - signedTransaction: signedTransaction, - fee: BigInt.from(fee.estimatedGasFee), - amount: amount.toString(), - feeCurrency: "ETH", - exponent: 18); + sendTransaction: sendTransaction, + signedTransaction: signedTransaction, + fee: Money(BigInt.from(fee.estimatedGasFee), CryptoCurrency.eth), + amount: withdrawAmount, + ); } catch (e) { if (e.toString().contains('insufficient funds for gas')) { final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address); @@ -145,27 +193,25 @@ class DEuro { try { await _checkEthBalanceForGasFees(priority); - final signedTransaction = await _savingsGateway.refreshBalance((owner: _address), + final signedTransaction = await _savings.refreshBalance((owner: _address), credentials: _wallet.evmChainPrivateKey, ); final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction( - amount: BigInt.zero, - contractAddress: _savingsGateway.self.address.hexEip55, - receivingAddressHex: _savingsGateway.self.address.hexEip55, + amount: Money.zero(_wallet.currency), + contractAddress: _savings.self.address.hexEip55, + receivingAddressHex: _savings.self.address.hexEip55, priority: priority, - data: _savingsGateway.self.abi.functions[15].encodeCall([_address]), + data: _savings.self.abi.functions[16].encodeCall([_address]), ); - final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); + sendTransaction() => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); return PendingEVMChainTransaction( - feeCurrency: "ETH", sendTransaction: sendTransaction, signedTransaction: signedTransaction, - fee: BigInt.from(fee.estimatedGasFee), - amount: 0.toString(), - exponent: 18, + fee: Money(BigInt.from(fee.estimatedGasFee), CryptoCurrency.eth), + amount: Money.zero(CryptoCurrency.deuro), ); } catch (e) { if (e.toString().contains('insufficient funds for gas')) { @@ -182,14 +228,12 @@ class DEuro { await _checkEthBalanceForGasFees(priority); return (await _wallet.createApprovalTransaction( - BigInt.parse( + Money(BigInt.parse( 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', radix: 16, - ), - _savingsGateway.self.address.hexEip55, - CryptoCurrency.deuro, + ), CryptoCurrency.deuro), + _savings.self.address.hexEip55, priority, - "ETH", )) as PendingEVMChainTransaction; } catch (e) { if (e.toString().contains('insufficient funds for gas')) { diff --git a/cw_evm/lib/deuro/deuro_savings_contract.dart b/cw_evm/lib/deuro/deuro_savings_contract.dart index 77316f456..9a8ca5c1b 100644 --- a/cw_evm/lib/deuro/deuro_savings_contract.dart +++ b/cw_evm/lib/deuro/deuro_savings_contract.dart @@ -1,51 +1,38 @@ +// @dart=3.0 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint // 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 // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:web3dart/web3dart.dart' as _i1; import 'dart:typed_data' as _i2; +import 'package:web3dart/web3dart.dart' as _i1; + final _contractAbi = _i1.ContractAbi.fromJson( - '[{"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"}]', - 'SavingsGateway', + '[{"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"}]', + 'Savings', ); -class SavingsGateway extends _i1.GeneratedContract { - SavingsGateway({ +class Savings extends _i1.GeneratedContract { + Savings({ required _i1.EthereumAddress address, required _i1.Web3Client client, int? chainId, }) : super( - _i1.DeployedContract( - _contractAbi, - address, - ), - client, - chainId, - ); + _i1.DeployedContract( + _contractAbi, + address, + ), + client, + chainId, + ); /// The optional [atBlock] parameter can be used to view historical data. When /// set, the function will be evaluated in the specified block. By default, the /// latest on-chain block will be used. - Future<_i1.EthereumAddress> GATEWAY({_i1.BlockNum? atBlock}) async { + Future accruedInterest( + {required _i1.EthereumAddress accountOwner, _i1.BlockNum? atBlock}) async { final function = self.abi.functions[1]; - assert(checkSignature(function, '338c5371')); - final params = []; - final response = await read( - function, - params, - atBlock, - ); - return (response[0] as _i1.EthereumAddress); - } - - /// The optional [atBlock] parameter can be used to view historical data. When - /// set, the function will be evaluated in the specified block. By default, the - /// latest on-chain block will be used. - Future accruedInterest({ - required _i1.EthereumAddress accountOwner, - _i1.BlockNum? atBlock, - }) async { - final function = self.abi.functions[2]; assert(checkSignature(function, '77267ec3')); final params = [accountOwner]; final response = await read( @@ -60,10 +47,10 @@ class SavingsGateway extends _i1.GeneratedContract { /// set, the function will be evaluated in the specified block. By default, the /// latest on-chain block will be used. Future accruedInterest$2( - ({_i1.EthereumAddress accountOwner, BigInt timestamp}) args, { - _i1.BlockNum? atBlock, - }) async { - final function = self.abi.functions[3]; + ({_i1.EthereumAddress accountOwner, BigInt timestamp}) args, { + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[2]; assert(checkSignature(function, 'a696399d')); final params = [ args.accountOwner, @@ -81,16 +68,31 @@ class SavingsGateway extends _i1.GeneratedContract { /// like the gas price, nonce and max gas. The `data` and `to` fields will be /// set by the contract. Future adjust( - ({BigInt targetAmount, _i2.Uint8List frontendCode}) args, { - required _i1.Credentials credentials, - _i1.Transaction? transaction, - }) async { + ({BigInt targetAmount}) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[3]; + assert(checkSignature(function, 'bdc65152')); + final params = [args.targetAmount]; + return write( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future applyChange({ + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { final function = self.abi.functions[4]; - assert(checkSignature(function, '753ef93c')); - final params = [ - args.targetAmount, - args.frontendCode, - ]; + assert(checkSignature(function, '4e65037e')); + final params = []; return write( credentials, transaction, @@ -103,10 +105,10 @@ class SavingsGateway extends _i1.GeneratedContract { /// set, the function will be evaluated in the specified block. By default, the /// latest on-chain block will be used. Future calculateInterest( - ({dynamic account, BigInt ticks}) args, { - _i1.BlockNum? atBlock, - }) async { - final function = self.abi.functions[7]; + ({dynamic account, BigInt ticks}) args, { + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[5]; assert(checkSignature(function, '7915ce20')); final params = [ args.account, @@ -120,6 +122,43 @@ class SavingsGateway extends _i1.GeneratedContract { return (response[0] as BigInt); } + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future claimInterest( + ({_i1.EthereumAddress target}) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[6]; + assert(checkSignature(function, '88d742c2')); + final params = [args.target]; + return write( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future claimableInterest( + ({_i1.EthereumAddress $param7}) args, { + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[7]; + assert(checkSignature(function, '4f24d20a')); + final params = [args.$param7]; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + /// The optional [atBlock] parameter can be used to view historical data. When /// set, the function will be evaluated in the specified block. By default, the /// latest on-chain block will be used. @@ -210,15 +249,55 @@ class SavingsGateway extends _i1.GeneratedContract { return (response[0] as BigInt); } + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future nonCompounding( + ({_i1.EthereumAddress $param8}) args, { + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[14]; + assert(checkSignature(function, 'cdb95c3b')); + final params = [args.$param8]; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as bool); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future proposeChange( + ({BigInt newRatePPM_, List<_i1.EthereumAddress> helpers}) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[15]; + assert(checkSignature(function, '1de252e4')); + final params = [ + args.newRatePPM_, + args.helpers, + ]; + return write( + credentials, + transaction, + function, + params, + ); + } + /// The optional [transaction] parameter can be used to override parameters /// like the gas price, nonce and max gas. The `data` and `to` fields will be /// set by the contract. Future<_i2.Uint8List> refreshBalance( - ({_i1.EthereumAddress owner}) args, { - required _i1.Credentials credentials, - _i1.Transaction? transaction, - }) async { - final function = self.abi.functions[15]; + ({_i1.EthereumAddress owner}) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[16]; assert(checkSignature(function, 'b77cd1c7')); final params = [args.owner]; return writeRaw( @@ -236,7 +315,7 @@ class SavingsGateway extends _i1.GeneratedContract { required _i1.Credentials credentials, _i1.Transaction? transaction, }) async { - final function = self.abi.functions[16]; + final function = self.abi.functions[17]; assert(checkSignature(function, '85bd12d1')); final params = []; return writeRaw( @@ -251,42 +330,15 @@ class SavingsGateway extends _i1.GeneratedContract { /// like the gas price, nonce and max gas. The `data` and `to` fields will be /// set by the contract. Future<_i2.Uint8List> save( - ({BigInt amount, _i2.Uint8List frontendCode}) args, { - required _i1.Credentials credentials, - _i1.Transaction? transaction, - }) async { - final function = self.abi.functions[17]; - assert(checkSignature(function, '9e2363dc')); + ({BigInt amount, bool compound}) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[18]; + assert(checkSignature(function, 'a776f78c')); final params = [ args.amount, - args.frontendCode, - ]; - return writeRaw( - credentials, - transaction, - function, - params, - ); - } - - /// The optional [transaction] parameter can be used to override parameters - /// like the gas price, nonce and max gas. The `data` and `to` fields will be - /// set by the contract. - Future<_i2.Uint8List> saveTo( - ({ - _i1.EthereumAddress owner, - BigInt amount, - _i2.Uint8List frontendCode - }) args, { - required _i1.Credentials credentials, - _i1.Transaction? transaction, - }) async { - final function = self.abi.functions[19]; - assert(checkSignature(function, 'cbcf9676')); - final params = [ - args.owner, - args.amount, - args.frontendCode, + args.compound, ]; return writeRaw( credentials, @@ -299,7 +351,7 @@ class SavingsGateway extends _i1.GeneratedContract { /// The optional [atBlock] parameter can be used to view historical data. When /// set, the function will be evaluated in the specified block. By default, the /// latest on-chain block will be used. - Future savings({ + Future savings({ required _i1.EthereumAddress accountOwner, _i1.BlockNum? atBlock, }) async { @@ -311,27 +363,40 @@ class SavingsGateway extends _i1.GeneratedContract { params, atBlock, ); - return Savings(response); + return SavingsData(response); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future ticks( + ({BigInt timestamp}) args, { + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[22]; + assert(checkSignature(function, '534cb30d')); + final params = [args.timestamp]; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); } /// The optional [transaction] parameter can be used to override parameters /// like the gas price, nonce and max gas. The `data` and `to` fields will be /// set by the contract. Future<_i2.Uint8List> withdraw( - ({ - _i1.EthereumAddress target, - BigInt amount, - _i2.Uint8List frontendCode - }) args, { + ({_i1.EthereumAddress target, BigInt amount}) args, { required _i1.Credentials credentials, _i1.Transaction? transaction, }) async { - final function = self.abi.functions[24]; - assert(checkSignature(function, '829a0476')); + final function = self.abi.functions[23]; + assert(checkSignature(function, '1791dc5e')); final params = [ args.target, args.amount, - args.frontendCode, ]; return writeRaw( credentials, @@ -341,6 +406,30 @@ class SavingsGateway extends _i1.GeneratedContract { ); } + /// Returns a live stream of all InterestClaimed events emitted by this contract. + Stream interestClaimedEvents({ + _i1.BlockNum? fromBlock, + _i1.BlockNum? toBlock, + }) { + final event = self.event('InterestClaimed'); + final filter = _i1.FilterOptions.events( + contract: self, + event: event, + fromBlock: fromBlock, + toBlock: toBlock, + ); + return client.events(filter).map((_i1.FilterEvent result) { + final decoded = event.decodeResults( + result.topics!, + result.data!, + ); + return InterestClaimed( + decoded, + result, + ); + }); + } + /// Returns a live stream of all InterestCollected events emitted by this contract. Stream interestCollectedEvents({ _i1.BlockNum? fromBlock, @@ -462,8 +551,8 @@ class SavingsGateway extends _i1.GeneratedContract { } } -class Savings { - Savings(List response) +class SavingsData { + SavingsData(List response) : saved = (response[0] as BigInt), ticks = (response[1] as BigInt); @@ -472,25 +561,42 @@ class Savings { final BigInt ticks; } +class InterestClaimed { + InterestClaimed( + List response, + this.event, + ) : account = (response[0] as _i1.EthereumAddress), + amount = (response[1] as BigInt); + + final _i1.EthereumAddress account; + + final BigInt amount; + + final _i1.FilterEvent event; +} + class InterestCollected { InterestCollected( - List response, - this.event, - ) : account = (response[0] as _i1.EthereumAddress), - interest = (response[1] as BigInt); + List response, + this.event, + ) : account = (response[0] as _i1.EthereumAddress), + interest = (response[1] as BigInt), + compounded = (response[2] as bool); final _i1.EthereumAddress account; final BigInt interest; + final bool compounded; + final _i1.FilterEvent event; } class RateChanged { RateChanged( - List response, - this.event, - ) : newRate = (response[0] as BigInt); + List response, + this.event, + ) : newRate = (response[0] as BigInt); final BigInt newRate; @@ -499,9 +605,9 @@ class RateChanged { class RateProposed { RateProposed( - List response, - this.event, - ) : who = (response[0] as _i1.EthereumAddress), + List response, + this.event, + ) : who = (response[0] as _i1.EthereumAddress), nextRate = (response[1] as BigInt), nextChange = (response[2] as BigInt); @@ -516,9 +622,9 @@ class RateProposed { class Saved { Saved( - List response, - this.event, - ) : account = (response[0] as _i1.EthereumAddress), + List response, + this.event, + ) : account = (response[0] as _i1.EthereumAddress), amount = (response[1] as BigInt); final _i1.EthereumAddress account; @@ -530,9 +636,9 @@ class Saved { class Withdrawn { Withdrawn( - List response, - this.event, - ) : account = (response[0] as _i1.EthereumAddress), + List response, + this.event, + ) : account = (response[0] as _i1.EthereumAddress), amount = (response[1] as BigInt); final _i1.EthereumAddress account; diff --git a/cw_evm/lib/deuro/deuro_savings_gateway_contract.dart b/cw_evm/lib/deuro/deuro_savings_gateway_contract.dart new file mode 100644 index 000000000..77316f456 --- /dev/null +++ b/cw_evm/lib/deuro/deuro_savings_gateway_contract.dart @@ -0,0 +1,543 @@ +// ignore_for_file: type=lint +// 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 +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:web3dart/web3dart.dart' as _i1; +import 'dart:typed_data' as _i2; + +final _contractAbi = _i1.ContractAbi.fromJson( + '[{"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"}]', + 'SavingsGateway', +); + +class SavingsGateway extends _i1.GeneratedContract { + SavingsGateway({ + required _i1.EthereumAddress address, + required _i1.Web3Client client, + int? chainId, + }) : super( + _i1.DeployedContract( + _contractAbi, + address, + ), + client, + chainId, + ); + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future<_i1.EthereumAddress> GATEWAY({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[1]; + assert(checkSignature(function, '338c5371')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as _i1.EthereumAddress); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future accruedInterest({ + required _i1.EthereumAddress accountOwner, + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[2]; + assert(checkSignature(function, '77267ec3')); + final params = [accountOwner]; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future accruedInterest$2( + ({_i1.EthereumAddress accountOwner, BigInt timestamp}) args, { + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[3]; + assert(checkSignature(function, 'a696399d')); + final params = [ + args.accountOwner, + args.timestamp, + ]; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future adjust( + ({BigInt targetAmount, _i2.Uint8List frontendCode}) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[4]; + assert(checkSignature(function, '753ef93c')); + final params = [ + args.targetAmount, + args.frontendCode, + ]; + return write( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future calculateInterest( + ({dynamic account, BigInt ticks}) args, { + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[7]; + assert(checkSignature(function, '7915ce20')); + final params = [ + args.account, + args.ticks, + ]; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future currentRatePPM({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[8]; + assert(checkSignature(function, '06a7b376')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future currentTicks({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[9]; + assert(checkSignature(function, 'b079f163')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future<_i1.EthereumAddress> deuro({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[10]; + assert(checkSignature(function, '82b8eaf5')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as _i1.EthereumAddress); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future<_i1.EthereumAddress> equity({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[11]; + assert(checkSignature(function, '91a0ac6a')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as _i1.EthereumAddress); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future nextChange({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[12]; + assert(checkSignature(function, 'b6f83c17')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future nextRatePPM({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[13]; + assert(checkSignature(function, '2e4b20ab')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future<_i2.Uint8List> refreshBalance( + ({_i1.EthereumAddress owner}) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[15]; + assert(checkSignature(function, 'b77cd1c7')); + final params = [args.owner]; + return writeRaw( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future<_i2.Uint8List> refreshMyBalance({ + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[16]; + assert(checkSignature(function, '85bd12d1')); + final params = []; + return writeRaw( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future<_i2.Uint8List> save( + ({BigInt amount, _i2.Uint8List frontendCode}) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[17]; + assert(checkSignature(function, '9e2363dc')); + final params = [ + args.amount, + args.frontendCode, + ]; + return writeRaw( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future<_i2.Uint8List> saveTo( + ({ + _i1.EthereumAddress owner, + BigInt amount, + _i2.Uint8List frontendCode + }) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[19]; + assert(checkSignature(function, 'cbcf9676')); + final params = [ + args.owner, + args.amount, + args.frontendCode, + ]; + return writeRaw( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future savings({ + required _i1.EthereumAddress accountOwner, + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[21]; + assert(checkSignature(function, '1f7cdd5f')); + final params = [accountOwner]; + final response = await read( + function, + params, + atBlock, + ); + return Savings(response); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future<_i2.Uint8List> withdraw( + ({ + _i1.EthereumAddress target, + BigInt amount, + _i2.Uint8List frontendCode + }) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[24]; + assert(checkSignature(function, '829a0476')); + final params = [ + args.target, + args.amount, + args.frontendCode, + ]; + return writeRaw( + credentials, + transaction, + function, + params, + ); + } + + /// Returns a live stream of all InterestCollected events emitted by this contract. + Stream interestCollectedEvents({ + _i1.BlockNum? fromBlock, + _i1.BlockNum? toBlock, + }) { + final event = self.event('InterestCollected'); + final filter = _i1.FilterOptions.events( + contract: self, + event: event, + fromBlock: fromBlock, + toBlock: toBlock, + ); + return client.events(filter).map((_i1.FilterEvent result) { + final decoded = event.decodeResults( + result.topics!, + result.data!, + ); + return InterestCollected( + decoded, + result, + ); + }); + } + + /// Returns a live stream of all RateChanged events emitted by this contract. + Stream rateChangedEvents({ + _i1.BlockNum? fromBlock, + _i1.BlockNum? toBlock, + }) { + final event = self.event('RateChanged'); + final filter = _i1.FilterOptions.events( + contract: self, + event: event, + fromBlock: fromBlock, + toBlock: toBlock, + ); + return client.events(filter).map((_i1.FilterEvent result) { + final decoded = event.decodeResults( + result.topics!, + result.data!, + ); + return RateChanged( + decoded, + result, + ); + }); + } + + /// Returns a live stream of all RateProposed events emitted by this contract. + Stream rateProposedEvents({ + _i1.BlockNum? fromBlock, + _i1.BlockNum? toBlock, + }) { + final event = self.event('RateProposed'); + final filter = _i1.FilterOptions.events( + contract: self, + event: event, + fromBlock: fromBlock, + toBlock: toBlock, + ); + return client.events(filter).map((_i1.FilterEvent result) { + final decoded = event.decodeResults( + result.topics!, + result.data!, + ); + return RateProposed( + decoded, + result, + ); + }); + } + + /// Returns a live stream of all Saved events emitted by this contract. + Stream savedEvents({ + _i1.BlockNum? fromBlock, + _i1.BlockNum? toBlock, + }) { + final event = self.event('Saved'); + final filter = _i1.FilterOptions.events( + contract: self, + event: event, + fromBlock: fromBlock, + toBlock: toBlock, + ); + return client.events(filter).map((_i1.FilterEvent result) { + final decoded = event.decodeResults( + result.topics!, + result.data!, + ); + return Saved( + decoded, + result, + ); + }); + } + + /// Returns a live stream of all Withdrawn events emitted by this contract. + Stream withdrawnEvents({ + _i1.BlockNum? fromBlock, + _i1.BlockNum? toBlock, + }) { + final event = self.event('Withdrawn'); + final filter = _i1.FilterOptions.events( + contract: self, + event: event, + fromBlock: fromBlock, + toBlock: toBlock, + ); + return client.events(filter).map((_i1.FilterEvent result) { + final decoded = event.decodeResults( + result.topics!, + result.data!, + ); + return Withdrawn( + decoded, + result, + ); + }); + } +} + +class Savings { + Savings(List response) + : saved = (response[0] as BigInt), + ticks = (response[1] as BigInt); + + final BigInt saved; + + final BigInt ticks; +} + +class InterestCollected { + InterestCollected( + List response, + this.event, + ) : account = (response[0] as _i1.EthereumAddress), + interest = (response[1] as BigInt); + + final _i1.EthereumAddress account; + + final BigInt interest; + + final _i1.FilterEvent event; +} + +class RateChanged { + RateChanged( + List response, + this.event, + ) : newRate = (response[0] as BigInt); + + final BigInt newRate; + + final _i1.FilterEvent event; +} + +class RateProposed { + RateProposed( + List response, + this.event, + ) : who = (response[0] as _i1.EthereumAddress), + nextRate = (response[1] as BigInt), + nextChange = (response[2] as BigInt); + + final _i1.EthereumAddress who; + + final BigInt nextRate; + + final BigInt nextChange; + + final _i1.FilterEvent event; +} + +class Saved { + Saved( + List response, + this.event, + ) : account = (response[0] as _i1.EthereumAddress), + amount = (response[1] as BigInt); + + final _i1.EthereumAddress account; + + final BigInt amount; + + final _i1.FilterEvent event; +} + +class Withdrawn { + Withdrawn( + List response, + this.event, + ) : account = (response[0] as _i1.EthereumAddress), + amount = (response[1] as BigInt); + + final _i1.EthereumAddress account; + + final BigInt amount; + + final _i1.FilterEvent event; +} diff --git a/cw_evm/lib/evm_chain_transaction_info.dart b/cw_evm/lib/evm_chain_transaction_info.dart index 128c00eab..6ce7a716c 100644 --- a/cw_evm/lib/evm_chain_transaction_info.dart +++ b/cw_evm/lib/evm_chain_transaction_info.dart @@ -1,18 +1,19 @@ // ignore_for_file: overridden_fields, annotate_overrides -import 'dart:math'; - +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/format_amount.dart'; -import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; +import 'package:cw_evm/evm_chain_registry.dart'; import 'package:cw_evm/utils/evm_chain_utils.dart'; class EVMChainTransactionInfo extends TransactionInfo { EVMChainTransactionInfo({ required this.id, required this.height, - required this.ethAmount, - required this.ethFee, + required this.amount, + required this.fee, required this.tokenSymbol, this.exponent = 18, required this.direction, @@ -24,19 +25,16 @@ class EVMChainTransactionInfo extends TransactionInfo { this.evmSignatureName, this.contractAddress, required this.chainId, - }) : amount = ethAmount.toInt(), - fee = ethFee.toInt(); + }); final String id; final int height; - final int amount; - final BigInt ethAmount; + final Money amount; final int exponent; final TransactionDirection direction; final DateTime date; final bool isPending; - final int fee; - final BigInt ethFee; + final Money fee; final int confirmations; final String tokenSymbol; String? _fiatAmount; @@ -49,39 +47,32 @@ class EVMChainTransactionInfo extends TransactionInfo { /// Get fee currency symbol based on wallet type String get feeCurrency => EVMChainUtils.getFeeCurrency(chainId); - String get _displayTokenSymbol { - if (chainId == 56 && tokenSymbol.toUpperCase() == 'BSC-USD') return 'USDT'; - - return tokenSymbol; - } - - @override - String amountFormatted() { - final amount = formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString()); - return '${amount.substring(0, min(10, amount.length))} $_displayTokenSymbol'; - } - @override String fiatAmount() => _fiatAmount ?? ''; @override void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); - @override - String feeFormatted() => '${formatFixed(ethFee, 18)} $feeCurrency'; - factory EVMChainTransactionInfo.fromJson(Map data, int chainId) { + final decimals = data['exponent'] as int? ?? 18; + final tokenSymbol = data['tokenSymbol'] as String; + final currency = + Erc20Token(name: '', symbol: tokenSymbol, contractAddress: '', decimal: decimals); + + final feeCurrency = + EvmChainRegistry().getChainConfig(chainId)?.nativeCurrency ?? CryptoCurrency.eth; + return EVMChainTransactionInfo( id: data['id'] as String, height: data['height'] as int, - ethAmount: BigInt.parse(data['amount'] as String), - exponent: data['exponent'] as int? ?? 18, - ethFee: BigInt.parse(data['fee'] as String), + amount: Money(BigInt.parse(data['amount'] as String), currency), + exponent: decimals, + fee: Money(BigInt.parse(data['fee'] as String), feeCurrency), direction: TransactionDirection.values[data['direction'] as int], date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), isPending: data['isPending'] as bool? ?? false, confirmations: data['confirmations'] as int, - tokenSymbol: data['tokenSymbol'] as String, + tokenSymbol: tokenSymbol, to: data['to'] as String?, from: data['from'] as String?, evmSignatureName: data['evmSignatureName'] as String?, @@ -93,9 +84,9 @@ class EVMChainTransactionInfo extends TransactionInfo { Map toJson() => { 'id': id, 'height': height, - 'amount': ethAmount.toString(), + 'amount': amount.amount.toString(), 'exponent': exponent, - 'fee': ethFee.toString(), + 'fee': fee.amount.toString(), 'direction': direction.index, 'date': date.millisecondsSinceEpoch, 'isPending': isPending, diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 66ec7577c..4b002e396 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; @@ -28,7 +29,6 @@ import 'package:cw_evm/evm_chain_default_tokens.dart'; import 'package:cw_evm/evm_chain_exceptions.dart'; import 'package:cw_evm/evm_chain_registry.dart'; import 'package:cw_evm/evm_chain_transaction_credentials.dart'; -import 'package:cw_evm/utils/evm_chain_formatter.dart'; import 'package:cw_evm/evm_chain_transaction_history.dart'; import 'package:cw_evm/evm_chain_transaction_model.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; @@ -103,7 +103,7 @@ abstract class EVMChainWalletBase walletInfo, initialChainId ?? _getInitialChainId(walletInfo.type)), balance = ObservableMap.of( { - nativeCurrency: initialBalance ?? EVMChainERC20Balance(BigInt.zero), + nativeCurrency: initialBalance ?? EVMChainERC20Balance(Money.zero(nativeCurrency)), }, ), super(walletInfo, derivationInfo) { @@ -411,20 +411,29 @@ abstract class EVMChainWalletBase EVMChainTransactionModel transactionModel, String address, ) { + final decimals = transactionModel.tokenDecimal ?? 18; + final tokenSymbol = transactionModel.tokenSymbol ?? + EVMChainUtils.getDefaultTokenSymbol(transactionModel.chainId); + + final amountCurrency = Erc20Token( + name: '', + contractAddress: transactionModel.contractAddress, + decimal: decimals, + symbol: tokenSymbol, + ); return EVMChainTransactionInfo( id: transactionModel.hash, height: transactionModel.blockNumber, - ethAmount: transactionModel.amount, + amount: Money(transactionModel.amount, amountCurrency), direction: transactionModel.from == address ? TransactionDirection.outgoing : TransactionDirection.incoming, isPending: false, date: transactionModel.date, confirmations: transactionModel.confirmations, - ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice, + fee: Money(BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice, currency), exponent: transactionModel.tokenDecimal ?? 18, - tokenSymbol: transactionModel.tokenSymbol ?? - EVMChainUtils.getDefaultTokenSymbol(transactionModel.chainId), + tokenSymbol: tokenSymbol, to: transactionModel.to, from: transactionModel.from, evmSignatureName: transactionModel.evmSignatureName, @@ -473,11 +482,10 @@ abstract class EVMChainWalletBase @override Future checkNodeHealth() async { try { - // Check native balance await _client.getBalance(_evmChainPrivateKey.address); - // Check USDC token balance - String usdcContractAddress = _getUSDCContractAddress(); + final usdcContractAddress = Erc20Token( + name: "USDC", symbol: "USDC", contractAddress: _getUSDCContractAddress(), decimal: 6); await _client.fetchERC20Balances(_evmChainPrivateKey.address, usdcContractAddress); @@ -531,7 +539,7 @@ abstract class EVMChainWalletBase // Ensure balance is initialized for current currency (in case currency changed) if (!balance.containsKey(currency)) { - balance[currency] = EVMChainERC20Balance(BigInt.zero); + balance[currency] = EVMChainERC20Balance(Money.zero(currency)); } await save(); @@ -757,7 +765,7 @@ abstract class EVMChainWalletBase /// - The addressHex for the receiving wallet, /// - A contract address which would be essential in determining if to calculate the estimate for ERC20 or native ETH Future calculateActualEstimatedFeeForCreateTransaction({ - required amount, + required Money amount, required String? contractAddress, required String receivingAddressHex, required TransactionPriority? priority, @@ -774,7 +782,6 @@ abstract class EVMChainWalletBase final gasBaseFee = await _client.getGasBaseFee(); final gasPrice = await _client.getGasUnitPrice(); - // Validate that we got valid gas price if (gasPrice <= 0) { printV('Invalid gas price received: $gasPrice'); throw EVMChainTransactionFeesException('Failed to retrieve gas price from node'); @@ -791,7 +798,7 @@ abstract class EVMChainWalletBase final estimatedGas = await _client.getEstimatedGasUnitsForTransaction( contractAddress: contractAddress, senderAddress: _evmChainPrivateKey.address, - value: EtherAmount.fromBigInt(EtherUnit.wei, amount!), + value: EtherAmount.fromBigInt(EtherUnit.wei, amount.amount), gasPrice: EtherAmount.fromInt(EtherUnit.wei, adjustedGasPrice), toAddress: EthereumAddress.fromHex(receivingAddressHex), maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), @@ -932,15 +939,15 @@ abstract class EVMChainWalletBase 'Currency ${_credentials.currency.title} ${_credentials.currency.tag} is not accessible in the wallet, try to enable it first.')); final currencyBalance = balance[transactionCurrency]!; - BigInt totalAmount = BigInt.zero; - BigInt estimatedFeesForTransaction = BigInt.zero; - int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18; - String? contractAddress; - int estimatedGasUnitsForTransaction = 0; - int maxFeePerGasForTransaction = 0; - String toAddress = _credentials.outputs.first.isParsedAddress + final toAddress = _credentials.outputs.first.isParsedAddress ? _credentials.outputs.first.extractedAddress! : _credentials.outputs.first.address; + var totalAmount = Money.zero(transactionCurrency); + var estimatedFeesForTransaction = Money.zero(currency); + var estimatedGasUnitsForTransaction = 0; + var maxFeePerGasForTransaction = 0; + + String? contractAddress; if (transactionCurrency is Erc20Token) { contractAddress = transactionCurrency.contractAddress; @@ -948,23 +955,12 @@ abstract class EVMChainWalletBase // so far this can not be made with Ethereum as Ethereum does not support multiple recipients if (hasMultiDestination) { - if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + if (outputs.any((item) => item.sendAll || item.cryptoAmount.amount <= BigInt.zero)) { throw EVMChainTransactionCreationException(transactionCurrency); } - totalAmount = outputs.fold( - BigInt.zero, - (acc, output) { - if (output.cryptoAmount != null && output.cryptoAmount!.isNotEmpty) { - return acc + - EVMChainFormatter.parseEVMChainAmountToBigInt( - output.cryptoAmount!, - decimals: exponent, - ); - } - return acc; - }, - ); + totalAmount = outputs.fold(Money.zero(transactionCurrency), + (acc, output) => acc + output.cryptoAmount.copyWith(currency: transactionCurrency)); final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction( amount: totalAmount, @@ -973,28 +969,22 @@ abstract class EVMChainWalletBase contractAddress: contractAddress, ); - estimatedFeesForTransaction = BigInt.from(gasFeesModel.estimatedGasFee); + estimatedFeesForTransaction = + estimatedFeesForTransaction.copyWith(amount: BigInt.from(gasFeesModel.estimatedGasFee)); estimatedGasUnitsForTransaction = gasFeesModel.estimatedGasUnits; maxFeePerGasForTransaction = gasFeesModel.maxFeePerGas; - if (currencyBalance.balance < totalAmount) { + if (currencyBalance.available < totalAmount) { throw EVMChainTransactionCreationException(transactionCurrency); } } else { final output = outputs.first; if (!output.sendAll) { - if (output.cryptoAmount != null && output.cryptoAmount!.isNotEmpty) { - totalAmount = EVMChainFormatter.parseEVMChainAmountToBigInt( - output.cryptoAmount!, - decimals: exponent, - ); - } else { - totalAmount = BigInt.zero; - } + totalAmount = output.cryptoAmount.copyWith(currency: transactionCurrency); } if (output.sendAll && transactionCurrency is Erc20Token) { - totalAmount = currencyBalance.balance; + totalAmount = currencyBalance.available; } final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction( @@ -1004,7 +994,8 @@ abstract class EVMChainWalletBase contractAddress: contractAddress, ); - estimatedFeesForTransaction = BigInt.from(gasFeesModel.estimatedGasFee); + estimatedFeesForTransaction = + estimatedFeesForTransaction.copyWith(amount: BigInt.from(gasFeesModel.estimatedGasFee)); estimatedGasUnitsForTransaction = gasFeesModel.estimatedGasUnits; maxFeePerGasForTransaction = gasFeesModel.maxFeePerGas; @@ -1012,30 +1003,30 @@ abstract class EVMChainWalletBase if (selectedChainId == 8453) { // Applying a small buffer to account for gas price fluctuations // 10% or minimum 10,000 wei, whichever is higher - final refinedGasFee = estimatedFeesForTransaction; + final refinedGasFee = estimatedFeesForTransaction.amount; final gasBufferPercent = refinedGasFee * BigInt.from(110) ~/ BigInt.from(100); final gasBufferMin = refinedGasFee + BigInt.from(10000); final gasBuffer = gasBufferPercent > gasBufferMin ? gasBufferPercent : gasBufferMin; // Using the buffered fee for the final amount - totalAmount = (currencyBalance.balance - gasBuffer); - estimatedFeesForTransaction = gasBuffer; + totalAmount = totalAmount.copyWith(amount: currencyBalance.available.amount - gasBuffer); + estimatedFeesForTransaction = estimatedFeesForTransaction.copyWith(amount: gasBuffer); } else { // Calculating the final amount with the estimated gas fee - totalAmount = (currencyBalance.balance - estimatedFeesForTransaction); + totalAmount = currencyBalance.available - estimatedFeesForTransaction; } } // check the fees on the base currency - if (estimatedFeesForTransaction > balance[currency]!.balance) { + if (estimatedFeesForTransaction > balance[currency]!.available) { throw EVMChainTransactionFeesException.fromCurrency(currency.title); } - if (currencyBalance.balance < totalAmount) { + if (currencyBalance.available < totalAmount) { throw EVMChainTransactionCreationException(transactionCurrency); } if (transactionCurrency is! Erc20Token && - totalAmount + estimatedFeesForTransaction > currencyBalance.balance) { + totalAmount + estimatedFeesForTransaction > currencyBalance.available) { throw EVMChainTransactionFeesException.fromCurrency(currency.title); } } @@ -1056,7 +1047,6 @@ abstract class EVMChainWalletBase currency: transactionCurrency, feeCurrency: EVMChainUtils.getFeeCurrency(selectedChainId), maxFeePerGas: maxFeePerGasForTransaction, - exponent: exponent, contractAddress: transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null, data: hexOpReturnMemo, @@ -1070,7 +1060,7 @@ abstract class EVMChainWalletBase Future createCallDataTransaction( String to, String dataHex, - BigInt valueWei, + Money valueWei, EVMChainTransactionPriority? priority, String? sourceTokenAddress, BigInt? sourceTokenAmount, { @@ -1101,37 +1091,36 @@ abstract class EVMChainWalletBase gas = GasParamsHandler.zero(); } - // Validate NATIVE Balance (for Gas + Value) - final nativeBal = balance[nativeCurrency]?.balance ?? BigInt.zero; - final requiredNative = valueWei + BigInt.from(gas.estimatedGasFee); + final nativeBal = balance[nativeCurrency]?.available ?? Money.zero(nativeCurrency); + var requiredNative = Money.fromInt(gas.estimatedGasFee, nativeCurrency); + + if (valueWei.currency == nativeCurrency) { + requiredNative += valueWei; + } if (requiredNative > nativeBal) { throw Exception('Not enough ${nativeCurrency.title} to cover value and fees.'); } - // Validate ERC20 Balance (Only if source is NOT native) final cleanAddress = sourceTokenAddress?.toLowerCase() ?? ''; - // Check for both 0xeeee... AND 0x0000... (Zero Address) - bool isNativeSource = sourceTokenAddress == null || + final isNativeSource = sourceTokenAddress == null || cleanAddress == '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' || cleanAddress == '0x0000000000000000000000000000000000000000'; if (!isNativeSource && sourceTokenAmount != null && sourceTokenAmount > BigInt.zero) { - // Filter list to find match. final matchingTokens = balance.keys .where((k) => k is Erc20Token && k.contractAddress.toLowerCase() == cleanAddress); if (matchingTokens.isEmpty) { - // Token is not in the wallet balance map -> Balance is 0 throw Exception('Insufficient token balance (Token not found in wallet).'); } final tokenKey = matchingTokens.first; - final tokenBalance = balance[tokenKey]?.balance ?? BigInt.zero; + final tokenBalance = balance[tokenKey]?.available ?? Money.zero(tokenKey); - if (tokenBalance < sourceTokenAmount) { - throw Exception('Insufficient ${tokenKey.title} balance to cover the transaction amount.'); + if (tokenBalance < Money(sourceTokenAmount, tokenKey)) { + throw Exception('Insufficient ${tokenKey.symbol} balance to cover the transaction amount.'); } } @@ -1144,13 +1133,12 @@ abstract class EVMChainWalletBase privateKey: _evmChainPrivateKey, toAddress: to, amount: valueWei, - gasFee: BigInt.from(gas.estimatedGasFee), + gasFee: Money.fromInt(gas.estimatedGasFee, currency), estimatedGasUnits: gasUnits, maxFeePerGas: gas.maxFeePerGas, priority: priority, currency: nativeCurrency, feeCurrency: nativeCurrency.title, - exponent: 18, contractAddress: null, data: dataHex, gasPrice: gas.gasPrice, @@ -1161,23 +1149,23 @@ abstract class EVMChainWalletBase } } - Future createApprovalTransaction(BigInt amount, String spender, - CryptoCurrency token, EVMChainTransactionPriority? priority, String feeCurrency, + Future createApprovalTransaction( + Money amount, String spender, EVMChainTransactionPriority? priority, {bool useBlinkProtection = true}) async { - final CryptoCurrency transactionCurrency = - balance.keys.firstWhere((element) => element.title == token.title); + final transactionCurrency = + balance.keys.firstWhere((element) => element.symbol == amount.currency.symbol); assert(transactionCurrency is Erc20Token); final data = _client.getEncodedDataForApprovalTransaction( contractAddress: EthereumAddress.fromHex((transactionCurrency as Erc20Token).contractAddress), - value: EtherAmount.fromBigInt(EtherUnit.wei, amount), + value: EtherAmount.fromBigInt(EtherUnit.wei, amount.amount), toAddress: EthereumAddress.fromHex(spender), ); - final tokenContract = (transactionCurrency as Erc20Token).contractAddress; + final tokenContract = transactionCurrency.contractAddress; final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction( - amount: BigInt.zero, + amount: Money.zero(currency), receivingAddressHex: tokenContract, priority: priority, contractAddress: tokenContract, @@ -1193,11 +1181,9 @@ abstract class EVMChainWalletBase spender: spender, amount: amount, priority: priority, - gasFee: BigInt.from(gasFeesModel.estimatedGasFee), + gasFee: Money.fromInt(gasFeesModel.estimatedGasFee, currency), maxFeePerGas: gasFeesModel.maxFeePerGas, - feeCurrency: feeCurrency, estimatedGasUnits: safeGasUnits, - exponent: transactionCurrency.decimal, contractAddress: tokenContract, gasPrice: gasFeesModel.gasPrice, useBlinkProtection: useBlinkProtection, @@ -1354,7 +1340,8 @@ abstract class EVMChainWalletBase String toJSON() => json.encode({ 'mnemonic': _mnemonic, 'private_key': privateKey, - 'balance': balance[currency]?.toJSON() ?? EVMChainERC20Balance(BigInt.zero).toJSON(), + 'balance': + balance[currency]?.toJSON() ?? EVMChainERC20Balance(Money.zero(currency)).toJSON(), 'passphrase': passphrase, 'selected_chain_id': selectedChainId, }); @@ -1370,9 +1357,9 @@ abstract class EVMChainWalletBase try { final balance = await _client.getBalance(_evmChainPrivateKey.address); - return EVMChainERC20Balance(balance.getInWei); + return EVMChainERC20Balance(Money(balance.getInWei, currency)); } catch (_) { - return balance[currency] ?? EVMChainERC20Balance(BigInt.zero); + return balance[currency] ?? EVMChainERC20Balance(Money.zero(currency)); } } @@ -1429,10 +1416,7 @@ abstract class EVMChainWalletBase try { if (token.enabled) { - balance[token] = await _client.fetchERC20Balances( - _evmChainPrivateKey.address, - token.contractAddress, - ); + balance[token] = await _client.fetchERC20Balances(_evmChainPrivateKey.address, token); } else { balance.remove(token); } @@ -1536,12 +1520,7 @@ abstract class EVMChainWalletBase if (newToken.enabled) { try { - final erc20Balance = await _client.fetchERC20Balances( - _evmChainPrivateKey.address, - newToken.contractAddress, - ); - - balance[newToken] = erc20Balance; + balance[newToken] = await _client.fetchERC20Balances(_evmChainPrivateKey.address, newToken); await evmChainErc20TokensBox.put(newToken.contractAddress, newToken); } on Exception catch (_) { @@ -1611,9 +1590,6 @@ abstract class EVMChainWalletBase if (!hasKeysFile) rethrow; } - final balance = EVMChainERC20Balance.fromJSON(data?['balance'] as String?) ?? - EVMChainERC20Balance(BigInt.zero); - final WalletKeysData keysData; // Migrate wallet from the old scheme to the new .keys file scheme if (!hasKeysFile) { @@ -1658,6 +1634,10 @@ abstract class EVMChainWalletBase // Use saved chainId if available, otherwise use the computed chainId final initialChainIdForWallet = savedChainId ?? chainId; + final balance = + EVMChainERC20Balance.fromJSON(data?['balance'] as String?, chainConfig.nativeCurrency) ?? + EVMChainERC20Balance(Money.zero(chainConfig.nativeCurrency)); + return EVMChainWallet( walletInfo: walletInfo, derivationInfo: await walletInfo.getDerivationInfo(), diff --git a/cw_evm/lib/evm_erc20_balance.dart b/cw_evm/lib/evm_erc20_balance.dart index 13ae310c8..b0a59b15b 100644 --- a/cw_evm/lib/evm_erc20_balance.dart +++ b/cw_evm/lib/evm_erc20_balance.dart @@ -1,33 +1,23 @@ -import 'dart:convert'; +import "dart:convert"; -import 'package:cw_core/balance.dart'; +import "package:cw_core/amount/money.dart"; +import "package:cw_core/balance.dart"; +import "package:cw_core/currency.dart"; class EVMChainERC20Balance extends Balance { - EVMChainERC20Balance(this.balance, {this.exponent = 18}) - : super(balance, balance); + EVMChainERC20Balance(Money balance) : super(balance, Money.zero(balance.currency)); - final BigInt balance; - final int exponent; + String toJSON() => json.encode({ "balanceInWei": available.amount.toString() }); - String toJSON() => json.encode({ - 'balanceInWei': balance.toString(), - 'exponent': exponent, - }); - - static EVMChainERC20Balance? fromJSON(String? jsonSource) { - if (jsonSource == null) { - return null; - } + static EVMChainERC20Balance? fromJSON(String? jsonSource, Currency currency) { + if (jsonSource == null) return null; final decoded = json.decode(jsonSource) as Map; try { - return EVMChainERC20Balance( - BigInt.parse(decoded['balanceInWei']), - exponent: decoded['exponent'], - ); + return EVMChainERC20Balance(Money(BigInt.parse(decoded["balanceInWei"]), currency)); } catch (e) { - return EVMChainERC20Balance(BigInt.zero); + return EVMChainERC20Balance(Money.zero(currency)); } } } diff --git a/cw_evm/lib/pending_evm_chain_transaction.dart b/cw_evm/lib/pending_evm_chain_transaction.dart index 3e9ad39f2..15ad59752 100644 --- a/cw_evm/lib/pending_evm_chain_transaction.dart +++ b/cw_evm/lib/pending_evm_chain_transaction.dart @@ -1,6 +1,6 @@ import 'dart:typed_data'; -import 'package:cw_core/format_fixed.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:web3dart/crypto.dart'; import 'package:hex/hex.dart' as Hex; @@ -8,36 +8,36 @@ import 'package:hex/hex.dart' as Hex; class PendingEVMChainTransaction with PendingTransaction { final Function sendTransaction; final Uint8List signedTransaction; - final BigInt fee; - final String feeCurrency; - final String amount; - final int exponent; final bool isInfiniteApproval; PendingEVMChainTransaction({ required this.sendTransaction, required this.signedTransaction, required this.fee, - required this.feeCurrency, required this.amount, - required this.exponent, this.isInfiniteApproval = false, }); @override String get amountFormatted { if (isInfiniteApproval) return "∞"; - return formatFixed(BigInt.parse(amount), exponent); + return amount.toString(); } + @override + final Money amount; + + @override + final Money fee; + @override Future commit() async => await sendTransaction(); @override - String get feeFormatted => "$feeFormattedValue $feeCurrency"; + String get feeFormatted => fee.toStringWithSymbol(fractionalDigits: 10); @override - String get feeFormattedValue => formatFixed(fee, 18, fractionalDigits: 10); + String get feeFormattedValue => fee.toStringWithPrecision(fractionalDigits: 10); @override String get hex => bytesToHex(signedTransaction, include0x: true); @@ -61,7 +61,5 @@ class PendingEVMChainTransaction with PendingTransaction { } @override - Future> commitUR() { - throw UnimplementedError(); - } + Future> commitUR() => throw UnimplementedError(); } diff --git a/cw_evm/lib/usdt0/usdt0_service.dart b/cw_evm/lib/usdt0/usdt0_service.dart index 963994b0c..56c82d14d 100644 --- a/cw_evm/lib/usdt0/usdt0_service.dart +++ b/cw_evm/lib/usdt0/usdt0_service.dart @@ -1,3 +1,4 @@ +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_evm/contract/erc20.dart'; @@ -80,11 +81,9 @@ class USDT0Service { ); final pendingApproval = await wallet.createApprovalTransaction( - maxUint, + Money(maxUint,token), adapter, - token, priority, - EVMChainUtils.getFeeCurrency(wallet.selectedChainId), useBlinkProtection: useBlinkProtection, ); await pendingApproval.commit(); @@ -116,7 +115,7 @@ class USDT0Service { return wallet.createCallDataTransaction( oftAddress, encoded.dataHex, - encoded.valueWei, + Money(encoded.valueWei, token), priority, token.contractAddress, amount, diff --git a/cw_evm/lib/utils/evm_chain_formatter.dart b/cw_evm/lib/utils/evm_chain_formatter.dart index f9f3a73bd..5a9c321c8 100644 --- a/cw_evm/lib/utils/evm_chain_formatter.dart +++ b/cw_evm/lib/utils/evm_chain_formatter.dart @@ -11,40 +11,6 @@ class EVMChainFormatter { } } - /// Parse EVM chain amount to BigInt to avoid integer overflow for large amounts - /// [decimals] defaults to 18 for native ETH/POL, but should be set to token decimals for ERC20 tokens - static BigInt parseEVMChainAmountToBigInt(String amount, {int decimals = evmDecimals}) { - try { - String cleanAmount = amount.replaceAll(',', '.'); - - bool isNegative = cleanAmount.startsWith('-'); - if (isNegative) { - cleanAmount = cleanAmount.substring(1); - } - - final parts = cleanAmount.split('.'); - String whole = parts[0].isEmpty ? '0' : parts[0]; - String fraction = parts.length > 1 ? parts[1] : ''; - - // 3. Strict Truncation Logic - // If fraction is longer than decimals, strictly cut it off. - // If shorter, pad it with zeros. - if (fraction.length > decimals) { - fraction = fraction.substring(0, decimals); - } else { - fraction = fraction.padRight(decimals, '0'); - } - - final wholeBigInt = BigInt.parse(whole); - final fractionBigInt = BigInt.parse(fraction); - final multiplier = BigInt.from(10).pow(decimals); - - return (wholeBigInt * multiplier) + fractionBigInt; - } catch (_) { - return BigInt.zero; - } - } - static String truncateDecimals(String amount, int decimals) { final parts = amount.split("."); diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 9d115ea10..f12f65a3b 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -4,6 +4,8 @@ import 'dart:ffi'; import 'dart:isolate'; import 'dart:math'; +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart'; @@ -118,9 +120,7 @@ String getSeedLegacy(String? language) { return legacy; } -String getPassphrase() { - return currentWallet?.getCacheAttribute(key: "cakewallet.passphrase") ?? ""; -} +String getPassphrase() => currentWallet?.getCacheAttribute(key: "cakewallet.passphrase") ?? ""; Map>> addressCache = {}; @@ -146,12 +146,11 @@ String getAddress({int accountIndex = 0, int addressIndex = 0}) { return addressCache[currentWallet!.ffiAddress()]![accountIndex]![addressIndex]!; } -int getFullBalance({int accountIndex = 0}) { - return currentWallet?.balance(accountIndex: accountIndex) ?? 0; -} +Money getFullBalance({int accountIndex = 0}) => + Money.fromInt(currentWallet?.balance(accountIndex: accountIndex) ?? 0, CryptoCurrency.xmr); -int getUnlockedBalance({int accountIndex = 0}) => - currentWallet?.unlockedBalance(accountIndex: accountIndex) ?? 0; +Money getUnlockedBalance({int accountIndex = 0}) => Money.fromInt( + currentWallet?.unlockedBalance(accountIndex: accountIndex) ?? 0, CryptoCurrency.xmr); int getCurrentHeight() => currentWallet?.blockChainHeight() ?? 0; @@ -417,13 +416,11 @@ Future setTrustedDaemon(bool trusted) async => Future trustedDaemon() async => currentWallet!.trustedDaemon(); -String signMessage(String message, {String address = ""}) { - return currentWallet!.signMessage(message: message, address: address); -} +String signMessage(String message, {String address = ""}) => + currentWallet!.signMessage(message: message, address: address); -bool verifyMessage(String message, String address, String signature) { - return currentWallet!.verifySignedMessage(message: message, address: address, signature: signature); -} +bool verifyMessage(String message, String address, String signature) => + currentWallet!.verifySignedMessage(message: message, address: address, signature: signature); Map> debugCallLength() => monero.debugCallLength; @@ -438,4 +435,4 @@ Map getWalletCacheDebug() { "error": e.toString(), }; } -} \ No newline at end of file +} diff --git a/cw_monero/lib/monero_balance.dart b/cw_monero/lib/monero_balance.dart new file mode 100644 index 000000000..eec2ec769 --- /dev/null +++ b/cw_monero/lib/monero_balance.dart @@ -0,0 +1,17 @@ +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/balance.dart'; +import 'package:cw_core/crypto_currency.dart'; + +class MoneroBalance extends Balance { + MoneroBalance({ + required this.fullBalance, + required Money unlockedBalance, + Money? frozen, + }) : super( + unlockedBalance, + fullBalance - unlockedBalance, + frozen: frozen ?? Money.zero(CryptoCurrency.xmr), + ); + + final Money fullBalance; +} diff --git a/cw_monero/lib/monero_transaction_info.dart b/cw_monero/lib/monero_transaction_info.dart index b7ddd0ca6..3f1567122 100644 --- a/cw_monero/lib/monero_transaction_info.dart +++ b/cw_monero/lib/monero_transaction_info.dart @@ -1,5 +1,4 @@ -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; @@ -15,23 +14,10 @@ class MoneroTransactionInfo extends TransactionInfo { final DateTime date; final int accountIndex; final bool isPending; - final int amount; - final int fee; + final Money amount; + final Money fee; final int addressIndex; final int confirmations; String? recipientAddress; String? key; - String? _fiatAmount; - - @override - String amountFormatted() => '${CryptoCurrency.xmr.formatAmount(BigInt.from(amount))} XMR'; - - @override - String fiatAmount() => _fiatAmount ?? ''; - - @override - void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); - - @override - String feeFormatted() => '${CryptoCurrency.xmr.formatAmount(BigInt.from(fee))} XMR'; } diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 95e02f9e1..b06cd4625 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -3,12 +3,11 @@ import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; -import 'package:cw_core/monero_amount_format.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/account.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/monero_balance.dart'; import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/monero_wallet_keys.dart'; import 'package:cw_core/monero_wallet_utils.dart'; @@ -30,6 +29,7 @@ import 'package:cw_monero/api/wallet.dart' as monero_wallet; import 'package:cw_monero/api/wallet_manager.dart'; import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart'; import 'package:cw_monero/ledger.dart'; +import 'package:cw_monero/monero_balance.dart'; import 'package:cw_monero/monero_transaction_creation_credentials.dart'; import 'package:cw_monero/monero_transaction_history.dart'; import 'package:cw_monero/monero_transaction_info.dart'; @@ -375,15 +375,15 @@ abstract class MoneroWalletBase extends WalletBase item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { - throw MoneroTransactionCreationException( - 'You do not have enough XMR to send this amount.'); + if (outputs.any((item) => item.sendAll || item.cryptoAmount.amount <= BigInt.zero)) { + throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.'); } - final int totalAmount = outputs.fold( - 0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); + final totalAmount = outputs.fold(0, (acc, value) => acc + value.cryptoAmount.amount.toInt()); - if (unlockedBalance < totalAmount) { - throw MoneroTransactionCreationException( - 'You do not have enough XMR to send this amount.'); + if (unlockedBalance < Money.fromInt(totalAmount, CryptoCurrency.xmr)) { + throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.'); } - if (inputs.isEmpty) MoneroTransactionCreationException( - 'No inputs selected'); + if (inputs.isEmpty) MoneroTransactionCreationException('No inputs selected'); final moneroOutputs = outputs.map((output) { - final outputAddress = - output.isParsedAddress ? output.extractedAddress : output.address; + final outputAddress = output.isParsedAddress ? output.extractedAddress : output.address; - return MoneroOutput( - address: outputAddress!, - amount: output.cryptoAmount!.replaceAll(',', '.')); + return MoneroOutput(address: outputAddress!, amount: output.cryptoAmount.toString()); }).toList(); pendingTransactionDescription = @@ -481,8 +473,7 @@ abstract class MoneroWalletBase extends WalletBase{ 'key': row.key, @@ -893,8 +883,7 @@ abstract class MoneroWalletBase extends WalletBase monero_wallet.getUnlockedBalance( - accountIndex: walletAddresses.account!.id); + Money _getUnlockedBalance() => + monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); - int _getFrozenBalance() { + Money _getFrozenBalance() { var frozenBalance = 0; - for (var coin in unspentCoinsInfo.values.where((element) => + for (final coin in unspentCoinsInfo.values.where((element) => element.walletId == id && element.accountIndex == walletAddresses.account!.id)) { if (coin.isFrozen && !coin.isSending) frozenBalance += coin.value; } - return frozenBalance; + return Money.fromInt(frozenBalance, CryptoCurrency.xmr); } void _onNewBlock(int height, int blocksLeft, double ptc) async { @@ -1013,9 +999,4 @@ abstract class MoneroWalletBase extends WalletBase setLedgerConnection(LedgerConnection connection) async { await enableLedgerExchange(connection); } - - @override - String formatCryptoAmount(String amount) { - return moneroAmountToString(amount: int.parse(amount)); - } } diff --git a/cw_monero/lib/pending_monero_transaction.dart b/cw_monero/lib/pending_monero_transaction.dart index c32a2cc9c..d9d1c434a 100644 --- a/cw_monero/lib/pending_monero_transaction.dart +++ b/cw_monero/lib/pending_monero_transaction.dart @@ -1,16 +1,14 @@ import 'dart:async'; import 'dart:ffi'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/pending_transaction.dart'; import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; -import 'package:cw_monero/api/transaction_history.dart' - as monero_transaction_history; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/amount_converter.dart'; - -import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_monero/api/transaction_history.dart' as monero_transaction_history; import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:monero/monero.dart' as monero; @@ -29,6 +27,10 @@ class PendingMoneroTransaction with PendingTransaction { final PendingTransactionDescription pendingTransactionDescription; final MoneroWalletBase wallet; + Money get amount => Money.fromInt(pendingTransactionDescription.amount, CryptoCurrency.xmr); + + Money get fee => Money.fromInt(pendingTransactionDescription.fee, CryptoCurrency.xmr); + @override String get id => pendingTransactionDescription.hash; @@ -36,15 +38,7 @@ class PendingMoneroTransaction with PendingTransaction { String get hex => pendingTransactionDescription.hex; @override - String get amountFormatted => AmountConverter.amountIntToString( - CryptoCurrency.xmr, pendingTransactionDescription.amount); - - @override - String get feeFormatted => "$feeFormattedValue XMR"; - - @override - String get feeFormattedValue => AmountConverter.amountIntToString( - CryptoCurrency.xmr, pendingTransactionDescription.fee); + String get amountFormatted => amount.toString(); @override bool shouldCommitUR() => isViewOnly && wallet.hardwareWalletType != HardwareWalletType.trezor; diff --git a/cw_nano/lib/banano_balance.dart b/cw_nano/lib/banano_balance.dart deleted file mode 100644 index 408b9dddb..000000000 --- a/cw_nano/lib/banano_balance.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:cw_core/balance.dart'; -import 'package:nanoutil/nanoutil.dart'; - -BigInt stringAmountToBigIntBanano(String amount) { - return BigInt.parse(NanoAmounts.getAmountAsRaw(amount, NanoAmounts.rawPerBanano)); -} - -class BananoBalance extends Balance { - final BigInt currentBalance; - final BigInt receivableBalance; - - BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(BigInt.zero, BigInt.zero); - - BananoBalance.fromFormattedString( - {required String formattedCurrentBalance, required String formattedReceivableBalance}) - : currentBalance = stringAmountToBigIntBanano(formattedCurrentBalance), - receivableBalance = stringAmountToBigIntBanano(formattedReceivableBalance), - super(BigInt.zero, BigInt.zero); - - BananoBalance.fromRawString( - {required String currentBalance, required String receivableBalance}) - : currentBalance = BigInt.parse(currentBalance), - receivableBalance = BigInt.parse(receivableBalance), - super(BigInt.zero, BigInt.zero); - - @override - String get formattedAvailableBalance { - return NanoAmounts.getRawAsUsableString(currentBalance.toString(), NanoAmounts.rawPerBanano); - } - - @override - String get formattedAdditionalBalance { - return NanoAmounts.getRawAsUsableString(receivableBalance.toString(), NanoAmounts.rawPerBanano); - } -} diff --git a/cw_nano/lib/nano_balance.dart b/cw_nano/lib/nano_balance.dart index ce9464696..c5559e6d3 100644 --- a/cw_nano/lib/nano_balance.dart +++ b/cw_nano/lib/nano_balance.dart @@ -1,36 +1,22 @@ +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/balance.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:nanoutil/nanoutil.dart'; - -BigInt stringAmountToBigIntNano(String amount) { - return BigInt.parse(NanoAmounts.getAmountAsRaw(amount, NanoAmounts.rawPerNano)); -} +import 'package:cw_core/crypto_currency.dart'; class NanoBalance extends Balance { - final BigInt currentBalance; - final BigInt receivableBalance; + final Money currentBalance; + final Money receivableBalance; - NanoBalance({required this.currentBalance, required this.receivableBalance}) : super(currentBalance, receivableBalance); + NanoBalance({required this.currentBalance, required this.receivableBalance}) + : super(currentBalance, receivableBalance); - NanoBalance.fromFormattedString( - {required String formattedCurrentBalance, required String formattedReceivableBalance}) - : currentBalance = stringAmountToBigIntNano(formattedCurrentBalance), - receivableBalance = stringAmountToBigIntNano(formattedReceivableBalance), - super(stringAmountToBigIntNano(formattedCurrentBalance), stringAmountToBigIntNano(formattedReceivableBalance)); - - NanoBalance.fromRawString( - {required String currentBalance, required String receivableBalance}) - : currentBalance = BigInt.parse(currentBalance), - receivableBalance = BigInt.parse(receivableBalance), - super(BigInt.parse(currentBalance), BigInt.parse(receivableBalance)); - - @override - String get formattedAvailableBalance { - return NanoAmounts.getRawAsUsableString(currentBalance.toString(), NanoAmounts.rawPerNano); - } - - @override - String get formattedAdditionalBalance { - return NanoAmounts.getRawAsUsableString(receivableBalance.toString(), NanoAmounts.rawPerNano); - } + factory NanoBalance.fromRawString({ + required String currentBalance, + required String receivableBalance, + }) => + NanoBalance( + currentBalance: + Money.tryParse(currentBalance, CryptoCurrency.nano) ?? Money.zero(CryptoCurrency.nano), + receivableBalance: Money.tryParse(receivableBalance, CryptoCurrency.nano) ?? + Money.zero(CryptoCurrency.nano), + ); } diff --git a/cw_nano/lib/nano_client.dart b/cw_nano/lib/nano_client.dart index 285469568..8cf1a66be 100644 --- a/cw_nano/lib/nano_client.dart +++ b/cw_nano/lib/nano_client.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:convert'; +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/nano_account_info_response.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; @@ -85,10 +87,10 @@ class NanoClient { throw Exception( "Error while trying to get balance! ${data["error"] != null ? data["error"] : ""}"); } - final String currentBalance = data["balance"] as String; - final String receivableBalance = data["receivable"] as String; - final BigInt cur = BigInt.parse(currentBalance); - final BigInt rec = BigInt.parse(receivableBalance); + final currentBalance = data["balance"] as String; + final receivableBalance = data["receivable"] as String; + final cur = Money(BigInt.parse(currentBalance), CryptoCurrency.nano); + final rec = Money(BigInt.parse(receivableBalance), CryptoCurrency.nano); return NanoBalance(currentBalance: cur, receivableBalance: rec); } @@ -258,7 +260,7 @@ class NanoClient { // first get the current account balance: if (balanceAfterTx == null) { - final BigInt currentBalance = (await getBalance(publicAddress)).currentBalance; + final BigInt currentBalance = (await getBalance(publicAddress)).currentBalance.amount; final BigInt txAmount = BigInt.parse(amountRaw); balanceAfterTx = currentBalance - txAmount; } diff --git a/cw_nano/lib/nano_transaction_info.dart b/cw_nano/lib/nano_transaction_info.dart index 9195a06ef..66dd78344 100644 --- a/cw_nano/lib/nano_transaction_info.dart +++ b/cw_nano/lib/nano_transaction_info.dart @@ -1,13 +1,14 @@ +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/format_amount.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; -import 'package:nanoutil/nanoutil.dart'; class NanoTransactionInfo extends TransactionInfo { NanoTransactionInfo({ required this.id, required this.height, - required this.amountRaw, + required Money amountRaw, this.tokenSymbol = "XNO", required this.direction, required this.confirmed, @@ -15,12 +16,11 @@ class NanoTransactionInfo extends TransactionInfo { required this.confirmations, required this.to, required this.from, - }) : this.amount = amountRaw.toInt(); + }) : this.amount = amountRaw; final String id; final int height; - final int amount; - final BigInt amountRaw; + final Money amount; final TransactionDirection direction; final DateTime date; final bool confirmed; @@ -32,28 +32,17 @@ class NanoTransactionInfo extends TransactionInfo { bool get isPending => !this.confirmed; - @override - String amountFormatted() { - final String amt = - NanoAmounts.getRawAsUsableString(amountRaw.toString(), NanoAmounts.rawPerNano); - final String acc = NanoAmounts.getRawAccuracy(amountRaw.toString(), NanoAmounts.rawPerNano); - return "$acc$amt $tokenSymbol"; - } - @override String fiatAmount() => _fiatAmount ?? ''; @override void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); - @override - String feeFormatted() => "0 XNO"; - factory NanoTransactionInfo.fromJson(Map data) { return NanoTransactionInfo( id: data['id'] as String, height: data['height'] as int, - amountRaw: BigInt.parse(data['amountRaw'] as String), + amountRaw: Money(BigInt.parse(data['amountRaw'] as String), CryptoCurrency.nano), direction: parseTransactionDirectionFromInt(data['direction'] as int), date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), confirmed: data['confirmed'] as bool, @@ -67,7 +56,7 @@ class NanoTransactionInfo extends TransactionInfo { Map toJson() => { 'id': id, 'height': height, - 'amountRaw': amountRaw.toString(), + 'amountRaw': amount.amount.toString(), 'direction': direction.index, 'date': date.millisecondsSinceEpoch, 'confirmed': confirmed, diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index 44b4a3c8a..86b197818 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; @@ -55,7 +56,10 @@ abstract class NanoWalletBase walletAddresses = NanoWalletAddresses(walletInfo), balance = ObservableMap.of({ CryptoCurrency.nano: initialBalance ?? - NanoBalance(currentBalance: BigInt.zero, receivableBalance: BigInt.zero) + NanoBalance( + currentBalance: Money.zero(CryptoCurrency.nano), + receivableBalance: Money.zero(CryptoCurrency.nano), + ) }), super(walletInfo, derivationInfo) { this.walletInfo = walletInfo; @@ -191,7 +195,7 @@ abstract class NanoWalletBase BigInt runningAmount = BigInt.zero; await _updateBalance(); - BigInt runningBalance = balance[currency]?.currentBalance ?? BigInt.zero; + BigInt runningBalance = balance[currency]?.currentBalance.amount ?? BigInt.zero; final List> blocks = []; String? previousHash; @@ -199,14 +203,12 @@ abstract class NanoWalletBase for (var txOut in credentials.outputs) { late BigInt amt; if (txOut.sendAll) { - amt = balance[currency]?.currentBalance ?? BigInt.zero; + amt = balance[currency]?.currentBalance.amount ?? BigInt.zero; } else { - amt = BigInt.tryParse(NanoAmounts.getAmountAsRaw( - txOut.cryptoAmount?.replaceAll(',', '.') ?? "0", NanoAmounts.rawPerNano)) ?? - BigInt.zero; + amt = txOut.cryptoAmount.amount; } - if (balance[currency]?.currentBalance != null && amt > balance[currency]!.currentBalance) { + if (balance[currency]?.currentBalance != null && amt > balance[currency]!.currentBalance.amount) { throw Exception("Trying to send more than entire balance!"); } @@ -233,7 +235,7 @@ abstract class NanoWalletBase } try { - if (runningAmount > balance[currency]!.currentBalance || runningBalance < BigInt.zero) { + if (runningAmount > balance[currency]!.currentBalance.amount || runningBalance < BigInt.zero) { throw Exception(("Trying to send more than entire balance!")); } } catch (e) { @@ -241,7 +243,7 @@ abstract class NanoWalletBase } return PendingNanoTransaction( - amount: runningAmount, + amount: Money(runningAmount, currency), id: "", nanoClient: _client, blocks: blocks, @@ -294,7 +296,7 @@ abstract class NanoWalletBase final bool isSend = transactionModel.type == "send"; result[transactionModel.hash] = NanoTransactionInfo( id: transactionModel.hash, - amountRaw: transactionModel.amount, + amountRaw: Money(transactionModel.amount, currency), height: transactionModel.height, direction: isSend ? TransactionDirection.outgoing : TransactionDirection.incoming, confirmed: transactionModel.confirmed, @@ -360,7 +362,7 @@ abstract class NanoWalletBase // get our balance: await _updateBalance(); // if we have anything to receive, process it: - if (balance[currency]!.receivableBalance > BigInt.zero) { + if (balance[currency]!.receivableBalance.amount > BigInt.zero) { await _receiveAll(); } }); @@ -465,8 +467,8 @@ abstract class NanoWalletBase // if we don't have a balance, we should at least create one, since it's a late binding // otherwise, it's better to just leave it as whatever it was before: if (balance[currency] == null) { - balance[currency] = - NanoBalance(currentBalance: BigInt.zero, receivableBalance: BigInt.zero); + balance[currency] = NanoBalance( + currentBalance: Money.zero(currency), receivableBalance: Money.zero(currency)); } } // don't save unnecessarily: diff --git a/cw_nano/lib/pending_nano_transaction.dart b/cw_nano/lib/pending_nano_transaction.dart index 8a759f55b..5922d2f93 100644 --- a/cw_nano/lib/pending_nano_transaction.dart +++ b/cw_nano/lib/pending_nano_transaction.dart @@ -1,6 +1,7 @@ +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_nano/nano_client.dart'; -import 'package:nanoutil/nanoutil.dart'; class PendingNanoTransaction with PendingTransaction { PendingNanoTransaction({ @@ -11,38 +12,26 @@ class PendingNanoTransaction with PendingTransaction { }); final NanoClient nanoClient; - final BigInt amount; final String id; final List> blocks; String hex = "unused"; @override - String get amountFormatted { - final String amt = NanoAmounts.getRawAsUsableString(amount.toString(), NanoAmounts.rawPerNano); - return amt; - } - - String get accurateAmountFormatted { - final String amt = NanoAmounts.getRawAsUsableString(amount.toString(), NanoAmounts.rawPerNano); - final String acc = NanoAmounts.getRawAccuracy(amount.toString(), NanoAmounts.rawPerNano); - return "$acc$amt"; - } + final Money amount; @override - String get feeFormatted => "$feeFormattedValue XNO"; + String get amountFormatted => amount.toString(); @override - String get feeFormattedValue => "0"; + Money get fee => Money.zero(CryptoCurrency.nano); @override Future commit() async { - for (var block in blocks) { + for (final block in blocks) { await nanoClient.processBlock(block, "send"); } } @override - Future> commitUR() { - throw UnimplementedError(); - } + Future> commitUR() => throw UnimplementedError(); } diff --git a/cw_solana/lib/default_spl_tokens.dart b/cw_solana/lib/default_spl_tokens.dart index 72c3ca4ff..324502fd9 100644 --- a/cw_solana/lib/default_spl_tokens.dart +++ b/cw_solana/lib/default_spl_tokens.dart @@ -765,4 +765,6 @@ class DefaultSPLTokens { return SPLToken.copyWith(token, icon: iconPath, tag: 'SOL'); }).toList(); + + SPLToken get usdc => _defaultTokens.where((e) => e.symbol == "USDC").first; } diff --git a/cw_solana/lib/pending_solana_transaction.dart b/cw_solana/lib/pending_solana_transaction.dart index ddfcf6540..7afeb5ffb 100644 --- a/cw_solana/lib/pending_solana_transaction.dart +++ b/cw_solana/lib/pending_solana_transaction.dart @@ -1,11 +1,10 @@ +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/pending_transaction.dart'; class PendingSolanaTransaction with PendingTransaction { - final double amount; final String serializedTransaction; final String destinationAddress; final Function sendTransaction; - final double fee; String? _sig; PendingSolanaTransaction({ @@ -16,6 +15,12 @@ class PendingSolanaTransaction with PendingTransaction { required this.sendTransaction, }); + @override + final Money amount; + + @override + final Money fee; + @override String get amountFormatted { String stringifiedAmount = amount.toString(); @@ -32,12 +37,6 @@ class PendingSolanaTransaction with PendingTransaction { _sig = await sendTransaction(); } - @override - String get feeFormatted => "$feeFormattedValue SOL"; - - @override - String get feeFormattedValue => fee.toString(); - @override String get hex => serializedTransaction; diff --git a/cw_solana/lib/solana_balance.dart b/cw_solana/lib/solana_balance.dart index 7be967fbe..bc796ef3b 100644 --- a/cw_solana/lib/solana_balance.dart +++ b/cw_solana/lib/solana_balance.dart @@ -1,44 +1,25 @@ -import 'dart:convert'; +import "dart:convert"; -import 'package:cw_core/balance.dart'; +import "package:cw_core/amount/money.dart"; +import "package:cw_core/balance.dart"; +import "package:cw_core/currency.dart"; class SolanaBalance extends Balance { - SolanaBalance(this.balance, bool isToken) : super( - BigInt.from(int.tryParse(balance.toStringAsFixed(isToken ? 6 : 9).replaceFirst(".", "")) ?? 0), - BigInt.from(int.tryParse(balance.toStringAsFixed(isToken ? 6 : 9).replaceFirst(".", "")) ?? 0)); + SolanaBalance(Money balance) : super(balance, balance.copyWith(amount: BigInt.zero)); - // Using raw amount from RPC to avoid decimals mismatch for SPL tokens. - SolanaBalance.forToken(BigInt rawAmount, double uiAmount) - : balance = uiAmount, - super(rawAmount, rawAmount); + factory SolanaBalance.zero(Currency currency) => SolanaBalance(Money.zero(currency)); - final double balance; - - String get formattedAdditionalBalance => _balanceFormatted(); - - String get formattedAvailableBalance => _balanceFormatted(); - - String _balanceFormatted() { - String stringBalance = balance.toString(); - if (stringBalance.toString().length >= 12) { - stringBalance = stringBalance.substring(0, 12); - } - return stringBalance; - } - - static SolanaBalance? fromJSON(String? jsonSource, bool isToken) { - if (jsonSource == null) { - return null; - } + static SolanaBalance? fromJSON(String? jsonSource, Currency currency) { + if (jsonSource == null) return null; final decoded = json.decode(jsonSource) as Map; try { - return SolanaBalance(decoded['balance'], isToken); + return SolanaBalance(currency.parseAmount(decoded["balance"])); } catch (e) { - return SolanaBalance(0.0, isToken); + return SolanaBalance(Money.zero(currency)); } } - String toJSON() => json.encode({'balance': balance.toString()}); + String toJSON() => json.encode({"balance": available.toString()}); } diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index 543e28026..ad72c74a9 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -1,9 +1,10 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:math' as math; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/solana_rpc_http_service.dart'; @@ -32,7 +33,7 @@ class TransactionFetchResult { class SolanaWalletClient { // Minimum amount in SOL to consider a transaction valid (to filter spam) - static const double minValidAmount = 0.00000003; + static Money minValidAmount = Money.parse("0.00000003", CryptoCurrency.sol); late final client = ProxyWrapper().getHttpIOClient(); SolanaRPC? _provider; @@ -61,24 +62,17 @@ class SolanaWalletClient { } } - Future getBalance(String walletAddress, {bool throwOnError = false}) async { + Future getBalance(String walletAddress, {bool throwOnError = false}) async { try { final balance = await _provider!.requestWithContext( - SolanaRPCGetBalance( - account: SolAddress(walletAddress), - ), + SolanaRPCGetBalance(account: SolAddress(walletAddress)), ); - - final balInLamp = balance.result.toDouble(); - - final solBalance = balInLamp / SolanaUtils.lamportsPerSol; - - return solBalance; + return Money(balance.result, CryptoCurrency.sol); } catch (_) { if (throwOnError) { rethrow; } - return 0.0; + return Money.zero(CryptoCurrency.sol); } } @@ -100,11 +94,11 @@ class SolanaWalletClient { } } - Future getSplTokenBalance(String mintAddress, String walletAddress, + Future getSplTokenBalance(SPLToken token, String walletAddress, {bool throwOnError = false}) async { try { // Fetch the token accounts (a token can have multiple accounts for various uses) - final tokenAccounts = await getSPLTokenAccounts(mintAddress, walletAddress); + final tokenAccounts = await getSPLTokenAccounts(token.mintAddress, walletAddress); // Handle scenario where there is no token account if (tokenAccounts == null || tokenAccounts.isEmpty) { @@ -112,8 +106,7 @@ class SolanaWalletClient { } // Sum raw amounts and ui amounts across all token accounts - BigInt totalRaw = BigInt.zero; - double totalUi = 0.0; + var totalRaw = BigInt.zero; for (var tokenAccount in tokenAccounts) { final tokenAmountResult = await _provider!.request( @@ -122,56 +115,43 @@ class SolanaWalletClient { final raw = BigInt.tryParse(tokenAmountResult.amount) ?? BigInt.zero; totalRaw += raw; - - final ui = tokenAmountResult.uiAmount ?? - (double.tryParse(tokenAmountResult.uiAmountString ?? '0') ?? 0.0); - totalUi += ui; } - return SolanaBalance.forToken(totalRaw, totalUi); + return SolanaBalance(Money(totalRaw, token)); } catch (_) { - if (throwOnError) { - rethrow; - } + if (throwOnError) rethrow; + return null; } } - Future getFeeForMessage(String message, Commitment commitment) async { + Future getFeeForMessage(String message, Commitment commitment) async { try { final feeForMessage = await _provider!.request( - SolanaRPCGetFeeForMessage( - encodedMessage: message, - commitment: commitment, - ), + SolanaRPCGetFeeForMessage(encodedMessage: message, commitment: commitment), ); - final fee = (feeForMessage?.toDouble() ?? 0.0) / SolanaUtils.lamportsPerSol; - return fee; + return Money(feeForMessage ?? BigInt.zero, CryptoCurrency.sol); } catch (_) { - return 0.0; + return Money.zero(CryptoCurrency.sol); } } - Future getEstimatedFee(SolanaPublicKey publicKey, Commitment commitment) async { + Future getEstimatedFee(SolanaPublicKey publicKey, Commitment commitment) async { final message = await _getMessageForNativeTransaction( publicKey: publicKey, destinationAddress: publicKey.toAddress().address, - lamports: SolanaUtils.lamportsPerSol, + lamports: Money(BigInt.from(1000000000), CryptoCurrency.sol), commitment: commitment, ); - final estimatedFee = await _getFeeFromCompiledMessage( - message, - commitment, - ); - return estimatedFee; + return _getFeeFromCompiledMessage(message, commitment); } Future?> parseTransaction({ VersionedTransactionResponse? txResponse, required String walletAddress, - String? splTokenSymbol, + SPLToken? splToken, }) async { if (txResponse == null) return null; @@ -182,8 +162,7 @@ class SolanaWalletClient { if (meta == null || transaction == null) return null; - final int fee = meta.fee; - final feeInSol = fee / SolanaUtils.lamportsPerSol; + final fee = meta.fee; final message = transaction.message; final instructions = message.compiledInstructions; @@ -201,12 +180,10 @@ class SolanaWalletClient { message: message, meta: meta, fee: fee, - feeInSol: feeInSol, walletAddress: walletAddress, signature: signature, blockTime: blockTime, instructions: instructions, - splTokenSymbol: splTokenSymbol, ); if (swapTransactions.isNotEmpty) return swapTransactions; @@ -230,7 +207,6 @@ class SolanaWalletClient { message: message, meta: meta, fee: fee, - feeInSol: feeInSol, feePayerIndex: feePayerIndex, walletAddress: walletAddress, signature: signature, @@ -248,12 +224,11 @@ class SolanaWalletClient { message: message, meta: meta, fee: fee, - feeInSol: feeInSol, instruction: instruction, walletAddress: walletAddress, signature: signature, blockTime: blockTime, - splTokenSymbol: splTokenSymbol, + splToken: splToken, ); if (transactionModel != null) { @@ -401,12 +376,10 @@ class SolanaWalletClient { required VersionedMessage message, required ConfirmedTransactionMeta meta, required int fee, - required double feeInSol, required String walletAddress, required String signature, required BigInt? blockTime, required List instructions, - String? splTokenSymbol, }) async { final List swapTransactions = []; @@ -452,7 +425,7 @@ class SolanaWalletClient { // Parse outgoing side (what was sent) double outgoingAmount = 0.0; - String outgoingTokenSymbol = ''; + Currency outgoingToken = CryptoCurrency.sol; String? outgoingMintAddress; String? outgoingFrom; String? outgoingTo; @@ -475,7 +448,7 @@ class SolanaWalletClient { if (balanceChange > BigInt.zero) { // The wallet sent SOL outgoingAmount = balanceChange.toDouble() / SolanaUtils.lamportsPerSol; - outgoingTokenSymbol = 'SOL'; + outgoingToken = CryptoCurrency.sol; outgoingMintAddress = null; outgoingFrom = walletAddress; // We find the intermediate account or swap program account @@ -519,7 +492,7 @@ class SolanaWalletClient { outgoingAmount = diff.toDouble(); outgoingMintAddress = mint; final token = await getTokenInfo(mint); - outgoingTokenSymbol = token?.symbol ?? 'TOKEN'; + outgoingToken = token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6); outgoingFrom = walletAddress; // We find the intermediate account if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) { @@ -541,7 +514,7 @@ class SolanaWalletClient { // Parse incoming side (what was received) double incomingAmount = 0.0; - String incomingTokenSymbol = ''; + Currency incomingToken = CryptoCurrency.sol; String? incomingMintAddress; String? incomingFrom; String? incomingTo; @@ -563,7 +536,7 @@ class SolanaWalletClient { if (balanceChange > BigInt.zero) { // The wallet received SOL incomingAmount = balanceChange.toDouble() / SolanaUtils.lamportsPerSol; - incomingTokenSymbol = 'SOL'; + incomingToken = CryptoCurrency.sol; incomingMintAddress = null; incomingTo = walletAddress; // We find the intermediate account @@ -650,7 +623,9 @@ class SolanaWalletClient { incomingAmount = diff.toDouble(); incomingMintAddress = mint; final token = await getTokenInfo(mint); - incomingTokenSymbol = token?.symbol ?? 'TOKEN'; + printV(token?.symbol); + printV(token?.decimals); + incomingToken = token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6); incomingTo = walletAddress; // We find the intermediate account if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) { @@ -679,13 +654,12 @@ class SolanaWalletClient { from: outgoingFrom, to: outgoingTo, id: outgoingId, - amount: outgoingAmount, + amount: Money.parse(outgoingAmount.toStringAsFixed(outgoingToken.decimals), outgoingToken), programId: outgoingMintAddress == null ? SystemProgramConst.programId.address : SPLTokenProgramConst.tokenProgramId.address, blockTimeInInt: blockTime?.toInt() ?? 0, - tokenSymbol: outgoingTokenSymbol, - fee: feeInSol, + fee: Money.fromInt(fee, CryptoCurrency.sol), )); } @@ -698,13 +672,12 @@ class SolanaWalletClient { from: incomingFrom, to: incomingTo, id: incomingId, - amount: incomingAmount, + amount: Money.parse(incomingAmount.toStringAsFixed(incomingToken.decimals), incomingToken), programId: incomingMintAddress == null ? SystemProgramConst.programId.address : SPLTokenProgramConst.tokenProgramId.address, blockTimeInInt: blockTime?.toInt() ?? 0, - tokenSymbol: incomingTokenSymbol, - fee: 0.0, // Fee only charged on outgoing side + fee: Money.zero(CryptoCurrency.sol), // Fee only charged on outgoing side )); } @@ -715,7 +688,6 @@ class SolanaWalletClient { required VersionedMessage message, required ConfirmedTransactionMeta meta, required int fee, - required double feeInSol, required int feePayerIndex, required String walletAddress, required String signature, @@ -754,10 +726,9 @@ class SolanaWalletClient { final netChange = walletPaidFee ? walletChange - BigInt.from(fee) : walletChange; final isOutgoing = netChange > BigInt.zero; - final amountLamports = netChange.abs(); - final amountInSol = amountLamports.toDouble() / SolanaUtils.lamportsPerSol; + final amountLamports = Money(netChange.abs(), CryptoCurrency.sol); - if (amountInSol < minValidAmount) return null; + if (amountLamports < minValidAmount) return null; // Find the most likely receiver, the account that has the largest opposite balance change. String? receiver; @@ -783,11 +754,10 @@ class SolanaWalletClient { from: isOutgoing ? walletAddress : receiver, to: isOutgoing ? receiver : walletAddress, id: signature, - amount: amountInSol, + amount: amountLamports, programId: SystemProgramConst.programId.address, - tokenSymbol: 'SOL', blockTimeInInt: blockTime?.toInt() ?? 0, - fee: feeInSol, + fee: Money.fromInt(fee, CryptoCurrency.sol), ); } @@ -795,12 +765,11 @@ class SolanaWalletClient { required VersionedMessage message, required ConfirmedTransactionMeta meta, required int fee, - required double feeInSol, required CompiledInstruction instruction, required String walletAddress, required String signature, required BigInt? blockTime, - String? splTokenSymbol, + SPLToken? splToken, }) async { final preTokenBalances = meta.preTokenBalances; final postTokenBalances = meta.postTokenBalances; @@ -891,11 +860,8 @@ class SolanaWalletClient { final sender = senderOwner ?? accountKeys[sourceAccountIndex].address; final receiver = receiverOwner ?? accountKeys[destinationAccountIndex].address; - String? tokenSymbol = splTokenSymbol; - - if (tokenSymbol == null && mintAddress != null) { - final token = await getTokenInfo(mintAddress); - tokenSymbol = token?.symbol; + if (splToken == null && mintAddress != null) { + splToken = await getTokenInfo(mintAddress); } return SolanaTransactionModel( @@ -903,20 +869,21 @@ class SolanaWalletClient { from: sender, to: receiver, id: signature, - amount: amount, + amount: Money.parse(amount.toStringAsFixed((splToken ?? CryptoCurrency.sol).decimals), + splToken ?? CryptoCurrency.sol), programId: SPLTokenProgramConst.tokenProgramId.address, blockTimeInInt: blockTime?.toInt() ?? 0, - tokenSymbol: tokenSymbol ?? '', - fee: feeInSol, + fee: Money.fromInt(fee, CryptoCurrency.sol), ); } /// Fetches a specific transaction by signature and parses it - /// 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 + /// 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 Future fetchTransactionBySignature({ required String signature, required String walletAddress, - String? splTokenSymbol, + SPLToken? splToken, }) async { try { final txResponse = await _provider!.request( @@ -936,7 +903,7 @@ class SolanaWalletClient { final parsed = await parseTransaction( txResponse: versionedResponse, walletAddress: walletAddress, - splTokenSymbol: splTokenSymbol, + splToken: splToken, ); if (parsed == null) return null; @@ -985,8 +952,7 @@ class SolanaWalletClient { /// Load the Address's transactions into the account Future> fetchTransactions( SolAddress address, { - String? splTokenSymbol, - int? splTokenDecimal, + SPLToken? splToken, Commitment? commitment, SolAddress? walletAddress, required void Function(List) onUpdate, @@ -1025,7 +991,7 @@ class SolanaWalletClient { final parsedTransactionsFutures = versionedBatchResponses.map((tx) => parseTransaction( txResponse: tx, - splTokenSymbol: splTokenSymbol, + splToken: splToken, walletAddress: walletAddress?.address ?? address.address, )); @@ -1057,8 +1023,7 @@ class SolanaWalletClient { Future> getSPLTokenTransfers({ required String mintAddress, - required String splTokenSymbol, - required int splTokenDecimal, + required SPLToken splToken, required SolanaPrivateKey privateKey, required void Function(List) onUpdate, }) async { @@ -1077,23 +1042,19 @@ class SolanaWalletClient { if (associatedTokenAccount == null) return []; - final accountPublicKey = associatedTokenAccount.address; - - final tokenTransactions = await fetchTransactions( - accountPublicKey, - splTokenSymbol: splTokenSymbol, - splTokenDecimal: splTokenDecimal, + return fetchTransactions( + associatedTokenAccount.address, + splToken: splToken, walletAddress: ownerWalletAddress, onUpdate: onUpdate, ); - - return tokenTransactions; } final Map tokenInfoCache = {}; Future getTokenInfo(String mintAddress) async { if (tokenInfoCache.containsKey(mintAddress)) { + printV("Cached"); return tokenInfoCache[mintAddress]; } else { final token = await fetchSPLTokenInfo(mintAddress); @@ -1119,16 +1080,15 @@ class SolanaWalletClient { }, ); + if (response.statusCode != 200) return null; final decodedResponse = jsonDecode(response.body) as Map; - final symbol = (decodedResponse['symbol'] ?? '') as String; - + final symbol = decodedResponse['symbol'] ?? ''; final name = decodedResponse['name'] ?? ''; final decimal = decodedResponse['decimals'] ?? '0'; final iconPath = decodedResponse['logo'] ?? ''; - String filteredTokenSymbol = - symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', ''); + final filteredTokenSymbol = symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', ''); return SPLToken( name: name, @@ -1182,20 +1142,18 @@ class SolanaWalletClient { SolanaRPC? get getSolanaProvider => _provider; Future signSolanaTransaction({ - required String tokenTitle, - required int tokenDecimals, - required double inputAmount, + required Money inputAmount, required String destinationAddress, required SolanaPrivateKey ownerPrivateKey, required bool isSendAll, - required double solBalance, + required Money solBalance, String? tokenMint, List references = const [], }) async { const commitment = Commitment.confirmed; - if (tokenTitle == CryptoCurrency.sol.title) { - final pendingNativeTokenTransaction = await _signNativeTokenTransaction( + if (inputAmount.currency == CryptoCurrency.sol) { + return _signNativeTokenTransaction( inputAmount: inputAmount, destinationAddress: destinationAddress, ownerPrivateKey: ownerPrivateKey, @@ -1203,10 +1161,9 @@ class SolanaWalletClient { isSendAll: isSendAll, solBalance: solBalance, ); - return pendingNativeTokenTransaction; } else { - final pendingSPLTokenTransaction = _signSPLTokenTransaction( - tokenDecimals: tokenDecimals, + return _signSPLTokenTransaction( + tokenDecimals: inputAmount.currency.decimals, tokenMint: tokenMint!, inputAmount: inputAmount, ownerPrivateKey: ownerPrivateKey, @@ -1214,7 +1171,6 @@ class SolanaWalletClient { commitment: commitment, solBalance: solBalance, ); - return pendingSPLTokenTransaction; } } @@ -1229,25 +1185,24 @@ class SolanaWalletClient { Future _getMessageForNativeTransaction({ required SolanaPublicKey publicKey, required String destinationAddress, - required int lamports, + required Money lamports, required Commitment commitment, }) async { final instructions = [ SystemProgram.transfer( from: publicKey.toAddress(), - layout: SystemTransferLayout(lamports: BigInt.from(lamports)), + layout: SystemTransferLayout(lamports: lamports.amount), to: SolAddress(destinationAddress), ), ]; final latestBlockhash = await _getLatestBlockhash(commitment); - final message = Message.compile( + return Message.compile( transactionInstructions: instructions, payer: publicKey.toAddress(), recentBlockhash: latestBlockhash, ); - return message; } Future _getMessageForSPLTokenTransaction({ @@ -1256,14 +1211,14 @@ class SolanaWalletClient { required int tokenDecimals, required SolAddress mintAddress, required SolAddress sourceAccount, - required int amount, + required Money amount, required Commitment commitment, required SolAddress tokenProgramId, }) async { final instructions = [ SPLTokenProgram.transferChecked( layout: SPLTokenTransferCheckedLayout( - amount: BigInt.from(amount), + amount: amount.amount, decimals: tokenDecimals, ), mint: mintAddress, @@ -1275,61 +1230,46 @@ class SolanaWalletClient { final latestBlockhash = await _getLatestBlockhash(commitment); - final message = Message.compile( + return Message.compile( transactionInstructions: instructions, payer: ownerAddress, recentBlockhash: latestBlockhash, ); - return message; } - Future _getFeeFromCompiledMessage(Message message, Commitment commitment) async { + Future _getFeeFromCompiledMessage(Message message, Commitment commitment) { final base64Message = base64Encode(message.serialize()); - - final fee = await getFeeForMessage(base64Message, commitment); - - return fee; + return getFeeForMessage(base64Message, commitment); } Future hasSufficientFundsLeftForRent({ - required double inputAmount, - required double solBalance, - required double fee, + required Money inputAmount, + required Money solBalance, + required Money fee, }) async { final rent = await _provider!.request( - SolanaRPCGetMinimumBalanceForRentExemption( - size: SolanaTokenAccountUtils.accountSize, - ), + SolanaRPCGetMinimumBalanceForRentExemption(size: SolanaTokenAccountUtils.accountSize), ); - final rentInSol = (rent.toDouble() / SolanaUtils.lamportsPerSol).toDouble(); - - final remnant = solBalance - (inputAmount + fee); - - if (remnant > rentInSol) return true; - - return false; + return (solBalance - (inputAmount + fee)) > Money(rent, CryptoCurrency.sol); } Future _signNativeTokenTransaction({ - required double inputAmount, + required Money inputAmount, required String destinationAddress, required SolanaPrivateKey ownerPrivateKey, required Commitment commitment, required bool isSendAll, - required double solBalance, + required Money solBalance, }) async { - // Convert SOL to lamport - int lamports = (inputAmount * SolanaUtils.lamportsPerSol).toInt(); - - Message message = await _getMessageForNativeTransaction( + final message = await _getMessageForNativeTransaction( publicKey: ownerPrivateKey.publicKey(), destinationAddress: destinationAddress, - lamports: lamports, + lamports: inputAmount, commitment: commitment, ); - SolAddress latestBlockhash = await _getLatestBlockhash(commitment); + final latestBlockhash = await _getLatestBlockhash(commitment); final fee = await _getFeeFromCompiledMessage( message, @@ -1337,7 +1277,7 @@ class SolanaWalletClient { ); if (!isSendAll) { - bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent( + final hasSufficientFundsLeft = await hasSufficientFundsLeftForRent( inputAmount: inputAmount, fee: fee, solBalance: solBalance, @@ -1350,8 +1290,7 @@ class SolanaWalletClient { String serializedTransaction; if (isSendAll) { - final feeInLamports = (fee * SolanaUtils.lamportsPerSol).toInt(); - final updatedLamports = lamports - feeInLamports; + final updatedLamports = inputAmount - fee; final transaction = _constructNativeTransaction( ownerPrivateKey: ownerPrivateKey, @@ -1369,7 +1308,7 @@ class SolanaWalletClient { ownerPrivateKey: ownerPrivateKey, destinationAddress: destinationAddress, latestBlockhash: latestBlockhash, - lamports: lamports, + lamports: inputAmount, ); serializedTransaction = await _signTransactionInternal( @@ -1383,33 +1322,29 @@ class SolanaWalletClient { commitment: commitment, ); - final pendingTransaction = PendingSolanaTransaction( + return PendingSolanaTransaction( amount: inputAmount, serializedTransaction: serializedTransaction, destinationAddress: destinationAddress, sendTransaction: sendTx, fee: fee, ); - - return pendingTransaction; } SolanaTransaction _constructNativeTransaction({ required SolanaPrivateKey ownerPrivateKey, required String destinationAddress, required SolAddress latestBlockhash, - required int lamports, + required Money lamports, }) { final owner = ownerPrivateKey.publicKey().toAddress(); - /// Create a transfer instruction to move funds from the owner to the receiver. final transferInstruction = SystemProgram.transfer( from: owner, - layout: SystemTransferLayout(lamports: BigInt.from(lamports)), + layout: SystemTransferLayout(lamports: lamports.amount), to: SolAddress(destinationAddress), ); - /// Construct a Solana transaction with the transfer instruction. return SolanaTransaction( instructions: [transferInstruction], recentBlockhash: latestBlockhash, @@ -1609,17 +1544,13 @@ class SolanaWalletClient { Future _signSPLTokenTransaction({ required int tokenDecimals, required String tokenMint, - required double inputAmount, + required Money inputAmount, required String destinationAddress, required SolanaPrivateKey ownerPrivateKey, required Commitment commitment, - required double solBalance, + required Money solBalance, }) async { final mintAddress = SolAddress(tokenMint); - - // Input by the user - final amount = (inputAmount * math.pow(10, tokenDecimals)).toInt(); - final tokenProgramId = await _getTokenProgramId(mintAddress); ProgramDerivedAddress? associatedSenderAccount; @@ -1751,7 +1682,7 @@ class SolanaWalletClient { destination: associatedRecipientAccount.address, mint: mintAddress, owner: ownerPrivateKey.publicKey().toAddress(), - amount: BigInt.from(amount), + amount: inputAmount.amount, decimals: tokenDecimals, ); @@ -1769,75 +1700,61 @@ class SolanaWalletClient { mintAddress: mintAddress, destinationAddress: associatedRecipientAccount.address, sourceAccount: associatedSenderAccount.address, - amount: amount, + amount: inputAmount, commitment: commitment, tokenProgramId: tokenProgramId, ); final fee = await _getFeeFromCompiledMessage(message, commitment); - bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent( - inputAmount: 0, + final hasSufficientFundsLeft = await hasSufficientFundsLeftForRent( + inputAmount: Money.zero(CryptoCurrency.sol), fee: fee, solBalance: solBalance, ); - if (!hasSufficientFundsLeft) { - throw SolanaSignSPLTokenTransactionRentException(); - } + if (!hasSufficientFundsLeft) throw SolanaSignSPLTokenTransactionRentException(); final serializedTransaction = await _signTransactionInternal( ownerPrivateKey: ownerPrivateKey, transaction: transaction, ); - sendTx() async => await sendTransaction( + sendTx() => sendTransaction( serializedTransaction: serializedTransaction, commitment: commitment, ); - final pendingTransaction = PendingSolanaTransaction( + return PendingSolanaTransaction( amount: inputAmount, serializedTransaction: serializedTransaction, destinationAddress: destinationAddress, sendTransaction: sendTx, fee: fee, ); - return pendingTransaction; } Future _signTransactionInternal({ required SolanaPrivateKey ownerPrivateKey, required SolanaTransaction transaction, }) async { - /// Sign the transaction with the owner's private key. final ownerSignature = ownerPrivateKey.sign(transaction.serializeMessage()); transaction.addSignature(ownerPrivateKey.publicKey().toAddress(), ownerSignature); - /// Serialize the transaction. - final serializedTransaction = transaction.serializeString(); - - return serializedTransaction; + return transaction.serializeString(); } Future sendTransaction({ required String serializedTransaction, - required Commitment commitment, - }) async { - try { - /// Send the transaction to the Solana network. - final signature = await _provider!.request( + required Commitment commitment + }) => + _provider!.request( SolanaRPCSendTransaction( encodedTransaction: serializedTransaction, commitment: commitment, ), ); - return signature; - } catch (e) { - throw Exception(e); - } - } Future getIconImageFromTokenUri(String uri) async { if (uri.isEmpty || uri == '…') return null; diff --git a/cw_solana/lib/solana_transaction_info.dart b/cw_solana/lib/solana_transaction_info.dart index 85c28ac07..94caeefa9 100644 --- a/cw_solana/lib/solana_transaction_info.dart +++ b/cw_solana/lib/solana_transaction_info.dart @@ -1,80 +1,69 @@ -import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; class SolanaTransactionInfo extends TransactionInfo { SolanaTransactionInfo({ required this.id, - required this.blockTime, + required this.date, required this.to, required this.from, required this.direction, - required this.solAmount, - this.tokenSymbol = "SOL", + required this.amount, required this.isPending, - required this.txFee, - }) : amount = solAmount.toInt(); + required this.fee, + }); + @override final String id; + @override final String? to; + @override final String? from; - final int amount; - final bool isPending; - final double solAmount; - final String tokenSymbol; - final DateTime blockTime; - final double txFee; - final TransactionDirection direction; - - String? _fiatAmount; @override String get txHash => id.replaceFirst(RegExp(r'_(outgoing|incoming)$'), ''); @override - DateTime get date => blockTime; - + final Money amount; @override - String amountFormatted() { - String stringBalance = solAmount.toString(); - if (stringBalance.toString().length >= 12) { - stringBalance = stringBalance.substring(0, 12); - } - return '$stringBalance $tokenSymbol'; - } - + final bool isPending; @override - String fiatAmount() => _fiatAmount ?? ''; - + final Money fee; @override - void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); - + final TransactionDirection direction; @override - String feeFormatted() => '${txFee.toString()} SOL'; + final DateTime date; factory SolanaTransactionInfo.fromJson(Map data) { + final symbol = data['tokenSymbol'] as String? ?? "SOL"; + final decimals = data['tokenDecimals'] as int? ?? 6; + + final currency = CryptoCurrency(name: symbol, title: symbol, decimals: decimals); + return SolanaTransactionInfo( id: data['id'] as String, - solAmount: data['solAmount'], + amount: Money.parse(data['solAmount'].toString(), currency), direction: parseTransactionDirectionFromInt(data['direction'] as int), - blockTime: DateTime.fromMillisecondsSinceEpoch(data['blockTime'] as int), + date: DateTime.fromMillisecondsSinceEpoch(data['blockTime'] as int), isPending: data['isPending'] as bool, - tokenSymbol: data['tokenSymbol'] as String, to: data['to'], from: data['from'], - txFee: data['txFee'], + fee: Money.parse(data['txFee'], CryptoCurrency.sol), ); } Map toJson() => { 'id': id, - 'solAmount': solAmount, + 'solAmount': amount.toString(), 'direction': direction.index, - 'blockTime': blockTime.millisecondsSinceEpoch, + 'blockTime': date.millisecondsSinceEpoch, 'isPending': isPending, - 'tokenSymbol': tokenSymbol, + 'tokenSymbol': amount.currency.symbol, + 'tokenDecimals': amount.currency.decimals, 'to': to, 'from': from, - 'txFee': txFee, + 'txFee': fee.toString(), }; } diff --git a/cw_solana/lib/solana_transaction_model.dart b/cw_solana/lib/solana_transaction_model.dart index c16c49258..6086ccb8a 100644 --- a/cw_solana/lib/solana_transaction_model.dart +++ b/cw_solana/lib/solana_transaction_model.dart @@ -1,3 +1,5 @@ +import 'package:cw_core/amount/money.dart'; + class SolanaTransactionModel { final String id; @@ -5,22 +7,16 @@ class SolanaTransactionModel { final String to; - final double amount; + final Money amount; - // If this is an outgoing transaction final bool isOutgoingTx; // The Program ID of this transaction, e.g, System Program, Token Program... final String programId; - // The DateTime from the UNIX timestamp of the block where the transaction was included final DateTime blockTime; - // The Transaction fee - final double fee; - - // The token symbol - final String tokenSymbol; + final Money fee; SolanaTransactionModel({ required this.id, @@ -30,18 +26,6 @@ class SolanaTransactionModel { required this.programId, required int blockTimeInInt, this.isOutgoingTx = false, - required this.tokenSymbol, required this.fee, }) : blockTime = DateTime.fromMillisecondsSinceEpoch(blockTimeInInt * 1000); - - factory SolanaTransactionModel.fromJson(Map json) => SolanaTransactionModel( - id: json['id'], - blockTimeInInt: int.parse(json["timeStamp"]) * 1000, - from: json["from"], - to: json["to"], - amount: double.parse(json["value"]), - programId: json["programId"], - fee: json['fee'], - tokenSymbol: json['tokenSymbol'], - ); } diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 76f2d8924..48a1b0e71 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; @@ -57,7 +58,7 @@ abstract class SolanaWalletBase _client = SolanaWalletClient(), walletAddresses = SolanaWalletAddresses(walletInfo), balance = ObservableMap.of( - {CryptoCurrency.sol: initialBalance ?? SolanaBalance(BigInt.zero.toDouble(), false)}), + {CryptoCurrency.sol: initialBalance ?? SolanaBalance.zero(CryptoCurrency.sol)}), super(walletInfo, derivationInfo) { this.walletInfo = walletInfo; transactionHistory = SolanaTransactionHistory( @@ -83,7 +84,7 @@ abstract class SolanaWalletBase SolanaWalletClient get client => _client; @observable - double? estimatedFee; + Money? estimatedFee; Timer? _transactionsUpdateTimer; @@ -214,9 +215,8 @@ abstract class SolanaWalletBase Future _getEstimatedFees() async { try { estimatedFee = await _client.getEstimatedFee(_solanaPublicKey, Commitment.confirmed); - printV(estimatedFee.toString()); } catch (e) { - estimatedFee = 0.0; + estimatedFee = Money.zero(currency); } } @@ -237,27 +237,16 @@ abstract class SolanaWalletBase orElse: () => throw Exception( 'Currency ${credentials.currency.title} ${credentials.currency.tag} is not accessible in the wallet, try to enable it first.')); - final walletBalanceForCurrency = balance[transactionCurrency]!.balance; + final walletBalanceForCurrency = balance[transactionCurrency]!.available; - final solBalance = balance[CryptoCurrency.sol]!.balance; + final solBalance = balance[CryptoCurrency.sol]!.available; - double totalAmount = 0.0; - - bool isSendAll = false; + var totalAmount = Money.zero(transactionCurrency); + var isSendAll = false; if (hasMultiDestination) { - if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { - throw SolanaTransactionWrongBalanceException(transactionCurrency); - } - - final totalAmountFromCredentials = - outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); - - totalAmount = totalAmountFromCredentials.toDouble(); - - if (walletBalanceForCurrency < totalAmount) { - throw SolanaTransactionWrongBalanceException(transactionCurrency); - } + // Solana doesn't have multi destination right now + throw SolanaTransactionCreationException(transactionCurrency); } else { final output = outputs.first; @@ -266,9 +255,7 @@ abstract class SolanaWalletBase if (isSendAll) { totalAmount = walletBalanceForCurrency; } else { - final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0'); - - totalAmount = totalOriginalAmount; + totalAmount = output.cryptoAmount.copyWith(currency: transactionCurrency); } if (walletBalanceForCurrency < totalAmount) { @@ -282,20 +269,16 @@ abstract class SolanaWalletBase tokenMint = (transactionCurrency as SPLToken).mintAddress; } - final pendingSolanaTransaction = await _client.signSolanaTransaction( + return _client.signSolanaTransaction( tokenMint: tokenMint, - tokenTitle: transactionCurrency.title, inputAmount: totalAmount, ownerPrivateKey: _solanaPrivateKey, - tokenDecimals: transactionCurrency.decimals, destinationAddress: solCredentials.outputs.first.isParsedAddress ? solCredentials.outputs.first.extractedAddress! : solCredentials.outputs.first.address, isSendAll: isSendAll, solBalance: solBalance, ); - - return pendingSolanaTransaction; } @override @@ -387,8 +370,7 @@ abstract class SolanaWalletBase try { return await _client.getSPLTokenTransfers( mintAddress: token.mintAddress, - splTokenSymbol: token.symbol, - splTokenDecimal: token.decimal, + splToken: token, privateKey: _solanaPrivateKey, onUpdate: updateTransactions, ); @@ -414,14 +396,13 @@ abstract class SolanaWalletBase id: transactionModel.id, to: transactionModel.to, from: transactionModel.from, - blockTime: transactionModel.blockTime, + date: transactionModel.blockTime, direction: transactionModel.isOutgoingTx ? TransactionDirection.outgoing : TransactionDirection.incoming, - solAmount: transactionModel.amount, + amount: transactionModel.amount, isPending: false, - txFee: transactionModel.fee, - tokenSymbol: transactionModel.tokenSymbol, + fee: transactionModel.fee, ); } @@ -497,7 +478,8 @@ abstract class SolanaWalletBase if (!hasKeysFile) rethrow; } - final balance = SolanaBalance.fromJSON(data?['balance'] as String?, false) ?? SolanaBalance(0.0, false); + final balance = SolanaBalance.fromJSON(data?['balance'] as String?, CryptoCurrency.sol) ?? + SolanaBalance.zero(CryptoCurrency.sol); final WalletKeysData keysData; // Migrate wallet from the old scheme to then new .keys file scheme @@ -545,7 +527,7 @@ abstract class SolanaWalletBase Future _fetchSOLBalance() async { final balance = await _client.getBalance(solanaAddress); - return SolanaBalance(balance, false); + return SolanaBalance(balance); } /// Internal helper to update SPL token balances. @@ -577,7 +559,7 @@ abstract class SolanaWalletBase final results = await Future.wait(batch.map((token) async { try { - final fetched = await _client.getSplTokenBalance(token.mintAddress, solanaAddress); + final fetched = await _client.getSplTokenBalance(token, solanaAddress); return MapEntry(token, fetched); } catch (e) { printV('Error fetching spl token (${token.symbol}) balance ${e.toString()}'); @@ -588,8 +570,7 @@ abstract class SolanaWalletBase for (final entry in results) { final token = entry.key; final fetchedBalance = entry.value; - final currentBalance = balance[token] ?? - SolanaBalance.forToken(BigInt.zero, 0.0); + final currentBalance = balance[token] ?? SolanaBalance.zero(token); balance[token] = fetchedBalance ?? currentBalance; } } @@ -605,7 +586,7 @@ abstract class SolanaWalletBase await _client.getBalance(solanaAddress, throwOnError: true); // Check USDC token balance - const usdcMintAddress = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; + final usdcMintAddress = DefaultSPLTokens().usdc; await _client.getSplTokenBalance(usdcMintAddress, solanaAddress, throwOnError: true); return true; @@ -694,9 +675,8 @@ abstract class SolanaWalletBase await splTokensBox.put(token.mintAddress, token); if (token.enabled) { - final tokenBalance = await _client.getSplTokenBalance(token.mintAddress, solanaAddress) ?? - balance[token] ?? - SolanaBalance.forToken(BigInt.zero, 0.0); + final tokenBalance = await _client.getSplTokenBalance(token, solanaAddress) ?? + balance[token] ?? SolanaBalance.zero(token); balance[token] = tokenBalance; } else { @@ -715,7 +695,8 @@ abstract class SolanaWalletBase } Future _removeTokenTransactionsInHistory(SPLToken token) async { - transactionHistory.transactions.removeWhere((key, value) => value.tokenSymbol == token.title); + transactionHistory.transactions + .removeWhere((key, value) => value.amount.currency.symbol == token.symbol); await transactionHistory.save(); } diff --git a/cw_tron/lib/default_tron_tokens.dart b/cw_tron/lib/default_tron_tokens.dart index 49c837523..ceafc6fc0 100644 --- a/cw_tron/lib/default_tron_tokens.dart +++ b/cw_tron/lib/default_tron_tokens.dart @@ -104,4 +104,6 @@ class DefaultTronTokens { return TronToken.copyWith(token, icon: iconPath, tag: 'TRX'); }).toList(); + + TronToken get usdt => _defaultTokens.where((e) => e.symbol == "USDT").first; } diff --git a/cw_tron/lib/pending_tron_transaction.dart b/cw_tron/lib/pending_tron_transaction.dart index 5a1afc527..7eac73494 100644 --- a/cw_tron/lib/pending_tron_transaction.dart +++ b/cw_tron/lib/pending_tron_transaction.dart @@ -1,13 +1,10 @@ - - +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:web3dart/crypto.dart'; class PendingTronTransaction with PendingTransaction { final Function sendTransaction; final List signedTransaction; - final String fee; - final String amount; PendingTronTransaction({ required this.sendTransaction, @@ -18,17 +15,17 @@ class PendingTronTransaction with PendingTransaction { }); @override - String get amountFormatted => amount; + final Money amount; + + @override + final Money fee; + + @override + String get amountFormatted => amount.toString(); @override Future commit() async => await sendTransaction(); - @override - String get feeFormatted => "$feeFormattedValue TRX"; - - @override - String get feeFormattedValue => fee; - @override String get hex => bytesToHex(signedTransaction); diff --git a/cw_tron/lib/tron_balance.dart b/cw_tron/lib/tron_balance.dart index 6c28ad337..c01c439ba 100644 --- a/cw_tron/lib/tron_balance.dart +++ b/cw_tron/lib/tron_balance.dart @@ -1,25 +1,25 @@ import 'dart:convert'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/balance.dart'; +import 'package:cw_core/currency.dart'; class TronBalance extends Balance { TronBalance(this.balance) : super(balance, balance); - final BigInt balance; + final Money balance; - String toJSON() => json.encode({ 'balance': balance.toString() }); + String toJSON() => json.encode({ 'balance': balance.amount.toString() }); - static TronBalance? fromJSON(String? jsonSource) { - if (jsonSource == null) { - return null; - } + static TronBalance? fromJSON(String? jsonSource, Currency currency) { + if (jsonSource == null) return null; final decoded = json.decode(jsonSource) as Map; try { - return TronBalance(BigInt.parse(decoded['balance'])); + return TronBalance(Money(BigInt.parse(decoded['balance']), currency)); } catch (e) { - return TronBalance(BigInt.zero); + return TronBalance(Money.zero(currency)); } } } diff --git a/cw_tron/lib/tron_client.dart b/cw_tron/lib/tron_client.dart index 18d797655..4b1ac4725 100644 --- a/cw_tron/lib/tron_client.dart +++ b/cw_tron/lib/tron_client.dart @@ -3,9 +3,12 @@ import 'dart:convert'; import 'dart:developer'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:cw_tron/default_tron_tokens.dart'; import 'package:cw_tron/pending_tron_transaction.dart'; import 'package:cw_tron/tron_abi.dart'; import 'package:cw_tron/tron_balance.dart'; @@ -234,26 +237,18 @@ class TronClient { } Future getTRCEstimatedFee(TronAddress ownerAddress) async { - String contractAddress = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'; - String constantAmount = - '0'; // We're using 0 as the base amount here as we get an error when balance is zero i.e for new wallets. + final usdtContractAddress = DefaultTronTokens().usdt.contractAddress; final contract = ContractABI.fromJson(trc20Abi, isTron: true); - final function = contract.functionFromName("transfer"); - /// address /// amount - final transferparams = [ - ownerAddress, - TronHelper.toSun(constantAmount), - ]; - - final contractAddr = TronAddress(contractAddress); + // We're using 0 as the base amount here as we get an error when balance is zero i.e for new wallets. + final transferParams = [ownerAddress, BigInt.zero]; final request = await _provider!.request( TronRequestTriggerConstantContract( ownerAddress: ownerAddress, - contractAddress: contractAddr, - data: function.encodeHex(transferparams), + contractAddress: TronAddress(usdtContractAddress), + data: function.encodeHex(transferParams), ), ); @@ -261,21 +256,19 @@ class TronClient { log("Tron TRC20 error: ${request.error} \n ${request.respose}"); } - final feeLimit = await getFeeLimit( + return getFeeLimit( request.transactionRaw!, ownerAddress, ownerAddress, energyUsed: request.energyUsed ?? 0, isEstimatedFeeFlow: true, ); - return feeLimit; } Future signTransaction({ required TronPrivateKey ownerPrivKey, required String toAddress, - required String amount, - required CryptoCurrency currency, + required Money amount, required BigInt tronBalance, required bool sendAll, }) async { @@ -285,9 +278,9 @@ class TronClient { // Define the receiving Tron address for the transaction. final receiverAddress = TronAddress(toAddress); - bool isNativeTransaction = currency == CryptoCurrency.trx; + final isNativeTransaction = amount.currency == CryptoCurrency.trx; - String totalAmount; + Money totalAmount; TransactionRaw rawTransaction; if (isNativeTransaction) { if (sendAll) { @@ -300,12 +293,10 @@ class TronClient { if (availableBandWidth >= 269) { totalAmount = amount; } else { - final amountInSun = TronHelper.toSun(amount).toInt(); - // 5000 added here is a buffer since we're working with "estimated" value of the fee. - final result = amountInSun - (_nativeTxEstimatedFee + 5000); + final result = amount.amount - BigInt.from(_nativeTxEstimatedFee + 5000); - totalAmount = TronHelper.fromSun(BigInt.from(result)); + totalAmount = amount.copyWith(amount: result); } } else { totalAmount = amount; @@ -318,7 +309,7 @@ class TronClient { sendAll, ); } else { - final tokenAddress = (currency as TronToken).contractAddress; + final tokenAddress = (amount.currency as TronToken).contractAddress; totalAmount = amount; rawTransaction = await _signTrcTokenTransaction( ownerAddress, @@ -339,7 +330,7 @@ class TronClient { return PendingTronTransaction( signedTransaction: signature, amount: totalAmount, - fee: TronHelper.fromSun(rawTransaction.feeLimit ?? BigInt.zero), + fee: Money(rawTransaction.feeLimit ?? BigInt.zero, CryptoCurrency.trx), sendTransaction: sendTx, id: rawTransaction.txID ); @@ -348,7 +339,7 @@ class TronClient { Future _signNativeTransaction( TronAddress ownerAddress, TronAddress receiverAddress, - String amount, + Money amount, BigInt tronBalance, bool sendAll, ) async { @@ -359,7 +350,7 @@ class TronClient { final block = await _provider!.request(TronRequestGetNowBlock()); // Create the transfer contract final contract = TransferContract( - amount: TronHelper.toSun(amount), + amount: amount.amount, ownerAddress: ownerAddress, toAddress: receiverAddress, ); @@ -374,8 +365,7 @@ class TronClient { // Set the transaction expiration time (maximum 24 hours) final expireTime = DateTime.now().add(const Duration(minutes: 30)); - // Create a raw transaction - TransactionRaw rawTransaction = TransactionRaw( + final rawTransaction = TransactionRaw( refBlockBytes: block.blockHeader.rawData.refBlockBytes, refBlockHash: block.blockHeader.rawData.refBlockHash, expiration: BigInt.from(expireTime.millisecondsSinceEpoch), @@ -394,45 +384,31 @@ class TronClient { ); } - rawTransaction = rawTransaction.copyWith( - feeLimit: BigInt.from(feeLimitToUse), - ); - - return rawTransaction; + return rawTransaction.copyWith(feeLimit: BigInt.from(feeLimitToUse)); } Future _signTrcTokenTransaction( TronAddress ownerAddress, TronAddress receiverAddress, - String amount, + Money amount, String contractAddress, BigInt tronBalance, ) async { final contract = ContractABI.fromJson(trc20Abi, isTron: true); final function = contract.functionFromName("transfer"); - - /// address /// amount - final transferparams = [ - receiverAddress, - TronHelper.toSun(amount), - ]; - - final contractAddr = TronAddress(contractAddress); - + final transferParams = [receiverAddress, amount.amount]; final request = await _provider!.request( TronRequestTriggerConstantContract( ownerAddress: ownerAddress, - contractAddress: contractAddr, - data: function.encodeHex(transferparams), + contractAddress: TronAddress(contractAddress), + data: function.encodeHex(transferParams), ), ); if (!request.isSuccess) { log("Tron TRC20 error: ${request.error} \n ${request.respose}"); - throw Exception( - 'An error occurred while creating the transfer request. Please try again.', - ); + throw Exception('An error occurred while creating the transfer request. Please try again.'); } final feeLimit = await getFeeLimit( @@ -442,20 +418,14 @@ class TronClient { energyUsed: request.energyUsed ?? 0, ); - final tronBalanceInt = tronBalance.toInt(); - - if (feeLimit > tronBalanceInt) { + if (feeLimit > tronBalance.toInt()) { final feeInTrx = TronHelper.fromSun(BigInt.parse(feeLimit.toString())); throw Exception( 'You don\'t have enough TRX to cover the transaction fee for this transaction. Please top up. Transaction fee: $feeInTrx TRX', ); } - final rawTransaction = request.transactionRaw!.copyWith( - feeLimit: BigInt.from(feeLimit), - ); - - return rawTransaction; + return request.transactionRaw!.copyWith(feeLimit: BigInt.from(feeLimit)); } Future sendTransaction({ @@ -480,20 +450,18 @@ class TronClient { } } - Future fetchTronTokenBalances(String userAddress, String contractAddress, {bool throwOnError = false}) async { + Future fetchTronTokenBalances(String userAddress, String contractAddress, + {bool throwOnError = false, required Currency currency}) async { try { final ownerAddress = TronAddress(userAddress); - final tokenAddress = TronAddress(contractAddress); - final contract = ContractABI.fromJson(trc20Abi, isTron: true); - final function = contract.functionFromName("balanceOf"); final request = await _provider!.request( TronRequestTriggerConstantContract.fromMethod( ownerAddress: ownerAddress, - contractAddress: tokenAddress, + contractAddress: TronAddress(contractAddress), function: function, params: [ownerAddress], ), @@ -501,12 +469,12 @@ class TronClient { final outputResult = request.outputResult?.first ?? BigInt.zero; - return TronBalance(outputResult); + return TronBalance(Money(outputResult, currency)); } catch (_) { if (throwOnError) { rethrow; } - return TronBalance(BigInt.zero); + return TronBalance(Money.zero(currency)); } } diff --git a/cw_tron/lib/tron_transaction_info.dart b/cw_tron/lib/tron_transaction_info.dart index 28c704d20..ed9e40656 100644 --- a/cw_tron/lib/tron_transaction_info.dart +++ b/cw_tron/lib/tron_transaction_info.dart @@ -1,41 +1,55 @@ -import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; -import 'package:on_chain/on_chain.dart' as onchain; import 'package:on_chain/tron/tron.dart'; class TronTransactionInfo extends TransactionInfo { TronTransactionInfo({ required this.id, - required this.tronAmount, - required this.txFee, + required this.amount, + required this.fee, required this.direction, required this.blockTime, required this.to, required this.from, required this.isPending, - this.tokenSymbol = 'TRX', - }) : amount = tronAmount.toInt(); + }); + @override final String id; + + @override final String? to; + + @override final String? from; - final int amount; - final BigInt tronAmount; - final String tokenSymbol; - final DateTime blockTime; + + @override + final Money amount; + + @override + final Money? fee; + + @override final bool isPending; - final int? txFee; + + @override final TransactionDirection direction; + final DateTime blockTime; + factory TronTransactionInfo.fromJson(Map data) { + final tokenSymbol = data['tokenSymbol'] as String; + final decimals = data['decimals'] as int? ?? CryptoCurrency.trx.decimals; + final currency = CryptoCurrency(name: tokenSymbol, title: tokenSymbol, decimals: decimals); + return TronTransactionInfo( id: data['id'] as String, - tronAmount: BigInt.parse(data['tronAmount']), - txFee: data['txFee'], + amount: Money(BigInt.parse(data['tronAmount']), currency), + fee: Money.tryParse(data['txFee']?.toString() ?? '0', CryptoCurrency.trx, isBaseUnit: true), direction: parseTransactionDirectionFromInt(data['direction'] as int), blockTime: DateTime.fromMillisecondsSinceEpoch(data['blockTime'] as int), - tokenSymbol: data['tokenSymbol'] as String, to: data['to'], from: data['from'], isPending: data['isPending'], @@ -44,41 +58,20 @@ class TronTransactionInfo extends TransactionInfo { Map toJson() => { 'id': id, - 'tronAmount': tronAmount.toString(), - 'txFee': txFee, + 'tronAmount': amount.amount.toString(), + 'txFee': fee?.amount.toString(), 'direction': direction.index, 'blockTime': blockTime.millisecondsSinceEpoch, - 'tokenSymbol': tokenSymbol, 'to': to, 'from': from, 'isPending': isPending, + 'tokenSymbol': amount.currency.symbol, + 'decimals': amount.currency.decimals }; @override DateTime get date => blockTime; - String? _fiatAmount; - - @override - String amountFormatted() { - String formattedAmount = _rawAmountAsString(tronAmount); - - return '$formattedAmount $tokenSymbol'; - } - - @override - String fiatAmount() => _fiatAmount ?? ''; - - @override - void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); - - @override - String feeFormatted() { - final formattedFee = onchain.TronHelper.fromSun(BigInt.from(txFee ?? 0)); - - return '$formattedFee TRX'; - } - String _rawAmountAsString(BigInt amount) { String formattedAmount = TronHelper.fromSun(amount); @@ -89,5 +82,5 @@ class TronTransactionInfo extends TransactionInfo { return formattedAmount; } - String rawTronAmount() => _rawAmountAsString(tronAmount); + String rawTronAmount() => _rawAmountAsString(amount.amount); } diff --git a/cw_tron/lib/tron_transaction_model.dart b/cw_tron/lib/tron_transaction_model.dart index 1748adc53..5aca17b74 100644 --- a/cw_tron/lib/tron_transaction_model.dart +++ b/cw_tron/lib/tron_transaction_model.dart @@ -1,4 +1,5 @@ import 'package:blockchain_utils/hex/hex.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:on_chain/on_chain.dart'; class TronTRC20TransactionModel extends TronTransactionModel { @@ -6,6 +7,11 @@ class TronTRC20TransactionModel extends TronTransactionModel { String? tokenSymbol; + int? decimals; + + CryptoCurrency get currency => CryptoCurrency( + name: tokenSymbol ?? "TRX", title: tokenSymbol ?? "TRX", decimals: decimals ?? 6); + int? timestamp; @override @@ -40,6 +46,7 @@ class TronTRC20TransactionModel extends TronTransactionModel { TronTRC20TransactionModel.fromJson(Map json) { transactionId = json['transaction_id']; tokenSymbol = json['token_info'] != null ? json['token_info']['symbol'] : null; + decimals = json['token_info'] != null ? json['token_info']['decimals'] : null; timestamp = json['block_timestamp']; from = json['from']; to = json['to']; diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index c8d9fb929..1a4d98b48 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:bip39/bip39.dart' as bip39; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; @@ -56,7 +57,7 @@ abstract class TronWalletBase _client = TronClient(), walletAddresses = TronWalletAddresses(walletInfo), balance = ObservableMap.of( - {CryptoCurrency.trx: initialBalance ?? TronBalance(BigInt.zero)}, + {CryptoCurrency.trx: initialBalance ?? TronBalance(Money.zero(CryptoCurrency.trx))}, ), super(walletInfo, derivationInfo) { this.walletInfo = walletInfo; @@ -93,10 +94,10 @@ abstract class TronWalletBase WalletAddresses walletAddresses; @observable - String? nativeTxEstimatedFee; + Money? nativeTxEstimatedFee; @observable - String? trc20EstimatedFee; + Money? trc20EstimatedFee; @override @observable @@ -145,7 +146,8 @@ abstract class TronWalletBase if (!hasKeysFile) rethrow; } - final balance = TronBalance.fromJSON(data?['balance'] as String?) ?? TronBalance(BigInt.zero); + final balance = TronBalance.fromJSON(data?['balance'] as String?, CryptoCurrency.trx) ?? + TronBalance(Money.zero(CryptoCurrency.trx)); final WalletKeysData keysData; // Migrate wallet from the old scheme to then new .keys file scheme @@ -253,10 +255,10 @@ abstract class TronWalletBase Future _getEstimatedFees() async { final nativeFee = await _getNativeTxFee(); - nativeTxEstimatedFee = TronHelper.fromSun(BigInt.from(nativeFee)); + nativeTxEstimatedFee = Money.fromInt(nativeFee, currency); final trc20Fee = await _getTrc20TxFee(); - trc20EstimatedFee = TronHelper.fromSun(BigInt.from(trc20Fee)); + trc20EstimatedFee = Money.fromInt(trc20Fee, currency); log('Native Estimated Fee: $nativeTxEstimatedFee'); log('TRC20 Estimated Fee: $trc20EstimatedFee'); @@ -321,21 +323,11 @@ abstract class TronWalletBase final walletBalanceForCurrency = balance[transactionCurrency]!.balance; - BigInt totalAmount = BigInt.zero; - bool shouldSendAll = false; + var totalAmount = Money.zero(transactionCurrency); + var shouldSendAll = false; if (hasMultiDestination) { - if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + // Tron does not have multi Destination right now throw TronTransactionCreationException(transactionCurrency); - } - - final totalAmountFromCredentials = - outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); - - totalAmount = BigInt.from(totalAmountFromCredentials); - - if (walletBalanceForCurrency < totalAmount) { - throw TronTransactionCreationException(transactionCurrency); - } } else { final output = outputs.first; @@ -344,25 +336,23 @@ abstract class TronWalletBase if (shouldSendAll) { totalAmount = walletBalanceForCurrency; } else { - final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0'); - totalAmount = TronHelper.toSun(totalOriginalAmount.toString()); + totalAmount = output.cryptoAmount.copyWith(currency: transactionCurrency); } - if (walletBalanceForCurrency < totalAmount || totalAmount < BigInt.zero) { + if (walletBalanceForCurrency < totalAmount || totalAmount < Money.zero(transactionCurrency)) { throw TronTransactionCreationException(transactionCurrency); } } - final tronBalance = balance[CryptoCurrency.trx]?.balance ?? BigInt.zero; + final tronBalance = balance[CryptoCurrency.trx]?.balance ?? Money.zero(CryptoCurrency.trx); final pendingTransaction = await _client.signTransaction( ownerPrivKey: _tronPrivateKey, toAddress: tronCredentials.outputs.first.isParsedAddress ? tronCredentials.outputs.first.extractedAddress! : tronCredentials.outputs.first.address, - amount: TronHelper.fromSun(totalAmount), - currency: transactionCurrency, - tronBalance: tronBalance, + amount: totalAmount, + tronBalance: tronBalance.amount, sendAll: shouldSendAll, ); @@ -394,33 +384,41 @@ abstract class TronWalletBase continue; } - // Filter out spam transaactions that involve receiving TRC10 assets transaction, we deal with TRX and TRC20 transactions + // Filter out spam transactions that involve receiving TRC10 assets transaction, we deal with TRX and TRC20 transactions if (transactionModel.contracts?.first.type == "TransferAssetContract") { continue; } - String? tokenSymbol; + var txCurrency = currency; if (transactionModel.contractAddress != null) { final tokenAddress = TronAddress(transactionModel.contractAddress!); - tokenSymbol = (await _client.getTokenDetail( + final tokenSymbol = (await _client.getTokenDetail( contract, "symbol", ownerAddress, tokenAddress, ) as String?) ?? ''; + + final decimals = (await _client.getTokenDetail( + contract, + "decimals", + ownerAddress, + tokenAddress, + ) as BigInt?)?.toInt() ?? txCurrency.decimals; + + txCurrency = CryptoCurrency(name: tokenSymbol, title: tokenSymbol, decimals: decimals); } result[transactionModel.hash] = TronTransactionInfo( id: transactionModel.hash, - tronAmount: transactionModel.amount ?? BigInt.zero, + amount: Money(transactionModel.amount ?? BigInt.zero, txCurrency), direction: TronAddress(transactionModel.from!, visible: false).toAddress() == address ? TransactionDirection.outgoing : TransactionDirection.incoming, blockTime: transactionModel.date, - txFee: transactionModel.fee, - tokenSymbol: tokenSymbol ?? "TRX", + fee: transactionModel.fee != null ? Money.fromInt(transactionModel.fee!, currency) : null, to: transactionModel.to, from: transactionModel.from, isPending: false, @@ -441,20 +439,19 @@ abstract class TronWalletBase final Map result = {}; - for (var transactionModel in transactions) { + for (final transactionModel in transactions) { if (transactionHistory.transactions.containsKey(transactionModel.hash)) { continue; } result[transactionModel.hash] = TronTransactionInfo( id: transactionModel.hash, - tronAmount: transactionModel.amount ?? BigInt.zero, + amount: Money(transactionModel.amount ?? BigInt.zero, transactionModel.currency), direction: transactionModel.from! == address ? TransactionDirection.outgoing : TransactionDirection.incoming, blockTime: transactionModel.date, - txFee: transactionModel.fee, - tokenSymbol: transactionModel.tokenSymbol ?? "TRX", + fee: transactionModel.fee != null ? Money.fromInt(transactionModel.fee!, currency) : null, to: transactionModel.to, from: transactionModel.from, isPending: false, @@ -514,7 +511,7 @@ abstract class TronWalletBase Future _fetchTronBalance() async { final balance = await _client.getBalance(_tronPublicKey.toAddress()); - return TronBalance(balance); + return TronBalance(Money(balance, CryptoCurrency.trx)); } Future _fetchTronTokenBalances() async { @@ -524,6 +521,7 @@ abstract class TronWalletBase balance[token] = await _client.fetchTronTokenBalances( _tronAddress, token.contractAddress, + currency: token, ); } else { balance.remove(token); @@ -542,8 +540,9 @@ abstract class TronWalletBase await _client.getBalance(_tronPublicKey.toAddress(), throwOnError: true); // Check USDT token balance - const usdtContractAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"; - await _client.fetchTronTokenBalances(_tronAddress, usdtContractAddress, throwOnError: true); + final usdtContractAddress = DefaultTronTokens().usdt.contractAddress; + await _client.fetchTronTokenBalances(_tronAddress, usdtContractAddress, + throwOnError: true, currency: CryptoCurrency.usdttrc20); return true; } catch (e) { @@ -582,6 +581,7 @@ abstract class TronWalletBase balance[newToken] = await _client.fetchTronTokenBalances( _tronAddress, newToken.contractAddress, + currency: token ); } else { balance.remove(newToken); @@ -599,7 +599,8 @@ abstract class TronWalletBase } Future _removeTokenTransactionsInHistory(TronToken token) async { - transactionHistory.transactions.removeWhere((key, value) => value.tokenSymbol == token.title); + transactionHistory.transactions + .removeWhere((key, value) => value.amount.currency.symbol == token.title); await transactionHistory.save(); } @@ -649,10 +650,9 @@ abstract class TronWalletBase @override Future verifyMessage(String message, String signature, {String? address}) async { - if (address == null) { - return false; - } - TronPublicKey pubKey = TronPublicKey.fromPersonalSignature(ascii.encode(message), signature)!; + if (address == null) return false; + + final pubKey = TronPublicKey.fromPersonalSignature(ascii.encode(message), signature)!; return pubKey.toAddress().toString() == address; } diff --git a/cw_wownero/lib/pending_wownero_transaction.dart b/cw_wownero/lib/pending_wownero_transaction.dart index b8aa7c4ed..1d1f55138 100644 --- a/cw_wownero/lib/pending_wownero_transaction.dart +++ b/cw_wownero/lib/pending_wownero_transaction.dart @@ -1,8 +1,7 @@ +import 'package:cw_core/amount/money.dart'; import 'package:cw_wownero/api/structs/pending_transaction.dart'; -import 'package:cw_wownero/api/transaction_history.dart' - as wownero_transaction_history; +import 'package:cw_wownero/api/transaction_history.dart' as wownero_transaction_history; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/amount_converter.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -19,6 +18,10 @@ class PendingWowneroTransaction with PendingTransaction { final PendingTransactionDescription pendingTransactionDescription; + Money get amount => Money.fromInt(pendingTransactionDescription.amount, CryptoCurrency.wow); + + Money get fee => Money.fromInt(pendingTransactionDescription.fee, CryptoCurrency.wow); + @override String get id => pendingTransactionDescription.hash; @@ -28,15 +31,7 @@ class PendingWowneroTransaction with PendingTransaction { String get txKey => pendingTransactionDescription.txKey; @override - String get amountFormatted => - AmountConverter.amountIntToString(CryptoCurrency.wow, pendingTransactionDescription.amount); - - @override - String get feeFormatted => "$feeFormattedValue WOW"; - - @override - String get feeFormattedValue => - AmountConverter.amountIntToString(CryptoCurrency.wow, pendingTransactionDescription.fee); + String get amountFormatted => amount.toString(); @override Future commit() async { diff --git a/cw_wownero/lib/wownero_balance.dart b/cw_wownero/lib/wownero_balance.dart new file mode 100644 index 000000000..53d717757 --- /dev/null +++ b/cw_wownero/lib/wownero_balance.dart @@ -0,0 +1,17 @@ +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/balance.dart'; +import 'package:cw_core/crypto_currency.dart'; + +class WowneroBalance extends Balance { + WowneroBalance({ + required this.fullBalance, + required Money unlockedBalance, + Money? frozen, + }) : super( + unlockedBalance, + fullBalance - unlockedBalance, + frozen: frozen ?? Money.zero(CryptoCurrency.wow), + ); + + final Money fullBalance; +} diff --git a/cw_wownero/lib/wownero_transaction_info.dart b/cw_wownero/lib/wownero_transaction_info.dart index db5345e5d..aafdc98e0 100644 --- a/cw_wownero/lib/wownero_transaction_info.dart +++ b/cw_wownero/lib/wownero_transaction_info.dart @@ -1,3 +1,5 @@ +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wownero_amount_format.dart'; import 'package:cw_core/parseBoolFromString.dart'; @@ -21,18 +23,18 @@ class WowneroTransactionInfo extends TransactionInfo { date = DateTime.fromMillisecondsSinceEpoch( (int.tryParse(map['timestamp'] as String? ?? '') ?? 0) * 1000), isPending = parseBoolFromString(map['isPending'] as String), - amount = map['amount'] as int, + amount = Money.fromInt(map['amount'] as int, CryptoCurrency.wow), accountIndex = int.parse(map['accountIndex'] as String), addressIndex = map['addressIndex'] as int, confirmations = map['confirmations'] as int, key = getTxKey((map['hash'] ?? '') as String), - fee = map['fee'] as int? ?? 0 { - additionalInfo = { - 'key': key, - 'accountIndex': accountIndex, - 'addressIndex': addressIndex - }; - } + fee = Money.fromInt(map['fee'] as int? ?? 0, CryptoCurrency.wow) { + additionalInfo = { + 'key': key, + 'accountIndex': accountIndex, + 'addressIndex': addressIndex + }; + } final String id; final String txHash; @@ -41,23 +43,17 @@ class WowneroTransactionInfo extends TransactionInfo { final DateTime date; final int accountIndex; final bool isPending; - final int amount; - final int fee; + final Money amount; + final Money fee; final int addressIndex; final int confirmations; String? recipientAddress; String? key; String? _fiatAmount; - @override - String amountFormatted() => '${formatAmount(wowneroAmountToString(amount: amount))} WOW'; - @override String fiatAmount() => _fiatAmount ?? ''; @override void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); - - @override - String feeFormatted() => '${formatAmount(wowneroAmountToString(amount: fee))} WOW'; } diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index 08b7e6541..d1e0538d1 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:isolate'; import 'package:cw_core/account.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/monero_wallet_keys.dart'; @@ -20,7 +21,6 @@ import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wownero_amount_format.dart'; -import 'package:cw_core/wownero_balance.dart'; import 'package:cw_wownero/api/account_list.dart'; import 'package:cw_wownero/api/coins_info.dart'; import 'package:cw_wownero/api/structs/pending_transaction.dart'; @@ -31,6 +31,7 @@ import 'package:cw_wownero/api/wownero_output.dart'; import 'package:cw_wownero/exceptions/wownero_transaction_creation_exception.dart'; import 'package:cw_wownero/exceptions/wownero_transaction_no_inputs_exception.dart'; import 'package:cw_wownero/pending_wownero_transaction.dart'; +import 'package:cw_wownero/wownero_balance.dart'; import 'package:cw_wownero/wownero_transaction_creation_credentials.dart'; import 'package:cw_wownero/wownero_transaction_history.dart'; import 'package:cw_wownero/wownero_transaction_info.dart'; @@ -56,8 +57,11 @@ abstract class WowneroWalletBase {required WalletInfo walletInfo, required DerivationInfo derivationInfo, required Box unspentCoinsInfo, required String password}) : balance = ObservableMap.of({ CryptoCurrency.wow: WowneroBalance( - fullBalance: wownero_wallet.getFullBalance(accountIndex: 0), - unlockedBalance: wownero_wallet.getFullBalance(accountIndex: 0)) + fullBalance: + Money.fromInt(wownero_wallet.getFullBalance(accountIndex: 0), CryptoCurrency.wow), + unlockedBalance: + Money.fromInt(wownero_wallet.getFullBalance(accountIndex: 0), CryptoCurrency.wow), + ) }), _isTransactionUpdating = false, _hasSyncAfterStartup = false, @@ -75,8 +79,11 @@ abstract class WowneroWalletBase balance = ObservableMap.of({ currency: WowneroBalance( - fullBalance: wownero_wallet.getFullBalance(accountIndex: account.id), - unlockedBalance: wownero_wallet.getUnlockedBalance(accountIndex: account.id)) + fullBalance: Money.fromInt( + wownero_wallet.getFullBalance(accountIndex: account.id), CryptoCurrency.wow), + unlockedBalance: Money.fromInt( + wownero_wallet.getUnlockedBalance(accountIndex: account.id), CryptoCurrency.wow), + ) }); _updateSubAddress(isEnabledAutoGenerateSubaddress, account: account); _askForUpdateTransactionHistory(); @@ -152,9 +159,13 @@ abstract class WowneroWalletBase await walletAddresses.init(); balance = ObservableMap.of({ currency: WowneroBalance( - fullBalance: wownero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id), - unlockedBalance: - wownero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id)) + fullBalance: Money.fromInt( + wownero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id), + CryptoCurrency.wow), + unlockedBalance: Money.fromInt( + wownero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id), + CryptoCurrency.wow), + ) }); _setListeners(); await updateTransactions(); @@ -290,13 +301,12 @@ abstract class WowneroWalletBase final spendAllCoins = inputs.length == unspentCoins.length; if (hasMultiDestination) { - if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + if (outputs.any((item) => item.sendAll || item.cryptoAmount.amount <= BigInt.zero)) { throw WowneroTransactionCreationException( 'You do not have enough WOW to send this amount.'); } - final int totalAmount = - outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); + final totalAmount = outputs.fold(0, (acc, value) => acc + value.cryptoAmount.amount.toInt()); final estimatedFee = calculateEstimatedFee(_credentials.priority, totalAmount); if (unlockedBalance < totalAmount) { @@ -311,8 +321,7 @@ abstract class WowneroWalletBase final wowneroOutputs = outputs.map((output) { final outputAddress = output.isParsedAddress ? output.extractedAddress : output.address; - return WowneroOutput( - address: outputAddress!, amount: output.cryptoAmount!.replaceAll(',', '.')); + return WowneroOutput(address: outputAddress!, amount: output.cryptoAmount.toString()); }).toList(); pendingTransactionDescription = await transaction_history.createTransactionMultDest( @@ -323,8 +332,8 @@ abstract class WowneroWalletBase } else { final output = outputs.first; final address = output.isParsedAddress ? output.extractedAddress : output.address; - final amount = output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.'); - final formattedAmount = output.sendAll ? null : output.formattedCryptoAmount; + final amount = output.sendAll ? null : output.cryptoAmount.toString(); + final formattedAmount = output.sendAll ? null : output.cryptoAmount.amount.toInt(); if ((formattedAmount != null && unlockedBalance < formattedAmount) || (formattedAmount == null && unlockedBalance <= 0)) { @@ -635,10 +644,10 @@ abstract class WowneroWalletBase row.isSpend ? TransactionDirection.outgoing : TransactionDirection.incoming, row.timeStamp, row.isPending, - row.amount, + Money.fromInt(row.amount, CryptoCurrency.wow), row.accountIndex, 0, - row.fee, + Money.fromInt(row.fee, CryptoCurrency.wow), row.confirmations, )..additionalInfo = { 'key': row.key, @@ -706,29 +715,31 @@ abstract class WowneroWalletBase final frozenBalance = _getFrozenBalance(); if (balance[currency]!.fullBalance != fullBalance || - balance[currency]!.unlockedBalance != unlockedBalance || - balance[currency]!.frozenBalance != frozenBalance) { + balance[currency]!.available != unlockedBalance || + balance[currency]!.frozen != frozenBalance) { balance[currency] = WowneroBalance( - fullBalance: fullBalance, unlockedBalance: unlockedBalance, frozenBalance: frozenBalance); + fullBalance: fullBalance, unlockedBalance: unlockedBalance, frozen: frozenBalance); } } Future _askForUpdateTransactionHistory() async => await updateTransactions(); - int _getFullBalance() => wownero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id); + Money _getFullBalance() => Money.fromInt( + wownero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id), CryptoCurrency.wow); - int _getUnlockedBalance() => - wownero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); + Money _getUnlockedBalance() => Money.fromInt( + wownero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id), + CryptoCurrency.wow); - int _getFrozenBalance() { + Money _getFrozenBalance() { var frozenBalance = 0; - for (var coin in unspentCoinsInfo.values.where((element) => + for (final coin in unspentCoinsInfo.values.where((element) => element.walletId == id && element.accountIndex == walletAddresses.account!.id)) { if (coin.isFrozen) frozenBalance += coin.value; } - return frozenBalance; + return Money.fromInt(frozenBalance, CryptoCurrency.wow); } void _onNewBlock(int height, int blocksLeft, double ptc) async { diff --git a/cw_zano/lib/api/model/transfer.dart b/cw_zano/lib/api/model/transfer.dart index 594490a4f..68d3cc932 100644 --- a/cw_zano/lib/api/model/transfer.dart +++ b/cw_zano/lib/api/model/transfer.dart @@ -1,3 +1,5 @@ +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/zano_asset.dart'; import 'package:cw_zano/api/model/employed_entries.dart'; @@ -113,7 +115,8 @@ class Transfer { confirmations: currentDaemonHeight - transfer.height, isIncome: single.isIncome, assetId: single.assetId, - amount: single.amount, + amount: Money(single.amount, + asset ?? CryptoCurrency(title: ticker, name: ticker, decimals: decimalPoint)), tokenSymbol: isSimple ? ticker : '*${ticker}', decimalPoint: decimalPoint, ); @@ -124,7 +127,7 @@ class Transfer { confirmations: currentDaemonHeight - transfer.height, isIncome: single.isIncome, assetId: single.assetId, - amount: amount, + amount: Money(amount, CryptoCurrency.zano), tokenSymbol: isSimple ? 'ZANO' : '*ZANO', ); }, diff --git a/cw_zano/lib/model/pending_zano_transaction.dart b/cw_zano/lib/model/pending_zano_transaction.dart index 99706d3ac..6d1868f8d 100644 --- a/cw_zano/lib/model/pending_zano_transaction.dart +++ b/cw_zano/lib/model/pending_zano_transaction.dart @@ -1,7 +1,7 @@ +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_zano/api/model/destination.dart'; import 'package:cw_zano/api/model/transfer_result.dart'; -import 'package:cw_zano/zano_formatter.dart'; import 'package:cw_zano/zano_wallet.dart'; class PendingZanoTransaction with PendingTransaction { @@ -11,19 +11,19 @@ class PendingZanoTransaction with PendingTransaction { required this.fee, required this.comment, required this.assetId, - required this.ticker, - this.decimalPoint = ZanoFormatter.defaultDecimalPoint, required this.amount, }); final ZanoWalletBase zanoWallet; final List destinations; - final BigInt fee; final String comment; final String assetId; - final String ticker; - final int decimalPoint; - final BigInt amount; + + @override + final Money amount; + + @override + final Money fee; @override String get id => transferResult?.txHash ?? ''; @@ -32,24 +32,16 @@ class PendingZanoTransaction with PendingTransaction { String get hex => ''; @override - String get amountFormatted => ZanoFormatter.bigIntAmountToString(amount, decimalPoint); - - @override - String get feeFormatted => "$feeFormattedValue ZANO"; - - @override - String get feeFormattedValue => ZanoFormatter.bigIntAmountToString(fee); + String get amountFormatted => amount.toString(); TransferResult? transferResult; @override Future commit() async { - transferResult = await zanoWallet.transfer(destinations, fee, comment); + transferResult = await zanoWallet.transfer(destinations, fee.amount, comment); zanoWallet.fetchTransactions(); } @override - Future> commitUR() { - throw UnimplementedError(); - } + Future> commitUR() => throw UnimplementedError(); } diff --git a/cw_zano/lib/model/zano_balance.dart b/cw_zano/lib/model/zano_balance.dart index 0f1e5c81a..d6a3c8185 100644 --- a/cw_zano/lib/model/zano_balance.dart +++ b/cw_zano/lib/model/zano_balance.dart @@ -1,17 +1,15 @@ +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/balance.dart'; -import 'package:cw_zano/zano_formatter.dart'; +import 'package:cw_core/currency.dart'; class ZanoBalance extends Balance { - final BigInt total; - final BigInt unlocked; - final int decimalPoint; - ZanoBalance({required this.total, required this.unlocked, this.decimalPoint = ZanoFormatter.defaultDecimalPoint}) : super(unlocked, (total - unlocked)); + final Money total; + final Money unlocked; - ZanoBalance.empty({this.decimalPoint = ZanoFormatter.defaultDecimalPoint}): total = BigInt.zero, unlocked = BigInt.zero, super(BigInt.zero, BigInt.zero); + ZanoBalance({required this.total, required this.unlocked}) : super(unlocked, (total - unlocked)); - @override - String get formattedAdditionalBalance => ZanoFormatter.bigIntAmountToString(total - unlocked, decimalPoint); - - @override - String get formattedAvailableBalance => ZanoFormatter.bigIntAmountToString(unlocked, decimalPoint); + ZanoBalance.empty(Currency currency) + : total = Money.zero(currency), + unlocked = Money.zero(currency), + super(Money.zero(currency), Money.zero(currency)); } diff --git a/cw_zano/lib/model/zano_transaction_info.dart b/cw_zano/lib/model/zano_transaction_info.dart index d643e9207..08cb145fe 100644 --- a/cw_zano/lib/model/zano_transaction_info.dart +++ b/cw_zano/lib/model/zano_transaction_info.dart @@ -1,4 +1,5 @@ -import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_zano/api/model/transfer.dart'; @@ -11,13 +12,13 @@ class ZanoTransactionInfo extends TransactionInfo { required this.direction, required this.date, required this.isPending, - required this.zanoAmount, + required this.amount, required this.fee, required this.confirmations, required this.tokenSymbol, required this.decimalPoint, required String assetId, - }) : amount = zanoAmount.isValidInt ? zanoAmount.toInt() : 0 { + }) { additionalInfo['assetId'] = assetId; } @@ -25,16 +26,15 @@ class ZanoTransactionInfo extends TransactionInfo { {required int confirmations, required bool isIncome, required String assetId, - required BigInt amount, + required Money amount, this.tokenSymbol = 'ZANO', this.decimalPoint = ZanoFormatter.defaultDecimalPoint}) : id = transfer.txHash, height = transfer.height, direction = isIncome ? TransactionDirection.incoming : TransactionDirection.outgoing, date = DateTime.fromMillisecondsSinceEpoch(transfer.timestamp * 1000), - zanoAmount = amount, - amount = amount.isValidInt ? amount.toInt() : 0, - fee = transfer.fee, + amount = amount, + fee = Money.fromInt(transfer.fee, CryptoCurrency.zano), confirmations = confirmations, isPending = confirmations < 10, recipientAddress = transfer.remoteAddresses.isNotEmpty @@ -54,28 +54,11 @@ class ZanoTransactionInfo extends TransactionInfo { final TransactionDirection direction; final DateTime date; final bool isPending; - final BigInt zanoAmount; - final int amount; - final int fee; + final Money amount; + final Money fee; final int confirmations; final int decimalPoint; late String recipientAddress; final String tokenSymbol; - String? _fiatAmount; String? key; - - @override - String amountFormatted() => - '${formatAmount(ZanoFormatter.bigIntAmountToString(zanoAmount, decimalPoint))} $tokenSymbol'; - - @override - String fiatAmount() => _fiatAmount ?? ''; - - @override - void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); - - @override - String feeFormatted() => '${formatAmount(ZanoFormatter.intAmountToString(fee))} $feeCurrency'; - - String get feeCurrency => 'ZANO'; } diff --git a/cw_zano/lib/zano_wallet.dart b/cw_zano/lib/zano_wallet.dart index 913bc083a..024d59b2b 100644 --- a/cw_zano/lib/zano_wallet.dart +++ b/cw_zano/lib/zano_wallet.dart @@ -3,6 +3,7 @@ import 'dart:core'; import 'dart:io'; import 'dart:math'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/node.dart'; @@ -26,7 +27,6 @@ import 'package:cw_zano/model/zano_transaction_creation_exception.dart'; import 'package:cw_zano/model/zano_transaction_credentials.dart'; import 'package:cw_zano/model/zano_transaction_info.dart'; import 'package:cw_zano/model/zano_wallet_keys.dart'; -import 'package:cw_zano/zano_formatter.dart'; import 'package:cw_zano/zano_transaction_history.dart'; import 'package:cw_zano/zano_wallet_addresses.dart'; import 'package:cw_zano/zano_wallet_api.dart'; @@ -106,7 +106,7 @@ abstract class ZanoWalletBase static final int _txChunkSize = (pow(2, 32) - 1).toInt(); ZanoWalletBase(WalletInfo walletInfo, DerivationInfo derivationInfo, String password) - : balance = ObservableMap.of({CryptoCurrency.zano: ZanoBalance.empty()}), + : balance = ObservableMap.of({CryptoCurrency.zano: ZanoBalance.empty(CryptoCurrency.zano)}), _isTransactionUpdating = false, _hasSyncAfterStartup = false, walletAddresses = ZanoWalletAddresses(walletInfo), @@ -199,8 +199,8 @@ abstract class ZanoWalletBase for (final item in result.wi.balances) { if (item.assetInfo.assetId == zanoAssetId) { balance[CryptoCurrency.zano] = ZanoBalance( - total: item.total, - unlocked: item.unlocked, + total: Money(item.total, CryptoCurrency.zano), + unlocked: Money(item.unlocked, CryptoCurrency.zano), ); } } @@ -232,24 +232,25 @@ abstract class ZanoWalletBase final isZano = credentials.currency == CryptoCurrency.zano; final outputs = credentials.outputs; final hasMultiDestination = outputs.length > 1; - final unlockedBalanceZano = balance[CryptoCurrency.zano]?.unlocked ?? BigInt.zero; - final unlockedBalanceCurrency = balance[credentials.currency]?.unlocked ?? BigInt.zero; - final fee = BigInt.from(calculateEstimatedFee(credentials.priority)); - late BigInt totalAmount; + final unlockedBalanceZano = balance[CryptoCurrency.zano]?.unlocked ?? Money.zero(CryptoCurrency.zano); + final unlockedBalanceCurrency = balance[credentials.currency]?.unlocked ?? Money.zero(credentials.currency); + final fee = Money(BigInt.from(calculateEstimatedFee(credentials.priority)), CryptoCurrency.zano); + + var totalAmount = Money.zero(credentials.currency); void checkForEnoughBalances() { if (isZano) { if (totalAmount + fee > unlockedBalanceZano) { throw ZanoTransactionCreationException( - "You don't have enough coins (required: ${ZanoFormatter.bigIntAmountToString(totalAmount + fee)} ZANO, unlocked ${ZanoFormatter.bigIntAmountToString(unlockedBalanceZano)} ZANO)."); + "You don't have enough coins (required: ${(totalAmount + fee).toStringWithSymbol()}, unlocked ${unlockedBalanceZano.toStringWithSymbol()})."); } } else { if (fee > unlockedBalanceZano) { throw ZanoTransactionCreationException( - "You don't have enough coins (required: ${ZanoFormatter.bigIntAmountToString(fee)} ZANO, unlocked ${ZanoFormatter.bigIntAmountToString(unlockedBalanceZano)} ZANO)."); + "You don't have enough coins (required: ${fee.toStringWithSymbol()}, unlocked ${unlockedBalanceZano.toStringWithSymbol()})."); } if (totalAmount > unlockedBalanceCurrency) { throw ZanoTransactionCreationException( - "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})."); + "You don't have enough coins (required: ${totalAmount.toStringWithSymbol()}, unlocked ${unlockedBalanceCurrency.toStringWithSymbol()})."); } } } @@ -257,15 +258,15 @@ abstract class ZanoWalletBase final assetId = isZano ? zanoAssetId : (credentials.currency as ZanoAsset).assetId; late List destinations; if (hasMultiDestination) { - if (outputs.any((output) => output.sendAll || (output.formattedCryptoAmount ?? 0) <= 0)) { + if (outputs.any((output) => output.sendAll || output.cryptoAmount.amount <= BigInt.zero)) { throw ZanoTransactionCreationException("You don't have enough coins."); } - totalAmount = outputs.fold( - BigInt.zero, (acc, value) => acc + BigInt.from(value.formattedCryptoAmount ?? 0)); + totalAmount = + outputs.fold(Money.zero(credentials.currency), (acc, value) => acc + value.cryptoAmount); checkForEnoughBalances(); destinations = outputs .map((output) => Destination( - amount: BigInt.from(output.formattedCryptoAmount ?? 0), + amount: output.cryptoAmount.amount, address: output.isParsedAddress ? output.extractedAddress! : output.address, assetId: assetId, )) @@ -279,12 +280,12 @@ abstract class ZanoWalletBase totalAmount = unlockedBalanceCurrency; } } else { - totalAmount = BigInt.from(output.formattedCryptoAmount!); + totalAmount = output.cryptoAmount; } checkForEnoughBalances(); destinations = [ Destination( - amount: totalAmount, + amount: totalAmount.amount, address: output.isParsedAddress ? output.extractedAddress! : output.address, assetId: assetId, ) @@ -296,8 +297,6 @@ abstract class ZanoWalletBase fee: fee, comment: outputs.first.note ?? '', assetId: assetId, - ticker: credentials.currency.title, - decimalPoint: credentials.currency.decimals, amount: totalAmount, ); } @@ -414,7 +413,7 @@ abstract class ZanoWalletBase Future checkNodeHealth() async { try { final status = await getWalletStatus(); - + return status.isDaemonConnected; } catch (_) { return false; @@ -453,7 +452,7 @@ abstract class ZanoWalletBase enabled: true, ); zanoAssets[asset.assetId] = asset; - balance[asset] = ZanoBalance.empty(decimalPoint: asset.decimalPoint); + balance[asset] = ZanoBalance.empty(asset); return asset; } @@ -555,32 +554,38 @@ abstract class ZanoWalletBase // 2. set whitelists available in balances as 'enabled' ('disabled' by default) for (final b in walletInfo.wi.balances) { if (b.assetId == zanoAssetId) { - balance[CryptoCurrency.zano] = ZanoBalance(total: b.total, unlocked: b.unlocked); + balance[CryptoCurrency.zano] = ZanoBalance( + total: Money(b.total, CryptoCurrency.zano), + unlocked: Money(b.unlocked, CryptoCurrency.zano), + ); } else { final asset = zanoAssets[b.assetId]; if (asset == null) { printV('balance for an unknown asset ${b.assetInfo.assetId}'); continue; } - if (balance.keys.any( - (element) => element is ZanoAsset && element.assetId == b.assetInfo.assetId)) { - balance[balance.keys.firstWhere((element) => - element is ZanoAsset && element.assetId == b.assetInfo.assetId)] = - ZanoBalance( - total: b.total, unlocked: b.unlocked, decimalPoint: asset.decimalPoint); + + final assetBalanceKey = + balance.keys.where((e) => e is ZanoAsset && e.assetId == asset.assetId).firstOrNull; + if (assetBalanceKey != null) { + balance[assetBalanceKey] = ZanoBalance( + total: Money(b.total, assetBalanceKey), + unlocked: Money(b.unlocked, assetBalanceKey), + ); } else { balance[asset] = ZanoBalance( - total: b.total, unlocked: b.unlocked, decimalPoint: asset.decimalPoint); + total: Money(b.total, asset), + unlocked: Money(b.unlocked, asset), + ); } } } await updateTransactions(); // removing balances for assets missing in wallet info balances balance.removeWhere( - (key, _) => - key != CryptoCurrency.zano && - !walletInfo.wi.balances - .any((element) => element.assetId == (key as ZanoAsset).assetId), + (key, _) => + key != CryptoCurrency.zano && + !walletInfo.wi.balances.any((element) => element.assetId == (key as ZanoAsset).assetId), ); } } diff --git a/cw_zcash/lib/src/pending_zcash_transaction.dart b/cw_zcash/lib/src/pending_zcash_transaction.dart index 28be24652..db4f59f7e 100644 --- a/cw_zcash/lib/src/pending_zcash_transaction.dart +++ b/cw_zcash/lib/src/pending_zcash_transaction.dart @@ -1,11 +1,11 @@ import 'dart:convert'; import 'dart:math'; +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_zcash/cw_zcash.dart'; -import 'package:cw_core/currency_for_wallet_type.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:cw_zcash/src/zcash_taddress_rotation.dart'; import 'package:warp_api/data_fb_generated.dart'; import 'package:warp_api/warp_api.dart'; @@ -23,7 +23,7 @@ class PendingZcashTransaction with PendingTransaction { final ZcashTransactionCredentials credentials; final String txPlan; String? _txId; - final int availableBalance; + final Money availableBalance; @override String get id => _txId ?? ''; @@ -32,31 +32,24 @@ class PendingZcashTransaction with PendingTransaction { String get hex => ''; @override - String get amountFormatted { - return walletTypeToCryptoCurrency(WalletType.zcash).formatAmount(BigInt.from(totalAmount)); - } + Money get amount => Money.fromInt(_totalAmount, CryptoCurrency.zec); - int get totalAmount { + @override + String get amountFormatted => amount.toString(); + + int get _totalAmount { final isAll = credentials.outputs.fold(false, (final a, final b) => a || (b.sendAll)); if (isAll) { - return availableBalance; + return availableBalance.amount.toInt(); } return credentials.outputs.fold( 0, - (final a, final b) => a + (b.formattedCryptoAmount ?? 0), + (final a, final b) => a + b.cryptoAmount.amount.toInt(), ); } @override - String get feeFormatted => - '$feeFormattedValue ${walletTypeToCryptoCurrency(WalletType.zcash).title}'; - - @override - late String feeFormattedValue = walletTypeToCryptoCurrency( - WalletType.zcash, - ).formatAmount(BigInt.from(fee)); - - int fee; + final Money fee; @override Future commit() async { @@ -73,7 +66,7 @@ class PendingZcashTransaction with PendingTransaction { txId: _txId, height: 0, timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, - value: -totalAmount, + value: -_totalAmount, ).pack, ), ), @@ -83,7 +76,8 @@ class PendingZcashTransaction with PendingTransaction { _txId ?? '', credentials.outputs.reduce((final o1, final o2) { return OutputInfo( - address: o1.address + "," + o2.address, + cryptoAmount: Money.zero(CryptoCurrency.zec), + address: "${o1.address},${o2.address}", sendAll: false, isParsedAddress: false, ); @@ -94,9 +88,7 @@ class PendingZcashTransaction with PendingTransaction { } @override - Future> commitUR() { - throw UnimplementedError('UR not supported for Zcash'); - } + Future> commitUR() => throw UnimplementedError('UR not supported for Zcash'); @override bool shouldCommitUR() => false; diff --git a/cw_zcash/lib/src/zcash_balance.dart b/cw_zcash/lib/src/zcash_balance.dart index ab5ec84d2..c76864adc 100644 --- a/cw_zcash/lib/src/zcash_balance.dart +++ b/cw_zcash/lib/src/zcash_balance.dart @@ -1,32 +1,13 @@ +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/crypto_currency.dart'; class ZcashBalance extends Balance { - ZcashBalance({required this.confirmed, required this.unconfirmed, required final int frozen}) - : _frozen = frozen, - super.fromInt(confirmed, unconfirmed); + ZcashBalance(super.available, super.unavailable, {required super.frozen}); - factory ZcashBalance.zero() => ZcashBalance(confirmed: 0, unconfirmed: 0, frozen: 0); - - final int confirmed; - final int unconfirmed; - final int _frozen; - BigInt get frozen => BigInt.from(_frozen); - - @override - String get formattedAvailableBalance { - return CryptoCurrency.zec.formatAmount(BigInt.from(confirmed)); - } - - @override - String get formattedAdditionalBalance { - if (unconfirmed == 0) return '0.0'; - return CryptoCurrency.zec.formatAmount(BigInt.from(unconfirmed)); - } - - @override - String get formattedUnAvailableBalance { - if (frozen == 0) return ''; - return CryptoCurrency.zec.formatAmount(BigInt.from(_frozen)); - } + factory ZcashBalance.zero() => ZcashBalance( + Money.zero(CryptoCurrency.zec), + Money.zero(CryptoCurrency.zec), + frozen: Money.zero(CryptoCurrency.zec), + ); } diff --git a/cw_zcash/lib/src/zcash_transaction_info.dart b/cw_zcash/lib/src/zcash_transaction_info.dart index 617bd046b..d307cafa7 100644 --- a/cw_zcash/lib/src/zcash_transaction_info.dart +++ b/cw_zcash/lib/src/zcash_transaction_info.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:io'; -import 'package:cw_core/currency_for_wallet_type.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; @@ -14,8 +14,8 @@ import 'package:path/path.dart' as p; class ZcashTransactionInfo extends TransactionInfo { ZcashTransactionInfo({ required final String id, - required final int amount, - required final int fee, + required final Money amount, + required final Money fee, required final TransactionDirection direction, required final bool isPending, required final DateTime date, @@ -54,16 +54,6 @@ class ZcashTransactionInfo extends TransactionInfo { String? _fiatAmount; - @override - String amountFormatted() => - '${walletTypeToCryptoCurrency(WalletType.zcash).formatAmount(BigInt.from(amount))} ${walletTypeToCryptoCurrency(WalletType.zcash).title}'; - - @override - String? feeFormatted() { - if (fee == null || fee == 0) return null; - return '${walletTypeToCryptoCurrency(WalletType.zcash).formatAmount(BigInt.from(fee!))} ${walletTypeToCryptoCurrency(WalletType.zcash).title}'; - } - @override String fiatAmount() => _fiatAmount ?? ''; diff --git a/cw_zcash/lib/src/zcash_wallet.dart b/cw_zcash/lib/src/zcash_wallet.dart index 0ade54d54..d255ae7fb 100644 --- a/cw_zcash/lib/src/zcash_wallet.dart +++ b/cw_zcash/lib/src/zcash_wallet.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/node.dart'; @@ -56,7 +57,7 @@ abstract class ZcashWalletBase @override ObservableMap balance = ObservableMap.of({ - CryptoCurrency.zec: ZcashBalance(confirmed: 0, unconfirmed: 0, frozen: 0), + CryptoCurrency.zec: ZcashBalance.zero(), }); static int internalCalculateEstimatedFee(final TransactionPriority priority, final int? amount) { @@ -135,33 +136,21 @@ abstract class ZcashWalletBase await updateBalance(); final zcashBalance = balance[CryptoCurrency.zec]; - final availableBalance = zcashBalance?.confirmed ?? 0; + final availableBalance = zcashBalance?.available ?? Money.zero(currency); final recipients = []; - int totalAmount = 0; + var totalAmount = Money.zero(currency); for (final output in creds.outputs) { - int amount; + Money amount; if (output.sendAll) { amount = availableBalance; } else { - amount = output.formattedCryptoAmount ?? 0; + amount = output.cryptoAmount; - if (amount == 0 && output.cryptoAmount != null && output.cryptoAmount!.isNotEmpty) { - try { - final parsedAmount = CryptoCurrency.zec.parseAmount( - output.cryptoAmount!.replaceAll(',', '.'), - ); - amount = parsedAmount.toInt(); - printV("Parsed amount from cryptoAmount '${output.cryptoAmount}': $amount"); - } catch (e) { - printV("Failed to parse cryptoAmount '${output.cryptoAmount}': $e"); - } - } - - if (amount <= 0) { + if (amount <= Money.zero(currency)) { throw Exception( - 'Invalid amount for output. Amount: ${output.cryptoAmount}, Formatted: ${output.formattedCryptoAmount}', + 'Invalid amount for output. Amount: ${output.cryptoAmount}, Formatted: ${output.cryptoAmount}', ); } } @@ -200,7 +189,7 @@ abstract class ZcashWalletBase final builder = RecipientObjectBuilder( address: address, pools: recipientPools, - amount: amount, + amount: amount.amount.toInt(), feeIncluded: output.sendAll, replyTo: false, memo: memo.isNotEmpty ? memo : null, @@ -240,7 +229,7 @@ abstract class ZcashWalletBase zcashWallet: this as ZcashWallet, credentials: creds, txPlan: txPlan, - fee: internalCalculateEstimatedFee(creds.priority, null), + fee: Money.fromInt(internalCalculateEstimatedFee(creds.priority, null), currency), availableBalance: availableBalance, ); } @@ -307,8 +296,8 @@ abstract class ZcashWalletBase final txInfo = ZcashTransactionInfo( id: txId.trim().replaceAll('"', ''), - amount: tx.value.abs(), - fee: 0, + amount: Money.fromInt(tx.value.abs(), currency), + fee: Money.zero(currency), direction: direction, isPending: tx.height == 0, date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), @@ -807,9 +796,9 @@ abstract class ZcashWalletBase unawaited(_autoShield()); balance[CryptoCurrency.zec] = ZcashBalance( - confirmed: confirmedSpendable, - unconfirmed: spendable - confirmedSpendable, - frozen: 0, + Money.fromInt(confirmedSpendable, currency), + Money.fromInt(spendable - confirmedSpendable, currency), + frozen: Money.zero(currency), ); } catch (e, stackTrace) { printV("Balance update error: $e"); @@ -1158,13 +1147,13 @@ abstract class ZcashWalletBase } _didRunRescanInternalChange = true; final bal = - balance[CryptoCurrency.zec]!.confirmed + - balance[CryptoCurrency.zec]!.unconfirmed + - balance[CryptoCurrency.zec]!.frozen.toInt(); + balance[CryptoCurrency.zec]!.available + + balance[CryptoCurrency.zec]!.unavailable + + (balance[CryptoCurrency.zec]!.frozen ?? Money.zero(CryptoCurrency.zec)); final osCacheDir = await getApplicationCacheDirectory(); final cacheDir = osCacheDir.createTempSync("zkool-import"); zkoolSweep = ZkoolSweep( - currentBalance: bal, + currentBalance: bal.amount.toInt(), cacheDir: cacheDir.path, seed: seed ?? '', passphrase: password, diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 6f5b9bfa4..a5d118322 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -156,13 +156,12 @@ class CWBitcoin extends Bitcoin { outputs .map((out) => OutputInfo( fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, + cryptoAmount: out.cryptoAmountMoney, address: out.address, note: out.note, sendAll: out.sendAll, extractedAddress: out.extractedAddress, isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount, memo: out.memo.isNotEmpty ? out.memo : null, extra: out.extra, )) @@ -191,7 +190,7 @@ class CWBitcoin extends Bitcoin { } @override - Future estimateFakeSendAllTxAmount(Object wallet, TransactionPriority priority, + Future estimateFakeSendAllTxAmount(WalletBase wallet, TransactionPriority priority, {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) async { try { final sk = ECPrivate.random(); @@ -233,7 +232,7 @@ class CWBitcoin extends Bitcoin { return estimatedTx.amount; } catch (_) { - return 0; + return Money.zero(wallet.currency); } } @@ -247,10 +246,6 @@ class CWBitcoin extends Bitcoin { String formatterBitcoinAmountToString({required int amount}) => bitcoinAmountToString(amount: amount); - @override - double formatterBitcoinAmountToDouble({required int amount}) => - bitcoinAmountToDouble(amount: amount); - @override int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount); diff --git a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart index 78fa71d49..5442dcff1 100644 --- a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart +++ b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart @@ -22,7 +22,6 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.da import 'package:cake_wallet/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; -import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; import 'package:cake_wallet/typography.dart'; @@ -36,7 +35,6 @@ import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:mobx/mobx.dart'; class CakePayBuyCardPage extends BasePage { @@ -612,16 +610,13 @@ class CakePayBuyCardPage extends BasePage { titleIconPath: _sendViewModel.selectedCryptoCurrency.iconPath, currency: _sendViewModel.selectedCryptoCurrency, amount: S.of(bottomSheetContext).send_amount, - amountValue: _sendViewModel.amountParsingProxy.getDisplayCryptoAmount( - _sendViewModel.pendingTransaction!.amountFormatted, - _sendViewModel.selectedCryptoCurrency), + amountValue: _sendViewModel.amountParsingProxy.asDisplayStringWithSymbol( + _sendViewModel.pendingTransaction!.amount), quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}', fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted, fee: S.of(bottomSheetContext).send_fee, - feeValue: "${_sendViewModel.amountParsingProxy.getDisplayCryptoAmount( - _sendViewModel.pendingTransaction!.feeFormattedValue, - _sendViewModel.selectedCryptoCurrency)} ${_sendViewModel.amountParsingProxy - .getCryptoSymbol(_sendViewModel.wallet.currency)}", + feeValue: _sendViewModel.amountParsingProxy.asDisplayStringWithSymbol( + _sendViewModel.pendingTransaction!.fee), feeFiatAmount: _sendViewModel.pendingTransactionFeeFiatAmountFormatted, outputs: displayingOutputs, footerType: FooterType.slideActionButton, @@ -699,13 +694,12 @@ class CakePayBuyCardPage extends BasePage { child: Icon(Icons.check, size: 16, color: Colors.white), ), output: displayingOutputs.first, - currency: _sendViewModel.selectedCryptoCurrency, amount: S.of(sheetCtx).send_amount, - amountValue: _sendViewModel.pendingTransaction!.amountFormatted, + amountValue: _sendViewModel.pendingTransaction!.amount.toStringWithSymbol(), quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}', fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted, fee: S.of(sheetCtx).send_fee, - feeValue: _sendViewModel.pendingTransaction!.feeFormatted, + feeValue: _sendViewModel.pendingTransaction!.fee.toStringWithSymbol(), feeFiatAmount: _sendViewModel.pendingTransactionFeeFiatAmountFormatted, paymentId: 'Order ID', paymentIdValue: order?.orderId ?? '', diff --git a/lib/core/amount_parsing_proxy.dart b/lib/core/amount_parsing_proxy.dart index 4b941129e..5a7e41789 100644 --- a/lib/core/amount_parsing_proxy.dart +++ b/lib/core/amount_parsing_proxy.dart @@ -1,7 +1,9 @@ import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart'; import 'package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; class AmountParsingProxy { @@ -20,70 +22,54 @@ class AmountParsingProxy { /// [getCryptoOutputAmount] turns the input [amount] into the preferred representation of [cryptoCurrency] String getDisplayCryptoAmount(String amount, CryptoCurrency cryptoCurrency) { - try { if (useSatoshi(cryptoCurrency) && amount.isNotEmpty) { - return cryptoCurrency.parseAmount(amount.withMaxDecimals(cryptoCurrency.decimals)).toString(); + return cryptoCurrency + .parseAmount(amount.withMaxDecimals(cryptoCurrency.decimals)) + .toStringWithPrecision(useBaseUnit: true); } return amount.withMaxDecimals(cryptoCurrency.decimals); - - } catch(_) { - printV("failed to parse amount $amount for currency ${cryptoCurrency.title}, falling back to showing unparsed"); + } catch (_) { + printV( + "failed to parse amount $amount for currency ${cryptoCurrency.title}, falling back to showing unparsed"); return amount; } - } - /// [getCryptoStringRepresentation] turns the input [amount] into the preferred representation of [cryptoCurrency] - String getDisplayCryptoString(int amount, CryptoCurrency cryptoCurrency) { - if (useSatoshi(cryptoCurrency)) { - return "$amount"; - } + String getDisplayCryptoString(int amount, CryptoCurrency cryptoCurrency) => + asDisplayString(Money.fromInt(amount, cryptoCurrency)); - return cryptoCurrency.formatAmount(BigInt.from(amount)); - } + /// [getDisplayCryptoString] turns the input [amount] into the preferred representation of [cryptoCurrency] + /// + /// if [displayMode] is [BitcoinAmountDisplayMode.satoshi] it returns 1 BTC as 100000000 + /// if [displayMode] is [BitcoinAmountDisplayMode.bitcoin] it returns 1 BTC as 1 + /// hardcoding fractionalDigits to 8 as it's a display string, so no need to show more than that + String asDisplayString(Money amount) => + amount.toStringWithPrecision(useBaseUnit: useSatoshi(amount.currency), fractionalDigits: 8); - String getDisplayCryptoStringFromBigInt(BigInt amount, CryptoCurrency cryptoCurrency) { - if (useSatoshi(cryptoCurrency)) { - return "$amount"; - } + /// [asDisplayStringWithSymbol] turns the input [amount] into the preferred representation of + /// [cryptoCurrency] following the symbol of the unit + /// + /// if [displayMode] is [BitcoinAmountDisplayMode.satoshi] it returns 1 BTC as 100000000 sats + /// if [displayMode] is [BitcoinAmountDisplayMode.bitcoin] it returns 1 BTC as 1 BTC + /// hardcoding fractionalDigits to 8 as it's a display string, so no need to show more than that + String asDisplayStringWithSymbol(Money amount) => + amount.toStringWithSymbol(useBaseUnit: useSatoshi(amount.currency), fractionalDigits: 8); - return cryptoCurrency.formatAmount(amount); - } + /// [parseCryptoString] turns the the display representation [string] into `Money` with [cryptoCurrency] + Money parseCryptoString(String amount, CryptoCurrency cryptoCurrency) => + Money.parse(amount, cryptoCurrency, isBaseUnit: useSatoshi(cryptoCurrency)); - /// [getCryptoStringFromDouble] turns the input [amount] into the preferred representation of [cryptoCurrency] and - String getDisplayCryptoStringFromDouble(double amount, CryptoCurrency cryptoCurrency) { - if (useSatoshi(cryptoCurrency)) { - return "$amount"; - } - - return cryptoCurrency.formatAmount(BigInt.from(amount)); - } - - /// [parseCryptoString] turns the input [string] into a `BigInt` presentation of the [cryptoCurrency] - BigInt parseCryptoString(String amount, CryptoCurrency cryptoCurrency) { - if (useSatoshi(cryptoCurrency)) { - return BigInt.parse(amount); - } - - return cryptoCurrency.parseAmount(amount); - } - - /// [tryParseCryptoString] tries to turn the display representation [string] into a `BigInt` presentation of the [cryptoCurrency] - BigInt? tryParseCryptoString(String amount, CryptoCurrency cryptoCurrency) { - if (useSatoshi(cryptoCurrency)) { - return BigInt.tryParse(amount); - } - - return cryptoCurrency.tryParseAmount(amount); - } + /// [tryParseCryptoString] tries to turn the display representation [string] into `Money` with [cryptoCurrency] + Money? tryParseCryptoString(String amount, CryptoCurrency cryptoCurrency) => + Money.tryParse(amount, cryptoCurrency, isBaseUnit: useSatoshi(cryptoCurrency)); /// [getCryptoSymbol] returns the correct Symbol related to the presentation String getCryptoSymbol(CryptoCurrency cryptoCurrency) => useSatoshi(cryptoCurrency) ? "sats" : cryptoCurrency.title.safeSubString(0, 8); - bool useSatoshi(CryptoCurrency cryptoCurrency) => + bool useSatoshi(Currency cryptoCurrency) => ([CryptoCurrency.btc, CryptoCurrency.btcln].contains(cryptoCurrency) && displayMode == BitcoinAmountDisplayMode.satoshi) || (CryptoCurrency.btcln == cryptoCurrency && diff --git a/lib/core/background_sync.dart b/lib/core/background_sync.dart index 7a50d2b1e..a63618d0c 100644 --- a/lib/core/background_sync.dart +++ b/lib/core/background_sync.dart @@ -213,7 +213,7 @@ class BackgroundSync { final action = tx.direction == TransactionDirection.incoming ? "Received" : "Sent"; if (sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false) { await showNotification( - "$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amountFormatted()}"); + "$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amount.toString()}"); } printV( "${wallet.currency.fullName} in ${wallet.name}: TX: ${tx.date} ${tx.amount} ${tx.direction}"); diff --git a/lib/core/csv_export_service.dart b/lib/core/csv_export_service.dart index ae6fa347a..ae75edb6a 100644 --- a/lib/core/csv_export_service.dart +++ b/lib/core/csv_export_service.dart @@ -80,21 +80,15 @@ class CsvExportService { ? tx.to! : (tx.outputAddresses?.firstOrNull ?? '')); - // Split "0.2769 LTC" → amount="0.2769", currency="LTC" - final (amount, currency) = _splitAmountCurrency(tx.amountFormatted()); - - // fee is stored as (inputAmount - totalOutputAmount) which is negative for incoming - // transactions — only include it when the raw value is a sensible positive fee. - final fee = tx.fee != null && tx.fee! > 0 ? (tx.feeFormatted() ?? '') : ''; - final (feeAmount, _) = fee.isNotEmpty ? _splitAmountCurrency(fee) : ('', ''); + final fee = tx.fee != null && !tx.fee!.isZero ? tx.fee.toString() : ''; return _row([ 'transaction', _isoDate(tx.date), type, - amount, - currency, - feeAmount, + tx.amount.toString(), + tx.amount.currency.symbol, + fee, tx.id, address, status, @@ -109,15 +103,6 @@ class CsvExportService { ]); } - /// Splits a formatted amount like "0.2769 LTC" into ("0.2769", "LTC"). - /// If there is no trailing symbol the whole string is returned as the amount. - (String, String) _splitAmountCurrency(String formatted) { - final trimmed = formatted.trim(); - final idx = trimmed.lastIndexOf(' '); - if (idx == -1) return (trimmed, ''); - return (trimmed.substring(0, idx), trimmed.substring(idx + 1)); - } - String _tradeRow(TradeListItem item) { final trade = item.trade; return _row([ diff --git a/lib/decred/cw_decred.dart b/lib/decred/cw_decred.dart index 7fe2ee5c2..18b955af8 100644 --- a/lib/decred/cw_decred.dart +++ b/lib/decred/cw_decred.dart @@ -19,9 +19,8 @@ class CWDecred extends Decred { DecredRestoreWalletFromPubkeyCredentials(name: name, pubkey: pubkey, password: password); @override - WalletService createDecredWalletService(Box unspentCoinSource) { - return DecredWalletService(unspentCoinSource); - } + WalletService createDecredWalletService(Box unspentCoinSource) => + DecredWalletService(unspentCoinSource); @override List getTransactionPriorities() => DecredTransactionPriority.all; @@ -39,18 +38,19 @@ class CWDecred extends Decred { @override Object createDecredTransactionCredentials(List outputs, TransactionPriority priority) => DecredTransactionCredentials( - outputs - .map((out) => OutputInfo( + outputs + .map((out) => OutputInfo( fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, + cryptoAmount: out.cryptoAmountMoney, address: out.address, note: out.note, sendAll: out.sendAll, extractedAddress: out.extractedAddress, isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount)) - .toList(), - priority: priority as DecredTransactionPriority); + )) + .toList(), + priority: priority as DecredTransactionPriority, + ); List getAddressInfos(Object wallet) { final decredWallet = wallet as DecredWallet; @@ -69,17 +69,6 @@ class CWDecred extends Decred { await decredWallet.walletAddresses.generateNewAddress(label); } - @override - String formatterDecredAmountToString({required int amount}) => - decredAmountToString(amount: amount); - - @override - double formatterDecredAmountToDouble({required int amount}) => - decredAmountToDouble(amount: amount); - - @override - int formatterStringDoubleToDecredAmount(String amount) => stringDoubleToDecredAmount(amount); - @override List getUnspents(Object wallet) { final decredWallet = wallet as DecredWallet; diff --git a/lib/entities/calculate_fiat_amount.dart b/lib/entities/calculate_fiat_amount.dart index e4e4b29a4..86f2dfe68 100644 --- a/lib/entities/calculate_fiat_amount.dart +++ b/lib/entities/calculate_fiat_amount.dart @@ -1,9 +1,11 @@ +import 'package:cw_core/amount/amount_sanitizer.dart'; + String calculateFiatAmount({double? price, String? cryptoAmount, bool raw = false}) { if (price == null || cryptoAmount == null) { return '0.00'; } - cryptoAmount = cryptoAmount.replaceAll(',', '.'); + cryptoAmount = cryptoAmount.sanitized(); final _amount = double.tryParse(cryptoAmount); if (_amount == null || _amount.isNaN) return '0.00'; diff --git a/lib/entities/fiat_currency.dart b/lib/entities/fiat_currency.dart index 55eef1afe..ca100332b 100644 --- a/lib/entities/fiat_currency.dart +++ b/lib/entities/fiat_currency.dart @@ -1,18 +1,26 @@ +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/currency.dart'; import 'package:cw_core/enumerable_item.dart'; class FiatCurrency extends EnumerableItem with Serializable implements Currency { const FiatCurrency({ - required String symbol, + required this.symbol, required this.countryCode, required this.fullName, this.decimals = 2, }) : super(title: symbol, raw: symbol); final String countryCode; + + @override final String fullName; + + @override final int decimals; + @override + final String symbol; + static List get all => _all.values.toList(); static List get currenciesAvailableToBuyWith => [ @@ -182,4 +190,10 @@ class FiatCurrency extends EnumerableItem with Serializable impl @override String get iconPath => "assets/images/flags/$countryCode.png"; + + @override + Money parseAmount(String value) => Money.parse(value, this); + + @override + Money? tryParseAmount(String value) => Money.tryParse(value, this); } diff --git a/lib/evm/cw_evm.dart b/lib/evm/cw_evm.dart index 44ed4bfae..2a028e5a1 100644 --- a/lib/evm/cw_evm.dart +++ b/lib/evm/cw_evm.dart @@ -105,26 +105,25 @@ class CWEVM extends EVM { TransactionPriority? priority, int? feeRate, bool useBlinkProtection = true, - }) { - return EVMChainTransactionCredentials( - outputs - .map((out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount, - memo: out.memo)) - .toList(), - priority: priority as EVMChainTransactionPriority?, - currency: currency, - feeRate: feeRate, - useBlinkProtection: useBlinkProtection, - ); - } + }) => + EVMChainTransactionCredentials( + outputs + .map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmountMoney, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + memo: out.memo, + )) + .toList(), + priority: priority as EVMChainTransactionPriority?, + currency: currency, + feeRate: feeRate, + useBlinkProtection: useBlinkProtection, + ); @override Object createEVMTransactionCredentialsRaw( @@ -144,29 +143,28 @@ class CWEVM extends EVM { } @override - TransactionInfo getTransactionInfo( - - { - required String id, - required int height, - required BigInt ethAmount, - required BigInt ethFee, - required String tokenSymbol, - int exponent = 18, - required TransactionDirection direction, - required bool isPending, - required DateTime date, - required int confirmations, - String? to, - String? from, - String? evmSignatureName, - String? contractAddress, - required int chainId, - }) => - EVMChainTransactionInfo(id: id, + TransactionInfo getTransactionInfo({ + required String id, + required int height, + required Money amount, + required Money fee, + required String tokenSymbol, + int exponent = 18, + required TransactionDirection direction, + required bool isPending, + required DateTime date, + required int confirmations, + String? to, + String? from, + String? evmSignatureName, + String? contractAddress, + required int chainId, + }) => + EVMChainTransactionInfo( + id: id, height: height, - ethAmount: ethAmount, - ethFee: ethFee, + amount: amount, + fee: fee, tokenSymbol: tokenSymbol, direction: direction, isPending: isPending, @@ -179,22 +177,6 @@ class CWEVM extends EVM { @override int formatterEVMParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount); - @override - double formatterEVMAmountToDouble({ - TransactionInfo? transaction, - BigInt? amount, - int exponent = 18, - }) { - assert(transaction != null || amount != null); - - if (transaction != null) { - transaction as EVMChainTransactionInfo; - return transaction.ethAmount / BigInt.from(10).pow(transaction.exponent); - } else { - return (amount!) / BigInt.from(10).pow(exponent); - } - } - @override List getERC20Currencies(WalletBase wallet) => (wallet as EVMChainWallet).erc20Currencies; @@ -294,20 +276,16 @@ class CWEVM extends EVM { @override Future createTokenApproval( WalletBase wallet, - BigInt amount, + Money amount, String spender, - CryptoCurrency token, TransactionPriority? priority, { bool useBlinkProtection = true, }) { final evmWallet = wallet as EVMChainWallet; - final feeCurrency = EVMChainUtils.getFeeCurrency(evmWallet.selectedChainId); return evmWallet.createApprovalTransaction( amount, spender, - token, priority as EVMChainTransactionPriority?, - feeCurrency, useBlinkProtection: useBlinkProtection, ); } @@ -317,7 +295,7 @@ class CWEVM extends EVM { WalletBase wallet, String to, String dataHex, - BigInt valueWei, + Money valueWei, TransactionPriority? priority, { bool useBlinkProtection = true, String? sourceTokenAddress, @@ -399,7 +377,7 @@ class CWEVM extends EVM { // Chain-specific integrations (only for Ethereum) @override - Future? getDEuroSavingsBalance(WalletBase wallet) { + Future? getDEuroSavingsBalance(WalletBase wallet) { if (wallet.chainId == 1 && wallet is EVMChainWallet) { return DEuro(wallet).savingsBalance; } @@ -407,7 +385,15 @@ class CWEVM extends EVM { } @override - Future? getDEuroAccruedInterest(WalletBase wallet) { + Future? getDEuroSavingsV1Balance(WalletBase wallet) { + if (wallet.chainId == 1 && wallet is EVMChainWallet) { + return DEuro(wallet).savingsBalanceV1; + } + return null; + } + + @override + Future? getDEuroAccruedInterest(WalletBase wallet) { if (wallet.chainId == 1 && wallet is EVMChainWallet) { return DEuro(wallet).accruedInterest; } @@ -448,6 +434,15 @@ class CWEVM extends EVM { return null; } + @override + Future? withdrawDEuroSavingV1( + WalletBase wallet, TransactionPriority priority) { + if (wallet.chainId == 1 && wallet is EVMChainWallet) { + return DEuro(wallet).withdrawSavingsV1(priority as EVMChainTransactionPriority); + } + return null; + } + @override Future? reinvestDEuroInterest( WalletBase wallet, TransactionPriority priority) { @@ -465,13 +460,6 @@ class CWEVM extends EVM { return null; } - - @override - BigInt getPendingTransactionFee(PendingTransaction tx) => (tx as PendingEVMChainTransaction).fee; - - @override - String getPendingTransactionAmount(PendingTransaction tx) => (tx as PendingEVMChainTransaction).amount; - // Registry helper methods static final EvmChainRegistry _registry = EvmChainRegistry(); @@ -532,14 +520,6 @@ class CWEVM extends EVM { return _registry.getWalletTypeByChainId(chainId); } - @override - BigInt? getERC20AvailableBalance(Object balance) { - if (balance is EVMChainERC20Balance) { - return balance.balance; - } - return null; - } - @override List getAllChains() { final allChains = _registry.getAllChains(); diff --git a/lib/exchange/provider/xoswap_exchange_provider.dart b/lib/exchange/provider/xoswap_exchange_provider.dart index 0c8dff7c8..2b69ef02a 100644 --- a/lib/exchange/provider/xoswap_exchange_provider.dart +++ b/lib/exchange/provider/xoswap_exchange_provider.dart @@ -18,9 +18,11 @@ class XOSwapExchangeProvider extends ExchangeProvider { } void _addAppVersionHeader() async { - final packageInfo = await PackageInfo.fromPlatform(); - final currentVersion = packageInfo.version; - _headers['App-Version'] = currentVersion; + try { + final packageInfo = await PackageInfo.fromPlatform(); + final currentVersion = packageInfo.version; + _headers['App-Version'] = currentVersion; + } catch (_) {} } static const _apiAuthority = 'exchange.exodus.io'; diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 0a378e219..eeefc921f 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -301,14 +301,14 @@ class CWMonero extends Monero { MoneroTransactionCreationCredentials( outputs: outputs .map((out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount)) + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmountMoney, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + )) .toList(), priority: priority as MoneroTransactionPriority); @@ -411,7 +411,7 @@ class CWMonero extends Monero { } @override - bool needExportOutputs(Object wallet, int amount) { + bool needExportOutputs(Object wallet, Money amount) { final moneroWallet = wallet as MoneroWallet; return moneroWallet.needExportOutputs(amount); } diff --git a/lib/nano/cw_nano.dart b/lib/nano/cw_nano.dart index c3a3730d2..aced443d6 100644 --- a/lib/nano/cw_nano.dart +++ b/lib/nano/cw_nano.dart @@ -150,13 +150,12 @@ class CWNano extends Nano { outputs .map((out) => OutputInfo( fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, + cryptoAmount: out.cryptoAmountMoney, address: out.address, note: out.note, sendAll: out.sendAll, extractedAddress: out.extractedAddress, isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount, )) .toList(), ); @@ -175,11 +174,6 @@ class CWNano extends Nano { return (wallet as NanoWallet).updateTransactions(); } - @override - BigInt getTransactionAmountRaw(TransactionInfo transactionInfo) { - return (transactionInfo as NanoTransactionInfo).amountRaw; - } - @override String getRepresentative(Object wallet) { return (wallet as NanoWallet).representative; diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart index fd0b69699..aa7cff5af 100644 --- a/lib/new-ui/pages/send_page.dart +++ b/lib/new-ui/pages/send_page.dart @@ -26,6 +26,8 @@ import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart'; import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; import 'package:cake_wallet/store/app_store.dart'; +import 'package:cw_core/amount/amount_sanitizer.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/lnurl.dart'; import 'package:cw_core/wallet_info.dart'; import "package:cw_core/wallet_type.dart"; @@ -123,7 +125,7 @@ class SendPageModes { helpContent: SendPageHelpContent( title: S.current.about_litecoin_privacy, imagePath: "assets/new-ui/mweb_help.svg", - description: S.current.mweb_help_desc_1 + "\n\n" + S.current.mweb_help_desc_2, + description: "${S.current.mweb_help_desc_1}\n\n${S.current.mweb_help_desc_2}", disclaimer: S.current.mweb_help_disclaimer), popOnConfirmation: false); @@ -134,7 +136,7 @@ class SendPageModes { helpContent: SendPageHelpContent( title: S.current.about_litecoin_privacy, imagePath: "assets/new-ui/mweb_help.svg", - description: S.current.mweb_help_desc_1 + "\n\n" + S.current.mweb_help_desc_2, + description: "${S.current.mweb_help_desc_1}\n\n${S.current.mweb_help_desc_2}", disclaimer: S.current.mweb_help_disclaimer), popOnConfirmation: false); @@ -184,12 +186,12 @@ class NewSendPage extends StatefulWidget { } class _NewSendPageState extends State { - bool _fiatInputMode = false; + int _selectedOutput = 0; - List _amountControllers = []; - List _addressControllers = []; - List _memoControllers = []; + final _amountControllers = []; + final _addressControllers = []; + final _memoControllers = []; final _formKey = GlobalKey(); final _addressFocusNode = FocusNode(); BuildContext? loadingBottomSheetContext; @@ -205,7 +207,7 @@ class _NewSendPageState extends State { reaction((_) => widget.sendViewModel.outputs[_selectedOutput].sendAll, ((bool all) { if (all) { - _fiatInputMode = false; + widget.sendViewModel.outputs[_selectedOutput].isFiatEntry = false; _amountControllers[_selectedOutput].text = S.current.all; } })); @@ -439,46 +441,46 @@ class _NewSendPageState extends State { ? widget.sendViewModel.allAmountValidator : widget.sendViewModel.amountValidator(output), amountController: _amountControllers[_selectedOutput], - currency: _fiatInputMode + currency: output.isFiatEntry ? widget.sendViewModel.fiatCurrency.title : widget.sendViewModel.selectedCryptoCurrencySymbol, - currencyIconPath: _fiatInputMode + currencyIconPath: output.isFiatEntry ? "" : widget.sendViewModel.selectedCryptoCurrency .iconPath ?? "", - hasPicker: (_fiatInputMode || + hasPicker: (output.isFiatEntry || widget.sendViewModel.hasMultipleTokens), - onPickerClicked: () { - _presentCurrencyPicker(context); - }, + onPickerClicked: () => _presentCurrencyPicker(context), + maxDecimals: output.isFiatEntry + ? widget.sendViewModel.fiatCurrency.decimals + : widget.sendViewModel.useBaseUnits + ? 0 + : widget + .sendViewModel.selectedCryptoCurrency.decimals, ), FiatAmountBar( - fiatInputMode: _fiatInputMode, + fiatInputMode: output.isFiatEntry, onSwitchButtonPressed: () { - setState(() { - if(!_fiatInputMode) { - widget.sendViewModel.outputs[_selectedOutput] - .cryptoAmount = - _amountControllers[_selectedOutput].text; - } - _fiatInputMode = !_fiatInputMode; - _amountControllers[_selectedOutput].text = - _fiatInputMode - ? output.fiatAmount - : output.displayCryptoAmount; - }); + widget.sendViewModel.outputs[_selectedOutput] + .isFiatEntry = !output.isFiatEntry; + _amountControllers[_selectedOutput].text = + output.isFiatEntry + ? output.fiatAmount + : output.displayCryptoAmount; }, fiatAmount: _wrapAmount(output.roundedFiatAmount(6), 20), cryptoAmount: _wrapAmount(output.roundedCryptoAmount(6), 20), allAmount: widget.sendViewModel.balance, - cryptoCurrency: + cryptoCurrencySymbol: widget.sendViewModel.selectedCryptoCurrencySymbol, - fiatCurrency: widget.sendViewModel.fiatCurrency.title, + fiatCurrencySymbol: + widget.sendViewModel.fiatCurrency.symbol, onAllButtonPressed: () async { output.setSendAll( await widget.sendViewModel.sendingBalance); + await output.calculateEstimatedFee(); }, ), ], @@ -642,10 +644,10 @@ class _NewSendPageState extends State { return; } - final amount = _amountControllers[_selectedOutput].text.replaceAll(',', '.'); + final amount = _amountControllers[_selectedOutput].text.sanitized(); final output = widget.sendViewModel.outputs[_selectedOutput]; - if (_fiatInputMode) { + if (output.isFiatEntry) { if (amount != output.fiatAmount) { output.sendAll = false; output.setFiatAmount(amount); @@ -705,11 +707,11 @@ class _NewSendPageState extends State { for (var i = 0; i < widget.sendViewModel.outputs.length; i++) { if (i < _amountControllers.length && !widget.sendViewModel.outputs[i].sendAll) { - if (_fiatInputMode) { + if (widget.sendViewModel.outputs[i].isFiatEntry) { widget.sendViewModel.outputs[i].setFiatAmount(_amountControllers[i].text); } else { final amount = widget.sendViewModel.amountParsingProxy.getCanonicalCryptoAmount( - _amountControllers[i].text.replaceAll(",", "."), + _amountControllers[i].text.sanitized(), widget.sendViewModel.selectedCryptoCurrency); widget.sendViewModel.outputs[i].setCryptoAmount(amount); } @@ -752,9 +754,9 @@ class _NewSendPageState extends State { } if (widget.sendViewModel.wallet.type == WalletType.monero) { - int amount = 0; + var amount = Money.zero(widget.sendViewModel.wallet.currency); for (var item in widget.sendViewModel.outputs) { - amount += item.formattedCryptoAmount; + amount += item.cryptoAmountMoney; } if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) { if (widget.sendViewModel.wallet.hardwareWalletType == HardwareWalletType.trezor) { @@ -811,13 +813,11 @@ class _NewSendPageState extends State { } void _presentCurrencyPicker(BuildContext context) { - if (!_fiatInputMode && !widget.sendViewModel.hasMultipleTokens) { - return; - } - final output = widget.sendViewModel.outputs[_selectedOutput]; - if (_fiatInputMode) { + if (!output.isFiatEntry && !widget.sendViewModel.hasMultipleTokens) return; + + if (output.isFiatEntry) { FiatCurrencyPickerSheet.show( context: context, selected: widget.sendViewModel.fiatCurrency, @@ -836,7 +836,7 @@ class _NewSendPageState extends State { amount: '${r.availableBalance} ${r.asset.title}', fiat: isFiatDisabled ? null - : '${r.fiatAvailableBalanceRaw} ${r.fiatCurrencyTicker}', + : '${r.fiatAvailableBalanceRaw} ${r.fiatCurrency?.symbol}', fiatValue: isFiatDisabled ? null : double.tryParse(r.fiatAvailableBalanceRaw), ), }; @@ -859,10 +859,11 @@ class _NewSendPageState extends State { void _handleLightningInvoicePaste() { try { - final lnAmount = CryptoCurrency.btcln.formatAmount( - BigInt.from(getBolt11Amount(_addressControllers[_selectedOutput].text) ?? 0)); - if (lnAmount != 0) { - _amountControllers[_selectedOutput].text = lnAmount; + final lnAmount = getBolt11Amount(_addressControllers[_selectedOutput].text) ?? + Money.zero(CryptoCurrency.btcln); + if (!lnAmount.isZero) { + _amountControllers[_selectedOutput].text = + widget.sendViewModel.amountParsingProxy.asDisplayString(lnAmount); } } catch (_) {} } @@ -885,6 +886,12 @@ class _NewSendPageState extends State { await widget.sendViewModel.fetchTokenForContractAddress(paymentRequest.contractAddress!); } + // This automatically switches to lightning mode if you are in a bitcoin wallet + if (result.addressDetectionResult?.detectedCurrency == CryptoCurrency.btcln) { + widget.sendViewModel.selectedCryptoCurrency = CryptoCurrency.btcln; + widget.sendViewModel.coinTypeToSpendFrom = UnspentCoinType.lightning; + } + switch (result.type) { case PaymentFlowType.singleWallet: case PaymentFlowType.multipleWallets: @@ -1216,7 +1223,7 @@ class _NewSendPageState extends State { try{ _amountControllers[_selectedOutput].text = widget.sendViewModel.amountParsingProxy .getDisplayCryptoAmount( - paymentRequest.amount, widget.sendViewModel.selectedCryptoCurrency); + paymentRequest.amount, widget.sendViewModel.selectedCryptoCurrency); } catch(e) { } diff --git a/lib/new-ui/pages/swap_page.dart b/lib/new-ui/pages/swap_page.dart index 1d8427891..1e897743e 100644 --- a/lib/new-ui/pages/swap_page.dart +++ b/lib/new-ui/pages/swap_page.dart @@ -8,7 +8,6 @@ import 'package:cake_wallet/exchange/exchange_trade_state.dart'; import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart'; import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart'; import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; 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'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/utils/debounce.dart'; +import 'package:cake_wallet/utils/decimal_input_formatter.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/permission_handler.dart'; import 'package:cw_core/wallet_info.dart'; @@ -137,9 +137,8 @@ class _NewSwapPageState extends State { }); }); receiveFiatAmountController.addListener(() { - if (!receiveKey.currentState!.amountFocusNode.hasFocus) { - return; - } + if (!receiveKey.currentState!.amountFocusNode.hasFocus) return; + widget.exchangeViewModel.enableFixedRateMode(); widget.exchangeViewModel.isSendAllEnabled = false; Future.delayed(Duration(milliseconds: 200)).then((_) { @@ -168,6 +167,8 @@ class _NewSwapPageState extends State { }); reaction((_) => widget.exchangeViewModel.depositAmount, (String amount) { + if (depositKey.currentState!.amountFocusNode.hasFocus) return; + if (widget.exchangeViewModel.isSendAllEnabled) { depositAmountController.text = S.of(context).all; } else if (depositAmountController.text != amount && amount != S.of(context).all) { @@ -191,14 +192,14 @@ class _NewSwapPageState extends State { widget.exchangeViewModel, widget.exchangeViewModel.depositCurrency, depositKey)); reaction( - (_) => widget.exchangeViewModel.receiveCurrency, - (CryptoCurrency currency) => - _onCurrencyChange(currency, widget.exchangeViewModel, receiveKey)); + (_) => widget.exchangeViewModel.receiveCurrency, + (currency) => _onCurrencyChange(currency, widget.exchangeViewModel, receiveKey), + ); reaction( - (_) => widget.exchangeViewModel.depositCurrency, - (CryptoCurrency currency) => - _onCurrencyChange(currency, widget.exchangeViewModel, depositKey)); + (_) => widget.exchangeViewModel.depositCurrency, + (currency) => _onCurrencyChange(currency, widget.exchangeViewModel, depositKey), + ); reaction((_) => widget.exchangeViewModel.depositAddress, (String address) { if (depositKey.currentState!.addressController.text != address) { @@ -206,7 +207,7 @@ class _NewSwapPageState extends State { } }); - reaction((_) => widget.exchangeViewModel.isDepositAddressEnabled, (bool isEnabled) {}); + reaction((_) => widget.exchangeViewModel.isDepositAddressEnabled, (isEnabled) {}); reaction((_) => widget.exchangeViewModel.receiveAmount, (String amount) { if (receiveKey.currentState!.amountController.text != amount) { @@ -398,14 +399,12 @@ class _NewSwapPageState extends State { if (key == depositKey && !isCurrentTypeWallet) exchangeViewModel.isSendFromExternal = true; if (key == depositKey && isCurrentTypeWallet) exchangeViewModel.isSendFromExternal = false; - - key.currentState!.changeSelectedCurrency(currency); - - if (key == depositKey) + if (key == depositKey) { key.currentState!.changeAddress( - address: isCurrentTypeWallet - ? exchangeViewModel.wallet.walletAddresses.addressForExchange - : ''); + address: + isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.addressForExchange : '', + ); + } } void _onWalletNameChange(ExchangeViewModel exchangeViewModel, CryptoCurrency currency, @@ -492,7 +491,7 @@ class _NewSwapPageState extends State { child: Form( key: formKey, child: SingleChildScrollView( - physics: ClampingScrollPhysics(), + physics: const ClampingScrollPhysics(), controller: ModalScrollController.of(context), child: Column( children: [ @@ -508,7 +507,8 @@ class _NewSwapPageState extends State { : null, key: depositKey, title: S.of(context).send, - initialCurrency: widget.exchangeViewModel.depositCurrency, + currency: widget.exchangeViewModel.depositCurrency, + useBaseUnit: widget.exchangeViewModel.useDepositBaseUnit, hasRefundAddress: true, currencies: widget.exchangeViewModel.depositCurrencies, onCurrencySelected: (currency) { @@ -522,11 +522,13 @@ class _NewSwapPageState extends State { value != S.of(context).all ? AmountValidator( isAutovalidate: true, - currency: widget.exchangeViewModel.isFixedRateMode + currency: widget.exchangeViewModel.isFixedRateMode ? widget.exchangeViewModel.receiveCurrency : widget.exchangeViewModel.depositCurrency, - minValue: widget.exchangeViewModel.limits.min.toString(), - maxValue: widget.exchangeViewModel.limits.max.toString(), + minValue: + widget.exchangeViewModel.limits.min.toString(), + maxValue: + widget.exchangeViewModel.limits.max.toString(), amountParsingProxy: widget.exchangeViewModel.amountParsingProxy, ).call(value) @@ -579,8 +581,9 @@ class _NewSwapPageState extends State { onDispose: disposeBestRateSync, key: receiveKey, title: S.of(context).receive, - initialCurrency: widget.exchangeViewModel.receiveCurrency, currencies: widget.exchangeViewModel.receiveCurrencies, + currency: widget.exchangeViewModel.receiveCurrency, + useBaseUnit: widget.exchangeViewModel.useReceiveBaseUnit, onCurrencySelected: (currency) { if (currency is CryptoCurrency) { widget.exchangeViewModel @@ -685,7 +688,6 @@ class _NewSwapPageState extends State { isLoading: widget.exchangeViewModel.tradeState is TradeIsCreating, ), ), - SizedBox() ], ), ) @@ -701,18 +703,14 @@ class _NewSwapPageState extends State { } bool _swapButtonDisabled() { - if (widget.exchangeViewModel.selectedProviders.isEmpty) { - return true; - } - if (widget.exchangeViewModel.receiveAddress.isEmpty) { - return true; - } - if (widget.exchangeViewModel.status is! SyncedSyncStatus) { - return true; - } - if (widget.exchangeViewModel.depositAmount.isEmpty) { - return true; - } + if (widget.exchangeViewModel.selectedProviders.isEmpty) return true; + + if (widget.exchangeViewModel.receiveAddress.isEmpty) return true; + + if (widget.exchangeViewModel.status is! SyncedSyncStatus) return true; + + if (widget.exchangeViewModel.depositAmount.isEmpty) return true; + return false; } } @@ -725,9 +723,7 @@ class SwapProviderPreview extends StatelessWidget { @override Widget build(BuildContext context) { return Observer(builder: (_) { - if (exchangeViewModel.depositAmount.isEmpty) { - return SizedBox.shrink(); - } + if (exchangeViewModel.depositAmount.isEmpty) return SizedBox.shrink(); final provider = exchangeViewModel.forcedProvider ?? exchangeViewModel.providerDisplay; final rate = exchangeViewModel.forcedProvider == null @@ -738,8 +734,10 @@ class SwapProviderPreview extends StatelessWidget { onTap: () { if (provider != null) { Navigator.of(context).push(CupertinoPageRoute( - builder: (context) => - Material(child: ProviderSelectorPage(exchangeViewModel: exchangeViewModel)))); + builder: (context) => Material( + child: ProviderSelectorPage(exchangeViewModel: exchangeViewModel), + ), + )); } }, child: Container( @@ -757,7 +755,7 @@ class SwapProviderPreview extends StatelessWidget { spacing: 12, children: [ if (provider != null) - CakeImageWidget(imageUrl:provider.description.image,width:28,height:28), + CakeImageWidget(imageUrl: provider.description.image, width: 28, height: 28), if (provider == null) CupertinoActivityIndicator(), Text( provider?.title ?? "${S.of(context).finding_provider}...", @@ -799,7 +797,7 @@ class SwapProviderPreview extends StatelessWidget { class SwapAmountBox extends StatefulWidget { SwapAmountBox({ Key? key, - required this.initialCurrency, + required this.currency, required this.currencies, required this.onCurrencySelected, this.currencyValueValidator, @@ -807,6 +805,7 @@ class SwapAmountBox extends StatefulWidget { this.title = '', this.hasRefundAddress = false, this.hasAllAmount = false, + this.useBaseUnit = false, this.allAmount, this.onPushPasteButton, this.onPushAddressBookButton, @@ -820,7 +819,8 @@ class SwapAmountBox extends StatefulWidget { final Function(Currency) onCurrencySelected; final String title; final bool isReceiverCard; - final Currency initialCurrency; + final Currency currency; + final bool useBaseUnit; final bool hasRefundAddress; final FormFieldValidator? currencyValueValidator; final FormFieldValidator? addressTextFieldValidator; @@ -848,10 +848,6 @@ class SwapAmountBoxState extends State { @override void initState() { - _selectedCurrency = widget.initialCurrency; - - super.initState(); - if (widget.isReceiverCard) { memoController.text = widget.exchangeViewModel.receiveAddressExtraId; @@ -869,6 +865,8 @@ class SwapAmountBoxState extends State { } }); } + + super.initState(); } @override @@ -881,18 +879,18 @@ class SwapAmountBoxState extends State { super.dispose(); } - late Currency _selectedCurrency; bool _fiatInputMode = false; @override Widget build(BuildContext context) { - final currencyToShow = (_selectedCurrency is CryptoCurrency) + final currencyToShow = (widget.currency is CryptoCurrency) ? widget.exchangeViewModel.amountParsingProxy - .getCryptoSymbol(_selectedCurrency as CryptoCurrency) - : _selectedCurrency.name.toUpperCase(); + .getCryptoSymbol(widget.currency as CryptoCurrency) + : widget.currency.name.toUpperCase(); - - final chainIconPath = (_selectedCurrency is CryptoCurrency) ? _getCurrencyChainIconPath(_selectedCurrency as CryptoCurrency) : null; + final chainIconPath = (widget.currency is CryptoCurrency) + ? _getCurrencyChainIconPath(widget.currency as CryptoCurrency) + : null; return Column( spacing: 12, @@ -928,36 +926,48 @@ class SwapAmountBoxState extends State { children: [ Flexible( child: IntrinsicWidth( - child: TextFormField( - keyboardType: - TextInputType.numberWithOptions(signed: false, decimal: true), - validator: _fiatInputMode ? null : widget.currencyValueValidator, - controller: - _fiatInputMode ? fiatAmountController : amountController, - focusNode: amountFocusNode, - style: TextStyle( + child: TextFormField( + keyboardType: TextInputType.numberWithOptions( + signed: false, + decimal: !widget.useBaseUnit, + ), + validator: _fiatInputMode ? null : widget.currencyValueValidator, + controller: + _fiatInputMode ? fiatAmountController : amountController, + focusNode: amountFocusNode, + style: TextStyle( fontSize: 28, color: Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.w400), - decoration: InputDecoration( - contentPadding: EdgeInsets.zero, - isDense: true, - hintText: "0", - fillColor: Colors.transparent, - hoverColor: Colors.transparent, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, + fontWeight: FontWeight.w400, + ), + decoration: InputDecoration( + contentPadding: EdgeInsets.zero, + isDense: true, + hintText: "0", + fillColor: Colors.transparent, + hoverColor: Colors.transparent, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + ), + inputFormatters: [ + DecimalInputFormatter( + maxDecimals: + widget.useBaseUnit ? 0 : widget.currency.decimals, + ), + ], ), - )), + ), ), if (_fiatInputMode) Center( - child: Text( - widget.exchangeViewModel.fiat.title, - style: TextStyle( + child: Text( + widget.exchangeViewModel.fiat.title, + style: TextStyle( fontSize: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant), - )), + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), ], ), ), @@ -966,17 +976,19 @@ class SwapAmountBoxState extends State { child: Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(999999)), + borderRadius: BorderRadius.circular(999999), + ), child: Padding( padding: const EdgeInsets.only(top: 4.0, bottom: 4, left: 4, right: 4), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - TokenImageWidget( - imageUrl: _selectedCurrency.iconPath ?? "", - size: 28, + CakeImageWidget( + imageUrl: widget.currency.iconPath ?? "", + width: 28, + height: 28, ), - SizedBox(width: 10), + const SizedBox(width: 10), Text( currencyToShow, textAlign: TextAlign.center, @@ -988,12 +1000,13 @@ class SwapAmountBoxState extends State { width: 12, height: 12, colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurfaceVariant, - BlendMode.srcIn), + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn, + ), ), - SizedBox(width: 6) + const SizedBox(width: 6) ] else - SizedBox(width: 10), + const SizedBox(width: 10), Container( width:16,height:16, decoration: BoxDecoration( @@ -1003,17 +1016,20 @@ class SwapAmountBoxState extends State { child: Padding( padding: const EdgeInsets.all(4.0), child: RotatedBox( - quarterTurns: 2, - child: CakeImageWidget(imageUrl: - "assets/new-ui/dropdown_arrow.svg", - width: 4, - height: 4, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary, BlendMode.srcIn), - )), + quarterTurns: 2, + child: CakeImageWidget( + imageUrl: "assets/new-ui/dropdown_arrow.svg", + width: 4, + height: 4, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + ), ), ), - SizedBox(width:4), + const SizedBox(width: 4), ], ), ), @@ -1029,36 +1045,33 @@ class SwapAmountBoxState extends State { !widget.exchangeViewModel.hasAllAmount); return FiatAmountBar( - foregroundElementColor: Theme.of(context).colorScheme.surfaceContainerHigh, - textColor: Theme.of(context).colorScheme.onSurfaceVariant, - allAmountColor: hasAllAmount ? null : Colors.transparent, - allAmountTextColor: - hasAllAmount ? null : Theme.of(context).colorScheme.onSurfaceVariant, - fiatInputMode: _fiatInputMode, - allAmount: !hasAllAmount - ? widget.isReceiverCard - ? null - : widget.exchangeViewModel.balanceDisplay - : widget.exchangeViewModel.depositAvailableAmount, - onSwitchButtonPressed: () { - setState(() { - _fiatInputMode = !_fiatInputMode; - }); - if (_fiatInputMode) { - updateFiatAmount(); - } - }, - onAllButtonPressed: widget.allAmount, - cryptoAmount: widget.isReceiverCard - ? widget.exchangeViewModel.roundedReceiveAmount(6) - : widget.exchangeViewModel.roundedDepositAmount(6), - fiatAmount: widget.isReceiverCard - ? widget.exchangeViewModel.roundedReceiveAmountFiat(6) - : widget.exchangeViewModel.roundedDepositAmountFiat(6), - cryptoCurrency: widget.isReceiverCard - ? widget.exchangeViewModel.receiveCurrency.title - : widget.exchangeViewModel.depositCurrency.title, - fiatCurrency: widget.exchangeViewModel.fiat.name); + foregroundElementColor: Theme.of(context).colorScheme.surfaceContainerHigh, + textColor: Theme.of(context).colorScheme.onSurfaceVariant, + allAmountColor: hasAllAmount ? null : Colors.transparent, + allAmountTextColor: + hasAllAmount ? null : Theme.of(context).colorScheme.onSurfaceVariant, + fiatInputMode: _fiatInputMode, + allAmount: !hasAllAmount + ? widget.isReceiverCard + ? null + : widget.exchangeViewModel.balanceDisplay + : widget.exchangeViewModel.depositAvailableAmount, + onSwitchButtonPressed: () { + setState(() => _fiatInputMode = !_fiatInputMode); + if (_fiatInputMode) { + updateFiatAmount(); + } + }, + onAllButtonPressed: widget.allAmount, + cryptoAmount: widget.isReceiverCard + ? widget.exchangeViewModel.roundedReceiveAmount(6) + : widget.exchangeViewModel.roundedDepositAmount(6), + fiatAmount: widget.isReceiverCard + ? widget.exchangeViewModel.roundedReceiveAmountFiat(6) + : widget.exchangeViewModel.roundedDepositAmountFiat(6), + cryptoCurrencySymbol: currencyToShow, + fiatCurrencySymbol: widget.exchangeViewModel.fiat.symbol, + ); }, ), Observer( @@ -1107,22 +1120,25 @@ class SwapAmountBoxState extends State { ? Theme.of(context).colorScheme.onPrimary : Theme.of(context).colorScheme.onSurface), ), - Text(addressDescription, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary)) + Text( + addressDescription, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary), + ) ], ), RotatedBox( quarterTurns: 2, - child: CakeImageWidget(imageUrl: - "assets/new-ui/dropdown_arrow.svg", + child: CakeImageWidget( + imageUrl: "assets/new-ui/dropdown_arrow.svg", colorFilter: ColorFilter.mode( - (addressEmpty && widget.isReceiverCard) - ? Theme.of(context).colorScheme.onPrimary - : Theme.of(context).colorScheme.onSurface, - BlendMode.srcIn), + (addressEmpty && widget.isReceiverCard) + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context).colorScheme.onSurface, + BlendMode.srcIn, + ), ), ) ], @@ -1146,18 +1162,14 @@ class SwapAmountBoxState extends State { widget.exchangeViewModel.receiveAddress.isEmpty) ...[ ModernButton.svg( svgPath: "assets/new-ui/paste.svg", - onPressed: () { - widget.onPushPasteButton?.call(context); - }, + onPressed: () => widget.onPushPasteButton?.call(context), size: 36, iconSize: 20, backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, ), ModernButton.svg( svgPath: "assets/new-ui/scan.svg", - onPressed: () { - _presentQRScanner(context); - }, + onPressed: () => _presentQRScanner(context), size: 36, iconSize: 20, backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, @@ -1172,7 +1184,7 @@ class SwapAmountBoxState extends State { final selected = widget.exchangeViewModel.receiveCurrency; final labelType = memoLabelTypeFor(selected); if (labelType == null) return const SizedBox.shrink(); - + final isDestinationTag = labelType == MemoLabelType.destinationTag; final hint = isDestinationTag ? S.of(context).destination_tag_optional @@ -1180,7 +1192,7 @@ class SwapAmountBoxState extends State { final disclaimer = isDestinationTag ? S.of(context).destination_tag_swap_disclaimer : S.of(context).memo_swap_disclaimer; - + return NewSendMemoInput( memoController: memoController, maxMemoLength: isDestinationTag ? 20 : 256, @@ -1197,10 +1209,6 @@ class SwapAmountBoxState extends State { ); } - void changeSelectedCurrency(Currency currency) { - setState(() => _selectedCurrency = currency); - } - void changeAddress({required String address}) { setState(() => addressController.text = _normalizeAddressFormat(address)); } @@ -1343,18 +1351,15 @@ class SwapAmountBoxState extends State { } String? _getCurrencyChainIconPath(CryptoCurrency curr) { - try{ + try { + if (curr.chainIconPath != null) return curr.chainIconPath!; - if(curr.chainIconPath != null) return curr.chainIconPath!; - - - if(curr.tag != null) { + if (curr.tag != null) { final currencyFromTag = CryptoCurrency.fromString(curr.tag!); - if(currencyFromTag.chainIconPath != null) { - return currencyFromTag.chainIconPath!; - } + + if (currencyFromTag.chainIconPath != null) return currencyFromTag.chainIconPath!; } - }catch(_){} + } catch (_) {} return null; } } diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart index 5613132bc..e4f88a7b7 100644 --- a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart @@ -24,7 +24,6 @@ import 'package:flutter/material.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import '../../../pages/receive_page.dart'; -import '../../../pages/scan_page.dart'; import 'coin_action_button.dart'; class CoinActionRow extends StatelessWidget { @@ -165,11 +164,11 @@ class CoinActionRow extends StatelessWidget { var unspentCoinType = UnspentCoinType.any; if (SendViewModelBase.isNonZeroAmountLightningInvoice(code)) { unspentCoinType = UnspentCoinType.lightning; - final amount = CryptoCurrency.btcln.formatAmount(BigInt.from(getBolt11Amount(code) ?? 0)); + final amount = getBolt11Amount(code)?.toString() ?? "0"; req = PaymentRequest(code, amount, "", "", ""); } else if (SendViewModelBase.isLnurlInvoice(code)) { unspentCoinType = UnspentCoinType.lightning; - final amount = CryptoCurrency.btcln.formatAmount(BigInt.from(await LNURL.getPayRequestAmount(code) ?? 0)); + final amount = (await LNURL.getPayRequestAmount(code))?.toString() ?? "0"; req = PaymentRequest(code, amount, "", "", ""); } else if (OpenCryptoPayService.isOpenCryptoPayQR(code)) { req = PaymentRequest(code, "", "", "", ""); diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_section.dart b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart index 8861523f3..4e34ff1ad 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_section.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/anonpay_history_tile.dart'; 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'; import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; import 'package:flutter/material.dart'; @@ -111,8 +113,8 @@ class HistorySection extends StatelessWidget { provider: trade.provider, date: DateFormat('HH:mm').format(item.trade.createdAt ?? DateTime.now()), - amount: trade.amountFormatted(), - receiveAmount: trade.receiveAmountFormatted(), + amount: dashboardViewModel.balanceDisplayMode == BalanceDisplayMode.hiddenBalance ? "---" : trade.amountFormatted(), + receiveAmount: dashboardViewModel.balanceDisplayMode == BalanceDisplayMode.hiddenBalance ? "---" : trade.receiveAmountFormatted(), roundedBottom: roundedBottom, roundedTop: roundedTop, bottomSeparator: !roundedBottom, @@ -132,7 +134,7 @@ class HistorySection extends StatelessWidget { child: HistoryOrderTile( date: DateFormat('HH:mm').format(item.order.createdAt), amount: item.orderFormattedAmount, - amountFiat: "USD 0.00", + amountFiat: "", roundedBottom: roundedBottom, roundedTop: roundedTop, bottomSeparator: !roundedBottom, @@ -149,8 +151,8 @@ class HistorySection extends StatelessWidget { child: PayjoinHistoryTile( createdAt: DateFormat('HH:mm').format(session.inProgressSince!), amount: dashboardViewModel.appStore.amountParsingProxy - .getDisplayCryptoString( - session.amount.toInt(), CryptoCurrency.btc), + .asDisplayString(Money( + session.amount, CryptoCurrency.btc)), currency: item.transaction?.from ?? "BTC", state: item.status, isSending: session.isSenderSession, diff --git a/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart b/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart index aa9b522a5..b01f4dea0 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart @@ -47,6 +47,8 @@ class _TransactionDetailsModalState extends State { @override Widget build(BuildContext context) { + final transactionInfoAmount = widget.transactionDetailsViewModel.transactionInfo.amount; + return DraggableScrollableSheet( expand: false, initialChildSize: 0.9, @@ -90,12 +92,12 @@ class _TransactionDetailsModalState extends State { ), CopyWrapper( requireLongPress: true, - data: ClipboardData(text: widget.transactionDetailsViewModel.formattedCryptoAmount), + data: ClipboardData(text: transactionInfoAmount.toString()), builder: (context, copied)=> AnimatedSwitcher( duration: Duration(milliseconds: 300), child: Text( key: ValueKey(copied), - copied ? S.of(context).copied : widget.transactionDetailsViewModel.formattedCryptoAmount, + copied ? S.of(context).copied : transactionInfoAmount.toStringWithSymbol(), style: TextStyle(fontSize: 28, color: copied ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface), ), ), @@ -122,7 +124,7 @@ class _TransactionDetailsModalState extends State { label: item.title, trailingWidget: shouldBuildBottomWidget ? null - : _buildTrailingWIdget(item), + : _buildTrailingWidget(item), bottomWidget: shouldBuildBottomWidget ? _buildBottomWidget(item) : null); @@ -203,7 +205,7 @@ class _TransactionDetailsModalState extends State { )); } - Widget _buildTrailingWIdget(TransactionDetailsListItem item) { + Widget _buildTrailingWidget(TransactionDetailsListItem item) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: switch (item.runtimeType) { diff --git a/lib/new-ui/widgets/coins_page/cards/balance_card.dart b/lib/new-ui/widgets/coins_page/cards/balance_card.dart index 88fff31db..8167ac492 100644 --- a/lib/new-ui/widgets/coins_page/cards/balance_card.dart +++ b/lib/new-ui/widgets/coins_page/cards/balance_card.dart @@ -70,8 +70,6 @@ class BalanceCard extends StatelessWidget { final height = width * 0.62; - - return AnimatedContainer( duration: designSwitchDuration, width: width, @@ -208,7 +206,7 @@ class BalanceCard extends StatelessWidget { }, child: Text( key: ValueKey(fiatFirst ? balance : fiatBalance), - fiatFirst ? "$assetName $balance" : "$fiatCurrencyTitle $fiatBalance", + fiatFirst ? "$assetName $balance" : fiatBalance, ), ), ), diff --git a/lib/new-ui/widgets/coins_page/cards/cards_view.dart b/lib/new-ui/widgets/coins_page/cards/cards_view.dart index 427660b90..238f20502 100644 --- a/lib/new-ui/widgets/coins_page/cards/cards_view.dart +++ b/lib/new-ui/widgets/coins_page/cards/cards_view.dart @@ -119,23 +119,22 @@ class _CardsViewState extends State { late final String walletBalance; late final String walletFiatBalance; if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) { - if(widget.dashboardViewModel.balanceViewModel.displayMode == BalanceDisplayMode.hiddenBalance) { + if (widget.dashboardViewModel.balanceViewModel.displayMode == + BalanceDisplayMode.hiddenBalance) { walletBalance = '●●●●●●'; walletFiatBalance = '●●●●●●'; } else { walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0"; walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00"; } - } else if(widget.dashboardViewModel.balanceViewModel.showCombinedBalance){ + } else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) { walletBalance = ""; walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance; - - }else { + } else { walletBalance = walletBalanceRecord?.availableBalance ?? "0"; - walletFiatBalance = walletBalanceRecord?.fiatAvailableBalanceRaw ?? "0.00"; + walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00"; } - // the card designs is empty if widget gets built before it loads. // should get populated before user sees anything final CardDesign cardDesign; @@ -191,7 +190,8 @@ class _CardsViewState extends State { assetName: assetName, capitalizeAssetName: _shouldCapitalizeAssetName(), balance: walletBalance, - fiatCurrencyTitle: walletBalanceRecord?.fiatCurrencyTicker ?? widget.dashboardViewModel.settingsStore.fiatCurrency.title, + fiatCurrencyTitle: walletBalanceRecord?.fiatCurrency?.title ?? + widget.dashboardViewModel.settingsStore.fiatCurrency.title, fiatFirst: widget.dashboardViewModel.balanceViewModel.showCombinedBalance, fiatBalance: walletFiatBalance, selected: _selectedIndex == visualIndex, diff --git a/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart b/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart index dc169e6b9..5246f611d 100644 --- a/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart +++ b/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart @@ -48,12 +48,10 @@ class UnconfirmedBalanceWidget extends StatelessWidget { showMaterialModalBottomSheet( backgroundColor: Colors.transparent, context: context, - builder: (context) { - return UnconfirmedBalanceModal( - balance: "${balance} ${currency.title}", + builder: (context) => UnconfirmedBalanceModal( + balance: balance.toStringWithSymbol(), currencyIconPath: currency.iconPath ?? "", - ); - }); + )); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), @@ -101,9 +99,8 @@ class UnconfirmedBalanceWidget extends StatelessWidget { ), ], ) - : const SizedBox( - width: double.infinity, - )); + : const SizedBox(width: double.infinity), + ); }); } } diff --git a/lib/new-ui/widgets/receive_page/receive_amount_modal.dart b/lib/new-ui/widgets/receive_page/receive_amount_modal.dart index 9ad112ffa..0a196f37e 100644 --- a/lib/new-ui/widgets/receive_page/receive_amount_modal.dart +++ b/lib/new-ui/widgets/receive_page/receive_amount_modal.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart'; import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:cake_wallet/utils/decimal_input_formatter.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; @@ -134,8 +135,16 @@ class _ReceiveAmountModalState extends State { textAlign: TextAlign.left, textAlignVertical: TextAlignVertical.center, controller: _amountController, - keyboardType: - TextInputType.numberWithOptions(signed: false, decimal: !widget.walletAddressListViewModel.useSatoshi), + keyboardType: TextInputType.numberWithOptions( + signed: false, + decimal: widget.walletAddressListViewModel.selectedCurrencyDecimals > 0, + ), + inputFormatters: [ + DecimalInputFormatter( + maxDecimals: + widget.walletAddressListViewModel.selectedCurrencyDecimals, + ), + ], decoration: InputDecoration( hint: Text( widget.walletAddressListViewModel.useSatoshi ? "0" : "0.00000000", @@ -150,6 +159,7 @@ class _ReceiveAmountModalState extends State { border: InputBorder.none, filled: true, fillColor: Colors.transparent, + ), style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), diff --git a/lib/new-ui/widgets/send_page/fiat_amount_bar.dart b/lib/new-ui/widgets/send_page/fiat_amount_bar.dart index 6fb3e4dc3..72445ada2 100644 --- a/lib/new-ui/widgets/send_page/fiat_amount_bar.dart +++ b/lib/new-ui/widgets/send_page/fiat_amount_bar.dart @@ -2,7 +2,6 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; class FiatAmountBar extends StatelessWidget { const FiatAmountBar( @@ -12,8 +11,8 @@ class FiatAmountBar extends StatelessWidget { this.onAllButtonPressed, required this.cryptoAmount, required this.fiatAmount, - required this.cryptoCurrency, - required this.fiatCurrency, + required this.cryptoCurrencySymbol, + required this.fiatCurrencySymbol, this.allAmount, this.foregroundElementColor, this.textColor, this.allAmountColor, this.allAmountTextColor}); @@ -24,8 +23,8 @@ class FiatAmountBar extends StatelessWidget { final String cryptoAmount; final String fiatAmount; - final String cryptoCurrency; - final String fiatCurrency; + final String cryptoCurrencySymbol; + final String fiatCurrencySymbol; final String? allAmount; final Color? foregroundElementColor; final Color? textColor; @@ -47,16 +46,15 @@ class FiatAmountBar extends StatelessWidget { iconSize: 18, onPressed: onSwitchButtonPressed, ), - Observer( - builder: (_) => GestureDetector( - onTap: onSwitchButtonPressed, - child: Text( - fiatInputMode - ? "${cryptoAmount.isEmpty ? "0" : cryptoAmount.withMaxDecimals(8)} ${cryptoCurrency}" - : "${fiatAmount.isEmpty ? "0" : fiatAmount} ${fiatCurrency}", - style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface), - ), - )), + GestureDetector( + onTap: onSwitchButtonPressed, + child: Text( + fiatInputMode + ? "${cryptoAmount.isEmpty ? "0" : cryptoAmount.withMaxDecimals(8)} $cryptoCurrencySymbol" + : "${fiatAmount.isEmpty ? "0" : fiatAmount} $fiatCurrencySymbol", + style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface), + ), + ), ], ), if (allAmount != null && allAmount!.isNotEmpty) diff --git a/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart b/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart index 94e8c1909..cea40e197 100644 --- a/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart +++ b/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart @@ -18,7 +18,6 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; enum l2actions { deposit, withdraw } diff --git a/lib/new-ui/widgets/send_page/send_amount_input.dart b/lib/new-ui/widgets/send_page/send_amount_input.dart index 5d51baba4..a9576d131 100644 --- a/lib/new-ui/widgets/send_page/send_amount_input.dart +++ b/lib/new-ui/widgets/send_page/send_amount_input.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart'; import 'package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:cake_wallet/utils/decimal_input_formatter.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -9,15 +10,18 @@ class NewSendAmountInput extends StatefulWidget { const NewSendAmountInput( {super.key, required this.currency, + required this.maxDecimals, required this.hasPicker, required this.onPickerClicked, required this.currencyIconPath, required this.amountController, - this.validator}); + this.validator, + }); final String currency; final String currencyIconPath; final bool hasPicker; + final int maxDecimals; final VoidCallback onPickerClicked; final TextEditingController amountController; final FormFieldValidator? validator; @@ -63,14 +67,19 @@ class _NewSendAmountInputState extends State { children: [ Expanded( child: TextField( - keyboardType: - TextInputType.numberWithOptions(signed: false, decimal: true), + keyboardType: TextInputType.numberWithOptions( + signed: false, + decimal: widget.maxDecimals > 0, + ), inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^\d*[.,]?\d*$')) + DecimalInputFormatter(maxDecimals: widget.maxDecimals), ], - onChanged: state.didChange, controller: widget.amountController, - decoration: InputDecoration(hintText: "0", errorMaxLines: 3), + decoration: InputDecoration( + hintText: widget.maxDecimals > 0 ? "0" : "0.00", + errorMaxLines: 3, + ), + onChanged: state.didChange, ), ), FloatingIconButton( @@ -81,7 +90,6 @@ class _NewSendAmountInputState extends State { widget.amountController.text = data.text!; } }), - SizedBox.shrink() ], ), ), diff --git a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart index 0b3a867d6..e573de4f2 100644 --- a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart +++ b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart @@ -17,13 +17,13 @@ import 'package:cake_wallet/utils/address_formatter.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:mobx/mobx.dart'; class SendConfirmSheet extends StatefulWidget { @@ -173,8 +173,8 @@ class SendTransactionDetails extends StatelessWidget { double sumBy(List list, double Function(T) picker) => list.map(picker).fold(0.0, (a, b) => a + b); - BigInt sumByBigInt(List list, BigInt Function(T) picker) => - list.map(picker).fold(BigInt.zero, (a, b) => a + b); + Money sumByMoney(List list, Money Function(T) picker, CryptoCurrency currency) => + list.map(picker).fold(Money.zero(currency), (a, b) => a + b); String sumStr(List list, double Function(T) picker) => sumBy(list, picker).toString(); @@ -184,220 +184,188 @@ class SendTransactionDetails extends StatelessWidget { return "${decimals == null ? str : str.withDecimals(decimals)} $unit"; } - Widget _buildMainContent(BuildContext context) { - final transaction = sendViewModel.pendingTransaction; + return Observer(builder: (context) { + final transaction = sendViewModel.pendingTransaction; - final currencySymbol = sendViewModel.amountParsingProxy.getCryptoSymbol(sendViewModel.selectedCryptoCurrency); + final currencySymbol = + sendViewModel.amountParsingProxy.getCryptoSymbol(sendViewModel.selectedCryptoCurrency); - final amount = (transaction == null) - ? sendViewModel.amountParsingProxy.getDisplayCryptoStringFromBigInt( - sumByBigInt(sendViewModel.outputs, (o) { + final amount = (transaction == null) + ? sendViewModel.amountParsingProxy.asDisplayString(sumByMoney(sendViewModel.outputs, (o) { + final zero = Money.zero(sendViewModel.selectedCryptoCurrency); if (o.sendAll) return sendViewModel.amountParsingProxy.tryParseCryptoString( sendViewModel.balance, sendViewModel.selectedCryptoCurrency) ?? - BigInt.zero; + zero; - return sendViewModel.selectedCryptoCurrency.tryParseAmount(o.cryptoAmount) ?? - BigInt.zero; - }), - sendViewModel.selectedCryptoCurrency) - : sendViewModel.amountParsingProxy.getDisplayCryptoAmount( - formatAmount(transaction.amountFormatted), sendViewModel.selectedCryptoCurrency).withMaxDecimals(8); + return sendViewModel.selectedCryptoCurrency.tryParseAmount(o.cryptoAmount) ?? zero; + }, sendViewModel.selectedCryptoCurrency)) + : sendViewModel.amountParsingProxy.asDisplayString(transaction.amount); - final fee = - "${(transaction == null) ? sendViewModel.amountParsingProxy.getDisplayCryptoStringFromBigInt(sumByBigInt( - sendViewModel.outputs.where((e) => !e.sendAll).toList(), - (o) { - if (sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln) { - return sendViewModel.amountParsingProxy.tryParseCryptoString( - o.estimatedFee.replaceAll(",", ""), - sendViewModel.selectedCryptoCurrency) ?? - BigInt.zero; - } - return sendViewModel.currency.tryParseAmount(o.estimatedFee.replaceAll(",", "")) ?? - BigInt.zero; - }, - ), sendViewModel.currency) : sendViewModel.amountParsingProxy.getDisplayCryptoAmount(transaction.feeFormattedValue, sendViewModel.currency)} ${sendViewModel.currencySymbol}"; + final fee = (transaction == null) + ? sendViewModel.amountParsingProxy.asDisplayString(sumByMoney( + sendViewModel.outputs, + (o) => o.estimatedFee, + sendViewModel.currency, + )) + : sendViewModel.amountParsingProxy.asDisplayString(transaction.fee); - final fiatAmount = (transaction == null) - ? sumWithUnit( - sendViewModel.outputs, - (o) => double.tryParse(o.fiatAmount.replaceAll(",", "")) ?? 0, - sendViewModel.fiatCurrency.title, - decimals:2 - ) - : sendViewModel.pendingTransactionFiatAmountFormatted; + final fiatAmount = (transaction == null) + ? sumWithUnit( + sendViewModel.outputs, + (o) => double.tryParse(o.fiatAmount.replaceAll(",", "")) ?? 0, + sendViewModel.fiatCurrency.title, + decimals: 2) + : sendViewModel.pendingTransactionFiatAmountFormatted; - final fiatFee = (transaction == null) - ? sumWithUnit( - sendViewModel.outputs, - (o) => double.tryParse(o.estimatedFeeFiatAmount.replaceAll(",", "")) ?? 0, - sendViewModel.fiatCurrency.title, - decimals:2 - ) - : sendViewModel.pendingTransactionFeeFiatAmountFormatted; + final fiatFee = (transaction == null) + ? sumWithUnit( + sendViewModel.outputs, + (o) => double.tryParse(o.estimatedFeeFiatAmount.replaceAll(",", "")) ?? 0, + sendViewModel.fiatCurrency.title, + decimals: 2) + : sendViewModel.pendingTransactionFeeFiatAmountFormatted; - final showAddress = !sendViewModel.outputs.any((e) => - RegExp(AddressValidator.bolt11InvoiceMatcher).hasMatch(e.address.toLowerCase()) || - RegExp(AddressValidator.lnurlMatcher).hasMatch(e.address.toLowerCase()) || - (e.isParsedAddress && - e.parsedAddress.addresses.isNotEmpty && - RegExp(AddressValidator.lnurlMatcher) - .hasMatch(e.parsedAddress.addresses.first.toLowerCase()))); + final showAddress = !sendViewModel.outputs.any((e) => + RegExp(AddressValidator.bolt11InvoiceMatcher).hasMatch(e.address.toLowerCase()) || + RegExp(AddressValidator.lnurlMatcher).hasMatch(e.address.toLowerCase()) || + (e.isParsedAddress && + e.parsedAddress.addresses.isNotEmpty && + RegExp(AddressValidator.lnurlMatcher) + .hasMatch(e.parsedAddress.addresses.firstOrNull?.toLowerCase() ?? ""))); - final outputs = sendViewModel.outputs; + final outputs = sendViewModel.outputs; - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - spacing: 24, - children: [ - Column( - children: [ - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - spacing: 4, - children: [ - Text( - amount, - style: TextStyle( - fontSize: 36, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface), - ), - Text(currencySymbol, - style: TextStyle( - fontSize: 36, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurfaceVariant)) - ], - ), - Text( - fiatAmount, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurfaceVariant), - ), - ], - ), - if (outputs.length >= 1 && (outputs.first.extractedAddress.isNotEmpty || outputs.first.address.isNotEmpty) && showAddress) + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + spacing: 24, + children: [ Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 12, children: [ + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4, + children: [ + Flexible( + child: Text( + amount, + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface), + ), + ), + Text(currencySymbol, + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant)) + ], + ), Text( - S.of(context).send_to, + fiatAmount, style: TextStyle( - fontSize: 16, + fontSize: 20, fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.onSurfaceVariant), ), - if (outputs.length == 1) - Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(16), - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: AddressFormatter.buildSegmentedAddress( - address: outputs.first.isParsedAddress ? outputs.first.extractedAddress : outputs.first.address, - evenTextStyle: - TextStyle(color: Theme.of(context).colorScheme.onSurface)), - ), - ) - else - AnimatedDropdown( - content: Column( - children: outputs - .map( - (item) => Column(children: [ - MultiSendAddressPreview( - index: outputs.indexOf(item) + 1, - address: item.isParsedAddress - ? item.extractedAddress - : item.address, - amount: - "${item.roundedCryptoAmount(8).withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.currency.title}", - fiatAmount: - "${item.fiatAmount.withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.fiatCurrency.title}", - ), - if(item != outputs.last) - Container(width: double.infinity, height: 1, color: Theme.of(context).colorScheme.surfaceContainerHigh) - ],), - ) - .toList(), - ), - dropdownText: "${outputs.length} ${S.of(context).addresses}"), ], ), - Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(16), - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(S.of(context).fee, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface)), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - fee.withLocalSeperator(sendViewModel.languageCode), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurfaceVariant), - ), - Text(fiatFee.withLocalSeperator(sendViewModel.languageCode), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurfaceVariant)) - ], - ) - ], - ), - ), - if (sendViewModel.isElectrumWallet) ...[ - Padding( - padding: EdgeInsets.symmetric(horizontal: 12), - child: Container( - height: 1, - color: Theme.of(context).colorScheme.surfaceContainerHigh, - ), + if (outputs.length >= 1 && + (outputs.first.extractedAddress.isNotEmpty || outputs.first.address.isNotEmpty) && + showAddress) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, + children: [ + Text( + S.of(context).send_to, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant), ), + if (outputs.length == 1) + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: AddressFormatter.buildSegmentedAddress( + address: outputs.first.isParsedAddress + ? outputs.first.extractedAddress + : outputs.first.address, + evenTextStyle: + TextStyle(color: Theme.of(context).colorScheme.onSurface)), + ), + ) + else + AnimatedDropdown( + content: Column( + children: outputs + .map( + (item) => Column( + children: [ + MultiSendAddressPreview( + index: outputs.indexOf(item) + 1, + address: item.isParsedAddress + ? item.extractedAddress + : item.address, + amount: + "${item.roundedCryptoAmount(8).withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.currency.title}", + fiatAmount: + "${item.fiatAmount.withDecimals(2).withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.fiatCurrency.title}", + ), + if (item != outputs.last) + Container( + width: double.infinity, + height: 1, + color: + Theme.of(context).colorScheme.surfaceContainerHigh) + ], + ), + ) + .toList(), + ), + dropdownText: "${outputs.length} ${S.of(context).addresses}"), + ], + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ Padding( padding: const EdgeInsets.all(12.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(S.of(context).network, + Text(S.of(context).fee, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, color: Theme.of(context).colorScheme.onSurface)), Column( + crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln - ? "Lightning" - : bitcoin!.getNetworkName(sendViewModel.wallet), + "${fee.withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.currencySymbol}", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Text(fiatFee.withLocalSeperator(sendViewModel.languageCode), style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, @@ -406,18 +374,52 @@ class SendTransactionDetails extends StatelessWidget { ) ], ), - ) + ), + if (sendViewModel.isElectrumWallet) ...[ + Padding( + padding: EdgeInsets.symmetric(horizontal: 12), + child: Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + ), + Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(S.of(context).network, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface)), + Column( + children: [ + Text( + sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln + ? "Lightning" + : bitcoin!.getNetworkName(sendViewModel.wallet), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant)) + ], + ) + ], + ), + ) + ], ], - ], + ), ), - ), - SendConfirmBottomWidget(sendViewModel: sendViewModel), - if(Platform.isAndroid) // spacing between bottom widget and system navbar - SizedBox(), - ], + SendConfirmBottomWidget(sendViewModel: sendViewModel), + if (Platform.isAndroid) // spacing between bottom widget and system navbar + SizedBox(), + ], + ), ), - ), - ); + ); + }); } String formatAmount(String amount) { diff --git a/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart b/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart index ede917cee..6a7f49900 100644 --- a/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart +++ b/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart @@ -14,6 +14,8 @@ import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/currencies_with_memo.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; @@ -159,14 +161,16 @@ class SwapTransactionDetails extends StatelessWidget { builder: (_) => NewListSections(showHeader: true, sections: { S.of(context).send: [ ListItemRegularRow( - showArrow: false, - keyValue: "send value", - label: exchangeViewModel.depositCurrency.fullName ?? "", - iconPath: exchangeViewModel.depositCurrency.iconPath ?? "", - badgeIconPath: _resolveChainBadgePath(exchangeViewModel.depositCurrency), - trailingText: exchangeTradeViewModel.trade.amountFormatted() + - " " + - (exchangeViewModel.depositCurrency.title)), + showArrow: false, + keyValue: "send value", + label: exchangeViewModel.depositCurrency.fullName ?? "", + iconPath: exchangeViewModel.depositCurrency.iconPath ?? "", + badgeIconPath: _resolveChainBadgePath(exchangeViewModel.depositCurrency), + trailingText: exchangeViewModel.amountParsingProxy + .asDisplayStringWithSymbol(exchangeViewModel.depositCurrency + .tryParseAmount(exchangeTradeViewModel.trade.amount) ?? + Money.zero(exchangeViewModel.depositCurrency)), + ), if (exchangeTradeViewModel.sendViewModel.pendingTransaction != null) ListItemRegularRow( showArrow: false, @@ -184,13 +188,15 @@ class SwapTransactionDetails extends StatelessWidget { ], S.of(context).receive: [ ListItemRegularRow( - showArrow: false, - keyValue: "receive value", - label: exchangeViewModel.receiveCurrency.fullName ?? "", - iconPath: exchangeViewModel.receiveCurrency.iconPath ?? "", - badgeIconPath: _resolveChainBadgePath(exchangeViewModel.receiveCurrency), - trailingText: - (receiveAmount) + " " + (exchangeViewModel.receiveCurrency.title)), + showArrow: false, + keyValue: "receive value", + label: exchangeViewModel.receiveCurrency.fullName ?? "", + iconPath: exchangeViewModel.receiveCurrency.iconPath ?? "", + badgeIconPath: _resolveChainBadgePath(exchangeViewModel.receiveCurrency), + trailingText: (receiveAmount.withMaxDecimals(8)) + + " " + + (exchangeViewModel.receiveCurrency.title), + ), ListItemRegularRow( keyValue: "receiver", label: S.of(context).to, diff --git a/lib/new-ui/widgets/swap_page/swap_limit_popup.dart b/lib/new-ui/widgets/swap_page/swap_limit_popup.dart index 3dea8a5b9..5e079f425 100644 --- a/lib/new-ui/widgets/swap_page/swap_limit_popup.dart +++ b/lib/new-ui/widgets/swap_page/swap_limit_popup.dart @@ -20,7 +20,9 @@ class SwapLimitPopup extends StatelessWidget { child: Container( width: double.infinity, child: Observer(builder: (_) { - final double? amount = double.tryParse(exchangeViewModel.depositAmountCanonical); + final amount = exchangeViewModel.hasDepositAmount + ? double.tryParse(exchangeViewModel.depositAmountCanonical) + : null; final max = exchangeViewModel.limits.max ?? double.infinity; final min = exchangeViewModel.limits.min ?? 0; final tooLarge = amount != null && max != 0 && amount > max; diff --git a/lib/new-ui/widgets/swap_page/swap_modal_header.dart b/lib/new-ui/widgets/swap_page/swap_modal_header.dart index c88387924..86f5d4969 100644 --- a/lib/new-ui/widgets/swap_page/swap_modal_header.dart +++ b/lib/new-ui/widgets/swap_page/swap_modal_header.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:flutter/material.dart'; class SwapModalHeader extends StatelessWidget { @@ -17,8 +18,12 @@ class SwapModalHeader extends StatelessWidget { width: 36, child: Stack( children: [ - Image.asset(fromIconPath, width: 24, height: 24), - Positioned(top: 12, left: 12, child: Image.asset(toIconPath, width: 24, height: 24)), + CakeImageWidget(imageUrl: fromIconPath, width: 24, height: 24), + Positioned( + top: 12, + left: 12, + child: CakeImageWidget(imageUrl: toIconPath, width: 24, height: 24), + ), ], ), ), diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index 7ca41a2c6..569a603e2 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -53,6 +53,8 @@ class CWSolana extends Solana { @override String getPublicKey(WalletBase wallet) => (wallet as SolanaWallet).solanaPublicKey.toAddress().address; + + @override Object createSolanaTransactionCredentials( List outputs, { required CryptoCurrency currency, @@ -60,14 +62,14 @@ class CWSolana extends Solana { SolanaTransactionCredentials( outputs .map((out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount)) + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmountMoney, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + )) .toList(), currency: currency, ); @@ -116,23 +118,15 @@ class CWSolana extends Solana { @override CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { - transaction as SolanaTransactionInfo; - if (transaction.tokenSymbol == CryptoCurrency.sol.title) { + if (transaction.amount.currency.symbol == CryptoCurrency.sol.symbol) { return CryptoCurrency.sol; } - wallet as SolanaWallet; - - return wallet.splTokenCurrencies.firstWhere( - (element) => transaction.tokenSymbol == element.symbol, + return (wallet as SolanaWallet).splTokenCurrencies.firstWhere( + (element) => transaction.amount.currency.symbol == element.symbol, ); } - @override - double getTransactionAmountRaw(TransactionInfo transactionInfo) { - return (transactionInfo as SolanaTransactionInfo).solAmount.toDouble(); - } - @override String getTokenAddress(CryptoCurrency asset) { // If it's already an SPLToken, use its mint address @@ -168,9 +162,7 @@ class CWSolana extends Solana { } @override - double? getEstimateFees(WalletBase wallet) { - return (wallet as SolanaWallet).estimatedFee; - } + Money? getEstimateFees(WalletBase wallet) => (wallet as SolanaWallet).estimatedFee; @override List getDefaultSPLTokens() => DefaultSPLTokens().initialSPLTokens; @@ -197,8 +189,8 @@ class CWSolana extends Solana { String base64Transaction, String requestId, String destinationAddress, - double amount, - double fee, + Money amount, + Money fee, ) async { final solanaWallet = wallet as SolanaWallet; final privateKey = solanaWallet.solanaPrivateKey; @@ -418,24 +410,18 @@ class CWSolana extends Solana { required String from, String? tokenSymbol, required TransactionDirection direction, - required double solAmount, + required Money amount, required bool isPending, - required double txFee, + required Money fee, }) => SolanaTransactionInfo( - id: id, - blockTime: blockTime, - to: to, - from: from, - tokenSymbol: tokenSymbol ?? "SOL", - direction: direction, - solAmount: solAmount, - isPending: isPending, - txFee: txFee); - - @override - double getPendingTransactionAmount(PendingTransaction tx) => (tx as PendingSolanaTransaction).amount; - - @override - double getPendingTransactionFee(PendingTransaction tx) => (tx as PendingSolanaTransaction).fee; + id: id, + date: blockTime, + to: to, + from: from, + direction: direction, + amount: amount, + isPending: isPending, + fee: fee, + ); } diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index d60eb08ca..40fa6c5cc 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -733,7 +733,7 @@ class ExchangePage extends BasePage { exchangeViewModel.depositAddress = await fetchParsedAddress(context, domain, exchangeViewModel.depositCurrency); }, - useSatoshis: exchangeViewModel.useSatoshiDeposit, + useSatoshis: exchangeViewModel.useDepositBaseUnit, ), ); @@ -782,7 +782,7 @@ class ExchangePage extends BasePage { exchangeViewModel.receiveAddress = await fetchParsedAddress(context, domain, exchangeViewModel.receiveCurrency); }, - useSatoshis: exchangeViewModel.useSatoshisReceive, + useSatoshis: exchangeViewModel.useReceiveBaseUnit, ), ); diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 6ebd2526f..1b1f670a6 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; @@ -11,7 +10,6 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_shee import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/utils/request_review_handler.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; -import 'package:cake_wallet/view_model/send/output.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter/material.dart'; @@ -330,10 +328,8 @@ class ExchangeTradeState extends State { fee: isEVMCompatibleChain(sendVM.walletType) ? S.of(bottomSheetContext).send_estimated_fee : S.of(bottomSheetContext).send_fee, - feeValue: "${sendVM.amountParsingProxy.getDisplayCryptoAmount( - sendVM.pendingTransaction!.feeFormattedValue, - sendVM.selectedCryptoCurrency)} ${sendVM.amountParsingProxy - .getCryptoSymbol(sendVM.wallet.currency)}", + feeValue: sendVM.amountParsingProxy.asDisplayStringWithSymbol( + sendVM.pendingTransaction!.fee), feeFiatAmount: sendVM.pendingTransactionFeeFiatAmountFormatted, outputs: sendVM.outputs, onSlideActionComplete: () async { diff --git a/lib/src/screens/integrations/deuro/savings_page.dart b/lib/src/screens/integrations/deuro/savings_page.dart index a08437822..c90d14416 100644 --- a/lib/src/screens/integrations/deuro/savings_page.dart +++ b/lib/src/screens/integrations/deuro/savings_page.dart @@ -58,7 +58,8 @@ class DEuroSavingsPage extends BasePage { Observer( builder: (_) => SavingsCard( interestRate: "${_dEuroViewModel.interestRateFormated}%", - savingsBalance: _dEuroViewModel.savingsBalanceFormated, + savingsBalance: + _dEuroViewModel.savingsBalance.toStringWithPrecision(fractionalDigits: 6), fiatSavingsBalance: _dEuroViewModel.fiatSavingsBalanceFormated, currency: CryptoCurrency.deuro, fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat, @@ -68,6 +69,10 @@ class DEuroSavingsPage extends BasePage { onTooltipPressed: () => _onSavingsTooltipPressed(context), isEnabled: _dEuroViewModel.isEnabled, isLoading: _dEuroViewModel.isLoading, + onWithdrawV1Pressed: () => _onWithdrawV1(context), + savingsBalanceV1: _dEuroViewModel.savingsBalanceV1 + ?.toStringWithPrecision(fractionalDigits: 6), + fiatSavingsBalanceV1: _dEuroViewModel.fiatSavingsBalanceV1Formated, ), ), Observer( @@ -126,6 +131,16 @@ class DEuroSavingsPage extends BasePage { _editSheetIsOpen = false; } + Future _onWithdrawV1(BuildContext context) async { + if (_editSheetIsOpen) return; + _editSheetIsOpen = true; + try { + await _requireHardwareWallet(context); + await _dEuroViewModel.prepareSavingsV1Withdraw(); + } catch (_) {} + _editSheetIsOpen = false; + } + Future _onSavingsRemove(BuildContext context) async { if (_editSheetIsOpen) return; _editSheetIsOpen = true; @@ -215,7 +230,7 @@ class DEuroSavingsPage extends BasePage { amount: S.of(bottomSheetContext).send_amount, amountValue: _dEuroViewModel.actionType == DEuroActionType.reinvest ? _dEuroViewModel.accruedInterestFormated - : tx.amountFormatted, + : tx.amount.toStringWithSymbol(), fiatAmountValue: _dEuroViewModel.actionType == DEuroActionType.reinvest ? _dEuroViewModel.fiatAccruedInterestFormated : _dEuroViewModel.pendingTransactionFiatAmountFormatted, @@ -251,7 +266,7 @@ class DEuroSavingsPage extends BasePage { amountValue: tx.amountFormatted, fiatAmountValue: "", fee: S.of(bottomSheetContext).send_estimated_fee, - feeValue: tx.feeFormatted, + feeValue: tx.fee.toStringWithSymbol(), feeFiatAmount: "", outputs: [], onSlideActionComplete: () { @@ -411,8 +426,8 @@ class DEuroSavingsPage extends BasePage { ? S.of(context).deuro_savings_available_to_add : S.of(context).deuro_savings_available_to_remove, balance: isAdding - ? _dEuroViewModel.accountBalanceFormated - : _dEuroViewModel.savingsBalanceFormated, + ? _dEuroViewModel.accountBalance + : _dEuroViewModel.savingsBalance, footerType: FooterType.none, maxHeight: MediaQuery.of(context).size.height * 0.8, ), diff --git a/lib/src/screens/integrations/deuro/widgets/savings_card_widget.dart b/lib/src/screens/integrations/deuro/widgets/savings_card_widget.dart index 3cb1ba67e..f82d9b923 100644 --- a/lib/src/screens/integrations/deuro/widgets/savings_card_widget.dart +++ b/lib/src/screens/integrations/deuro/widgets/savings_card_widget.dart @@ -23,6 +23,9 @@ class SavingsCard extends StatelessWidget { this.isLoading = false, this.fiatSavingsBalance, this.fiatCurrency, + this.savingsBalanceV1, + this.fiatSavingsBalanceV1, + this.onWithdrawV1Pressed, }); final bool isEnabled; @@ -30,8 +33,11 @@ class SavingsCard extends StatelessWidget { final String interestRate; final String savingsBalance; final String? fiatSavingsBalance; + final String? savingsBalanceV1; + final String? fiatSavingsBalanceV1; final FiatCurrency? fiatCurrency; final CryptoCurrency currency; + final VoidCallback? onWithdrawV1Pressed; final VoidCallback onAddSavingsPressed; final VoidCallback onRemoveSavingsPressed; final VoidCallback onApproveSavingsPressed; @@ -98,46 +104,72 @@ class SavingsCard extends StatelessWidget { onTooltipPressed: onTooltipPressed, ), ), + if (savingsBalanceV1 != null) + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: getAssetBalanceRow( + context, + title: "${S.of(context).deuro_savings_balance} V1", + amount: savingsBalanceV1!, + fiatAmount: fiatSavingsBalanceV1, + currency: currency, + fiatCurrency: fiatCurrency, + hideSymbol: true, + ), + ), isLoading ? CupertinoActivityIndicator(color: Theme.of(context).colorScheme.onSurface) : Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: isEnabled + children: savingsBalanceV1 != null ? [ Expanded( child: getButton( context, - label: S.of(context).deuro_savings_add, - imagePath: 'assets/images/received.png', - onPressed: onAddSavingsPressed, + label: "${S.of(context).deuro_savings_remove} V1", + onPressed: onWithdrawV1Pressed ?? () {}, backgroundColor: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.onPrimary, - ), - ), - SizedBox(width: 12), - Expanded( - child: getButton( - context, - label: S.of(context).deuro_savings_remove, - imagePath: 'assets/images/upload.png', - onPressed: onRemoveSavingsPressed, - backgroundColor: Theme.of(context).colorScheme.surface, - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), - ), - ] - : [ - Expanded( - child: getButton( - context, - label: S.of(context).deuro_savings_approve_app, - onPressed: onApproveSavingsPressed, - backgroundColor: Theme.of(context).colorScheme.primary, - color: Theme.of(context).colorScheme.onPrimary, - icon: Icons.check, + icon: Icons.arrow_downward, ), ) - ], + ] + : isEnabled + ? [ + Expanded( + child: getButton( + context, + label: S.of(context).deuro_savings_add, + imagePath: 'assets/images/received.png', + onPressed: onAddSavingsPressed, + backgroundColor: Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + SizedBox(width: 12), + Expanded( + child: getButton( + context, + label: S.of(context).deuro_savings_remove, + imagePath: 'assets/images/upload.png', + onPressed: onRemoveSavingsPressed, + backgroundColor: Theme.of(context).colorScheme.surface, + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + ), + ] + : [ + Expanded( + child: getButton( + context, + label: S.of(context).deuro_savings_approve_app, + onPressed: onApproveSavingsPressed, + backgroundColor: Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.onPrimary, + icon: Icons.check, + ), + ) + ], ), ], ), @@ -264,33 +296,33 @@ class SavingsCard extends StatelessWidget { ), ], ), - SizedBox( - child: Center( - child: Column( - children: [ - CakeImageWidget( - imageUrl: currency.iconPath, - height: 40, - width: 40, - errorWidget: Container( - height: 30.0, - width: 30.0, - child: Center( - child: Text( - currency.title.substring(0, min(currency.title.length, 2)), - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontSize: 11, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + if (!hideSymbol) + SizedBox( + child: Center( + child: Column( + children: [ + CakeImageWidget( + imageUrl: currency.iconPath, + height: 40, + width: 40, + errorWidget: Container( + height: 30.0, + width: 30.0, + child: Center( + child: Text( + currency.title.substring(0, min(currency.title.length, 2)), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: 11, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.surfaceContainer, ), ), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.surfaceContainer, - ), ), - ), - if (!hideSymbol) ...[ const SizedBox(height: 3), Text( currency.title, @@ -301,11 +333,10 @@ class SavingsCard extends StatelessWidget { height: 1, ), ), - ] - ], + ], + ), ), ), - ), ], ); } diff --git a/lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart b/lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart index 558a39182..9441a9566 100644 --- a/lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart +++ b/lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart @@ -3,11 +3,11 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/integrations/deuro/widgets/numpad.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cw_core/parse_fixed.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:flutter/material.dart'; class SavingsEditSheet extends BaseBottomSheet { - final String? balance; + final Money? balance; final String? balanceTitle; const SavingsEditSheet({ @@ -32,7 +32,7 @@ class SavingsEditSheet extends BaseBottomSheet { } class _SavingsEditBody extends StatefulWidget { - final String? balance; + final Money? balance; final String? balanceTitle; const _SavingsEditBody({this.balance, this.balanceTitle}); @@ -59,7 +59,7 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> { final FocusNode _numpadFocusNode = FocusNode(); void _onPressedAll() => setState(() { - amount = widget.balance!; + amount = widget.balance!.toString(); isValid = _validate(); }); @@ -79,9 +79,12 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> { }); bool _validate() { - final amountBigInt = parseFixed(amount, 18); - final balanceBigInt = parseFixed(widget.balance!, 18); - return balanceBigInt >= amountBigInt && amountBigInt > BigInt.zero; + try { + final amountBigInt = Money.parse(amount, widget.balance!.currency); + return widget.balance! >= amountBigInt && amountBigInt.sign > 0; + } catch (_) { + return false; + } } @override @@ -108,7 +111,7 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> { Divider(), AssetBalanceRow( title: widget.balanceTitle!, - amount: widget.balance!, + amount: widget.balance!.toStringWithSymbol(fractionalDigits: 6), onAllPressed: _onPressedAll, ), ], diff --git a/lib/src/screens/receive/widgets/currency_input_field.dart b/lib/src/screens/receive/widgets/currency_input_field.dart index a5b0800e2..fdffcae08 100644 --- a/lib/src/screens/receive/widgets/currency_input_field.dart +++ b/lib/src/screens/receive/widgets/currency_input_field.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cw_core/amount/amount_sanitizer.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -198,8 +199,7 @@ class CurrencyAmountTextField extends StatelessWidget { ), validator: isAmountEditable ? currencyValueValidator : null, onChanged: (value) { - var sanitized = - value.replaceAll(',', '.').withMaxDecimals(selectedCurrencyDecimals); + var sanitized = value.sanitized().withMaxDecimals(selectedCurrencyDecimals); if (selectedCurrencyDecimals == 0) { sanitized = sanitized.replaceAll('.', ''); diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index db0d5e883..d886963b2 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -35,6 +35,7 @@ import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; @@ -445,9 +446,9 @@ class SendPage extends BasePage { } if (sendViewModel.wallet.type == WalletType.monero) { - int amount = 0; + var amount = Money.zero(sendViewModel.wallet.currency); for (var item in sendViewModel.outputs) { - amount += item.formattedCryptoAmount; + amount += item.cryptoAmountMoney; } if (monero!.needExportOutputs(sendViewModel.wallet, amount)) { await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, diff --git a/lib/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart b/lib/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart index da91926b2..e98ead96a 100644 --- a/lib/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart +++ b/lib/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart @@ -3,7 +3,6 @@ import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/rounded_icon_button.dart'; import 'package:cake_wallet/utils/image_utill.dart'; import 'package:cake_wallet/view_model/send/output.dart'; -import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -11,7 +10,6 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget { const CakePayTransactionSentBottomSheet({ super.key, required this.titleText, - required this.currency, required this.amount, required this.amountValue, required this.fiatAmountValue, @@ -28,7 +26,6 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget { final String titleText; final Widget? titleIconWidget; - final CryptoCurrency currency; final String amount; final String amountValue; final String fiatAmountValue; @@ -41,32 +38,15 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget { final String paymentId; final String paymentIdValue; - TextStyle _titleStyle(BuildContext ctx) => - Theme - .of(ctx) - .textTheme - .bodyLarge!; + TextStyle _titleStyle(BuildContext ctx) => Theme.of(ctx).textTheme.bodyLarge!; - TextStyle _valueStyle(BuildContext ctx) => - Theme - .of(ctx) - .textTheme - .titleMedium! - .copyWith( + TextStyle _valueStyle(BuildContext ctx) => Theme.of(ctx).textTheme.titleMedium!.copyWith( fontSize: 18, fontWeight: FontWeight.w600, ); - TextStyle _subStyle(BuildContext ctx) => - Theme - .of(ctx) - .textTheme - .labelSmall! - .copyWith( - color: Theme - .of(ctx) - .colorScheme - .onSurfaceVariant, + TextStyle _subStyle(BuildContext ctx) => Theme.of(ctx).textTheme.labelSmall!.copyWith( + color: Theme.of(ctx).colorScheme.onSurfaceVariant, ); Widget _buildHeader(BuildContext ctx) => @@ -78,10 +58,7 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget { height: 5, decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), - color: Theme - .of(ctx) - .colorScheme - .onSurface, + color: Theme.of(ctx).colorScheme.onSurface, ), ), const SizedBox(height: 20), @@ -92,15 +69,11 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget { const SizedBox(width: 6), Text( titleText, - style: Theme - .of(ctx) - .textTheme - .titleLarge! - .copyWith( - fontSize: 20, - fontWeight: FontWeight.w600, - decoration: TextDecoration.none, - ), + style: Theme.of(ctx).textTheme.titleLarge!.copyWith( + fontSize: 20, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), ), ], ), @@ -116,7 +89,7 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget { _StandardTile( itemTitle: amount, titleStyle: _titleStyle(context), - itemValue: '$amountValue ${currency.title}', + itemValue: amountValue, itemValueStyle: _valueStyle(context), itemSubTitle: fiatAmountValue, itemSubTitleStyle: _subStyle(context), @@ -143,9 +116,7 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget { const SizedBox(height: 8), _StandardTile( itemTitle: paymentId + ': \n' + paymentIdValue, - titleStyle: _titleStyle(context).copyWith( - fontSize: 14 - ), + titleStyle: _titleStyle(context).copyWith(fontSize: 14), itemValue: '', itemValueStyle: _valueStyle(context), copyButton: true, @@ -161,28 +132,15 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget { Image.asset('assets/images/envelope.png'), const SizedBox(height: 18), Text( - S - .of(context) - .cake_pay_card_email_delivered_message, - style: Theme - .of(context) - .textTheme - .bodyLarge, + S.of(context).cake_pay_card_email_delivered_message, + style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, ), const SizedBox(height: 24), PrimaryButton( - text: S - .of(context) - .close, - color: Theme - .of(context) - .colorScheme - .primary, - textColor: Theme - .of(context) - .colorScheme - .onPrimaryContainer, + text: S.of(context).close, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimaryContainer, onPressed: onClose, ), ], @@ -194,10 +152,7 @@ class CakePayTransactionSentBottomSheet extends StatelessWidget { @override Widget build(BuildContext context) { - final maxHeight = MediaQuery - .of(context) - .size - .height * 0.9; + final maxHeight = MediaQuery.of(context).size.height * 0.9; return ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(30)), diff --git a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart index e78b4985c..140cfa970 100644 --- a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart +++ b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/utils/address_formatter.dart'; import 'package:cake_wallet/utils/image_utill.dart'; import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart'; import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/amount/amount_sanitizer.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/wallet_type.dart'; @@ -134,7 +135,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { ), StandardTile( itemTitle: amount, - itemValue: '$amountValue ${amountParsingProxy?.getCryptoSymbol(currency) ?? currency.title}', + itemValue: amountValue, itemTitleTextStyle: itemTitleTextStyle, itemSubTitle: fiatAmountValue, itemSubTitleTextStyle: itemSubTitleTextStyle, @@ -167,7 +168,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { final batchContactTitle = '${index + 1}/${outputs.length} - ${contactName.isEmpty ? 'Address' : contactName}'; final _address = item.isParsedAddress ? item.extractedAddress : item.address; - final _amount = '${item.cryptoAmount.replaceAll(',', '.')} ${amountParsingProxy?.getCryptoSymbol(currency) ?? currency.title}'; + final _amount = '${item.cryptoAmount.sanitized()} ${amountParsingProxy?.getCryptoSymbol(currency) ?? currency.title}'; return isBatchSending || (contactName.isNotEmpty && !isCakePayName) ? ExpansionAddressTile( contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact, diff --git a/lib/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart b/lib/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart index cd69285bc..3b2a9e630 100644 --- a/lib/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart +++ b/lib/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/swap_details_bottom_sheet.d import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/utils/address_formatter.dart'; import 'package:cake_wallet/utils/debounce.dart'; +import 'package:cw_core/amount/amount_sanitizer.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/crypto_amount_format.dart'; @@ -197,7 +198,7 @@ class SwapConfirmationContentState extends State { ], onChanged: (value) { final sanitized = value - .replaceAll(',', '.') + .sanitized() .withMaxDecimals(widget.exchangeViewModel.receiveCurrency.decimals); if (sanitized != _amountController.text) { // Update text while preserving a sane cursor position to avoid auto-selection diff --git a/lib/tron/cw_tron.dart b/lib/tron/cw_tron.dart index 42f22db31..daab68d8f 100644 --- a/lib/tron/cw_tron.dart +++ b/lib/tron/cw_tron.dart @@ -58,13 +58,12 @@ class CWTron extends Tron { .map( (out) => OutputInfo( fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, + cryptoAmount: out.cryptoAmountMoney, address: out.address, note: out.note, sendAll: out.sendAll, extractedAddress: out.extractedAddress, isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount, ), ) .toList(), @@ -97,22 +96,16 @@ class CWTron extends Tron { Future getTronToken(WalletBase wallet, String contractAddress) async => (wallet as TronWallet).getTronToken(contractAddress); - @override - double getTransactionAmountRaw(TransactionInfo transactionInfo) { - final amount = (transactionInfo as TronTransactionInfo).rawTronAmount(); - return double.parse(amount); - } - @override CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { transaction as TronTransactionInfo; - if (transaction.tokenSymbol == CryptoCurrency.trx.title) { + if (transaction.amount.currency.symbol == CryptoCurrency.trx.title) { return CryptoCurrency.trx; } wallet as TronWallet; - return wallet.tronTokenCurrencies.firstWhere( - (element) => transaction.tokenSymbol.toLowerCase() == element.symbol.toLowerCase()); + return wallet.tronTokenCurrencies.firstWhere((element) => + transaction.amount.currency.symbol.toLowerCase() == element.symbol.toLowerCase()); } @override @@ -123,11 +116,11 @@ class CWTron extends Tron { (wallet as TronWallet).getTronBase58AddressFromHex(hexAddress); @override - String? getTronNativeEstimatedFee(WalletBase wallet) => + Money? getTronNativeEstimatedFee(WalletBase wallet) => (wallet as TronWallet).nativeTxEstimatedFee; @override - String? getTronTRC20EstimatedFee(WalletBase wallet) => (wallet as TronWallet).trc20EstimatedFee; + Money? getTronTRC20EstimatedFee(WalletBase wallet) => (wallet as TronWallet).trc20EstimatedFee; @override void updateTronGridUsageState(WalletBase wallet, bool isEnabled) { @@ -157,9 +150,8 @@ class CWTron extends Tron { @override TransactionInfo getTransactionInfo({ required String id, - required BigInt tronAmount, - int? txFee, - String? tokenSymbol, + required Money amount, + Money? fee, required TransactionDirection direction, required DateTime blockTime, String? to, @@ -167,18 +159,11 @@ class CWTron extends Tron { required bool isPending, }) => TronTransactionInfo(id: id, - tronAmount: tronAmount, - tokenSymbol: tokenSymbol ?? "TRX", - txFee: txFee, + amount: amount, + fee: fee, direction: direction, blockTime: blockTime, to: to, from: from, isPending: isPending); - - @override - String getPendingTransactionAmount(PendingTransaction tx) => (tx as PendingTronTransaction).amount; - - @override - String getPendingTransactionFee(PendingTransaction tx) => (tx as PendingTronTransaction).amount; } diff --git a/lib/utils/decimal_input_formatter.dart b/lib/utils/decimal_input_formatter.dart new file mode 100644 index 000000000..18a2b5586 --- /dev/null +++ b/lib/utils/decimal_input_formatter.dart @@ -0,0 +1,19 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter/services.dart'; + +class DecimalInputFormatter extends TextInputFormatter { + const DecimalInputFormatter({this.maxDecimals = 7}) : assert(maxDecimals >= 0); + + final int maxDecimals; + + @override + TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + final text = newValue.text; + if (text.isEmpty) return newValue; + + if (S.current.all.startsWith(text)) return TextEditingValue(text: ""); + + final regex = maxDecimals == 0 ? RegExp(r'^\d*$') : RegExp('^\\d*([.,]\\d{0,$maxDecimals})?\$'); + return regex.hasMatch(text) ? newValue.copyWith(text: text.replaceAll(',', '.')) : oldValue; + } +} diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 42d289dd3..e46650517 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -306,6 +306,7 @@ class ExceptionHandler { "the timeout of the request was reached", "support for coin removed, your seedphrase:" + "Exception: Invalid image data" ]; static Future _addDeviceInfo(File file) async { diff --git a/lib/utils/payment_request.dart b/lib/utils/payment_request.dart index fdc48b1eb..06eda7b05 100644 --- a/lib/utils/payment_request.dart +++ b/lib/utils/payment_request.dart @@ -1,3 +1,5 @@ +import 'package:cw_core/amount/amount_sanitizer.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/lnurl.dart'; import 'package:cw_core/payment_uris.dart'; @@ -16,9 +18,8 @@ class PaymentRequest { } factory PaymentRequest.fromBolt11(String invoice) { - final amountRaw = getBolt11Amount(invoice) ?? 0; - final amount = CryptoCurrency.btcln.formatAmount(BigInt.from(amountRaw)); - return PaymentRequest(invoice, amount, '', 'lightning', null); + final amount = getBolt11Amount(invoice) ?? Money.zero(CryptoCurrency.btcln); + return PaymentRequest(invoice, amount.toString(), '', 'lightning', null); } factory PaymentRequest.fromUri(Uri? uri) { @@ -39,10 +40,10 @@ class PaymentRequest { address = uri.queryParameters['address'] ?? uri.path; try { - final lnAmount = - CryptoCurrency.btcln.formatAmount(BigInt.from(getBolt11Amount(uri.path) ?? 0)); + final lnAmount = getBolt11Amount(uri.path) ?? Money.zero(CryptoCurrency.btcln); + if (lnAmount != 0) { - amount = lnAmount; + amount = lnAmount.toString(); } } catch (_) {} if (amount.isEmpty) { @@ -116,8 +117,7 @@ class PaymentRequest { static bool _isAlreadyUsableAmount(String amount) { if (amount.isEmpty) return false; - // Try to parse as double - if successful, it's already in usable format - final parsed = double.tryParse(amount.replaceAll(',', '.')); + final parsed = double.tryParse(amount.sanitized()); if (parsed == null) return false; // Check if the amount contains a decimal point and is a reasonable number, diff --git a/lib/view_model/bridge/bridge_view_model.dart b/lib/view_model/bridge/bridge_view_model.dart index b178d8e1c..0f31a135d 100644 --- a/lib/view_model/bridge/bridge_view_model.dart +++ b/lib/view_model/bridge/bridge_view_model.dart @@ -17,6 +17,8 @@ import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/bridge_transfers_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/amount/amount_sanitizer.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; @@ -130,18 +132,18 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St } @computed - String get tokenBalanceFormatted { - if (selectedToken == null) return "0.00"; - - return amountParsingProxy.getDisplayCryptoStringFromBigInt( - selectedTokenBalance, selectedToken!, - ); -} + String get tokenBalanceFormatted { + if (selectedToken == null) return "0.00"; + + return amountParsingProxy.asDisplayString( + Money(selectedTokenBalance, selectedToken!), + ); + } @computed String get amountDisplayFormatted { if (selectedToken == null) return "0.00"; - + return amountParsingProxy.getDisplayCryptoAmount( amount.replaceAll(',', '.'), selectedToken!, @@ -174,10 +176,8 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St String get quoteNativeFee { if (quote == null) return '—'; - return amountParsingProxy.getDisplayCryptoStringFromBigInt( - quote!.nativeFee, - wallet.currency, - ); + return amountParsingProxy.asDisplayString( + Money(quote!.nativeFee, wallet.currency)); } @computed @@ -218,10 +218,10 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St if (amountError != null) return false; final validAmount = amountParsingProxy.tryParseCryptoString( - amount.replaceAll(',', '.'), + amount.sanitized(), selectedToken!, ); - return validAmount != null && validAmount > BigInt.zero; + return validAmount != null && validAmount > Money.zero(selectedToken!); } @computed @@ -231,7 +231,7 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St try { final bal = wallet.balance[selectedToken!]; - return bal?.fullAvailableBalance ?? BigInt.zero; + return bal?.available.amount ?? BigInt.zero; } catch (e) { return BigInt.zero; } @@ -247,7 +247,7 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St ); if (amountBigInt == null || amountBigInt == BigInt.zero) return null; - if (amountBigInt > selectedTokenBalance) { + if (amountBigInt.amount > selectedTokenBalance) { return 'Insufficient balance for ${selectedToken!.title} token.'; } @@ -281,9 +281,9 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St return; } setAmount( - amountParsingProxy.getDisplayCryptoStringFromBigInt( + amountParsingProxy.asDisplayString(Money( selectedTokenBalance, - token, + token) ), ); } @@ -382,23 +382,23 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St } ({String? error, BigInt? parsedAmount}) _parseAndValidateAmount(Erc20Token token) { - final amountBigInt = amountParsingProxy.tryParseCryptoString( + final parsedAmount = amountParsingProxy.tryParseCryptoString( amount.replaceAll(',', '.'), token, ); - if (amountBigInt == null || amountBigInt == BigInt.zero) { + if (parsedAmount == null || parsedAmount == Money(BigInt.zero, token)) { return (error: 'Invalid amount', parsedAmount: null); } - if (amountBigInt > selectedTokenBalance) { + if (parsedAmount.amount > selectedTokenBalance) { return ( error: 'Insufficient balance for ${token.title} token.', parsedAmount: null, ); } - return (error: null, parsedAmount: amountBigInt); + return (error: null, parsedAmount: parsedAmount.amount); } @action diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 41d5b21da..4f098c02f 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/core/utilities.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; @@ -10,8 +11,10 @@ import 'package:cake_wallet/evm/evm.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; @@ -28,47 +31,74 @@ part 'balance_view_model.g.dart'; class BalanceRecord { const BalanceRecord({ + required this.raw, required this.availableBalance, required this.additionalBalance, required this.secondAvailableBalance, required this.secondAdditionalBalance, required this.frozenBalance, - required this.fiatAvailableBalance, - required this.fiatAdditionalBalance, - required this.fiatFrozenBalance, - required this.fiatSecondAvailableBalance, - required this.fiatSecondAdditionalBalance, + required this.fiatAvailableBalanceRaw, + required this.fiatAdditionalBalanceRaw, + required this.fiatFrozenBalanceRaw, + required this.fiatSecondAvailableBalanceRaw, + required this.fiatSecondAdditionalBalanceRaw, required this.asset, required this.secondAsset, + required this.fiatCurrency, required this.formattedAssetTitle, + this.languageCode, }); - final String fiatAdditionalBalance; - final String fiatAvailableBalance; - final String fiatFrozenBalance; + final Balance raw; + + final String fiatAvailableBalanceRaw; + final String fiatAdditionalBalanceRaw; + final String fiatFrozenBalanceRaw; + final String fiatSecondAvailableBalanceRaw; + final String fiatSecondAdditionalBalanceRaw; + final String additionalBalance; final String availableBalance; final String frozenBalance; final String secondAvailableBalance; final String secondAdditionalBalance; - final String fiatSecondAdditionalBalance; - final String fiatSecondAvailableBalance; final CryptoCurrency asset; final CryptoCurrency secondAsset; + final FiatCurrency? fiatCurrency; final String formattedAssetTitle; + final String? languageCode; String get combinedAvailableBalance => - ((double.tryParse(availableBalance) ?? 0) + (double.tryParse(secondAvailableBalance) ?? 0)) - .toString().withMaxDecimals(8); + (raw.available + (raw.secondAvailable ?? Money.zero(raw.available.currency))) + .toString() + .withMaxDecimals(8); - String get combinedFiatAvailableBalance => - ((double.tryParse(fiatAvailableBalanceRaw) ?? 0) + - (double.tryParse(fiatSecondAvailableBalanceRaw) ?? 0)) - .toStringAsFixed(2); + String get combinedFiatAvailableBalance => fiatCurrency != null + ? "$fiatCurrency " + + _withLocalSeperator(((double.tryParse(fiatAvailableBalanceRaw) ?? 0) + + (double.tryParse(fiatSecondAvailableBalanceRaw) ?? 0)) + .toStringAsFixed(2)) + : ""; - String get fiatAvailableBalanceRaw => fiatAvailableBalance.split(" ").last; - String get fiatSecondAvailableBalanceRaw => fiatSecondAvailableBalance.split(" ").last; - String get fiatCurrencyTicker => fiatAvailableBalance.split(" ").first; + String get fiatAvailableBalance => + fiatCurrency != null ? "$fiatCurrency ${_withLocalSeperator(fiatAvailableBalanceRaw)}" : ""; + + String get fiatAdditionalBalance => + fiatCurrency != null ? "$fiatCurrency ${_withLocalSeperator(fiatAdditionalBalanceRaw)}" : ""; + + String get fiatFrozenBalance => + fiatCurrency != null ? "$fiatCurrency ${_withLocalSeperator(fiatFrozenBalanceRaw)}" : ""; + + String get fiatSecondAvailableBalance => fiatCurrency != null + ? "$fiatCurrency ${_withLocalSeperator(fiatSecondAvailableBalanceRaw)}" + : ""; + + String get fiatSecondAdditionalBalance => fiatCurrency != null + ? "$fiatCurrency ${_withLocalSeperator(fiatSecondAdditionalBalanceRaw)}" + : ""; + + String _withLocalSeperator(String rawAmount) => + languageCode == null ? rawAmount : rawAmount.withLocalSeperator(languageCode); } class BalanceViewModel = BalanceViewModelBase with _$BalanceViewModel; @@ -221,12 +251,13 @@ abstract class BalanceViewModelBase with Store { } } - String additionalBalance(CryptoCurrency cryptoCurrency) { + Money additionalBalance(CryptoCurrency cryptoCurrency) { final balance = _currencyBalance(cryptoCurrency); - if (displayMode == BalanceDisplayMode.hiddenBalance || balance.additional == BigInt.zero) return '0.0'; + if (displayMode == BalanceDisplayMode.hiddenBalance || balance.unavailable.isZero) + return Money.zero(cryptoCurrency); - return cryptoCurrency.formatAmount(balance.additional); + return balance.unavailable; } @computed @@ -237,25 +268,26 @@ abstract class BalanceViewModelBase with Store { if (displayMode == BalanceDisplayMode.hiddenBalance) { final fiatCurrency = settingsStore.fiatCurrency; return MapEntry( - key, - BalanceRecord( - availableBalance: '●●●●●●', - additionalBalance: additionalBalance(key), - frozenBalance: '', - secondAvailableBalance: '●●●●●●', - secondAdditionalBalance: '●●●●●●', - fiatAdditionalBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●', - fiatAvailableBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●', - fiatFrozenBalance: isFiatDisabled ? '' : '', - fiatSecondAvailableBalance: - isFiatDisabled ? '' : '${fiatCurrency.toString()}', - fiatSecondAdditionalBalance: - isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●', - asset: key, - secondAsset: secondAsset, - formattedAssetTitle: _formatterAsset(key))); + key, + BalanceRecord( + raw: value, + availableBalance: '●●●●●●', + additionalBalance: '', + frozenBalance: '', + secondAvailableBalance: '●●●●●●', + secondAdditionalBalance: '●●●●●●', + fiatAdditionalBalanceRaw: '●●●●●', + fiatAvailableBalanceRaw: '●●●●●', + fiatFrozenBalanceRaw: '', + fiatSecondAvailableBalanceRaw: '●●●●●', + fiatSecondAdditionalBalanceRaw: '●●●●●', + asset: key, + secondAsset: secondAsset, + fiatCurrency: isFiatDisabled ? null : fiatCurrency, + formattedAssetTitle: _formatterAsset(key), + ), + ); } - final fiatCurrency = settingsStore.fiatCurrency; final price = key.isPotentialScam ? 0.0 : fiatConversionStore.prices[key == CryptoCurrency.btcln ? CryptoCurrency.btc : key] ?? 0; @@ -264,48 +296,41 @@ abstract class BalanceViewModelBase with Store { // throw Exception('Price is null for: $key'); // } - final available = evm?.getERC20AvailableBalance(value) ?? - (value.fullAvailableBalance - (value.secondAvailable ?? BigInt.zero)); - final additional = evm?.getERC20AvailableBalance(value) ?? - (value.additional - (value.secondAdditional ?? BigInt.zero)); + final availableFiatBalance = + isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.available); - final availableFiatBalance = isFiatDisabled - ? '' - : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: key.formatAmount(available))}'; + final additionalFiatBalance = + isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.unavailable); - final additionalFiatBalance = isFiatDisabled - ? '' - : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: key.formatAmount(additional))}'; + final frozenFiatBalance = + isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.frozen); - final frozenFiatBalance = isFiatDisabled - ? '' - : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.frozen != null ? key.formatAmount(value.frozen!) : null)}'; - - final secondAvailableFiatBalance = isFiatDisabled - ? '' - : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.secondAvailable != null ? key.formatAmount(value.secondAvailable!) : null)}'; + final secondAvailableFiatBalance = + isFiatDisabled ? '' : _getFiatBalance(price: price, cryptoAmount: value.secondAvailable); final secondAdditionalFiatBalance = isFiatDisabled ? '' - : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.secondAdditional != null ? key.formatAmount(value.secondAdditional!) : null)}'; + : _getFiatBalance(price: price, cryptoAmount: value.secondUnavailable); return MapEntry( key, BalanceRecord( - availableBalance: _getFormattedCryptoAmount(key, available), - fiatAvailableBalance: availableFiatBalance, - additionalBalance: _getFormattedCryptoAmount(key, additional), - fiatAdditionalBalance: additionalFiatBalance, - frozenBalance: - (value.frozen ?? BigInt.zero) > BigInt.zero ? _getFormattedCryptoAmount(key, value.frozen??BigInt.zero) : '', - fiatFrozenBalance: frozenFiatBalance, - secondAvailableBalance: _getFormattedCryptoAmount(secondAsset, value.secondAvailable??BigInt.zero), - fiatSecondAvailableBalance: secondAvailableFiatBalance, - secondAdditionalBalance: _getFormattedCryptoAmount(secondAsset, value.secondAdditional??BigInt.zero), - fiatSecondAdditionalBalance: secondAdditionalFiatBalance, + raw: value, + availableBalance: _getFormattedCryptoAmount(value.available), + fiatAvailableBalanceRaw: availableFiatBalance, + additionalBalance: _getFormattedCryptoAmount(value.unavailable), + fiatAdditionalBalanceRaw: additionalFiatBalance, + frozenBalance: _getFormattedCryptoAmount(value.frozen), + fiatFrozenBalanceRaw: frozenFiatBalance, + secondAvailableBalance: _getFormattedCryptoAmount(value.secondAvailable), + fiatSecondAvailableBalanceRaw: secondAvailableFiatBalance, + secondAdditionalBalance: _getFormattedCryptoAmount(value.secondUnavailable), + fiatSecondAdditionalBalanceRaw: secondAdditionalFiatBalance, asset: key, secondAsset: secondAsset, + fiatCurrency: isFiatDisabled ? null : settingsStore.fiatCurrency, formattedAssetTitle: _formatterAsset(key), + languageCode: settingsStore.languageCode, ), ); }); @@ -316,7 +341,7 @@ abstract class BalanceViewModelBase with Store { bool hasAdditionalBalance(CryptoCurrency currency) { final isWalletTypeActivated = _hasAdditionalBalanceForWalletType(wallet.type); - final isNotZeroAmount = additionalBalance(currency) != "0.0"; + final isNotZeroAmount = !additionalBalance(currency).isZero; return isWalletTypeActivated && isNotZeroAmount; } @@ -324,9 +349,9 @@ abstract class BalanceViewModelBase with Store { @computed bool get hasSecondAdditionalBalance { if (wallet.type == WalletType.litecoin && mwebEnabled) { - return (wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0; + return (wallet.balance[CryptoCurrency.ltc]?.secondUnavailable ?? 0) != 0; } else if (wallet.type == WalletType.bitcoin) { - return (wallet.balance[CryptoCurrency.btc]?.secondAdditional ?? 0) != 0; + return (wallet.balance[CryptoCurrency.btc]?.secondUnavailable ?? 0) != 0; } return false; } @@ -351,12 +376,12 @@ abstract class BalanceViewModelBase with Store { WalletType.zcash ].contains(type); - String _getFormattedCryptoAmount(CryptoCurrency cryptoCurrency, BigInt? amount) { + String _getFormattedCryptoAmount(Money? amount) { if (amount == null) return ""; return appStore.amountParsingProxy - .getDisplayCryptoStringFromBigInt(amount, cryptoCurrency) - .withMaxDecimals(8).withLocalSeperator(settingsStore.languageCode); + .asDisplayString(amount) + .withLocalSeperator(settingsStore.languageCode); } @computed @@ -402,9 +427,9 @@ abstract class BalanceViewModelBase with Store { switch (sortBalanceBy) { case SortBalanceBy.FiatBalance: final aFiatBalance = _getFiatBalance( - price: fiatConversionStore.prices[a.asset] ?? 0, cryptoAmount: a.availableBalance); + price: fiatConversionStore.prices[a.asset] ?? 0, cryptoAmount: a.raw.available); final bFiatBalance = _getFiatBalance( - price: fiatConversionStore.prices[b.asset] ?? 0, cryptoAmount: b.availableBalance); + price: fiatConversionStore.prices[b.asset] ?? 0, cryptoAmount: b.raw.available); return (double.tryParse(bFiatBalance) ?? 0) .compareTo((double.tryParse(aFiatBalance)) ?? 0); @@ -425,7 +450,8 @@ abstract class BalanceViewModelBase with Store { } if (wallet.walletInfo.favoriteTokenAddress != null) { - return formattedBalances.firstWhereOrNull((item) => (getTokenAddressBasedOnWallet(item.asset) == + return formattedBalances.firstWhereOrNull((item) => + (getTokenAddressBasedOnWallet(item.asset) == wallet.walletInfo.favoriteTokenAddress)) ?? formattedBalances.elementAtOrNull(0); } @@ -455,8 +481,8 @@ abstract class BalanceViewModelBase with Store { @computed bool get showCombinedBalance { - if(wallet.type == WalletType.bitcoin) return false; - if(balances.values.length == 1) return false; + if (wallet.type == WalletType.bitcoin) return false; + if (balances.values.length == 1) return false; return wallet.walletInfo.showCombinedBalance; } @@ -470,15 +496,14 @@ abstract class BalanceViewModelBase with Store { double ret = 0.0; for (final curr in wallet.balance.keys) { final record = wallet.balance[curr]!; - final available = evm?.getERC20AvailableBalance(record) ?? - (record.fullAvailableBalance - (record.secondAvailable ?? BigInt.zero)); + final available = record.available - (record.secondAvailable ?? Money.zero(curr)); final price = fiatConversionStore.prices[curr] ?? 0; - ret += double.tryParse(calculateFiatAmount( - price: price, cryptoAmount: curr.formatAmount(available).replaceAll(",", "")) + printV(record.available); + ret += double.tryParse(calculateFiatAmount(price: price, cryptoAmount: available.toString()) .replaceAll(",", "")) ?? 0; } - return ret.toStringAsFixed(2); + return ret.toStringAsFixed(2).withLocalSeperator(settingsStore.languageCode); } Balance _currencyBalance(CryptoCurrency cryptoCurrency) { @@ -522,13 +547,12 @@ abstract class BalanceViewModelBase with Store { } } - String _getFiatBalance({required double price, String? cryptoAmount}) { - if (cryptoAmount == null || cryptoAmount.isEmpty || double.tryParse(cryptoAmount) == null) { - return '0.00'.withLocalSeperator(settingsStore.languageCode); + String _getFiatBalance({required double price, Money? cryptoAmount}) { + if (cryptoAmount == null) { + return '0.00'; } - return calculateFiatAmount(price: price, cryptoAmount: cryptoAmount) - .withLocalSeperator(settingsStore.languageCode); + return calculateFiatAmount(price: price, cryptoAmount: cryptoAmount.toString(), raw: true); } String _formatterAsset(CryptoCurrency asset) { diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 4cbd27633..0a38ff2d2 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -1,23 +1,17 @@ -import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/evm/evm.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/tron/tron.dart'; -import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; -import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cw_core/keyable.dart'; @@ -52,17 +46,12 @@ class TransactionListItem extends ActionListItem with Keyable { String get formattedCryptoAmount { if (displayMode == BalanceDisplayMode.hiddenBalance) return '---'; if (balanceViewModel.wallet.type == WalletType.bitcoin) { - final isLightning = (transaction.additionalInfo["isLightning"] as bool?) ?? false; - final crypto = isLightning ? CryptoCurrency.btcln : CryptoCurrency.btc; - final amount = _appStore.amountParsingProxy - .getDisplayCryptoString(transaction.amount, crypto) - .withMaxDecimals(8) + return _appStore.amountParsingProxy + .asDisplayStringWithSymbol(transaction.amount) .withLocalSeperator(_appStore.settingsStore.languageCode); - - return '$amount ${_appStore.amountParsingProxy.getCryptoSymbol(crypto)}'; } - return transaction.amountFormatted(); + return transaction.amount.toStringWithSymbol(); } String get formattedTitle { @@ -193,26 +182,18 @@ class TransactionListItem extends ActionListItem with Keyable { switch (balanceViewModel.wallet.type) { case WalletType.monero: - amount = calculateFiatAmountRaw( - cryptoAmount: monero!.formatterMoneroAmountToDouble(amount: transaction.amount), - price: price, - ).withLocalSeperator(_appStore.settingsStore.languageCode); - break; case WalletType.wownero: - amount = calculateFiatAmountRaw( - cryptoAmount: wownero!.formatterWowneroAmountToDouble(amount: transaction.amount), - price: price, - ).withLocalSeperator(_appStore.settingsStore.languageCode); - break; case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.dogecoin: + case WalletType.nano: + case WalletType.decred: + case WalletType.zcash: amount = calculateFiatAmountRaw( - cryptoAmount: bitcoin!.formatterBitcoinAmountToDouble(amount: transaction.amount), + cryptoAmount: double.parse(transaction.amount.toString()), price: price, ).withLocalSeperator(_appStore.settingsStore.languageCode); - break; case WalletType.ethereum: case WalletType.polygon: case WalletType.base: @@ -221,14 +202,7 @@ class TransactionListItem extends ActionListItem with Keyable { final asset = assetOfTransaction; final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( - cryptoAmount: evm!.formatterEVMAmountToDouble(transaction: transaction), - price: price, - ).withLocalSeperator(_appStore.settingsStore.languageCode); - break; - case WalletType.nano: - amount = calculateFiatAmountRaw( - cryptoAmount: double.parse(nanoUtil!.getRawAsUsableString( - nano!.getTransactionAmountRaw(transaction).toString(), nanoUtil!.rawPerNano)), + cryptoAmount: double.parse(transaction.amount.toString()), price: price, ).withLocalSeperator(_appStore.settingsStore.languageCode); break; @@ -236,16 +210,15 @@ class TransactionListItem extends ActionListItem with Keyable { final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction); final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( - cryptoAmount: solana!.getTransactionAmountRaw(transaction), + cryptoAmount: double.parse(transaction.amount.toString()), price: price, ).withLocalSeperator(_appStore.settingsStore.languageCode); break; case WalletType.tron: final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction); final price = balanceViewModel.fiatConversionStore.prices[asset]; - final cryptoAmount = tron!.getTransactionAmountRaw(transaction); amount = calculateFiatAmountRaw( - cryptoAmount: cryptoAmount, + cryptoAmount: double.parse(transaction.amount.toString()), price: price, ).withLocalSeperator(_appStore.settingsStore.languageCode); break; @@ -257,24 +230,10 @@ class TransactionListItem extends ActionListItem with Keyable { } final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( - cryptoAmount: zano!.formatterIntAmountToDouble( - amount: transaction.amount, currency: asset, forFee: false), + cryptoAmount: double.parse(transaction.amount.toString()), price: price, ).withLocalSeperator(_appStore.settingsStore.languageCode); break; - case WalletType.decred: - amount = calculateFiatAmountRaw( - cryptoAmount: decred!.formatterDecredAmountToDouble(amount: transaction.amount), - price: price, - ).withLocalSeperator(_appStore.settingsStore.languageCode); - break; - case WalletType.zcash: - amount = calculateFiatAmountRaw( - cryptoAmount: - zcash!.formatterZcashAmountToDouble(amount: BigInt.from(transaction.amount)), - price: price, - ).withLocalSeperator(_appStore.settingsStore.languageCode); - case WalletType.none: case WalletType.banano: case WalletType.haven: diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 792b23f12..e48515bb7 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -2,10 +2,12 @@ import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:developer'; +import 'dart:math' show min; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/core/create_trade_result.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; @@ -41,6 +43,8 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/utils/exchange_provider_logger.dart'; +import 'package:cw_core/amount/amount_sanitizer.dart'; +import 'package:cw_core/amount/money.dart'; import "package:cw_core/wallet_info.dart"; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/dashboard/trades_store.dart'; @@ -103,8 +107,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with ) : isSendAllEnabled = false, isFixedRateMode = false, isReceiveAmountEntered = false, - _depositAmount = '', - _receiveAmount = '', + _depositAmount = null, + _receiveAmount = null, receiveAddress = '', depositAddress = '', isDepositAddressEnabled = false, @@ -163,8 +167,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with }); isDepositAddressEnabled = !useSameWalletAddress(depositCurrency); - _depositAmount = ''; - _receiveAmount = ''; + _depositAmount = null; + _receiveAmount = null; receiveAddress = ''; depositAddress = useSameWalletAddress(depositCurrency) ? wallet.walletAddresses.addressForExchange : ''; @@ -237,12 +241,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with } final balance = wallet.balance[depositCurrency]; if (balance != null) { - depositAvailableAmount = _appStore.amountParsingProxy - .getDisplayCryptoStringFromBigInt(balance.fullAvailableBalance, depositCurrency); + depositAvailableAmount = _appStore.amountParsingProxy.asDisplayString(balance.available); return false; } return true; - }); + }).timeout(Duration(seconds: 20)); } else if (isEVMCompatibleChain(wallet.type)) { final currency = depositCurrency; final balanceCurrency = wallet.balance.keys.firstWhereOrNull( @@ -253,15 +256,15 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final balanceForCurrency = balanceCurrency != null ? wallet.balance[balanceCurrency] : null; if (depositCurrency == currency && balanceForCurrency != null) { - depositAvailableAmount = _appStore.amountParsingProxy - .getDisplayCryptoStringFromBigInt( - balanceForCurrency.fullAvailableBalance, balanceCurrency!); + depositAvailableAmount = + _appStore.amountParsingProxy.asDisplayStringWithSymbol(balanceForCurrency.available); } } else { final currency = depositCurrency; - final amount = _appStore.amountParsingProxy.getDisplayCryptoString( - (await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any)), depositCurrency); - if(depositCurrency == currency) { + final sendingBalance = Money.fromInt( + await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any), currency); + final amount = _appStore.amountParsingProxy.asDisplayStringWithSymbol(sendingBalance); + if (depositCurrency == currency) { depositAvailableAmount = amount; } } @@ -289,7 +292,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with ].contains(wallet.type); bool get hideAddressAfterExchange => - [WalletType.monero, WalletType.wownero, WalletType.zcash].contains(wallet.type); + [WalletType.monero, WalletType.wownero, WalletType.zcash].contains(wallet.type) || + isElectrumWallet; bool _useTorOnly; final ExchangeTemplateStore _exchangeTemplateStore; @@ -352,25 +356,29 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with ExchangeTradeState tradeState; @observable - String _depositAmount; + Money? _depositAmount; + + @computed + bool get hasDepositAmount => _depositAmount != null; @computed String get depositAmount => - _depositAmount == "..."? _depositAmount:amountParsingProxy.getDisplayCryptoAmount(_depositAmount, depositCurrency); + _depositAmount == null ? "" : amountParsingProxy.asDisplayString(_depositAmount!); @computed - String get depositAmountCanonical => _depositAmount; + String get depositAmountCanonical => _depositAmount == null ? "0.0" : _depositAmount.toString(); @observable - String _receiveAmount; + Money? _receiveAmount; @computed String get receiveAmount => - _receiveAmount == "..." ? _receiveAmount:amountParsingProxy.getDisplayCryptoAmount(_receiveAmount, receiveCurrency); + _receiveAmount == null ? "" : amountParsingProxy.asDisplayString(_receiveAmount!); @action // only set canonical formated amounts here; - void setCanonicalReceiveAmount(String value) => _receiveAmount = value; + void setCanonicalReceiveAmount(String value) => + _receiveAmount = Money.tryParse(value, receiveCurrency); @observable String depositAddress; @@ -486,7 +494,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @computed String? get balanceDisplay { CryptoCurrency? balanceCurrency; - if (isEVMCompatibleChain(wallet.type)) { + if (isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana || wallet.type == WalletType.tron) { balanceCurrency = wallet.balance.keys.firstWhereOrNull( (c) => c.title == depositCurrency.title && @@ -495,10 +503,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with } else { balanceCurrency = depositCurrency; } - final bal = balanceCurrency != null ? wallet.balance[balanceCurrency]?.fullAvailableBalance : null; + final bal = balanceCurrency != null ? wallet.balance[balanceCurrency]?.available : null; if (bal == null) return null; - return amountParsingProxy.getDisplayCryptoStringFromBigInt( - bal, balanceCurrency ?? depositCurrency); + return amountParsingProxy.asDisplayString(bal); } //* Still open to further optimize these checks @@ -572,10 +579,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with double bestRate = 0.0; @computed - bool get useSatoshiDeposit => _appStore.amountParsingProxy.useSatoshi(depositCurrency); + bool get useDepositBaseUnit => _appStore.amountParsingProxy.useSatoshi(depositCurrency); @computed - bool get useSatoshisReceive => _appStore.amountParsingProxy.useSatoshi(receiveCurrency); + bool get useReceiveBaseUnit => _appStore.amountParsingProxy.useSatoshi(receiveCurrency); @computed AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy; @@ -624,12 +631,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with String get receiveAmountFiatFormatted { var amount = '0.00'; try { - if (_receiveAmount.isNotEmpty) { + if (_receiveAmount != null) { if (fiatConversionStore.prices[receiveCurrency] == null) return ''; amount = calculateFiatAmount( price: fiatConversionStore.prices[receiveCurrency]!, - cryptoAmount: _receiveAmount, + cryptoAmount: _receiveAmount.toString(), ); } } catch (_) { @@ -642,12 +649,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with String get depositAmountFiatFormatted { var amount = '0.00'; try { - if (_depositAmount.isNotEmpty) { + if (_depositAmount != null) { if (fiatConversionStore.prices[depositCurrency] == null) return ''; amount = calculateFiatAmount( price: fiatConversionStore.prices[depositCurrency]!, - cryptoAmount: _depositAmount, + cryptoAmount: _depositAmount.toString(), ); } } catch (_) { @@ -660,12 +667,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with String get receiveAmountFiat { var amount = ''; try { - if (_receiveAmount.isNotEmpty) { + if (_receiveAmount != null) { if (fiatConversionStore.prices[receiveCurrency] == null) return ''; amount = calculateFiatAmount( price: fiatConversionStore.prices[receiveCurrency]!, - cryptoAmount: _receiveAmount, + cryptoAmount: _receiveAmount.toString(), raw: true); } } catch (_) { @@ -678,13 +685,14 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with String get depositAmountFiat { var amount = ''; try { - if (_depositAmount.isNotEmpty) { + if (_depositAmount != null) { if (fiatConversionStore.prices[depositCurrency] == null) return ''; amount = calculateFiatAmount( - price: fiatConversionStore.prices[depositCurrency]!, - cryptoAmount: _depositAmount, - raw: true); + price: fiatConversionStore.prices[depositCurrency]!, + cryptoAmount: _depositAmount.toString(), + raw: true, + ); } } catch (_) { log('Error calculating deposit amount fiat formatted: $_'); @@ -771,32 +779,37 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with } @action - Future changeReceiveAmount({required String amount}) async { - amount = amountParsingProxy.getCanonicalCryptoAmount(amount, receiveCurrency); - - _receiveAmount = amount; - + Future changeReceiveAmount({required String amount, bool isCanonical = false}) async { if (amount.isEmpty) { - _depositAmount = ''; - _receiveAmount = ''; + _depositAmount = null; + _receiveAmount = null; return; } - final _enteredAmount = double.tryParse(_receiveAmount.replaceAll(',', '.')) ?? 0; + _receiveAmount = isCanonical + ? Money.tryParse(amount.sanitized(), receiveCurrency) + : _appStore.amountParsingProxy.tryParseCryptoString(amount.sanitized(), receiveCurrency); + + if (_receiveAmount == null) { + _depositAmount = null; + return; + } + + final _enteredAmount = double.tryParse(_receiveAmount.toString()) ?? 0; if (bestRate == 0) { - _depositAmount = "..."; + _depositAmount = null; await calculateBestRate(); } final amount_ = _enteredAmount / (forcedProvider == null ? bestRate : forcedProviderRate); - _depositAmount = amount_.toString().withMaxDecimals(depositCurrency.decimals); + _depositAmount = Money.tryParse(amount_.toStringAsFixed(depositCurrency.decimals), depositCurrency); } @action void setReceiveAmountFromFiat({required String fiatAmount}) { - final _enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0.0; + final _enteredAmount = double.tryParse(fiatAmount.sanitized()) ?? 0.0; final price = fiatConversionStore.prices[receiveCurrency]; if (price == null || price == 0.0) return; @@ -809,7 +822,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @action void setDepositAmountFromFiat({required String fiatAmount}) { - final _enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0.0; + final _enteredAmount = double.tryParse(fiatAmount.sanitized()) ?? 0.0; final price = fiatConversionStore.prices[depositCurrency]; if (price == null || price == 0.0) return; @@ -822,34 +835,40 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @action Future changeDepositAmount({required String amount, bool isCanonical = false}) async { - _depositAmount = isCanonical ? amount.replaceAll(',', '.') : _appStore.amountParsingProxy - .getCanonicalCryptoAmount(amount.replaceAll(',', '.'), depositCurrency); - if (amount.isEmpty) { - _depositAmount = ''; - _receiveAmount = ''; + _depositAmount = null; + _receiveAmount = null; return; } + _depositAmount = isCanonical + ? Money.tryParse(amount.sanitized(), depositCurrency) + : _appStore.amountParsingProxy.tryParseCryptoString(amount.sanitized(), depositCurrency); + + if (_depositAmount == null) { + _receiveAmount = null; + return; + } /// For fixed-rate transactions, we don't want to recalculate receive amount /// as it should remain exactly what the user set if (isFixedRateMode) return; - final _enteredAmount = double.tryParse(_depositAmount) ?? 0; + final _enteredAmount = double.tryParse(_depositAmount.toString()) ?? 0; /// in case the best rate was not calculated yet if (bestRate == 0) { - _receiveAmount = "..."; + _receiveAmount = null; await calculateBestRate(); } final amount_ = _enteredAmount * (forcedProvider == null ? bestRate : forcedProviderRate); - _receiveAmount = amount_.toString().withMaxDecimals(receiveCurrency.decimals); + _receiveAmount = + Money.tryParse(amount_.toStringAsFixed(min(20, receiveCurrency.decimals)), receiveCurrency); } bool checkIfInputMeetsMinOrMaxCondition(String input) { - final _enteredAmount = double.tryParse(input.replaceAll(',', '.')) ?? 0; + final _enteredAmount = double.tryParse(input.sanitized()) ?? 0; double minLimit = limits.min ?? 0; double? maxLimit = limits.max; @@ -866,8 +885,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with return; } - final amount = double.tryParse(isFixedRateMode ? _receiveAmount : _depositAmount) ?? - initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency); + final amount = + double.tryParse(isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString()) ?? + initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency); forcedProviderRate = await forcedProvider!.fetchRate( from: depositCurrency, @@ -891,8 +911,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with bestRateProvider = null; return; } - final amount = double.tryParse(isFixedRateMode ? _receiveAmount : _depositAmount) ?? - initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency); + final amount = + double.tryParse(isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString()) ?? + initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency); final validProvidersForAmount = _tradeAvailableProviders.where((provider) { if (_excludeProviderForSwapAll(provider)) return false; @@ -1025,13 +1046,13 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @action Future createTrade() async { - final depositAmountValue = - double.tryParse(_depositAmount.replaceAll(',', '.')) ?? 0.0; + final depositAmountValue = _depositAmount ?? Money.zero(depositCurrency); + final receiveAmountValue = _receiveAmount ?? Money.zero(receiveCurrency); - final receiveAmountValue = - double.tryParse(_receiveAmount.replaceAll(',', '.')) ?? 0.0; - - if (depositAmountValue <= 0 || receiveAmountValue <= 0) { + if (depositAmountValue.isZero || + depositAmountValue.isNegative || + receiveAmountValue.isZero || + receiveAmountValue.isNegative) { ExchangeProviderLogger.logError( provider: forcedProvider?.description ?? bestRateProvider?.description, function: 'createTrade', @@ -1047,25 +1068,26 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with }, ); - final invalidAmountError = depositAmountValue <= 0 + final invalidAmountError = depositAmountValue.isZero || depositAmountValue.isNegative ? '$depositAmountValue is not a valid amount for depositAmount' : '$receiveAmountValue is not a valid amount for receiveAmount'; tradeState = TradeIsCreatedFailure( title: S.current.trade_not_created, - error: invalidAmountError, + error: invalidAmountError ); return; } if (isSendAllEnabled) { await calculateDepositAllAmount(); - final amount = double.tryParse(_depositAmount); + final amount = double.tryParse(_depositAmount.toString()); if (limits.min != null && amount != null && amount < limits.min!) { tradeState = TradeIsCreatedFailure( title: S.current.trade_not_created, - error: S.current.amount_is_below_minimum_limit(limits.min!.toString())); + error: S.current.amount_is_below_minimum_limit(limits.min!.toString()), + ); return; } } @@ -1090,8 +1112,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with if (forcedProvider != null && _excludeProviderForReceiveExtraId(forcedProvider!)) { tradeState = TradeIsCreatedFailure( - title: S.current.trade_not_created, - error: S.current.none_of_selected_providers_can_exchange); + title: S.current.trade_not_created, + error: S.current.none_of_selected_providers_can_exchange, + ); return; } @@ -1183,7 +1206,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with if (isFixedRateMode && provider.supportsFixedRate == false) { continue; } - + if (_excludeProviderForReceiveExtraId(provider)) { continue; } @@ -1200,13 +1223,13 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with bestRate = providerRate; bestRateProvider = provider; - await changeDepositAmount(amount: _depositAmount, isCanonical: true); + await changeDepositAmount(amount: _depositAmount.toString(), isCanonical: true); final request = TradeRequest( fromCurrency: depositCurrency, toCurrency: receiveCurrency, - fromAmount: _depositAmount.replaceAll(',', '.'), - toAmount: _receiveAmount.replaceAll(',', '.'), + fromAmount: _depositAmount.toString(), + toAmount: _receiveAmount.toString(), refundAddress: depositAddress, toAddress: receiveAddress, toAddressExtraId: receiveAddressExtraId.trim(), @@ -1218,8 +1241,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with await wallet.walletAddresses.saveAddressesInBox(); } - var amount = isFixedRateMode ? _receiveAmount : _depositAmount; - amount = amount.replaceAll(',', '.'); + var amount = isFixedRateMode ? _receiveAmount.toString() : _depositAmount.toString(); if (limitsState is LimitsLoadedSuccessfully) { if (double.tryParse(amount) == null) { @@ -1266,7 +1288,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with } tradesStore.setTrade(trade); - if (trade.provider != ExchangeProviderDescription.thorChain) await trade.save(); + if (trade.provider != ExchangeProviderDescription.thorChain) await trade.save(); tradeState = TradeIsCreatedSuccessfully(trade: trade); /// return after the first successful trade @@ -1310,8 +1332,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with void reset() { _initialPairBasedOnWallet(); isReceiveAmountEntered = false; - _depositAmount = ''; - _receiveAmount = ''; + _depositAmount = null; + _receiveAmount = null; depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : ''; receiveAddress = @@ -1347,8 +1369,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final amount = depositCurrency == CryptoCurrency.btcln // FIXME amount estimation is broken/impossible for ln, konsti suggested this - ? (amountParsingProxy - .parseCryptoString(depositAvailableAmount, depositCurrency).toInt() - 10) + ? (amountParsingProxy.parseCryptoString(depositAvailableAmount, depositCurrency) - + Money.fromInt(10, depositCurrency)) : await bitcoin!.estimateFakeSendAllTxAmount( wallet, priority, @@ -1359,7 +1381,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with : UnspentCoinType.any, ); - changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.from(amount)), isCanonical: true); + changeDepositAmount(amount: amount.toString(), isCanonical: true); } else if (wallet.type == WalletType.monero) { final amount = await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any); @@ -1377,7 +1399,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with return; } - final balanceAmount = _appStore.amountParsingProxy.getDisplayCryptoStringFromBigInt(balanceForCurrency.fullAvailableBalance, balanceCurrency!); + final balanceAmount = _appStore.amountParsingProxy.asDisplayString(balanceForCurrency.available); final balanceDouble = double.tryParse(balanceAmount.replaceAll(',', '.')) ?? 0.0; if (balanceDouble <= 0) { changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.zero), isCanonical: true); @@ -1405,7 +1427,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with return; } - final balanceWei = balanceForCurrency.fullAvailableBalance; + final balanceWei = balanceForCurrency.available.amount; final feeWei = BigInt.parse(feeString); final amountAfterFeeWei = balanceWei > feeWei ? balanceWei - feeWei : BigInt.zero; changeDepositAmount( @@ -1455,14 +1477,14 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with void _onPairChange({bool clearBoth = false}) { if (clearBoth) { - _depositAmount = ''; - _receiveAmount = ''; + _depositAmount = null; + _receiveAmount = null; if (!hasAllAmount) isSendAllEnabled = false; } else { if (isFixedRateMode) { - _depositAmount = ''; + _depositAmount = null; } else { - _receiveAmount = ''; + _receiveAmount = null; } } bestRate = 0.0; @@ -1565,6 +1587,21 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with } } + String? _addressTypeValidation(String refundAddress, String receiveAddress) { + final isRefundAddressSP = RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(refundAddress); + if (isRefundAddressSP) return 'Silent Payment ${S.current.address_not_allowed_as_refund}'; + + final isReceiveAddressSP = RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(receiveAddress); + if (isReceiveAddressSP) return 'Silent Payment ${S.current.address_not_allowed_as_receive}'; + + final isRefundAddressMWEB = RegExp(AddressValidator.mWebAddressPattern).hasMatch(refundAddress); + if (isRefundAddressMWEB) return 'MWEB ${S.current.address_not_allowed_as_refund}'; + + final isReceiveAddressMWEB = RegExp(AddressValidator.mWebAddressPattern).hasMatch(receiveAddress); + if (isReceiveAddressMWEB) return 'MWEB ${S.current.address_not_allowed_as_receive}'; + return null; + } + void _defineIsReceiveAmountEditable() { /*if ((provider is ChangeNowExchangeProvider) &&(depositCurrency == CryptoCurrency.xmr) @@ -1597,8 +1634,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @action void saveSelectedProviders() { - _depositAmount = ''; - _receiveAmount = ''; + _depositAmount = null; + _receiveAmount = null; isFixedRateMode = false; _defineIsReceiveAmountEditable(); bestRateProvider = null; diff --git a/lib/view_model/integrations/deuro_view_model.dart b/lib/view_model/integrations/deuro_view_model.dart index 777334e32..3f78fb6da 100644 --- a/lib/view_model/integrations/deuro_view_model.dart +++ b/lib/view_model/integrations/deuro_view_model.dart @@ -9,6 +9,7 @@ import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/parse_fixed.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -22,7 +23,9 @@ class DEuroViewModel = DEuroViewModelBase with _$DEuroViewModel; abstract class DEuroViewModelBase with Store { final AppStore _appStore; - static BigInt get MIN_ACCRUED_INTEREST => BigInt.parse("1000000000000"); + static Money get MIN_ACCRUED_INTEREST => Money(BigInt.parse("1000000000000"), CryptoCurrency.deuro); + + static Money get ZERO => Money.zero(CryptoCurrency.deuro); DEuroViewModelBase( this._appStore, this.balanceViewModel, this._settingsStore, this._fiatConversationStore, @@ -51,13 +54,13 @@ abstract class DEuroViewModelBase with Store { @computed String get pendingTransactionFiatAmountFormatted { - final amount = transaction == null ? '0.00' : _getDEuroFiatAmount(transaction!.amountFormatted); - return isFiatDisabled ? '' : '$amount ${fiat.title}'; + final amount = transaction == null ? "0.00" : _getDEuroFiatAmount(transaction!.amount); + return isFiatDisabled ? "" : "$amount ${fiat.symbol}"; } @computed String get pendingTransactionFeeFiatAmountFormatted { - var amount = '0.00'; + var amount = "0.00"; try { if (transaction != null) { final feeCurrency = CryptoCurrency.eth; @@ -67,42 +70,43 @@ abstract class DEuroViewModelBase with Store { ); } } catch (_) {} - return isFiatDisabled ? '' : '$amount ${fiat.title}'; + return isFiatDisabled ? "" : "$amount ${fiat.symbol}"; } @computed - String get accountBalanceFormated { + Money get accountBalance { final dEuroKey = balanceViewModel.balances.keys .firstWhereOrNull((e) => e.title == CryptoCurrency.deuro.title); - if (dEuroKey == null) return '0.00'; - return balanceViewModel.balances[dEuroKey]?.availableBalance ?? '0.00'; + if (dEuroKey == null) return ZERO; + return wallet.balance[dEuroKey]?.available ?? ZERO; } @observable - BigInt savingsBalance = BigInt.zero; + Money savingsBalance = ZERO; @computed - String get savingsBalanceFormated => - evm!.formatterEVMAmountToDouble(amount: savingsBalance).toStringAsFixed(6); + String get fiatSavingsBalanceFormated => _getDEuroFiatAmount(savingsBalance); + + @observable + Money? savingsBalanceV1 = null; @computed - String get fiatSavingsBalanceFormated => _getDEuroFiatAmount(savingsBalanceFormated); + String get fiatSavingsBalanceV1Formated => _getDEuroFiatAmount(savingsBalanceV1); @observable ExecutionState state = InitialExecutionState(); @observable - String interestRateFormated = '0'; + String interestRateFormated = "0"; @observable - BigInt accruedInterest = BigInt.zero; + Money accruedInterest = ZERO; @computed - String get accruedInterestFormated => - evm!.formatterEVMAmountToDouble(amount: accruedInterest).toStringAsFixed(6); + String get accruedInterestFormated => accruedInterest.toStringWithPrecision(fractionalDigits: 6); @computed - String get fiatAccruedInterestFormated => _getDEuroFiatAmount(accruedInterestFormated); + String get fiatAccruedInterestFormated => _getDEuroFiatAmount(accruedInterest); @observable BigInt approvedTokens = BigInt.zero; @@ -128,8 +132,12 @@ abstract class DEuroViewModelBase with Store { @action Future reloadSavingsUserData() async { approvedTokens = await evm!.getDEuroSavingsApproved(_appStore.wallet!) ?? BigInt.zero; - savingsBalance = await evm!.getDEuroSavingsBalance(_appStore.wallet!) ?? BigInt.zero; - accruedInterest = await evm!.getDEuroAccruedInterest(_appStore.wallet!) ?? BigInt.zero; + savingsBalance = await evm!.getDEuroSavingsBalance(_appStore.wallet!) ?? ZERO; + accruedInterest = await evm!.getDEuroAccruedInterest(_appStore.wallet!) ?? ZERO; + + final v1Balance = await evm!.getDEuroSavingsV1Balance(_appStore.wallet!) ?? ZERO; + savingsBalanceV1 = v1Balance.isZero ? null : v1Balance; + isLoading = false; } @@ -151,9 +159,8 @@ abstract class DEuroViewModelBase with Store { state = TransactionCommitting(); final priority = _appStore.settingsStore.getPriority(wallet.type, chainId: wallet.chainId)!; final approval = await evm!.enableDEuroSaving(_appStore.wallet!, priority); - if (approval == null) { - throw Exception('DEuro saving not available'); - } + if (approval == null) throw Exception("DEuro saving not available"); + approvalTransaction = approval; state = InitialExecutionState(); } catch (e) { @@ -167,13 +174,13 @@ abstract class DEuroViewModelBase with Store { state = TransactionCommitting(); if (amountRaw.isEmpty || amountRaw.trim().isEmpty) { - throw Exception('Invalid amount: amount cannot be empty'); + throw Exception("Invalid amount: amount cannot be empty"); } final amount = tryParseFixed(amountRaw, 18); if (amount == BigInt.zero || amount == null) { - throw Exception('Invalid amount: amount cannot be zero'); + throw Exception("Invalid amount: amount cannot be zero"); } final priority = _appStore.settingsStore.getPriority(wallet.type, chainId: wallet.chainId)!; @@ -181,9 +188,24 @@ abstract class DEuroViewModelBase with Store { final tx = await (isAdding ? evm!.addDEuroSaving(_appStore.wallet!, amount, priority) : evm!.removeDEuroSaving(_appStore.wallet!, amount, priority)); - if (tx == null) { - throw Exception('DEuro saving not available'); - } + if (tx == null) throw Exception("DEuro saving not available"); + + transaction = tx; + state = InitialExecutionState(); + } catch (e) { + state = FailureState(e.toString()); + } + } + + @action + Future prepareSavingsV1Withdraw() async { + try { + state = TransactionCommitting(); + actionType = DEuroActionType.withdraw; + final priority = _appStore.settingsStore.getPriority(wallet.type, chainId: wallet.chainId)!; + final tx = await evm!.withdrawDEuroSavingV1(_appStore.wallet!, priority); + if (tx == null) throw Exception("DEuro saving not available"); + transaction = tx; state = InitialExecutionState(); } catch (e) { @@ -193,13 +215,13 @@ abstract class DEuroViewModelBase with Store { Future prepareCollectInterest() async { if (accruedInterest < MIN_ACCRUED_INTEREST) { - state = FailureState('Accrued interest is below minimum threshold'); + state = FailureState("Accrued interest is below minimum threshold"); return; } final formatted = accruedInterestFormated; - if (formatted.isEmpty || formatted == '0.000000') { - state = FailureState('Invalid accrued interest amount'); + if (formatted.isEmpty || formatted == "0.000000") { + state = FailureState("Invalid accrued interest amount"); return; } @@ -212,9 +234,8 @@ abstract class DEuroViewModelBase with Store { actionType = DEuroActionType.reinvest; final priority = _appStore.settingsStore.getPriority(wallet.type, chainId: wallet.chainId)!; final tx = await evm!.reinvestDEuroInterest(_appStore.wallet!, priority); - if (tx == null) { - throw Exception('DEuro saving not available'); - } + if (tx == null) throw Exception("DEuro saving not available"); + transaction = tx; state = InitialExecutionState(); } catch (e) { @@ -261,7 +282,9 @@ abstract class DEuroViewModelBase with Store { state = InitialExecutionState(); } - String _getDEuroFiatAmount(String amount) { + String _getDEuroFiatAmount(Money? amount) { + if (amount == null) return "0.00"; + try { var dEuro = CryptoCurrency.deuro; final keys = _fiatConversationStore.prices.keys.toList(); @@ -270,19 +293,14 @@ abstract class DEuroViewModelBase with Store { } return calculateFiatAmount( price: _fiatConversationStore.prices[dEuro]!, - cryptoAmount: amount, + cryptoAmount: amount.toStringWithPrecision(fractionalDigits: 6), ); } catch (_) { - return '0.00'; + return "0.00"; } } } class NoEtherState extends ExecutionState {} -enum DEuroActionType { - deposit, - withdraw, - reinvest, - none; -} +enum DEuroActionType { deposit, withdraw, reinvest, none } diff --git a/lib/view_model/payjoin_details_view_model.dart b/lib/view_model/payjoin_details_view_model.dart index d6bebfe7f..85d0ab8d6 100644 --- a/lib/view_model/payjoin_details_view_model.dart +++ b/lib/view_model/payjoin_details_view_model.dart @@ -110,10 +110,10 @@ abstract class PayjoinDetailsViewModelBase with Store { value: '${transactionInfo!.height}', key: ValueKey('standard_list_item_transaction_details_height_key'), ), - if (transactionInfo!.feeFormatted()?.isNotEmpty ?? false) + if (transactionInfo!.fee != null) StandartListItem( title: S.current.transaction_details_fee, - value: transactionInfo!.feeFormatted()!, + value: transactionInfo!.fee!.toStringWithSymbol(), key: ValueKey('standard_list_item_transaction_details_fee_key'), ), ]); diff --git a/lib/view_model/payment/payment_view_model.dart b/lib/view_model/payment/payment_view_model.dart index 258758e9b..779e97183 100644 --- a/lib/view_model/payment/payment_view_model.dart +++ b/lib/view_model/payment/payment_view_model.dart @@ -93,7 +93,7 @@ abstract class PaymentViewModelBase with Store { if (currentWallet != null && currentWallet.type == detectedWalletType && !isEVMCompatibleChain(detectedWalletType!)) { - return PaymentFlowResult.currentWalletCompatible(); + return PaymentFlowResult.currentWalletCompatible(detectionResult); } if (detectedWalletType == WalletType.solana) { @@ -229,8 +229,12 @@ class PaymentFlowResult { ); /// Current wallet is compatible - factory PaymentFlowResult.currentWalletCompatible() => - PaymentFlowResult._(type: PaymentFlowType.currentWalletCompatible); + factory PaymentFlowResult.currentWalletCompatible( + AddressDetectionResult addressDetectionResult) => + PaymentFlowResult._( + type: PaymentFlowType.currentWalletCompatible, + addressDetectionResult: addressDetectionResult, + ); /// Single compatible wallet available factory PaymentFlowResult.singleWallet( diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 9c88d704f..543a541bd 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -1,6 +1,5 @@ import 'dart:math' show min; import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; @@ -9,7 +8,6 @@ import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/evm/evm.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; 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'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/tron/tron.dart'; -import 'package:cake_wallet/wownero/wownero.dart'; -import 'package:cake_wallet/zano/zano.dart'; -import 'package:cake_wallet/zcash/zcash.dart'; +import 'package:cw_core/amount/amount_sanitizer.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; -import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -50,7 +46,7 @@ abstract class OutputBase with Store { note = '', memo = "", extractedAddress = '', - estimatedFee = '0.0', + estimatedFee = Money.zero(cryptoCurrencyHandler()), parsedAddress = ParsedAddress(addresses: []) { autorun((_) { final status = _wallet.syncStatus; @@ -64,6 +60,9 @@ abstract class OutputBase with Store { bool get useSatoshi => _appStore.amountParsingProxy.useSatoshi(cryptoCurrencyHandler()); + @observable + bool isFiatEntry = false; + @observable String fiatAmount; @@ -74,8 +73,7 @@ abstract class OutputBase with Store { String? displayName; @computed - String get displayCryptoAmount => - _appStore.amountParsingProxy.getDisplayCryptoAmount(cryptoAmount, cryptoCurrencyHandler()); + String get displayCryptoAmount => _appStore.amountParsingProxy.asDisplayString(cryptoAmountMoney); @observable String cryptoFullBalance; @@ -106,9 +104,8 @@ abstract class OutputBase with Store { String roundedCryptoAmount(int digits) => displayCryptoAmount.withMaxDecimals(digits); String roundedFiatAmount(int digits) { - if (fiatAmount.split(".").last.length <= digits) { - return fiatAmount; - } + if (fiatAmount.split(".").last.length <= digits) return fiatAmount; + return double.tryParse(fiatAmount.replaceAll(",", ""))?.toStringAsPrecision(digits) ?? '0.0'; } @@ -116,65 +113,18 @@ abstract class OutputBase with Store { String? stealthAddress; @computed - int get formattedCryptoAmount { - int amount = 0; + Money get cryptoAmountMoney { + if (cryptoAmount.isEmpty) return Money.zero(cryptoCurrencyHandler()); try { - if (cryptoAmount.isNotEmpty) { - final _cryptoAmount = cryptoAmount.replaceAll(',', '.'); - int _amount = 0; - switch (walletType) { - case WalletType.monero: - _amount = monero!.formatterMoneroParseAmount(amount: _cryptoAmount); - break; - case WalletType.bitcoin: - case WalletType.litecoin: - case WalletType.bitcoinCash: - case WalletType.dogecoin: - _amount = cryptoCurrencyHandler().parseAmount(_cryptoAmount).toInt(); - break; - case WalletType.decred: - _amount = decred!.formatterStringDoubleToDecredAmount(_cryptoAmount); - break; - case WalletType.ethereum: - case WalletType.polygon: - case WalletType.base: - case WalletType.arbitrum: - case WalletType.bsc: - _amount = evm!.formatterEVMParseAmount(_cryptoAmount); - break; - case WalletType.wownero: - _amount = wownero!.formatterWowneroParseAmount(amount: _cryptoAmount); - break; - case WalletType.zano: - _amount = zano! - .formatterParseAmount(amount: _cryptoAmount, currency: cryptoCurrencyHandler()); - break; - case WalletType.zcash: - _amount = zcash!.formatterZcashParseAmount(_cryptoAmount); - break; - case WalletType.none: - case WalletType.haven: - case WalletType.nano: - case WalletType.banano: - case WalletType.solana: - case WalletType.tron: - break; - } - - if (_amount > 0) { - amount = _amount; - } - } + return cryptoCurrencyHandler().parseAmount(cryptoAmount.sanitized()); } catch (e) { - amount = 0; + return Money.zero(cryptoCurrencyHandler()); } - - return amount; } @observable - String estimatedFee; + Money estimatedFee; @action Future calculateEstimatedFee() async { @@ -188,7 +138,7 @@ abstract class OutputBase with Store { if (_settingsStore.getPriority(_wallet.type, chainId: _wallet.chainId) != null) { fee = _wallet.calculateEstimatedFee( _settingsStore.getPriority(_wallet.type, chainId: _wallet.chainId)!, - formattedCryptoAmount, + cryptoAmountMoney.amount.toInt(), ); } @@ -199,41 +149,35 @@ abstract class OutputBase with Store { case WalletType.bitcoinCash: case WalletType.dogecoin: case WalletType.decred: - estimatedFee = walletTypeToCryptoCurrency(_wallet.type).formatAmount(BigInt.from(fee)); + case WalletType.zano: + estimatedFee = Money.fromInt(fee, walletTypeToCryptoCurrency(_wallet.type)); break; case WalletType.bitcoin: if (cryptoCurrencyHandler() == CryptoCurrency.btcln) { - estimatedFee = - _appStore.amountParsingProxy.getDisplayCryptoString(10, cryptoCurrencyHandler()); + estimatedFee = Money.fromInt(10, cryptoCurrencyHandler()); break; } if (_settingsStore.getPriority(_wallet.type) == bitcoin!.getBitcoinTransactionPriorityCustom()) { fee = bitcoin!.getEstimatedFeeWithFeeRate( - _wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount); + _wallet, _settingsStore.customBitcoinFeeRate, cryptoAmountMoney.amount.toInt()); } - estimatedFee = _appStore.amountParsingProxy.getDisplayCryptoString(fee, cryptoCurrencyHandler()); + estimatedFee = Money.fromInt(fee, cryptoCurrencyHandler()); break; case WalletType.solana: - estimatedFee = solana!.getEstimateFees(_wallet).toString(); - break; - case WalletType.zano: - estimatedFee = zano! - .formatterIntAmountToDouble( - amount: fee, currency: cryptoCurrencyHandler(), forFee: true) - .toString(); + estimatedFee = solana!.getEstimateFees(_wallet) ?? Money.zero(CryptoCurrency.sol); break; case WalletType.tron: if (cryptoCurrencyHandler() == CryptoCurrency.trx) { - estimatedFee = tron!.getTronNativeEstimatedFee(_wallet).toString(); + estimatedFee = tron!.getTronNativeEstimatedFee(_wallet) ?? Money.zero(CryptoCurrency.trx); } else { - estimatedFee = tron!.getTronTRC20EstimatedFee(_wallet).toString(); + estimatedFee = tron!.getTronTRC20EstimatedFee(_wallet) ?? Money.zero(CryptoCurrency.trx); } break; case WalletType.zcash: - estimatedFee = zcash!.formatterZcashAmountToDouble(amount: BigInt.from(fee)).toString(); + estimatedFee = Money.fromInt(fee, cryptoCurrencyHandler()); break; /// EVMs @@ -242,16 +186,19 @@ abstract class OutputBase with Store { case WalletType.base: case WalletType.arbitrum: case WalletType.bsc: - final isNative = cryptoCurrencyHandler() == CryptoCurrency.eth || - cryptoCurrencyHandler() == CryptoCurrency.maticpoly || - cryptoCurrencyHandler() == CryptoCurrency.baseEth || - cryptoCurrencyHandler() == CryptoCurrency.arbEth || - cryptoCurrencyHandler() == CryptoCurrency.bnb; - String? fee = isNative + final isNative = [ + CryptoCurrency.eth, + CryptoCurrency.maticpoly, + CryptoCurrency.baseEth, + CryptoCurrency.arbEth, + CryptoCurrency.bnb + ].contains(cryptoCurrencyHandler()); + + final fee = isNative ? evm!.getEVMNativeEstimatedFee(_wallet) : evm!.getEVMERC20EstimatedFee(_wallet); - estimatedFee = formatFixed(BigInt.parse(fee ?? '0.0'), 18, fractionalDigits: 12); + estimatedFee = Money(BigInt.parse(fee ?? '0.0'), walletTypeToCryptoCurrency(_wallet.type)); break; /// end EVMs @@ -275,12 +222,12 @@ abstract class OutputBase with Store { try { final currency = (isEVMCompatibleChain(_wallet.type) || - [WalletType.solana, WalletType.tron].contains(_wallet.type)) + [WalletType.solana, WalletType.tron].contains(_wallet.type)) || + cryptoCurrencyHandler() == CryptoCurrency.btcln ? _wallet.currency : cryptoCurrencyHandler(); - final cryptoAmount = - double.parse(_appStore.amountParsingProxy.getCanonicalCryptoAmount(estimatedFee, currency)); + final cryptoAmount = double.parse(estimatedFee.toString()); return calculateFiatAmountRaw( price: _fiatConversationStore.prices[currency]!, cryptoAmount: cryptoAmount); @@ -294,7 +241,7 @@ abstract class OutputBase with Store { WalletType get walletType => _wallet.type; - final CryptoCurrency Function() cryptoCurrencyHandler; + final CryptoCurrency Function([CryptoCurrency?]) cryptoCurrencyHandler; final FiatConversionStore _fiatConversationStore; final AppStore _appStore; @@ -401,6 +348,10 @@ abstract class OutputBase with Store { if(parsedAddress.name.isNotEmpty) { displayName = parsedAddress.name; } + + if (parsedAddress.parseFrom == ParseFrom.lnurlpay) { + cryptoCurrencyHandler(CryptoCurrency.btcln); + } extractedAddress = await extractAddressFromParsed(context, parsedAddress); note = parsedAddress.description; } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 6c001c5b6..5c4b2c09c 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -53,6 +53,7 @@ import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_mod import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; import 'package:cake_wallet/zcash/zcash.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/erc20_token.dart'; @@ -121,7 +122,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor fiatFromSettings = _appStore.settingsStore.fiatCurrency, fiatCurrencies = FiatCurrency.all, super(appStore: _appStore) { - outputs.add(Output(wallet, _appStore, _fiatConversationStore, () => selectedCryptoCurrency)); + outputs.add(Output(wallet, _appStore, _fiatConversationStore, _outputCryptoCurrencyHandler)); unspentCoinsListViewModel .initialSetup() @@ -162,12 +163,19 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor bool get isEVMWallet => isEVMCompatibleChain(walletType); + @action + CryptoCurrency _outputCryptoCurrencyHandler([CryptoCurrency? override]) { + if (override != null && override != selectedCryptoCurrency) selectedCryptoCurrency = override; + + return selectedCryptoCurrency; + } + @action void setShowAddressBookPopup(bool value) => _settingsStore.showAddressBookPopupEnabled = value; @action void addOutput() => - outputs.add(Output(wallet, _appStore, _fiatConversationStore, () => selectedCryptoCurrency)); + outputs.add(Output(wallet, _appStore, _fiatConversationStore, _outputCryptoCurrencyHandler)); @action void removeOutput(Output output) { @@ -310,10 +318,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor // Handle case where balance might not be available yet (e.g., during chain switch) final balanceForCurrency = wallet.balance[selectedCryptoCurrency]; if (balanceForCurrency == null) { - return _appStore.amountParsingProxy.getDisplayCryptoString(0, selectedCryptoCurrency); + return _appStore.amountParsingProxy.asDisplayString(Money.zero(selectedCryptoCurrency)); } - return _appStore.amountParsingProxy.getDisplayCryptoStringFromBigInt( - wallet.balance[selectedCryptoCurrency]!.fullAvailableBalance, selectedCryptoCurrency); + return _appStore.amountParsingProxy + .asDisplayString(wallet.balance[selectedCryptoCurrency]!.available); } @action @@ -340,7 +348,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor // only for electrum, monero, wownero, decred wallets atm: switch (wallet.type) { case WalletType.bitcoin: - if (coinTypeToSpendFrom == UnspentCoinType.lightning) return balance; + if (selectedCryptoCurrency == CryptoCurrency.btcln) return balance; return _appStore.amountParsingProxy.getDisplayCryptoString( await unspentCoinsListViewModel.getSendingBalance(coinTypeToSpendFrom), walletTypeToCryptoCurrency(walletType)); @@ -420,6 +428,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor String get selectedCryptoCurrencySymbol => amountParsingProxy.getCryptoSymbol(selectedCryptoCurrency); + @computed + bool get useBaseUnits => amountParsingProxy.useSatoshi(selectedCryptoCurrency); + List currencies; WalletType get walletType => wallet.type; @@ -681,7 +692,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor wallet, routerTo, routerData, - BigInt.zero, + Money.zero(wallet.currency), priority, useBlinkProtection: canSupportBlinkProtection(selectedChainId) ? _settingsStore.useBlinkProtection @@ -804,7 +815,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor wallet, routerTo, routerData, - routerValueWei, + Money(routerValueWei, wallet.currency), priority, sourceTokenAddress: tokenContract, sourceTokenAmount: requiredAmount, @@ -842,7 +853,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor // Fallback to estimate if not available final fee = actualFee > 0 ? actualFee : 0.0005; - final amount = double.tryParse(trade.amount) ?? 0.0; + final fromCurrency = trade.from ?? CryptoCurrency.sol; + final amount = Money.tryParse(trade.amount, fromCurrency) ?? Money.zero(fromCurrency); pendingTransaction = await solana!.signAndPrepareJupiterSwapTransaction( wallet, @@ -850,7 +862,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor requestId!, trade.payoutAddress ?? '', amount, - fee, + Money.tryParse(fee.toString(), CryptoCurrency.sol) ?? Money.zero(CryptoCurrency.sol), ); state = ExecutedSuccessfullyState(); @@ -1104,41 +1116,41 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor // instead, they rely on the tx to eventually get fetched at sync time, which can take a while if(isEVMWallet) { wallet.transactionHistory.addOne(evm!.getTransactionInfo( - id: pendingTransaction!.evmTxHashFromRawHex!, - height: 0, - // FIXME(malik) this is even more critical given all the issues we had with decimal point parsing. with money it'll be easy - ethAmount: selectedCryptoCurrency.parseAmount(outputs.first.cryptoAmount), - ethFee: CryptoCurrency.eth.parseAmount(pendingTransaction!.feeFormattedValue), - tokenSymbol: selectedCryptoCurrency.title, - direction: TransactionDirection.outgoing, - isPending: true, - date: DateTime.now(), - confirmations: 0, - chainId: wallet.chainId ?? 0)); + id: pendingTransaction!.evmTxHashFromRawHex!, + height: 0, + amount: outputs.first.cryptoAmountMoney, + fee: pendingTransaction!.fee, + tokenSymbol: selectedCryptoCurrency.title, + direction: TransactionDirection.outgoing, + isPending: true, + date: DateTime.now(), + confirmations: 0, + chainId: wallet.chainId ?? 0, + )); } if(walletType == WalletType.solana) { wallet.transactionHistory.addOne(solana!.getTransactionInfo( - id: pendingTransaction!.id, - blockTime: DateTime.now(), - tokenSymbol: selectedCryptoCurrency.title, - to: "", - from: "", - direction: TransactionDirection.outgoing, - solAmount: solana!.getPendingTransactionAmount(pendingTransaction!), - isPending: true, - txFee: solana!.getPendingTransactionFee(pendingTransaction!))); + id: pendingTransaction!.id, + blockTime: DateTime.now(), + to: "", + from: "", + direction: TransactionDirection.outgoing, + amount: pendingTransaction!.amount, + isPending: true, + fee: pendingTransaction!.fee, + )); } if (walletType == WalletType.tron) { wallet.transactionHistory.addOne(tron!.getTransactionInfo( - id: pendingTransaction!.id, - blockTime: DateTime.now(), - tokenSymbol: selectedCryptoCurrency.title, - direction: TransactionDirection.outgoing, - tronAmount: selectedCryptoCurrency.parseAmount(outputs.first.cryptoAmount), - isPending: true, - txFee: CryptoCurrency.trx.parseAmount(outputs.first.estimatedFee).toInt())); + id: pendingTransaction!.id, + blockTime: DateTime.now(), + direction: TransactionDirection.outgoing, + amount: outputs.first.cryptoAmountMoney, + isPending: true, + fee: outputs.first.estimatedFee, + )); } if (pendingTransaction!.id.isNotEmpty) { @@ -1419,9 +1431,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return await evm!.createTokenApproval( wallet, - requiredAmount, + Money(requiredAmount, erc20Token), spender, - erc20Token, priority, useBlinkProtection: canSupportBlinkProtection(selectedChainId) ? _settingsStore.useBlinkProtection : false, diff --git a/lib/view_model/send/template_view_model.dart b/lib/view_model/send/template_view_model.dart index f3a7e0b13..b450c829b 100644 --- a/lib/view_model/send/template_view_model.dart +++ b/lib/view_model/send/template_view_model.dart @@ -23,8 +23,8 @@ abstract class TemplateViewModelBase with Store { _appStore = appStore, _fiatConversationStore = fiatConversationStore, _currency = wallet.currency, - output = Output(wallet, appStore, fiatConversationStore, () => wallet.currency) { - output = Output(_wallet, _appStore, _fiatConversationStore, () => _currency); + output = Output(wallet, appStore, fiatConversationStore, ([_]) => wallet.currency) { + output = Output(_wallet, _appStore, _fiatConversationStore, _outputCryptoCurrencyHandler); } @observable @@ -53,15 +53,20 @@ abstract class TemplateViewModelBase with Store { output.reset(); } - Template toTemplate({required String cryptoCurrency, required String fiatCurrency}) { - return Template( + Template toTemplate({required String cryptoCurrency, required String fiatCurrency}) => Template( isCurrencySelectedRaw: isCryptoSelected, nameRaw: name, addressRaw: address, cryptoCurrencyRaw: cryptoCurrency, fiatCurrencyRaw: fiatCurrency, amountRaw: output.cryptoAmount, - amountFiatRaw: output.fiatAmount); + amountFiatRaw: output.fiatAmount, + ); + + CryptoCurrency _outputCryptoCurrencyHandler([CryptoCurrency? override]) { + if (override != null && _currency != override) changeSelectedCurrency(override); + + return _currency; } @action diff --git a/lib/view_model/support_view_model.dart b/lib/view_model/support_view_model.dart index 052ab6478..96cf66fb9 100644 --- a/lib/view_model/support_view_model.dart +++ b/lib/view_model/support_view_model.dart @@ -56,11 +56,11 @@ abstract class SupportViewModelBase with Store { icon: 'assets/images/change_now.png', linkTitle: 'support@changenow.io', link: 'mailto:support@changenow.io'), - LinkListItem( - title: 'SideShift', - icon: 'assets/images/sideshift.png', - linkTitle: 'help.sideshift.ai', - link: 'https://help.sideshift.ai/en/'), + // LinkListItem( + // title: 'SideShift', + // icon: 'assets/images/sideshift.png', + // linkTitle: 'help.sideshift.ai', + // link: 'https://help.sideshift.ai/en/'), LinkListItem( title: 'SimpleSwap', icon: 'assets/images/simpleSwap.png', diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 6a38a45bb..7199ed1a9 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -79,22 +79,22 @@ class TxDetailRowDefinition { title: S.current.transaction_details_date, valueGetter: (vm) => DateFormatter.withCurrentLocal().format(vm.transactionInfo.date)), - TxDetailRowDefinition( - keyString: "standard_list_item_transaction_details_height_key", - title: S.current.transaction_details_height, - valueGetter: (vm) => vm.transactionInfo.height?.toString() ?? "", - applicable: (vm) => !([WalletType.solana, WalletType.tron].contains(vm.wallet.type) && - !isLightning(vm.transactionInfo))), - + keyString: "standard_list_item_transaction_details_height_key", + title: S.current.transaction_details_height, + valueGetter: (vm) => vm.transactionInfo.height?.toString() ?? "", + applicable: (vm) => + ![WalletType.solana, WalletType.tron].contains(vm.wallet.type) || + !isLightning(vm.transactionInfo), + ), TxDetailRowDefinition( keyString: "standard_list_item_transaction_details_fee_key", title: S.current.transaction_details_fee, - valueGetter: (vm) => vm.transactionInfo.feeFormatted()!, + valueGetter: (vm) => vm.transactionInfo.fee!.toStringWithSymbol(), applicable: (vm) => vm.wallet.type != WalletType.nano && - (vm.transactionInfo.feeFormatted() ?? "").isNotEmpty), + (vm.transactionInfo.fee?.toStringWithSymbol() ?? "").isNotEmpty), TxDetailRowDefinition( @@ -496,14 +496,15 @@ abstract class TransactionDetailsViewModelBase with Store { RBFListItems.add( StandartListItem( title: S.current.old_fee, - value: tx.feeFormatted() ?? '0.0', + value: tx.fee?.toStringWithSymbol() ?? '0.0', key: ValueKey('standard_list_item_rbf_old_fee_key'), ), ); if (transactionInfo.fee != null && rawTransaction.isNotEmpty) { final size = bitcoin!.getTransactionVSize(wallet, rawTransaction); - final recommendedRate = (transactionInfo.fee! / size).round() + 1; + final recommendedRate = (transactionInfo.fee! / BigInt.from(size)) + + transactionInfo.fee!.copyWith(amount: BigInt.one); RBFListItems.add( StandartListItem(title: 'New recommended fee rate', value: '$recommendedRate sat/byte')); @@ -609,14 +610,14 @@ abstract class TransactionDetailsViewModelBase with Store { if (wallet.type == WalletType.bitcoin) { final crypto = isLightning(transactionInfo) ? CryptoCurrency.btcln : CryptoCurrency.btc; final amount = _appStore.amountParsingProxy - .getDisplayCryptoString(transactionInfo.amount, crypto) + .asDisplayString(transactionInfo.amount) .withMaxDecimals(8) .withLocalSeperator(_appStore.settingsStore.languageCode); return '$amount ${_appStore.amountParsingProxy.getCryptoSymbol(crypto)}'; } - return transactionInfo.amountFormatted(); + return transactionInfo.amount.toStringWithSymbol(); } void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo, newFee); diff --git a/lib/wownero/cw_wownero.dart b/lib/wownero/cw_wownero.dart index 8ac6064dc..14f17a2bf 100644 --- a/lib/wownero/cw_wownero.dart +++ b/lib/wownero/cw_wownero.dart @@ -267,14 +267,14 @@ class CWWownero extends Wownero { WowneroTransactionCreationCredentials( outputs: outputs .map((out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount)) + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmountMoney, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + )) .toList(), priority: priority as MoneroTransactionPriority); diff --git a/lib/zano/cw_zano.dart b/lib/zano/cw_zano.dart index ac96cf804..d212d318e 100644 --- a/lib/zano/cw_zano.dart +++ b/lib/zano/cw_zano.dart @@ -70,23 +70,25 @@ class CWZano extends Zano { } @override - Object createZanoTransactionCredentials({required List outputs, required TransactionPriority priority, required CryptoCurrency currency}) { - return ZanoTransactionCredentials( - outputs: outputs - .map((out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount)) - .toList(), - priority: priority as MoneroTransactionPriority, - currency: currency, - ); - } + Object createZanoTransactionCredentials( + {required List outputs, + required TransactionPriority priority, + required CryptoCurrency currency}) => + ZanoTransactionCredentials( + outputs: outputs + .map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmountMoney, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + )) + .toList(), + priority: priority as MoneroTransactionPriority, + currency: currency, + ); @override double formatterIntAmountToDouble({required int amount, required CryptoCurrency currency, required bool forFee}) { diff --git a/lib/zcash/cw_zcash.dart b/lib/zcash/cw_zcash.dart index 1c9299640..9e4af1b29 100644 --- a/lib/zcash/cw_zcash.dart +++ b/lib/zcash/cw_zcash.dart @@ -43,23 +43,21 @@ class CWZcash extends Zcash { @override Object createZcashTransactionCredentials(List outputs, - {required CryptoCurrency currency, - int? feeRate, - TransactionPriority? priority}) { - final txPriority = (priority ?? getZcashTransactionPriorityAutomatic()) - as MoneroTransactionPriority; + {required CryptoCurrency currency, int? feeRate, TransactionPriority? priority}) { + final txPriority = + (priority ?? getZcashTransactionPriorityAutomatic()) as MoneroTransactionPriority; return ZcashTransactionCredentials( outputs: outputs .map((out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - memo: out.memo, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount)) + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmountMoney, + address: out.address, + note: out.note, + memo: out.memo, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + )) .toList(), priority: txPriority, currency: currency, @@ -90,7 +88,7 @@ class CWZcash extends Zcash { @override int formatterZcashParseAmount(String amount) { - return CryptoCurrency.zec.parseAmount(amount).toInt(); + return CryptoCurrency.zec.parseAmount(amount).amount.toInt(); } @override diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 436dbd209..125537f7b 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -40,6 +40,8 @@ "address_from_yat": "هذا العنوان من ${emoji} على Yat", "address_label": "تسمية العنوان", "address_label_explainer": "يمكنك الوصول إلى جميع عناوينك المستخدمة سابقًا وإدارتها بالضغط على «العناوين»", + "address_not_allowed_as_receive": "العنوان غير مسموح به كعنوان استلام", + "address_not_allowed_as_refund": "العنوان غير مسموح به كعنوان استرداد", "address_or_alias": "العنوان أو الاسم المستعار", "address_remove_contact": "إزالة جهة الاتصال", "address_remove_content": "هل أنت متأكد أنك تريد إزالة جهة الاتصال المحددة؟", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index d0cd89aaf..59d11edb6 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -40,6 +40,8 @@ "address_from_yat": "Този адрес е от ${emoji} в Yat", "address_label": "Етикет на адреса", "address_label_explainer": "Можете да получите достъп и да управлявате всички използвани преди адреси, като натиснете „Addresses“.", + "address_not_allowed_as_receive": "адресът не е разрешен като адрес за получаване", + "address_not_allowed_as_refund": "адресът не е разрешен като адрес за възстановяване на средства", "address_or_alias": "Адрес или псевдоним", "address_remove_contact": "Премахване на контакт", "address_remove_content": "Сигурни ли сте, че искате да премахнете избрания контакт?", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 2b1c944e9..c2c5c49c0 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -40,6 +40,8 @@ "address_from_yat": "Tato adresa je z ${emoji} na Yat", "address_label": "Štítek adresy", "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", + "address_not_allowed_as_receive": "adresa není povolena jako adresa pro příjem", + "address_not_allowed_as_refund": "address not allowed as refund address", "address_or_alias": "Adresa nebo alias", "address_remove_contact": "Odebrat kontakt", "address_remove_content": "Opravdu chcete odstranit vybraný kontakt?", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index eab227907..99bbb8b0d 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -40,6 +40,8 @@ "address_from_yat": "Diese Adresse stammt von ${emoji} auf Yat", "address_label": "Adressbezeichnung", "address_label_explainer": "Sie können auf alle Ihre zuvor verwendeten Adressen zugreifen und diese verwalten, indem Sie auf „Adressen“ tippen.", + "address_not_allowed_as_receive": "Adresse als Empfangsadresse nicht zulässig", + "address_not_allowed_as_refund": "Adresse als Rückerstattungsadresse nicht zulässig", "address_or_alias": "Adresse oder Alias", "address_remove_contact": "Kontakt entfernen", "address_remove_content": "Sind Sie sicher, dass Sie den ausgewählten Kontakt entfernen möchten?", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index b7f1832fd..1debd0a8f 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -40,6 +40,8 @@ "address_from_yat": "This address is from ${emoji} on Yat", "address_label": "Address label", "address_label_explainer": "You can access and manage all your previously used addressses by pressing Addresses", + "address_not_allowed_as_receive": "address not allowed as receive address", + "address_not_allowed_as_refund": "address not allowed as refund address", "address_or_alias": "Address or alias", "address_remove_contact": "Remove contact", "address_remove_content": "Are you sure that you want to remove selected contact?", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index aef5ee421..3b5560617 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -40,6 +40,8 @@ "address_from_yat": "Esta dirección proviene de ${emoji} en Yat", "address_label": "Etiqueta de la dirección", "address_label_explainer": "Puede acceder y gestionar todas las direcciones que ha usado anteriormente pulsando Direcciones", + "address_not_allowed_as_receive": "dirección no permitida como dirección de recepción", + "address_not_allowed_as_refund": "dirección no permitida como dirección de reembolso", "address_or_alias": "Dirección o alias", "address_remove_contact": "Eliminar contacto", "address_remove_content": "¿Seguro que deseas eliminar el contacto seleccionado?", diff --git a/res/values/strings_fa.arb b/res/values/strings_fa.arb index a53c84513..40b83e118 100644 --- a/res/values/strings_fa.arb +++ b/res/values/strings_fa.arb @@ -40,6 +40,8 @@ "address_from_yat": "این آدرس از ${emoji} در Yat است", "address_label": "برچسب آدرس", "address_label_explainer": "می‌توانید با زدن Addresses به همه آدرس‌هایی که قبلاً استفاده کرده‌اید دسترسی داشته باشید و آن‌ها را مدیریت کنید", + "address_not_allowed_as_receive": "آدرس به عنوان آدرس دریافت مجاز نیست", + "address_not_allowed_as_refund": "آدرس به عنوان آدرس بازپرداخت مجاز نیست", "address_or_alias": "آدرس یا نام مستعار", "address_remove_contact": "حذف مخاطب", "address_remove_content": "آیا مطمئن هستید که می‌خواهید مخاطب انتخاب‌شده را حذف کنید؟", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 7ba98d5d0..5327d2029 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -40,6 +40,8 @@ "address_from_yat": "Cette adresse provient de ${emoji} sur Yat", "address_label": "Libellé de l'adresse", "address_label_explainer": "Vous pouvez accéder à toutes vos adresses déjà utilisées et les gérer en appuyant sur Adresses", + "address_not_allowed_as_receive": "adresse non autorisée comme adresse de réception", + "address_not_allowed_as_refund": "adresse non autorisée comme adresse de remboursement", "address_or_alias": "Adresse ou alias", "address_remove_contact": "Supprimer le contact", "address_remove_content": "Êtes-vous sûr de vouloir supprimer le contact sélectionné ?", diff --git a/res/values/strings_gn.arb b/res/values/strings_gn.arb index 1fe8650d8..693fe80b0 100644 --- a/res/values/strings_gn.arb +++ b/res/values/strings_gn.arb @@ -35,6 +35,8 @@ "address_from_yat": "Ko dirección ou ${emoji} rehegua Yat-pe", "address_label": "Kundaharape reramoĩ", "address_label_explainer": "Ikatu reike ha remboguata opaite ne kundaha reipuruva’ekue eikutu hag̃ua Addresses.", + "address_not_allowed_as_receive": "dirección ndojehejáiva dirección de recepción ramo", + "address_not_allowed_as_refund": "dirección ndojehejáiva dirección de reembolso ramo", "address_or_alias": "Kundaharape térã alias", "address_remove_contact": "Emboguete hag̃ua pe contacto", "address_remove_content": "¿Eime hag̃ua remboguese pe contacto ojeporavóva?", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 63f7859d2..b4c142360 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -40,6 +40,8 @@ "address_from_yat": "Wannan adireshi daga ${emoji} ne a Yat", "address_label": "Alamar adireshi", "address_label_explainer": "Za ka iya samun dama da sarrafa duk adireshoshin da ka yi amfani da su a baya ta danna Addresses", + "address_not_allowed_as_receive": "adireshin da ba a yarda da adireshin karɓa ba", + "address_not_allowed_as_refund": "adireshin da ba a yarda a matsayin adireshin mayar da kuɗi ba", "address_or_alias": "Adireshi ko laƙabi", "address_remove_contact": "Cire tuntuɓa", "address_remove_content": "Kuna da tabbacin kuna so ku cire wannan lambar tuntuɓa da aka zaɓa?", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 31b053868..6f21d5d86 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -40,6 +40,8 @@ "address_from_yat": "यह पता Yat पर ${emoji} से है", "address_label": "पता लेबल", "address_label_explainer": "आप \"Addresses\" दबाकर अपने पहले उपयोग किए गए सभी पतों तक पहुंच सकते हैं और उन्हें प्रबंधित कर सकते हैं।", + "address_not_allowed_as_receive": "प्राप्त पते के रूप में पते की अनुमति नहीं है", + "address_not_allowed_as_refund": "धनवापसी पते के रूप में पते की अनुमति नहीं है", "address_or_alias": "पता या उपनाम", "address_remove_contact": "संपर्क हटाएँ", "address_remove_content": "क्या आप वाकई चयनित संपर्क को हटाना चाहते हैं?", @@ -732,8 +734,8 @@ "payjoin_unavailable_sheet_title": "Payjoin उपलब्ध क्यों नहीं है?", "payment_id": "भुगतान आईडी: ", "payment_made_easy": "भुगतान करना आसान", - "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।", "payment_was_received": "आपका भुगतान प्राप्त हो गया।", + "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।", "payments": "भुगतान", "pending": " (लंबित)", "percentageOf": "${amount} का", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 5d0634486..eb66c8e61 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -40,6 +40,8 @@ "address_from_yat": "Ova adresa je od ${emoji} na Yat-u", "address_label": "Oznaka adrese", "address_label_explainer": "Možete pristupiti svim prethodno korištenim adresama i upravljati njima pritiskom na Adrese", + "address_not_allowed_as_receive": "adresa nije dopuštena kao adresa primanja", + "address_not_allowed_as_refund": "adresa nije dopuštena kao adresa za povrat novca", "address_or_alias": "Adresa ili alias", "address_remove_contact": "Ukloni kontakt", "address_remove_content": "Jeste li sigurni da želite ukloniti odabrani kontakt?", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index f4c20d038..c81c4e463 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -40,6 +40,8 @@ "address_from_yat": "Այս հասցեն ստացվել է Yat-ում ${emoji}-ից", "address_label": "Հասցեի պիտակ", "address_label_explainer": "Դուք կարող եք մուտք գործել և կառավարել ձեր նախկինում օգտագործված բոլոր հասցեները՝ սեղմելով «Հասցեներ»", + "address_not_allowed_as_receive": "հասցեն չի թույլատրվում որպես ստացման հասցե", + "address_not_allowed_as_refund": "հասցեն չի թույլատրվում որպես վերադարձի հասցե", "address_or_alias": "Հասցե կամ այլանուն", "address_remove_contact": "Հեռացնել կոնտակտը", "address_remove_content": "Վստա՞հ եք, որ ցանկանում եք հեռացնել ընտրված կոնտակտը:", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 69e1c02f2..877fac35f 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -40,6 +40,8 @@ "address_from_yat": "Alamat ini berasal dari ${emoji} di Yat", "address_label": "Label alamat", "address_label_explainer": "Anda dapat mengakses dan mengelola semua alamat yang pernah Anda gunakan dengan menekan Alamat", + "address_not_allowed_as_receive": "alamat tidak diperbolehkan sebagai alamat terima", + "address_not_allowed_as_refund": "alamat tidak diperbolehkan sebagai alamat pengembalian dana", "address_or_alias": "Alamat atau alias", "address_remove_contact": "Hapus kontak", "address_remove_content": "Apakah Anda yakin ingin menghapus kontak yang dipilih?", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 231065c47..03da71fc5 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -40,6 +40,8 @@ "address_from_yat": "Questo indirizzo proviene da ${emoji} su Yat", "address_label": "Etichetta dell'indirizzo", "address_label_explainer": "Puoi accedere e gestire tutti gli indirizzi utilizzati in precedenza premendo Indirizzi", + "address_not_allowed_as_receive": "indirizzo non consentito come indirizzo di ricezione", + "address_not_allowed_as_refund": "indirizzo non consentito come indirizzo di rimborso", "address_or_alias": "Indirizzo o alias", "address_remove_contact": "Rimuovi contatto", "address_remove_content": "Sei sicuro di voler rimuovere il contatto selezionato?", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index ae37cf69f..87f1193ca 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -40,6 +40,8 @@ "address_from_yat": "このアドレスはYatの${emoji}からのものです", "address_label": "アドレスラベル", "address_label_explainer": "「アドレス」を押すと、これまでに使用したすべてのアドレスにアクセスして管理できます。", + "address_not_allowed_as_receive": "アドレスは受信アドレスとして許可されません", + "address_not_allowed_as_refund": "住所は払い戻し住所として使用できません", "address_or_alias": "アドレスまたはエイリアス", "address_remove_contact": "連絡先を削除", "address_remove_content": "選択した連絡先を削除してもよろしいですか?", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 60575472a..b756745bb 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -40,6 +40,8 @@ "address_from_yat": "이 주소는 Yat의 ${emoji}에서 가져온 것입니다", "address_label": "주소 라벨", "address_label_explainer": "\"주소\"를 눌러 이전에 사용한 모든 주소에 액세스하고 관리할 수 있습니다.", + "address_not_allowed_as_receive": "주소는 수신 주소로 허용되지 않습니다.", + "address_not_allowed_as_refund": "주소는 환불 주소로 허용되지 않습니다.", "address_or_alias": "주소 또는 별칭", "address_remove_contact": "연락처 삭제", "address_remove_content": "선택한 연락처를 정말로 삭제하시겠습니까?", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 65a356e7c..51f7c231f 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -40,6 +40,8 @@ "address_from_yat": "ဤလိပ်စာသည် Yat ပေါ်ရှိ ${emoji} မှ ဖြစ်သည်။", "address_label": "လိပ်စာ အညွှန်း", "address_label_explainer": "Addresses ကို နှိပ်ခြင်းဖြင့် သင် ယခင်အသုံးပြုခဲ့သည့် လိပ်စာများအားလုံးကို ဝင်ရောက်ကြည့်ရှု၍ စီမံခန့်ခွဲနိုင်ပါသည်။", + "address_not_allowed_as_receive": "လက်ခံလိပ်စာအဖြစ် ခွင့်မပြုပါ။", + "address_not_allowed_as_refund": "လိပ်စာကို ပြန်အမ်းငွေလိပ်စာအဖြစ် ခွင့်မပြုပါ။", "address_or_alias": "လိပ်စာ သို့မဟုတ် အမည်ပြောင်", "address_remove_contact": "အဆက်အသွယ်ကို ဖယ်ရှားပါ", "address_remove_content": "ရွေးချယ်ထားသော အဆက်အသွယ်ကို ဖယ်ရှားလိုသည်မှာ သေချာပါသလား။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 6f94d5ab3..9285015d8 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -40,6 +40,8 @@ "address_from_yat": "Dit adres is van ${emoji} op Yat", "address_label": "Adreslabel", "address_label_explainer": "Je kunt al je eerder gebruikte adressen openen en beheren door op Adressen te drukken", + "address_not_allowed_as_receive": "adres niet toegestaan ​​als ontvangstadres", + "address_not_allowed_as_refund": "adres niet toegestaan ​​als restitutieadres", "address_or_alias": "Adres of alias", "address_remove_contact": "Contact verwijderen", "address_remove_content": "Weet je zeker dat je het geselecteerde contact wilt verwijderen?", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 70fc17d89..16045cd4b 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -40,6 +40,8 @@ "address_from_yat": "Ten adres pochodzi z ${emoji} na Yat", "address_label": "Etykieta adresu", "address_label_explainer": "Możesz uzyskać dostęp do wszystkich wcześniej używanych adresów i zarządzać nimi, naciskając Adresy", + "address_not_allowed_as_receive": "adres nie jest dozwolony jako adres odbiorczy", + "address_not_allowed_as_refund": "adres nie jest dozwolony jako adres zwrotu", "address_or_alias": "Adres lub alias", "address_remove_contact": "Usuń kontakt", "address_remove_content": "Czy na pewno chcesz usunąć wybrany kontakt?", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 3e451aa56..5ef972b66 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -40,6 +40,8 @@ "address_from_yat": "Este endereço é do ${emoji} no Yat", "address_label": "Rótulo do endereço", "address_label_explainer": "Você pode acessar e gerenciar todos os seus endereços usados anteriormente pressionando Endereços", + "address_not_allowed_as_receive": "endereço não permitido como endereço de recebimento", + "address_not_allowed_as_refund": "endereço não permitido como endereço de reembolso", "address_or_alias": "Endereço ou alias", "address_remove_contact": "Remover contato", "address_remove_content": "Tem certeza de que deseja remover o contato selecionado?", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index fd65d4f4f..aaac422dd 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -40,6 +40,8 @@ "address_from_yat": "Этот адрес из ${emoji} на Yat", "address_label": "Метка адреса", "address_label_explainer": "Вы можете получить доступ ко всем ранее использованным адресам и управлять ими, нажав «Адреса».", + "address_not_allowed_as_receive": "адрес не разрешен в качестве адреса получения", + "address_not_allowed_as_refund": "адрес не разрешен в качестве адреса возврата", "address_or_alias": "Адрес или псевдоним", "address_remove_contact": "Удалить контакт", "address_remove_content": "Вы уверены, что хотите удалить выбранный контакт?", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index b8c3c6d53..669153910 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -40,6 +40,8 @@ "address_from_yat": "ที่อยู่นี้มาจาก ${emoji} บน Yat", "address_label": "ป้ายกำกับที่อยู่", "address_label_explainer": "คุณสามารถเข้าถึงและจัดการที่อยู่ที่เคยใช้ทั้งหมดได้โดยกดที่ “Addresses”", + "address_not_allowed_as_receive": "ไม่อนุญาตให้ใช้ที่อยู่เป็นที่อยู่รับ", + "address_not_allowed_as_refund": "ที่อยู่ไม่ได้รับอนุญาตเป็นที่อยู่คืนเงิน", "address_or_alias": "ที่อยู่หรือชื่อแทน", "address_remove_contact": "ลบผู้ติดต่อ", "address_remove_content": "คุณแน่ใจหรือไม่ว่าต้องการลบผู้ติดต่อที่เลือก?", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index c6bdadb11..8e23566a4 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -40,6 +40,8 @@ "address_from_yat": "Ang address na ito ay mula sa ${emoji} sa Yat", "address_label": "Label ng address", "address_label_explainer": "Maaari mong i-access at pamahalaan ang lahat ng dati mong ginamit na address sa pamamagitan ng pagpindot sa Mga Address", + "address_not_allowed_as_receive": "hindi pinapayagan ang address bilang address ng pagtanggap", + "address_not_allowed_as_refund": "hindi pinapayagan ang address bilang address ng refund", "address_or_alias": "Address o alyas", "address_remove_contact": "Alisin ang contact", "address_remove_content": "Sigurado ka bang gusto mong alisin ang napiling contact?", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index a4c08464c..beb1e6f02 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -40,6 +40,8 @@ "address_from_yat": "Bu adres, Yat'taki ${emoji} üzerinden alınmıştır", "address_label": "Adres etiketi", "address_label_explainer": "Adresler’e basarak daha önce kullandığınız tüm adreslere erişebilir ve bunları yönetebilirsiniz.", + "address_not_allowed_as_receive": "Adresin alma adresi olarak kullanılmasına izin verilmiyor", + "address_not_allowed_as_refund": "adresin geri ödeme adresi olarak kullanılmasına izin verilmiyor", "address_or_alias": "Adres veya takma ad", "address_remove_contact": "Kişiyi kaldır", "address_remove_content": "Seçili kişiyi kaldırmak istediğinizden emin misiniz?", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 885e53c03..53fee6efb 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -40,6 +40,8 @@ "address_from_yat": "Ця адреса з ${emoji} на Yat", "address_label": "Мітка адреси", "address_label_explainer": "Ви можете отримати доступ до всіх раніше використаних адрес і керувати ними, натиснувши «Адреси».", + "address_not_allowed_as_receive": "адреса не допускається як адреса отримання", + "address_not_allowed_as_refund": "адреса не допускається як адреса повернення коштів", "address_or_alias": "Адреса або псевдонім", "address_remove_contact": "Видалити контакт", "address_remove_content": "Ви впевнені, що хочете видалити вибраний контакт?", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index a4191a25e..b28375292 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -40,6 +40,8 @@ "address_from_yat": "یہ پتہ Yat پر ${emoji} سے ہے۔", "address_label": "ایڈریس لیبل", "address_label_explainer": "آپ \"Addresses\" دباکر اپنے پہلے استعمال شدہ تمام پتوں تک رسائی حاصل کر سکتے ہیں اور ان کا نظم کر سکتے ہیں", + "address_not_allowed_as_receive": "ایڈریس موصول ہونے کی اجازت نہیں ہے۔", + "address_not_allowed_as_refund": "ریفنڈ ایڈریس کے طور پر ایڈریس کی اجازت نہیں ہے۔", "address_or_alias": "ایڈریس یا عرف", "address_remove_contact": "رابطہ حذف کریں", "address_remove_content": "کیا آپ واقعی منتخب رابطہ حذف کرنا چاہتے ہیں؟", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 987c0c8e4..c5eb9647b 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -40,6 +40,8 @@ "address_from_yat": "Địa chỉ này lấy từ ${emoji} trên Yat", "address_label": "Nhãn địa chỉ", "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ỉ", + "address_not_allowed_as_receive": "địa chỉ không được phép làm địa chỉ nhận", + "address_not_allowed_as_refund": "địa chỉ không được phép làm địa chỉ hoàn tiền", "address_or_alias": "Địa chỉ hoặc bí danh", "address_remove_contact": "Xóa liên hệ", "address_remove_content": "Bạn có chắc chắn muốn xóa liên hệ đã chọn không?", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index f2af545d0..089e19207 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -40,6 +40,8 @@ "address_from_yat": "Àdírẹ́sì yìí wá láti ${emoji} lórí Yat", "address_label": "Aami adirẹsi", "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", + "address_not_allowed_as_receive": "adirẹsi ko gba ọ laaye bi gbigba adirẹsi", + "address_not_allowed_as_refund": "adiresi ko gba laaye bi adirẹsi agbapada", "address_or_alias": "Adirẹsi tàbí orúkọ-àpèjọ", "address_remove_contact": "Yọ olùbánisọ̀rọ̀ kúrò", "address_remove_content": "Ṣé ó dá ẹ lójú pé ẹ fẹ́ yọ ẹni ìbánisọ̀rọ̀ tí ẹ yàn kúrò?", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index c8db4e020..19a2e9a70 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -40,6 +40,8 @@ "address_from_yat": "此地址来自 Yat 上的 ${emoji}", "address_label": "地址标签", "address_label_explainer": "您可以点击“地址”来访问和管理您之前使用过的所有地址", + "address_not_allowed_as_receive": "地址不允许作为接收地址", + "address_not_allowed_as_refund": "地址不允许作为退款地址", "address_or_alias": "地址或别名", "address_remove_contact": "移除联系人", "address_remove_content": "您确定要移除所选联系人吗?", diff --git a/tool/configure.dart b/tool/configure.dart index a16d88f21..54febefbb 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -103,6 +103,7 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/hardware/hardware_wallet_service.dart'; import 'package:cw_core/node.dart'; @@ -213,12 +214,11 @@ abstract class Bitcoin { List getSilentPaymentAddresses(Object wallet); List getSilentPaymentReceivedAddresses(Object wallet); - Future estimateFakeSendAllTxAmount(Object wallet, TransactionPriority priority, + Future estimateFakeSendAllTxAmount(WalletBase wallet, TransactionPriority priority, {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}); List getSubAddresses(Object wallet); String formatterBitcoinAmountToString({required int amount}); - double formatterBitcoinAmountToDouble({required int amount}); int formatterStringDoubleToBitcoinAmount(String amount); String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate, {int? customRate}); @@ -320,6 +320,8 @@ abstract class Bitcoin { Future generateMonero(bool hasImplementation) async { final outputFile = File(moneroOutputPath); const moneroCommonHeaders = """ +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:mobx/mobx.dart'; @@ -388,30 +390,17 @@ class Subaddress { } class MoneroBalance extends Balance { - MoneroBalance({required this.fullBalance, required this.unlockedBalance}) - : formattedFullBalance = monero!.formatterMoneroAmountToString(amount: fullBalance), - formattedUnlockedBalance = - monero!.formatterMoneroAmountToString(amount: unlockedBalance), - super.fromInt(unlockedBalance, fullBalance); + MoneroBalance({ + required this.fullBalance, + required Money unlockedBalance, + Money? frozen, + }) : super( + unlockedBalance, + fullBalance - unlockedBalance, + frozen: frozen ?? Money.zero(CryptoCurrency.xmr), + ); - MoneroBalance.fromString( - {required this.formattedFullBalance, - required this.formattedUnlockedBalance}) - : fullBalance = monero!.formatterMoneroParseAmount(amount: formattedFullBalance), - unlockedBalance = monero!.formatterMoneroParseAmount(amount: formattedUnlockedBalance), - super.fromInt(monero!.formatterMoneroParseAmount(amount: formattedUnlockedBalance), - monero!.formatterMoneroParseAmount(amount: formattedFullBalance)); - - final int fullBalance; - final int unlockedBalance; - final String formattedFullBalance; - final String formattedUnlockedBalance; - - @override - String get formattedAvailableBalance => formattedUnlockedBalance; - - @override - String get formattedAdditionalBalance => formattedFullBalance; + final Money fullBalance; } abstract class MoneroWalletDetails { @@ -452,7 +441,7 @@ abstract class Monero { Map exportOutputsUR(Object wallet); - bool needExportOutputs(Object wallet, int amount); + bool needExportOutputs(Object wallet, Money amount); bool importKeyImagesUR(Object wallet, String ur); @@ -535,6 +524,8 @@ abstract class MoneroAccountList { Future generateWownero(bool hasImplementation) async { final outputFile = File(wowneroOutputPath); const wowneroCommonHeaders = """ +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:mobx/mobx.dart'; @@ -600,30 +591,17 @@ class Subaddress { } class WowneroBalance extends Balance { - WowneroBalance({required this.fullBalance, required this.unlockedBalance}) - : formattedFullBalance = wownero!.formatterWowneroAmountToString(amount: fullBalance), - formattedUnlockedBalance = - wownero!.formatterWowneroAmountToString(amount: unlockedBalance), - super.fromInt(unlockedBalance, fullBalance); + WowneroBalance({ + required this.fullBalance, + required Money unlockedBalance, + Money? frozen, + }) : super( + unlockedBalance, + fullBalance - unlockedBalance, + frozen: frozen ?? Money.zero(CryptoCurrency.wow), + ); - WowneroBalance.fromString( - {required this.formattedFullBalance, - required this.formattedUnlockedBalance}) - : fullBalance = wownero!.formatterWowneroParseAmount(amount: formattedFullBalance), - unlockedBalance = wownero!.formatterWowneroParseAmount(amount: formattedUnlockedBalance), - super.fromInt(wownero!.formatterWowneroParseAmount(amount: formattedUnlockedBalance), - wownero!.formatterWowneroParseAmount(amount: formattedFullBalance)); - - final int fullBalance; - final int unlockedBalance; - final String formattedFullBalance; - final String formattedUnlockedBalance; - - @override - String get formattedAvailableBalance => formattedUnlockedBalance; - - @override - String get formattedAdditionalBalance => formattedFullBalance; + final Money fullBalance; } abstract class WowneroWalletDetails { @@ -860,7 +838,6 @@ abstract class Nano { Object createNanoTransactionCredentials(List outputs); Future changeRep(Object wallet, String address); Future updateTransactions(Object wallet); - BigInt getTransactionAmountRaw(TransactionInfo transactionInfo); String getRepresentative(Object wallet); Future> getN2Reps(Object wallet); bool isRepOk(Object wallet); @@ -929,6 +906,7 @@ Future generateSolana(bool hasImplementation) async { const solanaCommonHeaders = """ import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/exchange/provider/jupiter_exchange_provider.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -995,10 +973,9 @@ abstract class Solana { Future getSPLToken(WalletBase wallet, String contractAddress); CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); - double getTransactionAmountRaw(TransactionInfo transactionInfo); String getTokenAddress(CryptoCurrency asset); List? getValidationLength(CryptoCurrency type); - double? getEstimateFees(WalletBase wallet); + Money? getEstimateFees(WalletBase wallet); List getDefaultSPLTokens(); List getDefaultTokenContractAddresses(); List getDefaultTokenSymbols(); @@ -1011,8 +988,8 @@ abstract class Solana { String base64Transaction, String requestId, String destinationAddress, - double amount, - double fee, + Money amount, + Money fee, ); // Fast transaction update after sending @@ -1035,22 +1012,16 @@ abstract class Solana { Future discoverAndAddWalletTokens(WalletBase wallet); - TransactionInfo getTransactionInfo( -{ + TransactionInfo getTransactionInfo({ required String id, required DateTime blockTime, required String to, required String from, - String? tokenSymbol, required TransactionDirection direction, - required double solAmount, + required Money amount, required bool isPending, - required double txFee, - } - ); - - double getPendingTransactionAmount(PendingTransaction tx); - double getPendingTransactionFee(PendingTransaction tx); + required Money fee, + }); } class JupiterSwapFailedException implements Exception { @@ -1093,6 +1064,7 @@ Future generateTron(bool hasImplementation) async { final outputFile = File(tronOutputPath); const tronCommonHeaders = """ import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/output_info.dart'; @@ -1102,7 +1074,6 @@ import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/tron_token.dart'; -import 'package:hive/hive.dart'; import 'package:cw_core/transaction_direction.dart'; """; @@ -1111,7 +1082,6 @@ import 'package:cw_evm/evm_chain_mnemonics.dart'; import 'package:cw_tron/tron_transaction_credentials.dart'; import 'package:cw_tron/tron_transaction_info.dart'; import 'package:cw_tron/tron_wallet_creation_credentials.dart'; -import 'package:cw_tron/pending_tron_transaction.dart'; import 'package:cw_tron/tron_client.dart'; import 'package:cw_tron/tron_wallet.dart'; @@ -1139,13 +1109,12 @@ abstract class Tron { Future deleteTronToken(WalletBase wallet, CryptoCurrency token); Future getTronToken(WalletBase wallet, String contractAddress); - double getTransactionAmountRaw(TransactionInfo transactionInfo); CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); String getTokenAddress(CryptoCurrency asset); String getTronBase58Address(String hexAddress, WalletBase wallet); - String? getTronNativeEstimatedFee(WalletBase wallet); - String? getTronTRC20EstimatedFee(WalletBase wallet); + Money? getTronNativeEstimatedFee(WalletBase wallet); + Money? getTronTRC20EstimatedFee(WalletBase wallet); void updateTronGridUsageState(WalletBase wallet, bool isEnabled); List getDefaultTronTokens(); @@ -1154,17 +1123,14 @@ abstract class Tron { bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); TransactionInfo getTransactionInfo({ required String id, - required BigInt tronAmount, - int? txFee, - String? tokenSymbol, + required Money amount, + Money? fee, required TransactionDirection direction, required DateTime blockTime, String? to, String? from, required bool isPending, }); - String getPendingTransactionFee(PendingTransaction tx); - String getPendingTransactionAmount(PendingTransaction tx); } """; @@ -1278,7 +1244,6 @@ import 'package:cw_decred/transaction_priority.dart'; import 'package:cw_decred/wallet.dart'; import 'package:cw_decred/wallet_service.dart'; import 'package:cw_decred/wallet_creation_credentials.dart'; -import 'package:cw_decred/amount_format.dart'; import 'package:cw_decred/transaction_credentials.dart'; import 'package:cw_decred/mnemonic.dart'; """; @@ -1305,10 +1270,6 @@ abstract class Decred { Future updateAddress(Object wallet, String address, String label); Future generateNewAddress(Object wallet, String label); - String formatterDecredAmountToString({required int amount}); - double formatterDecredAmountToDouble({required int amount}); - int formatterStringDoubleToDecredAmount(String amount); - List getUnspents(Object wallet); void updateUnspents(Object wallet); @@ -1395,6 +1356,7 @@ Future generateEVM(bool hasImplementation) async { import 'dart:math' as math; import 'package:cake_wallet/core/utilities.dart'; import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/amount/money.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; @@ -1519,11 +1481,6 @@ abstract class EVM { }); int formatterEVMParseAmount(String amount); - double formatterEVMAmountToDouble({ - TransactionInfo? transaction, - BigInt? amount, - int exponent = 18, - }); List getERC20Currencies(WalletBase wallet); Future addErc20Token(WalletBase wallet, CryptoCurrency token); @@ -1535,7 +1492,6 @@ abstract class EVM { void updateScanProviderUsageState(WalletBase wallet, bool isEnabled); Web3Client? getWeb3Client(WalletBase wallet); String getTokenAddress(CryptoCurrency asset); - BigInt? getERC20AvailableBalance(Object balance); Future isApprovalRequired( WalletBase wallet, @@ -1551,9 +1507,8 @@ abstract class EVM { Future createTokenApproval( WalletBase wallet, - BigInt amount, + Money amount, String spender, - CryptoCurrency token, TransactionPriority? priority, {bool useBlinkProtection = true} ); @@ -1562,7 +1517,7 @@ abstract class EVM { WalletBase wallet, String to, String dataHex, - BigInt valueWei, + Money valueWei, TransactionPriority? priority, {bool useBlinkProtection = true, String? sourceTokenAddress, @@ -1584,10 +1539,12 @@ abstract class EVM { String? getEVMERC20EstimatedFee(WalletBase wallet); // Chain-specific integrations (optional, can be null for non-Ethereum chains) - Future? getDEuroSavingsBalance(WalletBase wallet) => null; - Future? getDEuroAccruedInterest(WalletBase wallet) => null; + Future? getDEuroSavingsBalance(WalletBase wallet) => null; + Future? getDEuroSavingsV1Balance(WalletBase wallet) => null; + Future? getDEuroAccruedInterest(WalletBase wallet) => null; Future? getDEuroInterestRate(WalletBase wallet) => null; Future? getDEuroSavingsApproved(WalletBase wallet) => null; + Future? withdrawDEuroSavingV1(WalletBase wallet, TransactionPriority priority) => null; Future? addDEuroSaving(WalletBase wallet, BigInt amount, TransactionPriority priority) => null; Future? removeDEuroSaving(WalletBase wallet, BigInt amount, TransactionPriority priority) => null; Future? reinvestDEuroInterest(WalletBase wallet, TransactionPriority priority) => null; @@ -1646,12 +1603,11 @@ abstract class EVM { TransactionPriority priority, ); - TransactionInfo getTransactionInfo( - { + TransactionInfo getTransactionInfo({ required String id, required int height, - required BigInt ethAmount, - required BigInt ethFee, + required Money amount, + required Money fee, required String tokenSymbol, int exponent = 18, required TransactionDirection direction, @@ -1663,10 +1619,7 @@ abstract class EVM { String? evmSignatureName, String? contractAddress, required int chainId, - } - ); - BigInt getPendingTransactionFee(PendingTransaction tx); - String getPendingTransactionAmount(PendingTransaction tx); + }); Future discoverAndAddWalletTokens(WalletBase wallet); }