Skip to content

Instantly share code, notes, and snippets.

@blakejakopovic
Last active March 2, 2023 15:58
Show Gist options
  • Save blakejakopovic/76714633f844e1a8b343805997fea5d7 to your computer and use it in GitHub Desktop.
Save blakejakopovic/76714633f844e1a8b343805997fea5d7 to your computer and use it in GitHub Desktop.
Quick and dirty Rust Nostr ZAP Validation
// 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