Skip to content

Commit

Permalink
fix ZapReceipt.isValid
Browse files Browse the repository at this point in the history
  • Loading branch information
frnandu committed Dec 22, 2024
1 parent aa47585 commit 4f83859
Show file tree
Hide file tree
Showing 6 changed files with 308 additions and 241 deletions.
91 changes: 2 additions & 89 deletions packages/ndk/lib/domain_layer/usecases/lnurl/lnurl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,96 +56,9 @@ abstract class Lnurl {
}
}

/// creates an invoice with an optional zap request encoded if signer, pubKey & relays are non empty
static Future<String?> getInvoiceCode({
required String lud16Link,
required int amountSats,
ZapRequest? zapRequest,
String? comment,
http.Client? client
}) async {
var lnurlResponse = await getLnurlResponse(lud16Link, client: client);
if (lnurlResponse == null) {
return null;
}

var callback = lnurlResponse.callback!;
if (callback.contains("?")) {
callback += "&";
} else {
callback += "?";
}

final amount = amountSats * 1000;
callback += "amount=$amount";

if (comment != null && comment.trim() != '') {
var commentNum = lnurlResponse.commentAllowed;
if (commentNum != null) {
if (commentNum < comment.length) {
comment = comment.substring(0, commentNum);
}
callback += "&comment=${Uri.encodeQueryComponent(comment)}";
}
}

// ZAP ?
if (lnurlResponse.doesAllowsNostr && zapRequest!=null && zapRequest.sig.isNotEmpty) {
Logger.log.d(jsonEncode(zapRequest));
var eventStr = Uri.encodeQueryComponent(jsonEncode(zapRequest));
callback += "&nostr=$eventStr";
}

Logger.log.d("getInvoice callback $callback");

Uri uri = Uri.parse(callback);

try {
var response = await (client ?? http.Client()).get(uri);
final decodedResponse =
jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>;
return decodedResponse["pr"];
} catch (e) {
Logger.log.d(e);
}

return null;
}

static Future<ZapRequest> zapRequest({
required int amountSats,
required EventSigner signer,
required String pubKey,
String? eventId,
String? comment,
required Iterable<String> relays,
String? pollOption,
}) async {
if (amountSats<0) {
throw ArgumentError("amount cannot be < 0");
}
final amount = amountSats * 1000;

var tags = [
["relays", ...relays],
["amount", amount.toString()],
["p", pubKey],
];
if (eventId != null) {
tags.add(["e", eventId]);
}
if (pollOption != null) {
tags.add(["poll_option", pollOption]);
}
var event = ZapRequest(
pubKey: signer.getPublicKey(), tags: tags, content: comment??'');
await signer.sign(event);
return event;
}

/// extract amount from bolt11 in sats
static int getAmountFromBolt11(String bolt11) {
final numStr = subUntil(bolt11, "lnbc", "1p");
final numStr = _subUntil(bolt11, "lnbc", "1p");
if (numStr.isNotEmpty) {
var numStrLength = numStr.length;
if (numStrLength > 1) {
Expand All @@ -169,7 +82,7 @@ abstract class Lnurl {
return 0;
}

static String subUntil(String content, String before, String end) {
static String _subUntil(String content, String before, String end) {
var beforeLength = before.length;
var index = content.indexOf(before);
if (index < 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// invoice response
class InvoiceResponse {
String invoice;
int amountSats;
String? nostrPubkey;

/// .
InvoiceResponse({required this.invoice, this.nostrPubkey, required this.amountSats});
}
56 changes: 20 additions & 36 deletions packages/ndk/lib/domain_layer/usecases/zaps/zap_receipt.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'dart:convert';

import 'package:ndk/domain_layer/entities/nip_01_event.dart';
import 'package:ndk/domain_layer/usecases/lnurl/lnurl.dart';
import 'package:ndk/domain_layer/usecases/zaps/zap_request.dart';

import '../../../shared/logger/logger.dart';

Expand All @@ -19,9 +18,11 @@ class ZapReceipt {
String? comment;
String? sender;
String? anon;
String? lnurl;

ZapReceipt.fromEvent(Nip01Event event) {
String? zapRequestJson;
pubKey = event.pubKey;
if (event.kind == 9735) {
for (var tag in event.tags) {
if (tag[0] == 'bolt11') bolt11 = tag[1];
Expand All @@ -37,6 +38,7 @@ class ZapReceipt {
Nip01Event event = Nip01Event.fromJson(jsonDecode(zapRequestJson));
comment = event.content;
sender = event.pubKey;
lnurl = event.getFirstTag('lnurl');
String? amountString = event.getFirstTag('amount');
if (amountString != null && amountString.isNotEmpty) {
try {
Expand All @@ -54,50 +56,32 @@ class ZapReceipt {
}
List<String>? splitStrings = anon?.split('_');
if (splitStrings != null && splitStrings.length == 2) {
// /// recipient decrypt
// try {
// String contentBech32 = splitStrings[0];
// String ivBech32 = splitStrings[1];
// String? encryptedContent = bech32Decode(contentBech32,
// maxLength: contentBech32.length)['data'];
// String? iv =
// bech32Decode(ivBech32, maxLength: ivBech32.length)['data'];
//
// String encryptedContentBase64 =
// base64Encode(hexToBytes(encryptedContent!));
// String ivBase64 = base64Encode(hexToBytes(iv!));
//
// String eventString = await Nip4.decryptContent(
// '$encryptedContentBase64?iv=$ivBase64',
// recipient!,
// myPubkey,
// privkey);
//
// /// try to use sender decrypt
// if (eventString.isEmpty) {
// String derivedPrivkey =
// generateKeyPair(recipient, event.createdAt, privkey);
// eventString = await Nip4.decryptContent('$encryptedContent?iv=$iv',
// recipient, bip340.getPublicKey(derivedPrivkey), derivedPrivkey);
// }
// if (eventString.isNotEmpty) {
// Event privEvent = await Event.fromJson(jsonDecode(eventString));
// sender = privEvent.pubkey;
// content = privEvent.content;
// }
// } catch (_) {}
// TODO decrypt private zap
}
} else {
throw Exception("${event.kind} is not nip57 compatible");
}
}

bool isValid(String invoice) {
return bolt11 == invoice;
// TODO:
/// is valid
bool isValid({required String nostrPubKey, required String recipientLnurl}) {
// - The zap receipt event's pubkey MUST be the same as the recipient's lnurl provider's nostrPubkey (retrieved in step 1 of the protocol flow).
if (pubKey != nostrPubKey) {
return false;
}
// - The invoiceAmount contained in the bolt11 tag of the zap receipt MUST equal the amount tag of the zap request (if present).
if (bolt11!=null && bolt11!.isNotEmpty) {
if (amountSats != Lnurl.getAmountFromBolt11(bolt11!)) {
return false;
}
}
// - The lnurl tag of the zap request (if present) SHOULD equal the recipient's lnurl.
if (lnurl!=null && lnurl!.isNotEmpty) {
if (lnurl != recipientLnurl) {
return false;
}
}
return true;
}

@override
Expand Down
104 changes: 99 additions & 5 deletions packages/ndk/lib/domain_layer/usecases/zaps/zaps.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import 'dart:async';
import 'dart:convert';

import 'package:http/http.dart';
import 'package:ndk/domain_layer/usecases/nwc/nwc_connection.dart';
import 'package:ndk/domain_layer/usecases/zaps/Invoice_response.dart';
import 'package:ndk/domain_layer/usecases/zaps/zap_receipt.dart';
import 'package:ndk/domain_layer/usecases/zaps/zap_request.dart';

Expand All @@ -14,6 +17,8 @@ import '../nwc/nwc.dart';
import '../nwc/responses/pay_invoice_response.dart';
import '../requests/requests.dart';

import 'package:http/http.dart' as http;

/// Zaps
class Zaps {
final Requests _requests;
Expand All @@ -25,6 +30,94 @@ class Zaps {
}) : _requests = requests,
_nwc = nwc;

/// creates an invoice with an optional zap request encoded if signer, pubKey & relays are non empty
Future<InvoiceResponse?> fecthInvoice({
required String lud16Link,
required int amountSats,
ZapRequest? zapRequest,
String? comment,
http.Client? client
}) async {
var lnurlResponse = await Lnurl.getLnurlResponse(lud16Link, client: client);
if (lnurlResponse == null) {
return null;
}

var callback = lnurlResponse.callback!;
if (callback.contains("?")) {
callback += "&";
} else {
callback += "?";
}

final amount = amountSats * 1000;
callback += "amount=$amount";

if (comment != null && comment.trim() != '') {
var commentNum = lnurlResponse.commentAllowed;
if (commentNum != null) {
if (commentNum < comment.length) {
comment = comment.substring(0, commentNum);
}
callback += "&comment=${Uri.encodeQueryComponent(comment)}";
}
}

// ZAP ?
if (lnurlResponse.doesAllowsNostr && zapRequest!=null && zapRequest.sig.isNotEmpty) {
Logger.log.d(jsonEncode(zapRequest));
var eventStr = Uri.encodeQueryComponent(jsonEncode(zapRequest));
callback += "&nostr=$eventStr";
}

Logger.log.d("getInvoice callback $callback");

Uri uri = Uri.parse(callback);

try {
var response = await (client ?? http.Client()).get(uri);
final decodedResponse =
jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>;
String invoice = decodedResponse["pr"];
return InvoiceResponse(invoice: invoice, amountSats: amountSats, nostrPubkey: lnurlResponse.nostrPubkey);

} catch (e) {
Logger.log.d(e);
}
return null;
}

Future<ZapRequest> createZapRequest({
required int amountSats,
required EventSigner signer,
required String pubKey,
String? eventId,
String? comment,
required Iterable<String> relays,
String? pollOption,
}) async {
if (amountSats<0) {
throw ArgumentError("amount cannot be < 0");
}
final amount = amountSats * 1000;

var tags = [
["relays", ...relays],
["amount", amount.toString()],
["p", pubKey],
];
if (eventId != null) {
tags.add(["e", eventId]);
}
if (pollOption != null) {
tags.add(["poll_option", pollOption]);
}
var event = ZapRequest(
pubKey: signer.getPublicKey(), tags: tags, content: comment??'');
await signer.sign(event);
return event;
}

/// zap or pay some lnurl, for zap to be created it is necessary:
/// - that the lnurl has the allowsNostr: true
/// - non empty relays
Expand All @@ -47,15 +140,15 @@ class Zaps {
signer != null &&
relays != null &&
relays.isNotEmpty) {
zapRequest = await Lnurl.zapRequest(
zapRequest = await createZapRequest(
amountSats: amountSats,
signer: signer,
pubKey: pubKey,
comment: comment,
relays: relays,
eventId: eventId);
}
String? invoice = await Lnurl.getInvoiceCode(
InvoiceResponse? invoice = await fecthInvoice(
lud16Link: lud16Link!,
comment: comment,
amountSats: amountSats,
Expand All @@ -66,11 +159,11 @@ class Zaps {
}
try {
PayInvoiceResponse payResponse =
await _nwc.payInvoice(nwcConnection, invoice: invoice, timeout: Duration(seconds: 10));
await _nwc.payInvoice(nwcConnection, invoice: invoice.invoice, timeout: Duration(seconds: 10));
if (payResponse.preimage.isNotEmpty && payResponse.errorCode == null) {
ZapResponse zapResponse = ZapResponse(
payInvoiceResponse: payResponse);
if (zapRequest != null && fetchZapReceipt) {
if (zapRequest != null && fetchZapReceipt && invoice.nostrPubkey!=null && invoice.nostrPubkey!.isNotEmpty) {
// if it's a zap, try to find the zap receipt
zapResponse.receiptResponse = _requests.subscription(filters: [
eventId != null
Expand All @@ -90,7 +183,7 @@ class Zaps {
if (bolt11!=null && bolt11 == invoice || preimage!=null && preimage==payResponse.preimage) {
ZapReceipt receipt = ZapReceipt.fromEvent(event);
Logger.log.d("Zap Receipt: $receipt");
if (receipt.isValid(invoice)) {
if (receipt.isValid(nostrPubKey: invoice.nostrPubkey!, recipientLnurl: lnurl)) {
zapResponse.emitReceipt(receipt);
} else {
Logger.log.w("Zap Receipt invalid: $receipt");
Expand Down Expand Up @@ -120,6 +213,7 @@ class Zaps {
: Filter(kinds: [ZapReceipt.KIND], pTags: [pubKey])
]);
List<Nip01Event> events = await response.future;
// TODO how to check validity of zap receipts without nostrPubKey and recipientLnurl????
return events.map((event) => ZapReceipt.fromEvent(event)).toList();
}

Expand Down
Loading

0 comments on commit 4f83859

Please sign in to comment.