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 @@ -6,6 +6,13 @@ Soroban smart contracts for the AgentPay protocol: escrow, usage recording, and

- **escrow** — Records usage and supports settlement logic for machine-to-machine payments.

### Service metadata vs. registration

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.

## Prerequisites

- [Rust](https://rustup.rs/) (stable, with `rustfmt`)
Expand Down
20 changes: 20 additions & 0 deletions contracts/escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,26 @@ impl Escrow {
);
}

/// Admin-gated. Remove a service's metadata (description + owner).
/// Idempotent — clearing an absent entry is a no-op. After clearing,
/// `get_service_metadata` reads back `None`. Registration and usage
/// history live in independent slots and are untouched. Emits
/// `meta_clr(service_id)` (topic shortened to satisfy the 9-char
/// `symbol_short!` limit).
pub fn clear_service_metadata(env: Env, service_id: Symbol) {
let admin: Address = env
.storage()
.persistent()
.get(&DataKey::Admin)
.unwrap_or_else(|| panic_with_error!(&env, EscrowError::NotInitialized));
admin.require_auth();
env.storage()
.persistent()
.remove(&DataKey::ServiceMetadata(service_id.clone()));
env.events()
.publish((symbol_short!("meta_clr"),), service_id);
}

/// Read the on-chain schema version, or `1` (the implicit
/// pre-migration default) if absent.
pub fn get_schema_version(env: Env) -> u32 {
Expand Down
42 changes: 41 additions & 1 deletion contracts/escrow/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#![allow(deprecated)]

use super::*;
use soroban_sdk::{testutils::Address as _, Address, Symbol};
use soroban_sdk::{testutils::Address as _, Address, String, Symbol};

fn setup_initialized(env: &Env) -> (EscrowClient<'_>, Address) {
env.mock_all_auths();
Expand Down Expand Up @@ -248,3 +248,43 @@ fn test_record_usage_rejects_zero_requests() {
let service_id = Symbol::new(&env, "weather_api");
client.record_usage(&agent, &service_id, &0u32);
}

#[test]
fn test_clear_service_metadata_removes_entry() {
let env = Env::default();
let (client, _admin) = setup_initialized(&env);
let svc = Symbol::new(&env, "infer");
let owner = Address::generate(&env);
let desc = String::from_str(&env, "inference service");
client.set_service_metadata(&svc, &desc, &owner);
assert!(client.get_service_metadata(&svc).is_some());

client.clear_service_metadata(&svc);
assert!(client.get_service_metadata(&svc).is_none());
}

#[test]
fn test_clear_service_metadata_is_idempotent() {
let env = Env::default();
let (client, _admin) = setup_initialized(&env);
let svc = Symbol::new(&env, "never_set");
// Clearing a never-set entry is a no-op (no panic).
client.clear_service_metadata(&svc);
assert!(client.get_service_metadata(&svc).is_none());
}

#[test]
fn test_clear_service_metadata_leaves_registration_untouched() {
let env = Env::default();
let (client, _admin) = setup_initialized(&env);
let svc = Symbol::new(&env, "infer");
let owner = Address::generate(&env);
let desc = String::from_str(&env, "inference service");
client.register_service(&svc);
client.set_service_metadata(&svc, &desc, &owner);

client.clear_service_metadata(&svc);

assert!(client.get_service_metadata(&svc).is_none());
assert!(client.is_service_registered(&svc));
}
Loading