Ionize x Stellar - Soroban Contracts for charitable donations
Hunter Sides
Posted on August 19, 2024
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
- Install the Module: Install the dependency from npm.
- Import the Module: Import the module into your dApp.
- Configure the Module: Set it up according to your dApp's requirements.
- 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();
-
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 thewallet_type
of the charity, ensuring compatibility with various wallet types.
- The
-
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.
- The contract allows adding charities dynamically through the
-
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
-
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.
-
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.
- The contract now includes an
-
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.
-
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.
- The contract remains wallet agnostic, using a
-
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")
}
}
}
NPM Module Integration
To make this contract usable as an npm module, we would:
- Compile the Contract: The Rust code would be compiled to WebAssembly (Wasm), which can then be deployed on the Stellar network.
-
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;
Example of drop in dApp implementation via npm module
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
August 19, 2024
August 19, 2024