-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split WebhookCubit into two services, one for Webhook related logic one for LNURL service related logic. Other changes on ChangeLnAddressUsernameBottomSheet - fix: Use keyboard for email address - fix: remove input formatter - fix: trim the username value
- Loading branch information
1 parent
ea3f023
commit f1c6095
Showing
5 changed files
with
254 additions
and
135 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import 'dart:convert'; | ||
|
||
import 'package:breez_preferences/breez_preferences.dart'; | ||
import 'package:breez_sdk_liquid/breez_sdk_liquid.dart'; | ||
import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; | ||
import 'package:http/http.dart' as http; | ||
import 'package:l_breez/cubit/webhook/webhook_state.dart'; | ||
import 'package:logging/logging.dart'; | ||
|
||
final Logger _logger = Logger('LnUrlPayService'); | ||
|
||
class LnUrlPayService { | ||
static const String lnurlServiceURL = 'https://breez.fun'; | ||
|
||
final BreezSDKLiquid _breezSdkLiquid; | ||
final BreezPreferences _breezPreferences; | ||
|
||
LnUrlPayService(this._breezSdkLiquid, this._breezPreferences); | ||
|
||
Future<Map<String, String>> registerLnurlpay( | ||
WalletInfo walletInfo, | ||
String webhookUrl, { | ||
String? username, | ||
}) async { | ||
final String pubKey = walletInfo.pubkey; | ||
|
||
await _invalidatePreviousWebhookIfNeeded(pubKey, webhookUrl); | ||
|
||
final int currentTime = DateTime.now().millisecondsSinceEpoch ~/ 1000; | ||
|
||
// TODO(erdemyerebasmaz): Utilize user's username(only when user has created a new wallet) | ||
// TODO(erdemyerebasmaz): Handle multiple device setup cases | ||
final String? lnAddressUsername = | ||
(username ?? await _breezPreferences.getProfileName())?.replaceAll(' ', ''); | ||
|
||
final SignMessageResponse? signMessageRes = await _generateSignature( | ||
webhookUrl: webhookUrl, | ||
currentTime: currentTime, | ||
lnAddressUsername: lnAddressUsername, | ||
); | ||
|
||
if (signMessageRes == null) { | ||
throw Exception('Missing signature'); | ||
} | ||
|
||
final Map<String, String> response = await _postWebhookRegistrationRequest( | ||
pubKey: pubKey, | ||
webhookUrl: webhookUrl, | ||
currentTime: currentTime, | ||
lnAddressUsername: lnAddressUsername, | ||
signature: signMessageRes.signature, | ||
); | ||
|
||
await setLnUrlPayKey(webhookUrl: webhookUrl); | ||
return response; | ||
} | ||
|
||
Future<void> _invalidatePreviousWebhookIfNeeded(String pubKey, String webhookUrl) async { | ||
final String? lastUsedLnurlPay = await getLnUrlPayKey(); | ||
if (lastUsedLnurlPay != null && lastUsedLnurlPay != webhookUrl) { | ||
await _invalidateLnurlPay(pubKey, lastUsedLnurlPay); | ||
} | ||
} | ||
|
||
Future<SignMessageResponse?> _generateSignature({ | ||
required String webhookUrl, | ||
required int currentTime, | ||
String? lnAddressUsername, | ||
}) async { | ||
final String username = lnAddressUsername?.isNotEmpty == true ? '-$lnAddressUsername' : ''; | ||
final String message = '$currentTime-$webhookUrl$username'; | ||
|
||
final SignMessageRequest req = SignMessageRequest(message: message); | ||
return _breezSdkLiquid.instance?.signMessage(req: req); | ||
} | ||
|
||
Future<Map<String, String>> _postWebhookRegistrationRequest({ | ||
required String pubKey, | ||
required String webhookUrl, | ||
required int currentTime, | ||
required String signature, | ||
String? lnAddressUsername, | ||
}) async { | ||
final String lnurlWebhookUrl = '$lnurlServiceURL/lnurlpay/$pubKey'; | ||
final Uri uri = Uri.parse(lnurlWebhookUrl); | ||
|
||
final http.Response jsonResponse = await http.post( | ||
uri, | ||
body: jsonEncode( | ||
AddWebhookRequest( | ||
time: currentTime, | ||
webhookUrl: webhookUrl, | ||
username: lnAddressUsername, | ||
signature: signature, | ||
).toJson(), | ||
), | ||
); | ||
|
||
if (jsonResponse.statusCode == 200) { | ||
final Map<String, dynamic> data = jsonDecode(jsonResponse.body); | ||
final String lnurl = data['lnurl']; | ||
final String lnAddress = data.containsKey('lightning_address') ? data['lightning_address'] : ''; | ||
_logger.info('Registered LnUrl Webhook: $webhookUrl, lnurl = $lnurl, lnAddress = $lnAddress'); | ||
return <String, String>{ | ||
'lnurl': lnurl, | ||
'lnAddress': lnAddress, | ||
}; | ||
} else { | ||
// TODO(erdemyerebasmaz): Handle username conflicts(only when user has created a new wallet) | ||
// Add a random four-digit identifier, a discriminator, as a suffix if user's username is taken(~1/600 probability of conflict) | ||
// Add a retry & randomizer logic until first registration succeeds | ||
// TODO(erdemyerebasmaz): Handle custom username conflicts | ||
throw Exception('Failed to register LnUrl Webhook: ${jsonResponse.body}'); | ||
} | ||
} | ||
|
||
Future<void> updateLnAddressUsername(WalletInfo walletInfo, String username) async { | ||
final String? webhookUrl = await getLnUrlPayKey(); | ||
if (webhookUrl != null) { | ||
await registerLnurlpay(walletInfo, webhookUrl, username: username); | ||
} | ||
} | ||
|
||
Future<void> _invalidateLnurlPay(String pubKey, String toInvalidate) async { | ||
final String lnurlWebhookUrl = '$lnurlServiceURL/lnurlpay/$pubKey'; | ||
final int currentTime = DateTime.now().millisecondsSinceEpoch ~/ 1000; | ||
final SignMessageRequest req = SignMessageRequest(message: '$currentTime-$toInvalidate'); | ||
final SignMessageResponse? signMessageRes = _breezSdkLiquid.instance?.signMessage(req: req); | ||
if (signMessageRes == null) { | ||
throw Exception('Missing signature'); | ||
} | ||
final Uri uri = Uri.parse(lnurlWebhookUrl); | ||
final http.Response response = await http.delete( | ||
uri, | ||
body: jsonEncode( | ||
RemoveWebhookRequest( | ||
time: currentTime, | ||
webhookUrl: toInvalidate, | ||
signature: signMessageRes.signature, | ||
).toJson(), | ||
), | ||
); | ||
_logger.info('invalidate lnurl pay response: ${response.statusCode}'); | ||
await resetLnUrlPayKey(); | ||
} | ||
|
||
Future<void> setLnUrlPayKey({required String webhookUrl}) async { | ||
return await _breezPreferences.setLnUrlPayKey(webhookUrl); | ||
} | ||
|
||
Future<String?> getLnUrlPayKey() async { | ||
return await _breezPreferences.getLnUrlPayKey(); | ||
} | ||
|
||
Future<void> resetLnUrlPayKey() async { | ||
return await _breezPreferences.resetLnUrlPayKey(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,162 +1,84 @@ | ||
import 'dart:convert'; | ||
|
||
import 'package:breez_preferences/breez_preferences.dart'; | ||
import 'package:breez_sdk_liquid/breez_sdk_liquid.dart'; | ||
import 'package:firebase_notifications_client/firebase_notifications_client.dart'; | ||
import 'package:flutter/foundation.dart'; | ||
import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; | ||
import 'package:http/http.dart' as http; | ||
import 'package:hydrated_bloc/hydrated_bloc.dart'; | ||
import 'package:l_breez/cubit/cubit.dart'; | ||
import 'package:logging/logging.dart'; | ||
|
||
export 'lnurl_pay_service.dart'; | ||
export 'webhook_service.dart'; | ||
export 'webhook_state.dart'; | ||
|
||
final Logger _logger = Logger('WebhookCubit'); | ||
|
||
class WebhookCubit extends Cubit<WebhookState> { | ||
static const String notifierServiceURL = 'https://notifier.breez.technology'; | ||
static const String lnurlServiceURL = 'https://breez.fun'; | ||
|
||
final BreezSDKLiquid _breezSdkLiquid; | ||
final BreezPreferences _breezPreferences; | ||
final NotificationsClient _notifications; | ||
final WebhookService _webhookService; | ||
final LnUrlPayService _lnUrlPayService; | ||
|
||
WebhookCubit( | ||
this._breezSdkLiquid, | ||
this._breezPreferences, | ||
this._notifications, | ||
) : super(WebhookState()) { | ||
WebhookCubit(this._breezSdkLiquid, this._webhookService, this._lnUrlPayService) : super(WebhookState()) { | ||
_breezSdkLiquid.walletInfoStream.first.then( | ||
(GetInfoResponse getInfoResponse) => refreshWebhooks(walletInfo: getInfoResponse.walletInfo), | ||
); | ||
} | ||
|
||
Future<void> refreshWebhooks({WalletInfo? walletInfo, String? username}) async { | ||
_logger.info('Refreshing webhooks'); | ||
_logger.info('Refreshing Webhooks'); | ||
emit(WebhookState(isLoading: true)); | ||
try { | ||
walletInfo = walletInfo ?? (await _breezSdkLiquid.instance?.getInfo())?.walletInfo; | ||
if (walletInfo != null) { | ||
await _registerWebhooks(walletInfo, username: username); | ||
final String webhookUrl = await _webhookService.generateWebhookURL(); | ||
await _webhookService.registerWebhook(webhookUrl); | ||
final Map<String, String> lnUrlData = await _lnUrlPayService.registerLnurlpay( | ||
walletInfo, | ||
webhookUrl, | ||
username: username, | ||
); | ||
emit( | ||
WebhookState( | ||
lnurlPayUrl: lnUrlData['lnurl'], | ||
lnAddress: lnUrlData['lnAddress'], | ||
), | ||
); | ||
} else { | ||
throw Exception('Unable to retrieve wallet information.'); | ||
} | ||
} catch (err) { | ||
_logger.warning('Failed to refresh lnurlpay: $err'); | ||
_logger.warning('Failed to refresh webhooks: $err'); | ||
emit( | ||
state.copyWith( | ||
WebhookState( | ||
lnurlPayErrorTitle: 'Failed to refresh Lightning Address:', | ||
lnurlPayError: err.toString(), | ||
), | ||
); | ||
} finally { | ||
emit(state.copyWith(isLoading: false)); | ||
} | ||
} | ||
|
||
Future<void> _registerWebhooks(WalletInfo walletInfo, {String? username}) async { | ||
Future<void> updateLnAddressUsername({required String username}) async { | ||
emit(WebhookState(isLoading: true)); | ||
try { | ||
final String webhookUrl = await _generateWebhookURL(); | ||
await _breezSdkLiquid.instance?.registerWebhook(webhookUrl: webhookUrl); | ||
_logger.info('SDK webhook registered: $webhookUrl'); | ||
await _registerLnurlpay(walletInfo, webhookUrl, username: username); | ||
final GetInfoResponse? walletInfo = await _breezSdkLiquid.instance?.getInfo(); | ||
if (walletInfo != null) { | ||
await _lnUrlPayService.updateLnAddressUsername(walletInfo.walletInfo, username); | ||
final Map<String, String> lnUrlData = await _lnUrlPayService.registerLnurlpay( | ||
walletInfo.walletInfo, | ||
await _lnUrlPayService.getLnUrlPayKey() ?? '', | ||
username: username, | ||
); | ||
emit( | ||
WebhookState( | ||
lnurlPayUrl: lnUrlData['lnurl'], | ||
lnAddress: lnUrlData['lnAddress'], | ||
), | ||
); | ||
} | ||
} catch (err) { | ||
_logger.warning('Failed to register webhooks: $err'); | ||
emit(state.copyWith(lnurlPayErrorTitle: 'Failed to register webhooks:', lnurlPayError: err.toString())); | ||
rethrow; | ||
} | ||
} | ||
|
||
// TODO(erdemyerebasmaz): Make this a public method so that it can be used to customize LN Addresses | ||
// TODO(erdemyerebasmaz): Currently the only endpoint generates a webhook URL & registers to it beforehand, which is not necessary for customizing username | ||
Future<void> _registerLnurlpay( | ||
WalletInfo walletInfo, | ||
String webhookUrl, { | ||
String? username, | ||
}) async { | ||
final String? lastUsedLnurlPay = await _breezPreferences.getLnUrlPayKey(); | ||
if (lastUsedLnurlPay != null && lastUsedLnurlPay != webhookUrl) { | ||
await _invalidateLnurlPay(walletInfo, lastUsedLnurlPay); | ||
} | ||
// TODO(erdemyerebasmaz): Utilize user's username(only when user has created a new wallet) | ||
// TODO(erdemyerebasmaz): Handle multiple device setup cases | ||
String? lnAddressUsername = username ?? await _breezPreferences.getProfileName(); | ||
lnAddressUsername = lnAddressUsername?.replaceAll(' ', ''); | ||
final int currentTime = DateTime.now().millisecondsSinceEpoch ~/ 1000; | ||
final String optionalUsernameKey = lnAddressUsername != null ? '-$lnAddressUsername' : ''; | ||
final SignMessageRequest req = | ||
SignMessageRequest(message: '$currentTime-$webhookUrl$optionalUsernameKey'); | ||
final SignMessageResponse? signMessageRes = _breezSdkLiquid.instance?.signMessage(req: req); | ||
if (signMessageRes == null) { | ||
throw Exception('Missing signature'); | ||
} | ||
final String lnurlWebhookUrl = '$lnurlServiceURL/lnurlpay/${walletInfo.pubkey}'; | ||
final Uri uri = Uri.parse(lnurlWebhookUrl); | ||
final http.Response jsonResponse = await http.post( | ||
uri, | ||
body: jsonEncode( | ||
AddWebhookRequest( | ||
time: currentTime, | ||
webhookUrl: webhookUrl, | ||
username: lnAddressUsername, | ||
signature: signMessageRes.signature, | ||
).toJson(), | ||
), | ||
); | ||
if (jsonResponse.statusCode == 200) { | ||
final Map<String, dynamic> data = jsonDecode(jsonResponse.body); | ||
final String lnurl = data['lnurl']; | ||
final String lnAddress = data.containsKey('lightning_address') ? data['lightning_address'] : ''; | ||
_logger.info('lnurlpay webhook registered: $webhookUrl, lnurl = $lnurl, lnAddress = $lnAddress'); | ||
await _breezPreferences.setLnUrlPayKey(webhookUrl); | ||
emit(WebhookState(lnurlPayUrl: lnurl, lnAddress: lnAddress)); | ||
} else { | ||
// TODO(erdemyerebasmaz): Handle username conflicts(only when user has created a new wallet) | ||
// Add a random four-digit identifier, a discriminator, as a suffix if user's username is taken(~1/600 probability of conflict) | ||
// Add a retry & randomizer logic until first registration succeeds | ||
// TODO(erdemyerebasmaz): Handle custom username conflicts | ||
throw jsonResponse.body; | ||
} | ||
} | ||
|
||
Future<void> _invalidateLnurlPay( | ||
WalletInfo walletInfo, | ||
String toInvalidate, | ||
) async { | ||
final String lnurlWebhookUrl = '$lnurlServiceURL/lnurlpay/${walletInfo.pubkey}'; | ||
final int currentTime = DateTime.now().millisecondsSinceEpoch ~/ 1000; | ||
final SignMessageRequest req = SignMessageRequest(message: '$currentTime-$toInvalidate'); | ||
final SignMessageResponse? signMessageRes = _breezSdkLiquid.instance?.signMessage(req: req); | ||
if (signMessageRes == null) { | ||
throw Exception('Missing signature'); | ||
} | ||
final Uri uri = Uri.parse(lnurlWebhookUrl); | ||
final http.Response response = await http.delete( | ||
uri, | ||
body: jsonEncode( | ||
RemoveWebhookRequest( | ||
time: currentTime, | ||
webhookUrl: toInvalidate, | ||
signature: signMessageRes.signature, | ||
).toJson(), | ||
), | ||
); | ||
_logger.info('invalidate lnurl pay response: ${response.statusCode}'); | ||
await _breezPreferences.resetLnUrlPayKey(); | ||
} | ||
|
||
Future<String> _generateWebhookURL() async { | ||
final String? token = await _notifications.getToken(); | ||
_logger.info('Retrieved token, registering…'); | ||
final String platform = defaultTargetPlatform == TargetPlatform.iOS | ||
? 'ios' | ||
: defaultTargetPlatform == TargetPlatform.android | ||
? 'android' | ||
: ''; | ||
if (platform.isEmpty) { | ||
throw Exception('Notifications for platform is not supported'); | ||
emit( | ||
WebhookState( | ||
lnurlPayErrorTitle: 'Failed to update Lightning Address username:', | ||
lnurlPayError: err.toString(), | ||
), | ||
); | ||
} | ||
return '$notifierServiceURL/api/v1/notify?platform=$platform&token=$token'; | ||
} | ||
} |
Oops, something went wrong.