diff --git a/Cargo.lock b/Cargo.lock index 9fb0cb82..0d7abb41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,12 +14,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -50,22 +44,10 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c775f0d12169cba7aae4caeb547bb6a50781c7449a8aa53793827c9ec4abf488" dependencies = [ - "ark-ec 0.4.2", - "ark-ff 0.4.2", - "ark-serialize 0.4.2", - "ark-std 0.4.0", -] - -[[package]] -name = "ark-bls12-381" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" -dependencies = [ - "ark-ec 0.5.0", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", + "ark-ec", + "ark-ff", + "ark-serialize", + "ark-std", ] [[package]] @@ -85,34 +67,13 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" dependencies = [ - "ark-ff 0.4.2", - "ark-poly 0.4.2", - "ark-serialize 0.4.2", - "ark-std 0.4.0", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", "derivative", "hashbrown 0.13.2", - "itertools 0.10.5", - "num-traits", - "zeroize", -] - -[[package]] -name = "ark-ec" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" -dependencies = [ - "ahash", - "ark-ff 0.5.0", - "ark-poly 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "educe", - "fnv", - "hashbrown 0.15.3", - "itertools 0.13.0", - "num-bigint", - "num-integer", + "itertools", "num-traits", "zeroize", ] @@ -123,13 +84,13 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" dependencies = [ - "ark-ff-asm 0.4.2", - "ark-ff-macros 0.4.2", - "ark-serialize 0.4.2", - "ark-std 0.4.0", + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", "derivative", "digest", - "itertools 0.10.5", + "itertools", "num-bigint", "num-traits", "paste", @@ -137,26 +98,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ark-ff" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" -dependencies = [ - "ark-ff-asm 0.5.0", - "ark-ff-macros 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "arrayvec", - "digest", - "educe", - "itertools 0.13.0", - "num-bigint", - "num-traits", - "paste", - "zeroize", -] - [[package]] name = "ark-ff-asm" version = "0.4.2" @@ -167,16 +108,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "ark-ff-asm" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" -dependencies = [ - "quote", - "syn 2.0.101", -] - [[package]] name = "ark-ff-macros" version = "0.4.2" @@ -190,68 +121,27 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "ark-ff-macros" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "ark-poly" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" dependencies = [ - "ark-ff 0.4.2", - "ark-serialize 0.4.2", - "ark-std 0.4.0", + "ark-ff", + "ark-serialize", + "ark-std", "derivative", "hashbrown 0.13.2", ] -[[package]] -name = "ark-poly" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" -dependencies = [ - "ahash", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "educe", - "fnv", - "hashbrown 0.15.3", -] - [[package]] name = "ark-serialize" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ - "ark-serialize-derive 0.4.2", - "ark-std 0.4.0", - "digest", - "num-bigint", -] - -[[package]] -name = "ark-serialize" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" -dependencies = [ - "ark-serialize-derive 0.5.0", - "ark-std 0.5.0", - "arrayvec", + "ark-serialize-derive", + "ark-std", "digest", "num-bigint", ] @@ -267,17 +157,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "ark-serialize-derive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "ark-std" version = "0.4.0" @@ -288,22 +167,6 @@ dependencies = [ "rand 0.8.5", ] -[[package]] -name = "ark-std" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" -dependencies = [ - "num-traits", - "rand 0.8.5", -] - -[[package]] -name = "arrayvec" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" - [[package]] name = "autocfg" version = "1.4.0" @@ -678,18 +541,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "educe" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "either" version = "1.15.0" @@ -714,26 +565,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "enum-ordinalize" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" -dependencies = [ - "enum-ordinalize-derive", -] - -[[package]] -name = "enum-ordinalize-derive" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.4", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -866,9 +697,6 @@ name = "hashbrown" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" -dependencies = [ - "allocator-api2", -] [[package]] name = "heapless" @@ -977,15 +805,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.15" @@ -1519,7 +1338,7 @@ version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9336adeabcd6f636a4e0889c8baf494658ef5a3c4e7e227569acd2ce9091e85" dependencies = [ - "itertools 0.10.5", + "itertools", "proc-macro2", "quote", "syn 2.0.101", @@ -1599,10 +1418,10 @@ version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9766c5ad78e9d8ae10afbc076301f7d610c16407a1ebb230766dbe007a48725" dependencies = [ - "ark-bls12-381 0.4.0", - "ark-ec 0.4.2", - "ark-ff 0.4.2", - "ark-serialize 0.4.2", + "ark-bls12-381", + "ark-ec", + "ark-ff", + "ark-serialize", "curve25519-dalek", "ecdsa", "ed25519-dalek", @@ -1672,7 +1491,7 @@ version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0e6a1c5844257ce96f5f54ef976035d5bd0ee6edefaf9f5e0bcb8ea4b34228c" dependencies = [ - "itertools 0.10.5", + "itertools", "proc-macro2", "quote", "serde", @@ -1761,7 +1580,7 @@ checksum = "caa7114e2f031b6fbd30376e844f3c55f6daf56f6f9d33ce309f846ffced316d" dependencies = [ "darling", "heck", - "itertools 0.10.5", + "itertools", "macro-string", "proc-macro2", "quote", @@ -2018,17 +1837,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "syn" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "tempfile" version = "3.23.0" diff --git a/contracts/prompt-hash/src/contract.rs b/contracts/prompt-hash/src/contract.rs index ecda79af..ac082f8f 100644 --- a/contracts/prompt-hash/src/contract.rs +++ b/contracts/prompt-hash/src/contract.rs @@ -555,6 +555,40 @@ impl PromptHashTrait for PromptHashContract { royalty_amount, ); Ok(()) + fn revoke_access(env: Env, caller: Address, prompt_id: u128, buyer: Address) -> Result<(), Error> { + caller.require_auth(); + Storage::require_no_reentrancy(&env)?; + ensure(!Storage::is_paused(&env), Error::ContractIsPaused)?; + + let prompt = Storage::require_prompt(&env, prompt_id)?; + // Allow creator or moderator to revoke + let is_moderator = Storage::get_moderator_address(&env) == Some(caller.clone()); + ensure( + prompt.creator == caller || is_moderator, + Error::Unauthorized, + )?; + + Storage::revoke_purchase(&env, prompt_id, &buyer); + Events::emit_access_revoked(&env, prompt_id, buyer); + Ok(()) + } + + fn set_access_duration( + env: Env, + creator: Address, + prompt_id: u128, + duration_secs: u64, + ) -> Result<(), Error> { + creator.require_auth(); + Storage::require_no_reentrancy(&env)?; + ensure(!Storage::is_paused(&env), Error::ContractIsPaused)?; + + let prompt = Storage::require_prompt(&env, prompt_id)?; + ensure(prompt.creator == creator, Error::Unauthorized)?; + + Storage::set_access_duration(&env, prompt_id, duration_secs); + Events::emit_access_duration_set(&env, prompt_id, duration_secs); + Ok(()) } fn has_access(env: Env, user: Address, prompt_id: u128) -> Result { @@ -2061,12 +2095,19 @@ fn execute_buy( } } + let access_duration = Storage::get_access_duration(env, prompt_id); + let expires_at = if let Some(duration) = access_duration { + now.saturating_add(duration) + } else { + MAX_ACCESS_EXPIRY + }; + Storage::grant_purchase( env, &prompt, buyer, payment_amount_stroops, - MAX_ACCESS_EXPIRY, + expires_at, Settlement { buyer_amount: payment_amount_stroops, creator_amount, diff --git a/contracts/prompt-hash/src/events.rs b/contracts/prompt-hash/src/events.rs index 5c4c371a..86ea1fba 100644 --- a/contracts/prompt-hash/src/events.rs +++ b/contracts/prompt-hash/src/events.rs @@ -162,6 +162,20 @@ struct SubscriptionRenewed { pub renewal_count: u32, } +#[contractevent] +struct AccessDurationSet { + #[topic] + pub prompt_id: u128, + pub duration_secs: u64, +} + +#[contractevent] +struct AccessRevoked { + #[topic] + pub prompt_id: u128, + pub buyer: Address, +} + pub struct Events; impl Events { @@ -862,4 +876,16 @@ impl Events { } .publish(env); } + + pub fn emit_access_duration_set(env: &Env, prompt_id: u128, duration_secs: u64) { + AccessDurationSet { + prompt_id, + duration_secs, + } + .publish(env); + } + + pub fn emit_access_revoked(env: &Env, prompt_id: u128, buyer: Address) { + AccessRevoked { prompt_id, buyer }.publish(env); + } } diff --git a/contracts/prompt-hash/src/storage.rs b/contracts/prompt-hash/src/storage.rs index b8b21c57..ee1b868b 100644 --- a/contracts/prompt-hash/src/storage.rs +++ b/contracts/prompt-hash/src/storage.rs @@ -296,6 +296,26 @@ impl Storage { Self::extend_key_ttl(env, &key); } + pub fn revoke_purchase(env: &Env, prompt_id: u128, buyer: &Address) { + let key = DataKey::Purchase(prompt_id, buyer.clone()); + env.storage().persistent().remove(&key); + } + + pub fn set_access_duration(env: &Env, prompt_id: u128, duration_secs: u64) { + let key = DataKey::AccessDuration(prompt_id); + env.storage().persistent().set(&key, &duration_secs); + Self::extend_key_ttl(env, &key); + } + + pub fn get_access_duration(env: &Env, prompt_id: u128) -> Option { + let key = DataKey::AccessDuration(prompt_id); + let duration = env.storage().persistent().get(&key); + if env.storage().persistent().has(&key) { + Self::extend_key_ttl(env, &key); + } + duration + } + pub fn remove_purchase(env: &Env, prompt_id: u128, owner: &Address) { let key = DataKey::Purchase(prompt_id, owner.clone()); env.storage().persistent().remove(&key); diff --git a/contracts/prompt-hash/src/test.rs b/contracts/prompt-hash/src/test.rs index 72bcbf26..bf511e9a 100644 --- a/contracts/prompt-hash/src/test.rs +++ b/contracts/prompt-hash/src/test.rs @@ -6832,90 +6832,56 @@ fn test_lease_returns_insufficient_balance_when_wallet_unfunded() { #[test] -fn test_price_bounds() { +fn test_access_revocation() { let env = Env::default(); let context = setup(&env); let client = PromptHashContractClient::new(&env, &context.contract); let creator = Address::generate(&env); + let buyer = Address::generate(&env); - // Test setting price bounds - let min_price = Some(10_000); - let max_price = Some(500_000); - client.set_price_bounds(&context.admin, &context.admin_two, &min_price, &max_price); + let token = token::StellarAssetClient::new(&env, &context.xlm); + token.mint(&buyer, &100_000); - let bounds = client.get_price_bounds(); - assert_eq!(bounds, (min_price, max_price)); + let prompt_id = create_prompt(&env, &client, &creator, "Revokable Prompt", 10_000, &context.xlm); - // Try to create prompt below min price - should fail - let title = String::from_str(&env, "Title"); - let category = String::from_str(&env, "Category"); - let preview = String::from_str(&env, "Preview"); - let enc_prompt = String::from_str(&env, "Encrypted"); - let iv = String::from_str(&env, "IV"); - let wrapped_key = String::from_str(&env, "WrappedKey"); - let image_url = String::from_str(&env, "https://example.com/image.png"); + // Purchase the prompt + client.buy_prompt(&buyer, &prompt_id, &None, &10_000, &None); + assert!(client.has_access(&buyer, &prompt_id)); - let mut config = ListingConfig { - price: 5_000, - asset: context.xlm.clone(), - splits: Vec::new(&env), - expires_at: 0, - }; + // Revoke access + client.revoke_access(&creator, &prompt_id, &buyer); + assert!(!client.has_access(&buyer, &prompt_id)); +} + +#[test] +fn test_access_expiry_window() { + let env = Env::default(); + env.ledger().set_timestamp(100_000); + let context = setup(&env); + let client = PromptHashContractClient::new(&env, &context.contract); - let result = client.try_create_prompt( - &creator, - &image_url, - &title, - &category, - &preview, - &enc_prompt, - &iv, - &wrapped_key, - &BytesN::from_array(&env, &[0; 32]), - &config, - ); - assert_eq!(result, Err(Ok(Error::InvalidPrice))); + let creator = Address::generate(&env); + let buyer = Address::generate(&env); - // Try to create prompt above max price - should fail - config.price = 600_000; - let result = client.try_create_prompt( - &creator, - &image_url, - &title, - &category, - &preview, - &enc_prompt, - &iv, - &wrapped_key, - &BytesN::from_array(&env, &[0; 32]), - &config, - ); - assert_eq!(result, Err(Ok(Error::InvalidPrice))); + let token = token::StellarAssetClient::new(&env, &context.xlm); + token.mint(&buyer, &100_000); - // Try to create prompt within bounds - should succeed - config.price = 100_000; - let prompt_id = client.create_prompt( - &creator, - &image_url, - &title, - &category, - &preview, - &enc_prompt, - &iv, - &wrapped_key, - &BytesN::from_array(&env, &[0; 32]), - &config, - ); + let prompt_id = create_prompt(&env, &client, &creator, "Expiring Prompt", 10_000, &context.xlm); - // Try to update prompt above max price - should fail - let result = client.try_update_prompt_price(&creator, &prompt_id, &600_000); - assert_eq!(result, Err(Ok(Error::InvalidPrice))); + // Set 30 days access duration (30 * 24 * 60 * 60 = 2592000) + let duration_secs = 2_592_000; + client.set_access_duration(&creator, &prompt_id, &duration_secs); - // Try to update prompt below min price - should fail - let result = client.try_update_prompt_price(&creator, &prompt_id, &5_000); - assert_eq!(result, Err(Ok(Error::InvalidPrice))); + // Purchase the prompt + client.buy_prompt(&buyer, &prompt_id, &None, &10_000, &None); + assert!(client.has_access(&buyer, &prompt_id)); - // Try to update prompt within bounds - should succeed - client.update_prompt_price(&creator, &prompt_id, &200_000); + // Fast forward halfway through the duration + env.ledger().set_timestamp(100_000 + 1_000_000); + assert!(client.has_access(&buyer, &prompt_id)); + + // Fast forward past the duration + env.ledger().set_timestamp(100_000 + duration_secs + 1); + assert!(!client.has_access(&buyer, &prompt_id)); } diff --git a/contracts/prompt-hash/src/types.rs b/contracts/prompt-hash/src/types.rs index e93bc9eb..39d1e66d 100644 --- a/contracts/prompt-hash/src/types.rs +++ b/contracts/prompt-hash/src/types.rs @@ -493,6 +493,8 @@ pub trait PromptHashTrait { resale_price: i128, ) -> Result<(), Error>; + fn revoke_access(env: Env, caller: Address, prompt_id: u128, buyer: Address) -> Result<(), Error>; + fn set_access_duration(env: Env, creator: Address, prompt_id: u128, duration_secs: u64) -> Result<(), Error>; fn has_access(env: Env, user: Address, prompt_id: u128) -> Result; fn get_prompt(env: Env, prompt_id: u128) -> Result; fn get_all_prompts(env: Env) -> Result, Error>;