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
152 changes: 147 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,88 @@ Part of the LiquiFact stack: **frontend** (Next.js) | **backend** (this repo) |

---

## Environment Configuration

All environment variables are **validated at startup**. The application will fail immediately with actionable error messages if required or invalid variables are detected.

### Validation Behavior

- **Required Variables**: None are strictly required; all have secure defaults.
- **Fast Failure**: Invalid configurations are caught at startup before the server starts.
- **Security**: Sensitive values (database URLs, credentials) are **never logged** in error messages.
- **Type Safety**: Environment variables are coerced to correct types (number, boolean, URL) and validated.

### Environment Variables

| Variable | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
| `PORT` | number | No | `3001` | HTTP server port (1-65535) |
| `NODE_ENV` | string | No | `development` | Runtime environment (development, production, test) |
| `STELLAR_NETWORK` | string | No | `testnet` | Stellar network (testnet, public) |
| `HORIZON_URL` | url | No | `https://horizon-testnet.stellar.org` | Horizon API base URL |
| `SOROBAN_RPC_URL` | url | No | `https://soroban-testnet.stellar.org` | Soroban RPC endpoint URL |
| `DATABASE_URL` | url | No | null | PostgreSQL database connection URL (optional) |
| `REDIS_URL` | url | No | null | Redis cache connection URL (optional) |

### Configuration Examples

**Development (local)**
```bash
PORT=3001
NODE_ENV=development
STELLAR_NETWORK=testnet
HORIZON_URL=http://localhost:11626
SOROBAN_RPC_URL=http://localhost:8000
```

**Production**
```bash
PORT=8080
NODE_ENV=production
STELLAR_NETWORK=public
HORIZON_URL=https://horizon.stellar.org
SOROBAN_RPC_URL=https://soroban.stellar.org
DATABASE_URL=postgresql://user:[email protected]:5432/liquifact
REDIS_URL=redis://:[email protected]:6379
```

### Validation Rules

| Variable | Rules |
|----------|-------|
| `PORT` | Must be an integer between 1 and 65535 |
| `NODE_ENV` | Must be one of: `development`, `production`, `test` |
| `STELLAR_NETWORK` | Must be one of: `testnet`, `public` |
| `HORIZON_URL` | Must be a valid HTTP/HTTPS URL |
| `SOROBAN_RPC_URL` | Must be a valid HTTP/HTTPS URL |
| `DATABASE_URL` | Must be a valid database URL (postgresql, mysql, mongodb) |
| `REDIS_URL` | Must be a valid Redis URL |

### Error Examples

If configuration is invalid, the service will fail at startup with actionable messages:

```
Environment validation failed:
• Invalid type for PORT: Cannot convert to number: "abc"
• STELLAR_NETWORK must be one of: testnet, public
• HORIZON_URL must be a valid URL

Please check your environment configuration.
```

No sensitive values (passwords, URLs) are shown in error messages for security.

---

## Development

| Command | Description |
|----------------|--------------------------------|
| `npm run dev` | Start API with watch mode |
| `npm run start`| Start API (production-style) |
| `npm run lint` | Run ESLint on `src/` |
| `npm test` | Run Jest tests with coverage |

Default port: **3001**. After starting:

Expand All @@ -57,19 +132,84 @@ Default port: **3001**. After starting:
```
liquifact-backend/
├── src/
│ ├── config/
│ │ ├── env.js # Environment validation schema and loader
│ │ └── env.test.js # Comprehensive env validation tests
│ ├── services/
│ │ └── soroban.js # Contract interaction wrappers
│ ├── utils/
│ │ └── retry.js # Exponential backoff utility
│ └── index.js # Express app, routes
│ └── index.js # Express app, routes, startup validation
├── .env.example # Env template
├── eslint.config.js
└── package.json
```

---

## Resiliency & Retries
## Testing

This project maintains **95%+ test coverage** using Jest.

### Running Tests

```bash
# Run all tests with coverage report
npm test

# Run tests in watch mode
npm test -- --watch

# Run specific test file
npm test -- src/config/env.test.js
```

### Test Coverage

The test suite includes:

- **Configuration Validation** (`src/config/env.test.js`)
- Valid configurations (minimal, full, production, development)
- Type coercion and validation
- Port range validation (1-65535)
- Environment name validation
- Stellar network validation
- URL format and protocol validation
- Database URL validation (PostgreSQL, MySQL, MongoDB)
- Redis URL validation
- Error messages and security
- Edge cases (empty strings, whitespace, special characters)
- Missing and invalid variables
- Multiple validation errors

- **Retry Utility** (`src/utils/retry.test.js`)
- Success on first try
- Retry on transient failures
- Failure after retries exhausted
- Security caps validation
- Jitter and backoff calculations

### Security Notes

**Environment Validation Security**:

1. **Secure Defaults**: All variables either have safe defaults or are optional.
2. **No Credential Logging**: Database URLs, Redis URLs, and API keys are NEVER logged in error messages.
3. **Type Safety**: Values are coerced and validated to prevent injection attacks.
4. **Scope Isolation**: Environment variables are validated in isolation with no cross-script dependencies.
5. **Stderr Output**: Validation errors are sent to stderr, not stdout, preventing accidental logging to access logs.
6. **Fast Failure**: Invalid configurations are caught at startup, preventing degraded operation.
7. **Bounded Retries**: The retry utility has hard-capped retry counts and delays to prevent resource exhaustion.

**Best Practices**:

- Use strong, unique passwords in database/cache URLs.
- Rotate credentials regularly.
- Never commit `.env` files to version control.
- Use a secrets manager (AWS Secrets Manager, HashiCorp Vault) in production.
- Log validation errors to a secure audit trail, not to standard logs.

---

To ensure reliable communication with Soroban contract provider APIs, this backend implements a robust **Retry and Backoff** mechanism (`src/utils/retry.js`).

Expand Down Expand Up @@ -103,9 +243,11 @@ Ensure your branch passes these before opening a PR.
4. **Make changes**. Keep the style consistent:
- Run `npm run lint` and fix any issues.
- Use the existing Express/route patterns in `src/index.js`.
5. **Commit** with clear messages (e.g. `feat: add X`, `fix: Y`).
6. **Push** to your fork and open a **Pull Request** to `main`.
7. Wait for CI to pass and address any review feedback.
- Add or update tests as needed (see Testing section).
5. **Run tests**: `npm test` to ensure all tests pass and coverage remains at 95%+.
6. **Commit** with clear messages (e.g. `feat: add X`, `fix: Y`).
7. **Push** to your fork and open a **Pull Request** to `main`.
8. Wait for CI to pass and address any review feedback.

We welcome docs improvements, bug fixes, and new API endpoints aligned with the LiquiFact product (invoices, escrow, Stellar integration).

Expand Down
121 changes: 116 additions & 5 deletions coverage/clover.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<coverage generated="1774299285700" clover="3.2.0">
<project timestamp="1774299285700" name="All files">
<metrics statements="27" coveredstatements="27" conditionals="12" coveredconditionals="11" methods="6" coveredmethods="6" elements="45" coveredelements="44" complexity="0" loc="27" ncloc="27" packages="2" files="2" classes="2"/>
<coverage generated="1774413718577" clover="3.2.0">
<project timestamp="1774413718577" name="All files">
<metrics statements="132" coveredstatements="132" conditionals="86" coveredconditionals="78" methods="22" coveredmethods="22" elements="240" coveredelements="232" complexity="0" loc="132" ncloc="132" packages="3" files="3" classes="3"/>
<package name="config">
<metrics statements="105" coveredstatements="105" conditionals="74" coveredconditionals="67" methods="16" coveredmethods="16"/>
<file name="env.js" path="/Users/mac/Documents/Drip/Liquifact-backend/src/config/env.js">
<metrics statements="105" coveredstatements="105" conditionals="74" coveredconditionals="67" methods="16" coveredmethods="16"/>
<line num="14" count="1" type="stmt"/>
<line num="25" count="1" type="stmt"/>
<line num="37" count="1" type="stmt"/>
<line num="43" count="58" type="cond" truecount="4" falsecount="0"/>
<line num="44" count="5" type="stmt"/>
<line num="46" count="53" type="stmt"/>
<line num="54" count="59" type="stmt"/>
<line num="55" count="59" type="cond" truecount="2" falsecount="0"/>
<line num="56" count="3" type="stmt"/>
<line num="62" count="56" type="stmt"/>
<line num="70" count="59" type="stmt"/>
<line num="71" count="59" type="cond" truecount="2" falsecount="0"/>
<line num="72" count="2" type="stmt"/>
<line num="78" count="57" type="stmt"/>
<line num="86" count="61" type="stmt"/>
<line num="87" count="61" type="stmt"/>
<line num="88" count="60" type="cond" truecount="2" falsecount="0"/>
<line num="89" count="1" type="stmt"/>
<line num="95" count="59" type="stmt"/>
<line num="97" count="1" type="stmt"/>
<line num="110" count="61" type="stmt"/>
<line num="111" count="61" type="stmt"/>
<line num="112" count="60" type="cond" truecount="2" falsecount="0"/>
<line num="113" count="1" type="stmt"/>
<line num="119" count="59" type="stmt"/>
<line num="121" count="1" type="stmt"/>
<line num="134" count="11" type="cond" truecount="1" falsecount="1"/>
<line num="135" count="11" type="stmt"/>
<line num="136" count="11" type="stmt"/>
<line num="138" count="9" type="stmt"/>
<line num="139" count="9" type="cond" truecount="2" falsecount="0"/>
<line num="140" count="1" type="stmt"/>
<line num="146" count="8" type="stmt"/>
<line num="148" count="2" type="stmt"/>
<line num="161" count="7" type="cond" truecount="1" falsecount="1"/>
<line num="162" count="7" type="stmt"/>
<line num="163" count="7" type="stmt"/>
<line num="164" count="6" type="cond" truecount="2" falsecount="0"/>
<line num="165" count="1" type="stmt"/>
<line num="171" count="5" type="stmt"/>
<line num="173" count="1" type="stmt"/>
<line num="192" count="349" type="cond" truecount="5" falsecount="0"/>
<line num="193" count="3" type="stmt"/>
<line num="196" count="346" type="cond" truecount="5" falsecount="0"/>
<line num="198" count="63" type="stmt"/>
<line num="199" count="63" type="cond" truecount="2" falsecount="0"/>
<line num="200" count="5" type="stmt"/>
<line num="202" count="58" type="stmt"/>
<line num="205" count="18" type="cond" truecount="2" falsecount="0"/>
<line num="206" count="9" type="stmt"/>
<line num="208" count="9" type="cond" truecount="2" falsecount="0"/>
<line num="209" count="8" type="stmt"/>
<line num="211" count="1" type="stmt"/>
<line num="215" count="145" type="stmt"/>
<line num="216" count="145" type="cond" truecount="2" falsecount="0"/>
<line num="217" count="3" type="stmt"/>
<line num="219" count="142" type="stmt"/>
<line num="222" count="119" type="stmt"/>
<line num="225" count="1" type="stmt"/>
<line num="239" count="438" type="cond" truecount="5" falsecount="1"/>
<line num="240" count="1" type="stmt"/>
<line num="248" count="437" type="cond" truecount="5" falsecount="0"/>
<line num="249" count="115" type="cond" truecount="4" falsecount="0"/>
<line num="250" count="11" type="stmt"/>
<line num="252" count="104" type="stmt"/>
<line num="257" count="322" type="stmt"/>
<line num="258" count="322" type="stmt"/>
<line num="260" count="4" type="stmt"/>
<line num="268" count="318" type="cond" truecount="4" falsecount="0"/>
<line num="269" count="2" type="stmt"/>
<line num="273" count="316" type="cond" truecount="3" falsecount="1"/>
<line num="274" count="316" type="stmt"/>
<line num="275" count="316" type="cond" truecount="2" falsecount="0"/>
<line num="276" count="19" type="stmt"/>
<line num="284" count="297" type="stmt"/>
<line num="301" count="62" type="stmt"/>
<line num="302" count="62" type="stmt"/>
<line num="304" count="62" type="stmt"/>
<line num="305" count="434" type="stmt"/>
<line num="306" count="434" type="stmt"/>
<line num="308" count="434" type="cond" truecount="2" falsecount="0"/>
<line num="309" count="21" type="stmt"/>
<line num="315" count="413" type="stmt"/>
<line num="320" count="62" type="cond" truecount="2" falsecount="0"/>
<line num="321" count="19" type="stmt"/>
<line num="322" count="21" type="stmt"/>
<line num="325" count="19" type="stmt"/>
<line num="328" count="19" type="stmt"/>
<line num="329" count="19" type="stmt"/>
<line num="330" count="19" type="stmt"/>
<line num="333" count="43" type="stmt"/>
<line num="342" count="1" type="stmt"/>
<line num="351" count="2" type="stmt"/>
<line num="352" count="2" type="stmt"/>
<line num="353" count="2" type="stmt"/>
<line num="355" count="2" type="stmt"/>
<line num="356" count="14" type="stmt"/>
<line num="357" count="14" type="cond" truecount="1" falsecount="1"/>
<line num="358" count="14" type="cond" truecount="2" falsecount="0"/>
<line num="359" count="14" type="stmt"/>
<line num="361" count="14" type="stmt"/>
<line num="364" count="2" type="stmt"/>
<line num="374" count="14" type="stmt"/>
<line num="384" count="14" type="cond" truecount="1" falsecount="1"/>
<line num="387" count="1" type="stmt"/>
</file>
</package>
<package name="services">
<metrics statements="7" coveredstatements="7" conditionals="3" coveredconditionals="2" methods="3" coveredmethods="3"/>
<file name="soroban.js" path="/Users/victor/Desktop/Liquifact-backend/src/services/soroban.js">
<file name="soroban.js" path="/Users/mac/Documents/Drip/Liquifact-backend/src/services/soroban.js">
<metrics statements="7" coveredstatements="7" conditionals="3" coveredconditionals="2" methods="3" coveredmethods="3"/>
<line num="1" count="1" type="stmt"/>
<line num="10" count="8" type="stmt"/>
Expand All @@ -17,7 +128,7 @@
</package>
<package name="utils">
<metrics statements="20" coveredstatements="20" conditionals="9" coveredconditionals="9" methods="3" coveredmethods="3"/>
<file name="retry.js" path="/Users/victor/Desktop/Liquifact-backend/src/utils/retry.js">
<file name="retry.js" path="/Users/mac/Documents/Drip/Liquifact-backend/src/utils/retry.js">
<metrics statements="20" coveredstatements="20" conditionals="9" coveredconditionals="9" methods="3" coveredmethods="3"/>
<line num="29" count="9" type="stmt"/>
<line num="30" count="9" type="stmt"/>
Expand Down
Loading
Loading