FeeDefi - a service fee charity donation module dapps on Stellar

hunterionize

Hunter Sides

Posted on August 19, 2024

FeeDefi - a service fee charity donation module dapps on Stellar

Link to hosted DAPP

Overview

FeeDefi is a module for defi dapps on the Stellar network the utilizes the composability of the network to create simple to use 'dev to production' solution focused on 'Real world-ability'

Under the hood, FeeDefi uses Soroban Smart contracts to interface with the Stellar network.

Key Features

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.

Primary Purpose

FeeDefi was built as a way to demonstrate how the Stellar ecosystem can bring value to the world and how it's composability unlocks this at scale for dApps to easily implement.

Why Stellar? The high level answer -makes extensible adoption a reality and here's why

There's something in mathematics and programming called Big complexity and the a mathematical ecosystem is mathematically
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

Tutorial

Example of Drop-in dApp Implementation via NPM Module

To implement the donation module in your dApp, follow these steps:

  1. Install the Donation Module:
    Run the following command to install the donation module from npm.

    npm install stellar-donation-module
    
    
  2. Import the Module:
    Import the module into your dApp.

    import DonationModule from 'stellar-donation-module';
    
    
  3. Initialize the Module:
    Initialize the module with the necessary configuration.

    const client = new SorobanClient('<https://horizon.stellar.org>');
    const donationModule = new DonationModule(client);
    
    
  4. Add a Charity:
    Use the module to add a new charity.

    await donationModule.addCharity('St. Jude', 'GAX...5T', 'stellar', 'St. Jude Children\\'s Research Hospital', '<https://www.stjude.org>', 'Health');
    
    
  5. Update a Charity:
    Update the details of an existing charity.

    await donationModule.updateCharity('St. Jude', undefined, undefined, 'St. Jude Children\\'s Research Hospital - Updated Description');
    
    
  6. Donate to a Charity:
    Allow users to donate to a specific charity.

    const userAddress = 'GAH...7X';
    await donationModule.donate(userAddress, 'St. Jude', 1000);
    
    
  7. List All Charities:
    Retrieve a list of all available charities.

    const charities = await donationModule.listCharities();
    console.log(charities);
    
    
  8. Get Charity Details:
    Retrieve detailed information about a specific charity.

    const charityDetails = await donationModule.getCharityDetails('St. Jude');
    console.log(charityDetails);
    
    

By following these steps, you can seamlessly integrate the donation module into your dApp and enable users to make donations to their preferred charities.

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

const client = new SorobanClient('<https://horizon.stellar.org>');
const donationModule = new DonationModule(client);

// Example usage
async function exampleUsage() {
  // Add a Charity
  await donationModule.addCharity('St. Jude', 'GAX...5T', 'stellar', 'St. Jude Children\\'s Research Hospital', '<https://www.stjude.org>', 'Health');

  // Update a Charity
  await donationModule.updateCharity('St. Jude', undefined, undefined, 'St. Jude Children\\'s Research Hospital - Updated Description');

  // Donate to a Charity
  const userAddress = 'GAH...7X';
  await donationModule.donate(userAddress, 'St. Jude', 1000);

  // List All Charities
  const charities = await donationModule.listCharities();
  console.log(charities);

  // Get Charity Details
  const charityDetails = await donationModule.getCharityDetails('St. Jude');
  console.log(charityDetails);
}

exampleUsage();

Enter fullscreen mode Exit fullscreen mode
import DonationModule from 'stellar-donation-module';
import { SorobanClient } from 'stellar-sdk';

const client = new SorobanClient('<https://horizon.stellar.org>');
const donationModule = new DonationModule(client);

// Example usage
async function exampleUsage() {
  // Add a Charity
  await donationModule.addCharity('St. Jude', 'GAX...5T', 'stellar', 'St. Jude Children\\'s Research Hospital', '<https://www.stjude.org>', 'Health');

  // Update a Charity
  await donationModule.updateCharity('St. Jude', undefined, undefined, 'St. Jude Children\\'s Research Hospital - Updated Description');

  // Donate to a Charity
  const userAddress = 'GAH...7X';
  await donationModule.donate(userAddress, 'St. Jude', 1000);

  // List All Charities
  const charities = await donationModule.listCharities();
  console.log(charities);

  // Get Charity Details
  const charityDetails = await donationModule.getCharityDetails('St. Jude');
  console.log(charityDetails);
}

exampleUsage();

Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
hunterionize
Hunter Sides

Posted on August 19, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related