diff --git a/docs/canisters/deferred.md b/docs/canisters/deferred.md index c1ffbfc..4856165 100644 --- a/docs/canisters/deferred.md +++ b/docs/canisters/deferred.md @@ -17,7 +17,9 @@ - [increment\_contract\_value](#increment_contract_value) - [update\_contract\_buyers](#update_contract_buyers) - [update\_contract\_property](#update_contract_property) - - [admin\_set\_ekoke\_ledger\_canister](#admin_set_ekoke_reward_pool_canister) + - [update\_restricted\_contract\_property](#update_restricted_contract_property) + - [get\_restricted\_contract\_properties](#get_restricted_contract_properties) + - [admin\_set\_ekoke\_reward\_pool\_canister](#admin_set_ekoke_reward_pool_canister) - [admin\_set\_marketplace\_canister](#admin_set_marketplace_canister) - [admin\_set\_role](#admin_set_role) - [admin\_remove\_role](#admin_remove_role) @@ -63,6 +65,7 @@ A Contract is identified by the following properties - **reward**: the reward of EKOKE token given to a NFT buyer - **expiration**: contract expiration with syntax `YYYY-MM-DD`. - **properties**: contract properties and metadata +- **restricted_properties**: contract restricted properties ## Roles @@ -127,6 +130,18 @@ Change a contract property. Can be called by Agent, Custodian or seller. +### update_restricted_contract_property + +Create or change a contract restricted property. + +Can be called by Agent, Custodian or seller. + +### get_restricted_contract_properties + +Get the restricted contract properties. + +The properties returned are those only accessible to the caller + ### admin_set_ekoke_reward_pool_canister Update ekoke ledger canister principal diff --git a/integration-tests/tests/http/deferred.rs b/integration-tests/tests/http/deferred.rs index 0aadc5e..31b9d13 100644 --- a/integration-tests/tests/http/deferred.rs +++ b/integration-tests/tests/http/deferred.rs @@ -111,6 +111,7 @@ fn init_contract(env: &TestEnv) -> ID { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; // call register diff --git a/integration-tests/tests/inspect/deferred.rs b/integration-tests/tests/inspect/deferred.rs index 7e73fc6..7857e2b 100644 --- a/integration-tests/tests/inspect/deferred.rs +++ b/integration-tests/tests/inspect/deferred.rs @@ -94,6 +94,7 @@ fn test_should_inspect_update_contract_property() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; @@ -156,6 +157,7 @@ fn test_should_inspect_update_contract_property_is_not_authorized() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; @@ -204,6 +206,7 @@ fn test_should_inspect_update_contract_property_bad_key() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; @@ -246,6 +249,7 @@ fn test_should_inspect_update_contract_buyers() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; @@ -299,6 +303,7 @@ fn test_should_inspect_update_contract_buyers_not_seller() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; @@ -335,6 +340,7 @@ fn test_should_inspect_register_contract() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; @@ -369,6 +375,7 @@ fn test_should_inspect_register_contract_unauthorized() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; @@ -398,6 +405,7 @@ fn test_should_inspect_register_contract_no_sellers() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; @@ -430,6 +438,7 @@ fn test_should_inspect_register_contract_installments_not_multiple() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; @@ -462,6 +471,7 @@ fn test_should_inspect_register_contract_expired() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: Some("2021-01-01".to_string()), }; @@ -516,6 +526,7 @@ fn test_should_inspect_sign_contract() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; @@ -553,6 +564,7 @@ fn test_should_inspect_burn() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; diff --git a/integration-tests/tests/use_case/buy_marketplace_nft.rs b/integration-tests/tests/use_case/buy_marketplace_nft.rs index c30186b..4fb7ea9 100644 --- a/integration-tests/tests/use_case/buy_marketplace_nft.rs +++ b/integration-tests/tests/use_case/buy_marketplace_nft.rs @@ -128,6 +128,7 @@ fn setup_contract_marketplace(env: &TestEnv) -> ID { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; // call register diff --git a/integration-tests/tests/use_case/increment_contract_value.rs b/integration-tests/tests/use_case/increment_contract_value.rs index a3ac488..28ee8d5 100644 --- a/integration-tests/tests/use_case/increment_contract_value.rs +++ b/integration-tests/tests/use_case/increment_contract_value.rs @@ -45,6 +45,7 @@ fn test_as_seller_i_can_set_the_contract_buyers() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; diff --git a/integration-tests/tests/use_case/register_agency.rs b/integration-tests/tests/use_case/register_agency.rs index 133e765..b73f83b 100644 --- a/integration-tests/tests/use_case/register_agency.rs +++ b/integration-tests/tests/use_case/register_agency.rs @@ -25,6 +25,7 @@ fn test_should_register_agency_and_be_able_to_create_contract() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; diff --git a/integration-tests/tests/use_case/register_contract_buyers.rs b/integration-tests/tests/use_case/register_contract_buyers.rs index d86030d..387b4f8 100644 --- a/integration-tests/tests/use_case/register_contract_buyers.rs +++ b/integration-tests/tests/use_case/register_contract_buyers.rs @@ -24,6 +24,7 @@ fn test_as_seller_i_can_set_the_contract_buyers() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; diff --git a/integration-tests/tests/use_case/register_sell_contract.rs b/integration-tests/tests/use_case/register_sell_contract.rs index a2cf6c9..a6a080e 100644 --- a/integration-tests/tests/use_case/register_sell_contract.rs +++ b/integration-tests/tests/use_case/register_sell_contract.rs @@ -31,6 +31,7 @@ fn test_as_seller_i_can_register_a_sell_contract() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; diff --git a/integration-tests/tests/use_case/reserve_reward_pool.rs b/integration-tests/tests/use_case/reserve_reward_pool.rs index a64761b..a0c270b 100644 --- a/integration-tests/tests/use_case/reserve_reward_pool.rs +++ b/integration-tests/tests/use_case/reserve_reward_pool.rs @@ -30,6 +30,7 @@ fn test_should_reserve_a_reward_pool_on_ekoke() { "contract:address".to_string(), GenericValue::TextContent("via roma 10".to_string()), )], + restricted_properties: vec![], expiration: None, }; diff --git a/integration-tests/tests/use_case/update_contract_property.rs b/integration-tests/tests/use_case/update_contract_property.rs index b0acc8f..a323363 100644 --- a/integration-tests/tests/use_case/update_contract_property.rs +++ b/integration-tests/tests/use_case/update_contract_property.rs @@ -36,6 +36,7 @@ fn test_should_update_contract_property() { GenericValue::TextContent("Gino Valle".to_string()), ), ], + restricted_properties: vec![], expiration: None, }; diff --git a/src/declarations/deferred/deferred.did.d.ts b/src/declarations/deferred/deferred.did.d.ts index f2d088b..0e06062 100644 --- a/src/declarations/deferred/deferred.did.d.ts +++ b/src/declarations/deferred/deferred.did.d.ts @@ -53,6 +53,7 @@ export interface Contract { 'type' : ContractType, 'is_signed' : boolean, 'agency' : [] | [Agency], + 'restricted_properties' : Array<[string, RestrictedProperty]>, 'properties' : Array<[string, GenericValue]>, 'sellers' : Array, 'expiration' : [] | [string], @@ -65,6 +66,7 @@ export interface Contract { export interface ContractRegistration { 'value' : bigint, 'type' : ContractType, + 'restricted_properties' : Array<[string, RestrictedProperty]>, 'properties' : Array<[string, GenericValue]>, 'sellers' : Array, 'expiration' : [] | [string], @@ -115,7 +117,7 @@ export type GenericValue = { 'Nat64Content' : bigint } | { 'FloatContent' : number } | { 'Int16Content' : number } | { 'BlobContent' : Uint8Array | number[] } | - { 'NestedContent' : Vec } | + { 'NestedContent' : Array<[string, GenericValue]> } | { 'Principal' : Principal } | { 'TextContent' : string }; export interface HttpRequest { @@ -158,6 +160,13 @@ export type RejectionCode = { 'NoError' : null } | { 'Unknown' : null } | { 'SysFatal' : null } | { 'CanisterReject' : null }; +export interface RestrictedProperty { + 'value' : GenericValue, + 'access_list' : Array, +} +export type RestrictionLevel = { 'Buyer' : null } | + { 'Seller' : null } | + { 'Agent' : null }; export type Result = { 'Ok' : null } | { 'Err' : DeferredError }; export type Result_1 = { 'Ok' : bigint } | @@ -267,27 +276,6 @@ export interface TxEvent { 'details' : Array<[string, GenericValue]>, 'caller' : Principal, } -export type Vec = Array< - [ - string, - { 'Nat64Content' : bigint } | - { 'Nat32Content' : number } | - { 'BoolContent' : boolean } | - { 'Nat8Content' : number } | - { 'Int64Content' : bigint } | - { 'IntContent' : bigint } | - { 'NatContent' : bigint } | - { 'Nat16Content' : number } | - { 'Int32Content' : number } | - { 'Int8Content' : number } | - { 'FloatContent' : number } | - { 'Int16Content' : number } | - { 'BlobContent' : Uint8Array | number[] } | - { 'NestedContent' : Vec } | - { 'Principal' : Principal } | - { 'TextContent' : string }, - ] ->; export interface _SERVICE { 'admin_register_agency' : ActorMethod<[Principal, Agency], undefined>, 'admin_remove_role' : ActorMethod<[Principal, Role], Result>, @@ -301,6 +289,10 @@ export interface _SERVICE { 'cycles' : ActorMethod<[], bigint>, 'get_agencies' : ActorMethod<[], Array>, 'get_contract' : ActorMethod<[bigint], [] | [Contract]>, + 'get_restricted_contract_properties' : ActorMethod< + [bigint], + [] | [Array<[string, RestrictedProperty]>] + >, 'get_signed_contracts' : ActorMethod<[], Array>, 'get_token' : ActorMethod<[bigint], [] | [TokenInfo]>, 'get_unsigned_contracts' : ActorMethod<[], Array>, @@ -343,6 +335,10 @@ export interface _SERVICE { [bigint, string, GenericValue], Result >, + 'update_restricted_contract_property' : ActorMethod< + [bigint, string, RestrictedProperty], + Result + >, } export declare const idlFactory: IDL.InterfaceFactory; export declare const init: ({ IDL }: { IDL: IDL }) => IDL.Type[]; diff --git a/src/declarations/deferred/deferred.did.js b/src/declarations/deferred/deferred.did.js index ec8a8f6..a5ebf2a 100644 --- a/src/declarations/deferred/deferred.did.js +++ b/src/declarations/deferred/deferred.did.js @@ -1,5 +1,5 @@ export const idlFactory = ({ IDL }) => { - const Vec = IDL.Rec(); + const GenericValue = IDL.Rec(); const DeferredInitData = IDL.Record({ 'custodians' : IDL.Vec(IDL.Principal), 'ekoke_reward_pool_canister' : IDL.Principal, @@ -172,48 +172,34 @@ export const idlFactory = ({ IDL }) => { 'Sell' : IDL.Null, 'Financing' : IDL.Null, }); - Vec.fill( - IDL.Vec( - IDL.Tuple( - IDL.Text, - IDL.Variant({ - 'Nat64Content' : IDL.Nat64, - 'Nat32Content' : IDL.Nat32, - 'BoolContent' : IDL.Bool, - 'Nat8Content' : IDL.Nat8, - 'Int64Content' : IDL.Int64, - 'IntContent' : IDL.Int, - 'NatContent' : IDL.Nat, - 'Nat16Content' : IDL.Nat16, - 'Int32Content' : IDL.Int32, - 'Int8Content' : IDL.Int8, - 'FloatContent' : IDL.Float64, - 'Int16Content' : IDL.Int16, - 'BlobContent' : IDL.Vec(IDL.Nat8), - 'NestedContent' : Vec, - 'Principal' : IDL.Principal, - 'TextContent' : IDL.Text, - }), - ) - ) + GenericValue.fill( + IDL.Variant({ + 'Nat64Content' : IDL.Nat64, + 'Nat32Content' : IDL.Nat32, + 'BoolContent' : IDL.Bool, + 'Nat8Content' : IDL.Nat8, + 'Int64Content' : IDL.Int64, + 'IntContent' : IDL.Int, + 'NatContent' : IDL.Nat, + 'Nat16Content' : IDL.Nat16, + 'Int32Content' : IDL.Int32, + 'Int8Content' : IDL.Int8, + 'FloatContent' : IDL.Float64, + 'Int16Content' : IDL.Int16, + 'BlobContent' : IDL.Vec(IDL.Nat8), + 'NestedContent' : IDL.Vec(IDL.Tuple(IDL.Text, GenericValue)), + 'Principal' : IDL.Principal, + 'TextContent' : IDL.Text, + }) ); - const GenericValue = IDL.Variant({ - 'Nat64Content' : IDL.Nat64, - 'Nat32Content' : IDL.Nat32, - 'BoolContent' : IDL.Bool, - 'Nat8Content' : IDL.Nat8, - 'Int64Content' : IDL.Int64, - 'IntContent' : IDL.Int, - 'NatContent' : IDL.Nat, - 'Nat16Content' : IDL.Nat16, - 'Int32Content' : IDL.Int32, - 'Int8Content' : IDL.Int8, - 'FloatContent' : IDL.Float64, - 'Int16Content' : IDL.Int16, - 'BlobContent' : IDL.Vec(IDL.Nat8), - 'NestedContent' : Vec, - 'Principal' : IDL.Principal, - 'TextContent' : IDL.Text, + const RestrictionLevel = IDL.Variant({ + 'Buyer' : IDL.Null, + 'Seller' : IDL.Null, + 'Agent' : IDL.Null, + }); + const RestrictedProperty = IDL.Record({ + 'value' : GenericValue, + 'access_list' : IDL.Vec(RestrictionLevel), }); const Seller = IDL.Record({ 'principal' : IDL.Principal, @@ -225,6 +211,7 @@ export const idlFactory = ({ IDL }) => { 'type' : ContractType, 'is_signed' : IDL.Bool, 'agency' : IDL.Opt(Agency), + 'restricted_properties' : IDL.Vec(IDL.Tuple(IDL.Text, RestrictedProperty)), 'properties' : IDL.Vec(IDL.Tuple(IDL.Text, GenericValue)), 'sellers' : IDL.Vec(Seller), 'expiration' : IDL.Opt(IDL.Text), @@ -300,6 +287,7 @@ export const idlFactory = ({ IDL }) => { const ContractRegistration = IDL.Record({ 'value' : IDL.Nat64, 'type' : ContractType, + 'restricted_properties' : IDL.Vec(IDL.Tuple(IDL.Text, RestrictedProperty)), 'properties' : IDL.Vec(IDL.Tuple(IDL.Text, GenericValue)), 'sellers' : IDL.Vec(Seller), 'expiration' : IDL.Opt(IDL.Text), @@ -341,6 +329,11 @@ export const idlFactory = ({ IDL }) => { 'cycles' : IDL.Func([], [IDL.Nat], ['query']), 'get_agencies' : IDL.Func([], [IDL.Vec(Agency)], ['query']), 'get_contract' : IDL.Func([IDL.Nat], [IDL.Opt(Contract)], ['query']), + 'get_restricted_contract_properties' : IDL.Func( + [IDL.Nat], + [IDL.Opt(IDL.Vec(IDL.Tuple(IDL.Text, RestrictedProperty)))], + ['query'], + ), 'get_signed_contracts' : IDL.Func([], [IDL.Vec(IDL.Nat)], ['query']), 'get_token' : IDL.Func([IDL.Nat], [IDL.Opt(TokenInfo)], ['query']), 'get_unsigned_contracts' : IDL.Func([], [IDL.Vec(IDL.Nat)], ['query']), @@ -421,6 +414,11 @@ export const idlFactory = ({ IDL }) => { [Result], [], ), + 'update_restricted_contract_property' : IDL.Func( + [IDL.Nat, IDL.Text, RestrictedProperty], + [Result], + [], + ), }); }; export const init = ({ IDL }) => { diff --git a/src/deferred/deferred.did b/src/deferred/deferred.did index d0f34d7..64c6d75 100644 --- a/src/deferred/deferred.did +++ b/src/deferred/deferred.did @@ -53,6 +53,7 @@ type Contract = record { "type" : ContractType; is_signed : bool; agency : opt Agency; + restricted_properties : vec record { text; RestrictedProperty }; properties : vec record { text; GenericValue }; sellers : vec Seller; expiration : opt text; @@ -65,6 +66,7 @@ type Contract = record { type ContractRegistration = record { value : nat64; "type" : ContractType; + restricted_properties : vec record { text; RestrictedProperty }; properties : vec record { text; GenericValue }; sellers : vec Seller; expiration : opt text; @@ -121,7 +123,7 @@ type GenericValue = variant { FloatContent : float64; Int16Content : int16; BlobContent : vec nat8; - NestedContent : Vec; + NestedContent : vec record { text; GenericValue }; Principal : principal; TextContent : text; }; @@ -168,6 +170,11 @@ type RejectionCode = variant { SysFatal; CanisterReject; }; +type RestrictedProperty = record { + value : GenericValue; + access_list : vec RestrictionLevel; +}; +type RestrictionLevel = variant { Buyer; Seller; Agent }; type Result = variant { Ok; Err : DeferredError }; type Result_1 = variant { Ok : nat; Err : NftError }; type Result_2 = variant { Ok : bool; Err : NftError }; @@ -266,27 +273,6 @@ type TxEvent = record { details : vec record { text; GenericValue }; caller : principal; }; -type Vec = vec record { - text; - variant { - Nat64Content : nat64; - Nat32Content : nat32; - BoolContent : bool; - Nat8Content : nat8; - Int64Content : int64; - IntContent : int; - NatContent : nat; - Nat16Content : nat16; - Int32Content : int32; - Int8Content : int8; - FloatContent : float64; - Int16Content : int16; - BlobContent : vec nat8; - NestedContent : Vec; - Principal : principal; - TextContent : text; - }; -}; service : (DeferredInitData) -> { admin_register_agency : (principal, Agency) -> (); admin_remove_role : (principal, Role) -> (Result); @@ -300,6 +286,9 @@ service : (DeferredInitData) -> { cycles : () -> (nat) query; get_agencies : () -> (vec Agency) query; get_contract : (nat) -> (opt Contract) query; + get_restricted_contract_properties : (nat) -> ( + opt vec record { text; RestrictedProperty }, + ) query; get_signed_contracts : () -> (vec nat) query; get_token : (nat) -> (opt TokenInfo) query; get_unsigned_contracts : () -> (vec nat) query; @@ -336,4 +325,7 @@ service : (DeferredInitData) -> { transfer_from : (principal, principal, nat) -> (Result_1); update_contract_buyers : (nat, vec principal) -> (Result); update_contract_property : (nat, text, GenericValue) -> (Result); + update_restricted_contract_property : (nat, text, RestrictedProperty) -> ( + Result, + ); } \ No newline at end of file diff --git a/src/deferred/src/app.rs b/src/deferred/src/app.rs index 6bd0c07..65ee2fd 100644 --- a/src/deferred/src/app.rs +++ b/src/deferred/src/app.rs @@ -15,8 +15,9 @@ use async_trait::async_trait; use candid::{Nat, Principal}; use configuration::Configuration; use did::deferred::{ - Agency, Contract, ContractRegistration, DeferredError, DeferredInitData, DeferredResult, Role, - TokenError, TokenInfo, + Agency, Contract, ContractRegistration, DeferredError, DeferredInitData, DeferredResult, + RestrictedContractProperties, RestrictedProperty, RestrictionLevel, Role, TokenError, + TokenInfo, }; use did::ID; use dip721::{ @@ -113,7 +114,7 @@ impl Deferred { ContractStorage::update_contract_buyers(&contract_id, buyers) } - /// Update a contract property. Only the seller or an agent can call this function + /// Update a contract property. Only custodian,seller,agent can call this function pub fn update_contract_property( contract_id: ID, key: String, @@ -123,6 +124,51 @@ impl Deferred { ContractStorage::update_contract_property(&contract_id, key, value) } + /// Update a restricted contract property. Only custodian,seller,agent can call this function + pub fn update_restricted_contract_property( + contract_id: ID, + key: String, + value: RestrictedProperty, + ) -> DeferredResult<()> { + Inspect::inspect_update_contract_property(caller(), &contract_id, &key)?; + ContractStorage::update_restricted_contract_property(&contract_id, key, value) + } + + /// Get all the restricted contract properties based on the contract id and the caller access + pub fn get_restricted_contract_properties( + contract_id: ID, + ) -> Option { + let contract = ContractStorage::get_contract(&contract_id)?; + let mut caller_access_levels = vec![]; + let caller = caller(); + + if contract.buyers.contains(&caller) { + caller_access_levels.push(RestrictionLevel::Buyer); + } + if contract + .sellers + .iter() + .any(|seller| seller.principal == caller) + { + caller_access_levels.push(RestrictionLevel::Seller); + } + if Agents::get_agency_by_wallet(caller) == contract.agency { + caller_access_levels.push(RestrictionLevel::Agent); + } + + Some( + contract + .restricted_properties + .into_iter() + .filter(|(_, prop)| { + prop.access_list + .iter() + .any(|level| caller_access_levels.contains(level)) + }) + .collect(), + ) + } + /// Increment contract value. Only an admin or the associated agency can call this method pub async fn increment_contract_value( contract_id: ID, @@ -162,6 +208,7 @@ impl Deferred { id: next_contract_id.clone(), initial_value: data.value, properties: data.properties, + restricted_properties: data.restricted_properties, installments: data.installments, is_signed: false, r#type: data.r#type, @@ -634,6 +681,7 @@ mod test { currency: "EUR".to_string(), installments: 10, properties: vec![], + restricted_properties: vec![], r#type: did::deferred::ContractType::Financing, sellers: vec![Seller { principal: caller(), @@ -659,6 +707,7 @@ mod test { currency: "EUR".to_string(), installments: 10, properties: vec![], + restricted_properties: vec![], r#type: did::deferred::ContractType::Financing, sellers: vec![Seller { principal: caller(), @@ -1000,6 +1049,45 @@ mod test { assert_eq!(Agents::get_agency_by_wallet(wallet), Some(agency)); } + #[tokio::test] + async fn test_should_set_and_get_restricted_property() { + init_canister(); + let contract = ContractRegistration { + buyers: vec![caller()], + currency: "EUR".to_string(), + installments: 10, + properties: vec![], + restricted_properties: vec![], + r#type: did::deferred::ContractType::Financing, + sellers: vec![Seller { + principal: caller(), + quota: 100, + }], + value: 100, + expiration: Some("2048-01-01".to_string()), + }; + + assert_eq!(Deferred::register_contract(contract).unwrap(), 0_u64); + assert_eq!(Deferred::total_supply(), Nat::from(0_u64)); + assert_eq!(Deferred::get_unsigned_contracts(), vec![Nat::from(0_u64)]); + assert!(Deferred::sign_contract(0_u64.into()).await.is_ok()); + + assert!(Deferred::update_restricted_contract_property( + 0_u64.into(), + "contract:secret".to_string(), + RestrictedProperty { + value: GenericValue::TextContent("secret".to_string()), + access_list: vec![RestrictionLevel::Buyer], + } + ) + .is_ok()); + // get restricted properties + let restricted_properties = + Deferred::get_restricted_contract_properties(0_u64.into()).unwrap(); + + assert_eq!(restricted_properties.len(), 1); + } + fn init_canister() { Deferred::init(DeferredInitData { custodians: vec![caller()], diff --git a/src/deferred/src/app/storage/contracts.rs b/src/deferred/src/app/storage/contracts.rs index d641786..43607ed 100644 --- a/src/deferred/src/app/storage/contracts.rs +++ b/src/deferred/src/app/storage/contracts.rs @@ -1,5 +1,7 @@ use candid::{Nat, Principal}; -use did::deferred::{Contract, DeferredError, DeferredResult, Seller, Token, TokenError}; +use did::deferred::{ + Contract, DeferredError, DeferredResult, RestrictedProperty, Seller, Token, TokenError, +}; use did::{StorableNat, ID}; use dip721::{GenericValue, TokenIdentifier, TokenMetadata}; use itertools::Itertools; @@ -311,6 +313,28 @@ impl ContractStorage { }) } + /// Update restricted contract property + pub fn update_restricted_contract_property( + contract_id: &ID, + key: String, + value: RestrictedProperty, + ) -> DeferredResult<()> { + with_contract_mut(contract_id, |contract| { + let mut found = false; + for (k, v) in &mut contract.restricted_properties { + if k == &key { + *v = value.clone(); + found = true; + break; + } + } + if !found { + contract.restricted_properties.push((key, value)); + } + Ok(()) + }) + } + /// Update the contract buyers pub fn update_contract_buyers(contract_id: &ID, buyers: Vec) -> DeferredResult<()> { with_contract_mut(contract_id, |contract| { @@ -446,7 +470,7 @@ impl ContractStorage { mod test { use candid::Principal; - use did::deferred::Seller; + use did::deferred::{RestrictionLevel, Seller}; use pretty_assertions::assert_eq; use super::*; @@ -795,6 +819,7 @@ mod test { "contract:city".to_string(), dip721::GenericValue::TextContent("Rome".to_string()), )], + restricted_properties: vec![], agency: None, expiration: None, }; @@ -992,4 +1017,64 @@ mod test { GenericValue::TextContent("Trieste".to_string()) ); } + + #[test] + fn test_should_update_restricted_contract_property() { + let contract = with_mock_contract(1, 1, |contract| { + contract.restricted_properties.push(( + "contract:address".to_string(), + RestrictedProperty { + access_list: vec![RestrictionLevel::Seller], + value: dip721::GenericValue::TextContent("Rome".to_string()), + }, + )); + }); + assert!(ContractStorage::insert_contract(contract).is_ok()); + + assert!(ContractStorage::update_restricted_contract_property( + &1_u64.into(), + "contract:address".to_string(), + RestrictedProperty { + access_list: vec![RestrictionLevel::Agent, RestrictionLevel::Seller], + value: dip721::GenericValue::TextContent("Milan".to_string()), + }, + ) + .is_ok()); + assert_eq!( + ContractStorage::get_contract(&1_u64.into()) + .unwrap() + .restricted_properties + .iter() + .find(|(k, _)| k == "contract:address") + .unwrap() + .1, + RestrictedProperty { + access_list: vec![RestrictionLevel::Agent, RestrictionLevel::Seller], + value: GenericValue::TextContent("Milan".to_string()) + } + ); + + assert!(ContractStorage::update_restricted_contract_property( + &1_u64.into(), + "contract:addressLong".to_string(), + RestrictedProperty { + access_list: vec![RestrictionLevel::Agent, RestrictionLevel::Seller], + value: GenericValue::TextContent("Milan".to_string()) + } + ) + .is_ok()); + assert_eq!( + ContractStorage::get_contract(&1_u64.into()) + .unwrap() + .restricted_properties + .iter() + .find(|(k, _)| k == "contract:addressLong") + .unwrap() + .1, + RestrictedProperty { + access_list: vec![RestrictionLevel::Agent, RestrictionLevel::Seller], + value: GenericValue::TextContent("Milan".to_string()) + } + ); + } } diff --git a/src/deferred/src/app/test_utils.rs b/src/deferred/src/app/test_utils.rs index 0e3615e..831244e 100644 --- a/src/deferred/src/app/test_utils.rs +++ b/src/deferred/src/app/test_utils.rs @@ -1,5 +1,5 @@ use candid::Principal; -use did::deferred::{Agency, Contract, Seller, Token}; +use did::deferred::{Agency, Contract, RestrictedProperty, RestrictionLevel, Seller, Token}; use did::ID; use dip721::TokenIdentifier; @@ -45,6 +45,13 @@ pub fn mock_contract(id: u64, installments: u64) -> Contract { "contract:city".to_string(), dip721::GenericValue::TextContent("Rome".to_string()), )], + restricted_properties: vec![( + "contract:seller_address".to_string(), + RestrictedProperty { + access_list: vec![RestrictionLevel::Agent, RestrictionLevel::Seller], + value: dip721::GenericValue::TextContent("Via Roma 123".to_string()), + }, + )], agency: Some(mock_agency()), expiration: None, } diff --git a/src/deferred/src/lib.rs b/src/deferred/src/lib.rs index e22b23f..ab8a63d 100644 --- a/src/deferred/src/lib.rs +++ b/src/deferred/src/lib.rs @@ -6,7 +6,8 @@ use candid::{candid_method, Nat, Principal}; use did::deferred::{ - Agency, Contract, ContractRegistration, DeferredInitData, DeferredResult, Role, TokenInfo, + Agency, Contract, ContractRegistration, DeferredInitData, DeferredResult, RestrictedProperty, + Role, TokenInfo, }; use did::{HttpRequest, HttpResponse, ID}; use dip721::{Dip721 as _, GenericValue, TokenIdentifier}; @@ -95,6 +96,24 @@ pub fn update_contract_property( Deferred::update_contract_property(contract_id, key, value) } +#[update] +#[candid_method(update)] +pub fn update_restricted_contract_property( + contract_id: ID, + key: String, + value: RestrictedProperty, +) -> DeferredResult<()> { + Deferred::update_restricted_contract_property(contract_id, key, value) +} + +#[query] +#[candid_method(query)] +pub fn get_restricted_contract_properties( + contract_id: ID, +) -> Option> { + Deferred::get_restricted_contract_properties(contract_id) +} + #[query] #[candid_method(query)] pub fn get_unsigned_contracts() -> Vec { diff --git a/src/did/src/deferred.rs b/src/did/src/deferred.rs index 3ccea35..8aef794 100644 --- a/src/did/src/deferred.rs +++ b/src/did/src/deferred.rs @@ -9,7 +9,8 @@ pub type DeferredResult = Result; pub use self::canister::{DeferredInitData, Role, Roles, StorableTxEvent}; pub use self::contract::{ Agency, Continent, Contract, ContractProperties, ContractRegistration, ContractType, - GenericValue, Seller, Token, TokenIdentifier, TokenInfo, ID, + GenericValue, RestrictedContractProperties, RestrictedProperty, RestrictionLevel, Seller, + Token, TokenIdentifier, TokenInfo, ID, }; pub use self::error::{ConfigurationError, DeferredError, TokenError}; @@ -93,6 +94,13 @@ mod test { "Rome".to_string(), GenericValue::TextContent("Rome".to_string()), )], + restricted_properties: vec![( + "Secret".to_string(), + RestrictedProperty { + access_list: vec![RestrictionLevel::Agent], + value: GenericValue::TextContent("Secret".to_string()), + }, + )], agency: Some(Agency { name: "Agency".to_string(), address: "Address".to_string(), diff --git a/src/did/src/deferred/contract.rs b/src/did/src/deferred/contract.rs index 5157763..fd8fb51 100644 --- a/src/did/src/deferred/contract.rs +++ b/src/did/src/deferred/contract.rs @@ -42,6 +42,8 @@ pub struct Contract { pub currency: String, /// Data associated to the contract pub properties: ContractProperties, + /// Restricted data associated to the contract + pub restricted_properties: RestrictedContractProperties, /// Agency data pub agency: Option, /// Contract expiration date YYYY-MM-DD @@ -79,6 +81,27 @@ impl Storable for Contract { /// A list of properties associated to a contract pub type ContractProperties = Vec<(String, GenericValue)>; +/// A list of restricted properties associated to a contract +pub type RestrictedContractProperties = Vec<(String, RestrictedProperty)>; + +/// A restricted property, which defines the access level to the property and its value +#[derive(Clone, Debug, CandidType, PartialEq, Serialize, Deserialize)] +pub struct RestrictedProperty { + pub access_list: Vec, + pub value: GenericValue, +} + +/// A variant which defines the restriction level for a contract property +#[derive(Clone, Debug, CandidType, PartialEq, Eq, Serialize, Deserialize)] +pub enum RestrictionLevel { + /// Seller can access the property + Seller, + /// Buyer can access the property + Buyer, + /// Agent can access the property + Agent, +} + /// A variant which defines the contract type #[derive(Clone, Debug, CandidType, Serialize, Deserialize)] pub enum ContractType { @@ -106,4 +129,5 @@ pub struct ContractRegistration { pub installments: u64, pub expiration: Option, pub properties: ContractProperties, + pub restricted_properties: RestrictedContractProperties, } diff --git a/src/marketplace/src/client/deferred.rs b/src/marketplace/src/client/deferred.rs index 2dca2ba..0a3dce2 100644 --- a/src/marketplace/src/client/deferred.rs +++ b/src/marketplace/src/client/deferred.rs @@ -75,6 +75,7 @@ impl DeferredClient { value: 400_000, currency: "EUR".to_string(), properties: vec![], + restricted_properties: vec![], agency: None, expiration: None, },