DEV Community

Cover image for Step-by-Step Guide to Creating a Donations dApp with Stellar SDK
Jesus Esquer
Jesus Esquer

Posted on

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
Enter fullscreen mode Exit fullscreen mode

Then, set up the directory structure as follows:

  • Root Level Files: Configuration and metadata files like .gitignore, package.json, and README.md.
  • public: Static assets like icon.jpg and index.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).
    • hooks: Custom hooks (useSubmitForm.js).
    • utils: Utility functions and libraries.
    • stellarSDK: Stellar SDK-related utilities (stellarSDK.js).

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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);
  }
};

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;
};

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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);
  }
};

Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)