Skip to content

Commit

Permalink
PSKB+PSKT merge with omega branch (kaspanet#82)
Browse files Browse the repository at this point in the history
* TransactionInput signature_script as Option, add associated TS types

* restructure PSKT + WASM scaffolding (WIP)

* Base implementation for PSKB and usage in core account with generator wrapper (kaspanet#64)

* Base implementation for PSKB and usage in core account with generator wrapper stream handling.

* prune test file

* prune test file

* Converters for transanction and populated transaction.

* Optional signature import in PSKT conversion.

* PSKB wallet cli commands.

* More PSKB wallet cli commands for custom script locks.

* Serialization test case

* Reviews patches, cli script debug command added.

* Doc about fee per transaction for script unlocking UTXOS

* Parameter changed to priority_fee_sompi_per_transaction, revert function renaming.

* Error handling

* Adding type conversion. (kaspanet#76)

* fmt

* fix WASM32 PSKT function names

* refactor PSKB as a type wrapper + update serialization (kaspanet#86)

* Cleanup of unused JSON test file for PSKB and comments (kaspanet#87)

* Remove PSKB json test file.

* Remove/change old PSKB comments and commented out inclusions.

---------

Co-authored-by: 1bananagirl <[email protected]>
  • Loading branch information
aspect and 1bananagirl authored Aug 4, 2024
1 parent 02f2566 commit 7702b9f
Show file tree
Hide file tree
Showing 38 changed files with 2,171 additions and 519 deletions.
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ downcast.workspace = true
faster-hex.workspace = true
futures.workspace = true
js-sys.workspace = true
hex.workspace = true
kaspa-addresses.workspace = true
kaspa-bip32.workspace = true
kaspa-consensus-core.workspace = true
Expand All @@ -43,6 +44,7 @@ kaspa-rpc-core.workspace = true
kaspa-utils.workspace = true
kaspa-wallet-core.workspace = true
kaspa-wallet-keys.workspace = true
kaspa-wallet-pskt.workspace = true
kaspa-wrpc-client.workspace = true
nw-sys.workspace = true
pad.workspace = true
Expand Down
6 changes: 6 additions & 0 deletions cli/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ pub enum Error {

#[error(transparent)]
KaspaWalletKeys(#[from] kaspa_wallet_keys::error::Error),

#[error(transparent)]
PskbLockScriptSigError(#[from] kaspa_wallet_pskt::error::Error),

#[error("To hex serialization error")]
PskbSerializeToHexError,
}

impl Error {
Expand Down
3 changes: 2 additions & 1 deletion cli/src/modules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub mod network;
pub mod node;
pub mod open;
pub mod ping;
pub mod pskb;
pub mod reload;
pub mod rpc;
pub mod select;
Expand Down Expand Up @@ -57,7 +58,7 @@ pub fn register_handlers(cli: &Arc<KaspaCli>) -> Result<()> {
cli.handlers(),
[
account, address, close, connect, details, disconnect, estimate, exit, export, guide, help, history, rpc, list, miner,
message, monitor, mute, network, node, open, ping, reload, select, send, server, settings, sweep, track, transfer,
message, monitor, mute, network, node, open, ping, pskb, reload, select, send, server, settings, sweep, track, transfer,
wallet,
// halt,
// theme, start, stop
Expand Down
259 changes: 259 additions & 0 deletions cli/src/modules/pskb.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
#![allow(unused_imports)]

use crate::imports::*;
use kaspa_consensus_core::tx::{TransactionOutpoint, UtxoEntry};
use kaspa_wallet_core::account::pskb::finalize_pskt_one_or_more_sig_and_redeem_script;
use kaspa_wallet_pskt::prelude::{lock_script_sig_templating, script_sig_to_address, unlock_utxos_as_pskb, Bundle, Signer, PSKT};

#[derive(Default, Handler)]
#[help("Send a Kaspa transaction to a public address")]
pub struct Pskb;

impl Pskb {
async fn main(self: Arc<Self>, ctx: &Arc<dyn Context>, mut argv: Vec<String>, _cmd: &str) -> Result<()> {
let ctx = ctx.clone().downcast_arc::<KaspaCli>()?;

if !ctx.wallet().is_open() {
return Err(Error::WalletIsNotOpen);
}

let account = ctx.wallet().account()?;

if argv.is_empty() {
return self.display_help(ctx, argv).await;
}

let action = argv.remove(0);

match action.as_str() {
"create" => {
if argv.len() < 2 || argv.len() > 3 {
return self.display_help(ctx, argv).await;
} else {
let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(None).await?;
let _ = ctx.notifier().show(Notification::Processing).await;

let address = Address::try_from(argv.first().unwrap().as_str())?;
let amount_sompi = try_parse_required_nonzero_kaspa_as_sompi_u64(argv.get(1))?;
let outputs = PaymentOutputs::from((address, amount_sompi));
let priority_fee_sompi = try_parse_optional_kaspa_as_sompi_i64(argv.get(2))?.unwrap_or(0);
let abortable = Abortable::default();

let signer = account
.pskb_from_send_generator(
outputs.into(),
priority_fee_sompi.into(),
None,
wallet_secret.clone(),
payment_secret.clone(),
&abortable,
)
.await?;

match signer.serialize() {
Ok(encoded) => tprintln!(ctx, "{encoded}"),
Err(e) => return Err(e.into()),
}
}
}
"script" => {
if argv.len() < 2 || argv.len() > 4 {
return self.display_help(ctx, argv).await;
}

let subcommand = argv.remove(0);
let payload = argv.remove(0);

let receive_address = account.receive_address()?;
let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(None).await?;
let _ = ctx.notifier().show(Notification::Processing).await;

let script_sig = match lock_script_sig_templating(payload.clone(), Some(&receive_address.payload)) {
Ok(value) => value,
Err(e) => {
terrorln!(ctx, "{}", e.to_string());
return Err(e.into());
}
};

let script_p2sh = match script_sig_to_address(&script_sig, ctx.wallet().address_prefix()?) {
Ok(p2sh) => p2sh,
Err(e) => {
terrorln!(ctx, "Error generating script address: {}", e.to_string());
return Err(e.into());
}
};

match subcommand.as_str() {
"lock" => {
let amount_sompi = try_parse_required_nonzero_kaspa_as_sompi_u64(argv.first())?;
let outputs = PaymentOutputs::from((script_p2sh, amount_sompi));
let priority_fee_sompi = try_parse_optional_kaspa_as_sompi_i64(argv.get(1))?.unwrap_or(0);
let abortable = Abortable::default();

let signer = account
.pskb_from_send_generator(
outputs.into(),
priority_fee_sompi.into(),
None,
wallet_secret.clone(),
payment_secret.clone(),
&abortable,
)
.await?;

match signer.serialize() {
Ok(encoded) => tprintln!(ctx, "{encoded}"),
Err(e) => return Err(e.into()),
}
}
"unlock" => {
if argv.len() != 1 {
return self.display_help(ctx, argv).await;
}

// Get locked UTXO set.
let spend_utxos: Vec<kaspa_rpc_core::RpcUtxosByAddressesEntry> =
ctx.wallet().rpc_api().get_utxos_by_addresses(vec![script_p2sh.clone()]).await?;
let priority_fee_sompi = try_parse_optional_kaspa_as_sompi_i64(argv.first())?.unwrap_or(0) as u64;

if spend_utxos.is_empty() {
twarnln!(ctx, "No locked UTXO set found.");
return Ok(());
}

let references: Vec<(UtxoEntry, TransactionOutpoint)> =
spend_utxos.iter().map(|entry| (entry.utxo_entry.clone().into(), entry.outpoint.into())).collect();

let total_locked_sompi: u64 = spend_utxos.iter().map(|entry| entry.utxo_entry.amount).sum();

tprintln!(
ctx,
"{} locked UTXO{} found with total amount of {} KAS",
spend_utxos.len(),
if spend_utxos.len() == 1 { "" } else { "s" },
sompi_to_kaspa(total_locked_sompi)
);

// Sweep UTXO set.
match unlock_utxos_as_pskb(references, &receive_address, script_sig, priority_fee_sompi as u64) {
Ok(pskb) => {
let pskb_hex = pskb.serialize()?;
tprintln!(ctx, "{pskb_hex}");
}
Err(e) => tprintln!(ctx, "Error generating unlock PSKB: {}", e.to_string()),
}
}
"sign" => {
let pskb = Self::parse_input_pskb(argv.first().unwrap().as_str())?;

// Sign PSKB using the account's receiver address.
match account.pskb_sign(&pskb, wallet_secret.clone(), payment_secret.clone(), Some(&receive_address)).await {
Ok(signed_pskb) => {
let pskb_pack = String::try_from(signed_pskb)?;
tprintln!(ctx, "{pskb_pack}");
}
Err(e) => terrorln!(ctx, "{}", e.to_string()),
}
}
"address" => {
tprintln!(ctx, "\r\nP2SH address: {}", script_p2sh);
}
v => {
terrorln!(ctx, "unknown command: '{v}'\r\n");
return self.display_help(ctx, argv).await;
}
}
}
"sign" => {
if argv.len() != 1 {
return self.display_help(ctx, argv).await;
} else {
let (wallet_secret, payment_secret) = ctx.ask_wallet_secret(None).await?;
let pskb = Self::parse_input_pskb(argv.first().unwrap().as_str())?;

match account.pskb_sign(&pskb, wallet_secret.clone(), payment_secret.clone(), None).await {
Ok(signed_pskb) => {
let pskb_pack = String::try_from(signed_pskb)?;
tprintln!(ctx, "{pskb_pack}");
}
Err(e) => terrorln!(ctx, "{}", e.to_string()),
}
}
}
"send" => {
if argv.len() != 1 {
return self.display_help(ctx, argv).await;
} else {
let pskb = Self::parse_input_pskb(argv.first().unwrap().as_str())?;

match account.pskb_broadcast(&pskb).await {
Ok(sent) => tprintln!(ctx, "Sent transactions {:?}", sent),
Err(e) => terrorln!(ctx, "Send error {:?}", e),
}
}
}
"debug" => {
let pskb = Self::parse_input_pskb(argv.first().unwrap().as_str())?;

// Debug bundle view.
tprintln!(ctx, "{:?}", pskb);

match pskb.as_ref().first() {
Some(bundle_inner) => {
let pskt: PSKT<Signer> = PSKT::<Signer>::from(bundle_inner.to_owned());
let mut fin = pskt.finalizer();

fin = finalize_pskt_one_or_more_sig_and_redeem_script(fin).expect("Finalized PSKT");

// Verify if extraction is possible.
match fin.extractor() {
Ok(ex) => match ex.extract_tx() {
Ok(_) => tprintln!(
ctx,
"Transaction extracted successfuly meaning it is finalized with a valid script signature."
),
Err(e) => terrorln!(ctx, "Transaction extraction error: {}", e.to_string()),
},
Err(_) => twarnln!(ctx, "PSKB not finalized"),
}
}
None => {
twarnln!(ctx, "Debugging an empty PSKB");
}
}
}
v => {
tprintln!(ctx, "unknown command: '{v}'\r\n");
return self.display_help(ctx, argv).await;
}
}
Ok(())
}

fn parse_input_pskb(input: &str) -> Result<Bundle> {
match Bundle::try_from(input) {
Ok(bundle) => Ok(bundle),
Err(e) => Err(Error::custom(format!("Error while parsing input PSKB {}", e))),
}
}

async fn display_help(self: Arc<Self>, ctx: Arc<KaspaCli>, _argv: Vec<String>) -> Result<()> {
ctx.term().help(
&[
("pskb create <address> <amount> <priority fee>", "Create a PSKB from single send transaction"),
("pskb sign <pskb>", "Sign given PSKB"),
("pskb send <pskb>", "Broadcast bundled transactions"),
("pskb debug <payload>", "Print PSKB debug view"),
("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}}"),
("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}}"),
("pskb script sign <pskb>", "Sign all PSKB's P2SH locked inputs"),
("pskb script sign <pskb>", "Sign all PSKB's P2SH locked inputs"),
("pskb script address <pskb>", "Prints P2SH address"),
],
None,
)?;

Ok(())
}
}
2 changes: 1 addition & 1 deletion cli/src/modules/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ impl Send {
.await?;

tprintln!(ctx, "Send - {summary}");
// tprintln!(ctx, "\nSending {} KAS to {address}, tx ids:", sompi_to_kaspa_string(amount_sompi));
tprintln!(ctx, "\nSending {} KAS to {address}, tx ids:", sompi_to_kaspa_string(amount_sompi));
// tprintln!(ctx, "{}\n", ids.into_iter().map(|a| a.to_string()).collect::<Vec<_>>().join("\n"));

Ok(())
Expand Down
Loading

0 comments on commit 7702b9f

Please sign in to comment.