dubp_documents/transaction/
v10.rs1mod builder;
19mod input_proof_output;
20mod stringified;
21mod unsigned;
22
23pub use builder::TransactionDocumentV10Builder;
24pub use input_proof_output::{
25 TransactionInputUnlocksV10, TransactionInputV10, TransactionOutputV10,
26};
27pub use stringified::TransactionDocumentV10Stringified;
28pub use unsigned::UnsignedTransactionDocumentV10;
29
30use crate::*;
31
32const TX_V10_MAX_LINES: usize = 100;
33
34#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
38pub struct TransactionDocumentV10 {
39 text: Option<String>,
44
45 currency: String,
47 blockstamp: Blockstamp,
49 locktime: u64,
51 issuers: SmallVec<[ed25519::PublicKey; 1]>,
53 inputs: Vec<TransactionInputV10>,
55 unlocks: Vec<TransactionInputUnlocksV10>,
57 outputs: SmallVec<[TransactionOutputV10; 2]>,
59 comment: String,
61 signatures: SmallVec<[ed25519::Signature; 1]>,
63 hash: Option<Hash>,
65}
66
67impl<'a> TransactionDocumentTrait<'a> for TransactionDocumentV10 {
68 type Address = WalletScriptV10;
69 type Input = TransactionInputV10;
70 type Inputs = &'a [TransactionInputV10];
71 type InputUnlocks = TransactionInputUnlocksV10;
72 type InputsUnlocks = &'a [TransactionInputUnlocksV10];
73 type Output = TransactionOutputV10;
74 type Outputs = &'a [TransactionOutputV10];
75 type PubKey = ed25519::PublicKey;
76 type UnsignedDoc = UnsignedTransactionDocumentV10;
77
78 fn generate_simple_txs(
79 blockstamp: Blockstamp,
80 currency: String,
81 inputs_with_sum: (Vec<Self::Input>, SourceAmount),
82 issuer: Self::PubKey,
83 recipient: WalletScriptV10,
84 user_amount_and_comment: (SourceAmount, String),
85 cash_back_pubkey: Option<Self::PubKey>,
86 ) -> Vec<Self::UnsignedDoc> {
87 let (inputs, inputs_sum) = inputs_with_sum;
88 let (user_amount, user_comment) = user_amount_and_comment;
89 super::v10_gen::TransactionDocV10SimpleGen {
90 blockstamp,
91 currency,
92 inputs,
93 inputs_sum,
94 issuer,
95 recipient,
96 user_amount,
97 user_comment,
98 cash_back_pubkey,
99 }
100 .gen()
101 }
102 fn get_inputs(&'a self) -> Self::Inputs {
103 &self.inputs
104 }
105 fn get_inputs_unlocks(&'a self) -> Self::InputsUnlocks {
106 &self.unlocks
107 }
108 fn get_outputs(&'a self) -> Self::Outputs {
109 &self.outputs
110 }
111 fn verify(&self, expected_currency_opt: Option<&str>) -> Result<(), super::TxVerifyErr> {
112 if let Some(expected_currency) = expected_currency_opt {
113 if self.currency != expected_currency {
114 return Err(super::TxVerifyErr::WrongCurrency {
115 expected: expected_currency.to_owned(),
116 found: self.currency.clone(),
117 });
118 }
119 }
120 if self.inputs.is_empty() {
121 return Err(super::TxVerifyErr::NoInput);
122 }
123 if self.issuers.is_empty() {
124 return Err(super::TxVerifyErr::NoIssuer);
125 }
126 if self.outputs.is_empty() {
127 return Err(super::TxVerifyErr::NoOutput);
128 }
129 if self.inputs.len() != self.unlocks.len() {
130 return Err(super::TxVerifyErr::NotSameAmountOfInputsAndUnlocks(
131 self.inputs.len(),
132 self.unlocks.len(),
133 ));
134 }
135 self.verify_signatures().map_err(super::TxVerifyErr::Sigs)?;
136 let lines_count = self.compact_size_in_lines();
137 if lines_count > TX_V10_MAX_LINES {
138 return Err(super::TxVerifyErr::TooManyLines {
139 found: lines_count,
140 max: TX_V10_MAX_LINES,
141 });
142 }
143 let inputs_amount: SourceAmount = self.inputs.iter().map(|input| input.amount).sum();
144 let outputs_amount: SourceAmount = self.outputs.iter().map(|output| output.amount).sum();
145 if inputs_amount != outputs_amount {
146 return Err(
147 super::TxVerifyErr::NotSameSumOfInputsAmountAndOutputsAmount(
148 inputs_amount,
149 outputs_amount,
150 ),
151 );
152 }
153
154 Ok(())
155 }
156}
157
158impl TransactionDocumentV10 {
159 pub fn compact_size_in_lines(&self) -> usize {
161 let compact_lines = 2 + self.issuers.len() +
163 self.inputs.len() +
164 self.unlocks.len() +
165 self.outputs.len() +
166 self.signatures.len();
167 if !self.comment.is_empty() {
168 compact_lines + 1
169 } else {
170 compact_lines
171 }
172 }
173 pub fn compute_hash(&self) -> Hash {
175 let mut hashing_text = if let Some(ref text) = self.text {
176 text.clone()
177 } else {
178 panic!("Try to compute_hash of tx with None text !")
179 };
180 for sig in &self.signatures {
181 hashing_text.push_str(&sig.to_string());
182 hashing_text.push('\n');
183 }
184 Hash::compute(hashing_text.as_bytes())
185 }
186 pub fn get_hash_opt(&self) -> Option<Hash> {
188 self.hash
189 }
190 pub fn get_hash(&self) -> Hash {
192 if let Some(hash) = self.hash {
193 hash
194 } else {
195 self.compute_hash()
196 }
197 }
198 pub fn recipients_keys(&self) -> Vec<ed25519::PublicKey> {
199 let issuers = self.issuers.iter().copied().collect();
200 let mut pubkeys = BTreeSet::new();
201 for output in &self.outputs {
202 pubkeys.append(&mut output.conditions.script.pubkeys());
203 }
204 pubkeys.difference(&issuers).copied().collect()
205 }
206 pub fn reduce(&mut self) {
209 self.hash = Some(self.compute_hash());
210 self.text = None;
211 for output in &mut self.outputs {
212 output.reduce()
213 }
214 }
215 pub fn verify_comment(comment: &str) -> bool {
217 if comment.is_ascii() {
218 for char_ in comment.chars() {
219 match char_ {
220 c if c.is_ascii_alphanumeric() => continue,
221 '.' | ' ' | '-' | '_' | '\\' | ':' | '/' | ';' | '*' | '[' | ']' | '('
222 | ')' | '?' | '!' | '^' | '+' | '=' | '@' | '&' | '~' | '#' | '{' | '}'
223 | '|' | '<' | '>' | '%' => continue,
224 _ => return false,
225 }
226 }
227 true
228 } else {
229 false
230 }
231 }
232 pub fn writable_on(
234 &self,
235 inputs_written_on: &[u64],
236 inputs_scripts: &[WalletScriptV10],
237 ) -> Result<u64, SourceV10NotUnlockableError> {
238 assert_eq!(self.inputs.len(), inputs_written_on.len());
239 let mut tx_unlockable_on = 0;
240 #[allow(clippy::needless_range_loop)]
241 for i in 0..self.inputs.len() {
242 let source_unlockable_on = SourceV10::unlockable_on(
243 self.issuers.as_ref(),
244 self.unlocks[i].unlocks.as_ref(),
245 inputs_written_on[i],
246 &inputs_scripts[i],
247 )?;
248 if source_unlockable_on > tx_unlockable_on {
249 tx_unlockable_on = source_unlockable_on;
250 }
251 }
252 Ok(tx_unlockable_on)
253 }
254}
255
256impl Document for TransactionDocumentV10 {
257 type PublicKey = ed25519::PublicKey;
258
259 fn version(&self) -> usize {
260 10
261 }
262
263 fn currency(&self) -> &str {
264 &self.currency
265 }
266
267 fn blockstamp(&self) -> Blockstamp {
268 self.blockstamp
269 }
270
271 fn issuers(&self) -> SmallVec<[Self::PublicKey; 1]> {
272 self.issuers.iter().copied().collect()
273 }
274
275 fn signatures(&self) -> SmallVec<[<Self::PublicKey as PublicKey>::Signature; 1]> {
276 self.signatures.iter().copied().collect()
277 }
278
279 fn as_bytes(&self) -> BeefCow<[u8]> {
280 BeefCow::borrowed(self.as_text().as_bytes())
281 }
282}
283
284impl CompactTextDocument for TransactionDocumentV10 {
285 fn as_compact_text(&self) -> String {
286 let mut issuers_str = String::from("");
287 for issuer in &self.issuers {
288 issuers_str.push('\n');
289 issuers_str.push_str(&issuer.to_string());
290 }
291 let mut inputs_str = String::from("");
292 for input in &self.inputs {
293 inputs_str.push('\n');
294 inputs_str.push_str(&input.to_string());
295 }
296 let mut unlocks_str = String::from("");
297 for unlock in &self.unlocks {
298 unlocks_str.push('\n');
299 unlocks_str.push_str(&unlock.to_string());
300 }
301 let mut outputs_str = String::from("");
302 for output in &self.outputs {
303 outputs_str.push('\n');
304 outputs_str.push_str(&output.to_string());
305 }
306 let comment_str = if self.comment.is_empty() {
307 String::with_capacity(0)
308 } else {
309 format!("{}\n", self.comment)
310 };
311 let mut signatures_str = String::from("");
312 for sig in &self.signatures {
313 signatures_str.push_str(&sig.to_string());
314 signatures_str.push('\n');
315 }
316 signatures_str.pop();
318 format!(
319 "TX:10:{issuers_count}:{inputs_count}:{unlocks_count}:{outputs_count}:{has_comment}:{locktime}
320{blockstamp}{issuers}{inputs}{unlocks}{outputs}\n{comment}{signatures}",
321 issuers_count = self.issuers.len(),
322 inputs_count = self.inputs.len(),
323 unlocks_count = self.unlocks.len(),
324 outputs_count = self.outputs.len(),
325 has_comment = if self.comment.is_empty() { 0 } else { 1 },
326 locktime = self.locktime,
327 blockstamp = self.blockstamp,
328 issuers = issuers_str,
329 inputs = inputs_str,
330 unlocks = unlocks_str,
331 outputs = outputs_str,
332 comment = comment_str,
333 signatures = signatures_str,
334 )
335 }
336}
337
338impl TextDocument for TransactionDocumentV10 {
339 type CompactTextDocument_ = TransactionDocumentV10;
340
341 fn as_text(&self) -> &str {
342 if let Some(ref text) = self.text {
343 text
344 } else {
345 panic!("Try to get text of tx with None text !")
346 }
347 }
348
349 fn to_compact_document(&self) -> Cow<Self::CompactTextDocument_> {
350 Cow::Borrowed(self)
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::super::tests::tx_output_v10;
357 use super::*;
358 use smallvec::smallvec;
359 use std::str::FromStr;
360 use unwrap::unwrap;
361
362 #[test]
363 fn generate_real_document() {
364 let keypair = ed25519::KeyPairFromSeed32Generator::generate(unwrap!(
365 Seed32::from_base58("DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV"),
366 "Fail to parse Seed32"
367 ));
368 let pubkey = keypair.public_key();
369 let signator = keypair.generate_signator();
370
371 let sig = unwrap!(ed25519::Signature::from_base64(
372 "cq86RugQlqAEyS8zFkB9o0PlWPSb+a6D/MEnLe8j+okyFYf/WzI6pFiBkQ9PSOVn5I0dwzVXg7Q4N1apMWeGAg==",
373 ), "Fail to parse Signature");
374
375 let block = unwrap!(
376 Blockstamp::from_str(
377 "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
378 ),
379 "Fail to parse blockstamp"
380 );
381
382 let builder = TransactionDocumentV10Builder {
383 currency: "duniter_unit_test_currency",
384 blockstamp: block,
385 locktime: 0,
386 issuers: svec![pubkey],
387 inputs: &[TransactionInputV10 {
388 amount: SourceAmount::with_base0(10),
389 id: SourceIdV10::Ud(UdSourceIdV10 {
390 issuer: unwrap!(
391 ed25519::PublicKey::from_base58(
392 "DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV"
393 ),
394 "Fail to parse PublicKey"
395 ),
396 block_number: BlockNumber(0),
397 }),
398 }],
399 unlocks: &[TransactionInputUnlocksV10 {
400 index: 0,
401 unlocks: smallvec![WalletUnlockProofV10::Sig(0)],
402 }],
403 outputs: smallvec![tx_output_v10(
404 10,
405 "FD9wujR7KABw88RyKEGBYRLz8PA6jzVCbcBAsrBXBqSa",
406 )],
407 comment: "test",
408 hash: None,
409 };
410 assert!(builder
411 .clone()
412 .build_with_signature(svec![sig])
413 .verify_signatures()
414 .is_ok());
415 assert!(builder
416 .build_and_sign(vec![signator])
417 .verify_signatures()
418 .is_ok());
419 }
420
421 #[test]
422 fn compute_transaction_hash() {
423 let pubkey = unwrap!(
424 ed25519::PublicKey::from_base58("FEkbc4BfJukSWnCU6Hed6dgwwTuPFTVdgz5LpL4iHr9J"),
425 "Fail to parse PublicKey"
426 );
427
428 let sig = unwrap!(ed25519::Signature::from_base64(
429 "XEwKwKF8AI1gWPT7elR4IN+bW3Qn02Dk15TEgrKtY/S2qfZsNaodsLofqHLI24BBwZ5aadpC88ntmjo/UW9oDQ==",
430 ), "Fail to parse Signature");
431
432 let block = unwrap!(
433 Blockstamp::from_str(
434 "60-00001FE00410FCD5991EDD18AA7DDF15F4C8393A64FA92A1DB1C1CA2E220128D",
435 ),
436 "Fail to parse Blockstamp"
437 );
438
439 let builder = TransactionDocumentV10Builder {
440 currency: "g1",
441 blockstamp: block,
442 locktime: 0,
443 issuers: svec![pubkey],
444 inputs: &[TransactionInputV10 {
445 amount: SourceAmount::with_base0(950),
446 id: SourceIdV10::Utxo(UtxoIdV10 {
447 tx_hash: unwrap!(Hash::from_hex(
448 "2CF1ACD8FE8DC93EE39A1D55881C50D87C55892AE8E4DB71D4EBAB3D412AA8FD"
449 )),
450 output_index: 1,
451 }),
452 }],
453 unlocks: &[TransactionInputUnlocksV10::default()],
454 outputs: smallvec![
455 tx_output_v10(30, "38MEAZN68Pz1DTvT3tqgxx4yQP6snJCQhPqEFxbDk4aE"),
456 tx_output_v10(920, "FEkbc4BfJukSWnCU6Hed6dgwwTuPFTVdgz5LpL4iHr9J"),
457 ],
458 comment: "Pour cesium merci",
459 hash: None,
460 };
461 let tx_doc = builder.build_with_signature(svec![sig]);
462 assert!(tx_doc.verify_signatures().is_ok());
463 assert!(tx_doc.get_hash_opt().is_none());
464 assert_eq!(
465 tx_doc.get_hash(),
466 unwrap!(Hash::from_hex(
467 "876D2430E0B66E2CE4467866D8F923D68896CACD6AA49CDD8BDD0096B834DEF1"
468 ))
469 );
470 }
471 #[test]
472 fn verify_comment() {
473 assert!(TransactionDocumentV10::verify_comment(""));
474 assert!(TransactionDocumentV10::verify_comment("sntsrttfsrt"));
475 assert!(!TransactionDocumentV10::verify_comment("sntsrt,tfsrt"));
476 assert!(TransactionDocumentV10::verify_comment("sntsrt|tfsrt"));
477 }
478
479 #[test]
480 fn tx_sign() -> Result<(), TransactionSignErr> {
481 let signator =
482 ed25519::KeyPairFromSeed32Generator::generate(Seed32::default()).generate_signator();
483
484 let tx1 = TransactionDocumentV10Builder {
485 currency: "test",
486 blockstamp: Blockstamp::default(),
487 locktime: 0,
488 issuers: svec![signator.public_key()],
489 inputs: &[],
490 unlocks: &[],
491 outputs: SmallVec::new(),
492 comment: "",
493 hash: None,
494 }
495 .build_unsigned();
496
497 if let SignedOrUnsignedDocument::Signed(tx1) = tx1.sign(&signator)? {
498 assert!(tx1.verify_signatures().is_ok());
499 Ok(())
500 } else {
501 panic!("tx1.sign should return SignedOrUnsignedDocument::Signed(_)");
502 }
503 }
504}