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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 26 additions & 169 deletions context/progress-tracker.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# Progress Tracker β€” StepFi-API

Format: date, commit hash, what changed, why.
Update this file in every PR that changes functionality (not needed for
pure chore/docs commits). Direct pushes to main must also be logged here.
Update this file in every PR that changes functionality.

---
## 2026-08-25

- Commit hash: pending.
- Added a server-side `AdminGuard` for the `/admin` module tree that reads the current `users` row from Supabase on every request, instead of trusting the JWT role claim alone.
- Applied the guard to `GET /admin/audit-logs`, added explicit 401/403 responses, and logged denied admin attempts with the same audit log shape used by the audit interceptor.
- Expanded the `users.role` constraint to allow a real server-side `admin` role and updated the user profile DTO to reflect that role.
- Added unit coverage for admin, blocked admin, unauthenticated, and stale-token authorization branches.

## 2026-07-23

Expand All @@ -23,193 +28,45 @@ pure chore/docs commits). Direct pushes to main must also be logged here.

## 2026-07-18

- Added scheduled state reconciliation across indexed on-chain loan,
liquidity, reputation, and transaction state. The idempotent Cron job
resolves provisional loan IDs, repairs stale database state, backfills
missed transaction records, marks orphaned pending transactions, exports
drift metrics, and logs a structured report without making on-chain writes.
Cron is used instead of BullMQ per the API's post-Upstash architecture.
- Added scheduled state reconciliation across indexed on-chain loan, liquidity, reputation, and transaction state. The idempotent Cron job resolves provisional loan IDs, repairs stale database state, backfills missed transaction records, marks orphaned pending transactions, exports drift metrics, and logs a structured report without making on-chain writes.

## 2026-07-16

- Added wallet-bound user roles (sponsor/vendor/mentor): `role` column
migration, one-time `POST /users/me/role` (409 once set), role claim
in JWT, `RolesGuard` on vendor/liquidity endpoints (direct push,
parallel session)
- Enforced `@typescript-eslint/no-explicit-any` at error via new
`.eslintrc.js` (repo previously had a lint script but no ESLint
config); replaced all 32 explicit `any` usages in src/ with real
types (direct push, documented here per new git standards)
- Fixed liquidity overview endpoint to expose `lockedLiquidity`,
`availableLiquidity`, `totalShares`, `sharePrice` β€” the web sponsor
dashboard needed the locked vs available breakdown (commit 3961123)
- Added wallet-bound user roles (sponsor/vendor/mentor): `role` column migration, one-time `POST /users/me/role` (409 once set), role claim in JWT, `RolesGuard` on vendor/liquidity endpoints
- Enforced `@typescript-eslint/no-explicit-any` at error via new `.eslintrc.js`
- Replaced all 32 explicit `any` usages in `src/` with real types
- Fixed liquidity overview endpoint to expose `lockedLiquidity`, `availableLiquidity`, `totalShares`, `sharePrice`
- Fixed indexer cursor not persisting after the BullMQ→Cron migration
(commit c74a05f)
- Removed BullMQ entirely; all jobs now use `@Cron` or `setInterval`
(commit c9cfbd0)
- Migrated to a new Upstash Redis database after hitting the 500k
free-tier request limit (ops change, no commit)

## 2026-07-15

- Replaced BullMQ polling jobs with `@Cron` and `setInterval`, cutting
Redis usage by ~90% (commit d908c52)
- Replaced BullMQ polling jobs with `@Cron` and `setInterval`, cutting Redis usage by ~90%

## 2026-07-02

- Added Supabase keep-alive cron job to prevent free-tier project
pausing (commit 0ff9928)
- Indexer: self-healing reset of stale ledger checkpoint (commit b7f418c)
and jump-to-recent-ledger instead of incrementing by 1 (commit 4130db2)
- Fixed learner profile migration β€” quoted the reserved word
`current_role` (commit 422992b)
- Resolved npm audit vulnerabilities (commit 1bf7372)
- Repo hygiene: PR template + CODEOWNERS (b2bdfa4), issue template with
test requirements (78b76bb)
- Added Supabase keep-alive cron job to prevent free-tier project pausing
- Indexer: self-healing reset of stale ledger checkpoint and jump-to-recent-ledger instead of incrementing by 1
- Fixed learner profile migration β€” quoted the reserved word `current_role`
- Resolved npm audit vulnerabilities
- Repo hygiene: PR template + CODEOWNERS, issue template with test requirements

## 2026-06-27

- API Key authentication system for vendor service-to-service
integration (#60, commit 7524888)
- Extended learner onboarding profile endpoint β€” richer profile fields,
completion tracking (#59, commit be6ac8b)
- API Key authentication system for vendor service-to-service integration (#60, commit 7524888)
- Extended learner onboarding profile endpoint β€” richer profile fields, completion tracking (#59, commit be6ac8b)

## 2026-06-21

- Two-step repayment flow: `buildRepaymentXdr()` + submit endpoint so
wallets sign server-built XDR (#51, commit 4224bb6)
- Two-step repayment flow: `buildRepaymentXdr()` + submit endpoint so wallets sign server-built XDR (#51, commit 4224bb6)

## 2026-06-19

- Integrated Sentry for error tracking; fixed MetricsModule dependency
injection startup error (#55, commit 58848c1)
- Added `GET /api/v1/vouching/requests` for incoming vouch requests
(#54, commit cb60433)
- End-to-end loan lifecycle test with in-memory mock infrastructure
(#52, commit 5a4efa0)
- Integrated Sentry for error tracking; fixed MetricsModule dependency injection startup error (#55, commit 58848c1)
- Added `GET /api/v1/vouching/requests` for incoming vouch requests (#54, commit cb60433)
- End-to-end loan lifecycle test with in-memory mock infrastructure (#52, commit 5a4efa0)

## 2026-06-18

- Audit log for admin operations (#49, commit 760d459)
- SponsorsService: `getPool()`, `buildDepositXdr()`, `buildWithdrawXdr()`
with unit tests (#48, commit 1793032)

---

<<<<<<< Updated upstream
> Note (2026-07-16): this file previously contained StepFi-Contracts
> content copied from the wrong repo. Replaced with real StepFi-API
> history backfilled from `git log`. Entries older than 2026-06-18 are
> in git history but were never tracked here.
=======
## Completed

### Workspace Cleanup
- Removed dead code: `lp-contract` (superseded by `liquidity-pool-contract`)
- Removed empty placeholder: `adapter-trustless-contract`
- Updated `Cargo.toml` workspace members to reflect 5 active contracts
- Removed `[profile]` sections from individual contract `Cargo.toml` files (profiles belong in workspace root only)

### Renaming
- Renamed `merchant-registry-contract` β†’ `vendor-registry-contract`
- Updated all Rust source references: `merchant_registry_contract` β†’ `vendor_registry_contract`
- Updated all struct names: `MerchantRegistry*` β†’ `VendorRegistry*`
- Updated `Cargo.toml` dependency paths in `creditline-contract`

### Critical Fixes
- Added TTL constants (`PERSISTENT_TTL_THRESHOLD`, `PERSISTENT_TTL_EXTEND_TO`) to `creditline-contract/src/storage.rs`
- Added `upgrade()` function to all 5 contracts: reputation, creditline, liquidity-pool, vendor-registry, parameters
- All 5 contracts build cleanly: `cargo build` passes with zero errors (3 minor unused constant warnings β€” acceptable)

### Deployment
- Created `scripts/deploy-testnet.sh` β€” full deployment script covering all 5 contracts in correct dependency order
- Script outputs contract IDs and saves to `.env.contracts`
- StepFi-API deployed on Render βœ…
- Supabase project created, 24 migrations applied βœ…
- Upstash Redis connected βœ…
- Swagger docs live βœ…

### Documentation
- `README.md` fully rewritten as StepFi-Contracts

### CI Pipeline
- Created `.github/workflows/ci.yml` β€” runs on push/PR to `main`
- Steps: checkout β†’ setup Node 20 β†’ `npm ci` β†’ `npm run build` β†’ `npm test`
- `node_modules` cached via `actions/cache@v4` keyed on `package-lock.json` hash
- CI status badge added to `README.md` pointing at the workflow

### Vendor Approval Lifecycle
- Created database migration `20260817000001_add_vendor_status.sql` adding `status` column constrained to `pending`, `approved`, `suspended`, `rejected`, defaulting to `pending` and backfilling existing rows.
- Added `buildApproveVendorXdr` and `buildSuspendVendorXdr` methods to `VendorRegistryContractClient` and `IVendorRegistryClient` to construct unsigned Soroban transaction XDRs.
- Created `AdminGuard` to enforce allowlisted wallet access via `ADMIN_WALLETS` (401 for unauthenticated, 403 for non-admin).
- Created `AuditAction` decorator and `AuditInterceptor` for audit-logging privileged admin operations.
- Added `POST /vendors/:id/approve` and `POST /vendors/:id/suspend` endpoints returning unsigned XDRs, guarded with `JwtAuthGuard` and `AdminGuard`, decorated with full Swagger annotations and returning HTTP 409 Conflict for invalid vendor status transitions (`VENDOR_NOT_PENDING`, `VENDOR_NOT_APPROVED`).
- Integrated status updates into `TransactionStatusCheckerProcessor` to update local Supabase `vendors` status only after on-chain transaction confirmation.
### Learner Profile Auto-Creation
- Added automatic creation of `learner_profiles` records upon first sign-in in `AuthService.findOrCreateUser()`, ensuring `GET /learners/me` resolves immediately after authentication.
- Updated `auth.service.spec.ts` unit tests to cover table query and insertion handling for `learner_profiles`.


---

## In Progress

- None currently.

---

## Next Up (In Order)

1. **LoanType enum** β€” Add `LoanType::LearnerInstallment` variant to `creditline-contract/src/types.rs`
2. **Per-installment tracking** β€” Add `paid: bool` and `paid_at: u64` fields to `RepaymentInstallment` struct
3. **repay_installment()** β€” New function targeting a specific installment by index (instead of just reducing remaining balance)
4. **Learner grace period** β€” Make `grace_period_seconds` per-loan (not just global via parameters)
5. **Vouching contract** β€” New `vouching-contract` crate: `vouch()`, `revoke_vouch()`, `get_vouches()`, `get_vouch_count()`
6. **Reputation rules** β€” Update `creditline-contract` to call different reputation adjustments for `LoanType::LearnerInstallment`
7. **Testnet deployment** β€” Deploy all contracts, capture IDs, add to StepFi-API `.env`
8. **End-to-end validation** β€” Verify loan lifecycle on testnet via Stellar CLI

---

## Open Questions

- What token is used for loans β€” native XLM or a USDC anchor? (Affects token contract address in `initialize()`)
- Should the vouching contract be a standalone crate or logic added to `creditline-contract`? (Leaning toward standalone for modularity)
- What is the correct `grace_period_seconds` for learner installment loans? (Longer than standard BNPL β€” possibly 7-14 days per installment)
- Should sponsor pool deposits go through `liquidity-pool-contract` or a new `sponsor-pool-contract`?

---

## Architecture Decisions

- **5 contracts, not 6** β€” `lp-contract` was dead code, removed. `liquidity-pool-contract` is the canonical LP implementation.
- **Vendor over Merchant** β€” Renamed to reflect StepFi's learning-focused domain.
- **TTL approach** β€” Using 60-day threshold / 120-day extension constants. Off-chain indexer is responsible for bumping TTL on active loan entries.
- **Upgrade pattern** β€” All contracts have `upgrade()` gated by admin `require_auth()`. Admin address is set at `initialize()` and transferable via `set_admin()`.
- **Loan sharding** β€” 32 shards (`loan_id % 32`) in creditline-contract to distribute persistent storage keys and avoid hot-key contention.
- **Reentrancy** β€” Boolean `LOCKED` flag in instance storage. Cheaper than mutex, sufficient for Soroban's single-threaded execution model.

---

## Contract Deployment Status

| Contract | Testnet Deployed | Contract ID | Last Deployed |
|---|---|---|---|
| `reputation-contract` | ❌ No | β€” | β€” |
| `parameters-contract` | ❌ No | β€” | β€” |
| `vendor-registry-contract` | ❌ No | β€” | β€” |
| `liquidity-pool-contract` | ❌ No | β€” | β€” |
| `creditline-contract` | ❌ No | β€” | β€” |

> Update this table after running `scripts/deploy-testnet.sh`

---

## Session Notes

- Always run `cargo build` after any contract change before committing.
- Always run `cargo test` before marking any contract feature complete.
- Never modify storage key structures of a contract that has been deployed β€” it breaks existing data. Use a migration pattern or deploy a new contract.
- The `creditline-contract` depends on all other contracts β€” it must be initialized last.
- Do not add new workspace members to `Cargo.toml` without creating the full contract file structure first.
>>>>>>> Stashed changes
- SponsorsService: `getPool()`, `buildDepositXdr()`, `buildWithdrawXdr()` with unit tests (#48, commit 1793032)
31 changes: 20 additions & 11 deletions src/common/interceptors/audit.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,37 @@ import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { AUDIT_ACTION_KEY } from '../decorators/audit-action.decorator';

export function formatAuditAction(actionMeta: unknown): string {
if (!actionMeta) {
return 'unknown';
}

if (typeof actionMeta === 'string') {
return actionMeta;
}

const meta = actionMeta as { resource?: string; action?: string };
return `${meta.resource ?? ''}.${meta.action ?? ''}`;
}

@Injectable()
export class AuditInterceptor implements NestInterceptor {
private readonly logger = new Logger(AuditInterceptor.name);

constructor(private readonly reflector: Reflector) {}

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const actionMeta = this.reflector.get<any>(AUDIT_ACTION_KEY, context.getHandler());

if (!actionMeta) {
return next.handle();
}

const action =
typeof actionMeta === 'string'
? actionMeta
: `${(actionMeta as Record<string, string>).resource ?? ''}.${(actionMeta as Record<string, string>).action ?? ''}`;
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const actionMeta = this.reflector.get<unknown>(AUDIT_ACTION_KEY, context.getHandler());
const action = formatAuditAction(actionMeta);

const request = context.switchToHttp().getRequest();
const userWallet = request.user?.wallet ?? 'anonymous';
const params = request.params;

if (action === 'unknown') {
return next.handle();
}

this.logger.log({
event: 'AUDIT_ACTION_INITIATED',
action,
Expand Down
2 changes: 1 addition & 1 deletion src/database/repositories/users.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface UserPreferencesRecord {
theme: string;
}

export type UserRole = 'sponsor' | 'vendor' | 'mentor';
export type UserRole = 'sponsor' | 'vendor' | 'mentor' | 'admin';

export interface UserRecord {
id: string;
Expand Down
Loading
Loading