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
238 changes: 130 additions & 108 deletions stellar/Cargo.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions stellar/wraith-names/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,11 @@ impl WraithNamesContract {
.persistent()
.remove(&DataKey::Reverse(meta_hash));

// Remove metadata if any
env.storage()
.instance()
.remove(&DataKey::Metadata(name_hash.clone()));

// Remove name
env.storage().persistent().remove(&name_key);

Expand Down
154 changes: 154 additions & 0 deletions stellar/wraith-names/src/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use soroban_sdk::{contracterror, contracttype, BytesN, Map, String};

pub const MAX_TEXT_RECORD_KEY_BYTES: u32 = 64;
pub const MAX_TEXT_RECORD_VALUE_BYTES: u32 = 256;
pub const MAX_METADATA_TOTAL_BYTES: u32 = 1024;

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MetadataEntry {
/// Optional text records keyed by names such as "avatar", "twitter", or "description".
pub text_records: Map<String, String>,
/// Optional content hash for IPFS or similar content-addressed payloads.
pub content_hash: BytesN<32>,
}

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum MetadataError {
MetadataKeyTooLong = 101,
MetadataValueTooLong = 102,
MetadataRecordTooLong = 103,
MetadataTotalTooLong = 104,
MetadataNotFound = 105,
}

pub fn validate_text_record(key: &String, value: &String) -> Result<(), MetadataError> {
if key.len() > MAX_TEXT_RECORD_KEY_BYTES {
return Err(MetadataError::MetadataKeyTooLong);
}

if value.len() > MAX_TEXT_RECORD_VALUE_BYTES {
return Err(MetadataError::MetadataValueTooLong);
}

let record_len = key
.len()
.checked_add(value.len())
.ok_or(MetadataError::MetadataRecordTooLong)?;
if record_len > MAX_METADATA_TOTAL_BYTES {
return Err(MetadataError::MetadataRecordTooLong);
}

Ok(())
}

pub fn validate_text_records(text_records: &Map<String, String>) -> Result<(), MetadataError> {
let mut total = 0u32;

for (key, value) in text_records.iter() {
validate_text_record(&key, &value)?;

total = total
.checked_add(key.len())
.ok_or(MetadataError::MetadataTotalTooLong)?;
total = total
.checked_add(value.len())
.ok_or(MetadataError::MetadataTotalTooLong)?;
}

if total > MAX_METADATA_TOTAL_BYTES {
return Err(MetadataError::MetadataTotalTooLong);
}

Ok(())
}

pub fn validate_metadata_entry(entry: &MetadataEntry) -> Result<(), MetadataError> {
validate_text_records(&entry.text_records)?;

if entry.content_hash.len() != 32 {
return Err(MetadataError::MetadataRecordTooLong);
}

Ok(())
}

#[cfg(test)]
mod test {
use super::*;
use soroban_sdk::{BytesN, Env, String};

#[test]
fn test_metadata_entry_validation_accepts_opt_in_fields() {
let env = Env::default();

let mut text_records = Map::<String, String>::new(&env);
text_records.set(
String::from_str(&env, "avatar"),
String::from_str(&env, "https://example.com/avatar.png"),
);
text_records.set(
String::from_str(&env, "twitter"),
String::from_str(&env, "@wraithprotocol"),
);

let metadata = MetadataEntry {
text_records,
content_hash: BytesN::from_array(&env, &[9u8; 32]),
};

assert_eq!(validate_metadata_entry(&metadata), Ok(()));
}

#[test]
fn test_metadata_key_limit_is_enforced() {
let env = Env::default();
let key = String::from_str(&env, "a");
let value = String::from_str(&env, "x");

let long_key = String::from_str(
&env,
"012345678901234567890123456789012345678901234567890123456789012345",
);
assert_eq!(validate_text_record(&key, &value), Ok(()));
assert_eq!(
validate_text_record(&long_key, &value),
Err(MetadataError::MetadataKeyTooLong)
);
}

#[test]
fn test_metadata_total_limit_is_enforced() {
let env = Env::default();
let mut text_records = Map::<String, String>::new(&env);

// 5 records each ~206 bytes (key=1, value=205) = 1030 total > 1024 limit
text_records.set(
String::from_str(&env, "a"),
String::from_str(&env, &"v".repeat(205)),
);
text_records.set(
String::from_str(&env, "b"),
String::from_str(&env, &"v".repeat(205)),
);
text_records.set(
String::from_str(&env, "c"),
String::from_str(&env, &"v".repeat(205)),
);
text_records.set(
String::from_str(&env, "d"),
String::from_str(&env, &"v".repeat(205)),
);
text_records.set(
String::from_str(&env, "e"),
String::from_str(&env, &"v".repeat(205)),
);

assert_eq!(
validate_text_records(&text_records),
Err(MetadataError::MetadataTotalTooLong)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
{
"generators": {
"address": 2,
"nonce": 0
},
"auth": [
[],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
{
"function": {
"contract_fn": {
"contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
"function_name": "register",
"args": [
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
},
{
"string": "ivan"
},
{
"bytes": "06060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606"
}
]
}
},
"sub_invocations": []
}
]
],
[]
],
"ledger": {
"protocol_version": 22,
"sequence_number": 0,
"timestamp": 0,
"network_id": "0000000000000000000000000000000000000000000000000000000000000000",
"base_reserve": 0,
"min_persistent_entry_ttl": 4096,
"min_temp_entry_ttl": 16,
"max_entry_ttl": 6312000,
"ledger_entries": [
[
{
"contract_data": {
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
"key": "ledger_key_contract_instance",
"durability": "persistent"
}
},
[
{
"last_modified_ledger_seq": 0,
"data": {
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
"key": "ledger_key_contract_instance",
"durability": "persistent",
"val": {
"contract_instance": {
"executable": {
"wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"storage": [
{
"key": {
"vec": [
{
"symbol": "Name"
},
{
"bytes": "cd0b9452fc376fc4c35a60087b366f70d883fc901524daf1f122fbd319384f6a"
}
]
},
"val": {
"map": [
{
"key": {
"symbol": "name"
},
"val": {
"string": "ivan"
}
},
{
"key": {
"symbol": "owner"
},
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
}
},
{
"key": {
"symbol": "stealth_meta_address"
},
"val": {
"bytes": "06060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606"
}
}
]
}
},
{
"key": {
"vec": [
{
"symbol": "Reverse"
},
{
"bytes": "1352286f7dddf65efce52c789476c7e917ecdd98d1c1ca9ab2dde1e4964caefe"
}
]
},
"val": {
"bytes": "cd0b9452fc376fc4c35a60087b366f70d883fc901524daf1f122fbd319384f6a"
}
}
]
}
}
}
},
"ext": "v0"
},
4095
]
],
[
{
"contract_data": {
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
"nonce": 801925984706572462
}
},
"durability": "temporary"
}
},
[
{
"last_modified_ledger_seq": 0,
"data": {
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
"nonce": 801925984706572462
}
},
"durability": "temporary",
"val": "void"
}
},
"ext": "v0"
},
6311999
]
],
[
{
"contract_code": {
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
},
[
{
"last_modified_ledger_seq": 0,
"data": {
"contract_code": {
"ext": "v0",
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"code": ""
}
},
"ext": "v0"
},
4095
]
]
]
},
"events": []
}
Loading
Loading