Skip to content

Commit 2b9af11

Browse files
authored
Merge pull request #97 from dcorral/upstream-list-transfers-filter
list_transfers to take an AssetFilter and an optional txid
2 parents 03f6068 + ecb9842 commit 2b9af11

11 files changed

Lines changed: 235 additions & 34 deletions

File tree

bindings/c-ffi/src/lib.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ use rgb_lib::{
1616
keys::WitnessVersion,
1717
utils::BitcoinNetwork,
1818
wallet::{
19-
Online, OnlineOptions, Recipient, RefreshFilter, RgbWalletOpsOffline, RgbWalletOpsOnline,
20-
SinglesigKeys, SyncOptions, Wallet, WalletData,
19+
AssetFilter, Online, OnlineOptions, Recipient, RefreshFilter, RgbWalletOpsOffline,
20+
RgbWalletOpsOnline, SinglesigKeys, SyncOptions, Wallet, WalletData,
2121
},
2222
};
2323

@@ -381,9 +381,10 @@ pub extern "C" fn rgblib_list_transactions(
381381
#[unsafe(no_mangle)]
382382
pub extern "C" fn rgblib_list_transfers(
383383
wallet: &COpaqueStruct,
384-
asset_id_opt: *const c_char,
384+
asset_filter: *const c_char,
385+
txid_opt: *const c_char,
385386
) -> CResultString {
386-
list_transfers(wallet, asset_id_opt).into()
387+
list_transfers(wallet, asset_filter, txid_opt).into()
387388
}
388389

389390
#[unsafe(no_mangle)]

bindings/c-ffi/src/utils.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -562,11 +562,13 @@ pub(crate) fn list_transactions(
562562

563563
pub(crate) fn list_transfers(
564564
wallet: &COpaqueStruct,
565-
asset_id_opt: *const c_char,
565+
asset_filter: *const c_char,
566+
txid_opt: *const c_char,
566567
) -> Result<String, Error> {
567568
let wallet = Wallet::from_opaque(wallet)?;
568-
let asset_id = convert_optional_string(asset_id_opt);
569-
let res = wallet.list_transfers(asset_id)?;
569+
let asset_filter: AssetFilter = serde_json::from_str(&ptr_to_string(asset_filter))?;
570+
let txid = convert_optional_string(txid_opt);
571+
let res = wallet.list_transfers(asset_filter, txid)?;
570572
Ok(serde_json::to_string(&res)?)
571573
}
572574

bindings/uniffi/src/lib.rs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ use rgb_lib::{
1212
keys::{Keys, WitnessVersion},
1313
utils::BitcoinNetwork,
1414
wallet::{
15-
Address as RgbLibAddress, AssetCFA, AssetIFA, AssetNIA, AssetUDA, Assets,
16-
AssignmentsCollection, Balance, BlockTime, BtcBalance, BurnBeginResult, BurnDetails,
17-
Cosigner as CosignerData, DatabaseType, EmbeddedMedia, HubInfo, InflateBeginResult,
18-
InflateDetails, InitOperationResult, Invoice as RgbLibInvoice,
15+
Address as RgbLibAddress, AssetCFA, AssetFilter as RgbLibAssetFilter, AssetIFA, AssetNIA,
16+
AssetUDA, Assets, AssignmentsCollection, Balance, BlockTime, BtcBalance, BurnBeginResult,
17+
BurnDetails, Cosigner as CosignerData, DatabaseType, EmbeddedMedia, HubInfo,
18+
InflateBeginResult, InflateDetails, InitOperationResult, Invoice as RgbLibInvoice,
1919
InvoiceData as RgbLibInvoiceData, Media, Metadata, MultisigKeys, MultisigOnlineOptions,
2020
MultisigVotingStatus as RgbLibMultisigVotingStatus, MultisigWallet as RgbLibMultisigWallet,
2121
Online, OnlineOptions, Operation as RgbLibOperation, OperationInfo as RgbLibOperationInfo,
@@ -71,6 +71,21 @@ impl From<SyncOptions> for RgbLibSyncOptions {
7171
}
7272
}
7373

74+
pub enum AssetFilter {
75+
AnyOrNone,
76+
None,
77+
Id { asset_id: String },
78+
}
79+
impl From<AssetFilter> for RgbLibAssetFilter {
80+
fn from(orig: AssetFilter) -> Self {
81+
match orig {
82+
AssetFilter::AnyOrNone => RgbLibAssetFilter::AnyOrNone,
83+
AssetFilter::None => RgbLibAssetFilter::None,
84+
AssetFilter::Id { asset_id } => RgbLibAssetFilter::Id(asset_id),
85+
}
86+
}
87+
}
88+
7489
// temporary solution needed because the Enum attribute doesn't support the Remote one
7590
pub enum Assignment {
7691
Fungible { amount: u64 },
@@ -1296,10 +1311,14 @@ impl Wallet {
12961311
self._get_wallet().list_transactions(online, skip_sync)
12971312
}
12981313

1299-
fn list_transfers(&self, asset_id: Option<String>) -> Result<Vec<Transfer>, RgbLibError> {
1314+
fn list_transfers(
1315+
&self,
1316+
asset_filter: AssetFilter,
1317+
txid: Option<String>,
1318+
) -> Result<Vec<Transfer>, RgbLibError> {
13001319
Ok(self
13011320
._get_wallet()
1302-
.list_transfers(asset_id)?
1321+
.list_transfers(asset_filter.into(), txid)?
13031322
.into_iter()
13041323
.map(|t| t.into())
13051324
.collect())
@@ -1741,10 +1760,14 @@ impl MultisigWallet {
17411760
self._get_wallet().list_transactions(online, skip_sync)
17421761
}
17431762

1744-
fn list_transfers(&self, asset_id: Option<String>) -> Result<Vec<Transfer>, RgbLibError> {
1763+
fn list_transfers(
1764+
&self,
1765+
asset_filter: AssetFilter,
1766+
txid: Option<String>,
1767+
) -> Result<Vec<Transfer>, RgbLibError> {
17451768
Ok(self
17461769
._get_wallet()
1747-
.list_transfers(asset_id)?
1770+
.list_transfers(asset_filter.into(), txid)?
17481771
.into_iter()
17491772
.map(|t| t.into())
17501773
.collect())

bindings/uniffi/src/rgb-lib.udl

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,13 @@ interface Invoice {
331331
string invoice_string();
332332
};
333333

334+
[Enum]
335+
interface AssetFilter {
336+
AnyOrNone();
337+
None();
338+
Id(string asset_id);
339+
};
340+
334341
[Enum]
335342
interface Assignment {
336343
Fungible(u64 amount);
@@ -954,7 +961,7 @@ interface Wallet {
954961
sequence<Transaction> list_transactions(Online? online, boolean skip_sync);
955962

956963
[Throws=RgbLibError]
957-
sequence<Transfer> list_transfers(string? asset_id);
964+
sequence<Transfer> list_transfers(AssetFilter asset_filter, string? txid);
958965

959966
[Throws=RgbLibError]
960967
sequence<Unspent> list_unspents(
@@ -1120,7 +1127,7 @@ interface MultisigWallet {
11201127
sequence<Transaction> list_transactions(Online? online, boolean skip_sync);
11211128

11221129
[Throws=RgbLibError]
1123-
sequence<Transfer> list_transfers(string? asset_id);
1130+
sequence<Transfer> list_transfers(AssetFilter asset_filter, string? txid);
11241131

11251132
[Throws=RgbLibError]
11261133
sequence<Unspent> list_unspents(

src/wallet/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ pub use multisig::{
2525
OperationInfo, RespondToOperation, UserRole,
2626
};
2727
pub use objects::{
28-
Address, AssetCFA, AssetIFA, AssetNIA, AssetUDA, Assets, AssignmentsCollection, Balance,
29-
BlockTime, BtcBalance, DatabaseType, EmbeddedMedia, Invoice, InvoiceData, Media, Metadata,
30-
Online, Outpoint, PendingVanillaTx, ProofOfReserves, PsbtInputInfo, PsbtInspection,
28+
Address, AssetCFA, AssetFilter, AssetIFA, AssetNIA, AssetUDA, Assets, AssignmentsCollection,
29+
Balance, BlockTime, BtcBalance, DatabaseType, EmbeddedMedia, Invoice, InvoiceData, Media,
30+
Metadata, Online, Outpoint, PendingVanillaTx, ProofOfReserves, PsbtInputInfo, PsbtInspection,
3131
PsbtOutputInfo, ReceiveData, Recipient, RecipientInfo, RecipientType, RgbAllocation,
3232
RgbInputInfo, RgbInspection, RgbOperationInfo, RgbOutputInfo, RgbTransitionInfo, Token,
3333
TokenLight, Transaction, TransactionType, Transfer, TransferKind, TransferTransportEndpoint,

src/wallet/multisig.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,6 @@ struct ReceiveMetadata {
389389
/// Operations for multisig wallets.
390390
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
391391
#[cfg(any(feature = "electrum", feature = "esplora"))]
392-
#[cfg_attr(feature = "camel_case", serde(rename_all = "camelCase"))]
393392
pub enum Operation {
394393
// CreateUtxos variants
395394
/// Create UTXOs operation waiting for user's response (ACK/NACK)
@@ -930,7 +929,6 @@ pub struct OperationInfo {
930929
/// Response to an operation.
931930
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
932931
#[cfg(any(feature = "electrum", feature = "esplora"))]
933-
#[cfg_attr(feature = "camel_case", serde(rename_all = "camelCase"))]
934932
pub enum RespondToOperation {
935933
/// ACK the operation with a signed PSBT
936934
Ack(String),
@@ -952,7 +950,6 @@ pub struct InitOperationResult {
952950
/// The role of the user on the hub.
953951
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
954952
#[cfg(any(feature = "electrum", feature = "esplora"))]
955-
#[cfg_attr(feature = "camel_case", serde(rename_all = "camelCase"))]
956953
pub enum UserRole {
957954
/// A cosigner
958955
Cosigner,

src/wallet/objects.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,17 @@ pub struct BlockTime {
181181
// Assets, tokens & media
182182
// ────────────────────────────────────────────────────────────
183183

184+
/// An asset filter.
185+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
186+
pub enum AssetFilter {
187+
/// Match any or no asset
188+
AnyOrNone,
189+
/// Match no asset
190+
None,
191+
/// Match the asset with the given ID
192+
Id(String),
193+
}
194+
184195
/// An asset media file.
185196
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
186197
#[cfg_attr(feature = "camel_case", serde(rename_all = "camelCase"))]

src/wallet/offline.rs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2087,13 +2087,31 @@ pub trait WalletOffline: WalletBackup {
20872087
fn list_transfers_impl(
20882088
&self,
20892089
txn: &DbTxn,
2090-
asset_id: Option<String>,
2090+
asset_filter: AssetFilter,
2091+
txid: Option<String>,
20912092
) -> Result<Vec<Transfer>, Error> {
20922093
let db_data = txn.get_db_data(false)?;
2094+
let batch_transfer_idxs: Option<HashSet<i32>> = txid.map(|txid| {
2095+
db_data
2096+
.batch_transfers
2097+
.iter()
2098+
.filter(|b| b.txid.as_deref() == Some(txid.as_str()))
2099+
.map(|b| b.idx)
2100+
.collect()
2101+
});
20932102
let asset_transfer_ids: Vec<i32> = db_data
20942103
.asset_transfers
20952104
.iter()
2096-
.filter(|t| t.asset_id == asset_id)
2105+
.filter(|t| match &asset_filter {
2106+
AssetFilter::AnyOrNone => true,
2107+
AssetFilter::None => t.asset_id.is_none(),
2108+
AssetFilter::Id(asset_id) => t.asset_id.as_ref() == Some(asset_id),
2109+
})
2110+
.filter(|t| {
2111+
batch_transfer_idxs
2112+
.as_ref()
2113+
.is_none_or(|idxs| idxs.contains(&t.batch_transfer_idx))
2114+
})
20972115
.filter(|t| t.user_driven)
20982116
.map(|t| t.idx)
20992117
.collect();
@@ -2719,18 +2737,23 @@ pub trait RgbWalletOpsOffline: WalletOffline + WalletBackup {
27192737

27202738
/// List the RGB [`Transfer`]s known to the wallet.
27212739
///
2722-
/// When an `asset_id` is not provided, return transfers that are not connected to a specific
2723-
/// asset.
2724-
fn list_transfers(&self, asset_id: Option<String>) -> Result<Vec<Transfer>, Error> {
2740+
/// `asset_filter` selects transfers by asset. When a `txid` is provided, restrict the result to the
2741+
/// transfers committed by the on-chain transaction with that ID; an unknown `txid` yields an
2742+
/// empty list.
2743+
fn list_transfers(
2744+
&self,
2745+
asset_filter: AssetFilter,
2746+
txid: Option<String>,
2747+
) -> Result<Vec<Transfer>, Error> {
27252748
info!(
27262749
self.logger(),
2727-
"Listing transfers for asset '{:?}'...", asset_id
2750+
"Listing transfers for filter '{:?}' and txid '{:?}'...", asset_filter, txid
27282751
);
27292752
let txn = self.database().begin_transaction()?;
2730-
if let Some(asset_id) = &asset_id {
2753+
if let AssetFilter::Id(asset_id) = &asset_filter {
27312754
txn.check_asset_exists(asset_id.clone())?;
27322755
}
2733-
let transfers = self.list_transfers_impl(&txn, asset_id)?;
2756+
let transfers = self.list_transfers_impl(&txn, asset_filter, txid)?;
27342757
txn.commit()?;
27352758
info!(self.logger(), "List transfers completed");
27362759
Ok(transfers)

src/wallet/test/list_transfers.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,116 @@ fn success() {
166166
);
167167
}
168168

169+
#[cfg(feature = "electrum")]
170+
#[test]
171+
#[parallel]
172+
fn filters() {
173+
initialize();
174+
175+
let amount: u64 = 66;
176+
177+
let mut party = get_funded_party!();
178+
let mut rcv_party = get_funded_party!();
179+
180+
let asset_nia = party.issue_asset_nia(None);
181+
let asset_cfa = party.issue_asset_cfa(None, None);
182+
183+
// one batch tx carrying transfers of both assets
184+
let receive_nia = rcv_party.blind_receive();
185+
let receive_cfa = rcv_party.blind_receive();
186+
let recipient_map = HashMap::from([
187+
(
188+
asset_nia.asset_id.clone(),
189+
vec![Recipient {
190+
assignment: Assignment::Fungible(amount),
191+
recipient_id: receive_nia.recipient_id.clone(),
192+
witness_data: None,
193+
transport_endpoints: TRANSPORT_ENDPOINTS.clone(),
194+
}],
195+
),
196+
(
197+
asset_cfa.asset_id.clone(),
198+
vec![Recipient {
199+
assignment: Assignment::Fungible(amount),
200+
recipient_id: receive_cfa.recipient_id.clone(),
201+
witness_data: None,
202+
transport_endpoints: TRANSPORT_ENDPOINTS.clone(),
203+
}],
204+
),
205+
]);
206+
let txid = party.send_retry(&recipient_map);
207+
assert!(!txid.is_empty());
208+
209+
// AnyOrNone + txid: the tx's transfers across all assets
210+
let by_txid = party.list_transfers_filtered(AssetFilter::AnyOrNone, Some(&txid));
211+
assert_eq!(by_txid.len(), 2);
212+
assert!(by_txid.iter().all(|t| t.txid == Some(txid.clone())));
213+
214+
// Id + txid: intersection, restricted to the given asset
215+
let nia_by_txid =
216+
party.list_transfers_filtered(AssetFilter::Id(asset_nia.asset_id.clone()), Some(&txid));
217+
assert_eq!(nia_by_txid.len(), 1);
218+
let expected: Vec<i32> = party
219+
.list_transfers(Some(&asset_nia.asset_id))
220+
.into_iter()
221+
.filter(|t| t.txid == Some(txid.clone()))
222+
.map(|t| t.idx)
223+
.collect();
224+
assert_eq!(vec![nia_by_txid[0].idx], expected);
225+
226+
// AnyOrNone + no txid: the whole history (2 issuances + 2 sends)
227+
let all = party.list_transfers_filtered(AssetFilter::AnyOrNone, None);
228+
assert_eq!(all.len(), 4);
229+
230+
// None: receiver's pending blind receives not yet tied to an asset
231+
let pending = rcv_party.list_transfers_filtered(AssetFilter::None, None);
232+
assert_eq!(pending.len(), 2);
233+
234+
// extra receive that nothing is sent to, stays asset-less
235+
let receive_extra = rcv_party.blind_receive();
236+
237+
// settle the batch so the change is spendable again
238+
rcv_party.wait_for_refresh(None);
239+
// the refresh tied the 2 receives to their assets, only the extra one stays asset-less
240+
let all_rcv = rcv_party.list_transfers_filtered(AssetFilter::AnyOrNone, None);
241+
assert_eq!(all_rcv.len(), 3);
242+
let pending = rcv_party.list_transfers_filtered(AssetFilter::None, None);
243+
assert_eq!(pending.len(), 1);
244+
assert_eq!(pending[0].recipient_id, Some(receive_extra.recipient_id));
245+
// receiver side: the tx's transfers span 2 batch transfers sharing the same txid
246+
let rcv_by_txid = rcv_party.list_transfers_filtered(AssetFilter::AnyOrNone, Some(&txid));
247+
assert_eq!(rcv_by_txid.len(), 2);
248+
party.wait_for_refresh(None);
249+
mine(false);
250+
rcv_party.wait_for_refresh(None);
251+
party.wait_for_refresh(None);
252+
253+
// Id + txid of a tx not carrying that asset: empty
254+
let receive_nia_2 = rcv_party.blind_receive();
255+
let recipient_map = HashMap::from([(
256+
asset_nia.asset_id.clone(),
257+
vec![Recipient {
258+
assignment: Assignment::Fungible(amount),
259+
recipient_id: receive_nia_2.recipient_id.clone(),
260+
witness_data: None,
261+
transport_endpoints: TRANSPORT_ENDPOINTS.clone(),
262+
}],
263+
)]);
264+
let txid_2 = party.send_retry(&recipient_map);
265+
assert!(
266+
party
267+
.list_transfers_filtered(AssetFilter::Id(asset_cfa.asset_id.clone()), Some(&txid_2))
268+
.is_empty()
269+
);
270+
271+
// unknown txid: empty
272+
assert!(
273+
party
274+
.list_transfers_filtered(AssetFilter::AnyOrNone, Some(FAKE_TXID))
275+
.is_empty()
276+
);
277+
}
278+
169279
#[test]
170280
#[parallel]
171281
fn fail() {
@@ -174,4 +284,9 @@ fn fail() {
174284
// asset not found
175285
let result = party.list_transfers_result(Some("rgb1inexistent"));
176286
assert!(matches!(result, Err(Error::AssetNotFound { asset_id: _ })));
287+
288+
// asset not found also when a txid is given
289+
let result = party
290+
.list_transfers_filtered_result(AssetFilter::Id(s!("rgb1inexistent")), Some(FAKE_TXID));
291+
assert!(matches!(result, Err(Error::AssetNotFound { asset_id: _ })));
177292
}

src/wallet/test/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ const IDENT_NOT_ASCII_MSG: &str = "string '{0}' contains invalid character '{1}'
126126
const IDENT_NOT_START_MSG: &str = "string '{0}' must not start with character '{1}'.";
127127
#[cfg(any(feature = "electrum", feature = "esplora"))]
128128
const MIN_CONFIRMATIONS: u8 = 1;
129-
#[cfg(feature = "electrum")]
130129
const FAKE_TXID: &str = "e5a3e577309df31bd606f48049049d2e1e02b048206ba232944fcc053a176ccb";
131130
#[cfg(feature = "electrum")]
132131
const FAKE_OUTPOINT: &str = "e5a3e577309df31bd606f48049049d2e1e02b048206ba232944fcc053a176ccb:0";

0 commit comments

Comments
 (0)