kaspa_cli_lib/modules/
pskb.rs

1#![allow(unused_imports)]
2
3use crate::imports::*;
4use kaspa_addresses::Prefix;
5use kaspa_consensus_core::tx::{TransactionOutpoint, UtxoEntry};
6use kaspa_wallet_core::account::pskb::finalize_pskt_one_or_more_sig_and_redeem_script;
7use kaspa_wallet_pskt::{
8    prelude::{lock_script_sig_templating, script_sig_to_address, unlock_utxos_as_pskb, Bundle, Signer, PSKT},
9    pskt::Inner,
10};
11
12#[derive(Default, Handler)]
13#[help("Send a Kaspa transaction to a public address")]
14pub struct Pskb;
15
16impl Pskb {
17    async fn main(self: Arc<Self>, ctx: &Arc<dyn Context>, mut argv: Vec<String>, _cmd: &str) -> Result<()> {
18        let ctx = ctx.clone().downcast_arc::<KaspaCli>()?;
19
20        if !ctx.wallet().is_open() {
21            return Err(Error::WalletIsNotOpen);
22        }
23
24        if argv.is_empty() {
25            return self.display_help(ctx, argv).await;
26        }
27
28        let action = argv.remove(0);
29
30        match action.as_str() {
31            "create" => {
32                if argv.len() < 2 || argv.len() > 3 {
33                    return self.display_help(ctx, argv).await;
34                }
35                let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(None).await?;
36                let _ = ctx.notifier().show(Notification::Processing).await;
37
38                let address = Address::try_from(argv.first().unwrap().as_str())?;
39                let amount_sompi = try_parse_required_nonzero_kaspa_as_sompi_u64(argv.get(1))?;
40                let outputs = PaymentOutputs::from((address, amount_sompi));
41                let priority_fee_sompi = try_parse_optional_kaspa_as_sompi_i64(argv.get(2))?.unwrap_or(0);
42                let abortable = Abortable::default();
43
44                let account: Arc<dyn Account> = ctx.wallet().account()?;
45                let signer = account
46                    .pskb_from_send_generator(
47                        outputs.into(),
48                        priority_fee_sompi.into(),
49                        None,
50                        wallet_secret.clone(),
51                        payment_secret.clone(),
52                        &abortable,
53                    )
54                    .await?;
55
56                match signer.serialize() {
57                    Ok(encoded) => tprintln!(ctx, "{encoded}"),
58                    Err(e) => return Err(e.into()),
59                }
60            }
61            "script" => {
62                if argv.len() < 2 || argv.len() > 4 {
63                    return self.display_help(ctx, argv).await;
64                }
65                let subcommand = argv.remove(0);
66                let payload = argv.remove(0);
67                let account = ctx.wallet().account()?;
68                let receive_address = account.receive_address()?;
69                let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(None).await?;
70                let _ = ctx.notifier().show(Notification::Processing).await;
71
72                let script_sig = match lock_script_sig_templating(payload.clone(), Some(&receive_address.payload)) {
73                    Ok(value) => value,
74                    Err(e) => {
75                        terrorln!(ctx, "{}", e.to_string());
76                        return Err(e.into());
77                    }
78                };
79
80                let script_p2sh = match script_sig_to_address(&script_sig, ctx.wallet().address_prefix()?) {
81                    Ok(p2sh) => p2sh,
82                    Err(e) => {
83                        terrorln!(ctx, "Error generating script address: {}", e.to_string());
84                        return Err(e.into());
85                    }
86                };
87
88                match subcommand.as_str() {
89                    "lock" => {
90                        let amount_sompi = try_parse_required_nonzero_kaspa_as_sompi_u64(argv.first())?;
91                        let outputs = PaymentOutputs::from((script_p2sh, amount_sompi));
92                        let priority_fee_sompi = try_parse_optional_kaspa_as_sompi_i64(argv.get(1))?.unwrap_or(0);
93                        let abortable = Abortable::default();
94
95                        let signer = account
96                            .pskb_from_send_generator(
97                                outputs.into(),
98                                priority_fee_sompi.into(),
99                                None,
100                                wallet_secret.clone(),
101                                payment_secret.clone(),
102                                &abortable,
103                            )
104                            .await?;
105
106                        match signer.serialize() {
107                            Ok(encoded) => tprintln!(ctx, "{encoded}"),
108                            Err(e) => return Err(e.into()),
109                        }
110                    }
111                    "unlock" => {
112                        if argv.len() != 1 {
113                            return self.display_help(ctx, argv).await;
114                        }
115
116                        // Get locked UTXO set.
117                        let spend_utxos: Vec<kaspa_rpc_core::RpcUtxosByAddressesEntry> =
118                            ctx.wallet().rpc_api().get_utxos_by_addresses(vec![script_p2sh.clone()]).await?;
119                        let priority_fee_sompi = try_parse_optional_kaspa_as_sompi_i64(argv.first())?.unwrap_or(0) as u64;
120
121                        if spend_utxos.is_empty() {
122                            twarnln!(ctx, "No locked UTXO set found.");
123                            return Ok(());
124                        }
125
126                        let references: Vec<(UtxoEntry, TransactionOutpoint)> =
127                            spend_utxos.iter().map(|entry| (entry.utxo_entry.clone().into(), entry.outpoint.into())).collect();
128
129                        let total_locked_sompi: u64 = spend_utxos.iter().map(|entry| entry.utxo_entry.amount).sum();
130
131                        tprintln!(
132                            ctx,
133                            "{} locked UTXO{} found with total amount of {} KAS",
134                            spend_utxos.len(),
135                            if spend_utxos.len() == 1 { "" } else { "s" },
136                            sompi_to_kaspa(total_locked_sompi)
137                        );
138
139                        // Sweep UTXO set.
140                        match unlock_utxos_as_pskb(references, &receive_address, script_sig, priority_fee_sompi as u64) {
141                            Ok(pskb) => {
142                                let pskb_hex = pskb.serialize()?;
143                                tprintln!(ctx, "{pskb_hex}");
144                            }
145                            Err(e) => tprintln!(ctx, "Error generating unlock PSKB: {}", e.to_string()),
146                        }
147                    }
148                    "sign" => {
149                        let pskb = Self::parse_input_pskb(argv.first().unwrap().as_str())?;
150
151                        // Sign PSKB using the account's receiver address.
152                        match account.pskb_sign(&pskb, wallet_secret.clone(), payment_secret.clone(), Some(&receive_address)).await {
153                            Ok(signed_pskb) => {
154                                let pskb_pack = String::try_from(signed_pskb)?;
155                                tprintln!(ctx, "{pskb_pack}");
156                            }
157                            Err(e) => terrorln!(ctx, "{}", e.to_string()),
158                        }
159                    }
160                    "address" => {
161                        tprintln!(ctx, "\r\nP2SH address: {}", script_p2sh);
162                    }
163                    v => {
164                        terrorln!(ctx, "unknown command: '{v}'\r\n");
165                        return self.display_help(ctx, argv).await;
166                    }
167                }
168            }
169            "sign" => {
170                if argv.len() != 1 {
171                    return self.display_help(ctx, argv).await;
172                }
173                let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(None).await?;
174                let pskb = Self::parse_input_pskb(argv.first().unwrap().as_str())?;
175                let account = ctx.wallet().account()?;
176                match account.pskb_sign(&pskb, wallet_secret.clone(), payment_secret.clone(), None).await {
177                    Ok(signed_pskb) => {
178                        let pskb_pack = String::try_from(signed_pskb)?;
179                        tprintln!(ctx, "{pskb_pack}");
180                    }
181                    Err(e) => terrorln!(ctx, "{}", e.to_string()),
182                }
183            }
184            "send" => {
185                if argv.len() != 1 {
186                    return self.display_help(ctx, argv).await;
187                }
188                let pskb = Self::parse_input_pskb(argv.first().unwrap().as_str())?;
189                let account = ctx.wallet().account()?;
190                match account.pskb_broadcast(&pskb).await {
191                    Ok(sent) => tprintln!(ctx, "Sent transactions {:?}", sent),
192                    Err(e) => terrorln!(ctx, "Send error {:?}", e),
193                }
194            }
195            "debug" => {
196                if argv.len() != 1 {
197                    return self.display_help(ctx, argv).await;
198                }
199                let pskb = Self::parse_input_pskb(argv.first().unwrap().as_str())?;
200                tprintln!(ctx, "{:?}", pskb);
201            }
202            "parse" => {
203                if argv.len() != 1 {
204                    return self.display_help(ctx, argv).await;
205                }
206                let pskb = Self::parse_input_pskb(argv.first().unwrap().as_str())?;
207                tprintln!(ctx, "{}", pskb.display_format(ctx.wallet().network_id()?, sompi_to_kaspa_string_with_suffix));
208
209                for (pskt_index, bundle_inner) in pskb.0.iter().enumerate() {
210                    tprintln!(ctx, "PSKT #{:03} finalized check:", pskt_index + 1);
211                    let pskt: PSKT<Signer> = PSKT::<Signer>::from(bundle_inner.to_owned());
212
213                    let finalizer = pskt.finalizer();
214
215                    if let Ok(pskt_finalizer) = finalize_pskt_one_or_more_sig_and_redeem_script(finalizer) {
216                        // Verify if extraction is possible.
217                        match pskt_finalizer.extractor() {
218                            Ok(ex) => match ex.extract_tx() {
219                                Ok(_) => tprintln!(
220                                    ctx,
221                                    "  Transaction extracted successfully: PSKT is finalized with a valid script signature."
222                                ),
223                                Err(e) => terrorln!(ctx, "  PSKT transaction extraction error: {}", e.to_string()),
224                            },
225                            Err(_) => twarnln!(ctx, "  PSKT not finalized"),
226                        }
227                    } else {
228                        twarnln!(ctx, "  PSKT not signed");
229                    }
230                }
231            }
232            v => {
233                tprintln!(ctx, "unknown command: '{v}'\r\n");
234                return self.display_help(ctx, argv).await;
235            }
236        }
237        Ok(())
238    }
239
240    fn parse_input_pskb(input: &str) -> Result<Bundle> {
241        match Bundle::try_from(input) {
242            Ok(bundle) => Ok(bundle),
243            Err(e) => Err(Error::custom(format!("Error while parsing input PSKB {}", e))),
244        }
245    }
246
247    async fn display_help(self: Arc<Self>, ctx: Arc<KaspaCli>, _argv: Vec<String>) -> Result<()> {
248        ctx.term().help(
249            &[
250                ("pskb create <address> <amount> <priority fee>", "Create a PSKB from single send transaction"),
251                ("pskb sign <pskb>", "Sign given PSKB"),
252                ("pskb send <pskb>", "Broadcast bundled transactions"),
253                ("pskb debug <payload>", "Print PSKB debug view"),
254                ("pskb parse <payload>", "Print PSKB formatted view"),
255                ("pskb script lock <payload> <amount> [priority fee]", "Generate a PSKB with one send transaction to given P2SH payload. Optional public key placeholder in payload: {{pubkey}}"),
256                ("pskb script unlock <payload> <fee>", "Generate a PSKB to unlock UTXOS one by one from given P2SH payload. Fee amount will be applied to every spent UTXO, meaning every transaction. Optional public key placeholder in payload: {{pubkey}}"),
257                ("pskb script sign <pskb>", "Sign all PSKB's P2SH locked inputs"),
258                ("pskb script sign <pskb>", "Sign all PSKB's P2SH locked inputs"),
259                ("pskb script address <pskb>", "Prints P2SH address"),
260            ],
261            None,
262        )?;
263
264        Ok(())
265    }
266}