Skip to main content

linera_wallet_json/
wallet.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! A wallet persisted as a JSON file, tracking the client's chains and default chain.
5
6use std::{
7    iter::IntoIterator,
8    sync::{Arc, RwLock},
9};
10
11use futures::{stream, Stream};
12use linera_base::identifiers::{AccountOwner, ChainId};
13use linera_client::config::GenesisConfig;
14use linera_core::wallet::*;
15use linera_persistent::{self as persistent};
16
17#[derive(serde::Serialize, serde::Deserialize)]
18pub(crate) struct Data {
19    pub chains: Memory,
20    pub default: Arc<RwLock<Option<ChainId>>>,
21    pub genesis_config: GenesisConfig,
22}
23
24/// A wallet backed by a JSON file, holding the client's chains and which one is the default.
25pub struct PersistentWallet(persistent::File<Data>);
26
27// TODO(#5081): `persistent` is no longer necessary here, we can move the locking
28// logic right here
29
30impl linera_core::Wallet for PersistentWallet {
31    type Error = persistent::file::Error;
32
33    async fn get(&self, id: ChainId) -> Result<Option<Chain>, Self::Error> {
34        Ok(self.get(id))
35    }
36
37    async fn remove(&self, id: ChainId) -> Result<Option<Chain>, Self::Error> {
38        self.remove(id)
39    }
40
41    fn items(&self) -> impl Stream<Item = Result<(ChainId, Chain), Self::Error>> {
42        stream::iter(self.items().into_iter().map(Ok))
43    }
44
45    async fn insert(&self, id: ChainId, chain: Chain) -> Result<Option<Chain>, Self::Error> {
46        self.insert(id, &chain)
47    }
48
49    async fn try_insert(&self, id: ChainId, chain: Chain) -> Result<Option<Chain>, Self::Error> {
50        let chain = self.try_insert(id, chain)?;
51        self.save()?;
52        Ok(chain)
53    }
54
55    async fn modify(
56        &self,
57        id: ChainId,
58        f: impl Fn(&mut Chain) + Send,
59    ) -> Result<Option<()>, Self::Error> {
60        self.mutate(id, f).transpose()
61    }
62}
63
64impl Extend<(ChainId, Chain)> for PersistentWallet {
65    fn extend<It: IntoIterator<Item = (ChainId, Chain)>>(&mut self, chains: It) {
66        for (id, chain) in chains {
67            if self.0.chains.try_insert(id, chain).is_none() {
68                self.try_set_default(id);
69            }
70        }
71    }
72}
73
74impl PersistentWallet {
75    /// Returns the chain with the given ID, if it is in the wallet.
76    pub fn get(&self, id: ChainId) -> Option<Chain> {
77        self.0.chains.get(id)
78    }
79
80    /// Removes the chain with the given ID, adjusting the default chain if needed, and saves.
81    pub fn remove(&self, id: ChainId) -> Result<Option<Chain>, persistent::file::Error> {
82        let chain = self.0.chains.remove(id);
83        {
84            let mut default = self.0.default.write().unwrap();
85            if *default == Some(id) {
86                *default = None;
87            }
88            if default.is_none() {
89                let items = self.0.chains.items();
90                if items.len() == 1 {
91                    *default = Some(items[0].0);
92                }
93            }
94        }
95        self.0.save()?;
96        Ok(chain)
97    }
98
99    /// Returns all `(chain ID, chain)` pairs held by the wallet.
100    pub fn items(&self) -> Vec<(ChainId, Chain)> {
101        self.0.chains.items()
102    }
103
104    fn try_set_default(&self, id: ChainId) {
105        let mut guard = self.0.default.write().unwrap();
106        if guard.is_none() {
107            *guard = Some(id);
108        }
109    }
110
111    /// Inserts or replaces a chain, making it the default chain if it has an owner, and saves.
112    pub fn insert(
113        &self,
114        id: ChainId,
115        chain: &Chain,
116    ) -> Result<Option<Chain>, persistent::file::Error> {
117        let has_owner = chain.owner.is_some();
118        let old_chain = self.0.chains.insert(id, chain.clone());
119        if has_owner {
120            self.try_set_default(id);
121        }
122        self.0.save()?;
123        Ok(old_chain)
124    }
125
126    /// Inserts a chain only if its ID is not already present, making it the default if it is
127    /// the first chain, and saves.
128    pub fn try_insert(
129        &self,
130        id: ChainId,
131        chain: Chain,
132    ) -> Result<Option<Chain>, persistent::file::Error> {
133        let chain = self.0.chains.try_insert(id, chain);
134        if chain.is_none() {
135            self.try_set_default(id);
136        }
137        self.save()?;
138        Ok(chain)
139    }
140
141    /// Creates a new wallet file at `path` for the given genesis configuration.
142    pub fn create(
143        path: &std::path::Path,
144        genesis_config: GenesisConfig,
145    ) -> Result<Self, persistent::file::Error> {
146        Ok(Self(persistent::File::new(
147            path,
148            Data {
149                chains: Memory::default(),
150                default: Arc::new(RwLock::new(None)),
151                genesis_config,
152            },
153        )?))
154    }
155
156    /// Reads an existing wallet from the file at `path`.
157    pub fn read(path: &std::path::Path) -> Result<Self, persistent::file::Error> {
158        Ok(Self(persistent::File::read(path)?))
159    }
160
161    /// Returns the network's genesis configuration.
162    pub fn genesis_config(&self) -> &GenesisConfig {
163        &self.0.genesis_config
164    }
165
166    /// Returns the admin chain ID from the genesis configuration.
167    pub fn genesis_admin_chain_id(&self) -> ChainId {
168        self.0.genesis_config.admin_chain_id()
169    }
170
171    /// Returns the default chain, if one is set.
172    pub fn default_chain(&self) -> Option<ChainId> {
173        *self.0.default.read().unwrap()
174    }
175
176    /// Sets the default chain, which must already be in the wallet, and saves.
177    pub fn set_default_chain(&mut self, id: ChainId) -> Result<(), persistent::file::Error> {
178        assert!(self.0.chains.get(id).is_some());
179        *self.0.default.write().unwrap() = Some(id);
180        self.0.save()
181    }
182
183    /// Applies a mutation to the chain with the given ID, saving afterwards. Returns `None`
184    /// if the chain is not in the wallet.
185    pub fn mutate<R>(
186        &self,
187        chain_id: ChainId,
188        mutate: impl Fn(&mut Chain) -> R,
189    ) -> Option<Result<R, persistent::file::Error>> {
190        self.0
191            .chains
192            .mutate(chain_id, mutate)
193            .map(|outcome| self.0.save().map(|()| outcome))
194    }
195
196    /// Removes and returns the owner of the given chain, erroring if the chain or owner is absent.
197    pub fn forget_keys(&self, chain_id: ChainId) -> anyhow::Result<AccountOwner> {
198        self.mutate(chain_id, |chain| chain.owner.take())
199            .ok_or_else(|| anyhow::anyhow!("nonexistent chain `{chain_id}`"))??
200            .ok_or_else(|| anyhow::anyhow!("keypair not found for chain `{chain_id}`"))
201    }
202
203    /// Writes the wallet to its file.
204    pub fn save(&self) -> Result<(), persistent::file::Error> {
205        self.0.save()
206    }
207
208    /// Returns the number of chains in the wallet.
209    pub fn num_chains(&self) -> usize {
210        self.0.chains.items().len()
211    }
212
213    /// Returns the IDs of all chains in the wallet.
214    pub fn chain_ids(&self) -> Vec<ChainId> {
215        self.0.chains.chain_ids()
216    }
217
218    /// Returns the list of all chain IDs for which we have a secret key.
219    pub fn owned_chain_ids(&self) -> Vec<ChainId> {
220        self.0.chains.owned_chain_ids()
221    }
222
223    pub(crate) fn data(&self) -> &Data {
224        &self.0
225    }
226}