Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

External receive support #4

Merged
merged 10 commits into from
Mar 26, 2024
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
636 changes: 394 additions & 242 deletions Cargo.lock

Large diffs are not rendered by default.

17 changes: 8 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,16 @@ chrono = { version = "0.4.26", features = ["serde"] }
diesel = { version = "2.1", features = ["postgres", "postgres_backend", "r2d2", "chrono", "numeric"] }
dotenv = "0.15.0"
async-trait = "0.1.77"
fedimint-tbs = "0.2.2"
fedimint-core = "0.2.2"
fedimint-client = "0.2.2"
fedimint-wallet-client = "0.2.2"
fedimint-mint-client = "0.2.2"
bls12_381 = { version = "0.7.1", features = [ "zeroize", "groups" ] }
fedimint-ln-client = "0.2.2"
fedimint-tbs = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.2" }
fedimint-core = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.2" }
fedimint-client = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.2" }
fedimint-wallet-client = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.2" }
fedimint-mint-client = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.2" }
fedimint-ln-client = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.2" }
fedimint-ln-common = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.2" }
futures = "0.3.28"
url = "2.5.0"
itertools = "0.12.0"
lightning-invoice = "0.27.0"
hex = "0.4.3"
jwt-compact = { version = "0.8.0", features = ["es256k"] }
nostr = "0.26.0"
Expand All @@ -41,7 +40,7 @@ reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1.12.0", features = ["full"] }
tower-http = { version = "0.4.0", features = ["cors"] }
lazy-regex = "3.1.0"
multimint = { git = "https://github.com/Kodylow/multimint", rev = "00df9d34f0244d0200eee4d285094b22b34cf38b" }
multimint = { git = "https://github.com/fedimint/fedimint-clientd", rev = "16fe9dd32c745267304a55aacee9501050bb03fa" }
names = "0.14.0"

[dev-dependencies]
Expand Down
5 changes: 4 additions & 1 deletion migrations/2024-02-20-210617_user_info/up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ CREATE TABLE app_user (
name VARCHAR(255) NOT NULL UNIQUE,
unblinded_msg VARCHAR(255) NOT NULL UNIQUE,
federation_id VARCHAR(64) NOT NULL,
federation_invite_code VARCHAR(255) NOT NULL
federation_invite_code VARCHAR(255) NOT NULL,
invoice_index INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX idx_app_user_unblinded_msg ON app_user (unblinded_msg);
Expand All @@ -14,7 +15,9 @@ CREATE TABLE invoice (
id SERIAL PRIMARY KEY,
federation_id VARCHAR(64) NOT NULL,
op_id VARCHAR(64) NOT NULL,
preimage VARCHAR(64) NOT NULL,
app_user_id INTEGER NOT NULL references app_user(id),
user_invoice_index INTEGER NOT NULL,
bolt11 VARCHAR(2048) NOT NULL,
amount BIGINT NOT NULL,
state INTEGER NOT NULL DEFAULT 0
Expand Down
6 changes: 6 additions & 0 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub(crate) trait DBConnection {
fn set_invoice_state(&self, invoice: Invoice, s: i32) -> anyhow::Result<()>;
fn get_user_by_name(&self, name: String) -> anyhow::Result<Option<AppUser>>;
fn get_user_by_id(&self, id: i32) -> anyhow::Result<Option<AppUser>>;
fn get_user_and_increment_counter(&self, name: &str) -> anyhow::Result<Option<AppUser>>;
fn insert_new_zap(&self, new_zap: NewZap) -> anyhow::Result<Zap>;
fn get_zap_by_id(&self, id: i32) -> anyhow::Result<Option<Zap>>;
fn set_zap_event_id(&self, zap: Zap, event_id: String) -> anyhow::Result<()>;
Expand Down Expand Up @@ -66,6 +67,11 @@ impl DBConnection for PostgresConnection {
AppUser::get_by_id(conn, id)
}

fn get_user_and_increment_counter(&self, name: &str) -> anyhow::Result<Option<AppUser>> {
let conn = &mut self.db.get()?;
AppUser::get_by_name_and_increment_counter(conn, name)
}

fn insert_new_invoice(&self, new_invoice: NewInvoice) -> anyhow::Result<Invoice> {
let conn = &mut self.db.get()?;
new_invoice.insert(conn)
Expand Down
89 changes: 31 additions & 58 deletions src/invoice.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
use std::{collections::HashMap, str::FromStr, time::Duration};
use std::{collections::HashMap, str::FromStr};

use anyhow::{anyhow, Result};
use fedimint_client::{oplog::UpdateStreamOrOutcome, ClientArc};
use fedimint_core::{config::FederationId, core::OperationId, task::spawn, Amount};
use fedimint_client::oplog::UpdateStreamOrOutcome;
use fedimint_core::{config::FederationId, task::spawn};
use fedimint_ln_client::{LightningClientModule, LnReceiveState};
use fedimint_mint_client::{MintClientModule, OOBNotes};
use fedimint_ln_common::bitcoin::hashes::sha256::Hash as Sha256;
use fedimint_ln_common::bitcoin::hashes::Hash;
use fedimint_ln_common::bitcoin::secp256k1::{Secp256k1, SecretKey};
use fedimint_ln_common::lightning_invoice::{Currency, InvoiceBuilder, PaymentSecret};
use futures::StreamExt;
use itertools::Itertools;
use lightning_invoice::{Currency, InvoiceBuilder, PaymentSecret};
use log::{error, info};
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::{bitcoin::hashes::sha256::Hash as Sha256, Keys};
use nostr::Keys;
use nostr::{Event, EventBuilder, JsonUtil};
use nostr_sdk::Client;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -62,11 +62,10 @@ pub(crate) async fn handle_pending_invoices(state: &State) -> Result<()> {
let user = state
.db
.get_user_by_id(invoice.app_user_id)?
.map_or(Err(anyhow!("no user")), Ok)?;
.ok_or(anyhow!("no user"))?;
spawn_invoice_subscription(
state.clone(),
invoice,
client.clone(),
user.clone(),
subscription,
)
Expand All @@ -83,8 +82,7 @@ pub(crate) async fn handle_pending_invoices(state: &State) -> Result<()> {
pub(crate) async fn spawn_invoice_subscription(
state: State,
i: Invoice,
client: ClientArc,
userrelays: AppUser,
user: AppUser,
subscription: UpdateStreamOrOutcome<LnReceiveState>,
) {
spawn("waiting for invoice being paid", async move {
Expand All @@ -108,16 +106,7 @@ pub(crate) async fn spawn_invoice_subscription(
}
LnReceiveState::Claimed => {
info!("Payment claimed");
match notify_user(
client,
&nostr,
&state,
i.id,
i.amount as u64,
userrelays.clone(),
)
.await
{
match notify_user(&nostr, &state, &i, user).await {
Ok(_) => {
match state.db.set_invoice_state(i, InvoiceState::Settled as i32) {
Ok(_) => (),
Expand All @@ -140,54 +129,38 @@ pub(crate) async fn spawn_invoice_subscription(
}

async fn notify_user(
client: ClientArc,
nostr: &Client,
state: &State,
id: i32,
amount: u64,
app_user_relays: AppUser,
) -> Result<(), Box<dyn std::error::Error>> {
let mint = client.get_first_module::<MintClientModule>();
let (operation_id, notes) = mint
.spend_notes(Amount::from_msats(amount), Duration::from_secs(604800), ())
.await?;

send_nostr_dm(nostr, &app_user_relays, operation_id, amount, notes).await?;

// Send zap if needed
if let Some(zap) = state.db.get_zap_by_id(id)? {
let request = Event::from_json(zap.request.clone())?;
let event = create_zap_event(request, amount, nostr.keys().await)?;

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

state.db.set_zap_event_id(zap, event_id.to_string())?;
}

Ok(())
}

async fn send_nostr_dm(
nostr: &Client,
app_user_relays: &AppUser,
operation_id: OperationId,
amount: u64,
notes: OOBNotes,
invoice: &Invoice,
user: AppUser,
) -> Result<()> {
let zap = state.db.get_zap_by_id(invoice.id)?;

let dm = nostr
.send_direct_msg(
XOnlyPublicKey::from_str(&app_user_relays.pubkey).unwrap(),
XOnlyPublicKey::from_str(&user.pubkey)?,
json!({
"operationId": operation_id,
"amount": amount,
"notes": notes.to_string(),
"federation_id": invoice.federation_id,
"tweak_index": invoice.user_invoice_index,
"amount": invoice.amount,
"zap_request": zap.as_ref().map(|z| z.request.clone()),
})
.to_string(),
None,
)
.await?;

// Send zap if needed
if let Some(zap) = zap {
let request = Event::from_json(&zap.request)?;
let event = create_zap_event(request, invoice.amount as u64, nostr.keys().await)?;

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

state.db.set_zap_event_id(zap, event_id.to_string())?;
}

info!("Sent nostr dm: {dm}");
Ok(())
}
Expand Down
71 changes: 47 additions & 24 deletions src/lnurlp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,19 @@ use crate::{
State,
};
use anyhow::anyhow;
use fedimint_core::{config::FederationId, Amount};
use fedimint_core::{config::FederationId, Amount, BitcoinHash};
use fedimint_ln_client::LightningClientModule;
use fedimint_ln_common::bitcoin::hashes::sha256;
use fedimint_ln_common::bitcoin::secp256k1::Parity;
use fedimint_ln_common::lightning_invoice::{Bolt11InvoiceDescription, Sha256};
use nostr::{Event, JsonUtil, Kind};

use crate::routes::{LnurlStatus, LnurlType, LnurlWellKnownResponse};

fn calc_metadata(name: &str, domain: &str) -> String {
format!("[[\"text/identifier\",\"{name}@{domain}\"],[\"text/plain\",\"Sats for {name}\"]]")
}

pub async fn well_known_lnurlp(
state: &State,
name: String,
Expand All @@ -25,8 +32,8 @@ pub async fn well_known_lnurlp(
let res = LnurlWellKnownResponse {
callback: format!("{}/lnurlp/{}/callback", state.domain, name).parse()?,
max_sendable: Amount { msats: 100000 },
min_sendable: Amount { msats: 1000 },
metadata: "test metadata".to_string(), // TODO what should this be?
min_sendable: Amount { msats: MIN_AMOUNT },
metadata: calc_metadata(&name, &state.domain_no_http()),
comment_allowed: None,
tag: LnurlType::PayRequest,
status: LnurlStatus::Ok,
Expand All @@ -44,14 +51,17 @@ pub async fn lnurl_callback(
name: String,
params: LnurlCallbackParams,
) -> anyhow::Result<LnurlCallbackResponse> {
let user = state.db.get_user_by_name(name.clone())?;
let user = state.db.get_user_and_increment_counter(&name)?;
if user.is_none() {
return Err(anyhow!("NotFound"));
}
let user = user.expect("just checked");

if params.amount < MIN_AMOUNT {
return Err(anyhow::anyhow!("Amount < MIN_AMOUNT"));
return Err(anyhow::anyhow!(
"Amount ({}) < MIN_AMOUNT ({MIN_AMOUNT})",
params.amount
));
}

// verify nostr param is a zap request
Expand All @@ -70,26 +80,46 @@ pub async fn lnurl_callback(
.mm
.get_federation_client(federation_id)
.await
.map_or(Err(anyhow!("NotFound")), Ok)?;
.ok_or(anyhow!("NotFound"))?;

let ln = client.get_first_module::<LightningClientModule>();

let (op_id, pr) = ln
.create_bolt11_invoice(
Amount {
msats: params.amount,
},
"test invoice".to_string(), // todo set description hash properly
None,
// calculate description hash for invoice
let desc_hash = match params.nostr {
Some(ref nostr) => Sha256(sha256::Hash::hash(nostr.as_bytes())),
None => {
let metadata = calc_metadata(&name, &state.domain_no_http());
Sha256(sha256::Hash::hash(metadata.as_bytes()))
}
};

let invoice_index = user.invoice_index;

let gateway = state
.mm
.get_gateway(&federation_id)
.await
.ok_or(anyhow!("Not gateway configured for federation"))?;

let (op_id, pr, preimage) = ln
.create_bolt11_invoice_for_user_tweaked(
Amount::from_msats(params.amount),
Bolt11InvoiceDescription::Hash(&desc_hash),
Some(86_400), // 1 day expiry
user.pubkey().public_key(Parity::Even), // todo is this parity correct / easy to work with?
invoice_index as u64,
(),
Some(gateway),
)
.await?;

// insert invoice into db for later verification
let new_invoice = NewInvoice {
federation_id: federation_id.to_string(),
op_id: op_id.to_string(),
preimage: hex::encode(preimage),
app_user_id: user.id,
user_invoice_index: invoice_index,
bolt11: pr.to_string(),
amount: params.amount as i64,
state: InvoiceState::Pending as i32,
Expand All @@ -112,14 +142,7 @@ pub async fn lnurl_callback(
.await
.expect("subscribing to a just created operation can't fail");

spawn_invoice_subscription(
state.clone(),
created_invoice,
client,
user.clone(),
subscription,
)
.await;
spawn_invoice_subscription(state.clone(), created_invoice, user.clone(), subscription).await;

let verify_url = format!("{}/lnurlp/{}/verify/{}", state.domain, user.name, op_id);

Expand All @@ -141,12 +164,12 @@ pub async fn verify(
let invoice = state
.db
.get_invoice_by_op_id(op_id)?
.map_or(Err(anyhow::anyhow!("NotFound")), Ok)?;
.ok_or(anyhow::anyhow!("NotFound"))?;

let user = state
.db
.get_user_by_name(name)?
.map_or(Err(anyhow::anyhow!("NotFound")), Ok)?;
.ok_or(anyhow::anyhow!("NotFound"))?;

if invoice.app_user_id != user.id {
return Err(anyhow::anyhow!("NotFound"));
Expand All @@ -155,7 +178,7 @@ pub async fn verify(
let verify_response = LnurlVerifyResponse {
status: LnurlStatus::Ok,
settled: invoice.state == InvoiceState::Settled as i32,
preimage: "".to_string(), // TODO: figure out how to get the preimage from fedimint client
preimage: invoice.preimage,
pr: invoice.bolt11,
};

Expand Down
Loading