Skip to content

About

Split Bills. Pay On-Chain

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Latest commit

 

History

305 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SettleX Logo

SettleX — Split Bills. Pay On-Chain.

A decentralised expense-splitting application built on the Stellar Network.
Add group expenses, split them by any method, and settle every share directly from your wallet — confirmed on-chain in under 5 seconds.

Contract Deployed Sample Transaction CI Pipeline Stellar Testnet 45 Tests Passing

Live Demo  •  Demo Video


Table of Contents


Project Description

SettleX solves a fundamental problem with group expense sharing: apps like Splitwise only track IOUs — you still have to chase people for real money. SettleX closes the loop by connecting split calculations directly to on-chain payments.

Every payment produces a real, verifiable transaction hash on the Stellar blockchain — that hash, checkable on any explorer, is the evidence a payment happened. An on-chain Soroban smart contract additionally records each settlement so participants share one append-only history. Those records are currently self-attested: the contract stores what the member submits without checking it against Horizon, so treat the Stellar transaction as the proof and the contract record as bookkeeping (see Architecture and Limitations). The app refreshes shared expenses and trips about every five seconds while a page is visible.

Core design principles:

  • Non-custodial — your private key never leaves your wallet extension. SettleX only receives the signed transaction envelope.
  • No intermediary — money moves peer-to-peer between Stellar wallets; the app never holds funds.
  • On-chain receipts — every settled share has a Stellar transaction hash you can verify on stellar.expert.
  • Multi-wallet — supports Freighter, xBull, Lobstr, and Rabet with install detection. WalletConnect (mobile wallets) is documented as a one-step add in docs/WALLET_INTEGRATION.md.

Feature Status
Multi-wallet connect (Freighter, xBull, Lobstr, Rabet) Live
Create and split expenses — equal, percentage, custom weight Live
Pay shares with XLM (Stellar Payment operation) Live
SEP-0007 QR code generation for mobile wallets Live
Transaction hash receipt linked to Stellar Explorer Live
Soroban contract: append-only on-chain settlement records Live
Real-time event listening from contract (pmt_rec events) Live
Attestor-backed payment verification Not live yet
Trip mode — group expenses with net-balance settle-up Live
Cross-user expense/trip sync via secure 5-second polling Live
Net-balance algorithm (minimises transactions needed) Live
Mobile-responsive UI Live

Tech Stack

Layer Technology
Framework Next.js 14 (App Router, TypeScript)
Styling Tailwind CSS 3.4, Framer Motion
Blockchain SDK @stellar/stellar-sdk v14
Wallet support @creit.tech/stellar-wallets-kit v2
Smart contract Soroban (Rust, soroban-sdk v21)
Network Stellar Testnet (Horizon + Soroban RPC)
Database Supabase (PostgreSQL via authenticated Next.js proxy)
QR codes qrcode.react
Testing Jest 29 + ts-jest (45 unit tests)
State management React Context + HttpOnly session cookie + localStorage cache
UI primitives Radix UI, Lucide React

Screenshots

Home / Landing Page

Home


Dashboard — Wallet Connected & Balance Displayed

The dashboard shows your connected public key, live XLM balance fetched from Horizon, and the Testnet indicator.

Dashboard


Expenses — Wallet Options & Split View

The multi-wallet selector lets users connect via Freighter, xBull, or Lobstr. Wallets not installed show an "Install" badge. Below it the expenses list shows split breakdowns with Pay XLM buttons per share.

Expenses


Trips Overview

Trips


New Expense Form

Define members, set amounts, and choose the split mode (equal / percentage / custom weight). The form calculates each share in real time.

New Expense Form


Test Output — 45 Tests Passing

All three test suites passing with Jest + ts-jest.

Test Output


Deployment Proof (CLI)

Fresh testnet deployment output and contract ID confirmation.

Deployment Proof


How It Works

User connects wallet (Freighter / xBull / Lobstr)
              |
User creates expense, adds members, picks split mode
              |
App calculates each member's exact share
              |
Payer clicks "Pay XLM"
              |
    +---------+-----------+
    |                     |
buildTransaction()    checkIsPaid()     <- on-chain duplicate check (Soroban)
    |
TransactionBuilder constructs Payment operation
  · from:   payer public key
  · to:     payee Stellar address
  · amount: share amount in XLM
  · memo:   expense ID
    |
Wallet extension signs client-side
(private key never leaves the browser)
    |
Signed envelope submitted to Horizon REST API
    |
Stellar validators confirm in ~5 seconds
    |
recordPaymentOnChain() stores to Soroban contract:
  trip_id, expense_id, payer, member, amount, tx_hash
    |
tx hash stored in app, share marked "paid"
useContractEvents polls every 10s -> syncs state for all participants

Smart Contract

SettleX deploys a Soroban smart contract on Stellar Testnet as an immutable, tamper-proof ledger of all settled payments.

Deployed Contracts (Testnet)

Contract Role Contract ID Explorer
Settlement contract CAXVH54JVEUCLDQQW45SMRFTCAAVUGVBT4SBVLAGYDQ2YMAPK6YL6ADI View
Pool contract CB4P4EXLGS56IXVNU3PLJO2BHF5BEEPBBYJAHXEPZDSD2OISQIGO53JA View

Network: Stellar Testnet
Language: Rust (soroban-sdk v22.0.11)

Verified On-Chain Transactions

The final proof transaction above is verifiable evidence of the inter-contract path: settlement record_payment triggered pool withdraw in the same on-chain execution.

New deployments must use scripts/deploy-contract.sh. Both instances use Soroban constructors, so their admin state is initialized atomically during deployment. The script deploys the settlement instance, deploys the pool instance with the settlement address, and submits the admin-authenticated pool-reference update. Do not deploy the WASM and call init or init_pool manually; those legacy entry points reject calls after constructor initialization and are retained only for compatibility with older instances.

Security hardening note:

  • Pool credits are now admin-managed in the pool contract to prevent untrusted self-crediting.
  • Pool withdraw requires auth from both the configured settlement contract and the member. Credits can therefore only be consumed as part of a record_payment call — a member cannot drain their own credits directly, and no other contract can spend them. Rotating set_settlement_contract immediately revokes the previous contract's ability to withdraw.
  • Settlement records now carry an attested flag. With no attestor configured a record is self-attested — it proves only that a member wrote a string, and the contract verifies neither the payer, the amount, nor the tx_hash against Horizon. Treat the Stellar transaction on the explorer as the evidence, not the in-app record. set_attestor makes a verifier's co-signature mandatory and marks records attested: true; the verifier service itself is not yet built, so attestor-backed verification is not live in this repo.
  • clear_paid (admin-only) clears the ExpensePaid flag, so a bogus or mistaken record can no longer permanently block the legitimate one. It preserves the original entry in trip history.

Redeploy required. PaymentRecord gained a field and CONTRACT_VERSION is now 2. Records written by a v1 deployment cannot be read by this build — deploy fresh contracts and update NEXT_PUBLIC_CONTRACT_ID.


Project Status & Production Roadmap

Status: Testnet beta. SettleX runs entirely on Stellar Testnet — no real funds move yet. Settlement currently uses native XLM. USDC settlement, verified on-chain settlement, broader wallet support, and a Mainnet launch are on the roadmap. We state exactly where we are rather than overselling.

The complete, honest plan to take SettleX from project to shipped product — including the known trust gaps and how each one is closed — lives in docs/PRODUCT_CONVERSION_GUIDE.md.

Area Today Next
Network Stellar Testnet Mainnet (after a contract audit)
Settlement asset XLM (native) USDC via Stellar path payments
Settlement integrity On-chain receipt log (tx hash stored; not yet verified in-contract) Verified / atomic on-chain settlement
Wallets Freighter, xBull, Lobstr, Rabet + WalletConnect (mobile), passkey smart wallets
App shell Installable PWA, nonce-based CSP Push notifications, offline reads
Identity Wallet address (client-supplied) Sign-In-With-Stellar (signed challenge)

Verified locally with npm run build, npm run lint, and npm test.

Submission Checklist Evidence

This section maps the required submission checklist to concrete proof in this repository.

Requirement Proof
Public GitHub repository https://github.com/SnigdhaMandal/SettleX
README with complete documentation This file plus final docs links in Final Documentation Package
Minimum 8+ meaningful commits Local repository history: 44 commits
Live demo link https://settle-x-pi.vercel.app/
Screenshot: mobile responsive view UI pages were hardened for mobile in Phase 9. Add one fresh phone viewport screenshot to public/mobile-responsive.png and reference it here before final submission.
Screenshot or badge: CI/CD pipeline running CI badge at the top of this README (GitHub Actions ci.yml)
Contract addresses and transaction hash (inter-contract) Deployed Contracts and Verified On-Chain Transactions
Token or pool address (if custom token or pool deployed) Pool contract in Deployed Contracts

Contract Functions

Settlement Contract (SettleXContract)

Function Type Authorization Purpose
__constructor(admin) Constructor Admin Initializes contract admin and schema version at deployment
init(admin, pool_contract) Write Admin Legacy initialization entrypoint
upgrade(new_wasm_hash) Write Admin Atomically upgrades contract code to a new WASM hash
migrate(from_version) Write Admin Migrates contract state between schema versions
set_pool_contract(new_pool_contract) Write Admin Updates the linked settlement pool contract address
get_pool_contract() Read None Returns the current linked pool contract address
set_attestor(attestor) Write Admin Sets required co-signing attestor address; emits AttestorEventV1
clear_attestor() Write Admin Clears attestor, reverting records to self-attested
get_attestor() Read None Returns current attestor (if any) and bumps instance TTL
clear_paid(expense_id, member) Write Admin Emergency escape hatch: clears ExpensePaid flag to unblock a griefed expense
record_payment(trip_id, expense_id, payer, member, amount, tx_hash) Write Member (+ Attestor if set) Stores on-chain payment receipt with 32-byte transaction hash (BytesN<32>)
get_payment(trip_id, expense_id, member) Read None Fetches payment record for specific member
get_payments(trip_id, offset, limit) Read None Returns a bounded page of payment history
is_paid(expense_id, member) Read None Checks if a member has already settled a specific share
extend_trip(trip_id, payment_offset, expense_offset, limit) Write None (Permissionless) Renews storage TTLs for bounded history pages
get_version() Read None Returns current contract schema version
get_admin() Read None Returns current contract admin address

Pool Contract (SettlementPoolContract)

Function Type Authorization Purpose
__constructor(admin, settlement_contract) Constructor Admin Initializes pool admin and linked settlement contract
init_pool(admin, settlement_contract) Write Admin Legacy pool initialization entrypoint
upgrade(new_wasm_hash) Write Admin Atomically upgrades pool contract code
migrate(from_version) Write Admin Migrates pool storage between versions
set_settlement_contract(new_settlement_contract) Write Admin Updates authorized settlement contract address
get_settlement_contract() Read None Returns authorized settlement contract address
deposit(member, amount) Write Admin Admin-managed pool credit deposit for a member
withdraw(member, amount) Write Settlement Contract & Member Withdraws pool credits during payment settlement
get_balance(member) Read None Returns member's current pool credit balance
get_version() Read None Returns current pool schema version
get_admin() Read None Returns pool admin address

get_payments and is_paid are pure simulated reads; they do not renew storage TTLs. To keep history live, submit extend_trip as a real transaction (a keeper or any participant may submit it), then repeat with the returned payment and expense offsets until both are None. If an entry has already expired, include the archived footprint in the transaction so Soroban can restore it, or restore the trip's data before calling extend_trip.

Contract Error Codes

Settlement Contract Errors

Code Name Meaning
#1 InvalidAmount Payment amount is zero or negative
#2 AlreadyPaid Share is already settled on-chain
#3 EmptyId Trip ID or expense ID is empty
#4 AlreadyInitialized Contract has already been initialized
#5 NotInitialized Contract has not been initialized yet
#6 InvalidActor Invalid caller / address (e.g. payer equals member, or admin equals pool)
#7 IdTooLong Trip ID or expense ID exceeds maximum allowed length (64 bytes)
#8 AmountTooLarge Payment amount exceeds MAX_AMOUNT_STROOPS (1e15 stroops / 1e8 XLM)
#9 VersionMismatch State version mismatch during migration
#10 TxHashTooLong Transaction hash validation error
#11 NotPaid Attempted to clear an expense payment that was not marked as paid
#12 Unauthorized Caller lacks authorization (e.g. non-admin or missing attestor signature)
#13 InvalidPage Bounded pagination parameters are invalid (e.g. limit is 0)
#14 IndexOverflow Bounded trip index exceeded maximum capacity

Pool Contract Errors

Code Name Meaning
#101 AlreadyInitialized Pool has already been initialized
#102 NotInitialized Pool has not been initialized yet
#103 Unauthorized Caller is not authorized for pool operation
#104 InvalidAmount Deposit or withdrawal amount is zero or negative
#105 InsufficientBalance Member has insufficient balance for withdrawal
#106 BalanceOverflow Deposit would cause member balance to overflow
#107 VersionMismatch Pool state version mismatch during migration
#108 InvalidActor Invalid actor address specified
#109 AmountTooLarge Deposit or withdrawal exceeds MAX_AMOUNT_STROOPS

Why a Smart Contract?

In a Horizon-only setup there is no shared source of truth for payment status — any user could mark a share as paid without actually transferring XLM. The Soroban contract solves this:

  • record_payment stores the real Stellar tx_hash as proof of transfer.
  • is_paid acts as a duplicate guard — re-settling an already-paid share is rejected on-chain with AlreadyPaid (#2).
  • All records are permanently on the Stellar ledger and cannot be modified or deleted by anyone, including the app operator.

Real-Time Events

The contract emits a pmt_rec event for every confirmed payment. hooks/useContractEvents.ts polls the Soroban RPC every 10 seconds and on browser tab focus. New events are deduplicated by txHash and merged into React state — all participants see confirmed payments without refreshing the page.


Setup Instructions

Prerequisites

  • Node.js v18 or later
  • npm v9 or later
  • Freighter Wallet browser extension — freighter.app
    • In Freighter: Settings → Network → select Testnet
  • A funded Stellar Testnet account — get free XLM from Stellar Friendbot

1. Clone the repository

git clone https://github.com/SnigdhaMandal/SettleX.git
cd SettleX

2. Install dependencies

npm install

3. Create a Supabase project

SettleX uses Supabase for cross-user sync, refreshed about every five seconds while a page is visible. Without it, data only persists locally in the current browser session.

  1. Create a free project at supabase.com.
  2. In the SQL Editor, run the full schema from supabase-setup.sql (or follow docs/SUPABASE_SETUP.md for a walkthrough).
  3. Copy your Project URL and anon key from Project Settings → API.
  4. Copy your JWT Secret from the same page into SUPABASE_JWT_SECRET. Row Level Security authorizes on a wallet_address claim that the app's server signs with this secret — without it, every database call is rejected. See Wallet Authentication.

4. Configure environment variables

cp .env.local.example .env.local

Fill in your Supabase credentials. The contract ID and RPC URL are pre-filled with the deployed testnet values. See Environment Variables for the full reference.

5. Start the development server

npm run dev

Open http://localhost:3000 and connect your Freighter wallet (set to Testnet).

6. Build for production

npm run build
npm run start

Environment Variables

Create (or edit) .env.local in the project root:

# Stellar Network
NEXT_PUBLIC_STELLAR_NETWORK=TESTNET
NEXT_PUBLIC_HORIZON_URL=https://horizon-testnet.stellar.org
NEXT_PUBLIC_STELLAR_EXPLORER=https://stellar.expert/explorer/testnet

# Soroban Smart Contract (deployed — no changes needed)
NEXT_PUBLIC_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
NEXT_PUBLIC_CONTRACT_ID=CAXVH54JVEUCLDQQW45SMRFTCAAVUGVBT4SBVLAGYDQ2YMAPK6YL6ADI

# Supabase (from Project Settings → API in your Supabase dashboard)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here

# App
NEXT_PUBLIC_APP_NAME=SettleX
NEXT_PUBLIC_APP_VERSION=1.0.0

# ─── Server-only — never prefix these with NEXT_PUBLIC_ ─────────────────────
# Supabase Dashboard → Settings → API → JWT Secret.
SUPABASE_JWT_SECRET=your-supabase-jwt-secret
# Binds an auth challenge to the wallet it was issued to. Required in
# production and must differ from SUPABASE_JWT_SECRET (the server refuses to
# boot otherwise); falls back to it in dev. Generate: openssl rand -base64 48
AUTH_CHALLENGE_SECRET=
# Optional. Session lifetime in seconds (default 3600 = 1h, capped at 43200 = 12h).
AUTH_SESSION_TTL_SECONDS=43200

Variables with the NEXT_PUBLIC_ prefix are read client-side and are inlined into the JavaScript bundle — treat every one of them as public. The Supabase anon key is safe to expose because access is governed by Row Level Security.

SUPABASE_JWT_SECRET and AUTH_CHALLENGE_SECRET are not public. They are read only inside server-side auth and Supabase proxy route handlers. Prefixing either with NEXT_PUBLIC_ would ship it to every visitor and let anyone mint a token for any wallet.


Wallet Authentication

Your Stellar address is your identity, and the app proves you control it before the database will accept it — a SEP-10 style challenge/response:

  1. Challenge — POST /api/auth/challenge returns a transaction built for your address with sequence number 0, carrying a random nonce. Sequence 0 is always below an account's real sequence, so the network can never accept it: it is signable but unsubmittable, and costs no fee.
  2. Sign — your wallet (Freighter, xBull, Lobstr) signs it locally. Your private key never leaves the extension.
  3. Verify — POST /api/auth/verify checks the signature against the claimed public key and, only on success, mints a short-lived Supabase JWT whose wallet_address claim carries the proven address. The token is signed with SUPABASE_JWT_SECRET, which never reaches the browser.
  4. Authorize — /api/auth/verify sets the JWT as an HttpOnly, SameSite=Lax cookie and does not return it in JSON. Supabase REST calls go through same-origin Next.js handlers, which validate the cookie and forward the JWT to PostgREST. Every RLS policy reads the claim via public.settlex_wallet(); requests without a valid token get no rows.

The browser never reads or stores the JWT in localStorage; only non-secret session metadata is held in memory. The cookie survives reloads until expiry (one hour by default, configurable up to 12 hours), so a valid session can be checked on reload without another signature. Near expiry, the UI asks the wallet to sign again. Because an HttpOnly cookie cannot authenticate a browser WebSocket, expense and trip updates use five-second polling while the page is visible instead of Supabase Realtime. This is slightly less immediate but keeps the bearer token out of JavaScript. HttpOnly prevents token extraction, but an XSS can still perform actions through the active page until the session expires or is signed out.

Upgrading an existing deployment: earlier versions authorized on an x-wallet-address request header that the browser set itself. Because the anon key is public, anyone could send that header with any address and read or modify every user's data. Re-run supabase-setup.sql to replace those policies and set SUPABASE_JWT_SECRET. Until you do, the data layer is effectively unauthenticated.


Testing

JavaScript / TypeScript unit tests

npm test

For coverage report:

npm run test:coverage
Test file Coverage
__tests__/split/calculator.test.ts Equal split, custom/weighted split, amount validation, Stellar address format checks
__tests__/settlement/netBalance.test.ts Net-balance algorithm: simple debt, netting, three-person chains, wallet address passthrough
__tests__/utils/formatters.test.ts formatAddress, formatXLM, stroopsToXlm conversions

45 tests across 3 suites — all passing. See the test output screenshot in Screenshots.

Rust contract tests

cd contract
cargo test

8 tests: record_and_query, multiple_members, multiple_expenses_same_trip, duplicate_payment_rejected, zero_amount_rejected, negative_amount_rejected, is_paid_unknown_returns_false, get_payments_unknown_trip_is_empty.


Project Structure

settlex/
├── app/
│   ├── page.tsx                  # Landing page
│   ├── auth/page.tsx             # Authentication
│   ├── dashboard/page.tsx        # Dashboard (wallet + balance)
│   ├── expenses/page.tsx         # Expense list and management
│   ├── trips/
│   │   ├── page.tsx              # Trip list
│   │   └── [id]/page.tsx         # Trip detail — expenses + settle-up tab
│   └── api/auth/
│       ├── challenge/route.ts    # Issues the challenge transaction to sign
│       └── verify/route.ts       # Verifies the signature, mints the Supabase JWT
│
├── components/
│   ├── landing/                  # Hero, Features, HowItWorks, Pricing, Testimonials
│   ├── expenses/                 # ExpenseForm, SplitCalculator, PaymentRow, ReceiptModal
│   ├── payment/                  # PayButton, PaymentStatus, QRCodeDisplay, TransactionHash
│   ├── trips/                    # TripCard, TripForm, ExpenseList, SettlementSummary
│   ├── wallet/                   # ConnectWalletButton, WalletInfo, WalletGuard
│   └── ui/                       # Button, Modal, Badge, Spinner, Toast, Logo
│
├── context/
│   ├── ExpenseContext.tsx        # Global expense state
│   ├── TripContext.tsx           # Global trip state
│   ├── AuthContext.tsx           # Authentication state
│   └── WalletContext.tsx         # Wallet connection state
│
├── hooks/
│   ├── useWallet.ts              # Multi-wallet connection (StellarWalletsKit)
│   ├── useExpense.ts             # Expense CRUD
│   ├── useTrip.ts                # Trip CRUD
│   ├── usePayment.ts             # Full 7-state payment flow orchestration
│   └── useContractEvents.ts      # Real-time Soroban event polling
│
├── lib/
│   ├── auth/
│   │   ├── challenge.ts          # Challenge build + signature verification (server)
│   │   ├── jwt.ts                # Supabase access-token minting (server)
│   │   ├── serverConfig.ts       # Server-only auth secrets
│   │   └── rateLimit.ts          # Throttling for the public auth routes
│   ├── supabase/session.ts       # Wallet-authenticated Supabase client (browser)
│   ├── stellar/
│   │   ├── walletsKit.ts         # Stellar Wallets Kit adapter
│   │   ├── contract.ts           # Soroban contract calls + error decoding
│   │   ├── events.ts             # pmt_rec event fetch and decode
│   │   ├── buildTransaction.ts   # Stellar Payment transaction builder
│   │   ├── submitTransaction.ts  # Horizon broadcast
│   │   └── getBalance.ts         # Account balance query
│   ├── qr/generator.ts           # SEP-0007 URI + QR generation
│   └── settlement/netBalance.ts  # Net-balance optimisation algorithm
│
├── contract/
│   ├── contracts/settlement/      # Settlement contract crate and WASM
│   └── contracts/pool/            # Pool contract crate and WASM
│
├── __tests__/
│   ├── split/calculator.test.ts
│   ├── settlement/netBalance.test.ts
│   └── utils/formatters.test.ts
│
└── docs/
    ├── QUICKSTART.md
    ├── SUPABASE_SETUP.md
    ├── AUTHENTICATION_SETUP.md
    └── MANUAL_TESTING_GUIDE.md

Testnet Notes

  • This app runs entirely on Stellar Testnet — no real funds are used.
  • Fund your Freighter testnet wallet with the Friendbot:
    https://horizon-testnet.stellar.org/friendbot?addr=YOUR_PUBLIC_KEY
    
  • All transactions and contract events are visible at stellar.expert/explorer/testnet.
  • The deployed contract address and sample transaction on this page can be verified live on the explorer at any time.

Final Documentation Package

Product & strategy

Getting started & testing

Operations & architecture


License

MIT © 2026 SettleX

About

Split Bills. Pay On-Chain

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages