rgb/
runtime.rs

1// RGB smart contracts for Bitcoin & Lightning
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2019-2023 by
6//     Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2019-2023 LNP/BP Standards Association. All rights reserved.
9//
10// Licensed under the Apache License, Version 2.0 (the "License");
11// you may not use this file except in compliance with the License.
12// You may obtain a copy of the License at
13//
14//     http://www.apache.org/licenses/LICENSE-2.0
15//
16// Unless required by applicable law or agreed to in writing, software
17// distributed under the License is distributed on an "AS IS" BASIS,
18// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19// See the License for the specific language governing permissions and
20// limitations under the License.
21
22use 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    /// wallet with id '{0}' is not known to the system
72    #[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    /// the contract source doesn't provide all state information required by
86    /// the schema. This means that some of the global fields or assignments are
87    /// missed.
88    #[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}