1pub mod v10;
19pub mod v10_gen;
20
21use crate::*;
22
23pub use v10::{
24 TransactionDocumentV10, TransactionDocumentV10Builder, TransactionDocumentV10Stringified,
25 TransactionInputUnlocksV10, TransactionInputV10, TransactionOutputV10,
26 UnsignedTransactionDocumentV10,
27};
28
29#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
31pub struct UTXOConditions {
32 pub origin_str: Option<String>,
35 pub script: WalletScriptV10,
37}
38
39impl From<WalletScriptV10> for UTXOConditions {
40 fn from(script: WalletScriptV10) -> Self {
41 UTXOConditions {
42 origin_str: None,
43 script,
44 }
45 }
46}
47
48impl UTXOConditions {
49 pub fn reduce(&mut self) {
51 if self.check() {
52 self.origin_str = None;
53 }
54 }
55 pub fn check(&self) -> bool {
57 if let Some(ref origin_str) = self.origin_str {
58 origin_str == self.script.to_string().as_str()
59 } else {
60 true
61 }
62 }
63}
64
65impl ToString for UTXOConditions {
66 fn to_string(&self) -> String {
67 if let Some(ref origin_str) = self.origin_str {
68 origin_str.to_string()
69 } else {
70 self.script.to_string()
71 }
72 }
73}
74
75#[derive(Debug, Error)]
76pub enum TxVerifyErr {
77 #[error("Transaction must have at least one input")]
78 NoInput,
79 #[error("Transaction must have at least one issuer")]
80 NoIssuer,
81 #[error("Transaction must have at least one output")]
82 NoOutput,
83 #[error("Not same amount of inputs and unlocks: found {0} inputs and {1} unlocks.")]
84 NotSameAmountOfInputsAndUnlocks(usize, usize),
85 #[error("Not same sum of inputs amount and outputs amount: ({0}, {1}).")]
86 NotSameSumOfInputsAmountAndOutputsAmount(SourceAmount, SourceAmount),
87 #[error("{0}")]
88 Sigs(DocumentSigsErr),
89 #[error("Transaction too long: found {found} lines (max {max}).")]
90 TooManyLines { found: usize, max: usize },
91 #[error("Wrong currency: expected \'{expected}\' found \'{found}\'.")]
92 WrongCurrency { expected: String, found: String },
93}
94
95#[derive(Clone, Copy, Debug, Error)]
96pub enum GenTxError {
97 #[error("Too Many signers or recipients.")]
98 TooManySignersOrRecipients,
99}
100
101pub trait UnsignedTransactionDocumentTrait<'a>: Sized {
102 type PubKey: PublicKey;
103 type SignedDoc: TransactionDocumentTrait<'a>;
104
105 fn as_text(&self) -> &str;
107
108 fn sign<S: Signator<PublicKey = Self::PubKey>>(
110 self,
111 signator: &S,
112 ) -> Result<SignedOrUnsignedDocument<Self::SignedDoc, Self>, TransactionSignErr>;
113}
114
115pub trait TransactionDocumentTrait<'a>: Sized {
116 type Address;
117 type Input: 'a;
118 type Inputs: AsRef<[Self::Input]>;
119 type InputUnlocks: 'a;
120 type InputsUnlocks: AsRef<[Self::InputUnlocks]>;
121 type Output: 'a;
122 type Outputs: AsRef<[Self::Output]>;
123 type PubKey: PublicKey;
124 type UnsignedDoc: UnsignedTransactionDocumentTrait<'a>;
125
126 fn generate_simple_txs(
127 blockstamp: Blockstamp,
128 currency: String,
129 inputs_with_sum: (Vec<Self::Input>, SourceAmount),
130 issuer: Self::PubKey,
131 recipient: WalletScriptV10,
132 user_amount_and_comment: (SourceAmount, String),
133 cash_back_pubkey: Option<Self::PubKey>,
134 ) -> Vec<Self::UnsignedDoc>;
135 fn get_inputs(&'a self) -> Self::Inputs;
136 fn get_inputs_unlocks(&'a self) -> Self::InputsUnlocks;
137 fn get_outputs(&'a self) -> Self::Outputs;
138 fn verify(&self, expected_currency: Option<&str>) -> Result<(), TxVerifyErr>;
139}
140
141#[derive(Debug, Error, Eq, PartialEq)]
143pub enum TransactionSignErr {
144 #[error("The signator's public key is not in the transaction's issuers")]
146 InvalidSignator,
147 #[error("Signatures don\'t match: {0:?}")]
150 InvalidSignatures(HashMap<usize, SigError>),
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
157pub enum TransactionDocument {
158 V10(TransactionDocumentV10),
159}
160
161#[derive(Clone, Debug, Deserialize, Hash, Serialize, PartialEq, Eq)]
162pub enum TransactionDocumentStringified {
164 V10(TransactionDocumentV10Stringified),
165}
166
167impl ToStringObject for TransactionDocument {
168 type StringObject = TransactionDocumentStringified;
169
170 fn to_string_object(&self) -> TransactionDocumentStringified {
171 match self {
172 TransactionDocument::V10(tx_v10) => {
173 TransactionDocumentStringified::V10(tx_v10.to_string_object())
174 }
175 }
176 }
177}
178
179impl TransactionDocument {
180 pub fn compute_hash(&self) -> Hash {
182 match self {
183 TransactionDocument::V10(tx_v10) => tx_v10.compute_hash(),
184 }
185 }
186 pub fn get_hash_opt(&self) -> Option<Hash> {
188 match self {
189 TransactionDocument::V10(tx_v10) => tx_v10.get_hash_opt(),
190 }
191 }
192 pub fn get_hash(&mut self) -> Hash {
194 match self {
195 TransactionDocument::V10(tx_v10) => tx_v10.get_hash(),
196 }
197 }
198 pub fn reduce(&mut self) {
201 match self {
202 TransactionDocument::V10(tx_v10) => tx_v10.reduce(),
203 };
204 }
205}
206
207impl Document for TransactionDocument {
208 type PublicKey = PubKeyEnum;
209
210 fn version(&self) -> usize {
211 match self {
212 TransactionDocument::V10(tx_v10) => tx_v10.version(),
213 }
214 }
215
216 fn currency(&self) -> &str {
217 match self {
218 TransactionDocument::V10(tx_v10) => tx_v10.currency(),
219 }
220 }
221
222 fn blockstamp(&self) -> Blockstamp {
223 match self {
224 TransactionDocument::V10(tx_v10) => tx_v10.blockstamp(),
225 }
226 }
227
228 fn issuers(&self) -> SmallVec<[Self::PublicKey; 1]> {
229 match self {
230 TransactionDocument::V10(tx_v10) => svec![PubKeyEnum::Ed25519(tx_v10.issuers()[0])],
231 }
232 }
233
234 fn signatures(&self) -> SmallVec<[<Self::PublicKey as PublicKey>::Signature; 1]> {
235 match self {
236 TransactionDocument::V10(tx_v10) => svec![Sig::Ed25519(tx_v10.signatures()[0])],
237 }
238 }
239
240 fn as_bytes(&self) -> BeefCow<[u8]> {
241 match self {
242 TransactionDocument::V10(tx_v10) => tx_v10.as_bytes(),
243 }
244 }
245}
246
247impl CompactTextDocument for TransactionDocument {
248 fn as_compact_text(&self) -> String {
249 match self {
250 TransactionDocument::V10(tx_v10) => tx_v10.as_compact_text(),
251 }
252 }
253}
254
255impl TextDocument for TransactionDocument {
256 type CompactTextDocument_ = TransactionDocument;
257
258 fn as_text(&self) -> &str {
259 match self {
260 TransactionDocument::V10(tx_v10) => tx_v10.as_text(),
261 }
262 }
263
264 fn to_compact_document(&self) -> Cow<Self::CompactTextDocument_> {
265 Cow::Borrowed(self)
266 }
267}
268
269#[derive(Debug, Clone)]
271pub enum TransactionDocumentBuilder<'a> {
272 V10(TransactionDocumentV10Builder<'a>),
273}
274
275impl<'a> TextDocumentBuilder for TransactionDocumentBuilder<'a> {
276 type Document = TransactionDocument;
277 type Signator = SignatorEnum;
278
279 fn build_with_text_and_sigs(
280 self,
281 text: String,
282 signatures: SmallVec<
283 [<<Self::Document as Document>::PublicKey as PublicKey>::Signature; 1],
284 >,
285 ) -> TransactionDocument {
286 match self {
287 TransactionDocumentBuilder::V10(tx_v10_builder) => TransactionDocument::V10(
288 tx_v10_builder.build_with_text_and_sigs(
289 text,
290 signatures
291 .into_iter()
292 .filter_map(|sig| {
293 if let Sig::Ed25519(sig) = sig {
294 Some(sig)
295 } else {
296 None
297 }
298 })
299 .collect(),
300 ),
301 ),
302 }
303 }
304
305 fn generate_text(&self) -> String {
306 match self {
307 TransactionDocumentBuilder::V10(tx_v10_builder) => tx_v10_builder.generate_text(),
308 }
309 }
310}
311
312#[cfg(test)]
313pub(super) mod tests {
314 use super::*;
315 use smallvec::smallvec;
316 use std::str::FromStr;
317 use unwrap::unwrap;
318 use v10::{TransactionInputUnlocksV10, TransactionOutputV10};
319
320 pub(super) fn tx_output_v10(amount: i64, recv: &str) -> TransactionOutputV10 {
321 TransactionOutputV10 {
322 amount: SourceAmount::with_base0(amount),
323 conditions: UTXOConditions::from(WalletScriptV10::single(WalletConditionV10::Sig(
324 unwrap!(ed25519::PublicKey::from_base58(recv)),
325 ))),
326 }
327 }
328
329 #[test]
330 fn generate_real_document() {
331 let keypair = ed25519::KeyPairFromSeed32Generator::generate(unwrap!(
332 Seed32::from_base58("DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV"),
333 "Fail to parse Seed32"
334 ));
335 let pubkey = keypair.public_key();
336 let signator = keypair.generate_signator();
337
338 let sig = unwrap!(ed25519::Signature::from_base64(
339 "cq86RugQlqAEyS8zFkB9o0PlWPSb+a6D/MEnLe8j+okyFYf/WzI6pFiBkQ9PSOVn5I0dwzVXg7Q4N1apMWeGAg==",
340 ), "Fail to parse Signature");
341
342 let blockstamp = unwrap!(
343 Blockstamp::from_str(
344 "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
345 ),
346 "Fail to parse blockstamp"
347 );
348
349 let builder = TransactionDocumentV10Builder {
350 currency: "duniter_unit_test_currency",
351 blockstamp,
352 locktime: 0,
353 issuers: svec![pubkey],
354 inputs: &[TransactionInputV10 {
355 amount: SourceAmount::with_base0(10),
356 id: SourceIdV10::Ud(UdSourceIdV10 {
357 issuer: unwrap!(
358 ed25519::PublicKey::from_base58(
359 "DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV"
360 ),
361 "Fail to parse PublicKey"
362 ),
363 block_number: BlockNumber(0),
364 }),
365 }],
366 unlocks: &[TransactionInputUnlocksV10 {
367 index: 0,
368 unlocks: svec![WalletUnlockProofV10::Sig(0)],
369 }],
370 outputs: smallvec![tx_output_v10(
371 10,
372 "FD9wujR7KABw88RyKEGBYRLz8PA6jzVCbcBAsrBXBqSa",
373 )],
374 comment: "test",
375 hash: None,
376 };
377 assert!(builder
378 .clone()
379 .build_with_signature(svec![sig])
380 .verify_signatures()
381 .is_ok());
382 assert!(builder
383 .build_and_sign(vec![signator])
384 .verify_signatures()
385 .is_ok());
386 }
387
388 #[test]
389 fn compute_transaction_hash() {
390 let pubkey = unwrap!(
391 ed25519::PublicKey::from_base58("FEkbc4BfJukSWnCU6Hed6dgwwTuPFTVdgz5LpL4iHr9J"),
392 "Fail to parse PublicKey"
393 );
394
395 let sig = unwrap!(ed25519::Signature::from_base64(
396 "XEwKwKF8AI1gWPT7elR4IN+bW3Qn02Dk15TEgrKtY/S2qfZsNaodsLofqHLI24BBwZ5aadpC88ntmjo/UW9oDQ==",
397 ), "Fail to parse Signature");
398
399 let blockstamp = unwrap!(
400 Blockstamp::from_str(
401 "60-00001FE00410FCD5991EDD18AA7DDF15F4C8393A64FA92A1DB1C1CA2E220128D",
402 ),
403 "Fail to parse Blockstamp"
404 );
405
406 let builder = TransactionDocumentV10Builder {
407 currency: "g1",
408 blockstamp,
409 locktime: 0,
410 issuers: svec![pubkey],
411 inputs: &[TransactionInputV10 {
412 amount: SourceAmount::with_base0(950),
413 id: SourceIdV10::Utxo(UtxoIdV10 {
414 tx_hash: unwrap!(Hash::from_hex(
415 "2CF1ACD8FE8DC93EE39A1D55881C50D87C55892AE8E4DB71D4EBAB3D412AA8FD"
416 )),
417 output_index: 1,
418 }),
419 }],
420 unlocks: &[TransactionInputUnlocksV10::default()],
421 outputs: smallvec![
422 tx_output_v10(30, "38MEAZN68Pz1DTvT3tqgxx4yQP6snJCQhPqEFxbDk4aE"),
423 tx_output_v10(920, "FEkbc4BfJukSWnCU6Hed6dgwwTuPFTVdgz5LpL4iHr9J"),
424 ],
425 comment: "Pour cesium merci",
426 hash: None,
427 };
428 let mut tx_doc = TransactionDocument::V10(builder.build_with_signature(svec![sig]));
429 assert!(tx_doc.verify_signatures().is_ok());
430 assert!(tx_doc.get_hash_opt().is_none());
431 assert_eq!(
432 tx_doc.get_hash(),
433 unwrap!(Hash::from_hex(
434 "876D2430E0B66E2CE4467866D8F923D68896CACD6AA49CDD8BDD0096B834DEF1"
435 ))
436 );
437 }
438}