Skip to main content

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        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        // Extract contract-id from first piece
134        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        // Ensure, that the contract exists
141        if !controller.contract_exists(&contract_id)? {
142            return Ok(reject_404());
143        }
144
145        // Get top-level route
146        let route = match pieces.next() {
147            Some(r) => r,
148            None => {
149                // Get full contract info
150                let full_info = controller.contract_full(&contract_id)?;
151                return Ok(json_response(&full_info));
152            }
153        };
154
155        // Build truncated path
156        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                // TODO: The contract should also parse query parameters !
171                // TODO: URL-Decode !
172                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                // Extract pagination
182                let pagination = Pagination::from_query(query).unwrap_or_default();
183
184                // Get logs
185                let log = controller
186                    .logs(contract_id)
187                    .get_logs_paginated(pagination)?;
188
189                Ok(json_response(&log))
190            }
191            "txs" => {
192                // Extract pagination
193                let pagination = Pagination::from_query(query).unwrap_or_default();
194
195                // Get actions
196                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            // Same as empty path
239            "" => {
240                // TODO: Maybe we also add the package definition to this
241                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        // Extract contract-id from first piece
258        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        // Get top-level route
268        let route = match pieces.next() {
269            Some(r) => r,
270            None => return Ok(reject_404()),
271        };
272
273        // Build truncated path
274        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        // NOTE: The action route only has one additional path parameter
282        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                // Check request header
295                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)?; // NOTE: In this case writer and executor are identical
303                    match rt.http_post_action(&contract_id, trunc, payload.into(), &self.writer)? {
304                        Ok(action) => {
305                            // Perform dry-run of action ( and return action resp in case of error )
306                            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                // Build action response
331                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}