1use serde_json::{json, Value};
15use thiserror::Error;
16
17use super::schema::{ParamType, VariantCase};
18
19#[derive(Debug, Error)]
22pub enum EncodeError {
23 #[error("expected {expected} for a `{kind}` argument, got `{got}`")]
25 WrongShape {
26 kind: &'static str,
28 expected: &'static str,
30 got: String,
32 },
33
34 #[error("tuple arity mismatch: expected {expected} element(s), got {got}")]
36 TupleArity {
37 expected: usize,
39 got: usize,
41 },
42
43 #[error("missing record field `{0}`")]
45 MissingField(String),
46
47 #[error("unknown record field `{0}`")]
49 UnknownField(String),
50
51 #[error("unknown variant case `{0}`")]
53 UnknownCase(String),
54
55 #[error("variant value must be a single-key object naming the case")]
57 BadVariant,
58}
59
60fn shape_of(value: &Value) -> &'static str {
62 match value {
63 Value::Null => "null",
64 Value::Bool(_) => "bool",
65 Value::Number(_) => "number",
66 Value::String(_) => "string",
67 Value::Array(_) => "array",
68 Value::Object(_) => "object",
69 }
70}
71
72pub fn encode(param: &ParamType, value: &Value) -> Result<Value, EncodeError> {
80 marshal(param, value, false)
81}
82
83fn marshal(param: &ParamType, value: &Value, nested: bool) -> Result<Value, EncodeError> {
86 match param {
87 ParamType::Integer => match value {
88 Value::Number(_) | Value::String(_) => Ok(leaf("int", value, nested)),
89 other => Err(wrong_shape(
90 "integer",
91 "number or decimal/hex string",
92 other,
93 )),
94 },
95 ParamType::Boolean => match value {
96 Value::Bool(_) | Value::Number(_) | Value::String(_) => Ok(leaf("bool", value, nested)),
98 other => Err(wrong_shape("boolean", "bool", other)),
99 },
100 ParamType::Bytes => match value {
101 Value::String(_) | Value::Object(_) => Ok(leaf("bytes", value, nested)),
102 Value::Array(items) => {
106 let bytes = byte_array(items).ok_or_else(|| {
107 wrong_shape("bytes", "hex string, bytes envelope, or byte array", value)
108 })?;
109 let hex = Value::String(format!("0x{}", hex::encode(bytes)));
110 Ok(leaf("bytes", &hex, nested))
111 }
112 other => Err(wrong_shape(
113 "bytes",
114 "hex string, bytes envelope, or byte array",
115 other,
116 )),
117 },
118 ParamType::Address => match value {
119 Value::String(_) => Ok(leaf("address", value, nested)),
120 other => Err(wrong_shape("address", "bech32 or hex string", other)),
121 },
122 ParamType::UtxoRef => match value {
123 Value::String(_) => Ok(leaf("utxoRef", value, nested)),
124 other => Err(wrong_shape("utxoRef", "txid#index string", other)),
125 },
126
127 ParamType::Unit => Ok(json!({ "struct": { "constructor": 0, "fields": [] } })),
129
130 ParamType::List(inner) => {
131 let items = value
132 .as_array()
133 .ok_or_else(|| wrong_shape("list", "array", value))?;
134 let encoded = items
135 .iter()
136 .map(|v| marshal(inner, v, true))
137 .collect::<Result<Vec<_>, _>>()?;
138 Ok(json!({ "list": encoded }))
139 }
140
141 ParamType::Tuple(elem_types) => {
142 let items = value
143 .as_array()
144 .ok_or_else(|| wrong_shape("tuple", "array", value))?;
145 if items.len() != elem_types.len() {
146 return Err(EncodeError::TupleArity {
147 expected: elem_types.len(),
148 got: items.len(),
149 });
150 }
151 let encoded = elem_types
152 .iter()
153 .zip(items)
154 .map(|(t, v)| marshal(t, v, true))
155 .collect::<Result<Vec<_>, _>>()?;
156 Ok(json!({ "tuple": encoded }))
157 }
158
159 ParamType::Map(value_type) => {
160 let obj = value
161 .as_object()
162 .ok_or_else(|| wrong_shape("map", "object", value))?;
163 let mut keys: Vec<&String> = obj.keys().collect();
166 keys.sort();
167 let pairs = keys
168 .into_iter()
169 .map(|k| {
170 Ok(json!([
171 json!({ "string": k }),
172 marshal(value_type, &obj[k], true)?
173 ]))
174 })
175 .collect::<Result<Vec<_>, EncodeError>>()?;
176 Ok(json!({ "map": pairs }))
177 }
178
179 ParamType::Record(fields) => Ok(json!({
182 "struct": { "constructor": 0, "fields": marshal_record_fields(fields, value)? }
183 })),
184
185 ParamType::Variant(cases) => marshal_variant(cases, value),
186
187 ParamType::Utxo | ParamType::AnyAsset | ParamType::Unknown(_) => Ok(value.clone()),
190 }
191}
192
193fn byte_array(items: &[Value]) -> Option<Vec<u8>> {
196 items
197 .iter()
198 .map(|v| v.as_u64().filter(|b| *b <= u8::MAX as u64).map(|b| b as u8))
199 .collect()
200}
201
202fn leaf(tag: &str, value: &Value, nested: bool) -> Value {
205 if nested {
206 json!({ tag: value })
207 } else {
208 value.clone()
209 }
210}
211
212fn wrong_shape(kind: &'static str, expected: &'static str, got: &Value) -> EncodeError {
213 EncodeError::WrongShape {
214 kind,
215 expected,
216 got: shape_of(got).to_string(),
217 }
218}
219
220fn marshal_record_fields(
223 fields: &[(String, ParamType)],
224 value: &Value,
225) -> Result<Vec<Value>, EncodeError> {
226 let obj = value
227 .as_object()
228 .ok_or_else(|| wrong_shape("record", "object", value))?;
229
230 for key in obj.keys() {
231 if !fields.iter().any(|(name, _)| name == key) {
232 return Err(EncodeError::UnknownField(key.clone()));
233 }
234 }
235
236 fields
237 .iter()
238 .map(|(name, ty)| {
239 let field_value = obj
240 .get(name)
241 .ok_or_else(|| EncodeError::MissingField(name.clone()))?;
242 marshal(ty, field_value, true)
243 })
244 .collect()
245}
246
247fn marshal_variant(cases: &[VariantCase], value: &Value) -> Result<Value, EncodeError> {
250 let obj = value.as_object().ok_or(EncodeError::BadVariant)?;
251 if obj.len() != 1 {
252 return Err(EncodeError::BadVariant);
253 }
254 let (tag, payload) = obj.iter().next().expect("one entry");
255
256 let index = cases
257 .iter()
258 .position(|c| &c.tag == tag)
259 .ok_or_else(|| EncodeError::UnknownCase(tag.clone()))?;
260
261 let fields = match &*cases[index].fields {
262 ParamType::Record(field_types) => marshal_record_fields(field_types, payload)?,
263 other => vec![marshal(other, payload, true)?],
265 };
266
267 Ok(json!({ "struct": { "constructor": index, "fields": fields } }))
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use serde_json::json;
274 use std::collections::HashMap;
275
276 fn param_type(schema: &Value, components: &HashMap<String, Value>) -> ParamType {
279 ParamType::from_json_schema(schema, components)
280 }
281
282 fn wire_vectors() -> Value {
286 let manifest = env!("CARGO_MANIFEST_DIR");
287 let candidates = [
288 format!("{manifest}/tests/fixtures/wire-vectors.json"),
289 format!("{manifest}/../../sdk-spec/test-vectors/complex-types/wire-vectors.json"),
290 format!(
291 "{manifest}/../../../sdks/sdk-spec/test-vectors/complex-types/wire-vectors.json"
292 ),
293 ];
294 for path in candidates {
295 if let Ok(contents) = std::fs::read_to_string(&path) {
296 return serde_json::from_str(&contents).expect("wire-vectors.json parses");
297 }
298 }
299 panic!("could not locate wire-vectors.json in any known path");
300 }
301
302 fn components(vectors: &Value) -> HashMap<String, Value> {
303 vectors["components"]
304 .as_object()
305 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
306 .unwrap_or_default()
307 }
308
309 #[test]
310 fn encodes_all_accept_vectors() {
311 let vectors = wire_vectors();
312 let components = components(&vectors);
313
314 for vector in vectors["accept"].as_array().unwrap() {
315 let name = vector["name"].as_str().unwrap();
316 let param = param_type(&vector["schema"], &components);
317 let got = encode(¶m, &vector["value"])
318 .unwrap_or_else(|e| panic!("vector `{name}` failed to encode: {e}"));
319 assert_eq!(got, vector["tagged"], "vector `{name}` wire mismatch");
320 }
321 }
322
323 #[test]
324 fn rejects_all_reject_vectors() {
325 let vectors = wire_vectors();
326 let components = components(&vectors);
327
328 for vector in vectors["reject"].as_array().unwrap() {
329 let name = vector["name"].as_str().unwrap();
330 let param = param_type(&vector["schema"], &components);
331 let result = encode(¶m, &vector["value"]);
332 assert!(
333 result.is_err(),
334 "vector `{name}` should have been rejected, got {result:?}"
335 );
336 }
337 }
338
339 #[test]
340 fn record_field_order_follows_required_not_alphabetical() {
341 let schema = json!({
345 "type": "object",
346 "properties": {
347 "level": { "type": "integer" },
348 "tags": { "type": "array", "items": { "type": "integer" } }
349 },
350 "required": ["tags", "level"]
351 });
352 let param = param_type(&schema, &HashMap::new());
353 let got = encode(¶m, &json!({ "level": 7, "tags": [1, 2, 3] })).unwrap();
354 assert_eq!(
355 got,
356 json!({
357 "struct": {
358 "constructor": 0,
359 "fields": [{ "list": [{ "int": 1 }, { "int": 2 }, { "int": 3 }] }, { "int": 7 }]
360 }
361 })
362 );
363 }
364
365 #[test]
366 fn top_level_scalars_render_bare() {
367 let int = param_type(&json!({ "type": "integer" }), &HashMap::new());
369 assert_eq!(encode(&int, &json!(5)).unwrap(), json!(5));
370
371 let bytes = param_type(
372 &json!({ "$ref": "https://tx3.land/specs/v1beta0/tii#/$defs/Bytes" }),
373 &HashMap::new(),
374 );
375 assert_eq!(encode(&bytes, &json!("cafe")).unwrap(), json!("cafe"));
376 }
377
378 #[test]
379 fn nested_scalars_are_tagged() {
380 let list = param_type(
382 &json!({ "type": "array", "items": { "type": "integer" } }),
383 &HashMap::new(),
384 );
385 assert_eq!(
386 encode(&list, &json!([5])).unwrap(),
387 json!({ "list": [{ "int": 5 }] })
388 );
389 }
390
391 fn bytes_schema() -> Value {
392 json!({ "$ref": "https://tx3.land/specs/v1beta0/tii#/$defs/Bytes" })
393 }
394
395 fn list_of_bytes_schema() -> Value {
396 json!({ "type": "array", "items": bytes_schema() })
397 }
398
399 #[test]
400 fn native_byte_arrays_canonicalize_to_hex() {
401 let bytes = param_type(&bytes_schema(), &HashMap::new());
405 assert_eq!(
406 encode(&bytes, &json!([1, 1])).unwrap(),
407 json!("0x0101"),
408 "top-level byte array renders bare canonical hex"
409 );
410
411 let list = param_type(&list_of_bytes_schema(), &HashMap::new());
412 assert_eq!(
413 encode(&list, &json!([[1, 2]])).unwrap(),
414 json!({ "list": [{ "bytes": "0x0102" }] }),
415 "nested byte array renders tagged canonical hex"
416 );
417 }
418
419 #[test]
420 fn rejects_non_byte_arrays_for_bytes() {
421 let bytes = param_type(&bytes_schema(), &HashMap::new());
422 assert!(encode(&bytes, &json!([1, 256])).is_err());
423 assert!(encode(&bytes, &json!([1, -1])).is_err());
424 assert!(encode(&bytes, &json!(["aa", 1])).is_err());
425 assert!(encode(&bytes, &json!(true)).is_err());
426 }
427
428 #[test]
429 fn hydra_init_arg_shapes() {
430 let list = param_type(&list_of_bytes_schema(), &HashMap::new());
437
438 assert_eq!(
440 encode(&list, &json!(["0102", "0304"])).unwrap(),
441 json!({ "list": [{ "bytes": "0102" }, { "bytes": "0304" }] })
442 );
443 assert_eq!(
445 encode(&list, &json!([[1, 2]])).unwrap(),
446 json!({ "list": [{ "bytes": "0x0102" }] })
447 );
448 assert_eq!(
450 encode(&list, &json!([[222, 173, 190, 239]])).unwrap(),
451 json!({ "list": [{ "bytes": "0xdeadbeef" }] })
452 );
453 let bytes = param_type(&bytes_schema(), &HashMap::new());
455 assert_eq!(
456 encode(&bytes, &json!("abcd0123")).unwrap(),
457 json!("abcd0123")
458 );
459 }
460
461 #[test]
462 fn asteria_name_arg_shapes() {
463 let bytes = param_type(&bytes_schema(), &HashMap::new());
467 assert_eq!(
468 encode(&bytes, &json!("53484950313233")).unwrap(),
469 json!("53484950313233")
470 );
471 assert_eq!(
472 encode(&bytes, &json!([83, 72, 73, 80])).unwrap(),
473 json!("0x53484950")
474 );
475 }
476}