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

add NWC commands #1152

Closed
wants to merge 2 commits into from
Closed
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
110 changes: 99 additions & 11 deletions mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ use ::nostr::nips::nip47::Method;
use ::nostr::nips::nip57;
#[cfg(target_arch = "wasm32")]
use ::nostr::prelude::rand::rngs::OsRng;
use ::nostr::prelude::ZapRequestData;
use ::nostr::prelude::{LookupInvoiceResponseResult, TransactionType, ZapRequestData};
#[cfg(target_arch = "wasm32")]
use ::nostr::Tag;
use ::nostr::{EventBuilder, EventId, JsonUtil, Keys, Kind};
Expand Down Expand Up @@ -145,6 +145,7 @@ pub trait InvoiceHandler {
fn get_network(&self) -> Network;
async fn get_best_block(&self) -> Result<BestBlock, MutinyError>;
async fn lookup_payment(&self, payment_hash: &[u8; 32]) -> Option<MutinyInvoice>;
async fn get_payments_by_label(&self, label: &str) -> Result<Vec<MutinyInvoice>, MutinyError>;
async fn pay_invoice(
&self,
invoice: &Bolt11Invoice,
Expand All @@ -156,6 +157,15 @@ pub trait InvoiceHandler {
amount: u64,
labels: Vec<String>,
) -> Result<MutinyInvoice, MutinyError>;

async fn keysend(
&self,
to_node: PublicKey,
amt_sats: u64,
custom_tlvs: Vec<CustomTLV>,
labels: Vec<String>,
preimage: Option<[u8; 32]>,
) -> Result<MutinyInvoice, MutinyError>;
}

pub struct LnUrlParams {
Expand All @@ -164,6 +174,12 @@ pub struct LnUrlParams {
pub tag: String,
}

#[derive(Clone, Eq, PartialEq, Debug)]
pub struct CustomTLV {
pub tlv_type: u64,
pub value: String,
}

/// Plan is a subscription plan for Mutiny+
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Plan {
Expand Down Expand Up @@ -417,6 +433,60 @@ impl From<Bolt11Invoice> for MutinyInvoice {
}
}

impl From<MutinyInvoice> for LookupInvoiceResponseResult {
fn from(invoice: MutinyInvoice) -> Self {
let transaction_type = if invoice.inbound {
Some(TransactionType::Incoming)
} else {
Some(TransactionType::Outgoing)
};

let (description, description_hash) = match invoice.bolt11.as_ref() {
None => (None, None),
Some(invoice) => match invoice.description() {
Bolt11InvoiceDescription::Direct(desc) => (Some(desc.to_string()), None),
Bolt11InvoiceDescription::Hash(hash) => (None, Some(hash.0.to_string())),
},
};

// try to get created_at from invoice,
// if it is not set, use last_updated as that's our closest approximation
let created_at = invoice
.bolt11
.as_ref()
.map(|b| b.duration_since_epoch().as_secs())
.unwrap_or(invoice.last_updated);

let settled_at = if invoice.status == HTLCStatus::Succeeded {
Some(invoice.last_updated)
} else {
None
};

// only reveal preimage if it is settled
let preimage = if invoice.status == HTLCStatus::Succeeded {
invoice.preimage
} else {
None
};

LookupInvoiceResponseResult {
transaction_type,
invoice: invoice.bolt11.map(|i| i.to_string()),
description,
description_hash,
preimage,
payment_hash: invoice.payment_hash.into_32().to_lower_hex_string(),
amount: invoice.amount_sats.map(|a| a * 1_000).unwrap_or(0),
fees_paid: invoice.fees_paid.map(|a| a * 1_000).unwrap_or(0),
created_at,
expires_at: invoice.expire,
settled_at,
metadata: Default::default(),
}
}
}

impl From<MutinyInvoice> for PaymentInfo {
fn from(invoice: MutinyInvoice) -> Self {
let preimage: Option<[u8; 32]> = invoice
Expand Down Expand Up @@ -1264,16 +1334,8 @@ impl<S: MutinyStorage> MutinyWallet<S> {
if event.verify().is_ok() {
match event.kind {
Kind::WalletConnectRequest => {
match nostr.handle_nwc_request(*event, &self_clone).await {
Ok(Some(event)) => {
if let Err(e) = client.send_event(event).await {
log_warn!(logger, "Error sending NWC event: {e}");
}
}
Ok(None) => {} // no response
Err(e) => {
log_error!(logger, "Error handling NWC request: {e}");
}
if let Err(e) = nostr.handle_nwc_request(*event, &self_clone).await {
log_error!(logger, "Error handling NWC request: {e}")
}
}
Kind::EncryptedDirectMessage => {
Expand Down Expand Up @@ -3103,6 +3165,19 @@ impl<S: MutinyStorage> InvoiceHandler for MutinyWallet<S> {
.ok()
}

async fn get_payments_by_label(&self, label: &str) -> Result<Vec<MutinyInvoice>, MutinyError> {
let label_activity = self.get_label_activity(&label.to_string()).await?;
let mut invoices: Vec<MutinyInvoice> = Vec::with_capacity(label.len());

for item in label_activity {
if let ActivityItem::Lightning(mutiny_invoice) = item {
invoices.push(*mutiny_invoice);
}
}

Ok(invoices)
}

async fn pay_invoice(
&self,
invoice: &Bolt11Invoice,
Expand All @@ -3119,6 +3194,19 @@ impl<S: MutinyStorage> InvoiceHandler for MutinyWallet<S> {
) -> Result<MutinyInvoice, MutinyError> {
self.create_lightning_invoice(amount, labels).await
}

async fn keysend(
&self,
to_node: PublicKey,
amt_sats: u64,
custom_tlvs: Vec<CustomTLV>,
labels: Vec<String>,
preimage: Option<[u8; 32]>,
) -> Result<MutinyInvoice, MutinyError> {
self.node_manager
.keysend(None, to_node, amt_sats, custom_tlvs, labels, preimage)
.await
}
}

async fn create_federations<S: MutinyStorage>(
Expand Down
48 changes: 31 additions & 17 deletions mutiny-core/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::{
onchain::OnChainWallet,
peermanager::{GossipMessageHandler, PeerManagerImpl},
utils::{self, sleep},
MutinyInvoice, PrivacyLevel,
CustomTLV, MutinyInvoice, PrivacyLevel,
};
use crate::{fees::P2WSH_OUTPUT_SIZE, peermanager::connect_peer_if_necessary};
use crate::{keymanager::PhantomKeysManager, scorer::HubPreferentialScorer};
Expand All @@ -41,7 +41,6 @@ use futures_util::lock::Mutex;
use hex_conservative::DisplayHex;
use lightning::events::bump_transaction::{BumpTransactionEventHandler, Wallet};
use lightning::ln::channelmanager::ChannelDetails;
use lightning::ln::PaymentSecret;
use lightning::onion_message::messenger::OnionMessenger as LdkOnionMessenger;
use lightning::routing::scoring::ProbabilisticScoringDecayParameters;
use lightning::sign::{EntropySource, InMemorySigner, NodeSigner, Recipient};
Expand Down Expand Up @@ -1614,9 +1613,10 @@ impl<S: MutinyStorage> Node<S> {
&self,
to_node: PublicKey,
amt_sats: u64,
message: Option<String>,
custom_tlvs: Vec<CustomTLV>,
labels: Vec<String>,
payment_id: PaymentId,
preimage: Option<[u8; 32]>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is added but it's never used. I don't know why it would be useful.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is used from the NWC stuff, allows for specifying a preimage for the keysend.

We need it for ourself as well so we don't generate a completely random preimage for the event, need to do it deterministically so if we process the event multiple times we won't send the payment multiple times

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm generating the preimage deterministically from the nwc request event in the handle_nwc_keysend_payment method and passing it as an option. Got from @benthecarman that this would be needed to avoid making another keysend payment for the same event in case we try to process that same request event more than once.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whoops, saw your comment after posted mine lol

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked and didn't see it. Where is it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

) -> Result<MutinyInvoice, MutinyError> {
let amt_msats = amt_sats * 1_000;

Expand Down Expand Up @@ -1651,13 +1651,15 @@ impl<S: MutinyStorage> Node<S> {
sleep(1_000).await;
}

let mut entropy = [0u8; 32];
getrandom::getrandom(&mut entropy).map_err(|_| MutinyError::SeedGenerationFailed)?;
let payment_secret = PaymentSecret(entropy);

let mut entropy = [0u8; 32];
getrandom::getrandom(&mut entropy).map_err(|_| MutinyError::SeedGenerationFailed)?;
let preimage = PaymentPreimage(entropy);
let preimage = match preimage {
Some(value) => PaymentPreimage(value),
None => {
let mut entropy = [0u8; 32];
getrandom::getrandom(&mut entropy)
.map_err(|_| MutinyError::SeedGenerationFailed)?;
PaymentPreimage(entropy)
}
};

let payment_params = PaymentParameters::for_keysend(to_node, 40, false);
let route_params: RouteParameters = RouteParameters {
Expand All @@ -1666,13 +1668,17 @@ impl<S: MutinyStorage> Node<S> {
max_total_routing_fee_msat: None,
};

let recipient_onion = if let Some(msg) = message {
// keysend messages are encoded as TLV type 34349334
RecipientOnionFields::secret_only(payment_secret)
.with_custom_tlvs(vec![(34349334, msg.encode())])
let recipient_onion = if !custom_tlvs.is_empty() {
let custom_tlvs: Vec<(u64, Vec<u8>)> = custom_tlvs
.into_iter()
.map(|tlv| (tlv.tlv_type, tlv.value.encode()))
.collect();

RecipientOnionFields::spontaneous_empty()
.with_custom_tlvs(custom_tlvs)
.map_err(|_| {
log_error!(self.logger, "could not encode keysend message");
MutinyError::InvoiceCreationFailed
MutinyError::InvalidArgumentsError
})?
} else {
RecipientOnionFields::spontaneous_empty()
Expand Down Expand Up @@ -1732,17 +1738,25 @@ impl<S: MutinyStorage> Node<S> {
&self,
to_node: PublicKey,
amt_sats: u64,
message: Option<String>,
custom_tlvs: Vec<CustomTLV>,
labels: Vec<String>,
timeout_secs: Option<u64>,
preimage: Option<[u8; 32]>,
) -> Result<MutinyInvoice, MutinyError> {
let mut entropy = [0u8; 32];
getrandom::getrandom(&mut entropy).map_err(|_| MutinyError::SeedGenerationFailed)?;
let payment_id = PaymentId(entropy);

// initiate payment
let pay = self
.init_keysend_payment(to_node, amt_sats, message, labels.clone(), payment_id)
.init_keysend_payment(
to_node,
amt_sats,
custom_tlvs,
labels.clone(),
payment_id,
preimage,
)
.await?;

let timeout: u64 = timeout_secs.unwrap_or(DEFAULT_PAYMENT_TIMEOUT);
Expand Down
7 changes: 4 additions & 3 deletions mutiny-core/src/nodemanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use crate::labels::LabelStorage;
use crate::ldkstorage::CHANNEL_CLOSURE_PREFIX;
use crate::logging::LOGGING_KEY;
use crate::utils::{sleep, spawn};
use crate::MutinyInvoice;
use crate::MutinyWalletConfig;
use crate::{
chain::MutinyChain,
Expand All @@ -23,6 +22,7 @@ use crate::{
node::NodeBuilder,
storage::{MutinyStorage, DEVICE_ID_KEY, KEYCHAIN_STORE_KEY, NEED_FULL_SYNC_KEY},
};
use crate::{CustomTLV, MutinyInvoice};
use anyhow::anyhow;
use async_lock::RwLock;
use bdk::chain::{BlockId, ConfirmationTime};
Expand Down Expand Up @@ -1406,12 +1406,13 @@ impl<S: MutinyStorage> NodeManager<S> {
self_node_pubkey: Option<&PublicKey>,
to_node: PublicKey,
amt_sats: u64,
message: Option<String>,
custom_tlvs: Vec<CustomTLV>,
labels: Vec<String>,
preimage: Option<[u8; 32]>,
) -> Result<MutinyInvoice, MutinyError> {
let node = self.get_node_by_key_or_first(self_node_pubkey).await?;
log_debug!(self.logger, "Keysending to {to_node}");
node.keysend_with_timeout(to_node, amt_sats, message, labels, None)
node.keysend_with_timeout(to_node, amt_sats, custom_tlvs, labels, None, preimage)
.await
}

Expand Down
8 changes: 3 additions & 5 deletions mutiny-core/src/nostr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1531,7 +1531,7 @@ impl<S: MutinyStorage, P: PrimalApi, C: NostrClient> NostrManager<S, P, C> {
&self,
event: Event,
invoice_handler: &impl InvoiceHandler,
) -> anyhow::Result<Option<Event>> {
) -> anyhow::Result<()> {
let nwc = {
let vec = self.nwc.read().unwrap();
vec.iter()
Expand All @@ -1542,11 +1542,9 @@ impl<S: MutinyStorage, P: PrimalApi, C: NostrClient> NostrManager<S, P, C> {
self.storage.set_nwc_sync_time(event.created_at.as_u64())?;

if let Some(mut nwc) = nwc {
let event = nwc.handle_nwc_request(event, invoice_handler, self).await?;
Ok(event)
} else {
Ok(None)
nwc.handle_nwc_request(event, invoice_handler, self).await?;
}
Ok(())
}

pub(crate) fn save_nwc_profile(&self, nwc: NostrWalletConnect) -> Result<(), MutinyError> {
Expand Down
Loading
Loading