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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"name": "StellarLend Devnet",
"dockerComposeFile": "../docker/devnet/docker-compose.yml",
"service": "api",
"workspaceFolder": "/workspace",
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached",
"shutdownAction": "stopCompose",

"features": {
"ghcr.io/devcontainers/features/rust:1": {
"version": "latest",
"targets": "wasm32-unknown-unknown"
},
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/git:1": {}
},

"customizations": {
"vscode": {
"extensions": [
"rust-lang.rust-analyzer",
"ms-azuretools.vscode-docker",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"ms-vscode.vscode-typescript-next"
],
"settings": {
"terminal.integrated.defaultProfile.linux": "bash",
"rust-analyzer.cargo.target": "wasm32-unknown-unknown",
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
}
}
}
},

"forwardPorts": [
3000,
4000,
5432,
6379,
8000
],
"portsAttributes": {
"3000": { "label": "API", "onAutoForward": "notify" },
"4000": { "label": "Oracle", "onAutoForward": "notify" },
"5432": { "label": "PostgreSQL", "onAutoForward": "silent" },
"6379": { "label": "Redis", "onAutoForward": "silent" },
"8000": { "label": "Soroban RPC / Horizon", "onAutoForward": "notify" }
},

"postCreateCommand": "bash -c 'echo \"StellarLend Devnet ready. Run: cd docker/devnet && ./healthcheck.sh\" && which stellar > /dev/null 2>&1 || (curl -sf https://raw.githubusercontent.com/nicholasgasior/stellar-cli-install/main/install.sh | bash || echo \"Stellar CLI install skipped\")'",

"remoteUser": "node",

"remoteEnv": {
"STELLAR_NETWORK": "local",
"SOROBAN_RPC_URL": "http://soroban-devnet:8000/soroban/rpc",
"HORIZON_URL": "http://soroban-devnet:8000",
"FRIENDBOT_URL": "http://soroban-devnet:8000/friendbot",
"NETWORK_PASSPHRASE": "Standalone Network ; February 2017",
"DATABASE_URL": "postgres://stellarlend:stellarlend@postgres:5432/stellarlend",
"REDIS_URL": "redis://redis:6379"
}
}
79 changes: 79 additions & 0 deletions .github/workflows/storage-impact.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Storage Impact Analysis

on:
pull_request:
branches: [main]
paths:
- "stellar-lend/contracts/**/*.rs"

concurrency:
group: storage-impact-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read
pull-requests: write

jobs:
storage-impact:
name: Analyze Contract Storage Changes
runs-on: ubuntu-latest
steps:
- name: Checkout head commit
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
path: head

- name: Checkout base commit
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Run storage analyzer
id: analyze
run: |
chmod +x head/scripts/storage-analyzer/analyze.sh
chmod +x head/scripts/storage-analyzer/extract-storage.sh
head/scripts/storage-analyzer/analyze.sh \
"${{ github.workspace }}/base" \
"${{ github.workspace }}/head" \
> "${{ runner.temp }}/storage-report.md"
continue-on-error: true

- name: Read report
id: report
run: |
{
echo "body<<REPORT_EOF"
cat "${{ runner.temp }}/storage-report.md"
echo "REPORT_EOF"
} >> "$GITHUB_OUTPUT"

- name: Find existing comment
uses: peter-evans/find-comment@v3
id: find_comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: github-actions[bot]
body-includes: "## Storage Impact Report"

- name: Post or update PR comment
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find_comment.outputs.comment-id }}
edit-mode: replace
body: ${{ steps.report.outputs.body }}

- name: Fail on breaking changes
if: steps.analyze.outcome == 'failure'
run: |
echo "::error::Breaking storage layout changes detected. See the PR comment for details."
exit 1
2 changes: 2 additions & 0 deletions api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import configRoutes from './routes/config.routes';
import analyticsRoutes from './routes/analytics.routes';
import developerRoutes from './routes/developer.routes';
import mevRoutes from './routes/mev.routes';
import reputationRoutes from './routes/reputation.routes';

import { errorHandler } from './middleware/errorHandler';
import { idempotencyMiddleware } from './middleware/idempotency';
Expand Down Expand Up @@ -183,6 +184,7 @@ app.use('/api/verification', legacySecurityCompat, verificationRoutes);
app.use('/api/config', legacySystemCompat, configRoutes);
app.use('/api/analytics', legacySystemCompat, analyticsRoutes);
app.use('/api/mev', legacySecurityCompat, mevRoutes);
app.use('/api/reputation', reputationRoutes);

app.use(errorHandler);

Expand Down
112 changes: 112 additions & 0 deletions api/src/controllers/reputation.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Request, Response, NextFunction } from 'express';

/**
* GET /api/reputation/:address
* Fetch the reputation score and details for a given borrower address.
*/
export const getReputation = async (req: Request, res: Response, next: NextFunction) => {
try {
const { address } = req.params;

if (!address) {
return res.status(400).json({ success: false, error: 'Address parameter is required' });
}

// TODO: Integrate with Soroban contract client to call get_reputation(address)
const reputation = {
address,
total_repayments: 0,
on_time_repayments: 0,
defaults: 0,
total_borrowed: '0',
score: 0,
tier: 'Bronze',
last_activity_timestamp: 0,
};

return res.status(200).json({ success: true, reputation });
} catch (err) {
next(err);
}
};

/**
* GET /api/reputation/tiers
* List all reputation tiers and their associated benefits.
*/
export const getReputationTiers = async (_req: Request, res: Response, next: NextFunction) => {
try {
// TODO: Integrate with Soroban contract client to call get_tier_benefits for each tier
const tiers = [
{
tier: 'Bronze',
min_score: 0,
max_score: 249,
benefits: {
interest_rate_discount_bps: 0,
borrowing_limit_multiplier_bps: 10_000,
collateral_reduction_bps: 0,
},
},
{
tier: 'Silver',
min_score: 250,
max_score: 499,
benefits: {
interest_rate_discount_bps: 25,
borrowing_limit_multiplier_bps: 11_000,
collateral_reduction_bps: 100,
},
},
{
tier: 'Gold',
min_score: 500,
max_score: 749,
benefits: {
interest_rate_discount_bps: 50,
borrowing_limit_multiplier_bps: 12_500,
collateral_reduction_bps: 200,
},
},
{
tier: 'Platinum',
min_score: 750,
max_score: 1000,
benefits: {
interest_rate_discount_bps: 100,
borrowing_limit_multiplier_bps: 15_000,
collateral_reduction_bps: 300,
},
},
];

return res.status(200).json({ success: true, tiers });
} catch (err) {
next(err);
}
};

/**
* GET /api/reputation/leaderboard
* Return the top reputed borrowers, ordered by score descending.
* Accepts optional query param `limit` (default 10, max 100).
*/
export const getLeaderboard = async (req: Request, res: Response, next: NextFunction) => {
try {
const limit = Math.min(Math.max(Number(req.query.limit) || 10, 1), 100);

// TODO: Integrate with an indexer or off-chain store to query top borrowers
const leaderboard: Array<{
address: string;
score: number;
tier: string;
total_repayments: number;
on_time_repayments: number;
defaults: number;
}> = [];

return res.status(200).json({ success: true, leaderboard, total: leaderboard.length, limit });
} catch (err) {
next(err);
}
};
10 changes: 10 additions & 0 deletions api/src/routes/reputation.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Router } from 'express';
import * as reputationController from '../controllers/reputation.controller';

const router: Router = Router();

router.get('/tiers', reputationController.getReputationTiers);
router.get('/leaderboard', reputationController.getLeaderboard);
router.get('/:address', reputationController.getReputation);

export default router;
30 changes: 30 additions & 0 deletions docker/devnet/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# ---------------------------------------------------------------------------
# StellarLend Devnet -- Environment Variables
# Copy to .env and adjust as needed: cp .env.example .env
# ---------------------------------------------------------------------------

# -- Ports (host-side) -----------------------------------------------------
SOROBAN_RPC_PORT=8000
FRIENDBOT_PORT=8001
API_PORT=3000
ORACLE_PORT=4000
POSTGRES_PORT=5432
REDIS_PORT=6379

# -- API configuration -----------------------------------------------------
JWT_SECRET=devnet-jwt-secret-do-not-use-in-prod
CONTRACT_ID=PLACEHOLDER_CONTRACT_ID

# -- Soroban / Stellar network ---------------------------------------------
NETWORK_PASSPHRASE="Standalone Network ; February 2017"
SOROBAN_RPC_URL=http://localhost:8000/soroban/rpc
HORIZON_URL=http://localhost:8000
FRIENDBOT_URL=http://localhost:8000/friendbot

# -- Init script tuning ----------------------------------------------------
# Maximum seconds to wait for the devnet node to become healthy
HEALTH_TIMEOUT=120
# Number of test user accounts to create (user1 .. userN)
NUM_TEST_USERS=5
# Initial XLM funding per test account (via friendbot)
INITIAL_XLM=10000
Loading
Loading