Skip to content
Merged
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
57 changes: 32 additions & 25 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
# Supabase Configuration
# You can find these values in your Supabase Dashboard under Project Settings > API
NEXT_PUBLIC_SUPABASE_URL=your-project-url-here.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
# ── Database (Neon Postgres) ──────────────────────────────────────────────────
# Pooled connection string from the Neon console (Connection Details → Pooled).
# Used by the app (Drizzle over Neon's serverless driver), the keeper scripts,
# and `npm run db:migrate`. SERVER-ONLY.
DATABASE_URL=postgres://user:password@ep-xxx-pooler.region.aws.neon.tech/neondb?sslmode=require

# ── Sessions ──────────────────────────────────────────────────────────────────
# Signs the HttpOnly session cookie (HS256 JWT) issued after SEP-10 sign-in.
# At least 32 characters. Rotating it signs everyone out.
# openssl rand -base64 48
SESSION_SECRET=

# ── File storage (Vercel Blob) ────────────────────────────────────────────────
# KYC documents are stored as private blobs. Create a Blob store in the Vercel
# dashboard (Storage → Blob) and copy its read/write token.
BLOB_READ_WRITE_TOKEN=

# Application Configuration
# This is used for OAuth redirects and email confirmation links
# Public origin of the app (referral links, e-mail links, SIWS domain default).
# Local development: http://localhost:3000
# Production: https://your-domain.com
NEXT_PUBLIC_SITE_URL=http://localhost:3000
Expand Down Expand Up @@ -48,20 +60,17 @@ NEXT_PUBLIC_SIWS_DOMAIN=localhost:3000
# SEP-10 server signing key (S...). SERVER-ONLY — generate a dedicated key,
# never reuse the platform admin key. stellar keys generate trustlend-siws --global
SIWS_SERVER_SECRET=
#
# Secret used to deterministically derive each wallet's Supabase auth password
# (HMAC-SHA256 of the address). SERVER-ONLY — use a long random value and never
# rotate without a migration plan (rotating invalidates existing wallet logins).
SIWS_PASSWORD_SECRET=
NEXT_PUBLIC_STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org
NEXT_PUBLIC_STELLAR_FRIENDBOT_URL=https://friendbot.stellar.org

# Auth Role Persistence
# These keys are used internally to manage role-based redirects
NEXT_PUBLIC_PENDING_ROLE_KEY=trustlend_pending_role

# Comma-separated allowlist for Trade Vault admin panel access
# Example: admin1@tradevault.com,admin2@tradevault.com
# Comma-separated allowlist for the admin panel. Accepts e-mail addresses and
# Stellar public keys (G...), since accounts are wallet-based. An allowlisted
# account must ALSO have profiles.role = 'admin'.
# Example: GABC...XYZ,admin@example.com
TRADE_VAULT_ADMIN_EMAILS=

# ── Soroban RPC ───────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -122,9 +131,6 @@ ADMIN_SECRET_KEY=
DEFAULT_GRACE_PERIOD_DAYS=7
DEFAULT_INSURANCE_PAYOUT_DAYS=60

# Supabase service-role key — required by all crons for trusted DB access.
SUPABASE_SERVICE_ROLE_KEY=

# ── Decentralized Credit Oracle ───────────────────────────────────────────────
# The authorized oracle is the only account allowed to post off-chain credit
# scores on-chain (via `submit_credit_score`). Register it once after deploy:
Expand All @@ -140,17 +146,18 @@ NEXT_PUBLIC_ORACLE_ADDRESS=
ORACLE_SECRET_KEY=

# ── Liquidation Keeper (scripts/liquidation-keeper.ts) ────────────────────────
# Automated bot that liquidates under-collateralized loans. Deployed as a
# background worker that monitors every minute: vercel.json schedules
# POST /api/cron/liquidation on "* * * * *" (authenticated with Bearer CRON_SECRET,
# same as the other crons) — or run `npm run liquidation:keeper:service`
# Automated bot that liquidates under-collateralized loans. Vercel Hobby only
# permits daily crons, so vercel.json runs POST /api/cron/liquidation once a
# day as a safety net and .github/workflows/keepers.yml hits the same endpoint
# every 5 minutes (needs the KEEPER_BASE_URL + CRON_SECRET repo secrets) — or
# run `npm run liquidation:keeper:service`
# (--interval=60) self-hosted. One-shot `npm run liquidation:keeper` remains
# available for cron schedulers. Requires ADMIN_SECRET_KEY (above) to sign
# liquidation transactions, plus NEXT_PUBLIC_LENDING_CONTRACT_ID /
# NEXT_PUBLIC_REPUTATION_CONTRACT_ID / NEXT_PUBLIC_ADMIN_ADDRESS.
#
# Where to source open loans from: "db" (Supabase, default) or "chain"
# (iterate the LendingContract directly — no Supabase needed).
# Where to source open loans from: "db" (the database, default) or "chain"
# (iterate the LendingContract directly — no database needed).
LIQUIDATION_SOURCE=db
# Evaluate only; never submit a liquidation transaction. Useful for staging.
LIQUIDATION_DRY_RUN=false
Expand Down Expand Up @@ -287,10 +294,10 @@ ORACLE_DISCORD_WEBHOOK_URL=
# In CI these are repository *secrets*, not values in this file. Restore
# instructions and bucket setup live in docs/disaster-recovery.md.
#
# Direct Postgres connection string. Use the DIRECT connection (port 5432), not
# the pooled/pgbouncer one — pg_dump needs session-level features the pooler
# does not provide. Supabase: Project Settings → Database → Connection string.
DATABASE_URL=
# pg_dump needs the DIRECT (non-pooled) Neon connection string — the same
# DATABASE_URL as above with the "-pooler" segment removed from the host. Set
# BACKUP_DATABASE_URL when it differs; otherwise DATABASE_URL is used.
BACKUP_DATABASE_URL=
#
# Passphrase used to encrypt each dump with AES-256 before upload.
# ⚠️ Store this in a password manager as well as in CI. If it is lost, every
Expand Down
16 changes: 13 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
runs-on: ubuntu-latest
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
Expand All @@ -59,16 +60,25 @@ jobs:
- name: Lint
run: npm run lint

- name: Unit tests
run: npm test

- name: Build Next.js
# We set this environment variable because dummy keys are needed for the build if relying on env
# Placeholder env so the build never needs real secrets.
env:
NEXT_PUBLIC_SUPABASE_URL: "https://example.supabase.co"
NEXT_PUBLIC_SUPABASE_ANON_KEY: "dummy-key"
NEXT_PUBLIC_STELLAR_NETWORK: "testnet"
NEXT_PUBLIC_STELLAR_HORIZON_URL: "https://horizon-testnet.stellar.org"
NEXT_PUBLIC_ADMIN_ADDRESS: "GAJRNUO6HSMQG4FNHNWQVRXJZJZ7QRA7HXPYYB6H5PTA3EAAJXJNZD7U"
run: npm run build

# Apply pending Drizzle migrations to the production database before the
# deploy goes live. No-op until the DATABASE_URL secret exists.
- name: Migrate database
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && env.DATABASE_URL != '' }}
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: npm run db:migrate

- name: Deploy to Vercel
if: ${{ env.VERCEL_TOKEN != '' }}
run: npx vercel --prod --yes --token=${{ env.VERCEL_TOKEN }}
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/e2e-playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,13 @@ jobs:
- name: Build Next.js Application
# We supply dummy configuration since tests intercept the actual calls
env:
NEXT_PUBLIC_SUPABASE_URL: "https://example.supabase.co"
NEXT_PUBLIC_SUPABASE_ANON_KEY: "dummy-key"
NEXT_PUBLIC_STELLAR_NETWORK: "testnet"
NEXT_PUBLIC_STELLAR_HORIZON_URL: "https://horizon-testnet.stellar.org"
NEXT_PUBLIC_ADMIN_ADDRESS: "GAJRNUO6HSMQG4FNHNWQVRXJZJZ7QRA7HXPYYB6H5PTA3EAAJXJNZD7U"
run: npm run build

- name: Run Playwright tests
env:
NEXT_PUBLIC_SUPABASE_URL: "https://example.supabase.co"
NEXT_PUBLIC_SUPABASE_ANON_KEY: "dummy-key"
NEXT_PUBLIC_STELLAR_NETWORK: "testnet"
NEXT_PUBLIC_STELLAR_HORIZON_URL: "https://horizon-testnet.stellar.org"
NEXT_PUBLIC_ADMIN_ADDRESS: "GAJRNUO6HSMQG4FNHNWQVRXJZJZ7QRA7HXPYYB6H5PTA3EAAJXJNZD7U"
Expand Down
53 changes: 53 additions & 0 deletions .github/workflows/keepers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Keepers (liquidation + price oracle)

# Vercel Hobby only allows one cron run per day, which is far too slow for the
# liquidation keeper and the collateral price oracle. GitHub Actions can fire
# every 5 minutes, so it pings the same authenticated cron endpoints instead.
#
# Required repository secrets:
# KEEPER_BASE_URL e.g. https://trustlend-stellar.vercel.app (no trailing slash)
# CRON_SECRET must match the CRON_SECRET env var configured on Vercel
#
# The workflow is a no-op until both secrets exist, so it is safe on forks.

on:
schedule:
- cron: "*/5 * * * *"
workflow_dispatch:

concurrency:
group: keepers
cancel-in-progress: false

jobs:
run:
name: Trigger cron endpoints
runs-on: ubuntu-latest
timeout-minutes: 5
env:
KEEPER_BASE_URL: ${{ secrets.KEEPER_BASE_URL }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
steps:
- name: Skip when secrets are not configured
id: gate
run: |
if [ -z "$KEEPER_BASE_URL" ] || [ -z "$CRON_SECRET" ]; then
echo "KEEPER_BASE_URL / CRON_SECRET not set — nothing to do."
echo "configured=false" >> "$GITHUB_OUTPUT"
else
echo "configured=true" >> "$GITHUB_OUTPUT"
fi

- name: Liquidation keeper
if: steps.gate.outputs.configured == 'true'
run: |
curl --fail-with-body --silent --show-error --max-time 120 \
-X POST "$KEEPER_BASE_URL/api/cron/liquidation" \
-H "Authorization: Bearer $CRON_SECRET"

- name: Price oracle
if: steps.gate.outputs.configured == 'true'
run: |
curl --fail-with-body --silent --show-error --max-time 120 \
-X POST "$KEEPER_BASE_URL/api/cron/price-oracle" \
-H "Authorization: Bearer $CRON_SECRET"
Comment on lines +24 to +53
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<img src="https://img.shields.io/badge/Next.js-16-black?logo=next.js" alt="Next.js" />
<img src="https://img.shields.io/badge/React-19-20232A?logo=react" alt="React" />
<img src="https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript&logoColor=white" alt="TypeScript" />
<img src="https://img.shields.io/badge/Supabase-Backend-3ECF8E?logo=supabase&logoColor=white" alt="Supabase" />
<img src="https://img.shields.io/badge/Neon-Postgres-00E599?logo=postgresql&logoColor=white" alt="Neon Postgres" />
<img src="https://img.shields.io/badge/Stellar-Testnet-08B5E5" alt="Stellar" />
<img src="https://img.shields.io/badge/Soroban-Smart%20Contracts-111827" alt="Soroban" />
<img src="https://img.shields.io/badge/Stellar%20Wave-Issues%20in%20the%20stellar%20wave%20Program-6366f1" alt="Stellar Wave" />
Expand Down Expand Up @@ -61,7 +61,7 @@ TrustLend is designed as a foundational layer for decentralized, inclusive credi

## 🏗️ Architecture & Workflow

TrustLend uses a practical hybrid architecture: **fast UX off-chain** (Supabase/Next.js) combined with **trust-critical logic on-chain** (Soroban/Stellar). The diagram below maps every component and data flow across all six layers of the platform.
TrustLend uses a practical hybrid architecture: **fast UX off-chain** (Next.js + Neon Postgres) combined with **trust-critical logic on-chain** (Soroban/Stellar). The diagram below maps every component and data flow across all six layers of the platform.

```mermaid
flowchart TB
Expand All @@ -84,7 +84,7 @@ flowchart TB
subgraph Backend["⚙️ Backend Layer (Next.js)"]
direction TB
SA[("📡 Server Actions & API Routes<br/>app/actions + app/api")]
SB[("🗄️ Supabase<br/>PostgreSQL · Auth · RLS · Storage")]
SB[("🗄️ Neon Postgres<br/>Drizzle ORM · sessions · Vercel Blob")]
RM[("🔌 Soroban Client<br/>lib/stellar/soroban.ts")]
SC[("🔐 Server-side Contract Invoker<br/>lib/stellar/server-contract.ts")]
RC[("⚡ Redis Cache<br/>Simulation result cache")]
Expand Down Expand Up @@ -210,7 +210,7 @@ flowchart LR
style S fill:#3b82f6,color:#fff
```

1. **Onboarding:** User signs up via Supabase Auth, connects a Stellar wallet (Freighter / xBull / Albedo on desktop, or any WalletConnect v2 mobile wallet such as LOBSTR by scanning a QR code), completes KYC verification, and their on-chain reputation profile is initialized.
1. **Onboarding:** User signs in with their Stellar wallet (SEP-10 challenge signature; no passwords) (Freighter / xBull / Albedo on desktop, or any WalletConnect v2 mobile wallet such as LOBSTR by scanning a QR code), completes KYC verification, and their on-chain reputation profile is initialized.
2. **Borrowing:** Borrower submits a loan request. The Next.js backend calls `ReputationContract.calculate_max_loan` and `calculate_interest_rate` to determine eligibility and terms.
3. **Lending:** Lender reviews the request in the marketplace, approves it, and the `LendingContract.approve_loan` is called. Funds are locked via `EscrowContract.create_escrow_hold`.
4. **Disbursement:** After the 1-hour revocation window expires, the admin confirms disbursement. `EscrowContract.confirm_disbursement` releases funds to the borrower, and `LendingContract.activate_loan` marks the loan as active.
Expand All @@ -221,7 +221,7 @@ flowchart LR

| Automation | Trigger | Action |
|---|---|---|
| **Payment-Due Scheduler** | Vercel Cron (hourly) | Queries Supabase for loans due within 48h → Sends webhook & email |
| **Payment-Due Scheduler** | Vercel Cron (daily) | Queries the database for loans due within 48h → Sends webhook & email |
| **Default Management** | Vercel Cron (daily) | Checks overdue loans against ledger time → Marks defaulted on-chain → Proposes insurance payout via MultiSigAdmin (requires N-of-M human approval) |
| **Liquidation Keeper** | Manual / cron | Monitors LTV ratios against dynamic thresholds → Liquidates under-collateralized positions → Posts Slack/Discord alerts |
| **Oracle Credit Score** | Manual / cron | Posts verified off-chain credit scores to the Reputation contract |
Expand All @@ -233,7 +233,7 @@ flowchart LR
| Layer | Technology |
|---|---|
| **Frontend** | Next.js 16, React 19, TypeScript, Tailwind CSS 4, Framer Motion |
| **Backend & DB** | Supabase (Auth, Postgres RLS, Storage) |
| **Backend & DB** | Neon Postgres + Drizzle ORM, SEP-10 wallet sessions (`jose`), Vercel Blob for KYC files |
| **Blockchain** | Stellar Testnet, Soroban RPC, Horizon API |
| **Wallet** | Freighter Wallet, xBull, Albedo, WalletConnect v2 for mobile wallets (`@creit.tech/stellar-wallets-kit`) |
| **Smart Contracts** | Rust (Soroban, `wasm32v1-none`) — 8 contracts deployed |
Expand Down Expand Up @@ -293,7 +293,7 @@ npm run deploy:testnet:dry
### What it writes

Contract IDs land directly in `.env.local`. Keys already present are updated **in
place** — your Supabase keys, API secrets and comments are left untouched, and a
place** — your database URL, API secrets and comments are left untouched, and a
`.env.local.bak` is taken first. A reference copy also goes to `.env.contracts`.

| Contract | Env key |
Expand Down Expand Up @@ -368,8 +368,8 @@ TrustLend includes an automated scheduler that checks for loans with payment dea

### How It Works

1. An external scheduler (Vercel Cron or any HTTP trigger) calls `POST /api/cron/payment-due` hourly.
2. The route queries Supabase for `active` or `funded` loans with `due_at` between now and +48 hours.
1. An external scheduler (Vercel Cron or any HTTP trigger) calls `POST /api/cron/payment-due` daily.
2. The route queries the database for `active` or `funded` loans with `due_at` between now and +48 hours.
3. A POST webhook is sent to `WEBHOOK_NOTIFICATION_URL` for each qualifying loan.
4. The loan's `metadata.payment_due_notified_at` is set to prevent duplicate notifications.
5. Per-loan errors are logged without stopping the rest of the batch.
Expand All @@ -380,7 +380,7 @@ TrustLend includes an automated scheduler that checks for loans with payment dea
|---|---|
| `WEBHOOK_NOTIFICATION_URL` | URL of the notification service that receives payment-due webhook POSTs |
| `CRON_SECRET` | Secret token used to authenticate scheduler requests (`Authorization: Bearer <value>`) |
| `SUPABASE_SERVICE_ROLE_KEY` | Supabase service-role key (required for RLS-bypassing loan queries) |
| `DATABASE_URL` | Neon Postgres connection string |
| `RESEND_API_KEY` | Optional Resend API key for borrower email notifications |
| `RESEND_FROM_EMAIL` | Verified sender address used for TrustLend emails |
| `RESEND_REPLY_TO_EMAIL` | Optional reply-to address for support responses |
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ If you discover a security vulnerability within TrustLend, **do NOT open a publi
- **Frontend/UI Bugs:** Visual presentation flaws, CSS issues, or non-security UI bugs without impact on user funds or data.
- **Denial of Service (DoS):** Volumetric DoS/DDoS attacks against infrastructure or public Stellar RPC endpoints not caused by application design flaws.
- **Social Engineering:** Phishing, spam, or social engineering attacks targeted at TrustLend maintainers or users.
- **Third-Party Dependencies:** Vulnerabilities in underlying infrastructure (e.g. Stellar Core, Soroban SDK, Supabase platform) unless directly exploitable through TrustLend code logic.
- **Third-Party Dependencies:** Vulnerabilities in underlying infrastructure (e.g. Stellar Core, Soroban SDK, Neon, Vercel) unless directly exploitable through TrustLend code logic.
- **Known Issues:** Vulnerabilities already reported, tracked in public issues/PRs, or previously disclosed in security audit reports.

---
Expand Down
Loading
Loading