diff --git a/Cargo.lock b/Cargo.lock index 9648b789bb4d65f41aacf4c31367e66662587faa..8fd4d60894dfa6e1b161d27e51cc4946d444288c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,7 +94,7 @@ dependencies = [ [[package]] name = "duniter-documents" -version = "0.5.0" +version = "0.6.0" dependencies = [ "base58 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/documents/Cargo.toml b/documents/Cargo.toml index 2a42d7fd9d4e01bde39d80f0f12c663078a1707c..7200f0b650e9604ad2ccf75d6af14c05ca0867d3 100644 --- a/documents/Cargo.toml +++ b/documents/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "duniter-documents" -version = "0.5.0" +version = "0.6.0" authors = ["nanocryk <nanocryk@duniter.org>", "elois <elois@ifee.fr>"] description = "Handles Duniter documents" repository = "https://git.duniter.org/nodes/rust/duniter-rs" diff --git a/documents/blockchain/v10/documents/certification.rs b/documents/blockchain/v10/documents/certification.rs index d4de8631c1078f587aba3679dbe8328c294b82ac..46a4467fd11853a1ff10f24ae86304546bbedd1d 100644 --- a/documents/blockchain/v10/documents/certification.rs +++ b/documents/blockchain/v10/documents/certification.rs @@ -191,6 +191,16 @@ CertTimestamp: {blockstamp} blockstamp = self.blockstamp, ) } + + fn generate_compact_text(&self, signatures: Vec<ed25519::Signature>) -> String { + format!( + "{issuer}:{target}:{block_number}:{signature}", + issuer = self.issuer, + target = self.target, + block_number = self.blockstamp.id.0, + signature = signatures[0], + ) + } } /// Certification document parser @@ -297,6 +307,12 @@ mod tests { builder.build_and_sign(vec![prikey]).verify_signatures(), VerificationResult::Valid() ); + assert_eq!( + builder.generate_compact_text(vec![sig]), + "4tNQ7d9pj2Da5wUVoW9mFn7JjuPoowF977au8DdhEjVR:\ + DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV:36:\ + qfR6zqT1oJbqIsppOi64gC9yTtxb6g6XA9RYpulkq9ehMvqg2VYVigCbR0yVpqKFsnYiQTrnjgFuFRSJCJDfCw==" + ); } #[test] diff --git a/documents/blockchain/v10/documents/identity.rs b/documents/blockchain/v10/documents/identity.rs index ac7622ec78cb27ce47dfb36c28668dd7f2980b35..855d620557fb0abd8f9add187d5c3da157afeaa6 100644 --- a/documents/blockchain/v10/documents/identity.rs +++ b/documents/blockchain/v10/documents/identity.rs @@ -43,7 +43,7 @@ pub struct IdentityDocument { /// Currency. currency: String, /// Unique ID - unique_id: String, + username: String, /// Blockstamp blockstamp: Blockstamp, /// Document issuer (there should be only one). @@ -54,8 +54,8 @@ pub struct IdentityDocument { impl IdentityDocument { /// Unique ID - pub fn unique_id(&self) -> &str { - &self.unique_id + pub fn username(&self) -> &str { + &self.username } } @@ -102,7 +102,7 @@ pub struct IdentityDocumentBuilder<'a> { /// Document currency. pub currency: &'a str, /// Identity unique id. - pub unique_id: &'a str, + pub username: &'a str, /// Reference blockstamp. pub blockstamp: &'a Blockstamp, /// Document/identity issuer. @@ -118,7 +118,7 @@ impl<'a> IdentityDocumentBuilder<'a> { IdentityDocument { text, currency: self.currency.to_string(), - unique_id: self.unique_id.to_string(), + username: self.username.to_string(), blockstamp: *self.blockstamp, issuers: vec![*self.issuer], signatures, @@ -147,15 +147,25 @@ impl<'a> TextDocumentBuilder for IdentityDocumentBuilder<'a> { Type: Identity Currency: {currency} Issuer: {issuer} -UniqueID: {unique_id} +UniqueID: {username} Timestamp: {blockstamp} ", currency = self.currency, issuer = self.issuer, - unique_id = self.unique_id, + username = self.username, blockstamp = self.blockstamp ) } + + fn generate_compact_text(&self, signatures: Vec<ed25519::Signature>) -> String { + format!( + "{issuer}:{signature}:{blockstamp}:{username}", + issuer = self.issuer, + signature = signatures[0], + blockstamp = self.blockstamp, + username = self.username, + ) + } } /// Identity document parser @@ -182,7 +192,7 @@ impl StandardTextDocumentParser for IdentityDocumentParser { Ok(V10Document::Identity(IdentityDocument { text: doc.to_owned(), currency: currency.to_owned(), - unique_id: uid.to_owned(), + username: uid.to_owned(), blockstamp, issuers: vec![issuer], signatures, @@ -223,7 +233,7 @@ mod tests { let builder = IdentityDocumentBuilder { currency: "duniter_unit_test_currency", - unique_id: "tic", + username: "tic", blockstamp: &block, issuer: &pubkey, }; diff --git a/documents/blockchain/v10/documents/membership.rs b/documents/blockchain/v10/documents/membership.rs index 0fab5443d53f047616407afbc153dc491c60289d..9b9d41668d5e36940d14d5e0b4466427298353a8 100644 --- a/documents/blockchain/v10/documents/membership.rs +++ b/documents/blockchain/v10/documents/membership.rs @@ -190,6 +190,17 @@ CertTS: {ity_blockstamp} ity_blockstamp = self.identity_blockstamp, ) } + + fn generate_compact_text(&self, signatures: Vec<ed25519::Signature>) -> String { + format!( + "{issuer}:{signature}:{blockstamp}:{idty_blockstamp}:{username}", + issuer = self.issuer, + signature = signatures[0], + blockstamp = self.blockstamp, + idty_blockstamp = self.identity_blockstamp, + username = self.identity_username, + ) + } } /// Membership document parser @@ -284,6 +295,14 @@ mod tests { builder.build_and_sign(vec![prikey]).verify_signatures(), VerificationResult::Valid() ); + assert_eq!( + builder.generate_compact_text(vec![sig]), + "DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV:\ + s2hUbokkibTAWGEwErw6hyXSWlWFQ2UWs2PWx8d/kkElAyuuWaQq4Tsonuweh1xn4AC1TVWt4yMR3WrDdkhnAw==:\ + 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:\ + 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:\ + tic" + ); } #[test] diff --git a/documents/blockchain/v10/documents/mod.rs b/documents/blockchain/v10/documents/mod.rs index d2eb531b296e7c3314300e3a35427ae71bedd497..8a02f2ee0cdfb65debe2d972ebc944e57f75da06 100644 --- a/documents/blockchain/v10/documents/mod.rs +++ b/documents/blockchain/v10/documents/mod.rs @@ -28,17 +28,20 @@ pub mod identity; pub mod membership; pub mod certification; pub mod revocation; +pub mod transaction; pub use blockchain::v10::documents::identity::{IdentityDocument, IdentityDocumentBuilder}; pub use blockchain::v10::documents::membership::{MembershipDocument, MembershipDocumentParser}; pub use blockchain::v10::documents::certification::{CertificationDocument, CertificationDocumentParser}; pub use blockchain::v10::documents::revocation::{RevocationDocument, RevocationDocumentParser}; +pub use blockchain::v10::documents::transaction::{TransactionDocument, TransactionDocumentBuilder, + TransactionDocumentParser}; // Use of lazy_static so the regex is only compiled at first use. lazy_static! { static ref DOCUMENT_REGEX: Regex = Regex::new( - "^(?P<doc>Version: 10\n\ + "^(?P<doc>Version: (?P<version>[0-9]+)\n\ Type: (?P<type>[[:alpha:]]+)\n\ Currency: (?P<currency>[[:alnum:] _-]+)\n\ (?P<body>(?:.*\n)+?))\ @@ -56,7 +59,7 @@ pub enum V10Document { Block(), /// Transaction document. - Transaction(), + Transaction(Box<TransactionDocument>), /// Identity document. Identity(IdentityDocument), @@ -102,6 +105,13 @@ pub trait TextDocumentBuilder: DocumentBuilder { /// - Contains line breaks on all line. fn generate_text(&self) -> String; + /// Generate document compact text. + /// the compact format is the one used in the blocks. + /// + /// - Don't contains leading signatures + /// - Contains line breaks on all line. + fn generate_compact_text(&self, signatures: Vec<ed25519::Signature>) -> String; + /// Generate final document with signatures, and also return them in an array. /// /// Returns : @@ -139,6 +149,19 @@ pub enum V10DocumentParsingError { UnknownDocumentType(String), } +/// V10 Documents in separated parts +#[derive(Debug, Clone)] +pub struct V10DocumentParts { + /// Whole document in text + pub doc: String, + /// Payload + pub body: String, + /// Currency + pub currency: String, + /// Signatures + pub signatures: Vec<ed25519::Signature>, +} + trait StandardTextDocumentParser { fn parse_standard( doc: &str, @@ -148,15 +171,6 @@ trait StandardTextDocumentParser { ) -> Result<V10Document, V10DocumentParsingError>; } -trait CompactTextDocumentParser<D: TextDocument> { - fn parse_compact( - doc: &str, - body: &str, - currency: &str, - signatures: Vec<ed25519::Signature>, - ) -> Result<D, V10DocumentParsingError>; -} - /// A V10 document parser. #[derive(Debug, Clone, Copy)] pub struct V10DocumentParser; @@ -178,6 +192,13 @@ impl<'a> DocumentParser<&'a str, V10Document, V10DocumentParsingError> for V10Do match doctype { "Identity" => IdentityDocumentParser::parse_standard(doc, body, currency, sigs), "Membership" => MembershipDocumentParser::parse_standard(doc, body, currency, sigs), + "Certification" => { + CertificationDocumentParser::parse_standard(doc, body, currency, sigs) + } + "Revocation" => RevocationDocumentParser::parse_standard(doc, body, currency, sigs), + "Transaction" => { + TransactionDocumentParser::parse_standard(doc, body, currency, sigs) + } _ => Err(V10DocumentParsingError::UnknownDocumentType( doctype.to_string(), )), @@ -275,11 +296,11 @@ SoKwoa8PFfCDJWZ6dNCv7XstezHcc2BbKiJgVDXv82R5zYR83nis9dShLgWJ5w48noVUHimdngzYQneN fn parse_identity_document() { let text = "Version: 10 Type: Identity -Currency: duniter_unit_test_currency -Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV -UniqueID: tic +Currency: g1 +Issuer: D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx +UniqueID: elois Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 -1eubHHbuNfilHMM0G2bI30iZzebQ2cQ1PC7uPAw08FGMMmQCRerlF/3pc4sAcsnexsxBseA/3lY03KlONqJBAg=="; +Ydnclvw76/JHcKSmU9kl9Ie0ne5/X8NYOqPqbGnufIK3eEPRYYdEYaQh+zffuFhbtIRjv6m/DkVLH5cLy/IyAg=="; let doc = V10DocumentParser::parse(text).unwrap(); if let V10Document::Identity(doc) = doc { @@ -294,13 +315,13 @@ Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 fn parse_membership_document() { let text = "Version: 10 Type: Membership -Currency: duniter_unit_test_currency -Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV +Currency: g1 +Issuer: D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx Block: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 Membership: IN -UserID: tic +UserID: elois CertTS: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 -s2hUbokkibTAWGEwErw6hyXSWlWFQ2UWs2PWx8d/kkElAyuuWaQq4Tsonuweh1xn4AC1TVWt4yMR3WrDdkhnAw=="; +FFeyrvYio9uYwY5aMcDGswZPNjGLrl8THn9l3EPKSNySD3SDSHjCljSfFEwb87sroyzJQoVzPwER0sW/cbZMDg=="; let doc = V10DocumentParser::parse(text).unwrap(); if let V10Document::Membership(doc) = doc { @@ -310,4 +331,75 @@ s2hUbokkibTAWGEwErw6hyXSWlWFQ2UWs2PWx8d/kkElAyuuWaQq4Tsonuweh1xn4AC1TVWt4yMR3WrD panic!("Wrong document type"); } } + + #[test] + fn parse_certification_document() { + let text = "Version: 10 +Type: Certification +Currency: g1 +Issuer: 2sZF6j2PkxBDNAqUde7Dgo5x3crkerZpQ4rBqqJGn8QT +IdtyIssuer: 7jzkd8GiFnpys4X7mP78w2Y3y3kwdK6fVSLEaojd3aH9 +IdtyUniqueID: fbarbut +IdtyTimestamp: 98221-000000575AC04F5164F7A307CDB766139EA47DD249E4A2444F292BC8AAB408B3 +IdtySignature: DjeipIeb/RF0tpVCnVnuw6mH1iLJHIsDfPGLR90Twy3PeoaDz6Yzhc/UjLWqHCi5Y6wYajV0dNg4jQRUneVBCQ== +CertTimestamp: 99956-00000472758331FDA8388E30E50CA04736CBFD3B7C21F34E74707107794B56DD +Hkps1QU4HxIcNXKT8YmprYTVByBhPP1U2tIM7Z8wENzLKIWAvQClkAvBE7pW9dnVa18sJIJhVZUcRrPAZfmjBA=="; + + let doc = V10DocumentParser::parse(text).unwrap(); + if let V10Document::Certification(doc) = doc { + println!("Doc : {:?}", doc); + assert_eq!(doc.verify_signatures(), VerificationResult::Valid()) + } else { + panic!("Wrong document type"); + } + } + + #[test] + fn parse_revocation_document() { + let text = "Version: 10 +Type: Revocation +Currency: g1 +Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV +IdtyUniqueID: tic +IdtyTimestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 +IdtySignature: 1eubHHbuNfilHMM0G2bI30iZzebQ2cQ1PC7uPAw08FGMMmQCRerlF/3pc4sAcsnexsxBseA/3lY03KlONqJBAg== +XXOgI++6qpY9O31ml/FcfbXCE6aixIrgkT5jL7kBle3YOMr+8wrp7Rt+z9hDVjrNfYX2gpeJsuMNfG4T/fzVDQ=="; + + let doc = V10DocumentParser::parse(text).unwrap(); + if let V10Document::Revocation(doc) = doc { + println!("Doc : {:?}", doc); + assert_eq!(doc.verify_signatures(), VerificationResult::Valid()) + } else { + panic!("Wrong document type"); + } + } + + #[test] + fn parse_transaction_document() { + let text = "Version: 10 +Type: Transaction +Currency: g1 +Blockstamp: 107702-0000017CDBE974DC9A46B89EE7DC2BEE4017C43A005359E0853026C21FB6A084 +Locktime: 0 +Issuers: +Do6Y6nQ2KTo5fB4MXbSwabXVmXHxYRB9UUAaTPKn1XqC +Inputs: +1002:0:D:Do6Y6nQ2KTo5fB4MXbSwabXVmXHxYRB9UUAaTPKn1XqC:104937 +1002:0:D:Do6Y6nQ2KTo5fB4MXbSwabXVmXHxYRB9UUAaTPKn1XqC:105214 +Unlocks: +0:SIG(0) +1:SIG(0) +Outputs: +2004:0:SIG(DTgQ97AuJ8UgVXcxmNtULAs8Fg1kKC1Wr9SAS96Br9NG) +Comment: c est pour 2 mois d adhesion ressourcerie +lnpuFsIymgz7qhKF/GsZ3n3W8ZauAAfWmT4W0iJQBLKJK2GFkesLWeMj/+GBfjD6kdkjreg9M6VfkwIZH+hCCQ=="; + + let doc = V10DocumentParser::parse(text).unwrap(); + if let V10Document::Transaction(doc) = doc { + println!("Doc : {:?}", doc); + assert_eq!(doc.verify_signatures(), VerificationResult::Valid()) + } else { + panic!("Wrong document type"); + } + } } diff --git a/documents/blockchain/v10/documents/revocation.rs b/documents/blockchain/v10/documents/revocation.rs index 6b7538b9c472913d74821518400a7ce985e9e077..3760f6369157a7044318ee0d5a56f13c513ed10d 100644 --- a/documents/blockchain/v10/documents/revocation.rs +++ b/documents/blockchain/v10/documents/revocation.rs @@ -165,6 +165,13 @@ IdtySignature: {idty_sig} idty_sig = self.identity_sig, ) } + fn generate_compact_text(&self, signatures: Vec<ed25519::Signature>) -> String { + format!( + "{issuer}:{signature}", + issuer = self.issuer, + signature = signatures[0], + ) + } } /// Revocation document parser diff --git a/documents/blockchain/v10/documents/transaction.rs b/documents/blockchain/v10/documents/transaction.rs new file mode 100644 index 0000000000000000000000000000000000000000..f71372d95da17f2ef45607fe45b2f2806c05a8c1 --- /dev/null +++ b/documents/blockchain/v10/documents/transaction.rs @@ -0,0 +1,851 @@ +// 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 <https://www.gnu.org/licenses/>. + +//! 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<blockstamp>[0-9]+-[0-9A-F]{64})\nLocktime: (?P<locktime>[0-9]+)\nIssuers:(?P<issuers>(?:\n[1-9A-Za-z][^OIl]{43,44})+)Inputs:\n(?P<inputs>([0-9A-Za-z:]+\n)+)Unlocks:\n(?P<unlocks>([0-9]+:(SIG\([0-9]+\) ?|XHX\(\w+\) ?)+\n)+)Outputs:\n(?P<outputs>([0-9A-Za-z()&|: ]+\n)+)Comment:(?P<comment>[\\\w:/;*\[\]()?!^+=@&~#{}|<>%. -]{0,255})\n$"; + static ref ISSUER_REGEX: Regex = Regex::new("(?P<issuer>[1-9A-Za-z][^OIl]{43,44})\n").unwrap(); + static ref D_INPUT_REGEX: Regex = Regex::new( + "^(?P<amount>[1-9][0-9]*):(?P<base>[0-9]+):D:(?P<pubkey>[1-9A-Za-z][^OIl]{43,44}):(?P<block_number>[0-9]+)$" + ).unwrap(); + static ref T_INPUT_REGEX: Regex = Regex::new( + "^(?P<amount>[1-9][0-9]*):(?P<base>[0-9]+):T:(?P<tx_hash>[0-9A-F]{64}):(?P<tx_index>[0-9]+)$" + ).unwrap(); + static ref UNLOCKS_REGEX: Regex = Regex::new( + r"^(?P<index>[0-9]+):(?P<unlocks>(SIG\([0-9]+\) ?|XHX\(\w+\) ?)+)$" + ).unwrap(); + static ref UNLOCK_SIG_REGEX: Regex = + Regex::new(r"^SIG\((?P<index>[0-9]+)\)$").unwrap(); + static ref UNLOCK_XHX_REGEX: Regex = Regex::new(r"^XHX\((?P<code>\w+)\)$").unwrap(); + static ref OUTPUT_COND_SIG_REGEX: Regex = Regex::new(r"^SIG\((?P<pubkey>[1-9A-Za-z][^OIl]{43,44})\)$").unwrap(); + static ref OUTPUT_COND_XHX_REGEX: Regex = Regex::new(r"^XHX\((?P<hash>[0-9A-F]{64})\)$").unwrap(); + static ref OUTPUT_COND_CLTV_REGEX: Regex = Regex::new(r"^CLTV\((?P<timestamp>[0-9]+)\)$").unwrap(); + static ref OUTPUT_COND_CSV_REGEX: Regex = Regex::new(r"^CSV\((?P<timestamp>[0-9]+)\)$").unwrap(); + static ref OUPUT_CONDS_BRAKETS: Regex = Regex::new(r"^\((?P<conditions>[0-9A-Za-z()&| ]+)\)$").unwrap(); + static ref OUPUT_CONDS_AND: Regex = Regex::new(r"^(?P<conditions_group_1>[0-9A-Za-z()&| ]+) && (?P<conditions_group_2>[0-9A-Za-z()&| ]+)$").unwrap(); + static ref OUPUT_CONDS_OR: Regex = Regex::new(r"^(?P<conditions_group_1>[0-9A-Za-z()&| ]+) \|\| (?P<conditions_group_2>[0-9A-Za-z()&| ]+)$").unwrap(); + static ref OUTPUT_REGEX: Regex = Regex::new( + "^(?P<amount>[1-9][0-9]+):(?P<base>[0-9]+):(?P<conditions>[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<TransactionInput, V10DocumentParsingError> { + 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<TransactionUnlockProof, V10DocumentParsingError> { + 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<TransactionUnlockProof>, +} + +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<TransactionInputUnlocks, V10DocumentParsingError> { + 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<TransactionOuputCondition, V10DocumentParsingError> { + 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<TransactionOuputConditionGroup>), + /// And operator + And( + Box<TransactionOuputConditionGroup>, + Box<TransactionOuputConditionGroup>, + ), + /// Or operator + Or( + Box<TransactionOuputConditionGroup>, + Box<TransactionOuputConditionGroup>, + ), +} + +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<TransactionOuputConditionGroup, V10DocumentParsingError> { + 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<TransactionOuput, V10DocumentParsingError> { + 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<ed25519::PublicKey>, + /// Transaction inputs. + inputs: Vec<TransactionInput>, + /// Inputs unlocks. + unlocks: Vec<TransactionInputUnlocks>, + /// Transaction outputs. + outputs: Vec<TransactionOuput>, + /// Transaction comment + comment: String, + /// Document signature (there should be only one). + signatures: Vec<ed25519::Signature>, +} + +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<ed25519::PublicKey> { + &self.issuers + } + + fn signatures(&self) -> &Vec<ed25519::Signature> { + &self.signatures + } + + fn as_bytes(&self) -> &[u8] { + self.as_text().as_bytes() + } +} + +impl TextDocument for TransactionDocument { + fn as_text(&self) -> &str { + &self.text + } +} + +impl IntoSpecializedDocument<BlockchainProtocol> 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<ed25519::PublicKey>, + /// Transaction inputs. + pub inputs: &'a Vec<TransactionInput>, + /// Inputs unlocks. + pub unlocks: &'a Vec<TransactionInputUnlocks>, + /// Transaction ouputs. + pub outputs: &'a Vec<TransactionOuput>, + /// Transaction comment + pub comment: &'a str, +} + +impl<'a> TransactionDocumentBuilder<'a> { + fn build_with_text_and_sigs( + self, + text: String, + signatures: Vec<ed25519::Signature>, + ) -> 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<ed25519::Signature>) -> TransactionDocument { + self.build_with_text_and_sigs(self.generate_text(), signatures) + } + + fn build_and_sign(&self, private_keys: Vec<ed25519::PrivateKey>) -> 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, + ) + } + + fn generate_compact_text(&self, signatures: Vec<ed25519::Signature>) -> String { + let mut issuers_str = String::from(""); + for issuer in self.issuers { + issuers_str.push_str(&issuer.to_string()); + } + let mut inputs_str = String::from(""); + for input in self.inputs { + inputs_str.push_str(&input.to_string()); + } + let mut unlocks_str = String::from(""); + for unlock in self.unlocks { + unlocks_str.push_str(&unlock.to_string()); + } + let mut outputs_str = String::from(""); + for output in self.outputs { + outputs_str.push_str(&output.to_string()); + } + let mut signatures_str = String::from(""); + for sig in signatures { + 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} +{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, + ) + } +} + +/// 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<ed25519::Signature>, + ) -> Result<V10Document, V10DocumentParsingError> { + 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) { + println!("{:?}", &caps["issuer"]); + 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() + ); + + assert_eq!( + builder.generate_compact_text(vec![sig]), + "TX:10:1:1:1:1:1:0 +0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 +DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV +10:0:D:DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV:0 +0:SIG(0) +10:0:SIG(FD9wujR7KABw88RyKEGBYRLz8PA6jzVCbcBAsrBXBqSa) +test +pRQeKlzCsvPNmYAAkEP5jPPQO1RwrtFMRfCajEfkkrG0UQE0DhoTkxG3Zs2JFmvAFLw67pn1V5NQ08zsSfJkBg==" + ); + } + + #[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); + assert_eq!(doc.verify_signatures(), VerificationResult::Valid()) + } else { + panic!("Wrong document type"); + } + } +}