// Copyright (C) 2018 The Duniter Project Developers. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . //! Wrappers around Transaction documents. use std::ops::Deref; use duniter_crypto::keys::{PublicKey, ed25519}; use regex::Regex; use regex::RegexBuilder; use Blockstamp; use blockchain::{BlockchainProtocol, Document, DocumentBuilder, IntoSpecializedDocument}; use blockchain::v10::documents::{StandardTextDocumentParser, TextDocument, TextDocumentBuilder, V10Document, V10DocumentParsingError}; lazy_static! { static ref TRANSACTION_REGEX_SIZE: &'static usize = &40_000_000; static ref TRANSACTION_REGEX_BUILDER: &'static str = r"^Blockstamp: (?P[0-9]+-[0-9A-F]{64})\nLocktime: (?P[0-9]+)\nIssuers:(?P(?:\n[1-9A-Za-z][^OIl]{43,44})+)Inputs:\n(?P([0-9A-Za-z:]+\n)+)Unlocks:\n(?P([0-9]+:(SIG\([0-9]+\) ?|XHX\(\w+\) ?)+\n)+)Outputs:\n(?P([0-9A-Za-z()&|: ]+\n)+)Comment: (?P[\\\w:/;*\[\]()?!^+=@&~#{}|<>%. -]{0,255})\n$"; static ref ISSUER_REGEX: Regex = Regex::new("(?P[1-9A-Za-z][^OIl]{43,44})\n").unwrap(); static ref D_INPUT_REGEX: Regex = Regex::new( "^(?P[1-9][0-9]*):(?P[0-9]+):D:(?P[1-9A-Za-z][^OIl]{43,44}):(?P[0-9]+)$" ).unwrap(); static ref T_INPUT_REGEX: Regex = Regex::new( "^(?P[1-9][0-9]*):(?P[0-9]+):T:(?P[0-9A-F]{64}):(?P[0-9]+)$" ).unwrap(); static ref UNLOCKS_REGEX: Regex = Regex::new( r"^(?P[0-9]+):(?P(SIG\([0-9]+\) ?|XHX\(\w+\) ?)+)$" ).unwrap(); static ref UNLOCK_SIG_REGEX: Regex = Regex::new(r"^SIG\((?P[0-9]+)\)$").unwrap(); static ref UNLOCK_XHX_REGEX: Regex = Regex::new(r"^XHX\((?P\w+)\)$").unwrap(); static ref OUTPUT_COND_SIG_REGEX: Regex = Regex::new(r"^SIG\((?P[1-9A-Za-z][^OIl]{43,44})\)$").unwrap(); static ref OUTPUT_COND_XHX_REGEX: Regex = Regex::new(r"^XHX\((?P[0-9A-F]{64})\)$").unwrap(); static ref OUTPUT_COND_CLTV_REGEX: Regex = Regex::new(r"^CLTV\((?P[0-9]+)\)$").unwrap(); static ref OUTPUT_COND_CSV_REGEX: Regex = Regex::new(r"^CSV\((?P[0-9]+)\)$").unwrap(); static ref OUPUT_CONDS_BRAKETS: Regex = Regex::new(r"^\((?P[0-9A-Za-z()&| ]+)\)$").unwrap(); static ref OUPUT_CONDS_AND: Regex = Regex::new(r"^(?P[0-9A-Za-z()&| ]+) && (?P[0-9A-Za-z()&| ]+)$").unwrap(); static ref OUPUT_CONDS_OR: Regex = Regex::new(r"^(?P[0-9A-Za-z()&| ]+) \|\| (?P[0-9A-Za-z()&| ]+)$").unwrap(); static ref OUTPUT_REGEX: Regex = Regex::new( "^(?P[1-9][0-9]+):(?P[0-9]+):(?P[0-9A-Za-z()&| ]+)$" ).unwrap(); } /// Wrap a transaction input #[derive(Debug, Clone)] pub enum TransactionInput { /// Universal Dividend Input D(isize, usize, ed25519::PublicKey, u64), /// Previous Transaction Input T(isize, usize, String, usize), } impl ToString for TransactionInput { fn to_string(&self) -> String { match *self { TransactionInput::D(amount, base, pubkey, block_number) => { format!("{}:{}:D:{}:{}", amount, base, pubkey, block_number) } TransactionInput::T(amount, base, ref tx_hash, tx_index) => { format!("{}:{}:T:{}:{}", amount, base, tx_hash, tx_index) } } } } impl TransactionInput { fn parse_from_str(source: &str) -> Result { if let Some(caps) = D_INPUT_REGEX.captures(source) { let amount = &caps["amount"]; let base = &caps["base"]; let pubkey = &caps["pubkey"]; let block_number = &caps["block_number"]; Ok(TransactionInput::D( amount.parse().expect("fail to parse input amount !"), base.parse().expect("fail to parse input base !"), ed25519::PublicKey::from_base58(pubkey).expect("fail to parse input pubkey !"), block_number .parse() .expect("fail to parse input block_number !"), )) //Ok(TransactionInput::D(10, 0, PublicKey::from_base58("FD9wujR7KABw88RyKEGBYRLz8PA6jzVCbcBAsrBXBqSa").unwrap(), 0)) } else if let Some(caps) = T_INPUT_REGEX.captures(source) { let amount = &caps["amount"]; let base = &caps["base"]; let tx_hash = &caps["tx_hash"]; let tx_index = &caps["tx_index"]; Ok(TransactionInput::T( amount.parse().expect("fail to parse input amount"), base.parse().expect("fail to parse base amount"), String::from(tx_hash), tx_index.parse().expect("fail to parse tx_index amount"), )) } else { println!("Fail to parse this input = {:?}", source); Err(V10DocumentParsingError::InvalidInnerFormat(String::from( "Transaction2", ))) } } } /// Wrap a transaction unlock proof #[derive(Debug, Clone)] pub enum TransactionUnlockProof { /// Indicates that the signature of the corresponding key is at the bottom of the document Sig(usize), /// Provides the code to unlock the corresponding funds Xhx(String), } impl ToString for TransactionUnlockProof { fn to_string(&self) -> String { match *self { TransactionUnlockProof::Sig(ref index) => format!("SIG({})", index), TransactionUnlockProof::Xhx(ref hash) => format!("XHX({})", hash), } } } impl TransactionUnlockProof { fn parse_from_str(source: &str) -> Result { if let Some(caps) = UNLOCK_SIG_REGEX.captures(source) { let index = &caps["index"]; Ok(TransactionUnlockProof::Sig( index.parse().expect("fail to parse SIG unlock"), )) } else if let Some(caps) = UNLOCK_XHX_REGEX.captures(source) { let code = &caps["code"]; Ok(TransactionUnlockProof::Xhx(String::from(code))) } else { Err(V10DocumentParsingError::InvalidInnerFormat(String::from( "Transaction3", ))) } } } /// Wrap a transaction unlocks input #[derive(Debug, Clone)] pub struct TransactionInputUnlocks { /// Input index pub index: usize, /// List of proof to unlock funds pub unlocks: Vec, } impl ToString for TransactionInputUnlocks { fn to_string(&self) -> String { let mut result: String = format!("{}:", self.index); for unlock in &self.unlocks { result.push_str(&format!("{} ", unlock.to_string())); } let new_size = result.len() - 1; result.truncate(new_size); result } } impl TransactionInputUnlocks { fn parse_from_str(source: &str) -> Result { if let Some(caps) = UNLOCKS_REGEX.captures(source) { let index = &caps["index"].parse().expect("fail to parse unlock index"); let unlocks = &caps["unlocks"]; let unlocks_array: Vec<&str> = unlocks.split(' ').collect(); let mut unlocks = Vec::new(); for unlock in unlocks_array { unlocks.push(TransactionUnlockProof::parse_from_str(unlock)?); } Ok(TransactionInputUnlocks { index: *index, unlocks, }) } else { println!("Fail to parse this unlock = {:?}", source); Err(V10DocumentParsingError::InvalidInnerFormat(String::from( "Transaction4", ))) } } } /// Wrap a transaction ouput condition #[derive(Debug, Clone)] pub enum TransactionOuputCondition { /// The consumption of funds will require a valid signature of the specified key Sig(ed25519::PublicKey), /// The consumption of funds will require to provide a code with the hash indicated Xhx(String), /// Funds may not be consumed until the blockchain reaches the timestamp indicated. Cltv(u64), /// Funds may not be consumed before the duration indicated, starting from the timestamp of the block where the transaction is written. Csv(u64), } impl ToString for TransactionOuputCondition { fn to_string(&self) -> String { match *self { TransactionOuputCondition::Sig(ref pubkey) => format!("SIG({})", pubkey), TransactionOuputCondition::Xhx(ref hash) => format!("XHX({})", hash), TransactionOuputCondition::Cltv(timestamp) => format!("CLTV({})", timestamp), TransactionOuputCondition::Csv(duration) => format!("CSV({})", duration), } } } impl TransactionOuputCondition { fn parse_from_str(source: &str) -> Result { if let Some(caps) = OUTPUT_COND_SIG_REGEX.captures(source) { Ok(TransactionOuputCondition::Sig( ed25519::PublicKey::from_base58(&caps["pubkey"]) .expect("fail to parse SIG TransactionOuputCondition"), )) } else if let Some(caps) = OUTPUT_COND_XHX_REGEX.captures(source) { Ok(TransactionOuputCondition::Xhx(String::from(&caps["hash"]))) } else if let Some(caps) = OUTPUT_COND_CLTV_REGEX.captures(source) { Ok(TransactionOuputCondition::Cltv( caps["timestamp"] .parse() .expect("fail to parse CLTV TransactionOuputCondition"), )) } else if let Some(caps) = OUTPUT_COND_CSV_REGEX.captures(source) { Ok(TransactionOuputCondition::Csv( caps["duration"] .parse() .expect("fail to parse CSV TransactionOuputCondition"), )) } else { Err(V10DocumentParsingError::InvalidInnerFormat( "Transaction5".to_string(), )) } } } /// Wrap a transaction ouput condition group #[derive(Debug, Clone)] pub enum TransactionOuputConditionGroup { /// Single Single(TransactionOuputCondition), /// Brackets Brackets(Box), /// And operator And( Box, Box, ), /// Or operator Or( Box, Box, ), } impl ToString for TransactionOuputConditionGroup { fn to_string(&self) -> String { match *self { TransactionOuputConditionGroup::Single(ref condition) => condition.to_string(), TransactionOuputConditionGroup::Brackets(ref condition_group) => { format!("({})", condition_group.deref().to_string()) } TransactionOuputConditionGroup::And(ref condition_group_1, ref condition_group_2) => { format!( "{} && {}", condition_group_1.deref().to_string(), condition_group_2.deref().to_string() ) } TransactionOuputConditionGroup::Or(ref condition_group_1, ref condition_group_2) => { format!( "{} || {}", condition_group_1.deref().to_string(), condition_group_2.deref().to_string() ) } } } } impl TransactionOuputConditionGroup { fn parse_from_str( conditions: &str, ) -> Result { if let Ok(single_condition) = TransactionOuputCondition::parse_from_str(conditions) { Ok(TransactionOuputConditionGroup::Single(single_condition)) } else if let Some(caps) = OUPUT_CONDS_BRAKETS.captures(conditions) { let inner_conditions = TransactionOuputConditionGroup::parse_from_str(&caps["conditions"])?; Ok(TransactionOuputConditionGroup::Brackets(Box::new( inner_conditions, ))) } else if let Some(caps) = OUPUT_CONDS_AND.captures(conditions) { let conditions_group_1 = TransactionOuputConditionGroup::parse_from_str(&caps["conditions_group_1"])?; let conditions_group_2 = TransactionOuputConditionGroup::parse_from_str(&caps["conditions_group_2"])?; Ok(TransactionOuputConditionGroup::And( Box::new(conditions_group_1), Box::new(conditions_group_2), )) } else if let Some(caps) = OUPUT_CONDS_OR.captures(conditions) { let conditions_group_1 = TransactionOuputConditionGroup::parse_from_str(&caps["conditions_group_1"])?; let conditions_group_2 = TransactionOuputConditionGroup::parse_from_str(&caps["conditions_group_2"])?; Ok(TransactionOuputConditionGroup::Or( Box::new(conditions_group_1), Box::new(conditions_group_2), )) } else { println!("fail to parse this output condition = {:?}", conditions); Err(V10DocumentParsingError::InvalidInnerFormat(String::from( "Transaction6", ))) } } } /// Wrap a transaction ouput #[derive(Debug, Clone)] pub struct TransactionOuput { /// Amount pub amount: isize, /// Base pub base: usize, /// List of conditions for consum this output pub conditions: TransactionOuputConditionGroup, } impl ToString for TransactionOuput { fn to_string(&self) -> String { format!( "{}:{}:{}", self.amount, self.base, self.conditions.to_string() ) } } impl TransactionOuput { fn parse_from_str(source: &str) -> Result { if let Some(caps) = OUTPUT_REGEX.captures(source) { let amount = caps["amount"].parse().expect("fail to parse output amount"); let base = caps["base"].parse().expect("fail to parse base amount"); let conditions = TransactionOuputConditionGroup::parse_from_str(&caps["conditions"])?; Ok(TransactionOuput { conditions, amount, base, }) } else { Err(V10DocumentParsingError::InvalidInnerFormat( "Transaction7".to_string(), )) } } } /// Wrap a Transaction document. /// /// Must be created by parsing a text document or using a builder. #[derive(Debug, Clone)] pub struct TransactionDocument { /// Document as text. /// /// Is used to check signatures, and other values /// must be extracted from it. text: String, /// Currency. currency: String, /// Blockstamp blockstamp: Blockstamp, /// Locktime locktime: u64, /// Document issuer (there should be only one). issuers: Vec, /// Transaction inputs. inputs: Vec, /// Inputs unlocks. unlocks: Vec, /// Transaction outputs. outputs: Vec, /// Transaction comment comment: String, /// Document signature (there should be only one). signatures: Vec, } impl Document for TransactionDocument { type PublicKey = ed25519::PublicKey; type CurrencyType = str; fn version(&self) -> u16 { 10 } fn currency(&self) -> &str { &self.currency } fn issuers(&self) -> &Vec { &self.issuers } fn signatures(&self) -> &Vec { &self.signatures } fn as_bytes(&self) -> &[u8] { self.as_text().as_bytes() } } impl TextDocument for TransactionDocument { fn as_text(&self) -> &str { &self.text } fn generate_compact_text(&self) -> String { let mut issuers_str = String::from(""); for issuer in self.issuers.clone() { issuers_str.push_str("\n"); issuers_str.push_str(&issuer.to_string()); } let mut inputs_str = String::from(""); for input in self.inputs.clone() { inputs_str.push_str("\n"); inputs_str.push_str(&input.to_string()); } let mut unlocks_str = String::from(""); for unlock in self.unlocks.clone() { unlocks_str.push_str("\n"); unlocks_str.push_str(&unlock.to_string()); } let mut outputs_str = String::from(""); for output in self.outputs.clone() { outputs_str.push_str("\n"); outputs_str.push_str(&output.to_string()); } let mut signatures_str = String::from(""); for sig in self.signatures.clone() { signatures_str.push_str("\n"); signatures_str.push_str(&sig.to_string()); } format!( "TX:10:{issuers_count}:{inputs_count}:{unlocks_count}:{outputs_count}:{has_comment}:{locktime} {blockstamp}{issuers}{inputs}{unlocks}{outputs}\n{comment}{signatures}", issuers_count = self.issuers.len(), inputs_count = self.inputs.len(), unlocks_count = self.unlocks.len(), outputs_count = self.outputs.len(), has_comment = if self.comment.is_empty() { 0 } else { 1 }, locktime = self.locktime, blockstamp = self.blockstamp, issuers = issuers_str, inputs = inputs_str, unlocks = unlocks_str, outputs = outputs_str, comment = self.comment, signatures = signatures_str, ) } } impl IntoSpecializedDocument for TransactionDocument { fn into_specialized(self) -> BlockchainProtocol { BlockchainProtocol::V10(Box::new(V10Document::Transaction(Box::new(self)))) } } /// Transaction document builder. #[derive(Debug, Copy, Clone)] pub struct TransactionDocumentBuilder<'a> { /// Document currency. pub currency: &'a str, /// Reference blockstamp. pub blockstamp: &'a Blockstamp, /// Locktime pub locktime: &'a u64, /// Transaction Document issuers. pub issuers: &'a Vec, /// Transaction inputs. pub inputs: &'a Vec, /// Inputs unlocks. pub unlocks: &'a Vec, /// Transaction ouputs. pub outputs: &'a Vec, /// Transaction comment pub comment: &'a str, } impl<'a> TransactionDocumentBuilder<'a> { fn build_with_text_and_sigs( self, text: String, signatures: Vec, ) -> TransactionDocument { TransactionDocument { text, currency: self.currency.to_string(), blockstamp: *self.blockstamp, locktime: *self.locktime, issuers: self.issuers.clone(), inputs: self.inputs.clone(), unlocks: self.unlocks.clone(), outputs: self.outputs.clone(), comment: String::from(self.comment), signatures, } } } impl<'a> DocumentBuilder for TransactionDocumentBuilder<'a> { type Document = TransactionDocument; type PrivateKey = ed25519::PrivateKey; fn build_with_signature(&self, signatures: Vec) -> TransactionDocument { self.build_with_text_and_sigs(self.generate_text(), signatures) } fn build_and_sign(&self, private_keys: Vec) -> TransactionDocument { let (text, signatures) = self.build_signed_text(private_keys); self.build_with_text_and_sigs(text, signatures) } } impl<'a> TextDocumentBuilder for TransactionDocumentBuilder<'a> { fn generate_text(&self) -> String { let mut issuers_string: String = "".to_owned(); let mut inputs_string: String = "".to_owned(); let mut unlocks_string: String = "".to_owned(); let mut outputs_string: String = "".to_owned(); for issuer in self.issuers { issuers_string.push_str(&format!("{}\n", issuer.to_string())) } for input in self.inputs { inputs_string.push_str(&format!("{}\n", input.to_string())) } for unlock in self.unlocks { unlocks_string.push_str(&format!("{}\n", unlock.to_string())) } for output in self.outputs { outputs_string.push_str(&format!("{}\n", output.to_string())) } format!( "Version: 10 Type: Transaction Currency: {currency} Blockstamp: {blockstamp} Locktime: {locktime} Issuers: {issuers}Inputs: {inputs}Unlocks: {unlocks}Outputs: {outputs}Comment: {comment} ", currency = self.currency, blockstamp = self.blockstamp, locktime = self.locktime, issuers = issuers_string, inputs = inputs_string, unlocks = unlocks_string, outputs = outputs_string, comment = self.comment, ) } } /// Transaction document parser #[derive(Debug, Clone, Copy)] pub struct TransactionDocumentParser; impl StandardTextDocumentParser for TransactionDocumentParser { fn parse_standard( doc: &str, body: &str, currency: &str, signatures: Vec, ) -> Result { let tx_regex: Regex = RegexBuilder::new(&TRANSACTION_REGEX_BUILDER) .size_limit(**TRANSACTION_REGEX_SIZE) .build() .expect("fail to build TRANSACTION_REGEX !"); if let Some(caps) = tx_regex.captures(body) { let blockstamp = Blockstamp::from_string(&caps["blockstamp"]).expect("fail to parse blockstamp"); let locktime = caps["locktime"].parse().expect("fail to parse locktime"); let issuers_str = &caps["issuers"]; let inputs = &caps["inputs"]; let unlocks = &caps["unlocks"]; let outputs = &caps["outputs"]; let comment = String::from(&caps["comment"]); let mut issuers = Vec::new(); for caps in ISSUER_REGEX.captures_iter(issuers_str) { issuers.push( ed25519::PublicKey::from_base58(&caps["issuer"]).expect("fail to parse issuer"), ); } let inputs_array: Vec<&str> = inputs.split('\n').collect(); let mut inputs = Vec::new(); for input in inputs_array { if !input.is_empty() { inputs.push(TransactionInput::parse_from_str(input)?); } } let unlocks_array: Vec<&str> = unlocks.split('\n').collect(); let mut unlocks = Vec::new(); for unlock in unlocks_array { if !unlock.is_empty() { unlocks.push(TransactionInputUnlocks::parse_from_str(unlock)?); } } let outputs_array: Vec<&str> = outputs.split('\n').collect(); let mut outputs = Vec::new(); for output in outputs_array { if !output.is_empty() { outputs.push(TransactionOuput::parse_from_str(output)?); } } Ok(V10Document::Transaction(Box::new(TransactionDocument { text: doc.to_owned(), currency: currency.to_owned(), blockstamp, locktime, issuers, inputs, unlocks, outputs, comment, signatures, }))) } else { Err(V10DocumentParsingError::InvalidInnerFormat( "Transaction1".to_string(), )) } } } #[cfg(test)] mod tests { use super::*; use duniter_crypto::keys::{PrivateKey, PublicKey, Signature}; use blockchain::{Document, VerificationResult}; #[test] fn generate_real_document() { let pubkey = ed25519::PublicKey::from_base58( "DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV", ).unwrap(); let prikey = ed25519::PrivateKey::from_base58( "468Q1XtTq7h84NorZdWBZFJrGkB18CbmbHr9tkp9snt5G\ iERP7ySs3wM8myLccbAAGejgMRC9rqnXuW3iAfZACm7", ).unwrap(); let sig = ed25519::Signature::from_base64( "pRQeKlzCsvPNmYAAkEP5jPPQO1RwrtFMRfCajEfkkrG0UQE0DhoTkxG3Zs2JFmvAFLw67pn1V5NQ08zsSfJkBg==", ).unwrap(); let block = Blockstamp::from_string( "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", ).unwrap(); let builder = TransactionDocumentBuilder { currency: "duniter_unit_test_currency", blockstamp: &block, locktime: &0, issuers: &vec![pubkey], inputs: &vec![ TransactionInput::parse_from_str( "10:0:D:DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV:0", ).expect("fail to parse input !"), ], unlocks: &vec![ TransactionInputUnlocks::parse_from_str("0:SIG(0)") .expect("fail to parse unlock !"), ], outputs: &vec![ TransactionOuput::parse_from_str( "10:0:SIG(FD9wujR7KABw88RyKEGBYRLz8PA6jzVCbcBAsrBXBqSa)", ).expect("fail to parse output !"), ], comment: "test", }; println!( "Signature = {:?}", builder.build_and_sign(vec![prikey]).signatures() ); assert_eq!( builder.build_with_signature(vec![sig]).verify_signatures(), VerificationResult::Valid() ); assert_eq!( builder.build_and_sign(vec![prikey]).verify_signatures(), VerificationResult::Valid() ); } #[test] fn transaction_standard_regex() { let tx_regex: Regex = RegexBuilder::new(&TRANSACTION_REGEX_BUILDER) .size_limit(**TRANSACTION_REGEX_SIZE) .build() .expect("fail to build TRANSACTION_REGEX !"); assert!(tx_regex.is_match( "Blockstamp: 204-00003E2B8A35370BA5A7064598F628A62D4E9EC1936BE8651CE9A85F2E06981B Locktime: 0 Issuers: HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY CYYjHsNyg3HMRMpTHqCJAN9McjH5BwFLmDKGV3PmCuKp FD9wujR7KABw88RyKEGBYRLz8PA6jzVCbcBAsrBXBqSa Inputs: 40:2:T:6991C993631BED4733972ED7538E41CCC33660F554E3C51963E2A0AC4D6453D3:2 70:2:T:3A09A20E9014110FD224889F13357BAB4EC78A72F95CA03394D8CCA2936A7435:8 20:2:D:HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY:46 70:2:T:A0D9B4CDC113ECE1145C5525873821398890AE842F4B318BD076095A23E70956:3 20:2:T:67F2045B5318777CC52CD38B424F3E40DDA823FA0364625F124BABE0030E7B5B:5 15:2:D:FD9wujR7KABw88RyKEGBYRLz8PA6jzVCbcBAsrBXBqSa:46 Unlocks: 0:SIG(0) 1:XHX(7665798292) 2:SIG(0) 3:SIG(0) SIG(2) 4:SIG(0) SIG(1) SIG(2) 5:SIG(2) Outputs: 120:2:SIG(BYfWYFrsyjpvpFysgu19rGK3VHBkz4MqmQbNyEuVU64g) 146:2:SIG(DSz4rgncXCytsUMW2JU2yhLquZECD2XpEkpP9gG5HyAx) 49:2:(SIG(6DyGr5LFtFmbaJYRvcs9WmBsr4cbJbJ1EV9zBbqG7A6i) || XHX(3EB4702F2AC2FD3FA4FDC46A4FC05AE8CDEE1A85)) Comment: -----@@@----- (why not this comment?) " )); } #[test] fn parse_transaction_document() { let doc = "Version: 10 Type: Transaction Currency: duniter_unit_test_currency Blockstamp: 204-00003E2B8A35370BA5A7064598F628A62D4E9EC1936BE8651CE9A85F2E06981B Locktime: 0 Issuers: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV 4tNQ7d9pj2Da5wUVoW9mFn7JjuPoowF977au8DdhEjVR FD9wujR7KABw88RyKEGBYRLz8PA6jzVCbcBAsrBXBqSa Inputs: 40:2:T:6991C993631BED4733972ED7538E41CCC33660F554E3C51963E2A0AC4D6453D3:2 70:2:T:3A09A20E9014110FD224889F13357BAB4EC78A72F95CA03394D8CCA2936A7435:8 20:2:D:DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV:46 70:2:T:A0D9B4CDC113ECE1145C5525873821398890AE842F4B318BD076095A23E70956:3 20:2:T:67F2045B5318777CC52CD38B424F3E40DDA823FA0364625F124BABE0030E7B5B:5 15:2:D:FD9wujR7KABw88RyKEGBYRLz8PA6jzVCbcBAsrBXBqSa:46 Unlocks: 0:SIG(0) 1:XHX(7665798292) 2:SIG(0) 3:SIG(0) SIG(2) 4:SIG(0) SIG(1) SIG(2) 5:SIG(2) Outputs: 120:2:SIG(BYfWYFrsyjpvpFysgu19rGK3VHBkz4MqmQbNyEuVU64g) 146:2:SIG(DSz4rgncXCytsUMW2JU2yhLquZECD2XpEkpP9gG5HyAx) 49:2:(SIG(6DyGr5LFtFmbaJYRvcs9WmBsr4cbJbJ1EV9zBbqG7A6i) || XHX(3EB4702F2AC2FD3FA4FDC46A4FC05AE8CDEE1A85F2AC2FD3FA4FDC46A4FC01CA)) Comment: -----@@@----- (why not this comment?) "; let body = "Blockstamp: 204-00003E2B8A35370BA5A7064598F628A62D4E9EC1936BE8651CE9A85F2E06981B Locktime: 0 Issuers: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV 4tNQ7d9pj2Da5wUVoW9mFn7JjuPoowF977au8DdhEjVR FD9wujR7KABw88RyKEGBYRLz8PA6jzVCbcBAsrBXBqSa Inputs: 40:2:T:6991C993631BED4733972ED7538E41CCC33660F554E3C51963E2A0AC4D6453D3:2 70:2:T:3A09A20E9014110FD224889F13357BAB4EC78A72F95CA03394D8CCA2936A7435:8 20:2:D:DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV:46 70:2:T:A0D9B4CDC113ECE1145C5525873821398890AE842F4B318BD076095A23E70956:3 20:2:T:67F2045B5318777CC52CD38B424F3E40DDA823FA0364625F124BABE0030E7B5B:5 15:2:D:FD9wujR7KABw88RyKEGBYRLz8PA6jzVCbcBAsrBXBqSa:46 Unlocks: 0:SIG(0) 1:XHX(7665798292) 2:SIG(0) 3:SIG(0) SIG(2) 4:SIG(0) SIG(1) SIG(2) 5:SIG(2) Outputs: 120:2:SIG(BYfWYFrsyjpvpFysgu19rGK3VHBkz4MqmQbNyEuVU64g) 146:2:SIG(DSz4rgncXCytsUMW2JU2yhLquZECD2XpEkpP9gG5HyAx) 49:2:(SIG(6DyGr5LFtFmbaJYRvcs9WmBsr4cbJbJ1EV9zBbqG7A6i) || XHX(3EB4702F2AC2FD3FA4FDC46A4FC05AE8CDEE1A85F2AC2FD3FA4FDC46A4FC01CA)) Comment: -----@@@----- (why not this comment?) "; let currency = "duniter_unit_test_currency"; let signatures = vec![ Signature::from_base64( "kL59C1izKjcRN429AlKdshwhWbasvyL7sthI757zm1DfZTdTIctDWlKbYeG/tS7QyAgI3gcfrTHPhu1E1lKCBw==" ).expect("fail to parse test signature"), Signature::from_base64( "e3LpgB2RZ/E/BCxPJsn+TDDyxGYzrIsMyDt//KhJCjIQD6pNUxr5M5jrq2OwQZgwmz91YcmoQ2XRQAUDpe4BAw==" ).expect("fail to parse test signature"), Signature::from_base64( "w69bYgiQxDmCReB0Dugt9BstXlAKnwJkKCdWvCeZ9KnUCv0FJys6klzYk/O/b9t74tYhWZSX0bhETWHiwfpWBw==" ).expect("fail to parse test signature"), ]; let doc = TransactionDocumentParser::parse_standard(doc, body, currency, signatures) .expect("fail to parse test transaction document !"); if let V10Document::Transaction(doc) = doc { //println!("Doc : {:?}", doc); println!("{}", doc.generate_compact_text()); assert_eq!(doc.verify_signatures(), VerificationResult::Valid()); assert_eq!( doc.generate_compact_text(), "TX:10:3:6:6:3:1:0 204-00003E2B8A35370BA5A7064598F628A62D4E9EC1936BE8651CE9A85F2E06981B DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV 4tNQ7d9pj2Da5wUVoW9mFn7JjuPoowF977au8DdhEjVR FD9wujR7KABw88RyKEGBYRLz8PA6jzVCbcBAsrBXBqSa 40:2:T:6991C993631BED4733972ED7538E41CCC33660F554E3C51963E2A0AC4D6453D3:2 70:2:T:3A09A20E9014110FD224889F13357BAB4EC78A72F95CA03394D8CCA2936A7435:8 20:2:D:DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV:46 70:2:T:A0D9B4CDC113ECE1145C5525873821398890AE842F4B318BD076095A23E70956:3 20:2:T:67F2045B5318777CC52CD38B424F3E40DDA823FA0364625F124BABE0030E7B5B:5 15:2:D:FD9wujR7KABw88RyKEGBYRLz8PA6jzVCbcBAsrBXBqSa:46 0:SIG(0) 1:XHX(7665798292) 2:SIG(0) 3:SIG(0) SIG(2) 4:SIG(0) SIG(1) SIG(2) 5:SIG(2) 120:2:SIG(BYfWYFrsyjpvpFysgu19rGK3VHBkz4MqmQbNyEuVU64g) 146:2:SIG(DSz4rgncXCytsUMW2JU2yhLquZECD2XpEkpP9gG5HyAx) 49:2:(SIG(6DyGr5LFtFmbaJYRvcs9WmBsr4cbJbJ1EV9zBbqG7A6i) || XHX(3EB4702F2AC2FD3FA4FDC46A4FC05AE8CDEE1A85F2AC2FD3FA4FDC46A4FC01CA)) -----@@@----- (why not this comment?) kL59C1izKjcRN429AlKdshwhWbasvyL7sthI757zm1DfZTdTIctDWlKbYeG/tS7QyAgI3gcfrTHPhu1E1lKCBw== e3LpgB2RZ/E/BCxPJsn+TDDyxGYzrIsMyDt//KhJCjIQD6pNUxr5M5jrq2OwQZgwmz91YcmoQ2XRQAUDpe4BAw== w69bYgiQxDmCReB0Dugt9BstXlAKnwJkKCdWvCeZ9KnUCv0FJys6klzYk/O/b9t74tYhWZSX0bhETWHiwfpWBw==" ); } else { panic!("Wrong document type"); } } }