Last active
March 2, 2023 15:58
-
-
Save blakejakopovic/76714633f844e1a8b343805997fea5d7 to your computer and use it in GitHub Desktop.
Quick and dirty Rust Nostr ZAP Validation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://github.com/nostr-protocol/nips/blob/master/57.md | |
// | |
// Quick and dirty Rust Nostr ZAP Validation | |
// | |
// anyhow = "1.0.68" | |
// bech32 = "0.9" | |
// lightning-invoice = "0.21.0" | |
// nostr-rs-relay = "0.7.2" | |
// serde_json = "~1" | |
use anyhow::{anyhow,Result}; | |
use bech32::FromBase32; | |
use lightning_invoice::Invoice; | |
use nostr_rs_relay::event::Event; | |
use serde_json::Value; | |
fn get_event_first_tag_with_value(event: &Event, tag: &str) -> Option<String> { | |
event.tags | |
.iter() | |
.find(|t| t.get(0).map_or(false, |c| c.to_lowercase() == tag.to_lowercase())) | |
.map(|t| t[1].to_string()) | |
} | |
#[tokio::main] | |
async | |
fn main() -> Result<()> { | |
// Input: Event with kind = 9735 (already checked for valid signature, etc) | |
let zap_event: Event = serde_json::from_str(r#"{"id": "f481897ee877321783bb76133622b3cc344d691bb79cd6be88f44e819c3b2306", "sig": "269ca3bef5030618355a1221544fc6ffc83e40eac854ce36e1c1a59ce5cdaaa154058c73a3cbb44d7a2605a8edba516905684f5b57852fd06d0e089d041c970c", "kind": 9735, "tags": [["p", "b2dd40097e4d04b1a56fb3b65fc1d1aaf2929ad30fd842c74d68b9908744495b"], ["e", "98c033bbf644db5d040db0ae9d169c9df559d96851e9f4251d50c6c13e9a838d"], ["bolt11", "lnbc13370n1pjqpdthpp5hzt6kwcj5zzjf0j0raswrc2hka4vj8z2sq8mfa8n2l6fwrq39vashp5g0qx0vxke2gd7qprjudet5ek2rxp32tmft56g6dql8zrzcl3g5xqcqzpgxqzfvsp5c53d4dr3k23nt2emt2pggcsq42mcv6nj5lalws4fg47vhxcffu0s9qyyssqslw6r9zvnl0t938ksluxhv4z7h8h2duaqaaa09rsx9ug6y9xm98hrctrvjzykcvhht2ew7sznf4ahk9yxmenet5qp3thd9ggkxucursqkl8hp8"], ["description", "{\"id\":\"6583b7962694fb174974ddd281d181bdd35032f5abc1500525a81409e83e6c20\",\"pubkey\":\"9fec72d579baaa772af9e71e638b529215721ace6e0f8320725ecbf9f77f85b1\",\"created_at\":1677768055,\"kind\":9734,\"tags\":[[\"e\",\"98c033bbf644db5d040db0ae9d169c9df559d96851e9f4251d50c6c13e9a838d\"],[\"p\",\"b2dd40097e4d04b1a56fb3b65fc1d1aaf2929ad30fd842c74d68b9908744495b\"],[\"relays\",\"wss://nostr-pub.wellorder.net\",\"wss://nos.lol\",\"wss://nostr.mom\",\"wss://no.str.cr\",\"wss://e.nos.lol\",\"wss://nostr.wine\",\"wss://relay.nostrich.de\",\"wss://nostr.milou.lol\",\"wss://relay.nostr.band\"],[\"amount\",\"1337000\"]],\"content\":\"\",\"sig\":\"56610d3e54f8b26fd383b2afb6ffd2d890ec01d9fb725703285736ed0aa5e5557d223fa69db71542f4502ff9a4db318e2206384f07c50b36eca672d4bde3930c\"}"], ["preimage", "b897ab3b12a08524be4f1f60e1e157b76ac91c4a800fb4f4f357f4970c112b3b"]], "pubkey": "be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479", "content": "", "created_at": 1677768057}"#)?; | |
// Get first description tag (who sent the zap?) | |
let source_event: Event; | |
if let Some(description) = get_event_first_tag_with_value(&zap_event, "description") { | |
source_event = serde_json::from_str(&description)?; | |
println!("{source_event:?}"); | |
} else { | |
return Err(anyhow!("Missing source event")); | |
} | |
// Perhaps validate souce_event kind is 9734 | |
// let sender_comment = source_event.content; | |
// let zap_for_event = get_event_first_tag_with_value(&source_event, "e").unwrap(); | |
let zap_sender_pubkey = &source_event.pubkey; | |
let bolt11: String; | |
if let Some(invoice_str) = get_event_first_tag_with_value(&zap_event, "bolt11") { | |
bolt11 = invoice_str; | |
} else { | |
return Err(anyhow!("Missing source event")); | |
} | |
// Get first bolt11 tag value and parse | |
let invoice = bolt11.parse::<Invoice>().unwrap(); | |
println!("{invoice:?}"); | |
let amount_sat = invoice.amount_milli_satoshis().unwrap_or(0) / 1000; | |
// Get event first p-tag | |
let zap_recipient_pubkey: String; | |
if let Some(pubkey) = get_event_first_tag_with_value(&zap_event, "p") { | |
zap_recipient_pubkey = pubkey; | |
} else { | |
return Err(anyhow!("Missing ZAP receipient pubkey")); | |
} | |
// Lookup latest identity kind 0 (metadata) event for matching pubkey | |
// Lookup lud06 for pubkey (e.g. for b2dd40097e4d04b1a56fb3b65fc1d1aaf2929ad30fd842c74d68b9908744495b) | |
let lnurl = "LNURL1DP68GURN8GHJ7AMPD3KX2AR0VEEKZAR0WD5XJTNRDAKJ7TNHV4KXCTTTDEHHWM30D3H82UNVWQHK27R0W35KXURVV9HX2V33K7VKW6"; | |
// Decode lud06 (lightning url/address) into lookup url | |
let (_, data, _) = bech32::decode(&lnurl)?; | |
let decoded = Vec::<u8>::from_base32(&data)?; | |
// Decode URL | |
let url = match std::str::from_utf8(&decoded) { | |
Ok(url) => url, | |
Err(e) => panic!("Invalid UTF-8 sequence: {}", e), | |
}; | |
println!("{url}"); // e.g. https://walletofsatoshi.com/.well-known/lnurlp/exoticplane21 | |
// Lookup URL | |
let response_json = reqwest::get(url).await?.json::<Value>().await?; | |
println!("{response_json:?}"); | |
// Validation | |
// Check allowsNostr=true | |
if response_json["allowsNostr"] != true { | |
return Err(anyhow!("Not enabled for Nostr")); | |
} | |
// Check nostrPubkey=be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479 | |
// It should be the same as the 9735 event pubkey (usually a wallet provider) | |
if response_json["nostrPubkey"] != zap_event.pubkey { | |
return Err(anyhow!("Event pubkey mismatch with LNURL")); | |
} | |
println!("Valid ZAP: {zap_recipient_pubkey} receieved {amount_sat} sats from {zap_sender_pubkey}"); | |
// Valid ZAP: b2dd40097e4d04b1a56fb3b65fc1d1aaf2929ad30fd842c74d68b9908744495b receieved 1337 sats from 9fec72d579baaa772af9e71e638b529215721ace6e0f8320725ecbf9f77f85b1 | |
Ok(()) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment