1use std::collections::hash_map::Entry;
23use std::collections::HashMap;
24use std::convert::Infallible;
25use std::fs::{self, File};
26use std::io;
27use std::ops::{Deref, DerefMut};
28use std::path::PathBuf;
29
30use bitcoin::bip32::ExtendedPubKey;
31use rgbfs::StockFs;
32use rgbstd::containers::{Contract, LoadError, Transfer};
33use rgbstd::interface::BuilderError;
34use rgbstd::persistence::{Inventory, InventoryDataError, InventoryError, StashError, Stock};
35use rgbstd::resolvers::ResolveHeight;
36use rgbstd::validation::ResolveTx;
37use rgbstd::{validation, Chain};
38use strict_types::encoding::{DeserializeError, Ident, SerializeError};
39
40use crate::descriptor::RgbDescr;
41use crate::{RgbWallet, Tapret};
42
43#[derive(Debug, Display, Error, From)]
44#[display(inner)]
45pub enum RuntimeError {
46 #[from]
47 Io(io::Error),
48
49 #[from]
50 Yaml(serde_yaml::Error),
51
52 #[from]
53 Serialize(SerializeError),
54
55 #[from]
56 Deserialize(DeserializeError),
57
58 #[from]
59 Load(LoadError),
60
61 #[from]
62 Stash(StashError<Infallible>),
63
64 #[from]
65 #[from(InventoryDataError<Infallible>)]
66 Inventory(InventoryError<Infallible>),
67
68 #[from]
69 Builder(BuilderError),
70
71 #[display(doc_comments)]
73 WalletUnknown(Ident),
74
75 #[from]
76 Psbt(bitcoin::psbt::Error),
77
78 #[cfg(feature = "electrum")]
79 #[from]
80 Electrum(electrum_client::Error),
81
82 #[from]
83 InvalidConsignment(validation::Status),
84
85 #[display(doc_comments)]
89 IncompleteContract,
90
91 #[from]
92 Custom(String),
93}
94
95impl From<Infallible> for RuntimeError {
96 fn from(_: Infallible) -> Self { unreachable!() }
97}
98
99#[derive(Getters)]
100pub struct Runtime {
101 stock_path: PathBuf,
102 wallets_path: PathBuf,
103 #[getter(skip)]
104 stock: Stock,
105 wallets: HashMap<Ident, RgbDescr>,
106 #[getter(as_copy)]
107 chain: Chain,
108}
109
110impl Deref for Runtime {
111 type Target = Stock;
112 fn deref(&self) -> &Self::Target { &self.stock }
113}
114
115impl DerefMut for Runtime {
116 fn deref_mut(&mut self) -> &mut Self::Target { &mut self.stock }
117}
118
119#[allow(clippy::result_large_err)]
120impl Runtime {
121 pub fn load(mut data_dir: PathBuf, chain: Chain) -> Result<Self, RuntimeError> {
122 data_dir.push(chain.to_string());
123 #[cfg(feature = "log")]
124 debug!("Using data directory '{}'", data_dir.display());
125 fs::create_dir_all(&data_dir)?;
126
127 let mut stock_path = data_dir.clone();
128 stock_path.push("stock.dat");
129 #[cfg(feature = "log")]
130 debug!("Reading stock from '{}'", stock_path.display());
131 let stock = if !stock_path.exists() {
132 #[cfg(feature = "log")]
133 info!("Stock file not found, creating default stock");
134 #[cfg(feature = "cli")]
135 eprintln!("Stock file not found, creating default stock");
136 let stock = Stock::default();
137 stock.store(&stock_path)?;
138 stock
139 } else {
140 Stock::load(&stock_path)?
141 };
142
143 let mut wallets_path = data_dir.clone();
144 wallets_path.push("wallets.yml");
145 #[cfg(feature = "log")]
146 debug!("Reading wallets from '{}'", wallets_path.display());
147 let wallets = if !wallets_path.exists() {
148 #[cfg(feature = "log")]
149 info!("Wallet file not found, creating new wallet list");
150 #[cfg(feature = "cli")]
151 eprintln!("Wallet file not found, creating new wallet list");
152 empty!()
153 } else {
154 let wallets_fd = File::open(&wallets_path)?;
155 serde_yaml::from_reader(&wallets_fd)?
156 };
157
158 Ok(Self {
159 stock_path,
160 wallets_path,
161 stock,
162 wallets,
163 chain,
164 })
165 }
166
167 pub fn unload(self) {}
168
169 pub fn create_wallet(
170 &mut self,
171 name: &Ident,
172 xpub: ExtendedPubKey,
173 ) -> Result<&RgbDescr, RuntimeError> {
174 let descr = RgbDescr::Tapret(Tapret {
175 xpub,
176 taprets: empty!(),
177 });
178 let entry = match self.wallets.entry(name.clone()) {
179 Entry::Occupied(_) => return Err(format!("wallet named {name} already exists").into()),
180 Entry::Vacant(entry) => entry.insert(descr),
181 };
182 Ok(entry)
183 }
184
185 pub fn wallet(&mut self, name: &Ident) -> Result<RgbWallet, RuntimeError> {
186 let descr = self
187 .wallets
188 .get(name)
189 .ok_or(RuntimeError::WalletUnknown(name.clone()))?;
190 Ok(RgbWallet::new(descr.clone()))
191 }
192
193 pub fn import_contract<R: ResolveHeight>(
194 &mut self,
195 contract: Contract,
196 resolver: &mut R,
197 ) -> Result<validation::Status, RuntimeError>
198 where
199 R::Error: 'static,
200 {
201 self.stock
202 .import_contract(contract, resolver)
203 .map_err(RuntimeError::from)
204 }
205
206 pub fn validate_transfer(
207 &mut self,
208 transfer: Transfer,
209 resolver: &mut impl ResolveTx,
210 ) -> Result<Transfer, RuntimeError> {
211 transfer
212 .validate(resolver)
213 .map_err(|invalid| invalid.validation_status().expect("just validated").clone())
214 .map_err(RuntimeError::from)
215 }
216
217 pub fn accept_transfer<R: ResolveHeight>(
218 &mut self,
219 transfer: Transfer,
220 resolver: &mut R,
221 force: bool,
222 ) -> Result<validation::Status, RuntimeError>
223 where
224 R::Error: 'static,
225 {
226 self.stock
227 .accept_transfer(transfer, resolver, force)
228 .map_err(RuntimeError::from)
229 }
230}
231
232impl Drop for Runtime {
233 fn drop(&mut self) {
234 self.stock
235 .store(&self.stock_path)
236 .expect("unable to save stock");
237 let wallets_fd = File::create(&self.wallets_path)
238 .expect("unable to access wallet file; wallets are not saved");
239 serde_yaml::to_writer(wallets_fd, &self.wallets).expect("unable to save wallets");
240 }
241}