37
Cargo.lock
generated
37
Cargo.lock
generated
@@ -575,7 +575,7 @@ dependencies = [
|
||||
"bitcoin_hashes 0.14.0",
|
||||
"hex-conservative 0.2.1",
|
||||
"hex_lit",
|
||||
"secp256k1",
|
||||
"secp256k1 0.29.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -2449,6 +2449,26 @@ dependencies = [
|
||||
"redox_syscall 0.5.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lightning-invoice"
|
||||
version = "0.33.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4254e7d05961a3728bc90737c522e7091735ba6f2f71014096d4b3eb4ee5d89"
|
||||
dependencies = [
|
||||
"bech32",
|
||||
"bitcoin",
|
||||
"lightning-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lightning-types"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2cd84d4e71472035903e43caded8ecc123066ce466329ccd5ae537a8d5488c7"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
@@ -2794,7 +2814,7 @@ dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
"instant",
|
||||
"scrypt",
|
||||
"secp256k1",
|
||||
"secp256k1 0.29.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"unicode-normalization",
|
||||
@@ -2859,6 +2879,7 @@ dependencies = [
|
||||
"hex",
|
||||
"image",
|
||||
"jni",
|
||||
"lightning-invoice",
|
||||
"mime_guess",
|
||||
"nostr 0.37.0",
|
||||
"nostrdb",
|
||||
@@ -2867,6 +2888,7 @@ dependencies = [
|
||||
"profiling",
|
||||
"puffin",
|
||||
"puffin_egui",
|
||||
"secp256k1 0.30.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -4179,6 +4201,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252"
|
||||
dependencies = [
|
||||
"bitcoin_hashes 0.14.0",
|
||||
"rand 0.8.5",
|
||||
"secp256k1-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1-sys"
|
||||
version = "0.10.1"
|
||||
|
||||
@@ -64,6 +64,8 @@ mime_guess = "2.0.5"
|
||||
pretty_assertions = "1.4.1"
|
||||
jni = "0.21.1"
|
||||
profiling = "1.0"
|
||||
lightning-invoice = "0.33.1"
|
||||
secp256k1 = "0.30.0"
|
||||
|
||||
[profile.small]
|
||||
inherits = 'release'
|
||||
|
||||
@@ -36,6 +36,8 @@ profiling = { workspace = true }
|
||||
nwc = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
bech32 = { workspace = true }
|
||||
lightning-invoice = { workspace = true }
|
||||
secp256k1 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
mod networking;
|
||||
mod networking;
|
||||
mod zap;
|
||||
292
crates/notedeck/src/zaps/zap.rs
Normal file
292
crates/notedeck/src/zaps/zap.rs
Normal file
@@ -0,0 +1,292 @@
|
||||
use enostr::{NoteId, Pubkey};
|
||||
use image::EncodableLayout;
|
||||
use lightning_invoice::Bolt11Invoice;
|
||||
use secp256k1::{schnorr::Signature, Message, Secp256k1, XOnlyPublicKey};
|
||||
use sha2::Digest;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum ZapTarget {
|
||||
Profile(Pubkey),
|
||||
Note(NoteZapTarget),
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct NoteZapTarget {
|
||||
pub note_id: NoteId,
|
||||
pub author: Pubkey,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct Zap {
|
||||
pub sender: Pubkey,
|
||||
pub target: ZapTarget,
|
||||
pub invoice: Bolt11Invoice,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Zap {
|
||||
pub fn from_zap_event(zap_event: nostrdb::Note, sender: &Pubkey) -> Option<Self> {
|
||||
if sender.bytes() != zap_event.pubkey() {
|
||||
// Make sure that we only create a zap event if it is authorized by the profile or event
|
||||
return None;
|
||||
}
|
||||
|
||||
let zap_tags = get_zap_tags(zap_event)?;
|
||||
let invoice = zap_tags.bolt11.parse::<Bolt11Invoice>().ok()?;
|
||||
|
||||
// invoice must be specific
|
||||
invoice.amount_milli_satoshis()?;
|
||||
|
||||
if let Some(preimage) = zap_tags.preimage {
|
||||
if !preimage_matches_invoice(&invoice, preimage) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let Ok(zap_req) = enostr::Note::from_json(zap_tags.description) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if !valid_zap_request(zap_req) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let zap_target = determine_zap_target(&zap_tags)?;
|
||||
|
||||
Some(Zap {
|
||||
sender: *sender,
|
||||
target: zap_target,
|
||||
invoice,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn event_tag<'a>(ev: nostrdb::Note<'a>, name: &str) -> Option<&'a str> {
|
||||
ev.tags().iter().find_map(|tag| {
|
||||
if tag.count() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cur_name = tag.get_str(0)?;
|
||||
|
||||
if cur_name != name {
|
||||
return None;
|
||||
}
|
||||
|
||||
tag.get_str(1)
|
||||
})
|
||||
}
|
||||
|
||||
fn determine_zap_target(tags: &ZapTags) -> Option<ZapTarget> {
|
||||
if let Some(note_zapped) = tags.note_zapped {
|
||||
Some(ZapTarget::Note(NoteZapTarget {
|
||||
note_id: NoteId::new(*note_zapped),
|
||||
author: Pubkey::new(*tags.recipient),
|
||||
}))
|
||||
} else {
|
||||
Some(ZapTarget::Profile(Pubkey::new(*tags.recipient)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn event_commitment(
|
||||
pubkey: Pubkey,
|
||||
created_at: u64,
|
||||
kind: u64,
|
||||
tags: Vec<Vec<String>>,
|
||||
content: String,
|
||||
) -> String {
|
||||
// Serialize the content and tags into JSON strings.
|
||||
let content_json = serde_json::to_string(&content).expect("Failed to serialize content");
|
||||
let tags_json = serde_json::to_string(&tags).expect("Failed to serialize tags");
|
||||
|
||||
format!(
|
||||
"[0,\"{}\",{},{},{},{}]",
|
||||
pubkey.hex(),
|
||||
created_at,
|
||||
kind,
|
||||
tags_json,
|
||||
content_json
|
||||
)
|
||||
}
|
||||
|
||||
// TODO(kernelkind): i think we may be able to validate just with the nostrdb::Note. Not exactly sure yet how though
|
||||
fn valid_zap_request(note: enostr::Note) -> bool {
|
||||
let sig = note.sig.clone();
|
||||
|
||||
let commitment = event_commitment(
|
||||
note.pubkey,
|
||||
note.created_at,
|
||||
note.kind,
|
||||
note.tags,
|
||||
note.content,
|
||||
);
|
||||
|
||||
let commitment_bytes = commitment.as_bytes();
|
||||
let hash = sha256(commitment_bytes);
|
||||
let check_noteid = NoteId::new(hash);
|
||||
|
||||
if note.id != check_noteid {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Ok(sig_bytes) = hex::decode(sig) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let sig_bytes: Option<[u8; 64]> = sig_bytes.try_into().ok();
|
||||
|
||||
let Some(sig_bytes) = sig_bytes else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if !verify_schnorr_signature(¬e.pubkey, &sig_bytes, note.id.bytes()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn sha256(input: &[u8]) -> [u8; 32] {
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(input);
|
||||
let result = hasher.finalize();
|
||||
result.into()
|
||||
}
|
||||
|
||||
pub fn verify_schnorr_signature(
|
||||
pubkey_bytes: &[u8; 32],
|
||||
sig_bytes: &[u8; 64],
|
||||
msg_bytes: &[u8; 32],
|
||||
) -> bool {
|
||||
let secp = Secp256k1::verification_only();
|
||||
|
||||
let Ok(xonly_pubkey) = XOnlyPublicKey::from_slice(pubkey_bytes) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(sig) = Signature::from_slice(sig_bytes) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let msg = Message::from_digest(*msg_bytes);
|
||||
|
||||
secp.verify_schnorr(&sig, msg.as_ref(), &xonly_pubkey)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn preimage_matches_invoice(invoice: &Bolt11Invoice, preimage: &str) -> bool {
|
||||
let Ok(preimage_bytes) = hex::decode(preimage.as_bytes()) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
invoice.payment_secret().0 == preimage_bytes.as_bytes()
|
||||
}
|
||||
|
||||
struct ZapTags<'a> {
|
||||
pub bolt11: &'a str,
|
||||
pub preimage: Option<&'a str>,
|
||||
pub description: &'a str,
|
||||
pub recipient: &'a [u8; 32],
|
||||
pub note_zapped: Option<&'a [u8; 32]>,
|
||||
}
|
||||
fn get_zap_tags(ev: nostrdb::Note) -> Option<ZapTags> {
|
||||
let mut bolt11 = None;
|
||||
let mut preimage = None;
|
||||
let mut description = None;
|
||||
let mut recipient = None;
|
||||
let mut note_zapped = None;
|
||||
|
||||
for tag in ev.tags() {
|
||||
// Only process tags with at least two elements.
|
||||
if tag.count() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(cur_name) = tag.get_str(0) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if cur_name == "bolt11" {
|
||||
bolt11 = tag.get_str(1);
|
||||
} else if cur_name == "preimage" {
|
||||
preimage = tag.get_str(1);
|
||||
} else if cur_name == "description" {
|
||||
description = tag.get_str(1);
|
||||
} else if cur_name == "p" {
|
||||
recipient = tag.get_id(1);
|
||||
} else if cur_name == "e" {
|
||||
note_zapped = tag.get_id(1);
|
||||
}
|
||||
|
||||
if bolt11.is_some()
|
||||
&& preimage.is_some()
|
||||
&& description.is_some()
|
||||
&& recipient.is_some()
|
||||
&& note_zapped.is_some()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Some(ZapTags {
|
||||
bolt11: bolt11?,
|
||||
preimage,
|
||||
description: description?,
|
||||
recipient: recipient?,
|
||||
note_zapped,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enostr::Pubkey;
|
||||
|
||||
use crate::zaps::zap::{valid_zap_request, Zap};
|
||||
|
||||
// a random zap receipt
|
||||
const ZAP_RECEIPT: &str = r#"{"kind":9735,"id":"c8a5767f33cd73716cf670c9615a73ec50cb91c373100f6c0d5cc160237b58dc","pubkey":"be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479","created_at":1743191143,"tags":[["p","1af54955936be804f95010647ea5ada5c7627eddf0734a7f813bba0e31eed960"],["e","ec998b249a8c366358c264f0932a9b433ac60b1c2f630cb24a604560873f7030"],["bolt11","lnbc330n1pn7dlrrpp566sfk69zda849huwjw6wepw3uzxxp4mp9np54qx49ruw8cuv86ushp52te27l4jadsz0u76jvgsk5uekl04tujpjkt9cc7duu0jfzp9zdtscqzzsxqyz5vqsp5m3tzc7ryp5f9fv90v27uyrrd4qfmj5lrwv9rvmvum3v50kdph23s9qxpqysgqut2ssf0m7nmtd73cwqk7qfw4sw6zlj598sjdxmdsepmvn0ptamnhf45c425h26juzcfupegltefwsf8qav2ldell7v9fpc0y23nl0kgqtf432g"],["description","{\"id\":\"73d05cfe976bb56b139b6cd04286a801b20cc0b01070886d6e3176ff2e107833\",\"pubkey\":\"d4338b7c3306491cfdf54914d1a52b80a965685f7361311eae5f3eaff1d23a5b\",\"created_at\":1743191138,\"kind\":9734,\"tags\":[[\"e\",\"ec998b249a8c366358c264f0932a9b433ac60b1c2f630cb24a604560873f7030\"],[\"p\",\"1af54955936be804f95010647ea5ada5c7627eddf0734a7f813bba0e31eed960\"],[\"relays\",\"wss://nosdrive.app/relay\"],[\"alt\",\"Zap request\"]],\"content\":\"\",\"sig\":\"2091b7f720586d7420ea7a90406ea856378339c8b0b3f3e695ccbfebaa8c4ea20a3cb850ff18cae957aa2e0ecb06c386d0bd27aa7a13bf7a8f7425a4c2a57903\"}"],["preimage","13821fcf87afa4c3bb753d62949481969e6af8fca9867d753e3503bd45e2814e"]],"content":"","sig":"d15aecbd1d0d289f99ffbf4d0b7c77c24875ed38fed13deee4e2e1254bcd05bda8dca3bb2858b5c3167749b4afa732f4670b9df54904786614252b4ed7916e5f"}"#;
|
||||
|
||||
const ZAP_REQ: &str = r#"{"id":"73d05cfe976bb56b139b6cd04286a801b20cc0b01070886d6e3176ff2e107833","pubkey":"d4338b7c3306491cfdf54914d1a52b80a965685f7361311eae5f3eaff1d23a5b","created_at":1743191138,"kind":9734,"tags":[["e","ec998b249a8c366358c264f0932a9b433ac60b1c2f630cb24a604560873f7030"],["p","1af54955936be804f95010647ea5ada5c7627eddf0734a7f813bba0e31eed960"],["relays","wss://nosdrive.app/relay"],["alt","Zap request"]],"content":"","sig":"2091b7f720586d7420ea7a90406ea856378339c8b0b3f3e695ccbfebaa8c4ea20a3cb850ff18cae957aa2e0ecb06c386d0bd27aa7a13bf7a8f7425a4c2a57903"}"#;
|
||||
|
||||
#[test]
|
||||
fn test_valid_zap_req() {
|
||||
let note = enostr::Note::from_json(ZAP_REQ).unwrap();
|
||||
|
||||
assert!(valid_zap_request(note));
|
||||
}
|
||||
|
||||
fn enostr_note_to_nostrdb_note<'a>(note: &'a enostr::Note) -> Option<nostrdb::Note<'a>> {
|
||||
let mut n = nostrdb::NoteBuilder::new()
|
||||
.pubkey(¬e.pubkey)
|
||||
.created_at(note.created_at)
|
||||
.kind(note.kind.try_into().ok()?)
|
||||
.content(¬e.content);
|
||||
|
||||
for tag in ¬e.tags {
|
||||
n = n.start_tag();
|
||||
|
||||
for tag_ind in tag {
|
||||
n = n.tag_str(&tag_ind);
|
||||
}
|
||||
}
|
||||
|
||||
n.build()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zap_event() {
|
||||
let pk =
|
||||
Pubkey::from_hex("be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479")
|
||||
.unwrap();
|
||||
|
||||
let note = enostr::Note::from_json(ZAP_RECEIPT).unwrap();
|
||||
let nostrdb_note = enostr_note_to_nostrdb_note(¬e).unwrap();
|
||||
|
||||
let zap = Zap::from_zap_event(nostrdb_note, &pk);
|
||||
|
||||
assert!(zap.is_some());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user