sonicapi/
builders.rs

1// SONIC: Standard library for formally-verifiable distributed contracts
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Designed in 2019-2025 by Dr Maxim Orlovsky <orlovsky@ubideco.org>
6// Written in 2024-2025 by Dr Maxim Orlovsky <orlovsky@ubideco.org>
7//
8// Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland.
9// Copyright (C) 2024-2025 Laboratories for Ubiquitous Deterministic Computing (UBIDECO),
10//                         Institute for Distributed and Cognitive Systems (InDCS), Switzerland.
11// Copyright (C) 2019-2025 Dr Maxim Orlovsky.
12// All rights under the above copyrights are reserved.
13//
14// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
15// in compliance with the License. You may obtain a copy of the License at
16//
17//        http://www.apache.org/licenses/LICENSE-2.0
18//
19// Unless required by applicable law or agreed to in writing, software distributed under the License
20// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
21// or implied. See the License for the specific language governing permissions and limitations under
22// the License.
23
24use std::convert::Infallible;
25use std::ops::{Deref, DerefMut};
26
27use amplify::confinement::SmallVec;
28use amplify::num::u256;
29use chrono::{DateTime, Utc};
30use strict_encoding::TypeName;
31use strict_types::{StrictVal, TypeSystem};
32use ultrasonic::{
33    fe256, AuthToken, CallId, CellAddr, CellLock, CodexId, Consensus, ContractId, ContractMeta, ContractName, Genesis,
34    Identity, Input, Issue, Operation, StateCell, StateData, StateValue,
35};
36
37use crate::{Api, Articles, DataCell, Issuer, IssuerId, MethodName, StateAtom, StateName};
38
39#[derive(Clone, PartialEq, Eq, Debug)]
40#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
41pub struct NamedState<T> {
42    pub name: StateName,
43    #[cfg_attr(feature = "serde", serde(flatten))]
44    pub state: T,
45}
46
47impl NamedState<DataCell> {
48    pub fn new_unlocked(name: impl Into<StateName>, auth: impl Into<AuthToken>, data: impl Into<StrictVal>) -> Self {
49        NamedState { name: name.into(), state: DataCell::new_unlocked(auth, data) }
50    }
51}
52
53#[derive(Clone, PartialEq, Eq, Debug)]
54#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
55pub struct CoreParams {
56    pub method: MethodName,
57    pub global: Vec<NamedState<StateAtom>>,
58    pub owned: Vec<NamedState<DataCell>>,
59}
60
61impl CoreParams {
62    pub fn new(method: impl Into<MethodName>) -> Self {
63        Self { method: method.into(), global: none!(), owned: none!() }
64    }
65
66    pub fn push_global_verified(&mut self, name: impl Into<StateName>, state: impl Into<StateAtom>) {
67        self.global
68            .push(NamedState { name: name.into(), state: state.into() });
69    }
70
71    pub fn push_owned_unlocked(
72        &mut self,
73        name: impl Into<StateName>,
74        auth: impl Into<AuthToken>,
75        data: impl Into<StrictVal>,
76    ) {
77        self.owned.push(NamedState::new_unlocked(name, auth, data));
78    }
79}
80
81#[derive(Copy, Clone, PartialEq, Eq, Debug)]
82#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase", untagged))]
83pub enum VersionRange {
84    Range { min: u16, max: u16 },
85    After { min: u16 },
86    Before { max: u16 },
87}
88
89#[derive(Copy, Clone, PartialEq, Eq, Debug, From)]
90#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase", untagged))]
91pub enum IssuerSpec {
92    #[from]
93    Exact(IssuerId),
94
95    #[from]
96    Latest(CodexId),
97
98    #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
99    ExactVer { codex_id: CodexId, version: u16 },
100
101    #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
102    VersionRange { codex_id: CodexId, version: VersionRange },
103}
104
105impl IssuerSpec {
106    pub fn check(&self, issuer_id: IssuerId) -> bool {
107        match self {
108            IssuerSpec::Exact(id) => *id == issuer_id,
109            IssuerSpec::Latest(codex_id) => *codex_id == issuer_id.codex_id,
110            IssuerSpec::ExactVer { codex_id, version } => {
111                *codex_id == issuer_id.codex_id && *version == issuer_id.version
112            }
113            IssuerSpec::VersionRange { codex_id, version: VersionRange::After { min } } => {
114                *codex_id == issuer_id.codex_id && issuer_id.version >= *min
115            }
116            IssuerSpec::VersionRange { codex_id, version: VersionRange::Before { max } } => {
117                *codex_id == issuer_id.codex_id && issuer_id.version < *max
118            }
119            IssuerSpec::VersionRange { codex_id, version: VersionRange::Range { min, max } } => {
120                *codex_id == issuer_id.codex_id && (*min..*max).contains(&issuer_id.version)
121            }
122        }
123    }
124
125    pub fn codex_id(&self) -> CodexId {
126        match self {
127            IssuerSpec::Exact(id) => id.codex_id,
128            IssuerSpec::Latest(codex_id) => *codex_id,
129            IssuerSpec::ExactVer { codex_id, .. } => *codex_id,
130            IssuerSpec::VersionRange { codex_id, .. } => *codex_id,
131        }
132    }
133}
134
135#[derive(Clone, PartialEq, Eq, Debug)]
136#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
137pub struct IssueParams {
138    pub issuer: IssuerSpec,
139    pub name: TypeName,
140    pub consensus: Consensus,
141    pub testnet: bool,
142    pub timestamp: Option<DateTime<Utc>>,
143    #[cfg_attr(feature = "serde", serde(flatten))]
144    pub core: CoreParams,
145}
146
147impl Deref for IssueParams {
148    type Target = CoreParams;
149
150    fn deref(&self) -> &Self::Target { &self.core }
151}
152
153impl DerefMut for IssueParams {
154    fn deref_mut(&mut self) -> &mut Self::Target { &mut self.core }
155}
156
157impl IssueParams {
158    pub fn new_testnet(codex_id: CodexId, name: impl Into<TypeName>, consensus: Consensus) -> Self {
159        Self {
160            issuer: IssuerSpec::Latest(codex_id),
161            name: name.into(),
162            consensus,
163            testnet: true,
164            timestamp: None,
165            core: CoreParams::new("issue"),
166        }
167    }
168
169    pub fn set_timestamp(&mut self, timestamp: DateTime<Utc>) { self.timestamp = Some(timestamp); }
170
171    pub fn set_timestamp_now(&mut self) { self.timestamp = Some(Utc::now()); }
172}
173
174impl Issuer {
175    pub fn start_issue(self, method: impl Into<MethodName>, consensus: Consensus, testnet: bool) -> IssueBuilder {
176        let builder = Builder::new(self.call_id(method));
177        IssueBuilder { builder, issuer: self, testnet, consensus }
178    }
179
180    pub fn start_issue_mainnet(self, method: impl Into<MethodName>, consensus: Consensus) -> IssueBuilder {
181        self.start_issue(method, consensus, false)
182    }
183    pub fn start_issue_testnet(self, method: impl Into<MethodName>, consensus: Consensus) -> IssueBuilder {
184        self.start_issue(method, consensus, true)
185    }
186
187    pub fn issue(self, params: IssueParams) -> Articles {
188        if !params.issuer.check(self.issuer_id()) {
189            panic!("issuer version does not match requested version");
190        }
191
192        let mut builder = self.start_issue(params.core.method, params.consensus, params.testnet);
193
194        for NamedState { name, state } in params.core.global {
195            builder = builder.append(name, state.verified, state.unverified)
196        }
197        for NamedState { name, state } in params.core.owned {
198            builder = builder.assign(name, state.auth, state.data, state.lock)
199        }
200
201        let timestamp = params.timestamp.unwrap_or_else(Utc::now).timestamp();
202        builder.finish(params.name, timestamp)
203    }
204}
205
206#[derive(Clone, Debug)]
207pub struct IssueBuilder {
208    builder: Builder,
209    issuer: Issuer,
210    testnet: bool,
211    consensus: Consensus,
212}
213
214impl IssueBuilder {
215    pub fn append(mut self, name: impl Into<StateName>, data: StrictVal, raw: Option<StrictVal>) -> Self {
216        self.builder = self
217            .builder
218            .add_global(name, data, raw, self.issuer.default_api(), self.issuer.types());
219        self
220    }
221
222    pub fn assign(
223        mut self,
224        name: impl Into<StateName>,
225        auth: AuthToken,
226        data: StrictVal,
227        lock: Option<CellLock>,
228    ) -> Self {
229        self.builder = self
230            .builder
231            .add_owned(name, auth, data, lock, self.issuer.default_api(), self.issuer.types());
232        self
233    }
234
235    pub fn finish(self, name: impl Into<TypeName>, timestamp: i64) -> Articles {
236        let meta = ContractMeta {
237            consensus: self.consensus,
238            testnet: self.testnet,
239            timestamp,
240            features: default!(),
241            name: ContractName::Named(name.into()),
242            issuer: Identity::default(),
243        };
244        let genesis = self.builder.issue_genesis(self.issuer.codex_id());
245        let (codex, semantics) = self.issuer.dismember();
246        let issue = Issue { version: default!(), meta, codex, genesis };
247        Articles::with(semantics, issue, None, |_, _, _| -> Result<_, Infallible> { unreachable!() })
248            .expect("broken issue builder")
249    }
250}
251
252#[derive(Clone, Debug)]
253pub struct Builder {
254    call_id: CallId,
255    destructible_out: SmallVec<StateCell>,
256    immutable_out: SmallVec<StateData>,
257}
258
259impl Builder {
260    pub fn new(call_id: CallId) -> Self { Builder { call_id, destructible_out: none!(), immutable_out: none!() } }
261
262    pub fn add_global(
263        mut self,
264        name: impl Into<StateName>,
265        data: StrictVal,
266        raw: Option<StrictVal>,
267        api: &Api,
268        sys: &TypeSystem,
269    ) -> Self {
270        let name = name.into();
271        let data = api
272            .build_immutable(name.clone(), data, raw, sys)
273            .unwrap_or_else(|e| panic!("invalid immutable state '{name}'; {e}"));
274        self.immutable_out
275            .push(data)
276            .expect("too many state elements");
277        self
278    }
279
280    pub fn add_owned(
281        mut self,
282        name: impl Into<StateName>,
283        auth: AuthToken,
284        data: StrictVal,
285        lock: Option<CellLock>,
286        api: &Api,
287        sys: &TypeSystem,
288    ) -> Self {
289        let data = api
290            .build_destructible(name, data, sys)
291            .expect("invalid destructible state");
292        let cell = StateCell { data, auth, lock };
293        self.destructible_out
294            .push(cell)
295            .expect("too many state elements");
296        self
297    }
298
299    pub fn issue_genesis(self, codex_id: CodexId) -> Genesis {
300        Genesis {
301            version: default!(),
302            codex_id,
303            call_id: self.call_id,
304            nonce: fe256::from(u256::ZERO),
305            blank0: zero!(),
306            blank1: zero!(),
307            blank2: zero!(),
308            destructible_out: self.destructible_out,
309            immutable_out: self.immutable_out,
310        }
311    }
312}
313
314#[derive(Clone, Debug)]
315pub struct BuilderRef<'c> {
316    type_system: &'c TypeSystem,
317    api: &'c Api,
318    inner: Builder,
319}
320
321impl<'c> BuilderRef<'c> {
322    pub fn new(api: &'c Api, call_id: CallId, sys: &'c TypeSystem) -> Self {
323        BuilderRef { type_system: sys, api, inner: Builder::new(call_id) }
324    }
325
326    pub fn add_global(mut self, name: impl Into<StateName>, data: StrictVal, raw: Option<StrictVal>) -> Self {
327        self.inner = self
328            .inner
329            .add_global(name, data, raw, self.api, self.type_system);
330        self
331    }
332
333    pub fn add_owned(
334        mut self,
335        name: impl Into<StateName>,
336        auth: AuthToken,
337        data: StrictVal,
338        lock: Option<CellLock>,
339    ) -> Self {
340        self.inner = self
341            .inner
342            .add_owned(name, auth, data, lock, self.api, self.type_system);
343        self
344    }
345
346    pub fn issue_genesis(self, codex_id: CodexId) -> Genesis { self.inner.issue_genesis(codex_id) }
347}
348
349#[derive(Clone, Debug)]
350pub struct OpBuilder {
351    contract_id: ContractId,
352    destructible_in: SmallVec<Input>,
353    immutable_in: SmallVec<CellAddr>,
354    inner: Builder,
355}
356
357impl OpBuilder {
358    pub fn new(contract_id: ContractId, call_id: CallId) -> Self {
359        let inner = Builder::new(call_id);
360        Self {
361            contract_id,
362            destructible_in: none!(),
363            immutable_in: none!(),
364            inner,
365        }
366    }
367
368    pub fn add_global(
369        mut self,
370        name: impl Into<StateName>,
371        data: StrictVal,
372        raw: Option<StrictVal>,
373        api: &Api,
374        sys: &TypeSystem,
375    ) -> Self {
376        self.inner = self.inner.add_global(name, data, raw, api, sys);
377        self
378    }
379
380    pub fn add_owned(
381        mut self,
382        name: impl Into<StateName>,
383        auth: AuthToken,
384        data: StrictVal,
385        lock: Option<CellLock>,
386        api: &Api,
387        sys: &TypeSystem,
388    ) -> Self {
389        self.inner = self.inner.add_owned(name, auth, data, lock, api, sys);
390        self
391    }
392
393    pub fn access(mut self, addr: CellAddr) -> Self {
394        self.immutable_in
395            .push(addr)
396            .expect("number of read memory cells exceeds 64k limit");
397        self
398    }
399
400    pub fn destroy(mut self, addr: CellAddr) -> Self {
401        let input = Input { addr, witness: StateValue::None };
402        self.destructible_in
403            .push(input)
404            .expect("the number of inputs exceeds the 64k limit");
405        self
406    }
407
408    pub fn destroy_satisfy(
409        mut self,
410        addr: CellAddr,
411        name: impl Into<StateName>,
412        witness: StrictVal,
413        api: &Api,
414        sys: &TypeSystem,
415    ) -> Self {
416        let witness = api
417            .build_witness(name, witness, sys)
418            .expect("invalid witness data");
419        let input = Input { addr, witness };
420        self.destructible_in
421            .push(input)
422            .expect("the number of inputs exceeds the 64k limit");
423        self
424    }
425
426    pub fn finalize(self) -> Operation {
427        Operation {
428            version: default!(),
429            contract_id: self.contract_id,
430            call_id: self.inner.call_id,
431            nonce: fe256::from(u256::ZERO),
432            witness: none!(),
433            destructible_in: self.destructible_in,
434            immutable_in: self.immutable_in,
435            destructible_out: self.inner.destructible_out,
436            immutable_out: self.inner.immutable_out,
437        }
438    }
439}
440
441#[derive(Clone, Debug)]
442pub struct OpBuilderRef<'c> {
443    type_system: &'c TypeSystem,
444    api: &'c Api,
445    inner: OpBuilder,
446}
447
448impl<'c> OpBuilderRef<'c> {
449    pub fn new(api: &'c Api, contract_id: ContractId, call_id: CallId, sys: &'c TypeSystem) -> Self {
450        let inner = OpBuilder::new(contract_id, call_id);
451        Self { api, type_system: sys, inner }
452    }
453
454    pub fn add_global(mut self, name: impl Into<StateName>, data: StrictVal, raw: Option<StrictVal>) -> Self {
455        self.inner = self
456            .inner
457            .add_global(name, data, raw, self.api, self.type_system);
458        self
459    }
460
461    pub fn add_owned(
462        mut self,
463        name: impl Into<StateName>,
464        auth: AuthToken,
465        data: StrictVal,
466        lock: Option<CellLock>,
467    ) -> Self {
468        self.inner = self
469            .inner
470            .add_owned(name, auth, data, lock, self.api, self.type_system);
471        self
472    }
473
474    pub fn access(mut self, addr: CellAddr) -> Self {
475        self.inner = self.inner.access(addr);
476        self
477    }
478
479    pub fn destroy(mut self, addr: CellAddr) -> Self {
480        self.inner = self.inner.destroy(addr);
481        self
482    }
483
484    pub fn destroy_satisfy(
485        mut self,
486        addr: CellAddr,
487        name: impl Into<StateName>,
488        witness: StrictVal,
489        api: &Api,
490        sys: &TypeSystem,
491    ) -> Self {
492        self.inner = self.inner.destroy_satisfy(addr, name, witness, api, sys);
493        self
494    }
495
496    pub fn finalize(self) -> Operation { self.inner.finalize() }
497}
498
499#[cfg(test)]
500mod test {
501    #![cfg_attr(coverage_nightly, coverage(off))]
502
503    use core::str::FromStr;
504
505    use super::*;
506    use crate::ApisChecksum;
507
508    #[test]
509    fn issuer_spec_yaml_latest() {
510        let val = IssuerSpec::Latest(strict_dumb!());
511        let s = "AAAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA#origami-bruno-life
512";
513        assert_eq!(serde_yaml::to_string(&val).unwrap(), s);
514        assert_eq!(serde_yaml::from_str::<IssuerSpec>(s).unwrap(), val);
515    }
516
517    #[test]
518    fn issuer_spec_yaml_exact() {
519        let val = IssuerSpec::Exact(strict_dumb!());
520        let s = "\
521codexId: AAAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA#origami-bruno-life
522version: 0
523checksum: AAAAAA
524";
525        assert_eq!(serde_yaml::to_string(&val).unwrap(), s);
526        assert_eq!(serde_yaml::from_str::<IssuerSpec>(s).unwrap(), val);
527    }
528
529    #[test]
530    fn issuer_spec_yaml_ver_nosum() {
531        let val = IssuerSpec::ExactVer { codex_id: strict_dumb!(), version: 0 };
532        let s = "\
533codexId: AAAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA#origami-bruno-life
534version: 0
535";
536        assert_eq!(serde_yaml::to_string(&val).unwrap(), s);
537        assert_eq!(serde_yaml::from_str::<IssuerSpec>(s).unwrap(), val);
538    }
539
540    #[test]
541    fn issuer_spec_yaml_min() {
542        let val = IssuerSpec::VersionRange {
543            codex_id: strict_dumb!(),
544            version: VersionRange::After { min: 0 },
545        };
546        let s = "\
547codexId: AAAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA#origami-bruno-life
548version:
549  min: 0
550";
551        assert_eq!(serde_yaml::to_string(&val).unwrap(), s);
552        assert_eq!(serde_yaml::from_str::<IssuerSpec>(s).unwrap(), val);
553    }
554
555    #[test]
556    fn issuer_spec_yaml_max() {
557        let val = IssuerSpec::VersionRange {
558            codex_id: strict_dumb!(),
559            version: VersionRange::Before { max: 1 },
560        };
561        let s = "\
562codexId: AAAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA#origami-bruno-life
563version:
564  max: 1
565";
566        assert_eq!(serde_yaml::to_string(&val).unwrap(), s);
567        assert_eq!(serde_yaml::from_str::<IssuerSpec>(s).unwrap(), val);
568    }
569
570    #[test]
571    fn issuer_spec_yaml_range() {
572        let val = IssuerSpec::VersionRange {
573            codex_id: strict_dumb!(),
574            version: VersionRange::Range { min: 0, max: 1 },
575        };
576        let s = "\
577codexId: AAAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA#origami-bruno-life
578version:
579  min: 0
580  max: 1
581";
582        assert_eq!(serde_yaml::to_string(&val).unwrap(), s);
583        assert_eq!(serde_yaml::from_str::<IssuerSpec>(s).unwrap(), val);
584    }
585
586    #[test]
587    fn issuer_display_fromstr() {
588        let s = "nmThRWDr-0hOJgJt-OFVCZTA-XX8aOWj-bkqWzK7-_jAtdhQ/0#NRIsWA";
589        let issuer_id = IssuerId::from_str(s).unwrap();
590        assert_eq!(issuer_id.to_string(), s);
591        assert_eq!(issuer_id.codex_id, CodexId::from_str(s.split_once("/").unwrap().0).unwrap());
592        assert_eq!(issuer_id.checksum, ApisChecksum::from_str(s.split_once("#").unwrap().1).unwrap());
593        assert_eq!(issuer_id.version, 0);
594    }
595
596    #[test]
597    fn issuer_check() {
598        let s = "nmThRWDr-0hOJgJt-OFVCZTA-XX8aOWj-bkqWzK7-_jAtdhQ/0#NRIsWA";
599        let issuer_id = IssuerId::from_str(s).unwrap();
600        assert!(IssuerSpec::Exact(issuer_id).check(issuer_id));
601
602        let mut changed_ver = issuer_id;
603        changed_ver.version += 1;
604        assert!(!IssuerSpec::Exact(issuer_id).check(changed_ver));
605        assert!(IssuerSpec::Latest(issuer_id.codex_id).check(changed_ver));
606        assert!(!IssuerSpec::ExactVer { codex_id: issuer_id.codex_id, version: issuer_id.version }.check(changed_ver));
607
608        let mut changed_sum = issuer_id;
609        changed_sum.checksum = ApisChecksum::from_str("rLAuRQ").unwrap();
610        assert!(!IssuerSpec::Exact(issuer_id).check(changed_sum));
611        assert!(IssuerSpec::Latest(issuer_id.codex_id).check(changed_sum));
612        assert!(IssuerSpec::ExactVer { codex_id: issuer_id.codex_id, version: issuer_id.version }.check(changed_sum));
613    }
614}