diff --git a/libs/sdk-bindings/src/breez_sdk.udl b/libs/sdk-bindings/src/breez_sdk.udl index cd028c526..d52263de4 100644 --- a/libs/sdk-bindings/src/breez_sdk.udl +++ b/libs/sdk-bindings/src/breez_sdk.udl @@ -199,6 +199,11 @@ enum PaymentTypeFilter { "ClosedChannel", }; +dictionary MetadataFilter { + string json_path; + string json_value; +}; + enum PaymentStatus { "Pending", "Complete", @@ -221,10 +226,12 @@ dictionary Payment { string? error; string? description; PaymentDetails details; + string? metadata; }; dictionary ListPaymentsRequest { sequence? filters = null; + sequence? metadata_filters = null; i64? from_timestamp = null; i64? to_timestamp = null; boolean? include_failures = null; @@ -771,11 +778,14 @@ interface BlockingBreezServices { [Throws=SdkError] void backup(); + [Throws=SdkError] + sequence list_payments(ListPaymentsRequest req); + [Throws=SdkError] Payment? payment_by_hash(string hash); [Throws=SdkError] - sequence list_payments(ListPaymentsRequest req); + void set_payment_metadata(string hash, string metadata); [Throws=SdkError] RedeemOnchainFundsResponse redeem_onchain_funds(RedeemOnchainFundsRequest req); diff --git a/libs/sdk-bindings/src/uniffi_binding.rs b/libs/sdk-bindings/src/uniffi_binding.rs index 0f1d2dc0d..3fd49a44e 100644 --- a/libs/sdk-bindings/src/uniffi_binding.rs +++ b/libs/sdk-bindings/src/uniffi_binding.rs @@ -11,16 +11,16 @@ use breez_sdk_core::{ LnUrlPayRequest, LnUrlPayRequestData, LnUrlPayResult, LnUrlPaySuccessData, LnUrlWithdrawRequest, LnUrlWithdrawRequestData, LnUrlWithdrawResult, LnUrlWithdrawSuccessData, LocaleOverrides, LocalizedName, LogEntry, LogStream, LspInformation, - MaxReverseSwapAmountResponse, MessageSuccessActionData, MetadataItem, Network, NodeConfig, - NodeCredentials, NodeState, OpenChannelFeeRequest, OpenChannelFeeResponse, OpeningFeeParams, - OpeningFeeParamsMenu, Payment, PaymentDetails, PaymentFailedData, PaymentStatus, PaymentType, - PaymentTypeFilter, PrepareRedeemOnchainFundsRequest, PrepareRedeemOnchainFundsResponse, - PrepareRefundRequest, PrepareRefundResponse, Rate, ReceiveOnchainRequest, - ReceivePaymentRequest, ReceivePaymentResponse, RecommendedFees, RedeemOnchainFundsRequest, - RedeemOnchainFundsResponse, RefundRequest, RefundResponse, ReportIssueRequest, - ReportPaymentFailureDetails, ReverseSwapFeesRequest, ReverseSwapInfo, ReverseSwapPairInfo, - ReverseSwapStatus, RouteHint, RouteHintHop, SendOnchainRequest, SendOnchainResponse, - SendPaymentRequest, SendPaymentResponse, SendSpontaneousPaymentRequest, + MaxReverseSwapAmountResponse, MessageSuccessActionData, MetadataFilter, MetadataItem, Network, + NodeConfig, NodeCredentials, NodeState, OpenChannelFeeRequest, OpenChannelFeeResponse, + OpeningFeeParams, OpeningFeeParamsMenu, Payment, PaymentDetails, PaymentFailedData, + PaymentStatus, PaymentType, PaymentTypeFilter, PrepareRedeemOnchainFundsRequest, + PrepareRedeemOnchainFundsResponse, PrepareRefundRequest, PrepareRefundResponse, Rate, + ReceiveOnchainRequest, ReceivePaymentRequest, ReceivePaymentResponse, RecommendedFees, + RedeemOnchainFundsRequest, RedeemOnchainFundsResponse, RefundRequest, RefundResponse, + ReportIssueRequest, ReportPaymentFailureDetails, ReverseSwapFeesRequest, ReverseSwapInfo, + ReverseSwapPairInfo, ReverseSwapStatus, RouteHint, RouteHintHop, SendOnchainRequest, + SendOnchainResponse, SendPaymentRequest, SendPaymentResponse, SendSpontaneousPaymentRequest, ServiceHealthCheckResponse, SignMessageRequest, SignMessageResponse, StaticBackupRequest, StaticBackupResponse, SuccessActionProcessed, SwapInfo, SwapStatus, Symbol, TlvEntry, UnspentTransactionOutput, UrlSuccessActionData, @@ -170,6 +170,10 @@ impl BlockingBreezServices { rt().block_on(self.breez_services.payment_by_hash(hash)) } + pub fn set_payment_metadata(&self, hash: String, metadata: String) -> SdkResult<()> { + rt().block_on(self.breez_services.set_payment_metadata(hash, metadata)) + } + pub fn pay_lnurl(&self, req: LnUrlPayRequest) -> Result { rt().block_on(self.breez_services.lnurl_pay(req)) } diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index 42f52b4d0..0d6f39aad 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -574,6 +574,13 @@ impl BreezServices { Ok(self.persister.get_payment_by_hash(&hash)?) } + /// Set the external metadata of a payment as a valid JSON string + pub async fn set_payment_metadata(&self, hash: String, metadata: String) -> SdkResult<()> { + Ok(self + .persister + .set_payment_external_metadata(hash, metadata)?) + } + /// Redeem on-chain funds from closed channels to the specified on-chain address, with the given feerate pub async fn redeem_onchain_funds( &self, @@ -996,6 +1003,7 @@ impl BreezServices { pending_expiration_block: None, }, }, + metadata: None, }], false, )?; @@ -1533,6 +1541,7 @@ impl BreezServices { }, }, error: None, + metadata: None, }) } @@ -2286,6 +2295,7 @@ pub(crate) mod tests { pending_expiration_block: None, }, }, + metadata: None, }, Payment { id: payment_hash_lnurl_withdraw.to_string(), @@ -2312,6 +2322,7 @@ pub(crate) mod tests { pending_expiration_block: None, }, }, + metadata: None, }, Payment { id: payment_hash_with_lnurl_success_action.to_string(), @@ -2338,6 +2349,7 @@ pub(crate) mod tests { pending_expiration_block: None, }, }, + metadata: None, }, Payment { id: hex::encode(payment_hash_swap.clone()), @@ -2364,6 +2376,7 @@ pub(crate) mod tests { pending_expiration_block: None, }, }, + metadata: None, }, ]; let node_api = Arc::new(MockNodeAPI::new(dummy_node_state.clone())); diff --git a/libs/sdk-core/src/bridge_generated.io.rs b/libs/sdk-core/src/bridge_generated.io.rs index dc9797c6f..40e55ffed 100644 --- a/libs/sdk-core/src/bridge_generated.io.rs +++ b/libs/sdk-core/src/bridge_generated.io.rs @@ -424,6 +424,15 @@ pub extern "C" fn new_box_autoadd_u64_0(value: u64) -> *mut u64 { support::new_leak_box_ptr(value) } +#[no_mangle] +pub extern "C" fn new_list_metadata_filter_0(len: i32) -> *mut wire_list_metadata_filter { + let wrap = wire_list_metadata_filter { + ptr: support::new_leak_vec_ptr(::new_with_null_ptr(), len), + len, + }; + support::new_leak_box_ptr(wrap) +} + #[no_mangle] pub extern "C" fn new_list_payment_type_filter_0(len: i32) -> *mut wire_list_payment_type_filter { let wrap = wire_list_payment_type_filter { @@ -690,6 +699,15 @@ impl Wire2Api for wire_GreenlightNodeConfig { } } +impl Wire2Api> for *mut wire_list_metadata_filter { + fn wire2api(self) -> Vec { + let vec = unsafe { + let wrap = support::box_from_leak_ptr(self); + support::vec_from_leak_ptr(wrap.ptr, wrap.len) + }; + vec.into_iter().map(Wire2Api::wire2api).collect() + } +} impl Wire2Api> for *mut wire_list_payment_type_filter { fn wire2api(self) -> Vec { let vec = unsafe { @@ -703,6 +721,7 @@ impl Wire2Api for wire_ListPaymentsRequest { fn wire2api(self) -> ListPaymentsRequest { ListPaymentsRequest { filters: self.filters.wire2api(), + metadata_filters: self.metadata_filters.wire2api(), from_timestamp: self.from_timestamp.wire2api(), to_timestamp: self.to_timestamp.wire2api(), include_failures: self.include_failures.wire2api(), @@ -772,6 +791,14 @@ impl Wire2Api for wire_LnUrlWithdrawRequestData { } } } +impl Wire2Api for wire_MetadataFilter { + fn wire2api(self) -> MetadataFilter { + MetadataFilter { + json_path: self.json_path.wire2api(), + json_value: self.json_value.wire2api(), + } + } +} impl Wire2Api for wire_NodeConfig { fn wire2api(self) -> NodeConfig { @@ -995,6 +1022,13 @@ pub struct wire_GreenlightNodeConfig { invite_code: *mut wire_uint_8_list, } +#[repr(C)] +#[derive(Clone)] +pub struct wire_list_metadata_filter { + ptr: *mut wire_MetadataFilter, + len: i32, +} + #[repr(C)] #[derive(Clone)] pub struct wire_list_payment_type_filter { @@ -1006,6 +1040,7 @@ pub struct wire_list_payment_type_filter { #[derive(Clone)] pub struct wire_ListPaymentsRequest { filters: *mut wire_list_payment_type_filter, + metadata_filters: *mut wire_list_metadata_filter, from_timestamp: *mut i64, to_timestamp: *mut i64, include_failures: *mut bool, @@ -1067,6 +1102,13 @@ pub struct wire_LnUrlWithdrawRequestData { max_withdrawable: u64, } +#[repr(C)] +#[derive(Clone)] +pub struct wire_MetadataFilter { + json_path: *mut wire_uint_8_list, + json_value: *mut wire_uint_8_list, +} + #[repr(C)] #[derive(Clone)] pub struct wire_OpenChannelFeeRequest { @@ -1332,6 +1374,7 @@ impl NewWithNullPtr for wire_ListPaymentsRequest { fn new_with_null_ptr() -> Self { Self { filters: core::ptr::null_mut(), + metadata_filters: core::ptr::null_mut(), from_timestamp: core::ptr::null_mut(), to_timestamp: core::ptr::null_mut(), include_failures: core::ptr::null_mut(), @@ -1434,6 +1477,21 @@ impl Default for wire_LnUrlWithdrawRequestData { } } +impl NewWithNullPtr for wire_MetadataFilter { + fn new_with_null_ptr() -> Self { + Self { + json_path: core::ptr::null_mut(), + json_value: core::ptr::null_mut(), + } + } +} + +impl Default for wire_MetadataFilter { + fn default() -> Self { + Self::new_with_null_ptr() + } +} + impl Default for wire_NodeConfig { fn default() -> Self { Self::new_with_null_ptr() diff --git a/libs/sdk-core/src/bridge_generated.rs b/libs/sdk-core/src/bridge_generated.rs index 66d549ce6..4568251b3 100644 --- a/libs/sdk-core/src/bridge_generated.rs +++ b/libs/sdk-core/src/bridge_generated.rs @@ -73,6 +73,7 @@ use crate::models::LnUrlWithdrawResult; use crate::models::LnUrlWithdrawSuccessData; use crate::models::LogEntry; use crate::models::MaxReverseSwapAmountResponse; +use crate::models::MetadataFilter; use crate::models::Network; use crate::models::NodeConfig; use crate::models::NodeCredentials; @@ -1665,6 +1666,7 @@ impl support::IntoDart for Payment { self.error.into_dart(), self.description.into_dart(), self.details.into_into_dart().into_dart(), + self.metadata.into_dart(), ] .into_dart() } diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs index c4c07172f..1b891977a 100644 --- a/libs/sdk-core/src/greenlight/node_api.rs +++ b/libs/sdk-core/src/greenlight/node_api.rs @@ -1528,6 +1528,7 @@ impl TryFrom for Payment { pending_expiration_block: None, }, }, + metadata: None, }) } // fn from(p: OffChainPayment) -> Self { @@ -1568,6 +1569,7 @@ impl TryFrom for Payment { pending_expiration_block: None, }, }, + metadata: None, }) } } @@ -1623,6 +1625,7 @@ impl TryFrom for Payment { pending_expiration_block: None, }, }, + metadata: None, }) } } @@ -1665,6 +1668,7 @@ impl TryFrom for Payment { pending_expiration_block: None, }, }, + metadata: None, }) } } @@ -1730,6 +1734,7 @@ impl TryFrom for Payment { pending_expiration_block: None, }, }, + metadata: None, }) } } diff --git a/libs/sdk-core/src/models.rs b/libs/sdk-core/src/models.rs index 8826c86bc..03d25b4be 100644 --- a/libs/sdk-core/src/models.rs +++ b/libs/sdk-core/src/models.rs @@ -550,6 +550,15 @@ pub enum PaymentTypeFilter { ClosedChannel, } +/// A metadata filter which can be applied when retrieving the transaction list +pub struct MetadataFilter { + /// Specifies which field to apply the filter on, using the JSON path format + pub json_path: String, + /// Specifies which JSON value to filter for. + /// As such, strings must be wrapped with quotes ("") in order to be properly filtered + pub json_value: String, +} + /// Different types of supported feerates pub enum FeeratePreset { Regular, @@ -610,7 +619,7 @@ pub enum PaymentStatus { Failed = 2, } -/// Represents a payment, including its [PaymentType] and [PaymentDetails]. +/// Represents a payment, including its [PaymentType] and [PaymentDetails] #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] pub struct Payment { pub id: String, @@ -623,6 +632,7 @@ pub struct Payment { pub error: Option, pub description: Option, pub details: PaymentDetails, + pub metadata: Option, } /// Represents a payments external information. @@ -640,6 +650,7 @@ pub struct PaymentExternalInfo { #[derive(Default)] pub struct ListPaymentsRequest { pub filters: Option>, + pub metadata_filters: Option>, /// Epoch time, in seconds pub from_timestamp: Option, /// Epoch time, in seconds diff --git a/libs/sdk-core/src/persist/migrations.rs b/libs/sdk-core/src/persist/migrations.rs index 83133c411..84e5f0221 100644 --- a/libs/sdk-core/src/persist/migrations.rs +++ b/libs/sdk-core/src/persist/migrations.rs @@ -415,7 +415,7 @@ pub(crate) fn current_migrations() -> Vec<&'static str> { "SELECT 1;", // Placeholder statement, to avoid that column is added twice (from sync fn below and here) "ALTER TABLE channels ADD COLUMN alias_local TEXT;", "ALTER TABLE channels ADD COLUMN alias_remote TEXT;", - "ALTER TABLE channels ADD COLUMN closing_txid TEXT;" + "ALTER TABLE channels ADD COLUMN closing_txid TEXT;", ] } @@ -541,5 +541,12 @@ pub(crate) fn current_sync_migrations() -> Vec<&'static str> { ALTER TABLE payments_external_info ADD COLUMN attempted_error TEXT; ", + " + CREATE TABLE IF NOT EXISTS payments_metadata ( + payment_id TEXT NOT NULL PRIMARY KEY, + metadata TEXT, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ) STRICT; + ", ] } diff --git a/libs/sdk-core/src/persist/sync.rs b/libs/sdk-core/src/persist/sync.rs index 85ea22dbf..02747e3ab 100644 --- a/libs/sdk-core/src/persist/sync.rs +++ b/libs/sdk-core/src/persist/sync.rs @@ -162,17 +162,33 @@ impl SqliteStorage { // sync remote payments_external_info table tx.execute( " - INSERT into sync.payments_external_info - SELECT - payment_id, - lnurl_success_action, - ln_address, - lnurl_metadata, - lnurl_withdraw_endpoint, - attempted_amount_msat, - attempted_error - FROM remote_sync.payments_external_info - WHERE payment_id NOT IN (SELECT payment_id FROM sync.payments_external_info);", + INSERT into sync.payments_external_info + SELECT + payment_id, + lnurl_success_action, + ln_address, + lnurl_metadata, + lnurl_withdraw_endpoint, + attempted_amount_msat, + attempted_error + FROM remote_sync.payments_external_info + WHERE payment_id NOT IN (SELECT payment_id FROM sync.payments_external_info);", + [], + )?; + + // sync remote payments_metadata table + tx.execute( + " + INSERT OR REPLACE INTO sync.payments_metadata + SELECT + remote_sync.payments_metadata.payment_id, + remote_sync.payments_metadata.metadata, + remote_sync.payments_metadata.updated_at + FROM remote_sync.payments_metadata + LEFT JOIN sync.payments_metadata + ON sync.payments_metadata.payment_id = remote_sync.payments_metadata.payment_id + WHERE + remote_sync.payments_metadata.updated_at > sync.payments_metadata.updated_at;", [], )?; diff --git a/libs/sdk-core/src/persist/transactions.rs b/libs/sdk-core/src/persist/transactions.rs index 96280609b..97be387e2 100644 --- a/libs/sdk-core/src/persist/transactions.rs +++ b/libs/sdk-core/src/persist/transactions.rs @@ -1,14 +1,17 @@ use super::db::SqliteStorage; -use super::error::PersistResult; +use super::error::{PersistError, PersistResult}; use crate::lnurl::pay::model::SuccessActionProcessed; -use crate::models::*; +use crate::{ensure_sdk, models::*}; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; use rusqlite::Row; use rusqlite::{named_params, params, OptionalExtension}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; +use serde_json::{Map, Value}; use std::str::FromStr; +const METADATA_MAX_LEN: usize = 1000; + impl SqliteStorage { /// Inserts payments into the payments table. These can be pending, completed and failed payments. Before /// persisting, it automatically deletes previously pending payments @@ -101,6 +104,40 @@ impl SqliteStorage { Ok(()) } + /// Updates the metadata object associated to a payment + pub fn set_payment_external_metadata( + &self, + payment_hash: String, + new_metadata: String, + ) -> PersistResult<()> { + ensure_sdk!( + new_metadata.len() <= METADATA_MAX_LEN, + PersistError::Generic(anyhow::anyhow!( + "Max metadata size ({} characters) has been exceeded", + METADATA_MAX_LEN + )) + ); + + let _ = serde_json::from_str::>(&new_metadata)?; + + self.get_connection()?.execute( + " + INSERT OR REPLACE INTO sync.payments_metadata( + payment_id, + metadata, + updated_at + ) + VALUES ( + ?1, + json(?2), + CURRENT_TIMESTAMP + );", + params![payment_hash, new_metadata], + )?; + + Ok(()) + } + /// Updates attempted error data associated with this payment pub fn update_payment_attempted_error( &self, @@ -155,6 +192,7 @@ impl SqliteStorage { pub fn list_payments(&self, req: ListPaymentsRequest) -> PersistResult> { let where_clause = filter_to_where_clause( req.filters, + &req.metadata_filters, req.from_timestamp, req.to_timestamp, req.include_failures, @@ -180,11 +218,15 @@ impl SqliteStorage { e.lnurl_withdraw_endpoint, e.attempted_amount_msat, e.attempted_error, - o.payer_amount_msat + o.payer_amount_msat, + m.metadata FROM payments p LEFT JOIN sync.payments_external_info e ON p.id = e.payment_id + LEFT JOIN sync.payments_metadata m + ON + p.id = m.payment_id LEFT JOIN sync.open_channel_payment_info o ON p.id = o.payment_hash @@ -196,8 +238,32 @@ impl SqliteStorage { .as_str(), )?; + let mut params: HashMap = HashMap::new(); + + if let Some(metadata_filters) = &req.metadata_filters { + metadata_filters.iter().enumerate().for_each( + |( + i, + MetadataFilter { + json_path, + json_value, + }, + )| { + params.insert(format!(":json_path_{i}"), format!("$.{json_path}")); + params.insert(format!(":json_value_{i}"), json_value.clone()); + }, + ) + } + let vec: Vec = stmt - .query_map([], |row| self.sql_row_to_payment(row))? + .query_map( + params + .iter() + .map(|(k, v)| (k.as_str(), v as &dyn ToSql)) + .collect::>() + .as_slice(), + |row| self.sql_row_to_payment(row), + )? .map(|i| i.unwrap()) .collect(); @@ -229,11 +295,15 @@ impl SqliteStorage { e.lnurl_withdraw_endpoint, e.attempted_amount_msat, e.attempted_error, - o.payer_amount_msat + o.payer_amount_msat, + m.metadata FROM payments p LEFT JOIN sync.payments_external_info e ON p.id = e.payment_id + LEFT JOIN sync.payments_metadata m + ON + p.id = m.payment_id LEFT JOIN sync.open_channel_payment_info o ON p.id = o.payment_hash @@ -276,6 +346,7 @@ impl SqliteStorage { description: row.get(6)?, details: row.get(7)?, error: row.get(13)?, + metadata: row.get(15)?, }; if let PaymentDetails::Ln { ref mut data } = payment.details { @@ -300,6 +371,7 @@ impl SqliteStorage { fn filter_to_where_clause( type_filters: Option>, + metadata_filters: &Option>, from_timestamp: Option, to_timestamp: Option, include_failures: Option, @@ -345,6 +417,12 @@ fn filter_to_where_clause( } } + if let Some(filters) = metadata_filters { + filters.iter().enumerate().for_each(|(i, _)| { + where_clause.push(format!("metadata->:json_path_{i} = :json_value_{i}")); + }); + } + let mut where_clause_str = String::new(); if !where_clause.is_empty() { where_clause_str = String::from("where "); @@ -476,6 +554,7 @@ fn test_ln_transactions() -> PersistResult<(), Box> { pending_expiration_block: None, }, }, + metadata: None, }, Payment { id: payment_hash_with_lnurl_withdraw.to_string(), @@ -502,6 +581,7 @@ fn test_ln_transactions() -> PersistResult<(), Box> { pending_expiration_block: None, }, }, + metadata: None, }, Payment { id: hex::encode(payment_hash_with_swap_info.clone()), @@ -528,6 +608,7 @@ fn test_ln_transactions() -> PersistResult<(), Box> { pending_expiration_block: None, }, }, + metadata: None, }, ]; let failed_txs = [Payment { @@ -555,6 +636,7 @@ fn test_ln_transactions() -> PersistResult<(), Box> { pending_expiration_block: None, }, }, + metadata: None, }]; let storage = SqliteStorage::new(test_utils::create_test_sql_dir()); storage.init()?; @@ -670,5 +752,47 @@ fn test_ln_transactions() -> PersistResult<(), Box> { assert_eq!(retrieve_txs.len(), 1); assert_eq!(retrieve_txs[0].id, payment_hash_with_lnurl_withdraw); + // test json metadata validation + assert!(storage + .set_payment_external_metadata( + payment_hash_with_lnurl_withdraw.to_string(), + r#"{ "malformed: true }"#.to_string() + ) + .is_err()); + + // test metadata set and filter + let test_json = r#"{"supportsBoolean":true,"supportsInt":10,"supportsString":"supports string","supportsNested":{"value":[1,2]}}"#; + let test_json_filters = Some(vec![ + MetadataFilter { + json_path: "supportsBoolean".to_string(), + json_value: "true".to_string(), + }, + MetadataFilter { + json_path: "supportsInt".to_string(), + json_value: "10".to_string(), + }, + MetadataFilter { + json_path: "supportsString".to_string(), + json_value: r#""supports string""#.to_string(), + }, + MetadataFilter { + json_path: "supportsNested.value".to_string(), + json_value: "[1,2]".to_string(), + }, + ]); + + storage.set_payment_external_metadata( + payment_hash_with_lnurl_withdraw.to_string(), + test_json.to_string(), + )?; + + let retrieve_txs = storage.list_payments(ListPaymentsRequest { + metadata_filters: test_json_filters, + ..Default::default() + })?; + assert_eq!(retrieve_txs.len(), 1); + assert_eq!(retrieve_txs[0].id, payment_hash_with_lnurl_withdraw); + assert_eq!(retrieve_txs[0].metadata, Some(test_json.to_string()),); + Ok(()) } diff --git a/libs/sdk-core/src/swap_in/swap.rs b/libs/sdk-core/src/swap_in/swap.rs index 641d3c3d5..1a858aed0 100644 --- a/libs/sdk-core/src/swap_in/swap.rs +++ b/libs/sdk-core/src/swap_in/swap.rs @@ -875,6 +875,7 @@ mod tests { pending_expiration_block: None, }, }, + metadata: None, }; persister.insert_or_update_payments(&vec![payment.clone()], false)?; diff --git a/libs/sdk-flutter/ios/Classes/bridge_generated.h b/libs/sdk-flutter/ios/Classes/bridge_generated.h index 3c27519ba..9381460eb 100644 --- a/libs/sdk-flutter/ios/Classes/bridge_generated.h +++ b/libs/sdk-flutter/ios/Classes/bridge_generated.h @@ -77,8 +77,19 @@ typedef struct wire_list_payment_type_filter { int32_t len; } wire_list_payment_type_filter; +typedef struct wire_MetadataFilter { + struct wire_uint_8_list *json_path; + struct wire_uint_8_list *json_value; +} wire_MetadataFilter; + +typedef struct wire_list_metadata_filter { + struct wire_MetadataFilter *ptr; + int32_t len; +} wire_list_metadata_filter; + typedef struct wire_ListPaymentsRequest { struct wire_list_payment_type_filter *filters; + struct wire_list_metadata_filter *metadata_filters; int64_t *from_timestamp; int64_t *to_timestamp; bool *include_failures; @@ -406,6 +417,8 @@ uint32_t *new_box_autoadd_u32_0(uint32_t value); uint64_t *new_box_autoadd_u64_0(uint64_t value); +struct wire_list_metadata_filter *new_list_metadata_filter_0(int32_t len); + struct wire_list_payment_type_filter *new_list_payment_type_filter_0(int32_t len); struct wire_list_tlv_entry *new_list_tlv_entry_0(int32_t len); @@ -501,6 +514,7 @@ static int64_t dummy_method_to_enforce_bundling(void) { dummy_var ^= ((int64_t) (void*) new_box_autoadd_static_backup_request_0); dummy_var ^= ((int64_t) (void*) new_box_autoadd_u32_0); dummy_var ^= ((int64_t) (void*) new_box_autoadd_u64_0); + dummy_var ^= ((int64_t) (void*) new_list_metadata_filter_0); dummy_var ^= ((int64_t) (void*) new_list_payment_type_filter_0); dummy_var ^= ((int64_t) (void*) new_list_tlv_entry_0); dummy_var ^= ((int64_t) (void*) new_uint_8_list_0); diff --git a/libs/sdk-flutter/lib/bridge_generated.dart b/libs/sdk-flutter/lib/bridge_generated.dart index f12e08487..d5e0c0cb1 100644 --- a/libs/sdk-flutter/lib/bridge_generated.dart +++ b/libs/sdk-flutter/lib/bridge_generated.dart @@ -629,6 +629,7 @@ class InvoicePaidDetails { /// Represents a list payments request. class ListPaymentsRequest { final List? filters; + final List? metadataFilters; /// Epoch time, in seconds final int? fromTimestamp; @@ -641,6 +642,7 @@ class ListPaymentsRequest { const ListPaymentsRequest({ this.filters, + this.metadataFilters, this.fromTimestamp, this.toTimestamp, this.includeFailures, @@ -1035,6 +1037,21 @@ class MessageSuccessActionData { }); } +/// A metadata filter which can be applied when retrieving the transaction list +class MetadataFilter { + /// Specifies which field to apply the filter on, using the JSON path format + final String jsonPath; + + /// Specifies which JSON value to filter for. + /// As such, strings must be wrapped with quotes ("") in order to be properly filtered + final String jsonValue; + + const MetadataFilter({ + required this.jsonPath, + required this.jsonValue, + }); +} + /// The different supported bitcoin networks enum Network { /// Mainnet @@ -1145,7 +1162,7 @@ class OpeningFeeParamsMenu { }); } -/// Represents a payment, including its [PaymentType] and [PaymentDetails]. +/// Represents a payment, including its [PaymentType] and [PaymentDetails] class Payment { final String id; final PaymentType paymentType; @@ -1158,6 +1175,7 @@ class Payment { final String? error; final String? description; final PaymentDetails details; + final String? metadata; const Payment({ required this.id, @@ -1169,6 +1187,7 @@ class Payment { this.error, this.description, required this.details, + this.metadata, }); } @@ -3445,7 +3464,7 @@ class BreezSdkCoreImpl implements BreezSdkCore { Payment _wire2api_payment(dynamic raw) { final arr = raw as List; - if (arr.length != 9) throw Exception('unexpected arr length: expect 9 but see ${arr.length}'); + if (arr.length != 10) throw Exception('unexpected arr length: expect 10 but see ${arr.length}'); return Payment( id: _wire2api_String(arr[0]), paymentType: _wire2api_payment_type(arr[1]), @@ -3456,6 +3475,7 @@ class BreezSdkCoreImpl implements BreezSdkCore { error: _wire2api_opt_String(arr[6]), description: _wire2api_opt_String(arr[7]), details: _wire2api_payment_details(arr[8]), + metadata: _wire2api_opt_String(arr[9]), ); } @@ -4042,6 +4062,15 @@ class BreezSdkCorePlatform extends FlutterRustBridgeBase { return raw; } + @protected + ffi.Pointer api2wire_list_metadata_filter(List raw) { + final ans = inner.new_list_metadata_filter_0(raw.length); + for (var i = 0; i < raw.length; ++i) { + _api_fill_to_wire_metadata_filter(raw[i], ans.ref.ptr[i]); + } + return ans; + } + @protected ffi.Pointer api2wire_list_payment_type_filter(List raw) { final ans = inner.new_list_payment_type_filter_0(raw.length); @@ -4096,6 +4125,11 @@ class BreezSdkCorePlatform extends FlutterRustBridgeBase { return raw == null ? ffi.nullptr : api2wire_box_autoadd_u64(raw); } + @protected + ffi.Pointer api2wire_opt_list_metadata_filter(List? raw) { + return raw == null ? ffi.nullptr : api2wire_list_metadata_filter(raw); + } + @protected ffi.Pointer api2wire_opt_list_payment_type_filter( List? raw) { @@ -4293,6 +4327,7 @@ class BreezSdkCorePlatform extends FlutterRustBridgeBase { void _api_fill_to_wire_list_payments_request(ListPaymentsRequest apiObj, wire_ListPaymentsRequest wireObj) { wireObj.filters = api2wire_opt_list_payment_type_filter(apiObj.filters); + wireObj.metadata_filters = api2wire_opt_list_metadata_filter(apiObj.metadataFilters); wireObj.from_timestamp = api2wire_opt_box_autoadd_i64(apiObj.fromTimestamp); wireObj.to_timestamp = api2wire_opt_box_autoadd_i64(apiObj.toTimestamp); wireObj.include_failures = api2wire_opt_box_autoadd_bool(apiObj.includeFailures); @@ -4341,6 +4376,11 @@ class BreezSdkCorePlatform extends FlutterRustBridgeBase { wireObj.max_withdrawable = api2wire_u64(apiObj.maxWithdrawable); } + void _api_fill_to_wire_metadata_filter(MetadataFilter apiObj, wire_MetadataFilter wireObj) { + wireObj.json_path = api2wire_String(apiObj.jsonPath); + wireObj.json_value = api2wire_String(apiObj.jsonValue); + } + void _api_fill_to_wire_node_config(NodeConfig apiObj, wire_NodeConfig wireObj) { if (apiObj is NodeConfig_Greenlight) { var pre_config = api2wire_box_autoadd_greenlight_node_config(apiObj.config); @@ -4484,7 +4524,7 @@ class BreezSdkCoreWire implements FlutterRustBridgeWireBase { : _lookup = lookup; void store_dart_post_cobject( - DartPostCObjectFnType ptr, + int ptr, ) { return _store_dart_post_cobject( ptr, @@ -4492,9 +4532,8 @@ class BreezSdkCoreWire implements FlutterRustBridgeWireBase { } late final _store_dart_post_cobjectPtr = - _lookup>('store_dart_post_cobject'); - late final _store_dart_post_cobject = - _store_dart_post_cobjectPtr.asFunction(); + _lookup>('store_dart_post_cobject'); + late final _store_dart_post_cobject = _store_dart_post_cobjectPtr.asFunction(); Object get_dart_object( int ptr, @@ -5281,8 +5320,8 @@ class BreezSdkCoreWire implements FlutterRustBridgeWireBase { late final _wire_execute_command = _wire_execute_commandPtr.asFunction)>(); - ffi.Pointer new_box_autoadd_bool_0( - bool value, + ffi.Pointer new_box_autoadd_bool_0( + ffi.Pointer value, ) { return _new_box_autoadd_bool_0( value, @@ -5290,9 +5329,9 @@ class BreezSdkCoreWire implements FlutterRustBridgeWireBase { } late final _new_box_autoadd_bool_0Ptr = - _lookup Function(ffi.Bool)>>('new_box_autoadd_bool_0'); + _lookup Function(ffi.Pointer)>>('new_box_autoadd_bool_0'); late final _new_box_autoadd_bool_0 = - _new_box_autoadd_bool_0Ptr.asFunction Function(bool)>(); + _new_box_autoadd_bool_0Ptr.asFunction Function(ffi.Pointer)>(); ffi.Pointer new_box_autoadd_buy_bitcoin_request_0() { return _new_box_autoadd_buy_bitcoin_request_0(); @@ -5596,6 +5635,20 @@ class BreezSdkCoreWire implements FlutterRustBridgeWireBase { late final _new_box_autoadd_u64_0 = _new_box_autoadd_u64_0Ptr.asFunction Function(int)>(); + ffi.Pointer new_list_metadata_filter_0( + int len, + ) { + return _new_list_metadata_filter_0( + len, + ); + } + + late final _new_list_metadata_filter_0Ptr = + _lookup Function(ffi.Int32)>>( + 'new_list_metadata_filter_0'); + late final _new_list_metadata_filter_0 = + _new_list_metadata_filter_0Ptr.asFunction Function(int)>(); + ffi.Pointer new_list_payment_type_filter_0( int len, ) { @@ -5754,20 +5807,37 @@ final class wire_list_payment_type_filter extends ffi.Struct { external int len; } +final class wire_MetadataFilter extends ffi.Struct { + external ffi.Pointer json_path; + + external ffi.Pointer json_value; +} + +final class wire_list_metadata_filter extends ffi.Struct { + external ffi.Pointer ptr; + + @ffi.Int32() + external int len; +} + final class wire_ListPaymentsRequest extends ffi.Struct { external ffi.Pointer filters; + external ffi.Pointer metadata_filters; + external ffi.Pointer from_timestamp; external ffi.Pointer to_timestamp; - external ffi.Pointer include_failures; + external ffi.Pointer include_failures; external ffi.Pointer offset; external ffi.Pointer limit; } +typedef bool = ffi.NativeFunction)>; + final class wire_SendPaymentRequest extends ffi.Struct { external ffi.Pointer bolt11; @@ -5825,7 +5895,7 @@ final class wire_ReceivePaymentRequest extends ffi.Struct { external ffi.Pointer opening_fee_params; - external ffi.Pointer use_description_hash; + external ffi.Pointer use_description_hash; external ffi.Pointer expiry; @@ -5980,10 +6050,6 @@ final class wire_ReverseSwapFeesRequest extends ffi.Struct { external ffi.Pointer send_amount_sat; } -typedef DartPostCObjectFnType - = ffi.Pointer message)>>; -typedef DartPort = ffi.Int64; - const int SWAP_PAYMENT_FEE_EXPIRY_SECONDS = 172800; const int INVOICE_PAYMENT_FEE_EXPIRY_SECONDS = 3600; diff --git a/libs/sdk-flutter/pubspec.lock b/libs/sdk-flutter/pubspec.lock index da660a91e..37fd0d59f 100644 --- a/libs/sdk-flutter/pubspec.lock +++ b/libs/sdk-flutter/pubspec.lock @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.2" convert: dependency: transitive description: @@ -303,10 +303,10 @@ packages: dependency: transitive description: name: http - sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.0" http_multi_server: dependency: transitive description: @@ -383,10 +383,10 @@ packages: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.9.1" mime: dependency: transitive description: @@ -415,10 +415,10 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: eeb2d1428ee7f4170e2bd498827296a18d4e7fc462b71727d111c0ac7707cfa6 url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.0.1" pointycastle: dependency: transitive description: @@ -532,18 +532,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" stream_transform: dependency: transitive description: @@ -572,10 +572,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.6.0" timing: dependency: transitive description: @@ -628,10 +628,10 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -657,5 +657,5 @@ packages: source: hosted version: "2.1.1" sdks: - dart: ">=3.2.0 <4.0.0" + dart: ">=3.1.0 <4.0.0" flutter: ">=3.10.0" diff --git a/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt b/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt index 112b524e2..30f6b7a2c 100644 --- a/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt +++ b/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt @@ -750,6 +750,14 @@ fun asListPaymentsRequest(listPaymentsRequest: ReadableMap): ListPaymentsRequest } else { null } + val metadataFilters = + if (hasNonNullKey(listPaymentsRequest, "metadataFilters")) { + listPaymentsRequest.getArray("metadataFilters")?.let { + asPaymentMetadataFilterList(it) + } + } else { + null + } val fromTimestamp = if (hasNonNullKey( listPaymentsRequest, @@ -775,6 +783,7 @@ fun asListPaymentsRequest(listPaymentsRequest: ReadableMap): ListPaymentsRequest val limit = if (hasNonNullKey(listPaymentsRequest, "limit")) listPaymentsRequest.getInt("limit").toUInt() else null return ListPaymentsRequest( filters, + metadataFilters, fromTimestamp, toTimestamp, includeFailures, @@ -786,6 +795,7 @@ fun asListPaymentsRequest(listPaymentsRequest: ReadableMap): ListPaymentsRequest fun readableMapOf(listPaymentsRequest: ListPaymentsRequest): ReadableMap { return readableMapOf( "filters" to listPaymentsRequest.filters?.let { readableArrayOf(it) }, + "metadataFilters" to listPaymentsRequest.metadataFilters?.let { readableArrayOf(it) }, "fromTimestamp" to listPaymentsRequest.fromTimestamp, "toTimestamp" to listPaymentsRequest.toTimestamp, "includeFailures" to listPaymentsRequest.includeFailures, @@ -1837,6 +1847,7 @@ fun asPayment(payment: ReadableMap): Payment? { val error = if (hasNonNullKey(payment, "error")) payment.getString("error") else null val description = if (hasNonNullKey(payment, "description")) payment.getString("description") else null val details = payment.getMap("details")?.let { asPaymentDetails(it) }!! + val metadata = if (hasNonNullKey(payment, "metadata")) payment.getString("metadata") else null return Payment( id, paymentType, @@ -1847,6 +1858,7 @@ fun asPayment(payment: ReadableMap): Payment? { error, description, details, + metadata, ) } @@ -1861,6 +1873,7 @@ fun readableMapOf(payment: Payment): ReadableMap { "error" to payment.error, "description" to payment.description, "details" to readableMapOf(payment.details), + "metadata" to payment.metadata, ) } @@ -1915,6 +1928,43 @@ fun asPaymentFailedDataList(arr: ReadableArray): List { return list } +fun asPaymentMetadataFilter(paymentMetadataFilter: ReadableMap): PaymentMetadataFilter? { + if (!validateMandatoryFields( + paymentMetadataFilter, + arrayOf( + "searchPath", + "searchValue", + ), + ) + ) { + return null + } + val searchPath = paymentMetadataFilter.getString("searchPath")!! + val searchValue = paymentMetadataFilter.getString("searchValue")!! + return PaymentMetadataFilter( + searchPath, + searchValue, + ) +} + +fun readableMapOf(paymentMetadataFilter: PaymentMetadataFilter): ReadableMap { + return readableMapOf( + "searchPath" to paymentMetadataFilter.searchPath, + "searchValue" to paymentMetadataFilter.searchValue, + ) +} + +fun asPaymentMetadataFilterList(arr: ReadableArray): List { + val list = ArrayList() + for (value in arr.toArrayList()) { + when (value) { + is ReadableMap -> list.add(asPaymentMetadataFilter(value)!!) + else -> throw SdkException.Generic(errUnexpectedType("${value::class.java.name}")) + } + } + return list +} + fun asPrepareRedeemOnchainFundsRequest(prepareRedeemOnchainFundsRequest: ReadableMap): PrepareRedeemOnchainFundsRequest? { if (!validateMandatoryFields( prepareRedeemOnchainFundsRequest, @@ -4103,6 +4153,7 @@ fun pushToArray( is LspInformation -> array.pushMap(readableMapOf(value)) is OpeningFeeParams -> array.pushMap(readableMapOf(value)) is Payment -> array.pushMap(readableMapOf(value)) + is PaymentMetadataFilter -> array.pushMap(readableMapOf(value)) is PaymentTypeFilter -> array.pushString(value.name.lowercase()) is Rate -> array.pushMap(readableMapOf(value)) is ReverseSwapInfo -> array.pushMap(readableMapOf(value)) diff --git a/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKModule.kt b/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKModule.kt index 31b918766..3cb11a973 100644 --- a/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKModule.kt +++ b/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKModule.kt @@ -404,6 +404,25 @@ class BreezSDKModule(reactContext: ReactApplicationContext) : ReactContextBaseJa } } + @ReactMethod + fun listPayments( + req: ReadableMap, + promise: Promise, + ) { + executor.execute { + try { + val listPaymentsRequest = + asListPaymentsRequest(req) ?: run { + throw SdkException.Generic(errMissingMandatoryField("req", "ListPaymentsRequest")) + } + val res = getBreezServices().listPayments(listPaymentsRequest) + promise.resolve(readableArrayOf(res)) + } catch (e: Exception) { + promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e) + } + } + } + @ReactMethod fun paymentByHash( hash: String, @@ -420,18 +439,15 @@ class BreezSDKModule(reactContext: ReactApplicationContext) : ReactContextBaseJa } @ReactMethod - fun listPayments( - req: ReadableMap, + fun setPaymentMetadata( + hash: String, + metadata: String, promise: Promise, ) { executor.execute { try { - val listPaymentsRequest = - asListPaymentsRequest(req) ?: run { - throw SdkException.Generic(errMissingMandatoryField("req", "ListPaymentsRequest")) - } - val res = getBreezServices().listPayments(listPaymentsRequest) - promise.resolve(readableArrayOf(res)) + getBreezServices().setPaymentMetadata(hash, metadata) + promise.resolve(readableMapOf("status" to "ok")) } catch (e: Exception) { promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e) } diff --git a/libs/sdk-react-native/ios/BreezSDKMapper.swift b/libs/sdk-react-native/ios/BreezSDKMapper.swift index 24b95dc70..b62ca65e9 100644 --- a/libs/sdk-react-native/ios/BreezSDKMapper.swift +++ b/libs/sdk-react-native/ios/BreezSDKMapper.swift @@ -828,6 +828,11 @@ enum BreezSDKMapper { filters = try asPaymentTypeFilterList(arr: filtersTmp) } + var metadataFilters: [PaymentMetadataFilter]? + if let metadataFiltersTmp = listPaymentsRequest["metadataFilters"] as? [[String: Any?]] { + metadataFilters = try asPaymentMetadataFilterList(arr: metadataFiltersTmp) + } + var fromTimestamp: Int64? if hasNonNilKey(data: listPaymentsRequest, key: "fromTimestamp") { guard let fromTimestampTmp = listPaymentsRequest["fromTimestamp"] as? Int64 else { @@ -866,6 +871,7 @@ enum BreezSDKMapper { return ListPaymentsRequest( filters: filters, + metadataFilters: metadataFilters, fromTimestamp: fromTimestamp, toTimestamp: toTimestamp, includeFailures: includeFailures, @@ -877,6 +883,7 @@ enum BreezSDKMapper { static func dictionaryOf(listPaymentsRequest: ListPaymentsRequest) -> [String: Any?] { return [ "filters": listPaymentsRequest.filters == nil ? nil : arrayOf(paymentTypeFilterList: listPaymentsRequest.filters!), + "metadataFilters": listPaymentsRequest.metadataFilters == nil ? nil : arrayOf(paymentMetadataFilterList: listPaymentsRequest.metadataFilters!), "fromTimestamp": listPaymentsRequest.fromTimestamp == nil ? nil : listPaymentsRequest.fromTimestamp, "toTimestamp": listPaymentsRequest.toTimestamp == nil ? nil : listPaymentsRequest.toTimestamp, "includeFailures": listPaymentsRequest.includeFailures == nil ? nil : listPaymentsRequest.includeFailures, @@ -2042,6 +2049,14 @@ enum BreezSDKMapper { } let details = try asPaymentDetails(paymentDetails: detailsTmp) + var metadata: String? + if hasNonNilKey(data: payment, key: "metadata") { + guard let metadataTmp = payment["metadata"] as? String else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "metadata")) + } + metadata = metadataTmp + } + return Payment( id: id, paymentType: paymentType, @@ -2051,7 +2066,8 @@ enum BreezSDKMapper { status: status, error: error, description: description, - details: details + details: details, + metadata: metadata ) } @@ -2066,6 +2082,7 @@ enum BreezSDKMapper { "error": payment.error == nil ? nil : payment.error, "description": payment.description == nil ? nil : payment.description, "details": dictionaryOf(paymentDetails: payment.details), + "metadata": payment.metadata == nil ? nil : payment.metadata, ] } @@ -2130,6 +2147,44 @@ enum BreezSDKMapper { return paymentFailedDataList.map { v -> [String: Any?] in dictionaryOf(paymentFailedData: v) } } + static func asPaymentMetadataFilter(paymentMetadataFilter: [String: Any?]) throws -> PaymentMetadataFilter { + guard let searchPath = paymentMetadataFilter["searchPath"] as? String else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "searchPath", typeName: "PaymentMetadataFilter")) + } + guard let searchValue = paymentMetadataFilter["searchValue"] as? String else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "searchValue", typeName: "PaymentMetadataFilter")) + } + + return PaymentMetadataFilter( + searchPath: searchPath, + searchValue: searchValue + ) + } + + static func dictionaryOf(paymentMetadataFilter: PaymentMetadataFilter) -> [String: Any?] { + return [ + "searchPath": paymentMetadataFilter.searchPath, + "searchValue": paymentMetadataFilter.searchValue, + ] + } + + static func asPaymentMetadataFilterList(arr: [Any]) throws -> [PaymentMetadataFilter] { + var list = [PaymentMetadataFilter]() + for value in arr { + if let val = value as? [String: Any?] { + var paymentMetadataFilter = try asPaymentMetadataFilter(paymentMetadataFilter: val) + list.append(paymentMetadataFilter) + } else { + throw SdkError.Generic(message: errUnexpectedType(typeName: "PaymentMetadataFilter")) + } + } + return list + } + + static func arrayOf(paymentMetadataFilterList: [PaymentMetadataFilter]) -> [Any] { + return paymentMetadataFilterList.map { v -> [String: Any?] in dictionaryOf(paymentMetadataFilter: v) } + } + static func asPrepareRedeemOnchainFundsRequest(prepareRedeemOnchainFundsRequest: [String: Any?]) throws -> PrepareRedeemOnchainFundsRequest { guard let toAddress = prepareRedeemOnchainFundsRequest["toAddress"] as? String else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "toAddress", typeName: "PrepareRedeemOnchainFundsRequest")) diff --git a/libs/sdk-react-native/ios/RNBreezSDK.m b/libs/sdk-react-native/ios/RNBreezSDK.m index a0d84487e..44f18e0c2 100644 --- a/libs/sdk-react-native/ios/RNBreezSDK.m +++ b/libs/sdk-react-native/ios/RNBreezSDK.m @@ -126,6 +126,12 @@ @interface RCT_EXTERN_MODULE(RNBreezSDK, RCTEventEmitter) reject: (RCTPromiseRejectBlock)reject ) +RCT_EXTERN_METHOD( + listPayments: (NSDictionary*)req + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject +) + RCT_EXTERN_METHOD( paymentByHash: (NSString*)hash resolve: (RCTPromiseResolveBlock)resolve @@ -133,7 +139,8 @@ @interface RCT_EXTERN_MODULE(RNBreezSDK, RCTEventEmitter) ) RCT_EXTERN_METHOD( - listPayments: (NSDictionary*)req + setPaymentMetadata: (NSString*)hash + metadata: (NSString*)metadata resolve: (RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject ) diff --git a/libs/sdk-react-native/ios/RNBreezSDK.swift b/libs/sdk-react-native/ios/RNBreezSDK.swift index 16c5bb4cf..5d7d8d310 100644 --- a/libs/sdk-react-native/ios/RNBreezSDK.swift +++ b/libs/sdk-react-native/ios/RNBreezSDK.swift @@ -287,6 +287,17 @@ class RNBreezSDK: RCTEventEmitter { } } + @objc(listPayments:resolve:reject:) + func listPayments(_ req: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + do { + let listPaymentsRequest = try BreezSDKMapper.asListPaymentsRequest(listPaymentsRequest: req) + var res = try getBreezServices().listPayments(req: listPaymentsRequest) + resolve(BreezSDKMapper.arrayOf(paymentList: res)) + } catch let err { + rejectErr(err: err, reject: reject) + } + } + @objc(paymentByHash:resolve:reject:) func paymentByHash(_ hash: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { do { @@ -301,12 +312,11 @@ class RNBreezSDK: RCTEventEmitter { } } - @objc(listPayments:resolve:reject:) - func listPayments(_ req: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + @objc(setPaymentMetadata:metadata:resolve:reject:) + func setPaymentMetadata(_ hash: String, metadata: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { do { - let listPaymentsRequest = try BreezSDKMapper.asListPaymentsRequest(listPaymentsRequest: req) - var res = try getBreezServices().listPayments(req: listPaymentsRequest) - resolve(BreezSDKMapper.arrayOf(paymentList: res)) + try getBreezServices().setPaymentMetadata(hash: hash, metadata: metadata) + resolve(["status": "ok"]) } catch let err { rejectErr(err: err, reject: reject) } diff --git a/libs/sdk-react-native/src/index.ts b/libs/sdk-react-native/src/index.ts index 856f6d220..c9b97730a 100644 --- a/libs/sdk-react-native/src/index.ts +++ b/libs/sdk-react-native/src/index.ts @@ -129,6 +129,7 @@ export type LnInvoice = { export type ListPaymentsRequest = { filters?: PaymentTypeFilter[] + metadataFilters?: PaymentMetadataFilter[] fromTimestamp?: number toTimestamp?: number includeFailures?: boolean @@ -298,6 +299,7 @@ export type Payment = { error?: string description?: string details: PaymentDetails + metadata?: string } export type PaymentFailedData = { @@ -306,6 +308,11 @@ export type PaymentFailedData = { invoice?: LnInvoice } +export type PaymentMetadataFilter = { + searchPath: string + searchValue: string +} + export type PrepareRedeemOnchainFundsRequest = { toAddress: string satPerVbyte: number @@ -877,14 +884,18 @@ export const backup = async (): Promise => { await BreezSDK.backup() } +export const listPayments = async (req: ListPaymentsRequest): Promise => { + const response = await BreezSDK.listPayments(req) + return response +} + export const paymentByHash = async (hash: string): Promise => { const response = await BreezSDK.paymentByHash(hash) return response } -export const listPayments = async (req: ListPaymentsRequest): Promise => { - const response = await BreezSDK.listPayments(req) - return response +export const setPaymentMetadata = async (hash: string, metadata: string): Promise => { + await BreezSDK.setPaymentMetadata(hash, metadata) } export const redeemOnchainFunds = async (req: RedeemOnchainFundsRequest): Promise => { diff --git a/tools/sdk-cli/src/command_handlers.rs b/tools/sdk-cli/src/command_handlers.rs index 750527ca9..b7e2b73cf 100644 --- a/tools/sdk-cli/src/command_handlers.rs +++ b/tools/sdk-cli/src/command_handlers.rs @@ -219,6 +219,7 @@ pub(crate) async fn handle_command( let payments = sdk()? .list_payments(ListPaymentsRequest { filters: None, + metadata_filters: None, from_timestamp, to_timestamp, include_failures: Some(include_failures),