diff --git a/README.md b/README.md index 949ba8f..923f37a 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ cargo run -- --development-mode=true --network=regtest --bitcoind-rpc-host=loca Paul ``` -cargo run -- --development-mode=true --network=regtest --bitcoind-rpc-host=localhost --bitcoind-rpc-port=18443 --bitcoind-rpc-username=admin1 --bitcoind-rpc-password=123 --data-dir=./.data +cargo run -- --development-mode=true --network=regtest --bitcoind-rpc-host=localhost --bitcoind-rpc-port=18443 --bitcoind-rpc-username=polaruser --bitcoind-rpc-password=polarpass --data-dir=./.data ``` The GRPC should run on `5401` diff --git a/mobile/lib/data/channel.dart b/mobile/lib/data/channel.dart index a894e3f..6189eda 100644 --- a/mobile/lib/data/channel.dart +++ b/mobile/lib/data/channel.dart @@ -5,7 +5,9 @@ import 'package:flutter/material.dart'; import 'package:pln/generated/pln.pbgrpc.dart'; import 'package:riverpod/riverpod.dart'; +// ignore: depend_on_referenced_packages import 'package:fixnum/fixnum.dart' show Int64; +import 'package:pln/grpc.dart'; import '../grpc.dart'; @@ -41,7 +43,9 @@ class Channel { } class ChannelNotifier extends StateNotifier { - ChannelNotifier() : super(null); + ChannelNotifier(this.ref) : super(null); + + final Ref ref; Timer? _timer; @@ -51,6 +55,7 @@ class ChannelNotifier extends StateNotifier { } openChannel() async { + final plnClient = ref.read(plnClientProvider)!; try { debugPrint("creating channel..."); final response = await plnClient.openChannel(OpenChannelRequest( @@ -66,6 +71,7 @@ class ChannelNotifier extends StateNotifier { } checkChannelStatus() async { + final plnClient = ref.read(plnClientProvider)!; try { final response = await plnClient.getChannel(GetChannelRequest(id: state?.id)); @@ -103,5 +109,5 @@ class ChannelNotifier extends StateNotifier { } final channelProvider = StateNotifierProvider((ref) { - return ChannelNotifier(); + return ChannelNotifier(ref); }); diff --git a/mobile/lib/data/prefs.dart b/mobile/lib/data/prefs.dart new file mode 100644 index 0000000..6ca52ac --- /dev/null +++ b/mobile/lib/data/prefs.dart @@ -0,0 +1,41 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +// ignore: constant_identifier_names +const String PLN_GRPC_ENDPOINT = "PLN_GRPC_ENDPOINT"; + +class PrefsStateNotifier extends StateNotifier { + PrefsStateNotifier(this.prefs) + : super(prefs.get(PLN_GRPC_ENDPOINT) as String?); + +// String _federationCode; + SharedPreferences prefs; + + /// Updates the value asynchronously. + Future update(String? value) async { + if (value != null) { + await prefs.setString(PLN_GRPC_ENDPOINT, value); + } else { + await prefs.remove(PLN_GRPC_ENDPOINT); + } + super.state = value; + } + + /// Do not use the setter for state. + /// Instead, use `await update(value).` + @override + set state(String? value) { + assert(false, + "Don't use the setter for state. Instead use `await update(value)`."); + Future(() async { + await update(value); + }); + } +} + +StateNotifierProvider createPrefProvider({ + required SharedPreferences Function(Ref) prefs, +}) { + return StateNotifierProvider( + (ref) => PrefsStateNotifier(prefs(ref))); +} diff --git a/mobile/lib/data/send.dart b/mobile/lib/data/send.dart index 0b93f6a..98e6e79 100644 --- a/mobile/lib/data/send.dart +++ b/mobile/lib/data/send.dart @@ -39,19 +39,21 @@ int btcToSats(btc) { } class SendNotifier extends StateNotifier { - SendNotifier() : super(null); + SendNotifier(this.ref) : super(null); + + final Ref ref; createSendFromInvoice(String invoice) async { final req = Bolt11PaymentRequest(invoice); debugPrint("amount: ${(req.amount.toDouble() * 100000000).toInt()}"); var description = ""; - req.tags.forEach((TaggedField t) { - print("${t.type}: ${t.data}"); + for (var t in req.tags) { + debugPrint("${t.type}: ${t.data}"); if (t.type == "description") { description = t.data; } - }); + } state = Send( invoice: invoice, amountSats: btcToSats(req.amount), @@ -64,6 +66,7 @@ class SendNotifier extends StateNotifier { } pay() async { + final plnClient = ref.read(plnClientProvider)!; try { debugPrint("paying..."); final response = await plnClient.sendPayment( @@ -77,6 +80,7 @@ class SendNotifier extends StateNotifier { } checkPaymentStatus() async { + final plnClient = ref.read(plnClientProvider)!; try { debugPrint("checking status for ${state?.invoice}"); final response = await plnClient @@ -98,5 +102,5 @@ class SendNotifier extends StateNotifier { } final sendProvider = StateNotifierProvider((ref) { - return SendNotifier(); + return SendNotifier(ref); }); diff --git a/mobile/lib/grpc.dart b/mobile/lib/grpc.dart index 4025886..dc987f9 100644 --- a/mobile/lib/grpc.dart +++ b/mobile/lib/grpc.dart @@ -1,13 +1,42 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:grpc/grpc.dart'; import 'package:pln/generated/pln.pbgrpc.dart'; +import 'package:pln/main.dart'; -final _channel = ClientChannel( - 'localhost', - port: 5401, - options: ChannelOptions( - credentials: const ChannelCredentials.insecure(), - codecRegistry: CodecRegistry(codecs: const [GzipCodec(), IdentityCodec()]), - ), -); +final plnClientProvider = + StateNotifierProvider((ref) { + return PlnClient(ref); +}); -final plnClient = ManagerClient(_channel); +class PlnClient extends StateNotifier { + PlnClient(this.ref) : super(null); + + final Ref ref; + + Future restartClient() async { + debugPrint("restarting grpc client"); + + try { + final prefs = ref.read(prefProvider); + + final uri = Uri.parse(prefs ?? "http://localhost:5401"); + + // TODO is this all I need from the URI? + final channel = ClientChannel(uri.host + uri.path, + port: uri.port, + options: ChannelOptions( + credentials: const ChannelCredentials.insecure(), + codecRegistry: + CodecRegistry(codecs: const [GzipCodec(), IdentityCodec()]), + )); + state = ManagerClient(channel); + } catch (e) { + throw ("couldn't connect"); + } + } + + ManagerClient? get() { + return state; + } +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 18c992e..1d8aa6c 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,9 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:pln/data/prefs.dart'; import 'package:pln/router.dart'; import 'package:pln/theme.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +late SharedPreferences prefs; + +final prefProvider = createPrefProvider( + prefs: (_) => prefs, +); + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + prefs = await SharedPreferences.getInstance(); -void main() { runApp(const ProviderScope(child: MyApp())); } diff --git a/mobile/lib/pln_appbar.dart b/mobile/lib/pln_appbar.dart index 085d191..04dcf36 100644 --- a/mobile/lib/pln_appbar.dart +++ b/mobile/lib/pln_appbar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'constants.dart'; @@ -28,11 +29,16 @@ class PlnAppBar extends StatelessWidget implements PreferredSizeWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(title, - style: Theme.of(context) - .textTheme - .headline1 - ?.copyWith(color: home ? blue : black)), + home + ? GestureDetector( + onTap: () => context.go("/welcome"), + child: Text(title, + style: Theme.of(context) + .textTheme + .headline1 + ?.copyWith(color: blue)), + ) + : Text(title, style: Theme.of(context).textTheme.headline1), backAction != null ? InkWell( onTap: backAction, diff --git a/mobile/lib/router.dart b/mobile/lib/router.dart index c7607bd..91da2a8 100644 --- a/mobile/lib/router.dart +++ b/mobile/lib/router.dart @@ -2,7 +2,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter/material.dart'; +import 'package:pln/main.dart'; import 'package:pln/screens/channel_status.dart'; +import 'package:pln/screens/welcome.dart'; import './screens/channel.dart'; import 'screens/channel_fund.dart'; @@ -14,7 +16,7 @@ import 'screens/send_status.dart'; /// Caches and Exposes a [GoRouter] final routerProvider = Provider((ref) { - final router = RouterNotifier(); + final router = RouterNotifier(ref); return GoRouter( debugLogDiagnostics: true, // For demo purposes @@ -25,22 +27,23 @@ final routerProvider = Provider((ref) { }); class RouterNotifier extends ChangeNotifier { - // final Ref _ref; + final Ref _ref; - // /// This implementation exploits `ref.listen()` to add a simple callback that - // /// calls `notifyListeners()` whenever there's change onto a desider provider. - // RouterNotifier(this._ref) { - // _ref.listen( - // prefProvider, - // (_, __) => notifyListeners(), - // ); - // } + /// This implementation exploits `ref.listen()` to add a simple callback that + /// calls `notifyListeners()` whenever there's change onto a desider provider. + RouterNotifier(this._ref) { + _ref.listen( + prefProvider, + (_, __) => notifyListeners(), + ); + } String? _redirectLogic(GoRouterState state) { return null; } List get _routes => [ + GoRoute(path: "/welcome", builder: (context, state) => const Welcome()), GoRoute(path: "/", builder: (context, state) => const Home(), routes: [ GoRoute( path: "send", diff --git a/mobile/lib/screens/home.dart b/mobile/lib/screens/home.dart index a3dd540..ffabf67 100644 --- a/mobile/lib/screens/home.dart +++ b/mobile/lib/screens/home.dart @@ -7,22 +7,32 @@ import 'package:pln/pln_appbar.dart'; import 'package:pln/widgets/balance.dart'; import 'package:pln/widgets/button.dart'; -// final balanceFutureProvider = FutureProvider( -// (ref) async { -// await Future.delayed(const Duration(milliseconds: 200)); -// final response = -// await plnClient.getBalance(GetBalanceRequest()); // our future -// return response.amtSatoshis.toInt(); //returns a list of all the hospitals -// }, -// ); - -final balanceStreamProvider = StreamProvider.autoDispose((ref) { - Stream getStatus() async* { +final balanceStreamProvider = StreamProvider.autoDispose((ref) { + Stream getStatus() async* { while (true) { - await Future.delayed(const Duration(seconds: 1)); - final response = await plnClient.getBalance(GetBalanceRequest()); + try { + final plnClient = ref.read(plnClientProvider); + if (plnClient == null) { + final plnClientNotifier = ref.read(plnClientProvider.notifier); + await plnClientNotifier.restartClient(); + final plnClient = ref.read(plnClientProvider); + if (plnClient == null) { + throw ("no client"); + } + await Future.delayed(const Duration(seconds: 5)); + final response = await plnClient.getBalance(GetBalanceRequest()); + yield response.amtSatoshis.toInt(); + } else { + await Future.delayed(const Duration(seconds: 1)); + final response = await plnClient.getBalance(GetBalanceRequest()); + yield response.amtSatoshis.toInt(); + } + } catch (e) { + debugPrint("error: ${e.toString()}"); + yield null; + // yield null; - yield response.amtSatoshis.toInt(); + } } } @@ -34,7 +44,7 @@ class Home extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - AsyncValue state = ref.watch(balanceStreamProvider); + AsyncValue state = ref.watch(balanceStreamProvider); _refresh() async { ref.refresh(balanceStreamProvider); @@ -54,25 +64,54 @@ class Home extends ConsumerWidget { children: [ state.when( data: (balance) => GestureDetector( - onTap: _refresh, child: Balance(balance)), + onTap: _refresh, + child: balance != null + ? Balance(balance) + : const Text("no connection")), loading: () => const CircularProgressIndicator(), error: (err, _) => Text(err.toString())), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BlandButton( - text: "Send", - onPressed: () => context.go("/send")), - const SizedBox(height: 12), - BlandButton( - text: "Receive", - onPressed: () => context.go("/receive")), - const SizedBox(height: 12), - BlandButton( - text: "Open Channel", - onPressed: () => context.go("/channel"), - ) - ], + state.when( + data: (balance) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BlandButton( + disabled: balance == null, + text: "Send", + onPressed: () => context.go("/send")), + const SizedBox(height: 12), + BlandButton( + disabled: balance == null, + text: "Receive", + onPressed: () => context.go("/receive")), + const SizedBox(height: 12), + BlandButton( + disabled: balance == null, + text: "Open Channel", + onPressed: () => context.go("/channel"), + ) + ], + ), + loading: () => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BlandButton( + disabled: true, + text: "Send", + onPressed: () => context.go("/send")), + const SizedBox(height: 12), + BlandButton( + disabled: true, + text: "Receive", + onPressed: () => context.go("/receive")), + const SizedBox(height: 12), + BlandButton( + disabled: true, + text: "Open Channel", + onPressed: () => context.go("/channel"), + ) + ], + ), + error: (err, _) => const SizedBox(height: 0), ) ])))); } diff --git a/mobile/lib/screens/setup.dart b/mobile/lib/screens/setup.dart deleted file mode 100644 index e4a1041..0000000 --- a/mobile/lib/screens/setup.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class Home extends ConsumerWidget { - const Home({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return const SafeArea( - child: Scaffold( - body: Padding( - padding: EdgeInsets.all(24.0), - child: Center(child: Text("Hello"))))); - } -} diff --git a/mobile/lib/screens/welcome.dart b/mobile/lib/screens/welcome.dart new file mode 100644 index 0000000..934b7d8 --- /dev/null +++ b/mobile/lib/screens/welcome.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:pln/grpc.dart'; +import 'package:pln/main.dart'; +import 'package:pln/pln_appbar.dart'; +import 'package:pln/widgets/button.dart'; +import 'package:pln/widgets/text_field.dart'; + +class Welcome extends ConsumerWidget { + const Welcome({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final endpointTextController = TextEditingController(); + final prefsNotifier = ref.read(prefProvider.notifier); + final plnClient = ref.read(plnClientProvider.notifier); + + _connect() async { + try { + await prefsNotifier.update(endpointTextController.text).then((_) async { + debugPrint("updating endpoint: ${endpointTextController.text}"); + try { + await plnClient.restartClient().then((_) => context.go("/")); + } catch (e) { + throw ("couldn't connect using ${endpointTextController.text}"); + } + }); + } catch (e) { + debugPrint('Caught error: $e'); + } + } + + final endpoint = ref.read(prefProvider); + + return SafeArea( + child: Scaffold( + appBar: const PlnAppBar(title: "Welcome"), + body: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24.0), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BlandTextField( + controller: endpointTextController, + prompt: "Paste gRPC endpoint", + iconData: Icons.add_link, + ), + const SizedBox(height: 24.0), + Text( + "current endpoint: ${endpoint ?? "no endpoint set"}"), + ], + ), + const SizedBox(height: 0), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BlandButton( + text: "Connect", + onPressed: () async { + await _connect(); + }), + ], + ) + ])))); + } +} diff --git a/mobile/lib/widgets/button.dart b/mobile/lib/widgets/button.dart index dc4a039..e35b3f5 100644 --- a/mobile/lib/widgets/button.dart +++ b/mobile/lib/widgets/button.dart @@ -5,7 +5,9 @@ import '../constants.dart'; class BlandButton extends StatelessWidget { final VoidCallback? onPressed; final String text; - const BlandButton({Key? key, required this.text, this.onPressed}) + final bool disabled; + const BlandButton( + {Key? key, required this.text, this.onPressed, this.disabled = false}) : super(key: key); @override @@ -14,6 +16,7 @@ class BlandButton extends StatelessWidget { style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), textStyle: Theme.of(context).textTheme.headline3, + elevation: 5, onPrimary: black, // textStyle: const TextStyle(color: black), @@ -22,7 +25,8 @@ class BlandButton extends StatelessWidget { borderRadius: BorderRadius.circular(0), ), ), - onPressed: onPressed ?? () => debugPrint('unimplemented'), + onPressed: + disabled ? null : onPressed ?? () => debugPrint('unimplemented'), child: Text(text)); } } diff --git a/mobile/macos/Flutter/GeneratedPluginRegistrant.swift b/mobile/macos/Flutter/GeneratedPluginRegistrant.swift index 8236f57..8ad28b4 100644 --- a/mobile/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/mobile/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,8 +5,10 @@ import FlutterMacOS import Foundation +import shared_preferences_macos import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/mobile/macos/Podfile.lock b/mobile/macos/Podfile.lock index d09d6e7..291d12f 100644 --- a/mobile/macos/Podfile.lock +++ b/mobile/macos/Podfile.lock @@ -1,20 +1,26 @@ PODS: - FlutterMacOS (1.0.0) + - shared_preferences_macos (0.0.1): + - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) + - shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral + shared_preferences_macos: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727 url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 80ae3b1..3450c3d 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -99,6 +99,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" fixnum: dependency: transitive description: @@ -240,6 +254,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.7" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" plugin_platform_interface: dependency: transitive description: @@ -247,6 +289,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" protobuf: dependency: "direct main" description: @@ -282,6 +331,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.15" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.12" + shared_preferences_ios: + dependency: transitive + description: + name: shared_preferences_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" sky_engine: dependency: transitive description: flutter @@ -406,6 +511,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.7.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+1" sdks: dart: ">=2.17.1 <3.0.0" flutter: ">=3.0.0" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index af042ea..c2b091c 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: url_launcher: ^6.1.3 intl: ^0.17.0 bolt11_decoder: ^1.0.1 + shared_preferences: ^2.0.15 dev_dependencies: flutter_test: diff --git a/plncore/src/services/manager.rs b/plncore/src/services/manager.rs index 21ca5ae..16e110a 100644 --- a/plncore/src/services/manager.rs +++ b/plncore/src/services/manager.rs @@ -31,6 +31,7 @@ use senseicore::services::admin::{AdminRequest, AdminResponse, AdminService}; pub enum ManagerRequest { GetStatus {}, + StartNodes {}, OpenChannel { pubkey: String, connection_string: String, @@ -52,6 +53,7 @@ pub enum ManagerRequest { #[serde(untagged)] pub enum ManagerResponse { GetStatus { running: bool }, + StartNodes {}, OpenChannel { id: String, address: String }, GetChannel { status: String }, SendPayment { status: String }, @@ -64,8 +66,7 @@ pub enum ManagerResponse { pub struct ManagerService { pub admin_service: Arc, pub root_node_pubkey: String, - internal_channel_id_map: Arc>>, - internal_node_map: Arc>>, + internal_channel_id_map: Arc>>, } impl ManagerService { @@ -74,11 +75,16 @@ impl ManagerService { admin_service, root_node_pubkey, internal_channel_id_map: Arc::new(Mutex::new(HashMap::new())), - internal_node_map: Arc::new(Mutex::new(vec![])), } } } +struct PendingChannelDetails { + from_node: String, + to_node: String, + amt: u64, +} + #[derive(Serialize, Debug)] pub enum Error { Generic(String), @@ -139,7 +145,38 @@ impl ManagerService { None => Ok(ManagerResponse::GetStatus { running: false }), } } - // TODO + ManagerRequest::StartNodes {} => { + println!("Trying to start all nodes..."); + let get_nodes_req = AdminRequest::ListNodes { + pagination: PaginationRequest::default(), + }; + let get_node_resp = self.admin_service.call(get_nodes_req).await.unwrap(); // TODO do not unwrap + let pubkeys: Vec = match get_node_resp { + AdminResponse::ListNodes { + nodes, + pagination: _, + } => Some(nodes.into_iter().map(|node| node.pubkey).collect()), + _ => None, + } + .unwrap(); + for node_pubkey in pubkeys { + println!("Starting node: {}", node_pubkey.as_str()); + let pubkey = node_pubkey.to_string(); + let start_node_req = AdminRequest::StartNode { + pubkey, + passphrase: "password".to_string(), + }; + let create_node_resp = self.admin_service.call(start_node_req).await.unwrap(); // TODO do not unwrap + match create_node_resp { + AdminResponse::StartNode { macaroon: _ } => { + println!("Started node: {}", node_pubkey.as_str()); + () + } + _ => println!("Could not start node: {}", node_pubkey.as_str()), + } + } + Ok(ManagerResponse::StartNodes {}) + } ManagerRequest::OpenChannel { pubkey, connection_string, @@ -189,11 +226,6 @@ impl ManagerService { } .unwrap(); - self.internal_node_map - .lock() - .await - .push(new_node.pubkey.clone()); - // Then get an address for the node let node_directory = self.admin_service.node_directory.lock().await; let node = match node_directory.get(&new_node.pubkey) { @@ -247,7 +279,7 @@ impl ManagerService { public: false, }; let channel_req = NodeRequest::OpenChannels { - channels: vec![channel], + channels: vec![channel.clone()], }; let channel_resp = node_spawn.call(channel_req).await.unwrap(); @@ -261,10 +293,18 @@ impl ManagerService { .unwrap(); // TODO map channel id to an internal id - println!("channel_id: {}", channel_id); - map.lock() - .await - .insert(internal_channel_id_copy.to_string(), channel_id); + println!( + "internal_channel_id: {},channel_id: {}", + internal_channel_id_copy, channel_id + ); + map.lock().await.insert( + internal_channel_id_copy.to_string(), + PendingChannelDetails { + from_node: node_spawn.get_pubkey(), + to_node: pubkey, + amt: channel.clone().amt_satoshis, + }, + ); return; } // TODO delete @@ -278,21 +318,21 @@ impl ManagerService { address, }) } - // TODO ManagerRequest::GetChannel { id } => { // find the id in the map let channel_map = self.internal_channel_id_map.lock().await; - let channel_id = channel_map.get(&id); - if let Some(_chan_id) = channel_id { + let pending_channel_deatils = channel_map.get(&id); + if let Some(pending_channel_deatils) = pending_channel_deatils { // for each node in our pubkey list, check the channel status - let node_list = self.internal_node_map.lock().await; let node_directory = self.admin_service.node_directory.lock().await; + for (node_pubkey, node_handle) in node_directory.iter() { + if pending_channel_deatils.from_node != String::from(node_pubkey) { + continue; + } - for node_pubkey in node_list.iter() { - // find the node - let node = match node_directory.get(node_pubkey) { - Some(Some(node_handle)) => Ok(node_handle.node.clone()), - _ => Err("node not found"), + let node = match node_handle { + Some(node) => Some(node.node.clone()), + None => None, } .unwrap(); @@ -305,47 +345,42 @@ impl ManagerService { }, }; let channel_resp = node.call(channel_req).await.unwrap(); - let channel_result = match channel_resp { + let channel_status = match channel_resp { senseicore::services::node::NodeResponse::ListChannels { channels, pagination: _, } => { - dbg!(channels.clone()); + let mut status = "not_found"; for channel in channels { - /* - dbg!(channel.channel_id.clone()); - dbg!(String::from(chan_id)); - dbg!(channel.channel_id.clone() == String::from(chan_id)); - if channel.channel_id == String::from(chan_id) { + dbg!(channel.clone()); + if channel.clone().channel_value_satoshis + == pending_channel_deatils.amt + && channel.clone().counterparty_pubkey + == pending_channel_deatils.to_node + { if channel.is_funding_locked { - return Ok(ManagerResponse::GetChannel { - status: "good".to_string(), - }); + status = "good"; + } else { + status = "pending"; } } - */ - - // TODO only looking for single channel - // sense I can't come back and look for - // channel id based on temporary sensei id - if channel.is_funding_locked { - return Ok(ManagerResponse::GetChannel { - status: "good".to_string(), - }); - } - continue; } + Some(status) + } + _ => None, + }; + + // if no good or pending status, then go to next node + if let Some(status) = channel_status { + if status == "good" { + return Ok(ManagerResponse::GetChannel { + status: "good".to_string(), + }); + } else if status == "pending" { return Ok(ManagerResponse::GetChannel { status: "pending".to_string(), }); } - _ => Some(ManagerResponse::GetChannel { - status: "pending".to_string(), - }), - }; - - if channel_result.is_some() { - return Ok(channel_result.unwrap()); } } @@ -354,36 +389,36 @@ impl ManagerService { }) } else { Ok(ManagerResponse::GetChannel { - status: "pending".to_string(), + status: "bad".to_string(), }) } } - // TODO ManagerRequest::SendPayment { invoice } => { // for each node in our pubkey list, attempt payment - let node_list = self.internal_node_map.lock().await; let node_directory = self.admin_service.node_directory.lock().await; - - for node_pubkey in node_list.iter() { - // find the node - let node = match node_directory.get(node_pubkey) { - Some(Some(node_handle)) => Ok(node_handle.node.clone()), - _ => Err("node not found"), + for (_node_pubkey, node_handle) in node_directory.iter() { + let node = match node_handle { + Some(node) => Some(node.node.clone()), + None => None, } .unwrap(); - let balance_req = NodeRequest::SendPayment { + let send_payment_req = NodeRequest::SendPayment { invoice: invoice.clone(), }; - let balance_resp = node.call(balance_req).await.unwrap(); - let status = match balance_resp { - senseicore::services::node::NodeResponse::SendPayment {} => "pending", - _ => "bad", - }; - if status == "pending" { - return Ok(ManagerResponse::SendPayment { - status: "pending".to_string(), - }); + let payment_resp = node.call(send_payment_req).await; + + // Don't fail if couldn't send, try next node in loop + if let Ok(send_payment_resp) = payment_resp { + let status = match send_payment_resp { + senseicore::services::node::NodeResponse::SendPayment {} => "pending", + _ => "bad", + }; + if status == "pending" { + return Ok(ManagerResponse::SendPayment { + status: "pending".to_string(), + }); + } } } @@ -395,20 +430,16 @@ impl ManagerService { ManagerRequest::SendStatus { invoice: _ } => Ok(ManagerResponse::SendStatus { status: "good".to_string(), }), - // TODO ManagerRequest::GetBalance {} => { // get the amount from each of the nodes we have saved let mut amt_satoshis = 0; // for each node in our pubkey list, check the channel status - let node_list = self.internal_node_map.lock().await; let node_directory = self.admin_service.node_directory.lock().await; - - for node_pubkey in node_list.iter() { - // find the node - let node = match node_directory.get(node_pubkey) { - Some(Some(node_handle)) => Ok(node_handle.node.clone()), - _ => Err("node not found"), + for (_node_pubkey, node_handle) in node_directory.iter() { + let node = match node_handle { + Some(node) => Some(node.node.clone()), + None => None, } .unwrap(); diff --git a/src/main.rs b/src/main.rs index f5f62fe..85a250b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -279,8 +279,14 @@ fn main() -> Result<()> { _ => false, }; - // got a node! - println!("node: {}, running: {}", root_node.get_pubkey(), status); + // got a root node! + println!("root node: {}, running: {}", root_node.get_pubkey(), status); + + // now start any nodes we have in our db + let _start_res = manager_service + .clone() + .call(ManagerRequest::StartNodes {}) + .await; let router = Router::new();