iota_config/
genesis.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{
6    collections::{BTreeMap, HashMap},
7    fs::File,
8    io::{BufReader, BufWriter},
9    path::Path,
10};
11
12use anyhow::{Context, Result};
13use fastcrypto::{
14    encoding::{Base64, Encoding},
15    hash::HashFunction,
16};
17use iota_types::{
18    base_types::{IotaAddress, ObjectID},
19    clock::Clock,
20    committee::{Committee, CommitteeWithNetworkMetadata, EpochId, ProtocolVersion},
21    crypto::DefaultHash,
22    deny_list_v1::get_deny_list_root_object,
23    effects::{TransactionEffects, TransactionEvents},
24    error::IotaResult,
25    iota_system_state::{
26        IotaSystemState, IotaSystemStateTrait, IotaSystemStateWrapper, IotaValidatorGenesis,
27        get_iota_system_state, get_iota_system_state_wrapper,
28    },
29    messages_checkpoint::{
30        CertifiedCheckpointSummary, CheckpointContents, CheckpointSummary, VerifiedCheckpoint,
31    },
32    object::Object,
33    storage::ObjectStore,
34    transaction::Transaction,
35};
36use serde::{Deserialize, Deserializer, Serialize, Serializer};
37use tracing::trace;
38
39#[derive(Clone, Debug)]
40pub struct Genesis {
41    checkpoint: CertifiedCheckpointSummary,
42    checkpoint_contents: CheckpointContents,
43    transaction: Transaction,
44    effects: TransactionEffects,
45    events: TransactionEvents,
46    objects: Vec<Object>,
47}
48
49#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
50pub struct UnsignedGenesis {
51    pub checkpoint: CheckpointSummary,
52    pub checkpoint_contents: CheckpointContents,
53    pub transaction: Transaction,
54    pub effects: TransactionEffects,
55    pub events: TransactionEvents,
56    pub objects: Vec<Object>,
57}
58
59// Hand implement PartialEq in order to get around the fact that AuthSigs don't
60// impl Eq
61impl PartialEq for Genesis {
62    fn eq(&self, other: &Self) -> bool {
63        self.checkpoint.data() == other.checkpoint.data()
64            && {
65                let this = self.checkpoint.auth_sig();
66                let other = other.checkpoint.auth_sig();
67
68                this.epoch == other.epoch
69                    && this.signature.as_ref() == other.signature.as_ref()
70                    && this.signers_map == other.signers_map
71            }
72            && self.checkpoint_contents == other.checkpoint_contents
73            && self.transaction == other.transaction
74            && self.effects == other.effects
75            && self.objects == other.objects
76    }
77}
78
79impl Eq for Genesis {}
80
81impl Genesis {
82    pub fn new(
83        checkpoint: CertifiedCheckpointSummary,
84        checkpoint_contents: CheckpointContents,
85        transaction: Transaction,
86        effects: TransactionEffects,
87        events: TransactionEvents,
88        objects: Vec<Object>,
89    ) -> Self {
90        Self {
91            checkpoint,
92            checkpoint_contents,
93            transaction,
94            effects,
95            events,
96            objects,
97        }
98    }
99
100    pub fn into_objects(self) -> Vec<Object> {
101        self.objects
102    }
103
104    pub fn objects(&self) -> &[Object] {
105        &self.objects
106    }
107
108    pub fn object(&self, id: ObjectID) -> Option<Object> {
109        self.objects.iter().find(|o| o.id() == id).cloned()
110    }
111
112    pub fn transaction(&self) -> &Transaction {
113        &self.transaction
114    }
115
116    pub fn effects(&self) -> &TransactionEffects {
117        &self.effects
118    }
119    pub fn events(&self) -> &TransactionEvents {
120        &self.events
121    }
122
123    pub fn checkpoint(&self) -> VerifiedCheckpoint {
124        self.checkpoint
125            .clone()
126            .try_into_verified(&self.committee().unwrap())
127            .unwrap()
128    }
129
130    pub fn checkpoint_contents(&self) -> &CheckpointContents {
131        &self.checkpoint_contents
132    }
133
134    pub fn epoch(&self) -> EpochId {
135        0
136    }
137
138    pub fn validator_set_for_tooling(&self) -> Vec<IotaValidatorGenesis> {
139        self.iota_system_object()
140            .into_genesis_version_for_tooling()
141            .validators
142            .active_validators
143    }
144
145    pub fn committee_with_network(&self) -> CommitteeWithNetworkMetadata {
146        self.iota_system_object().get_current_epoch_committee()
147    }
148
149    pub fn reference_gas_price(&self) -> u64 {
150        self.iota_system_object().reference_gas_price()
151    }
152
153    // TODO: No need to return IotaResult. Also consider return &.
154    pub fn committee(&self) -> IotaResult<Committee> {
155        Ok(self.committee_with_network().committee().clone())
156    }
157
158    pub fn iota_system_wrapper_object(&self) -> IotaSystemStateWrapper {
159        get_iota_system_state_wrapper(&self.objects())
160            .expect("IOTA System State Wrapper object must always exist")
161    }
162
163    pub fn contains_migrations(&self) -> bool {
164        self.checkpoint_contents.size() > 1
165    }
166
167    pub fn iota_system_object(&self) -> IotaSystemState {
168        get_iota_system_state(&self.objects()).expect("IOTA System State object must always exist")
169    }
170
171    pub fn clock(&self) -> Clock {
172        let clock = self
173            .objects()
174            .iter()
175            .find(|o| o.id() == ObjectID::CLOCK)
176            .expect("clock must always exist")
177            .data
178            .as_struct_opt()
179            .expect("clock must be a Move object");
180        bcs::from_bytes::<Clock>(clock.contents())
181            .expect("clock object deserialization cannot fail")
182    }
183
184    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, anyhow::Error> {
185        let path = path.as_ref();
186        trace!("reading Genesis from {}", path.display());
187        let read = File::open(path)
188            .with_context(|| format!("unable to load Genesis from {}", path.display()))?;
189        bcs::from_reader(BufReader::new(read))
190            .with_context(|| format!("unable to parse Genesis from {}", path.display()))
191    }
192
193    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), anyhow::Error> {
194        let path = path.as_ref();
195        trace!("writing Genesis to {}", path.display());
196        let mut write = BufWriter::new(File::create(path)?);
197        bcs::serialize_into(&mut write, &self)
198            .with_context(|| format!("unable to save Genesis to {}", path.display()))?;
199        Ok(())
200    }
201
202    pub fn to_bytes(&self) -> Vec<u8> {
203        bcs::to_bytes(self).expect("failed to serialize genesis")
204    }
205
206    pub fn hash(&self) -> [u8; 32] {
207        use std::io::Write;
208
209        let mut digest = DefaultHash::default();
210        digest.write_all(&self.to_bytes()).unwrap();
211        let hash = digest.finalize();
212        hash.into()
213    }
214}
215
216impl Serialize for Genesis {
217    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
218    where
219        S: Serializer,
220    {
221        use serde::ser::Error;
222
223        #[derive(Serialize)]
224        struct RawGenesis<'a> {
225            checkpoint: &'a CertifiedCheckpointSummary,
226            checkpoint_contents: &'a CheckpointContents,
227            transaction: &'a Transaction,
228            effects: &'a TransactionEffects,
229            events: &'a TransactionEvents,
230            objects: &'a [Object],
231        }
232
233        let raw_genesis = RawGenesis {
234            checkpoint: &self.checkpoint,
235            checkpoint_contents: &self.checkpoint_contents,
236            transaction: &self.transaction,
237            effects: &self.effects,
238            events: &self.events,
239            objects: &self.objects,
240        };
241
242        if serializer.is_human_readable() {
243            let bytes = bcs::to_bytes(&raw_genesis).map_err(|e| Error::custom(e.to_string()))?;
244            let s = Base64::encode(bytes);
245            serializer.serialize_str(&s)
246        } else {
247            raw_genesis.serialize(serializer)
248        }
249    }
250}
251
252impl<'de> Deserialize<'de> for Genesis {
253    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
254    where
255        D: Deserializer<'de>,
256    {
257        use serde::de::Error;
258
259        #[derive(Deserialize)]
260        struct RawGenesis {
261            checkpoint: CertifiedCheckpointSummary,
262            checkpoint_contents: CheckpointContents,
263            transaction: Transaction,
264            effects: TransactionEffects,
265            events: TransactionEvents,
266            objects: Vec<Object>,
267        }
268
269        let raw_genesis = if deserializer.is_human_readable() {
270            let s = String::deserialize(deserializer)?;
271            let bytes = Base64::decode(&s).map_err(|e| Error::custom(e.to_string()))?;
272            bcs::from_bytes(&bytes).map_err(|e| Error::custom(e.to_string()))?
273        } else {
274            RawGenesis::deserialize(deserializer)?
275        };
276
277        Ok(Genesis {
278            checkpoint: raw_genesis.checkpoint,
279            checkpoint_contents: raw_genesis.checkpoint_contents,
280            transaction: raw_genesis.transaction,
281            effects: raw_genesis.effects,
282            events: raw_genesis.events,
283            objects: raw_genesis.objects,
284        })
285    }
286}
287
288impl UnsignedGenesis {
289    pub fn objects(&self) -> &[Object] {
290        &self.objects
291    }
292
293    pub fn object(&self, id: ObjectID) -> Option<Object> {
294        self.objects.iter().find(|o| o.id() == id).cloned()
295    }
296
297    pub fn transaction(&self) -> &Transaction {
298        &self.transaction
299    }
300
301    pub fn effects(&self) -> &TransactionEffects {
302        &self.effects
303    }
304    pub fn events(&self) -> &TransactionEvents {
305        &self.events
306    }
307
308    pub fn checkpoint(&self) -> &CheckpointSummary {
309        &self.checkpoint
310    }
311
312    pub fn checkpoint_contents(&self) -> &CheckpointContents {
313        &self.checkpoint_contents
314    }
315
316    pub fn epoch(&self) -> EpochId {
317        0
318    }
319
320    pub fn iota_system_wrapper_object(&self) -> IotaSystemStateWrapper {
321        get_iota_system_state_wrapper(&self.objects())
322            .expect("IOTA System State Wrapper object must always exist")
323    }
324
325    pub fn iota_system_object(&self) -> IotaSystemState {
326        get_iota_system_state(&self.objects()).expect("IOTA System State object must always exist")
327    }
328
329    pub fn has_randomness_state_object(&self) -> bool {
330        self.objects()
331            .get_object(&ObjectID::RANDOMNESS_STATE)
332            .is_some()
333    }
334
335    pub fn has_bridge_object(&self) -> bool {
336        self.objects()
337            .get_object(&ObjectID::GENESIS_BRIDGE)
338            .is_some()
339    }
340
341    pub fn has_coin_deny_list_object(&self) -> bool {
342        get_deny_list_root_object(&self.objects()).is_some()
343    }
344}
345
346#[derive(Clone, Debug, Serialize, Deserialize)]
347#[serde(rename_all = "kebab-case")]
348pub struct GenesisChainParameters {
349    pub protocol_version: u64,
350    pub chain_start_timestamp_ms: u64,
351    pub epoch_duration_ms: u64,
352
353    // Validator committee parameters
354    pub max_validator_count: u64,
355    pub min_validator_joining_stake: u64,
356    pub validator_low_stake_threshold: u64,
357    pub validator_very_low_stake_threshold: u64,
358    pub validator_low_stake_grace_period: u64,
359}
360
361/// Initial set of parameters for a chain.
362#[derive(Serialize, Deserialize)]
363pub struct GenesisCeremonyParameters {
364    #[serde(default = "GenesisCeremonyParameters::default_timestamp_ms")]
365    pub chain_start_timestamp_ms: u64,
366
367    /// protocol version that the chain starts at.
368    #[serde(default = "ProtocolVersion::max")]
369    pub protocol_version: ProtocolVersion,
370
371    #[serde(default = "GenesisCeremonyParameters::default_allow_insertion_of_extra_objects")]
372    pub allow_insertion_of_extra_objects: bool,
373
374    /// The duration of an epoch, in milliseconds.
375    #[serde(default = "GenesisCeremonyParameters::default_epoch_duration_ms")]
376    pub epoch_duration_ms: u64,
377}
378
379impl GenesisCeremonyParameters {
380    pub fn new() -> Self {
381        Self {
382            chain_start_timestamp_ms: Self::default_timestamp_ms(),
383            protocol_version: ProtocolVersion::MAX,
384            allow_insertion_of_extra_objects: true,
385            epoch_duration_ms: Self::default_epoch_duration_ms(),
386        }
387    }
388
389    fn default_timestamp_ms() -> u64 {
390        std::time::SystemTime::now()
391            .duration_since(std::time::UNIX_EPOCH)
392            .unwrap()
393            .as_millis() as u64
394    }
395
396    fn default_allow_insertion_of_extra_objects() -> bool {
397        true
398    }
399
400    fn default_epoch_duration_ms() -> u64 {
401        // 24 hrs
402        24 * 60 * 60 * 1000
403    }
404
405    pub fn to_genesis_chain_parameters(&self) -> GenesisChainParameters {
406        GenesisChainParameters {
407            protocol_version: self.protocol_version.as_u64(),
408            chain_start_timestamp_ms: self.chain_start_timestamp_ms,
409            epoch_duration_ms: self.epoch_duration_ms,
410            max_validator_count: iota_types::governance::MAX_VALIDATOR_COUNT,
411            min_validator_joining_stake: iota_types::governance::MIN_VALIDATOR_JOINING_STAKE_NANOS,
412            validator_low_stake_threshold:
413                iota_types::governance::VALIDATOR_LOW_STAKE_THRESHOLD_NANOS,
414            validator_very_low_stake_threshold:
415                iota_types::governance::VALIDATOR_VERY_LOW_STAKE_THRESHOLD_NANOS,
416            validator_low_stake_grace_period:
417                iota_types::governance::VALIDATOR_LOW_STAKE_GRACE_PERIOD,
418        }
419    }
420}
421
422impl Default for GenesisCeremonyParameters {
423    fn default() -> Self {
424        Self::new()
425    }
426}
427
428#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
429#[serde(rename_all = "kebab-case")]
430pub struct TokenDistributionSchedule {
431    pub pre_minted_supply: u64,
432    pub allocations: Vec<TokenAllocation>,
433}
434
435impl TokenDistributionSchedule {
436    pub fn contains_timelocked_stake(&self) -> bool {
437        self.allocations
438            .iter()
439            .find_map(|allocation| allocation.staked_with_timelock_expiration)
440            .is_some()
441    }
442
443    pub fn validate(&self) {
444        let mut total_nanos = self.pre_minted_supply;
445
446        for allocation in &self.allocations {
447            total_nanos = total_nanos
448                .checked_add(allocation.amount_nanos)
449                .expect("TokenDistributionSchedule allocates more than the maximum supply which equals u64::MAX");
450        }
451    }
452
453    pub fn check_minimum_stake_for_validators<I: IntoIterator<Item = IotaAddress>>(
454        &self,
455        validators: I,
456    ) -> Result<()> {
457        let mut validators: HashMap<IotaAddress, u64> =
458            validators.into_iter().map(|a| (a, 0)).collect();
459
460        // Check that all allocations are for valid validators, while summing up all
461        // allocations for each validator
462        for allocation in &self.allocations {
463            if let Some(staked_with_validator) = &allocation.staked_with_validator {
464                *validators
465                    .get_mut(staked_with_validator)
466                    .expect("allocation must be staked with valid validator") +=
467                    allocation.amount_nanos;
468            }
469        }
470
471        // Check that all validators have sufficient stake allocated to ensure they meet
472        // the minimum stake threshold
473        let minimum_required_stake = iota_types::governance::VALIDATOR_LOW_STAKE_THRESHOLD_NANOS;
474        for (validator, stake) in validators {
475            if stake < minimum_required_stake {
476                anyhow::bail!(
477                    "validator {validator} has '{stake}' stake and does not meet the minimum required stake threshold of '{minimum_required_stake}'"
478                );
479            }
480        }
481        Ok(())
482    }
483
484    pub fn new_for_validators_with_default_allocation<I: IntoIterator<Item = IotaAddress>>(
485        validators: I,
486    ) -> Self {
487        let default_allocation = iota_types::governance::VALIDATOR_LOW_STAKE_THRESHOLD_NANOS;
488
489        let allocations = validators
490            .into_iter()
491            .map(|a| TokenAllocation {
492                recipient_address: a,
493                amount_nanos: default_allocation,
494                staked_with_validator: Some(a),
495                staked_with_timelock_expiration: None,
496            })
497            .collect();
498
499        let schedule = Self {
500            pre_minted_supply: 0,
501            allocations,
502        };
503
504        schedule.validate();
505        schedule
506    }
507
508    /// Helper to read a TokenDistributionSchedule from a csv file.
509    ///
510    /// The file is encoded such that the final entry in the CSV file is used to
511    /// denote the allocation to the stake subsidy fund.
512    ///
513    /// Comments are optional, and start with a `#` character.
514    /// Only entries that start with this character are treated as comments.
515    pub fn from_csv<R: std::io::Read>(reader: R) -> Result<Self> {
516        let mut reader = csv_reader_with_comments(reader);
517        let mut allocations: Vec<TokenAllocation> =
518            reader.deserialize().collect::<Result<_, _>>()?;
519
520        let pre_minted_supply = allocations.pop().unwrap();
521        assert_eq!(
522            IotaAddress::ZERO,
523            pre_minted_supply.recipient_address,
524            "final allocation must be for the pre-minted supply amount",
525        );
526        assert!(
527            pre_minted_supply.staked_with_validator.is_none(),
528            "cannot stake the pre-minted supply amount",
529        );
530
531        let schedule = Self {
532            pre_minted_supply: pre_minted_supply.amount_nanos,
533            allocations,
534        };
535
536        schedule.validate();
537        Ok(schedule)
538    }
539
540    pub fn to_csv<W: std::io::Write>(&self, writer: W) -> Result<()> {
541        let mut writer = csv::Writer::from_writer(writer);
542
543        for allocation in &self.allocations {
544            writer.serialize(allocation)?;
545        }
546
547        writer.serialize(TokenAllocation {
548            recipient_address: IotaAddress::ZERO,
549            amount_nanos: self.pre_minted_supply,
550            staked_with_validator: None,
551            staked_with_timelock_expiration: None,
552        })?;
553
554        Ok(())
555    }
556}
557
558#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
559#[serde(rename_all = "kebab-case")]
560pub struct TokenAllocation {
561    /// Indicates the address that owns the tokens. It means that this
562    /// `TokenAllocation` can serve to stake some funds to the
563    /// `staked_with_validator` during genesis, but it's the `recipient_address`
564    /// which will receive the associated StakedIota (or TimelockedStakedIota)
565    /// object.
566    pub recipient_address: IotaAddress,
567    /// Indicates an amount of nanos that is:
568    /// - minted for the `recipient_address` and staked to a validator, only in
569    ///   the case `staked_with_validator` is Some
570    /// - minted for the `recipient_address` and transferred that address,
571    ///   otherwise.
572    pub amount_nanos: u64,
573
574    /// Indicates if this allocation should be staked at genesis and with which
575    /// validator
576    pub staked_with_validator: Option<IotaAddress>,
577    /// Indicates if this allocation should be staked with timelock at genesis
578    /// and contains its timelock_expiration
579    pub staked_with_timelock_expiration: Option<u64>,
580}
581
582#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
583pub struct TokenDistributionScheduleBuilder {
584    pre_minted_supply: u64,
585    allocations: Vec<TokenAllocation>,
586}
587
588impl TokenDistributionScheduleBuilder {
589    #[expect(clippy::new_without_default)]
590    pub fn new() -> Self {
591        Self {
592            pre_minted_supply: 0,
593            allocations: vec![],
594        }
595    }
596
597    pub fn set_pre_minted_supply(&mut self, pre_minted_supply: u64) {
598        self.pre_minted_supply = pre_minted_supply;
599    }
600
601    pub fn default_allocation_for_validators<I: IntoIterator<Item = IotaAddress>>(
602        &mut self,
603        validators: I,
604    ) {
605        let default_allocation = iota_types::governance::VALIDATOR_LOW_STAKE_THRESHOLD_NANOS;
606
607        for validator in validators {
608            self.add_allocation(TokenAllocation {
609                recipient_address: validator,
610                amount_nanos: default_allocation,
611                staked_with_validator: Some(validator),
612                staked_with_timelock_expiration: None,
613            });
614        }
615    }
616
617    pub fn add_allocation(&mut self, allocation: TokenAllocation) {
618        self.allocations.push(allocation);
619    }
620
621    pub fn build(&self) -> TokenDistributionSchedule {
622        let schedule = TokenDistributionSchedule {
623            pre_minted_supply: self.pre_minted_supply,
624            allocations: self.allocations.clone(),
625        };
626
627        schedule.validate();
628        schedule
629    }
630}
631
632/// Represents the allocation of stake and gas payment to a validator.
633#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
634#[serde(rename_all = "kebab-case")]
635pub struct ValidatorAllocation {
636    /// The validator address receiving the stake and/or gas payment
637    pub validator: IotaAddress,
638    /// The amount of nanos to stake to the validator
639    pub amount_nanos_to_stake: u64,
640    /// The amount of nanos to transfer as gas payment to the validator
641    pub amount_nanos_to_pay_gas: u64,
642}
643
644/// Represents a delegation of stake and gas payment to a validator,
645/// coming from a delegator. This struct is used to serialize and deserialize
646/// delegations to and from a csv file.
647#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
648#[serde(rename_all = "kebab-case")]
649pub struct Delegation {
650    /// The address from which to take the nanos for staking/gas
651    pub delegator: IotaAddress,
652    /// The allocation to a validator receiving a stake and/or a gas payment
653    #[serde(flatten)]
654    pub validator_allocation: ValidatorAllocation,
655}
656
657/// Represents genesis delegations to validators.
658///
659/// This struct maps a delegator address to a list of validators and their
660/// stake and gas allocations. Each ValidatorAllocation contains the address of
661/// a validator that will receive an amount of nanos to stake and an amount as
662/// gas payment.
663#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
664#[serde(rename_all = "kebab-case")]
665pub struct Delegations {
666    pub allocations: BTreeMap<IotaAddress, Vec<ValidatorAllocation>>,
667}
668
669impl Delegations {
670    pub fn new_for_validators_with_default_allocation(
671        validators: impl IntoIterator<Item = IotaAddress>,
672        delegator: IotaAddress,
673    ) -> Self {
674        let validator_allocations = validators
675            .into_iter()
676            .map(|address| ValidatorAllocation {
677                validator: address,
678                amount_nanos_to_stake: iota_types::governance::MIN_VALIDATOR_JOINING_STAKE_NANOS,
679                amount_nanos_to_pay_gas: 0,
680            })
681            .collect();
682
683        let mut allocations = BTreeMap::new();
684        allocations.insert(delegator, validator_allocations);
685
686        Self { allocations }
687    }
688
689    /// Helper to read a Delegations struct from a csv file.
690    ///
691    /// The file is encoded such that the final entry in the CSV file is used to
692    /// denote the allocation coming from a delegator. It must be in the
693    /// following format:
694    /// `delegator,validator,amount-nanos-to-stake,amount-nanos-to-pay-gas
695    /// <delegator1-address>,<validator-1-address>,2000000000000000,5000000000
696    /// <delegator1-address>,<validator-2-address>,3000000000000000,5000000000
697    /// <delegator2-address>,<validator-3-address>,4500000000000000,5000000000`
698    ///
699    /// Comments are optional, and start with a `#` character.
700    /// Only entries that start with this character are treated as comments.
701    pub fn from_csv<R: std::io::Read>(reader: R) -> Result<Self> {
702        let mut reader = csv_reader_with_comments(reader);
703
704        let mut delegations = Self::default();
705        for delegation in reader.deserialize::<Delegation>() {
706            let delegation = delegation?;
707            delegations
708                .allocations
709                .entry(delegation.delegator)
710                .or_default()
711                .push(delegation.validator_allocation);
712        }
713
714        Ok(delegations)
715    }
716
717    /// Helper to write a Delegations struct into a csv file.
718    ///
719    /// It writes in the following format:
720    /// `delegator,validator,amount-nanos-to-stake,amount-nanos-to-pay-gas
721    /// <delegator1-address>,<validator-1-address>,2000000000000000,5000000000
722    /// <delegator1-address>,<validator-2-address>,3000000000000000,5000000000
723    /// <delegator2-address>,<validator-3-address>,4500000000000000,5000000000`
724    pub fn to_csv<W: std::io::Write>(&self, writer: W) -> Result<()> {
725        let mut writer = csv::Writer::from_writer(writer);
726
727        writer.write_record([
728            "delegator",
729            "validator",
730            "amount-nanos-to-stake",
731            "amount-nanos-to-pay-gas",
732        ])?;
733
734        for (&delegator, validator_allocations) in &self.allocations {
735            for validator_allocation in validator_allocations {
736                writer.write_record(&[
737                    delegator.to_string(),
738                    validator_allocation.validator.to_string(),
739                    validator_allocation.amount_nanos_to_stake.to_string(),
740                    validator_allocation.amount_nanos_to_pay_gas.to_string(),
741                ])?;
742            }
743        }
744
745        Ok(())
746    }
747}
748
749/// Helper function to create a CSV reader with custom settings.
750/// In this case, it sets the comment character to `#`.
751pub fn csv_reader_with_comments<R: std::io::Read>(reader: R) -> csv::Reader<R> {
752    csv::ReaderBuilder::new()
753        .comment(Some(b'#'))
754        .from_reader(reader)
755}