linera_service/
wallet.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    iter::IntoIterator,
6    sync::{Arc, RwLock},
7};
8
9use futures::{stream, Stream};
10use linera_base::{
11    data_types::{ChainDescription, ChainOrigin},
12    identifiers::{AccountOwner, ChainId},
13};
14use linera_client::config::GenesisConfig;
15use linera_core::wallet;
16use linera_persistent as persistent;
17
18#[derive(serde::Serialize, serde::Deserialize)]
19struct Data {
20    pub chains: wallet::Memory,
21    default: Arc<RwLock<Option<ChainId>>>,
22    genesis_config: GenesisConfig,
23}
24
25struct ChainDetails {
26    is_default: bool,
27    is_admin: bool,
28    origin: Option<ChainOrigin>,
29    chain_id: ChainId,
30    user_chain: wallet::Chain,
31}
32
33impl ChainDetails {
34    fn new(chain_id: ChainId, wallet: &Data) -> Self {
35        let Some(user_chain) = wallet.chains.get(chain_id) else {
36            panic!("Chain {} not found.", chain_id);
37        };
38        ChainDetails {
39            is_default: Some(chain_id) == *wallet.default.read().unwrap(),
40            is_admin: chain_id == wallet.genesis_config.admin_chain_id(),
41            chain_id,
42            origin: wallet
43                .genesis_config
44                .chains
45                .iter()
46                .find(|description| description.id() == chain_id)
47                .map(ChainDescription::origin),
48            user_chain,
49        }
50    }
51
52    fn print_paragraph(&self) {
53        println!("-----------------------");
54        println!("{:<20}  {}", "Chain ID:", self.chain_id);
55
56        let mut tags = Vec::new();
57        if self.is_default {
58            tags.push("DEFAULT");
59        }
60        if self.is_admin {
61            tags.push("ADMIN");
62        }
63        if self.user_chain.is_follow_only() {
64            tags.push("FOLLOW-ONLY");
65        }
66        if !tags.is_empty() {
67            println!("{:<20}  {}", "Tags:", tags.join(", "));
68        }
69
70        match self.origin {
71            Some(ChainOrigin::Root(_)) | None => {
72                println!("{:<20}  -", "Parent chain:");
73            }
74            Some(ChainOrigin::Child { parent, .. }) => {
75                println!("{:<20}  {parent}", "Parent chain:");
76            }
77        }
78
79        if let Some(owner) = &self.user_chain.owner {
80            println!("{:<20}  {owner}", "Default owner:");
81        } else {
82            println!("{:<20}  No owner key", "Default owner:");
83        }
84
85        println!("{:<20}  {}", "Timestamp:", self.user_chain.timestamp);
86        println!("{:<20}  {}", "Blocks:", self.user_chain.next_block_height);
87
88        if let Some(epoch) = self.user_chain.epoch {
89            println!("{:<20}  {epoch}", "Epoch:");
90        } else {
91            println!("{:<20}  -", "Epoch:");
92        }
93
94        if let Some(hash) = self.user_chain.block_hash {
95            println!("{:<20}  {hash}", "Latest block hash:");
96        }
97
98        if self.user_chain.pending_proposal.is_some() {
99            println!("{:<20}  present", "Pending proposal:");
100        }
101    }
102}
103
104pub struct Wallet(persistent::File<Data>);
105
106// TODO(#5081): `persistent` is no longer necessary here, we can move the locking
107// logic right here
108
109impl linera_core::Wallet for Wallet {
110    type Error = persistent::file::Error;
111
112    async fn get(&self, id: ChainId) -> Result<Option<wallet::Chain>, Self::Error> {
113        Ok(self.get(id))
114    }
115
116    async fn remove(&self, id: ChainId) -> Result<Option<wallet::Chain>, Self::Error> {
117        self.remove(id)
118    }
119
120    fn items(&self) -> impl Stream<Item = Result<(ChainId, wallet::Chain), Self::Error>> {
121        stream::iter(self.items().into_iter().map(Ok))
122    }
123
124    async fn insert(
125        &self,
126        id: ChainId,
127        chain: wallet::Chain,
128    ) -> Result<Option<wallet::Chain>, Self::Error> {
129        self.insert(id, &chain)
130    }
131
132    async fn try_insert(
133        &self,
134        id: ChainId,
135        chain: wallet::Chain,
136    ) -> Result<Option<wallet::Chain>, Self::Error> {
137        let chain = self.try_insert(id, chain)?;
138        self.save()?;
139        Ok(chain)
140    }
141
142    async fn modify(
143        &self,
144        id: ChainId,
145        f: impl FnMut(&mut wallet::Chain) + Send,
146    ) -> Result<Option<()>, Self::Error> {
147        self.mutate(id, f).transpose()
148    }
149}
150
151impl Extend<(ChainId, wallet::Chain)> for Wallet {
152    fn extend<It: IntoIterator<Item = (ChainId, wallet::Chain)>>(&mut self, chains: It) {
153        for (id, chain) in chains {
154            if self.0.chains.try_insert(id, chain).is_none() {
155                self.try_set_default(id);
156            }
157        }
158    }
159}
160
161impl Wallet {
162    pub fn get(&self, id: ChainId) -> Option<wallet::Chain> {
163        self.0.chains.get(id)
164    }
165
166    pub fn remove(&self, id: ChainId) -> Result<Option<wallet::Chain>, persistent::file::Error> {
167        let chain = self.0.chains.remove(id);
168        {
169            let mut default = self.0.default.write().unwrap();
170            if *default == Some(id) {
171                *default = None;
172            }
173        }
174        self.0.save()?;
175        Ok(chain)
176    }
177
178    pub fn items(&self) -> Vec<(ChainId, wallet::Chain)> {
179        self.0.chains.items()
180    }
181
182    fn try_set_default(&self, id: ChainId) {
183        let mut guard = self.0.default.write().unwrap();
184        if guard.is_none() {
185            *guard = Some(id);
186        }
187    }
188
189    pub fn insert(
190        &self,
191        id: ChainId,
192        chain: &wallet::Chain,
193    ) -> Result<Option<wallet::Chain>, persistent::file::Error> {
194        let has_owner = chain.owner.is_some();
195        let old_chain = self.0.chains.insert(id, chain.clone());
196        if has_owner {
197            self.try_set_default(id);
198        }
199        self.0.save()?;
200        Ok(old_chain)
201    }
202
203    pub fn try_insert(
204        &self,
205        id: ChainId,
206        chain: wallet::Chain,
207    ) -> Result<Option<wallet::Chain>, persistent::file::Error> {
208        let chain = self.0.chains.try_insert(id, chain);
209        if chain.is_none() {
210            self.try_set_default(id);
211        }
212        self.save()?;
213        Ok(chain)
214    }
215
216    pub fn create(
217        path: &std::path::Path,
218        genesis_config: GenesisConfig,
219    ) -> Result<Self, persistent::file::Error> {
220        Ok(Self(persistent::File::new(
221            path,
222            Data {
223                chains: wallet::Memory::default(),
224                default: Arc::new(RwLock::new(None)),
225                genesis_config,
226            },
227        )?))
228    }
229
230    pub fn read(path: &std::path::Path) -> Result<Self, persistent::file::Error> {
231        Ok(Self(persistent::File::read(path)?))
232    }
233
234    pub fn genesis_config(&self) -> &GenesisConfig {
235        &self.0.genesis_config
236    }
237
238    pub fn genesis_admin_chain_id(&self) -> ChainId {
239        self.0.genesis_config.admin_chain_id()
240    }
241
242    // TODO(#5082): now that wallets only store chains, not keys, there's not much point in
243    // allowing wallets with no default chain (i.e. no chains)
244    pub fn default_chain(&self) -> Option<ChainId> {
245        *self.0.default.read().unwrap()
246    }
247
248    pub fn pretty_print(&self, chain_ids: Vec<ChainId>) {
249        let chain_ids: Vec<_> = chain_ids.into_iter().collect();
250        let total_chains = chain_ids.len();
251
252        let plural_s = if total_chains == 1 { "" } else { "s" };
253        tracing::info!("Found {total_chains} chain{plural_s}");
254
255        let mut chains = chain_ids
256            .into_iter()
257            .map(|chain_id| ChainDetails::new(chain_id, &self.0))
258            .collect::<Vec<_>>();
259        // Print first the default, then the admin chain, then other root chains, and finally the
260        // child chains.
261        chains.sort_unstable_by_key(|chain| {
262            let root_id = chain
263                .origin
264                .and_then(|origin| origin.root())
265                .unwrap_or(u32::MAX);
266            let chain_id = chain.chain_id;
267            (!chain.is_default, !chain.is_admin, root_id, chain_id)
268        });
269        for chain in chains {
270            chain.print_paragraph();
271        }
272        println!("------------------------");
273    }
274
275    pub fn set_default_chain(&mut self, id: ChainId) -> Result<(), persistent::file::Error> {
276        assert!(self.0.chains.get(id).is_some());
277        *self.0.default.write().unwrap() = Some(id);
278        self.0.save()
279    }
280
281    pub fn mutate<R>(
282        &self,
283        chain_id: ChainId,
284        mutate: impl FnMut(&mut wallet::Chain) -> R,
285    ) -> Option<Result<R, persistent::file::Error>> {
286        self.0
287            .chains
288            .mutate(chain_id, mutate)
289            .map(|outcome| self.0.save().map(|()| outcome))
290    }
291
292    pub fn forget_keys(&self, chain_id: ChainId) -> anyhow::Result<AccountOwner> {
293        self.mutate(chain_id, |chain| chain.owner.take())
294            .ok_or_else(|| anyhow::anyhow!("nonexistent chain `{chain_id}`"))??
295            .ok_or_else(|| anyhow::anyhow!("keypair not found for chain `{chain_id}`"))
296    }
297
298    pub fn forget_chain(&self, chain_id: ChainId) -> anyhow::Result<wallet::Chain> {
299        let chain = self
300            .0
301            .chains
302            .remove(chain_id)
303            .ok_or_else(|| anyhow::anyhow!("nonexistent chain `{chain_id}`"))?;
304        self.0.save()?;
305        Ok(chain)
306    }
307
308    pub fn save(&self) -> Result<(), persistent::file::Error> {
309        self.0.save()
310    }
311
312    pub fn num_chains(&self) -> usize {
313        self.0.chains.items().len()
314    }
315
316    pub fn chain_ids(&self) -> Vec<ChainId> {
317        self.0.chains.chain_ids()
318    }
319
320    /// Returns the list of all chain IDs for which we have a secret key.
321    pub fn owned_chain_ids(&self) -> Vec<ChainId> {
322        self.0.chains.owned_chain_ids()
323    }
324}