breez_sdk_liquid/payjoin/
side_swap.rs

1use std::{collections::HashMap, str::FromStr};
2
3use super::{
4    error::{PayjoinError, PayjoinResult},
5    model::{
6        AcceptedAsset, AcceptedAssetsRequest, AcceptedAssetsResponse, Request, Response,
7        SignRequest, StartRequest, Utxo,
8    },
9    pset::PsetInput,
10    utxo_select::utxo_select,
11    PayjoinService,
12};
13use boltz_client::Secp256k1;
14use log::{debug, error};
15use lwk_wollet::{
16    bitcoin::base64::{self, Engine as _},
17    elements::{
18        self,
19        confidential::{self, AssetBlindingFactor, ValueBlindingFactor},
20        pset::PartiallySignedTransaction,
21        secp256k1_zkp::Generator,
22        Address, AssetId, Transaction, TxOutSecrets,
23    },
24};
25use sdk_common::{
26    ensure_sdk,
27    prelude::{parse_json, FiatAPI, RestClient},
28    utils::Arc,
29};
30use serde::{de::DeserializeOwned, Serialize};
31use tokio::sync::OnceCell;
32
33use crate::payjoin::{
34    model::{InOut, Recipient},
35    network_fee::TxFee,
36    pset::blind::remove_explicit_values,
37    utxo_select::UtxoSelectRequest,
38};
39use crate::persist::Persister;
40use crate::{
41    model::{Config, LiquidNetwork},
42    payjoin::pset::{construct_pset, ConstructPsetRequest, PsetOutput},
43};
44use crate::{utils, wallet::OnchainWallet};
45
46const PRODUCTION_SIDESWAP_URL: &str = "https://api.sideswap.io/payjoin";
47const TESTNET_SIDESWAP_URL: &str = "https://api-testnet.sideswap.io/payjoin";
48// Base fee in USD represented in satoshis ($0.04)
49const SIDESWAP_BASE_USD_FEE_SAT: f64 = 4_000_000.0;
50
51pub(crate) struct SideSwapPayjoinService {
52    config: Config,
53    fiat_api: Arc<dyn FiatAPI>,
54    persister: Arc<Persister>,
55    onchain_wallet: Arc<dyn OnchainWallet>,
56    rest_client: Arc<dyn RestClient>,
57    accepted_assets: OnceCell<AcceptedAssetsResponse>,
58}
59
60impl SideSwapPayjoinService {
61    pub fn new(
62        config: Config,
63        fiat_api: Arc<dyn FiatAPI>,
64        persister: Arc<Persister>,
65        onchain_wallet: Arc<dyn OnchainWallet>,
66        rest_client: Arc<dyn RestClient>,
67    ) -> Self {
68        Self {
69            config,
70            fiat_api,
71            persister,
72            onchain_wallet,
73            rest_client,
74            accepted_assets: OnceCell::new(),
75        }
76    }
77
78    fn get_url(&self) -> PayjoinResult<&str> {
79        match self.config.network {
80            LiquidNetwork::Mainnet => Ok(PRODUCTION_SIDESWAP_URL),
81            LiquidNetwork::Testnet => Ok(TESTNET_SIDESWAP_URL),
82            network => Err(PayjoinError::generic(format!(
83                "Payjoin not supported on {network}"
84            ))),
85        }
86    }
87
88    async fn post_request<I: Serialize, O: DeserializeOwned>(&self, body: &I) -> PayjoinResult<O> {
89        let headers = HashMap::from([("Content-Type".to_string(), "application/json".to_string())]);
90        let body = serde_json::to_string(body)?;
91        debug!("Posting request to SideSwap: {body}");
92        let (response, status_code) = self
93            .rest_client
94            .post(self.get_url()?, Some(headers), Some(body))
95            .await?;
96        if status_code != 200 {
97            error!("Received status code {status_code} response from SideSwap");
98            return Err(PayjoinError::service_connectivity(format!(
99                "Failed to post request to SideSwap: {response}"
100            )));
101        }
102        debug!("Received response from SideSwap: {response}");
103        Ok(parse_json(&response)?)
104    }
105}
106
107#[sdk_macros::async_trait]
108impl PayjoinService for SideSwapPayjoinService {
109    async fn fetch_accepted_assets(&self) -> PayjoinResult<Vec<AcceptedAsset>> {
110        let accepted_assets = self
111            .accepted_assets
112            .get_or_try_init(|| async {
113                debug!("Initializing accepted_assets from SideSwap");
114                let accepted_assets_request = Request::AcceptedAssets(AcceptedAssetsRequest {});
115                let response: Response = self.post_request(&accepted_assets_request).await?;
116                match response {
117                    Response::AcceptedAssets(accepted_assets) => Ok(accepted_assets),
118                    _ => Err(PayjoinError::service_connectivity(
119                        "Failed to request accepted assets from SideSwap",
120                    )),
121                }
122            })
123            .await?;
124
125        Ok(accepted_assets.accepted_asset.clone())
126    }
127
128    async fn estimate_payjoin_tx_fee(&self, asset_id: &str, amount_sat: u64) -> PayjoinResult<f64> {
129        // Check the asset is accepted
130        let fee_asset = AssetId::from_str(asset_id)?;
131        let accepted_assets = self.fetch_accepted_assets().await?;
132        ensure_sdk!(
133            accepted_assets
134                .iter()
135                .any(|asset| asset.asset_id == asset_id),
136            PayjoinError::generic("Asset not accepted by SideSwap")
137        );
138
139        // Get and check the wallet asset balance
140        let wallet_asset_balance: u64 = self
141            .onchain_wallet
142            .asset_utxos(&fee_asset)
143            .await?
144            .iter()
145            .map(|utxo| utxo.unblinded.value)
146            .sum();
147        ensure_sdk!(
148            wallet_asset_balance > amount_sat,
149            PayjoinError::InsufficientFunds
150        );
151
152        // Fetch the fiat rates
153        let asset_metadata =
154            self.persister
155                .get_asset_metadata(asset_id)?
156                .ok_or(PayjoinError::generic(format!(
157                    "No asset metadata available for {asset_id}"
158                )))?;
159        let Some(fiat_id) = asset_metadata.fiat_id.clone() else {
160            return Err(PayjoinError::generic(format!(
161                "No fiat ID available in asset metadata for {asset_id}"
162            )));
163        };
164        let fiat_rates = self.fiat_api.fetch_fiat_rates().await?;
165        let usd_index_price = fiat_rates
166            .iter()
167            .find(|rate| rate.coin == "USD")
168            .map(|rate| rate.value)
169            .ok_or(PayjoinError::generic("No rate available for USD"))?;
170        let asset_index_price = fiat_rates
171            .iter()
172            .find(|rate| rate.coin == fiat_id)
173            .map(|rate| rate.value)
174            .ok_or(PayjoinError::generic(format!(
175                "No rate available for {fiat_id}"
176            )))?;
177
178        let fixed_fee = (SIDESWAP_BASE_USD_FEE_SAT / usd_index_price * asset_index_price) as u64;
179        // Fees assuming we have:
180        // - 1 input for the server (lbtc)
181        // - 1 input for the user (asset)
182        // - 1 output for the user (asset change)
183        // - 1 output for the recipient (asset)
184        // - 1 output for the server (asset fee)
185        // - 1 output for the server (lbtc change)
186        let network_fee = TxFee {
187            server_inputs: 1,
188            user_inputs: 1,
189            outputs: 4,
190        }
191        .fee();
192        let fee_sat = (network_fee as f64 * asset_index_price) as u64 + fixed_fee;
193        ensure_sdk!(
194            wallet_asset_balance >= amount_sat + fee_sat,
195            PayjoinError::InsufficientFunds
196        );
197
198        // The estimation accuracy gives a fee to two decimal places
199        let mut fee = asset_metadata.amount_from_sat(fee_sat);
200        fee = (fee * 100.0).ceil() / 100.0;
201
202        debug!("Estimated payjoin server fee: {fee} ({fee_sat} satoshi units)");
203
204        Ok(fee)
205    }
206
207    async fn build_payjoin_tx(
208        &self,
209        recipient_address: &str,
210        asset_id: &str,
211        amount_sat: u64,
212    ) -> PayjoinResult<(Transaction, u64)> {
213        let fee_asset = AssetId::from_str(asset_id)?;
214        let wallet_utxos = self
215            .onchain_wallet
216            .asset_utxos(&fee_asset)
217            .await?
218            .iter()
219            .map(Utxo::from)
220            .collect::<Vec<_>>();
221        ensure_sdk!(!wallet_utxos.is_empty(), PayjoinError::InsufficientFunds);
222
223        let address = Address::from_str(recipient_address).map_err(|e| {
224            PayjoinError::generic(format!(
225                "Recipient address {recipient_address} is not a valid ElementsAddress: {e:?}"
226            ))
227        })?;
228        let recipients = vec![Recipient {
229            address,
230            asset_id: fee_asset,
231            amount: amount_sat,
232        }];
233
234        let start_request = Request::Start(StartRequest {
235            asset_id: asset_id.to_string(),
236            user_agent: "breezsdk".to_string(),
237            api_key: self.config.sideswap_api_key.clone(),
238        });
239        let response: Response = self.post_request(&start_request).await?;
240        let Response::Start(start_response) = response else {
241            return Err(PayjoinError::service_connectivity(
242                "Failed to start payjoin",
243            ));
244        };
245        ensure_sdk!(
246            start_response.fee_address.is_blinded(),
247            PayjoinError::generic("Server fee address is not blinded")
248        );
249        ensure_sdk!(
250            start_response.change_address.is_blinded(),
251            PayjoinError::generic("Server change address is not blinded")
252        );
253        ensure_sdk!(
254            !start_response.utxos.is_empty(),
255            PayjoinError::generic("Server utxos are empty")
256        );
257
258        let policy_asset = utils::lbtc_asset_id(self.config.network);
259        let utxo_select_res = utxo_select(UtxoSelectRequest {
260            policy_asset,
261            fee_asset,
262            price: start_response.price,
263            fixed_fee: start_response.fixed_fee,
264            wallet_utxos: wallet_utxos.iter().map(Into::into).collect(),
265            server_utxos: start_response.utxos.iter().map(Into::into).collect(),
266            user_outputs: recipients
267                .iter()
268                .map(|recipient| InOut {
269                    asset_id: recipient.asset_id,
270                    value: recipient.amount,
271                })
272                .collect(),
273        })?;
274        ensure_sdk!(
275            utxo_select_res.user_outputs.len() == recipients.len(),
276            PayjoinError::generic("Output/recipient lengths mismatch")
277        );
278
279        let mut inputs = Vec::new();
280        let mut outputs = Vec::new();
281
282        // Set the wallet and server inputs
283        inputs.append(&mut select_utxos(
284            wallet_utxos,
285            utxo_select_res
286                .user_inputs
287                .into_iter()
288                .chain(utxo_select_res.client_inputs.into_iter())
289                .collect(),
290        )?);
291        inputs.append(&mut select_utxos(
292            start_response.utxos,
293            utxo_select_res.server_inputs,
294        )?);
295
296        // Set the outputs
297        let server_fee = utxo_select_res.server_fee;
298
299        // Recipient outputs
300        for (output, recipient) in utxo_select_res
301            .user_outputs
302            .iter()
303            .zip(recipients.into_iter())
304        {
305            debug!(
306                "Payjoin recipent output: {} value: {}",
307                recipient.address, output.value
308            );
309            outputs.push(PsetOutput {
310                asset_id: output.asset_id,
311                amount: output.value,
312                address: recipient.address,
313            });
314        }
315
316        // Change outputs
317        for output in utxo_select_res
318            .change_outputs
319            .iter()
320            .chain(utxo_select_res.fee_change.iter())
321        {
322            let address = self.onchain_wallet.next_unused_change_address().await?;
323            debug!("Payjoin change output: {address} value: {}", output.value);
324            outputs.push(PsetOutput {
325                asset_id: output.asset_id,
326                amount: output.value,
327                address,
328            });
329        }
330
331        // Server fee output
332        debug!(
333            "Payjoin server fee output: {} value: {}",
334            start_response.fee_address, server_fee.value
335        );
336        outputs.push(PsetOutput {
337            asset_id: server_fee.asset_id,
338            amount: server_fee.value,
339            address: start_response.fee_address,
340        });
341
342        // Server change output
343        if let Some(output) = utxo_select_res.server_change {
344            debug!(
345                "Payjoin server change output: {} value: {}",
346                start_response.change_address, output.value
347            );
348            outputs.push(PsetOutput {
349                asset_id: output.asset_id,
350                amount: output.value,
351                address: start_response.change_address,
352            });
353        }
354
355        // Construct the PSET
356        let blinded_pset = construct_pset(ConstructPsetRequest {
357            policy_asset,
358            inputs,
359            outputs,
360            network_fee: utxo_select_res.network_fee.value,
361        })?;
362
363        let mut pset = blinded_pset.clone();
364        remove_explicit_values(&mut pset);
365        let server_pset = elements::encode::serialize(&pset);
366
367        // Send the signing request
368        let sign_request = Request::Sign(SignRequest {
369            order_id: start_response.order_id,
370            pset: base64::engine::general_purpose::STANDARD.encode(&server_pset),
371        });
372        let response: Response = self.post_request(&sign_request).await?;
373        let Response::Sign(sign_response) = response else {
374            return Err(PayjoinError::service_connectivity("Failed to sign payjoin"));
375        };
376
377        // Copy the signed inputs to the blinded PSET
378        let server_signed_pset = elements::encode::deserialize::<PartiallySignedTransaction>(
379            &base64::engine::general_purpose::STANDARD.decode(&sign_response.pset)?,
380        )?;
381        let server_signed_blinded_pset = copy_signatures(blinded_pset, server_signed_pset)?;
382
383        let tx = self
384            .onchain_wallet
385            .sign_pset(server_signed_blinded_pset)
386            .await?;
387        Ok((tx, server_fee.value))
388    }
389}
390
391impl From<&Utxo> for InOut {
392    fn from(utxo: &Utxo) -> Self {
393        Self {
394            asset_id: utxo.asset_id,
395            value: utxo.value,
396        }
397    }
398}
399
400fn copy_signatures(
401    mut dst_pset: PartiallySignedTransaction,
402    src_pset: PartiallySignedTransaction,
403) -> PayjoinResult<PartiallySignedTransaction> {
404    ensure_sdk!(
405        dst_pset.inputs().len() == src_pset.inputs().len(),
406        PayjoinError::generic("Input lengths mismatch")
407    );
408    ensure_sdk!(
409        dst_pset.outputs().len() == src_pset.outputs().len(),
410        PayjoinError::generic("Output lengths mismatch")
411    );
412    for (dst_input, src_input) in dst_pset
413        .inputs_mut()
414        .iter_mut()
415        .zip(src_pset.inputs().iter())
416    {
417        if src_input.final_script_witness.is_some() {
418            dst_input.final_script_sig = src_input.final_script_sig.clone();
419            dst_input.final_script_witness = src_input.final_script_witness.clone();
420        }
421    }
422    Ok(dst_pset)
423}
424
425fn select_utxos(mut utxos: Vec<Utxo>, in_outs: Vec<InOut>) -> PayjoinResult<Vec<PsetInput>> {
426    let secp = Secp256k1::new();
427    let mut selected = Vec::new();
428    for in_out in in_outs {
429        let index = utxos
430            .iter()
431            .position(|utxo| utxo.asset_id == in_out.asset_id && utxo.value == in_out.value)
432            .ok_or(PayjoinError::generic("Failed to find utxo"))?;
433        let utxo = utxos.remove(index);
434
435        let (asset_commitment, value_commitment) = if utxo.asset_bf == AssetBlindingFactor::zero()
436            || utxo.value_bf == ValueBlindingFactor::zero()
437        {
438            (
439                confidential::Asset::Explicit(utxo.asset_id),
440                confidential::Value::Explicit(utxo.value),
441            )
442        } else {
443            let gen =
444                Generator::new_blinded(&secp, utxo.asset_id.into_tag(), utxo.asset_bf.into_inner());
445            (
446                confidential::Asset::Confidential(gen),
447                confidential::Value::new_confidential(&secp, utxo.value, gen, utxo.value_bf),
448            )
449        };
450
451        let input = PsetInput {
452            txid: utxo.txid,
453            vout: utxo.vout,
454            script_pub_key: utxo.script_pub_key,
455            asset_commitment,
456            value_commitment,
457            tx_out_sec: TxOutSecrets {
458                asset: utxo.asset_id,
459                asset_bf: utxo.asset_bf,
460                value: utxo.value,
461                value_bf: utxo.value_bf,
462            },
463        };
464        debug!("Payjoin input: {} vout: {}", input.txid, input.vout);
465        selected.push(input);
466    }
467    Ok(selected)
468}
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473
474    use anyhow::Result;
475    use lwk_wollet::{
476        elements::{OutPoint, Script, Txid},
477        Chain, WalletTxOut,
478    };
479    use sdk_common::prelude::{BreezServer, MockResponse, MockRestClient, STAGING_BREEZSERVER_URL};
480    use serde_json::json;
481
482    use crate::{
483        model::Signer,
484        test_utils::{
485            persist::create_persister,
486            wallet::{MockSigner, MockWallet},
487        },
488    };
489
490    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
491    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
492
493    fn create_sideswap_payjoin_service(
494        persister: Arc<Persister>,
495    ) -> Result<(Arc<MockWallet>, Arc<MockRestClient>, SideSwapPayjoinService)> {
496        let config = Config::testnet_esplora(None);
497        let breez_server = Arc::new(BreezServer::new(STAGING_BREEZSERVER_URL.to_string(), None)?);
498        let signer: Arc<Box<dyn Signer>> = Arc::new(Box::new(MockSigner::new()?));
499        let onchain_wallet = Arc::new(MockWallet::new(signer.clone())?);
500        let rest_client = Arc::new(MockRestClient::new());
501
502        Ok((
503            onchain_wallet.clone(),
504            rest_client.clone(),
505            SideSwapPayjoinService::new(
506                config,
507                breez_server,
508                persister,
509                onchain_wallet,
510                rest_client,
511            ),
512        ))
513    }
514
515    fn create_utxos(asset: AssetId, values: Vec<u64>) -> Vec<WalletTxOut> {
516        let txid =
517            Txid::from_str("0000000000000000000000000000000000000000000000000000000000000001")
518                .unwrap();
519        let script_pubkey =
520            Script::from_str("76a914000000000000000000000000000000000000000088ac").unwrap();
521
522        values.into_iter().map(|value| {
523            WalletTxOut {
524                outpoint: OutPoint::new(txid, 0),
525                script_pubkey: script_pubkey.clone(),
526                height: Some(10),
527                unblinded: TxOutSecrets {
528                    asset,
529                    value,
530                    asset_bf: AssetBlindingFactor::zero(),
531                    value_bf: ValueBlindingFactor::zero(),
532                },
533                wildcard_index: 0,
534                ext_int: Chain::Internal,
535                is_spent: false,
536                address: Address::from_str("lq1pqw8ct25kd47dejyesyvk3g2kaf8s9uhq4se7r2kj9y9hhvu9ug5thxlpn9y63s78kc2mcp6nujavckvr42q7hwkhqq9hfz46nth22hfp3em0ulm4nsuf").unwrap(),
537            }
538        }).collect()
539    }
540
541    #[sdk_macros::async_test_all]
542    async fn test_fetch_accepted_assets_error() -> Result<()> {
543        create_persister!(persister);
544        let (_, mock_rest_client, payjoin_service) =
545            create_sideswap_payjoin_service(persister).unwrap();
546
547        mock_rest_client.add_response(MockResponse::new(400, "".to_string()));
548
549        let res = payjoin_service.fetch_accepted_assets().await;
550        assert!(res.is_err());
551
552        Ok(())
553    }
554
555    #[sdk_macros::async_test_all]
556    async fn test_fetch_accepted_assets() -> Result<()> {
557        create_persister!(persister);
558        let (_, mock_rest_client, payjoin_service) =
559            create_sideswap_payjoin_service(persister).unwrap();
560        let asset_id = AssetId::from_slice(&[2; 32]).unwrap().to_string();
561
562        let response_body =
563            json!({"accepted_assets": {"accepted_asset":[{"asset_id": asset_id}]}}).to_string();
564        mock_rest_client.add_response(MockResponse::new(200, response_body));
565
566        let res = payjoin_service.fetch_accepted_assets().await;
567        assert!(res.is_ok());
568        let accepted_assets = res.unwrap();
569        assert_eq!(accepted_assets.len(), 1);
570        assert_eq!(accepted_assets[0].asset_id, asset_id);
571
572        Ok(())
573    }
574
575    #[sdk_macros::async_test_all]
576    async fn test_estimate_payjoin_tx_fee_error() -> Result<()> {
577        create_persister!(persister);
578        let (_, mock_rest_client, payjoin_service) =
579            create_sideswap_payjoin_service(persister).unwrap();
580        let asset_id = AssetId::from_slice(&[2; 32]).unwrap().to_string();
581
582        mock_rest_client.add_response(MockResponse::new(400, "".to_string()));
583
584        let amount_sat = 500_000;
585        let res = payjoin_service
586            .estimate_payjoin_tx_fee(&asset_id, amount_sat)
587            .await;
588        assert!(res.is_err());
589
590        Ok(())
591    }
592
593    #[sdk_macros::async_test_all]
594    async fn test_estimate_payjoin_tx_fee_no_utxos() -> Result<()> {
595        create_persister!(persister);
596        let (_, mock_rest_client, payjoin_service) =
597            create_sideswap_payjoin_service(persister).unwrap();
598        let asset_id = AssetId::from_slice(&[2; 32]).unwrap().to_string();
599
600        let response_body =
601            json!({"accepted_assets": {"accepted_asset":[{"asset_id": asset_id}]}}).to_string();
602        mock_rest_client.add_response(MockResponse::new(200, response_body));
603
604        let amount_sat = 500_000;
605        let res = payjoin_service
606            .estimate_payjoin_tx_fee(&asset_id, amount_sat)
607            .await;
608        assert!(res.is_err());
609        assert_eq!(res.unwrap_err().to_string(), "Cannot pay: not enough funds");
610
611        Ok(())
612    }
613
614    #[sdk_macros::async_test_all]
615    async fn test_estimate_payjoin_tx_fee_no_asset_metadata() -> Result<()> {
616        create_persister!(persister);
617        let (mock_wallet, mock_rest_client, payjoin_service) =
618            create_sideswap_payjoin_service(persister).unwrap();
619        let asset_id = AssetId::from_slice(&[2; 32]).unwrap();
620        let asset_id_str = asset_id.to_string();
621
622        // Mock the accepted assets response
623        let accepted_assets_response = json!({
624            "accepted_assets": {
625                "accepted_asset":[{"asset_id": asset_id_str}]
626            }
627        })
628        .to_string();
629        mock_rest_client.add_response(MockResponse::new(200, accepted_assets_response));
630
631        // Set up the mock wallet to return some UTXOs for the test asset
632        let utxos = create_utxos(asset_id, vec![1_000_000]);
633        mock_wallet.set_utxos(utxos);
634
635        let amount_sat = 500_000;
636        let res = payjoin_service
637            .estimate_payjoin_tx_fee(&asset_id_str, amount_sat)
638            .await;
639
640        assert_eq!(res.unwrap_err().to_string(), "No asset metadata available for 0202020202020202020202020202020202020202020202020202020202020202");
641
642        Ok(())
643    }
644
645    #[sdk_macros::async_test_all]
646    #[ignore = "Requires a mockable FiatAPI"]
647
648    async fn test_estimate_payjoin_tx_fee() -> Result<()> {
649        create_persister!(persister);
650        let (mock_wallet, mock_rest_client, payjoin_service) =
651            create_sideswap_payjoin_service(persister).unwrap();
652        let asset_id =
653            AssetId::from_str("b612eb46313a2cd6ebabd8b7a8eed5696e29898b87a43bff41c94f51acef9d73")
654                .unwrap();
655        let asset_id_str = asset_id.to_string();
656
657        // Mock the accepted assets response
658        let accepted_assets_response = json!({
659            "accepted_assets": {
660                "accepted_asset":[{"asset_id": asset_id_str}]
661            }
662        })
663        .to_string();
664        mock_rest_client.add_response(MockResponse::new(200, accepted_assets_response));
665
666        // TODO: Mock the FiatAPI response as the staging BreezServer currently times out
667
668        // Set up the mock wallet to return some UTXOs for the test asset
669        let utxos = create_utxos(asset_id, vec![1_000_000]);
670        mock_wallet.set_utxos(utxos);
671
672        let amount_sat = 500_000;
673        let res = payjoin_service
674            .estimate_payjoin_tx_fee(&asset_id_str, amount_sat)
675            .await;
676
677        assert_eq!(res.unwrap_err().to_string(), "Cannot pay: not enough funds");
678
679        Ok(())
680    }
681}