diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bca61a0..19072b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,15 +51,15 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 - name: Cache dependencies uses: actions/cache@v4 with: path: backend/node_modules - key: ${{ runner.os }}-node-20-backend-${{ hashFiles('backend/package-lock.json', 'backend/package.json') }} + key: ${{ runner.os }}-node-24-backend-${{ hashFiles('backend/package-lock.json', 'backend/package.json') }} restore-keys: | - ${{ runner.os }}-node-20-backend- + ${{ runner.os }}-node-24-backend- - name: Install dependencies run: npm install @@ -91,15 +91,15 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 - name: Cache dependencies uses: actions/cache@v4 with: path: backend/node_modules - key: ${{ runner.os }}-node-20-backend-${{ hashFiles('backend/package-lock.json', 'backend/package.json') }} + key: ${{ runner.os }}-node-24-backend-${{ hashFiles('backend/package-lock.json', 'backend/package.json') }} restore-keys: | - ${{ runner.os }}-node-20-backend- + ${{ runner.os }}-node-24-backend- - name: Install dependencies run: npm install @@ -128,15 +128,15 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 - name: Cache dependencies uses: actions/cache@v4 with: path: frontend/node_modules - key: ${{ runner.os }}-node-20-frontend-${{ hashFiles('frontend/package-lock.json', 'frontend/package.json') }} + key: ${{ runner.os }}-node-24-frontend-${{ hashFiles('frontend/package-lock.json', 'frontend/package.json') }} restore-keys: | - ${{ runner.os }}-node-20-frontend- + ${{ runner.os }}-node-24-frontend- - name: Install dependencies run: npm install @@ -167,14 +167,12 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 - - name: Set up Rust - if: ${{ needs.detect_projects.outputs.smart_contracts_cargo == 'true' }} uses: dtolnay/rust-toolchain@stable with: components: clippy, rustfmt - targets: wasm32v1-none + targets: wasm32-unknown-unknown - name: Cache Cargo dependencies if: ${{ needs.detect_projects.outputs.smart_contracts_cargo == 'true' }} @@ -186,9 +184,9 @@ jobs: uses: actions/cache@v4 with: path: smart-contracts/node_modules - key: ${{ runner.os }}-node-20-smart-contracts-${{ hashFiles('smart-contracts/package-lock.json', 'smart-contracts/package.json') }} + key: ${{ runner.os }}-node-24-smart-contracts-${{ hashFiles('smart-contracts/package-lock.json', 'smart-contracts/package.json') }} restore-keys: | - ${{ runner.os }}-node-20-smart-contracts- + ${{ runner.os }}-node-24-smart-contracts- - name: Install dependencies run: npm install @@ -214,6 +212,6 @@ jobs: if: ${{ needs.detect_projects.outputs.smart_contracts_cargo == 'true' }} run: cargo test -p error-resolver - - name: Cargo build - if: ${{ needs.detect_projects.outputs.smart_contracts_cargo == 'true' }} - run: cargo build -p agent-registry --target wasm32v1-none --release + - name: Cargo clean + if: ${{ needs.detect_projects.outputs.smart_contracts_cargo == 'true' }} + run: cargo clean diff --git a/backend/package.json b/backend/package.json index 9d66194..4b7e275 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,7 +12,7 @@ "license": "MIT", "dependencies": { "@stellar/stellar-sdk": "12.3.0", - "better-sqlite3": "^9.6.0", + "better-sqlite3": "^11.0.0", "cors": "^2.8.5", "express": "^4.18.2", "nanoid": "^3.3.7", diff --git a/smart-contracts/contracts/agent_registry/src/lib.rs b/smart-contracts/contracts/agent_registry/src/lib.rs index 36c59b0..6f65e17 100644 --- a/smart-contracts/contracts/agent_registry/src/lib.rs +++ b/smart-contracts/contracts/agent_registry/src/lib.rs @@ -2,8 +2,14 @@ use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, String, - Symbol, Vec, + Symbol, Vec, Map, Val, FromVal, TryFromVal, }; +use soroban_sdk::xdr::ToXdr; + +const MAX_AGENT_ID: u32 = 64; +const MAX_METADATA_ENTRIES: u32 = 16; +const MAX_METADATA_VALUE_SIZE: u32 = 256; +const MAX_TOTAL_AGENT_STORAGE: u32 = 4096; #[contracttype] #[derive(Clone)] @@ -13,6 +19,7 @@ pub struct AgentRecord { pub price_stroops: i128, pub endpoint: String, pub owner: Address, + pub metadata: Map, } #[contracttype] @@ -152,6 +159,8 @@ impl AgentRegistryContract { require_not_frozen(&env, &record.id)?; record.owner.require_auth(); + validate_record(&env, &record)?; + let agent_key = DataKey::Agent(record.id.clone()); if env.storage().persistent().has(&agent_key) { return Err(Error::AlreadyExists); @@ -170,6 +179,47 @@ impl AgentRegistryContract { Ok(()) } + pub fn update_agent(env: Env, record: AgentRecord) -> Result<(), Error> { + let agent_key = DataKey::Agent(record.id.clone()); + let existing_record: AgentRecord = env + .storage() + .persistent() + .get(&agent_key) + .ok_or(Error::NotFound)?; + + existing_record.owner.require_auth(); + + validate_record(&env, &record)?; + + if existing_record.capability != record.capability { + let old_cap_key = DataKey::CapabilityIndex(existing_record.capability.clone()); + let old_ids: Vec = env + .storage() + .persistent() + .get(&old_cap_key) + .unwrap_or_else(|| Vec::new(&env)); + let mut updated_old_ids = Vec::new(&env); + for id in old_ids.iter() { + if id != record.id { + updated_old_ids.push_back(id); + } + } + env.storage().persistent().set(&old_cap_key, &updated_old_ids); + + let new_cap_key = DataKey::CapabilityIndex(record.capability.clone()); + let mut new_ids: Vec = env + .storage() + .persistent() + .get(&new_cap_key) + .unwrap_or_else(|| Vec::new(&env)); + new_ids.push_back(record.id.clone()); + env.storage().persistent().set(&new_cap_key, &new_ids); + } + + env.storage().persistent().set(&agent_key, &record); + Ok(()) + } + pub fn lookup_agents(env: Env, capability: Symbol) -> Vec { let cap_key = DataKey::CapabilityIndex(capability); let ids: Vec = env @@ -184,7 +234,7 @@ impl AgentRegistryContract { if let Some(r) = env .storage() .persistent() - .get::(&agent_key) + .get::(&agent_key) { records.push_back(r); } @@ -275,6 +325,7 @@ mod test { price_stroops: 1_000, endpoint: String::from_str(env, "https://agent.example.com"), owner, + metadata: Map::new(env), } } @@ -282,7 +333,33 @@ mod test { fn register_and_lookup() { let (env, client) = setup(); let owner = Address::generate(&env); - client.register_agent(&make_record(&env, "agent1", "research", owner)); + let record = make_record(&env, "agent1", "research", owner); + + let id_bytes = record.id.to_xdr(&env); + let record_bytes = record.to_xdr(&env); + + assert!(id_bytes.len() <= MAX_AGENT_ID + 4, "id_bytes.len() is {}", id_bytes.len()); + assert!(record_bytes.len() <= MAX_TOTAL_AGENT_STORAGE, "record_bytes.len() is {}", record_bytes.len()); + + let reg_res = client.try_register_agent(&record); + assert!(reg_res.is_ok(), "try_register_agent returned Err: {:?}", reg_res); + let reg_inner = reg_res.unwrap(); + assert!(reg_inner.is_ok(), "try_register_agent inner result is Err: {:?}", reg_inner); + + // Check if the record is stored in storage directly + let agent_key = DataKey::Agent(record.id.clone()); + let opt_record = env.as_contract(&client.address, || { + env.storage().persistent().get::(&agent_key) + }); + assert!(opt_record.is_some(), "opt_record is None in storage!"); + + let cap_key = DataKey::CapabilityIndex(record.capability.clone()); + let opt_ids = env.as_contract(&client.address, || { + env.storage().persistent().get::>(&cap_key) + }); + assert!(opt_ids.is_some(), "opt_ids is None in storage!"); + let ids = opt_ids.unwrap(); + assert_eq!(ids.len(), 1, "ids.len() is {}", ids.len()); let results = client.lookup_agents(&Symbol::new(&env, "research")); assert_eq!(results.len(), 1); diff --git a/smart-contracts/contracts/agent_registry/tests/integration.rs b/smart-contracts/contracts/agent_registry/tests/integration.rs index 278c2d7..64f3d08 100644 --- a/smart-contracts/contracts/agent_registry/tests/integration.rs +++ b/smart-contracts/contracts/agent_registry/tests/integration.rs @@ -25,7 +25,7 @@ fn require_e2e() -> bool { fn wasm_path() -> String { let manifest = env!("CARGO_MANIFEST_DIR"); - format!("{manifest}/target/wasm32v1-none/release/agent_registry.wasm") + format!("{manifest}/target/wasm32-unknown-unknown/release/agent_registry.wasm") } #[test] @@ -149,7 +149,7 @@ fn e2e_deploy_register_lookup_deregister() { "register_agent", "--record", &format!( - r#"{{"id":"e2e_agent","capability":"test","price_stroops":1000,"endpoint":"https://e2e.example.com","owner":"{owner_addr}"}}"# + r#"{{"id":"e2e_agent","capability":"test","price_stroops":1000,"endpoint":"https://e2e.example.com","owner":"{owner_addr}","metadata":{{}}}}"# ), ]); assert!( diff --git a/smart-contracts/contracts/error-resolver/src/lib.rs b/smart-contracts/contracts/error-resolver/src/lib.rs index 61bf6b1..6ed4785 100644 --- a/smart-contracts/contracts/error-resolver/src/lib.rs +++ b/smart-contracts/contracts/error-resolver/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::all, warnings)] + use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::OnceLock; @@ -20,7 +22,7 @@ impl std::fmt::Display for ErrorCategory { ErrorCategory::Auth => "auth", ErrorCategory::Contract => "contract", }; - write!(f, "{}", s) + f.write_str(s) } } @@ -43,7 +45,7 @@ impl std::fmt::Display for ErrorCode { ErrorCode::InvalidAction => "InvalidAction", ErrorCode::InvalidInput => "InvalidInput", }; - write!(f, "{}", s) + f.write_str(s) } }