Skip to content

Commit

Permalink
Merge branch 'main' into bloc_cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
mocodesmo committed Jan 5, 2025
2 parents d018d34 + c518ffb commit 01b186f
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 67 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ Suggestion to this roadmap can be proposed as Github issues.
- [ ] Biometric authentification
- [ ] Show fiat value of transactions at the approximated time they were made
- [ ] Spanish and French translactions
- [ ] Payjoin integration
- [x] Payjoin integration
- [ ] Integrate Bull Bitcoin Fee multiple
- [ ] Auto-consolidation mode for spend
- [ ] Small UTXO warning and consolidation suggestions
Expand Down
1 change: 1 addition & 0 deletions lib/_pkg/electrum_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Future<bool> isElectrumLive(String electrumUrl) async {
url,
port,
timeout: const Duration(seconds: 15),
onBadCertificate: (_) => true,
);
final Map<String, dynamic> request = {
'jsonrpc': '2.0',
Expand Down
131 changes: 76 additions & 55 deletions lib/_pkg/payjoin/manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ const List<String> _ohttpRelayUrls = [

const payjoinDirectoryUrl = 'https://payjo.in';

sealed class SessionError {
final String message;

const SessionError._(this.message);

factory SessionError.recoverable(String message) = RecoverableError;
factory SessionError.unrecoverable(String message) = UnrecoverableError;
}

class RecoverableError extends SessionError {
const RecoverableError(super.message) : super._();
}

class UnrecoverableError extends SessionError {
const UnrecoverableError(super.message) : super._();
}

class PayjoinManager {
PayjoinManager(this._walletTx, this._payjoinStorage);
final WalletTx _walletTx;
Expand Down Expand Up @@ -122,12 +139,15 @@ class PayjoinManager {
await _payjoinStorage.markSenderSessionComplete(pjUri);
completer.complete(null);
}
} else if (message is Err) {
} else if (message is SessionError) {
PayjoinEventBus().emit(
PayjoinSendFailureEvent(pjUri: pjUri, error: message),
PayjoinSendFailureEvent(pjUri: pjUri, error: message.message),
);
if (message is UnrecoverableError) {
await _payjoinStorage.markSenderSessionUnrecoverable(pjUri);
}
await _cleanupSession(pjUri);
completer.complete(message);
completer.complete(Err(message.message));
}
});

Expand Down Expand Up @@ -253,15 +273,21 @@ class PayjoinManager {
// TODO PROPAGATE ERROR TO UI TOAST / TRANSACTION HISTORY
debugPrint(e.toString());
await _cleanupSession(receiver.id());
await _payjoinStorage
.markReceiverSessionUnrecoverable(receiver.id());
completer.complete(
Err(
e.toString(),
),
);
}
} else if (message is Err) {
} else if (message is SessionError) {
await _cleanupSession(receiver.id());
completer.complete(message);
if (message is UnrecoverableError) {
await _payjoinStorage
.markReceiverSessionUnrecoverable(receiver.id());
}
completer.complete(Err(message.toString()));
}
});

Expand Down Expand Up @@ -301,12 +327,14 @@ class PayjoinManager {
.where(
(session) =>
session.walletId == wallet.id &&
session.status != PayjoinSessionStatus.success,
session.status != PayjoinSessionStatus.success &&
session.status != PayjoinSessionStatus.unrecoverable,
)
.toList();
final filteredSenders = senderSessions.where((session) {
return session.walletId == wallet.id &&
session.status != PayjoinSessionStatus.success;
session.status != PayjoinSessionStatus.success &&
session.status != PayjoinSessionStatus.unrecoverable;
}).toList();

final spawnedReceivers = filteredReceivers.map((session) {
Expand Down Expand Up @@ -369,6 +397,7 @@ class PayjoinManager {

enum PayjoinSessionStatus {
pending,
unrecoverable,
success,
}

Expand Down Expand Up @@ -474,21 +503,19 @@ Future<void> _isolateSender(List<dynamic> args) async {
// Reconstruct the Sender from the JSON
final sender = Sender.fromJson(senderJson);

// Run the sender logic inside the isolate
try {
final proposalPsbt = await _runSender(sender, sendPort: sendPort);
if (proposalPsbt == null) throw Exception('proposalPsbt is null');
sendPort.send({
'type': 'psbt_to_sign',
'psbt': proposalPsbt,
});
} catch (e) {
sendPort.send(Err(e.toString()));
sendPort.send(e);
}
}

/// Top-level function that attempts to run payjoin sender (V2 protocol first, fallback to V1).
Future<String?> _runSender(Sender sender, {required SendPort sendPort}) async {
Future<String> _runSender(Sender sender, {required SendPort sendPort}) async {
final dio = Dio();

try {
Expand All @@ -507,24 +534,26 @@ Future<String?> _runSender(Sender sender, {required SendPort sendPort}) async {
sendPort.send({'type': 'request_posted'});

while (true) {
try {
final (getRequest, getReqCtx) = await getCtx.extractReq(
ohttpRelay: await _randomOhttpRelayUrl(),
);
final getRes = await _postRequest(dio, getRequest);
final proposalPsbt = await getCtx.processResponse(
response: getRes.data as List<int>,
ohttpCtx: getReqCtx,
);
if (proposalPsbt != null) return proposalPsbt;
} catch (e) {
print('Error occurred while processing payjoin: $e');
// Loop until a valid response is found
}
final (getRequest, getReqCtx) = await getCtx.extractReq(
ohttpRelay: await _randomOhttpRelayUrl(),
);
final getRes = await _postRequest(dio, getRequest);
final proposalPsbt = await getCtx.processResponse(
response: getRes.data as List<int>,
ohttpCtx: getReqCtx,
);
if (proposalPsbt != null) return proposalPsbt;
}
} catch (e) {
// If V2 fails, attempt V1
return await _runSenderV1(sender, dio, sendPort);
if (e is PayjoinException &&
// TODO condition on error type instead of message content
e.message?.contains('parse receiver public key') == true) {
return await _runSenderV1(sender, dio, sendPort);
} else if (e is DioException) {
throw Exception(SessionError.recoverable(e.toString()));
} else {
throw Exception(SessionError.unrecoverable(e.toString()));
}
}
}

Expand Down Expand Up @@ -652,7 +681,7 @@ Future<void> _isolateReceiver(List<dynamic> args) async {
return payjoinProposal;
} catch (e) {
print('Error occurred while finalizing proposal: $e');
throw Exception('Error occurred while finalizing proposal');
rethrow;
}
}

Expand All @@ -669,10 +698,10 @@ Future<void> _isolateReceiver(List<dynamic> args) async {
'type': 'proposal_sent',
});
} catch (e) {
try {
isolateTomainSendPort.send(Err(e.toString()));
} catch (e) {
print('$e');
if (e is DioException) {
isolateTomainSendPort.send(SessionError.recoverable(e.toString()));
} else {
isolateTomainSendPort.send(SessionError.unrecoverable(e.toString()));
}
}
}
Expand All @@ -681,34 +710,26 @@ Future<UncheckedProposal> _receiveUncheckedProposal(
Dio dio,
Receiver receiver,
) async {
try {
while (true) {
final (req, context) = await receiver.extractReq();
final ohttpResponse = await _postRequest(dio, req);
final proposal = await receiver.processRes(
body: ohttpResponse.data as List<int>,
ctx: context,
);
if (proposal != null) {
return proposal;
}
while (true) {
final (req, context) = await receiver.extractReq();
final ohttpResponse = await _postRequest(dio, req);
final proposal = await receiver.processRes(
body: ohttpResponse.data as List<int>,
ctx: context,
);
if (proposal != null) {
return proposal;
}
} catch (e) {
throw Exception('Error occurred while processing payjoin receiver: $e');
}
}

Future<void> _respondProposal(Dio dio, PayjoinProposal proposal) async {
try {
final (postReq, ohttpCtx) = await proposal.extractV2Req();
final postRes = await _postRequest(dio, postReq);
await proposal.processRes(
res: postRes.data as List<int>,
ohttpContext: ohttpCtx,
);
} catch (e) {
throw Exception('Error occurred while processing payjoin: $e');
}
final (postReq, ohttpCtx) = await proposal.extractV2Req();
final postRes = await _postRequest(dio, postReq);
await proposal.processRes(
res: postRes.data as List<int>,
ohttpContext: ohttpCtx,
);
}

/// Posts a request via dio and returns the response.
Expand Down
45 changes: 45 additions & 0 deletions lib/_pkg/payjoin/storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,28 @@ class PayjoinStorage {
}
}

Future<Err?> markReceiverSessionUnrecoverable(String id) async {
try {
final (session, err) = await readReceiverSession(id);
if (err != null) return err;

final updatedSession = RecvSession(
session!.isTestnet,
session.receiver,
session.walletId,
PayjoinSessionStatus.unrecoverable,
);

await _hiveStorage.saveValue(
key: receiverPrefix + id,
value: jsonEncode(updatedSession.toJson()),
);
return null;
} catch (e) {
return Err(e.toString());
}
}

Future<(List<RecvSession>, Err?)> readAllReceivers() async {
//deleteAllSessions();
try {
Expand Down Expand Up @@ -186,6 +208,29 @@ class PayjoinStorage {
}
}

Future<Err?> markSenderSessionUnrecoverable(String pjUri) async {
try {
final (session, err) = await readSenderSession(pjUri);
if (err != null) return err;

final updatedSession = SendSession(
session!.isTestnet,
session.sender,
session.walletId,
session.pjUri,
PayjoinSessionStatus.unrecoverable,
);

await _hiveStorage.saveValue(
key: senderPrefix + pjUri,
value: jsonEncode(updatedSession.toJson()),
);
return null;
} catch (e) {
return Err(e.toString());
}
}

Future<(List<SendSession>, Err?)> readAllSenders() async {
try {
final (allData, err) = await _hiveStorage.getAll();
Expand Down
17 changes: 12 additions & 5 deletions lib/receive/bloc/receive_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,17 @@ class ReceiveCubit extends Cubit<ReceiveState> {
await isPayjoinEnabled();
await loadAddress();

payjoinInit();
await payjoinInit();
}

void payjoinInit() {
Future<void> payjoinInit() async {
final baseType = state.wallet!.baseWalletType;

if (state.paymentNetwork == PaymentNetwork.bitcoin &&
state.defaultAddress != null &&
state.isPayjoin &&
baseType == BaseWalletType.Bitcoin) {
receivePayjoin(
await receivePayjoin(
state.wallet!.isTestnet(),
state.defaultAddress!.address,
);
Expand Down Expand Up @@ -203,7 +203,8 @@ class ReceiveCubit extends Cubit<ReceiveState> {

emit(
state.copyWith(
loadingAddress: false,
loadingAddress: state
.isPayjoin, // Keep loading while the payjoin receiver is being initialized, if it's enabled.
errLoadingAddress: '',
),
);
Expand Down Expand Up @@ -359,7 +360,13 @@ class ReceiveCubit extends Cubit<ReceiveState> {

Future<void> receivePayjoin(bool isTestnet, String address) async {
final receiver = await _payjoinManager.initReceiver(isTestnet, address);
emit(state.copyWith(payjoinReceiver: receiver));
emit(
state.copyWith(
payjoinReceiver: receiver,
loadingAddress:
false, // Stop loading now that the receiver is initialized.
),
);
_payjoinManager.spawnNewReceiver(
isTestnet: isTestnet,
receiver: receiver,
Expand Down
21 changes: 15 additions & 6 deletions lib/receive/receive_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ class _Screen extends StatelessWidget {
final showQR = context.select(
(ReceiveCubit x) => x.state.showQR(swapTx, isChainSwap: isChainSwap),
);
final loadingAddress =
context.select((ReceiveCubit x) => x.state.loadingAddress);

final watchOnly =
context.select((WalletBloc x) => x.state.wallet.watchOnly());
Expand Down Expand Up @@ -319,12 +321,19 @@ class _Screen extends StatelessWidget {
const SwapFeesDetails(),
],
if (isChainSwap == false && showQR) ...[
const ReceiveQR(),
const Gap(8),
ReceiveAddress(
swapTx: swapTx,
addressQr: addressQr,
),
if (loadingAddress)
const Center(child: CircularProgressIndicator())
else
Column(
children: [
const ReceiveQR(),
const Gap(8),
ReceiveAddress(
swapTx: swapTx,
addressQr: addressQr,
),
],
),
const Gap(8),
if (shouldShowForm) const BitcoinReceiveForm(),
if (paymentNetwork == PaymentNetwork.lightning || formSubmitted)
Expand Down

0 comments on commit 01b186f

Please sign in to comment.