Skip to content
Snippets Groups Projects
Commit e625c84b authored by Éloïs's avatar Éloïs
Browse files

Merge branch '19-add-transaction-document-structure-building-and-parsing' into 'dev'

Resolve "Add Transaction document structure, building and parsing"

Closes #19

See merge request !41
parents 927620a1 5c4c046e
Branches
Tags documents/v0.7.0
1 merge request!41Resolve "Add Transaction document structure, building and parsing"
...@@ -94,7 +94,7 @@ dependencies = [ ...@@ -94,7 +94,7 @@ dependencies = [
[[package]] [[package]]
name = "duniter-documents" name = "duniter-documents"
version = "0.5.0" version = "0.6.0"
dependencies = [ dependencies = [
"base58 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "base64 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
......
[package] [package]
name = "duniter-documents" name = "duniter-documents"
version = "0.5.0" version = "0.6.0"
authors = ["nanocryk <nanocryk@duniter.org>", "elois <elois@ifee.fr>"] authors = ["nanocryk <nanocryk@duniter.org>", "elois <elois@ifee.fr>"]
description = "Handles Duniter documents" description = "Handles Duniter documents"
repository = "https://git.duniter.org/nodes/rust/duniter-rs" repository = "https://git.duniter.org/nodes/rust/duniter-rs"
......
...@@ -191,6 +191,16 @@ CertTimestamp: {blockstamp} ...@@ -191,6 +191,16 @@ CertTimestamp: {blockstamp}
blockstamp = self.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 /// Certification document parser
...@@ -297,6 +307,12 @@ mod tests { ...@@ -297,6 +307,12 @@ mod tests {
builder.build_and_sign(vec![prikey]).verify_signatures(), builder.build_and_sign(vec![prikey]).verify_signatures(),
VerificationResult::Valid() VerificationResult::Valid()
); );
assert_eq!(
builder.generate_compact_text(vec![sig]),
"4tNQ7d9pj2Da5wUVoW9mFn7JjuPoowF977au8DdhEjVR:\
DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV:36:\
qfR6zqT1oJbqIsppOi64gC9yTtxb6g6XA9RYpulkq9ehMvqg2VYVigCbR0yVpqKFsnYiQTrnjgFuFRSJCJDfCw=="
);
} }
#[test] #[test]
......
...@@ -43,7 +43,7 @@ pub struct IdentityDocument { ...@@ -43,7 +43,7 @@ pub struct IdentityDocument {
/// Currency. /// Currency.
currency: String, currency: String,
/// Unique ID /// Unique ID
unique_id: String, username: String,
/// Blockstamp /// Blockstamp
blockstamp: Blockstamp, blockstamp: Blockstamp,
/// Document issuer (there should be only one). /// Document issuer (there should be only one).
...@@ -54,8 +54,8 @@ pub struct IdentityDocument { ...@@ -54,8 +54,8 @@ pub struct IdentityDocument {
impl IdentityDocument { impl IdentityDocument {
/// Unique ID /// Unique ID
pub fn unique_id(&self) -> &str { pub fn username(&self) -> &str {
&self.unique_id &self.username
} }
} }
...@@ -102,7 +102,7 @@ pub struct IdentityDocumentBuilder<'a> { ...@@ -102,7 +102,7 @@ pub struct IdentityDocumentBuilder<'a> {
/// Document currency. /// Document currency.
pub currency: &'a str, pub currency: &'a str,
/// Identity unique id. /// Identity unique id.
pub unique_id: &'a str, pub username: &'a str,
/// Reference blockstamp. /// Reference blockstamp.
pub blockstamp: &'a Blockstamp, pub blockstamp: &'a Blockstamp,
/// Document/identity issuer. /// Document/identity issuer.
...@@ -118,7 +118,7 @@ impl<'a> IdentityDocumentBuilder<'a> { ...@@ -118,7 +118,7 @@ impl<'a> IdentityDocumentBuilder<'a> {
IdentityDocument { IdentityDocument {
text, text,
currency: self.currency.to_string(), currency: self.currency.to_string(),
unique_id: self.unique_id.to_string(), username: self.username.to_string(),
blockstamp: *self.blockstamp, blockstamp: *self.blockstamp,
issuers: vec![*self.issuer], issuers: vec![*self.issuer],
signatures, signatures,
...@@ -147,15 +147,25 @@ impl<'a> TextDocumentBuilder for IdentityDocumentBuilder<'a> { ...@@ -147,15 +147,25 @@ impl<'a> TextDocumentBuilder for IdentityDocumentBuilder<'a> {
Type: Identity Type: Identity
Currency: {currency} Currency: {currency}
Issuer: {issuer} Issuer: {issuer}
UniqueID: {unique_id} UniqueID: {username}
Timestamp: {blockstamp} Timestamp: {blockstamp}
", ",
currency = self.currency, currency = self.currency,
issuer = self.issuer, issuer = self.issuer,
unique_id = self.unique_id, username = self.username,
blockstamp = self.blockstamp 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 /// Identity document parser
...@@ -182,7 +192,7 @@ impl StandardTextDocumentParser for IdentityDocumentParser { ...@@ -182,7 +192,7 @@ impl StandardTextDocumentParser for IdentityDocumentParser {
Ok(V10Document::Identity(IdentityDocument { Ok(V10Document::Identity(IdentityDocument {
text: doc.to_owned(), text: doc.to_owned(),
currency: currency.to_owned(), currency: currency.to_owned(),
unique_id: uid.to_owned(), username: uid.to_owned(),
blockstamp, blockstamp,
issuers: vec![issuer], issuers: vec![issuer],
signatures, signatures,
...@@ -223,7 +233,7 @@ mod tests { ...@@ -223,7 +233,7 @@ mod tests {
let builder = IdentityDocumentBuilder { let builder = IdentityDocumentBuilder {
currency: "duniter_unit_test_currency", currency: "duniter_unit_test_currency",
unique_id: "tic", username: "tic",
blockstamp: &block, blockstamp: &block,
issuer: &pubkey, issuer: &pubkey,
}; };
......
...@@ -190,6 +190,17 @@ CertTS: {ity_blockstamp} ...@@ -190,6 +190,17 @@ CertTS: {ity_blockstamp}
ity_blockstamp = self.identity_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 /// Membership document parser
...@@ -284,6 +295,14 @@ mod tests { ...@@ -284,6 +295,14 @@ mod tests {
builder.build_and_sign(vec![prikey]).verify_signatures(), builder.build_and_sign(vec![prikey]).verify_signatures(),
VerificationResult::Valid() VerificationResult::Valid()
); );
assert_eq!(
builder.generate_compact_text(vec![sig]),
"DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV:\
s2hUbokkibTAWGEwErw6hyXSWlWFQ2UWs2PWx8d/kkElAyuuWaQq4Tsonuweh1xn4AC1TVWt4yMR3WrDdkhnAw==:\
0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:\
0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:\
tic"
);
} }
#[test] #[test]
......
...@@ -28,17 +28,20 @@ pub mod identity; ...@@ -28,17 +28,20 @@ pub mod identity;
pub mod membership; pub mod membership;
pub mod certification; pub mod certification;
pub mod revocation; pub mod revocation;
pub mod transaction;
pub use blockchain::v10::documents::identity::{IdentityDocument, IdentityDocumentBuilder}; pub use blockchain::v10::documents::identity::{IdentityDocument, IdentityDocumentBuilder};
pub use blockchain::v10::documents::membership::{MembershipDocument, MembershipDocumentParser}; pub use blockchain::v10::documents::membership::{MembershipDocument, MembershipDocumentParser};
pub use blockchain::v10::documents::certification::{CertificationDocument, pub use blockchain::v10::documents::certification::{CertificationDocument,
CertificationDocumentParser}; CertificationDocumentParser};
pub use blockchain::v10::documents::revocation::{RevocationDocument, RevocationDocumentParser}; 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. // Use of lazy_static so the regex is only compiled at first use.
lazy_static! { lazy_static! {
static ref DOCUMENT_REGEX: Regex = Regex::new( 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\ Type: (?P<type>[[:alpha:]]+)\n\
Currency: (?P<currency>[[:alnum:] _-]+)\n\ Currency: (?P<currency>[[:alnum:] _-]+)\n\
(?P<body>(?:.*\n)+?))\ (?P<body>(?:.*\n)+?))\
...@@ -56,7 +59,7 @@ pub enum V10Document { ...@@ -56,7 +59,7 @@ pub enum V10Document {
Block(), Block(),
/// Transaction document. /// Transaction document.
Transaction(), Transaction(Box<TransactionDocument>),
/// Identity document. /// Identity document.
Identity(IdentityDocument), Identity(IdentityDocument),
...@@ -102,6 +105,13 @@ pub trait TextDocumentBuilder: DocumentBuilder { ...@@ -102,6 +105,13 @@ pub trait TextDocumentBuilder: DocumentBuilder {
/// - Contains line breaks on all line. /// - Contains line breaks on all line.
fn generate_text(&self) -> String; 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. /// Generate final document with signatures, and also return them in an array.
/// ///
/// Returns : /// Returns :
...@@ -139,6 +149,19 @@ pub enum V10DocumentParsingError { ...@@ -139,6 +149,19 @@ pub enum V10DocumentParsingError {
UnknownDocumentType(String), 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 { trait StandardTextDocumentParser {
fn parse_standard( fn parse_standard(
doc: &str, doc: &str,
...@@ -148,15 +171,6 @@ trait StandardTextDocumentParser { ...@@ -148,15 +171,6 @@ trait StandardTextDocumentParser {
) -> Result<V10Document, V10DocumentParsingError>; ) -> 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. /// A V10 document parser.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct V10DocumentParser; pub struct V10DocumentParser;
...@@ -178,6 +192,13 @@ impl<'a> DocumentParser<&'a str, V10Document, V10DocumentParsingError> for V10Do ...@@ -178,6 +192,13 @@ impl<'a> DocumentParser<&'a str, V10Document, V10DocumentParsingError> for V10Do
match doctype { match doctype {
"Identity" => IdentityDocumentParser::parse_standard(doc, body, currency, sigs), "Identity" => IdentityDocumentParser::parse_standard(doc, body, currency, sigs),
"Membership" => MembershipDocumentParser::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( _ => Err(V10DocumentParsingError::UnknownDocumentType(
doctype.to_string(), doctype.to_string(),
)), )),
...@@ -275,11 +296,11 @@ SoKwoa8PFfCDJWZ6dNCv7XstezHcc2BbKiJgVDXv82R5zYR83nis9dShLgWJ5w48noVUHimdngzYQneN ...@@ -275,11 +296,11 @@ SoKwoa8PFfCDJWZ6dNCv7XstezHcc2BbKiJgVDXv82R5zYR83nis9dShLgWJ5w48noVUHimdngzYQneN
fn parse_identity_document() { fn parse_identity_document() {
let text = "Version: 10 let text = "Version: 10
Type: Identity Type: Identity
Currency: duniter_unit_test_currency Currency: g1
Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV Issuer: D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx
UniqueID: tic UniqueID: elois
Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
1eubHHbuNfilHMM0G2bI30iZzebQ2cQ1PC7uPAw08FGMMmQCRerlF/3pc4sAcsnexsxBseA/3lY03KlONqJBAg=="; Ydnclvw76/JHcKSmU9kl9Ie0ne5/X8NYOqPqbGnufIK3eEPRYYdEYaQh+zffuFhbtIRjv6m/DkVLH5cLy/IyAg==";
let doc = V10DocumentParser::parse(text).unwrap(); let doc = V10DocumentParser::parse(text).unwrap();
if let V10Document::Identity(doc) = doc { if let V10Document::Identity(doc) = doc {
...@@ -294,13 +315,13 @@ Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 ...@@ -294,13 +315,13 @@ Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
fn parse_membership_document() { fn parse_membership_document() {
let text = "Version: 10 let text = "Version: 10
Type: Membership Type: Membership
Currency: duniter_unit_test_currency Currency: g1
Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV Issuer: D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx
Block: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 Block: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
Membership: IN Membership: IN
UserID: tic UserID: elois
CertTS: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 CertTS: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
s2hUbokkibTAWGEwErw6hyXSWlWFQ2UWs2PWx8d/kkElAyuuWaQq4Tsonuweh1xn4AC1TVWt4yMR3WrDdkhnAw=="; FFeyrvYio9uYwY5aMcDGswZPNjGLrl8THn9l3EPKSNySD3SDSHjCljSfFEwb87sroyzJQoVzPwER0sW/cbZMDg==";
let doc = V10DocumentParser::parse(text).unwrap(); let doc = V10DocumentParser::parse(text).unwrap();
if let V10Document::Membership(doc) = doc { if let V10Document::Membership(doc) = doc {
...@@ -310,4 +331,75 @@ s2hUbokkibTAWGEwErw6hyXSWlWFQ2UWs2PWx8d/kkElAyuuWaQq4Tsonuweh1xn4AC1TVWt4yMR3WrD ...@@ -310,4 +331,75 @@ s2hUbokkibTAWGEwErw6hyXSWlWFQ2UWs2PWx8d/kkElAyuuWaQq4Tsonuweh1xn4AC1TVWt4yMR3WrD
panic!("Wrong document type"); 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");
}
}
} }
...@@ -165,6 +165,13 @@ IdtySignature: {idty_sig} ...@@ -165,6 +165,13 @@ IdtySignature: {idty_sig}
idty_sig = self.identity_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 /// Revocation document parser
......
// 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");
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment