Step-by-Step Guide to Creating a Donations dApp with Stellar SDK
Introduction
Welcome! In this post, we'll guide you through creating a donation dApp powered by the Stellar SDK. This app handles real-time, secure, and transparent transactions using blockchain technology. You can explore the deployed final product here.
This app aims to create a platform that enables transparent charitable donations. Donors can track how their funds are used and ensure they reach the intended recipients, enhancing trust in charitable organizations.
This project was also created as an entry for Stellar’s Build Better on Stellar: Smart Contract Challenge. Please check out the submission and give it a like here.
Technologies Used
- Create React App: Scaffolding and managing the React application.
- React: The core library for building the user interface.
- Stellar SDK: Interacting with the Stellar blockchain to process transactions.
- react-router-dom: Handling routing and navigation within the application.
- GitHub Pages: Deploying the application.
Prerequisites
This tutorial is friendly for beginners with a basic familiarity with React.js. You'll need Node.js and an IDE, like VS Code. You can find Node.js here and VS Code here.
Project Setup
First, create a React application by running the following command:
charity-dapp-ui/
│ .gitignore
│ package-lock.json
│ package.json
│ README.md
├───public
│ icon.jpg
│ index.html
└───node_modules
└───src
│ App.css
│ App.js
│ App.test.js
│ index.css
│ index.js
│ logo.svg
│ reportWebVitals.js
│ setupTests.js
├───components
│ │ Home.js
│ ├───auth
│ │ Login.js
│ │ PrivateRoute.js
│ │ Register.js
│ └───dashboard
│ ├───charity
│ │ CharityDashboard.js
│ │ CharityProfile.js
│ ├───common
│ │ DonationsList.js
│ └───donor
│ DonorDashboard.js
├───hooks
│ useSubmitForm.js
└───utils
└───stellarSDK
stellarSDK.js
Then, set up the directory structure as follows:
- Root Level Files: Configuration and metadata files like
.gitignore
,package.json
, andREADME.md
. - public: Static assets like
icon.jpg
andindex.html
. - node_modules: Lists all installed NPM dependencies.
- src: Source code of the application.
- components: Contains React components.
- auth: Authentication-related components (
Login.js
,PrivateRoute.js
,Register.js
). - common: Shared components (
Wrapper.js
). - dashboard: Dashboard components.
- charity: Charity-specific dashboard components (
CharityDashboard.js
,CharityProfile.js
). - common: Common dashboard components (
DonationsList.js
). - donor: Donor-specific dashboard components (
DonorDashboard.js
).
- charity: Charity-specific dashboard components (
- auth: Authentication-related components (
- hooks: Custom hooks (
useSubmitForm.js
). - utils: Utility functions and libraries.
- stellarSDK: Stellar SDK-related utilities (
stellarSDK.js
).
- components: Contains React components.
Step-by-Step Development
Installing Stellar SDK
After creating the necessary directories and files, install the Stellar SDK by running:
npm install --save stellar-sdk
Setting Up Routing
Next, set up the router and routes in App.js
by pasting the following code:
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import Home from "./components/Home";
import Login from "./components/auth/Login";
import Register from "./components/auth/Register";
import DonorDashboard from "./components/dashboard/donor/DonorDashboard";
import CharityDashboard from "./components/dashboard/charity/CharityDashboard";
import PrivateRoute from "./components/auth/PrivateRoute";
import CharityProfile from "./components/dashboard/charity/CharityProfile";
import "./App.css";
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route
path="/donor-dashboard"
element={
<PrivateRoute>
<DonorDashboard />
</PrivateRoute>
}
/>
<Route
path="/charity-dashboard"
element={
<PrivateRoute>
<CharityDashboard />
</PrivateRoute>
}
/>
<Route path="/charity-profile" element={<CharityProfile />} />
</Routes>
</Router>
);
}
export default App;
Here, we import Router
, Routes
, and Route
from react-router-dom
, React’s built-in library for handling routing. Then, we import all the components that will have a route.
The Router
wraps around Routes
, where each Route
has a path and an associated component. For private routes, we'll create a PrivateRoute.js
component:
import { Navigate } from "react-router-dom";
const PrivateRoute = ({ children }) => {
const currentUser = localStorage.getItem("currentUser");
if (!currentUser) {
return <Navigate to="/login" />;
}
return children;
};
export default PrivateRoute;
This component ensures that only registered users can access certain routes. It checks for the presence of a "currentUser" item in local storage and redirects the user to the login page if not found.
Creating Components
Home Component
In Home.js
, add the following code:
import { useNavigate } from "react-router-dom";
const Home = () => {
const navigate = useNavigate();
return (
<div>
<div>
<h1>Welcome to Stellar Decentralized Donations!</h1>
<p>
Empowering change, one donation at a time. Whether you’re here to give
or to receive, our platform makes it simple, transparent, and secure.
Join our community by logging in or registering today!
</p>
</div>
<div>
<button onClick={() => navigate("/login")}>Login</button>
<button onClick={() => navigate("/register")}>Register</button>
</div>
</div>
);
};
export default Home;
The Home component includes a greeting, a description, and two buttons that redirect users to the Register and Login pages.
Register Component
In Register.js
, add the following code:
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { registerUserInBlockchain } from "../../utils/stellarSDK/stellarSDK";
import useSubmitForm from "../../hooks/useSubmitForm";
function Register() {
const navigate = useNavigate();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [inputPublicKey, setInputPublicKey] = useState("");
const [role, setRole] = useState("donor"); // or charity
const registerUserInLocal = (publicKey) => {
// Validation for name, email, password
if (!name || !email || !password) {
alert("Please fill all the fields");
return;
}
const users = JSON.parse(localStorage.getItem("users")) || [];
const newUser = { name, email, password, role, publicKey };
users.push(newUser);
localStorage.setItem("users", JSON.stringify(users));
};
const handleRegister = async () => {
if (inputPublicKey) {
registerUserInLocal(inputPublicKey);
} else {
let [keyPair] = await registerUserInBlockchain();
alert(`Registered successfully!
Your public key is ${keyPair.publicKey()}
Private key is ${keyPair.secret()}.
Please save these keys for future use.
Private key is not stored anywhere and cannot be recovered!`);
registerUserInLocal(keyPair.publicKey());
}
// Redirect to login page
navigate("/login");
};
const { isLoading, handleSubmit } = useSubmitForm(handleRegister);
return (
<div>
<h2>Register</h2>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<select value={role} onChange={(e) => setRole(e.target.value)}>
<option value="donor">Donor</option>
<option value="charity">Charity</option>
</select>
<p>
If you have a public key, enter it here to link it to your account:
</p>
<input
type="text"
placeholder="Public Key"
value={inputPublicKey}
onChange={(e) => setInputPublicKey(e.target.value)}
/>
{isLoading ? (
<button disabled>Loading...</button>
) : (
<button type="submit">
{inputPublicKey ? "Register" : "Register & Create Stellar Account"}
</button>
)}
</form>
</div>
);
}
export default Register;
This component handles user registration. It includes form validation, allowing users to either register with an existing Stellar public key or generate a new one. User details are saved in local storage, and if necessary, a new Stellar account is created.
Custom Hook: useSubmitForm
In useSubmitForm.js
, add the following code:
import { useState } from "react";
const useSubmitForm = (handleSubmitFunction) => {
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
await handleSubmitFunction();
} catch (error) {
console.error("An error occurred:", error);
} finally {
setIsLoading(false);
}
};
return { isLoading, handleSubmit };
};
export default useSubmitForm;
This custom hook handles form submissions. It manages loading states and prevents default form behavior to avoid page reloads.
Utils: Stellar SDK (registerUserInBlockchain)
In stellarSDK.js
, add the following code:
const StellarSdk = require("stellar-sdk");
const server = new StellarSdk.Horizon.Server(
"https://horizon-testnet.stellar.org"
);
export const registerUserInBlockchain = async () => {
const pair = StellarSdk.Keypair.random();
const publicKey = pair.publicKey();
try {
const response = await fetch(
`https://friendbot.stellar.org?addr=${encodeURIComponent(publicKey)}`
);
const responseJSON = await response.json();
console.log("SUCCESS! You have a new account :)\n", responseJSON);
return [pair, publicKey];
} catch (e) {
console.error("Error creating test account!", e);
}
};
This utility function registers a user on the Stellar blockchain by generating a key pair and funding the account. Any errors are caught and logged.
Login Component
In Login.js
, add the following code:
import { useState } from "react";
import { useNavigate } from "react-router-dom";
const Login = () => {
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleLogin = (e) => {
e.preventDefault();
const users = JSON.parse(localStorage.getItem("users")) || [];
const user = users.find(
(user) => user.email === email && user.password === password
);
if (!user) {
alert("Invalid credentials");
return;
}
const dashboardPath =
user.role === "charity" ? "/charity-dashboard" : "/donor-dashboard";
navigate(dashboardPath);
localStorage.setItem("currentUser", JSON.stringify(user));
};
return (
<div className="login">
<h2>Login</h2>
<form onSubmit={handleLogin}>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
</div>
);
};
export default Login;
The Login component checks user credentials against those stored in local storage. If there is a match, the user is redirected to their respective dashboard (Donor or Charity).
Charity Components
Charity Dashboard
In CharityDashboard.js
, add the following code:
import { useState, useEffect } from "react";
import { fetchDonationHistory } from "../../../utils/stellarSDK/stellarSDK";
import DonationsList from "../common/DonationsList";
import { Link } from "react-router-dom";
const CharityDashboard = () => {
const charityProfile = JSON.parse(localStorage.getItem("currentUser")) || {};
const [charityPublicKey] = useState(charityProfile.publicKey || "");
const [charityName] = useState(charityProfile.name || "");
const [donationHistory, setDonationHistory] = useState([]);
useEffect(() => {
if (!charityPublicKey) return;
const fetchHistory = async (charityPublicKey) => {
const donationHistory = await fetchDonationHistory(charityPublicKey);
setDonationHistory(donationHistory);
};
fetchHistory(charityPublicKey);
}, [charityPublicKey]);
return (
<div>
<div>
<h2>Hello, {charityName}! Let's make an impact.</h2>
</div>
<Link to="/charity-profile">Edit Profile</Link>
<h2>Received Donations</h2>
<DonationsList donations={donationHistory} />
</div>
);
};
export default CharityDashboard;
This component fetches and displays the charity’s donation history. It also includes a link to edit the charity’s profile.
Utils: Stellar SDK (fetchDonationHistory)
In stellarSDK.js
, add the following code:
export const fetchDonationHistory = async (sourcePublicKey) => {
try {
const account = await server.loadAccount(sourcePublicKey);
const transactions = await server
.transactions()
.forAccount(account.accountId())
.call();
return transactions?.records;
} catch (e) {
console.error("An error has occured fetching donation history", e);
}
};
This utility function retrieves all transactions associated with a given Stellar account.
DonationList Component
In DonationList.js
, add the following code:
import { useState, useEffect } from "react";
import { extractTransactionDetails } from "../../../utils/stellarSDK/stellarSDK";
const DonationsList = ({ donations }) => {
const [transactionDetails, setTransactionDetails] = useState([]);
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleString("en-US", { timeZone: "UTC" });
};
useEffect(() => {
const getTransactionDetails = async (donations) => {
try {
const transactionDetails = await Promise.all(
donations.map((donation) => extractTransactionDetails(donation))
);
return transactionDetails;
} catch (error) {
console.error("Error fetching transaction details:", error);
return [];
}
};
getTransactionDetails(donations).then((transactionDetails) =>
setTransactionDetails(transactionDetails)
);
}, [donations]);
return (
<div>
<ul>
{transactionDetails?.map(
(transaction, index) =>
Number(transaction.amount).toFixed(2) > 0 && (
<li key={index}>
{Number(transaction.amount).toFixed(2)} XLM from{" "}
{transaction.from} to {transaction.to} on{" "}
{formatDate(transaction.createdAt)} UTC
</li>
)
)}
</ul>
</div>
);
};
export default DonationsList;
This component displays a list of donations with details such as the donor, recipient, amount, and date.
Utils: Stellar SDK (extractTransactionDetails)
Inside stellarSDK.js add at the bottom the following function:
export const extractTransactionDetails = async (transaction) => {
const operations = await fetchOperationsForTransaction(transaction.id);
const transactionDetails = {
id: transaction.id,
createdAt: transaction.created_at,
amount: 0,
to: "",
from: "",
};
operations.forEach((operation) => {
if (operation.type === "payment") {
transactionDetails.amount = operation.amount;
transactionDetails.to = operation.to;
transactionDetails.from = operation.from;
}
});
return transactionDetails;
};
export const fetchOperationsForTransaction = async (transactionId) => {
const url = `https://horizon-testnet.stellar.org/transactions/${transactionId}/operations`;
const response = await fetch(url);
const operations = await response.json();
return operations._embedded.records;
};
In this util function, we take one transaction as a parameter, then for the transaction we get the operations using fetchOperationsForTransaction, with the transaction id.
For each operation, we return an object with the amount, to, and from details.
Charity Profile
In CharityProfile.js
, add the following code:
import { useState, useEffect } from "react";
const CharityProfile = () => {
const [charityPublicKey] = useState("");
const [profile, setProfile] = useState({
name: "",
mission: "",
contact: "",
goal: 0,
raised: 0,
});
useEffect(() => {
const fetchProfile = async (charityPublicKey) => {
const mockProfile = {
name: "Charity Name",
mission: "Our mission is to help those in need.",
contact: "contact@charity.org",
goal: 1000,
raised: 200, // This should be fetched from the Stellar transactions
};
setProfile(mockProfile);
};
fetchProfile(charityPublicKey);
}, [charityPublicKey]);
const handleInputChange = (e) => {
const { name, value } = e.target;
setProfile((prevProfile) => ({ ...prevProfile, [name]: value }));
};
const saveProfile = () => {
// Save profile to blockchain
console.log("Saving profile to blockchain", profile);
// Save profile to local storage
const charityProfile =
JSON.parse(localStorage.getItem("charityProfile")) || {};
// Merge the new profile with the existing profile
// check if public key from charity matches the public key in local storage
// link the two profiles
const updatedProfile = { ...charityProfile, ...profile };
localStorage.setItem("charityProfile", JSON.stringify(updatedProfile));
};
return (
<div>
<h1>Charity Profile</h1>
<div>
<label>Name:</label>
<input
type="text"
name="name"
value={profile.name}
onChange={handleInputChange}
placeholder="Charity Name"
/>
</div>
<div>
<label>Mission:</label>
<textarea
name="mission"
value={profile.mission}
onChange={handleInputChange}
placeholder="Mission"
/>
</div>
<div>
<label>Contact:</label>
<input
type="text"
name="contact"
value={profile.contact}
onChange={handleInputChange}
placeholder="Contact"
/>
</div>
<div>
<label>Goal:</label>
<input
type="number"
name="goal"
value={profile.goal}
onChange={handleInputChange}
placeholder="Goal"
/>
</div>
<div>
<label>Raised:</label>
<input type="number" name="raised" value={profile.raised} readOnly />
</div>
<button onClick={saveProfile}>Save Profile</button>
<h2>Progress</h2>
<p>
{((profile.raised / profile.goal) * 100).toFixed(2)}% of goal reached
</p>
</div>
);
};
export default CharityProfile;
This component allows charities to update their profile information and track the progress of their fundraising efforts.
Donor Dashboard
In DonorDashboard.js
, add the following code:
import { useEffect, useState } from "react";
import {
fetchDonationHistory,
sendPayment,
} from "../../../utils/stellarSDK/stellarSDK";
import DonationsList from "../common/DonationsList";
import useSubmitForm from "../../../hooks/useSubmitForm";
const DonorDashboard = () => {
const donorProfile = JSON.parse(localStorage.getItem("currentUser")) || {};
const [sourceSecretKey, setSourceSecretKey] = useState("");
const [destinationPublicKey, setDestinationPublicKey] = useState("");
const [amount, setAmount] = useState(0);
const [donationHistory, setDonationHistory] = useState([]);
const [donorPublicKey] = useState(donorProfile.publicKey || "");
const [donorName] = useState(donorProfile.name || "");
const submitDonation = async (e) => {
if (!sourceSecretKey || !destinationPublicKey || !amount) {
alert("Please fill all the fields");
return;
}
if (isNaN(amount) || parseFloat(amount) <= 0) {
alert("Please enter a valid amount");
return;
}
try {
const result = await sendPayment(
sourceSecretKey,
destinationPublicKey,
amount
);
if (result.successful) {
alert("Donation sent successfully!");
} else {
alert(
"Error sending donation. Please double check your details. And try again."
);
}
} catch (error) {
console.error("Error sending donation:", error);
}
};
const { isLoading, handleSubmit } = useSubmitForm(submitDonation);
useEffect(() => {
if (!donorPublicKey && isLoading) return;
const fetchHistory = async (donorPublicKey) => {
const donationHisory = await fetchDonationHistory(donorPublicKey);
setDonationHistory(donationHisory);
};
fetchHistory(donorPublicKey);
}, [donorPublicKey, isLoading]);
return (
<div>
<h2>Welcome, {donorName}! Ready to make a difference?</h2>
<form onSubmit={handleSubmit}>
<div>
<label>Source Secret Key:</label>
<input
type="text"
value={sourceSecretKey}
onChange={(e) => setSourceSecretKey(e.target.value)}
placeholder="Source Secret Key"
/>
</div>
<div>
<label>Destination Public Key:</label>
<input
type="text"
value={destinationPublicKey}
onChange={(e) => setDestinationPublicKey(e.target.value)}
placeholder="Destination Public Key"
/>
</div>
<div>
<label>Amount</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount"
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? "Sending..." : "Send Donation"}
</button>
</form>
<div>
<h2>Donation History</h2>
<DonationsList donations={donationHistory} />
</div>
</div>
);
};
export default DonorDashboard;
The Donor Dashboard allows users to make donations by entering their Stellar secret key, the recipient’s public key, and the amount. It also displays the donor’s transaction history.
Utils: Stellar SDK (sendPayment)
In stellarSDK.js
, add the following code:
export const sendPayment = async (
sourceSecretKey,
destinationPublicKey,
amount
) => {
const sourceKeypair = StellarSdk.Keypair.fromSecret(sourceSecretKey);
const sourcePublicKey = sourceKeypair.publicKey();
try {
const account = await server.loadAccount(sourcePublicKey);
const fee = await server.fetchBaseFee();
const transaction = new StellarSdk.TransactionBuilder(account, {
fee,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(
StellarSdk.Operation.payment({
destination: destinationPublicKey,
asset: StellarSdk.Asset.native(),
amount: amount.toString(),
})
)
.setTimeout(30)
.build();
transaction.sign(sourceKeypair);
const result = await server.submitTransaction(transaction);
console.log("Success! Results:", result);
return result;
} catch (e) {
console.error("An error has occured sending payment", e);
}
};
This utility function handles the payment process on the Stellar network, signing and submitting transactions.
Conclusion
By following this guide, you’ve built a functional charity donation application using the Stellar SDK. This app allows users to make secure, transparent donations and track how their contributions are being used.
Blockchain technology enhances transparency and builds trust between donors and charitable organizations.
If you’re interested in expanding this project, consider exploring some of the following features:
- Multi-currency support.
- Additional authentication methods.
- Integrating a proof-of-use system for donations.
Feel free to clone the code here or test the live app. This guide didn’t cover styling, but the deployed version uses Tailwind CSS for a clean, modern look.
Thank you for following along! If you encounter any issues, check the console for errors, and don't hesitate to reach out with questions. Happy coding!
Subscribe to my newsletter
Read articles from Jesus Esquer directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by