mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-27 06:18:50 +01:00
* 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>
656 lines
23 KiB
Dart
656 lines
23 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:bip39/bip39.dart' as bip39;
|
|
import 'package:bitcoin_base/bitcoin_base.dart';
|
|
import 'package:blockchain_utils/blockchain_utils.dart';
|
|
import 'package:cw_bitcoin/.secrets.g.dart' as secrets;
|
|
import 'package:cw_bitcoin/address_from_output.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_wallet_addresses.dart';
|
|
import 'package:cw_bitcoin/electrum_balance.dart';
|
|
import 'package:cw_bitcoin/electrum_derivations.dart';
|
|
import 'package:cw_bitcoin/electrum_transaction_info.dart';
|
|
import 'package:cw_bitcoin/electrum_wallet.dart';
|
|
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
|
|
import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart';
|
|
import 'package:cw_bitcoin/lightning/lightning_wallet.dart';
|
|
import 'package:cw_bitcoin/hardware/bitcoin_ledger_service.dart';
|
|
import 'package:cw_bitcoin/payjoin/manager.dart';
|
|
import 'package:cw_bitcoin/payjoin/storage.dart';
|
|
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
|
|
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/payjoin_session.dart';
|
|
import 'package:cw_core/pending_transaction.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/zpub.dart';
|
|
import 'package:cw_core/wallet_info.dart';
|
|
import 'package:cw_core/wallet_keys_file.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:hive/hive.dart';
|
|
import 'package:ledger_bitcoin/psbt.dart';
|
|
import 'package:mobx/mobx.dart';
|
|
import 'package:ur/cbor_lite.dart';
|
|
import 'package:ur/ur.dart';
|
|
import 'package:ur/ur_decoder.dart';
|
|
|
|
part 'bitcoin_wallet.g.dart';
|
|
|
|
class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
|
|
|
|
abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
|
BitcoinWalletBase({
|
|
required String password,
|
|
required WalletInfo walletInfo,
|
|
required DerivationInfo derivationInfo,
|
|
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
|
required Box<PayjoinSession> payjoinBox,
|
|
required EncryptionFileUtils encryptionFileUtils,
|
|
Uint8List? seedBytes,
|
|
String? mnemonic,
|
|
String? xpub,
|
|
String? addressPageType,
|
|
BasedUtxoNetwork? networkParam,
|
|
List<BitcoinAddressRecord>? initialAddresses,
|
|
ElectrumBalance? initialBalance,
|
|
ElectrumBalance? initialLightningBalance,
|
|
Map<String, int>? initialRegularAddressIndex,
|
|
Map<String, int>? initialChangeAddressIndex,
|
|
String? passphrase,
|
|
List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
|
|
int initialSilentAddressIndex = 0,
|
|
bool? alwaysScan,
|
|
bool? useLightning,
|
|
String? cachedLightningAddress,
|
|
}) : super(
|
|
mnemonic: mnemonic,
|
|
passphrase: passphrase,
|
|
xpub: xpub,
|
|
password: password,
|
|
walletInfo: walletInfo,
|
|
derivationInfo: derivationInfo,
|
|
unspentCoinsInfo: unspentCoinsInfo,
|
|
network: networkParam == null
|
|
? BitcoinNetwork.mainnet
|
|
: networkParam == BitcoinNetwork.mainnet
|
|
? BitcoinNetwork.mainnet
|
|
: BitcoinNetwork.testnet,
|
|
initialAddresses: initialAddresses,
|
|
initialBalance: initialBalance,
|
|
seedBytes: seedBytes,
|
|
encryptionFileUtils: encryptionFileUtils,
|
|
currency:
|
|
networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc,
|
|
alwaysScan: alwaysScan,
|
|
useLightning: useLightning ?? true,
|
|
) {
|
|
// in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here)
|
|
// the sideHd derivation path = m/84'/0'/0'/1 (account 1, index unspecified here)
|
|
// String derivationPath = walletInfo.derivationInfo!.derivationPath!;
|
|
// String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1";
|
|
// final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType);
|
|
|
|
if (mnemonic != null && this.useLightning && LightningWallet.isAvailable) {
|
|
try {
|
|
lightningWallet = LightningWallet(
|
|
mnemonic: mnemonic,
|
|
passphrase: passphrase,
|
|
seedBytes: seedBytes,
|
|
apiKey: secrets.breezApiKey,
|
|
lnurlDomain: "cake.cash",
|
|
cachedAddress: cachedLightningAddress,
|
|
);
|
|
} catch (e) {
|
|
printV(e);
|
|
}
|
|
}
|
|
|
|
payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this);
|
|
walletAddresses = BitcoinWalletAddresses(
|
|
walletInfo,
|
|
initialAddresses: initialAddresses,
|
|
initialRegularAddressIndex: initialRegularAddressIndex,
|
|
initialChangeAddressIndex: initialChangeAddressIndex,
|
|
initialSilentAddresses: initialSilentAddresses,
|
|
initialSilentAddressIndex: initialSilentAddressIndex,
|
|
mainHdByType: mainHdByType,
|
|
sideHdByType: sideHdByType,
|
|
legacyMainHd: mainHd,
|
|
legacySideHd: sideHd,
|
|
network: networkParam ?? network,
|
|
masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
|
|
isHardwareWallet: walletInfo.isHardwareWallet,
|
|
payjoinManager: payjoinManager,
|
|
lightningWallet: lightningWallet,
|
|
);
|
|
|
|
if (lightningWallet != null) {
|
|
walletAddresses.setLightningAddress(walletInfo.name);
|
|
}
|
|
autorun((_) {
|
|
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
|
|
});
|
|
|
|
reaction((_) => this.useLightning, (bool useLightning) {
|
|
if (useLightning && LightningWallet.isAvailable) {
|
|
if (mnemonic != null) {
|
|
lightningWallet = LightningWallet(
|
|
mnemonic: mnemonic,
|
|
passphrase: passphrase,
|
|
seedBytes: seedBytes,
|
|
apiKey: secrets.breezApiKey,
|
|
lnurlDomain: "cake.cash",
|
|
cachedAddress: cachedLightningAddress,
|
|
);
|
|
walletAddresses.setLightningAddress(walletInfo.name);
|
|
}
|
|
} else {
|
|
lightningWallet = null;
|
|
}
|
|
});
|
|
|
|
if (initialLightningBalance != null) {
|
|
balance[CryptoCurrency.btcln] = initialLightningBalance;
|
|
}
|
|
}
|
|
|
|
bool get isLightningInitialized => lightningWallet?.isInitialized == true;
|
|
|
|
@override
|
|
bool get hasRescan => true;
|
|
|
|
static Future<BitcoinWallet> create({
|
|
required String mnemonic,
|
|
required String password,
|
|
required WalletInfo walletInfo,
|
|
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
|
required Box<PayjoinSession> payjoinBox,
|
|
required EncryptionFileUtils encryptionFileUtils,
|
|
String? passphrase,
|
|
String? addressPageType,
|
|
BasedUtxoNetwork? network,
|
|
List<BitcoinAddressRecord>? initialAddresses,
|
|
List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
|
|
ElectrumBalance? initialBalance,
|
|
Map<String, int>? initialRegularAddressIndex,
|
|
Map<String, int>? initialChangeAddressIndex,
|
|
int initialSilentAddressIndex = 0,
|
|
}) async {
|
|
late Uint8List seedBytes;
|
|
|
|
final derivationInfo = await walletInfo.getDerivationInfo();
|
|
|
|
switch (derivationInfo.derivationType) {
|
|
case DerivationType.bip39:
|
|
seedBytes = await bip39.mnemonicToSeed(
|
|
mnemonic,
|
|
passphrase: passphrase ?? "",
|
|
);
|
|
break;
|
|
case DerivationType.electrum:
|
|
default:
|
|
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
|
|
break;
|
|
}
|
|
|
|
return BitcoinWallet(
|
|
mnemonic: mnemonic,
|
|
passphrase: passphrase ?? "",
|
|
password: password,
|
|
walletInfo: walletInfo,
|
|
derivationInfo: derivationInfo,
|
|
unspentCoinsInfo: unspentCoinsInfo,
|
|
initialAddresses: initialAddresses,
|
|
initialSilentAddresses: initialSilentAddresses,
|
|
initialSilentAddressIndex: initialSilentAddressIndex,
|
|
initialBalance: initialBalance,
|
|
encryptionFileUtils: encryptionFileUtils,
|
|
seedBytes: seedBytes,
|
|
initialRegularAddressIndex: initialRegularAddressIndex,
|
|
initialChangeAddressIndex: initialChangeAddressIndex,
|
|
addressPageType: addressPageType,
|
|
networkParam: network,
|
|
payjoinBox: payjoinBox,
|
|
useLightning: true,
|
|
);
|
|
}
|
|
|
|
static Future<BitcoinWallet> open({
|
|
required String name,
|
|
required WalletInfo walletInfo,
|
|
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
|
required Box<PayjoinSession> payjoinBox,
|
|
required String password,
|
|
required EncryptionFileUtils encryptionFileUtils,
|
|
}) async {
|
|
final network = walletInfo.network != null
|
|
? BasedUtxoNetwork.fromName(walletInfo.network!)
|
|
: BitcoinNetwork.mainnet;
|
|
|
|
final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
|
|
|
|
ElectrumWalletSnapshot? snp = null;
|
|
|
|
try {
|
|
snp = await ElectrumWalletSnapshot.load(
|
|
encryptionFileUtils,
|
|
name,
|
|
walletInfo.type,
|
|
password,
|
|
network,
|
|
);
|
|
} catch (e) {
|
|
if (!hasKeysFile) rethrow;
|
|
}
|
|
|
|
final WalletKeysData keysData;
|
|
// Migrate wallet from the old scheme to then new .keys file scheme
|
|
if (!hasKeysFile) {
|
|
keysData = WalletKeysData(
|
|
mnemonic: snp!.mnemonic,
|
|
xPub: snp.xpub,
|
|
passphrase: snp.passphrase,
|
|
);
|
|
} else {
|
|
keysData = await WalletKeysFile.readKeysFile(
|
|
name,
|
|
walletInfo.type,
|
|
password,
|
|
encryptionFileUtils,
|
|
);
|
|
}
|
|
|
|
final derivationInfo = await walletInfo.getDerivationInfo();
|
|
|
|
// set the default if not present:
|
|
derivationInfo.derivationPath ??= snp?.derivationPath ?? electrum_path;
|
|
derivationInfo.derivationType ??= snp?.derivationType ?? DerivationType.electrum;
|
|
if (derivationInfo.derivationType == DerivationType.unknown) {
|
|
if (snp?.derivationPath == electrum_path || snp?.derivationType == DerivationType.electrum) {
|
|
derivationInfo.derivationPath = electrum_path;
|
|
derivationInfo.derivationType = DerivationType.electrum;
|
|
} else {
|
|
derivationInfo.derivationPath = segwit_path;
|
|
derivationInfo.derivationType = DerivationType.bip39;
|
|
}
|
|
}
|
|
await derivationInfo.save();
|
|
|
|
Uint8List? seedBytes = null;
|
|
final mnemonic = keysData.mnemonic;
|
|
final passphrase = keysData.passphrase;
|
|
|
|
if (mnemonic != null) {
|
|
switch (derivationInfo.derivationType) {
|
|
case DerivationType.electrum:
|
|
seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
|
|
break;
|
|
case DerivationType.bip39:
|
|
default:
|
|
seedBytes = await bip39.mnemonicToSeed(
|
|
mnemonic,
|
|
passphrase: passphrase ?? '',
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return BitcoinWallet(
|
|
mnemonic: mnemonic,
|
|
xpub: keysData.xPub != null ? convertZpubToXpub(keysData.xPub!) : null,
|
|
password: password,
|
|
passphrase: passphrase,
|
|
walletInfo: walletInfo,
|
|
derivationInfo: derivationInfo,
|
|
unspentCoinsInfo: unspentCoinsInfo,
|
|
initialAddresses: snp?.addresses,
|
|
initialSilentAddresses: snp?.silentAddresses,
|
|
initialSilentAddressIndex: snp?.silentAddressIndex ?? 0,
|
|
initialBalance: snp?.balance,
|
|
initialLightningBalance: snp?.lightningBalance,
|
|
encryptionFileUtils: encryptionFileUtils,
|
|
seedBytes: seedBytes,
|
|
initialRegularAddressIndex: snp?.regularAddressIndex,
|
|
initialChangeAddressIndex: snp?.changeAddressIndex,
|
|
addressPageType: snp?.addressPageType,
|
|
networkParam: network,
|
|
alwaysScan: snp?.alwaysScan,
|
|
useLightning: snp?.useLightning,
|
|
cachedLightningAddress: snp?.cachedLightningAddress,
|
|
payjoinBox: payjoinBox,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> close({bool shouldCleanup = false}) async {
|
|
payjoinManager.cleanupSessions();
|
|
await lightningWallet?.close();
|
|
super.close(shouldCleanup: shouldCleanup);
|
|
}
|
|
|
|
@override
|
|
Future<ElectrumBalance> fetchBalances() async {
|
|
final balance = await super.fetchBalances();
|
|
if (!isLightningInitialized || lightningWallet == null) {
|
|
return balance;
|
|
}
|
|
|
|
final lBalance = await lightningWallet!.getBalance();
|
|
|
|
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 ?? Money.zero(currency),
|
|
);
|
|
}
|
|
|
|
@override
|
|
@action
|
|
Future<void> subscribeForUpdates() async {
|
|
if (isLightningInitialized && lightningWallet != null) {
|
|
lightningWallet!.setEventListener(
|
|
onTransactionEvent: (tx) async {
|
|
if (transactionHistory.transactions[tx.id]?.isPending != tx.isPending) {
|
|
transactionHistory.addOne(tx);
|
|
await transactionHistory.save();
|
|
await fetchBalances();
|
|
}
|
|
},
|
|
onCreateDepositTransactionEvent: (txs) async {
|
|
if (txs.isNotEmpty) {
|
|
transactionHistory.addMany(txs);
|
|
await transactionHistory.save();
|
|
}
|
|
},
|
|
onUpdateDepositTransactionEvent: (txs) async {
|
|
if (txs.isNotEmpty) {
|
|
txs.forEach((tx) => transactionHistory.transactions.remove(tx.id));
|
|
await transactionHistory.save();
|
|
}
|
|
},
|
|
onBalanceChangedEvent: fetchBalances,
|
|
);
|
|
}
|
|
|
|
return super.subscribeForUpdates();
|
|
}
|
|
|
|
@override
|
|
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
|
|
if (lightningWallet != null) {
|
|
final existingTx = transactionHistory.transactions.values
|
|
.where((e) => (e.additionalInfo["isLightning"] as bool?) == true)
|
|
.lastOrNull;
|
|
|
|
lightningWallet!.getTransactionHistory(fromDate: existingTx?.date).then((lnHistory) async {
|
|
transactionHistory.addMany(lnHistory);
|
|
await transactionHistory.save();
|
|
}).onError((_, __) {});
|
|
}
|
|
|
|
return super.fetchTransactions();
|
|
}
|
|
|
|
LightningWallet? lightningWallet;
|
|
|
|
late final PayjoinManager payjoinManager;
|
|
|
|
bool get isPayjoinAvailable => unspentCoinsInfo.values
|
|
.where((element) => element.walletId == id && element.isSending && !element.isFrozen)
|
|
.isNotEmpty;
|
|
|
|
Future<PsbtV2> buildPsbt({
|
|
required List<BitcoinBaseOutput> outputs,
|
|
required List<OutputInfo> cwOutputs,
|
|
required BigInt fee,
|
|
required BasedUtxoNetwork network,
|
|
required List<UtxoWithAddress> utxos,
|
|
required Map<String, PublicKeyWithDerivationPath> publicKeys,
|
|
required Uint8List masterFingerprint,
|
|
String? memo,
|
|
bool enableRBF = false,
|
|
BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
|
|
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
|
|
}) async {
|
|
final psbtReadyInputs = <PSBTReadyUtxoWithAddress>[];
|
|
for (final utxo in utxos) {
|
|
final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
|
|
final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
|
|
|
|
psbtReadyInputs.add(PSBTReadyUtxoWithAddress(
|
|
utxo: utxo.utxo,
|
|
rawTx: rawTx,
|
|
ownerDetails: utxo.ownerDetails,
|
|
ownerDerivationPath: publicKeyAndDerivationPath.derivationPath,
|
|
ownerMasterFingerprint: masterFingerprint,
|
|
ownerPublicKey: publicKeyAndDerivationPath.publicKey,
|
|
));
|
|
}
|
|
|
|
return PSBTTransactionBuild(
|
|
inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF, cwOutputs: cwOutputs)
|
|
.psbt;
|
|
}
|
|
|
|
@override
|
|
Future<BtcTransaction> buildHardwareWalletTransaction({
|
|
required List<BitcoinBaseOutput> outputs,
|
|
required BigInt fee,
|
|
required BasedUtxoNetwork network,
|
|
required List<UtxoWithAddress> utxos,
|
|
required List<OutputInfo> cwOutputs,
|
|
required Map<String, PublicKeyWithDerivationPath> publicKeys,
|
|
String? memo,
|
|
bool enableRBF = false,
|
|
BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
|
|
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
|
|
}) async {
|
|
final masterFingerprint =
|
|
await (hardwareWalletService as BitcoinHardwareWalletService).getMasterFingerprint();
|
|
|
|
final psbt = await buildPsbt(
|
|
outputs: outputs,
|
|
fee: fee,
|
|
network: network,
|
|
utxos: utxos,
|
|
cwOutputs: cwOutputs,
|
|
publicKeys: publicKeys,
|
|
masterFingerprint: masterFingerprint,
|
|
memo: memo,
|
|
enableRBF: enableRBF,
|
|
inputOrdering: inputOrdering,
|
|
outputOrdering: outputOrdering,
|
|
);
|
|
|
|
final psbtStr = base64Encode(psbt.serialize());
|
|
if (hardwareWalletService is BitcoinLedgerService && derivationInfo.derivationPath != null) {
|
|
(hardwareWalletService as BitcoinLedgerService)
|
|
.setAccountDerivationPath(derivationInfo.derivationPath!);
|
|
}
|
|
|
|
final rawHex = await hardwareWalletService!.signTransaction(transaction: psbtStr);
|
|
return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex));
|
|
}
|
|
|
|
@override
|
|
Future<PendingTransaction> createTransaction(Object credentials) async {
|
|
credentials = credentials as BitcoinTransactionCredentials;
|
|
final lnAddr = credentials.outputs.first.isParsedAddress ? credentials.outputs.first.extractedAddress! : credentials.outputs.first.address;
|
|
|
|
final isLNCompatible = await lightningWallet?.isCompatible(lnAddr);
|
|
if ((credentials.coinTypeToSpendFrom == UnspentCoinType.lightning && lightningWallet != null) ||
|
|
isLNCompatible == true) {
|
|
Money amount;
|
|
if (credentials.outputs.first.sendAll) {
|
|
amount = await lightningWallet!.getBalance();
|
|
} else {
|
|
amount = credentials.outputs.first.cryptoAmount;
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
final payjoinUri = credentials.payjoinUri;
|
|
if (payjoinUri == null && !tx.shouldCommitUR()) return tx;
|
|
|
|
final transaction = await buildPsbt(
|
|
utxos: tx.utxos,
|
|
outputs: tx.outputs
|
|
.map((e) => BitcoinOutput(
|
|
address: addressFromScript(e.scriptPubKey),
|
|
value: e.amount,
|
|
isSilentPayment: e.isSilentPayment,
|
|
isChange: e.isChange,
|
|
))
|
|
.toList(),
|
|
cwOutputs: credentials.outputs,
|
|
fee: tx.fee.amount,
|
|
network: network,
|
|
memo: credentials.outputs.first.memo,
|
|
outputOrdering: BitcoinOrdering.none,
|
|
enableRBF: true,
|
|
publicKeys: tx.publicKeys!,
|
|
masterFingerprint: Uint8List.fromList([0, 0, 0, 0]));
|
|
|
|
if (tx.shouldCommitUR()) {
|
|
tx.unsignedPsbt = transaction.asPsbtV0();
|
|
return tx;
|
|
}
|
|
|
|
final originalPsbt =
|
|
await signPsbt(base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys());
|
|
|
|
tx.commitOverride = () async {
|
|
final sender =
|
|
await payjoinManager.initSender(payjoinUri!, originalPsbt, int.parse(tx.feeRate));
|
|
payjoinManager.spawnNewSender(sender: sender, pjUrl: payjoinUri, amount: tx.amount.amount);
|
|
};
|
|
|
|
return tx;
|
|
}
|
|
|
|
List<UtxoWithPrivateKey> getUtxoWithPrivateKeys() => unspentCoins
|
|
.where((e) => (e.isSending && !e.isFrozen))
|
|
.map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
|
|
.toList();
|
|
|
|
Future<void> commitPsbt(String finalizedPsbt) {
|
|
final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt));
|
|
|
|
final btcTx = BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract()));
|
|
|
|
return PendingBitcoinTransaction(
|
|
btcTx,
|
|
type,
|
|
electrumClient: electrumClient,
|
|
amount: Money.zero(currency),
|
|
fee: Money.zero(currency),
|
|
feeRate: "",
|
|
network: network,
|
|
hasChange: true,
|
|
isViewOnly: false,
|
|
).commit();
|
|
}
|
|
|
|
Future<String> signPsbt(String preProcessedPsbt, List<UtxoWithPrivateKey> utxos) async {
|
|
final psbt = PsbtV2()..deserializeV0(base64Decode(preProcessedPsbt));
|
|
|
|
await psbt.signWithUTXO(utxos, (txDigest, utxo, key, sighash) {
|
|
return utxo.utxo.isP2tr()
|
|
? key.signTapRoot(
|
|
txDigest,
|
|
sighash: sighash,
|
|
tweak: utxo.utxo.isSilentPayment != true,
|
|
)
|
|
: key.signInput(txDigest, sigHash: sighash);
|
|
}, (txId, vout) async {
|
|
final txHex = await electrumClient.getTransactionHex(hash: txId);
|
|
final output = BtcTransaction.fromRaw(txHex).outputs[vout];
|
|
return TaprootAmountScriptPair(output.amount, output.scriptPubKey);
|
|
});
|
|
|
|
psbt.finalizeV0();
|
|
return base64Encode(psbt.asPsbtV0());
|
|
}
|
|
|
|
Future<void> commitPsbtUR(List<String> urCodes) async {
|
|
if (urCodes.isEmpty) throw Exception("No QR code got scanned");
|
|
bool isUr = urCodes.any((str) {
|
|
return str.startsWith("ur:psbt/");
|
|
});
|
|
if (isUr) {
|
|
final ur = URDecoder();
|
|
for (final inp in urCodes) {
|
|
ur.receivePart(inp);
|
|
}
|
|
final result = (ur.result as UR);
|
|
final cbor = result.cbor;
|
|
final cborDecoder = CBORDecoder(cbor);
|
|
final out = cborDecoder.decodeBytes();
|
|
final bytes = out.$1;
|
|
final base64psbt = base64Encode(bytes);
|
|
final psbt = PsbtV2()..deserializeV0(base64Decode(base64psbt));
|
|
|
|
// psbt.finalize();
|
|
final finalized = base64Encode(psbt.serialize());
|
|
await commitPsbt(finalized);
|
|
} else {
|
|
final btcTx = BtcTransaction.fromRaw(urCodes.first);
|
|
|
|
return PendingBitcoinTransaction(
|
|
btcTx,
|
|
type,
|
|
electrumClient: electrumClient,
|
|
amount: Money.zero(currency),
|
|
fee: Money.zero(currency),
|
|
feeRate: "",
|
|
network: network,
|
|
hasChange: true,
|
|
isViewOnly: false,
|
|
).commit();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<String> signMessage(String message, {String? address = null}) async {
|
|
if (walletInfo.isHardwareWallet) {
|
|
final addressEntry = address != null
|
|
? walletAddresses.allAddresses.firstWhere((element) => element.address == address)
|
|
: null;
|
|
final index = addressEntry?.index ?? 0;
|
|
final isChange = addressEntry?.isHidden == true ? 1 : 0;
|
|
final derivationInfo = await walletInfo.getDerivationInfo();
|
|
final accountPath = derivationInfo.derivationPath;
|
|
final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null;
|
|
|
|
final signature = await hardwareWalletService!
|
|
.signMessage(message: ascii.encode(message), derivationPath: derivationPath);
|
|
return base64Encode(signature);
|
|
}
|
|
|
|
return super.signMessage(message, address: address);
|
|
}
|
|
}
|