diff --git a/packages/ndk/example/zaps/receipts_for_event.dart b/packages/ndk/example/zaps/receipts_for_event.dart new file mode 100644 index 00000000..6164cd5e --- /dev/null +++ b/packages/ndk/example/zaps/receipts_for_event.dart @@ -0,0 +1,36 @@ +// ignore_for_file: avoid_print + +import 'package:ndk/ndk.dart'; + +void main() async { + final ndk = Ndk.defaultConfig(); + + print("fetching zap receipts for single event "); + final receipts = await ndk.zaps.fetchZappedReceipts( + pubKey: + "30782a8323b7c98b172c5a2af7206bb8283c655be6ddce11133611a03d5f1177", + eventId: + "d7bc29fa3c55ac525a3d5f2021211edb672b58565225dec423479a0875feea9d"); + + // Sort eventReceipts by amountSats in descending order + receipts + .sort((a, b) => (b.amountSats ?? 0).compareTo(a.amountSats ?? 0)); + + // Sort profileReceipts by amountSats in descending order + receipts + .sort((a, b) => (b.amountSats ?? 0).compareTo(a.amountSats ?? 0)); + + int eventSum = 0; + for (var receipt in receipts) { + String? sender; + if (receipt.sender!=null) { + Metadata? metadata = await ndk.metadata.loadMetadata(receipt.sender!); + sender = metadata?.name; + } + print("${sender!=null?"from ${sender} ":""} ${receipt.amountSats} sats ${receipt.comment}"); + eventSum += receipt.amountSats ?? 0; + } + print("${receipts.length} receipts, total of $eventSum sats"); + + await ndk.destroy(); +} diff --git a/packages/ndk/example/zaps/receipts_for_profile.dart b/packages/ndk/example/zaps/receipts_for_profile.dart new file mode 100644 index 00000000..b9438c8c --- /dev/null +++ b/packages/ndk/example/zaps/receipts_for_profile.dart @@ -0,0 +1,30 @@ +// ignore_for_file: avoid_print + +import 'package:ndk/ndk.dart'; + +void main() async { + final ndk = Ndk.defaultConfig(); + + print("fetching zap receipts for profile "); + final profileReceipts = await ndk.zaps.fetchZappedReceipts( + pubKey: "30782a8323b7c98b172c5a2af7206bb8283c655be6ddce11133611a03d5f1177", + ); + + // Sort profileReceipts by amountSats in descending order + profileReceipts + .sort((a, b) => (b.amountSats ?? 0).compareTo(a.amountSats ?? 0)); + + int profileSum = 0; + for (var receipt in profileReceipts) { + String? sender; + if (receipt.sender!=null) { + Metadata? metadata = await ndk.metadata.loadMetadata(receipt.sender!); + sender = metadata?.name; + } + print("${sender!=null?"from ${sender} ":""} ${receipt.amountSats} sats ${receipt.comment}"); + profileSum += receipt.amountSats ?? 0; + } + print("${profileReceipts.length} receipts, total of $profileSum sats"); + + await ndk.destroy(); +} diff --git a/packages/ndk/lib/domain_layer/entities/nip_01_event.dart b/packages/ndk/lib/domain_layer/entities/nip_01_event.dart index f082b103..df95d2d4 100644 --- a/packages/ndk/lib/domain_layer/entities/nip_01_event.dart +++ b/packages/ndk/lib/domain_layer/entities/nip_01_event.dart @@ -131,17 +131,7 @@ class Nip01Event { /// return first `e` tag found String? getEId() { - for (var tag in tags) { - if (tag.length > 1) { - var key = tag[0]; - var value = tag[1]; - - if (key == "e") { - return value; - } - } - } - return null; + return getFirstTag("e"); } /// return all tags that match given `tag` @@ -189,12 +179,17 @@ class Nip01Event { /// return first found `d` tag String? getDtag() { + return getFirstTag("d"); + } + + /// Get first tag matching given name + String? getFirstTag(String name) { for (var tag in tags) { if (tag.length > 1) { var key = tag[0]; var value = tag[1]; - if (key == "d") { + if (key == name) { return value; } } diff --git a/packages/ndk/lib/domain_layer/usecases/lnurl/lnurl.dart b/packages/ndk/lib/domain_layer/usecases/lnurl/lnurl.dart index d2a9c64c..67b13729 100644 --- a/packages/ndk/lib/domain_layer/usecases/lnurl/lnurl.dart +++ b/packages/ndk/lib/domain_layer/usecases/lnurl/lnurl.dart @@ -142,4 +142,46 @@ abstract class Lnurl { await signer.sign(event); return event; } + + /// extract amount from bolt11 in sats + static int getAmountFromBolt11(String bolt11) { + final numStr = subUntil(bolt11, "lnbc", "1p"); + if (numStr.isNotEmpty) { + var numStrLength = numStr.length; + if (numStrLength > 1) { + var lastStr = numStr.substring(numStr.length - 1); + var pureNumStr = numStr.substring(0, numStr.length - 1); + var pureNum = int.tryParse(pureNumStr); + if (pureNum != null) { + if (lastStr == "p") { + return (pureNum * 0.0001).round(); + } else if (lastStr == "n") { + return (pureNum * 0.1).round(); + } else if (lastStr == "u") { + return (pureNum * 100).round(); + } else if (lastStr == "m") { + return (pureNum * 100000).round(); + } + } + } + } + + return 0; + } + + static String subUntil(String content, String before, String end) { + var beforeLength = before.length; + var index = content.indexOf(before); + if (index < 0) { + return ""; + } + + var index2 = content.indexOf(end, index + beforeLength); + if (index2 <= 0) { + return ""; + } + + return content.substring(index + beforeLength, index2); + } + } diff --git a/packages/ndk/lib/domain_layer/usecases/zaps/zap_receipt.dart b/packages/ndk/lib/domain_layer/usecases/zaps/zap_receipt.dart index 154b0a03..a6ccd07f 100644 --- a/packages/ndk/lib/domain_layer/usecases/zaps/zap_receipt.dart +++ b/packages/ndk/lib/domain_layer/usecases/zaps/zap_receipt.dart @@ -1,33 +1,27 @@ 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'; class ZapReceipt { static const KIND = 9735; int? paidAt; + int? amountSats; String? pubKey; String? bolt11; String? preimage; - String? zapRequestJson; String? recipient; String? eventId; - String? zapContent; + String? comment; String? sender; String? anon; - ZapReceipt( - this.paidAt, - this.pubKey, - this.bolt11, - this.preimage, - this.zapRequestJson, - this.recipient, - this.eventId, - this.zapContent, - this.sender); - ZapReceipt.fromEvent(Nip01Event event) { + String? zapRequestJson; if (event.kind == 9735) { for (var tag in event.tags) { if (tag[0] == 'bolt11') bolt11 = tag[1]; @@ -39,15 +33,25 @@ class ZapReceipt { if (tag[0] == 'P') sender = tag[1]; } paidAt = event.createdAt; - if (zapRequestJson != null) { - try { - Map map = jsonDecode(zapRequestJson!); - zapContent = map['content']; - sender = map['pubkey']; - } catch (_) { - zapContent = ''; + if (zapRequestJson != null && zapRequestJson.isNotEmpty) { + Nip01Event event = Nip01Event.fromJson(jsonDecode(zapRequestJson)); + comment = event.content; + sender = event.pubKey; + String? amountString = event.getFirstTag('amount'); + if (amountString != null && amountString.isNotEmpty) { + try { + double? amount = double.tryParse(amountString); + if (amount != null) { + amountSats = (amount / 1000).round(); + } + } catch (e) { + Logger.log.w(e); + } } } + if (amountSats == null && bolt11 != null) { + amountSats = Lnurl.getAmountFromBolt11(bolt11!); + } List? splitStrings = anon?.split('_'); if (splitStrings != null && splitStrings.length == 2) { // /// recipient decrypt @@ -94,11 +98,10 @@ class ZapReceipt { // - 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). // - The invoiceAmount contained in the bolt11 tag of the zap receipt MUST equal the amount tag of the zap request (if present). // - The lnurl tag of the zap request (if present) SHOULD equal the recipient's lnurl. - // - SHA256(description) MUST match the description hash in the bolt11 invoice. } @override String toString() { - return 'ZapReceipt(paidAt: $paidAt, pubKey: $pubKey, bolt11: $bolt11, preimage: $preimage, description: $zapRequestJson, recipient: $recipient, eventId: $eventId, zapContent: $zapContent, sender: $sender, anon: $anon)'; + return 'ZapReceipt(paidAt: $paidAt, pubKey: $pubKey, bolt11: $bolt11, preimage: $preimage, amount: $amountSats, recipient: $recipient, eventId: $eventId, comment: $comment, sender: $sender, anon: $anon)'; } } diff --git a/packages/ndk/lib/domain_layer/usecases/zaps/zap_request.dart b/packages/ndk/lib/domain_layer/usecases/zaps/zap_request.dart index 1f1c5e9a..27d81599 100644 --- a/packages/ndk/lib/domain_layer/usecases/zaps/zap_request.dart +++ b/packages/ndk/lib/domain_layer/usecases/zaps/zap_request.dart @@ -1,8 +1,10 @@ import 'package:ndk/domain_layer/entities/nip_01_event.dart'; +/// Zap Request class ZapRequest extends Nip01Event { static const int KIND = 9734; + /// Zap Request ZapRequest( {required super.pubKey, required super.tags, required super.content}) : super(kind: KIND); diff --git a/packages/ndk/lib/domain_layer/usecases/zaps/zaps.dart b/packages/ndk/lib/domain_layer/usecases/zaps/zaps.dart index ba4ce289..2eb65217 100644 --- a/packages/ndk/lib/domain_layer/usecases/zaps/zaps.dart +++ b/packages/ndk/lib/domain_layer/usecases/zaps/zaps.dart @@ -105,6 +105,23 @@ class Zaps { return ZapResponse(error: e.toString()); } } + + /// fetch all zap receipts matching given pubKey and optional event id, in sats + Future> fetchZappedReceipts({required String pubKey, String? eventId, Duration? timeout} ) async { + NdkResponse? response = _requests.query(filters: [ + eventId!=null? + Filter( + kinds: [ZapReceipt.KIND], + eTags: [eventId], + pTags: [pubKey]) + : + Filter( + kinds: [ZapReceipt.KIND], + pTags: [pubKey]) + ], timeout: timeout?? Duration(seconds: 20)); + List events = await response.future; + return events.map((event) => ZapReceipt.fromEvent(event)).toList(); + } } /// zap response