Introduce Money class and refactor currency handling (#3157)

* feat: add `Money` class for precise currency handling and amount parsing

- Introduced `Money` class for handling money values with fixed-point precision.
- Added parsing and comparison methods to `CryptoCurrency` and `FiatCurrency` interfaces.
- Implemented `smartAmountSanitizer` for format normalization.

Includes extensive unit tests for validation.

* refactor: optimize Money class with new factories and string formatting improvements

* refactor: unify balance handling with `Money` refactor

* refactor: update `ElectrumBalance` and transaction logic to use `Money` for currency precision

* refactor: standardize balance handling with `Money` across `Monero`, `Wownero`, and `Zano` wallets

* refactor: migrate EVM balance and transaction handling to `Money` for consistent currency precision

* refactor: migrate Decred balance and transaction handling to `Money` for consistent currency precision

* refactor: migrate nano balance and transaction handling to `Money` for consistent currency precision

* refactor: migrate Solana balance and transaction logic to `Money` for precise currency handling

* refactor: migrate Tron balance and transaction handling to `Money` for consistent currency precision

* refactor: migrate Zcash balance and transaction handling to `Money` for consistent currency precision

* refactor: adjust lib to handle money

* refactor: remove legacy Decred amount formatting and migrate to `Money`

* refactor: migrate pending transaction amount and fee to `Money`

* refactor: migrate dEuro savings and transaction logic to `Money` for improved precision and consistency

* refactor: migrate Bitcoin and lightning transaction logic to `Money` for consistent currency precision

* refactor: migrate Wownero, EVM, Zano, and Zcash transaction logic to `Money` for consistent and precise currency handling

* refactor: standardize balance handling across wallets, migrate to `Money.zero` and refine `Balance` class for consistency

* refactor: migrate Solana transaction and wallet logic to `Money` for consistent and precise currency handling

* refactor: improve fiat balance and formatting for consistency on dashboard

* fix: fiat input mode switching and standardize crypto amount display logic

* fix: cw_evm generator

* refactor: migrate exchange amount handling to `Money` for improved precision and consistency

* test: remove redundant and commented-out tests, tidy test cases, and apply minor formatting adjustments

* fix using Money wrapper in BridgeViewModel and USDT0Service

* refactor: streamline `AmountConverter` and `LightningWallet`, add `cw_core` tests to GitHub workflow, and remove unused/deprecated logic

* refactor: enhance `changeReceiveAmount` with `isCanonical` support, improve null handling, and refine amount parsing logic

* minor fix

* try/catch the lightning log file error

* feat: add copy-to-clipboard functionality for transaction amounts, fix typo in trailing widget method name (#3219)

* Cw 1234 disallow sp and mweb addresses as refund for swaps (#3187)

* disallow SP and MWEB for swaps

* Simplify address selection in addressForExchange

Refactor addressForExchange method to simplify address selection logic.

* validate refund and receive address types

* localize address validation messages

* minor ui fix [skip ci]

* minor fix [skip ci]

* Revert "localize address validation messages"

* fix translations

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* refactor: replace manual amount sanitization with `sanitized()` extension across the codebase, add unit tests for `AmountSanitizer`

* fix: correct `sanitized()` test expectations for decimal handling in `amount_sanitizer_test.dart`

* refactor: fee display logic and append currency symbol in `send_confirm_sheet.dart`

* refactor: simplify `ZcashBalance` class by replacing custom fields with inherited properties, update references accordingly

* refactor: replace hardcoded USDT contract address with `DefaultTronTokens` lookup, improve maintainability

* refactor: replace `currency` with `inputAmount.currency` in Solana transaction signing methods to avoid inconsistency

* fix: handle null `fee` in `electrum_transaction_info.dart` to prevent potential runtime errors

* refactor: replace raw fee and amount handling with `Money` object for improved consistency and maintainability

* fix: limit fiat balance display to 2 decimal places in `balance_view_model.dart`

* refactor: replace raw `amount` and `fee` types with `Money` in Solana transaction methods for consistency and precision

* refactor: replace raw `amount` and `fee` handling with `Money` and improve balance calculation consistency across view models

* refactor: replace raw balance strings with `Money` and simplify `UnconfirmedBalanceModal` widget logic

* refactor: replace `print` with `printV` for better logging, add equality and hashCode overrides to `ERC20Currency` for object comparison

* feat: prepare deuro savings migration

* feat: add V1 savings migration support and improve formatting consistency in DEuro view model

* feat: add V1 savings withdrawal support and update associated UI components and logic

* refactor: replace `Image.asset` with `CakeImageWidget`, update balance string handling, and streamline address validation logic

* refactor: use `Money.tryParse` with fallback to `Money.zero` in `NanoBalance.fromRawString` for safer balance parsing

* refactor: replace `ERC20Currency` with `Erc20Token` for consistency and remove unused `evm_erc20_currency.dart` file

* refactor: add computed `hasDepositAmount` for deposit null-checks, optimize balance handling in `zano_wallet`, update Breez SDK to v0.14.0, and implement equality/hashCode for `Currency`

* refactor: rename `amountSats` to `amount` in `PrepareLnurlPayRequest` for consistency

* revert: downgrade Breez SDK to v0.11.0 in `pubspec.yaml` and update relevant references in `pubspec.lock`

* fix: use `cryptoAmount.copyWith` for currency consistency and replace `Image.asset` with `CakeImageWidget` in swap address modal

* fix: ensure currency consistency by using `cryptoAmount.copyWith` in Solana and EVM wallet transactions

* refactor: simplify CSV row generation logic by removing `_splitAmountCurrency` and streamlining amount/fee handling

* fix: LateInitializationError Field 'sdk' has not been initialized

* fix: use `Money.tryParse` for safer parsing of deposit and receive amounts

* chore: remove debug print statement

* refactor: update transaction processing to consistently use `Money` for amounts and fees

* refactor: improve currency and amount handling, simplify logic, and enhance code readability in Send and Swap pages

* refactor: integrate `DecimalInputFormatter` for consistent decimal input across Send and Swap pages, simplify input handling logic

* fix: "fiatMode" per output on sendPage

* refactor: make transaction_details_height applicable more readable

* fix: use correct base unit for deposit

* refactor: use Money in estimateFakeSendAllTxAmount method

* fix decred frozen balance
minor fixes

* fix decred frozen balance
minor fixes

* fix: automatically switch to lightning if lnurl or invoice detected

* fix: getBolt11Amount test cases

* fix: standardise crypto amount formatting and display

* fix: use base unit for Tron transaction fee parsing
* refactor: hardcode fractional digits to 8 in `AmountParsingProxy` display methods
* refactor: ensure consistent 8-decimal precision for fee formatting in `PendingTransaction`
* fix: use `AmountParsingProxy` for available balance display in exchange view model
* fix: improve amount formatting and layout in swap and send confirmation sheets
* chore: remove redundant decimal truncation in balance view model and send confirmation sheet

* fix: allow for editing text field if it is filled with "ALL"

* fix: allow for editing text field if it is filled with "ALL"

* fix: update currency detection logic for lightning mode

* avoid potential infinite loop [skip ci]

* fix: getDisplayCryptoAmount

* fix: update estimated fee calculation

* chore: simplify fee calculation logic [skip-ci]

* fix value display in hidden mode

* fix stale state in send confirm sheet

* fix decimals for mweb

* fix available balance display for sol/trx on swap

* restore proper hint text

* fix swap receive amount calculation for xno

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
Co-authored-by: Serhii <17529954+serhii-bor@users.noreply.github.com>
Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
This commit is contained in:
Konstantin Ullrich
2026-06-30 02:45:44 +03:00
committed by GitHub
co-authored by Omar Hatem Serhii Robert Malikowski
parent 8768aa8995
commit 75f3fe735b
178 changed files with 4238 additions and 3560 deletions
@@ -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
+20 -19
View File
@@ -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,
+18 -5
View File
@@ -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;
}
}
+32 -29
View File
@@ -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,
});
}
+17 -29
View File
@@ -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<String>? inputAddresses,
List<String>? 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<dynamic>? ?? [];
final outputAddresses = data['outputAddresses'] as List<dynamic>? ?? [];
final unspents = data['unspents'] as List<dynamic>? ?? [];
final additionalInfo = data['additionalInfo'] as Map<String, dynamic>?;
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<String, dynamic>))
.toList(),
isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false,
additionalInfo: data['additionalInfo'] as Map<String, dynamic>?,
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 = <String, dynamic>{};
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;
+91 -86
View File
@@ -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<EstimatedTxResult> estimateTxForAmount(
int credentialsAmount,
Money credentialsAmount,
List<BitcoinOutput> outputs,
List<BitcoinOutput> 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<int> _maxSpendableNoChangeAmount({
Future<Money> _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<int> 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<int>(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<PendingBitcoinTransaction> replaceByFee(String hash, int newFee) async {
@@ -2195,14 +2203,14 @@ abstract class ElectrumWalletBase
int totalOutAmount =
outputs.fold<int>(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<void> _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<void> _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<UtxoWithAddress> utxos;
final List<ECPrivateInfo> inputPrivKeyInfos;
final Map<String, PublicKeyWithDerivationPath> publicKeys; // PubKey to derivationPath
final int fee;
final int amount;
final Money fee;
final Money amount;
final bool spendsSilentPayment;
// final bool sendsToSilentPayment;
+20 -3
View File
@@ -170,9 +170,22 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@computed
List<BitcoinAddressRecord> 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 = <BitcoinAddressRecord>[
...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;
}
+11 -3
View File
@@ -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};
+53 -27
View File
@@ -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<SdkEvent>? _eventSubscription;
Stream<SdkEvent>? _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<void> 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<BigInt> getBalance() async {
Future<Money> 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<String> 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},
@@ -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<String> Function() commitOverride;
final List<void Function()> _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;
+23 -36
View File
@@ -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<void> 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<String> inputAddresses = {};
var amount = 0;
var inputAddresses = <String>{};
var output = convert.AccumulatorSink<Digest>();
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,
+14 -6
View File
@@ -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
+28 -51
View File
@@ -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<void> _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<String> 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"),
});
}
}
@@ -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 = <void Function(ElectrumTransactionInfo transaction)>[];
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<void Function(ElectrumTransactionInfo transaction)> _listeners;
@override
Future<void> 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<Map<String, String>> commitUR() {
throw UnimplementedError();
}
}
+16
View File
@@ -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);
}
+211
View File
@@ -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<Money> {
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;
}
}
-74
View File
@@ -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';
+14 -20
View File
@@ -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;
}
+25 -13
View File
@@ -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<int> with Serializable<int> implements Currency {
const CryptoCurrency({
@@ -21,18 +21,30 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> 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<String> 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<int> with Serializable<int> 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);
}
+17 -1
View File
@@ -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;
}
-2
View File
@@ -1,5 +1,3 @@
import 'package:flutter/foundation.dart';
abstract class EnumerableItem<T> {
const EnumerableItem({required this.title, required this.raw});
+1 -1
View File
@@ -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;
+1 -7
View File
@@ -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;
+6 -4
View File
@@ -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<int?> getPayRequestAmount(String lnurl) async {
static Future<Money?> 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;
}
-17
View File
@@ -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;
}
+19 -18
View File
@@ -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<String, dynamic> extra;
}
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<String, dynamic> extra;
}
+1 -1
View File
@@ -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);
+8 -9
View File
@@ -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<void> commit();
+8 -6
View File
@@ -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<String, dynamic> additionalInfo = {};
String? _fiatAmount;
String fiatAmount() => _fiatAmount ?? '';
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
}
-2
View File
@@ -43,8 +43,6 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
ObservableMap<CryptoCurrency, BalanceType> get balance;
String formatCryptoAmount(String amount) => amount;
SyncStatus get syncStatus;
set syncStatus(SyncStatus status);
-17
View File
@@ -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;
}
@@ -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");
});
});
}
+351
View File
@@ -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<Comparable<Money>>());
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<Exception>()),
);
});
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");
});
});
});
}
+14 -11
View File
@@ -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));
});
});
}
-26
View File
@@ -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;
}
+9 -19
View File
@@ -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));
}
+15 -24
View File
@@ -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<void> 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<void> commit() async {
return send();
}
Future<void> commit() => send();
@override
Future<Map<String, String>> commitUR() {
throw UnimplementedError();
}
Future<Map<String, String>> commitUR() => throw UnimplementedError();
}
+3 -22
View File
@@ -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);
}
+33 -38
View File
@@ -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<void> init() async {
final getSeed = () async {
@@ -354,13 +349,12 @@ abstract class DecredWalletBase
Future<PendingTransaction> 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<Map<String, DecredTransactionInfo>> fetchTransactions() async {
return this.fetchFiveTransactions(0);
}
Future<Map<String, DecredTransactionInfo>> fetchTransactions() => fetchFiveTransactions(0);
Future<Map<String, DecredTransactionInfo>> fetchFiveTransactions(int from) async {
try {
final res = await _libwallet.listTransactions(walletInfo.name, from.toString(), "5");
final decoded = json.decode(res);
var txs = <String, DecredTransactionInfo>{};
final txs = <String, DecredTransactionInfo>{};
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<void> 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 = <Unspent>[];
final unspents = <Unspent>[];
for (final d in decoded) {
final spendable = d["spendable"] ?? false;
if (!spendable) {
-1
View File
@@ -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<
+1 -1
View File
@@ -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;
+22 -29
View File
@@ -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<PendingEVMChainTransaction> 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<PendingEVMChainTransaction> 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<EVMChainERC20Balance> fetchERC20Balances(
EthereumAddress userAddress, String contractAddress) async {
Future<EVMChainERC20Balance> 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()}');
}
+96 -52
View File
@@ -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<BigInt> get savingsBalance async =>
(await _savingsGateway.savings(accountOwner: _address)).saved;
Future<Money> get savingsBalance async =>
Money((await _savings.savings(accountOwner: _address)).saved, CryptoCurrency.deuro);
Future<BigInt> get accruedInterest => _savingsGateway.accruedInterest(accountOwner: _address);
Future<Money> get savingsBalanceV1 async => Money(
(await _getSavingsGateway(_wallet.getWeb3Client()!).savings(accountOwner: _address)).saved,
CryptoCurrency.deuro);
Future<BigInt> get interestRate => _savingsGateway.currentRatePPM();
Future<Money> get accruedInterest async =>
Money(await _savings.accruedInterest(accountOwner: _address), CryptoCurrency.deuro);
Future<BigInt> get approvedBalance => _dEuro.allowance(_address, _savingsGateway.self.address);
Future<BigInt> get interestRate => _savings.currentRatePPM();
Future<BigInt> get approvedBalance => _dEuro.allowance(_address, _savings.self.address);
Future<void> _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<PendingEVMChainTransaction> 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')) {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+23 -32
View File
@@ -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<String, dynamic> 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<String, dynamic> 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,
+74 -94
View File
@@ -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<CryptoCurrency, EVMChainERC20Balance>.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<bool> 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<GasParamsHandler> 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>(
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>(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<PendingTransaction> 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<PendingTransaction> createApprovalTransaction(BigInt amount, String spender,
CryptoCurrency token, EVMChainTransactionPriority? priority, String feeCurrency,
Future<PendingTransaction> 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(),
+10 -20
View File
@@ -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));
}
}
}
+11 -13
View File
@@ -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<void> 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<Map<String, String>> commitUR() {
throw UnimplementedError();
}
Future<Map<String, String>> commitUR() => throw UnimplementedError();
}
+3 -4
View File
@@ -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,
-34
View File
@@ -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(".");
+12 -15
View File
@@ -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<int, Map<int, Map<int, String>>> 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<bool> 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<String, List<int>> debugCallLength() => monero.debugCallLength;
@@ -438,4 +435,4 @@ Map<String, dynamic> getWalletCacheDebug() {
"error": e.toString(),
};
}
}
}
+17
View File
@@ -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;
}
+3 -17
View File
@@ -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';
}
+28 -47
View File
@@ -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<MoneroBalance,
};
}
bool needExportOutputs(int amount) {
bool needExportOutputs(Money amount) {
if (int.tryParse(currentWallet!.secretSpendKey()) != 0) {
return false;
}
// viewOnlyBalance - balance that we can spend
// TODO(mrcyjanek): remove hasUnknownKeyImages when we cleanup coin control
return (currentWallet!.viewOnlyBalance(
accountIndex: walletAddresses.account!.id) < amount) ||
currentWallet!.hasUnknownKeyImages();
return (currentWallet!.viewOnlyBalance(accountIndex: walletAddresses.account!.id) <
amount.amount.toInt()) ||
currentWallet!.hasUnknownKeyImages();
}
MoneroTrezorService? trezorService;
@@ -444,30 +444,22 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
}
if (hasMultiDestination) {
if (outputs.any(
(item) => 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<MoneroBalance,
final output = outputs.first;
final address =
output.isParsedAddress ? output.extractedAddress : output.address;
final amount =
output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.');
final amount = output.sendAll ? null : output.cryptoAmount.toString();
// if ((formattedAmount != null && unlockedBalance < formattedAmount) ||
// (formattedAmount == null && unlockedBalance <= 0)) {
@@ -492,8 +483,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
// 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.');
// }
if (inputs.isEmpty) MoneroTransactionCreationException(
'No inputs selected');
if (inputs.isEmpty) MoneroTransactionCreationException('No inputs selected');
pendingTransactionDescription =
await transaction_history.createTransactionSync(
address: address!,
@@ -835,10 +825,10 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
: TransactionDirection.incoming,
row.timeStamp,
row.isPending,
row.amount,
Money.fromInt(row.amount, currency),
row.accountIndex,
0,
row.fee,
Money.fromInt(row.fee,currency),
row.confirmations,
)..additionalInfo = <String, dynamic>{
'key': row.key,
@@ -893,8 +883,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
}
int _getHeightDistance(DateTime date) {
final distance =
DateTime.now().difference(date).inSeconds;
final distance = DateTime.now().difference(date).inSeconds;
final daysTmp = (distance / 86400).round();
final days = daysTmp < 1 ? 1 : daysTmp;
@@ -915,31 +904,28 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
void _askForUpdateBalance() {
final unlockedBalance = _getUnlockedBalance();
final fullBalance = monero_wallet.getFullBalance(
accountIndex: walletAddresses.account!.id);
final fullBalance = monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id);
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] = MoneroBalance(
fullBalance: fullBalance,
unlockedBalance: unlockedBalance,
frozenBalance: frozenBalance);
fullBalance: fullBalance, unlockedBalance: unlockedBalance, frozen: frozenBalance);
}
}
int _getUnlockedBalance() => 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<MoneroBalance,
Future<void> setLedgerConnection(LedgerConnection connection) async {
await enableLedgerExchange(connection);
}
@override
String formatCryptoAmount(String amount) {
return moneroAmountToString(amount: int.parse(amount));
}
}
+9 -15
View File
@@ -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;
-35
View File
@@ -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);
}
}
+16 -30
View File
@@ -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),
);
}
+7 -5
View File
@@ -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;
}
+7 -18
View File
@@ -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<String, dynamic> 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<String, dynamic> toJson() => {
'id': id,
'height': height,
'amountRaw': amountRaw.toString(),
'amountRaw': amount.amount.toString(),
'direction': direction.index,
'date': date.millisecondsSinceEpoch,
'confirmed': confirmed,
+15 -13
View File
@@ -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<CryptoCurrency, NanoBalance>.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<Map<String, String>> 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:
+7 -18
View File
@@ -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<Map<String, String>> 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<void> commit() async {
for (var block in blocks) {
for (final block in blocks) {
await nanoClient.processBlock(block, "send");
}
}
@override
Future<Map<String, String>> commitUR() {
throw UnimplementedError();
}
Future<Map<String, String>> commitUR() => throw UnimplementedError();
}
+2
View File
@@ -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;
}
@@ -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;
+11 -30
View File
@@ -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()});
}
+102 -185
View File
@@ -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<double> getBalance(String walletAddress, {bool throwOnError = false}) async {
Future<Money> 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<SolanaBalance?> getSplTokenBalance(String mintAddress, String walletAddress,
Future<SolanaBalance?> 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<double> getFeeForMessage(String message, Commitment commitment) async {
Future<Money> 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<double> getEstimatedFee(SolanaPublicKey publicKey, Commitment commitment) async {
Future<Money> 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<List<SolanaTransactionModel>?> 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<CompiledInstruction> instructions,
String? splTokenSymbol,
}) async {
final List<SolanaTransactionModel> 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<TransactionFetchResult?> 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<List<SolanaTransactionModel>> fetchTransactions(
SolAddress address, {
String? splTokenSymbol,
int? splTokenDecimal,
SPLToken? splToken,
Commitment? commitment,
SolAddress? walletAddress,
required void Function(List<SolanaTransactionModel>) 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<List<SolanaTransactionModel>> getSPLTokenTransfers({
required String mintAddress,
required String splTokenSymbol,
required int splTokenDecimal,
required SPLToken splToken,
required SolanaPrivateKey privateKey,
required void Function(List<SolanaTransactionModel>) 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<String, SPLToken?> tokenInfoCache = {};
Future<SPLToken?> 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<String, dynamic>;
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<PendingSolanaTransaction> 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<String> 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<Message> _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<Message> _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<double> _getFeeFromCompiledMessage(Message message, Commitment commitment) async {
Future<Money> _getFeeFromCompiledMessage(Message message, Commitment commitment) {
final base64Message = base64Encode(message.serialize());
final fee = await getFeeForMessage(base64Message, commitment);
return fee;
return getFeeForMessage(base64Message, commitment);
}
Future<bool> 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<PendingSolanaTransaction> _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<PendingSolanaTransaction> _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<String> _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<String> 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<String?> getIconImageFromTokenUri(String uri) async {
if (uri.isEmpty || uri == '…') return null;
+27 -38
View File
@@ -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<String, dynamic> 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<String, dynamic> 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(),
};
}
+4 -20
View File
@@ -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<String, dynamic> 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'],
);
}
+26 -45
View File
@@ -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<CryptoCurrency, SolanaBalance>.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<void> _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<SolanaBalance> _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<void> _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();
}
+2
View File
@@ -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;
}
+8 -11
View File
@@ -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<int> 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<void> commit() async => await sendTransaction();
@override
String get feeFormatted => "$feeFormattedValue TRX";
@override
String get feeFormattedValue => fee;
@override
String get hex => bytesToHex(signedTransaction);
+8 -8
View File
@@ -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));
}
}
}
+32 -64
View File
@@ -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<int> 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<PendingTronTransaction> 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<TransactionRaw> _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<TransactionRaw> _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<String> sendTransaction({
@@ -480,20 +450,18 @@ class TronClient {
}
}
Future<TronBalance> fetchTronTokenBalances(String userAddress, String contractAddress, {bool throwOnError = false}) async {
Future<TronBalance> 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));
}
}
+33 -40
View File
@@ -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<String, dynamic> 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<String, dynamic> 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);
}
+7
View File
@@ -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<String, dynamic> 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'];
+44 -44
View File
@@ -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<CryptoCurrency, TronBalance>.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<void> _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<String, TronTransactionInfo> 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<TronBalance> _fetchTronBalance() async {
final balance = await _client.getBalance(_tronPublicKey.toAddress());
return TronBalance(balance);
return TronBalance(Money(balance, CryptoCurrency.trx));
}
Future<void> _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<void> _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<bool> 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;
}
@@ -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<void> commit() async {
+17
View File
@@ -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;
}
+12 -16
View File
@@ -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 = <String, dynamic>{
'key': key,
'accountIndex': accountIndex,
'addressIndex': addressIndex
};
}
fee = Money.fromInt(map['fee'] as int? ?? 0, CryptoCurrency.wow) {
additionalInfo = <String, dynamic>{
'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';
}
+37 -26
View File
@@ -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> unspentCoinsInfo, required String password})
: balance = ObservableMap<CryptoCurrency, WowneroBalance>.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<CryptoCurrency, WowneroBalance>.of(<CryptoCurrency, WowneroBalance>{
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<CryptoCurrency, WowneroBalance>.of(<CryptoCurrency, WowneroBalance>{
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 = <String, dynamic>{
'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<void> _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 {
+5 -2
View File
@@ -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',
);
},
+10 -18
View File
@@ -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<Destination> 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<void> commit() async {
transferResult = await zanoWallet.transfer(destinations, fee, comment);
transferResult = await zanoWallet.transfer(destinations, fee.amount, comment);
zanoWallet.fetchTransactions();
}
@override
Future<Map<String, String>> commitUR() {
throw UnimplementedError();
}
Future<Map<String, String>> commitUR() => throw UnimplementedError();
}
+9 -11
View File
@@ -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));
}
+9 -26
View File
@@ -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';
}
+38 -33
View File
@@ -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<Destination> 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<bool> 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),
);
}
}
+15 -23
View File
@@ -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<bool>(false, (final a, final b) => a || (b.sendAll));
if (isAll) {
return availableBalance;
return availableBalance.amount.toInt();
}
return credentials.outputs.fold<int>(
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<void> 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<Map<String, String>> commitUR() {
throw UnimplementedError('UR not supported for Zcash');
}
Future<Map<String, String>> commitUR() => throw UnimplementedError('UR not supported for Zcash');
@override
bool shouldCommitUR() => false;
+7 -26
View File
@@ -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),
);
}
+3 -13
View File
@@ -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 ?? '';
+19 -30
View File
@@ -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<CryptoCurrency, ZcashBalance> 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 = <Recipient>[];
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,
+3 -8
View File
@@ -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<int> estimateFakeSendAllTxAmount(Object wallet, TransactionPriority priority,
Future<Money> 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);
@@ -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 ?? '',
+32 -46
View File
@@ -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 &&
+1 -1
View File
@@ -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}");
+4 -19
View File
@@ -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([
+9 -20
View File
@@ -19,9 +19,8 @@ class CWDecred extends Decred {
DecredRestoreWalletFromPubkeyCredentials(name: name, pubkey: pubkey, password: password);
@override
WalletService createDecredWalletService(Box<UnspentCoinsInfo> unspentCoinSource) {
return DecredWalletService(unspentCoinSource);
}
WalletService createDecredWalletService(Box<UnspentCoinsInfo> unspentCoinSource) =>
DecredWalletService(unspentCoinSource);
@override
List<TransactionPriority> getTransactionPriorities() => DecredTransactionPriority.all;
@@ -39,18 +38,19 @@ class CWDecred extends Decred {
@override
Object createDecredTransactionCredentials(List<Output> 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<WalletInfoAddressInfo> 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<Unspent> getUnspents(Object wallet) {
final decredWallet = wallet as DecredWallet;
+3 -1
View File
@@ -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';
+15 -1
View File
@@ -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<String> with Serializable<String> 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<FiatCurrency> get all => _all.values.toList();
static List<FiatCurrency> get currenciesAvailableToBuyWith => [
@@ -182,4 +190,10 @@ class FiatCurrency extends EnumerableItem<String> with Serializable<String> 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);
}
+61 -81
View File
@@ -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<Erc20Token> getERC20Currencies(WalletBase wallet) =>
(wallet as EVMChainWallet).erc20Currencies;
@@ -294,20 +276,16 @@ class CWEVM extends EVM {
@override
Future<PendingTransaction> 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<BigInt>? getDEuroSavingsBalance(WalletBase wallet) {
Future<Money>? getDEuroSavingsBalance(WalletBase wallet) {
if (wallet.chainId == 1 && wallet is EVMChainWallet) {
return DEuro(wallet).savingsBalance;
}
@@ -407,7 +385,15 @@ class CWEVM extends EVM {
}
@override
Future<BigInt>? getDEuroAccruedInterest(WalletBase wallet) {
Future<Money>? getDEuroSavingsV1Balance(WalletBase wallet) {
if (wallet.chainId == 1 && wallet is EVMChainWallet) {
return DEuro(wallet).savingsBalanceV1;
}
return null;
}
@override
Future<Money>? 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<PendingTransaction>? withdrawDEuroSavingV1(
WalletBase wallet, TransactionPriority priority) {
if (wallet.chainId == 1 && wallet is EVMChainWallet) {
return DEuro(wallet).withdrawSavingsV1(priority as EVMChainTransactionPriority);
}
return null;
}
@override
Future<PendingTransaction>? 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<ChainInfo> getAllChains() {
final allChains = _registry.getAllChains();
@@ -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';
+9 -9
View File
@@ -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);
}

Some files were not shown because too many files have changed in this diff Show More