1use borderless::events::CallAction;
2use borderless::hash::Hash256;
3use borderless::http::queries::Pagination;
4use borderless::BorderlessId;
5use borderless::ContractId;
6use borderless_kv_store::{backend::lmdb::Lmdb, Db};
7use http::method::Method;
8use parking_lot::Mutex;
9use std::convert::Infallible;
10use std::future::Future;
11use std::{
12 pin::Pin,
13 sync::Arc,
14 task::{Context, Poll},
15 time::Instant,
16};
17
18pub use super::*;
19use crate::log_shim::*;
20use crate::{db::controller::Controller, rt::contract::Runtime};
21
22pub trait ActionWriter: Clone + Send + Sync {
23 type Error: std::fmt::Display + Send + Sync;
24
25 fn write_action(
26 &self,
27 cid: ContractId,
28 action: CallAction,
29 ) -> impl Future<Output = Result<Hash256, Self::Error>> + Send;
30}
31
32#[derive(Clone)]
36pub struct NoActionWriter;
37
38impl ActionWriter for NoActionWriter {
39 type Error = Infallible;
40
41 async fn write_action(
42 &self,
43 _cid: ContractId,
44 _action: CallAction,
45 ) -> Result<Hash256, Self::Error> {
46 Ok(Hash256::zero())
47 }
48}
49
50#[derive(Serialize)]
51pub struct ActionResp {
52 pub success: bool,
53 pub action: CallAction,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub error: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub tx_hash: Option<Hash256>,
58}
59
60#[derive(Clone)]
62pub struct ContractService<A, S = Lmdb>
63where
64 A: ActionWriter + 'static,
65 S: Db + 'static,
66{
67 rt: Arc<Mutex<Runtime<S>>>,
68 db: S,
69 writer: BorderlessId,
72 action_writer: A,
73}
74
75impl<A, S> ContractService<A, S>
76where
77 A: ActionWriter + 'static,
78 S: Db + 'static,
79{
80 pub fn new(db: S, rt: Runtime<S>, action_writer: A, writer: BorderlessId) -> Self {
81 Self {
82 rt: Arc::new(Mutex::new(rt)),
83 db,
84 writer,
85 action_writer,
86 }
87 }
88
89 pub fn with_shared(
90 db: S,
91 rt: Arc<Mutex<Runtime<S>>>,
92 action_writer: A,
93 writer: BorderlessId,
94 ) -> Self {
95 Self {
96 rt,
97 db,
98 writer,
99 action_writer,
100 }
101 }
102
103 async fn process_rq(&self, req: Request) -> crate::Result<Response> {
104 let start = Instant::now();
105 let path = req.uri().path().to_string();
106 let result = match *req.method() {
107 Method::GET => self.process_get_rq(req),
108 Method::POST => self.process_post_rq(req).await,
109 _ => Ok(method_not_allowed()),
110 };
111 let elapsed = start.elapsed();
112 match &result {
113 Ok(res) => info!(
114 "Request success. path={path}. Time elapsed: {elapsed:?}, status={}",
115 res.status()
116 ),
117 Err(e) => warn!("Request failed. path={path}. Time elapsed: {elapsed:?}, error={e}"),
118 }
119 result
120 }
121
122 fn process_get_rq(&self, req: Request) -> crate::Result<Response> {
123 let path = req.uri().path();
124 let query = req.uri().query();
125
126 if path == "/" {
127 let contracts = self.rt.lock().available_contracts()?;
128 return Ok(json_response(&contracts));
129 }
130
131 let mut pieces = path.split('/').skip(1);
132
133 let contract_id: ContractId = match pieces.next().and_then(|first| first.parse().ok()) {
135 Some(cid) => cid,
136 None => return Ok(reject_404()),
137 };
138 let controller = Controller::new(&self.db);
139
140 if !controller.contract_exists(&contract_id)? {
142 return Ok(reject_404());
143 }
144
145 let route = match pieces.next() {
147 Some(r) => r,
148 None => {
149 let full_info = controller.contract_full(&contract_id)?;
151 return Ok(json_response(&full_info));
152 }
153 };
154
155 let mut trunc = String::new();
157 for piece in pieces {
158 trunc.push('/');
159 trunc.push_str(piece);
160 }
161 if trunc.is_empty() {
162 trunc.push('/');
163 }
164 if let Some(query) = query {
165 trunc.push('?');
166 trunc.push_str(query);
167 }
168 match route {
169 "state" => {
170 let mut rt = self.rt.lock();
173 let (status, payload) = rt.http_get_state(&contract_id, trunc)?;
174 if status == 200 {
175 Ok(json_body(payload))
176 } else {
177 Ok(reject_404())
178 }
179 }
180 "logs" => {
181 let pagination = Pagination::from_query(query).unwrap_or_default();
183
184 let log = controller
186 .logs(contract_id)
187 .get_logs_paginated(pagination)?;
188
189 Ok(json_response(&log))
190 }
191 "txs" => {
192 let pagination = Pagination::from_query(query).unwrap_or_default();
194
195 let paginated = controller
197 .actions(contract_id)
198 .get_tx_action_paginated(pagination)?;
199
200 Ok(json_response(&paginated))
201 }
202 "info" => {
203 let info = controller.contract_info(&contract_id)?;
204 Ok(json_response_nested(info, &trunc))
205 }
206 "desc" => {
207 let desc = controller.contract_desc(&contract_id)?;
208 Ok(json_response_nested(desc, &trunc))
209 }
210 "meta" => {
211 let meta = controller.contract_meta(&contract_id)?;
212 Ok(json_response_nested(meta, &trunc))
213 }
214 "symbols" => {
215 let mut rt = self.rt.lock();
216 let symbols = rt.get_symbols(&contract_id)?;
217 Ok(json_response(&symbols))
218 }
219 "pkg" => match trunc.as_str() {
220 "/" => {
221 let result = controller
222 .contract_pkg_full(&contract_id)?
223 .map(|r| r.into_dto());
224 Ok(json_response(&result))
225 }
226 "/def" => {
227 let result = controller
228 .contract_pkg_def(&contract_id)?
229 .map(|r| r.into_dto());
230 Ok(json_response(&result))
231 }
232 "/source" => {
233 let result = controller.contract_pkg_source(&contract_id)?;
234 Ok(json_response(&result))
235 }
236 _ => Ok(reject_404()),
237 },
238 "" => {
240 let full_info = controller.contract_full(&contract_id)?;
242 Ok(json_response(&full_info))
243 }
244 _ => Ok(reject_404()),
245 }
246 }
247
248 async fn process_post_rq(&self, req: Request) -> crate::Result<Response> {
249 let path = req.uri().path();
250
251 if path == "/" {
252 return Ok(method_not_allowed());
253 }
254
255 let mut pieces = path.split('/').skip(1);
256
257 let cid_str = match pieces.next() {
259 Some(s) => s,
260 None => return Ok(method_not_allowed()),
261 };
262 let contract_id: ContractId = match cid_str.parse() {
263 Ok(cid) => cid,
264 Err(e) => return Ok(bad_request(format!("failed to parse contract-id - {e}"))),
265 };
266
267 let route = match pieces.next() {
269 Some(r) => r,
270 None => return Ok(reject_404()),
271 };
272
273 let mut trunc = String::new();
275 let mut cnt = 0;
276 for piece in pieces {
277 trunc.push('/');
278 trunc.push_str(piece);
279 cnt += 1;
280 }
281 if cnt > 1 {
283 return Ok(reject_404());
284 }
285 if trunc.is_empty() {
286 trunc.push('/');
287 }
288 if let Some(query) = req.uri().query() {
289 trunc.push('?');
290 trunc.push_str(query);
291 }
292 match route {
293 "action" => {
294 let (parts, payload) = req.into_parts();
296 if !check_json_content(&parts) {
297 return Ok(unsupported_media_type());
298 }
299
300 let action = {
301 let mut rt = self.rt.lock();
302 rt.set_executor(self.writer)?; match rt.http_post_action(&contract_id, trunc, payload.into(), &self.writer)? {
304 Ok(action) => {
305 if let Err(e) = rt.perform_dry_run(&contract_id, &action, &self.writer)
307 {
308 let resp = ActionResp {
309 success: false,
310 action,
311 error: Some(e.to_string()),
312 tx_hash: None,
313 };
314 return Ok(json_response(&resp));
315 }
316
317 action
318 }
319 Err((status, err)) => {
320 return Ok(err_response(status.try_into().unwrap(), err))
321 }
322 }
323 };
324 let tx_hash = self
325 .action_writer
326 .write_action(contract_id, action.clone())
327 .await
328 .map_err(|e| crate::Error::msg(format!("failed to write action: {e}")))?;
329
330 let resp = ActionResp {
332 success: true,
333 error: None,
334 action,
335 tx_hash: Some(tx_hash),
336 };
337 Ok(json_response(&resp))
338 }
339 "" => Ok(method_not_allowed()),
340 _ => Ok(reject_404()),
341 }
342 }
343}
344
345impl<A, S> Service<Request> for ContractService<A, S>
346where
347 A: ActionWriter + 'static,
348 S: Db + 'static,
349{
350 type Response = Response;
351 type Error = Infallible;
352 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
353
354 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
355 Poll::Ready(Ok(()))
356 }
357
358 fn call(&mut self, req: Request) -> Self::Future {
359 let this = self.clone();
360 let fut = async move {
361 let result: Response = match this.process_rq(req).await {
362 Ok(r) => r,
363 Err(e) => into_server_error(e),
364 };
365 Ok(result)
366 };
367 Box::pin(fut)
368 }
369}