DEV Community

Ganesh oli
Ganesh oli

Posted on

Donation ink! smart contract

Donation smart contract is useful when you are learning substrate based ink! smart contract. This contract is based on ink! v4.0.0 and use Vec and Mapping data structure. Vec(i.e contiguous growable array type) is used to store and return Donation list and Mapping (i.e key-value pairs directly into contract storage) which is used to store Donation (i.e value) with respect to donation_id (i.e key). There are also other data structure like AccountId (i.e 32-byte type), u128 (to store balance) and i32 (to store donation id).


Through this contract you can:

  1. Initialize beneficiary account while contract deployment.
  2. Can change beneficiary at any time.
  3. Donate the beneficiary address.
  4. Can view donation list.

Before start writing ink! smart contract, you need to setup rust, cargo and cargo contract (i.e Setup and deployment tool for developing Wasm based smart contracts via ink!). You can setup rust and cargo from here and can install latest cargo contract from here.

After successfully setup all requirement, you can create ink! smart contract using following command.

cargo contract new <contract_name>
contract_name must not start with alphabetic character and contains `alphanumeric characters and underscores`
Enter fullscreen mode Exit fullscreen mode

Successfully create contract can be build with following command

cargo contract build
if you wan't to build in release, then add --release flag
Enter fullscreen mode Exit fullscreen mode

Replace, your #[ink(storage)] Macros with the below code snippet.
owner: Contract Address Owner
beneficiary: Beneficiary account where you can donate balance.
donations: mapping to storage donation data.
donation_id: id for donation

    #[ink(storage)]
    pub struct DonationContract {
        owner: AccountId,
        beneficiary: AccountId,
        donations: Mapping<DonationId, Donation>,
        donation_id: i32,
    }
Enter fullscreen mode Exit fullscreen mode

Donation is Custom Data Structures, we have to define separately above the #[ink(storage)] macro. Derive traits Decode and Encode provide a way to serialize and deserialize rust data type into complete binary format, you can learn more from here. This data structure defines two fields:
account: AccountId, user who donate.
amount: u128, user how much he donate.

    #[derive(scale::Decode, scale::Encode)]
    #[cfg_attr(
        feature = "std",
        derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout)
    )]
    pub struct Donation {
        account: AccountId,
        amount: u128,
    }
Enter fullscreen mode Exit fullscreen mode

Since, ink!v4.0.0 remove AccountId default implementation, we can't use Default macro inside derive, So we have to define separately just below Donation struct.

    // Donation struct default implementations
    impl Default for Donation {
        fn default() -> Self {
            Self {
                account: zero_address(),
                amount: 0,
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

We defined zero_address() function, separately, which you can define like this:

   /// Helper for referencing the zero address (`0x00`). Note that in practice this address should
    /// not be treated in any special way (such as a default placeholder) since it has a known
    /// private key.
    fn zero_address() -> AccountId {
        [0u8; 32].into()
    }
Enter fullscreen mode Exit fullscreen mode

Every ink! contract has one or more constructor. Our constructor is defined below. You can include this code in your #[ink(constructor)] macro. If you want to learn more click here.
Self::env().caller() gives owner of the contract. The owner will be whoever deploy the contract.
Return all the fields from #[ink(storage)] macro like:

  1. owner
  2. beneficiary which is provided while contract instantiation.
  3. donations mapping we can define like Mapping::default()
  4. donation_id can be initialize with 1 or Default::default()
        #[ink(constructor)]
        pub fn new(beneficiary: AccountId) -> Self {
            let owner = Self::env().caller();
            Self {
                owner,
                beneficiary,
                donations: Mapping::default(),
                donation_id: 1,
            }
        }

Enter fullscreen mode Exit fullscreen mode

Normal user can view beneficiary address anytime. Only owner of the contract can change the beneficiary address. These are public function which can be represent by #[ink(message)] macro, then only you can view those api while interaction with frontend. Contract must contains at least one or many #[ink(message)], you can get more info from here

        #[ink(message)]
        pub fn get_beneficiary(&self) -> Option<AccountId> {
            Some(self.beneficiary)
        }

        #[ink(message)]
        pub fn change_beneficiary(&mut self, new_beneficiary: AccountId) {
            let caller = self.env().caller();
            assert!(
                self.owner == caller,
                "Only owner can change the beneficiary account"
            );
            self.beneficiary = new_beneficiary;
        }

Enter fullscreen mode Exit fullscreen mode

assert!() macro check whether caller of the function is owner or not. If not owner then, he can't change the new beneficiary address. In order to update value we call self and mut so that beneficiary from storage can be updated easily, as you might be aware about rust.

Add another function which is also public below.

        #[ink(message, payable)]
        pub fn donation(&mut self) {
            // Account who is donating
            let caller = self.env().caller();
            let donation_id = self.next_donation_id();

            // Donation amount
            let donation_amount = self.env().transferred_value();
            let mut donated_so_far = self.donations.get(donation_id).unwrap_or_default();

            assert!(donation_amount > 0, "Cannot transfer 0 donation");

            // Total donation amount so far by caller
            donated_so_far.amount += donation_amount;
            donated_so_far.account = caller;

            // Insert total donation amount with respect to caller
            self.donations.insert(donation_id, &donated_so_far);

            // Send donation amount to the beneficiary account
            self.env()
                .transfer(self.beneficiary, donation_amount)
                .unwrap_or_default();
        }
Enter fullscreen mode Exit fullscreen mode

This is function is payable, which means it allow receiving value. You can deep dive with this macro from here.

  1. caller who is calling this function while donating amount.
  2. donation_id is local variable which store id from next_donation_id() function which is private function and defined below.
        pub fn next_donation_id(&mut self) -> DonationId {
            let id = self.donation_id;
            self.donation_id += 1;
            id
        }
    }
Enter fullscreen mode Exit fullscreen mode
  1. self.env().transferred_value() returns the value for contract execution more info here.

    1. donated_so_far variable which storage donation so far by account.
  2. If donation more than 0, then donation store in storage.

In this way wasm based ink! smart contract can be written. This is way basic contract for beginner level. You can find all the code in my git hub account. I haven't included here but you can find unit test of this contract as well.

GitHub link

Top comments (0)