Skip to content

Commit

Permalink
Merge pull request #4 from MutinyWallet/external-receive
Browse files Browse the repository at this point in the history
External receive support
  • Loading branch information
TonyGiorgio authored Mar 26, 2024
2 parents 1e2d9c9 + f5283de commit 040553d
Show file tree
Hide file tree
Showing 12 changed files with 618 additions and 349 deletions.
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

0 comments on commit 040553d

Please sign in to comment.