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