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
2 changes: 2 additions & 0 deletions crates/cli/src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub enum Error {
UnsupportedNetwork,
InflateError(String),
InvalidInput(String),
InvalidUrl(String),
Utf8Error(Utf8Error),
FromUtf8Error(FromUtf8Error),
ReqwestError(reqwest::Error),
Expand Down Expand Up @@ -45,6 +46,7 @@ impl std::fmt::Display for Error {
f.write_str("unexpected input size, must be 32 bytes or less")
}
Error::InvalidInput(v) => write!(f, "invalid input: {}", v),
Error::InvalidUrl(v) => write!(f, "invalid URL: {}", v),
Error::ReqwestError(v) => write!(f, "{}", v),
Error::InflateError(v) => write!(f, "{}", v),
Error::Utf8Error(v) => write!(f, "{}", v),
Expand Down
22 changes: 22 additions & 0 deletions crates/cli/src/meta/magic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ pub enum KnownMagic {
DotrainSourceV1 = 0xffa15ef0fc437099,
/// Dotrain instance meta v1
DotrainGuiStateV1 = 0xffda7b2fb167c286,
/// Signed context oracle endpoint v1
/// Payload is raw UTF-8 bytes containing the oracle endpoint URL.
/// Used in order metadata to tell takers where to GET signed context data.
RaindexSignedContextOracleV1 = 0xff7a1507ba4419ca,
}

impl KnownMagic {
Expand Down Expand Up @@ -71,6 +75,9 @@ impl TryFrom<u64> for KnownMagic {
v if v == KnownMagic::RainlangSourceV1 as u64 => Ok(KnownMagic::RainlangSourceV1),
v if v == KnownMagic::DotrainSourceV1 as u64 => Ok(KnownMagic::DotrainSourceV1),
v if v == KnownMagic::DotrainGuiStateV1 as u64 => Ok(KnownMagic::DotrainGuiStateV1),
v if v == KnownMagic::RaindexSignedContextOracleV1 as u64 => {
Ok(KnownMagic::RaindexSignedContextOracleV1)
}
_ => Err(crate::error::Error::UnknownMagic),
}
}
Expand Down Expand Up @@ -176,4 +183,19 @@ mod tests {

assert_eq!(hex::encode(magic_number_after_prefix), "ffda7b2fb167c286");
}

#[test]
fn test_signed_context_oracle_v1() {
let magic_number = KnownMagic::RaindexSignedContextOracleV1;
let magic_number_after_prefix = magic_number.to_prefix_bytes();

assert_eq!(hex::encode(magic_number_after_prefix), "ff7a1507ba4419ca");
}

#[test]
fn test_signed_context_oracle_v1_roundtrip() {
let magic_number = KnownMagic::RaindexSignedContextOracleV1;
let from_u64 = KnownMagic::try_from(magic_number as u64).unwrap();
assert_eq!(magic_number, from_u64);
}
}
5 changes: 4 additions & 1 deletion crates/cli/src/meta/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub enum KnownMeta {
AddressList,
DotrainSourceV1,
DotrainGuiStateV1,
RaindexSignedContextOracleV1,
}

impl TryFrom<KnownMagic> for KnownMeta {
Expand All @@ -58,6 +59,7 @@ impl TryFrom<KnownMagic> for KnownMeta {
Ok(KnownMeta::ExpressionDeployerV2BytecodeV1)
}
KnownMagic::RainlangSourceV1 => Ok(KnownMeta::RainlangSourceV1),
KnownMagic::RaindexSignedContextOracleV1 => Ok(KnownMeta::RaindexSignedContextOracleV1),
_ => Err(Error::UnsupportedMeta),
}
}
Expand Down Expand Up @@ -288,7 +290,8 @@ impl RainMetaDocumentV1Item {
| KnownMagic::ExpressionDeployerV2BytecodeV1
| KnownMagic::DotrainSourceV1
| KnownMagic::DotrainGuiStateV1
| KnownMagic::RainlangSourceV1 => T::try_from(self),
| KnownMagic::RainlangSourceV1
| KnownMagic::RaindexSignedContextOracleV1 => T::try_from(self),
_ => Err(Error::UnsupportedMeta)?,
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/cli/src/meta/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ pub mod interpreter_caller;
pub mod op;
pub mod rainlang;
pub mod rainlangsource;
pub mod raindex_signed_context_oracle;
pub mod solidity_abi;
192 changes: 192 additions & 0 deletions crates/cli/src/meta/types/raindex_signed_context_oracle/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
use serde::{Deserialize, Serialize};
use url::Url;

use crate::{
error::Error,
meta::{ContentEncoding, ContentLanguage, ContentType, KnownMagic, RainMetaDocumentV1Item},
};

#[cfg(target_family = "wasm")]
use wasm_bindgen_utils::{impl_wasm_traits, prelude::*};

/// Signed Context Oracle V1 meta.
///
/// Contains a validated URL pointing to a GET endpoint that returns
/// signed context data for use in Rain order evaluation.
///
/// The endpoint must return JSON: `{signer, context, signature}` mapping
/// directly to `SignedContextV1`.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[cfg_attr(target_family = "wasm", derive(Tsify))]
pub struct RaindexSignedContextOracleV1(pub String);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The pub inner field breaks the URL validation invariant.

new() and parse() ensure only valid URLs are stored, but pub String lets callers bypass this entirely:

// Compiles fine, no validation
let bad = RaindexSignedContextOracleV1("not a url".into());

This also means parsed_url() can unexpectedly fail on an instance that was "successfully" constructed.

Make the field private to enforce the invariant:

Proposed fix
-pub struct RaindexSignedContextOracleV1(pub String);
+pub struct RaindexSignedContextOracleV1(String);

If WASM/serde deserialization needs to construct this type, add a custom Deserialize impl (or a #[serde(try_from = "String")]) that validates via Url::parse. Based on learnings, the url crate is already a dependency and Url provides validation, type safety, and serde support — consider wrapping Url directly if Tsify/WASM constraints allow it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/cli/src/meta/types/raindex_signed_context_oracle/mod.rs` at line 21,
The struct RaindexSignedContextOracleV1 currently exposes its inner String as
pub which allows callers to construct invalid instances and break the URL
validation invariant; make the inner field private (remove pub) so instances can
only be created through the validated constructors (new() and parse()), update
any call sites to use those constructors, and if deserialization/wasm needs
require construction add a custom serde::Deserialize impl or use
#[serde(try_from = "String")] that calls Url::parse (or wrap Url directly) so
parsed_url() can never fail at runtime.


#[cfg(target_family = "wasm")]
impl_wasm_traits!(RaindexSignedContextOracleV1);

impl RaindexSignedContextOracleV1 {
/// Create a new RaindexSignedContextOracleV1 from a URL.
/// Validates that the input is a well-formed URL.
pub fn new(url: Url) -> Self {
Self(url.to_string())
}

/// Parse and create a new RaindexSignedContextOracleV1 from a string.
/// Returns an error if the string is not a valid URL.
pub fn parse(url: &str) -> Result<Self, Error> {
let parsed = Url::parse(url).map_err(|e| Error::InvalidUrl(e.to_string()))?;
Ok(Self::new(parsed))
}

/// Get the oracle URL as a string.
pub fn url(&self) -> &str {
&self.0
}

/// Get the oracle URL as a parsed Url.
pub fn parsed_url(&self) -> Result<Url, Error> {
Url::parse(&self.0).map_err(|e| Error::InvalidUrl(e.to_string()))
}
Comment on lines +46 to +48
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

parsed_url() would be unnecessary if Url were stored directly.

If you keep String internally, this method is fine but its fallibility is misleading when the struct is properly constructed. If you switch the inner type to url::Url, this becomes a simple accessor and cannot fail. Based on learnings, in the rain.metadata crate, prefer wrapping url::Url instead of String for types that represent URLs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/cli/src/meta/types/raindex_signed_context_oracle/mod.rs` around lines
46 - 48, The parsed_url() method exists only because the newtype stores a
String; change the inner type from String to url::Url so URL parsing happens at
construction and parsed_url() can become a simple infallible accessor returning
&Url (or expose the inner Url directly). Update the newtype definition (the
tuple struct holding self.0), constructors/parsers that currently accept strings
to parse into Url at creation, and replace calls to parsed_url() with the new
accessor to remove the needless Result handling.


/// Encode this oracle descriptor as a `RainMetaDocumentV1Item`.
/// The payload is raw UTF-8 bytes of the URL string.
pub fn to_meta_item(&self) -> RainMetaDocumentV1Item {
RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(self.0.as_bytes().to_vec()),
magic: KnownMagic::RaindexSignedContextOracleV1,
content_type: ContentType::None,
content_encoding: ContentEncoding::None,
content_language: ContentLanguage::None,
}
}

/// Encode as a complete Rain meta document (with magic prefix).
pub fn cbor_encode(&self) -> Result<Vec<u8>, Error> {
RainMetaDocumentV1Item::cbor_encode_seq(
&vec![self.to_meta_item()],
KnownMagic::RainMetaDocumentV1,
)
}

/// Try to extract a `RaindexSignedContextOracleV1` from decoded meta items.
/// Returns `Ok(None)` if no oracle meta is found.
/// Returns `Err` if oracle meta is found but cannot be decoded.
pub fn find_in_items(items: &[RainMetaDocumentV1Item]) -> Result<Option<Self>, Error> {
match items
.iter()
.find(|item| matches!(item.magic, KnownMagic::RaindexSignedContextOracleV1))
{
Some(item) => Ok(Some(Self::try_from(item.clone())?)),
None => Ok(None),
}
}
}

impl TryFrom<RainMetaDocumentV1Item> for RaindexSignedContextOracleV1 {
type Error = Error;
fn try_from(value: RainMetaDocumentV1Item) -> Result<Self, Self::Error> {
if !matches!(value.magic, KnownMagic::RaindexSignedContextOracleV1) {
return Err(Error::UnsupportedMeta);
}
let bytes = value.unpack()?;
let url_str = String::from_utf8(bytes)?;
Self::parse(&url_str)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_roundtrip() {
let url = "https://oracle.example.com/prices/eth-usd";
let oracle = RaindexSignedContextOracleV1::parse(url).unwrap();

// Encode to Rain meta document
let encoded = oracle.cbor_encode().unwrap();

// Should start with Rain meta document magic
assert!(encoded.starts_with(&KnownMagic::RainMetaDocumentV1.to_prefix_bytes()));

// Decode back
let items = RainMetaDocumentV1Item::cbor_decode(&encoded).unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].magic, KnownMagic::RaindexSignedContextOracleV1);

// Extract oracle
let decoded = RaindexSignedContextOracleV1::try_from(items[0].clone()).unwrap();
assert_eq!(decoded.url(), url);
}

#[test]
fn test_find_in_items() {
let url = "https://oracle.example.com/prices/eth-usd";
let oracle = RaindexSignedContextOracleV1::parse(url).unwrap();
let item = oracle.to_meta_item();

let found = RaindexSignedContextOracleV1::find_in_items(&[item]).unwrap().unwrap();
assert_eq!(found.url(), url);
}

#[test]
fn test_find_in_items_missing() {
let items: Vec<RainMetaDocumentV1Item> = vec![];
assert!(RaindexSignedContextOracleV1::find_in_items(&items).unwrap().is_none());
}

#[test]
fn test_find_in_items_decode_error() {
// Oracle magic but invalid payload (not valid UTF-8 URL)
let item = RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(vec![0xFF, 0xFE]),
magic: KnownMagic::RaindexSignedContextOracleV1,
content_type: ContentType::None,
content_encoding: ContentEncoding::None,
content_language: ContentLanguage::None,
};
assert!(RaindexSignedContextOracleV1::find_in_items(&[item]).is_err());
}

#[test]
fn test_new_with_url() {
let url = Url::parse("https://example.com/feed").unwrap();
let oracle = RaindexSignedContextOracleV1::new(url);
assert_eq!(oracle.url(), "https://example.com/feed");
}

#[test]
fn test_parse_valid_url() {
let oracle = RaindexSignedContextOracleV1::parse("https://example.com/feed").unwrap();
assert_eq!(oracle.url(), "https://example.com/feed");
}

#[test]
fn test_parse_invalid_url() {
assert!(RaindexSignedContextOracleV1::parse("not a url").is_err());
}

#[test]
fn test_parse_empty_url() {
assert!(RaindexSignedContextOracleV1::parse("").is_err());
}

#[test]
fn test_wrong_magic_fails() {
let item = RainMetaDocumentV1Item {
payload: serde_bytes::ByteBuf::from(b"https://example.com".to_vec()),
magic: KnownMagic::DotrainSourceV1,
content_type: ContentType::None,
content_encoding: ContentEncoding::None,
content_language: ContentLanguage::None,
};
assert!(RaindexSignedContextOracleV1::try_from(item).is_err());
}

#[test]
fn test_parsed_url() {
let oracle = RaindexSignedContextOracleV1::parse("https://example.com/feed?pair=eth-usd").unwrap();
let parsed = oracle.parsed_url().unwrap();
assert_eq!(parsed.host_str(), Some("example.com"));
assert_eq!(parsed.path(), "/feed");
}
}
3 changes: 3 additions & 0 deletions foundry.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"lib/forge-std": {
"rev": "1801b0541f4fda118a10798fd3486bb7051c5dd6"
},
"lib/rain.deploy": {
"rev": "e419a46e2a1317a63639ac13fc5a22d7e967fbef"
}
}
Loading