Skip to content

Commit

Permalink
Trigger send to BTC address flow when camera reads a BTC address
Browse files Browse the repository at this point in the history
  • Loading branch information
ademar111190 committed Oct 10, 2023
1 parent d4e9e5f commit e7451d7
Show file tree
Hide file tree
Showing 11 changed files with 365 additions and 133 deletions.
114 changes: 41 additions & 73 deletions lib/bloc/input/input_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import 'dart:async';

import 'package:breez_sdk/breez_sdk.dart';
import 'package:breez_sdk/bridge_generated.dart';
import 'package:c_breez/bloc/input/input_data.dart';
import 'package:c_breez/bloc/input/input_printer.dart';
import 'package:c_breez/bloc/input/input_source.dart';
import 'package:c_breez/bloc/input/input_state.dart';
import 'package:c_breez/models/invoice.dart';
import 'package:c_breez/services/device.dart';
Expand All @@ -15,14 +18,16 @@ class InputBloc extends Cubit<InputState> {
final BreezSDK _breezLib;
final LightningLinksService _lightningLinks;
final Device _device;
final InputPrinter _printer;

final _decodeInvoiceController = StreamController<String>();
final _decodeInvoiceController = StreamController<InputData>();

InputBloc(
this._breezLib,
this._lightningLinks,
this._device,
) : super(InputState()) {
this._printer,
) : super(const InputState.empty()) {
_initializeInputBloc();
}

Expand All @@ -32,9 +37,9 @@ class InputBloc extends Cubit<InputState> {
_watchIncomingInvoices().listen((inputState) => emit(inputState!));
}

void addIncomingInput(String bolt11) {
_log.fine("addIncomingInput: $bolt11");
_decodeInvoiceController.add(bolt11);
void addIncomingInput(String bolt11, InputSource source) {
_log.fine("addIncomingInput: $bolt11 source: $source");
_decodeInvoiceController.add(InputData(data: bolt11, source: source));
}

Future trackPayment(String paymentHash) async {
Expand All @@ -48,104 +53,67 @@ class InputBloc extends Cubit<InputState> {
Stream<InputState?> _watchIncomingInvoices() {
return Rx.merge([
_decodeInvoiceController.stream.doOnData((event) => _log.fine("decodeInvoiceController: $event")),
_lightningLinks.linksNotifications.doOnData((event) => _log.fine("lightningLinks: $event")),
_device.clipboardStream.distinct().skip(1).doOnData((event) => _log.fine("clipboardStream: $event")),
_lightningLinks.linksNotifications
.map((data) => InputData(data: data, source: InputSource.hyperlink))
.doOnData((event) => _log.fine("lightningLinks: $event")),
_device.clipboardStream.distinct().skip(1)
.map((data) => InputData(data: data, source: InputSource.clipboard))
.doOnData((event) => _log.fine("clipboardStream: $event")),
]).asyncMap((input) async {
_log.fine("Incoming input: '$input'");
// Emit an empty InputState with isLoading to display a loader on UI layer
emit(InputState(isLoading: true));
emit(const InputState.loading());
try {
final parsedInput = await parseInput(input: input);
// Todo: Merge these functions w/o sacrificing readability
_logParsedInput(parsedInput);
return await _handleParsedInput(parsedInput);
final parsedInput = await parseInput(input: input.data);
return await _handleParsedInput(parsedInput, input.source);
} catch (e) {
_log.severe("Failed to parse input", e);
return InputState(isLoading: false);
return const InputState.empty();
}
});
}

Future<InputState> handlePaymentRequest({required InputType_Bolt11 inputData}) async {
Future<InputState> handlePaymentRequest(InputType_Bolt11 inputData, InputSource source) async {
final LNInvoice lnInvoice = inputData.invoice;

NodeState? nodeState = await _breezLib.nodeInfo();
if (nodeState == null || nodeState.id == lnInvoice.payeePubkey) {
return InputState(isLoading: false);
return const InputState.empty();
}
var invoice = Invoice(
final invoice = Invoice(
bolt11: lnInvoice.bolt11,
paymentHash: lnInvoice.paymentHash,
description: lnInvoice.description ?? "",
amountMsat: lnInvoice.amountMsat ?? 0,
expiry: lnInvoice.expiry,
);

return InputState(inputData: invoice);
return InputState.invoice(invoice, source);
}

Future<InputState> _handleParsedInput(InputType parsedInput) async {
Future<InputState> _handleParsedInput(InputType parsedInput, InputSource source) async {
_log.fine("handleParsedInput: $source => ${_printer.inputTypeToString(parsedInput)}");
InputState result;
if (parsedInput is InputType_Bolt11) {
return await handlePaymentRequest(inputData: parsedInput);
} else if (parsedInput is InputType_LnUrlPay ||
parsedInput is InputType_LnUrlWithdraw ||
parsedInput is InputType_LnUrlAuth ||
parsedInput is InputType_LnUrlError ||
parsedInput is InputType_NodeId) {
return InputState(inputData: parsedInput);
} else {
return InputState(isLoading: false);
}
}

void _logParsedInput(InputType parsedInput) {
// Todo: Find a better way to serialize parsed input
_log.fine("Parsed input type: '${parsedInput.runtimeType.toString()}");
if (parsedInput is InputType_Bolt11) {
final lnInvoice = parsedInput.invoice;
_log.info(
"bolt11: ${lnInvoice.bolt11}\n"
"payeePubkey: ${lnInvoice.payeePubkey}\n"
"paymentHash: ${lnInvoice.paymentHash}\n"
"description: ${lnInvoice.description}\n"
"descriptionHash: ${lnInvoice.descriptionHash}\n"
"amountMsat: ${lnInvoice.amountMsat}\n"
"timestamp: ${lnInvoice.timestamp}\n"
"expiry: ${lnInvoice.expiry}\n"
"routingHints: ${lnInvoice.routingHints}\n"
"paymentSecret: ${lnInvoice.paymentSecret}",
);
result = await handlePaymentRequest(parsedInput, source);
} else if (parsedInput is InputType_LnUrlPay) {
final lnUrlPayReqData = parsedInput.data;
_log.info(
"${lnUrlPayReqData.toString()}\n"
"callback: ${lnUrlPayReqData.callback}\n"
"minSendable: ${lnUrlPayReqData.minSendable}\n"
"maxSendable: ${lnUrlPayReqData.maxSendable}\n"
"metadataStr: ${lnUrlPayReqData.metadataStr}\n"
"commentAllowed: ${lnUrlPayReqData.commentAllowed}\n"
"domain: ${lnUrlPayReqData.domain}",
);
result = InputState.lnUrlPay(parsedInput.data, source);
} else if (parsedInput is InputType_LnUrlWithdraw) {
final lnUrlWithdrawReqData = parsedInput.data;
_log.info(
"callback: ${lnUrlWithdrawReqData.callback}\n"
"k1: ${lnUrlWithdrawReqData.k1}\n"
"defaultDescription: ${lnUrlWithdrawReqData.defaultDescription}\n"
"minWithdrawable: ${lnUrlWithdrawReqData.minWithdrawable}\n"
"maxWithdrawable: ${lnUrlWithdrawReqData.maxWithdrawable}",
);
result = InputState.lnUrlWithdraw(parsedInput.data, source);
} else if (parsedInput is InputType_LnUrlAuth) {
final lnUrlAuthReqData = parsedInput.data;
_log.info("k1: ${lnUrlAuthReqData.k1}");
result = InputState.lnUrlAuth(parsedInput.data, source);
} else if (parsedInput is InputType_LnUrlError) {
final lnUrlErrorData = parsedInput.data;
_log.info("reason: ${lnUrlErrorData.reason}");
result = InputState.lnUrlError(parsedInput.data, source);
} else if (parsedInput is InputType_NodeId) {
_log.info("nodeId: ${parsedInput.nodeId}");
} else if (parsedInput is InputType_Url) {
_log.info("url: ${parsedInput.url}");
result = InputState.nodeId(parsedInput.nodeId, source);
} else if (parsedInput is InputType_BitcoinAddress) {
result = InputState.bitcoinAddress(parsedInput.address, source);
} else if(parsedInput is InputType_Url) {
result = InputState.url(parsedInput.url, source);
} else {
result = const InputState.empty();
}
_log.fine("handleParsedInput: result: $result");
return result;
}

Future<InputType> parseInput({required String input}) async => await _breezLib.parseInput(input: input);
Expand Down
16 changes: 16 additions & 0 deletions lib/bloc/input/input_data.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:c_breez/bloc/input/input_source.dart';

class InputData {
final String data;
final InputSource source;

const InputData({
required this.data,
required this.source,
});

@override
String toString() {
return 'InputData{data: $data, source: $source}';
}
}
75 changes: 75 additions & 0 deletions lib/bloc/input/input_printer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import 'package:breez_sdk/bridge_generated.dart';

class InputPrinter {
const InputPrinter();

String inputTypeToString(InputType inputType) {
if (inputType is InputType_BitcoinAddress) {
return _bitcoinAddressToString(inputType);
} else if (inputType is InputType_Bolt11) {
return _bolt11ToString(inputType);
} else if (inputType is InputType_NodeId) {
return _nodeIdToString(inputType);
} else if (inputType is InputType_Url) {
return _urlToString(inputType);
} else if (inputType is InputType_LnUrlPay) {
return _lnUrlPayToString(inputType);
} else if (inputType is InputType_LnUrlWithdraw) {
return _lnUrlWithdrawToString(inputType);
} else if (inputType is InputType_LnUrlAuth) {
return _lnUrlAuthToString(inputType);
} else if (inputType is InputType_LnUrlError) {
return _lnUrlErrorToString(inputType);
} else {
return "Unknown InputType";
}
}

String _bitcoinAddressToString(InputType_BitcoinAddress inputType) {
final data = inputType.address;
return "BitcoinAddress(address: ${data.address}, network: ${data.network}, amountSat: ${data.amountSat}, "
"label: ${data.label}, message: ${data.message})";
}

String _bolt11ToString(InputType_Bolt11 inputType) {
final data = inputType.invoice;
return "Bolt11(invoice: ${data.bolt11}, paymentHash: ${data.paymentHash}, "
"description: ${data.description}, amountMsat: ${data.amountMsat}, expiry: ${data.expiry}, "
"payeePubkey: ${data.payeePubkey}, descriptionHash: ${data.descriptionHash}, "
"timestamp: ${data.timestamp}, routingHints: ${data.routingHints}, "
"paymentSecret: ${data.paymentSecret})";
}

String _nodeIdToString(InputType_NodeId inputType) {
return "NodeId(nodeId: ${inputType.nodeId})";
}

String _urlToString(InputType_Url inputType) {
return "Url(url: ${inputType.url})";
}

String _lnUrlPayToString(InputType_LnUrlPay inputType) {
final data = inputType.data;
return "LnUrlPayRequestData(callback: ${data.callback}, minSendable: ${data.minSendable}, "
"maxSendable: ${data.maxSendable}, metadata: ${data.metadataStr}, "
"commentAllowed: ${data.commentAllowed}, domain: ${data.domain}, lnAddr: ${data.lnAddress})";
}

String _lnUrlWithdrawToString(InputType_LnUrlWithdraw inputType) {
final data = inputType.data;
return "LnUrlWithdrawRequestData(callback: ${data.callback}, minWithdrawable: ${data.minWithdrawable}, "
"maxWithdrawable: ${data.maxWithdrawable}, defaultDescription: ${data.defaultDescription}, "
"k1: ${data.k1})";
}

String _lnUrlAuthToString(InputType_LnUrlAuth inputType) {
final data = inputType.data;
return "LnUrlAuthRequestData(k1: ${data.k1}, action: ${data.action}, domain: ${data.domain}, "
"url: ${data.url})";
}

String _lnUrlErrorToString(InputType_LnUrlError inputType) {
final data = inputType.data;
return "LnUrlError(reason: ${data.reason})";
}
}
6 changes: 6 additions & 0 deletions lib/bloc/input/input_source.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
enum InputSource {
input_field,
qrcode_reader,
clipboard,
hyperlink,
}
Loading

0 comments on commit e7451d7

Please sign in to comment.