1use core::convert::Infallible;
25use core::fmt::{self, Display, Formatter};
26use core::str::FromStr;
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, Consensus, ContractId};
34
35use crate::LIB_NAME_SONIC;
36
37pub type StateName = VariantName;
38pub type MethodName = VariantName;
39
40#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, 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 owned: Option<StateName>,
48}
49
50impl CallState {
51 pub fn new(method: impl Into<MethodName>) -> Self { Self { method: method.into(), owned: None } }
52
53 pub fn with(method: impl Into<MethodName>, owned: impl Into<StateName>) -> Self {
54 Self { method: method.into(), owned: Some(owned.into()) }
55 }
56}
57
58#[derive(Clone, Eq, PartialEq, Debug)]
102#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
103#[cfg_attr(
104 feature = "serde",
105 serde(bound(
106 serialize = "T: serde::Serialize, A: serde::Serialize",
107 deserialize = "T: serde::Deserialize<'de>, A: serde::Deserialize<'de>"
108 ))
109)]
110pub struct CallRequest<T = CallScope, A = AuthToken> {
111 pub scope: T,
112 pub layer1: Layer1,
113 pub api: Option<TypeName>,
114 pub call: Option<CallState>,
115 pub auth: A,
116 pub data: Option<StrictVal>,
117 pub lock: Option<TinyBlob>,
118 pub expiry: Option<DateTime<Utc>>,
119 pub endpoints: ConfinedVec<Endpoint, 0, 10>,
120 pub unknown_query: IndexMap<String, String>,
121}
122
123impl<Q: Display + FromStr, A> CallRequest<CallScope<Q>, A> {
124 pub fn unwrap_contract_with<E>(
125 self,
126 f: impl FnOnce(Q) -> Result<ContractId, E>,
127 ) -> Result<CallRequest<ContractId, A>, E> {
128 let id = match self.scope {
129 CallScope::ContractId(id) => id,
130 CallScope::ContractQuery(query) => f(query)?,
131 };
132 Ok(CallRequest {
133 scope: id,
134 layer1: self.layer1,
135 api: self.api,
136 call: self.call,
137 auth: self.auth,
138 data: self.data,
139 lock: self.lock,
140 expiry: self.expiry,
141 endpoints: self.endpoints,
142 unknown_query: self.unknown_query,
143 })
144 }
145}
146
147#[derive(Copy, Clone, Eq, PartialEq, Debug)]
148#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
149pub struct Layer1 {
150 pub consensus: Consensus,
151 pub testnet: bool,
152}
153
154impl Layer1 {
155 pub fn new(consensus: Consensus, testnet: bool) -> Self { Self { consensus, testnet } }
156}
157
158impl Display for Layer1 {
159 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
160 let s = match (self.consensus, self.testnet) {
161 (Consensus::None, false) => "~",
162 (Consensus::None, true) => "test",
163 (Consensus::Bitcoin, false) => "bc",
164 (Consensus::Bitcoin, true) => "tb",
165 (Consensus::Liquid, false) => "lq",
166 (Consensus::Liquid, true) => "tl",
167 (Consensus::Prime, false) => "pr",
168 (Consensus::Prime, true) => "tp",
169 };
170 f.write_str(s)
171 }
172}
173
174impl FromStr for Layer1 {
175 type Err = ParseLayer1Error;
176
177 fn from_str(s: &str) -> Result<Self, Self::Err> {
178 let s = s.to_lowercase();
179 Ok(match s.as_str() {
180 "~" => Layer1::new(Consensus::None, false),
181 "test" => Layer1::new(Consensus::None, true),
182 "bc" => Layer1::new(Consensus::Bitcoin, false),
183 "tb" => Layer1::new(Consensus::Bitcoin, true),
184 "lq" => Layer1::new(Consensus::Liquid, false),
185 "tl" => Layer1::new(Consensus::Liquid, true),
186 "pr" => Layer1::new(Consensus::Prime, false),
187 "tp" => Layer1::new(Consensus::Prime, true),
188 _ => return Err(ParseLayer1Error),
189 })
190 }
191}
192
193#[derive(Copy, Clone, PartialEq, Eq, Debug, Display, Error)]
194#[display("invalid layer 1 name")]
195pub struct ParseLayer1Error;
196
197#[derive(Clone, Eq, PartialEq, Debug, Display)]
198#[cfg_attr(
199 feature = "serde",
200 derive(Serialize, Deserialize),
201 serde(
202 try_from = "String",
203 into = "String",
204 bound(serialize = "Q: Display + FromStr + Clone", deserialize = "Q: Display + FromStr"),
205 )
206)]
207pub enum CallScope<Q: Display + FromStr = String> {
208 #[display(inner)]
209 ContractId(ContractId),
210
211 #[display("contract:{0}")]
212 ContractQuery(Q),
213}
214
215impl<Q: Display + FromStr> FromStr for CallScope<Q> {
216 type Err = Baid64ParseError;
217
218 fn from_str(s: &str) -> Result<Self, Self::Err> {
219 match ContractId::from_str(s) {
220 Ok(id) => Ok(Self::ContractId(id)),
221 Err(err_contract_id) => match s.strip_prefix("contract:") {
222 Some(query_str) => Q::from_str(query_str)
223 .map(Self::ContractQuery)
224 .map_err(|_| err_contract_id),
225 None => Err(err_contract_id),
226 },
227 }
228 }
229}
230
231impl<Q: Display + FromStr> TryFrom<String> for CallScope<Q> {
232 type Error = Baid64ParseError;
233
234 fn try_from(value: String) -> Result<Self, Self::Error> { Self::from_str(&value) }
235}
236
237impl<Q: Display + FromStr + Clone> From<CallScope<Q>> for String {
238 fn from(value: CallScope<Q>) -> Self { value.to_string() }
239}
240
241#[derive(Clone, Eq, PartialEq, Debug, Display)]
242#[display(inner)]
243#[non_exhaustive]
244#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
245#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
246pub enum Endpoint {
247 JsonRpc(String),
248 RestHttp(String),
249 WebSockets(String),
250 Storm(String),
251 UnspecifiedMeans(String),
252}
253
254impl FromStr for Endpoint {
255 type Err = Infallible;
256
257 fn from_str(s: &str) -> Result<Self, Self::Err> {
258 let s = s.to_lowercase();
259 #[allow(clippy::if_same_then_else)] if s.starts_with("http://") || s.starts_with("https://") {
261 Ok(Endpoint::RestHttp(s))
262 } else if s.starts_with("http+json-rpc://") || s.starts_with("https+json-rpc://") {
263 Ok(Endpoint::JsonRpc(s))
264 } else if s.starts_with("ws://") || s.starts_with("wss://") {
265 Ok(Endpoint::WebSockets(s))
266 } else if s.starts_with("storm://") {
267 Ok(Endpoint::Storm(s))
268 } else {
269 Ok(Endpoint::UnspecifiedMeans(s.to_string()))
270 }
271 }
272}
273
274impl TryFrom<String> for Endpoint {
275 type Error = Infallible;
276
277 fn try_from(value: String) -> Result<Self, Self::Error> { Self::from_str(&value) }
278}
279
280impl From<Endpoint> for String {
281 fn from(value: Endpoint) -> Self { value.to_string() }
282}