diff --git a/common/src/account_transfers.rs b/common/src/account_transfers.rs index 6f9c220..0db51dd 100644 --- a/common/src/account_transfers.rs +++ b/common/src/account_transfers.rs @@ -14,9 +14,10 @@ use sha2::{Digest, Sha256}; use std::cell::RefCell; use crate::{ - account_balance_add, account_balance_get, account_balance_sub, get_timestamp_ns, - ledger_add_reputation_change, slice_to_32_bytes_array, DccIdentity, TokenAmountE9s, - TransferError, LABEL_DC_TOKEN_TRANSFER, MINTING_ACCOUNT, MINTING_ACCOUNT_PRINCIPAL, + account_balance_add, account_balance_get, account_balance_sub, amount_as_string, + get_pubkey_from_principal, get_timestamp_ns, ledger_add_reputation_change, + slice_to_32_bytes_array, DccIdentity, TokenAmountE9s, TransferError, LABEL_DC_TOKEN_TRANSFER, + MINTING_ACCOUNT, MINTING_ACCOUNT_PRINCIPAL, }; thread_local! { @@ -63,34 +64,33 @@ pub fn ledger_funds_transfer( pub fn charge_fees_to_account_and_bump_reputation( ledger: &mut LedgerMap, - dcc_id_charge: &DccIdentity, - dcc_id_bump_reputation: &DccIdentity, + dcc_id: &DccIdentity, amount_e9s: TokenAmountE9s, ) -> Result<(), String> { if amount_e9s == 0 { return Ok(()); } - let balance_from_after = - account_balance_get(&dcc_id_charge.as_icrc_compatible_account()) - amount_e9s; + let from_icrc1_account = dcc_id.as_icrc_compatible_account(); + let balance_from_before = account_balance_get(&from_icrc1_account); match ledger_funds_transfer( ledger, // Burn 0 tokens, and transfer the entire amount_e9s to the fee accounts FundsTransfer::new( - dcc_id_charge.as_icrc_compatible_account(), + from_icrc1_account, MINTING_ACCOUNT, amount_e9s.into(), Some(fees_sink_accounts()), Some(get_timestamp_ns()), vec![], 0, - balance_from_after, + balance_from_before.saturating_sub(amount_e9s), 0, ), ) { Ok(_) => Ok(ledger_add_reputation_change( ledger, - dcc_id_bump_reputation, - amount_e9s.min(i64::MAX as TokenAmountE9s) as i64, + dcc_id, + amount_e9s as i64, )?), Err(e) => { info!("Failed to charge fees: {}", e); @@ -101,26 +101,26 @@ pub fn charge_fees_to_account_and_bump_reputation( pub fn charge_fees_to_account_no_bump_reputation( ledger: &mut LedgerMap, - dcc_identity: &DccIdentity, + dcc_id_charge: &DccIdentity, amount_e9s: TokenAmountE9s, ) -> Result<(), String> { if amount_e9s == 0 { return Ok(()); } - let balance_from_after = - account_balance_get(&dcc_identity.as_icrc_compatible_account()) - amount_e9s; + let from_icrc1_account = dcc_id_charge.as_icrc_compatible_account(); + let balance_from_before = account_balance_get(&from_icrc1_account); match ledger_funds_transfer( ledger, // Burn 0 tokens, and transfer the entire amount_e9s to the fee accounts FundsTransfer::new( - dcc_identity.as_icrc_compatible_account(), + from_icrc1_account, MINTING_ACCOUNT, Some(amount_e9s), Some(fees_sink_accounts()), Some(get_timestamp_ns()), vec![], 0, - balance_from_after, + balance_from_before.saturating_sub(amount_e9s), 0, ), ) { @@ -132,6 +132,78 @@ pub fn charge_fees_to_account_no_bump_reputation( } } +pub enum IncreaseReputation { + None, + Sender, + Recipient, +} + +pub fn do_funds_transfer( + ledger: &mut LedgerMap, + from_dcc_id: &DccIdentity, + to_icrc1_account: &IcrcCompatibleAccount, + transfer_amount_e9s: TokenAmountE9s, + fees_amount_e9s: TokenAmountE9s, + memo: &[u8], + increase_reputation: IncreaseReputation, +) -> Result { + let from_icrc1_account = from_dcc_id.as_icrc_compatible_account(); + + if transfer_amount_e9s == 0 { + return Ok("Nothing to transfer".to_string()); + } + let balance_from_before = account_balance_get(&from_icrc1_account); + let balance_to_before = account_balance_get(to_icrc1_account); + if balance_from_before < transfer_amount_e9s + fees_amount_e9s { + return Err(format!( + "Not enough funds to transfer: {} < {} + {}", + balance_from_before, transfer_amount_e9s, fees_amount_e9s + )); + } + let balance_from_after = balance_from_before - transfer_amount_e9s - fees_amount_e9s; + match ledger_funds_transfer( + ledger, + FundsTransfer::new( + from_icrc1_account.clone(), + to_icrc1_account.clone(), + Some(fees_amount_e9s), + Some(fees_sink_accounts()), + Some(get_timestamp_ns()), + memo.to_vec(), + transfer_amount_e9s, + balance_from_after, + balance_to_before + transfer_amount_e9s, + ), + ) { + Ok(_) => { + let response = format!( + "Transferred {} tokens from {} \t to account {}, and charged fees {} tokens", + amount_as_string(transfer_amount_e9s), + from_icrc1_account, + to_icrc1_account, + amount_as_string(fees_amount_e9s) + ); + match increase_reputation { + IncreaseReputation::None => (), + IncreaseReputation::Sender => { + ledger_add_reputation_change(ledger, from_dcc_id, fees_amount_e9s as i64)?; + } + IncreaseReputation::Recipient => { + let to_dcc_identity = to_icrc1_account + .to_dcc_identity() + .expect("Failed to get dcc identity from icrc1 account"); + ledger_add_reputation_change(ledger, &to_dcc_identity, fees_amount_e9s as i64)?; + } + } + Ok(response) + } + Err(e) => { + info!("Failed to charge fees: {}", e); + Err(e.to_string()) + } + } +} + #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq, Hash)] pub struct IcrcCompatibleAccount { pub owner: Principal, @@ -201,6 +273,14 @@ impl IcrcCompatibleAccount { pub fn from_bytes(bytes: &[u8]) -> Result { borsh::from_slice(bytes) } + + pub fn to_dcc_identity(&self) -> Option { + let owner_bytes = get_pubkey_from_principal(self.owner); + if owner_bytes.is_empty() { + return None; + } + DccIdentity::new_verifying_from_bytes(&owner_bytes).ok() + } } impl From<&str> for IcrcCompatibleAccount { diff --git a/common/src/contract_sign_reply.rs b/common/src/contract_sign_reply.rs index e2fe8c7..b958640 100644 --- a/common/src/contract_sign_reply.rs +++ b/common/src/contract_sign_reply.rs @@ -6,8 +6,9 @@ use ledger_map::LedgerMap; use serde::{Deserialize, Serialize}; use crate::{ - amount_as_string, charge_fees_to_account_and_bump_reputation, contract_sign_fee_e9s, - contracts_cache_open_remove, fn_info, ContractSignRequestPayload, DccIdentity, + account_balance_get, amount_as_string, charge_fees_to_account_no_bump_reputation, + contract_sign_fee_e9s, contracts_cache_open_remove, do_funds_transfer, fn_info, + ledger_add_reputation_change, ContractSignRequestPayload, DccIdentity, LABEL_CONTRACT_SIGN_REPLY, LABEL_CONTRACT_SIGN_REQUEST, }; @@ -117,10 +118,11 @@ pub fn do_contract_sign_reply( reply_serialized: Vec, crypto_signature: Vec, ) -> Result { - let dcc_id = DccIdentity::new_verifying_from_bytes(&pubkey_bytes).unwrap(); - dcc_id.verify_bytes(&reply_serialized, &crypto_signature)?; + let provider_dcc_id = DccIdentity::new_verifying_from_bytes(&pubkey_bytes).unwrap(); + provider_dcc_id.verify_bytes(&reply_serialized, &crypto_signature)?; + let provider_icrc1 = provider_dcc_id.as_icrc_compatible_account(); - fn_info!("{}", dcc_id); + fn_info!("{}", provider_dcc_id); let cs_reply = ContractSignReply::try_from_slice(&reply_serialized).unwrap(); let contract_id = cs_reply.contract_id(); @@ -134,14 +136,44 @@ pub fn do_contract_sign_reply( if pubkey_bytes != cs_req.provider_pubkey_bytes() { return Err(format!( "Contract signing reply signed and submitted by {} does not match the provider public key {} from contract req 0x{}", - dcc_id, DccIdentity::new_verifying_from_bytes(cs_req.provider_pubkey_bytes()).unwrap(), hex::encode(contract_id) + provider_dcc_id, DccIdentity::new_verifying_from_bytes(cs_req.provider_pubkey_bytes()).unwrap(), hex::encode(contract_id) )); } + let fees_e9s = contract_sign_fee_e9s(cs_req.payment_amount_e9s()); + + let requester_dcc_id = + DccIdentity::new_verifying_from_bytes(cs_req.requester_pubkey_bytes()).unwrap(); + let requester_icrc1 = requester_dcc_id.as_icrc_compatible_account(); + let requester_balance = account_balance_get(&requester_icrc1); + let payload = ContractSignReplyPayload::new(reply_serialized, crypto_signature); let payload_serialized = borsh::to_vec(&payload).unwrap(); - let fees = contract_sign_fee_e9s(cs_req.payment_amount_e9s()); - charge_fees_to_account_and_bump_reputation(ledger, &dcc_id, &dcc_id, fees)?; + if cs_reply.sign_accepted() { + // Accepted: charge the requester the full amount + let charge_amount_e9s = cs_req.payment_amount_e9s() + fees_e9s; + if requester_balance < charge_amount_e9s { + return Err(format!( + "Requester {} does not have enough funds. At least {} tokens are required, and {} are available", + requester_icrc1, + amount_as_string(charge_amount_e9s), + amount_as_string(requester_balance) + )); + } + do_funds_transfer( + ledger, + &requester_dcc_id, + &provider_icrc1, + cs_req.payment_amount_e9s(), + fees_e9s, + cs_req.request_memo().as_bytes(), + crate::IncreaseReputation::Recipient, + )?; + } else { + // Else, charge the provider the response fee, and revert the requester's reputation + charge_fees_to_account_no_bump_reputation(ledger, &provider_dcc_id, fees_e9s)?; + ledger_add_reputation_change(ledger, &requester_dcc_id, -(fees_e9s as i64))?; + } ledger.upsert( LABEL_CONTRACT_SIGN_REPLY, @@ -152,7 +184,7 @@ pub fn do_contract_sign_reply( contracts_cache_open_remove(contract_id); format!( "Contract signing reply submitted! Thank you. You have been charged {} tokens as a fee, and your reputation has been bumped accordingly", - amount_as_string(fees) + amount_as_string(fees_e9s) ) }) .map_err(|e| e.to_string()) diff --git a/common/src/contract_sign_request.rs b/common/src/contract_sign_request.rs index c4af3a8..75c0b48 100644 --- a/common/src/contract_sign_request.rs +++ b/common/src/contract_sign_request.rs @@ -9,8 +9,8 @@ use serde::{Deserialize, Serialize, Serializer}; use sha2::{Digest, Sha256}; use crate::{ - amount_as_string, charge_fees_to_account_and_bump_reputation, fn_info, AHashMap, DccIdentity, - TokenAmountE9s, LABEL_CONTRACT_SIGN_REQUEST, + account_balance_get, amount_as_string, charge_fees_to_account_and_bump_reputation, fn_info, + AHashMap, DccIdentity, TokenAmountE9s, LABEL_CONTRACT_SIGN_REQUEST, }; pub type ContractId = Vec; @@ -267,14 +267,29 @@ pub fn do_contract_sign_request( let contract_req = ContractSignRequest::try_from_slice(&request_serialized).unwrap(); - let fees = contract_sign_fee_e9s(contract_req.payment_amount_e9s()); + let fees_e9s = contract_sign_fee_e9s(contract_req.payment_amount_e9s()); + + let requester_dcc_id = + DccIdentity::new_verifying_from_bytes(contract_req.requester_pubkey_bytes()).unwrap(); + let requester_icrc1 = requester_dcc_id.as_icrc_compatible_account(); + let requester_balance = account_balance_get(&requester_icrc1); + let expected_min_balance = contract_req.payment_amount_e9s() + fees_e9s; + + if requester_balance < expected_min_balance { + return Err(format!( + "Signing of this contract requires at least {} tokens. Requester {} has only {} tokens", + amount_as_string(expected_min_balance), + requester_icrc1, + amount_as_string(requester_balance) + )); + } let payload = ContractSignRequestPayload::new(&request_serialized, &crypto_signature).unwrap(); let payload_bytes = borsh::to_vec(&payload).unwrap(); - charge_fees_to_account_and_bump_reputation(ledger, &dcc_id, &dcc_id, fees)?; - let contract_id = payload.calc_contract_id(); + charge_fees_to_account_and_bump_reputation(ledger, &dcc_id, fees_e9s)?; + let contract_id = payload.calc_contract_id(); ledger.upsert( LABEL_CONTRACT_SIGN_REQUEST, contract_id, @@ -284,7 +299,7 @@ pub fn do_contract_sign_request( format!( "Contract signing req 0x{} submitted! Thank you. You have been charged {} tokens as a fee, and your reputation has been bumped accordingly. Please check back for a response from the provider.", hex::encode(contract_id), - amount_as_string(fees) + amount_as_string(fees_e9s) ) }).map_err(|e| e.to_string()) } diff --git a/common/src/lib.rs b/common/src/lib.rs index ce53e33..d2ced58 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -106,7 +106,7 @@ pub const MAX_PUBKEY_BYTES: usize = 32; pub const MEMO_BYTES_MAX: usize = 32; /// Reduction of reputations for all accounts, based on time (per block), in parts per million pub const REPUTATION_AGING_PPM: u64 = 1_000; -pub const MAX_REPUTATION_INCREASE_PER_TX: i64 = DC_TOKEN_DECIMALS_DIV as i64 / 1000; +pub const MAX_REPUTATION_INCREASE_PER_TX: i64 = DC_TOKEN_DECIMALS_DIV as i64 * 10; // Max 10 DC tokens per transaction pub const REWARD_HALVING_AFTER_BLOCKS: u64 = 210_000; // halve the rewards every 210000 reward distributions pub const DATA_PULL_BYTES_BEFORE_LEN: u16 = 16; // How many bytes before the pulled data should be compared as a quick sanity check diff --git a/ic-canister/tests/test_canister.rs b/ic-canister/tests/test_canister.rs index 8c4d633..884bc63 100644 --- a/ic-canister/tests/test_canister.rs +++ b/ic-canister/tests/test_canister.rs @@ -110,10 +110,10 @@ fn upgrade_test_canister(pic: &PocketIc, can: Principal) -> Result<(), pocket_ic pic.upgrade_canister(can, CANISTER_WASM.clone(), no_args, None) } -fn get_account_balance(pic: &PocketIc, can: Principal, account: &Icrc1Account) -> Nat { +fn get_account_balance(pic: &PocketIc, can: &Principal, account: &Icrc1Account) -> Nat { query_check_and_decode!( pic, - can, + *can, "icrc1_balance_of", encode_one(account).expect("failed to encode"), Nat @@ -322,9 +322,9 @@ fn user_register( (dcc_identity, result) } -fn identity_reputation_get(pic: &PocketIc, can: Principal, identity: &Vec) -> u64 { - let args = Encode!(&identity).unwrap(); - query_check_and_decode!(pic, can, "get_identity_reputation", args, u64) +fn get_id_reputation(pic: &PocketIc, can: &Principal, dcc_id: &DccIdentity) -> u64 { + let args = Encode!(&dcc_id.to_bytes_verifying()).unwrap(); + query_check_and_decode!(pic, *can, "get_identity_reputation", args, u64) } fn np_check_in( @@ -374,11 +374,11 @@ fn test_balances_and_transfers() { let account_b = icrc1_account_from_slice(b"B"); assert_eq!( - get_account_balance(&pic, c, &account_a), + get_account_balance(&pic, &c, &account_a), 0u64 as TokenAmountE9s ); assert_eq!( - get_account_balance(&pic, c, &account_b), + get_account_balance(&pic, &c, &account_b), 0u64 as TokenAmountE9s ); @@ -388,9 +388,9 @@ fn test_balances_and_transfers() { let response = mint_tokens_for_test(&pic, c, &account_a, amount_mint); println!("mint_tokens_for_test response: {:?}", response); - assert_eq!(get_account_balance(&pic, c, &account_a), amount_mint); + assert_eq!(get_account_balance(&pic, &c, &account_a), amount_mint); assert_eq!( - get_account_balance(&pic, c, &account_b), + get_account_balance(&pic, &c, &account_b), 0u64 as TokenAmountE9s ); @@ -401,18 +401,18 @@ fn test_balances_and_transfers() { println!("icrc1_transfer response: {:?}", response); assert_eq!( - get_account_balance(&pic, c, &account_a), + get_account_balance(&pic, &c, &account_a), amount_mint - amount_send - DC_TOKEN_TRANSFER_FEE_E9S ); - assert_eq!(get_account_balance(&pic, c, &account_b), amount_send); + assert_eq!(get_account_balance(&pic, &c, &account_b), amount_send); upgrade_test_canister(&pic, c).expect("Canister upgrade failed"); assert_eq!( - get_account_balance(&pic, c, &account_a), + get_account_balance(&pic, &c, &account_a), amount_mint - amount_send - DC_TOKEN_TRANSFER_FEE_E9S ); - assert_eq!(get_account_balance(&pic, c, &account_b), amount_send); + assert_eq!(get_account_balance(&pic, &c, &account_b), amount_send); } fn commit(pic: &PocketIc, can: Principal) { @@ -443,7 +443,7 @@ fn test_np_registration_and_check_in() { // np_past now has 50 * 100 = 5000 tokens let amount: TokenAmountE9s = 5000u32 as TokenAmountE9s * DC_TOKEN_DECIMALS_DIV; assert_eq!( - get_account_balance(&p, c, &np_past.as_icrc_compatible_account().into()), + get_account_balance(&p, &c, &np_past.as_icrc_compatible_account().into()), amount ); @@ -451,7 +451,7 @@ fn test_np_registration_and_check_in() { let (np1, reg1) = np_register(&p, c, b"np1", 0); assert_eq!(reg1.unwrap_err(), "InsufficientFunds: account w7shl-xsw5s-kduqo-kx77s-nxs35-4zdh3-3tpob-nr4yc-2c6zw-qeyzj-rqe has 0 and requested 500000000".to_string()); assert_eq!( - get_account_balance(&p, c, &np1.as_icrc_compatible_account().into()), + get_account_balance(&p, &c, &np1.as_icrc_compatible_account().into()), 0u64 ); @@ -460,8 +460,8 @@ fn test_np_registration_and_check_in() { commit(&p, c); // Initial reputation is 0 - assert_eq!(identity_reputation_get(&p, c, &np1.to_bytes_verifying()), 0); - assert_eq!(identity_reputation_get(&p, c, &np2.to_bytes_verifying()), 0); + assert_eq!(get_id_reputation(&p, &c, &np1), 0); + assert_eq!(get_id_reputation(&p, &c, &np2), 0); let np_past_acct = np_past.as_icrc_compatible_account().into(); let np2_acct = np2.as_icrc_compatible_account().into(); @@ -471,11 +471,11 @@ fn test_np_registration_and_check_in() { assert!(response.is_ok()); assert_eq!( - get_account_balance(&p, c, &np_past.as_icrc_compatible_account().into()), + get_account_balance(&p, &c, &np_past.as_icrc_compatible_account().into()), amount - amount_send - DC_TOKEN_TRANSFER_FEE_E9S ); assert_eq!( - get_account_balance(&p, c, &np2.as_icrc_compatible_account().into()), + get_account_balance(&p, &c, &np2.as_icrc_compatible_account().into()), amount_send ); @@ -483,7 +483,7 @@ fn test_np_registration_and_check_in() { let (np1, reg1) = np_register(&p, c, b"np1", 0); assert_eq!(reg1.unwrap_err(), "InsufficientFunds: account w7shl-xsw5s-kduqo-kx77s-nxs35-4zdh3-3tpob-nr4yc-2c6zw-qeyzj-rqe has 0 and requested 500000000".to_string()); assert_eq!( - get_account_balance(&p, c, &np1.as_icrc_compatible_account().into()), + get_account_balance(&p, &c, &np1.as_icrc_compatible_account().into()), 0u64 ); @@ -494,18 +494,18 @@ fn test_np_registration_and_check_in() { "Registration complete! Thank you. You have been charged 0.500000000 tokens".to_string() ); assert_eq!( - get_account_balance(&p, c, &np2.as_icrc_compatible_account().into()), + get_account_balance(&p, &c, &np2.as_icrc_compatible_account().into()), 9500000000u64 ); upgrade_test_canister(&p, c).expect("Canister upgrade failed"); assert_eq!( - get_account_balance(&p, c, &np2.as_icrc_compatible_account().into()), + get_account_balance(&p, &c, &np2.as_icrc_compatible_account().into()), 9500000000u64 ); assert_eq!( - get_account_balance(&p, c, &np1.as_icrc_compatible_account().into()), + get_account_balance(&p, &c, &np1.as_icrc_compatible_account().into()), 0u64 ); @@ -520,25 +520,25 @@ fn test_np_registration_and_check_in() { // Now np2 got a reward of 50 tokens distributed to it // The balance is 50 (reward) + 10 (np_past transfer) - 0.5 (reg fee) - 0.5 (check in) = 59000000000 e9s assert_eq!( - get_account_balance(&p, c, &np2.as_icrc_compatible_account().into()), + get_account_balance(&p, &c, &np2.as_icrc_compatible_account().into()), 59000000000u64 ); upgrade_test_canister(&p, c).expect("Canister upgrade failed"); assert_eq!( - get_account_balance(&p, c, &np2.as_icrc_compatible_account().into()), + get_account_balance(&p, &c, &np2.as_icrc_compatible_account().into()), 59000000000u64 ); assert_eq!( - get_account_balance(&p, c, &np1.as_icrc_compatible_account().into()), + get_account_balance(&p, &c, &np1.as_icrc_compatible_account().into()), 0u64 ); // Registration itself does not affect the reputation. reward_e9s_per_block_recalculate(); - assert_eq!(identity_reputation_get(&p, c, &np1.to_bytes_verifying()), 0); - assert_eq!(identity_reputation_get(&p, c, &np2.to_bytes_verifying()), 0); + assert_eq!(get_id_reputation(&p, &c, &np1), 0); + assert_eq!(get_id_reputation(&p, &c, &np2), 0); } #[test] @@ -578,12 +578,12 @@ fn test_reputation() { test_ffwd_to_next_block(ts_ns, &p, c); - assert_eq!(identity_reputation_get(&p, c, &np1.to_bytes_verifying()), 0); - assert_eq!(identity_reputation_get(&p, c, &np2.to_bytes_verifying()), 0); - assert_eq!(identity_reputation_get(&p, c, &np3.to_bytes_verifying()), 0); + assert_eq!(get_id_reputation(&p, &c, &np1), 0); + assert_eq!(get_id_reputation(&p, &c, &np2), 0); + assert_eq!(get_id_reputation(&p, &c, &np3), 0); - assert_eq!(identity_reputation_get(&p, c, &u1.to_bytes_verifying()), 0); - assert_eq!(identity_reputation_get(&p, c, &u2.to_bytes_verifying()), 0); + assert_eq!(get_id_reputation(&p, &c, &u1), 0); + assert_eq!(get_id_reputation(&p, &c, &u2), 0); } // FIXME: add tests for profile update @@ -639,8 +639,9 @@ fn contract_sign_request( can: &Principal, requester_dcc_id: &DccIdentity, provider_pubkey_bytes: &[u8], - offering_id: String, + offering_id: &str, memo: String, + amount: TokenAmountE9s, ) -> Result { let requester_pubkey_bytes = requester_dcc_id.to_bytes_verifying(); let req = ContractSignRequest::new( @@ -648,14 +649,14 @@ fn contract_sign_request( "invalid test ssh key".to_string(), "invalid test contact info".to_string(), provider_pubkey_bytes, - offering_id, + offering_id.to_string(), None, None, None, - 100, + amount, vec![PaymentEntryWithAmount { e: PaymentEntry::new("on_demand", "hour", 1), - amount_e9s: 100, + amount_e9s: amount, }], None, memo, @@ -674,12 +675,12 @@ fn contract_sign_request( fn contracts_list_pending( pic: &PocketIc, - can: Principal, + can: &Principal, pubkey_bytes: Option>, ) -> Vec<(ContractId, ContractReqSerialized)> { query_check_and_decode!( pic, - can, + *can, "contracts_list_pending", Encode!(&pubkey_bytes).unwrap(), Vec<(ContractId, ContractReqSerialized)> @@ -688,14 +689,14 @@ fn contracts_list_pending( fn contract_sign_reply( pic: &PocketIc, - can: Principal, + can: &Principal, replier_dcc_id: &DccIdentity, requester_dcc_id: &DccIdentity, reply: &ContractSignReply, ) -> Result { let payload_bytes = borsh::to_vec(reply).unwrap(); let payload_sig_bytes = replier_dcc_id.sign(&payload_bytes).unwrap().to_bytes(); - update_check_and_decode!(pic, can, requester_dcc_id.to_ic_principal(), "contract_sign_reply", Encode!( + update_check_and_decode!(pic, *can, requester_dcc_id.to_ic_principal(), "contract_sign_reply", Encode!( &replier_dcc_id.to_bytes_verifying(), &payload_bytes, &payload_sig_bytes @@ -754,11 +755,58 @@ fn test_offerings() { let search_results = offering_search(&p, c, "memory < 512MB"); assert_eq!(search_results.len(), 0); - // Sign a contract + // Test for contract signing let offering_id = offering.matches_search("memory >= 512MB")[0].clone(); assert_eq!(offering_id, "xxx-small"); let u1 = user_register(&p, c, b"u1", 2 * DC_TOKEN_DECIMALS_DIV).0; + + assert_eq!(get_id_reputation(&p, &c, &u1), 0); + assert_eq!(get_id_reputation(&p, &c, &np1), 0); + + // Test the rejection of a contract signing + contract_req_sign_flow(&p, &c, &np1, &u1, &offering_id, false); + + // Test the acceptance of a contract signing + contract_req_sign_flow(&p, &c, &np1, &u1, &offering_id, true); + let np1_rep = get_id_reputation(&p, &c, &np1); + let u1_rep = get_id_reputation(&p, &c, &u1); + + let pending_contracts = contracts_list_pending(&p, &c, None); + assert_eq!(pending_contracts.len(), 0); + let pending_contracts = contracts_list_pending(&p, &c, Some(np1.to_bytes_verifying())); + assert_eq!(pending_contracts.len(), 0); + test_ffwd_to_next_block(ts_ns, &p, c); + + let pending_contracts = contracts_list_pending(&p, &c, None); + assert_eq!(pending_contracts.len(), 0); + let pending_contracts = contracts_list_pending(&p, &c, Some(np1.to_bytes_verifying())); + assert_eq!(pending_contracts.len(), 0); + + assert_eq!(get_id_reputation(&p, &c, &np1), np1_rep); + assert_eq!(get_id_reputation(&p, &c, &u1), u1_rep); +} + +fn contract_req_sign_flow( + p: &PocketIc, + c: &Principal, + np1: &DccIdentity, + u1: &DccIdentity, + offering_id: &str, + accept: bool, +) { + if accept { + println!("Testing an accept of a contract signing"); + } else { + println!("Testing a rejection of a contract signing"); + } + let np1_balance_before = get_account_balance(&p, c, &np1.as_icrc_compatible_account().into()); + let np1_rep_before = get_id_reputation(&p, c, &np1); + let u1_balance_before = get_account_balance(&p, c, &u1.as_icrc_compatible_account().into()); + let u1_rep_before = get_id_reputation(&p, c, &u1); + + let contract_amount: TokenAmountE9s = 1_000_000_000; + let contract_step_fee = contract_amount / 100; // 1% fee contract_sign_request( &p, &c, @@ -766,9 +814,24 @@ fn test_offerings() { &np1.to_bytes_verifying(), offering_id, "test_memo".to_string(), + contract_amount, ) .unwrap(); + assert_eq!( + get_account_balance(&p, c, &u1.as_icrc_compatible_account().into()), + u1_balance_before.clone() - contract_step_fee + ); + assert_eq!( + get_account_balance(&p, c, &np1.as_icrc_compatible_account().into()), + np1_balance_before + ); + assert_eq!(get_id_reputation(&p, c, &np1), np1_rep_before); + assert_eq!( + get_id_reputation(&p, c, &u1), + u1_rep_before + contract_step_fee + ); + let pending_contracts = contracts_list_pending(&p, c, None); assert_eq!(pending_contracts.len(), 1); @@ -785,21 +848,40 @@ fn test_offerings() { np1.to_bytes_verifying(), "test_memo_wrong", contract_id, - true, + accept, "Thank you for signing up", "Here are some details", ); let res = contract_sign_reply(&p, c, &np1, &u1, &reply).unwrap(); - assert_eq!(res, "Contract signing reply submitted! Thank you. You have been charged 0.000000001 tokens as a fee, and your reputation has been bumped accordingly"); + assert_eq!(res, "Contract signing reply submitted! Thank you. You have been charged 0.010000000 tokens as a fee, and your reputation has been bumped accordingly"); - let pending_contracts = contracts_list_pending(&p, c, None); - assert_eq!(pending_contracts.len(), 0); - let pending_contracts = contracts_list_pending(&p, c, Some(np1.to_bytes_verifying())); - assert_eq!(pending_contracts.len(), 0); - test_ffwd_to_next_block(ts_ns, &p, c); - - let pending_contracts = contracts_list_pending(&p, c, None); - assert_eq!(pending_contracts.len(), 0); - let pending_contracts = contracts_list_pending(&p, c, Some(np1.to_bytes_verifying())); - assert_eq!(pending_contracts.len(), 0); + if accept { + assert_eq!( + get_account_balance(&p, c, &u1.as_icrc_compatible_account().into()), + u1_balance_before - 2 * contract_step_fee - contract_amount + ); + assert_eq!( + get_account_balance(&p, c, &np1.as_icrc_compatible_account().into()), + np1_balance_before + contract_amount + ); + assert_eq!( + get_id_reputation(&p, c, &np1), + np1_rep_before + contract_step_fee + ); + assert_eq!( + get_id_reputation(&p, c, &u1), + u1_rep_before + contract_step_fee + ); + } else { + assert_eq!( + get_account_balance(&p, c, &u1.as_icrc_compatible_account().into()), + u1_balance_before - contract_step_fee + ); + assert_eq!( + get_account_balance(&p, c, &np1.as_icrc_compatible_account().into()), + np1_balance_before - contract_step_fee + ); + assert_eq!(get_id_reputation(&p, c, &np1), np1_rep_before); + assert_eq!(get_id_reputation(&p, c, &u1), u1_rep_before); + } }