From e4476cd6497f4a4d650cd1dedb7a88bbbe0da05b Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 7 Mar 2024 20:36:00 -0800 Subject: [PATCH 1/3] Add sat flow command --- src/subcommand.rs | 4 ++ src/subcommand/flow.rs | 91 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 src/subcommand/flow.rs diff --git a/src/subcommand.rs b/src/subcommand.rs index 2da87735db..4078bc16b7 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -5,6 +5,7 @@ pub mod decode; pub mod env; pub mod epochs; pub mod find; +mod flow; pub mod index; pub mod list; pub mod parse; @@ -29,6 +30,8 @@ pub(crate) enum Subcommand { Epochs, #[command(about = "Find a satoshi's current location")] Find(find::Find), + #[command(about = "Display PSBT sat flow")] + Flow(flow::Flow), #[command(subcommand, about = "Index commands")] Index(index::IndexSubcommand), #[command(about = "List the satoshis in an output")] @@ -61,6 +64,7 @@ impl Subcommand { Self::Env(env) => env.run(), Self::Epochs => epochs::run(), Self::Find(find) => find.run(settings), + Self::Flow(flow) => flow.run(settings), Self::Index(index) => index.run(settings), Self::List(list) => list.run(settings), Self::Parse(parse) => parse.run(), diff --git a/src/subcommand/flow.rs b/src/subcommand/flow.rs new file mode 100644 index 0000000000..48ca012143 --- /dev/null +++ b/src/subcommand/flow.rs @@ -0,0 +1,91 @@ +use {super::*, base64::Engine, bitcoin::psbt::Psbt}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Output { + inputs: Vec>, + outputs: Vec>, + fee: Vec, +} + +#[derive(Debug, Parser)] +pub(crate) struct Flow { + psbt: PathBuf, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +struct Range { + start: u64, + end: u64, + name: String, +} + +impl From<(u64, u64)> for Range { + fn from((start, end): (u64, u64)) -> Self { + Self { + start, + end, + name: Sat(start).name(), + } + } +} + +impl Flow { + pub(crate) fn run(self, settings: Settings) -> SubcommandResult { + let psbt = fs::read(self.psbt).unwrap(); + + let psbt = Psbt::deserialize(&psbt).unwrap(); + + let index = Index::open(&settings)?; + + index.update()?; + + let mut inputs = Vec::new(); + + for input in psbt.unsigned_tx.input { + inputs.push(index.list(input.previous_output)?.unwrap()); + } + + let mut fee: VecDeque<(u64, u64)> = inputs.iter().flatten().copied().collect(); + + let mut outputs = Vec::new(); + + for output in psbt.unsigned_tx.output { + let mut ranges = Vec::new(); + + let mut deficit = output.value; + + while deficit > 0 { + let (start, end) = fee + .pop_front() + .context("inputs insufficient to pay for outputs")?; + + let size = end - start; + + let (start, end) = if size <= deficit { + (start, end) + } else { + fee.push_front((start + deficit, end)); + (start, start + deficit) + }; + + ranges.push((start, end)); + + deficit -= end - start; + } + + outputs.push(ranges); + } + + Ok(Some(Box::new(Output { + fee: fee.into_iter().map(|range| range.into()).collect(), + inputs: inputs + .into_iter() + .map(|ranges| ranges.into_iter().map(|range| range.into()).collect()) + .collect(), + outputs: outputs + .into_iter() + .map(|ranges| ranges.into_iter().map(|range| range.into()).collect()) + .collect(), + }))) + } +} From 35e7dcdde071380af4409a93ec5072989b6e0b2a Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 8 Mar 2024 21:44:00 -0800 Subject: [PATCH 2/3] Check input vs output value --- src/subcommand/flow.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/subcommand/flow.rs b/src/subcommand/flow.rs index 48ca012143..34db7d70bf 100644 --- a/src/subcommand/flow.rs +++ b/src/subcommand/flow.rs @@ -31,9 +31,15 @@ impl From<(u64, u64)> for Range { impl Flow { pub(crate) fn run(self, settings: Settings) -> SubcommandResult { - let psbt = fs::read(self.psbt).unwrap(); + let base64 = fs::read(self.psbt).unwrap(); - let psbt = Psbt::deserialize(&psbt).unwrap(); + let s = std::str::from_utf8(&base64).unwrap(); + + let bytes = &base64::engine::general_purpose::STANDARD + .decode(s.trim()) + .unwrap(); + + let psbt = Psbt::deserialize(bytes).unwrap(); let index = Index::open(&settings)?; @@ -47,6 +53,20 @@ impl Flow { let mut fee: VecDeque<(u64, u64)> = inputs.iter().flatten().copied().collect(); + let input_value = fee.iter().map(|(start, end)| end - start).sum::(); + + let output_value = psbt + .unsigned_tx + .output + .iter() + .map(|input| input.value) + .sum::(); + + ensure!( + input_value >= output_value, + "insufficient inputs to pay for outputs: {input_value} < {output_value}", + ); + let mut outputs = Vec::new(); for output in psbt.unsigned_tx.output { From 7a86b444c3529737d92c04c7caa6a15e6dce0fad Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 8 Mar 2024 22:08:07 -0800 Subject: [PATCH 3/3] Support binary psbts --- src/subcommand/flow.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/subcommand/flow.rs b/src/subcommand/flow.rs index 34db7d70bf..0e530150d6 100644 --- a/src/subcommand/flow.rs +++ b/src/subcommand/flow.rs @@ -9,6 +9,8 @@ pub struct Output { #[derive(Debug, Parser)] pub(crate) struct Flow { + #[arg(long)] + binary: bool, psbt: PathBuf, } @@ -31,15 +33,15 @@ impl From<(u64, u64)> for Range { impl Flow { pub(crate) fn run(self, settings: Settings) -> SubcommandResult { - let base64 = fs::read(self.psbt).unwrap(); - - let s = std::str::from_utf8(&base64).unwrap(); - - let bytes = &base64::engine::general_purpose::STANDARD - .decode(s.trim()) - .unwrap(); - - let psbt = Psbt::deserialize(bytes).unwrap(); + let psbt = if self.binary { + fs::read(self.psbt).unwrap() + } else { + base64::engine::general_purpose::STANDARD + .decode(fs::read_to_string(self.psbt).unwrap().trim()) + .unwrap() + }; + + let psbt = Psbt::deserialize(&psbt).unwrap(); let index = Index::open(&settings)?;