Skip to main content

accumulate_client/protocol/
mod.rs

1//! Accumulate protocol structures and envelope encoding
2//!
3//! This module provides transaction envelope building and serialization
4//! that matches the TypeScript SDK implementation exactly.
5
6#![allow(missing_docs)]
7
8use crate::codec::{canonical_json, sha256_bytes};
9use crate::crypto::ed25519_helper::{Ed25519Helper, Keypair};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14pub mod envelope;
15pub mod transaction;
16
17// Re-export envelope and transaction modules (currently empty)
18// pub use envelope::*;
19// pub use transaction::*;
20
21/// Transaction envelope containing transaction and signatures
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct TransactionEnvelope {
24    pub signatures: Vec<TransactionSignature>,
25    pub transaction: Vec<Transaction>,
26}
27
28/// Transaction signature for Accumulate protocol
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30pub struct TransactionSignature {
31    #[serde(rename = "type")]
32    pub signature_type: String,
33    #[serde(rename = "publicKey")]
34    pub public_key: String,
35    pub signature: String,
36    pub signer: String,
37    #[serde(rename = "signerVersion")]
38    pub signer_version: u64,
39    pub timestamp: u64,
40    #[serde(rename = "transactionHash")]
41    pub transaction_hash: String,
42}
43
44/// Transaction structure
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46pub struct Transaction {
47    pub header: TransactionHeader,
48    pub body: Value,
49}
50
51/// Transaction header
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53pub struct TransactionHeader {
54    pub principal: String,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub initiator: Option<String>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub timestamp: Option<u64>,
59}
60
61/// Envelope builder for creating transaction envelopes
62#[derive(Debug, Clone, Copy)]
63pub struct EnvelopeBuilder;
64
65impl EnvelopeBuilder {
66    /// Create a new transaction envelope with signature
67    pub fn create_envelope(
68        transaction: Transaction,
69        keypair: &Keypair,
70        signer_url: &str,
71        signer_version: u64,
72    ) -> Result<TransactionEnvelope, EnvelopeError> {
73        // Serialize transaction to canonical JSON
74        let tx_value = serde_json::to_value(&transaction)?;
75        let canonical = canonical_json(&tx_value);
76
77        // Hash the canonical transaction
78        let tx_hash = sha256_bytes(canonical.as_bytes());
79        let tx_hash_hex = hex::encode(tx_hash);
80
81        // Sign the transaction hash
82        let signature = Ed25519Helper::sign_bytes(keypair, &tx_hash);
83
84        // Get current timestamp in microseconds
85        let timestamp = SystemTime::now()
86            .duration_since(UNIX_EPOCH)
87            .map_err(|e| EnvelopeError::TimestampError(e.to_string()))?
88            .as_micros() as u64;
89
90        // Create signature object
91        let tx_signature = TransactionSignature {
92            signature_type: "ed25519".to_string(),
93            public_key: hex::encode(Ed25519Helper::public_key_bytes(keypair)),
94            signature: hex::encode(signature.to_bytes()),
95            signer: signer_url.to_string(),
96            signer_version,
97            timestamp,
98            transaction_hash: tx_hash_hex,
99        };
100
101        Ok(TransactionEnvelope {
102            signatures: vec![tx_signature],
103            transaction: vec![transaction],
104        })
105    }
106
107    /// Create envelope from JSON transaction body
108    pub fn create_envelope_from_json(
109        principal: &str,
110        body: Value,
111        keypair: &Keypair,
112        signer_url: &str,
113        signer_version: u64,
114    ) -> Result<TransactionEnvelope, EnvelopeError> {
115        let header = TransactionHeader {
116            principal: principal.to_string(),
117            initiator: None,
118            timestamp: None,
119        };
120
121        let transaction = Transaction { header, body };
122
123        Self::create_envelope(transaction, keypair, signer_url, signer_version)
124    }
125
126    /// Create envelope with initiator
127    pub fn create_envelope_with_initiator(
128        principal: &str,
129        initiator: &str,
130        body: Value,
131        keypair: &Keypair,
132        signer_url: &str,
133        signer_version: u64,
134    ) -> Result<TransactionEnvelope, EnvelopeError> {
135        let header = TransactionHeader {
136            principal: principal.to_string(),
137            initiator: Some(initiator.to_string()),
138            timestamp: None,
139        };
140
141        let transaction = Transaction { header, body };
142
143        Self::create_envelope(transaction, keypair, signer_url, signer_version)
144    }
145
146    /// Serialize envelope to canonical JSON
147    pub fn serialize_envelope(envelope: &TransactionEnvelope) -> Result<String, EnvelopeError> {
148        let value = serde_json::to_value(envelope)?;
149        Ok(canonical_json(&value))
150    }
151
152    /// Verify envelope signature
153    pub fn verify_envelope(envelope: &TransactionEnvelope) -> Result<(), EnvelopeError> {
154        if envelope.signatures.is_empty() || envelope.transaction.is_empty() {
155            return Err(EnvelopeError::InvalidEnvelope(
156                "Missing signatures or transactions".to_string(),
157            ));
158        }
159
160        let transaction = &envelope.transaction[0];
161        let signature = &envelope.signatures[0];
162
163        // Recreate transaction hash
164        let tx_value = serde_json::to_value(transaction)?;
165        let canonical = canonical_json(&tx_value);
166        let computed_hash = hex::encode(sha256_bytes(canonical.as_bytes()));
167
168        // Verify hash matches
169        if computed_hash != signature.transaction_hash {
170            return Err(EnvelopeError::HashMismatch {
171                expected: signature.transaction_hash.clone(),
172                computed: computed_hash,
173            });
174        }
175
176        // Verify signature
177        let public_key_bytes = hex::decode(&signature.public_key)
178            .map_err(|e| EnvelopeError::InvalidSignature(e.to_string()))?;
179        let signature_bytes = hex::decode(&signature.signature)
180            .map_err(|e| EnvelopeError::InvalidSignature(e.to_string()))?;
181
182        if public_key_bytes.len() != 32 || signature_bytes.len() != 64 {
183            return Err(EnvelopeError::InvalidSignature(
184                "Invalid key or signature length".to_string(),
185            ));
186        }
187
188        let mut pk_array = [0u8; 32];
189        let mut sig_array = [0u8; 64];
190        pk_array.copy_from_slice(&public_key_bytes);
191        sig_array.copy_from_slice(&signature_bytes);
192
193        let public_key = Ed25519Helper::public_key_from_bytes(&pk_array)
194            .map_err(|e| EnvelopeError::InvalidSignature(e.to_string()))?;
195        let signature_obj = Ed25519Helper::signature_from_bytes(&sig_array)
196            .map_err(|e| EnvelopeError::InvalidSignature(e.to_string()))?;
197
198        let tx_hash_bytes = hex::decode(&signature.transaction_hash)
199            .map_err(|e| EnvelopeError::InvalidSignature(e.to_string()))?;
200
201        Ed25519Helper::verify(&public_key, &tx_hash_bytes, &signature_obj)
202            .map_err(|e| EnvelopeError::VerificationFailed(e.to_string()))?;
203
204        Ok(())
205    }
206}
207
208/// Envelope-related errors
209#[derive(Debug, thiserror::Error)]
210pub enum EnvelopeError {
211    #[error("JSON serialization error: {0}")]
212    JsonError(#[from] serde_json::Error),
213
214    #[error("Timestamp error: {0}")]
215    TimestampError(String),
216
217    #[error("Invalid envelope: {0}")]
218    InvalidEnvelope(String),
219
220    #[error("Hash mismatch: expected {expected}, computed {computed}")]
221    HashMismatch { expected: String, computed: String },
222
223    #[error("Invalid signature: {0}")]
224    InvalidSignature(String),
225
226    #[error("Signature verification failed: {0}")]
227    VerificationFailed(String),
228}
229
230/// Helper functions for common transaction types
231pub mod helpers {
232    use super::*;
233    use serde_json::json;
234
235    /// Create send tokens transaction body
236    pub fn create_send_tokens_body(to_url: &str, amount: &str, _token_url: Option<&str>) -> Value {
237        json!({
238            "type": "sendTokens",
239            "to": [{
240                "url": to_url,
241                "amount": amount
242            }]
243        })
244    }
245
246    /// Create create identity transaction body
247    pub fn create_identity_body(url: &str, public_key_hash: &str) -> Value {
248        json!({
249            "type": "createIdentity",
250            "url": url,
251            "keyBook": {
252                "publicKeyHash": public_key_hash
253            }
254        })
255    }
256
257    /// Create add credits transaction body
258    pub fn create_add_credits_body(recipient: &str, amount: u64, oracle: Option<&str>) -> Value {
259        json!({
260            "type": "addCredits",
261            "recipient": recipient,
262            "amount": amount,
263            "oracle": oracle.unwrap_or("")
264        })
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use serde_json::json;
272
273    #[test]
274    fn test_envelope_creation() {
275        let hex_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
276        let keypair = Ed25519Helper::keypair_from_hex(hex_key).unwrap();
277
278        let body = helpers::create_send_tokens_body("acc://bob.acme/tokens", "1000", None);
279
280        let envelope = EnvelopeBuilder::create_envelope_from_json(
281            "acc://alice.acme/tokens",
282            body,
283            &keypair,
284            "acc://alice.acme/book/1",
285            1,
286        )
287        .unwrap();
288
289        assert_eq!(envelope.signatures.len(), 1);
290        assert_eq!(envelope.transaction.len(), 1);
291        assert_eq!(envelope.signatures[0].signature_type, "ed25519");
292        assert_eq!(envelope.transaction[0].header.principal, "acc://alice.acme/tokens");
293    }
294
295    #[test]
296    fn test_envelope_serialization() {
297        let hex_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
298        let keypair = Ed25519Helper::keypair_from_hex(hex_key).unwrap();
299
300        let body = helpers::create_send_tokens_body("acc://bob.acme/tokens", "1000", None);
301
302        let envelope = EnvelopeBuilder::create_envelope_from_json(
303            "acc://alice.acme/tokens",
304            body,
305            &keypair,
306            "acc://alice.acme/book/1",
307            1,
308        )
309        .unwrap();
310
311        let serialized = EnvelopeBuilder::serialize_envelope(&envelope).unwrap();
312        assert!(serialized.contains("signatures"));
313        assert!(serialized.contains("transaction"));
314        assert!(serialized.contains("ed25519"));
315    }
316
317    #[test]
318    fn test_envelope_verification() {
319        let hex_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
320        let keypair = Ed25519Helper::keypair_from_hex(hex_key).unwrap();
321
322        let body = helpers::create_send_tokens_body("acc://bob.acme/tokens", "1000", None);
323
324        let envelope = EnvelopeBuilder::create_envelope_from_json(
325            "acc://alice.acme/tokens",
326            body,
327            &keypair,
328            "acc://alice.acme/book/1",
329            1,
330        )
331        .unwrap();
332
333        let result = EnvelopeBuilder::verify_envelope(&envelope);
334        assert!(result.is_ok());
335    }
336
337    #[test]
338    fn test_transaction_helpers() {
339        let send_body = helpers::create_send_tokens_body("acc://recipient", "500", None);
340        assert_eq!(send_body["type"], "sendTokens");
341
342        let identity_body = helpers::create_identity_body("acc://new-identity", "pubkey123");
343        assert_eq!(identity_body["type"], "createIdentity");
344
345        let credits_body = helpers::create_add_credits_body("acc://recipient", 1000, None);
346        assert_eq!(credits_body["type"], "addCredits");
347    }
348}