Skip to main content

avalanche_types/txs/
transferable.rs

1use std::cmp::Ordering;
2
3use crate::{ids, key, platformvm, txs};
4use serde::{Deserialize, Serialize};
5
6/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#TransferableOutput>
7/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#TransferableOut>
8/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferOutput>
9#[derive(Debug, Serialize, Deserialize, Eq, Clone)]
10pub struct Output {
11    #[serde(rename = "assetID")]
12    pub asset_id: ids::Id,
13
14    /// Packer skips serialization due to serialize:"false" in avalanchego.
15    #[serde(rename = "fxID", skip_serializing_if = "Option::is_none")]
16    pub fx_id: Option<ids::Id>,
17
18    /// The underlying type is one of the following:
19    ///
20    /// "*secp256k1fx.TransferOutput"
21    /// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferOutput>
22    ///
23    /// "*platformvm.StakeableLockOut" which embeds "*secp256k1fx.TransferOutput"
24    /// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm#StakeableLockOut>
25    ///
26    /// MUST: only one of the following can be "Some".
27    #[serde(rename = "output")]
28    pub transfer_output: Option<key::secp256k1::txs::transfer::Output>,
29    pub stakeable_lock_out: Option<platformvm::txs::StakeableLockOut>,
30}
31
32impl Default for Output {
33    fn default() -> Self {
34        Self {
35            asset_id: ids::Id::empty(),
36            fx_id: None,
37            transfer_output: None,
38            stakeable_lock_out: None,
39        }
40    }
41}
42
43/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#SortTransferableOutputs>
44impl Ord for Output {
45    fn cmp(&self, other: &Output) -> Ordering {
46        let asset_id_ord = self.asset_id.cmp(&(other.asset_id));
47        if asset_id_ord != Ordering::Equal {
48            // no need to compare further
49            return asset_id_ord;
50        }
51        if self.transfer_output.is_none()
52            && self.stakeable_lock_out.is_none()
53            && other.transfer_output.is_none()
54            && other.stakeable_lock_out.is_none()
55        {
56            // should never happen but sorting/ordering shouldn't worry about this...
57            // can't compare anymore, so thus return here...
58            return Ordering::Equal;
59        }
60
61        // unlike "avalanchego", we want ordering without "codec" dependency and marshal-ing
62        // just check type ID header
63        let type_id_self = {
64            if self.transfer_output.is_some() {
65                key::secp256k1::txs::transfer::Output::type_id()
66            } else {
67                platformvm::txs::StakeableLockOut::type_id()
68            }
69        };
70        let type_id_other = {
71            if other.transfer_output.is_some() {
72                key::secp256k1::txs::transfer::Output::type_id()
73            } else {
74                platformvm::txs::StakeableLockOut::type_id()
75            }
76        };
77        let type_id_ord = type_id_self.cmp(&type_id_other);
78        if type_id_ord != Ordering::Equal {
79            // no need to compare further
80            return type_id_ord;
81        }
82
83        // both instances have the same type!!!
84        // just use the ordering of underlying types
85        match type_id_self {
86            7 => {
87                // "key::secp256k1::txs::transfer::Output"
88                // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferOutput
89                let transfer_output_self = self.transfer_output.clone().unwrap();
90                let transfer_output_other = other.transfer_output.clone().unwrap();
91                transfer_output_self.cmp(&transfer_output_other)
92            }
93            22 => {
94                // "platformvm::txs::StakeableLockOut"
95                // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm#StakeableLockOut
96                let stakeable_lock_out_self = self.stakeable_lock_out.clone().unwrap();
97                let stakeable_lock_out_other = other.stakeable_lock_out.clone().unwrap();
98                stakeable_lock_out_self.cmp(&stakeable_lock_out_other)
99            }
100            _ => {
101                // should never happen
102                panic!("unexpected type ID {} for TransferableOutput", type_id_self);
103            }
104        }
105    }
106}
107
108impl PartialOrd for Output {
109    fn partial_cmp(&self, other: &Output) -> Option<Ordering> {
110        Some(self.cmp(other))
111    }
112}
113
114impl PartialEq for Output {
115    fn eq(&self, other: &Output) -> bool {
116        self.cmp(other) == Ordering::Equal
117    }
118}
119
120/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#SortTransferableOutputs>
121/// ref. "avalanchego/vms/components/avax.TestTransferableOutputSorting"
122/// RUST_LOG=debug cargo test --package avalanche-types --lib -- txs::transferable::test_sort_transferable_outputs --exact --show-output
123#[test]
124fn test_sort_transferable_outputs() {
125    use crate::ids::short;
126
127    let mut outputs: Vec<Output> = Vec::new();
128    for i in (0..10).rev() {
129        outputs.push(Output {
130            asset_id: ids::Id::from_slice(&[i as u8, 2, 2, 3, 4, 5, 6, 7, 8, 9]),
131            stakeable_lock_out: Some(platformvm::txs::StakeableLockOut {
132                locktime: i as u64,
133                transfer_output: key::secp256k1::txs::transfer::Output {
134                    amount: (i + 1) as u64,
135                    output_owners: key::secp256k1::txs::OutputOwners {
136                        locktime: i as u64,
137                        threshold: i as u32,
138                        addresses: vec![
139                            short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5]),
140                            short::Id::from_slice(&[i as u8, 2, 2, 3, 4, 5]),
141                        ],
142                    },
143                },
144            }),
145            ..Output::default()
146        });
147        outputs.push(Output {
148            asset_id: ids::Id::from_slice(&[i as u8, 2, 2, 3, 4, 5, 6, 7, 8, 9]),
149            stakeable_lock_out: Some(platformvm::txs::StakeableLockOut {
150                locktime: i as u64,
151                transfer_output: key::secp256k1::txs::transfer::Output {
152                    amount: (i + 1) as u64,
153                    output_owners: key::secp256k1::txs::OutputOwners {
154                        locktime: i as u64,
155                        threshold: i as u32,
156                        addresses: vec![
157                            short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5]),
158                            short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5]),
159                        ],
160                    },
161                },
162            }),
163            ..Output::default()
164        });
165        outputs.push(Output {
166            asset_id: ids::Id::from_slice(&[i as u8, 2, 2, 3, 4, 5, 6, 7, 8, 9]),
167            stakeable_lock_out: Some(platformvm::txs::StakeableLockOut {
168                locktime: i as u64,
169                transfer_output: key::secp256k1::txs::transfer::Output {
170                    amount: (i + 1) as u64,
171                    output_owners: key::secp256k1::txs::OutputOwners {
172                        locktime: i as u64,
173                        threshold: i as u32,
174                        addresses: vec![short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5])],
175                    },
176                },
177            }),
178            ..Output::default()
179        });
180        outputs.push(Output {
181            asset_id: ids::Id::from_slice(&[i as u8, 2, 2, 3, 4, 5, 6, 7, 8, 9]),
182            stakeable_lock_out: Some(platformvm::txs::StakeableLockOut {
183                locktime: i as u64,
184                transfer_output: key::secp256k1::txs::transfer::Output {
185                    amount: i as u64,
186                    output_owners: key::secp256k1::txs::OutputOwners {
187                        locktime: i as u64,
188                        threshold: i as u32,
189                        addresses: vec![short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5])],
190                    },
191                },
192            }),
193            ..Output::default()
194        });
195        outputs.push(Output {
196            asset_id: ids::Id::from_slice(&[i as u8, 2, 2, 3, 4, 5, 6, 7, 8, 9]),
197            transfer_output: Some(key::secp256k1::txs::transfer::Output {
198                amount: i as u64,
199                output_owners: key::secp256k1::txs::OutputOwners {
200                    locktime: i as u64,
201                    threshold: i as u32,
202                    addresses: vec![short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5])],
203                },
204            }),
205            ..Output::default()
206        });
207        outputs.push(Output {
208            asset_id: ids::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
209            stakeable_lock_out: Some(platformvm::txs::StakeableLockOut {
210                locktime: i as u64,
211                transfer_output: key::secp256k1::txs::transfer::Output {
212                    amount: (i + 1) as u64,
213                    output_owners: key::secp256k1::txs::OutputOwners {
214                        locktime: i as u64,
215                        threshold: i as u32,
216                        addresses: vec![short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5])],
217                    },
218                },
219            }),
220            ..Output::default()
221        });
222        outputs.push(Output {
223            asset_id: ids::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
224            stakeable_lock_out: Some(platformvm::txs::StakeableLockOut {
225                locktime: i as u64,
226                transfer_output: key::secp256k1::txs::transfer::Output {
227                    amount: i as u64,
228                    output_owners: key::secp256k1::txs::OutputOwners {
229                        locktime: i as u64,
230                        threshold: i as u32,
231                        addresses: vec![short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5])],
232                    },
233                },
234            }),
235            ..Output::default()
236        });
237        outputs.push(Output {
238            asset_id: ids::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
239            transfer_output: Some(key::secp256k1::txs::transfer::Output {
240                amount: i as u64,
241                output_owners: key::secp256k1::txs::OutputOwners {
242                    locktime: i as u64,
243                    threshold: i as u32,
244                    addresses: vec![short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5])],
245                },
246            }),
247            ..Output::default()
248        });
249    }
250    assert!(!cmp_manager::is_sorted_and_unique(&outputs));
251    outputs.sort();
252
253    let mut sorted_outputs: Vec<Output> = Vec::new();
254    for i in 0..10 {
255        sorted_outputs.push(Output {
256            asset_id: ids::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
257            transfer_output: Some(key::secp256k1::txs::transfer::Output {
258                amount: i as u64,
259                output_owners: key::secp256k1::txs::OutputOwners {
260                    locktime: i as u64,
261                    threshold: i as u32,
262                    addresses: vec![short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5])],
263                },
264            }),
265            ..Output::default()
266        });
267        sorted_outputs.push(Output {
268            asset_id: ids::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
269            stakeable_lock_out: Some(platformvm::txs::StakeableLockOut {
270                locktime: i as u64,
271                transfer_output: key::secp256k1::txs::transfer::Output {
272                    amount: i as u64,
273                    output_owners: key::secp256k1::txs::OutputOwners {
274                        locktime: i as u64,
275                        threshold: i as u32,
276                        addresses: vec![short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5])],
277                    },
278                },
279            }),
280            ..Output::default()
281        });
282        sorted_outputs.push(Output {
283            asset_id: ids::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
284            stakeable_lock_out: Some(platformvm::txs::StakeableLockOut {
285                locktime: i as u64,
286                transfer_output: key::secp256k1::txs::transfer::Output {
287                    amount: (i + 1) as u64,
288                    output_owners: key::secp256k1::txs::OutputOwners {
289                        locktime: i as u64,
290                        threshold: i as u32,
291                        addresses: vec![short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5])],
292                    },
293                },
294            }),
295            ..Output::default()
296        });
297        sorted_outputs.push(Output {
298            asset_id: ids::Id::from_slice(&[i as u8, 2, 2, 3, 4, 5, 6, 7, 8, 9]),
299            transfer_output: Some(key::secp256k1::txs::transfer::Output {
300                amount: i as u64,
301                output_owners: key::secp256k1::txs::OutputOwners {
302                    locktime: i as u64,
303                    threshold: i as u32,
304                    addresses: vec![short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5])],
305                },
306            }),
307            ..Output::default()
308        });
309        sorted_outputs.push(Output {
310            asset_id: ids::Id::from_slice(&[i as u8, 2, 2, 3, 4, 5, 6, 7, 8, 9]),
311            stakeable_lock_out: Some(platformvm::txs::StakeableLockOut {
312                locktime: i as u64,
313                transfer_output: key::secp256k1::txs::transfer::Output {
314                    amount: i as u64,
315                    output_owners: key::secp256k1::txs::OutputOwners {
316                        locktime: i as u64,
317                        threshold: i as u32,
318                        addresses: vec![short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5])],
319                    },
320                },
321            }),
322            ..Output::default()
323        });
324        sorted_outputs.push(Output {
325            asset_id: ids::Id::from_slice(&[i as u8, 2, 2, 3, 4, 5, 6, 7, 8, 9]),
326            stakeable_lock_out: Some(platformvm::txs::StakeableLockOut {
327                locktime: i as u64,
328                transfer_output: key::secp256k1::txs::transfer::Output {
329                    amount: (i + 1) as u64,
330                    output_owners: key::secp256k1::txs::OutputOwners {
331                        locktime: i as u64,
332                        threshold: i as u32,
333                        addresses: vec![short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5])],
334                    },
335                },
336            }),
337            ..Output::default()
338        });
339        sorted_outputs.push(Output {
340            asset_id: ids::Id::from_slice(&[i as u8, 2, 2, 3, 4, 5, 6, 7, 8, 9]),
341            stakeable_lock_out: Some(platformvm::txs::StakeableLockOut {
342                locktime: i as u64,
343                transfer_output: key::secp256k1::txs::transfer::Output {
344                    amount: (i + 1) as u64,
345                    output_owners: key::secp256k1::txs::OutputOwners {
346                        locktime: i as u64,
347                        threshold: i as u32,
348                        addresses: vec![
349                            short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5]),
350                            short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5]),
351                        ],
352                    },
353                },
354            }),
355            ..Output::default()
356        });
357        sorted_outputs.push(Output {
358            asset_id: ids::Id::from_slice(&[i as u8, 2, 2, 3, 4, 5, 6, 7, 8, 9]),
359            stakeable_lock_out: Some(platformvm::txs::StakeableLockOut {
360                locktime: i as u64,
361                transfer_output: key::secp256k1::txs::transfer::Output {
362                    amount: (i + 1) as u64,
363                    output_owners: key::secp256k1::txs::OutputOwners {
364                        locktime: i as u64,
365                        threshold: i as u32,
366                        addresses: vec![
367                            short::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5]),
368                            short::Id::from_slice(&[i as u8, 2, 2, 3, 4, 5]),
369                        ],
370                    },
371                },
372            }),
373            ..Output::default()
374        });
375    }
376    assert!(cmp_manager::is_sorted_and_unique(&outputs));
377    assert_eq!(outputs, sorted_outputs);
378}
379
380/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#TransferableInput>
381/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#TransferableIn>
382/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferInput>
383#[derive(Debug, Serialize, Deserialize, Eq, Clone)]
384pub struct Input {
385    #[serde(flatten)]
386    pub utxo_id: txs::utxo::Id,
387
388    #[serde(rename = "assetID")]
389    pub asset_id: ids::Id,
390
391    #[serde(rename = "fxID")]
392    pub fx_id: ids::Id, // skip serialization due to serialize:"false"
393
394    /// The underlying type is one of the following:
395    ///
396    /// "*secp256k1fx.TransferInput"
397    /// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferInput>
398    ///
399    /// "*platformvm.StakeableLockIn" which embeds "*secp256k1fx.TransferInput"
400    /// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm#StakeableLockIn>
401    ///
402    /// MUST: only one of the following can be "Some".
403    #[serde(rename = "input")]
404    pub transfer_input: Option<key::secp256k1::txs::transfer::Input>,
405    pub stakeable_lock_in: Option<platformvm::txs::StakeableLockIn>,
406}
407
408impl Default for Input {
409    fn default() -> Self {
410        Self {
411            utxo_id: txs::utxo::Id::default(),
412            asset_id: ids::Id::empty(),
413            fx_id: ids::Id::empty(),
414            transfer_input: None,
415            stakeable_lock_in: None,
416        }
417    }
418}
419
420/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#SortTransferableInputs>
421///
422/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#SortTransferableInputsWithSigners>
423impl Ord for Input {
424    fn cmp(&self, other: &Input) -> Ordering {
425        self.utxo_id
426            .tx_id
427            .cmp(&(other.utxo_id.tx_id)) // returns when "utxo_id.tx_id"s are not Equal
428            .then_with(
429                || self.utxo_id.output_index.cmp(&other.utxo_id.output_index), // if "utxo_id.tx_id"s are Equal, compare "output_index"
430            )
431    }
432}
433
434impl PartialOrd for Input {
435    fn partial_cmp(&self, other: &Input) -> Option<Ordering> {
436        Some(self.cmp(other))
437    }
438}
439
440impl PartialEq for Input {
441    fn eq(&self, other: &Input) -> bool {
442        self.cmp(other) == Ordering::Equal
443    }
444}
445
446/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#SortTransferableInputs>
447/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#SortTransferableInputsWithSigners>
448/// ref. "avalanchego/vms/components/avax.TestTransferableInputSorting"
449/// RUST_LOG=debug cargo test --package avalanche-types --lib -- txs::transferable::test_sort_transferable_inputs --exact --show-output
450#[test]
451fn test_sort_transferable_inputs() {
452    let mut inputs: Vec<Input> = Vec::new();
453    for i in (0..10).rev() {
454        inputs.push(Input {
455            utxo_id: txs::utxo::Id {
456                tx_id: ids::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
457                output_index: (i + 1) as u32,
458                ..txs::utxo::Id::default()
459            },
460            ..Input::default()
461        });
462        inputs.push(Input {
463            utxo_id: txs::utxo::Id {
464                tx_id: ids::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
465                output_index: i as u32,
466                ..txs::utxo::Id::default()
467            },
468            ..Input::default()
469        });
470    }
471    assert!(!cmp_manager::is_sorted_and_unique(&inputs));
472    inputs.sort();
473
474    let mut sorted_inputs: Vec<Input> = Vec::new();
475    for i in 0..10 {
476        sorted_inputs.push(Input {
477            utxo_id: txs::utxo::Id {
478                tx_id: ids::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
479                output_index: i as u32,
480                ..txs::utxo::Id::default()
481            },
482            ..Input::default()
483        });
484        sorted_inputs.push(Input {
485            utxo_id: txs::utxo::Id {
486                tx_id: ids::Id::from_slice(&[i as u8, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
487                output_index: (i + 1) as u32,
488                ..txs::utxo::Id::default()
489            },
490            ..Input::default()
491        });
492    }
493    assert!(cmp_manager::is_sorted_and_unique(&sorted_inputs));
494    assert_eq!(inputs, sorted_inputs);
495}