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("integer", "number or decimal/hex string", other)),
90 },
91 ParamType::Boolean => match value {
92 Value::Bool(_) | Value::Number(_) | Value::String(_) => Ok(leaf("bool", value, nested)),
94 other => Err(wrong_shape("boolean", "bool", other)),
95 },
96 ParamType::Bytes => match value {
97 Value::String(_) | Value::Object(_) => Ok(leaf("bytes", value, nested)),
98 other => Err(wrong_shape("bytes", "hex string or bytes envelope", other)),
99 },
100 ParamType::Address => match value {
101 Value::String(_) => Ok(leaf("address", value, nested)),
102 other => Err(wrong_shape("address", "bech32 or hex string", other)),
103 },
104 ParamType::UtxoRef => match value {
105 Value::String(_) => Ok(leaf("utxoRef", value, nested)),
106 other => Err(wrong_shape("utxoRef", "txid#index string", other)),
107 },
108
109 ParamType::Unit => Ok(json!({ "struct": { "constructor": 0, "fields": [] } })),
111
112 ParamType::List(inner) => {
113 let items = value
114 .as_array()
115 .ok_or_else(|| wrong_shape("list", "array", value))?;
116 let encoded = items
117 .iter()
118 .map(|v| marshal(inner, v, true))
119 .collect::<Result<Vec<_>, _>>()?;
120 Ok(json!({ "list": encoded }))
121 }
122
123 ParamType::Tuple(elem_types) => {
124 let items = value
125 .as_array()
126 .ok_or_else(|| wrong_shape("tuple", "array", value))?;
127 if items.len() != elem_types.len() {
128 return Err(EncodeError::TupleArity {
129 expected: elem_types.len(),
130 got: items.len(),
131 });
132 }
133 let encoded = elem_types
134 .iter()
135 .zip(items)
136 .map(|(t, v)| marshal(t, v, true))
137 .collect::<Result<Vec<_>, _>>()?;
138 Ok(json!({ "tuple": encoded }))
139 }
140
141 ParamType::Map(value_type) => {
142 let obj = value
143 .as_object()
144 .ok_or_else(|| wrong_shape("map", "object", value))?;
145 let mut keys: Vec<&String> = obj.keys().collect();
148 keys.sort();
149 let pairs = keys
150 .into_iter()
151 .map(|k| Ok(json!([json!({ "string": k }), marshal(value_type, &obj[k], true)?])))
152 .collect::<Result<Vec<_>, EncodeError>>()?;
153 Ok(json!({ "map": pairs }))
154 }
155
156 ParamType::Record(fields) => Ok(json!({
159 "struct": { "constructor": 0, "fields": marshal_record_fields(fields, value)? }
160 })),
161
162 ParamType::Variant(cases) => marshal_variant(cases, value),
163
164 ParamType::Utxo | ParamType::AnyAsset | ParamType::Unknown(_) => Ok(value.clone()),
167 }
168}
169
170fn leaf(tag: &str, value: &Value, nested: bool) -> Value {
173 if nested {
174 json!({ tag: value })
175 } else {
176 value.clone()
177 }
178}
179
180fn wrong_shape(kind: &'static str, expected: &'static str, got: &Value) -> EncodeError {
181 EncodeError::WrongShape {
182 kind,
183 expected,
184 got: shape_of(got).to_string(),
185 }
186}
187
188fn marshal_record_fields(
191 fields: &[(String, ParamType)],
192 value: &Value,
193) -> Result<Vec<Value>, EncodeError> {
194 let obj = value
195 .as_object()
196 .ok_or_else(|| wrong_shape("record", "object", value))?;
197
198 for key in obj.keys() {
199 if !fields.iter().any(|(name, _)| name == key) {
200 return Err(EncodeError::UnknownField(key.clone()));
201 }
202 }
203
204 fields
205 .iter()
206 .map(|(name, ty)| {
207 let field_value = obj
208 .get(name)
209 .ok_or_else(|| EncodeError::MissingField(name.clone()))?;
210 marshal(ty, field_value, true)
211 })
212 .collect()
213}
214
215fn marshal_variant(cases: &[VariantCase], value: &Value) -> Result<Value, EncodeError> {
218 let obj = value.as_object().ok_or(EncodeError::BadVariant)?;
219 if obj.len() != 1 {
220 return Err(EncodeError::BadVariant);
221 }
222 let (tag, payload) = obj.iter().next().expect("one entry");
223
224 let index = cases
225 .iter()
226 .position(|c| &c.tag == tag)
227 .ok_or_else(|| EncodeError::UnknownCase(tag.clone()))?;
228
229 let fields = match &*cases[index].fields {
230 ParamType::Record(field_types) => marshal_record_fields(field_types, payload)?,
231 other => vec![marshal(other, payload, true)?],
233 };
234
235 Ok(json!({ "struct": { "constructor": index, "fields": fields } }))
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use serde_json::json;
242 use std::collections::HashMap;
243
244 fn param_type(schema: &Value, components: &HashMap<String, Value>) -> ParamType {
247 ParamType::from_json_schema(schema, components)
248 }
249
250 fn wire_vectors() -> Value {
254 let manifest = env!("CARGO_MANIFEST_DIR");
255 let candidates = [
256 format!("{manifest}/tests/fixtures/wire-vectors.json"),
257 format!("{manifest}/../../sdk-spec/test-vectors/complex-types/wire-vectors.json"),
258 format!("{manifest}/../../../sdks/sdk-spec/test-vectors/complex-types/wire-vectors.json"),
259 ];
260 for path in candidates {
261 if let Ok(contents) = std::fs::read_to_string(&path) {
262 return serde_json::from_str(&contents).expect("wire-vectors.json parses");
263 }
264 }
265 panic!("could not locate wire-vectors.json in any known path");
266 }
267
268 fn components(vectors: &Value) -> HashMap<String, Value> {
269 vectors["components"]
270 .as_object()
271 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
272 .unwrap_or_default()
273 }
274
275 #[test]
276 fn encodes_all_accept_vectors() {
277 let vectors = wire_vectors();
278 let components = components(&vectors);
279
280 for vector in vectors["accept"].as_array().unwrap() {
281 let name = vector["name"].as_str().unwrap();
282 let param = param_type(&vector["schema"], &components);
283 let got = encode(¶m, &vector["value"])
284 .unwrap_or_else(|e| panic!("vector `{name}` failed to encode: {e}"));
285 assert_eq!(got, vector["tagged"], "vector `{name}` wire mismatch");
286 }
287 }
288
289 #[test]
290 fn rejects_all_reject_vectors() {
291 let vectors = wire_vectors();
292 let components = components(&vectors);
293
294 for vector in vectors["reject"].as_array().unwrap() {
295 let name = vector["name"].as_str().unwrap();
296 let param = param_type(&vector["schema"], &components);
297 let result = encode(¶m, &vector["value"]);
298 assert!(
299 result.is_err(),
300 "vector `{name}` should have been rejected, got {result:?}"
301 );
302 }
303 }
304
305 #[test]
306 fn record_field_order_follows_required_not_alphabetical() {
307 let schema = json!({
311 "type": "object",
312 "properties": {
313 "level": { "type": "integer" },
314 "tags": { "type": "array", "items": { "type": "integer" } }
315 },
316 "required": ["tags", "level"]
317 });
318 let param = param_type(&schema, &HashMap::new());
319 let got = encode(¶m, &json!({ "level": 7, "tags": [1, 2, 3] })).unwrap();
320 assert_eq!(
321 got,
322 json!({
323 "struct": {
324 "constructor": 0,
325 "fields": [{ "list": [{ "int": 1 }, { "int": 2 }, { "int": 3 }] }, { "int": 7 }]
326 }
327 })
328 );
329 }
330
331 #[test]
332 fn top_level_scalars_render_bare() {
333 let int = param_type(&json!({ "type": "integer" }), &HashMap::new());
335 assert_eq!(encode(&int, &json!(5)).unwrap(), json!(5));
336
337 let bytes = param_type(
338 &json!({ "$ref": "https://tx3.land/specs/v1beta0/tii#/$defs/Bytes" }),
339 &HashMap::new(),
340 );
341 assert_eq!(encode(&bytes, &json!("cafe")).unwrap(), json!("cafe"));
342 }
343
344 #[test]
345 fn nested_scalars_are_tagged() {
346 let list = param_type(
348 &json!({ "type": "array", "items": { "type": "integer" } }),
349 &HashMap::new(),
350 );
351 assert_eq!(
352 encode(&list, &json!([5])).unwrap(),
353 json!({ "list": [{ "int": 5 }] })
354 );
355 }
356}