1use serde::{Deserialize, Serialize};
2
3use crate::protocol::fragmentation::Fragment;
4use nox_crypto::sphinx::surb::Surb;
5
6pub const PAYLOAD_VERSION: u8 = 1;
8
9pub fn encode_payload<T: Serialize>(value: &T) -> Result<Vec<u8>, String> {
11 let body = bincode::serialize(value).map_err(|e| e.to_string())?;
12 let mut out = Vec::with_capacity(1 + body.len());
13 out.push(PAYLOAD_VERSION);
14 out.extend_from_slice(&body);
15 Ok(out)
16}
17
18pub fn decode_payload<T: for<'de> Deserialize<'de>>(bytes: &[u8]) -> Result<T, String> {
20 match bytes.split_first() {
21 None => Err("empty payload bytes".into()),
22 Some((&ver, body)) => {
23 if ver != PAYLOAD_VERSION {
24 return Err(format!("unsupported payload version {ver}"));
25 }
26 bincode::deserialize(body).map_err(|e| e.to_string())
27 }
28 }
29}
30
31pub fn decode_payload_limited<T: for<'de> Deserialize<'de>>(
33 bytes: &[u8],
34 max_bytes: u64,
35) -> Result<T, String> {
36 use bincode::Options;
37 match bytes.split_first() {
38 None => Err("empty payload bytes".into()),
39 Some((&ver, body)) => {
40 if ver != PAYLOAD_VERSION {
41 return Err(format!("unsupported payload version {ver}"));
42 }
43 bincode::DefaultOptions::new()
44 .with_limit(max_bytes)
45 .with_fixint_encoding()
46 .allow_trailing_bytes()
47 .deserialize(body)
48 .map_err(|e| e.to_string())
49 }
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub enum RelayerPayload {
55 SubmitTransaction {
57 to: [u8; 20],
58 data: Vec<u8>,
59 },
60 Dummy {
61 padding: Vec<u8>,
62 },
63 Heartbeat {
64 id: u64,
65 timestamp: u64,
66 },
67 Fragment {
68 frag: Fragment,
69 },
70 AnonymousRequest {
71 inner: Vec<u8>,
72 reply_surbs: Vec<Surb>,
73 },
74 ServiceResponse {
75 request_id: u64,
76 fragment: Fragment,
77 },
78 NeedMoreSurbs {
81 request_id: u64,
82 fragments_remaining: u32,
83 },
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub enum ServiceRequest {
88 Echo {
89 data: Vec<u8>,
90 },
91 HttpRequest {
92 method: String,
93 url: String,
94 headers: Vec<(String, String)>,
95 body: Vec<u8>,
96 },
97 RpcRequest {
99 method: String,
100 params: Vec<u8>,
101 id: u64,
102 rpc_url: Option<String>,
103 },
104 SubmitTransaction {
105 to: [u8; 20],
106 data: Vec<u8>,
107 },
108 BroadcastSignedTransaction {
110 signed_tx: Vec<u8>,
111 rpc_url: Option<String>,
112 rpc_method: Option<String>,
114 },
115 ReplenishSurbs {
117 request_id: u64,
118 surbs: Vec<nox_crypto::sphinx::surb::Surb>,
119 },
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct RpcResponse {
124 pub id: u64,
125 pub result: Result<Vec<u8>, String>,
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_broadcast_signed_transaction_roundtrip() {
134 let fake_signed_tx = vec![0xf8, 0x65, 0x80, 0x84, 0x3b, 0x9a, 0xca, 0x00];
135 let request = ServiceRequest::BroadcastSignedTransaction {
136 signed_tx: fake_signed_tx.clone(),
137 rpc_url: None,
138 rpc_method: None,
139 };
140
141 let encoded = encode_payload(&request).expect("encode");
142 assert_eq!(encoded[0], PAYLOAD_VERSION);
143
144 let decoded: ServiceRequest = decode_payload(&encoded).expect("decode");
145
146 match decoded {
147 ServiceRequest::BroadcastSignedTransaction {
148 signed_tx,
149 rpc_url,
150 rpc_method,
151 } => {
152 assert_eq!(signed_tx, fake_signed_tx);
153 assert!(rpc_url.is_none());
154 assert!(rpc_method.is_none());
155 }
156 other => panic!("expected BroadcastSignedTransaction, got {other:?}"),
157 }
158 }
159
160 #[test]
161 fn test_broadcast_with_custom_rpc_roundtrip() {
162 let request = ServiceRequest::BroadcastSignedTransaction {
163 signed_tx: vec![0xf8, 0x65],
164 rpc_url: Some("https://rpc.ankr.com/eth".to_string()),
165 rpc_method: Some("sendTransaction".to_string()),
166 };
167
168 let encoded = encode_payload(&request).expect("encode");
169 let decoded: ServiceRequest = decode_payload(&encoded).expect("decode");
170
171 match decoded {
172 ServiceRequest::BroadcastSignedTransaction {
173 signed_tx,
174 rpc_url,
175 rpc_method,
176 } => {
177 assert_eq!(signed_tx, vec![0xf8, 0x65]);
178 assert_eq!(rpc_url.as_deref(), Some("https://rpc.ankr.com/eth"));
179 assert_eq!(rpc_method.as_deref(), Some("sendTransaction"));
180 }
181 other => panic!("expected BroadcastSignedTransaction, got {other:?}"),
182 }
183 }
184
185 #[test]
186 fn test_relayer_payload_roundtrip() {
187 let payload = RelayerPayload::Heartbeat {
188 id: 42,
189 timestamp: 1_700_000_000,
190 };
191 let encoded = encode_payload(&payload).expect("encode");
192 assert_eq!(encoded[0], PAYLOAD_VERSION);
193 let decoded: RelayerPayload = decode_payload(&encoded).expect("decode");
194 match decoded {
195 RelayerPayload::Heartbeat { id, timestamp } => {
196 assert_eq!(id, 42);
197 assert_eq!(timestamp, 1_700_000_000);
198 }
199 other => panic!("expected Heartbeat, got {other:?}"),
200 }
201 }
202
203 #[test]
204 fn test_decode_rejects_unsupported_version() {
205 let mut bad = vec![99u8];
207 bad.extend_from_slice(
208 &bincode::serialize(&ServiceRequest::Echo { data: vec![1] }).unwrap(),
209 );
210 let result: Result<ServiceRequest, _> = decode_payload(&bad);
211 assert!(result.is_err());
212 assert!(result.unwrap_err().contains("unsupported payload version"));
213 }
214
215 #[test]
216 fn test_decode_rejects_empty_bytes() {
217 let result: Result<ServiceRequest, _> = decode_payload(&[]);
218 assert!(result.is_err());
219 }
220
221 #[test]
223 fn test_ts_sdk_anonymous_request_decode() {
224 let ts_bytes: Vec<u8> = vec![
225 0x01, 0x04, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
226 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x45,
227 0x54, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x74, 0x74, 0x70, 0x73,
228 0x3a, 0x2f, 0x2f, 0x68, 0x74, 0x74, 0x70, 0x62, 0x69, 0x6e, 0x2e, 0x6f, 0x72, 0x67,
229 0x2f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x2f, 0x31, 0x30, 0x32, 0x34, 0x00, 0x00, 0x00,
230 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
231 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
232 ];
233 assert_eq!(ts_bytes.len(), 91);
234
235 let rust_inner = encode_payload(&ServiceRequest::HttpRequest {
237 method: "GET".to_string(),
238 url: "https://httpbin.org/bytes/1024".to_string(),
239 headers: vec![],
240 body: vec![],
241 })
242 .unwrap();
243 let rust_bytes = encode_payload(&RelayerPayload::AnonymousRequest {
244 inner: rust_inner,
245 reply_surbs: vec![],
246 })
247 .unwrap();
248
249 assert_eq!(ts_bytes, rust_bytes, "TS SDK / Rust encoding mismatch");
250
251 let payload = decode_payload_limited::<RelayerPayload>(&ts_bytes, 65536)
253 .expect("TS SDK bytes must decode successfully");
254 match payload {
255 RelayerPayload::AnonymousRequest { inner, reply_surbs } => {
256 assert!(reply_surbs.is_empty());
257 let sr = decode_payload_limited::<ServiceRequest>(&inner, 65536)
258 .expect("inner ServiceRequest must decode");
259 match sr {
260 ServiceRequest::HttpRequest {
261 method,
262 url,
263 headers,
264 body,
265 } => {
266 assert_eq!(method, "GET");
267 assert_eq!(url, "https://httpbin.org/bytes/1024");
268 assert!(headers.is_empty());
269 assert!(body.is_empty());
270 }
271 other => panic!(
272 "expected HttpRequest, got variant {:?}",
273 std::mem::discriminant(&other)
274 ),
275 }
276 }
277 other => panic!(
278 "expected AnonymousRequest, got variant {:?}",
279 std::mem::discriminant(&other)
280 ),
281 }
282 }
283}