From c55a8c0701a0e3f0e5459db779f1575106ec5730 Mon Sep 17 00:00:00 2001 From: rmn-boiko Date: Tue, 27 Oct 2026 16:42:10 +0100 Subject: [PATCH 1/2] Add btc send endpoints --- CHANGELOG.md | 9 ++ README.md | 7 +- docs/swagger/swagger.yaml | 193 +++++++++++++++++++++++++++++++++++- src/api/mod.rs | 5 + src/api/node_api.rs | 64 ++++++++++-- src/wallet/context.rs | 9 +- src/wallet/models.rs | 84 ++++++++++++++++ src/wallet/state.rs | 27 +++-- src/wallet/wallet_thread.rs | 17 ++-- 9 files changed, 381 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55afb26..8a42a8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ All notable changes to this project are documented here. The format follows ### Added +- `/wallet/sendbtcbegin` and `/wallet/sendbtcend` send BTC from the wallet's + vanilla side with a PSBT signed by the client; the end call returns the + broadcast `txid`. +- `/wallet/witnessreceive` creates an RGB invoice paid to a new output of the + sender's transaction, so the wallet needs no colorable UTXO up front. It + takes the same body as `/wallet/blindreceive`. - Opt-in BFA support: set `bfa_enabled = true` and `eth_rpc_url` in `[wallet]`. Wallets then accept the BFA schema and validate BFA consignments against the EVM RPC. BFA stays disabled without the opt-in. @@ -30,6 +36,9 @@ All notable changes to this project are documented here. The format follows ### Changed +- `/wallet/listtransfers` accepts `asset_id: null` to list the transfers not + yet tied to any asset (receives created without one). The field is still + required. - Responses follow the pinned rgb-lib models: `listassets` has a `bfa` group, transfers can be `WaitingBroadcast`, allocations can be `BridgeRight` and UTXOs carry `derivation_index`. diff --git a/README.md b/README.md index ca80363..3edfae9 100644 --- a/README.md +++ b/README.md @@ -69,14 +69,17 @@ The begin/end pattern allows clients to sign PSBTs externally: | POST | `/assetbalance` | Get balance for an asset | | POST | `/listunspents` | List unspent outputs | | POST | `/listtransactions` | List BTC transactions | -| POST | `/listtransfers` | List RGB transfers for an asset | -| POST | `/blindreceive` | Generate RGB invoice | +| POST | `/listtransfers` | List RGB transfers for an asset (`asset_id: null`: transfers not yet tied to one) | +| POST | `/blindreceive` | Generate RGB invoice (blinded UTXO) | +| POST | `/witnessreceive` | Generate RGB invoice paid to a new output of the sender's transaction | | POST | `/issueassetnia` | Issue new NIA token | | POST | `/issueassetifa` | Issue IFA tokens and reserve inflation rights | | POST | `/inflatebegin` | Create additional IFA tokens (unsigned PSBT) | | POST | `/inflateend` | Finalize IFA inflation (signed PSBT) | | POST | `/createutxosbegin` | Create UTXOs (unsigned PSBT) | | POST | `/createutxosend` | Finalize UTXO creation (signed PSBT) | +| POST | `/sendbtcbegin` | Send BTC (unsigned PSBT) | +| POST | `/sendbtcend` | Finalize BTC send (signed PSBT) | | POST | `/sendbegin` | Send RGB token (unsigned PSBT) | | POST | `/sendend` | Finalize token send (signed PSBT) | | POST | `/burnbegin` | Burn BFA or IFA tokens (unsigned PSBT) | diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 3c8b545..fc31f68 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -659,6 +659,106 @@ paths: $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' + /wallet/sendbtcbegin: + post: + summary: Send BTC Begin + description: >- + Prepare a BTC send from the wallet's vanilla side. Sign the returned + PSBT client-side and submit it to `/wallet/sendbtcend`. + operationId: send_btc_begin_wallet_sendbtcbegin_post + parameters: + - name: xpub-van + in: header + required: true + schema: + type: string + title: Xpub Van + - name: xpub-col + in: header + required: true + schema: + type: string + title: Xpub Col + - name: master-fingerprint + in: header + required: true + schema: + type: string + title: Master Fingerprint + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SendBtcBeginRequestModel' + responses: + '200': + description: The unsigned PSBT to be signed by the client. + content: + application/json: + schema: + $ref: '#/components/schemas/PsbtResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '409': + $ref: '#/components/responses/Conflict' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + /wallet/sendbtcend: + post: + summary: Send BTC End + description: Finalize and broadcast a BTC send with the client-signed PSBT. + operationId: send_btc_end_wallet_sendbtcend_post + parameters: + - name: xpub-van + in: header + required: true + schema: + type: string + title: Xpub Van + - name: xpub-col + in: header + required: true + schema: + type: string + title: Xpub Col + - name: master-fingerprint + in: header + required: true + schema: + type: string + title: Master Fingerprint + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SendAssetEndRequestModel' + responses: + '200': + description: The broadcast transaction. + content: + application/json: + schema: + $ref: '#/components/schemas/SendBtcResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '409': + $ref: '#/components/responses/Conflict' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' /wallet/burnbegin: post: summary: Burn Begin @@ -865,6 +965,61 @@ paths: $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' + /wallet/witnessreceive: + post: + summary: Generate Witness Invoice + description: >- + Like `/wallet/blindreceive`, but the invoice is paid to a new output of + the sender's transaction (a `wvout:` recipient) instead of a blinded + UTXO this wallet already holds, so no colorable UTXO is needed up front. + The sender funds that output (`witness_data.amount_sat`). + operationId: generate_witness_invoice_wallet_witnessreceive_post + parameters: + - name: xpub-van + in: header + required: true + schema: + type: string + title: Xpub Van + - name: xpub-col + in: header + required: true + schema: + type: string + title: Xpub Col + - name: master-fingerprint + in: header + required: true + schema: + type: string + title: Master Fingerprint + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RgbInvoiceRequestModel' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/ReceiveData' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' /blindreceive: post: summary: Generate Invoice (deprecated alias) @@ -1927,8 +2082,14 @@ components: ListTransfersRequestModel: properties: asset_id: - type: string + anyOf: + - type: string + - type: 'null' title: Asset Id + description: >- + The asset whose transfers to list. `null` lists the transfers not yet + tied to any asset (receives created without one). The field is + required either way. type: object required: - asset_id @@ -2091,6 +2252,36 @@ components: type: object title: RgbInvoiceRequestModel description: Request model for RGB invoices. + SendBtcBeginRequestModel: + properties: + address: + type: string + title: Address + description: Bitcoin address to pay. + amount: + type: integer + title: Amount + description: Amount to send, in sats. + fee_rate: + type: integer + title: Fee Rate + description: Bitcoin transaction fee rate in sat/vB. + type: object + required: + - address + - amount + - fee_rate + title: SendBtcBeginRequestModel + SendBtcResponse: + properties: + txid: + type: string + title: Txid + description: TXID of the broadcast transaction. + type: object + required: + - txid + title: SendBtcResponse SendAssetBeginRequestModel: properties: recipient_map: diff --git a/src/api/mod.rs b/src/api/mod.rs index 5c15589..2091f73 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -64,6 +64,11 @@ impl ApiService for WalletSrv { resource("/getconsignment").route(post().to(node_api::get_consignment)), ) .service(resource("/blindreceive").route(post().to(node_api::blind_receive))) + .service( + resource("/witnessreceive").route(post().to(node_api::witness_receive)), + ) + .service(resource("/sendbtcbegin").route(post().to(node_api::send_btc_begin))) + .service(resource("/sendbtcend").route(post().to(node_api::send_btc_end))) .service(resource("/failtransfers").route(post().to(node_api::fail_transfers))) .service( resource("/listtransactions").route(post().to(node_api::list_transactions)), diff --git a/src/api/node_api.rs b/src/api/node_api.rs index 6f40c26..1aea610 100644 --- a/src/api/node_api.rs +++ b/src/api/node_api.rs @@ -113,11 +113,6 @@ pub struct RegisterRes { pub btc_balance: rgb_lib::wallet::BtcBalance, } -#[derive(serde::Deserialize)] -pub struct AssetId { - pub asset_id: String, -} - #[derive(serde::Deserialize)] pub struct AssetBalanceReq { /// `assetId` is the pre-unification spelling, accepted for compatibility. @@ -166,6 +161,12 @@ pub struct CreateUtxosRes { pub created: usize, } +#[derive(serde::Serialize)] +pub struct SendBtcRes { + /// TXID of the broadcast transaction. + pub txid: String, +} + #[derive(serde::Serialize)] pub struct FailTransferRes { /// Whether any transfer changed to the failed status. @@ -247,14 +248,11 @@ pub async fn list_assets( pub async fn list_transfers( ctx: Data, wallet_key: XWalletKey, - req: Json, + req: Json, ) -> Result>, ApiError> { let ctx = derive_wctx(&ctx, wallet_key).await?; - let list = ctx - .transfers_by_asset(&req.asset_id) - .await - .map_err(map_rgb_error)?; + let list = ctx.transfers_by_asset(req.0).await.map_err(map_rgb_error)?; Ok(Json(list)) } @@ -418,10 +416,49 @@ pub async fn blind_receive( req: Json, ) -> Result, ApiError> { let ctx = derive_wctx(&ctx, wallet_key).await?; - let res = ctx.receive_token(req.0).await.map_err(map_rgb_error)?; + let res = ctx + .receive_token(ReceiveKind::Blind, req.0) + .await + .map_err(map_rgb_error)?; Ok(Json(res)) } +pub async fn witness_receive( + ctx: Data, + wallet_key: XWalletKey, + req: Json, +) -> Result, ApiError> { + let ctx = derive_wctx(&ctx, wallet_key).await?; + let res = ctx + .receive_token(ReceiveKind::Witness, req.0) + .await + .map_err(map_rgb_error)?; + Ok(Json(res)) +} + +pub async fn send_btc_begin( + ctx: Data, + wallet_key: XWalletKey, + req: Json, +) -> Result, ApiError> { + let ctx = derive_wctx(&ctx, wallet_key).await?; + let psbt = ctx.send_btc_begin(req.0).await.map_err(map_rgb_error)?; + Ok(Json(PsbtRes { psbt })) +} + +pub async fn send_btc_end( + ctx: Data, + wallet_key: XWalletKey, + req: Json, +) -> Result, ApiError> { + let ctx = derive_wctx(&ctx, wallet_key).await?; + let txid = ctx + .send_btc_end(req.0.signed_psbt) + .await + .map_err(map_rgb_error)?; + Ok(Json(SendBtcRes { txid })) +} + pub async fn refresh( ctx: Data, wallet_key: XWalletKey, @@ -471,6 +508,10 @@ mod tests { }) .unwrap(), serde_json::to_string(&CreateUtxosRes { created: 3 }).unwrap(), + serde_json::to_string(&SendBtcRes { + txid: "f00d".to_string(), + }) + .unwrap(), serde_json::to_string(&FailTransferRes { changed: true }).unwrap(), serde_json::to_string(&AddressRes { address: "bcrt1qxy".to_string(), @@ -484,6 +525,7 @@ mod tests { [ r#"{"psbt":"cHNidP8B"}"#, r#"{"created":3}"#, + r#"{"txid":"f00d"}"#, r#"{"changed":true}"#, r#"{"address":"bcrt1qxy"}"#, r#"{}"#, diff --git a/src/wallet/context.rs b/src/wallet/context.rs index 676ebd6..eea2239 100644 --- a/src/wallet/context.rs +++ b/src/wallet/context.rs @@ -134,13 +134,10 @@ impl WalletCtx { pub async fn transfers_by_asset( &self, - asset_id: &str, + req: ListTransfersReq, ) -> Result, rgb_lib::Error> { let (resp, recv) = oneshot::channel(); - let cmd = WalletCmd::TransfersByAsset { - asset_id: asset_id.to_owned(), - resp, - }; + let cmd = WalletCmd::TransfersByAsset { req, resp }; self.send_cmd(cmd, recv).await } @@ -196,10 +193,12 @@ impl WalletCtx { pub async fn receive_token( &self, + kind: ReceiveKind, req: ReceiveReq, ) -> Result { let (resp, recv) = oneshot::channel(); let cmd = WalletCmd::Receive { + kind, req: req.to_owned(), resp, }; diff --git a/src/wallet/models.rs b/src/wallet/models.rs index 18e3e8e..c254301 100644 --- a/src/wallet/models.rs +++ b/src/wallet/models.rs @@ -56,6 +56,39 @@ fn default_duration_seconds() -> Option { Some(3600) } +/// Which kind of RGB invoice a receive creates. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ReceiveKind { + /// The sender assigns to a blinded UTXO this wallet already holds. + Blind, + /// The sender's transaction creates a new output paying this wallet, so + /// no colorable UTXO is needed up front. + Witness, +} + +/// `ListTransfersRequestModel` in the API spec. +#[derive(Clone, Debug, serde::Deserialize)] +pub struct ListTransfersReq { + /// The asset whose transfers to list; `null` lists the transfers not yet + /// tied to any asset (receives created without one). The field itself is + /// required, so a client that forgets it gets an error rather than the + /// asset-less list. + #[serde(deserialize_with = "Option::deserialize")] + pub asset_id: Option, +} + +impl ListTransfersReq { + pub fn asset_filter(&self) -> rgb_lib::wallet::AssetFilter { + match &self.asset_id { + Some(id) => rgb_lib::wallet::AssetFilter::Id(id.clone()), + // Not `AnyOrNone`: callers merge this with per-asset listings, and + // a transfer must not come back twice. + None => rgb_lib::wallet::AssetFilter::None, + } + } +} + +/// `SendBtcBeginRequestModel` in the API spec. #[derive(Clone, serde::Deserialize, serde::Serialize)] pub struct SendBtcReq { pub address: String, @@ -275,3 +308,54 @@ mod burn_tests { } } } + +#[cfg(test)] +mod request_tests { + use super::*; + use rgb_lib::wallet::AssetFilter; + + fn list_transfers(body: serde_json::Value) -> Result { + serde_json::from_value(body) + } + + #[test] + fn list_transfers_by_asset_matches_only_that_asset() { + let req = list_transfers(serde_json::json!({ "asset_id": "rgb:x" })).unwrap(); + assert_eq!(req.asset_filter(), AssetFilter::Id("rgb:x".into())); + } + + #[test] + fn list_transfers_with_a_null_asset_matches_asset_less_transfers_only() { + // Not AnyOrNone: callers merge this with the per-asset listings. + let req = list_transfers(serde_json::json!({ "asset_id": null })).unwrap(); + assert_eq!(req.asset_filter(), AssetFilter::None); + } + + #[test] + fn list_transfers_still_requires_the_asset_id_field() { + assert!(list_transfers(serde_json::json!({})).is_err()); + } + + #[test] + fn send_btc_requires_address_amount_and_fee_rate() { + let req: SendBtcReq = serde_json::from_value(serde_json::json!({ + "address": "bcrt1qxy", + "amount": 1000, + "fee_rate": 2, + })) + .unwrap(); + assert_eq!( + (req.address.as_str(), req.amount, req.fee_rate), + ("bcrt1qxy", 1000, 2) + ); + for missing in ["address", "amount", "fee_rate"] { + let mut body = + serde_json::json!({ "address": "bcrt1qxy", "amount": 1000, "fee_rate": 2 }); + body.as_object_mut().unwrap().remove(missing); + assert!( + serde_json::from_value::(body).is_err(), + "{missing}" + ); + } + } +} diff --git a/src/wallet/state.rs b/src/wallet/state.rs index 914badb..312ddd7 100644 --- a/src/wallet/state.rs +++ b/src/wallet/state.rs @@ -6,6 +6,7 @@ use rgb_lib::wallet::{ }; use super::Config; +use super::ReceiveKind; fn unix_now() -> u64 { std::time::SystemTime::now() @@ -70,8 +71,9 @@ impl RgbWalletState { }) } - pub fn blind_receive_token( + pub fn receive_token( &mut self, + kind: ReceiveKind, asset_id: Option, assigment: rgb_lib::Assignment, duration_seconds: Option, @@ -87,13 +89,22 @@ impl RgbWalletState { let expiration_timestamp = unix_now().saturating_add(u64::from(duration_seconds.unwrap_or(3600))); - let rd = self.wallet.blind_receive( - asset_id, - assigment, - expiration_timestamp, - transport_endpoints, - min_confirmations, - )?; + let rd = match kind { + ReceiveKind::Blind => self.wallet.blind_receive( + asset_id, + assigment, + expiration_timestamp, + transport_endpoints, + min_confirmations, + )?, + ReceiveKind::Witness => self.wallet.witness_receive( + asset_id, + assigment, + expiration_timestamp, + transport_endpoints, + min_confirmations, + )?, + }; Ok(rd) } diff --git a/src/wallet/wallet_thread.rs b/src/wallet/wallet_thread.rs index 41c98ae..f3ed52d 100644 --- a/src/wallet/wallet_thread.rs +++ b/src/wallet/wallet_thread.rs @@ -48,7 +48,7 @@ pub(crate) enum WalletCmd { resp: ResultChan, }, TransfersByAsset { - asset_id: String, + req: ListTransfersReq, resp: ResultChan>, }, TransfersByRecipient { @@ -73,6 +73,7 @@ pub(crate) enum WalletCmd { resp: ResultChan, }, Receive { + kind: ReceiveKind, req: ReceiveReq, resp: ResultChan, }, @@ -388,18 +389,19 @@ fn handle_wallet_cmd(wallet_state: Arc>, id: String, cmd: } } } - WalletCmd::TransfersByAsset { asset_id, resp } => { + WalletCmd::TransfersByAsset { req, resp } => { match wallet_state .lock() .unwrap() .wallet - .list_transfers(wallet::AssetFilter::Id(asset_id.clone()), None) + .list_transfers(req.asset_filter(), None) { Ok(val) => resp.send(Ok(val)).is_ok(), Err(err) => { log_rgb_err!( err, - "list transfers by asset: wid={id} asset={asset_id} error={:?}", + "list transfers by asset: wid={id} asset={:?} error={:?}", + req.asset_id, err ); resp.send(Err(err)).is_ok() @@ -500,14 +502,15 @@ fn handle_wallet_cmd(wallet_state: Arc>, id: String, cmd: } } } - WalletCmd::Receive { req, resp } => { + WalletCmd::Receive { kind, req, resp } => { // A missing amount and an explicit 0 both mean "no amount // restriction": an invoice demanding exactly 0 units is useless. let assignment = match req.amount { Some(amount) if amount > 0 => rgb_lib::Assignment::Fungible(amount), _ => rgb_lib::Assignment::Any, }; - match wallet_state.lock().unwrap().blind_receive_token( + match wallet_state.lock().unwrap().receive_token( + kind, req.asset_id, assignment, req.duration_seconds, @@ -515,7 +518,7 @@ fn handle_wallet_cmd(wallet_state: Arc>, id: String, cmd: ) { Ok(val) => resp.send(Ok(val)).is_ok(), Err(err) => { - log_rgb_err!(err, "blind recieve: wid={id} error={:?}", err); + log_rgb_err!(err, "receive: wid={id} kind={kind:?} error={:?}", err); resp.send(Err(err)).is_ok() } } From 11226af476cbef3121672ecb4b12c388e63d5e0e Mon Sep 17 00:00:00 2001 From: rmn-boiko Date: Tue, 27 Oct 2026 17:34:30 +0100 Subject: [PATCH 2/2] Clean new endpoints --- CHANGELOG.md | 3 --- README.md | 2 +- docs/swagger/swagger.yaml | 8 +------ src/api/node_api.rs | 12 ++++++++-- src/wallet/context.rs | 7 ++++-- src/wallet/models.rs | 45 ------------------------------------- src/wallet/wallet_thread.rs | 9 ++++---- 7 files changed, 21 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a42a8c..a78323d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,9 +36,6 @@ All notable changes to this project are documented here. The format follows ### Changed -- `/wallet/listtransfers` accepts `asset_id: null` to list the transfers not - yet tied to any asset (receives created without one). The field is still - required. - Responses follow the pinned rgb-lib models: `listassets` has a `bfa` group, transfers can be `WaitingBroadcast`, allocations can be `BridgeRight` and UTXOs carry `derivation_index`. diff --git a/README.md b/README.md index 3edfae9..08e12fa 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ The begin/end pattern allows clients to sign PSBTs externally: | POST | `/assetbalance` | Get balance for an asset | | POST | `/listunspents` | List unspent outputs | | POST | `/listtransactions` | List BTC transactions | -| POST | `/listtransfers` | List RGB transfers for an asset (`asset_id: null`: transfers not yet tied to one) | +| POST | `/listtransfers` | List RGB transfers for an asset | | POST | `/blindreceive` | Generate RGB invoice (blinded UTXO) | | POST | `/witnessreceive` | Generate RGB invoice paid to a new output of the sender's transaction | | POST | `/issueassetnia` | Issue new NIA token | diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index fc31f68..a19c30d 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -2082,14 +2082,8 @@ components: ListTransfersRequestModel: properties: asset_id: - anyOf: - - type: string - - type: 'null' + type: string title: Asset Id - description: >- - The asset whose transfers to list. `null` lists the transfers not yet - tied to any asset (receives created without one). The field is - required either way. type: object required: - asset_id diff --git a/src/api/node_api.rs b/src/api/node_api.rs index 1aea610..694593b 100644 --- a/src/api/node_api.rs +++ b/src/api/node_api.rs @@ -113,6 +113,11 @@ pub struct RegisterRes { pub btc_balance: rgb_lib::wallet::BtcBalance, } +#[derive(serde::Deserialize)] +pub struct AssetId { + pub asset_id: String, +} + #[derive(serde::Deserialize)] pub struct AssetBalanceReq { /// `assetId` is the pre-unification spelling, accepted for compatibility. @@ -248,11 +253,14 @@ pub async fn list_assets( pub async fn list_transfers( ctx: Data, wallet_key: XWalletKey, - req: Json, + req: Json, ) -> Result>, ApiError> { let ctx = derive_wctx(&ctx, wallet_key).await?; - let list = ctx.transfers_by_asset(req.0).await.map_err(map_rgb_error)?; + let list = ctx + .transfers_by_asset(&req.asset_id) + .await + .map_err(map_rgb_error)?; Ok(Json(list)) } diff --git a/src/wallet/context.rs b/src/wallet/context.rs index eea2239..e02645d 100644 --- a/src/wallet/context.rs +++ b/src/wallet/context.rs @@ -134,10 +134,13 @@ impl WalletCtx { pub async fn transfers_by_asset( &self, - req: ListTransfersReq, + asset_id: &str, ) -> Result, rgb_lib::Error> { let (resp, recv) = oneshot::channel(); - let cmd = WalletCmd::TransfersByAsset { req, resp }; + let cmd = WalletCmd::TransfersByAsset { + asset_id: asset_id.to_owned(), + resp, + }; self.send_cmd(cmd, recv).await } diff --git a/src/wallet/models.rs b/src/wallet/models.rs index c254301..93ef8e4 100644 --- a/src/wallet/models.rs +++ b/src/wallet/models.rs @@ -66,28 +66,6 @@ pub enum ReceiveKind { Witness, } -/// `ListTransfersRequestModel` in the API spec. -#[derive(Clone, Debug, serde::Deserialize)] -pub struct ListTransfersReq { - /// The asset whose transfers to list; `null` lists the transfers not yet - /// tied to any asset (receives created without one). The field itself is - /// required, so a client that forgets it gets an error rather than the - /// asset-less list. - #[serde(deserialize_with = "Option::deserialize")] - pub asset_id: Option, -} - -impl ListTransfersReq { - pub fn asset_filter(&self) -> rgb_lib::wallet::AssetFilter { - match &self.asset_id { - Some(id) => rgb_lib::wallet::AssetFilter::Id(id.clone()), - // Not `AnyOrNone`: callers merge this with per-asset listings, and - // a transfer must not come back twice. - None => rgb_lib::wallet::AssetFilter::None, - } - } -} - /// `SendBtcBeginRequestModel` in the API spec. #[derive(Clone, serde::Deserialize, serde::Serialize)] pub struct SendBtcReq { @@ -312,29 +290,6 @@ mod burn_tests { #[cfg(test)] mod request_tests { use super::*; - use rgb_lib::wallet::AssetFilter; - - fn list_transfers(body: serde_json::Value) -> Result { - serde_json::from_value(body) - } - - #[test] - fn list_transfers_by_asset_matches_only_that_asset() { - let req = list_transfers(serde_json::json!({ "asset_id": "rgb:x" })).unwrap(); - assert_eq!(req.asset_filter(), AssetFilter::Id("rgb:x".into())); - } - - #[test] - fn list_transfers_with_a_null_asset_matches_asset_less_transfers_only() { - // Not AnyOrNone: callers merge this with the per-asset listings. - let req = list_transfers(serde_json::json!({ "asset_id": null })).unwrap(); - assert_eq!(req.asset_filter(), AssetFilter::None); - } - - #[test] - fn list_transfers_still_requires_the_asset_id_field() { - assert!(list_transfers(serde_json::json!({})).is_err()); - } #[test] fn send_btc_requires_address_amount_and_fee_rate() { diff --git a/src/wallet/wallet_thread.rs b/src/wallet/wallet_thread.rs index f3ed52d..5335b9b 100644 --- a/src/wallet/wallet_thread.rs +++ b/src/wallet/wallet_thread.rs @@ -48,7 +48,7 @@ pub(crate) enum WalletCmd { resp: ResultChan, }, TransfersByAsset { - req: ListTransfersReq, + asset_id: String, resp: ResultChan>, }, TransfersByRecipient { @@ -389,19 +389,18 @@ fn handle_wallet_cmd(wallet_state: Arc>, id: String, cmd: } } } - WalletCmd::TransfersByAsset { req, resp } => { + WalletCmd::TransfersByAsset { asset_id, resp } => { match wallet_state .lock() .unwrap() .wallet - .list_transfers(req.asset_filter(), None) + .list_transfers(wallet::AssetFilter::Id(asset_id.clone()), None) { Ok(val) => resp.send(Ok(val)).is_ok(), Err(err) => { log_rgb_err!( err, - "list transfers by asset: wid={id} asset={:?} error={:?}", - req.asset_id, + "list transfers by asset: wid={id} asset={asset_id} error={:?}", err ); resp.send(Err(err)).is_ok()