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 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 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 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 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}