diff --git a/cw_lightning/lib/lightning_balance.dart b/cw_lightning/lib/lightning_balance.dart new file mode 100644 index 000000000..bd7ed1143 --- /dev/null +++ b/cw_lightning/lib/lightning_balance.dart @@ -0,0 +1,40 @@ +import 'dart:convert'; +import 'package:cw_bitcoin/bitcoin_amount_format.dart'; +import 'package:cw_core/balance.dart'; + +class LightningBalance extends Balance { + const LightningBalance({required this.confirmed, required this.unconfirmed, required this.frozen}) + : super(confirmed, unconfirmed); + + static LightningBalance? fromJSON(String? jsonSource) { + if (jsonSource == null) { + return null; + } + + final decoded = json.decode(jsonSource) as Map; + + return LightningBalance( + confirmed: decoded['confirmed'] as int? ?? 0, + unconfirmed: decoded['unconfirmed'] as int? ?? 0, + frozen: decoded['frozen'] as int? ?? 0); + } + + final int confirmed; + final int unconfirmed; + final int frozen; + + @override + String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen); + + @override + String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed); + + @override + String get formattedUnAvailableBalance { + final frozenFormatted = bitcoinAmountToString(amount: frozen); + return frozenFormatted == '0.0' ? '' : frozenFormatted; + } + + String toJSON() => + json.encode({'confirmed': confirmed, 'unconfirmed': unconfirmed, 'frozen': frozen}); +} diff --git a/cw_lightning/lib/lightning_wallet.dart b/cw_lightning/lib/lightning_wallet.dart index 4a127a862..a4bf3ae8b 100644 --- a/cw_lightning/lib/lightning_wallet.dart +++ b/cw_lightning/lib/lightning_wallet.dart @@ -1,10 +1,24 @@ +import 'dart:convert'; import 'dart:io'; import 'package:breez_sdk/breez_sdk.dart'; import 'package:breez_sdk/bridge_generated.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; +import 'package:cw_bitcoin/bitcoin_wallet_keys.dart'; +import 'package:cw_bitcoin/electrum.dart'; +import 'package:cw_bitcoin/electrum_balance.dart'; +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_core/crypto_currency.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/utils/file.dart'; +import 'package:cw_lightning/lightning_balance.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter/foundation.dart'; @@ -13,17 +27,40 @@ import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:path_provider/path_provider.dart'; import 'package:cw_lightning/.secrets.g.dart' as secrets; +import 'package:cw_core/wallet_base.dart'; part 'lightning_wallet.g.dart'; class LightningWallet = LightningWalletBase with _$LightningWallet; -abstract class LightningWalletBase extends ElectrumWallet with Store { - int satBalance = 0; +// abstract class LightningWalletBase extends ElectrumWallet with Store { +class LightningWalletBase + extends WalletBase + with Store { + final bitcoin.HDWallet hd; + final String mnemonic; + String _password; + late ElectrumClient electrumClient; + + @override + @observable + late ObservableMap balance; + + @override + late ElectrumWalletAddresses walletAddresses; + + bitcoin.NetworkType networkType = bitcoin.bitcoin; + + @override + BitcoinWalletKeys get keys => + BitcoinWalletKeys(wif: hd.wif!, privateKey: hd.privKey!, publicKey: hd.pubKey!); + + @override + @observable + SyncStatus syncStatus; LightningWalletBase( {required String mnemonic, @@ -31,22 +68,23 @@ abstract class LightningWalletBase extends ElectrumWallet with Store { required WalletInfo walletInfo, required Box unspentCoinsInfo, required Uint8List seedBytes, + ElectrumClient? electrumClient, List? initialAddresses, - ElectrumBalance? initialBalance, + LightningBalance? initialBalance, int initialRegularAddressIndex = 0, int initialChangeAddressIndex = 0}) - : super( - mnemonic: mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - networkType: bitcoin.bitcoin, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - seedBytes: seedBytes, - currency: CryptoCurrency.btc) { + : hd = bitcoin.HDWallet.fromSeed(seedBytes, network: bitcoin.bitcoin).derivePath("m/0'/0"), + syncStatus = NotConnectedSyncStatus(), + mnemonic = mnemonic, + _password = password, + balance = ObservableMap.of({ + CryptoCurrency.btc: + initialBalance ?? const LightningBalance(confirmed: 0, unconfirmed: 0, frozen: 0) + }), + super(walletInfo) { + transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password); walletAddresses = BitcoinWalletAddresses(walletInfo, - electrumClient: electrumClient, + electrumClient: electrumClient ?? ElectrumClient(), initialAddresses: initialAddresses, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, @@ -54,6 +92,8 @@ abstract class LightningWalletBase extends ElectrumWallet with Store { sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"), networkType: networkType); + this.electrumClient = electrumClient ?? ElectrumClient(); + // initialize breeze: try { setupBreez(seedBytes); @@ -72,7 +112,6 @@ abstract class LightningWalletBase extends ElectrumWallet with Store { required WalletInfo walletInfo, required Box unspentCoinsInfo, List? initialAddresses, - ElectrumBalance? initialBalance, int initialRegularAddressIndex = 0, int initialChangeAddressIndex = 0}) async { return LightningWallet( @@ -81,7 +120,6 @@ abstract class LightningWalletBase extends ElectrumWallet with Store { walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, - initialBalance: initialBalance, seedBytes: await mnemonicToSeedBytes(mnemonic), initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex); @@ -100,7 +138,6 @@ abstract class LightningWalletBase extends ElectrumWallet with Store { walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp.addresses, - initialBalance: snp.balance, seedBytes: await mnemonicToSeedBytes(snp.mnemonic), initialRegularAddressIndex: snp.regularAddressIndex, initialChangeAddressIndex: snp.changeAddressIndex); @@ -109,7 +146,11 @@ abstract class LightningWalletBase extends ElectrumWallet with Store { Future setupBreez(Uint8List seedBytes) async { // Initialize SDK logs listener final sdk = BreezSDK(); - sdk.initialize(); + try { + sdk.initialize(); + } catch (e) { + print("Error initializing Breez: $e"); + } NodeConfig breezNodeConfig = NodeConfig.greenlight( config: GreenlightNodeConfig( @@ -128,20 +169,162 @@ abstract class LightningWalletBase extends ElectrumWallet with Store { workingDir = "$workingDir/wallets/lightning/${walletInfo.name}/breez/"; new Directory(workingDir).createSync(recursive: true); breezConfig = breezConfig.copyWith(workingDir: workingDir); - await sdk.connect(config: breezConfig, seed: seedBytes); + try { + await sdk.disconnect(); + await sdk.connect(config: breezConfig, seed: seedBytes); + } catch (e) { + print("Error connecting to Breez: $e"); + } sdk.nodeStateStream.listen((event) { - // print("Node state: $event"); + print("Node state: $event"); if (event == null) return; - // int balanceSat = event.maxPayableMsat ~/ 1000; - // print("sats: $balanceSat"); - balance[CryptoCurrency.btc] = ElectrumBalance( - confirmed: event.maxPayableMsat, - unconfirmed: event.maxReceivableMsat, + int balanceSat = event.maxPayableMsat ~/ 1000; + print("sats: $balanceSat"); + balance[CryptoCurrency.btc] = LightningBalance( + confirmed: event.maxPayableMsat ~/ 1000, + unconfirmed: event.maxReceivableMsat ~/ 1000, frozen: 0, ); }); print("initialized breez: ${(await sdk.isInitialized())}"); } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) { + throw UnimplementedError("calculateEstimatedFee"); + } + + @action + @override + Future startSync() async { + try { + syncStatus = AttemptingSyncStatus(); + + // TODO: CW-563 Implement sync + + syncStatus = SyncedSyncStatus(); + } catch (e) { + print(e); + syncStatus = FailedSyncStatus(); + rethrow; + } + } + + @override + Future changePassword(String password) { + throw UnimplementedError("changePassword"); + } + + @override + void close() {} + + @action + @override + Future connectToNode({required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + syncStatus = ConnectedSyncStatus(); + } catch (e) { + print(e); + syncStatus = FailedSyncStatus(); + } + } + + @override + Future createTransaction(Object credentials) async { + throw UnimplementedError("createTransaction"); + } + + @override + Future> fetchTransactions() async { + // String address = _publicAddress!; + + // final transactions = await _client.fetchTransactions(address); + + // final Map result = {}; + + // for (var transactionModel in transactions) { + // final bool isSend = transactionModel.type == "send"; + // result[transactionModel.hash] = NanoTransactionInfo( + // id: transactionModel.hash, + // amountRaw: transactionModel.amount, + // height: transactionModel.height, + // direction: isSend ? TransactionDirection.outgoing : TransactionDirection.incoming, + // confirmed: transactionModel.confirmed, + // date: transactionModel.date ?? DateTime.now(), + // confirmations: transactionModel.confirmed ? 1 : 0, + // to: isSend ? transactionModel.account : address, + // from: isSend ? address : transactionModel.account, + // ); + // } + + // return result; + return {}; + } + + @override + Future rescan({required int height}) async => throw UnimplementedError(); + + Future init() async { + await walletAddresses.init(); + await transactionHistory.init(); + await save(); + } + + String toJSON() => json.encode({ + 'mnemonic': mnemonic, + 'account_index': walletAddresses.currentReceiveAddressIndex.toString(), + 'change_address_index': walletAddresses.currentChangeAddressIndex.toString(), + 'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(), + 'balance': balance[currency]?.toJSON() + }); + + @override + Future save() async { + final path = await makePath(); + await write(path: path, password: _password, data: toJSON()); + await transactionHistory.save(); + } + + Future updateBalance() async { + // balance[currency] = await _fetchBalances(); + await save(); + } + + @override + String get seed => mnemonic; + + Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + + // String toJSON() => json.encode({ + // 'seedKey': _hexSeed, + // 'mnemonic': _mnemonic, + // 'currentBalance': balance[currency]?.currentBalance.toString() ?? "0", + // 'receivableBalance': balance[currency]?.receivableBalance.toString() ?? "0", + // 'derivationType': _derivationType.toString() + // }); + + @override + Future renameWalletFiles(String newWalletName) async { + final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); + final currentWalletFile = File(currentWalletPath); + + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type); + final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName'); + + // Copies current wallet files into new wallet name's dir and files + if (currentWalletFile.existsSync()) { + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + await currentWalletFile.copy(newWalletPath); + } + if (currentTransactionsFile.existsSync()) { + final newDirPath = await pathForWalletDir(name: newWalletName, type: type); + await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName'); + } + + // Delete old name's dir and files + await Directory(currentDirPath).delete(recursive: true); + } } diff --git a/lib/src/screens/receive/lightning_invoice_page.dart b/lib/src/screens/receive/lightning_invoice_page.dart index ab2aab935..f4be9bca4 100644 --- a/lib/src/screens/receive/lightning_invoice_page.dart +++ b/lib/src/screens/receive/lightning_invoice_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/src/screens/receive/widgets/lightning_input_form.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; @@ -13,11 +14,13 @@ import 'package:cake_wallet/src/screens/receive/widgets/anonpay_input_form.dart' import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; import 'package:cake_wallet/view_model/lightning_invoice_page_view_model.dart'; import 'package:cake_wallet/view_model/lightning_view_model.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; @@ -190,8 +193,6 @@ class LightningInvoicePage extends BasePage { text: S.of(context).create_invoice, onPressed: () { FocusScope.of(context).unfocus(); - lightningViewModel.createInvoice( - amount: _amountController.text, description: _descriptionController.text); lightningInvoicePageViewModel.setRequestParams( inputAmount: _amountController.text, inputDescription: _descriptionController.text, @@ -233,24 +234,30 @@ class LightningInvoicePage extends BasePage { }); reaction((_) => lightningInvoicePageViewModel.state, (ExecutionState state) { - if (state is ExecutedSuccessfullyState) { - // Navigator.pushNamed(context, Routes.anonPayReceivePage, arguments: state.payload); - lightningViewModel.createInvoice( - amount: state.payload["amount"] as String, - description: state.payload["description"] as String?, - ); - } + // if (state is ExecutedSuccessfullyState) { + // // Navigator.pushNamed(context, Routes.anonPayReceivePage, arguments: state.payload); + // lightningViewModel.createInvoice( + // amount: state.payload["amount"] as String, + // description: state.payload["description"] as String?, + // ); + // } if (state is ExecutedSuccessfullyState) { showPopUp( context: context, builder: (BuildContext context) { - return AlertWithOneAction( - // alertTitle: S.of(context).invoice_created, - alertTitle: "Invoice created TODO", - alertContent: state.payload as String, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); + return AlertWithTwoActions( + // alertTitle: S.of(context).invoice_created, + alertTitle: "Invoice created TODO: CW-563", + alertContent: state.payload as String, + rightButtonText: S.of(context).ok, + actionRightButton: () => Navigator.of(context).pop(), + actionLeftButton: () async { + Clipboard.setData(ClipboardData(text: state.payload as String)); + showBar(context, S.of(context).copied_to_clipboard); + }, + leftButtonText: S.of(context).copy, + ); }); } diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index e2c0382b0..6c7a6ee51 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -18,15 +18,15 @@ import 'package:mobx/mobx.dart'; part 'balance_view_model.g.dart'; class BalanceRecord { - const BalanceRecord({ - required this.availableBalance, - required this.additionalBalance, - required this.frozenBalance, - required this.fiatAvailableBalance, - required this.fiatAdditionalBalance, - required this.fiatFrozenBalance, - required this.asset, - required this.formattedAssetTitle}); + const BalanceRecord( + {required this.availableBalance, + required this.additionalBalance, + required this.frozenBalance, + required this.fiatAvailableBalance, + required this.fiatAdditionalBalance, + required this.fiatFrozenBalance, + required this.asset, + required this.formattedAssetTitle}); final String fiatAdditionalBalance; final String fiatAvailableBalance; final String fiatFrozenBalance; @@ -41,12 +41,10 @@ class BalanceViewModel = BalanceViewModelBase with _$BalanceViewModel; abstract class BalanceViewModelBase with Store { BalanceViewModelBase( - {required this.appStore, - required this.settingsStore, - required this.fiatConvertationStore}) - : isReversing = false, - isShowCard = appStore.wallet!.walletInfo.isShowIntroCakePayCard, - wallet = appStore.wallet! { + {required this.appStore, required this.settingsStore, required this.fiatConvertationStore}) + : isReversing = false, + isShowCard = appStore.wallet!.walletInfo.isShowIntroCakePayCard, + wallet = appStore.wallet! { reaction((_) => appStore.wallet, _onWalletChange); } @@ -60,8 +58,7 @@ abstract class BalanceViewModelBase with Store { bool isReversing; @observable - WalletBase, TransactionInfo> - wallet; + WalletBase, TransactionInfo> wallet; @computed double get price { @@ -97,7 +94,7 @@ abstract class BalanceViewModelBase with Store { String get asset { final typeFormatted = walletTypeToString(appStore.wallet!.type); - switch(wallet.type) { + switch (wallet.type) { case WalletType.haven: return '$typeFormatted Assets'; default: @@ -120,7 +117,7 @@ abstract class BalanceViewModelBase with Store { @computed String get availableBalanceLabel { - switch(wallet.type) { + switch (wallet.type) { case WalletType.monero: case WalletType.haven: case WalletType.ethereum: @@ -135,7 +132,7 @@ abstract class BalanceViewModelBase with Store { @computed String get additionalBalanceLabel { - switch(wallet.type) { + switch (wallet.type) { case WalletType.monero: case WalletType.haven: case WalletType.ethereum: @@ -144,6 +141,8 @@ abstract class BalanceViewModelBase with Store { case WalletType.nano: case WalletType.banano: return S.current.receivable_balance; + case WalletType.lightning: + return "CW-563 Max Receivable"; default: return S.current.unconfirmed; } @@ -228,15 +227,17 @@ abstract class BalanceViewModelBase with Store { Map get balances { return wallet.balance.map((key, value) { if (displayMode == BalanceDisplayMode.hiddenBalance) { - return MapEntry(key, BalanceRecord( - availableBalance: '---', - additionalBalance: '---', - frozenBalance: '---', - fiatAdditionalBalance: isFiatDisabled ? '' : '---', - fiatAvailableBalance: isFiatDisabled ? '' : '---', - fiatFrozenBalance: isFiatDisabled ? '' : '---', - asset: key, - formattedAssetTitle: _formatterAsset(key))); + return MapEntry( + key, + BalanceRecord( + availableBalance: '---', + additionalBalance: '---', + frozenBalance: '---', + fiatAdditionalBalance: isFiatDisabled ? '' : '---', + fiatAvailableBalance: isFiatDisabled ? '' : '---', + fiatFrozenBalance: isFiatDisabled ? '' : '---', + asset: key, + formattedAssetTitle: _formatterAsset(key))); } final fiatCurrency = settingsStore.fiatCurrency; final price = fiatConvertationStore.prices[key] ?? 0; @@ -245,25 +246,23 @@ abstract class BalanceViewModelBase with Store { // throw Exception('Price is null for: $key'); // } - final additionalFiatBalance = isFiatDisabled ? '' : (fiatCurrency.toString() - + ' ' - + _getFiatBalance( - price: price, - cryptoAmount: value.formattedAdditionalBalance)); + final additionalFiatBalance = isFiatDisabled + ? '' + : (fiatCurrency.toString() + + ' ' + + _getFiatBalance(price: price, cryptoAmount: value.formattedAdditionalBalance)); - final availableFiatBalance = isFiatDisabled ? '' : (fiatCurrency.toString() - + ' ' - + _getFiatBalance( - price: price, - cryptoAmount: value.formattedAvailableBalance)); - - - final frozenFiatBalance = isFiatDisabled ? '' : (fiatCurrency.toString() - + ' ' - + _getFiatBalance( - price: price, - cryptoAmount: getFormattedFrozenBalance(value))); + final availableFiatBalance = isFiatDisabled + ? '' + : (fiatCurrency.toString() + + ' ' + + _getFiatBalance(price: price, cryptoAmount: value.formattedAvailableBalance)); + final frozenFiatBalance = isFiatDisabled + ? '' + : (fiatCurrency.toString() + + ' ' + + _getFiatBalance(price: price, cryptoAmount: getFormattedFrozenBalance(value))); return MapEntry( key, @@ -276,12 +275,11 @@ abstract class BalanceViewModelBase with Store { fiatFrozenBalance: frozenFiatBalance, asset: key, formattedAssetTitle: _formatterAsset(key))); - }); + }); } @computed bool get hasAdditionalBalance => !isEVMCompatibleChain(wallet.type); - @computed List get formattedBalances { @@ -358,9 +356,7 @@ abstract class BalanceViewModelBase with Store { @action void _onWalletChange( - WalletBase, - TransactionInfo>? - wallet) { + WalletBase, TransactionInfo>? wallet) { if (wallet == null) { return; } @@ -371,7 +367,7 @@ abstract class BalanceViewModelBase with Store { } @action - Future disableIntroCakePayCard () async { + Future disableIntroCakePayCard() async { const cardDisplayStatus = false; wallet.walletInfo.showIntroCakePayCard = cardDisplayStatus; await wallet.walletInfo.save(); @@ -401,6 +397,6 @@ abstract class BalanceViewModelBase with Store { } } - String getFormattedFrozenBalance(Balance walletBalance) => walletBalance.formattedUnAvailableBalance; + String getFormattedFrozenBalance(Balance walletBalance) => + walletBalance.formattedUnAvailableBalance; } - diff --git a/lib/view_model/lightning_view_model.dart b/lib/view_model/lightning_view_model.dart index 1940fb6f8..c4450f69d 100644 --- a/lib/view_model/lightning_view_model.dart +++ b/lib/view_model/lightning_view_model.dart @@ -78,12 +78,13 @@ abstract class LightningViewModelBase with Store { Future createInvoice({required String amount, String? description}) async { final sdk = await BreezSDK(); + print(amount); + print("@@@@@@@@@@@@@@"); final req = ReceivePaymentRequest( - amountMsat: int.parse(amount) * 1000, + amountMsat: (double.parse(amount) * 100000000).round(), description: description ?? '', ); final res = await sdk.receivePayment(req: req); - print(res.lnInvoice.bolt11); return res.lnInvoice.bolt11; } @@ -96,7 +97,7 @@ abstract class LightningViewModelBase with Store { // final res = await sdk.receivePayment(req: req); // print(res.lnInvoice.); // return res.lnInvoice.bolt11; - // TODO: figure out how to get the limits - return ['1000', '20000']; + // TODO: CW-563 figure out how to get the limits + return ['0', '20000']; } }