Skip to content
Merged
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ A service's metadata (`description` + `owner`) and its registration flag live in
independent storage slots. `clear_service_metadata` (admin-gated, idempotent)
removes only the metadata; the registration flag and per-(agent, service) usage
history are untouched.

`register_service_with_metadata(service_id, description, owner)` is an
admin-gated convenience that does both in one atomic call: it sets the
registration flag and persists the metadata, with a single auth check and a
single `svc_reg(service_id, owner)` event. It is equivalent to calling
`register_service` then `set_service_metadata`. Re-registering an existing id
overwrites its metadata idempotently, and an empty `description` is accepted.
### Admin proposal validation

`propose_admin_transfer` rejects proposing the current admin as the new admin
Expand Down
43 changes: 39 additions & 4 deletions contracts/escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,17 @@ fn write_flag(env: &Env, key: &DataKey, value: bool) {
env.storage().persistent().set(key, &value);
}

/// Persist a service's metadata (`description`, `owner`) under
/// `DataKey::ServiceMetadata(service_id)`. Shared by `set_service_metadata`
/// and `register_service_with_metadata` so the storage shape stays in one
/// place. Performs no auth — callers are responsible for admin gating.
fn write_service_metadata(env: &Env, service_id: &Symbol, description: String, owner: Address) {
env.storage().persistent().set(
&DataKey::ServiceMetadata(service_id.clone()),
&ServiceMetadata { description, owner },
);
}

#[contract]
pub struct Escrow;

Expand Down Expand Up @@ -666,10 +677,34 @@ impl Escrow {
.get(&DataKey::Admin)
.unwrap_or_else(|| panic_with_error!(&env, EscrowError::NotInitialized));
admin.require_auth();
env.storage().persistent().set(
&DataKey::ServiceMetadata(service_id),
&ServiceMetadata { description, owner },
);
write_service_metadata(&env, &service_id, description, owner);
}

/// Register a service and set its metadata in a single admin-gated,
/// atomic call. Equivalent to calling `register_service` followed by
/// `set_service_metadata`, but with one auth check and one event so
/// indexers see registration and metadata land together.
///
/// Sets `ServiceRegistered(service_id) = true` and persists
/// `ServiceMetadata(service_id)` (`description` + `owner`). Idempotent:
/// re-registering an existing id overwrites its metadata. An empty
/// `description` is accepted. Emits `svc_reg(service_id, owner)`.
pub fn register_service_with_metadata(
env: Env,
service_id: Symbol,
description: String,
owner: Address,
) {
let admin: Address = env
.storage()
.persistent()
.get(&DataKey::Admin)
.unwrap_or_else(|| panic_with_error!(&env, EscrowError::NotInitialized));
admin.require_auth();
write_flag(&env, &DataKey::ServiceRegistered(service_id.clone()), true);
write_service_metadata(&env, &service_id, description, owner.clone());
env.events()
.publish((symbol_short!("svc_reg"),), (service_id, owner));
}

/// Transfer ownership of a service's metadata to `new_owner`,
Expand Down
92 changes: 92 additions & 0 deletions contracts/escrow/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -697,3 +697,95 @@ fn test_pause_pause_unpause_ends_unpaused() {

assert!(!client.is_paused());
}

#[test]
fn test_register_service_with_metadata_sets_flag_and_metadata() {
let env = Env::default();
let (client, _admin) = setup_initialized(&env);
let svc = Symbol::new(&env, "infer");
let owner = Address::generate(&env);
let description = String::from_str(&env, "GPU inference endpoint");

client.register_service_with_metadata(&svc, &description, &owner);

// A single call sets both the registration flag and the metadata.
assert!(client.is_service_registered(&svc));
let meta = client.get_service_metadata(&svc).unwrap();
assert_eq!(meta.description, description);
assert_eq!(meta.owner, owner);
}

#[test]
fn test_register_service_with_metadata_emits_svc_reg_event() {
let env = Env::default();
let (client, _admin) = setup_initialized(&env);
let svc = Symbol::new(&env, "infer");
let owner = Address::generate(&env);
let description = String::from_str(&env, "GPU inference endpoint");

client.register_service_with_metadata(&svc, &description, &owner);

let events = env.events().all();
assert!(!events.is_empty());
let (_addr, topics, data) = events.last().unwrap();
let expected_topics: soroban_sdk::Vec<soroban_sdk::Val> =
(symbol_short!("svc_reg"),).into_val(&env);
assert_eq!(topics, expected_topics);
let decoded: (Symbol, Address) = data.into_val(&env);
assert_eq!(decoded, (svc.clone(), owner.clone()));
}

#[test]
fn test_register_service_with_metadata_overwrites_idempotently() {
let env = Env::default();
let (client, _admin) = setup_initialized(&env);
let svc = Symbol::new(&env, "infer");
let owner1 = Address::generate(&env);
let owner2 = Address::generate(&env);
let desc1 = String::from_str(&env, "first");
let desc2 = String::from_str(&env, "second");

client.register_service_with_metadata(&svc, &desc1, &owner1);
// Re-registering the same id overwrites the metadata and keeps it registered.
client.register_service_with_metadata(&svc, &desc2, &owner2);

assert!(client.is_service_registered(&svc));
let meta = client.get_service_metadata(&svc).unwrap();
assert_eq!(meta.description, desc2);
assert_eq!(meta.owner, owner2);
}

#[test]
fn test_register_service_with_metadata_allows_empty_description() {
let env = Env::default();
let (client, _admin) = setup_initialized(&env);
let svc = Symbol::new(&env, "infer");
let owner = Address::generate(&env);
let empty = String::from_str(&env, "");

client.register_service_with_metadata(&svc, &empty, &owner);

assert!(client.is_service_registered(&svc));
let meta = client.get_service_metadata(&svc).unwrap();
assert_eq!(meta.description, empty);
assert_eq!(meta.owner, owner);
}

#[test]
#[should_panic]
fn test_register_service_with_metadata_rejects_non_admin() {
let env = Env::default();
let contract_id = env.register_contract(None, Escrow);
let client = EscrowClient::new(&env, &contract_id);
let admin = Address::generate(&env);
env.mock_all_auths();
client.init(&admin);

// Clear all authorizations so the admin.require_auth() inside the
// entrypoint has nothing to satisfy it and the call is rejected.
let svc = Symbol::new(&env, "infer");
let owner = Address::generate(&env);
let description = String::from_str(&env, "GPU inference endpoint");
env.set_auths(&[]);
client.register_service_with_metadata(&svc, &description, &owner);
}
Loading