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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,16 @@ The begin/end pattern allows clients to sign PSBTs externally:
| POST | `/listunspents` | List unspent outputs |
| POST | `/listtransactions` | List BTC transactions |
| POST | `/listtransfers` | List RGB transfers for an asset |
| POST | `/blindreceive` | Generate RGB invoice |
| 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) |
Expand Down
185 changes: 185 additions & 0 deletions docs/swagger/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -2091,6 +2246,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:
Expand Down
5 changes: 5 additions & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down
52 changes: 51 additions & 1 deletion src/api/node_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,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.
Expand Down Expand Up @@ -418,10 +424,49 @@ pub async fn blind_receive(
req: Json<ReceiveReq>,
) -> Result<Json<rgb_lib::wallet::ReceiveData>, 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<WalletCtx>,
wallet_key: XWalletKey,
req: Json<ReceiveReq>,
) -> Result<Json<rgb_lib::wallet::ReceiveData>, 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<WalletCtx>,
wallet_key: XWalletKey,
req: Json<SendBtcReq>,
) -> Result<Json<PsbtRes>, 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<WalletCtx>,
wallet_key: XWalletKey,
req: Json<Psbt>,
) -> Result<Json<SendBtcRes>, 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<WalletCtx>,
wallet_key: XWalletKey,
Expand Down Expand Up @@ -471,6 +516,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(),
Expand All @@ -484,6 +533,7 @@ mod tests {
[
r#"{"psbt":"cHNidP8B"}"#,
r#"{"created":3}"#,
r#"{"txid":"f00d"}"#,
r#"{"changed":true}"#,
r#"{"address":"bcrt1qxy"}"#,
r#"{}"#,
Expand Down
2 changes: 2 additions & 0 deletions src/wallet/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,12 @@ impl WalletCtx {

pub async fn receive_token(
&self,
kind: ReceiveKind,
req: ReceiveReq,
) -> Result<rgb_lib::wallet::ReceiveData, rgb_lib::Error> {
let (resp, recv) = oneshot::channel();
let cmd = WalletCmd::Receive {
kind,
req: req.to_owned(),
resp,
};
Expand Down
39 changes: 39 additions & 0 deletions src/wallet/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ fn default_duration_seconds() -> Option<u32> {
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,
}

/// `SendBtcBeginRequestModel` in the API spec.
#[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct SendBtcReq {
pub address: String,
Expand Down Expand Up @@ -275,3 +286,31 @@ mod burn_tests {
}
}
}

#[cfg(test)]
mod request_tests {
use super::*;

#[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::<SendBtcReq>(body).is_err(),
"{missing}"
);
}
}
}
Loading
Loading