mod cbor;
mod client;
mod facilitator;
pub use cbor::{encode_builder_code_suffix, parse_builder_code_suffix_from_calldata};
pub use client::BuilderCodeClient;
use compact_str::CompactString;
pub use facilitator::{BuilderCodeFacilitatorConfig, BuilderCodeFacilitatorExtension};
use r402_protocol::extension::{AdvertiseContext, Extension};
use r402_protocol::payment::ExtensionEntry;
use serde_json::{Value, json};
pub const BUILDER_CODE: &str = "builder-code";
pub const ERC_8021_MARKER: [u8; 16] = [
0x80, 0x21, 0x80, 0x21, 0x80, 0x21, 0x80, 0x21, 0x80, 0x21, 0x80, 0x21, 0x80, 0x21, 0x80, 0x21,
];
pub const ERC_8021_MARKER_HEX: &str = "80218021802180218021802180218021";
pub const SCHEMA_2_ID: u8 = 0x02;
pub const MAX_CLIENT_SERVICE_CODES: usize = 5;
pub const MAX_SERVER_SERVICE_CODES: usize = 5;
pub const MAX_FACILITATOR_SERVICE_CODES: usize = 1;
pub const MAX_SERVICE_CODES: usize =
MAX_CLIENT_SERVICE_CODES + MAX_SERVER_SERVICE_CODES + MAX_FACILITATOR_SERVICE_CODES;
pub const MAX_ECHOED_SERVICE_CODES: usize = MAX_CLIENT_SERVICE_CODES + MAX_SERVER_SERVICE_CODES;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BuilderCodeData {
pub a: Option<CompactString>,
pub w: Option<CompactString>,
pub s: Vec<CompactString>,
}
impl BuilderCodeData {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.a.is_none() && self.w.is_none() && self.s.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum BuilderCodeError {
#[error(
"Invalid builder code: \"{0}\". Must be 1-32 characters, lowercase alphanumeric and underscores only."
)]
InvalidCode(String),
#[error("Too many service codes: {got} exceeds the maximum of {max}.")]
TooManyServiceCodes {
got: usize,
max: usize,
},
}
#[must_use]
pub fn is_valid_builder_code(code: &str) -> bool {
let len = code.len();
(1..=32).contains(&len)
&& code
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
}
pub(crate) fn validate_code(code: &str) -> Result<(), BuilderCodeError> {
if is_valid_builder_code(code) {
Ok(())
} else {
Err(BuilderCodeError::InvalidCode(code.to_owned()))
}
}
pub(crate) fn validate_codes<'a, I>(
codes: I,
max: usize,
) -> Result<Vec<CompactString>, BuilderCodeError>
where
I: IntoIterator<Item = &'a str>,
{
let mut out = Vec::new();
for code in codes {
validate_code(code)?;
out.push(CompactString::from(code));
}
if out.len() > max {
return Err(BuilderCodeError::TooManyServiceCodes {
got: out.len(),
max,
});
}
Ok(out)
}
fn info_schema() -> Value {
json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"a": {
"type": "string",
"pattern": "^[a-z0-9_]{1,32}$",
"description": "App builder code",
},
"w": {
"type": "string",
"pattern": "^[a-z0-9_]{1,32}$",
"description": "Wallet builder code",
},
"s": {
"type": "array",
"maxItems": MAX_SERVICE_CODES,
"items": {
"type": "string",
"pattern": "^[a-z0-9_]{1,32}$",
},
"description": "Service builder codes",
},
},
"additionalProperties": false,
})
}
pub fn declare_builder_code_extension(
app_code: &str,
service_codes: &[&str],
) -> Result<ExtensionEntry, BuilderCodeError> {
validate_code(app_code)?;
let codes = validate_codes(service_codes.iter().copied(), MAX_SERVER_SERVICE_CODES)?;
let info = if codes.is_empty() {
json!({ "a": app_code })
} else {
json!({ "a": app_code, "s": codes })
};
Ok(ExtensionEntry::with_schema(info, info_schema()))
}
#[derive(Debug, Clone)]
pub struct BuilderCodeExtension {
app_code: CompactString,
service_codes: Vec<CompactString>,
}
impl BuilderCodeExtension {
pub fn try_new(app_code: impl AsRef<str>) -> Result<Self, BuilderCodeError> {
let app_code = app_code.as_ref();
validate_code(app_code)?;
Ok(Self {
app_code: CompactString::from(app_code),
service_codes: Vec::new(),
})
}
pub fn with_service_codes<'a, I>(mut self, codes: I) -> Result<Self, BuilderCodeError>
where
I: IntoIterator<Item = &'a str>,
{
self.service_codes = validate_codes(codes, MAX_SERVER_SERVICE_CODES)?;
Ok(self)
}
}
impl Extension for BuilderCodeExtension {
fn id(&self) -> &'static str {
BUILDER_CODE
}
fn advertise(&self, _ctx: &AdvertiseContext<'_>) -> Option<ExtensionEntry> {
let refs: Vec<&str> = self
.service_codes
.iter()
.map(CompactString::as_str)
.collect();
declare_builder_code_extension(self.app_code.as_str(), &refs).ok()
}
}
pub(crate) fn extract_info(extensions: &r402_protocol::payment::Extensions) -> Option<Value> {
let entry = extensions.get(BUILDER_CODE)?;
let value = entry.to_value();
if let Some(info) = value.get("info").cloned()
&& info.is_object()
{
return Some(info);
}
Some(value)
}
pub(crate) fn normalize_service_codes(raw: &Value) -> Vec<CompactString> {
match raw {
Value::String(s) => vec![CompactString::from(s.as_str())],
Value::Array(items) => items
.iter()
.filter_map(Value::as_str)
.map(CompactString::from)
.collect(),
_ => Vec::new(),
}
}