Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions libs/rs-sdk-integration-tests/src/http_batch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use std::collections::BTreeMap;

use seda_sdk_rs::{
bytes::ToBytes,
http::{http_fetch_batch, HttpFetchMethod, HttpFetchOptions},
process::Process,
};

pub fn test_http_fetch_batch_success() {
let requests = vec![
("https://jsonplaceholder.typicode.com/todos/1", None),
("https://jsonplaceholder.typicode.com/todos/2", None),
];

let responses = http_fetch_batch(requests);

if responses.len() != 2 {
Process::error(&format!("expected 2 responses, got {}", responses.len()).to_bytes());
}

let all_ok = responses.iter().all(|r| r.is_ok());
if !all_ok {
Process::error(&"not all responses were successful".to_bytes());
}

Process::success(&format!("{}:{}", responses[0].status, responses[1].status).to_bytes());
}

pub fn test_http_fetch_batch_partial_failure() {
let requests = vec![
("https://jsonplaceholder.typicode.com/todos/1", None),
("https://invalid-domain-that-does-not-exist.example/foo", None),
];

let responses = http_fetch_batch(requests);

if responses.len() != 2 {
Process::error(&format!("expected 2 responses, got {}", responses.len()).to_bytes());
}

let first_ok = responses[0].is_ok();
let second_ok = responses[1].is_ok();

Process::success(&format!("{}:{}", first_ok, second_ok).to_bytes());
}

pub fn test_http_fetch_batch_options() {
let mut headers = BTreeMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
headers.insert("X-Custom".to_string(), "batch-test".to_string());

let requests = vec![
(
"https://api.example.com/get",
Some(HttpFetchOptions {
method: HttpFetchMethod::Get,
headers: BTreeMap::new(),
body: None,
timeout_ms: Some(5000),
}),
),
(
"https://api.example.com/post",
Some(HttpFetchOptions {
method: HttpFetchMethod::Post,
headers,
body: Some(r#"{"key":"value"}"#.to_bytes()),
timeout_ms: Some(3000),
}),
),
];

let responses = http_fetch_batch(requests);

if responses.len() != 2 {
Process::error(&format!("expected 2 responses, got {}", responses.len()).to_bytes());
}

let first_body = String::from_utf8(responses[0].bytes.clone()).unwrap_or_default();
let second_body = String::from_utf8(responses[1].bytes.clone()).unwrap_or_default();

Process::success(&format!("{}|{}", first_body, second_body).to_bytes());
}
13 changes: 13 additions & 0 deletions libs/rs-sdk-integration-tests/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
mod clock;
mod crypto;
mod http;
mod http_batch;
mod infinite_loop;
mod proxy_http;
mod proxy_http_batch;
mod random_get;
mod tally;
mod vm_tests;

use clock::test_clock_time_get;
use crypto::{test_keccak256, test_secp256k1_verify_invalid, test_secp256k1_verify_valid};
use http::*;
use http_batch::{test_http_fetch_batch_options, test_http_fetch_batch_partial_failure, test_http_fetch_batch_success};
use infinite_loop::{test_infinite_loop, test_infinite_loop_http_fetch};
use proxy_http::{test_generate_proxy_http_message, test_proxy_http_fetch};
use proxy_http_batch::{
test_proxy_http_fetch_batch_options, test_proxy_http_fetch_batch_partial_failure,
test_proxy_http_fetch_batch_success,
};
use random_get::test_random_get;
use seda_sdk_rs::{bytes::ToBytes, process::Process};
use tally::{
Expand Down Expand Up @@ -48,6 +55,12 @@ fn main() {
"testLongFetch" => test_long_fetch(),
"testClockTimeGet" => test_clock_time_get(),
"testHttpFetchAccessFile" => test_http_fetch_access_file(),
"testHttpFetchBatchSuccess" => test_http_fetch_batch_success(),
"testHttpFetchBatchPartialFailure" => test_http_fetch_batch_partial_failure(),
"testHttpFetchBatchOptions" => test_http_fetch_batch_options(),
"testProxyHttpFetchBatchSuccess" => test_proxy_http_fetch_batch_success(),
"testProxyHttpFetchBatchPartialFailure" => test_proxy_http_fetch_batch_partial_failure(),
"testProxyHttpFetchBatchOptions" => test_proxy_http_fetch_batch_options(),
_ => Process::error(&"No argument given".to_bytes()),
}
}
86 changes: 86 additions & 0 deletions libs/rs-sdk-integration-tests/src/proxy_http_batch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use std::collections::BTreeMap;

use seda_sdk_rs::{
bytes::ToBytes,
http::{HttpFetchMethod, HttpFetchOptions},
process::Process,
proxy_http_fetch::proxy_http_fetch_batch,
};

pub fn test_proxy_http_fetch_batch_success() {
let requests = vec![
("https://proxy1.example.com/data", None, None),
("https://proxy2.example.com/data", None, None),
];

let responses = proxy_http_fetch_batch(requests);

if responses.len() != 2 {
Process::error(&format!("expected 2 responses, got {}", responses.len()).to_bytes());
}

let all_ok = responses.iter().all(|r| r.is_ok());
if !all_ok {
Process::error(&"not all responses were successful".to_bytes());
}

Process::success(&format!("{}:{}", responses[0].status, responses[1].status).to_bytes());
}

pub fn test_proxy_http_fetch_batch_partial_failure() {
let requests = vec![
("https://proxy1.example.com/data", None, None),
("https://proxy2.example.com/error", None, None),
];

let responses = proxy_http_fetch_batch(requests);

if responses.len() != 2 {
Process::error(&format!("expected 2 responses, got {}", responses.len()).to_bytes());
}

let first_ok = responses[0].is_ok();
let second_ok = responses[1].is_ok();

Process::success(&format!("{}:{}", first_ok, second_ok).to_bytes());
}

pub fn test_proxy_http_fetch_batch_options() {
let mut headers = BTreeMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
headers.insert("X-Proxy-Custom".to_string(), "batch-proxy-test".to_string());

let requests = vec![
(
"https://proxy.example.com/get",
None,
Some(HttpFetchOptions {
method: HttpFetchMethod::Get,
headers: BTreeMap::new(),
body: None,
timeout_ms: Some(5000),
}),
),
(
"https://proxy.example.com/post",
None,
Some(HttpFetchOptions {
method: HttpFetchMethod::Post,
headers,
body: Some(r#"{"proxy":"data"}"#.to_bytes()),
timeout_ms: Some(3000),
}),
),
];

let responses = proxy_http_fetch_batch(requests);

if responses.len() != 2 {
Process::error(&format!("expected 2 responses, got {}", responses.len()).to_bytes());
}

let first_body = String::from_utf8(responses[0].bytes.clone()).unwrap_or_default();
let second_body = String::from_utf8(responses[1].bytes.clone()).unwrap_or_default();

Process::success(&format!("{}|{}", first_body, second_body).to_bytes());
}
50 changes: 50 additions & 0 deletions libs/rs-sdk/sdk/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,53 @@ pub fn http_fetch<URL: ToString>(url: URL, options: Option<HttpFetchOptions>) ->

HttpFetchResponse::from_promise(promise_status)
}

/// Batch HTTP fetch for multiple URLs with their respective options.
///
/// This function allows you to execute multiple HTTP fetch actions in one call.
/// Each entry in the input `Vec` is a tuple of URL and an optional `HttpFetchOptions`.
/// Returns a vector of `HttpFetchResponse` in the same order.
///
/// # Example
/// ```no_run
/// use seda_sdk_rs::http::{http_fetch_batch, HttpFetchOptions};
/// let requests: Vec<(&str, Option<HttpFetchOptions>)> = vec![
/// ("https://weather.example.com", None),
/// ("https://news.example.com", None),
/// ];
/// let responses = http_fetch_batch(requests);
/// assert_eq!(responses.len(), 2);
/// ```
///
pub fn http_fetch_batch<URL: ToString>(requests: Vec<(URL, Option<HttpFetchOptions>)>) -> Vec<HttpFetchResponse> {
#[derive(Serialize)]
struct HttpFetchBatchPayload {
requests: Vec<HttpFetchAction>,
}

let payload = HttpFetchBatchPayload {
requests: requests
.into_iter()
.map(|(url, options)| HttpFetchAction {
url: url.to_string(),
options: options.unwrap_or_default(),
})
.collect(),
};

let action = serde_json::to_string(&payload).unwrap();
let result_length = unsafe { super::raw::http_fetch_batch(action.as_ptr(), action.len() as u32) };
let mut result_data_ptr = vec![0; result_length as usize];

unsafe {
super::raw::call_result_write(result_data_ptr.as_mut_ptr(), result_length);
}

let promise_statuses: Vec<PromiseStatus> =
serde_json::from_slice(&result_data_ptr).expect("Could not deserialize http_fetch_batch");

promise_statuses
.into_iter()
.map(HttpFetchResponse::from_promise)
.collect()
}
2 changes: 1 addition & 1 deletion libs/rs-sdk/sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ mod vm_modes;
pub use http::{http_fetch, HttpFetchMethod, HttpFetchOptions, HttpFetchResponse};
pub use keccak256::keccak256;
pub use process::Process;
pub use proxy_http_fetch::{generate_proxy_http_signing_message, proxy_http_fetch};
pub use proxy_http_fetch::{generate_proxy_http_signing_message, proxy_http_fetch, proxy_http_fetch_batch};
pub use secp256k1::secp256k1_verify;
pub use tally::*;

Expand Down
54 changes: 54 additions & 0 deletions libs/rs-sdk/sdk/src/proxy_http_fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,60 @@ pub fn proxy_http_fetch<URL: ToString>(
HttpFetchResponse::from_promise(promise_status)
}

/// Batch proxy HTTP fetch for multiple URLs with their respective options.
///
/// This function allows you to execute multiple proxy HTTP fetch actions in one call.
/// Each entry in the input `Vec` is a tuple of URL, optional public key, and optional `HttpFetchOptions`.
/// Returns a vector of `HttpFetchResponse` in the same order.
///
/// # Example
/// ```no_run
/// use seda_sdk_rs::proxy_http_fetch::proxy_http_fetch_batch;
/// use seda_sdk_rs::http::HttpFetchOptions;
/// let requests: Vec<(&str, Option<String>, Option<HttpFetchOptions>)> = vec![
/// ("https://proxy1.example.com", None, None),
/// ("https://proxy2.example.com", None, None),
/// ];
/// let responses = proxy_http_fetch_batch(requests);
/// assert_eq!(responses.len(), 2);
/// ```
///
pub fn proxy_http_fetch_batch<URL: ToString>(
requests: Vec<(URL, Option<String>, Option<HttpFetchOptions>)>,
) -> Vec<HttpFetchResponse> {
#[derive(Serialize)]
struct ProxyHttpFetchBatchPayload {
requests: Vec<ProxyHttpFetchAction>,
}

let payload = ProxyHttpFetchBatchPayload {
requests: requests
.into_iter()
.map(|(url, public_key, options)| ProxyHttpFetchAction {
url: url.to_string(),
public_key,
options: options.unwrap_or_default(),
})
.collect(),
};

let action = serde_json::to_string(&payload).unwrap();
let result_length = unsafe { super::raw::proxy_http_fetch_batch(action.as_ptr(), action.len() as u32) };
let mut result_data_ptr = vec![0; result_length as usize];

unsafe {
super::raw::call_result_write(result_data_ptr.as_mut_ptr(), result_length);
}

let promise_statuses: Vec<PromiseStatus> =
serde_json::from_slice(&result_data_ptr).expect("Could not deserialize proxy_http_fetch_batch");

promise_statuses
.into_iter()
.map(HttpFetchResponse::from_promise)
.collect()
}

/// Generates the message which the data proxy hashed and signed. This can be useful when you need to verify
/// the data proxy signature in the tally phase. With this message there is no need to include the entire request
/// and response data in the execution result.
Expand Down
2 changes: 2 additions & 0 deletions libs/rs-sdk/sdk/src/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ extern "C" {

// Call actions
pub fn http_fetch(action: *const u8, action_length: u32) -> u32;
pub fn http_fetch_batch(action: *const u8, action_length: u32) -> u32;
pub fn proxy_http_fetch(action: *const u8, action_length: u32) -> u32;
pub fn proxy_http_fetch_batch(action: *const u8, action_length: u32) -> u32;

// Reading call actions result
pub fn call_result_write(result: *const u8, result_length: u32);
Expand Down
39 changes: 38 additions & 1 deletion libs/vm/src/data-request-vm-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ import { VmError, VmErrorType } from "./errors.js";
import { readStream } from "./services/read-stream.js";
import type {
HttpFetchAction,
HttpFetchBatchAction,
ProxyHttpFetchAction,
ProxyHttpFetchBatchAction,
} from "./types/vm-actions.js";
import {
HttpFetchBatchResponse,
HttpFetchResponse,
ProxyHttpFetchBatchResponse,
} from "./types/vm-actions.js";
import { HttpFetchResponse } from "./types/vm-actions.js";
import type { VmAdapter } from "./types/vm-adapter.js";
import type { VmCallData } from "./types/vm-call-data.js";
import { VM_MODE_DR, VM_MODE_ENV_KEY } from "./types/vm-modes.js";
Expand Down Expand Up @@ -102,6 +108,37 @@ export default class DataRequestVmAdapter implements VmAdapter {
throw new VmError("Unimplemented");
}

async proxyHttpFetchBatch(
action: ProxyHttpFetchBatchAction,
): Promise<ProxyHttpFetchBatchResponse> {
const responses = await Promise.all(
action.requests.map((request) =>
this.proxyHttpFetch({
url: request.url,
options: request.options,
type: "proxy-http-fetch-action",
}),
),
);

return new ProxyHttpFetchBatchResponse(responses);
}

async httpFetchBatch(
action: HttpFetchBatchAction,
): Promise<HttpFetchBatchResponse> {
const responses = await Promise.all(
action.requests.map((request) =>
this.httpFetch({
...request,
type: "http-fetch-action",
}),
),
);

return new HttpFetchBatchResponse(responses);
}

async httpFetch(
action: HttpFetchAction,
): Promise<PromiseStatus<HttpFetchResponse>> {
Expand Down
Loading
Loading