1use alloc::collections::BTreeMap;
26use core::borrow::Borrow;
27use core::cell::RefCell;
28use std::collections::HashMap;
29use std::io;
30
31use amplify::confinement::{KeyedCollection, SmallOrdMap};
32use amplify::MultiError;
33use commit_verify::StrictHash;
34use hypersonic::{
35 AcceptError, AuthToken, CallParams, CodexId, ContractId, ContractName, Opid, Stock,
36};
37use indexmap::{IndexMap, IndexSet};
38use rgb::RgbSeal;
39use strict_encoding::{
40 ReadRaw, StrictDecode, StrictDumb, StrictEncode, StrictReader, StrictWriter, WriteRaw,
41};
42use strict_types::StrictVal;
43
44use crate::{
45 parse_consignment, Articles, Consensus, Consignment, ConsumeError, Contract, ContractRef,
46 ContractState, CreateParams, Identity, ImmutableState, Issuer, Operation, OwnedState, Pile,
47 SigBlob, StateName, Stockpile, WitnessStatus,
48};
49
50pub const CONSIGN_VERSION: u16 = 0;
51#[cfg(feature = "binfile")]
52pub use _fs::CONSIGN_MAGIC_NUMBER;
53
54#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display)]
55#[display("{contract_id}/{state_name}")]
56#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
57pub struct ContractStateName {
58 pub contract_id: ContractId,
59 pub state_name: StateName,
60}
61
62impl ContractStateName {
63 pub fn new(contract_id: ContractId, state_name: StateName) -> Self {
64 ContractStateName { contract_id, state_name }
65 }
66}
67
68#[derive(Clone, Eq, PartialEq, Debug)]
69#[cfg_attr(
70 feature = "serde",
71 derive(Serialize, Deserialize),
72 serde(
73 rename_all = "camelCase",
74 bound = "Seal: serde::Serialize + for<'d> serde::Deserialize<'d>"
75 )
76)]
77pub struct WalletState<Seal> {
78 pub immutable: BTreeMap<ContractStateName, Vec<ImmutableState>>,
79 pub owned: BTreeMap<ContractStateName, Vec<OwnedState<Seal>>>,
80 pub aggregated: BTreeMap<ContractStateName, StrictVal>,
81}
82
83impl<Seal> Default for WalletState<Seal> {
84 fn default() -> Self { Self { immutable: bmap! {}, owned: bmap! {}, aggregated: bmap! {} } }
85}
86
87impl<Seal> WalletState<Seal> {
88 pub fn from_contracts_state(
89 contracts: impl IntoIterator<Item = (ContractId, ContractState<Seal>)>,
90 ) -> Self {
91 let mut wallet_state = WalletState::default();
92 for (contract_id, contract_state) in contracts {
93 for (state_name, state) in contract_state.immutable {
94 wallet_state
95 .immutable
96 .insert(ContractStateName::new(contract_id, state_name), state);
97 }
98 for (state_name, state) in contract_state.owned {
99 wallet_state
100 .owned
101 .insert(ContractStateName::new(contract_id, state_name), state);
102 }
103 for (state_name, state) in contract_state.aggregated {
104 wallet_state
105 .aggregated
106 .insert(ContractStateName::new(contract_id, state_name), state);
107 }
108 }
109 wallet_state
110 }
111}
112
113#[derive(Clone, Debug)]
120pub struct Contracts<
121 Sp,
122 S = HashMap<CodexId, Issuer>,
124 C = HashMap<ContractId, Contract<<Sp as Stockpile>::Stock, <Sp as Stockpile>::Pile>>,
125> where
126 Sp: Stockpile,
127 S: KeyedCollection<Key = CodexId, Value = Issuer>,
128 C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
129{
130 issuers: RefCell<S>,
131 contracts: RefCell<C>,
132 persistence: Sp,
133}
134
135impl<Sp, S, C> Contracts<Sp, S, C>
136where
137 Sp: Stockpile,
138 S: KeyedCollection<Key = CodexId, Value = Issuer>,
139 C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
140{
141 pub fn load(persistence: Sp) -> Self
142 where
143 S: Default,
144 C: Default,
145 {
146 Self { issuers: none!(), contracts: none!(), persistence }
147 }
148
149 fn with_contract<R>(
150 &self,
151 id: ContractId,
152 f: impl FnOnce(&Contract<Sp::Stock, Sp::Pile>) -> R,
153 or: Option<R>,
154 ) -> R {
155 if self.contracts.borrow().contains_key(&id) {
158 return f(self.contracts.borrow().get(&id).unwrap());
159 }
160 if let Some(contract) = self.persistence.contract(id) {
161 let res = f(&contract);
162 self.contracts.borrow_mut().insert(id, contract);
163 res
164 } else if let Some(or) = or {
165 or
166 } else {
167 panic!("Contract {id} not found")
168 }
169 }
170
171 fn with_contract_mut<R>(
172 &mut self,
173 id: ContractId,
174 f: impl FnOnce(&mut Contract<Sp::Stock, Sp::Pile>) -> R,
175 ) -> R {
176 if self.contracts.borrow().contains_key(&id) {
179 return f(self.contracts.borrow_mut().get_mut(&id).unwrap());
180 }
181 if let Some(mut contract) = self.persistence.contract(id) {
182 let res = f(&mut contract);
183 self.contracts.borrow_mut().insert(id, contract);
184 res
185 } else {
186 panic!("Contract {id} not found")
187 }
188 }
189
190 #[cfg(feature = "async")]
191 #[allow(clippy::await_holding_refcell_ref)]
192 async fn with_contract_mut_async<R>(
193 &mut self,
194 id: ContractId,
195 f: impl AsyncFnOnce(&mut Contract<Sp::Stock, Sp::Pile>) -> R,
196 ) -> R {
197 if self.contracts.borrow().contains_key(&id) {
200 return f(self.contracts.borrow_mut().get_mut(&id).unwrap()).await;
201 }
202 if let Some(mut contract) = self.persistence.contract(id) {
203 let res = f(&mut contract).await;
204 self.contracts.borrow_mut().insert(id, contract);
205 res
206 } else {
207 panic!("Contract {id} not found")
208 }
209 }
210}
211
212impl<Sp, S, C> Contracts<Sp, S, C>
213where
214 Sp: Stockpile,
215 S: KeyedCollection<Key = CodexId, Value = Issuer>,
216 C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
217{
218 pub fn codex_ids(&self) -> impl Iterator<Item = CodexId> + use<'_, Sp, S, C> {
219 self.persistence.codex_ids()
220 }
221
222 pub fn issuers_count(&self) -> usize { self.persistence.issuers_count() }
223
224 pub fn has_issuer(&self, codex_id: CodexId) -> bool { self.persistence.has_issuer(codex_id) }
225
226 pub fn issuers(&self) -> impl Iterator<Item = (CodexId, Issuer)> + use<'_, Sp, S, C> {
227 self.persistence
228 .codex_ids()
229 .filter_map(|codex_id| self.issuer(codex_id).map(|schema| (codex_id, schema)))
230 }
231
232 pub fn issuer(&self, codex_id: CodexId) -> Option<Issuer> {
233 if let Some(issuer) = self.issuers.borrow().get(&codex_id) {
234 return Some(issuer.clone());
235 };
236 let issuer = self.persistence.issuer(codex_id)?;
237 self.issuers.borrow_mut().insert(codex_id, issuer);
238 self.issuers.borrow().get(&codex_id).cloned()
239 }
240
241 pub fn contracts_count(&self) -> usize { self.persistence.contracts_count() }
242
243 pub fn has_contract(&self, contract_id: ContractId) -> bool {
244 self.persistence.has_contract(contract_id)
245 }
246
247 pub fn contract_ids(&self) -> impl Iterator<Item = ContractId> + use<'_, Sp, S, C> {
248 self.persistence.contract_ids()
249 }
250
251 pub fn contract_state(
260 &self,
261 contract_id: ContractId,
262 ) -> ContractState<<Sp::Pile as Pile>::Seal> {
263 self.with_contract(contract_id, |contract| contract.state(), None)
264 }
265
266 pub fn contract_articles(&self, contract_id: ContractId) -> Articles {
267 self.with_contract(contract_id, |contract| contract.articles().clone(), None)
268 }
269
270 pub fn find_contract_id(&self, r: impl Into<ContractRef>) -> Option<ContractId> {
271 match r.into() {
272 ContractRef::Id(id) if self.has_contract(id) => Some(id),
273 ContractRef::Id(_) => None,
274 ContractRef::Name(name) => {
275 let name = ContractName::Named(name);
276 if let Some(id) = self
277 .contracts
278 .borrow()
279 .iter()
280 .find(|(_, contract)| contract.articles().issue().meta.name == name)
281 .map(|(id, _)| *id)
282 {
283 return Some(id);
284 }
285 self.persistence
286 .contract_ids()
287 .filter_map(|id| self.persistence.contract(id))
288 .find(|contract| contract.articles().issue().meta.name == name)
289 .map(|contract| contract.contract_id())
290 }
291 }
292 }
293
294 pub fn import_issuer(&mut self, issuer: Issuer) -> Result<CodexId, Sp::Error> {
295 let codex_id = issuer.codex_id();
296 let schema = self.persistence.import_issuer(issuer)?;
297 self.issuers.borrow_mut().insert(codex_id, schema);
298 Ok(codex_id)
299 }
300
301 fn check_layer1(
302 &self,
303 consensus: Consensus,
304 testnet: bool,
305 ) -> Result<(), MultiError<IssuerError, <Sp::Stock as Stock>::Error, <Sp::Pile as Pile>::Error>>
306 {
307 if consensus != self.persistence.consensus() {
308 return Err(MultiError::A(IssuerError::ConsensusMismatch));
309 }
310 if testnet != self.persistence.is_testnet() {
311 Err(if testnet {
312 MultiError::A(IssuerError::TestnetMismatch)
313 } else {
314 MultiError::A(IssuerError::MainnetMismatch)
315 })
316 } else {
317 Ok(())
318 }
319 }
320
321 pub fn issue(
322 &mut self,
323 params: CreateParams<<<Sp::Pile as Pile>::Seal as RgbSeal>::Definition>,
324 ) -> Result<
325 ContractId,
326 MultiError<IssuerError, <Sp::Stock as Stock>::Error, <Sp::Pile as Pile>::Error>,
327 > {
328 self.check_layer1(params.consensus, params.testnet)?;
329 let contract = self.persistence.issue(params)?;
330 let id = contract.contract_id();
331 self.contracts.borrow_mut().insert(id, contract);
332 Ok(id)
333 }
334
335 pub fn contract_call(
346 &mut self,
347 contract_id: ContractId,
348 call: CallParams,
349 seals: SmallOrdMap<u16, <<Sp::Pile as Pile>::Seal as RgbSeal>::Definition>,
350 ) -> Result<Operation, MultiError<AcceptError, <Sp::Stock as Stock>::Error>> {
351 self.with_contract_mut(contract_id, |contract| contract.call(call, seals))
352 }
353
354 #[cfg(not(feature = "async"))]
355 pub fn update_witnesses<E: core::error::Error>(
360 &mut self,
361 resolver: impl Fn(<<Sp::Pile as Pile>::Seal as RgbSeal>::WitnessId) -> Result<WitnessStatus, E>,
362 last_block_height: u64,
363 min_conformations: u32,
364 ) -> Result<(), MultiError<SyncError<E>, <Sp::Stock as Stock>::Error>> {
365 let mut changed_statuses = IndexMap::<_, WitnessStatus>::new();
366 let contract_ids = self.persistence.contract_ids().collect::<IndexSet<_>>();
367 for contract_id in contract_ids {
368 self.with_contract_mut(
369 contract_id,
370 |contract| -> Result<(), MultiError<SyncError<E>, <Sp::Stock as Stock>::Error>> {
371 for witness_id in contract.witness_ids() {
372 let old_status = contract.witness_status(witness_id);
373 if matches!(old_status, WitnessStatus::Mined(height) if last_block_height - height.get() > min_conformations as u64) {
374 continue
375 }
376 let new_status = match changed_statuses.get(&witness_id) {
377 None => resolver(witness_id)
378 .map_err(SyncError::Status)
379 .map_err(MultiError::A),
380 Some(witness_id) => Ok(*witness_id),
381 }?;
382 if new_status != old_status {
383 changed_statuses.insert(witness_id, new_status);
384 }
385 }
386 contract
387 .sync(changed_statuses.iter().map(|(id, status)| (*id, *status)))
388 .map_err(MultiError::from_other_a)?;
389 Ok(())
390 },
391 )?;
392 }
393 Ok(())
394 }
395
396 #[cfg(feature = "async")]
397 pub async fn update_witnesses_async<E: core::error::Error>(
402 &mut self,
403 resolver: impl AsyncFn(
404 <<Sp::Pile as Pile>::Seal as RgbSeal>::WitnessId,
405 ) -> Result<WitnessStatus, E>,
406 last_block_height: u64,
407 min_conformations: u32,
408 ) -> Result<(), MultiError<SyncError<E>, <Sp::Stock as Stock>::Error>> {
409 let mut changed_statuses = IndexMap::<_, WitnessStatus>::new();
410 let contract_ids = self.persistence.contract_ids().collect::<IndexSet<_>>();
411 for contract_id in contract_ids {
412 self.with_contract_mut_async(
413 contract_id,
414 async |contract| -> Result<(), MultiError<SyncError<E>, <Sp::Stock as Stock>::Error>> {
415 for witness_id in contract.witness_ids() {
416 let old_status = contract.witness_status(witness_id);
417 if matches!(old_status, WitnessStatus::Mined(height) if last_block_height - height.get() > min_conformations as u64) {
418 continue
419 }
420 let new_status = match changed_statuses.get(&witness_id) {
421 None => resolver(witness_id)
422 .await
423 .map_err(SyncError::Status)
424 .map_err(MultiError::A),
425 Some(witness_id) => Ok(*witness_id),
426 }?;
427 if new_status != old_status {
428 changed_statuses.insert(witness_id, new_status);
429 }
430 }
431 contract
432 .sync(changed_statuses.iter().map(|(id, status)| (*id, *status)))
433 .map_err(MultiError::from_other_a)?;
434 Ok(())
435 },
436 ).await?;
437 }
438 Ok(())
439 }
440
441 pub fn include(
448 &mut self,
449 contract_id: ContractId,
450 opid: Opid,
451 pub_witness: &<<Sp::Pile as Pile>::Seal as RgbSeal>::Published,
452 anchor: <<Sp::Pile as Pile>::Seal as RgbSeal>::Client,
453 ) {
454 self.with_contract_mut(contract_id, |contract| contract.include(opid, anchor, pub_witness))
455 }
456
457 pub fn export(
468 &self,
469 contract_id: ContractId,
470 writer: StrictWriter<impl WriteRaw>,
471 ) -> io::Result<()>
472 where
473 <<Sp::Pile as Pile>::Seal as RgbSeal>::Client: StrictDumb + StrictEncode,
474 <<Sp::Pile as Pile>::Seal as RgbSeal>::Published: StrictDumb + StrictEncode,
475 <<Sp::Pile as Pile>::Seal as RgbSeal>::WitnessId: StrictEncode,
476 {
477 self.with_contract(contract_id, |contract| contract.export(writer), None)
478 }
479
480 pub fn purge(&mut self, contract_id: ContractId) -> Result<(), Sp::Error> {
482 self.contracts.borrow_mut().remove(&contract_id);
483 self.persistence.purge(contract_id)?;
484 Ok(())
485 }
486
487 pub fn consign(
499 &mut self,
500 contract_id: ContractId,
501 terminals: impl IntoIterator<Item = impl Borrow<AuthToken>>,
502 writer: StrictWriter<impl WriteRaw>,
503 ) -> io::Result<()>
504 where
505 <<Sp::Pile as Pile>::Seal as RgbSeal>::Client: StrictDumb + StrictEncode,
506 <<Sp::Pile as Pile>::Seal as RgbSeal>::Published: StrictDumb + StrictEncode,
507 <<Sp::Pile as Pile>::Seal as RgbSeal>::WitnessId: StrictEncode,
508 {
509 self.with_contract_mut(contract_id, |contract| contract.consign(terminals, writer))
510 }
511
512 pub fn consume<E>(
528 &mut self,
529 allow_unknown: bool,
530 reader: &mut StrictReader<impl ReadRaw>,
531 seal_resolver: impl FnMut(
532 &Operation,
533 )
534 -> BTreeMap<u16, <<Sp::Pile as Pile>::Seal as RgbSeal>::Definition>,
535 sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>,
536 ) -> Result<
537 (),
538 MultiError<
539 ConsumeError<<<Sp::Pile as Pile>::Seal as RgbSeal>::Definition>,
540 <Sp::Stock as Stock>::Error,
541 <Sp::Pile as Pile>::Error,
542 >,
543 >
544 where
545 <Sp::Pile as Pile>::Conf: From<<Sp::Stock as Stock>::Conf>,
546 <<Sp::Pile as Pile>::Seal as RgbSeal>::Client: StrictDecode,
547 <<Sp::Pile as Pile>::Seal as RgbSeal>::Published: StrictDecode,
548 <<Sp::Pile as Pile>::Seal as RgbSeal>::WitnessId: StrictDecode,
549 {
550 let contract_id = parse_consignment(reader).map_err(MultiError::from_a)?;
552 if !self.has_contract(contract_id) {
553 if allow_unknown {
554 let consignment = Consignment::strict_decode(reader).map_err(MultiError::from_a)?;
555 let articles = consignment
560 .articles(sig_validator)
561 .map_err(MultiError::from_a)?;
562 self.check_layer1(
563 articles.contract_meta().consensus,
564 articles.contract_meta().testnet,
565 )
566 .map_err(MultiError::from_other_a)?;
567
568 let contract = self.persistence.import_contract(articles, consignment)?;
569 self.contracts.borrow_mut().insert(contract_id, contract);
570 Ok(())
571 } else {
572 Err(MultiError::A(ConsumeError::UnknownContract(contract_id)))
573 }
574 } else {
575 self.with_contract_mut(contract_id, |contract| {
576 contract.consume_internal(reader, seal_resolver, sig_validator)
577 })
578 .map_err(|err| match err {
579 MultiError::A(a) => MultiError::A(a),
580 MultiError::B(b) => MultiError::B(b),
581 MultiError::C(_) => unreachable!(),
582 })
583 }
584 }
585}
586
587#[derive(Clone, PartialEq, Eq, Debug, Display, Error, From)]
588#[display(doc_comments)]
589pub enum IssuerError {
590 IssuerMismatch,
592 ConsensusMismatch,
594 TestnetMismatch,
596 MainnetMismatch,
598 UnknownCodex(CodexId),
600
601 #[from]
602 #[display(inner)]
603 Inner(hypersonic::IssueError),
604}
605
606#[cfg(feature = "binfile")]
607mod _fs {
608 use std::path::Path;
609
610 use binfile::BinFile;
611 use strict_encoding::StreamReader;
612
613 use super::*;
614
615 pub const CONSIGN_MAGIC_NUMBER: u64 = u64::from_be_bytes(*b"RGBCNSGN");
616
617 impl<Sp, S, C> Contracts<Sp, S, C>
618 where
619 Sp: Stockpile,
620 S: KeyedCollection<Key = CodexId, Value = Issuer>,
621 C: KeyedCollection<Key = ContractId, Value = Contract<Sp::Stock, Sp::Pile>>,
622 {
623 pub fn export_to_file(
634 &self,
635 path: impl AsRef<Path>,
636 contract_id: ContractId,
637 ) -> io::Result<()>
638 where
639 <<Sp::Pile as Pile>::Seal as RgbSeal>::Client: StrictDumb + StrictEncode,
640 <<Sp::Pile as Pile>::Seal as RgbSeal>::Published: StrictDumb + StrictEncode,
641 <<Sp::Pile as Pile>::Seal as RgbSeal>::WitnessId: StrictEncode,
642 {
643 self.with_contract(contract_id, |contract| contract.export_to_file(path), None)
644 }
645
646 pub fn consign_to_file(
658 &self,
659 path: impl AsRef<Path>,
660 contract_id: ContractId,
661 terminals: impl IntoIterator<Item = impl Borrow<AuthToken>>,
662 ) -> io::Result<()>
663 where
664 <<Sp::Pile as Pile>::Seal as RgbSeal>::Client: StrictDumb + StrictEncode,
665 <<Sp::Pile as Pile>::Seal as RgbSeal>::Published: StrictDumb + StrictEncode,
666 <<Sp::Pile as Pile>::Seal as RgbSeal>::WitnessId: StrictEncode,
667 {
668 self.with_contract(
669 contract_id,
670 |contract| contract.consign_to_file(path, terminals),
671 None,
672 )
673 }
674
675 pub fn consume_from_file<E>(
692 &mut self,
693 allow_unknown: bool,
694 path: impl AsRef<Path>,
695 seal_resolver: impl FnMut(
696 &Operation,
697 ) -> BTreeMap<
698 u16,
699 <<Sp::Pile as Pile>::Seal as RgbSeal>::Definition,
700 >,
701 sig_validator: impl FnOnce(StrictHash, &Identity, &SigBlob) -> Result<(), E>,
702 ) -> Result<
703 (),
704 MultiError<
705 ConsumeError<<<Sp::Pile as Pile>::Seal as RgbSeal>::Definition>,
706 <Sp::Stock as Stock>::Error,
707 <Sp::Pile as Pile>::Error,
708 >,
709 >
710 where
711 <Sp::Pile as Pile>::Conf: From<<Sp::Stock as Stock>::Conf>,
712 <<Sp::Pile as Pile>::Seal as RgbSeal>::Client: StrictDecode,
713 <<Sp::Pile as Pile>::Seal as RgbSeal>::Published: StrictDecode,
714 <<Sp::Pile as Pile>::Seal as RgbSeal>::WitnessId: StrictDecode,
715 {
716 let file = BinFile::<CONSIGN_MAGIC_NUMBER, CONSIGN_VERSION>::open(path)
717 .map_err(MultiError::from_a)?;
718 let mut reader = StrictReader::with(StreamReader::new::<{ usize::MAX }>(file));
719 self.consume(allow_unknown, &mut reader, seal_resolver, sig_validator)
720 }
721 }
722}
723
724#[derive(Debug, Display, Error, From)]
725#[display(doc_comments)]
726pub enum SyncError<E: core::error::Error> {
727 Wallet(E),
729
730 Status(E),
732
733 #[from]
734 #[display(inner)]
735 Forward(AcceptError),
736}