Skip to content

Commit

Permalink
RCM: Radix Connect Relay (#107)
Browse files Browse the repository at this point in the history
* wip

* wip

* response encoding

* wip

* wip

* wip

* wip

* wip

* rename

* wip

* wip

* wip

* wip

* fix one

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* [ABW-3207] app link parsing (#102)

* Radix Connect app link parsing

* pr review changes

* add missing model files

* add parser test for invalid url

* update invalid url test input data

* update session_id

* reafactor link parse method

* change interaction_id.rs

* add addition dapp_request tests

* add util function for parsing url

* add test for invalid url parsing

* use defined constructors for request objects

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* fix

* wip

* wip

---------

Co-authored-by: jakub-rdx <[email protected]>
  • Loading branch information
GhenadieVP and jakub-rdx authored Apr 29, 2024
1 parent 6b3a807 commit e38020d
Show file tree
Hide file tree
Showing 41 changed files with 1,637 additions and 310 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ extension URLRequest {
switch sargon.method {
case .post:
request.httpMethod = "POST" // FIXME: embed in sargon
case .get:
request.httpMethod = "GET"
}

request.httpBody = sargon.body
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import com.radixdlt.sargon.NetworkMethod

fun NetworkMethod.toHttpMethod(): String = when (this) {
NetworkMethod.POST -> "POST"
NetworkMethod.GET -> "GET"
}
8 changes: 4 additions & 4 deletions src/core/assert_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ fn base_assert_equality_after_json_roundtrip<T>(
let serialized = serde_json::to_value(model).unwrap();
let deserialized: T = serde_json::from_value(json.clone()).unwrap();
if expect_eq {
assert_eq!(&deserialized, model, "Expected `model: T` and `T` deserialized from `json_string`, to be equal, but they were not.");
pretty_assertions::assert_eq!(&deserialized, model, "Expected `model: T` and `T` deserialized from `json_string`, to be equal, but they were not.");
assert_json_include!(actual: serialized, expected: json);
} else {
assert_ne!(model, &deserialized);
assert_ne!(&deserialized, model, "Expected difference between `model: T` and `T` deserialized from `json_string`, but they were unexpectedly equal.");
assert_ne!(serialized, json, "Expected difference between `json` (string) and json serialized from `model`, but they were unexpectedly equal.");
pretty_assertions::assert_ne!(model, &deserialized);
pretty_assertions::assert_ne!(&deserialized, model, "Expected difference between `model: T` and `T` deserialized from `json_string`, but they were unexpectedly equal.");
pretty_assertions::assert_ne!(serialized, json, "Expected difference between `json` (string) and json serialized from `model`, but they were unexpectedly equal.");
}
}

Expand Down
146 changes: 3 additions & 143 deletions src/gateway_api/client/gateway_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,14 @@ use crate::prelude::*;

/// An HTTP client for consuming the Radix ⛩️ Gateway API ([docs]).
///
/// A `GatewayClient` needs a "network antenna" to be able to execute the
/// network requests - which is a trait that clients implement on the FFI side
/// (iOS/Android) an "installs" when initiating an instance of the `GatewayClient`.
///
/// The implementing FFI clients can then consume the Radix Gateway API to e.g.
/// fetch the XRD balance of an account address or submit a signed transaction.
///
/// [docs]: https://radix-babylon-gateway-api.redoc.ly/
#[derive(uniffi::Object)]
pub struct GatewayClient {
/// An object implementing the `NetworkAntenna` traits, which iOS/Android
/// clients pass into the constructor of this GatewayClient, so that it can
/// execute network requests.
pub network_antenna: Arc<dyn NetworkAntenna>,
/// The HTTP client that actually executes the network requests.
pub http_client: HttpClient,

/// The gateway this GatewayClient talks to, which is a (URL, NetworkID) tuple
/// essentially.
Expand All @@ -32,7 +26,7 @@ impl GatewayClient {
gateway: Gateway,
) -> Self {
Self {
network_antenna,
http_client: HttpClient { network_antenna },
gateway,
}
}
Expand All @@ -52,55 +46,6 @@ impl GatewayClient {
}
}

/// A mocked network antenna, useful for testing.
#[derive(Debug)]
struct MockAntenna {
hard_coded_status: u16,
hard_coded_body: BagOfBytes,
spy: fn(NetworkRequest) -> (),
}

#[allow(unused)]
impl MockAntenna {
fn with_spy(
status: u16,
body: impl Into<BagOfBytes>,
spy: fn(NetworkRequest) -> (),
) -> Self {
Self {
hard_coded_status: status,
hard_coded_body: body.into(),
spy,
}
}

fn new(status: u16, body: impl Into<BagOfBytes>) -> Self {
Self::with_spy(status, body, |_| {})
}

fn with_response<T>(response: T) -> Self
where
T: Serialize,
{
let body = serde_json::to_vec(&response).unwrap();
Self::new(200, body)
}
}

#[async_trait::async_trait]
impl NetworkAntenna for MockAntenna {
async fn execute_network_request(
&self,
request: NetworkRequest,
) -> Result<NetworkResponse> {
(self.spy)(request);
Ok(NetworkResponse {
status_code: self.hard_coded_status,
body: self.hard_coded_body.clone(),
})
}
}

#[cfg(test)]
impl From<()> for BagOfBytes {
fn from(_value: ()) -> Self {
Expand All @@ -120,91 +65,6 @@ mod tests {
#[allow(clippy::upper_case_acronyms)]
type SUT = GatewayClient;

#[actix_rt::test]
async fn execute_network_request_invalid_url() {
let mock_antenna = MockAntenna::new(200, ());
let base = "http://example.com";
let sut = SUT::with_gateway(
Arc::new(mock_antenna),
Gateway::declare(base, NetworkID::Stokenet),
);
let bad_path = "https://exa%23mple.org";
let bad_value = format!("{}/{}", base, bad_path);
let req = sut.post_empty::<i8, i8, _>(bad_path, res_id);
let result = timeout(MAX, req).await.unwrap();
assert_eq!(
result,
Err(CommonError::NetworkRequestInvalidUrl { bad_value })
)
}

#[actix_rt::test]
async fn execute_network_request_bad_status_code() {
let mock_antenna = MockAntenna::new(
404, // bad code
(),
);
let sut = SUT::new(Arc::new(mock_antenna), NetworkID::Stokenet);
let req = sut.current_epoch();
let result = timeout(MAX, req).await.unwrap();
assert_eq!(result, Err(CommonError::NetworkResponseBadCode))
}

#[actix_rt::test]
async fn execute_network_request_empty_body() {
let mock_antenna = MockAntenna::new(
200,
(), // empty body
);
let sut = SUT::new(Arc::new(mock_antenna), NetworkID::Stokenet);
let req = sut.current_epoch();
let result = timeout(MAX, req).await.unwrap();
assert_eq!(result, Err(CommonError::NetworkResponseEmptyBody))
}

#[actix_rt::test]
async fn execute_network_request_invalid_json() {
let mock_antenna = MockAntenna::new(
200,
BagOfBytes::sample_aced(), // wrong JSON
);
let sut = SUT::new(Arc::new(mock_antenna), NetworkID::Stokenet);
let req = sut.current_epoch();
let result = timeout(MAX, req).await.unwrap();
assert_eq!(
result,
Err(CommonError::NetworkResponseJSONDeserialize {
into_type: "TransactionConstructionResponse".to_owned()
})
)
}

#[actix_rt::test]
async fn spy_headers() {
let mock_antenna = MockAntenna::with_spy(200, (), |request| {
assert_eq!(
request
.headers
.keys()
.map(|v| v.to_string())
.collect::<BTreeSet<String>>(),
[
"RDX-Client-Version",
"RDX-Client-Name",
"accept",
"content-Type",
"user-agent"
]
.into_iter()
.map(|s| s.to_owned())
.collect::<BTreeSet<String>>()
)
});
let sut = SUT::new(Arc::new(mock_antenna), NetworkID::Stokenet);
let req = sut.current_epoch();
drop(timeout(MAX, req).await.unwrap());
}

#[actix_rt::test]
async fn test_submit_notarized_transaction_mock_duplicate() {
let mock_antenna =
Expand Down
93 changes: 20 additions & 73 deletions src/gateway_api/client/gateway_client_dispatch_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,23 @@ impl GatewayClient {
U: for<'a> Deserialize<'a>,
F: Fn(U) -> Result<V, CommonError>,
{
self.dispatch_network_request(path, NetworkMethod::Post, request, map)
// Append relative path to base url
let path = path.as_ref();
let url = self.gateway.url.join(path).map_err(|e| {
let bad_value = format!("{}{}", self.gateway.url, path);
error!(
"Failed to parse URL, error: {:?}, from string: {}",
e, &bad_value
);
CommonError::NetworkRequestInvalidUrl { bad_value }
})?;

let request = NetworkRequest::new_post(url)
.with_gateway_api_headers()
.with_serializing_body(request)?;

self.http_client
.execute_request_with_map(request, map)
.await
}

Expand All @@ -42,60 +58,8 @@ pub(crate) const fn res_id<T>(x: T) -> Result<T, CommonError> {
std::convert::identity::<Result<T, CommonError>>(Ok(x))
}

///
/// Private
///
impl GatewayClient {
fn model_from_response<U>(
&self,
response: NetworkResponse,
) -> Result<U, CommonError>
where
U: for<'a> Deserialize<'a>,
{
if let 200..=299 = response.status_code {
// all good
} else {
return Err(CommonError::NetworkResponseBadCode);
}

if response.body.is_empty() {
return Err(CommonError::NetworkResponseEmptyBody);
}

serde_json::from_slice::<U>(&response.body).map_err(|_| {
CommonError::NetworkResponseJSONDeserialize {
into_type: type_name::<U>(),
}
})
}

async fn dispatch_network_request<T, U, V, F>(
&self,
path: impl AsRef<str>,
method: NetworkMethod,
request: T,
map: F,
) -> Result<V, CommonError>
where
T: Serialize,
U: for<'a> Deserialize<'a>,
F: Fn(U) -> Result<V, CommonError>,
{
// JSON serialize request into body bytes
let body = BagOfBytes::from(serde_json::to_vec(&request).unwrap());

// Append relative path to base url
let path = path.as_ref();
let url = self.gateway.url.join(path).map_err(|e| {
let bad_value = format!("{}{}", self.gateway.url, path);
error!(
"Failed to parse URL, error: {:?}, from string: {}",
e, &bad_value
);
CommonError::NetworkRequestInvalidUrl { bad_value }
})?;

impl NetworkRequest {
fn with_gateway_api_headers(self) -> Self {
let headers = HashMap::<String, String>::from_iter([
("content-Type".to_owned(), "application/json".to_owned()),
("accept".to_owned(), "application/json".to_owned()),
Expand All @@ -104,23 +68,6 @@ impl GatewayClient {
("RDX-Client-Version".to_owned(), "1.5.1".to_owned()),
]);

let request = NetworkRequest {
url,
body,
method,
headers,
};

// Let Swift side make network request and await response
let response = self
.network_antenna
.execute_network_request(request)
.await?;

// Read out HTTP body from response and JSON parse it into U
let model = self.model_from_response(response)?;

// Map U -> V
map(model)
self.with_headers(headers)
}
}
2 changes: 0 additions & 2 deletions src/gateway_api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ mod client;
mod endpoints;
mod methods;
mod models;
mod network_antenna;

pub use client::*;
pub use endpoints::*;
pub use methods::*;
pub use models::*;
pub use network_antenna::*;
17 changes: 0 additions & 17 deletions src/gateway_api/network_antenna/network_antenna.rs

This file was deleted.

6 changes: 0 additions & 6 deletions src/gateway_api/network_antenna/network_method.rs

This file was deleted.

10 changes: 0 additions & 10 deletions src/gateway_api/network_antenna/network_request.rs

This file was deleted.

9 changes: 0 additions & 9 deletions src/gateway_api/network_antenna/network_response.rs

This file was deleted.

Loading

0 comments on commit e38020d

Please sign in to comment.