Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 9 additions & 104 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,110 +1,15 @@
# BasicPay <!-- omit in toc -->
# Basic Payment App

> The app that lets you pay, _basically_, anyone.
A simple Stellar payment application built with React and JavaScript.

An example payments application demonstrating integrations of various Stellar
features and SEPs in a user-facing product.
## Features

> :warning: **CAUTION:** Although BasicPay is a full-fledged application on
> Stellar's Testnet, it has been built solely to showcase Stellar functionality
> for the educational purposes of this tutorial, not to be copied, pasted, and
> used on Mainnet.
- Send payments to contacts
- View transaction history
- Connect to Stellar network

## Table of Contents <!-- omit in toc -->

- [Companion App](#companion-app)
- [Companion Tutorial](#companion-tutorial)
- [Development Instructions](#development-instructions)
- [Where to Start in this Repository?](#where-to-start-in-this-repository)
- [Stellar](#stellar)
- [SEPs](#seps)
- [Other Stellar Functionality](#other-stellar-functionality)
- [SvelteKit](#sveltekit)
- [Pages and Routes](#pages-and-routes)
- [Stores](#stores)

## Companion App

This isn't just a codebase, it's a fully functioning testnet wallet! You can
check it out and use it here: <https://basicpay.pages.dev>

## Companion Tutorial

This application was built to coincide with a written tutorial, located in the
Stellar documentation. This tutorial is a "nearly comprehensive" guide to
building the features in this app. Following along with the tutorial and this
source code repository, you can get a solid understanding of building an
application on the Stellar network.

Find the start the tutorial here:
<https://developers.stellar.org/docs/building-apps/example-application-tutorial/overview>

## Development Instructions

Clone the repository:
## Getting Started

```bash
git clone https://github.com/stellar/basic-payment-app.git
```

Change into the repository app:

```bash
cd basic-payment-app
```

Install dependencies and start the development server.

```bash
yarn install
yarn dev
```

You can now visit the local development site in your browser at
<http://localhost:5173>

## Where to Start in this Repository?

We've worked to document everything in this repo, so start exploring wherever
you like. We'd suggest looking into the following areas:

### Stellar

The heart and soul of this application is interacting with the Stellar network.

#### SEPs

Most of the Stellar interactions take place through the use of SEPs (Stellar
Ecosystem Proposals). These interactions are coded in the
`/src/lib/stellar/sep*.js` files.

#### Other Stellar Functionality

More generic Stellar functionality are located in the same directory:

- `/src/lib/stellar/horizonQueries.js` for querying information from the network
- `/src/lib/stellar/transactions.js` for building different kinds of Stelar
transactions

### SvelteKit

This application is built using SvelteKit. We don't want to make this much of a
"SvelteKit Tutorial," but here's what you may want to explore:

#### Pages and Routes

We primarily use SvelteKit for its routing capability. You can begin exploring
the SvelteKit components by reading the comments in the
`/src/routes/dashboard/+page.svelte` file.

#### Stores

We also have implemented a few custom stores to keep track of contact names and
addresses, user KYC information, a list of anchor transfers, etc. A good place
to start exploring these stores is the `/src/lib/stores/contactsStore.js` file.

---

> **Note:** This repository is not in scope for the Stellar Development
> Foundation bug bounty program. Vulnerabilities found in this repo are not
> eligible for rewards.
npm install
npm start
72 changes: 21 additions & 51 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,53 +1,23 @@
{
"name": "bpa",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"test": "vitest"
},
"type": "module",
"packageManager": "pnpm@11.28.2",
"devDependencies": {
"@eslint/compat": "^2.1.1",
"@eslint/js": "^10.0.1",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.70.3",
"@sveltejs/vite-plugin-svelte": "^7.3.1",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.20",
"@tailwindcss/vite": "^4.3.3",
"@types/node": "^24.19.0",
"daisyui": "^5.7.46",
"eslint": "^10.11.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.23.0",
"globals": "^17.12.0",
"msw": "^2.15.0",
"prettier": "^3.9.9",
"prettier-plugin-svelte": "^4.1.1",
"prettier-plugin-tailwindcss": "^0.8.1",
"svelte": "^5.57.1",
"svelte-check": "^4.7.6",
"svelte-copy": "^2.0.0",
"svelte-feather-icons": "^4.2.0",
"svelte-simple-modal": "^2.0.0",
"tailwindcss": "^4.3.3",
"typescript": "^6.0.3",
"typescript-eslint": "^8.70.1",
"uuid": "^14.0.2",
"vite": "^8.3.1",
"vitest": "^5.0.2"
},
"dependencies": {
"@creit.tech/stellar-wallets-kit": "^2.7.0",
"@stellar/stellar-sdk": "17.0.1",
"@stellar/typescript-wallet-sdk-km": "^5.0.0"
}
"name": "basic-payment-app",
"version": "1.0.0",
"description": "A basic Stellar payment application",
"main": "src/index.js",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test"
},
"dependencies": {
"stellar-sdk": "^12.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"react-scripts": "5.0.1"
},
"browserslist": {
"production": [">0.2%", "not dead", "not op_mini all"],
"development": ["last 1 chrome version", "last 1 firefox version", "last 1 safari version"]
}
}
181 changes: 181 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React, { useState, useEffect } from 'react';
import ContactList from './ContactList';
import PaymentForm from './PaymentForm';
import TransactionHistory from './TransactionHistory';
import { connectToNetwork, sendPayment, sendSacTransfer, getBalance } from './stellar';

const App = () => {
const [serverUrl, setServerUrl] = useState('https://horizon-testnet.stellar.org');
const [networkPassphrase, setNetworkPassphrase] = useState(
'Test SDF Network ; September 2015'
);
const [sourceSeed, setSourceSeed] = useState('');
const [connected, setConnected] = useState(false);
const [account, setAccount] = useState(null);
const [balance, setBalance] = useState(null);
const [error, setError] = useState(null);
const [transactions, setTransactions] = useState([]);
const [contacts, setContacts] = useState([
{ id: 1, name: 'Alice', address: 'GABCDEFGHIJKLMNOPQRSTUVWXYZabcdef' },
{ id: 2, name: 'Bob', address: 'CSacTestContractAddress123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' },
{ id: 3, name: 'Carol', address: 'GBXYZABCDEFGHIJKLMNOPQRSTUVWXYZabcdef' },
]);
const [selectedContact, setSelectedContact] = useState(null);
const [amount, setAmount] = useState('');
const [assetCode, setAssetCode] = useState('XLM');
const [assetIssuer, setAssetIssuer] = useState('');

useEffect(() => {
if (connected && sourceSeed) {
loadAccount();
}
}, [connected, sourceSeed]);

const loadAccount = async () => {
try {
setError(null);
const server = new StellarSdk.Server(serverUrl);
const sourceKeypair = StellarSdk.Keypair.fromSecret(sourceSeed);
const sourceAccount = await server.loadAccount(sourceKeypair.publicKey());

setAccount({
publicKey: sourceKeypair.publicKey(),
sequence: sourceAccount.sequence,
});

let totalBalance = '0';
for (const entry of sourceAccount.balances) {
if (entry.asset_type === 'native') {
totalBalance = entry.balance;
}
}
setBalance(totalBalance);
} catch (err) {
setError('Failed to load account: ' + err.message);
}
};

const handleConnect = () => {
if (!sourceSeed) {
setError('Please enter your seed phrase');
return;
}
try {
StellarSdk.Keypair.fromSecret(sourceSeed);
setConnected(true);
setError(null);
} catch (err) {
setError('Invalid seed phrase');
}
};

const handleSendPayment = async () => {
if (!sourceSeed || !selectedContact) {
setError('Please fill in all fields');
return;
}
try {
setError(null);
const server = new StellarSdk.Server(serverUrl);
const sourceKeypair = StellarSdk.Keypair.fromSecret(sourceSeed);
const sourceAccount = await server.loadAccount(sourceKeypair.publicKey());

const asset = assetCode === 'XLM'
? StellarSdk.Asset.native()
: new StellarSdk.Asset(assetCode, assetIssuer);

const result = await sendPayment(server, sourceKeypair, sourceAccount, selectedContact, amount, asset);
setTransactions([result, ...transactions]);
setAmount('');
setSelectedContact(null);
await loadAccount();
} catch (err) {
setError('Payment failed: ' + err.message);
}
};

const handleSendSacTransfer = async () => {
if (!sourceSeed || !selectedContact) {
setError('Please fill in all fields');
return;
}
try {
setError(null);
const server = new StellarSdk.Server(serverUrl);
const sourceKeypair = StellarSdk.Keypair.fromSecret(sourceSeed);
const sourceAccount = await server.loadAccount(sourceKeypair.publicKey());

const asset = assetCode === 'XLM'
? StellarSdk.Asset.native()
: new StellarSdk.Asset(assetCode, assetIssuer);

const result = await sendSacTransfer(server, sourceKeypair, sourceAccount, selectedContact, amount, asset);
setTransactions([result, ...transactions]);
setAmount('');
setSelectedContact(null);
await loadAccount();
} catch (err) {
setError('SAC transfer failed: ' + err.message);
}
};

return (
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
<h1>Basic Payment App</h1>

<div style={{ marginBottom: '20px', padding: '15px', border: '1px solid #ccc', borderRadius: '8px' }}>
<h3>Connection Settings</h3>
<input
type="text"
placeholder="Horizon Server URL"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
style={{ display: 'block', width: '100%', marginBottom: '10px', padding: '8px' }}
/>
<input
type="password"
placeholder="Source Seed / Secret Key"
value={sourceSeed}
onChange={(e) => setSourceSeed(e.target.value)}
style={{ display: 'block', width: '100%', marginBottom: '10px', padding: '8px' }}
/>
<button onClick={handleConnect} style={{ padding: '10px 20px', cursor: 'pointer' }}>
Connect
</button>
{connected && <span style={{ color: 'green', marginLeft: '10px' }}>✓ Connected</span>}
{error && <span style={{ color: 'red', marginLeft: '10px' }}>{error}</span>}
{account && (
<div style={{ marginTop: '10px' }}>
<p>Account: {account.publicKey}</p>
<p>Sequence: {account.sequence}</p>
<p>Balance: {balance} XLM</p>
</div>
)}
</div>

{!connected ? (
<ContactList contacts={contacts} onSelect={setSelectedContact} />
) : (
<>
<PaymentForm
selectedContact={selectedContact}
amount={amount}
assetCode={assetCode}
assetIssuer={assetIssuer}
onAmountChange={setAmount}
onAssetCodeChange={setAssetCode}
onAssetIssuerChange={setAssetIssuer}
onSelectContact={setSelectedContact}
onSend={handleSendPayment}
onSendSac={handleSendSacTransfer}
contacts={contacts}
isSacTransfer={false}
/>
<TransactionHistory transactions={transactions} />
</>
)}
</div>
);
};

export default App;
Loading