Skip to content
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
5 changes: 5 additions & 0 deletions crates/nostr/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@

-->

## Unreleased

### Added
- Add nip47 holdinvoice methods and notification

## v0.43.0 - 2025/07/28

### Breaking changes
Expand Down
180 changes: 180 additions & 0 deletions crates/nostr/src/nips/nip47.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ impl fmt::Display for Method {
Method::ListTransactions => write!(f, "list_transactions"),
Method::GetBalance => write!(f, "get_balance"),
Method::GetInfo => write!(f, "get_info"),
Method::MakeHoldInvoice => write!(f, "make_hold_invoice"),
Method::CancelHoldInvoice => write!(f, "cancel_hold_invoice"),
Method::SettleHoldInvoice => write!(f, "settle_hold_invoice"),
}
}
}
Expand All @@ -153,6 +156,9 @@ impl FromStr for Method {
"list_transactions" => Ok(Method::ListTransactions),
"get_balance" => Ok(Method::GetBalance),
"get_info" => Ok(Method::GetInfo),
"make_hold_invoice" => Ok(Method::MakeHoldInvoice),
"cancel_hold_invoice" => Ok(Method::CancelHoldInvoice),
"settle_hold_invoice" => Ok(Method::SettleHoldInvoice),
_ => Err(Error::InvalidURI),
}
}
Expand Down Expand Up @@ -203,6 +209,15 @@ pub enum Method {
/// Get Info
#[serde(rename = "get_info")]
GetInfo,
/// Make Hold Invoice
#[serde(rename = "make_hold_invoice")]
MakeHoldInvoice,
/// Cancel Hold Invoice
#[serde(rename = "cancel_hold_invoice")]
CancelHoldInvoice,
/// Settle Hold Invoice
#[serde(rename = "settle_hold_invoice")]
SettleHoldInvoice,
}

/// Nostr Wallet Connect Request
Expand All @@ -226,6 +241,12 @@ pub enum RequestParams {
GetBalance,
/// Get Info
GetInfo,
/// Make Hold Invoice
MakeHoldInvoice(MakeHoldInvoiceRequest),
/// Cancel Hold Invoice
CancelHoldInvoice(CancelHoldInvoiceRequest),
/// Settle Hold Invoice
SettleHoldInvoice(SettleHoldInvoiceRequest),
}

impl Serialize for RequestParams {
Expand All @@ -249,6 +270,9 @@ impl Serialize for RequestParams {
let map = serializer.serialize_map(None)?;
map.end()
}
RequestParams::MakeHoldInvoice(p) => p.serialize(serializer),
RequestParams::CancelHoldInvoice(p) => p.serialize(serializer),
RequestParams::SettleHoldInvoice(p) => p.serialize(serializer),
}
}
}
Expand Down Expand Up @@ -381,6 +405,41 @@ pub struct ListTransactionsRequest {
pub transaction_type: Option<TransactionType>,
}

/// Make Hold Invoice Request
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MakeHoldInvoiceRequest {
/// Amount in millisatoshis
pub amount: u64,
/// Invoice description
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Invoice description hash
#[serde(skip_serializing_if = "Option::is_none")]
pub description_hash: Option<String>,
/// Invoice expiry in seconds
#[serde(skip_serializing_if = "Option::is_none")]
pub expiry: Option<u64>,
/// payment_hash
pub payment_hash: String,
/// The minimum CLTV delta to use for the final hop
#[serde(skip_serializing_if = "Option::is_none")]
pub cltv_expiry_delta: Option<u32>,
}

/// Cancel Hold Invoice Request
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CancelHoldInvoiceRequest {
/// payment_hash
pub payment_hash: String,
}

/// Settle Hold Invoice Request
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SettleHoldInvoiceRequest {
/// preimage
pub preimage: String,
}

/// NIP47 Request
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
pub struct Request {
Expand Down Expand Up @@ -507,6 +566,18 @@ impl Request {
}
Method::GetBalance => RequestParams::GetBalance,
Method::GetInfo => RequestParams::GetInfo,
Method::MakeHoldInvoice => {
let params: MakeHoldInvoiceRequest = serde_json::from_value(template.params)?;
RequestParams::MakeHoldInvoice(params)
}
Method::SettleHoldInvoice => {
let params: SettleHoldInvoiceRequest = serde_json::from_value(template.params)?;
RequestParams::SettleHoldInvoice(params)
}
Method::CancelHoldInvoice => {
let params: CancelHoldInvoiceRequest = serde_json::from_value(template.params)?;
RequestParams::CancelHoldInvoice(params)
}
};

Ok(Self {
Expand Down Expand Up @@ -645,6 +716,42 @@ pub struct GetInfoResponse {
pub notifications: Vec<String>,
}

/// Make Hold Invoice Response
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct MakeHoldInvoiceResponse {
/// Transaction type
#[serde(rename = "type")]
pub transaction_type: TransactionType,
/// Bolt11 invoice
#[serde(skip_serializing_if = "Option::is_none")]
pub invoice: Option<String>,
/// Description
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Description hash
#[serde(skip_serializing_if = "Option::is_none")]
pub description_hash: Option<String>,
/// Payment hash
pub payment_hash: String,
/// Amount in millisatoshis
pub amount: u64,
/// Creation timestamp
pub created_at: Timestamp,
/// Expiration timestamp
pub expires_at: Timestamp,
/// Metadata
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Value>,
}

/// Cancel Hold Invoice Response
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct CancelHoldInvoiceResponse {}

/// Settle Hold Invoice Response
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct SettleHoldInvoiceResponse {}

/// NIP47 Response Result
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResponseResult {
Expand All @@ -666,6 +773,12 @@ pub enum ResponseResult {
GetBalance(GetBalanceResponse),
/// Get Info
GetInfo(GetInfoResponse),
/// Make Hold Invoice
MakeHoldInvoice(MakeHoldInvoiceResponse),
/// Cancel Hold Invoice
CancelHoldInvoice(CancelHoldInvoiceResponse),
/// Settle Hold Invoice
SettleHoldInvoice(SettleHoldInvoiceResponse),
}

impl Serialize for ResponseResult {
Expand All @@ -687,6 +800,9 @@ impl Serialize for ResponseResult {
}
ResponseResult::GetBalance(p) => p.serialize(serializer),
ResponseResult::GetInfo(p) => p.serialize(serializer),
ResponseResult::MakeHoldInvoice(p) => p.serialize(serializer),
ResponseResult::CancelHoldInvoice(p) => p.serialize(serializer),
ResponseResult::SettleHoldInvoice(p) => p.serialize(serializer),
}
}
}
Expand Down Expand Up @@ -770,6 +886,18 @@ impl Response {
let result: GetInfoResponse = serde_json::from_value(result)?;
ResponseResult::GetInfo(result)
}
Method::MakeHoldInvoice => {
let result: MakeHoldInvoiceResponse = serde_json::from_value(result)?;
ResponseResult::MakeHoldInvoice(result)
}
Method::CancelHoldInvoice => {
let result: CancelHoldInvoiceResponse = serde_json::from_value(result)?;
ResponseResult::CancelHoldInvoice(result)
}
Method::SettleHoldInvoice => {
let result: SettleHoldInvoiceResponse = serde_json::from_value(result)?;
ResponseResult::SettleHoldInvoice(result)
}
};

Ok(Self {
Expand Down Expand Up @@ -1044,13 +1172,17 @@ pub enum NotificationType {
/// A payment was successfully sent by the wallet
#[serde(rename = "payment_sent")]
PaymentSent,
/// A hold invoice has enough funds locked
#[serde(rename = "hold_invoice_accepted")]
HoldInvoiceAccepted,
}

impl fmt::Display for NotificationType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NotificationType::PaymentReceived => write!(f, "payment_received"),
NotificationType::PaymentSent => write!(f, "payment_sent"),
NotificationType::HoldInvoiceAccepted => write!(f, "hold_invoice_accepted"),
}
}
}
Expand All @@ -1062,6 +1194,7 @@ impl FromStr for NotificationType {
match s {
"payment_received" => Ok(NotificationType::PaymentReceived),
"payment_sent" => Ok(NotificationType::PaymentSent),
"hold_invoice_accepted" => Ok(NotificationType::HoldInvoiceAccepted),
_ => Err(Error::InvalidURI),
}
}
Expand Down Expand Up @@ -1107,6 +1240,10 @@ impl Notification {
let result: PaymentNotification = serde_json::from_value(result)?;
NotificationResult::PaymentSent(result)
}
NotificationType::HoldInvoiceAccepted => {
let result: HoldInvoiceAcceptedNotification = serde_json::from_value(result)?;
NotificationResult::HoldInvoiceAccepted(result)
}
};

Ok(Self {
Expand All @@ -1126,6 +1263,17 @@ impl Notification {

Err(Error::UnexpectedResult)
}

/// Convert [Notification] to [HoldInvoiceAcceptedNotification]
pub fn to_holdinvoice_accepted_notification(
self,
) -> Result<HoldInvoiceAcceptedNotification, Error> {
if let NotificationResult::HoldInvoiceAccepted(result) = self.notification {
return Ok(result);
}

Err(Error::UnexpectedResult)
}
}

impl JsonUtil for Notification {
Expand All @@ -1149,6 +1297,8 @@ pub enum NotificationResult {
PaymentReceived(PaymentNotification),
/// Payment sent
PaymentSent(PaymentNotification),
/// Hold invoice accepted (locked in)
HoldInvoiceAccepted(HoldInvoiceAcceptedNotification),
}

impl Serialize for NotificationResult {
Expand All @@ -1159,6 +1309,7 @@ impl Serialize for NotificationResult {
match self {
NotificationResult::PaymentReceived(p) => p.serialize(serializer),
NotificationResult::PaymentSent(p) => p.serialize(serializer),
NotificationResult::HoldInvoiceAccepted(p) => p.serialize(serializer),
}
}
}
Expand Down Expand Up @@ -1198,6 +1349,35 @@ pub struct PaymentNotification {
pub metadata: Option<Value>,
}

/// Hold Invoice accepted notification
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct HoldInvoiceAcceptedNotification {
/// Transaction type
#[serde(rename = "type")]
pub transaction_type: TransactionType,
/// Bolt11 invoice
pub invoice: String,
/// Description
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Description hash
#[serde(skip_serializing_if = "Option::is_none")]
pub description_hash: Option<String>,
/// Payment hash
pub payment_hash: String,
/// Amount in millisatoshis
pub amount: u64,
/// Creation timestamp
pub created_at: Timestamp,
/// Expiration timestamp
pub expires_at: Timestamp,
/// Settled deadline in blockheight
pub settle_deadline: u32,
/// Optional metadata about the payment
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Value>,
}

#[cfg(test)]
mod tests {
use core::str::FromStr;
Expand Down
19 changes: 19 additions & 0 deletions crates/nwc/examples/notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ async fn main() -> Result<()> {
print_payment_details(&payment);
}
}
NotificationType::HoldInvoiceAccepted => {
if let Ok(hold_accepted) = notification.to_holdinvoice_accepted_notification() {
println!("🟡 Hold Invoice Accepted!");
print_holdinvoice_details(&hold_accepted);
}
}
}
Ok(false) // Continue processing
}) => {
Expand Down Expand Up @@ -91,3 +97,16 @@ fn print_payment_details(payment: &PaymentNotification) {
}
println!();
}

fn print_holdinvoice_details(hold_accepted: &HoldInvoiceAcceptedNotification) {
println!(" 💰 Amount: {} msat", hold_accepted.amount);
if let Some(description) = &hold_accepted.description {
println!(" 📝 Description: {}", description);
}
println!(" 🔗 Payment Hash: {}", hold_accepted.payment_hash);
println!(
" 📅 Settle until blockheight: {}",
hold_accepted.settle_deadline
);
println!();
}