1use std::str::FromStr as _;
2
3use anyhow::{anyhow, Result};
4use http::header::{HeaderMap, HeaderName, HeaderValue};
5use oasis_types::{Address, RpcError};
6use uuid::Uuid;
7
8#[cfg(not(target_env = "sgx"))]
9use reqwest::Client;
10
11use crate::api::*;
12
13pub trait Gateway {
14 fn deploy(&self, initcode: &[u8]) -> Result<Address, RpcError>;
18
19 fn rpc(&self, address: Address, payload: &[u8]) -> Result<Vec<u8>, RpcError>;
21}
22
23pub struct HttpGateway {
39 url: String,
41
42 headers: HeaderMap,
44
45 client: Client,
47
48 polling_params: PollingParams,
50}
51
52#[derive(Clone, Debug)]
53pub struct HttpGatewayBuilder {
54 url: String,
55 api_key: Option<String>,
56 headers: HeaderMap,
57 polling_params: PollingParams,
58}
59
60impl HttpGatewayBuilder {
61 pub fn new(url: impl AsRef<str>) -> Self {
62 Self {
63 url: url.as_ref().to_string(),
64 ..Default::default()
65 }
66 }
67
68 pub fn api_key(mut self, api_key: impl AsRef<str>) -> Self {
70 self.api_key = Some(api_key.as_ref().to_string());
71 self
72 }
73
74 pub fn header(mut self, name: impl AsRef<[u8]>, value: impl AsRef<[u8]>) -> Result<Self> {
76 self.headers.insert(
77 HeaderName::from_bytes(name.as_ref())?,
78 HeaderValue::from_bytes(value.as_ref())?,
79 );
80 Ok(self)
81 }
82
83 pub fn headers(mut self, headers: HeaderMap) -> Self {
85 self.headers = headers;
86 self
87 }
88
89 pub fn polling_params(mut self, params: PollingParams) -> Self {
91 self.polling_params = params;
92 self
93 }
94
95 pub fn build(self) -> HttpGateway {
98 let session_key = Uuid::new_v4().to_string();
99
100 let mut headers = self.headers;
101 headers.insert("X-OASIS-INSECURE-AUTH", HeaderValue::from_static("1"));
102 if let Some(api_key) = self.api_key {
103 headers.insert(
104 "X-OASIS-LOGIN-TOKEN",
105 HeaderValue::from_str(&api_key).unwrap(),
106 );
107 }
108 headers.insert(
109 "X-OASIS-SESSION-KEY",
110 HeaderValue::from_str(&session_key).unwrap(),
111 );
112
113 HttpGateway::new(self.url, headers, self.polling_params)
114 }
115}
116
117impl Default for HttpGatewayBuilder {
118 fn default() -> Self {
119 Self {
120 url: "https://gateway.devnet.oasiscloud.io".to_string(),
121 api_key: None,
122 headers: HeaderMap::new(),
123 polling_params: PollingParams::default(),
124 }
125 }
126}
127
128#[derive(Clone, Copy, Debug)]
129pub struct PollingParams {
130 pub sleep_duration: u64,
132
133 pub max_attempts: u32,
135}
136
137impl Default for PollingParams {
138 fn default() -> Self {
139 Self {
140 sleep_duration: 500,
141 max_attempts: 20,
142 }
143 }
144}
145
146impl HttpGateway {
147 pub fn new(url: String, headers: HeaderMap, polling_params: PollingParams) -> Self {
149 Self {
150 url,
151 headers,
152 client: Client::new(),
153 polling_params,
154 }
155 }
156
157 fn post_and_poll(&self, api: DeveloperGatewayApi, body: GatewayRequest) -> Result<Event> {
159 let response: AsyncResponse = self.request(api.method, api.url, body)?;
160
161 match self.poll_for_response(response.id)? {
162 Event::Error { description, .. } => Err(anyhow!("{}", description)),
163 e => Ok(e),
164 }
165 }
166
167 fn poll_for_response(&self, request_id: u64) -> Result<Event> {
170 let PollingParams {
171 sleep_duration,
172 max_attempts,
173 } = self.polling_params;
174
175 let poll_request = GatewayRequest::Poll {
176 offset: request_id,
177 count: 1, discard_previous: true,
179 };
180
181 for attempt in 0..max_attempts {
182 let events: PollEventResponse = self.request(
183 SERVICE_POLL_API.method,
184 &SERVICE_POLL_API.url,
185 &poll_request,
186 )?;
187
188 let event = events.events.first();
190 if let Some(e) = event {
191 return Ok(e.clone());
192 }
193
194 info!(
195 "polling... (request id: {}, attempt: {})",
196 request_id, attempt
197 );
198
199 #[cfg(not(target_env = "sgx"))]
200 std::thread::sleep(std::time::Duration::from_millis(sleep_duration));
201
202 #[cfg(target_env = "sgx")]
204 {
205 let start = std::time::Instant::now();
206 let duration = std::time::Duration::from_millis(sleep_duration);
207 while start.elapsed() < duration {
208 std::thread::yield_now();
209 }
210 }
211 }
212 Err(anyhow!("Exceeded max polling attempts"))
213 }
214
215 fn request<P: serde::Serialize, Q: serde::de::DeserializeOwned>(
218 &self,
219 method: RequestMethod,
220 url: &str,
221 payload: P,
222 ) -> Result<Q> {
223 let url = if self.url.ends_with('/') {
224 format!("{}{}", self.url, url)
225 } else {
226 format!("{}/{}", self.url, url)
227 };
228 let builder = match method {
229 RequestMethod::GET => self.client.get(&url),
230 RequestMethod::POST => self.client.post(&url),
231 };
232
233 let mut res = builder
234 .headers(self.headers.clone())
235 .json(&payload)
236 .send()?;
237 if res.status().is_success() {
238 Ok(res.json()?)
239 } else {
240 Err(anyhow!("gateway returned error: {}", res.status()))
241 }
242 }
243}
244
245impl Gateway for HttpGateway {
246 fn deploy(&self, initcode: &[u8]) -> std::result::Result<Address, RpcError> {
247 let initcode_hex = hex::encode(initcode);
248 info!("deploying service `{}`", &initcode_hex[..32]);
249
250 let body = GatewayRequest::Deploy {
251 data: format!("0x{}", initcode_hex),
252 };
253
254 self.post_and_poll(SERVICE_DEPLOY_API, body)
255 .and_then(|event| {
256 match event {
257 Event::DeployService { address, .. } => {
258 Ok(Address::from_str(&address[2..] )?)
259 }
260 e => Err(anyhow!("expecting `DeployService` event. got {:?}", e)),
261 }
262 })
263 .map_err(RpcError::Gateway)
264 }
265
266 fn rpc(&self, address: Address, payload: &[u8]) -> std::result::Result<Vec<u8>, RpcError> {
267 info!("making RPC to {}", address);
268
269 let body = GatewayRequest::Execute {
270 address: address.to_string(),
271 data: format!("0x{}", hex::encode(payload)),
272 };
273
274 self.post_and_poll(SERVICE_EXECUTE_API, body)
275 .and_then(|event| match event {
276 Event::ExecuteService { output, .. } => Ok(hex::decode(&output[2..])?),
277 e => Err(anyhow!("expecting `ExecuteService` event. got {:?}", e)),
278 })
279 .map_err(RpcError::Gateway)
280 }
281}
282
283#[cfg(all(test, not(target_env = "sgx")))]
284mod tests {
285 use super::*;
286
287 use mockito::mock;
288 use serde_json::json;
289
290 const API_KEY: &str = "AAACL7PMQhh3/rxLr9KJpsAJhz5zBlpAB73uwgAt/6BQ4+Bw";
292 const PAYLOAD_HEX: &str = "0x144c6bda090723de712e52b92b4c758d78348ddce9aa80ca8ef51125bfb308";
293 const FIXTURE_ADDR: &str = "0xb8b3666d8fea887d97ab54f571b8e5020c5c8b58";
294
295 #[test]
296 fn test_deploy() {
297 let fixture_addr = Address::from_str(&FIXTURE_ADDR[2..]).unwrap();
298 let poll_id = 42;
299
300 let _m_deploy = mock("POST", "/v0/api/service/deploy")
301 .match_header("content-type", "application/json")
302 .match_header("x-oasis-login-token", API_KEY)
303 .match_body(mockito::Matcher::Json(json!({ "data": PAYLOAD_HEX })))
304 .with_header("content-type", "text/json")
305 .with_body(json!({ "id": poll_id }).to_string())
306 .create();
307
308 let _m_poll = mock("POST", "/v0/api/service/poll")
309 .match_header("content-type", "application/json")
310 .match_header("x-oasis-login-token", API_KEY)
311 .match_body(mockito::Matcher::Json(json!({
312 "offset": poll_id,
313 "count": 1,
314 "discard_previous": true,
315 })))
316 .with_header("content-type", "text/json")
317 .with_body(
318 json!({
319 "offset": poll_id,
320 "events": [
321 { "id": poll_id, "address": FIXTURE_ADDR }
322 ]
323 })
324 .to_string(),
325 )
326 .create();
327
328 let gateway = HttpGatewayBuilder::new(mockito::server_url())
329 .api_key(API_KEY)
330 .build();
331 let addr = gateway
332 .deploy(&hex::decode(&PAYLOAD_HEX[2..]).unwrap())
333 .unwrap();
334
335 assert_eq!(addr, fixture_addr);
336 }
337
338 #[test]
339 fn test_rpc() {
340 let fixture_addr = Address::from_str(&FIXTURE_ADDR[2..]).unwrap();
341 let poll_id = 42;
342 let expected_output = "hello, client!";
343 let hex_output = "0x".to_string() + &hex::encode(expected_output.as_bytes());
344
345 let _m_execute = mock("POST", "/v0/api/service/execute")
346 .match_header("content-type", "application/json")
347 .match_header("x-oasis-login-token", mockito::Matcher::Missing)
348 .match_body(mockito::Matcher::Json(json!({
349 "address": FIXTURE_ADDR,
350 "data": PAYLOAD_HEX,
351 })))
352 .with_header("content-type", "text/json")
353 .with_body(json!({ "id": poll_id }).to_string())
354 .create();
355
356 let _m_poll = mock("POST", "/v0/api/service/poll")
357 .match_header("content-type", "application/json")
358 .match_header("x-oasis-login-token", mockito::Matcher::Missing)
359 .match_body(mockito::Matcher::Json(json!({
360 "offset": poll_id,
361 "count": 1,
362 "discard_previous": true,
363 })))
364 .with_header("content-type", "text/json")
365 .with_body(
366 json!({
367 "offset": poll_id,
368 "events": [
369 { "id": poll_id, "address": FIXTURE_ADDR, "output": hex_output }
370 ]
371 })
372 .to_string(),
373 )
374 .create();
375
376 let gateway = HttpGatewayBuilder::new(mockito::server_url()).build();
377 let output = gateway
378 .rpc(fixture_addr, &hex::decode(&PAYLOAD_HEX[2..]).unwrap())
379 .unwrap();
380
381 assert_eq!(output, expected_output.as_bytes());
382 }
383
384 #[test]
385 fn test_error() {
386 let fixture_addr = Address::from_str(&FIXTURE_ADDR[2..]).unwrap();
387 let poll_id = 42;
388 let err_code = 99;
389 let err_msg = "error!";
390
391 let _m_execute = mock("POST", "/v0/api/service/execute")
392 .match_header("content-type", "application/json")
393 .match_body(mockito::Matcher::Json(json!({
394 "address": FIXTURE_ADDR,
395 "data": PAYLOAD_HEX,
396 })))
397 .with_header("content-type", "text/json")
398 .with_body(json!({ "id": poll_id }).to_string())
399 .create();
400
401 let _m_poll = mock("POST", "/v0/api/service/poll")
402 .match_header("content-type", "application/json")
403 .match_body(mockito::Matcher::Json(json!({
404 "offset": poll_id,
405 "count": 1,
406 "discard_previous": true,
407 })))
408 .with_header("content-type", "text/json")
409 .with_body(
410 json!({
411 "offset": poll_id,
412 "events": [
413 { "id": poll_id, "error_code": err_code, "description": err_msg }
414 ]
415 })
416 .to_string(),
417 )
418 .create();
419
420 let gateway = HttpGatewayBuilder::new(mockito::server_url())
421 .api_key(API_KEY)
422 .build();
423 let err_output = gateway
424 .rpc(fixture_addr, &hex::decode(&PAYLOAD_HEX[2..]).unwrap())
425 .unwrap_err();
426
427 assert!(err_output.to_string().contains(err_msg))
428 }
429}