diff --git a/docs/canisters/deferred.md b/docs/canisters/deferred.md index 319896c..6595ad6 100644 --- a/docs/canisters/deferred.md +++ b/docs/canisters/deferred.md @@ -19,6 +19,7 @@ - [update\_contract\_property](#update_contract_property) - [update\_restricted\_contract\_property](#update_restricted_contract_property) - [get\_restricted\_contract\_properties](#get_restricted_contract_properties) + - [withdraw\_contract\_deposit](#withdraw_contract_deposit) - [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) @@ -59,7 +60,8 @@ A Contract is identified by the following properties - **currency**: the currency used to represent the value - **agency**: the agency which has created the contract - **sellers**: the contract sellers. Cannot be empty -- **buyers**: the contract buyers. Cannot be empty +- **buyers**: the contract buyers. Cannot be empty. It also contains the deposit account +- **deposit**: buyer deposit amount (FIAT and ICP) - **is_signed**: if signed the contract tokens can be sold. The token must be signed by custodians (or DAO) - **type**: the contract type (Sell / Funding) - **reward**: the reward of EKOKE token given to a NFT buyer @@ -142,6 +144,18 @@ Get the restricted contract properties. The properties returned are those only accessible to the caller +### withdraw_contract_deposit + +Update endpoint called by the seller once all the NFTs have been bought by the contract buyers. + +The seller provides the contract id he wants to withdraw for and an optional ICRC subaccount. + +If the all the NFTs have been paid by the buyer, the deferred canister will transfer the deposit amount to the caller. + +The seller will receive only `deposit.value_icp / seller.quota` its part of the deposit. + +Each seller must call this method to withdraw their quota of the contract + ### admin_set_ekoke_reward_pool_canister Update ekoke ledger canister principal diff --git a/integration-tests/src/client/deferred.rs b/integration-tests/src/client/deferred.rs index e498de0..aba99b9 100644 --- a/integration-tests/src/client/deferred.rs +++ b/integration-tests/src/client/deferred.rs @@ -2,6 +2,7 @@ use candid::{Encode, Nat, Principal}; use did::deferred::{Agency, Contract, ContractRegistration, DeferredResult, TokenInfo}; use did::ID; use dip721_rs::{GenericValue, NftError, TokenIdentifier, TokenMetadata}; +use icrc::icrc1::account::Subaccount; use crate::actor::{admin, alice}; use crate::TestEnv; @@ -53,6 +54,25 @@ impl<'a> DeferredClient<'a> { res } + pub fn withdraw_contract_deposit( + &self, + caller: Principal, + contract_id: ID, + subaccount: Option, + ) -> DeferredResult<()> { + let res: DeferredResult<()> = self + .env + .update( + self.env.deferred_id, + caller, + "withdraw_contract_deposit", + Encode!(&contract_id, &subaccount).unwrap(), + ) + .unwrap(); + + res + } + pub fn update_contract_buyers( &self, caller: Principal, diff --git a/integration-tests/tests/use_case/mod.rs b/integration-tests/tests/use_case/mod.rs index 7573596..200af7a 100644 --- a/integration-tests/tests/use_case/mod.rs +++ b/integration-tests/tests/use_case/mod.rs @@ -7,3 +7,4 @@ mod register_contract_buyers; mod register_sell_contract; mod reserve_reward_pool; mod update_contract_property; +mod withdraw_contract_deposit; diff --git a/integration-tests/tests/use_case/register_contract.rs b/integration-tests/tests/use_case/register_contract.rs new file mode 100644 index 0000000..920b723 --- /dev/null +++ b/integration-tests/tests/use_case/register_contract.rs @@ -0,0 +1,64 @@ +use did::deferred::{Buyers, ContractRegistration, ContractType, Deposit, GenericValue, Seller}; +use icrc::icrc1::account::Account; +use integration_tests::actor::{admin, alice, bob}; +use integration_tests::client::{DeferredClient, IcrcLedgerClient}; +use integration_tests::TestEnv; +use pretty_assertions::assert_eq; + +#[test] +#[serial_test::serial] +fn test_as_agency_i_can_register_contract() { + let env = TestEnv::init(); + let deferred_client = DeferredClient::from(&env); + + let registration_data = ContractRegistration { + r#type: ContractType::Sell, + sellers: vec![Seller { + principal: alice(), + quota: 100, + }], + buyers: Buyers { + principals: vec![bob()], + deposit_account: Account::from(alice()), + }, + deposit: Deposit { + value_fiat: 20_000, + value_icp: 100, + }, + value: 400_000, + currency: "EUR".to_string(), + installments: 400_000 / 100, + properties: vec![( + "contract:address".to_string(), + GenericValue::TextContent("via roma 10".to_string()), + )], + restricted_properties: vec![], + expiration: None, + }; + let deposit_value_icp = registration_data.deposit.value_icp; + // approve deposit + crate::helper::contract_deposit( + &env, + registration_data.buyers.deposit_account, + deposit_value_icp, + ); + + // call register + let contract_id = deferred_client + .register_contract(admin(), registration_data) + .unwrap(); + + // sign contract + let res = deferred_client.sign_contract(contract_id.clone()); + assert!(res.is_ok()); + + // verify deposit + let icp_ledger_client = IcrcLedgerClient::new(env.icp_ledger_id, &env); + let subaccount = crate::helper::contract_subaccount(&contract_id); + + let current_canister_balance = icp_ledger_client.icrc1_balance_of(Account { + owner: env.deferred_id, + subaccount: Some(subaccount), + }); + assert_eq!(current_canister_balance, deposit_value_icp); +} diff --git a/integration-tests/tests/use_case/withdraw_contract_deposit.rs b/integration-tests/tests/use_case/withdraw_contract_deposit.rs new file mode 100644 index 0000000..66aced3 --- /dev/null +++ b/integration-tests/tests/use_case/withdraw_contract_deposit.rs @@ -0,0 +1,107 @@ +use did::deferred::{Buyers, ContractRegistration, ContractType, Deposit, GenericValue, Seller}; +use icrc::icrc1::account::Account; +use integration_tests::actor::{admin, alice, bob, charlie, charlie_account}; +use integration_tests::client::{DeferredClient, IcrcLedgerClient, MarketplaceClient}; +use integration_tests::TestEnv; +use pretty_assertions::assert_eq; + +#[test] +#[serial_test::serial] +fn test_as_seller_i_should_withdraw_contract_deposit_after_being_paid() { + let env = TestEnv::init(); + let deferred_client = DeferredClient::from(&env); + let marketplace_client = MarketplaceClient::from(&env); + let icp_ledger_client = IcrcLedgerClient::new(env.icp_ledger_id, &env); + + let registration_data = ContractRegistration { + r#type: ContractType::Sell, + sellers: vec![ + Seller { + principal: alice(), + quota: 60, + }, + Seller { + principal: bob(), + quota: 40, + }, + ], + buyers: Buyers { + principals: vec![charlie()], + deposit_account: Account::from(charlie()), + }, + deposit: Deposit { + value_fiat: 20_000, + value_icp: 4_000 * 100_000_000, // 4_000 ICP + }, + value: 400_000, + currency: "EUR".to_string(), + installments: 2, + properties: vec![( + "contract:address".to_string(), + GenericValue::TextContent("via roma 10".to_string()), + )], + restricted_properties: vec![], + expiration: None, + }; + let deposit_value_icp = registration_data.deposit.value_icp; + // approve deposit + crate::helper::contract_deposit( + &env, + registration_data.buyers.deposit_account, + deposit_value_icp, + ); + + // call register + let contract_id = deferred_client + .register_contract(admin(), registration_data) + .unwrap(); + + // sign contract + let res = deferred_client.sign_contract(contract_id.clone()); + assert!(res.is_ok()); + + // we need to buy all the contracts :( + let contract = deferred_client.get_contract(&contract_id).unwrap(); + for token in contract.tokens { + // get nft price + let icp_price = marketplace_client + .get_token_price_icp(charlie(), &token) + .unwrap(); + // approve on icp ledger client a spend for token price to marketplace canister + icp_ledger_client + .icrc2_approve( + charlie(), + Account::from(env.marketplace_id), + icp_price.into(), + charlie_account().subaccount, + ) + .unwrap(); + assert!(marketplace_client + .buy_token(charlie(), &token, &charlie_account().subaccount) + .is_ok()); + } + + // get fee + let fee = icp_ledger_client.icrc1_fee(); + // withdraw deposit + let alice_balance = icp_ledger_client.icrc1_balance_of(Account::from(alice())); + assert!(deferred_client + .withdraw_contract_deposit(alice(), contract_id.clone(), None) + .is_ok()); + // verify balance + let new_balance = icp_ledger_client.icrc1_balance_of(Account::from(alice())); + let expected_balance = alice_balance + (deposit_value_icp * 60 / 100) - fee.clone(); + let diff = expected_balance.clone() - new_balance.clone(); + println!("diff: {:?}", diff); + assert_eq!(new_balance, expected_balance); + + // withdraw deposit for bob + let bob_balance = icp_ledger_client.icrc1_balance_of(Account::from(bob())); + assert!(deferred_client + .withdraw_contract_deposit(bob(), contract_id.clone(), None) + .is_ok()); + // verify balance + let new_balance = icp_ledger_client.icrc1_balance_of(Account::from(bob())); + let expected_balance = bob_balance + (deposit_value_icp * 40 / 100) - fee; + assert_eq!(new_balance, expected_balance); +} diff --git a/src/declarations/deferred/deferred.did b/src/declarations/deferred/deferred.did index 7f3d16d..30af084 100644 --- a/src/declarations/deferred/deferred.did +++ b/src/declarations/deferred/deferred.did @@ -82,6 +82,7 @@ type ContractType = variant { Sell; Financing }; type DeferredError = variant { Nft : NftError; Ekoke : EkokeError; + Withdraw : WithdrawError; Configuration : ConfigurationError_1; Unauthorized; Token : TokenError; @@ -285,6 +286,12 @@ type TxEvent = record { details : vec record { text; GenericValue }; caller : principal; }; +type WithdrawError = variant { + InvalidTransferAmount : record { nat64; nat8 }; + ContractNotFound : nat; + DepositTransferFailed : TransferError; + ContractNotPaid : nat; +}; service : (DeferredInitData) -> { admin_register_agency : (principal, Agency) -> (); admin_remove_role : (principal, Role) -> (Result); @@ -342,4 +349,5 @@ service : (DeferredInitData) -> { update_restricted_contract_property : (nat, text, RestrictedProperty) -> ( Result, ); + withdraw_contract_deposit : (nat, opt blob) -> (Result); } \ No newline at end of file diff --git a/src/declarations/deferred/deferred.did.d.ts b/src/declarations/deferred/deferred.did.d.ts index 8ceeb4b..c73e199 100644 --- a/src/declarations/deferred/deferred.did.d.ts +++ b/src/declarations/deferred/deferred.did.d.ts @@ -88,6 +88,7 @@ export type ContractType = { 'Sell' : null } | { 'Financing' : null }; export type DeferredError = { 'Nft' : NftError } | { 'Ekoke' : EkokeError } | + { 'Withdraw' : WithdrawError } | { 'Configuration' : ConfigurationError_1 } | { 'Unauthorized' : null } | { 'Token' : TokenError } | @@ -296,6 +297,10 @@ export interface TxEvent { 'details' : Array<[string, GenericValue]>, 'caller' : Principal, } +export type WithdrawError = { 'InvalidTransferAmount' : [bigint, number] } | + { 'ContractNotFound' : bigint } | + { 'DepositTransferFailed' : TransferError } | + { 'ContractNotPaid' : bigint }; export interface _SERVICE { 'admin_register_agency' : ActorMethod<[Principal, Agency], undefined>, 'admin_remove_role' : ActorMethod<[Principal, Role], Result>, @@ -362,6 +367,10 @@ export interface _SERVICE { [bigint, string, RestrictedProperty], Result >, + 'withdraw_contract_deposit' : ActorMethod< + [bigint, [] | [Uint8Array | number[]]], + Result + >, } export declare const idlFactory: IDL.InterfaceFactory; export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[]; diff --git a/src/declarations/deferred/deferred.did.js b/src/declarations/deferred/deferred.did.js index a71c322..3ddb003 100644 --- a/src/declarations/deferred/deferred.did.js +++ b/src/declarations/deferred/deferred.did.js @@ -134,6 +134,12 @@ export const idlFactory = ({ IDL }) => { 'Icrc2Transfer' : TransferFromError, 'Ecdsa' : EcdsaError, }); + const WithdrawError = IDL.Variant({ + 'InvalidTransferAmount' : IDL.Tuple(IDL.Nat64, IDL.Nat8), + 'ContractNotFound' : IDL.Nat, + 'DepositTransferFailed' : TransferError, + 'ContractNotPaid' : IDL.Nat, + }); const ConfigurationError_1 = IDL.Variant({ 'CustodialsCantBeEmpty' : IDL.Null, 'AnonymousCustodial' : IDL.Null, @@ -170,6 +176,7 @@ export const idlFactory = ({ IDL }) => { const DeferredError = IDL.Variant({ 'Nft' : NftError, 'Ekoke' : EkokeError, + 'Withdraw' : WithdrawError, 'Configuration' : ConfigurationError_1, 'Unauthorized' : IDL.Null, 'Token' : TokenError, @@ -447,6 +454,11 @@ export const idlFactory = ({ IDL }) => { [Result], [], ), + 'withdraw_contract_deposit' : IDL.Func( + [IDL.Nat, IDL.Opt(IDL.Vec(IDL.Nat8))], + [Result], + [], + ), }); }; export const init = ({ IDL }) => { diff --git a/src/declarations/marketplace/marketplace.did b/src/declarations/marketplace/marketplace.did index 9f736aa..30731fe 100644 --- a/src/declarations/marketplace/marketplace.did +++ b/src/declarations/marketplace/marketplace.did @@ -32,6 +32,7 @@ type ConfigurationError_1 = variant { type DeferredError = variant { Nft : NftError; Ekoke : EkokeError; + Withdraw : WithdrawError; Configuration : ConfigurationError_1; Unauthorized; Token : TokenError; @@ -151,6 +152,12 @@ type TransferFromError = variant { TooOld; InsufficientFunds : record { balance : nat }; }; +type WithdrawError = variant { + InvalidTransferAmount : record { nat64; nat8 }; + ContractNotFound : nat; + DepositTransferFailed : TransferError; + ContractNotPaid : nat; +}; service : (MarketplaceInitData) -> { admin_cycles : () -> (nat) query; admin_set_admins : (vec principal) -> (Result); diff --git a/src/declarations/marketplace/marketplace.did.d.ts b/src/declarations/marketplace/marketplace.did.d.ts index fe6b98d..7fd7331 100644 --- a/src/declarations/marketplace/marketplace.did.d.ts +++ b/src/declarations/marketplace/marketplace.did.d.ts @@ -31,6 +31,7 @@ export type ConfigurationError_1 = { 'CustodialsCantBeEmpty' : null } | { 'AnonymousCustodial' : null }; export type DeferredError = { 'Nft' : NftError } | { 'Ekoke' : EkokeError } | + { 'Withdraw' : WithdrawError } | { 'Configuration' : ConfigurationError_1 } | { 'Unauthorized' : null } | { 'Token' : TokenError } | @@ -142,6 +143,10 @@ export type TransferFromError = { { 'CreatedInFuture' : { 'ledger_time' : bigint } } | { 'TooOld' : null } | { 'InsufficientFunds' : { 'balance' : bigint } }; +export type WithdrawError = { 'InvalidTransferAmount' : [bigint, number] } | + { 'ContractNotFound' : bigint } | + { 'DepositTransferFailed' : TransferError } | + { 'ContractNotPaid' : bigint }; export interface _SERVICE { 'admin_cycles' : ActorMethod<[], bigint>, 'admin_set_admins' : ActorMethod<[Array], Result>, diff --git a/src/declarations/marketplace/marketplace.did.js b/src/declarations/marketplace/marketplace.did.js index 8eb7992..a39fa30 100644 --- a/src/declarations/marketplace/marketplace.did.js +++ b/src/declarations/marketplace/marketplace.did.js @@ -116,6 +116,12 @@ export const idlFactory = ({ IDL }) => { 'Icrc2Transfer' : TransferFromError, 'Ecdsa' : EcdsaError, }); + const WithdrawError = IDL.Variant({ + 'InvalidTransferAmount' : IDL.Tuple(IDL.Nat64, IDL.Nat8), + 'ContractNotFound' : IDL.Nat, + 'DepositTransferFailed' : TransferError, + 'ContractNotPaid' : IDL.Nat, + }); const ConfigurationError_1 = IDL.Variant({ 'CustodialsCantBeEmpty' : IDL.Null, 'AnonymousCustodial' : IDL.Null, @@ -152,6 +158,7 @@ export const idlFactory = ({ IDL }) => { const DeferredError = IDL.Variant({ 'Nft' : NftError, 'Ekoke' : EkokeError, + 'Withdraw' : WithdrawError, 'Configuration' : ConfigurationError_1, 'Unauthorized' : IDL.Null, 'Token' : TokenError, diff --git a/src/deferred/deferred.did b/src/deferred/deferred.did index 7f3d16d..30af084 100644 --- a/src/deferred/deferred.did +++ b/src/deferred/deferred.did @@ -82,6 +82,7 @@ type ContractType = variant { Sell; Financing }; type DeferredError = variant { Nft : NftError; Ekoke : EkokeError; + Withdraw : WithdrawError; Configuration : ConfigurationError_1; Unauthorized; Token : TokenError; @@ -285,6 +286,12 @@ type TxEvent = record { details : vec record { text; GenericValue }; caller : principal; }; +type WithdrawError = variant { + InvalidTransferAmount : record { nat64; nat8 }; + ContractNotFound : nat; + DepositTransferFailed : TransferError; + ContractNotPaid : nat; +}; service : (DeferredInitData) -> { admin_register_agency : (principal, Agency) -> (); admin_remove_role : (principal, Role) -> (Result); @@ -342,4 +349,5 @@ service : (DeferredInitData) -> { update_restricted_contract_property : (nat, text, RestrictedProperty) -> ( Result, ); + withdraw_contract_deposit : (nat, opt blob) -> (Result); } \ No newline at end of file diff --git a/src/deferred/src/app.rs b/src/deferred/src/app.rs index 513ea5e..98ac277 100644 --- a/src/deferred/src/app.rs +++ b/src/deferred/src/app.rs @@ -17,7 +17,7 @@ use configuration::Configuration; use did::deferred::{ Agency, Contract, ContractRegistration, DeferredError, DeferredInitData, DeferredResult, RestrictedContractProperties, RestrictedProperty, RestrictionLevel, Role, TokenError, - TokenInfo, + TokenInfo, WithdrawError, }; use did::ID; use dip721_rs::{ @@ -272,6 +272,68 @@ impl Deferred { ContractStorage::sign_contract_and_mint_tokens(&contract_id, tokens) } + /// Call for the contract seller to withdraw the buyer deposit in case the contract has been completely paid + pub async fn withdraw_contract_deposit( + contract_id: ID, + withdraw_subaccount: Option, + ) -> DeferredResult<()> { + Inspect::inspect_is_seller(caller(), contract_id.clone())?; + + // check if the contract has been paid + // get contract + let contract = ContractStorage::get_contract(&contract_id).ok_or( + DeferredError::Withdraw(WithdrawError::ContractNotFound(contract_id.clone())), + )?; + + // check if all the tokens are burned (bought by the contract buyer) + if contract.tokens.iter().any(|token_id| { + ContractStorage::get_token(token_id) + .map(|token| !token.is_burned) + .unwrap_or_default() + }) { + return Err(DeferredError::Withdraw(WithdrawError::ContractNotPaid( + contract_id, + ))); + } + + // transfer the deposit to the seller + let icp_ledger_client = IcrcLedgerClient::new(Configuration::get_icp_ledger_canister()); + // get fee + let icp_fee = icp_ledger_client + .icrc1_fee() + .await + .map_err(|(code, msg)| DeferredError::CanisterCall(code, msg))?; + + // get seller quota + let seller_quota = contract + .sellers + .iter() + .find(|seller| seller.principal == caller()) + .map(|seller| seller.quota) + .unwrap(); // unwrap is safe because the caller is the seller + + let transfer_amount = (contract.deposit.value_icp.checked_mul(seller_quota as u64)) + .and_then(|value| value.checked_div(100)) + .map(|value| value - icp_fee) + .ok_or(DeferredError::Withdraw( + WithdrawError::InvalidTransferAmount(contract.deposit.value_icp, seller_quota), + ))?; + + icp_ledger_client + .icrc1_transfer( + Account { + owner: caller(), + subaccount: withdraw_subaccount, + }, + transfer_amount, + ) + .await + .map_err(|(code, msg)| DeferredError::CanisterCall(code, msg))? + .map_err(|err| DeferredError::Withdraw(WithdrawError::DepositTransferFailed(err)))?; + + Ok(()) + } + /// Update marketplace canister id and update the operator for all the tokens pub fn admin_set_marketplace_canister(canister: Principal) { if !Inspect::inspect_is_custodian(caller()) { @@ -720,7 +782,7 @@ mod test { use did::deferred::{Buyers, Deposit, Seller}; use pretty_assertions::{assert_eq, assert_ne}; - use test_utils::bob_account; + use test_utils::{alice, bob_account}; use self::test_utils::{bob, mock_agency}; use super::test_utils::store_mock_contract; @@ -857,6 +919,118 @@ mod test { assert_eq!(Deferred::dip721_total_supply(), Nat::from(20_u64)); } + #[tokio::test] + async fn test_should_withdraw_contract_deposit() { + init_canister(); + + let contract_id = 1; + test_utils::store_mock_contract_with( + &[1, 2, 3], + contract_id, + |contract| { + contract.value = 400_000; + contract.deposit = Deposit { + value_fiat: 10000, + value_icp: 100_000_000, + }; + contract.sellers = vec![ + Seller { + principal: caller(), + quota: 50, + }, + Seller { + principal: bob(), + quota: 50, + }, + ]; + }, + |token| { + token.is_burned = true; + token.owner = Some(caller()); + }, + ); + + // withdraw deposit + assert!( + Deferred::withdraw_contract_deposit(contract_id.into(), None) + .await + .is_ok() + ); + } + + #[tokio::test] + async fn test_should_not_withdraw_contract_deposit_if_not_burned_yet() { + init_canister(); + + let contract_id = 1; + test_utils::store_mock_contract_with( + &[1, 2, 3], + contract_id, + |contract| { + contract.deposit = Deposit { + value_fiat: 10000, + value_icp: 100_000_000, + }; + contract.sellers = vec![ + Seller { + principal: caller(), + quota: 50, + }, + Seller { + principal: bob(), + quota: 50, + }, + ]; + }, + |token| token.is_burned = false, + ); + + // withdraw deposit + assert!( + Deferred::withdraw_contract_deposit(contract_id.into(), None) + .await + .is_err() + ); + } + + #[tokio::test] + async fn test_should_not_withdraw_contract_deposit_if_not_seller() { + init_canister(); + + let contract_id = 1; + test_utils::store_mock_contract_with( + &[1, 2, 3], + contract_id, + |contract| { + contract.deposit = Deposit { + value_fiat: 10000, + value_icp: 100, + }; + contract.sellers = vec![ + Seller { + principal: alice(), + quota: 50, + }, + Seller { + principal: bob(), + quota: 50, + }, + ]; + }, + |token| { + token.is_burned = true; + token.owner = Some(alice()); + }, + ); + + // withdraw deposit + assert!( + Deferred::withdraw_contract_deposit(contract_id.into(), None) + .await + .is_err() + ); + } + #[test] fn test_should_update_contract_buyers() { init_canister(); diff --git a/src/deferred/src/app/test_utils.rs b/src/deferred/src/app/test_utils.rs index 9834fff..71c8774 100644 --- a/src/deferred/src/app/test_utils.rs +++ b/src/deferred/src/app/test_utils.rs @@ -139,10 +139,6 @@ pub fn bob() -> Principal { Principal::from_text("bs5l3-6b3zu-dpqyj-p2x4a-jyg4k-goneb-afof2-y5d62-skt67-3756q-dqe").unwrap() } -pub fn alice_account() -> Account { - Account::from(alice()) -} - pub fn bob_account() -> Account { Account::from(bob()) } diff --git a/src/deferred/src/inspect.rs b/src/deferred/src/inspect.rs index 5d591cf..4e7128a 100644 --- a/src/deferred/src/inspect.rs +++ b/src/deferred/src/inspect.rs @@ -5,6 +5,7 @@ use dip721_rs::GenericValue; use ic_cdk::api; #[cfg(target_family = "wasm")] use ic_cdk_macros::inspect_message; +use icrc::icrc1::account::Subaccount; use crate::app::Inspect; use crate::utils::caller; @@ -56,6 +57,10 @@ fn inspect_message_impl() { ) .is_ok() } + "withdraw_contract_deposit" => { + let id = api::call::arg_data::<(ID, Option)>().0; + Inspect::inspect_is_seller(caller(), id).is_ok() + } "get_unsigned_contracts" => { Inspect::inspect_is_agent(caller()) || Inspect::inspect_is_custodian(caller()) } diff --git a/src/deferred/src/lib.rs b/src/deferred/src/lib.rs index 2ad5f9a..deae383 100644 --- a/src/deferred/src/lib.rs +++ b/src/deferred/src/lib.rs @@ -21,6 +21,7 @@ mod inspect; mod utils; use app::Deferred; +use icrc::icrc1::account::Subaccount; #[init] pub fn init(init_data: DeferredInitData) { @@ -126,6 +127,15 @@ pub fn update_contract_buyers(contract_id: ID, buyers: Vec) -> Deferr Deferred::update_contract_buyers(contract_id, buyers) } +#[update] +#[candid_method(update)] +pub async fn withdraw_contract_deposit( + contract_id: ID, + withdraw_subaccount: Option, +) -> DeferredResult<()> { + Deferred::withdraw_contract_deposit(contract_id, withdraw_subaccount).await +} + #[update] #[candid_method(update)] pub fn admin_set_ekoke_reward_pool_canister(canister_id: Principal) { diff --git a/src/did/src/deferred.rs b/src/did/src/deferred.rs index 69b9ca5..7da0f94 100644 --- a/src/did/src/deferred.rs +++ b/src/did/src/deferred.rs @@ -12,7 +12,7 @@ pub use self::contract::{ Deposit, GenericValue, RestrictedContractProperties, RestrictedProperty, RestrictionLevel, Seller, Token, TokenIdentifier, TokenInfo, ID, }; -pub use self::error::{ConfigurationError, DeferredError, TokenError}; +pub use self::error::{ConfigurationError, DeferredError, TokenError, WithdrawError}; #[cfg(test)] mod test { diff --git a/src/did/src/deferred/error.rs b/src/did/src/deferred/error.rs index 8961b92..ecced37 100644 --- a/src/did/src/deferred/error.rs +++ b/src/did/src/deferred/error.rs @@ -1,6 +1,7 @@ use candid::{CandidType, Deserialize, Nat}; use dip721_rs::{NftError, TokenIdentifier}; use ic_cdk::api::call::RejectionCode; +use icrc::icrc1::transfer::TransferError; use icrc::icrc2::transfer_from::TransferFromError; use thiserror::Error; @@ -15,6 +16,8 @@ pub enum DeferredError { Ekoke(#[from] EkokeError), #[error("token error: {0}")] Token(TokenError), + #[error("withdraw error: {0}")] + Withdraw(WithdrawError), #[error("configuration error: {0}")] Configuration(ConfigurationError), #[error("storage error")] @@ -84,3 +87,15 @@ pub enum ConfigurationError { #[error("the canister custodial cannot be anonymous")] AnonymousCustodial, } + +#[derive(Clone, Debug, Error, CandidType, PartialEq, Eq, Deserialize)] +pub enum WithdrawError { + #[error("the provided contract ID ({0}) doesn't exist in the canister storage")] + ContractNotFound(ID), + #[error("the contract {0} has not been completely paid yet")] + ContractNotPaid(ID), + #[error("deposit transfer failed: {0}")] + DepositTransferFailed(TransferError), + #[error("invalid transfer amount: {0} for quota {1}")] + InvalidTransferAmount(u64, u8), +} diff --git a/src/marketplace/marketplace.did b/src/marketplace/marketplace.did index 9f736aa..30731fe 100644 --- a/src/marketplace/marketplace.did +++ b/src/marketplace/marketplace.did @@ -32,6 +32,7 @@ type ConfigurationError_1 = variant { type DeferredError = variant { Nft : NftError; Ekoke : EkokeError; + Withdraw : WithdrawError; Configuration : ConfigurationError_1; Unauthorized; Token : TokenError; @@ -151,6 +152,12 @@ type TransferFromError = variant { TooOld; InsufficientFunds : record { balance : nat }; }; +type WithdrawError = variant { + InvalidTransferAmount : record { nat64; nat8 }; + ContractNotFound : nat; + DepositTransferFailed : TransferError; + ContractNotPaid : nat; +}; service : (MarketplaceInitData) -> { admin_cycles : () -> (nat) query; admin_set_admins : (vec principal) -> (Result); diff --git a/src/marketplace/src/client/deferred.rs b/src/marketplace/src/client/deferred.rs index 0610e73..b8c2a5e 100644 --- a/src/marketplace/src/client/deferred.rs +++ b/src/marketplace/src/client/deferred.rs @@ -2,8 +2,8 @@ use candid::Nat; use candid::Principal; #[cfg(not(target_arch = "wasm32"))] -use did::deferred::{Contract, Seller, Token}; -use did::deferred::{Deposit, TokenIdentifier, TokenInfo}; +use did::deferred::{Contract, Deposit, Seller, Token}; +use did::deferred::{TokenIdentifier, TokenInfo}; #[cfg(target_arch = "wasm32")] use did::marketplace::MarketplaceError; use did::marketplace::MarketplaceResult;