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