mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
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:
co-authored by
Omar Hatem
Serhii
Robert Malikowski
parent
8768aa8995
commit
75f3fe735b
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
abstract class EnumerableItem<T> {
|
||||
const EnumerableItem({required this.title, required this.raw});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()}');
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(".");
|
||||
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 ?? '';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ?? '',
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user