Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
Add zaps support
Browse files Browse the repository at this point in the history
  • Loading branch information
benthecarman committed Dec 31, 2023
1 parent 9ca0895 commit debe3c1
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 17 deletions.
43 changes: 37 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ serde_json = "1.0.108"
tokio = { version = "1.34.0", features = ["full"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
lightning-invoice = "0.27.0"
fedimint-client = { git = "https://github.com/fedimint/fedimint", branch = "releases/v0.2" }
fedimint-core = { git = "https://github.com/fedimint/fedimint", branch = "releases/v0.2" }
fedimint-wallet-client = { git = "https://github.com/fedimint/fedimint", branch = "releases/v0.2" }
Expand All @@ -37,3 +38,4 @@ sqlb = "0.4.0"
futures = "0.3.30"
xmpp = "0.5.0"
itertools = "0.12.0"
hex = "0.4.3"
6 changes: 6 additions & 0 deletions migrations/20231224194609_create_tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,9 @@ CREATE TABLE invoice (
amount BIGINT NOT NULL,
state INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE zaps
(
id INTEGER NOT NULL PRIMARY KEY references invoice (id),
request TEXT NOT NULL,
event_id VARCHAR(64)
);
5 changes: 4 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use fedimint_client::derivable_secret::DerivableSecret;
use fedimint_client::secret::{PlainRootSecretStrategy, RootSecretStrategy};
use fedimint_core::api::InviteCode;
use nostr::hashes::hex::FromHex;
use nostr::key::FromSkStr;
use nostr::Keys;
use std::env;
use std::path::PathBuf;
use std::str::FromStr;
Expand All @@ -19,7 +21,7 @@ pub struct Config {
pub root_secret: DerivableSecret,
pub fm_db_path: PathBuf,
pub pg_db: String,
pub nostr_sk: String,
pub nostr_sk: Keys,
pub default_relay: String,
pub xmpp_username: String,
pub xmpp_password: String,
Expand Down Expand Up @@ -48,6 +50,7 @@ impl Config {
let pg_db = env::var("DATABASE_URL").expect("DATABASE_URL must be set");

let nostr_sk = env::var("NOSTR_SK").expect("NOSTR_SK must be set");
let nostr_sk = Keys::from_sk_str(&nostr_sk).expect("Invalid NOSTR_SK");

let default_relay =
env::var("DEFAULT_NOSTR_RELAY").expect("DEFAULT_NOSTR_RELAY must be set");
Expand Down
1 change: 1 addition & 0 deletions src/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod invoice;
pub mod invoice_state;
pub mod relay;
mod store;
pub mod zap;

use crate::model::store::{new_db_pool, Db};
use anyhow::Result;
Expand Down
51 changes: 51 additions & 0 deletions src/model/zap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#![allow(dead_code)]

use super::{
base::{self, DbBmc},
ModelManager,
};
use anyhow::Result;
use nostr::EventId;
use serde::Serialize;
use sqlb::Fields;
use sqlx::FromRow;

#[derive(Debug, Clone, Fields, FromRow, Serialize)]
pub struct Zap {
pub id: i32,
pub request: String,
pub event_id: Option<String>,
}

#[derive(Debug, Clone, Fields, FromRow, Serialize)]
pub struct ZapForUpdate {
pub event_id: String,
}

pub struct ZapBmc;

impl DbBmc for ZapBmc {
const TABLE: &'static str = "zaps";
}

impl ZapBmc {
pub async fn create(mm: &ModelManager, inv_c: Zap) -> Result<i32> {
base::create::<Self, _>(mm, inv_c).await
}

pub async fn get(mm: &ModelManager, id: i32) -> Result<Zap> {
base::get::<Self, _>(mm, id).await
}

pub async fn set_event_id(mm: &ModelManager, id: i32, event_id: EventId) -> Result<()> {
let u = ZapForUpdate {
event_id: event_id.to_hex(),
};
base::update::<Self, _>(mm, id, u).await?;
Ok(())
}

pub async fn delete(mm: &ModelManager, id: i32) -> Result<()> {
base::delete::<Self>(mm, id).await
}
}
87 changes: 85 additions & 2 deletions src/router/handlers/lnurlp/callback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,22 @@ use fedimint_core::{core::OperationId, task::spawn, Amount};
use fedimint_ln_client::{LightningClientModule, LnReceiveState};
use fedimint_mint_client::{MintClientModule, OOBNotes};
use futures::StreamExt;
use lightning_invoice::{Currency, InvoiceBuilder, PaymentSecret};
use nostr::bitcoin::hashes::sha256::Hash as Sha256;
use nostr::hashes::Hash;
use nostr::key::{Secp256k1, SecretKey};
use nostr::prelude::rand::rngs::OsRng;
use nostr::prelude::rand::RngCore;
use nostr::secp256k1::XOnlyPublicKey;
use nostr::{Event, EventBuilder, JsonUtil, Kind};
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{error, info};
use url::Url;
use xmpp::{parsers::message::MessageType, Jid};

use crate::model::invoice_state::InvoiceState;
use crate::model::zap::{Zap, ZapBmc};
use crate::{
config::CONFIG,
error::AppError,
Expand All @@ -43,6 +51,8 @@ pub struct LnurlCallbackParams {
pub comment: Option<String>, // Optional parameter to pass the LN WALLET user's comment to LN SERVICE
#[serde(default, deserialize_with = "empty_string_as_none")]
pub proofofpayer: Option<String>, // Optional ephemeral secp256k1 public key generated by payer
#[serde(default, deserialize_with = "empty_string_as_none")]
pub nostr: Option<String>, // Optional zap request
}

#[derive(Serialize, Deserialize)]
Expand Down Expand Up @@ -81,6 +91,19 @@ pub async fn handle_callback(
status: StatusCode::BAD_REQUEST,
});
}

// verify nostr param is a zap request
if params
.nostr
.as_ref()
.is_some_and(|n| Event::from_json(n).is_ok_and(|e| e.kind == Kind::ZapRequest))
{
return Err(AppError {
error: anyhow::anyhow!("Invalid nostr event"),
status: StatusCode::BAD_REQUEST,
});
}

let nip05relays = AppUserRelaysBmc::get_by(&state.mm, NameOrPubkey::Name, &username).await?;

let ln = state.fm.get_first_module::<LightningClientModule>();
Expand All @@ -89,7 +112,7 @@ pub async fn handle_callback(
Amount {
msats: params.amount,
},
"test invoice".to_string(),
"test invoice".to_string(), // todo set description hash properly
None,
(),
)
Expand All @@ -107,6 +130,19 @@ pub async fn handle_callback(
)
.await?;

// save nostr zap request
if let Some(request) = params.nostr {
ZapBmc::create(
&state.mm,
Zap {
id,
request,
event_id: None,
},
)
.await?;
}

// create subscription to operation
let subscription = ln
.subscribe_ln_receive(op_id)
Expand Down Expand Up @@ -154,7 +190,7 @@ pub(crate) async fn spawn_invoice_subscription(
let invoice = InvoiceBmc::set_state(&state.mm, id, InvoiceState::Settled)
.await
.expect("settling invoice can't fail");
notify_user(&state, invoice.amount as u64, userrelays.clone())
notify_user(&state, id, invoice.amount as u64, userrelays.clone())
.await
.expect("notifying user can't fail");
break;
Expand All @@ -167,6 +203,7 @@ pub(crate) async fn spawn_invoice_subscription(

async fn notify_user(
state: &AppState,
id: i32,
amount: u64,
app_user_relays: AppUserRelays,
) -> Result<(), Box<dyn std::error::Error>> {
Expand All @@ -179,6 +216,18 @@ async fn notify_user(
"xmpp" => send_xmpp_msg(&app_user_relays, operation_id, amount, notes).await,
_ => Err(anyhow::anyhow!("Unsupported dm_type")),
}?;

// Send zap if needed
if let Ok(zap) = ZapBmc::get(&state.mm, id).await {
let request = Event::from_json(zap.request)?;
let event = create_nostr_event(request, amount)?;

let event_id = state.nostr.send_event(event).await?;
info!("Broadcasted zap {event_id}!");

ZapBmc::set_event_id(&state.mm, id, event_id).await?;
}

Ok(())
}

Expand Down Expand Up @@ -236,3 +285,37 @@ async fn send_xmpp_msg(

Ok(())
}

/// Creates a nostr zap event with a fake invoice
fn create_nostr_event(request: Event, amt_msats: u64) -> Result<Event> {
let preimage = &mut [0u8; 32];
OsRng.fill_bytes(preimage);
let invoice_hash = Sha256::hash(preimage);

let payment_secret = &mut [0u8; 32];
OsRng.fill_bytes(payment_secret);

let priv_key_bytes = &mut [0u8; 32];
OsRng.fill_bytes(priv_key_bytes);
let private_key = SecretKey::from_slice(priv_key_bytes)?;

let desc_hash = Sha256::hash(request.as_json().as_bytes());

let fake_invoice = InvoiceBuilder::new(Currency::Bitcoin)
.amount_milli_satoshis(amt_msats)
.description_hash(desc_hash)
.current_timestamp()
.payment_hash(invoice_hash)
.payment_secret(PaymentSecret(*payment_secret))
.min_final_cltv_expiry_delta(144)
.build_signed(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key))?;

let event = EventBuilder::new_zap_receipt(
fake_invoice.to_string(),
Some(hex::encode(preimage)),
request,
)
.to_event(&CONFIG.nostr_sk)?;

Ok(event)
}
6 changes: 2 additions & 4 deletions src/router/handlers/lnurlp/well_known.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::str::FromStr;

use super::{LnurlStatus, LnurlType};
use crate::config::CONFIG;
use crate::error::AppError;
Expand Down Expand Up @@ -65,7 +63,7 @@ pub async fn handle_well_known(
) -> Result<Json<LnurlWellKnownResponse>, AppError> {
// see if username exists in nostr.json
info!("well_known called with username: {}", username);
let app_user = AppUserBmc::get_by(&state.mm, NameOrPubkey::Name, &username).await?;
let _app_user = AppUserBmc::get_by(&state.mm, NameOrPubkey::Name, &username).await?;

let res = LnurlWellKnownResponse {
callback: format!("http://{}/lnurlp/{}/callback", CONFIG.domain, username).parse()?,
Expand All @@ -75,7 +73,7 @@ pub async fn handle_well_known(
comment_allowed: None,
tag: LnurlType::PayRequest,
status: LnurlStatus::Ok,
nostr_pubkey: Some(XOnlyPublicKey::from_str(&app_user.pubkey)?),
nostr_pubkey: Some(CONFIG.nostr_sk.public_key()),
allows_nostr: true,
};

Expand Down
Loading

0 comments on commit debe3c1

Please sign in to comment.