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 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 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 if !controller.contract_exists(&contract_id)? {
136 return Ok(reject_404());
137 }
138
139 let route = match pieces.next() {
141 Some(r) => r,
142 None => {
143 let full_info = controller.contract_full(&contract_id)?;
145 return Ok(json_response(&full_info));
146 }
147 };
148
149 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 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 let pagination = Pagination::from_query(query).unwrap_or_default();
176
177 let log = controller
179 .logs(contract_id)
180 .get_logs_paginated(pagination)?;
181
182 Ok(json_response(&log))
183 }
184 "txs" => {
185 let pagination = Pagination::from_query(query).unwrap_or_default();
187
188 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 "" => {
233 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 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 let route = match pieces.next() {
262 Some(r) => r,
263 None => return Ok(reject_404()),
264 };
265
266 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 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 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)?; match rt.http_post_action(&contract_id, trunc, payload.into(), &self.writer)? {
297 Ok(action) => {
298 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 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}