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

Add zaps support #8

Merged
merged 1 commit into from
Dec 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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_zap_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_zap_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
Loading