sonic_callreq/
data.rs

1// SONIC: Toolchain 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 core::fmt::Display;
25use core::str::FromStr;
26use std::convert::Infallible;
27
28use amplify::confinement::{ConfinedVec, TinyBlob};
29use baid64::Baid64ParseError;
30use chrono::{DateTime, Utc};
31use indexmap::IndexMap;
32use strict_types::{StrictType, StrictVal, TypeName, VariantName};
33use ultrasonic::{AuthToken, ContractId};
34
35use crate::LIB_NAME_SONIC;
36
37pub type StateName = VariantName;
38pub type MethodName = VariantName;
39
40/// Combination of a method name and an optional state name used in API requests.
41#[derive(Clone, Eq, PartialEq, Hash, Debug)]
42#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
43#[strict_type(lib = LIB_NAME_SONIC)]
44#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase", bound = ""))]
45pub struct CallState {
46    pub method: MethodName,
47    pub destructible: Option<StateName>,
48}
49
50impl CallState {
51    pub fn new(method: impl Into<MethodName>) -> Self { Self { method: method.into(), destructible: None } }
52
53    pub fn with(method: impl Into<MethodName>, destructible: impl Into<StateName>) -> Self {
54        Self {
55            method: method.into(),
56            destructible: Some(destructible.into()),
57        }
58    }
59}
60
61/// Call request provides information for constructing [`hypersonic::CallParams`].
62///
63/// Request doesn't specify the used capabilities of the contract (blockchain, if any; type of
64/// single-use seals) since each contract is strictly committed and can be used under one and just
65/// one type of capabilities.
66///
67/// # URI form
68///
69/// Call request can be represented as a URI using `contract:` scheme in the following format:
70///
71/// ```text
72/// contract:CONTRACT-ID/API/METHOD/STATE/AUTH/DATA+STON?expiry=DATETIME&lock=BASE64&endpoints=E1,
73/// E2#CHECK
74/// ```
75///
76/// NB: Parsing and producing URI form requires use of `uri` feature.
77///
78/// ## Path
79///
80/// Instead of Contract ID a string query against a set of contracts can be used; for instance,
81/// describing contract capabilities.
82///
83/// Some path components of the URI may be skipped. In this case URI is parsed in the following way:
84/// - 3-component path, starting with `/`, provides name of the used interface standard,
85///   authentication token and state information;
86/// - 3-component path, not starting with `/`, provides contract ID and auth token, and should use a
87///   default method and name state from the contract default API;
88/// - 4-component path - contract ID and state name are given in addition to the auth token, a
89///   default method used from the contract default API;
90/// - 5-component path - all parameters except API name are given.
91///
92/// ## Query
93///
94/// Supported URI query parameters are:
95/// - `expiry`: ISO-8601 datetime string;
96/// - `lock`: Base64-encoded lock script conditions;
97/// - `endpoints`: comma-separated URLs with the endpoints for uploading a resulting
98///   deeds/consignment stream.
99///
100/// ## Fragment
101///
102/// Optional fragment may be present and should represent a checksum value for the URI string
103/// preceding the fragment.
104#[derive(Clone, Eq, PartialEq, Debug)]
105pub struct CallRequest<T = CallScope, A = AuthToken> {
106    pub scope: T,
107    pub api: Option<TypeName>,
108    pub call: Option<CallState>,
109    pub auth: A,
110    pub data: Option<StrictVal>,
111    pub lock: Option<TinyBlob>,
112    pub expiry: Option<DateTime<Utc>>,
113    pub endpoints: ConfinedVec<Endpoint, 0, 10>,
114    pub unknown_query: IndexMap<String, String>,
115}
116
117impl<Q: Display + FromStr, A> CallRequest<CallScope<Q>, A> {
118    pub fn unwrap_contract_with<E>(
119        self,
120        f: impl FnOnce(Q) -> Result<ContractId, E>,
121    ) -> Result<CallRequest<ContractId, A>, E> {
122        let id = match self.scope {
123            CallScope::ContractId(id) => id,
124            CallScope::ContractQuery(query) => f(query)?,
125        };
126        Ok(CallRequest {
127            scope: id,
128            api: self.api,
129            call: self.call,
130            auth: self.auth,
131            data: self.data,
132            lock: self.lock,
133            expiry: self.expiry,
134            endpoints: self.endpoints,
135            unknown_query: self.unknown_query,
136        })
137    }
138}
139
140#[derive(Clone, Eq, PartialEq, Debug, Display)]
141pub enum CallScope<Q: Display + FromStr = String> {
142    #[display(inner)]
143    ContractId(ContractId),
144
145    #[display("contract:{0}")]
146    ContractQuery(Q),
147}
148
149impl<Q: Display + FromStr> FromStr for CallScope<Q> {
150    type Err = Baid64ParseError;
151
152    fn from_str(s: &str) -> Result<Self, Self::Err> {
153        match ContractId::from_str(s) {
154            Err(err1) => {
155                let s = s.trim_start_matches("contract:");
156                let query = Q::from_str(s).map_err(|_| err1)?;
157                Ok(Self::ContractQuery(query))
158            }
159            Ok(id) => Ok(Self::ContractId(id)),
160        }
161    }
162}
163
164#[derive(Clone, Eq, PartialEq, Debug, Display)]
165#[display(inner)]
166#[non_exhaustive]
167pub enum Endpoint {
168    JsonRpc(String),
169    RestHttp(String),
170    WebSockets(String),
171    Storm(String),
172    UnspecifiedMeans(String),
173}
174
175impl FromStr for Endpoint {
176    type Err = Infallible;
177
178    fn from_str(s: &str) -> Result<Self, Self::Err> {
179        let s = s.to_lowercase();
180        #[allow(clippy::if_same_then_else)] // Some wierd clippy bug
181        if s.starts_with("http://") || s.starts_with("https://") {
182            Ok(Endpoint::RestHttp(s))
183        } else if s.starts_with("http+json-rpc://") || s.starts_with("https+json-rpc://") {
184            Ok(Endpoint::RestHttp(s))
185        } else if s.starts_with("ws://") || s.starts_with("wss://") {
186            Ok(Endpoint::WebSockets(s))
187        } else if s.starts_with("storm://") {
188            Ok(Endpoint::Storm(s))
189        } else {
190            Ok(Endpoint::UnspecifiedMeans(s.to_string()))
191        }
192    }
193}