Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle LN Address username conflicts on new wallet registration flow #344

Merged
merged 29 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
58187b8
Register user profile name as username for lightning address
dangeross Jan 10, 2025
7b86b7a
Decouple users Display Name from their LN Address username
dangeross Jan 10, 2025
81866b8
Allow registering with a custom username
erdemyerebasmaz Jan 16, 2025
a0f6d34
Save both LNURLp & LN Address on WebhookState
erdemyerebasmaz Jan 16, 2025
beddf91
Display LN Address on "Receive via LN Address" page
erdemyerebasmaz Jan 16, 2025
c527e33
Add TODO comments for LN Address
erdemyerebasmaz Jan 16, 2025
f937e79
Refactor WebhookCubit
erdemyerebasmaz Jan 21, 2025
1bab410
Differentiate webhook & lnurlpay service errors
erdemyerebasmaz Jan 23, 2025
6c158c0
Wait for update to finish before popping ln address sheet
erdemyerebasmaz Jan 23, 2025
5ae1521
convert username to lowercase and trim whitespace
erdemyerebasmaz Jan 23, 2025
8714a29
Return lnurl data from updateLnAddressUsername
erdemyerebasmaz Jan 27, 2025
978c595
fix: correct the error & title value for webhook registration errors
erdemyerebasmaz Jan 27, 2025
3e0b7a6
Store & use the LN Address username when refreshing webhooks
erdemyerebasmaz Jan 27, 2025
7119707
Store LN Address Username on secure storage
erdemyerebasmaz Jan 27, 2025
5bfd299
[WIP] Refactor WebhookCubit & replace it with LnAddressCubit
erdemyerebasmaz Jan 27, 2025
104edb8
Validate username length for LN Address
erdemyerebasmaz Jan 28, 2025
395c566
chore: renaming to be consistent with LNURL service
erdemyerebasmaz Jan 28, 2025
69beaaa
chore: rename webhook registration status on secure storage
erdemyerebasmaz Jan 28, 2025
0a97d35
fix: convert sanitezed username to lowercase & remove all whitespace
erdemyerebasmaz Jan 28, 2025
4d3eab3
add error handling on WebhookService
erdemyerebasmaz Jan 28, 2025
99799e6
refactor LnUrlPayService: extract retry logic to a function
erdemyerebasmaz Jan 28, 2025
be54971
refactor LnAddressCubit to improve readability
erdemyerebasmaz Jan 28, 2025
2e1c848
fix: use the same timestamp for generating the signature & registrati…
erdemyerebasmaz Jan 28, 2025
718b17d
refactor: Remove unused values on BreezPreferences
erdemyerebasmaz Jan 28, 2025
fb3acbf
Add TODO comment for _registerWithRetries
erdemyerebasmaz Jan 29, 2025
2b036ee
refactor: improve readability of setupLightningAddress function
erdemyerebasmaz Jan 29, 2025
89df12f
refactor: improve readability of _resolveUsername
erdemyerebasmaz Jan 29, 2025
72701f4
docs: Add docstrings for LnAddressCubit functions
erdemyerebasmaz Jan 29, 2025
af55603
Rename GetInfoResponse variables for consistency (#341)
erdemyerebasmaz Jan 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,10 @@ class App extends StatelessWidget {
),
),
BlocProvider<UserProfileCubit>(
create: (BuildContext context) => UserProfileCubit(),
create: (BuildContext context) => UserProfileCubit(injector.breezPreferences),
),
BlocProvider<WebhookCubit>(
lazy: false,
create: (BuildContext context) => WebhookCubit(
injector.breezSdkLiquid,
injector.breezPreferences,
injector.notifications,
),
BlocProvider<LnAddressCubit>(
create: (BuildContext context) => LnAddressCubitFactory.create(injector),
),
BlocProvider<CurrencyCubit>(
create: (BuildContext context) => CurrencyCubit(injector.breezSdkLiquid),
Expand Down
2 changes: 1 addition & 1 deletion lib/cubit/cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ export 'connectivity/connectivity_cubit.dart';
export 'csv_exporter.dart';
export 'currency/currency_cubit.dart';
export 'input/input_cubit.dart';
export 'ln_address/ln_address_cubit.dart';
export 'lnurl/lnurl_cubit.dart';
export 'model/models.dart';
export 'payment_limits/payment_limits_cubit.dart';
export 'payments/payments_cubit.dart';
export 'refund/refund_cubit.dart';
export 'security/security_cubit.dart';
export 'user_profile/user_profile_cubit.dart';
export 'webhook/webhook_cubit.dart';
256 changes: 256 additions & 0 deletions lib/cubit/ln_address/ln_address_cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import 'package:breez_preferences/breez_preferences.dart';
import 'package:breez_sdk_liquid/breez_sdk_liquid.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_breez_liquid/flutter_breez_liquid.dart';
import 'package:l_breez/cubit/cubit.dart';
import 'package:logging/logging.dart';
import 'package:service_injector/service_injector.dart';

export 'ln_address_state.dart';
export 'models/models.dart';
export 'services/services.dart';
export 'utils/utils.dart';

final Logger _logger = Logger('LnAddressCubit');

class LnAddressCubitFactory {
static LnAddressCubit create(ServiceInjector injector) {
final BreezSDKLiquid breezSdkLiquid = injector.breezSdkLiquid;
final BreezPreferences breezPreferences = injector.breezPreferences;

final WebhookService webhookService = WebhookService(breezSdkLiquid, injector.notifications);

return LnAddressCubit(
breezSdkLiquid: breezSdkLiquid,
breezPreferences: breezPreferences,
lnAddressService: LnUrlPayService(),
webhookService: webhookService,
);
}
}

class LnAddressCubit extends Cubit<LnAddressState> {
final BreezSDKLiquid breezSdkLiquid;
final BreezPreferences breezPreferences;
final LnUrlPayService lnAddressService;
final WebhookService webhookService;

LnAddressCubit({
required this.breezSdkLiquid,
required this.breezPreferences,
required this.lnAddressService,
required this.webhookService,
}) : super(const LnAddressState());

/// Sets up or updates the Lightning Address.
///
/// - If [baseUsername] is provided, the function updates the Lightning Address username.
/// - Otherwise, it initializes a new Lightning Address or refreshes an existing one.
Future<void> setupLightningAddress({String? baseUsername}) async {
final bool isUpdating = baseUsername != null;
final String actionMessage =
isUpdating ? 'Update LN Address Username to: $baseUsername' : 'Setup Lightning Address';
_logger.info(actionMessage);

emit(
state.copyWith(
status: isUpdating ? state.status : LnAddressStatus.loading,
updateStatus: isUpdating ? const LnAddressUpdateStatus(status: UpdateStatus.loading) : null,
),
);

try {
final RegisterLnurlPayResponse registrationResponse = await _setupAndRegisterLnAddress(
baseUsername: baseUsername,
);

emit(
state.copyWith(
status: LnAddressStatus.success,
lnurl: registrationResponse.lnurl,
lnAddress: registrationResponse.lightningAddress,
updateStatus: isUpdating ? const LnAddressUpdateStatus(status: UpdateStatus.success) : null,
),
);
} catch (e, stackTrace) {
_logger.severe('Failed to $actionMessage', e, stackTrace);
final LnAddressStatus status = isUpdating ? state.status : LnAddressStatus.error;
final Object? error = isUpdating ? null : e;
final LnAddressUpdateStatus? updateStatus =
isUpdating ? _createErrorUpdateStatus(e, actionMessage) : null;
emit(
state.copyWith(
status: status,
error: error,
updateStatus: updateStatus,
),
);
}
}

LnAddressUpdateStatus _createErrorUpdateStatus(Object e, String action) {
final String errorMessage = e is RegisterLnurlPayException
? (e.responseBody?.isNotEmpty == true ? e.responseBody! : e.message)
: e is UsernameConflictException
? e.toString()
: 'Failed to $action';

return LnAddressUpdateStatus(
status: UpdateStatus.error,
error: e,
errorMessage: errorMessage,
);
}

/// Registers or updates an LNURL webhook for a Lightning Address.
///
/// - If [baseUsername] is provided, it updates the existing registration.
/// - Otherwise, it determines a suitable username and registers a new webhook.
Future<RegisterLnurlPayResponse> _setupAndRegisterLnAddress({String? baseUsername}) async {
final WalletInfo walletInfo = await _getWalletInfo();
final String webhookUrl = await _setupWebhook(walletInfo.pubkey);
final String? username = baseUsername ?? await _resolveUsername();
final int time = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final String signature = await _generateWebhookSignature(time, webhookUrl, username);
final RegisterLnurlPayRequest request = RegisterLnurlPayRequest(
time: time,
webhookUrl: webhookUrl,
signature: signature,
username: username,
);
return await _registerLnurlWebhook(
pubKey: walletInfo.pubkey,
request: request,
);
}

Future<WalletInfo> _getWalletInfo() async {
final WalletInfo? walletInfo = (await breezSdkLiquid.instance?.getInfo())?.walletInfo;
if (walletInfo == null) {
throw Exception('Failed to retrieve wallet info');
}
return walletInfo;
}

/// Sets up a webhook for the given public key.
/// - Generates a new webhook URL, unregisters any existing webhook if needed,
/// - Registers the new webhook, and stores the webhook URL in preferences.
Future<String> _setupWebhook(String pubKey) async {
_logger.info('Setting up webhook');
final String webhookUrl = await webhookService.generateWebhookUrl();
await _unregisterExistingWebhookIfNeeded(pubKey: pubKey, webhookUrl: webhookUrl);
await webhookService.register(webhookUrl);
await breezPreferences.setWebhookUrl(webhookUrl);
return webhookUrl;
}

/// Checks if there is an existing webhook URL and unregisters it if different from the provided one.
Future<void> _unregisterExistingWebhookIfNeeded({
required String pubKey,
required String webhookUrl,
}) async {
final String? existingWebhook = await breezPreferences.webhookUrl;
if (existingWebhook != null && existingWebhook != webhookUrl) {
_logger.info('Unregistering existing webhook: $existingWebhook');
await _unregisterWebhook(existingWebhook, pubKey);
breezPreferences.removeWebhookUrl();
}
}

/// Unregisters a webhook for a given public key.
Future<void> _unregisterWebhook(String webhookUrl, String pubKey) async {
final int time = DateTime.now().millisecondsSinceEpoch ~/ 1000;

final String message = '$time-$webhookUrl';
final String signature = await _signMessage(message);

final UnregisterLnurlPayRequest invalidateWebhookRequest = UnregisterLnurlPayRequest(
time: time,
webhookUrl: webhookUrl,
signature: signature,
);

await lnAddressService.unregister(pubKey, invalidateWebhookRequest);
}

/// Signs the given message with the private key.
Future<String> _signMessage(String message) async {
_logger.info('Signing message: $message');

final SignMessageResponse? signMessageRes = breezSdkLiquid.instance?.signMessage(
req: SignMessageRequest(message: message),
);

if (signMessageRes == null) {
throw Exception('Failed to sign message');
}

_logger.info('Successfully signed message');
return signMessageRes.signature;
}

/// Resolves the appropriate username for LNURL registration.
///
/// - If the webhook is not yet registered, it utilizes default profile name as username.
/// - If the webhook is already registered, it retrieves the stored username from [BreezPreferences].
Future<String?> _resolveUsername() async {
final bool isLnUrlWebhookRegistered = await breezPreferences.isLnUrlWebhookRegistered;

if (!isLnUrlWebhookRegistered) {
final String? profileName = await breezPreferences.profileName;
final String formattedUsername = UsernameFormatter.formatDefaultProfileName(profileName);
_logger.info('Registering LNURL Webhook: Using formatted profile name: $formattedUsername');
return formattedUsername;
}

// TODO(erdemyerebasmaz): Add null-handling to revert to the profile name if the stored username is null.
final String? storedUsername = await breezPreferences.lnAddressUsername;
_logger.info('Refreshing LNURL Webhook: Using stored username: $storedUsername');
return storedUsername;
}

/// Signs a webhook request message for authentication and validation purposes.
Future<String> _generateWebhookSignature(int time, String webhookUrl, String? username) async {
_logger.info('Generating webhook signature');
final String usernameComponent = username?.isNotEmpty == true ? '-$username' : '';
final String message = '$time-$webhookUrl$usernameComponent';
final String signature = await _signMessage(message);
_logger.info('Successfully generated webhook signature');
return signature;
}

/// Registers an LNURL webhook with the provided public key and request.
///
/// - Saves the username to [BreezPreferences] if present and
/// - Sets webhook as registered on [BreezPreferences] if succeeds
Future<RegisterLnurlPayResponse> _registerLnurlWebhook({
required String pubKey,
required RegisterLnurlPayRequest request,
}) async {
final RegisterLnurlPayResponse registrationResponse = await lnAddressService.register(
pubKey: pubKey,
request: request,
);

final String? username = request.username;
if (username != null && username.isNotEmpty) {
await breezPreferences.setLnAddressUsername(username);
}

_logger.info(
'Successfully registered LNURL Webhook: $registrationResponse',
);

await breezPreferences.setLnUrlWebhookRegistered();
return registrationResponse;
}

void clearUpdateStatus() {
_logger.info('Clearing LnAddressUpdateStatus');
emit(
state.copyWith(
updateStatus: const LnAddressUpdateStatus(),
),
);
}
}
35 changes: 35 additions & 0 deletions lib/cubit/ln_address/ln_address_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:l_breez/cubit/cubit.dart';

enum LnAddressStatus { initial, loading, success, error }

class LnAddressState {
final String? lnurl;
final String? lnAddress;
final LnAddressStatus status;
final LnAddressUpdateStatus updateStatus;
final Object? error;

const LnAddressState({
this.lnurl,
this.lnAddress,
this.status = LnAddressStatus.initial,
this.updateStatus = const LnAddressUpdateStatus(),
this.error,
});

LnAddressState copyWith({
String? lnurl,
String? lnAddress,
LnAddressStatus? status,
LnAddressUpdateStatus? updateStatus,
Object? error,
}) {
return LnAddressState(
lnurl: lnurl ?? this.lnurl,
lnAddress: lnAddress ?? this.lnAddress,
status: status ?? this.status,
updateStatus: updateStatus ?? this.updateStatus,
error: error ?? this.error,
);
}
}
48 changes: 48 additions & 0 deletions lib/cubit/ln_address/models/exceptions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
class RegisterLnurlPayException implements Exception {
final String message;
final int? statusCode;
final String? responseBody;

RegisterLnurlPayException(this.message, {this.statusCode, this.responseBody});

@override
String toString() =>
'RegisterLnurlPayException: $message${statusCode != null ? ' (Status: $statusCode)' : ''}';
}

class UnregisterLnurlPayException implements Exception {
final String message;

UnregisterLnurlPayException(this.message);

@override
String toString() => message;
}

class UsernameConflictException implements Exception {
@override
String toString() => 'Username is already taken';
}

class MaxRetriesExceededException implements Exception {
@override
String toString() => 'Maximum retry attempts exceeded';
}

class GenerateWebhookUrlException implements Exception {
final String message;

GenerateWebhookUrlException(this.message);

@override
String toString() => message;
}

class RegisterWebhookException implements Exception {
final String message;

RegisterWebhookException(this.message);

@override
String toString() => message;
}
25 changes: 25 additions & 0 deletions lib/cubit/ln_address/models/ln_address_update_status.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
enum UpdateStatus { initial, loading, success, error }

class LnAddressUpdateStatus {
final UpdateStatus status;
final Object? error;
final String? errorMessage;

const LnAddressUpdateStatus({
this.status = UpdateStatus.initial,
this.error,
this.errorMessage,
});

LnAddressUpdateStatus copyWith({
UpdateStatus? status,
Object? error,
String? errorMessage,
}) {
return LnAddressUpdateStatus(
status: status ?? this.status,
error: error ?? this.error,
errorMessage: errorMessage ?? this.errorMessage,
);
}
}
Loading