DEV Community

Cover image for Ionize x Stellar - Soroban Contracts for charitable donations
Hunter Sides
Hunter Sides

Posted on • Edited on • Originally published at ionize.notion.site

Ionize x Stellar - Soroban Contracts for charitable donations

Overview

Link to hosted DAPP

This project aims to create a developer-friendly solution that enables charitable donations using Stellar wallets within DeFi applications. The developed module is marketable, cross-border, modular, platform-agnostic, highly available, secure, and provides tangible real-world value that appeals to both crypto and non-crypto users.

Key Features

  • Marketable: Designed to be appealing and useful for a wide range of applications.
  • Cross-border: Supports international transactions and donations.
  • Modular: Easily integrated into various dApps.
  • Platform Agnostic: Compatible with any platform supporting Stellar.

Primary Purpose

The primary purpose of this project is to define a charity donation transaction with a focus on modularity, usability, scalability, and composability. The more effective the contract, the more developers will use it, ultimately reaching more users and bringing real-world value.

Understanding the scalability and performance of smart contracts is critical, especially in a cross-border context. Concepts like Big O Notation are essential for evaluating the efficiency of our algorithms within the smart contract. For a deeper understanding, you can explore the basics of Big O Notation in this article.

Additionally, ensuring that our smart contract remains maintainable and understandable is crucial. One way to assess this is by measuring Cyclomatic Complexity, which helps in determining the complexity of our code. While useful, it’s important to be aware of its limitations, as discussed in this blog post.

The goal is to create a multi-purpose, highly composable, minimalistic, and tangible smart contract that prioritizes both the user's and developer's experiences. This involves making decisions that enhance the overall usability and effectiveness of the dApp feature.

User Interaction

Users will have the option to round up their transaction fees and donate the rounded-up portion to a charity of their choice. The list of charities is predefined and sourced from Soroban smart contracts when transacting on the Stellar network.

Integration

The module is a drop-in component that works with any dApp transacting on Stellar. Integration is straightforward: install the dependency from npm, import it into your dApp, and configure it according to your needs.

Technologies Used

  • Blockchain Network: Stellar
  • Smart Contracts: Soroban
  • Front-end Framework: Next.js

How to Use

  1. Install the Module: Install the dependency from npm.
  2. Import the Module: Import the module into your dApp.
  3. Configure the Module: Set it up according to your dApp's requirements.
  4. Enable Donations: Allow users to round up their transaction fees and donate to a charity of their choice.

Example Code

javascriptCopy code
// Example of how to import and use the module in a Next.js application

import { DonationModule } from 'stellar-donation-module';

// Initialize the module
const donationModule = new DonationModule({
  // Configuration options
});

// Use the module in your dApp
donationModule.enableDonations();

Enter fullscreen mode Exit fullscreen mode
  1. Wallet Agnostic Design:
    • The Wallet trait defines a common interface that any wallet implementation can follow.
    • The donate function dynamically loads the correct wallet module based on the wallet_type of the charity, ensuring compatibility with various wallet types.
  2. Modular and Scalable:
    • The contract allows adding charities dynamically through the add_charity function.
    • The charity data is stored in a Vec<Charity>, making it easy to manage multiple charities and their associated wallets.
    • This design can be extended easily to support more functionalities, such as removing charities, listing all charities, etc.
  3. Performance and Maintainability:
    • By adhering to the single responsibility principle, each function focuses on a single task, which helps keep the Cyclomatic Complexity low.
    • The search for a charity in donate is an O(n) operation, which is acceptable given the expected size of the charity list. If scalability becomes an issue, we can switch to a more efficient data structure (e.g., a hash map).
    • The contract is designed to be both efficient and easy to maintain, with clear separation of concerns and a low number of conditional branches.

Capabilities

  1. Extended Charity Metadata:
    • Description: Each charity now includes a description, allowing users to understand its purpose.
    • Website: Optional field for the charity’s website, providing users with more information.
    • Category: Optional field to classify charities, making it easier for users to find charities aligned with their interests.
  2. Update Functionality:
    • The contract now includes an update_charity function, allowing administrators to update charity information dynamically. This could include changing wallet addresses, updating descriptions, or modifying other metadata.
  3. Listing and Retrieval:
    • List Charities: Users can retrieve a list of all available charities with their names and descriptions, making it easier to browse and select a charity.
    • Get Charity Details: Users can retrieve detailed information about a specific charity, including its name, address, description, website, and category.
  4. Wallet Agnostic Design:
    • The contract remains wallet agnostic, using a wallet_type parameter to dynamically load the appropriate wallet module based on the charity’s specified type.
  5. Enhanced NPM Module Integration:
    • The npm package can now provide a richer API, including methods for adding, updating, listing, and retrieving charities.
rustCopy code
use soroban_sdk::{contractimpl, Env, Symbol, Address, Vec, contracttype, BytesN};

// Define an interface for generic wallet operations
#[contracttype]
pub trait Wallet {
    fn get_balance(env: &Env, address: &Address) -> i64;
    fn transfer(env: &Env, from: &Address, to: &Address, amount: i64) -> Result<(), &'static str>;
}

// Define the contract structure
pub struct DonationContract;

// Define a charity structure to hold charity data
#[derive(Default)]
pub struct Charity {
    name: Symbol,
    address: Address,
    wallet_type: Symbol,
    description: Symbol,  // Description of the charity
    website: Option<Symbol>,  // Optional website for more information
    category: Option<Symbol>,  // Optional category for easier classification
}

impl Charity {
    pub fn new(name: Symbol, address: Address, wallet_type: Symbol, description: Symbol, website: Option<Symbol>, category: Option<Symbol>) -> Self {
        Charity { name, address, wallet_type, description, website, category }
    }
}

// Implement the contract logic
#[contractimpl]
impl DonationContract {
    // Function to add a new charity
    pub fn add_charity(env: Env, charities: &mut Vec<Charity>, name: Symbol, address: Address, wallet_type: Symbol, description: Symbol, website: Option<Symbol>, category: Option<Symbol>) {
        let charity = Charity::new(name, address, wallet_type, description, website, category);
        charities.push_back(charity);
    }

    // Function to update charity information
    pub fn update_charity(env: Env, charities: &mut Vec<Charity>, name: Symbol, new_address: Option<Address>, new_wallet_type: Option<Symbol>, new_description: Option<Symbol>, new_website: Option<Symbol>, new_category: Option<Symbol>) -> Result<(), &'static str> {
        let charity_opt = charities.iter_mut().find(|c| c.name == name);

        if let Some(charity) = charity_opt {
            if let Some(address) = new_address { charity.address = address; }
            if let Some(wallet_type) = new_wallet_type { charity.wallet_type = wallet_type; }
            if let Some(description) = new_description { charity.description = description; }
            if let Some(website) = new_website { charity.website = website; }
            if let Some(category) = new_category { charity.category = category; }
            Ok(())
        } else {
            Err("Charity not found")
        }
    }

    // Function to handle donations
    pub fn donate(env: Env, charities: Vec<Charity>, user: Address, charity_name: Symbol, amount: i64) -> Result<(), &'static str> {
        // Find the charity by name
        let charity_opt = charities.iter().find(|&c| c.name == charity_name);

        // If charity is found
        if let Some(charity) = charity_opt {
            // Use the wallet type to transfer funds in a wallet-agnostic manner
            let wallet_module = env.get_symbol::<Wallet>(&charity.wallet_type)?;
            let user_balance = wallet_module.get_balance(&env, &user);

            // Ensure the user has enough balance
            if user_balance >= amount {
                wallet_module.transfer(&env, &user, &charity.address, amount)?;
                Ok(())
            } else {
                Err("Insufficient balance for donation")
            }
        } else {
            Err("Charity not found")
        }
    }

    // Function to list all charities
    pub fn list_charities(env: Env, charities: Vec<Charity>) -> Vec<(Symbol, Symbol)> {
        charities.iter().map(|c| (c.name.clone(), c.description.clone())).collect()
    }

    // Function to retrieve charity details by name
    pub fn get_charity_details(env: Env, charities: Vec<Charity>, charity_name: Symbol) -> Result<(Symbol, Address, Symbol, Option<Symbol>, Option<Symbol>), &'static str> {
        let charity_opt = charities.iter().find(|&c| c.name == charity_name);

        if let Some(charity) = charity_opt {
            Ok((charity.name.clone(), charity.address.clone(), charity.description.clone(), charity.website.clone(), charity.category.clone()))
        } else {
            Err("Charity not found")
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

NPM Module Integration

To make this contract usable as an npm module, we would:

  1. Compile the Contract: The Rust code would be compiled to WebAssembly (Wasm), which can then be deployed on the Stellar network.
  2. Create an NPM Package:
    • The package would include the compiled Wasm contract and a JavaScript/TypeScript wrapper to interface with the contract.
    • The wrapper would provide methods for interacting with the contract, such as addCharity, donate, etc.
    • Example of the wrapper API:
typescriptCopy code
import { SorobanClient } from 'stellar-sdk';

class DonationModule {
    private client: SorobanClient;

    constructor(client: SorobanClient) {
        this.client = client;
    }

    async addCharity(name: string, address: string, walletType: string, description: string, website?: string, category?: string) {
        // Call the add_charity function on the contract
    }

    async updateCharity(name: string, newAddress?: string, newWalletType?: string, newDescription?: string, newWebsite?: string, newCategory?: string) {
        // Call the update_charity function on the contract
    }

    async donate(userAddress: string, charityName: string, amount: number) {
        // Call the donate function on the contract
    }

    async listCharities() {
        // Call the list_charities function and return the result
    }

    async getCharityDetails(charityName: string) {
        // Call the get_charity_details function and return the result
    }
}

export default DonationModule;

Enter fullscreen mode Exit fullscreen mode

Example of drop in dApp implementation via npm module

Top comments (0)