diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index e2a4028..8b9a21d 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -714,9 +714,9 @@ dependencies = [ [[package]] name = "near-sys" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e307313276eaeced2ca95740b5639e1f3125b7c97f0a1151809d105f1aa8c6d3" +checksum = "397688591acf8d3ebf2c2485ba32d4b24fc10aad5334e3ad8ec0b7179bfdf06b" [[package]] name = "near-vm-errors" @@ -1291,9 +1291,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" [[package]] name = "toml_edit" diff --git a/contracts/donation/Cargo.toml b/contracts/donation/Cargo.toml index 78ffb4f..d282761 100644 --- a/contracts/donation/Cargo.toml +++ b/contracts/donation/Cargo.toml @@ -9,6 +9,7 @@ crate-type = ["cdylib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] near-sdk = "4.1.1" +# near-sdk = "5.0.0-alpha.2" # [profile.release] # removed as added to root Cargo.toml # codegen-units = 1 # # Tell `rustc` to optimize for small code size. diff --git a/contracts/donation/README.md b/contracts/donation/README.md index e5b56b0..724ddbd 100644 --- a/contracts/donation/README.md +++ b/contracts/donation/README.md @@ -75,6 +75,12 @@ pub struct Donation { } ``` +### Storage + +The storage-related methods (`storage_deposit`, `storage_withdraw` and `storage_balance_of`) are utilized for fungible token (FT) donations, where the user must prepay storage on this Donation contract - to cover the storage of the Donation data - before calling `ft_transfer_call` on the FT contract. + +This is a simplified version of the [Storage Management standard](https://nomicon.io/Standards/StorageManagement). + ### Contract Source Metadata _NB: Below implemented as per NEP 0330 (https://github.com/near/NEPs/blob/master/neps/nep-0330.md), with addition of `commit_hash`_ @@ -120,6 +126,13 @@ pub fn donate( ) -> Donation +// STORAGE + +pub fn storage_deposit(&mut self) -> U128 + +pub fn storage_withdraw(&mut self, amount: Option) -> U128 + + // OWNER #[payable] @@ -145,6 +158,7 @@ pub fn self_set_source_metadata(&mut self, source_metadata: ContractSourceMetada pub fn get_config(&self) -> Config + // DONATIONS pub fn get_donations(&self, from_index: Option, limit: Option) -> Vec @@ -171,10 +185,17 @@ pub fn get_donations_for_ft( limit: Option, ) -> Vec + +// STORAGE + +pub fn storage_balance_of(&self, account_id: &AccountId) -> U128 + + // OWNER pub fn get_owner(&self) -> AccountId + // SOURCE METADATA pub fn get_contract_source_metadata(&self) -> Option diff --git a/contracts/donation/out/main.wasm b/contracts/donation/out/main.wasm index 59c078a..db53105 100755 Binary files a/contracts/donation/out/main.wasm and b/contracts/donation/out/main.wasm differ diff --git a/contracts/donation/src/constants.rs b/contracts/donation/src/constants.rs index bfd75c4..a314523 100644 --- a/contracts/donation/src/constants.rs +++ b/contracts/donation/src/constants.rs @@ -1,4 +1,11 @@ +use crate::*; + pub const MAX_PROTOCOL_FEE_BASIS_POINTS: u32 = 1000; pub const MAX_REFERRAL_FEE_BASIS_POINTS: u32 = 200; pub const EVENT_JSON_PREFIX: &str = "EVENT_JSON:"; + +pub const TGAS: u64 = 1_000_000_000_000; // 1 TGAS +pub const XCC_GAS_DEFAULT: u64 = TGAS * 10; // 10 TGAS +pub const NO_DEPOSIT: Balance = 0; +pub const ONE_YOCTO: Balance = 1; diff --git a/contracts/donation/src/donations.rs b/contracts/donation/src/donations.rs index c2d50ed..f916aad 100644 --- a/contracts/donation/src/donations.rs +++ b/contracts/donation/src/donations.rs @@ -1,9 +1,11 @@ use crate::*; // Donation is the data structure that is stored within the contract + +// DEPRECATED (V1) #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Clone)] #[serde(crate = "near_sdk::serde")] -pub struct Donation { +pub struct DonationV1 { /// Unique identifier for the donation pub id: DonationId, /// ID of the donor @@ -26,118 +28,544 @@ pub struct Donation { pub referrer_fee: Option, } +// Donation is the data structure that is stored within the contract +#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct Donation { + /// Unique identifier for the donation + pub id: DonationId, + /// ID of the donor + pub donor_id: AccountId, + /// Amount donated + pub total_amount: u128, // changed from string to int for lower storage + consistency + /// FT id (e.g. "near") + pub ft_id: AccountId, + /// Optional message from the donor + pub message: Option, + /// Timestamp when the donation was made + pub donated_at_ms: TimestampMs, + /// ID of the account receiving the donation + pub recipient_id: AccountId, + /// Protocol fee + pub protocol_fee: u128, // changed from string to int for lower storage + consistency + /// Referrer ID + pub referrer_id: Option, + /// Referrer fee + pub referrer_fee: Option, // changed from string to int for lower storage + consistency +} + #[derive(BorshSerialize, BorshDeserialize)] pub enum VersionedDonation { + V1(DonationV1), Current(Donation), } impl From for Donation { fn from(donation: VersionedDonation) -> Self { match donation { + VersionedDonation::V1(v1) => Donation { + id: v1.id, + donor_id: v1.donor_id, + total_amount: v1.total_amount.0, + ft_id: v1.ft_id, + message: v1.message, + donated_at_ms: v1.donated_at_ms, + recipient_id: v1.recipient_id, + protocol_fee: v1.protocol_fee.0, + referrer_id: v1.referrer_id, + referrer_fee: v1.referrer_fee.map(|v| v.0), + }, VersionedDonation::Current(current) => current, } } } +/// Ephemeral-only (used in views) +#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct DonationExternal { + /// Unique identifier for the donation + pub id: DonationId, + /// ID of the donor + pub donor_id: AccountId, + /// Amount donated + pub total_amount: U128, + /// FT id (e.g. "near") + pub ft_id: AccountId, + /// Optional message from the donor + pub message: Option, + /// Timestamp when the donation was made + pub donated_at_ms: TimestampMs, + /// ID of the account receiving the donation + pub recipient_id: AccountId, + /// Protocol fee + pub protocol_fee: U128, + /// Referrer ID + pub referrer_id: Option, + /// Referrer fee + pub referrer_fee: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(crate = "near_sdk::serde")] +pub struct FtReceiverMsg { + pub recipient_id: AccountId, + pub referrer_id: Option, + pub message: Option, + pub bypass_protocol_fee: Option, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[serde(crate = "near_sdk::serde")] +pub enum TransferType { + DonationTransfer, + ProtocolFeeTransfer, + ReferrerFeeTransfer, +} + #[near_bindgen] impl Contract { + /// FT equivalent of donate, for use with FTs that implement NEP-144 + pub fn ft_on_transfer( + &mut self, + sender_id: AccountId, + amount: U128, + msg: String, + ) -> PromiseOrValue { + let ft_id = env::predecessor_account_id(); + let msg_json: FtReceiverMsg = near_sdk::serde_json::from_str(&msg) + .expect("Invalid msg string. Must implement FtReceiverMsg."); + log!(format!( + "Recipient ID {:?}, Referrer ID {:?}, Amount {}, Message {:?}", + msg_json.recipient_id, msg_json.referrer_id, amount.0, msg_json.message + )); + + // calculate amounts + let (protocol_fee, referrer_fee, remainder) = self.calculate_fees_and_remainder( + amount.0, + msg_json.referrer_id.clone(), + msg_json.bypass_protocol_fee, + ); + + // create and insert donation record + let initial_storage_usage = env::storage_usage(); + let donation = self.create_and_insert_donation_record( + sender_id.clone(), + amount, + ft_id.clone(), + msg_json.message.clone(), + env::block_timestamp_ms(), + msg_json.recipient_id.clone(), + U128::from(protocol_fee), + msg_json.referrer_id.clone(), + referrer_fee, + ); + + // verify and update storage balance for FT donation + self.verify_and_update_storage_balance(sender_id.clone(), initial_storage_usage); + + // transfer donation + log!(format!( + "Transferring donation {} ({}) to {}", + remainder, ft_id, msg_json.recipient_id + )); + self.handle_transfer_donation( + msg_json.recipient_id.clone(), + remainder, + remainder, + donation.clone(), + ); + + // NB: fees will be transferred in transfer_funds_callback after successful transfer of donation + + // return # unused tokens as per NEP-144 standard + PromiseOrValue::Value(U128(0)) + } + #[payable] pub fn donate( &mut self, recipient_id: AccountId, message: Option, - mut referrer_id: Option, + referrer_id: Option, bypass_protocol_fee: Option, - ) -> Donation { - // user has to pay for storage + ) -> PromiseOrValue { + // calculate amounts + let amount = env::attached_deposit(); + let (protocol_fee, referrer_fee, mut remainder) = self.calculate_fees_and_remainder( + amount.clone(), + referrer_id.clone(), + bypass_protocol_fee, + ); + + // create and insert donation record let initial_storage_usage = env::storage_usage(); + let donation = self.create_and_insert_donation_record( + env::predecessor_account_id(), + U128::from(amount), + AccountId::new_unchecked("near".to_string()), + message, + env::block_timestamp_ms(), + recipient_id.clone(), + U128::from(protocol_fee), + referrer_id.clone(), + referrer_fee, + ); - // calculate protocol fee (unless bypassed) - let amount = env::attached_deposit(); + // assert that donation after fees > storage cost + let required_deposit = calculate_required_storage_deposit(initial_storage_usage); + require!( + remainder > required_deposit, + format!( + "Must attach {} yoctoNEAR to cover storage", + required_deposit + ) + ); + remainder -= required_deposit; + + // transfer donation + log!(format!( + "Transferring donation {} to {}", + remainder, recipient_id + )); + self.handle_transfer_donation(recipient_id.clone(), remainder, remainder, donation.clone()) + // * NB: fees will be transferred in transfer_funds_callback after successful transfer of donation + } + + pub(crate) fn calculate_fees_and_remainder( + &self, + amount: u128, + referrer_id: Option, + bypass_protocol_fee: Option, + ) -> (u128, Option, u128) { + // calculate protocol fee + let mut remainder = amount; let protocol_fee = if bypass_protocol_fee.unwrap_or(false) { 0 } else { self.calculate_protocol_fee(amount) }; - let mut remainder: u128 = amount; + remainder -= protocol_fee; // calculate referrer fee, if applicable let mut referrer_fee = None; if let Some(_referrer_id) = referrer_id.clone() { - // if referrer ID is provided, check that it isn't caller or recipient. If it is, set to None - if _referrer_id == env::predecessor_account_id() || _referrer_id == recipient_id { - referrer_id = None; - } else { - let referrer_amount = self.calculate_referrer_fee(amount); - remainder -= referrer_amount; - referrer_fee = Some(U128::from(referrer_amount)); - } + let referrer_amount = self.calculate_referrer_fee(amount); + remainder -= referrer_amount; + referrer_fee = Some(U128::from(referrer_amount)); } - // get donation count, which will be incremented to create the unique donation ID - let donation_count = self.donations_by_id.len(); + (protocol_fee, referrer_fee, remainder) + } - // format donation record + pub(crate) fn create_and_insert_donation_record( + &mut self, + donor_id: AccountId, + total_amount: U128, + ft_id: AccountId, + message: Option, + donated_at_ms: TimestampMs, + recipient_id: AccountId, + protocol_fee: U128, + referrer_id: Option, + referrer_fee: Option, + ) -> Donation { let donation = Donation { - id: (donation_count + 1) as DonationId, - donor_id: env::predecessor_account_id(), - total_amount: U128::from(amount), - ft_id: AccountId::new_unchecked("near".to_string()), // for now, only NEAR is supported + id: self.next_donation_id, + donor_id, + total_amount: total_amount.0, + ft_id, message, - donated_at_ms: env::block_timestamp_ms(), - recipient_id: recipient_id.clone(), - protocol_fee: U128::from(protocol_fee), - referrer_id: referrer_id.clone(), - referrer_fee, + donated_at_ms, + recipient_id, + protocol_fee: protocol_fee.0, + referrer_id, + referrer_fee: referrer_fee.map(|v| v.0), }; + // increment next_donation_id + self.next_donation_id += 1; + // insert mapping records - self.insert_donation_record(&donation); + self.insert_donation_record_internal(&donation); - // assert that donation after fees covers storage cost + donation + } + + pub(crate) fn verify_and_update_storage_balance( + &mut self, + sender_id: AccountId, + initial_storage_usage: u64, + ) { + // verify that deposit is sufficient to cover storage let required_deposit = calculate_required_storage_deposit(initial_storage_usage); - require!( - remainder > required_deposit, - format!( - "Must attach {} yoctoNEAR to cover storage", - required_deposit - ) + let storage_balance = self.storage_balance_of(&sender_id); + assert!( + storage_balance.0 >= required_deposit, + "{} must add storage deposit of at least {} yoctoNEAR to cover Donation storage", + sender_id, + required_deposit ); - remainder -= required_deposit; - // transfer protocol fee - if protocol_fee > 0 { - log!(format!( - "Transferring protocol fee {} to {}", - protocol_fee, self.protocol_fee_recipient_account - )); - Promise::new(self.protocol_fee_recipient_account.clone()).transfer(protocol_fee); + log!("Old storage balance: {}", storage_balance.0); + // deduct storage deposit from user's balance + let new_storage_balance = storage_balance.0 - required_deposit; + self.storage_deposits + .insert(&sender_id, &new_storage_balance); + log!("New storage balance: {}", new_storage_balance); + log!(format!( + "Deducted {} yoctoNEAR from {}'s storage balance to cover storage", + required_deposit, sender_id + )); + } + + pub(crate) fn handle_transfer( + &self, + recipient_id: AccountId, + amount: u128, + remainder: Balance, + donation: Donation, + transfer_type: TransferType, + ) -> PromiseOrValue { + if donation.ft_id == AccountId::new_unchecked("near".to_string()) { + PromiseOrValue::Promise( + Promise::new(recipient_id).transfer(amount).then( + Self::ext(env::current_account_id()) + .with_static_gas(Gas(XCC_GAS_DEFAULT)) + .transfer_funds_callback(remainder, donation.clone(), transfer_type), + ), + ) + } else { + let ft_transfer_args = json!({ "receiver_id": recipient_id, "amount": amount }) + .to_string() + .into_bytes(); + PromiseOrValue::Promise( + Promise::new(donation.ft_id.clone()) + .function_call( + "ft_transfer".to_string(), + ft_transfer_args, + ONE_YOCTO, + Gas(XCC_GAS_DEFAULT), + ) + .then( + Self::ext(env::current_account_id()) + .with_static_gas(Gas(XCC_GAS_DEFAULT)) + .transfer_funds_callback(remainder, donation.clone(), transfer_type), + ), + ) } + } + + pub(crate) fn handle_transfer_donation( + &self, + recipient_id: AccountId, + amount: u128, + remainder: Balance, + donation: Donation, + ) -> PromiseOrValue { + self.handle_transfer( + recipient_id, + amount, + remainder, + donation, + TransferType::DonationTransfer, + ) + } + + pub(crate) fn handle_transfer_protocol_fee( + &self, + recipient_id: AccountId, + amount: u128, + remainder: Balance, + donation: Donation, + ) -> PromiseOrValue { + self.handle_transfer( + recipient_id, + amount, + remainder, + donation, + TransferType::ProtocolFeeTransfer, + ) + } + + pub(crate) fn handle_transfer_referrer_fee( + &self, + recipient_id: AccountId, + amount: u128, + remainder: Balance, + donation: Donation, + ) -> PromiseOrValue { + self.handle_transfer( + recipient_id, + amount, + remainder, + donation, + TransferType::ReferrerFeeTransfer, + ) + } + + /// Verifies whether donation & fees have been paid out for a given donation + #[private] + pub fn transfer_funds_callback( + &mut self, + remainder: Balance, + mut donation: Donation, + transfer_type: TransferType, + #[callback_result] call_result: Result<(), PromiseError>, + ) -> Option { + let is_ft_transfer = donation.ft_id != AccountId::new_unchecked("near".to_string()); + if call_result.is_err() { + // ERROR CASE HANDLING + // 1. If donation transfer failed, delete Donation record and return all funds to donor. NB: fees have not been transferred yet. + // 2. If protocol fee transfer failed, update donation record to indicate protocol fee of "0". NB: donation has already been transferred to recipient and this cannot be reversed. + // 3. If referrer fee transfer failed, update donation record to indicate referrer fee of "0". NB: donation has already been transferred to recipient and this cannot be reversed. + match transfer_type { + TransferType::DonationTransfer => { + log!(format!( + "Error transferring donation {:?} to {}. Returning funds to donor.", + donation.total_amount, donation.recipient_id + )); + // return funds to donor + if is_ft_transfer { + let donation_transfer_args = + json!({ "receiver_id": donation.donor_id, "amount": donation.total_amount.clone() }) + .to_string() + .into_bytes(); + Promise::new(AccountId::new_unchecked(donation.ft_id.to_string())) + .function_call( + "ft_transfer".to_string(), + donation_transfer_args, + ONE_YOCTO, + Gas(XCC_GAS_DEFAULT), + ); + } else { + Promise::new(donation.donor_id.clone()).transfer(donation.total_amount); + } + // delete donation record, and refund freed storage cost to donor's storage balance + let initial_storage_usage = env::storage_usage(); + self.remove_donation_record_internal(&donation); + let storage_freed = initial_storage_usage - env::storage_usage(); + let cost_freed = env::storage_byte_cost() * Balance::from(storage_freed); + let storage_balance = self.storage_balance_of(&donation.donor_id); + let new_storage_balance = storage_balance.0 + cost_freed; + log!("Old storage balance: {}", storage_balance.0); + log!("New storage balance: {}", new_storage_balance); + self.storage_deposits + .insert(&donation.donor_id, &new_storage_balance); // TODO: check if this is hackable, e.g. if user can withdraw all their storage before this callback runs and therefore get a higher refund + log!(format!( + "Refunded {} yoctoNEAR to {}'s storage balance for freed storage", + cost_freed, donation.donor_id + )); + None + } + TransferType::ProtocolFeeTransfer => { + log!(format!( + "Error transferring protocol fee {:?} to {}. Returning funds to donor.", + donation.protocol_fee, self.protocol_fee_recipient_account + )); + // return funds to donor + if is_ft_transfer { + let donation_transfer_args = + json!({ "receiver_id": donation.donor_id, "amount": donation.protocol_fee }) + .to_string() + .into_bytes(); + Promise::new(AccountId::new_unchecked(donation.ft_id.to_string())) + .function_call( + "ft_transfer".to_string(), + donation_transfer_args, + ONE_YOCTO, + Gas(XCC_GAS_DEFAULT), + ); + } else { + Promise::new(donation.donor_id.clone()).transfer(donation.protocol_fee); + } + // update fee on Donation record to indicate error transferring funds + donation.protocol_fee = 0; + self.donations_by_id.insert( + &donation.id.clone(), + &VersionedDonation::Current(donation.clone()), + ); + Some(self.format_donation(&donation)) + } + TransferType::ReferrerFeeTransfer => { + log!(format!( + "Error transferring referrer fee {:?} to {:?}. Returning funds to donor.", + donation.referrer_fee, donation.referrer_id + )); + // return funds to donor + if is_ft_transfer { + let donation_transfer_args = + json!({ "receiver_id": donation.donor_id, "amount": donation.referrer_fee }) + .to_string() + .into_bytes(); + Promise::new(AccountId::new_unchecked(donation.ft_id.to_string())) + .function_call( + "ft_transfer".to_string(), + donation_transfer_args, + ONE_YOCTO, + Gas(XCC_GAS_DEFAULT), + ); + } else { + Promise::new(donation.donor_id.clone()) + .transfer(donation.referrer_fee.unwrap()); + } + // update fee on Donation record to indicate error transferring funds + donation.referrer_fee = Some(0); + self.donations_by_id.insert( + &donation.id.clone(), + &VersionedDonation::Current(donation.clone()), + ); + Some(self.format_donation(&donation)) + } + } + } else { + // SUCCESS CASE HANDLING + if transfer_type == TransferType::DonationTransfer { + log!(format!( + "Successfully transferred donation {} to {}!", + remainder, donation.recipient_id + )); - // transfer referrer fee - if let (Some(referrer_fee), Some(referrer_id)) = (referrer_fee, referrer_id) { - if referrer_fee.0 > 0 { + // transfer protocol fee log!(format!( - "Transferring referrer fee {} to {}", - referrer_fee.0, referrer_id + "Transferring protocol fee {:?} ({}) to {}", + donation.protocol_fee, donation.ft_id, self.protocol_fee_recipient_account )); - Promise::new(referrer_id).transfer(referrer_fee.0); - } - } + self.handle_transfer_protocol_fee( + self.protocol_fee_recipient_account.clone(), + donation.protocol_fee.clone(), + remainder, + donation.clone(), + ); - // transfer donation - log!(format!( - "Transferring donation {} to {}", - remainder, recipient_id - )); - Promise::new(recipient_id).transfer(remainder); + // transfer referrer fee + if let (Some(referrer_fee), Some(referrer_id)) = + (donation.referrer_fee.clone(), donation.referrer_id.clone()) + { + log!(format!( + "Transferring referrer fee {:?} ({}) to {}", + referrer_fee.clone(), + donation.ft_id, + referrer_id + )); + self.handle_transfer_referrer_fee( + referrer_id.clone(), + referrer_fee.clone(), + remainder, + donation.clone(), + ); + } - // log event - log_donation_event(&donation); + // log event indicating successful donation/transfer! + log_donation_event(&self.format_donation(&donation)); - // return donation - donation + // return donation + Some(self.format_donation(&donation)) + } else { + None + } + } } pub(crate) fn calculate_protocol_fee(&self, amount: u128) -> u128 { @@ -154,7 +582,7 @@ impl Contract { fee_amount / total_basis_points } - pub(crate) fn insert_donation_record(&mut self, donation: &Donation) { + pub(crate) fn insert_donation_record_internal(&mut self, donation: &Donation) { self.donations_by_id .insert(&donation.id, &VersionedDonation::Current(donation.clone())); // add to donations-by-recipient mapping @@ -199,22 +627,53 @@ impl Contract { self.donation_ids_by_ft_id .insert(&donation.ft_id, &donation_ids_by_ft_set); // add to total donations amount - self.total_donations_amount += donation.total_amount.0; + self.total_donations_amount += donation.total_amount; // add to net donations amount - let mut net_donation_amount = donation.total_amount.0 - donation.protocol_fee.0; + let mut net_donation_amount = donation.total_amount - donation.protocol_fee; if let Some(referrer_fee) = donation.referrer_fee { - net_donation_amount -= referrer_fee.0; - self.total_referrer_fees += referrer_fee.0; + net_donation_amount -= referrer_fee; + self.total_referrer_fees += referrer_fee; } self.net_donations_amount += net_donation_amount; // add to total protocol fees - self.total_protocol_fees += donation.protocol_fee.0; + self.total_protocol_fees += donation.protocol_fee; + } + + pub(crate) fn remove_donation_record_internal(&mut self, donation: &Donation) { + self.donations_by_id.remove(&donation.id); + // remove from donations-by-recipient mapping + let mut donation_ids_by_recipient_set = self + .donation_ids_by_recipient_id + .get(&donation.recipient_id) + .unwrap(); + donation_ids_by_recipient_set.remove(&donation.id); + self.donation_ids_by_recipient_id + .insert(&donation.recipient_id, &donation_ids_by_recipient_set); + + // remove from donations-by-donor mapping + let mut donation_ids_by_donor_set = self + .donation_ids_by_donor_id + .get(&donation.donor_id) + .unwrap(); + donation_ids_by_donor_set.remove(&donation.id); + self.donation_ids_by_donor_id + .insert(&donation.donor_id, &donation_ids_by_donor_set); + + // remove from donations-by-ft mapping + let mut donation_ids_by_ft_set = self.donation_ids_by_ft_id.get(&donation.ft_id).unwrap(); + donation_ids_by_ft_set.remove(&donation.id); + self.donation_ids_by_ft_id + .insert(&donation.ft_id, &donation_ids_by_ft_set); } // GETTERS // get_donations // get_matching_pool_balance - pub fn get_donations(&self, from_index: Option, limit: Option) -> Vec { + pub fn get_donations( + &self, + from_index: Option, + limit: Option, + ) -> Vec { let start_index: u128 = from_index.unwrap_or_default(); assert!( (self.donations_by_id.len() as u128) >= start_index, @@ -226,14 +685,14 @@ impl Contract { .iter() .skip(start_index as usize) .take(limit) - .map(|(_, v)| Donation::from(v)) + .map(|(_, v)| self.format_donation(&Donation::from(v))) .collect() } - pub fn get_donation_by_id(&self, donation_id: DonationId) -> Option { + pub fn get_donation_by_id(&self, donation_id: DonationId) -> Option { self.donations_by_id .get(&donation_id) - .map(|v| Donation::from(v)) + .map(|v| self.format_donation(&Donation::from(v))) } pub fn get_donations_for_recipient( @@ -241,7 +700,7 @@ impl Contract { recipient_id: AccountId, from_index: Option, limit: Option, - ) -> Vec { + ) -> Vec { let start_index: u128 = from_index.unwrap_or_default(); // TODO: ADD BELOW BACK IN // assert!( @@ -257,7 +716,11 @@ impl Contract { .iter() .skip(start_index as usize) .take(limit) - .map(|donation_id| Donation::from(self.donations_by_id.get(&donation_id).unwrap())) + .map(|donation_id| { + self.format_donation(&Donation::from( + self.donations_by_id.get(&donation_id).unwrap(), + )) + }) .collect() } else { vec![] @@ -269,7 +732,7 @@ impl Contract { donor_id: AccountId, from_index: Option, limit: Option, - ) -> Vec { + ) -> Vec { let start_index: u128 = from_index.unwrap_or_default(); // TODO: ADD BELOW BACK IN // assert!( @@ -285,7 +748,11 @@ impl Contract { .iter() .skip(start_index as usize) .take(limit) - .map(|donation_id| Donation::from(self.donations_by_id.get(&donation_id).unwrap())) + .map(|donation_id| { + self.format_donation(&Donation::from( + self.donations_by_id.get(&donation_id).unwrap(), + )) + }) .collect() } else { vec![] @@ -297,7 +764,7 @@ impl Contract { ft_id: AccountId, from_index: Option, limit: Option, - ) -> Vec { + ) -> Vec { let start_index: u128 = from_index.unwrap_or_default(); // TODO: ADD BELOW BACK IN // assert!( @@ -313,10 +780,29 @@ impl Contract { .iter() .skip(start_index as usize) .take(limit) - .map(|donation_id| Donation::from(self.donations_by_id.get(&donation_id).unwrap())) + .map(|donation_id| { + self.format_donation(&Donation::from( + self.donations_by_id.get(&donation_id).unwrap(), + )) + }) .collect() } else { vec![] } } + + pub(crate) fn format_donation(&self, donation: &Donation) -> DonationExternal { + DonationExternal { + id: donation.id, + donor_id: donation.donor_id.clone(), + total_amount: U128(donation.total_amount), + ft_id: donation.ft_id.clone(), + message: donation.message.clone(), + donated_at_ms: donation.donated_at_ms, + recipient_id: donation.recipient_id.clone(), + protocol_fee: U128(donation.protocol_fee), + referrer_id: donation.referrer_id.clone(), + referrer_fee: donation.referrer_fee.map(|v| U128(v)), + } + } } diff --git a/contracts/donation/src/events.rs b/contracts/donation/src/events.rs index 2c64678..54073d6 100644 --- a/contracts/donation/src/events.rs +++ b/contracts/donation/src/events.rs @@ -1,7 +1,7 @@ use crate::*; /// donation -pub(crate) fn log_donation_event(donation: &Donation) { +pub(crate) fn log_donation_event(donation: &DonationExternal) { env::log_str( format!( "{}{}", diff --git a/contracts/donation/src/lib.rs b/contracts/donation/src/lib.rs index 51b313c..a04a1c5 100644 --- a/contracts/donation/src/lib.rs +++ b/contracts/donation/src/lib.rs @@ -3,8 +3,8 @@ use near_sdk::collections::{LazyOption, LookupMap, UnorderedMap, UnorderedSet}; use near_sdk::json_types::{U128, U64}; use near_sdk::serde::{Deserialize, Serialize}; use near_sdk::{ - env, log, near_bindgen, require, serde_json::json, AccountId, Balance, BorshStorageKey, - PanicOnDefault, Promise, + env, log, near_bindgen, require, serde_json::json, AccountId, Balance, BorshStorageKey, Gas, + PanicOnDefault, Promise, PromiseError, PromiseOrValue, }; pub mod constants; @@ -13,6 +13,7 @@ pub mod events; pub mod internal; pub mod owner; pub mod source; +pub mod storage; pub mod utils; pub use crate::constants::*; pub use crate::donations::*; @@ -20,12 +21,13 @@ pub use crate::events::*; pub use crate::internal::*; pub use crate::owner::*; pub use crate::source::*; +pub use crate::storage::*; pub use crate::utils::*; type DonationId = u64; type TimestampMs = u64; -/// DEPRECATED (V1) Registry Contract +/// DEPRECATED (V1) Donation Contract #[near_bindgen] #[derive(BorshDeserialize, BorshSerialize)] pub struct ContractV1 { @@ -41,10 +43,10 @@ pub struct ContractV1 { donation_ids_by_ft_id: LookupMap>, } -/// CURRENT Registry Contract +/// DEPRECATED (V2) Donation Contract #[near_bindgen] #[derive(BorshDeserialize, BorshSerialize)] -pub struct Contract { +pub struct ContractV2 { /// Contract "source" metadata, as specified in NEP 0330 (https://github.com/near/NEPs/blob/master/neps/nep-0330.md), with addition of `commit_hash` contract_source_metadata: LazyOption, owner: AccountId, @@ -56,25 +58,59 @@ pub struct Contract { donation_ids_by_donor_id: LookupMap>, donation_ids_by_ft_id: LookupMap>, total_donations_amount: Balance, // Add total_donations_amount to track total donations amount without iterating through all donations - net_donations_amount: Balance, // Add net_donations_amount to track net donations amount (after fees) without iterating through all donations - total_protocol_fees: Balance, // Add total_protocol_fees to track total protocol fees without iterating through all donations - total_referrer_fees: Balance, // Add total_referrer_fees to track total referral fees without iterating through all donations + net_donations_amount: Balance, // Add net_donations_amount to track net donations amount (after fees) without iterating through all donations + total_protocol_fees: Balance, // Add total_protocol_fees to track total protocol fees without iterating through all donations + total_referrer_fees: Balance, // Add total_referrer_fees to track total referral fees without iterating through all donations } - -#[derive(BorshSerialize, BorshDeserialize)] -pub enum VersionedContract { - Current(Contract), +/// CURRENT Donation Contract +#[near_bindgen] +#[derive(BorshDeserialize, BorshSerialize)] +pub struct Contract { + /// Contract "source" metadata, as specified in NEP 0330 (https://github.com/near/NEPs/blob/master/neps/nep-0330.md), with addition of `commit_hash` + contract_source_metadata: LazyOption, + owner: AccountId, + protocol_fee_basis_points: u32, + referral_fee_basis_points: u32, + protocol_fee_recipient_account: AccountId, + donations_by_id: UnorderedMap, + donation_ids_by_recipient_id: LookupMap>, + donation_ids_by_donor_id: LookupMap>, + donation_ids_by_ft_id: LookupMap>, + total_donations_amount: Balance, + net_donations_amount: Balance, + total_protocol_fees: Balance, + total_referrer_fees: Balance, + next_donation_id: DonationId, // Add next_donation_id to track next donation id and handle failed donations without accidental overwrites + storage_deposits: UnorderedMap, // Add storage_deposits to track storage deposits for FTs } +// #[derive(BorshSerialize, BorshDeserialize)] +// pub enum VersionedContract { +// Current(Contract), +// V1(ContractV1), +// } -/// Convert VersionedContract to Contract -impl From for Contract { - fn from(contract: VersionedContract) -> Self { - match contract { - VersionedContract::Current(current) => current, - } - } -} +// /// Convert VersionedContract to Contract +// impl From for Contract { +// fn from(contract: VersionedContract) -> Self { +// match contract { +// VersionedContract::Current(current) => current, +// VersionedContract::V1(v1) => Contract { +// contract_source_metadata: v1.contract_source_metadata, +// owner: v1.owner, +// protocol_fee_basis_points: v1.protocol_fee_basis_points, +// referral_fee_basis_points: v1.referral_fee_basis_points, +// protocol_fee_recipient_account: v1.protocol_fee_recipient_account, +// donations_by_id: v1.donations_by_id, +// donation_ids_by_recipient_id: v1.donation_ids_by_recipient_id, +// donation_ids_by_donor_id: v1.donation_ids_by_donor_id, +// donation_ids_by_ft_id: v1.donation_ids_by_ft_id, +// next_donation_id: 0, +// storage_deposits: v1.storage_deposits, +// }, +// } +// } +// } /// NOT stored in contract storage; only used for get_config response #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Clone)] @@ -101,6 +137,7 @@ pub enum StorageKey { DonationIdsByFtId, DonationIdsByFtIdInner { ft_id: AccountId }, SourceMetadata, + StorageDeposits, } #[near_bindgen] @@ -131,6 +168,8 @@ impl Contract { StorageKey::SourceMetadata, Some(&VersionedContractSourceMetadata::Current(source_metadata)), ), + next_donation_id: 1, + storage_deposits: UnorderedMap::new(StorageKey::StorageDeposits), } } @@ -189,6 +228,31 @@ impl Contract { // contract_source_metadata: old_state.contract_source_metadata, // } // } + + // LEAVING FOR REFERENCE - this is the initFunction used in upgrade from v2.0.0 to v3.0.0 + // #[private] + // #[init(ignore_state)] + // pub fn migrate() -> Self { + // let old_state: ContractV2 = env::state_read().expect("state read failed"); + // let num_donations = old_state.donations_by_id.len(); + // Self { + // owner: old_state.owner, + // protocol_fee_basis_points: old_state.protocol_fee_basis_points, + // referral_fee_basis_points: old_state.referral_fee_basis_points, + // protocol_fee_recipient_account: old_state.protocol_fee_recipient_account, + // donations_by_id: old_state.donations_by_id, + // donation_ids_by_recipient_id: old_state.donation_ids_by_recipient_id, + // donation_ids_by_donor_id: old_state.donation_ids_by_donor_id, + // donation_ids_by_ft_id: old_state.donation_ids_by_ft_id, + // total_donations_amount: old_state.total_donations_amount, + // net_donations_amount: old_state.net_donations_amount, + // total_protocol_fees: old_state.total_protocol_fees, + // total_referrer_fees: old_state.total_referrer_fees, + // contract_source_metadata: old_state.contract_source_metadata, + // next_donation_id: num_donations as u64 + 1, + // storage_deposits: UnorderedMap::new(StorageKey::StorageDeposits), + // } + // } } impl Default for Contract { @@ -216,6 +280,8 @@ impl Default for Contract { }, )), ), + next_donation_id: 1, + storage_deposits: UnorderedMap::new(StorageKey::StorageDeposits), } } } diff --git a/contracts/donation/src/storage.rs b/contracts/donation/src/storage.rs new file mode 100644 index 0000000..ff1509b --- /dev/null +++ b/contracts/donation/src/storage.rs @@ -0,0 +1,56 @@ +use crate::*; + +#[near_bindgen] +impl Contract { + #[payable] + pub fn storage_deposit(&mut self) -> U128 { + let mut deposit = env::attached_deposit(); + let initial_storage_usage = env::storage_usage(); + let existing_mapping = self.storage_deposits.get(&env::predecessor_account_id()); + if existing_mapping.is_none() { + // insert record here and check how much storage was used, then subtract that cost from the deposit + self.storage_deposits + .insert(&env::predecessor_account_id(), &0); + let storage_usage = env::storage_usage() - initial_storage_usage; + let required_deposit = storage_usage as u128 * env::storage_byte_cost(); + assert!( + deposit >= required_deposit, + "The deposit is less than the required storage amount." + ); + deposit -= required_deposit; + } + let account_id = env::predecessor_account_id(); + let storage_balance = self.storage_balance_of(&account_id); + let new_storage_balance = storage_balance.0 + deposit; + self.storage_deposits + .insert(&account_id, &new_storage_balance); + new_storage_balance.into() + } + + pub fn storage_withdraw(&mut self, amount: Option) -> U128 { + let account_id = env::predecessor_account_id(); + let storage_balance = self.storage_balance_of(&account_id); + let amount = amount.map(|a| a.0).unwrap_or(storage_balance.0); + assert!( + amount <= storage_balance.0, + "The withdrawal amount can't exceed the account storage balance." + ); + let remainder = storage_balance.0 - amount; + if remainder > 0 { + self.storage_deposits.insert(&account_id, &remainder); + Promise::new(account_id).transfer(amount); + } else { + // remove mapping and refund user for freed storage + let initial_storage_usage = env::storage_usage(); + self.storage_deposits.remove(&account_id); + let storage_usage = initial_storage_usage - env::storage_usage(); + let refund = storage_usage as u128 * env::storage_byte_cost(); + Promise::new(account_id).transfer(refund); + } + remainder.into() + } + + pub fn storage_balance_of(&self, account_id: &AccountId) -> U128 { + self.storage_deposits.get(account_id).unwrap_or(0).into() + } +} diff --git a/contracts/test/donation/config.ts b/contracts/test/donation/config.ts index 4f0d257..b44af26 100644 --- a/contracts/test/donation/config.ts +++ b/contracts/test/donation/config.ts @@ -1,4 +1,4 @@ -export const contractId = "dev-1698342270489-65130206647584"; +export const contractId = "dev-1705952970616-18051433080691"; export const networkId = "testnet"; export const nodeUrl = `https://rpc.${networkId}.near.org`;