Skip to main content

kaspa_consensus_client/
covenant.rs

1//!
2//! Client-side WASM wrapper for [`kaspa_consensus_core::tx::GenesisCovenantGroup`].
3//!
4
5#![allow(non_snake_case)]
6
7use crate::imports::*;
8use crate::result::Result as ClientResult;
9use borsh::{BorshDeserialize, BorshSerialize};
10use kaspa_consensus_core::errors::tx::PopulateGenesisCovenantsError;
11use kaspa_consensus_core::tx::{CovenantBinding as CoreCovenantBinding, GenesisCovenantGroup as CoreGenesisCovenantGroup};
12use kaspa_hashes::Hash;
13use kaspa_wasm_core::types::NumberArray;
14use workflow_wasm::error::Error as WasmError;
15
16#[wasm_bindgen(typescript_custom_section)]
17const TS_COVENANT_BINDING: &'static str = r#"
18/**
19 * A covenant binding binds a transaction output to the covenant and input authorizing its creation.
20 *
21 * @category Consensus
22 */
23export interface ICovenantBinding {
24    authorizingInput: number;
25    covenantId: HexString;
26}
27"#;
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Copy, BorshSerialize, BorshDeserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct CovenantBindingInner {
32    pub authorizing_input: u16,
33    pub covenant_id: Hash,
34}
35
36impl CovenantBindingInner {
37    pub fn new(authorizing_input: u16, covenant_id: Hash) -> Self {
38        Self { authorizing_input, covenant_id }
39    }
40}
41
42#[derive(Debug, Serialize, Deserialize, Clone, CastFromJs)]
43#[serde(rename_all = "camelCase")]
44#[wasm_bindgen(inspectable)]
45pub struct CovenantBinding {
46    inner: Arc<Mutex<CovenantBindingInner>>,
47}
48
49impl BorshSerialize for CovenantBinding {
50    fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
51        let inner = self.inner.lock().unwrap();
52        BorshSerialize::serialize(&*inner, writer)
53    }
54}
55
56impl BorshDeserialize for CovenantBinding {
57    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
58        let inner = CovenantBindingInner::deserialize_reader(reader)?;
59        Ok(Self { inner: Arc::new(Mutex::new(inner)) })
60    }
61}
62
63impl CovenantBinding {
64    pub fn inner(&self) -> MutexGuard<'_, CovenantBindingInner> {
65        self.inner.lock().unwrap()
66    }
67}
68
69#[wasm_bindgen]
70impl CovenantBinding {
71    #[wasm_bindgen(constructor)]
72    pub fn new(authorizing_input: u16, covenant_id: Hash) -> Self {
73        let inner = Arc::new(Mutex::new(CovenantBindingInner::new(authorizing_input, covenant_id)));
74        Self { inner }
75    }
76
77    #[wasm_bindgen(setter, js_name = authorizingInput)]
78    pub fn set_authorizing_input(&mut self, v: u16) {
79        self.inner().authorizing_input = v;
80    }
81
82    #[wasm_bindgen(getter, js_name = authorizingInput)]
83    pub fn get_authorizing_input(&self) -> u16 {
84        self.inner().authorizing_input
85    }
86
87    #[wasm_bindgen(setter, js_name = covenantId)]
88    pub fn set_covenant_id(&mut self, v: Hash) {
89        self.inner().covenant_id = v;
90    }
91
92    #[wasm_bindgen(getter, js_name = covenantId)]
93    pub fn get_covenant_id(&self) -> Hash {
94        self.inner().covenant_id
95    }
96
97    #[wasm_bindgen(js_name = toJSON)]
98    pub fn to_js_object(&self) -> Result<Object, WasmError> {
99        let obj = Object::new();
100        obj.set("authorizingInput", &self.get_authorizing_input().into())?;
101        obj.set("covenantId", &self.get_covenant_id().to_string().into())?;
102        Ok(obj)
103    }
104}
105
106impl From<CoreCovenantBinding> for CovenantBinding {
107    fn from(covenant: CoreCovenantBinding) -> Self {
108        CovenantBinding::new(covenant.authorizing_input, covenant.covenant_id)
109    }
110}
111
112impl From<CovenantBinding> for CoreCovenantBinding {
113    fn from(covenant: CovenantBinding) -> Self {
114        CoreCovenantBinding::new(covenant.get_authorizing_input(), covenant.get_covenant_id())
115    }
116}
117
118impl TryCastFromJs for CovenantBinding {
119    type Error = WasmError;
120
121    fn try_cast_from<'a, R>(value: &'a R) -> Result<Cast<'a, Self>, Self::Error>
122    where
123        R: AsRef<JsValue> + 'a,
124    {
125        Self::resolve(value, || {
126            let Some(object) = Object::try_from(value.as_ref()) else { return Err(Self::Error::NotAnObject) };
127            let authorizing_input = object.get_u16("authorizingInput")?;
128            let covenant_id = object.get_value("covenantId")?.try_into_owned()?;
129            Ok(CovenantBinding::new(authorizing_input, covenant_id))
130        })
131    }
132}
133
134#[wasm_bindgen(typescript_custom_section)]
135const TS_GENESIS_COVENANT_GROUP: &'static str = r#"
136/**
137 * A genesis covenant group for bulk covenant binding population.
138 *
139 * @category Consensus
140 */
141export interface IGenesisCovenantGroup {
142    authorizingInput: number;
143    outputs: number[];
144}
145"#;
146
147#[wasm_bindgen]
148extern "C" {
149    /// WASM (TypeScript) type representing an array of [`GenesisCovenantGroup`] objects.
150    ///
151    /// @category Consensus
152    #[wasm_bindgen(extends = js_sys::Array, typescript_type = "(IGenesisCovenantGroup | GenesisCovenantGroup)[]")]
153    pub type GenesisCovenantGroupArrayT;
154}
155
156impl TryFrom<&GenesisCovenantGroupArrayT> for Vec<CoreGenesisCovenantGroup> {
157    type Error = Error;
158    fn try_from(value: &GenesisCovenantGroupArrayT) -> Result<Self, Self::Error> {
159        if value.is_array() {
160            value.iter().map(|v| GenesisCovenantGroup::try_owned_from(v).map(|gcp| gcp.inner().clone()).map_err(Error::from)).collect()
161        } else {
162            Err(PopulateGenesisCovenantsError::InvalidGenesisCovenantGroupArray.into())
163        }
164    }
165}
166
167/// A genesis covenant group for bulk covenant binding population.
168///
169/// All listed outputs are bound to the same covenant id, derived from the
170/// authorizing input outpoint and this exact ordered output list.
171/// @category Consensus
172#[derive(Debug, Clone, Serialize, Deserialize, CastFromJs)]
173#[serde(rename_all = "camelCase")]
174#[wasm_bindgen(inspectable)]
175pub struct GenesisCovenantGroup {
176    inner: CoreGenesisCovenantGroup,
177}
178
179impl GenesisCovenantGroup {
180    pub fn new(authorizing_input: u16, outputs: Vec<u32>) -> Self {
181        Self { inner: CoreGenesisCovenantGroup::new(authorizing_input, outputs) }
182    }
183
184    pub fn inner(&self) -> &CoreGenesisCovenantGroup {
185        &self.inner
186    }
187
188    pub fn outputs(&self) -> Vec<u32> {
189        self.inner.outputs.clone()
190    }
191
192    pub fn set_outputs(&mut self, outputs: Vec<u32>) {
193        self.inner.outputs = outputs
194    }
195}
196
197impl From<CoreGenesisCovenantGroup> for GenesisCovenantGroup {
198    fn from(inner: CoreGenesisCovenantGroup) -> Self {
199        Self { inner }
200    }
201}
202
203impl From<GenesisCovenantGroup> for CoreGenesisCovenantGroup {
204    fn from(wrapper: GenesisCovenantGroup) -> Self {
205        wrapper.inner
206    }
207}
208
209#[wasm_bindgen]
210impl GenesisCovenantGroup {
211    #[wasm_bindgen(constructor)]
212    pub fn ctor(authorizing_input: u16, outputs: NumberArray) -> ClientResult<Self> {
213        let outputs: Vec<u32> = serde_wasm_bindgen::from_value(outputs.into())?;
214        Ok(Self { inner: CoreGenesisCovenantGroup::new(authorizing_input, outputs) })
215    }
216
217    #[wasm_bindgen(getter, js_name = authorizingInput)]
218    pub fn authorizing_input(&self) -> u16 {
219        self.inner.authorizing_input
220    }
221
222    #[wasm_bindgen(setter, js_name = authorizingInput)]
223    pub fn set_authorizing_input(&mut self, value: u16) {
224        self.inner.authorizing_input = value;
225    }
226
227    #[wasm_bindgen(setter = outputs, js_name = outputs)]
228    pub fn js_set_outputs(&mut self, outputs: NumberArray) -> ClientResult<()> {
229        let outputs: Vec<u32> = serde_wasm_bindgen::from_value(outputs.into())?;
230        self.inner.outputs = outputs;
231        Ok(())
232    }
233
234    #[wasm_bindgen(getter, js_name = outputs)]
235    pub fn js_get_outputs(&self) -> NumberArray {
236        serde_wasm_bindgen::to_value(&self.inner.outputs)
237            .expect("serializing genesis covenant outputs should not fail")
238            .unchecked_into()
239    }
240
241    #[wasm_bindgen(js_name = "toJSON")]
242    pub fn to_js_object(&self) -> Result<Object, WasmError> {
243        let obj = Object::new();
244        obj.set("authorizingInput", &self.inner.authorizing_input.into())?;
245        obj.set("outputs", &js_sys::Array::from_iter(self.inner.outputs.iter().map(|&v| JsValue::from(v))))?;
246        Ok(obj)
247    }
248
249    #[wasm_bindgen(js_name = "toString")]
250    pub fn js_to_string(&self) -> Result<js_sys::JsString, WasmError> {
251        Ok(js_sys::JSON::stringify(&self.to_js_object()?.into())?)
252    }
253}
254
255impl TryCastFromJs for GenesisCovenantGroup {
256    type Error = WasmError;
257
258    fn try_cast_from<'a, R>(value: &'a R) -> Result<Cast<'a, Self>, Self::Error>
259    where
260        R: AsRef<JsValue> + 'a,
261    {
262        Self::resolve(value, || {
263            let Some(object) = Object::try_from(value.as_ref()) else {
264                return Err(Self::Error::NotAnObject);
265            };
266            let authorizing_input = object.get_u16("authorizingInput")?;
267            let outputs: Vec<u32> =
268                serde_wasm_bindgen::from_value(object.get_value("outputs")?).map_err(|err| Self::Error::from(JsValue::from(err)))?;
269            Ok(Self { inner: CoreGenesisCovenantGroup::new(authorizing_input, outputs) })
270        })
271    }
272}
273
274#[cfg(all(test, target_arch = "wasm32"))]
275mod tests {
276    use super::*;
277    use kaspa_hashes::HASH_SIZE;
278    use std::str::FromStr;
279    use wasm_bindgen::JsValue;
280    use wasm_bindgen_test::wasm_bindgen_test;
281
282    // -------------------------------------
283    // GenesisCovenantGroup tests
284
285    // Helper - convert &[u32] to a JS Array
286    fn to_js_array(values: &[u32]) -> js_sys::Array {
287        let arr = js_sys::Array::new();
288        for &v in values {
289            arr.push(&JsValue::from(v));
290        }
291        arr
292    }
293
294    // Helper - construct GenesisCovenantGroup via WASM constructor
295    fn construct_genesis_covenant_group(authorizing_input: u16, outputs: &[u32]) -> GenesisCovenantGroup {
296        GenesisCovenantGroup::ctor(authorizing_input, to_js_array(outputs).unchecked_into()).expect("constructor should succeed")
297    }
298
299    // Helper - build plain GenesisCovenantGroup JS object
300    fn construct_genesis_covenant_group_plain(authorizing_input: u16, outputs: &[u32]) -> JsValue {
301        let obj = Object::new();
302        obj.set("authorizingInput", &JsValue::from(authorizing_input)).expect("set authorizingInput");
303        obj.set("outputs", &to_js_array(outputs).into()).expect("set outputs");
304        obj.into()
305    }
306
307    // Helper - get outputs field
308    fn get_genesis_covenant_group_outputs(group: &GenesisCovenantGroup) -> Vec<u32> {
309        serde_wasm_bindgen::from_value(group.outputs().into()).expect("outputs should deserialize")
310    }
311
312    #[wasm_bindgen_test]
313    fn test_genesis_covenant_group_construction() {
314        let group = construct_genesis_covenant_group(1, &[0, 1, 2]);
315
316        assert_eq!(group.authorizing_input(), 1);
317        assert_eq!(get_genesis_covenant_group_outputs(&group), vec![0, 1, 2]);
318    }
319
320    #[wasm_bindgen_test]
321    fn test_genesis_covenant_group_authorizing_input_setter() {
322        let mut group = construct_genesis_covenant_group(0, &[0, 1]);
323
324        group.set_authorizing_input(7);
325        assert_eq!(group.authorizing_input(), 7);
326    }
327
328    #[wasm_bindgen_test]
329    fn test_genesis_covenant_group_outputs_setter() {
330        let mut group = construct_genesis_covenant_group(0, &[0, 1]);
331
332        group.js_set_outputs(to_js_array(&[3, 4, 5]).unchecked_into()).expect("set_outputs should succeed");
333
334        assert_eq!(get_genesis_covenant_group_outputs(&group), vec![3, 4, 5]);
335    }
336
337    #[wasm_bindgen_test]
338    fn test_genesis_covenant_group_to_json() {
339        let group = construct_genesis_covenant_group(2, &[0, 1]);
340
341        let obj = group.to_js_object().expect("to_js_object should succeed");
342        let auth_input = js_sys::Reflect::get(&obj, &JsValue::from_str("authorizingInput")).expect("should have authorizingInput");
343        assert_eq!(auth_input.as_f64().expect("should be a number"), 2.0);
344
345        let outputs = js_sys::Reflect::get(&obj, &JsValue::from_str("outputs")).expect("should have outputs");
346        let outputs_array: &js_sys::Array = outputs.dyn_ref::<js_sys::Array>().expect("outputs should be an array");
347        assert_eq!(outputs_array.length(), 2);
348        assert_eq!(outputs_array.get(0).as_f64().expect("should be a number"), 0.0);
349        assert_eq!(outputs_array.get(1).as_f64().expect("should be a number"), 1.0);
350
351        // Also verify toString() round-trips through JSON.stringify correctly
352        let json_str: String = group.js_to_string().expect("js_to_string should succeed").into();
353        let parsed: serde_json::Value = serde_json::from_str(&json_str).expect("should be valid JSON");
354        assert_eq!(parsed["authorizingInput"], 2);
355        assert_eq!(parsed["outputs"], serde_json::json!([0, 1]));
356    }
357
358    #[wasm_bindgen_test]
359    fn test_genesis_covenant_group_try_cast_from_plain_object() {
360        let js_val = construct_genesis_covenant_group_plain(1, &[0, 2]);
361        let group = GenesisCovenantGroup::try_owned_from(&js_val).expect("try_cast_from should succeed");
362        assert_eq!(group.inner().authorizing_input, 1);
363        assert_eq!(group.inner().outputs, vec![0, 2]);
364    }
365
366    #[wasm_bindgen_test]
367    fn test_genesis_covenant_group_array_t_from_plain_objects() {
368        let arr = js_sys::Array::new();
369        arr.push(&construct_genesis_covenant_group_plain(0, &[0, 1]));
370        arr.push(&construct_genesis_covenant_group_plain(1, &[2, 3, 4]));
371
372        let typed_arr: &GenesisCovenantGroupArrayT = arr.unchecked_ref();
373        let groups: Vec<CoreGenesisCovenantGroup> = Vec::try_from(typed_arr).expect("should convert from plain objects");
374
375        assert_eq!(groups.len(), 2);
376        assert_eq!(groups[0].authorizing_input, 0);
377        assert_eq!(groups[0].outputs, vec![0, 1]);
378        assert_eq!(groups[1].authorizing_input, 1);
379        assert_eq!(groups[1].outputs, vec![2, 3, 4]);
380    }
381
382    #[wasm_bindgen_test]
383    fn test_genesis_covenant_group_array_t_from_wasm_instances() {
384        let g0 = construct_genesis_covenant_group(5, &[10, 11]);
385        let g1 = construct_genesis_covenant_group(6, &[20]);
386
387        let arr = js_sys::Array::new();
388        arr.push(&JsValue::from(g0));
389        arr.push(&JsValue::from(g1));
390
391        let typed_arr: &GenesisCovenantGroupArrayT = arr.unchecked_ref();
392        let groups: Vec<CoreGenesisCovenantGroup> = Vec::try_from(typed_arr).expect("should convert from WASM instances");
393
394        assert_eq!(groups.len(), 2);
395        assert_eq!(groups[0].authorizing_input, 5);
396        assert_eq!(groups[0].outputs, vec![10, 11]);
397        assert_eq!(groups[1].authorizing_input, 6);
398        assert_eq!(groups[1].outputs, vec![20]);
399    }
400
401    // -------------------------------------
402    // CovenantBinding tests
403
404    #[wasm_bindgen_test]
405    fn test_covenant_binding_construction() {
406        let covenant_id = Hash::from_bytes([0xab; HASH_SIZE]);
407        let binding = CovenantBinding::new(0, covenant_id);
408
409        assert_eq!(binding.get_authorizing_input(), 0);
410        assert_eq!(binding.get_covenant_id(), covenant_id);
411    }
412
413    #[wasm_bindgen_test]
414    fn test_covenant_binding_construction_plain() {
415        let covenant_id = Hash::from_bytes([0xab; HASH_SIZE]);
416
417        let obj = Object::new();
418        obj.set("authorizingInput", &JsValue::from(0)).expect("set authorizingInput");
419        obj.set("covenantId", &JsValue::from_str(&covenant_id.to_string())).expect("set covenantId");
420
421        let binding = CovenantBinding::try_owned_from(obj).expect("try_cast_from failed");
422
423        assert_eq!(binding.get_authorizing_input(), 0);
424        assert_eq!(binding.get_covenant_id(), covenant_id);
425    }
426
427    #[wasm_bindgen_test]
428    fn test_covenant_binding_to_json() {
429        let covenant_id_in = Hash::from_bytes([0xab; HASH_SIZE]);
430        let binding = CovenantBinding::new(0, covenant_id_in);
431
432        let obj = binding.to_js_object().expect("to_js_object failed");
433
434        let authorizing_input_out = obj.get_u16("authorizingInput").expect("failed to get authorizingInput");
435        let covenant_id_out =
436            Hash::from_str(&obj.get_string("covenantId").expect("failed to get covenantId")).expect("failed to create Hash from str");
437
438        assert_eq!(0, authorizing_input_out);
439        assert_eq!(covenant_id_in, covenant_id_out);
440    }
441
442    #[wasm_bindgen_test]
443    fn test_covenant_binding_authorizing_input_setter() {
444        let covenant_id = Hash::from_bytes([0xab; HASH_SIZE]);
445        let mut binding = CovenantBinding::new(0, covenant_id);
446
447        let new_input = 1;
448
449        binding.set_authorizing_input(new_input);
450
451        assert_eq!(binding.get_authorizing_input(), new_input);
452    }
453
454    #[wasm_bindgen_test]
455    fn test_covenant_binding_covenant_id_setter() {
456        let covenant_id = Hash::from_bytes([0xab; HASH_SIZE]);
457        let mut binding = CovenantBinding::new(0, covenant_id);
458
459        let new_covenant_id = Hash::from_bytes([0xac; HASH_SIZE]);
460
461        binding.set_covenant_id(new_covenant_id);
462
463        assert_eq!(binding.get_covenant_id(), new_covenant_id);
464    }
465}