Skip to content

Commit

Permalink
feat: withdraw_contract_deposit
Browse files Browse the repository at this point in the history
  • Loading branch information
veeso committed Jul 31, 2024
1 parent 92bf3d5 commit c03ae62
Show file tree
Hide file tree
Showing 20 changed files with 479 additions and 10 deletions.
16 changes: 15 additions & 1 deletion docs/canisters/deferred.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions integration-tests/src/client/deferred.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -53,6 +54,25 @@ impl<'a> DeferredClient<'a> {
res
}

pub fn withdraw_contract_deposit(
&self,
caller: Principal,
contract_id: ID,
subaccount: Option<Subaccount>,
) -> 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,
Expand Down
1 change: 1 addition & 0 deletions integration-tests/tests/use_case/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ mod register_contract_buyers;
mod register_sell_contract;
mod reserve_reward_pool;
mod update_contract_property;
mod withdraw_contract_deposit;
64 changes: 64 additions & 0 deletions integration-tests/tests/use_case/register_contract.rs
Original file line number Diff line number Diff line change
@@ -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);
}
107 changes: 107 additions & 0 deletions integration-tests/tests/use_case/withdraw_contract_deposit.rs
Original file line number Diff line number Diff line change
@@ -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);
}
8 changes: 8 additions & 0 deletions src/declarations/deferred/deferred.did
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ type ContractType = variant { Sell; Financing };
type DeferredError = variant {
Nft : NftError;
Ekoke : EkokeError;
Withdraw : WithdrawError;
Configuration : ConfigurationError_1;
Unauthorized;
Token : TokenError;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -342,4 +349,5 @@ service : (DeferredInitData) -> {
update_restricted_contract_property : (nat, text, RestrictedProperty) -> (
Result,
);
withdraw_contract_deposit : (nat, opt blob) -> (Result);
}
9 changes: 9 additions & 0 deletions src/declarations/deferred/deferred.did.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } |
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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[];
12 changes: 12 additions & 0 deletions src/declarations/deferred/deferred.did.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 }) => {
Expand Down
7 changes: 7 additions & 0 deletions src/declarations/marketplace/marketplace.did
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type ConfigurationError_1 = variant {
type DeferredError = variant {
Nft : NftError;
Ekoke : EkokeError;
Withdraw : WithdrawError;
Configuration : ConfigurationError_1;
Unauthorized;
Token : TokenError;
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/declarations/marketplace/marketplace.did.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } |
Expand Down Expand Up @@ -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<Principal>], Result>,
Expand Down
Loading

0 comments on commit c03ae62

Please sign in to comment.