octra_sqlite/protocol/
tx.rs1use serde::Serialize;
2
3#[derive(Clone, PartialEq, Serialize)]
4pub struct Tx {
5 pub from: String,
6 pub to_: String,
7 pub amount: String,
8 pub nonce: i64,
9 pub ou: String,
10 pub timestamp: f64,
11 pub op_type: String,
12 #[serde(skip_serializing_if = "String::is_empty")]
13 pub encrypted_data: String,
14 #[serde(skip_serializing_if = "String::is_empty")]
15 pub message: String,
16 pub signature: String,
17 pub public_key: String,
18}
19
20pub fn canonical_tx(tx: &Tx) -> String {
21 let mut s = String::new();
22 s.push_str("{\"from\":\"");
23 s.push_str(&escape_json_string(&tx.from));
24 s.push_str("\",\"to_\":\"");
25 s.push_str(&escape_json_string(&tx.to_));
26 s.push_str("\",\"amount\":\"");
27 s.push_str(&escape_json_string(&tx.amount));
28 s.push_str("\",\"nonce\":");
29 s.push_str(&tx.nonce.to_string());
30 s.push_str(",\"ou\":\"");
31 s.push_str(&escape_json_string(&tx.ou));
32 s.push_str("\",\"timestamp\":");
33 s.push_str(&canonical_timestamp(tx.timestamp));
34 s.push_str(",\"op_type\":\"");
35 s.push_str(&escape_json_string(&tx.op_type));
36 s.push('"');
37 if !tx.encrypted_data.is_empty() {
38 s.push_str(",\"encrypted_data\":\"");
39 s.push_str(&escape_json_string(&tx.encrypted_data));
40 s.push('"');
41 }
42 if !tx.message.is_empty() {
43 s.push_str(",\"message\":\"");
44 s.push_str(&escape_json_string(&tx.message));
45 s.push('"');
46 }
47 s.push('}');
48 s
49}
50
51fn canonical_timestamp(value: f64) -> String {
52 let mut text = serde_json::to_string(&value).unwrap_or_else(|_| format!("{value}"));
53 if !text.contains('.') && !text.contains('e') && !text.contains('E') {
54 text.push_str(".0");
55 }
56 text
57}
58
59fn escape_json_string(value: &str) -> String {
60 value
61 .replace('\\', "\\\\")
62 .replace('"', "\\\"")
63 .replace('\u{0008}', "\\b")
64 .replace('\u{000c}', "\\f")
65 .replace('\n', "\\n")
66 .replace('\r', "\\r")
67 .replace('\t', "\\t")
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn canonical_tx_omits_empty_optional_fields() {
76 let tx = Tx {
77 from: "octA".into(),
78 to_: "octB".into(),
79 amount: "0".into(),
80 nonce: 7,
81 ou: "200000".into(),
82 timestamp: 1.0,
83 op_type: "deploy_circle".into(),
84 encrypted_data: String::new(),
85 message: "{\"runtime\":\"wasm_v1\",\"code_b64\":\"QUJD\"}".into(),
86 signature: String::new(),
87 public_key: String::new(),
88 };
89 let canonical = canonical_tx(&tx);
90 assert!(!canonical.contains("encrypted_data"));
91 assert!(canonical.contains("\"op_type\":\"deploy_circle\""));
92 assert!(canonical.contains("\\\"runtime\\\":\\\"wasm_v1\\\""));
93 }
94
95 #[test]
96 fn wire_tx_omits_empty_optional_fields() {
97 let tx = Tx {
98 from: "octA".into(),
99 to_: "octB".into(),
100 amount: "0".into(),
101 nonce: 7,
102 ou: "200000".into(),
103 timestamp: 1.0,
104 op_type: "deploy_circle".into(),
105 encrypted_data: String::new(),
106 message: "{\"runtime\":\"wasm_v1\",\"code_b64\":\"QUJD\"}".into(),
107 signature: "sig".into(),
108 public_key: "pub".into(),
109 };
110 let wire = serde_json::to_value(tx).unwrap();
111 assert!(wire.get("encrypted_data").is_none());
112 assert!(wire.get("message").is_some());
113 }
114
115 #[test]
116 fn canonical_tx_matches_field_order() {
117 let tx = Tx {
118 from: "octA".into(),
119 to_: "octB".into(),
120 amount: "0".into(),
121 nonce: 7,
122 ou: "1000".into(),
123 timestamp: 1.0,
124 op_type: "circle_call".into(),
125 encrypted_data: "exec".into(),
126 message: "[\"select 1;\"]".into(),
127 signature: String::new(),
128 public_key: String::new(),
129 };
130 assert_eq!(
131 canonical_tx(&tx),
132 "{\"from\":\"octA\",\"to_\":\"octB\",\"amount\":\"0\",\"nonce\":7,\"ou\":\"1000\",\"timestamp\":1.0,\"op_type\":\"circle_call\",\"encrypted_data\":\"exec\",\"message\":\"[\\\"select 1;\\\"]\"}"
133 );
134 }
135}