linera_client/
wallet.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{collections::BTreeMap, iter::IntoIterator};
5
6use linera_base::{
7    crypto::CryptoHash,
8    data_types::{BlockHeight, ChainDescription, Timestamp},
9    ensure,
10    identifiers::{AccountOwner, ChainId},
11};
12use linera_core::{client::PendingProposal, data_types::ChainInfo};
13use serde::{Deserialize, Serialize};
14
15use crate::{config::GenesisConfig, error, Error};
16
17#[derive(Serialize, Deserialize)]
18pub struct Wallet {
19    pub chains: BTreeMap<ChainId, UserChain>,
20    pub default: Option<ChainId>,
21    pub genesis_config: GenesisConfig,
22}
23
24impl Extend<UserChain> for Wallet {
25    fn extend<Chains: IntoIterator<Item = UserChain>>(&mut self, chains: Chains) {
26        for chain in chains.into_iter() {
27            self.insert(chain);
28        }
29    }
30}
31
32impl Wallet {
33    pub fn new(genesis_config: GenesisConfig) -> Self {
34        Wallet {
35            chains: BTreeMap::new(),
36            default: None,
37            genesis_config,
38        }
39    }
40
41    pub fn get(&self, chain_id: ChainId) -> Option<&UserChain> {
42        self.chains.get(&chain_id)
43    }
44
45    pub fn insert(&mut self, chain: UserChain) {
46        if self.default.is_none() {
47            self.default = Some(chain.chain_id);
48        }
49
50        self.chains.insert(chain.chain_id, chain);
51    }
52
53    pub fn forget_keys(&mut self, chain_id: &ChainId) -> Result<AccountOwner, Error> {
54        let chain = self
55            .chains
56            .get_mut(chain_id)
57            .ok_or(error::Inner::NonexistentChain(*chain_id))?;
58
59        let owner = chain
60            .owner
61            .take()
62            .ok_or(error::Inner::NonexistentKeypair(*chain_id))?;
63
64        Ok(owner)
65    }
66
67    pub fn forget_chain(&mut self, chain_id: &ChainId) -> Result<UserChain, Error> {
68        let user_chain = self
69            .chains
70            .remove(chain_id)
71            .ok_or::<Error>(error::Inner::NonexistentChain(*chain_id).into())?;
72        Ok(user_chain)
73    }
74
75    pub fn default_chain(&self) -> Option<ChainId> {
76        self.default
77    }
78
79    pub fn first_non_admin_chain(&self) -> Option<ChainId> {
80        self.chain_ids()
81            .into_iter()
82            .find(|chain_id| *chain_id != self.genesis_config.admin_id())
83    }
84
85    pub fn chain_ids(&self) -> Vec<ChainId> {
86        self.chains.keys().copied().collect()
87    }
88
89    /// Returns the list of all chain IDs for which we have a secret key.
90    pub fn owned_chain_ids(&self) -> Vec<ChainId> {
91        self.chains
92            .iter()
93            .filter_map(|(chain_id, chain)| chain.owner.is_some().then_some(*chain_id))
94            .collect()
95    }
96
97    pub fn num_chains(&self) -> usize {
98        self.chains.len()
99    }
100
101    pub fn chains_mut(&mut self) -> impl Iterator<Item = &mut UserChain> {
102        self.chains.values_mut()
103    }
104
105    pub fn assign_new_chain_to_owner(
106        &mut self,
107        owner: AccountOwner,
108        chain_id: ChainId,
109        timestamp: Timestamp,
110    ) -> Result<(), Error> {
111        let user_chain = UserChain {
112            chain_id,
113            owner: Some(owner),
114            block_hash: None,
115            timestamp,
116            next_block_height: BlockHeight(0),
117            pending_proposal: None,
118        };
119        self.insert(user_chain);
120        Ok(())
121    }
122
123    pub fn set_default_chain(&mut self, chain_id: ChainId) -> Result<(), Error> {
124        ensure!(
125            self.chains.contains_key(&chain_id),
126            error::Inner::NonexistentChain(chain_id)
127        );
128        self.default = Some(chain_id);
129        Ok(())
130    }
131
132    pub fn update_from_info(
133        &mut self,
134        pending_proposal: Option<PendingProposal>,
135        owner: Option<AccountOwner>,
136        info: &ChainInfo,
137    ) {
138        self.insert(UserChain {
139            chain_id: info.chain_id,
140            owner,
141            block_hash: info.block_hash,
142            next_block_height: info.next_block_height,
143            timestamp: info.timestamp,
144            pending_proposal,
145        });
146    }
147
148    pub fn genesis_admin_chain(&self) -> ChainId {
149        self.genesis_config.admin_id()
150    }
151
152    pub fn genesis_config(&self) -> &GenesisConfig {
153        &self.genesis_config
154    }
155}
156
157#[derive(Serialize, Deserialize)]
158pub struct UserChain {
159    pub chain_id: ChainId,
160    /// The owner of the chain, if we own it.
161    pub owner: Option<AccountOwner>,
162    pub block_hash: Option<CryptoHash>,
163    pub timestamp: Timestamp,
164    pub next_block_height: BlockHeight,
165    pub pending_proposal: Option<PendingProposal>,
166}
167
168impl Clone for UserChain {
169    fn clone(&self) -> Self {
170        Self {
171            chain_id: self.chain_id,
172            owner: self.owner,
173            block_hash: self.block_hash,
174            timestamp: self.timestamp,
175            next_block_height: self.next_block_height,
176            pending_proposal: self.pending_proposal.clone(),
177        }
178    }
179}
180
181impl UserChain {
182    /// Create a user chain that we own.
183    pub fn make_initial(
184        owner: AccountOwner,
185        description: ChainDescription,
186        timestamp: Timestamp,
187    ) -> Self {
188        Self {
189            chain_id: description.into(),
190            owner: Some(owner),
191            block_hash: None,
192            timestamp,
193            next_block_height: BlockHeight::ZERO,
194            pending_proposal: None,
195        }
196    }
197
198    /// Creates an entry for a chain that we don't own. The timestamp must be the genesis
199    /// timestamp or earlier.
200    pub fn make_other(chain_id: ChainId, timestamp: Timestamp) -> Self {
201        Self {
202            chain_id,
203            owner: None,
204            block_hash: None,
205            timestamp,
206            next_block_height: BlockHeight::ZERO,
207            pending_proposal: None,
208        }
209    }
210}