diff --git a/.env.example b/.env.example
index d038f5b..6c10224 100644
--- a/.env.example
+++ b/.env.example
@@ -89,7 +89,7 @@ NEXT_PUBLIC_POOLED_LENDING_CONTRACT_ID=
NEXT_PUBLIC_GOVERNANCE_CONTRACT_ID=
# N-of-M multisig gating rare, high-impact admin config changes (issue #73):
# whitelisting collateral assets, fee tables, linking governance/oracle, and
-# moving insurance-fund balances. See contracts/MULTISIG_ADMIN.md.
+# moving insurance-fund balances. See docs/contracts/multisig-admin.md.
NEXT_PUBLIC_MULTISIG_ADMIN_CONTRACT_ID=
# TLEND governance token and its distribution contracts (issue #107). Optional —
@@ -98,24 +98,6 @@ NEXT_PUBLIC_TLEND_TOKEN_CONTRACT_ID=
NEXT_PUBLIC_TLEND_VESTING_CONTRACT_ID=
NEXT_PUBLIC_TLEND_AIRDROP_CONTRACT_ID=
-# Soroban event indexer read models
-# Point these at a Mercury, Ensorcel, or custom subgraph/indexer deployment.
-# During migration the app tries the indexer first and falls back to Supabase.
-# Set TRUSTLEND_INDEXER_READ_MODE=required only after the indexer is fully backfilled.
-TRUSTLEND_INDEXER_READ_MODE=fallback
-TRUSTLEND_INDEXER_GRAPHQL_URL=
-TRUSTLEND_INDEXER_REST_URL=
-TRUSTLEND_INDEXER_API_KEY=
-
-# Optional: override the default GraphQL documents if your indexer schema uses
-# different collection/filter names. The app falls back to REST when only
-# TRUSTLEND_INDEXER_REST_URL is configured.
-TRUSTLEND_INDEXER_BORROWER_LOANS_QUERY=
-TRUSTLEND_INDEXER_LENDER_LOANS_QUERY=
-TRUSTLEND_INDEXER_ADMIN_LOANS_QUERY=
-TRUSTLEND_INDEXER_REPUTATION_EVENTS_QUERY=
-TRUSTLEND_INDEXER_ESCROW_EVENTS_QUERY=
-
# ── Admin Stellar Address ─────────────────────────────────────────────────────
# This is the public G... address of the account that has administrative
# privileges on the contracts (e.g., freezing accounts, triggering payouts).
@@ -303,7 +285,7 @@ ORACLE_DISCORD_WEBHOOK_URL=
# Backup" GitHub Actions workflow (.github/workflows/db-backup.yml).
#
# In CI these are repository *secrets*, not values in this file. Restore
-# instructions and bucket setup live in DISASTER_RECOVERY.md.
+# 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
@@ -318,7 +300,7 @@ BACKUP_ENCRYPTION_KEY=
#
# Destination bucket name, without the s3:// prefix. Should live in a different
# account or at least a different region from the database, and have versioning
-# plus a restrictive bucket policy. See DISASTER_RECOVERY.md.
+# plus a restrictive bucket policy. See docs/disaster-recovery.md.
S3_BUCKET=
#
# Optional. Key prefix within the bucket (default: backups).
diff --git a/.github/workflows/db-backup.yml b/.github/workflows/db-backup.yml
index 0b2144e..87fddb8 100644
--- a/.github/workflows/db-backup.yml
+++ b/.github/workflows/db-backup.yml
@@ -83,7 +83,7 @@ jobs:
`Run: ${runUrl}`,
'',
'The database is running without a fresh off-site backup until this is fixed.',
- 'See DISASTER_RECOVERY.md for the runbook.',
+ 'See docs/disaster-recovery.md for the runbook.',
].join('\n');
// Reuse an existing open report rather than filing one per night.
diff --git a/.gitignore b/.gitignore
index ece2c5e..3a813a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,7 +58,6 @@ lib/contracts/generated/
# Per-network deployment state used by `npm run deploy:testnet -- --resume`
contracts/.deployments/
-/docs
# playwright
playwright-report/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 571519f..d1c42c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,12 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
rejection, and pushed on-chain. Fallback chain is live → cached → on-chain
TWAP → refuse to publish. The liquidation keeper now values positions with the
live price instead of a hardcoded constant. See
- [ORACLE_PRICE_FEEDS.md](ORACLE_PRICE_FEEDS.md).
+ [docs/oracle-price-feeds.md](docs/oracle-price-feeds.md).
- Referral programme (#266): every user gets a unique invite link, and when an
invited friend's first loan is funded the referrer's bonus is transferred
automatically by the new `referral_rewards` Soroban contract during
`activate_loan`. Includes a referral dashboard, attribution APIs, and
- `sql/09_referral_program.sql`. See [REFERRAL_PROGRAM.md](REFERRAL_PROGRAM.md).
+ `sql/09_referral_program.sql`. See [docs/referral-program.md](docs/referral-program.md).
- Borrowing user guide and FAQ at `/docs/borrowing`, covering the step-by-step
borrowing process, how the liquidation threshold is calculated, Health Factor
bands, and 15 frequently asked questions. Linked from the borrower dashboard
diff --git a/Dockerfile.dev b/Dockerfile.dev
deleted file mode 100644
index 64656ca..0000000
--- a/Dockerfile.dev
+++ /dev/null
@@ -1,22 +0,0 @@
-FROM node:20-alpine
-
-# Set working directory
-WORKDIR /app
-
-# Install system dependencies that might be needed for native node modules
-RUN apk add --no-cache libc6-compat python3 make g++
-
-# Copy package files
-COPY package.json package-lock.json* ./
-
-# Install dependencies
-RUN npm install
-
-# Copy the rest of the application
-COPY . .
-
-# Expose Next.js default port
-EXPOSE 3000
-
-# Start the application in development mode
-CMD ["npm", "run", "dev"]
diff --git a/OPTIMIZATION_SUMMARY.md b/OPTIMIZATION_SUMMARY.md
deleted file mode 100644
index fe5ae8d..0000000
--- a/OPTIMIZATION_SUMMARY.md
+++ /dev/null
@@ -1,226 +0,0 @@
-# Issue #39: Pool Query Performance Optimization - Summary
-
-## What Was Done
-
-Optimized Supabase database query performance for large pool lists by consolidating waterfall queries, implementing explicit column selection, and adding database indexes.
-
-## Files Created
-
-### 1. **lib/db/pools.ts** (530 lines)
-Core optimization module with reusable, typed functions for all pool queries.
-
-**Functions**:
-- `fetchPools()` - Fetch paginated pools with filters and sorting
-- `fetchPoolById()` - Fetch single pool by ID
-- `fetchActivePoolsWithLiquidity()` - Fetch active pools for auto-matching
-- `fetchAdminDashboardPools()` - Fetch pools + loans for admin dashboard
-
-**Features**:
-- ✓ Explicit column selection (no `SELECT *`)
-- ✓ Type-safe Pool interface
-- ✓ Pagination support
-- ✓ Error handling with descriptive messages
-- ✓ Index recommendations in comments
-
-### 2. **sql/04_pool_performance_rpc.sql** (130 lines)
-Database migration with RPC functions for complex queries.
-
-**Functions**:
-- `get_lending_pools_paginated()` - SQL RPC for paginated pool fetching
-- `get_active_pools_with_liquidity()` - SQL RPC for auto-matching queries
-
-**Features**:
-- ✓ Index recommendations
-- ✓ Security-definer for proper authorization
-- ✓ Atomic operations
-- ✓ Comments with usage examples
-
-### 3. **lib/db/pools.test.ts** (500+ lines)
-Comprehensive test suite verifying optimization benefits.
-
-**Tests**:
-- ✓ Query count verification (1 query per function)
-- ✓ Data shape validation
-- ✓ Pagination correctness
-- ✓ Filtering by status
-- ✓ Error handling
-- ✓ Profile relation cardinality
-- ✓ Performance metrics
-
-### 4. **POOL_OPTIMIZATION.md** (400+ lines)
-Complete documentation of optimization work, performance improvements, and implementation guide.
-
-## Files Modified
-
-### 1. **app/api/pools/route.ts**
-- Replaced direct queries with `fetchPools()`
-- Added pagination metadata to response
-- Enhanced query parameter support
-
-**Before**: 2 sequential queries
-**After**: 1 optimized query
-
-### 2. **app/dashboard/admin/pools/page.tsx**
-- Replaced waterfall queries with `fetchAdminDashboardPools()`
-- Queries now execute in parallel
-
-**Before**: 2 sequential queries
-**After**: 2 parallel queries
-
-### 3. **app/actions/admin-pools.ts**
-- Updated `approveLoan()` to use `fetchPoolById()`
-- Updated `runAutoMatch()` to use `fetchActivePoolsWithLiquidity()`
-- Improved performance of auto-matching logic
-
-**Before**: 2 + N sequential queries
-**After**: 2 + batched updates
-
-## Performance Improvements
-
-### Query Count Reduction
-- **Admin dashboard**: 2 queries → 1 parallel batch (50% faster)
-- **Pool list**: 2-3 queries → 1 query (60-66% fewer queries)
-- **Auto-match**: ~4 queries → 2-3 queries (25-50% reduction)
-
-### Network Round-Trips
-- **Before**: Multiple round-trips (waterfall)
-- **After**: Parallel queries reduce latency by ~50%
-
-### Database Load
-- **Explicit columns**: 40-60% less data transferred
-- **Indexed queries**: 50-200ms faster on large datasets
-- **Pre-sorted results**: Eliminates client-side sorting overhead
-
-### Estimated Improvements (Large Datasets)
-| Operation | Before | After | Gain |
-|-----------|--------|-------|------|
-| Fetch 10k pools (paginated) | ~500ms | ~50ms | 90% faster |
-| Admin dashboard load | ~400ms | ~250ms | 37% faster |
-| Auto-match 100 loans | ~1500ms | ~600ms | 60% faster |
-
-## Backward Compatibility
-
-✓ All changes are backward compatible
-- Existing API responses still work
-- New fields are optional
-- Component interfaces unchanged
-- No database schema changes required
-
-## Next Steps (Admin Action)
-
-1. **Apply Recommended Indexes** in Supabase:
- ```sql
- CREATE INDEX IF NOT EXISTS idx_lending_pools_status_available
- ON public.lending_pools (status, available_liquidity DESC);
-
- CREATE INDEX IF NOT EXISTS idx_lending_pools_created_at_desc
- ON public.lending_pools (created_at DESC);
-
- CREATE INDEX IF NOT EXISTS idx_lending_pools_available_liquidity_desc
- ON public.lending_pools (available_liquidity DESC);
- ```
-
-2. **Apply RPC Migration**:
- - Run `sql/04_pool_performance_rpc.sql` in Supabase SQL Editor
- - Verifies new RPC functions are created
-
-3. **Test in Staging**:
- - Admin dashboard pool loading
- - Auto-matching with pending loans
- - Pagination with parameters
-
-4. **Monitor Performance**:
- - Database query logs
- - Network waterfall in browser DevTools
- - Verify query time improvements
-
-## Testing
-
-All code passes TypeScript compilation checks ✓
-
-Run tests:
-```bash
-npm test -- pools.test.ts
-```
-
-## Code Quality
-
-- ✓ Full TypeScript types
-- ✓ Comprehensive JSDoc comments
-- ✓ Index recommendations documented
-- ✓ Error handling with descriptive messages
-- ✓ Performance notes in comments
-- ✓ BEFORE/AFTER comparisons in code
-
-## Key Learnings
-
-1. **Explicit Column Selection**: Using specific columns instead of `SELECT *` reduces data transfer by 40-60%
-
-2. **Parallel Queries**: Even with 2 queries, running them in parallel saves ~50% latency
-
-3. **Pre-sorted Database Results**: Sorting at DB level and pre-sorting results eliminates client-side overhead
-
-4. **Index Strategy**: Composite indexes on (status, available_liquidity) enable index-only scans for common queries
-
-5. **Type Safety**: Centralized query functions make type handling consistent and testable
-
-## Example Usage
-
-### Fetch Active Pools (Auto-Match)
-```typescript
-import { fetchActivePoolsWithLiquidity } from '@/lib/db/pools';
-
-const activePools = await fetchActivePoolsWithLiquidity(supabase, 0);
-// Pre-sorted by available_liquidity DESC, single query
-```
-
-### Fetch Paginated Pools
-```typescript
-import { fetchPools } from '@/lib/db/pools';
-
-const result = await fetchPools(supabase, {
- status: 'active',
- limit: 20,
- offset: 0,
- orderBy: 'created_at',
- orderDirection: 'desc'
-});
-
-// Returns: { pools, totalCount, estimatedTotalCount, hasMore }
-```
-
-### Admin Dashboard
-```typescript
-import { fetchAdminDashboardPools } from '@/lib/db/pools';
-
-const { pools, pendingLoans } = await fetchAdminDashboardPools(supabase);
-// Parallel queries for both data
-```
-
-## Files Changed Summary
-
-```
-Created:
-+ lib/db/pools.ts (optimized query module)
-+ sql/04_pool_performance_rpc.sql (RPC migration)
-+ lib/db/pools.test.ts (comprehensive tests)
-+ POOL_OPTIMIZATION.md (detailed documentation)
-+ OPTIMIZATION_SUMMARY.md (this file)
-
-Modified:
-~ app/api/pools/route.ts (use fetchPools)
-~ app/dashboard/admin/pools/page.tsx (use fetchAdminDashboardPools)
-~ app/actions/admin-pools.ts (use optimized functions)
-```
-
-## References
-
-- Issue: #39 "Optimize Supabase database query performance for large pool lists"
-- Related: N+1 query problem, Supabase best practices, database indexing
-- Documentation: See POOL_OPTIMIZATION.md for detailed guide
-
----
-
-**Status**: ✅ Complete and ready for testing
-
-**Quality**: All files pass TypeScript checks, comprehensive tests included, documentation complete
diff --git a/POOL_OPTIMIZATION.md b/POOL_OPTIMIZATION.md
deleted file mode 100644
index 53c75de..0000000
--- a/POOL_OPTIMIZATION.md
+++ /dev/null
@@ -1,374 +0,0 @@
-# Pool Query Performance Optimization - Issue #39
-
-## Overview
-
-This document describes the optimization work done to improve Supabase database query performance for large pool lists in TrustLend.
-
-**Issue**: Waterfall queries when fetching pools data causing N+1 query problems and excessive network round-trips.
-
-**Solution**: Consolidated queries, optimized with indexes, and created reusable query functions.
-
----
-
-## Changes Made
-
-### 1. Created Optimized Query Module: `lib/db/pools.ts`
-
-**Purpose**: Central module for all pool database operations with consistent patterns and performance optimizations.
-
-**Key Functions**:
-
-#### `fetchPools(supabase, options)`
-- Fetches paginated pools with optional filtering
-- **Before**: Multiple sequential queries for filters, sorting, pagination
-- **After**: Single query with all filters applied
-- **Features**:
- - Explicit column selection (no `SELECT *`)
- - Status filtering support
- - Pagination with limit/offset
- - Customizable ordering (created_at or available_liquidity)
- - Row count metadata for UI pagination
-
-```typescript
-const result = await fetchPools(supabase, {
- status: 'active',
- limit: 20,
- offset: 0,
- orderBy: 'created_at',
- orderDirection: 'desc'
-});
-// Returns: { pools, totalCount, estimatedTotalCount, hasMore }
-```
-
-#### `fetchPoolById(supabase, poolId)`
-- Fetch single pool by ID
-- **Performance**: Direct lookup with explicit columns
-- **Use cases**: Pool detail views, pre-approval checks
-
-```typescript
-const pool = await fetchPoolById(supabase, 'pool-uuid');
-// Returns: Pool | null
-```
-
-#### `fetchActivePoolsWithLiquidity(supabase, minimumLiquidity)`
-- Optimized query for auto-matching operations
-- **Before**: Fetch all active pools, filter in application
-- **After**: DB-level filtering with optimized index
-- **Index used**: `idx_lending_pools_status` (or composite `idx_lending_pools_status_available`)
-
-```typescript
-const pools = await fetchActivePoolsWithLiquidity(supabase, 0);
-// Returns: Pool[] pre-sorted by available_liquidity DESC
-```
-
-#### `fetchAdminDashboardPools(supabase)`
-- Fetch pools + pending loans for admin dashboard
-- **Optimization**: Parallel queries instead of sequential
-- **Before**: 2 waterfall queries
-- **After**: 2 parallel queries (faster)
-
-```typescript
-const { pools, pendingLoans } = await fetchAdminDashboardPools(supabase);
-```
-
-### 2. Created RPC Migration: `sql/04_pool_performance_rpc.sql`
-
-**Purpose**: Add database-level functions for complex queries with atomic operations.
-
-**Functions**:
-
-#### `get_lending_pools_paginated(...)`
-SQL RPC function for fetching paginated pools with filters. Can be called directly if RPC endpoint is exposed.
-
-```sql
-SELECT * FROM public.get_lending_pools_paginated(
- status_filter := 'active',
- page_limit := 20,
- page_offset := 0,
- order_by_col := 'created_at',
- order_asc := false
-);
-```
-
-**Parameters**:
-- `status_filter`: Optional pool status filter
-- `page_limit`: Number of results per page
-- `page_offset`: Pagination offset
-- `order_by_col`: Sort column ('created_at', 'available_liquidity', 'total_liquidity')
-- `order_asc`: Sort direction
-
-#### `get_active_pools_with_liquidity(min_liquidity)`
-SQL RPC for fetching active pools with minimum liquidity threshold.
-
-### 3. Recommended Indexes
-
-**Critical Indexes for Optimal Performance**:
-
-```sql
--- 1. Composite index for active pools filtering
-CREATE INDEX IF NOT EXISTS idx_lending_pools_status_available
-ON public.lending_pools (status, available_liquidity DESC);
-
--- 2. Index for default sort order (created_at)
-CREATE INDEX IF NOT EXISTS idx_lending_pools_created_at_desc
-ON public.lending_pools (created_at DESC);
-
--- 3. Index for alternative sort (available liquidity)
-CREATE INDEX IF NOT EXISTS idx_lending_pools_available_liquidity_desc
-ON public.lending_pools (available_liquidity DESC);
-```
-
-**Current Status**:
-- ✓ `idx_lending_pools_status` exists in schema
-- ⚠️ Additional indexes recommended for production (see below)
-
-### 4. Updated Routes & Actions
-
-#### `app/api/pools/route.ts`
-- Now uses `fetchPools()` function
-- Supports pagination parameters: `limit`, `offset`
-- Supports filtering: `status`
-- Supports custom ordering: `orderBy`, `orderDirection`
-- Response includes pagination metadata
-
-**Before**:
-```json
-{
- "success": true,
- "pools": [...]
-}
-```
-
-**After**:
-```json
-{
- "success": true,
- "pools": [...],
- "pagination": {
- "limit": 10,
- "offset": 0,
- "hasMore": true,
- "estimatedTotal": 145
- }
-}
-```
-
-#### `app/dashboard/admin/pools/page.tsx`
-- Replaced waterfall queries with `fetchAdminDashboardPools()`
-- Queries now execute in parallel
-- Cleaner error handling
-
-#### `app/actions/admin-pools.ts`
-- `approveLoan()`: Uses `fetchPoolById()` instead of direct query
-- `runAutoMatch()`: Uses `fetchActivePoolsWithLiquidity()` for optimal pool selection
-
-**Before** (runAutoMatch):
-1. Fetch pending loans
-2. Fetch active pools
-3. Loop through loans
-4. For each loan, update loan and pool (N queries)
-
-**After** (runAutoMatch):
-1. Fetch pending loans (1 query)
-2. Fetch active pools (1 query, pre-sorted by liquidity)
-3. Loop through loans with local state tracking
-4. Batch updates in parallel
-
-### 5. Added Comprehensive Tests: `lib/db/pools.test.ts`
-
-**Test Coverage**:
-- ✓ Only 1 query executed per function (not N)
-- ✓ Correct data shape returned
-- ✓ Pagination works correctly
-- ✓ Filtering by status works
-- ✓ Error handling works
-- ✓ Relation cardinality handling (profiles array/object)
-
-**Run Tests**:
-```bash
-npm test -- pools.test.ts
-```
-
----
-
-## Performance Improvements
-
-### Query Count Reduction
-
-| Operation | Before | After | Improvement |
-|-----------|--------|-------|-------------|
-| fetchPools | 2-3 queries | 1 query | 60-66% fewer queries |
-| fetchPoolById | 1 query | 1 query | No change (already optimal) |
-| fetchActivePoolsWithLiquidity | 1 query (no filter) | 1 query (with filter) | -50% filtering overhead |
-| fetchAdminDashboardPools | 2 sequential | 2 parallel | ~50% faster (parallelization) |
-| runAutoMatch | 2 + N queries | 2 + N/2 queries (batched) | Variable improvement |
-
-### Network Round-Trip Reduction
-
-**For typical admin dashboard load**:
-- **Before**: 2 sequential queries (2 round-trips minimum)
-- **After**: 2 parallel queries (1 round-trip)
-- **Savings**: 50% reduction in round-trip time
-
-### Database Load Reduction
-
-**For large pool lists** (10k+ pools):
-- **Explicit column selection**: 40-60% less data transferred
-- **Index usage**: 50-200ms faster queries on unindexed columns
-- **Filtered queries**: Reduced full table scans
-
-### Estimated Improvements on Large Datasets
-
-| Scenario | Before | After | Improvement |
-|----------|--------|-------|-------------|
-| Fetch 10k pools, 10 at a time | ~500ms | ~50ms | **90% faster** |
-| Admin dashboard (pools + loans) | ~400ms sequential | ~250ms parallel | **37% faster** |
-| Auto-match 100 loans to 20 pools | ~1500ms | ~600ms | **60% faster** |
-
----
-
-## Implementation Checklist
-
-### Completed ✓
-- [x] Created `lib/db/pools.ts` with optimized functions
-- [x] Created `sql/04_pool_performance_rpc.sql` migration
-- [x] Updated `app/api/pools/route.ts` to use new functions
-- [x] Updated `app/dashboard/admin/pools/page.tsx`
-- [x] Updated `app/actions/admin-pools.ts`
-- [x] Added comprehensive tests in `lib/db/pools.test.ts`
-- [x] Added performance documentation
-
-### Next Steps (Manual Admin Action Required)
-
-1. **Apply Recommended Indexes in Supabase**:
- ```sql
- -- Run in Supabase SQL Editor
- CREATE INDEX IF NOT EXISTS idx_lending_pools_status_available
- ON public.lending_pools (status, available_liquidity DESC);
-
- CREATE INDEX IF NOT EXISTS idx_lending_pools_created_at_desc
- ON public.lending_pools (created_at DESC);
-
- CREATE INDEX IF NOT EXISTS idx_lending_pools_available_liquidity_desc
- ON public.lending_pools (available_liquidity DESC);
- ```
-
-2. **Apply RPC Migration**:
- - Run `sql/04_pool_performance_rpc.sql` in Supabase SQL Editor
- - Verifies functions are created: `get_lending_pools_paginated`, `get_active_pools_with_liquidity`
-
-3. **Test in Staging**:
- - Verify admin dashboard loads pools correctly
- - Test auto-matching with pending loans
- - Test pagination with `limit` and `offset` parameters
-
-4. **Monitor Performance**:
- - Check database query logs for query optimization
- - Monitor network waterfall in browser DevTools
- - Verify estimated query time improvements
-
----
-
-## Migration Guide for Other Queries
-
-To apply this pattern to other database operations:
-
-1. **Analyze the current query pattern**:
- - Identify waterfall queries (sequential awaits)
- - Count round-trips to database
- - Check for `SELECT *` usage
-
-2. **Create optimized function**:
- ```typescript
- export async function fetchMyData(
- supabase: SupabaseClient,
- filters: { ... }
- ) {
- let query = supabase
- .from("my_table")
- .select("col1, col2, col3") // Explicit columns, no SELECT *
- .order("col1", { ascending: false });
-
- if (filters.status) {
- query = query.eq("status", filters.status);
- }
-
- const { data, error } = await query;
- // Handle error, transform data
- }
- ```
-
-3. **Add tests**:
- - Verify only 1 query is made
- - Verify correct data shape
- - Verify error handling
-
-4. **Update calling code**:
- - Replace waterfall queries
- - Use new function consistently
-
----
-
-## Backward Compatibility
-
-All changes are backward compatible:
-- Existing API routes still work with old response format
-- New pagination fields are optional and don't break existing code
-- Component interfaces unchanged (props accept same Pool type)
-
----
-
-## Debugging Guide
-
-### Query Not Optimizing as Expected
-
-**Check**:
-1. Are you awaiting every query? (Sequential = not parallel)
-2. Is `SELECT *` used? (Change to explicit columns)
-3. Are filters applied at database level? (Not in application)
-4. Is the correct index being used? (Check query plan in Supabase)
-
-**Verify in Supabase**:
-```sql
-EXPLAIN ANALYZE
-SELECT id, name, status, apr_bps, total_liquidity, available_liquidity
-FROM public.lending_pools
-WHERE status = 'active'
-ORDER BY created_at DESC
-LIMIT 10;
-```
-
-### Performance Issues After Migration
-
-**Possible Causes**:
-1. **Missing indexes**: Run recommended index creation queries
-2. **Network latency**: Check browser network tab for slow round-trips
-3. **Large payload**: Verify explicit column selection (no SELECT *)
-4. **Suboptimal query plan**: Check EXPLAIN output
-
-**Debugging Steps**:
-```typescript
-// Add timing logs
-const start = performance.now();
-const result = await fetchPools(supabase);
-console.log(`Query took ${performance.now() - start}ms`);
-```
-
----
-
-## References
-
-- **Supabase Documentation**: https://supabase.com/docs/reference/javascript/select
-- **PostgreSQL Query Performance**: https://www.postgresql.org/docs/current/using-explain.html
-- **Index Strategies**: https://use-the-index-luke.com/
-- **N+1 Query Problem**: https://www.sqlinjection.net/table-in-from-clause/
-
----
-
-## Questions or Issues?
-
-For questions about this optimization:
-1. Check `POOL_OPTIMIZATION.md` (this file)
-2. Review comments in `lib/db/pools.ts` and migration file
-3. Run tests: `npm test -- pools.test.ts`
-4. Check browser DevTools network tab for query count
diff --git a/QUICK_START_POOL_QUERIES.md b/QUICK_START_POOL_QUERIES.md
deleted file mode 100644
index df231fc..0000000
--- a/QUICK_START_POOL_QUERIES.md
+++ /dev/null
@@ -1,310 +0,0 @@
-# Quick Start: Pool Query Optimization
-
-## TL;DR
-
-Use `lib/db/pools.ts` functions instead of direct Supabase queries for all pool operations.
-
-## Function Reference
-
-### Fetch Paginated Pools
-```typescript
-import { fetchPools } from '@/lib/db/pools';
-
-// With filters and pagination
-const result = await fetchPools(supabase, {
- status: 'active',
- limit: 20,
- offset: 0,
- orderBy: 'created_at',
- orderDirection: 'desc'
-});
-
-console.log(result.pools); // Pool[]
-console.log(result.hasMore); // boolean
-console.log(result.estimatedTotalCount); // number
-```
-
-### Fetch Single Pool
-```typescript
-import { fetchPoolById } from '@/lib/db/pools';
-
-const pool = await fetchPoolById(supabase, 'pool-uuid');
-if (pool) {
- console.log(pool.name);
-}
-```
-
-### Fetch Active Pools (Auto-Matching)
-```typescript
-import { fetchActivePoolsWithLiquidity } from '@/lib/db/pools';
-
-// Get pools sorted by available liquidity
-const pools = await fetchActivePoolsWithLiquidity(supabase, 0);
-// Optional: minimum liquidity threshold
-const richPools = await fetchActivePoolsWithLiquidity(supabase, 50000);
-```
-
-### Fetch Admin Dashboard Data
-```typescript
-import { fetchAdminDashboardPools } from '@/lib/db/pools';
-
-const { pools, pendingLoans } = await fetchAdminDashboardPools(supabase);
-```
-
-## API Endpoint
-
-GET `/api/pools`
-
-**Query Parameters**:
-- `status`: 'active' | 'paused' | 'closed' (optional)
-- `limit`: 1-100 (default: 10)
-- `offset`: number (default: 0)
-- `orderBy`: 'created_at' | 'available_liquidity' (default: 'created_at')
-- `orderDirection`: 'asc' | 'desc' (default: 'desc')
-
-**Response**:
-```json
-{
- "success": true,
- "pools": [
- {
- "id": "uuid",
- "name": "Pool Name",
- "description": "...",
- "status": "active",
- "apr_bps": 1500,
- "total_liquidity": 100000,
- "available_liquidity": 50000,
- "total_borrowed": 50000,
- "created_at": "2024-01-01T00:00:00Z",
- "updated_at": "2024-01-01T00:00:00Z"
- }
- ],
- "pagination": {
- "limit": 10,
- "offset": 0,
- "hasMore": true,
- "estimatedTotal": 45
- }
-}
-```
-
-## Do's and Don'ts
-
-### ✅ DO
-```typescript
-// Use the optimized functions
-const pools = await fetchPools(supabase);
-
-// Explicit columns in custom queries
-.select("id, name, status, apr_bps")
-
-// Parallel queries
-await Promise.all([
- fetchPools(supabase),
- fetchAdminDashboardPools(supabase)
-])
-```
-
-### ❌ DON'T
-```typescript
-// Don't use SELECT *
-.select("*")
-
-// Don't make sequential/waterfall queries
-const pools = await fetchPools(supabase);
-const loans = await fetchLoans(supabase); // Wait, should be parallel
-
-// Don't bypass the module
-supabase.from("lending_pools").select("*")...
-```
-
-## Common Patterns
-
-### List All Active Pools
-```typescript
-const { pools } = await fetchPools(supabase, {
- status: 'active',
- limit: 100
-});
-```
-
-### Pagination
-```typescript
-const page1 = await fetchPools(supabase, { limit: 20, offset: 0 });
-const page2 = await fetchPools(supabase, { limit: 20, offset: 20 });
-```
-
-### Sort by Liquidity
-```typescript
-const pools = await fetchPools(supabase, {
- orderBy: 'available_liquidity',
- orderDirection: 'desc'
-});
-```
-
-### Auto-Matching
-```typescript
-const activePools = await fetchActivePoolsWithLiquidity(supabase, 0);
-// Pre-sorted by available_liquidity DESC
-```
-
-## Performance Checklist
-
-- [ ] Using `lib/db/pools.ts` functions?
-- [ ] No `SELECT *` in queries?
-- [ ] Parallel queries where applicable?
-- [ ] Pagination applied for large lists?
-- [ ] Explicit column selection?
-
-## Debugging
-
-### Check Query Count
-```typescript
-// Browser DevTools → Network tab
-// Should see 1-2 requests for pool operations
-// NOT multiple sequential requests
-```
-
-### Verify Query Performance
-```typescript
-const start = performance.now();
-const result = await fetchPools(supabase);
-console.log(`Query took ${performance.now() - start}ms`);
-// Should be <100ms for typical queries
-```
-
-### Test Pagination
-```typescript
-const result = await fetchPools(supabase, { limit: 10, offset: 0 });
-console.log(result.hasMore); // true if more results available
-console.log(result.pools.length); // 10 (or less if last page)
-```
-
-## Type Reference
-
-```typescript
-interface Pool {
- id: string;
- name: string;
- description: string | null;
- status: 'active' | 'paused' | 'closed';
- apr_bps: number;
- total_liquidity: number;
- available_liquidity: number;
- total_borrowed: number;
- created_at: string;
- updated_at: string;
-}
-
-interface PoolFetchOptions {
- status?: 'active' | 'paused' | 'closed';
- limit?: number;
- offset?: number;
- orderBy?: 'created_at' | 'available_liquidity';
- orderDirection?: 'asc' | 'desc';
-}
-
-interface PoolFetchResult {
- pools: Pool[];
- totalCount: number;
- estimatedTotalCount: number;
- hasMore: boolean;
-}
-```
-
-## Need Help?
-
-1. Check `lib/db/pools.ts` for detailed JSDoc comments
-2. Read `POOL_OPTIMIZATION.md` for deep dive
-3. Run tests: `npm test -- pools.test.ts`
-4. Check browser DevTools network tab
-
-## Examples
-
-### React Component
-```typescript
-'use client';
-import { useEffect, useState } from 'react';
-import { getServiceRoleClient } from '@/lib/supabase/server';
-import { fetchPools } from '@/lib/db/pools';
-
-export function PoolList() {
- const [pools, setPools] = useState([]);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- const load = async () => {
- try {
- const supabase = getServiceRoleClient();
- if (!supabase) return;
-
- const result = await fetchPools(supabase, {
- status: 'active',
- limit: 10
- });
- setPools(result.pools);
- } catch (err) {
- console.error('Failed to load pools:', err);
- } finally {
- setLoading(false);
- }
- };
-
- load();
- }, []);
-
- if (loading) return
Loading...
;
- return (
-
- {pools.map(pool => (
-
{pool.name}
- ))}
-
- );
-}
-```
-
-### API Route
-```typescript
-import { fetchPools } from '@/lib/db/pools';
-import { getServiceRoleClient } from '@/lib/supabase/server';
-
-export async function GET(request) {
- const supabase = getServiceRoleClient();
- if (!supabase) {
- return Response.json({ error: 'Service unavailable' }, { status: 500 });
- }
-
- const { searchParams } = new URL(request.url);
- const result = await fetchPools(supabase, {
- status: searchParams.get('status'),
- limit: parseInt(searchParams.get('limit') || '10'),
- offset: parseInt(searchParams.get('offset') || '0')
- });
-
- return Response.json(result);
-}
-```
-
-### Server Action
-```typescript
-'use server';
-import { fetchPoolById } from '@/lib/db/pools';
-import { getServerSupabaseClient } from '@/lib/supabase/server';
-
-export async function approvePoolFunding(poolId: string) {
- const supabase = await getServerSupabaseClient();
- if (!supabase) throw new Error('Service unavailable');
-
- const pool = await fetchPoolById(supabase, poolId);
- if (!pool) throw new Error('Pool not found');
- if (pool.available_liquidity <= 0) throw new Error('Insufficient liquidity');
-
- // Approve funding...
-}
-```
-
----
-
-For questions, see `POOL_OPTIMIZATION.md` or run tests.
diff --git a/README.md b/README.md
index d3c291c..28a9bcd 100644
--- a/README.md
+++ b/README.md
@@ -21,8 +21,8 @@
Live Production |
Video Demo |
- Roadmap |
- Getting Started Guide |
+ Roadmap |
+ Getting Started Guide |
Contributing Guidelines
@@ -55,23 +55,7 @@ TrustLend is designed as a foundational layer for decentralized, inclusive credi
4. **Institutional Underwriting:** Enabling institutional liquidity providers to plug proprietary risk models into TrustLend's smart contracts to automatically fund specific borrower profiles.
5. **Global Fiat On/Off Ramps:** Deepening integration with Stellar anchors to allow seamless fiat borrowing and repayment in local currencies worldwide.
-*We welcome open-source contributors to help us build this vision! Check out our [Roadmap](ROADMAP.md) for upcoming milestones.*
-
----
-
-## 📸 Platform Sneak Peek
-
-### Borrower & Lender Dashboards
-
-
-
-
-
-### Admin Controls & Verification
-
-
-
-
+*We welcome open-source contributors to help us build this vision! Check out our [Roadmap](docs/roadmap.md) for upcoming milestones.*
---
@@ -84,7 +68,6 @@ flowchart TB
%% ── Style definitions ────────────────────────────────────────────────────
classDef client fill:#3b82f6,color:#fff,stroke:#2563eb,stroke-width:2px
classDef backend fill:#8b5cf6,color:#fff,stroke:#7c3aed,stroke-width:2px
- classDef indexer fill:#06b6d4,color:#fff,stroke:#0891b2,stroke-width:2px
classDef automation fill:#f59e0b,color:#1e293b,stroke:#d97706,stroke-width:2px
classDef chain fill:#10b981,color:#fff,stroke:#059669,stroke-width:2px
classDef external fill:#64748b,color:#fff,stroke:#475569,stroke-width:2px
@@ -108,15 +91,6 @@ flowchart TB
EM[("📧 Email Service Resend · Payment notices")]
end
- %% ── Indexer Layer (Cyan) ────────────────────────────────────────────────
- subgraph Indexer["🔍 Indexer Layer (SubQuery)"]
- direction TB
- SQ[("📥 SubQuery Soroban Indexer project.yaml")]
- GR[("🗃️ GraphQL API schema.graphql")]
- RS[("📡 REST API Read-model fallback")]
- HM[("📊 Horizon Sync Health indexer_health table")]
- end
-
%% ── Automation Layer (Amber) ─────────────────────────────────────────────
subgraph Automation["⏰ Automation Layer (Cron / Vercel)"]
direction TB
@@ -169,16 +143,6 @@ flowchart TB
SA --> EM
SA --> WH
- %% ── Indexer ──────────────────────────────────────────────────────────────
- SQ -->|"captures events from"| SR
- SQ --> GR
- SQ --> RS
- GR -->|"GraphQL read-model"| SA
- RS -->|"REST read-model"| SA
- HZ -->|"ledger stream"| HM
- HM -->|"persists sync state"| SB
- HZ -.->|"also feeds direct reads"| SB
-
%% ── Automation ───────────────────────────────────────────────────────────
PD -->|"queries due loans"| SB
PD --> WH
@@ -271,9 +235,8 @@ flowchart LR
| **Frontend** | Next.js 16, React 19, TypeScript, Tailwind CSS 4, Framer Motion |
| **Backend & DB** | Supabase (Auth, Postgres RLS, Storage) |
| **Blockchain** | Stellar Testnet, Soroban RPC, Horizon API |
-| **Wallet** | Freighter Wallet, xBull, Albedo, WalletConnect v2 for mobile wallets (`@stellar/freighter-api`, `@creit.tech/stellar-wallets-kit`) |
+| **Wallet** | Freighter Wallet, xBull, Albedo, WalletConnect v2 for mobile wallets (`@creit.tech/stellar-wallets-kit`) |
| **Smart Contracts** | Rust (Soroban, `wasm32v1-none`) — 8 contracts deployed |
-| **Indexer** | SubQuery (`@subql/node-stellar`, `@subql/query`) — GraphQL + REST |
| **Cache** | Upstash Redis |
| **Automation** | Vercel Cron Jobs |
| **Email** | Resend |
@@ -283,7 +246,7 @@ flowchart LR
## ⚙️ Getting Started (Local Development)
-> 📖 **New contributors should start with the [Getting Started Guide](GETTING_STARTED.md)** for a thorough walkthrough covering Soroban CLI setup, contract compilation, database setup, and the full test suite.
+> 📖 **New contributors should start with the [Getting Started Guide](docs/getting-started.md)** for a thorough walkthrough covering Soroban CLI setup, contract compilation, database setup, and the full test suite.
### Quick Start
@@ -304,7 +267,7 @@ docker-compose up
```
### Need more detail?
-See the [complete setup guide →](GETTING_STARTED.md)
+See the [complete setup guide →](docs/getting-started.md)
---
@@ -561,7 +524,7 @@ If you discover a security vulnerability within TrustLend, please refer to our [
The PostgreSQL database is dumped, encrypted with AES-256 and uploaded to Amazon S3
every night at 00:00 UTC by the [Automated DB Backup](.github/workflows/db-backup.yml)
workflow. Restore steps, bucket/IAM setup and the quarterly restore drill are
-documented in [DISASTER_RECOVERY.md](DISASTER_RECOVERY.md).
+documented in [docs/disaster-recovery.md](docs/disaster-recovery.md).
---
diff --git a/SOROBAN_INDEXER_MIGRATION.md b/SOROBAN_INDEXER_MIGRATION.md
deleted file mode 100644
index c9a7039..0000000
--- a/SOROBAN_INDEXER_MIGRATION.md
+++ /dev/null
@@ -1,119 +0,0 @@
-# Soroban Event Indexer Migration
-
-TrustLend can read dashboard loan, reputation, and escrow state from a Soroban
-event indexer while keeping Supabase as the migration fallback.
-
-## Contracts To Index
-
-Configure Mercury, Ensorcel, or a custom Soroban indexer to subscribe to these
-contract IDs:
-
-- `NEXT_PUBLIC_LENDING_CONTRACT_ID`
-- `NEXT_PUBLIC_REPUTATION_CONTRACT_ID`
-- `NEXT_PUBLIC_ESCROW_CONTRACT_ID`
-
-## Event Topics
-
-The contracts emit the following event topics for read-model indexing:
-
-### Lending
-
-- `(loan, request)` with `(loan_id, borrower, amount, duration_days, interest_rate_bps, total_due, due_at)`
-- `(loan, approved)` with `(loan_id, lender, escrow_id)`
-- `(loan, revoked)` with `loan_id`
-- `(loan, active)` with `loan_id`
-- `(loan, payment)` with `(loan_id, amount, remaining_due, status)`
-- `(loan, default)` with `loan_id`
-
-### Reputation
-
-- `(oracle, set)` with `oracle`
-- `(oracle, score)` with `(borrower, credit_score, boost_bps)`
-- `(rep, event)` with `(borrower, event, delta, new_score)`
-- `(rep, totals)` with `(borrower, borrowed_delta, repaid_delta)`
-- `(rep, freeze)` with `(borrower, is_frozen)`
-
-### Escrow
-
-- `(escrow, deposit)` with `(lender, loan_id, amount)`
-- `(escrow, withdraw)` with `(lender, loan_id, amount)`
-- `(escrow, transfer)` with `(escrow_id, loan_id, borrower, amount)`
-
-Amounts emitted by Soroban contracts are in stroops. The dashboard converts
-indexed loan amounts back to XLM for table display.
-
-## Expected Read Models
-
-The app's server-side adapter is in `lib/indexer/read-model.ts`. It supports
-GraphQL or REST and normalizes common snake_case and camelCase field names.
-
-Recommended collections:
-
-- `loans`
-- `reputationEvents`
-- `escrowEvents`
-
-Recommended loan fields:
-
-- `id` or `loanId`
-- `borrowerId` and/or `borrowerAddress`
-- `lenderId` and/or `lenderAddress`
-- `status`
-- `principalAmount` or `amount`
-- `repaidAmount`
-- `aprBps` or `interestRateBps`
-- `durationDays`
-- `dueAt`
-- `createdAt`
-- `escrowId`
-
-Recommended reputation event fields:
-
-- `borrowerId` and/or `borrowerAddress`
-- `eventType`
-- `pointsDelta`
-- `scoreAfter`
-- `createdAt`
-
-Recommended escrow event fields:
-
-- `loanId`
-- `lenderAddress`
-- `borrowerAddress`
-- `amount`
-- `eventType`
-- `txHash`
-- `createdAt`
-
-## Runtime Configuration
-
-Use `.env` values:
-
-```env
-TRUSTLEND_INDEXER_READ_MODE=fallback
-TRUSTLEND_INDEXER_GRAPHQL_URL=https://your-indexer/graphql
-TRUSTLEND_INDEXER_REST_URL=
-TRUSTLEND_INDEXER_API_KEY=
-```
-
-`fallback` mode tries the indexer first and falls back to Supabase if the
-indexer is unavailable or still backfilling. Use `required` only after the
-subgraph is fully caught up. Use `disabled` to force the old Supabase reads.
-
-If your GraphQL schema differs from the default collection names or filter
-syntax, set any of these variables with a full GraphQL document:
-
-- `TRUSTLEND_INDEXER_BORROWER_LOANS_QUERY`
-- `TRUSTLEND_INDEXER_LENDER_LOANS_QUERY`
-- `TRUSTLEND_INDEXER_ADMIN_LOANS_QUERY`
-- `TRUSTLEND_INDEXER_REPUTATION_EVENTS_QUERY`
-- `TRUSTLEND_INDEXER_ESCROW_EVENTS_QUERY`
-
-REST deployments should expose:
-
-- `GET /loans`
-- `GET /reputation-events`
-- `GET /escrow-events`
-
-The adapter passes filters such as `borrowerId`, `borrowerAddress`,
-`lenderId`, `lenderAddress`, and `limit` as query parameters.
diff --git a/__tests__/lib/indexer/read-model.test.ts b/__tests__/lib/indexer/read-model.test.ts
deleted file mode 100644
index 45fd418..0000000
--- a/__tests__/lib/indexer/read-model.test.ts
+++ /dev/null
@@ -1,252 +0,0 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import type { PaginationOptions } from "@/lib/indexer/read-model";
-
-// We'll test the helper functions directly and the reader functions with mocked fetch
-const ORIGINAL_ENV = process.env;
-
-describe("PaginationOptions interface", () => {
- it("accepts limit only", () => {
- const opts: PaginationOptions = { limit: 10 };
- expect(opts.limit).toBe(10);
- expect(opts.offset).toBeUndefined();
- expect(opts.after).toBeUndefined();
- });
-
- it("accepts limit with offset", () => {
- const opts: PaginationOptions = { limit: 20, offset: 40 };
- expect(opts.limit).toBe(20);
- expect(opts.offset).toBe(40);
- });
-
- it("accepts limit with cursor", () => {
- const opts: PaginationOptions = { limit: 15, after: "YXJyYXljb25uZWN0aW9uOjI=" };
- expect(opts.limit).toBe(15);
- expect(opts.after).toBe("YXJyYXljb25uZWN0aW9uOjI=");
- });
-
- it("accepts all three params", () => {
- const opts: PaginationOptions = { limit: 5, offset: 10, after: "cursor123" };
- expect(opts.limit).toBe(5);
- expect(opts.offset).toBe(10);
- expect(opts.after).toBe("cursor123");
- });
-
- it("allows null after (explicit no-cursor)", () => {
- const opts: PaginationOptions = { limit: 25, after: null };
- expect(opts.after).toBeNull();
- });
-
- it("allows zero offset", () => {
- const opts: PaginationOptions = { limit: 50, offset: 0 };
- expect(opts.offset).toBe(0);
- });
-});
-
-describe("buildPaginationVariables", () => {
- beforeEach(() => {
- vi.resetModules();
- });
-
- it("returns defaults when no options provided", async () => {
- const { buildPaginationVariables } = await import("@/lib/indexer/read-model");
- const vars = buildPaginationVariables();
- expect(vars).toEqual({ limit: 50, offset: 0, after: null });
- });
-
- it("returns defaults for empty options", async () => {
- const { buildPaginationVariables } = await import("@/lib/indexer/read-model");
- const vars = buildPaginationVariables({});
- expect(vars).toEqual({ limit: 50, offset: 0, after: null });
- });
-
- it("merges provided values with defaults", async () => {
- const { buildPaginationVariables } = await import("@/lib/indexer/read-model");
- const vars = buildPaginationVariables({ limit: 10, offset: 20 });
- expect(vars).toEqual({ limit: 10, offset: 20, after: null });
- });
-
- it("includes cursor when provided", async () => {
- const { buildPaginationVariables } = await import("@/lib/indexer/read-model");
- const vars = buildPaginationVariables({ limit: 5, after: "cursor123" });
- expect(vars).toEqual({ limit: 5, offset: 0, after: "cursor123" });
- });
-});
-
-describe("buildRestPaginationParams", () => {
- beforeEach(() => {
- vi.resetModules();
- });
-
- it("returns defaults when no options provided", async () => {
- const { buildRestPaginationParams } = await import("@/lib/indexer/read-model");
- const params = buildRestPaginationParams();
- expect(params).toEqual({ limit: 50, offset: 0, after: null });
- });
-
- it("returns provided pagination values", async () => {
- const { buildRestPaginationParams } = await import("@/lib/indexer/read-model");
- const params = buildRestPaginationParams({ limit: 25, offset: 50, after: null });
- expect(params).toEqual({ limit: 25, offset: 50, after: null });
- });
-});
-
-describe("getIndexedAdminReadModel with pagination", () => {
- beforeEach(() => {
- vi.resetModules();
- });
-
- afterEach(() => {
- process.env = ORIGINAL_ENV;
- });
-
- it("passes limit as first arg to the resolver", async () => {
- // Set up env to avoid early return
- process.env = { ...ORIGINAL_ENV };
-
- // We'll test that the exported function accepts the old (number) signature
- const { getIndexedAdminReadModel, buildPaginationVariables } = await import("@/lib/indexer/read-model");
-
- // Verify the internals work by testing the helper directly
- const vars = buildPaginationVariables({ limit: 500 });
- expect(vars.limit).toBe(500);
- expect(vars.offset).toBe(0);
-
- // The function itself returns [] when no indexer URL is configured
- const result = await getIndexedAdminReadModel(500);
- expect(result).toEqual({ loans: [], reputationEvents: [], escrowEvents: [] });
- });
-
- it("accepts optional PaginationOptions as second arg", async () => {
- // This verifies the overloaded signature compiles and runs
- const { getIndexedAdminReadModel, buildPaginationVariables } = await import("@/lib/indexer/read-model");
-
- const vars = buildPaginationVariables({ limit: 100, offset: 200 });
- expect(vars).toEqual({ limit: 100, offset: 200, after: null });
-
- const result = await getIndexedAdminReadModel(undefined, { limit: 100, offset: 200 });
- expect(result).toEqual({ loans: [], reputationEvents: [], escrowEvents: [] });
- });
-
- it("backwards-compatible: calling with just limit returns empty when unconfigured", async () => {
- // Explicitly no env set
- process.env = { ...ORIGINAL_ENV };
- delete process.env.TRUSTLEND_INDEXER_GRAPHQL_URL;
- delete process.env.TRUSTLEND_INDEXER_REST_URL;
-
- const { getIndexedAdminReadModel } = await import("@/lib/indexer/read-model");
- const result = await getIndexedAdminReadModel(500);
- expect(result).toEqual({ loans: [], reputationEvents: [], escrowEvents: [] });
- });
-});
-
-describe("GraphQL queries contain pagination args", () => {
- beforeEach(() => {
- vi.resetModules();
- });
-
- it("BORROWER_LOANS_QUERY includes offset and after variables", async () => {
- const mod = await import("@/lib/indexer/read-model");
- // Access the private query string — we'll verify by checking buildPaginationVariables
- const vars = mod.buildPaginationVariables({ limit: 10, offset: 5, after: "cursor" });
- expect(vars).toHaveProperty("limit", 10);
- expect(vars).toHaveProperty("offset", 5);
- expect(vars).toHaveProperty("after", "cursor");
- });
-
- it("buildRestPaginationParams includes limit, offset, and after", async () => {
- const mod = await import("@/lib/indexer/read-model");
- const params = mod.buildRestPaginationParams({ limit: 25, offset: 50, after: "next-cursor" });
- expect(params.limit).toBe(25);
- expect(params.offset).toBe(50);
- expect(params.after).toBe("next-cursor");
- });
-});
-
-describe("reader functions accept pagination options (integration)", () => {
- beforeEach(() => {
- vi.resetModules();
- });
-
- afterEach(() => {
- process.env = ORIGINAL_ENV;
- });
-
- it("skips indexer call when mode is disabled (borrower)", async () => {
- process.env = { ...ORIGINAL_ENV, TRUSTLEND_INDEXER_READ_MODE: "disabled" };
-
- const { getIndexedBorrowerReadModel } = await import("@/lib/indexer/read-model");
- const result = await getIndexedBorrowerReadModel({
- userId: "u1",
- walletAddress: "GABC",
- limit: 10,
- offset: 20,
- });
-
- expect(result).toEqual({ loans: [], reputationEvents: [], escrowEvents: [] });
- });
-
- it("returns empty when no indexer URL is configured (borrower)", async () => {
- process.env = { ...ORIGINAL_ENV };
- delete process.env.TRUSTLEND_INDEXER_GRAPHQL_URL;
- delete process.env.TRUSTLEND_INDEXER_REST_URL;
-
- const { getIndexedBorrowerReadModel } = await import("@/lib/indexer/read-model");
- const result = await getIndexedBorrowerReadModel({
- userId: "u1",
- walletAddress: "GABC",
- limit: 25,
- offset: 0,
- });
-
- expect(result).toEqual({ loans: [], reputationEvents: [], escrowEvents: [] });
- });
-
- it("returns empty when no indexer URL is configured (lender)", async () => {
- process.env = { ...ORIGINAL_ENV };
- delete process.env.TRUSTLEND_INDEXER_GRAPHQL_URL;
- delete process.env.TRUSTLEND_INDEXER_REST_URL;
-
- const { getIndexedLenderReadModel } = await import("@/lib/indexer/read-model");
- const result = await getIndexedLenderReadModel({
- userId: "u1",
- walletAddress: "GABC",
- limit: 30,
- offset: 15,
- });
-
- expect(result).toEqual({ loans: [], reputationEvents: [], escrowEvents: [] });
- });
-
- it("returns empty when no indexer URL is configured (admin)", async () => {
- process.env = { ...ORIGINAL_ENV };
- delete process.env.TRUSTLEND_INDEXER_GRAPHQL_URL;
- delete process.env.TRUSTLEND_INDEXER_REST_URL;
-
- const { getIndexedAdminReadModel } = await import("@/lib/indexer/read-model");
- const result = await getIndexedAdminReadModel(500);
-
- expect(result).toEqual({ loans: [], reputationEvents: [], escrowEvents: [] });
- });
-
- it("passes cursor-based pagination when after is provided", async () => {
- process.env = { ...ORIGINAL_ENV };
- delete process.env.TRUSTLEND_INDEXER_GRAPHQL_URL;
- delete process.env.TRUSTLEND_INDEXER_REST_URL;
-
- const { buildPaginationVariables } = await import("@/lib/indexer/read-model");
- const vars = buildPaginationVariables({ limit: 20, after: "cursor-value" });
-
- expect(vars).toEqual({ limit: 20, offset: 0, after: "cursor-value" });
- });
-
- it("accepts PaginationOptions as second arg to getIndexedAdminReadModel", async () => {
- process.env = { ...ORIGINAL_ENV };
- delete process.env.TRUSTLEND_INDEXER_GRAPHQL_URL;
- delete process.env.TRUSTLEND_INDEXER_REST_URL;
-
- const { getIndexedAdminReadModel } = await import("@/lib/indexer/read-model");
- const result = await getIndexedAdminReadModel(undefined, { limit: 100, offset: 200 });
-
- expect(result).toEqual({ loans: [], reputationEvents: [], escrowEvents: [] });
- });
-});
diff --git a/app/actions/admin-pools.ts b/app/actions/admin-pools.ts
index 0ac786e..9985cb7 100644
--- a/app/actions/admin-pools.ts
+++ b/app/actions/admin-pools.ts
@@ -323,10 +323,6 @@ export async function runAutoMatch(): Promise<{
};
}
}
-tanceof Error ? err.message : "Auto-match failed",
- };
- }
-}
// ── Set pool borrow cap ──────────────────────────────────────────────────────
/**
diff --git a/app/dashboard/admin/page.tsx b/app/dashboard/admin/page.tsx
index bb3ddc6..1d1aa49 100644
--- a/app/dashboard/admin/page.tsx
+++ b/app/dashboard/admin/page.tsx
@@ -7,11 +7,6 @@ import {
presentAdminMetrics,
} from "@/lib/dashboard/metrics";
import { getServiceRoleClient } from "@/lib/supabase/server";
-import {
- getIndexedAdminReadModel,
- isIndexerConfigured,
- isIndexerRequired,
-} from "@/lib/indexer/read-model";
import Link from "next/link";
import { formatCurrency } from "@/lib/utils/formatting";
@@ -70,22 +65,7 @@ export default async function AdminDashboardPage() {
const profiles = profilesRes.data ?? [];
const dbLoans = loansRes.data ?? [];
- let indexedLoans: typeof dbLoans | null = null;
- if (isIndexerConfigured()) {
- try {
- const indexed = await getIndexedAdminReadModel(10);
- indexedLoans = indexed.loans.map((loan) => ({
- id: loan.id,
- borrower_id: loan.borrowerId ?? loan.borrowerAddress ?? "",
- status: loan.status === "pending" ? "requested" : loan.status,
- principal_amount: loan.principalAmount / 10000000,
- requested_at: loan.requestedAt ?? loan.createdAt,
- })) as typeof dbLoans;
- } catch (error) {
- if (isIndexerRequired()) throw error;
- }
- }
- const loans = indexedLoans?.length ? indexedLoans : dbLoans;
+ const loans = dbLoans;
const repayments = repaymentsRes.data ?? [];
const ledgerRows = ledgerRes.data ?? [];
const fraudSignals = fraudRes.data ?? [];
diff --git a/app/dashboard/admin/pools/page.tsx b/app/dashboard/admin/pools/page.tsx
index fef7ed5..ac655ac 100644
--- a/app/dashboard/admin/pools/page.tsx
+++ b/app/dashboard/admin/pools/page.tsx
@@ -38,6 +38,7 @@ export default async function AdminPoolsPage() {
apr_bps: Number(p.apr_bps ?? 0),
total_liquidity: Number(p.total_liquidity ?? 0),
available_liquidity: Number(p.available_liquidity ?? 0),
+ borrow_cap: p.borrow_cap ?? null,
}));
const pendingLoans = rawLoans.map((l) => ({
diff --git a/app/dashboard/borrower/page.tsx b/app/dashboard/borrower/page.tsx
index cfd6e02..d21bb7c 100644
--- a/app/dashboard/borrower/page.tsx
+++ b/app/dashboard/borrower/page.tsx
@@ -15,11 +15,6 @@ import { WithdrawToFiatButton } from "@/components/dashboard/WithdrawToFiatButto
import { borrowerNavLinks } from "@/lib/dashboard/borrower-links";
import { getFundingProgress } from "@/lib/loans/funding";
import { HealthFactorGauge } from "@/components/dashboard/HealthFactorGauge";
-import {
- getIndexedBorrowerReadModel,
- isIndexerConfigured,
- isIndexerRequired,
-} from "@/lib/indexer/read-model";
import { formatCurrency } from "@/lib/utils/formatting";
// ── Inline SVG illustrations ───────────────────────────────────────────────
@@ -49,7 +44,7 @@ function EmptyLoansIllustration() {
export default async function BorrowerDashboardPage() {
const { user } = await requireAuthenticatedUser("borrower");
const walletAddress = String(user.user_metadata?.wallet_address ?? "") || null;
- const metrics = await getBorrowerDashboardMetrics(user.id, walletAddress);
+ const metrics = await getBorrowerDashboardMetrics(user.id);
const supabase = await getServerSupabaseClient();
const srClient = getServiceRoleClient();
@@ -72,29 +67,7 @@ export default async function BorrowerDashboardPage() {
const profile = profileRes.data;
const dbLoans = loansRes.data ?? [];
- let indexedLoans: typeof dbLoans | null = null;
- if (isIndexerConfigured() && walletAddress) {
- try {
- const indexed = await getIndexedBorrowerReadModel({
- userId: user.id,
- walletAddress,
- limit: 20,
- });
- indexedLoans = indexed.loans.map((loan) => ({
- id: loan.id,
- status: loan.status === "pending" ? "requested" : loan.status,
- principal_amount: loan.principalAmount / 10000000,
- repaid_amount: loan.repaidAmount / 10000000,
- apr_bps: loan.aprBps,
- duration_days: loan.durationDays,
- due_at: loan.dueAt,
- created_at: loan.createdAt,
- })) as typeof dbLoans;
- } catch (error) {
- if (isIndexerRequired()) throw error;
- }
- }
- const loans = indexedLoans?.length ? indexedLoans : dbLoans;
+ const loans = dbLoans;
// Stellar TX lookups
const loanIds = loans.map((l) => String(l.id));
diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx
index 52d93ca..2e102c6 100644
--- a/app/dashboard/layout.tsx
+++ b/app/dashboard/layout.tsx
@@ -1,4 +1,4 @@
-import { GlobalErrorBoundary } from "@/components/GlobalErrorBoundary"
+import { GlobalErrorBoundary } from "@/components/dashboard/GlobalErrorBoundary"
import { RpcHealthProvider } from "@/components/RpcHealthProvider"
import { ReferralCapture } from "@/components/dashboard/ReferralCapture"
diff --git a/app/dashboard/lender/page.tsx b/app/dashboard/lender/page.tsx
index 3c890dd..322fc16 100644
--- a/app/dashboard/lender/page.tsx
+++ b/app/dashboard/lender/page.tsx
@@ -11,18 +11,13 @@ import {
} from "@/lib/supabase/server";
import { formatTokenBalance } from "@/lib/utils/formatting";
import { lenderNavLinks } from "@/lib/dashboard/lender-links";
-import {
- getIndexedLenderReadModel,
- isIndexerConfigured,
- isIndexerRequired,
-} from "@/lib/indexer/read-model";
import Link from "next/link";
export default async function LenderHomePage() {
const { user } = await requireAuthenticatedUser("lender");
const walletAddress =
String(user.user_metadata?.wallet_address ?? "") || null;
- const metrics = await getLenderDashboardMetrics(user.id, walletAddress);
+ const metrics = await getLenderDashboardMetrics(user.id);
const supabase = await getServerSupabaseClient();
const srClient = getServiceRoleClient();
@@ -81,39 +76,9 @@ export default async function LenderHomePage() {
const openLoanCount = openLoanCountRes.count ?? 0;
const isKycVerified = profile?.kyc_status === "verified";
- let indexedLoans: Array> = [];
- let indexedP2pInvestments: typeof dbP2pInvestments = [];
- if (isIndexerConfigured() && walletAddress) {
- try {
- const indexed = await getIndexedLenderReadModel({
- userId: user.id,
- walletAddress,
- limit: 20,
- });
- const escrowTxByLoanId = new Map(
- indexed.escrowEvents.map((event) => [event.loanId, event.txHash ?? ""]),
- );
- indexedLoans = indexed.loans.map((loan) => ({
- id: loan.id,
- status: loan.status,
- repaid_amount: loan.repaidAmount / 10000000,
- principal_amount: loan.principalAmount / 10000000,
- }));
- indexedP2pInvestments = indexed.loans.map((loan) => ({
- id: `indexed-${loan.id}`,
- ref_id: loan.id,
- amount: loan.principalAmount / 10000000,
- status: "confirmed",
- metadata: JSON.stringify({ txHash: escrowTxByLoanId.get(loan.id) ?? "" }),
- created_at: loan.createdAt,
- })) as typeof dbP2pInvestments;
- } catch (error) {
- if (isIndexerRequired()) throw error;
- }
- }
- const p2pInvestments = indexedP2pInvestments.length ? indexedP2pInvestments : dbP2pInvestments;
- const allLoansArray = indexedLoans.length ? indexedLoans : allLoansRes.data ?? [];
+ const p2pInvestments = dbP2pInvestments;
+ const allLoansArray = allLoansRes.data ?? [];
const loanMap = Object.fromEntries(
allLoansArray.map((l) => [String(l.id), l]),
);
diff --git a/assets/admin/admin-loanmonitor.png b/assets/admin/admin-loanmonitor.png
deleted file mode 100644
index e26b7a1..0000000
Binary files a/assets/admin/admin-loanmonitor.png and /dev/null differ
diff --git a/assets/admin/admin-overview.png b/assets/admin/admin-overview.png
deleted file mode 100644
index 1d0c029..0000000
Binary files a/assets/admin/admin-overview.png and /dev/null differ
diff --git a/assets/admin/admin-poolmanagement.png b/assets/admin/admin-poolmanagement.png
deleted file mode 100644
index 5dea129..0000000
Binary files a/assets/admin/admin-poolmanagement.png and /dev/null differ
diff --git a/assets/admin/admin-securtyflags.png b/assets/admin/admin-securtyflags.png
deleted file mode 100644
index fd82f1b..0000000
Binary files a/assets/admin/admin-securtyflags.png and /dev/null differ
diff --git a/assets/admin/admin-treasury.png b/assets/admin/admin-treasury.png
deleted file mode 100644
index a912024..0000000
Binary files a/assets/admin/admin-treasury.png and /dev/null differ
diff --git a/assets/admin/admin-users&segments.png b/assets/admin/admin-users&segments.png
deleted file mode 100644
index 585f9c5..0000000
Binary files a/assets/admin/admin-users&segments.png and /dev/null differ
diff --git a/assets/admin/adminKYC verify.png b/assets/admin/adminKYC verify.png
deleted file mode 100644
index 632c1c2..0000000
Binary files a/assets/admin/adminKYC verify.png and /dev/null differ
diff --git a/assets/auth.png b/assets/auth.png
deleted file mode 100644
index 8ada1b7..0000000
Binary files a/assets/auth.png and /dev/null differ
diff --git a/assets/borrower/applyLoan-tab.png b/assets/borrower/applyLoan-tab.png
deleted file mode 100644
index 73899e0..0000000
Binary files a/assets/borrower/applyLoan-tab.png and /dev/null differ
diff --git a/assets/borrower/home-tab.png b/assets/borrower/home-tab.png
deleted file mode 100644
index 9136d9e..0000000
Binary files a/assets/borrower/home-tab.png and /dev/null differ
diff --git a/assets/borrower/profile-tab.png b/assets/borrower/profile-tab.png
deleted file mode 100644
index b7205de..0000000
Binary files a/assets/borrower/profile-tab.png and /dev/null differ
diff --git a/assets/borrower/repay-tab.png b/assets/borrower/repay-tab.png
deleted file mode 100644
index bae274d..0000000
Binary files a/assets/borrower/repay-tab.png and /dev/null differ
diff --git a/assets/borrower/task-tab.png b/assets/borrower/task-tab.png
deleted file mode 100644
index b742191..0000000
Binary files a/assets/borrower/task-tab.png and /dev/null differ
diff --git a/assets/lender/history-tab.png b/assets/lender/history-tab.png
deleted file mode 100644
index 1bb4533..0000000
Binary files a/assets/lender/history-tab.png and /dev/null differ
diff --git a/assets/lender/home.png b/assets/lender/home.png
deleted file mode 100644
index f0037be..0000000
Binary files a/assets/lender/home.png and /dev/null differ
diff --git a/assets/lender/loan-marketplace.png b/assets/lender/loan-marketplace.png
deleted file mode 100644
index 1b89353..0000000
Binary files a/assets/lender/loan-marketplace.png and /dev/null differ
diff --git a/assets/lender/pool-investment.png b/assets/lender/pool-investment.png
deleted file mode 100644
index 70f4771..0000000
Binary files a/assets/lender/pool-investment.png and /dev/null differ
diff --git a/assets/main-dashboard.png b/assets/main-dashboard.png
deleted file mode 100644
index 59c8f00..0000000
Binary files a/assets/main-dashboard.png and /dev/null differ
diff --git a/assets/mobile/auth-mobile.png b/assets/mobile/auth-mobile.png
deleted file mode 100644
index ecdabd5..0000000
Binary files a/assets/mobile/auth-mobile.png and /dev/null differ
diff --git a/assets/mobile/landing-mobile-view.png b/assets/mobile/landing-mobile-view.png
deleted file mode 100644
index a897b01..0000000
Binary files a/assets/mobile/landing-mobile-view.png and /dev/null differ
diff --git a/assets/test/contracts-test.png b/assets/test/contracts-test.png
deleted file mode 100644
index 1575ab0..0000000
Binary files a/assets/test/contracts-test.png and /dev/null differ
diff --git a/assets/test/e2e-test.png b/assets/test/e2e-test.png
deleted file mode 100644
index 1c36ee5..0000000
Binary files a/assets/test/e2e-test.png and /dev/null differ
diff --git a/components/ErrorFallback.tsx b/components/ErrorFallback.tsx
deleted file mode 100644
index a3215f5..0000000
--- a/components/ErrorFallback.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-'use client'
-
-import { Button } from '@/components/ui/button'
-
-interface ErrorFallbackProps {
- error?: Error
- resetError?: () => void
-}
-
-export function ErrorFallback({ error, resetError }: ErrorFallbackProps) {
- return (
-
-
-
-
-
Something went wrong
-
- An unexpected error occurred while rendering this page. Our team has been notified.
-
-
- {error && (
-
- {error.name || 'Error'}: {error.message}
-
- )}
-
- {resetError && (
-
- Try Again
-
- )}
-
-
- )
-}
diff --git a/components/GlobalErrorBoundary.tsx b/components/GlobalErrorBoundary.tsx
deleted file mode 100644
index 1afc56b..0000000
--- a/components/GlobalErrorBoundary.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-'use client'
-
-import { Component, type ErrorInfo, type ReactNode } from 'react'
-import { ErrorFallback } from './ErrorFallback'
-
-interface GlobalErrorBoundaryProps {
- children: ReactNode
- fallback?: ReactNode
-}
-
-interface GlobalErrorBoundaryState {
- hasError: boolean
- error: Error | null
-}
-
-export class GlobalErrorBoundary extends Component<
- GlobalErrorBoundaryProps,
- GlobalErrorBoundaryState
-> {
- constructor(props: GlobalErrorBoundaryProps) {
- super(props)
- this.state = { hasError: false, error: null }
- }
-
- static getDerivedStateFromError(error: Error): GlobalErrorBoundaryState {
- return { hasError: true, error }
- }
-
- componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
- console.error('[GlobalErrorBoundary] Caught error:', error, errorInfo)
- }
-
- handleReset = (): void => {
- this.setState({ hasError: false, error: null })
- }
-
- render(): ReactNode {
- if (this.state.hasError) {
- if (this.props.fallback) {
- return this.props.fallback
- }
-
- return (
-
- )
- }
-
- return this.props.children
- }
-}
diff --git a/components/auth/AuthAccessButton.tsx b/components/auth/AuthAccessButton.tsx
deleted file mode 100644
index 647383b..0000000
--- a/components/auth/AuthAccessButton.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-"use client";
-
-import { useState, useRef, useEffect } from "react";
-import { X } from "lucide-react";
-import { FocusTrap } from "@/components/ui/FocusTrap";
-import { type UserRole } from "@/lib/auth/roles";
-import { StellarSignInButton } from "@/components/auth/StellarSignInButton";
-
-interface AuthAccessButtonProps {
- className?: string;
- buttonLabel?: string;
-}
-
-export function AuthAccessButton({ className, buttonLabel = "Sign in" }: AuthAccessButtonProps) {
- const [open, setOpen] = useState(false);
- const [role, setRole] = useState("borrower");
- const modalRef = useRef(null);
- const closeButtonRef = useRef(null);
-
- const closeModal = () => {
- setOpen(false);
- };
-
- // Close on Escape key
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === "Escape" && open) {
- closeModal();
- }
- };
- document.addEventListener("keydown", handleKeyDown);
- return () => document.removeEventListener("keydown", handleKeyDown);
- }, [open]);
-
- // Return focus to trigger button when modal closes
- useEffect(() => {
- if (!open) {
- const trigger = document.querySelector(".auth-trigger-text")?.closest("button") as HTMLElement;
- trigger?.focus();
- }
- }, [open]);
-
- return (
- <>
- setOpen(true)}>
- {buttonLabel}
-
-
- {open ? (
-
-
- event.stopPropagation()}
- ref={modalRef}
- >
-
-
-
-
-
Auth setup
-
Choose role and sign in
-
- Dialog for signing in to TrustLend as a borrower or lender
-
-
-
- setRole("borrower")}
- >
- Borrower
-
- setRole("lender")}
- >
- Lender
-
-
-
-
-
-
-
-
- Your role will be securely locked to your wallet on first sign-in.
-
-
-
-
- ) : null}
- >
- );
-}
diff --git a/components/dashboard/AvailablePools.tsx b/components/dashboard/AvailablePools.tsx
deleted file mode 100644
index 32ff692..0000000
--- a/components/dashboard/AvailablePools.tsx
+++ /dev/null
@@ -1,443 +0,0 @@
-"use client";
-
-/**
- * AvailablePools – Client Component
- *
- * Fetches available lending pools and the lender's positions from the
- * Supabase browser client *after* the page shell has rendered, so the
- * user immediately sees a polished skeleton instead of a blank screen.
- *
- * isLoading state conditionally renders while the
- * data is in-flight, and then transitions to the real pool cards via
- * Framer Motion once loaded.
- */
-
-import { useEffect, useState, useCallback } from "react";
-import { motion, AnimatePresence } from "framer-motion";
-import { getBrowserSupabaseClient } from "@/lib/supabase/client";
-import { formatTokenBalance, formatCurrency } from "@/lib/utils/formatting";
-import { TermTooltip } from "@/components/ui/TermTooltip";
-import type { GlossaryTermKey } from "@/lib/glossary/terms";
-import { PoolCardSkeleton } from "./PoolCardSkeleton";
-
-// ────────────────────────────────────────────────────────────────────────────
-// Types
-// ────────────────────────────────────────────────────────────────────────────
-
-interface Pool {
- id: string;
- name: string;
- status: string;
- apr_bps: number;
- total_liquidity: number;
- available_liquidity: number;
-}
-
-interface Position {
- id: string;
- pool_id: string;
- status: string;
- principal_amount: number;
- earned_interest: number;
-}
-
-interface AvailablePoolsProps {
- /**
- * Optional server-side pre-fetched pools.
- * If supplied the component skips the client-side fetch and goes
- * straight to the rendered state (no skeleton flicker).
- */
- initialPools?: Pool[];
- /** Pre-fetched positions passed from the server component. */
- initialPositions?: Position[];
-}
-
-// ────────────────────────────────────────────────────────────────────────────
-// Status badge
-// ────────────────────────────────────────────────────────────────────────────
-
-function StatusBadge({ status }: { status: string }) {
- const isActive = status.toLowerCase() === "active";
- return (
-
- {status}
-
- );
-}
-
-// ────────────────────────────────────────────────────────────────────────────
-// Individual pool card
-// ────────────────────────────────────────────────────────────────────────────
-
-function PoolCard({
- pool,
- myPosition,
- index,
-}: {
- pool: Pool;
- myPosition?: Position;
- index: number;
-}) {
- const apr = (Number(pool.apr_bps ?? 0) / 100).toFixed(2);
- const totalSize = formatTokenBalance(Number(pool.total_liquidity ?? 0));
- const available = formatTokenBalance(Number(pool.available_liquidity ?? 0));
-
- return (
-
- {/* Decorative gradient orb for invested pools */}
- {myPosition && (
-
- )}
-
- {/* Header */}
-
-
-
- 🏦
-
-
-
- {pool.name}
-
-
- {String(pool.id).slice(0, 8)}
-
-
-
-
-
-
- {/* Divider */}
-
-
- {/* Stats */}
-
- {([
- { label: "APR", value: `${apr}%`, accent: "#22cf9d", term: "APR" },
- { label: "Total Size", value: totalSize, accent: undefined, term: undefined },
- { label: "Available", value: available, accent: undefined, term: undefined },
- ] as Array<{
- label: string;
- value: string;
- accent?: string;
- term?: GlossaryTermKey;
- }>).map(({ label, value, accent, term }) => (
-
-
- {label}
- {term && }
-
-
- {value}
-
-
- ))}
-
-
- {/* Footer: my stake */}
-
-
-
- My Stake
-
- {myPosition ? (
-
- {formatCurrency(Number(myPosition.principal_amount ?? 0))} ✅
-
- ) : (
-
—
- )}
-
-
- {/* APR badge pill */}
-
- {apr}% APR
-
-
-
- );
-}
-
-// ────────────────────────────────────────────────────────────────────────────
-// Error state
-// ────────────────────────────────────────────────────────────────────────────
-
-function ErrorState({ message, onRetry }: { message: string; onRetry: () => void }) {
- return (
-
-
- ⚠️ {message}
-
-
- Retry
-
-
- );
-}
-
-// ────────────────────────────────────────────────────────────────────────────
-// Main component
-// ────────────────────────────────────────────────────────────────────────────
-
-export function AvailablePools({ initialPools, initialPositions }: AvailablePoolsProps) {
- const [pools, setPools] = useState(initialPools ?? []);
- const [positions, setPositions] = useState(initialPositions ?? []);
-
- // isLoading is true until we have confirmed data (or an error)
- const [isLoading, setIsLoading] = useState(
- !initialPools || initialPools.length === 0
- );
- const [error, setError] = useState(null);
-
- const fetchData = useCallback(async () => {
- setIsLoading(true);
- setError(null);
-
- try {
- const supabase = getBrowserSupabaseClient();
- if (!supabase) throw new Error("Supabase client not initialised — check env vars.");
-
- const [poolsRes, positionsRes] = await Promise.all([
- supabase
- .from("lending_pools")
- .select("id, name, status, apr_bps, total_liquidity, available_liquidity")
- .order("created_at", { ascending: false })
- .limit(8),
- supabase
- .from("pool_positions")
- .select("id, pool_id, status, principal_amount, earned_interest")
- .order("opened_at", { ascending: true }),
- ]);
-
- if (poolsRes.error) throw new Error(poolsRes.error.message);
- if (positionsRes.error) throw new Error(positionsRes.error.message);
-
- setPools(poolsRes.data ?? []);
- setPositions(positionsRes.data ?? []);
- } catch (err) {
- setError(err instanceof Error ? err.message : "Failed to load pools.");
- } finally {
- setIsLoading(false);
- }
- }, []);
-
- useEffect(() => {
- // If server already provided pools, skip the client fetch
- if (initialPools && initialPools.length > 0) {
- setIsLoading(false);
- return;
- }
- void fetchData();
- }, [initialPools, fetchData]);
-
- // ── Conditionally render skeleton while isLoading is true ──────────────
- if (isLoading) {
- return ;
- }
-
- if (error) {
- return ;
- }
-
- if (pools.length === 0) {
- return (
-
- No lending pools yet
-
- No lending pools have been created yet. Check back soon.
-
-
- );
- }
-
- // ── Render pool cards once loaded ──────────────────────────────────────
- return (
-
-
- {/* Section header */}
-
-
- Available Lending Pools
-
-
- positions.find((pos) => String(pos.pool_id) === String(p.id))
- )
- ? "rgba(34,207,157,0.12)"
- : "rgba(126,47,208,0.1)",
- color: pools.some((p) =>
- positions.find((pos) => String(pos.pool_id) === String(p.id))
- )
- ? "#22cf9d"
- : "#7e2fd0",
- borderRadius: "9999px",
- padding: "0.2rem 0.65rem",
- fontSize: "0.73rem",
- fontWeight: 700,
- }}
- >
- {pools.length} pool{pools.length !== 1 ? "s" : ""}
-
-
-
- {/* Pool cards grid */}
-
- {pools.map((pool, i) => {
- const myPosition = positions.find(
- (pos) => String(pos.pool_id) === String(pool.id)
- );
- return (
-
- );
- })}
-
-
-
- );
-}
diff --git a/components/dashboard/FinanceChart.tsx b/components/dashboard/FinanceChart.tsx
deleted file mode 100644
index 6f921ee..0000000
--- a/components/dashboard/FinanceChart.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { BarChartSkeleton } from "@/components/dashboard/ChartSkeleton";
-
-interface FinanceChartPoint {
- label: string;
- valueA: number;
- valueB: number;
-}
-
-interface FinanceChartProps {
- title: string;
- legendA: string;
- legendB: string;
- points: FinanceChartPoint[];
- /** Show animated skeleton placeholder when true. */
- loading?: boolean;
-}
-
-export function FinanceChart({ title, legendA, legendB, points, loading = false }: FinanceChartProps) {
- // ── Loading state: show skeleton ────────────────────────────────────────────
- if (loading) {
- return ;
- }
-
- const maxValue = Math.max(1, ...points.flatMap((point) => [point.valueA, point.valueB]));
-
- return (
-
-
-
{title}
-
- {legendA}
- {legendB}
-
-
-
- {points.map((point) => (
-
-
-
-
-
-
{point.label}
-
- ))}
-
-
- );
-}
diff --git a/components/dashboard/RoleDashboardScreen.tsx b/components/dashboard/RoleDashboardScreen.tsx
deleted file mode 100644
index 93c0e6f..0000000
--- a/components/dashboard/RoleDashboardScreen.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-"use client";
-
-import Link from "next/link";
-import { useRouter } from "next/navigation";
-import { useEffect, useState } from "react";
-import { getBrowserSupabaseClient } from "@/lib/supabase/client";
-import {
- getDashboardPath,
- isUserRole,
- normalizeUserRole,
- type UserRole,
-} from "@/lib/auth/roles";
-
-interface RoleMetric {
- label: string;
- value: string;
-}
-
-interface RoleDashboardScreenProps {
- expectedRole: UserRole;
- heading: string;
- description: string;
- metrics: RoleMetric[];
- primaryHref: string;
- primaryLabel: string;
- secondaryHref: string;
- secondaryLabel: string;
-}
-
-export function RoleDashboardScreen({
- expectedRole,
- heading,
- description,
- metrics,
- primaryHref,
- primaryLabel,
- secondaryHref,
- secondaryLabel,
-}: RoleDashboardScreenProps) {
- const router = useRouter();
- const [ready, setReady] = useState(false);
- const [email, setEmail] = useState(null);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- let cancelled = false;
-
- const ensureRoleAccess = async () => {
- const supabase = getBrowserSupabaseClient();
- if (!supabase) {
- if (!cancelled) {
- setError("Set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in your environment.");
- }
- return;
- }
-
- const { data } = await supabase.auth.getSession();
- const session = data.session;
-
- if (!session) {
- router.replace("/");
- return;
- }
-
- const metadataRole = session.user.user_metadata?.account_type;
- const roleFromUser = normalizeUserRole(metadataRole);
-
- if (isUserRole(metadataRole) && roleFromUser !== expectedRole) {
- router.replace(getDashboardPath(roleFromUser));
- return;
- }
-
- if (!isUserRole(metadataRole)) {
- const { error: updateError } = await supabase.auth.updateUser({
- data: {
- ...session.user.user_metadata,
- account_type: expectedRole,
- },
- });
-
- if (updateError) {
- if (!cancelled) {
- setError(updateError.message);
- }
- return;
- }
- }
-
- if (!cancelled) {
- setEmail(session.user.email ?? null);
- setReady(true);
- }
- };
-
- void ensureRoleAccess();
-
- return () => {
- cancelled = true;
- };
- }, [expectedRole, router]);
-
- const handleSignOut = async () => {
- const supabase = getBrowserSupabaseClient();
- if (!supabase) {
- return;
- }
-
- await supabase.auth.signOut();
- router.replace("/");
- };
-
- if (error) {
- return (
-
- {error}
-
- );
- }
-
- if (!ready) {
- return (
-
- Loading your {expectedRole} workspace...
-
- );
- }
-
- return (
-
-
-
-
-
{expectedRole} dashboard
-
{heading}
-
{description}
-
-
- Sign out
-
-
-
- Signed in as: {email ?? "Unknown"}
-
-
- {metrics.map((item) => (
-
- {item.value}
- {item.label}
-
- ))}
-
-
-
-
- {primaryLabel}
-
-
- {secondaryLabel}
-
-
-
-
- );
-}
diff --git a/components/dashboard/SorobanProfileCard.tsx b/components/dashboard/SorobanProfileCard.tsx
deleted file mode 100644
index 7a85639..0000000
--- a/components/dashboard/SorobanProfileCard.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-"use client";
-
-import { useEffect, useCallback, useState } from "react";
-import { Loader2, ShieldCheck, AlertCircle, Rocket } from "lucide-react";
-import { ReputationContract } from "@/lib/contracts";
-import { useTransactionSimulation } from "@/lib/stellar/use-transaction-simulation";
-import { ConfirmTransactionModal } from "@/components/ui/ConfirmTransactionModal";
-
-interface SorobanProfileCardProps {
- walletAddress: string | null;
-}
-
-const REPUTATION_CONTRACT_ID =
- process.env.NEXT_PUBLIC_REPUTATION_CONTRACT_ID ?? "";
-
-export function SorobanProfileCard({ walletAddress }: SorobanProfileCardProps) {
- const [profileExists, setProfileExists] = useState(null);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const tx = useTransactionSimulation();
-
- const checkProfile = useCallback(async () => {
- if (!walletAddress) return;
- try {
- const exists = await ReputationContract.hasProfile(walletAddress, walletAddress);
- setProfileExists(exists);
- } catch (err) {
- console.error("[TrustLend] Failed to check on-chain profile:", err);
- }
- }, [walletAddress]);
-
- useEffect(() => {
- if (walletAddress) {
- checkProfile();
- }
- }, [walletAddress, checkProfile]);
-
- const handleInitialize = async () => {
- if (!walletAddress) return;
- tx.preview({
- label: "Initialize On-Chain Profile",
- contractId: REPUTATION_CONTRACT_ID,
- method: "init_borrower",
- args: [walletAddress],
- callerAddress: walletAddress,
- details: {
- Address: walletAddress,
- Action: "Create borrower reputation profile",
- },
- });
- };
-
- const handleConfirmInit = async () => {
- if (!walletAddress) return;
- setLoading(true);
- setError(null);
- try {
- await tx.confirm(async () => {
- await ReputationContract.initBorrowerProfile(walletAddress);
- });
- setProfileExists(true);
- } catch (err) {
- setError(err instanceof Error ? err.message : "Failed to initialize profile");
- } finally {
- setLoading(false);
- }
- };
-
- if (!walletAddress) return null;
-
- if (profileExists === true) {
- return (
-
-
-
-
-
-
-
Stellar Profile Active
-
- Your borrower reputation is now being tracked on the Stellar network. Repay loans on time to build your score.
-
-
-
-
- );
- }
-
- return (
- <>
-
-
-
-
-
-
-
Initialize On-Chain Profile
-
- Your wallet is connected, but your reputation profile hasn't been created on Stellar yet.
- This is required to apply for micro-loans.
-
-
- {error && (
-
- )}
-
-
- {loading ? (
- <>
-
- Initializing...
- >
- ) : "Create On-Chain Profile"}
-
-
-
-
-
- {/* Simulation preview modal */}
-
- >
- );
-}
diff --git a/components/dashboard/WalletSetupCard.tsx b/components/dashboard/WalletSetupCard.tsx
deleted file mode 100644
index 34f4a8d..0000000
--- a/components/dashboard/WalletSetupCard.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { STELLAR_TESTNET, formatWalletAddress } from "@/lib/stellar/testnet";
-
-interface WalletSetupCardProps {
- walletAddress: string | null;
-}
-
-export function WalletSetupCard({ walletAddress }: WalletSetupCardProps) {
- const shortAddress = formatWalletAddress(walletAddress);
-
- return (
-
- Wallet Setup
- Stellar Testnet and test XLM
-
- Use Freighter on the Stellar testnet, fund the account with Friendbot, and keep this wallet ready for contract testing.
-
-
-
-
-
Network
-
{STELLAR_TESTNET.networkName}
-
-
-
Horizon
-
Testnet API
-
-
-
Wallet
-
{shortAddress}
-
-
-
-
- Install Freighter and create a new testnet account.
- Fund the account with Friendbot test XLM.
- Save the public key in your profile once wallet sync is wired.
- Use this same wallet for contract deploys and loan testing.
-
-
-
-
-
- Network passphrase: {STELLAR_TESTNET.networkPassphrase}
-
-
- Horizon endpoint: {STELLAR_TESTNET.horizonUrl}
-
-
- );
-}
\ No newline at end of file
diff --git a/components/landing/SectionTitle.tsx b/components/landing/SectionTitle.tsx
deleted file mode 100644
index 4bbb8f1..0000000
--- a/components/landing/SectionTitle.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-interface SectionTitleProps {
- kicker: string;
- title: string;
- description: string;
-}
-
-export function SectionTitle({ kicker, title, description }: SectionTitleProps) {
- return (
-
-
{kicker}
-
- {title}
-
-
{description}
-
- );
-}
diff --git a/components/ui/table.tsx b/components/ui/table.tsx
deleted file mode 100644
index 2425105..0000000
--- a/components/ui/table.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { clsx } from "clsx";
-import { type HTMLAttributes, type TableHTMLAttributes, type ThHTMLAttributes, type TdHTMLAttributes } from "react";
-
-export function TableWrap({ className, ...props }: HTMLAttributes) {
- return
;
-}
-
-export function Table({ className, ...props }: TableHTMLAttributes) {
- return ;
-}
-
-export function TableHead({ className, ...props }: HTMLAttributes) {
- return ;
-}
-
-export function TableBody({ className, ...props }: HTMLAttributes) {
- return ;
-}
-
-export function TableTh({ className, ...props }: ThHTMLAttributes) {
- return ;
-}
-
-export function TableTd({ className, ...props }: TdHTMLAttributes) {
- return ;
-}
diff --git a/contracts/multisig_admin/src/lib.rs b/contracts/multisig_admin/src/lib.rs
index 768081d..7961a8b 100644
--- a/contracts/multisig_admin/src/lib.rs
+++ b/contracts/multisig_admin/src/lib.rs
@@ -10,7 +10,7 @@
//! activation, payment recording, default marking, escrow disbursement
//! confirmation) — those must stay single-signer/backend-automatable, or the
//! platform's existing cron jobs and liquidation keeper stop functioning.
-//! See `contracts/MULTISIG_ADMIN.md` for the full rationale.
+//! See `docs/contracts/multisig-admin.md` for the full rationale.
//!
//! Flow: `propose` (any registered signer) → `approve` (N distinct signers,
//! asynchronously, over separate transactions) → `execute` (permissionless
diff --git a/docker-compose.subquery.yml b/docker-compose.subquery.yml
deleted file mode 100644
index b148b7a..0000000
--- a/docker-compose.subquery.yml
+++ /dev/null
@@ -1,126 +0,0 @@
-version: "3.8"
-
-# ──────────────────────────────────────────────────────────────────────────────
-# TrustLend — SubQuery Soroban Indexer Docker Compose
-# ──────────────────────────────────────────────────────────────────────────────
-# Spins up three services:
-# 1. postgres — Persistent store for indexed chain data
-# 2. subquery-node — SubQuery Soroban indexer (listens to Stellar events)
-# 3. graphql-engine — GraphQL API over the indexed data
-#
-# Usage:
-# docker compose -f docker-compose.subquery.yml up -d
-#
-# The GraphQL playground is available at http://localhost:3001/graphql
-# after the indexer finishes its initial sync.
-#
-# Environment variables (create a .env.subquery file or export):
-# SUBQUERY_DB_PASSWORD — PostgreSQL password (default: postgres)
-# SUBQUERY_DB_USER — PostgreSQL user (default: postgres)
-# SUBQUERY_DB_NAME — PostgreSQL database (default: subquery)
-# SUBQUERY_NETWORK_ENDPOINT — Soroban RPC endpoint (default: testnet)
-# SUBQUERY_START_BLOCK — Start block height (default: 12345678)
-# SUBQUERY_BATCH_SIZE — Batch size for fetching (default: 100)
-# ──────────────────────────────────────────────────────────────────────────────
-
-x-env-defaults: &env-defaults
- DB_HOST: postgres
- DB_PORT: 5432
- DB_USER: ${SUBQUERY_DB_USER:-postgres}
- DB_PASS: ${SUBQUERY_DB_PASSWORD:-postgres}
- DB_DATABASE: ${SUBQUERY_DB_NAME:-subquery}
-
-services:
- # ── PostgreSQL ────────────────────────────────────────────────────────────
- postgres:
- image: postgres:16-alpine
- container_name: trustlend-subquery-db
- restart: unless-stopped
- ports:
- # Host port 5433 avoids conflict with any local PostgreSQL on 5432.
- - "127.0.0.1:5433:5432"
- environment:
- POSTGRES_PASSWORD: ${SUBQUERY_DB_PASSWORD:-postgres}
- POSTGRES_DB: ${SUBQUERY_DB_NAME:-subquery}
- volumes:
- - subquery-postgres-data:/var/lib/postgresql/data
- healthcheck:
- test: ["CMD-SHELL", "pg_isready -U ${SUBQUERY_DB_USER:-postgres} -d ${SUBQUERY_DB_NAME:-subquery}"]
- interval: 5s
- timeout: 5s
- retries: 5
-
- # ── SubQuery Node (Indexer) ───────────────────────────────────────────────
- subquery-node:
- build:
- context: .
- dockerfile: subquery/Dockerfile
- container_name: trustlend-subquery-node
- restart: unless-stopped
- depends_on:
- postgres:
- condition: service_healthy
- environment:
- <<: *env-defaults
- DB_SCHEMA: trustlend
- # Network configuration
- CHAIN_ID: ${SUBQUERY_CHAIN_ID:-Test SDF Network ; September 2015}
- NETWORK_ENDPOINT: ${SUBQUERY_NETWORK_ENDPOINT:-https://soroban-testnet.stellar.org}
- # Optional: set a specific API key if the RPC endpoint requires one
- # SOROBAN_RPC_API_KEY: ${SUBQUERY_RPC_API_KEY:-}
- volumes:
- # Mount the project root so the node can read project.yaml and schema.graphql
- - ./project.yaml:/app/project.yaml:ro
- - ./schema.graphql:/app/schema.graphql:ro
- # Mount the built mappings
- - subquery-dist:/app/dist
- command:
- - "-f=/app"
- - "--db-schema=trustlend"
- - "--db-host=postgres"
- - "--db-port=5432"
- - "--db-user=${SUBQUERY_DB_USER:-postgres}"
- - "--db-pass=${SUBQUERY_DB_PASSWORD:-postgres}"
- - "--db-database=${SUBQUERY_DB_NAME:-subquery}"
- - "--batch-size=${SUBQUERY_BATCH_SIZE:-100}"
- # Uncomment and set a specific start block to override project.yaml
- # - "--start-block=${SUBQUERY_START_BLOCK:-12345678}"
- logging:
- driver: "json-file"
- options:
- max-size: "10m"
- max-file: "3"
-
- # ── GraphQL Engine (Query Service) ────────────────────────────────────────
- graphql-engine:
- image: onfinality/subql-query:latest
- container_name: trustlend-subquery-graphql
- restart: unless-stopped
- depends_on:
- - postgres
- - subquery-node
- ports:
- - "3001:3000"
- environment:
- <<: *env-defaults
- DB_SCHEMA: trustlend
- command:
- - "--name=trustlend"
- - "--db-schema=trustlend"
- - "--db-host=postgres"
- - "--db-port=5432"
- - "--db-user=${SUBQUERY_DB_USER:-postgres}"
- - "--db-pass=${SUBQUERY_DB_PASSWORD:-postgres}"
- - "--db-database=${SUBQUERY_DB_NAME:-subquery}"
- # Enable the GraphQL playground in dev
- - "--playground=true"
- logging:
- driver: "json-file"
- options:
- max-size: "10m"
- max-file: "3"
-
-volumes:
- subquery-postgres-data:
- driver: local
- subquery-dist:
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index 13c8fe1..0000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-version: '3.8'
-
-services:
- web:
- build:
- context: .
- dockerfile: Dockerfile.dev
- ports:
- - "3000:3000"
- volumes:
- # Mount the local codebase to the container for hot-reloading
- - .:/app
- # Use an anonymous volume for node_modules to avoid Windows/Linux conflicts
- - /app/node_modules
- - /app/.next
- env_file:
- - .env.local
- environment:
- - NODE_ENV=development
- # Enable polling for file changes (required for Windows hot-reload to work consistently)
- - WATCHPACK_POLLING=true
- - CHOKIDAR_USEPOLLING=true
-
- # Note on Supabase:
- # This setup binds the Next.js app to your local machine.
- # If you are running the local Supabase CLI (`npx supabase start`),
- # you should set your NEXT_PUBLIC_SUPABASE_URL in `.env.local` to
- # `http://host.docker.internal:54321` (or whichever port your local Supabase uses)
- # so the Docker container can reach it.
diff --git a/DOCS_APR_FORMULAS.md b/docs/apr-formulas.md
similarity index 100%
rename from DOCS_APR_FORMULAS.md
rename to docs/apr-formulas.md
diff --git a/SIWS_AUTH.md b/docs/auth-siws.md
similarity index 100%
rename from SIWS_AUTH.md
rename to docs/auth-siws.md
diff --git a/contracts/FLASH_LOANS.md b/docs/contracts/flash-loans.md
similarity index 100%
rename from contracts/FLASH_LOANS.md
rename to docs/contracts/flash-loans.md
diff --git a/contracts/GOVERNANCE.md b/docs/contracts/governance.md
similarity index 100%
rename from contracts/GOVERNANCE.md
rename to docs/contracts/governance.md
diff --git a/contracts/MULTISIG_ADMIN.md b/docs/contracts/multisig-admin.md
similarity index 100%
rename from contracts/MULTISIG_ADMIN.md
rename to docs/contracts/multisig-admin.md
diff --git a/contracts/ORACLE_INTEGRATION.md b/docs/contracts/oracle-integration.md
similarity index 100%
rename from contracts/ORACLE_INTEGRATION.md
rename to docs/contracts/oracle-integration.md
diff --git a/DEFAULT_AUTOMATION.md b/docs/default-automation.md
similarity index 100%
rename from DEFAULT_AUTOMATION.md
rename to docs/default-automation.md
diff --git a/DISASTER_RECOVERY.md b/docs/disaster-recovery.md
similarity index 100%
rename from DISASTER_RECOVERY.md
rename to docs/disaster-recovery.md
diff --git a/FORMAL_VERIFICATION.md b/docs/formal-verification.md
similarity index 99%
rename from FORMAL_VERIFICATION.md
rename to docs/formal-verification.md
index 684126d..023ff98 100644
--- a/FORMAL_VERIFICATION.md
+++ b/docs/formal-verification.md
@@ -207,5 +207,5 @@ contracts/lending/
Cargo.toml # Added proptest dev-dependency
.github/workflows/
formal-verification.yml # CI workflow
-FORMAL_VERIFICATION.md # This file (in repo root)
+docs/formal-verification.md # This file
```
diff --git a/GETTING_STARTED.md b/docs/getting-started.md
similarity index 97%
rename from GETTING_STARTED.md
rename to docs/getting-started.md
index 329f819..db35d2f 100644
--- a/GETTING_STARTED.md
+++ b/docs/getting-started.md
@@ -434,6 +434,6 @@ This recreates the `.husky/_/` directory and ensures hooks are activated. The `n
## Next Steps
- Read the [Contributing Guidelines](CONTRIBUTING.md) for the PR workflow.
-- Check the [Roadmap](ROADMAP.md) for upcoming features.
-- Browse project documentation: [Flash Loans](contracts/FLASH_LOANS.md), [MultiSig Admin](contracts/MULTISIG_ADMIN.md), [Oracle Integration](contracts/ORACLE_INTEGRATION.md), [Governance](contracts/GOVERNANCE.md).
+- Check the [Roadmap](roadmap.md) for upcoming features.
+- Browse project documentation: [Flash Loans](contracts/flash-loans.md), [MultiSig Admin](contracts/multisig-admin.md), [Oracle Integration](contracts/oracle-integration.md), [Governance](contracts/governance.md).
- Join the community discussions on GitHub Issues.
diff --git a/LIQUIDATION_KEEPER.md b/docs/liquidation-keeper.md
similarity index 100%
rename from LIQUIDATION_KEEPER.md
rename to docs/liquidation-keeper.md
diff --git a/ORACLE_PRICE_FEEDS.md b/docs/oracle-price-feeds.md
similarity index 100%
rename from ORACLE_PRICE_FEEDS.md
rename to docs/oracle-price-feeds.md
diff --git a/REFERRAL_PROGRAM.md b/docs/referral-program.md
similarity index 100%
rename from REFERRAL_PROGRAM.md
rename to docs/referral-program.md
diff --git a/ROADMAP.md b/docs/roadmap.md
similarity index 100%
rename from ROADMAP.md
rename to docs/roadmap.md
diff --git a/SEP24_FIAT_RAMP.md b/docs/sep24-fiat-ramp.md
similarity index 100%
rename from SEP24_FIAT_RAMP.md
rename to docs/sep24-fiat-ramp.md
diff --git a/lib/config/kyc-verification.config.ts b/lib/config/kyc-verification.config.ts
deleted file mode 100644
index 6562613..0000000
--- a/lib/config/kyc-verification.config.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * KYC Verification Configuration
- *
- * Enhanced security configuration for KYC verification process
- * Based on Level 5 user feedback: Strengthen fraud detection and validation
- *
- * Implementation: Stricter validation rules, enhanced document verification,
- * improved fraud detection mechanisms
- */
-
-export const KYC_VERIFICATION_CONFIG = {
- // Enhanced validation strictness
- validation: {
- strict_mode: true,
- require_face_verification: true,
- document_liveness_check: true,
- additional_verification_questions: true,
- },
-
- // Fraud detection thresholds
- fraud_detection: {
- enable_ml_scoring: true,
- risk_score_threshold: 0.3,
- geographic_velocity_check: true,
- document_tampering_detection: true,
- duplicate_submission_check: true,
- },
-
- // Document verification
- documents: {
- require_government_id: true,
- support_biometric_matching: true,
- enable_document_expiry_check: true,
- require_recent_proof_of_address: true,
- },
-
- // Implementation date
- implemented_date: '2026-04-25',
- feedback_source: 'Level 5 User: Souvik Mandal',
- feedback_priority: 'CRITICAL',
-};
-
-export const getKYCConfig = () => KYC_VERIFICATION_CONFIG;
diff --git a/lib/config/lender-pool-security.config.ts b/lib/config/lender-pool-security.config.ts
deleted file mode 100644
index 39883a0..0000000
--- a/lib/config/lender-pool-security.config.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Lender Pool Security Configuration
- *
- * Enhanced security measures for protecting lender investments
- * Based on Level 5 user feedback:
- * - Saurav Suman: Pool section security for interest preservation
- * - Subham Singha: Lender-side safety mechanisms
- *
- * Implementation: Fund protection, default risk management,
- * interest rate safeguards, deposit verification
- */
-
-export const LENDER_POOL_SECURITY_CONFIG = {
- // Interest rate protection
- interest_protection: {
- enable_rate_locking: true,
- prevent_rate_modification_after_deposit: true,
- interest_accrual_verification: true,
- automated_interest_calculation: true,
- },
-
- // Deposit security
- deposit_security: {
- require_deposit_verification: true,
- minimum_deposit_amount: 100, // USDC equivalent
- maximum_deposit_concentration: 0.2, // 20% of total pool
- escrow_holding_period: 3600, // 1 hour in seconds
- },
-
- // Default risk management
- default_management: {
- enable_default_insurance: true,
- automatic_default_detection: true,
- graceful_default_handling: true,
- liquidation_protection: true,
- recovery_fund_allocation: 0.02, // 2% recovery fund
- },
-
- // Fund preservation
- fund_preservation: {
- separate_borrower_lender_wallets: true,
- enable_multi_sig_approvals: false, // Future enhancement
- cold_storage_integration: false, // Future enhancement
- emergency_withdrawal_enabled: true,
- },
-
- // Implementation tracking
- implemented_date: '2026-04-25',
- feedback_sources: [
- 'Level 5 User: Saurav Suman (Lender)',
- 'Level 5 User: Subham Singha (Lender)',
- ],
- feedback_priority: 'HIGH',
-};
-
-export const getPoolSecurityConfig = () => LENDER_POOL_SECURITY_CONFIG;
diff --git a/lib/content/landing-content.ts b/lib/content/landing-content.ts
index 1d39ba6..17ea904 100644
--- a/lib/content/landing-content.ts
+++ b/lib/content/landing-content.ts
@@ -207,7 +207,7 @@ export const trustBadges: TrustBadge[] = [
{
label: "Formally verified contracts",
detail: "Core accounting invariants proved with Kani and property tests in CI.",
- href: `${REPO_URL}/blob/main/FORMAL_VERIFICATION.md`,
+ href: `${REPO_URL}/blob/main/docs/formal-verification.md`,
icon: "verified",
external: true,
},
diff --git a/lib/dashboard/metrics.ts b/lib/dashboard/metrics.ts
index f427831..b964c82 100644
--- a/lib/dashboard/metrics.ts
+++ b/lib/dashboard/metrics.ts
@@ -1,11 +1,4 @@
import { getServerSupabaseClient } from "@/lib/supabase/server";
-import {
- getIndexedAdminReadModel,
- getIndexedBorrowerReadModel,
- getIndexedLenderReadModel,
- isIndexerConfigured,
- isIndexerRequired,
-} from "@/lib/indexer/read-model";
export interface BorrowerDashboardMetrics {
reputationScore: number;
@@ -73,43 +66,7 @@ const ACTIVE_LOAN_STATUSES = ["active", "funded", "approved"];
export async function getBorrowerDashboardMetrics(
userId: string,
- walletAddress?: string | null,
): Promise {
- if (isIndexerConfigured() && walletAddress) {
- try {
- const indexed = await getIndexedBorrowerReadModel({ userId, walletAddress });
- if (indexed.loans.length === 0 && indexed.reputationEvents.length === 0) {
- throw new Error("Indexer borrower read model is empty");
- }
- const reputationPoints = indexed.reputationEvents.reduce(
- (sum, row) => sum + Number(row.pointsDelta ?? 0),
- 0,
- );
- const reputation =
- indexed.reputationEvents.find((row) => Number.isFinite(row.scoreAfter ?? NaN))
- ?.scoreAfter ?? Math.max(0, 250 + reputationPoints);
- const pendingLoans = indexed.loans.filter((loan) =>
- ["pending", "requested"].includes(loan.status),
- ).length;
- const activeLoans = indexed.loans.filter((loan) =>
- ACTIVE_LOAN_STATUSES.includes(loan.status),
- ).length;
- const repaidLoans = indexed.loans.filter((loan) => loan.status === "repaid").length;
- const defaultedLoans = indexed.loans.filter((loan) => loan.status === "defaulted").length;
- const repaymentBase = repaidLoans + defaultedLoans;
- const repaymentRate = repaymentBase > 0 ? (repaidLoans / repaymentBase) * 100 : 100;
-
- return {
- reputationScore: Math.max(0, Number(reputation ?? 0)),
- availableCredit: Math.max(0, Number(reputation ?? 0)) * 10,
- activeLoans,
- pendingLoans,
- repaymentRate,
- };
- } catch (error) {
- if (isIndexerRequired()) throw error;
- }
- }
const supabase = await getServerSupabaseClient();
@@ -155,32 +112,14 @@ export async function getBorrowerDashboardMetrics(
export async function getLenderDashboardMetrics(
userId: string,
- walletAddress?: string | null,
): Promise {
- let indexedDeployed = 0;
- let indexedEarnings = 0;
- let indexedActive = 0;
-
- if (isIndexerConfigured() && walletAddress) {
- try {
- const indexed = await getIndexedLenderReadModel({ userId, walletAddress });
- indexedDeployed = indexed.loans.reduce((sum, loan) => sum + loan.principalAmount, 0);
- indexedEarnings = indexed.loans.reduce(
- (sum, loan) => sum + Math.max(0, loan.repaidAmount - loan.principalAmount),
- 0,
- );
- indexedActive = indexed.loans.filter((loan) => ACTIVE_LOAN_STATUSES.includes(loan.status)).length;
- } catch (error) {
- if (isIndexerRequired()) throw error;
- }
- }
const { getServerSupabaseClient, getServiceRoleClient } = await import("@/lib/supabase/server");
const supabase = await getServerSupabaseClient();
const srClient = getServiceRoleClient();
if (!supabase || !srClient) {
- return { deployedCapital: indexedDeployed, totalEarnings: indexedEarnings, activePositions: indexedActive, defaultRate: 0 };
+ return { deployedCapital: 0, totalEarnings: 0, activePositions: 0, defaultRate: 0 };
}
try {
@@ -255,9 +194,9 @@ export async function getLenderDashboardMetrics(
p2pActiveCount = (loans ?? []).filter(l => l.status === "active").length;
}
- const deployedCapital = poolDeployed + (indexedDeployed || p2pDeployed);
- const totalEarnings = poolEarnings + (indexedEarnings || p2pProfit);
- const activePositions = poolActive + (indexedActive || p2pActiveCount);
+ const deployedCapital = poolDeployed + p2pDeployed;
+ const totalEarnings = poolEarnings + p2pProfit;
+ const activePositions = poolActive + p2pActiveCount;
const defaultRate = 0;
return { deployedCapital, totalEarnings, activePositions, defaultRate };
@@ -271,22 +210,6 @@ export async function getAdminDashboardMetrics(): Promise
if (!supabase) return { totalUsers: 0, totalLoans: 0, activeLoans: 0, highRiskUsers: 0 };
try {
- let indexedLoanCounts: Pick | null = null;
- if (isIndexerConfigured()) {
- try {
- const indexed = await getIndexedAdminReadModel(500);
- if (indexed.loans.length > 0) {
- indexedLoanCounts = {
- totalLoans: indexed.loans.length,
- activeLoans: indexed.loans.filter((loan) =>
- [...ACTIVE_LOAN_STATUSES, "pending", "requested"].includes(loan.status),
- ).length,
- };
- }
- } catch (error) {
- if (isIndexerRequired()) throw error;
- }
- }
const [usersRes, totalLoansRes, activeLoansRes, highRiskRes] = await Promise.all([
supabase.from("profiles").select("id", { count: "exact", head: true }),
@@ -298,8 +221,8 @@ export async function getAdminDashboardMetrics(): Promise
]);
return {
totalUsers: usersRes.count ?? 0,
- totalLoans: indexedLoanCounts?.totalLoans ?? totalLoansRes.count ?? 0,
- activeLoans: indexedLoanCounts?.activeLoans ?? activeLoansRes.count ?? 0,
+ totalLoans: totalLoansRes.count ?? 0,
+ activeLoans: activeLoansRes.count ?? 0,
highRiskUsers: highRiskRes.count ?? 0,
};
} catch {
diff --git a/lib/indexer/read-model.ts b/lib/indexer/read-model.ts
deleted file mode 100644
index aacc04b..0000000
--- a/lib/indexer/read-model.ts
+++ /dev/null
@@ -1,463 +0,0 @@
-type IndexerMode = "disabled" | "fallback" | "required";
-
-export interface IndexedLoan {
- id: string;
- borrowerId?: string | null;
- borrowerAddress?: string | null;
- lenderId?: string | null;
- lenderAddress?: string | null;
- status: string;
- principalAmount: number;
- repaidAmount: number;
- aprBps: number;
- durationDays: number;
- dueAt: string | null;
- createdAt: string | null;
- requestedAt?: string | null;
- escrowId?: string | number | null;
-}
-
-export interface IndexedReputationEvent {
- id?: string;
- borrowerId?: string | null;
- borrowerAddress?: string | null;
- eventType?: string | null;
- pointsDelta: number;
- scoreAfter?: number | null;
- createdAt?: string | null;
-}
-
-export interface IndexedEscrowEvent {
- id?: string;
- loanId: string;
- lenderAddress?: string | null;
- borrowerAddress?: string | null;
- amount: number;
- eventType?: string | null;
- txHash?: string | null;
- createdAt?: string | null;
-}
-
-export interface IndexedDashboardReadModel {
- loans: IndexedLoan[];
- reputationEvents: IndexedReputationEvent[];
- escrowEvents: IndexedEscrowEvent[];
-}
-
-/**
- * Pagination options for indexer queries.
- *
- * Both `first` and `offset` are standard SubQuery GraphQL pagination params.
- * `after` enables cursor-based pagination (takes precedence over `offset`).
- *
- * Examples:
- * // First page, 20 items
- * { limit: 20 }
- * // Second page via offset
- * { limit: 20, offset: 20 }
- * // Next page via cursor
- * { limit: 20, after: "YXJyYXljb25uZWN0aW9uOjI=" }
- */
-export interface PaginationOptions {
- /** Maximum records to return (default: 50). */
- limit?: number;
- /** Zero-based offset for skip-based pagination. */
- offset?: number;
- /** Cursor string for cursor-based pagination (overrides offset). */
- after?: string | null;
-}
-
-interface ReadOptions extends PaginationOptions {
- userId?: string | null;
- walletAddress?: string | null;
-}
-
-const DEFAULT_LIMIT = 50;
-const DEFAULT_OFFSET = 0;
-
-function getMode(): IndexerMode {
- const raw = (process.env.TRUSTLEND_INDEXER_READ_MODE ?? "fallback").toLowerCase();
- if (raw === "required" || raw === "disabled") return raw;
- return "fallback";
-}
-
-export function isIndexerConfigured(): boolean {
- return Boolean(process.env.TRUSTLEND_INDEXER_GRAPHQL_URL || process.env.TRUSTLEND_INDEXER_REST_URL);
-}
-
-export function isIndexerRequired(): boolean {
- return getMode() === "required";
-}
-
-function getHeaders(): HeadersInit {
- const headers: HeadersInit = { "content-type": "application/json" };
- const token = process.env.TRUSTLEND_INDEXER_API_KEY;
- if (token) {
- headers.authorization = `Bearer ${token}`;
- }
- return headers;
-}
-
-function unwrapRows(payload: unknown, preferredKey: string): T[] {
- if (!payload || typeof payload !== "object") return [];
- const record = payload as Record;
- const direct = record[preferredKey];
- if (Array.isArray(direct)) return direct as T[];
- if (direct && typeof direct === "object" && Array.isArray((direct as Record).nodes)) {
- return (direct as Record).nodes as T[];
- }
- if (Array.isArray(record.nodes)) return record.nodes as T[];
- for (const value of Object.values(record)) {
- if (Array.isArray(value)) return value as T[];
- if (value && typeof value === "object" && Array.isArray((value as Record).nodes)) {
- return (value as Record).nodes as T[];
- }
- }
- return [];
-}
-
-async function requestGraphql(
- query: string,
- variables: Record,
- preferredKey: string,
-): Promise {
- const url = process.env.TRUSTLEND_INDEXER_GRAPHQL_URL;
- if (!url) return [];
-
- const res = await fetch(url, {
- method: "POST",
- headers: getHeaders(),
- body: JSON.stringify({ query, variables }),
- cache: "no-store",
- });
- if (!res.ok) throw new Error(`Indexer GraphQL returned ${res.status}`);
-
- const json = (await res.json()) as { data?: unknown; errors?: unknown };
- if (json.errors) throw new Error("Indexer GraphQL returned errors");
- return unwrapRows(json.data, preferredKey);
-}
-
-async function requestRest(
- path: string,
- params: Record,
- preferredKey: string,
-): Promise {
- const base = process.env.TRUSTLEND_INDEXER_REST_URL;
- if (!base) return [];
-
- const url = new URL(path.replace(/^\//, ""), base.endsWith("/") ? base : `${base}/`);
- for (const [key, value] of Object.entries(params)) {
- if (value !== undefined && value !== null && value !== "") {
- url.searchParams.set(key, String(value));
- }
- }
-
- const res = await fetch(url, { headers: getHeaders(), cache: "no-store" });
- if (!res.ok) throw new Error(`Indexer REST returned ${res.status}`);
-
- const json = await res.json();
- return Array.isArray(json) ? (json as T[]) : unwrapRows(json, preferredKey);
-}
-
-/**
- * Build pagination variables for GraphQL queries.
- */
-export function buildPaginationVariables(pagination?: PaginationOptions): Record {
- return {
- limit: pagination?.limit ?? DEFAULT_LIMIT,
- offset: pagination?.offset ?? DEFAULT_OFFSET,
- after: pagination?.after ?? null,
- };
-}
-
-/**
- * Build pagination params for REST API calls.
- */
-export function buildRestPaginationParams(pagination?: PaginationOptions): Record {
- return {
- limit: pagination?.limit ?? DEFAULT_LIMIT,
- offset: pagination?.offset ?? DEFAULT_OFFSET,
- after: pagination?.after ?? null,
- };
-}
-
-async function readIndexed({
- graphqlQuery,
- graphqlQueryEnv,
- restPath,
- variables,
- restParams,
- preferredKey,
-}: {
- graphqlQuery: string;
- graphqlQueryEnv: string;
- restPath: string;
- variables: Record;
- restParams: Record;
- preferredKey: string;
-}): Promise {
- if (getMode() === "disabled" || !isIndexerConfigured()) return [];
-
- const configuredQuery = process.env[graphqlQueryEnv];
- if (process.env.TRUSTLEND_INDEXER_GRAPHQL_URL) {
- return requestGraphql(configuredQuery || graphqlQuery, variables, preferredKey);
- }
- return requestRest(restPath, restParams, preferredKey);
-}
-
-function toNumber(value: unknown): number {
- if (typeof value === "number") return Number.isFinite(value) ? value : 0;
- if (typeof value === "bigint") return Number(value);
- if (typeof value === "string") {
- const parsed = Number(value);
- return Number.isFinite(parsed) ? parsed : 0;
- }
- return 0;
-}
-
-function toStringOrNull(value: unknown): string | null {
- if (value === undefined || value === null || value === "") return null;
- return String(value);
-}
-
-function normalizeLoan(row: Record): IndexedLoan {
- const principal =
- row.principalAmount ?? row.principal_amount ?? row.amount ?? row.totalPrincipal ?? 0;
- return {
- id: String(row.id ?? row.loanId ?? row.loan_id ?? ""),
- borrowerId: toStringOrNull(row.borrowerId ?? row.borrower_id),
- borrowerAddress: toStringOrNull(row.borrowerAddress ?? row.borrower),
- lenderId: toStringOrNull(row.lenderId ?? row.lender_id),
- lenderAddress: toStringOrNull(row.lenderAddress ?? row.lender),
- status: String(row.status ?? "requested").toLowerCase(),
- principalAmount: toNumber(principal),
- repaidAmount: toNumber(row.repaidAmount ?? row.repaid_amount ?? row.totalRepaid ?? 0),
- aprBps: toNumber(row.aprBps ?? row.apr_bps ?? row.interestRateBps ?? row.interest_rate_bps ?? 0),
- durationDays: toNumber(row.durationDays ?? row.duration_days ?? 0),
- dueAt: toStringOrNull(row.dueAt ?? row.due_at),
- createdAt: toStringOrNull(row.createdAt ?? row.created_at ?? row.ledgerTimestamp),
- requestedAt: toStringOrNull(row.requestedAt ?? row.requested_at),
- escrowId: toStringOrNull(row.escrowId ?? row.escrow_id),
- };
-}
-
-function normalizeReputationEvent(row: Record): IndexedReputationEvent {
- return {
- id: toStringOrNull(row.id) ?? undefined,
- borrowerId: toStringOrNull(row.borrowerId ?? row.user_id),
- borrowerAddress: toStringOrNull(row.borrowerAddress ?? row.borrower),
- eventType: toStringOrNull(row.eventType ?? row.event_type),
- pointsDelta: toNumber(row.pointsDelta ?? row.points_delta ?? row.delta),
- scoreAfter: toNumber(row.scoreAfter ?? row.score_after ?? row.score),
- createdAt: toStringOrNull(row.createdAt ?? row.created_at ?? row.ledgerTimestamp),
- };
-}
-
-function normalizeEscrowEvent(row: Record): IndexedEscrowEvent {
- return {
- id: toStringOrNull(row.id) ?? undefined,
- loanId: String(row.loanId ?? row.loan_id ?? row.refId ?? ""),
- lenderAddress: toStringOrNull(row.lenderAddress ?? row.lender),
- borrowerAddress: toStringOrNull(row.borrowerAddress ?? row.borrower),
- amount: toNumber(row.amount),
- eventType: toStringOrNull(row.eventType ?? row.event_type ?? row.type),
- txHash: toStringOrNull(row.txHash ?? row.tx_hash ?? row.transactionHash),
- createdAt: toStringOrNull(row.createdAt ?? row.created_at ?? row.ledgerTimestamp),
- };
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// GraphQL Queries (now with offset + cursor-based pagination)
-// ─────────────────────────────────────────────────────────────────────────────
-
-const BORROWER_LOANS_QUERY = `
- query TrustLendBorrowerLoans(
- $userId: String, $walletAddress: String,
- $limit: Int!, $offset: Int, $after: String
- ) {
- loans(
- first: $limit
- offset: $offset
- after: $after
- orderBy: createdAt
- orderDirection: desc
- where: { borrowerId: $userId, borrowerAddress: $walletAddress }
- ) {
- id borrowerId borrowerAddress lenderId lenderAddress status principalAmount repaidAmount
- aprBps durationDays dueAt createdAt requestedAt escrowId
- }
- }
-`;
-
-const LENDER_LOANS_QUERY = `
- query TrustLendLenderLoans(
- $userId: String, $walletAddress: String,
- $limit: Int!, $offset: Int, $after: String
- ) {
- loans(
- first: $limit
- offset: $offset
- after: $after
- orderBy: createdAt
- orderDirection: desc
- where: { lenderId: $userId, lenderAddress: $walletAddress }
- ) {
- id borrowerId borrowerAddress lenderId lenderAddress status principalAmount repaidAmount
- aprBps durationDays dueAt createdAt requestedAt escrowId
- }
- }
-`;
-
-const ADMIN_LOANS_QUERY = `
- query TrustLendAdminLoans($limit: Int!, $offset: Int, $after: String) {
- loans(first: $limit, offset: $offset, after: $after, orderBy: createdAt, orderDirection: desc) {
- id borrowerId borrowerAddress lenderId lenderAddress status principalAmount repaidAmount
- aprBps durationDays dueAt createdAt requestedAt escrowId
- }
- }
-`;
-
-const REPUTATION_EVENTS_QUERY = `
- query TrustLendReputationEvents(
- $userId: String, $walletAddress: String,
- $limit: Int!, $offset: Int, $after: String
- ) {
- reputationEvents(
- first: $limit
- offset: $offset
- after: $after
- orderBy: createdAt
- orderDirection: desc
- where: { borrowerId: $userId, borrowerAddress: $walletAddress }
- ) {
- id borrowerId borrowerAddress eventType pointsDelta scoreAfter createdAt
- }
- }
-`;
-
-const ESCROW_EVENTS_QUERY = `
- query TrustLendEscrowEvents(
- $walletAddress: String,
- $limit: Int!, $offset: Int, $after: String
- ) {
- escrowEvents(
- first: $limit
- offset: $offset
- after: $after
- orderBy: createdAt
- orderDirection: desc
- where: { lenderAddress: $walletAddress }
- ) {
- id loanId lenderAddress borrowerAddress amount eventType txHash createdAt
- }
- }
-`;
-
-// ─────────────────────────────────────────────────────────────────────────────
-// Public reader functions
-// ─────────────────────────────────────────────────────────────────────────────
-
-export async function getIndexedBorrowerReadModel(options: ReadOptions): Promise {
- const variables = {
- userId: options.userId ?? "",
- walletAddress: options.walletAddress ?? "",
- ...buildPaginationVariables(options),
- };
-
- const restParams = {
- borrowerId: options.userId,
- borrowerAddress: options.walletAddress,
- ...buildRestPaginationParams(options),
- };
-
- const [loans, reputationEvents] = await Promise.all([
- readIndexed>({
- graphqlQuery: BORROWER_LOANS_QUERY,
- graphqlQueryEnv: "TRUSTLEND_INDEXER_BORROWER_LOANS_QUERY",
- restPath: "/loans",
- variables,
- restParams,
- preferredKey: "loans",
- }),
- readIndexed>({
- graphqlQuery: REPUTATION_EVENTS_QUERY,
- graphqlQueryEnv: "TRUSTLEND_INDEXER_REPUTATION_EVENTS_QUERY",
- restPath: "/reputation-events",
- variables,
- restParams,
- preferredKey: "reputationEvents",
- }),
- ]);
-
- return {
- loans: loans.map(normalizeLoan).filter((loan) => loan.id),
- reputationEvents: reputationEvents.map(normalizeReputationEvent),
- escrowEvents: [],
- };
-}
-
-export async function getIndexedLenderReadModel(options: ReadOptions): Promise {
- const variables = {
- userId: options.userId ?? "",
- walletAddress: options.walletAddress ?? "",
- ...buildPaginationVariables(options),
- };
-
- const restParams = {
- lenderId: options.userId,
- lenderAddress: options.walletAddress,
- ...buildRestPaginationParams(options),
- };
-
- const [loans, escrowEvents] = await Promise.all([
- readIndexed>({
- graphqlQuery: LENDER_LOANS_QUERY,
- graphqlQueryEnv: "TRUSTLEND_INDEXER_LENDER_LOANS_QUERY",
- restPath: "/loans",
- variables,
- restParams,
- preferredKey: "loans",
- }),
- readIndexed>({
- graphqlQuery: ESCROW_EVENTS_QUERY,
- graphqlQueryEnv: "TRUSTLEND_INDEXER_ESCROW_EVENTS_QUERY",
- restPath: "/escrow-events",
- variables,
- restParams: {
- lenderAddress: options.walletAddress,
- ...buildRestPaginationParams(options),
- },
- preferredKey: "escrowEvents",
- }),
- ]);
-
- return {
- loans: loans.map(normalizeLoan).filter((loan) => loan.id),
- reputationEvents: [],
- escrowEvents: escrowEvents.map(normalizeEscrowEvent).filter((event) => event.loanId),
- };
-}
-
-export async function getIndexedAdminReadModel(
- limit?: number,
- options?: PaginationOptions,
-): Promise {
- const pagination: PaginationOptions = { ...options, limit: limit ?? options?.limit ?? DEFAULT_LIMIT };
- const variables = buildPaginationVariables(pagination);
- const restParams = buildRestPaginationParams(pagination);
-
- const loans = await readIndexed>({
- graphqlQuery: ADMIN_LOANS_QUERY,
- graphqlQueryEnv: "TRUSTLEND_INDEXER_ADMIN_LOANS_QUERY",
- restPath: "/loans",
- variables,
- restParams,
- preferredKey: "loans",
- });
-
- return {
- loans: loans.map(normalizeLoan).filter((loan) => loan.id),
- reputationEvents: [],
- escrowEvents: [],
- };
-}
diff --git a/lib/level5-feedback.ts b/lib/level5-feedback.ts
deleted file mode 100644
index 3698444..0000000
--- a/lib/level5-feedback.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Level 5: User Feedback Implementation
- *
- * This file documents user feedback implementation from Level 5 onboarding phase.
- *
- * User Feedback Summary:
- * - 5 unique users tested TrustLend (3 Borrowers, 2 Lenders)
- * - Key feedback: Enhanced KYC security, improved lender protection, pool safety
- * - Implementation: 5 commits addressing all critical feedback items
- */
-
-export const LEVEL5_USER_FEEDBACK = {
- phase: 'User Onboarding & Feedback Collection',
- startDate: '2026-04-22',
- endDate: '2026-04-24',
- usersOnboarded: 5,
- feedbackImplemented: true,
- criticalIssuesAddressed: 3,
- commitIds: [
- 'daa8141', // KYC enhancement
- '8c3b9d4', // Lender pool security
- '2f5e1a6', // Documentation
- '5d8c2b7', // Lender protection
- '9e2f5c1' // Project roadmap
- ]
-};
diff --git a/lib/pool-security-config.ts b/lib/pool-security-config.ts
deleted file mode 100644
index 5da2c12..0000000
--- a/lib/pool-security-config.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Lender Pool Security Enhancements
- *
- * This module contains security configurations for lender pool operations.
- * Based on user feedback (Saurav Suman - Level 5 Testing):
- * "Enhanced security for preserving lenders' interest in Pool section"
- *
- * Implemented features:
- * - Enhanced interest rate protection
- * - Pool deposit verification
- * - Default risk management
- * - Fund preservation protocols
- */
-
-export const POOL_SECURITY_CONFIG = {
- enabled: true,
- features: {
- interestProtection: true,
- depositVerification: true,
- defaultRiskManagement: true,
- fundPreservation: true
- },
- level5Feedback: 'Saurav Suman',
- feedbackTheme: 'Lender pool security and interest preservation',
- implementationDate: '2026-04-24'
-};
diff --git a/lib/scheduler/default-management.ts b/lib/scheduler/default-management.ts
index 51f94b3..a93ebf8 100644
--- a/lib/scheduler/default-management.ts
+++ b/lib/scheduler/default-management.ts
@@ -15,7 +15,7 @@
* the kind of rare, high-impact operation that needs N-of-M human approval.
* The automation's own key must be a REGISTERED SIGNER on the MultiSigAdmin
* contract so it can propose; a human still has to approve + execute before
- * any funds actually move. See `contracts/MULTISIG_ADMIN.md`.
+ * any funds actually move. See `docs/contracts/multisig-admin.md`.
*
* Every step is idempotent (guarded by Supabase state) and individually
* error-handled so one bad loan never aborts the whole run.
diff --git a/package-lock.json b/package-lock.json
index dee6c3f..8a649df 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,6 @@
"name": "trustlend",
"version": "0.1.0",
"dependencies": {
- "@albedo-link/intent": "^0.13.0",
"@creit.tech/stellar-wallets-kit": "^2.5.0",
"@radix-ui/react-tooltip": "^1.2.16",
"@stellar/freighter-api": "^6.0.1",
@@ -34,7 +33,6 @@
"@commitlint/config-conventional": "^19.8.0",
"@commitlint/types": "^19.8.0",
"@playwright/test": "^1.62.0",
- "@subql/types-stellar": "^5.2.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/pdfkit": "^0.17.6",
@@ -45,7 +43,6 @@
"cross-env": "^10.1.0",
"eslint": "^9",
"eslint-config-next": "16.2.3",
- "ethers": "^6.17.0",
"husky": "^9.1.7",
"tailwindcss": "^4",
"tsx": "^4.19.2",
@@ -59,12 +56,6 @@
"integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==",
"license": "MIT"
},
- "node_modules/@albedo-link/intent": {
- "version": "0.13.0",
- "resolved": "https://registry.npmjs.org/@albedo-link/intent/-/intent-0.13.0.tgz",
- "integrity": "sha512-A8CBXqGQEBMXhwxNXj5inC6HLjyx5Do7jW99NOFeecYd1nPUq8gfM0tvoNoR8H8JQ11aTl9tyQBuu/+l3xeBnQ==",
- "license": "MIT"
- },
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -4994,71 +4985,6 @@
"pnpm": ">=9.0.0"
}
},
- "node_modules/@stellar/stellar-base": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-14.1.0.tgz",
- "integrity": "sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw==",
- "deprecated": "This package is now rolled into @stellar/stellar-sdk. Please use @stellar/stellar-sdk to continue receiving updates and support.",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@noble/curves": "^1.9.6",
- "@stellar/js-xdr": "^3.1.2",
- "base32.js": "^0.1.0",
- "bignumber.js": "^9.3.1",
- "buffer": "^6.0.3",
- "sha.js": "^2.4.12"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@stellar/stellar-base/node_modules/@noble/curves": {
- "version": "1.9.7",
- "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
- "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@noble/hashes": "1.8.0"
- },
- "engines": {
- "node": "^14.21.3 || >=16"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/@stellar/stellar-base/node_modules/@noble/hashes": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
- "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^14.21.3 || >=16"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/@stellar/stellar-base/node_modules/@stellar/js-xdr": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.2.tgz",
- "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==",
- "dev": true,
- "license": "Apache-2.0"
- },
- "node_modules/@stellar/stellar-base/node_modules/bignumber.js": {
- "version": "9.3.1",
- "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
- "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
"node_modules/@stellar/stellar-sdk": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-16.0.1.tgz",
@@ -5085,116 +5011,6 @@
"node": ">=22.0.0"
}
},
- "node_modules/@subql/types-core": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@subql/types-core/-/types-core-2.2.0.tgz",
- "integrity": "sha512-aDA49e3mu0pC/AItemrBANBKXnbiZaKeImjkDN2lhEeyy8zercnXWKX0sRtXWyJsTsxe/dDwH/hykAUgx8bvOw==",
- "dev": true,
- "license": "GPL-3.0",
- "dependencies": {
- "package-json-type": "^1.0.3",
- "pino": "^6.13.3"
- }
- },
- "node_modules/@subql/types-core/node_modules/pino": {
- "version": "6.14.0",
- "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz",
- "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-redact": "^3.0.0",
- "fast-safe-stringify": "^2.0.8",
- "flatstr": "^1.0.12",
- "pino-std-serializers": "^3.1.0",
- "process-warning": "^1.0.0",
- "quick-format-unescaped": "^4.0.3",
- "sonic-boom": "^1.0.2"
- },
- "bin": {
- "pino": "bin.js"
- }
- },
- "node_modules/@subql/types-core/node_modules/pino-std-serializers": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz",
- "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@subql/types-core/node_modules/process-warning": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz",
- "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@subql/types-core/node_modules/sonic-boom": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz",
- "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "atomic-sleep": "^1.0.0",
- "flatstr": "^1.0.12"
- }
- },
- "node_modules/@subql/types-stellar": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@subql/types-stellar/-/types-stellar-5.2.0.tgz",
- "integrity": "sha512-chgnVwRLXysr6dXsNZVsHmAwWb4yC1uNxLfU+tWoRVFiWcykZF+0CfeAVBP3WS050cEVDK3w/NsIHvnrtMPYQw==",
- "dev": true,
- "license": "GPL-3.0",
- "dependencies": {
- "@stellar/stellar-sdk": "^14.4.3",
- "@subql/types-core": "^2.1.0"
- }
- },
- "node_modules/@subql/types-stellar/node_modules/@stellar/stellar-sdk": {
- "version": "14.6.1",
- "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-14.6.1.tgz",
- "integrity": "sha512-A1rQWDLdUasXkMXnYSuhgep+3ZZzyuXJKdt5/KAIc0gkmSp906HTvUpbT4pu+bVr41tu0+J4Ugz9J4BQAGGytg==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@stellar/stellar-base": "^14.1.0",
- "axios": "^1.13.3",
- "bignumber.js": "^9.3.1",
- "commander": "^14.0.2",
- "eventsource": "^2.0.2",
- "feaxios": "^0.0.23",
- "randombytes": "^2.1.0",
- "toml": "^3.0.0",
- "urijs": "^1.19.1"
- },
- "bin": {
- "stellar-js": "bin/stellar-js"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@subql/types-stellar/node_modules/bignumber.js": {
- "version": "9.3.1",
- "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
- "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/@subql/types-stellar/node_modules/eventsource": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
- "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- }
- },
"node_modules/@supabase/auth-js": {
"version": "2.103.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.103.0.tgz",
@@ -7556,13 +7372,6 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
- "node_modules/aes-js": {
- "version": "4.0.0-beta.5",
- "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz",
- "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -9702,85 +9511,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/ethers": {
- "version": "6.17.0",
- "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.17.0.tgz",
- "integrity": "sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==",
- "dev": true,
- "funding": [
- {
- "type": "individual",
- "url": "https://github.com/sponsors/ethers-io/"
- },
- {
- "type": "individual",
- "url": "https://www.buymeacoffee.com/ricmoo"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "@adraffy/ens-normalize": "1.11.1",
- "@noble/curves": "1.2.0",
- "@noble/hashes": "1.3.2",
- "@types/node": "22.7.5",
- "aes-js": "4.0.0-beta.5",
- "tslib": "2.7.0",
- "ws": "8.21.0"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/ethers/node_modules/@noble/curves": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
- "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@noble/hashes": "1.3.2"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/ethers/node_modules/@noble/hashes": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
- "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 16"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/ethers/node_modules/@types/node": {
- "version": "22.7.5",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
- "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": "~6.19.2"
- }
- },
- "node_modules/ethers/node_modules/tslib": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
- "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
- "dev": true,
- "license": "0BSD"
- },
- "node_modules/ethers/node_modules/undici-types": {
- "version": "6.19.8",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
- "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
@@ -9885,23 +9615,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/fast-redact": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
- "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/fast-safe-stringify": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
- "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/fast-stable-stringify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz",
@@ -10000,13 +9713,6 @@
"node": ">=16"
}
},
- "node_modules/flatstr": {
- "version": "1.0.12",
- "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz",
- "integrity": "sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/flatted": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
@@ -12672,13 +12378,6 @@
"node": ">=6"
}
},
- "node_modules/package-json-type": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/package-json-type/-/package-json-type-1.1.2.tgz",
- "integrity": "sha512-QMy9vgEVIkHp/6LI0H7v6P+7ZFp0zQyPZfv8rGV/JoDhu63WMgzoQS34Pm+T0S5j/b9Zbkrx4SE8YOv2huou6Q==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@@ -13559,27 +13258,6 @@
"node": ">= 0.4"
}
},
- "node_modules/sha.js": {
- "version": "2.4.12",
- "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
- "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
- "dev": true,
- "license": "(MIT AND BSD-3-Clause)",
- "dependencies": {
- "inherits": "^2.0.4",
- "safe-buffer": "^5.2.1",
- "to-buffer": "^1.2.0"
- },
- "bin": {
- "sha.js": "bin.js"
- },
- "engines": {
- "node": ">= 0.10"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/sha1": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz",
@@ -14227,21 +13905,6 @@
"node": ">=14.0.0"
}
},
- "node_modules/to-buffer": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
- "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "isarray": "^2.0.5",
- "safe-buffer": "^5.2.1",
- "typed-array-buffer": "^1.0.3"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -14255,13 +13918,6 @@
"node": ">=8.0"
}
},
- "node_modules/toml": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
- "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -14756,13 +14412,6 @@
"punycode": "^2.1.0"
}
},
- "node_modules/urijs": {
- "version": "1.19.11",
- "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz",
- "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/utf-8-validate": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz",
diff --git a/package.json b/package.json
index 920db43..ab488ca 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,6 @@
"test:e2e": "playwright test"
},
"dependencies": {
- "@albedo-link/intent": "^0.13.0",
"@creit.tech/stellar-wallets-kit": "^2.5.0",
"@radix-ui/react-tooltip": "^1.2.16",
"@stellar/freighter-api": "^6.0.1",
@@ -52,7 +51,6 @@
"@commitlint/config-conventional": "^19.8.0",
"@commitlint/types": "^19.8.0",
"@playwright/test": "^1.62.0",
- "@subql/types-stellar": "^5.2.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/pdfkit": "^0.17.6",
@@ -63,7 +61,6 @@
"cross-env": "^10.1.0",
"eslint": "^9",
"eslint-config-next": "16.2.3",
- "ethers": "^6.17.0",
"husky": "^9.1.7",
"tailwindcss": "^4",
"tsx": "^4.19.2",
diff --git a/project.yaml b/project.yaml
deleted file mode 100644
index 3673513..0000000
--- a/project.yaml
+++ /dev/null
@@ -1,61 +0,0 @@
-# SubQuery Manifest File for Soroban Smart Contracts
-# See docker-compose.subquery.yml for Docker-based local development.
-
-specVersion: 1.0.0
-name: trustlend-soroban-indexer
-version: 0.1.0
-runner:
- node:
- name: '@subql/node-stellar'
- version: '*'
- query:
- name: '@subql/query'
- version: '*'
-description: >-
- Indexer to capture deposits, borrows, and liquidations from TrustLend Soroban contracts
-repository: 'https://github.com/thisisouvik/trustlend-stellar'
-schema:
- file: ./schema.graphql
-
-network:
- # Stellar Testnet passphrase and RPC endpoint
- chainId: 'Test SDF Network ; September 2015'
- endpoint: 'https://soroban-testnet.stellar.org'
- # Genesis ledger to start indexing from
- genesisHash: '0000000000000000000000000000000000000000000000000000000000000000'
-
-dataSources:
- # ── Lending Contract ────────────────────────────────────────────────────
- - kind: stellar/Soroban
- startBlock: 12345678 # TODO: Replace with actual lending contract deployment ledger height
- mapping:
- file: ./dist/index.js
- handlers:
- - handler: handleLendingEvent
- kind: stellar/SorobanEventHandler
- filter:
- contractId: "NEXT_PUBLIC_LENDING_CONTRACT_ID"
- topics:
- - "loan"
-
- # ── Reputation Contract ─────────────────────────────────────────────────
- - kind: stellar/Soroban
- startBlock: 12345678 # TODO: Replace with actual reputation contract deployment ledger height
- mapping:
- file: ./dist/index.js
- handlers:
- - handler: handleReputationEvent
- kind: stellar/SorobanEventHandler
- filter:
- contractId: "NEXT_PUBLIC_REPUTATION_CONTRACT_ID"
-
- # ── Escrow Contract ─────────────────────────────────────────────────────
- - kind: stellar/Soroban
- startBlock: 12345678 # TODO: Replace with actual escrow contract deployment ledger height
- mapping:
- file: ./dist/index.js
- handlers:
- - handler: handleEscrowEvent
- kind: stellar/SorobanEventHandler
- filter:
- contractId: "NEXT_PUBLIC_ESCROW_CONTRACT_ID"
diff --git a/schema.graphql b/schema.graphql
deleted file mode 100644
index c156272..0000000
--- a/schema.graphql
+++ /dev/null
@@ -1,35 +0,0 @@
-# SubQuery Schema definition for TrustLend events
-# TODO (SubQuery Indexer Migration):
-# Define schema mappings for Deposit, Borrow, and Liquidation events.
-
-type Deposit @entity {
- id: ID! # Transaction Hash + Event Index
- poolId: String! @index
- lenderAddress: String! @index
- amount: BigInt!
- ledgerHeight: Int!
- timestamp: Date!
-}
-
-type Borrow @entity {
- id: ID! # Transaction Hash + Event Index
- loanId: String! @index
- borrowerAddress: String! @index
- amount: BigInt!
- durationDays: Int!
- interestRateBps: Int!
- totalDue: BigInt!
- ledgerHeight: Int!
- timestamp: Date!
-}
-
-type Liquidation @entity {
- id: ID! # Transaction Hash + Event Index
- loanId: String! @index
- borrowerAddress: String! @index
- liquidatorAddress: String!
- repaidAmount: BigInt!
- collateralSeized: BigInt!
- ledgerHeight: Int!
- timestamp: Date!
-}
diff --git a/scripts/backup.sh b/scripts/backup.sh
index 8cbf037..27ef952 100644
--- a/scripts/backup.sh
+++ b/scripts/backup.sh
@@ -6,7 +6,7 @@
# AES-256 before it ever leaves the machine, uploads it to S3, verifies the
# uploaded object, and prunes backups past the retention window.
#
-# Restore instructions live in DISASTER_RECOVERY.md. A backup nobody has
+# Restore instructions live in docs/disaster-recovery.md. A backup nobody has
# restored is a hypothesis, not a backup — please run the quarterly drill.
#
# Usage:
@@ -35,7 +35,7 @@ BACKUP_S3_PREFIX="${BACKUP_S3_PREFIX:-backups}"
BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-30}"
BACKUP_DRY_RUN="${BACKUP_DRY_RUN:-0}"
# Comma-separated schemas to skip. Empty by default: a backup missing data is
-# worse than one carrying extra. See DISASTER_RECOVERY.md for Supabase notes.
+# worse than one carrying extra. See docs/disaster-recovery.md for Supabase notes.
BACKUP_EXCLUDE_SCHEMAS="${BACKUP_EXCLUDE_SCHEMAS:-}"
# Minimum plausible dump size; guards against silently archiving an empty file.
BACKUP_MIN_BYTES="${BACKUP_MIN_BYTES:-1024}"
@@ -46,7 +46,7 @@ fail() { printf '%s ERROR: %s\n' "[$(date -u +%H:%M:%S)]" "$*" >&2; exit 1; }
require_env() {
local name="$1"
if [[ -z "${!name:-}" ]]; then
- fail "$name is not set. See .env.example and DISASTER_RECOVERY.md."
+ fail "$name is not set. See .env.example and docs/disaster-recovery.md."
fi
}
@@ -168,7 +168,7 @@ log " Uploaded and verified (${remote_bytes} bytes)."
# ── 6. Retention ──────────────────────────────────────────────────────────────
# An S3 lifecycle policy on the bucket is the more robust way to do this (it
-# keeps working even if this job stops running) — see DISASTER_RECOVERY.md.
+# keeps working even if this job stops running) — see docs/disaster-recovery.md.
# This prune is a self-contained backstop for buckets without one.
log "6/6 Pruning backups older than ${BACKUP_RETENTION_DAYS} days…"
# No trailing 'Z': S3 reports LastModified as "...T00:00:00+00:00", and this is a
diff --git a/scripts/liquidation-keeper.ts b/scripts/liquidation-keeper.ts
index 0fa5adb..04a853f 100644
--- a/scripts/liquidation-keeper.ts
+++ b/scripts/liquidation-keeper.ts
@@ -27,7 +27,7 @@
// npm run liquidation:keeper -- --interval=60 # background service, poll every 60s
// npm run liquidation:keeper:service # shorthand: poll every minute
// POST /api/cron/liquidation (Vercel Cron, * * * * *) — deployed worker, see
-// vercel.json + LIQUIDATION_KEEPER.md
+// vercel.json + docs/liquidation-keeper.md
//
// ── Required env ─────────────────────────────────────────────────────────────
// ADMIN_SECRET_KEY, NEXT_PUBLIC_LENDING_CONTRACT_ID,
diff --git a/scripts/price-oracle-keeper.ts b/scripts/price-oracle-keeper.ts
index ccc03a6..4331864 100644
--- a/scripts/price-oracle-keeper.ts
+++ b/scripts/price-oracle-keeper.ts
@@ -371,7 +371,7 @@ async function main(): Promise {
if (!cfg.dryRun) {
console.warn(
"[price-oracle] On-chain publishing is not configured; running in dry-run mode. " +
- "See ORACLE_PRICE_FEEDS.md for the set_asset_oracle_prices wiring.",
+ "See docs/oracle-price-feeds.md for the set_asset_oracle_prices wiring.",
);
}
diff --git a/sql/05_horizon_sync_schema.sql b/sql/05_horizon_sync_schema.sql
deleted file mode 100644
index 5da3877..0000000
--- a/sql/05_horizon_sync_schema.sql
+++ /dev/null
@@ -1,44 +0,0 @@
-create table if not exists public.horizon_sync_state (
- id text primary key,
- last_synced_ledger integer not null default 0,
- last_synced_cursor text,
- last_synced_at timestamptz,
- consecutive_failures integer not null default 0,
- last_error text,
- updated_at timestamptz not null default now()
-);
-
-create index if not exists idx_horizon_sync_state_updated_at on public.horizon_sync_state(updated_at desc);
-
-create table if not exists public.indexer_health (
- id boolean primary key default true,
- last_successful_check timestamptz,
- last_failed_check timestamptz,
- consecutive_failures integer not null default 0,
- is_degraded boolean not null default false,
- updated_at timestamptz not null default now(),
- constraint indexer_health_single_row check (id = true)
-);
-
-drop trigger if exists trg_horizon_sync_state_updated_at on public.horizon_sync_state;
-create trigger trg_horizon_sync_state_updated_at
-before update on public.horizon_sync_state
-for each row execute function public.set_updated_at();
-
-drop trigger if exists trg_indexer_health_updated_at on public.indexer_health;
-create trigger trg_indexer_health_updated_at
-before update on public.indexer_health
-for each row execute function public.set_updated_at();
-
-insert into public.indexer_health (id) values (true)
-on conflict (id) do nothing;
-
-alter table public.chain_events
- add column if not exists ledger integer;
-
-create index if not exists idx_chain_events_ledger on public.chain_events(ledger);
-
-alter table public.loans
- add column if not exists contract_loan_id text;
-
-create unique index if not exists idx_loans_contract_loan_id on public.loans(contract_loan_id);
diff --git a/subquery/Dockerfile b/subquery/Dockerfile
deleted file mode 100644
index 0d06943..0000000
--- a/subquery/Dockerfile
+++ /dev/null
@@ -1,46 +0,0 @@
-# ──────────────────────────────────────────────────────────────────────────────
-# TrustLend — SubQuery Soroban Indexer Dockerfile
-# ──────────────────────────────────────────────────────────────────────────────
-# Multi-stage build:
-# 1. Install dependencies and build the SubQuery project (codegen + compile)
-# 2. Run with the official @subql/node-stellar image
-# ──────────────────────────────────────────────────────────────────────────────
-
-# ── Stage 1: Builder ──────────────────────────────────────────────────────────
-FROM node:20-alpine AS builder
-
-WORKDIR /subquery
-
-# Install build toolchain
-RUN apk add --no-cache python3 make g++ git
-
-# Copy SubQuery project files
-COPY subquery/package.json subquery/tsconfig.json ./
-COPY subquery/src/ ./src/
-COPY project.yaml ./project.yaml
-COPY schema.graphql ./schema.graphql
-
-# Install dependencies
-RUN npm ci --ignore-scripts
-
-# Generate GraphQL types and compile TypeScript mappings
-RUN npx subql codegen
-RUN npx subql build
-
-# ── Stage 2: Runtime ──────────────────────────────────────────────────────────
-FROM onfinality/subql-node-stellar:latest AS runner
-
-LABEL org.opencontainers.image.source="https://github.com/thisisouvik/trustlend-stellar"
-LABEL org.opencontainers.image.description="TrustLend Soroban SubQuery Indexer"
-LABEL org.opencontainers.image.licenses="MIT"
-
-# Copy the built project from the builder stage
-COPY --from=builder /subquery/dist/ /app/dist/
-COPY --from=builder /subquery/project.yaml /app/project.yaml
-COPY --from=builder /subquery/schema.graphql /app/schema.graphql
-
-# The official subql-node-stellar image expects the project at /app
-WORKDIR /app
-
-# Default command — override via docker-compose or CLI
-CMD ["-f=/app", "--db-schema=trustlend"]
diff --git a/subquery/package.json b/subquery/package.json
deleted file mode 100644
index 5fe574d..0000000
--- a/subquery/package.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "name": "trustlend-soroban-indexer",
- "version": "0.1.0",
- "private": true,
- "scripts": {
- "codegen": "subql codegen",
- "build": "subql build",
- "start:docker": "docker compose -f ../docker-compose.subquery.yml pull && docker compose -f ../docker-compose.subquery.yml up --remove-orphans",
- "dev": "subql codegen && subql build && docker compose -f ../docker-compose.subquery.yml up"
- },
- "dependencies": {
- "@subql/node-stellar": "^4.0.0",
- "@subql/query": "^4.0.0",
- "@subql/types-stellar": "^4.0.0",
- "ethers": "^6.0.0",
- "reflect-metadata": "^0.2.2"
- },
- "devDependencies": {
- "@subql/cli": "^4.0.0",
- "typescript": "^5.0.0"
- }
-}
diff --git a/subquery/src/mappings/handlers.ts b/subquery/src/mappings/handlers.ts
deleted file mode 100644
index cd5d434..0000000
--- a/subquery/src/mappings/handlers.ts
+++ /dev/null
@@ -1,345 +0,0 @@
-// TrustLend — SubQuery Soroban Event Mapping Handlers
-//
-// Maps on-chain Soroban contract events to the GraphQL entities defined in
-// schema.graphql. Handles events from three contracts:
-// - Lending Contract (loans, payments, defaults)
-// - Reputation Contract (scores, oracle updates, freezes)
-// - Escrow Contract (deposits, withdrawals, transfers)
-//
-// Event topic formats follow the conventions in SOROBAN_INDEXER_MIGRATION.md.
-//
-// Amounts emitted by Soroban contracts are in stroops (1 XLM = 10_000_000 stroops).
-// The read-model layer (lib/indexer/read-model.ts) converts them to XLM for display.
-
-import type {
- SorobanEvent,
-} from "@subql/types-stellar";
-
-// ─── Type helpers ────────────────────────────────────────────────────────────
-
-/** Decode a Soroban ScVal address to a G... string. */
-function addressToString(raw: unknown): string {
- if (typeof raw === "string") return raw;
- if (raw && typeof raw === "object" && "contractId" in (raw as Record)) {
- return (raw as Record).contractId as string;
- }
- return String(raw ?? "");
-}
-
-/** Decode an i128 ScVal to a BigNumber. */
-function toBigNumber(raw: unknown): bigint {
- if (typeof raw === "bigint") return raw;
- if (typeof raw === "number") return BigInt(raw);
- if (typeof raw === "string") return BigInt(raw);
- return 0n;
-}
-
-/** Decode a u32 ScVal to a number. */
-function toNumber(raw: unknown): number {
- if (typeof raw === "number") return raw;
- if (typeof raw === "bigint") return Number(raw);
- if (typeof raw === "string") return parseInt(raw, 10);
- return Number(raw ?? 0);
-}
-
-/** Decode a symbol ScVal to a string. */
-function symbolToString(raw: unknown): string {
- if (typeof raw === "string") return raw;
- if (raw && typeof raw === "object" && "symbol" in (raw as Record)) {
- return (raw as Record).symbol as string;
- }
- return String(raw ?? "");
-}
-
-// ─── Dispatch Handlers (exported — match project.yaml handler names) ───────
-
-/**
- * Dispatch handler for all Lending contract events.
- * Registered in project.yaml as `handleLendingEvent` with filter contractId
- * = NEXT_PUBLIC_LENDING_CONTRACT_ID and topic prefix "loan".
- * Routes to sub-handlers based on the second topic element.
- */
-export async function handleLendingEvent(rawEvent: SorobanEvent): Promise {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const event = rawEvent as any;
- const { topic, data, ledger } = event;
- const eventType = symbolToString(topic[1] ?? "");
- const params = extractParams(data);
-
- switch (eventType) {
- case "request":
- await handleLoanRequested(ledger, params);
- break;
- case "approved":
- await handleLoanApproved(params);
- break;
- case "revoked":
- await handleLoanRevoked(params);
- break;
- case "active":
- await handleLoanActivated(params);
- break;
- case "payment":
- await handleLoanPayment(params);
- break;
- case "default":
- await handleLoanDefaulted(ledger, params);
- break;
- default:
- logger.warn(`[lending] Unknown event type: ${eventType}`);
- }
-}
-
-/**
- * Dispatch handler for all Reputation contract events.
- * Registered in project.yaml as `handleReputationEvent` with filter
- * contractId = NEXT_PUBLIC_REPUTATION_CONTRACT_ID.
- */
-export async function handleReputationEvent(rawEvent: SorobanEvent): Promise {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const event = rawEvent as any;
- const { topic, data } = event;
- const eventType = symbolToString(topic[0] ?? "");
- const subType = symbolToString(topic[1] ?? "");
- const params = extractParams(data);
-
- const compoundType = `${eventType}.${subType}`;
-
- switch (compoundType) {
- case "oracle.set":
- await handleOracleSet(params);
- break;
- case "oracle.score":
- await handleOracleScore(params);
- break;
- case "rep.event":
- await handleRepEvent(params);
- break;
- case "rep.totals":
- await handleRepTotals(params);
- break;
- case "rep.freeze":
- await handleRepFreeze(params);
- break;
- default:
- logger.warn(`[reputation] Unknown event type: ${compoundType}`);
- }
-}
-
-/**
- * Dispatch handler for all Escrow contract events.
- * Registered in project.yaml as `handleEscrowEvent` with filter
- * contractId = NEXT_PUBLIC_ESCROW_CONTRACT_ID.
- */
-export async function handleEscrowEvent(rawEvent: SorobanEvent): Promise {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const event = rawEvent as any;
- const { topic, data, ledger } = event;
- const eventType = symbolToString(topic[1] ?? "");
- const params = extractParams(data);
-
- switch (eventType) {
- case "deposit":
- await handleEscrowDeposit(ledger, params);
- break;
- case "withdraw":
- await handleEscrowWithdraw(params);
- break;
- case "transfer":
- await handleEscrowTransfer(params);
- break;
- default:
- logger.warn(`[escrow] Unknown event type: ${eventType}`);
- }
-}
-
-// ─── Sub-handlers: Lending ────────────────────────────────────────────────────
-
-/**
- * Handle `(loan, request)` — (loan_id, borrower, amount, duration_days,
- * interest_rate_bps, total_due, due_at)
- */
-async function handleLoanRequested(
- ledger: { sequence: number; timestamp?: number },
- params: unknown[],
-): Promise {
- const loanId = toNumber(params[0]);
- const borrower = addressToString(params[1]);
- const amount = toBigNumber(params[2]);
- const durationDays = toNumber(params[3]);
- const interestRateBps = toNumber(params[4]);
- const totalDue = toBigNumber(params[5]);
-
- await store.set(
- "Borrow",
- `${ledger.sequence}-${loanId}`,
- {
- id: `${ledger.sequence}-${loanId}`,
- loanId: loanId.toString(),
- borrowerAddress: borrower,
- amount,
- durationDays,
- interestRateBps,
- totalDue,
- ledgerHeight: ledger.sequence,
- timestamp: new Date(ledger.timestamp ?? Date.now()),
- },
- );
-
- logger.info(`[loan] Requested #${loanId} by ${borrower.slice(0, 6)}…`);
-}
-
-async function handleLoanApproved(params: unknown[]): Promise {
- const loanId = toNumber(params[0]);
- const lender = addressToString(params[1]);
- const escrowId = toNumber(params[2]);
- logger.info(`[loan] Approved #${loanId} by ${lender.slice(0, 6)}… (escrow #${escrowId})`);
-}
-
-async function handleLoanRevoked(params: unknown[]): Promise {
- const loanId = toNumber(params[0]);
- logger.info(`[loan] Revoked #${loanId}`);
-}
-
-async function handleLoanActivated(params: unknown[]): Promise {
- const loanId = toNumber(params[0]);
- logger.info(`[loan] Activated #${loanId}`);
-}
-
-async function handleLoanPayment(params: unknown[]): Promise {
- const loanId = toNumber(params[0]);
- const amount = toBigNumber(params[1]);
- const remainingDue = toBigNumber(params[2]);
- const status = symbolToString(params[3]);
- logger.info(`[loan] Payment #${loanId}: ${amount.toString()} stroops, status=${status}`);
-}
-
-async function handleLoanDefaulted(
- ledger: { sequence: number; timestamp?: number },
- params: unknown[],
-): Promise {
- const loanId = toNumber(params[0]);
-
- await store.set(
- "Liquidation",
- `liq-${ledger.sequence}-${loanId}`,
- {
- id: `liq-${ledger.sequence}-${loanId}`,
- loanId: loanId.toString(),
- borrowerAddress: "",
- liquidatorAddress: "",
- repaidAmount: 0n,
- collateralSeized: 0n,
- ledgerHeight: ledger.sequence,
- timestamp: new Date(ledger.timestamp ?? Date.now()),
- },
- );
-
- logger.info(`[loan] Defaulted #${loanId}`);
-}
-
-// ─── Sub-handlers: Reputation ───────────────────────────────────────────────
-
-async function handleOracleSet(params: unknown[]): Promise {
- const oracle = addressToString(params[0]);
- logger.info(`[rep] Oracle set to ${oracle.slice(0, 6)}…`);
-}
-
-async function handleOracleScore(params: unknown[]): Promise {
- const borrower = addressToString(params[0]);
- const creditScore = toNumber(params[1]);
- const boostBps = toNumber(params[2]);
- logger.info(`[rep] Oracle score for ${borrower.slice(0, 6)}…: ${creditScore} (boost ${boostBps} bps)`);
-}
-
-async function handleRepEvent(params: unknown[]): Promise {
- const borrower = addressToString(params[0]);
- const eventType = symbolToString(params[1]);
- const delta = toBigNumber(params[2]);
- const newScore = toBigNumber(params[3]);
- logger.info(`[rep] Event ${eventType} for ${borrower.slice(0, 6)}…: delta=${delta.toString()} score=${newScore.toString()}`);
-}
-
-async function handleRepTotals(params: unknown[]): Promise {
- const borrower = addressToString(params[0]);
- const borrowedDelta = toBigNumber(params[1]);
- const repaidDelta = toBigNumber(params[2]);
- logger.info(`[rep] Totals for ${borrower.slice(0, 6)}…: borrowed=${borrowedDelta.toString()} repaid=${repaidDelta.toString()}`);
-}
-
-async function handleRepFreeze(params: unknown[]): Promise {
- const borrower = addressToString(params[0]);
- const isFrozen = Boolean(params[1]);
- logger.info(`[rep] ${isFrozen ? "Froze" : "Unfroze"} ${borrower.slice(0, 6)}…`);
-}
-
-// ─── Sub-handlers: Escrow ───────────────────────────────────────────────────
-
-async function handleEscrowDeposit(
- ledger: { sequence: number; timestamp?: number },
- params: unknown[],
-): Promise {
- const lender = addressToString(params[0]);
- const loanId = toNumber(params[1]);
- const amount = toBigNumber(params[2]);
-
- await store.set(
- "Deposit",
- `dep-${ledger.sequence}-${loanId}`,
- {
- id: `dep-${ledger.sequence}-${loanId}`,
- poolId: loanId.toString(),
- lenderAddress: lender,
- amount,
- ledgerHeight: ledger.sequence,
- timestamp: new Date(ledger.timestamp ?? Date.now()),
- },
- );
-
- logger.info(`[escrow] Deposit loan #${loanId} by ${lender.slice(0, 6)}…: ${amount.toString()} stroops`);
-}
-
-async function handleEscrowWithdraw(params: unknown[]): Promise {
- const lender = addressToString(params[0]);
- const loanId = toNumber(params[1]);
- const amount = toBigNumber(params[2]);
- logger.info(`[escrow] Withdraw loan #${loanId} by ${lender.slice(0, 6)}…: ${amount.toString()} stroops`);
-}
-
-async function handleEscrowTransfer(params: unknown[]): Promise {
- const escrowId = toNumber(params[0]);
- const loanId = toNumber(params[1]);
- const borrower = addressToString(params[2]);
- const amount = toBigNumber(params[3]);
- logger.info(`[escrow] Transfer escrow #${escrowId} loan #${loanId} to ${borrower.slice(0, 6)}…: ${amount.toString()} stroops`);
-}
-
-// ─── Helpers ──────────────────────────────────────────────────────────────────
-
-/**
- * Extract the decoded ScVal parameters from a Soroban event's data payload.
- * Soroban events emit a Vec; each element in the array is one parameter.
- */
-function extractParams(data: unknown): unknown[] {
- if (Array.isArray(data)) return data;
- if (data && typeof data === "object") {
- const record = data as Record;
- // Some indexer SDKs wrap the params in a `values` or `data` field
- if (Array.isArray(record.values)) return record.values;
- if (Array.isArray(record.data)) return record.data;
- }
- return [];
-}
-
-// ─── Re-export store and logger from SubQuery runtime ─────────────────────────
-// These are injected by the SubQuery node at runtime.
-interface Store {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- set(entity: string, id: string, data: any): Promise;
-}
-declare const store: Store;
-declare const logger: {
- info: (msg: string) => void;
- warn: (msg: string) => void;
- error: (msg: string) => void;
-};
diff --git a/subquery/tsconfig.json b/subquery/tsconfig.json
deleted file mode 100644
index 313e707..0000000
--- a/subquery/tsconfig.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "compilerOptions": {
- "target": "es2020",
- "module": "commonjs",
- "rootDir": "src",
- "outDir": "dist",
- "strict": true,
- "esModuleInterop": true,
- "skipLibCheck": true,
- "forceConsistentCasingInFileNames": true,
- "resolveJsonModule": true,
- "declaration": true,
- "declarationMap": true,
- "sourceMap": true
- },
- "include": ["src/**/*"],
- "exclude": ["node_modules", "dist"]
-}
diff --git a/vitest.config.ts b/vitest.config.ts
index 8d63e63..5ea8eea 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -5,6 +5,8 @@ export default defineConfig({
test: {
environment: "node",
globals: true,
+ // Playwright specs live in e2e/ and must not be collected by vitest.
+ exclude: ["**/node_modules/**", "**/.next/**", "e2e/**", "contracts/**"],
},
resolve: {
alias: {