borderless_runtime/http/
contract.rs

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/// A dummy implementation of an action-writer, that does nothing with the action.
33///
34/// Useful for testing.
35#[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/// Simple service around the runtime
61#[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    // TODO: This is not optimal. The runtime is not tied to a tx-writer,
70    // and for our multi-tenant contract-node we require this to be flexible.
71    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        info!("Finished executing request. path={path}. Time elapsed: {elapsed:?}");
113        result
114    }
115
116    fn process_get_rq(&self, req: Request) -> crate::Result<Response> {
117        let path = req.uri().path();
118        let query = req.uri().query();
119
120        if path == "/" {
121            let contracts = self.rt.lock().available_contracts()?;
122            return Ok(json_response(&contracts));
123        }
124
125        let mut pieces = path.split('/').skip(1);
126
127        // Extract contract-id from first piece
128        let contract_id: ContractId = match pieces.next().and_then(|first| first.parse().ok()) {
129            Some(cid) => cid,
130            None => return Ok(reject_404()),
131        };
132        let controller = Controller::new(&self.db);
133
134        // Ensure, that the contract exists
135        if !controller.contract_exists(&contract_id)? {
136            return Ok(reject_404());
137        }
138
139        // Get top-level route
140        let route = match pieces.next() {
141            Some(r) => r,
142            None => {
143                // Get full contract info
144                let full_info = controller.contract_full(&contract_id)?;
145                return Ok(json_response(&full_info));
146            }
147        };
148
149        // Build truncated path
150        let mut trunc = String::new();
151        for piece in pieces {
152            trunc.push('/');
153            trunc.push_str(piece);
154        }
155        if trunc.is_empty() {
156            trunc.push('/');
157        }
158        if let Some(query) = query {
159            trunc.push('?');
160            trunc.push_str(query);
161        }
162        match route {
163            "state" => {
164                // TODO: The contract should also parse query parameters !
165                let mut rt = self.rt.lock();
166                let (status, payload) = rt.http_get_state(&contract_id, trunc)?;
167                if status == 200 {
168                    Ok(json_body(payload))
169                } else {
170                    Ok(reject_404())
171                }
172            }
173            "logs" => {
174                // Extract pagination
175                let pagination = Pagination::from_query(query).unwrap_or_default();
176
177                // Get logs
178                let log = controller
179                    .logs(contract_id)
180                    .get_logs_paginated(pagination)?;
181
182                Ok(json_response(&log))
183            }
184            "txs" => {
185                // Extract pagination
186                let pagination = Pagination::from_query(query).unwrap_or_default();
187
188                // Get actions
189                let paginated = controller
190                    .actions(contract_id)
191                    .get_tx_action_paginated(pagination)?;
192
193                Ok(json_response(&paginated))
194            }
195            "info" => {
196                let info = controller.contract_info(&contract_id)?;
197                Ok(json_response_nested(info, &trunc))
198            }
199            "desc" => {
200                let desc = controller.contract_desc(&contract_id)?;
201                Ok(json_response_nested(desc, &trunc))
202            }
203            "meta" => {
204                let meta = controller.contract_meta(&contract_id)?;
205                Ok(json_response_nested(meta, &trunc))
206            }
207            "symbols" => {
208                let mut rt = self.rt.lock();
209                let symbols = rt.get_symbols(&contract_id)?;
210                Ok(json_response(&symbols))
211            }
212            "pkg" => match trunc.as_str() {
213                "/" => {
214                    let result = controller
215                        .contract_pkg_full(&contract_id)?
216                        .map(|r| r.into_dto());
217                    Ok(json_response(&result))
218                }
219                "/def" => {
220                    let result = controller
221                        .contract_pkg_def(&contract_id)?
222                        .map(|r| r.into_dto());
223                    Ok(json_response(&result))
224                }
225                "/source" => {
226                    let result = controller.contract_pkg_source(&contract_id)?;
227                    Ok(json_response(&result))
228                }
229                _ => Ok(reject_404()),
230            },
231            // Same as empty path
232            "" => {
233                // TODO: Maybe we also add the package definition to this
234                let full_info = controller.contract_full(&contract_id)?;
235                Ok(json_response(&full_info))
236            }
237            _ => Ok(reject_404()),
238        }
239    }
240
241    async fn process_post_rq(&self, req: Request) -> crate::Result<Response> {
242        let path = req.uri().path();
243
244        if path == "/" {
245            return Ok(method_not_allowed());
246        }
247
248        let mut pieces = path.split('/').skip(1);
249
250        // Extract contract-id from first piece
251        let cid_str = match pieces.next() {
252            Some(s) => s,
253            None => return Ok(method_not_allowed()),
254        };
255        let contract_id: ContractId = match cid_str.parse() {
256            Ok(cid) => cid,
257            Err(e) => return Ok(bad_request(format!("failed to parse contract-id - {e}"))),
258        };
259
260        // Get top-level route
261        let route = match pieces.next() {
262            Some(r) => r,
263            None => return Ok(reject_404()),
264        };
265
266        // Build truncated path
267        let mut trunc = String::new();
268        let mut cnt = 0;
269        for piece in pieces {
270            trunc.push('/');
271            trunc.push_str(piece);
272            cnt += 1;
273        }
274        // NOTE: The action route only has one additional path parameter
275        if cnt > 1 {
276            return Ok(reject_404());
277        }
278        if trunc.is_empty() {
279            trunc.push('/');
280        }
281        if let Some(query) = req.uri().query() {
282            trunc.push('?');
283            trunc.push_str(query);
284        }
285        match route {
286            "action" => {
287                // Check request header
288                let (parts, payload) = req.into_parts();
289                if !check_json_content(&parts) {
290                    return Ok(unsupported_media_type());
291                }
292
293                let action = {
294                    let mut rt = self.rt.lock();
295                    rt.set_executor(self.writer)?; // NOTE: In this case writer and executor are identical
296                    match rt.http_post_action(&contract_id, trunc, payload.into(), &self.writer)? {
297                        Ok(action) => {
298                            // Perform dry-run of action ( and return action resp in case of error )
299                            if let Err(e) = rt.perform_dry_run(&contract_id, &action, &self.writer)
300                            {
301                                let resp = ActionResp {
302                                    success: false,
303                                    action,
304                                    error: Some(e.to_string()),
305                                    tx_hash: None,
306                                };
307                                return Ok(json_response(&resp));
308                            }
309
310                            action
311                        }
312                        Err((status, err)) => {
313                            return Ok(err_response(status.try_into().unwrap(), err))
314                        }
315                    }
316                };
317                let tx_hash = self
318                    .action_writer
319                    .write_action(contract_id, action.clone())
320                    .await
321                    .map_err(|e| crate::Error::msg(format!("failed to write action: {e}")))?;
322
323                // Build action response
324                let resp = ActionResp {
325                    success: true,
326                    error: None,
327                    action,
328                    tx_hash: Some(tx_hash),
329                };
330                Ok(json_response(&resp))
331            }
332            "" => Ok(method_not_allowed()),
333            _ => Ok(reject_404()),
334        }
335    }
336}
337
338impl<A, S> Service<Request> for ContractService<A, S>
339where
340    A: ActionWriter + 'static,
341    S: Db + 'static,
342{
343    type Response = Response;
344    type Error = Infallible;
345    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
346
347    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
348        Poll::Ready(Ok(()))
349    }
350
351    fn call(&mut self, req: Request) -> Self::Future {
352        let this = self.clone();
353        let fut = async move {
354            let result: Response = match this.process_rq(req).await {
355                Ok(r) => r,
356                Err(e) => into_server_error(e),
357            };
358            Ok(result)
359        };
360        Box::pin(fut)
361    }
362}