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
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Local Aegis SDK development — copy to `.env` and fill in values.
# Never commit `.env` (see .gitignore).

# Required after you deploy the Aegis contract to local Quickstart / standalone.
AEGIS_CONTRACT_ID=

# Optional overrides (defaults match Stellar Quickstart on localhost:8000)
# AEGIS_LOCAL_RPC_URL=http://localhost:8000/soroban/rpc
# AEGIS_LOCAL_NETWORK_PASSPHRASE=Standalone Network ; February 2017
# AEGIS_LOCAL_HORIZON_URL=http://localhost:8000
# AEGIS_LOCAL_FRIENDBOT_URL=http://localhost:8000/friendbot

# Optional ephemeral signer for local write calls. Prefer Keypair.random() in code.
# Never paste a testnet/mainnet secret here.
# AEGIS_LOCAL_SECRET_KEY=
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.idea
node_modules
dist
dist
.env
.env.local
.env.*.local
7 changes: 4 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ We welcome open-source contributions! As middleware, this SDK is critical for th
1. **Fork & Clone:** Fork the repo and clone it locally.
2. **Install Dependencies:** Run `npm install`.
3. **Branching:** Use `feat/`, `fix/`, or `chore/` prefixes.
4. **Testing:** You MUST write unit tests in `tests/` for any new methods added. PRs without test coverage will be rejected. For predictable SDK responses without live RPC, use the mock client from `@aegis/sdk/testing` (see `docs/testing.md`). Behavior changes should follow the [Test-First Contribution Guide](docs/test-first-contribution.md), including happy-path, negative-path, and no-test justification rules.
5. **Formatting:** Ensure `npm run lint` and `npm run format` pass before opening a PR.
6. **CI Verification:** Run `npm run check` locally to verify that build, unit tests, and runtime compatibility checks pass. PRs with failing GitHub Actions CI checks will not be reviewed or merged until all status checks are green (see [CI Pass Requirements](docs/ci-pass-requirements.md) and [CI Resolution Workflow](docs/ci-resolution-workflow.md)).
4. **Local network (optional):** For integration against Quickstart, see [Local development](docs/local-development.md) (`npm run local:up`, `createLocalClient`, `AEGIS_CONTRACT_ID`). Unit tests should keep using `@aegis/sdk/testing` mocks — do not require Docker for `npm test`.
5. **Testing:** You MUST write unit tests in `tests/` for any new methods added. PRs without test coverage will be rejected. For predictable SDK responses without live RPC, use the mock client from `@aegis/sdk/testing` (see `docs/testing.md`). Behavior changes should follow the [Test-First Contribution Guide](docs/test-first-contribution.md), including happy-path, negative-path, and no-test justification rules.
6. **Formatting:** Ensure `npm run lint` and `npm run format` pass before opening a PR.
7. **CI Verification:** Run `npm run check` locally to verify that build, unit tests, and runtime compatibility checks pass. PRs with failing GitHub Actions CI checks will not be reviewed or merged until all status checks are green (see [CI Pass Requirements](docs/ci-pass-requirements.md) and [CI Resolution Workflow](docs/ci-resolution-workflow.md)).

Search the codebase for `// TODO:` comments to find areas that need immediate help!

Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,29 @@ if (event.kind === 'transfer') {

See [Contract Event Decoder](./docs/contract-events.md) for supported topics, unknown fallback behaviour, and dashboard integration guidance.

## Local Development

Use the local-development helpers against Stellar Quickstart / standalone
instead of hand-rolling `environment: 'local'`:

```typescript
import { createLocalClient, checkLocalNetwork } from '@aegis/sdk';
import { Keypair } from '@stellar/stellar-sdk';

const client = createLocalClient({
contractId: process.env.AEGIS_CONTRACT_ID!,
env: process.env,
keypair: Keypair.random(),
});

const health = await checkLocalNetwork(client);
```

See [Local development](./docs/local-development.md) for Docker Compose
(`npm run local:up`), env vars, loopback policy, and security assumptions.
Environment presets (testnet / local / mainnet) are documented in
[Environment presets](./docs/environments.md).

## Testing
To run the SDK unit tests locally:

Expand Down
26 changes: 26 additions & 0 deletions docker-compose.local.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Local Stellar Quickstart for Aegis SDK development.
# Usage: npm run local:up
#
# After the container is healthy, deploy the Aegis contract with stellar-cli
# against the standalone network and set AEGIS_CONTRACT_ID in your .env.

services:
stellar-quickstart:
image: stellar/quickstart:latest
container_name: aegis-sdk-local-stellar
ports:
- '8000:8000'
# --local selects Networks.STANDALONE; --enable-soroban-rpc exposes RPC.
command: ['--local', '--enable-soroban-rpc']
healthcheck:
test:
[
'CMD',
'curl',
'-sf',
'http://localhost:8000/soroban/rpc',
]
interval: 10s
timeout: 5s
retries: 30
start_period: 40s
10 changes: 10 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ The entry point for interacting with the Aegis Protocol.

Either `environment` or both `rpcUrl` and `networkPassphrase` must be provided. Invalid or unsafe configuration (malformed URLs, insecure `http://` overrides outside the `local` preset, empty passphrases, unavailable environments) throws a `ConfigValidationError`.

### Local development helpers

For Quickstart / standalone workflows prefer:

* `createLocalClient(options)` / `resolveLocalConfig(options)` — typed local defaults, injected env loading, required contract ID, loopback enforcement
* `checkLocalNetwork(target)` — readiness probe (`ready` | `unavailable` | `misconfigured`)
* `LOCAL_DEV_DEFAULTS` / `LOCAL_ENV_KEYS` — shared constants

See [Local development](./local-development.md). Environment presets: [Environment Presets](./environments.md).

### Client Modules
* `client.compliance`: Whitelist & KYC verification module (`ComplianceModule`).
* `client.asset`: Minting & transferring RWA tokens module (`AssetModule`).
Expand Down
13 changes: 13 additions & 0 deletions docs/environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ Overrides are validated: malformed URLs and empty passphrases throw a `ConfigVal
Plain `http://` URLs are only accepted when `environment: 'local'` — using `http://` against
`testnet` or `mainnet` throws, since it almost always indicates a misconfigured endpoint.

## Local development workflow

For contributor machines and CI sandboxes, prefer the dedicated local helpers
over a bare `environment: 'local'` preset:

* `createLocalClient` / `resolveLocalConfig` — defaults, env loading, required contract ID
* Loopback / `host.docker.internal` enforcement for plain `http://` RPC URLs
* `checkLocalNetwork` — readiness probe with “start Docker” hints
* `npm run local:up` — optional Quickstart via `docker-compose.local.yml`

See [Local development](./local-development.md) for the full workflow, env var
table, security assumptions, and reviewer checklist.

## The `mainnet` Preset Is Gated

The Aegis protocol has not yet been audited/deployed on Stellar mainnet, so the `mainnet`
Expand Down
181 changes: 181 additions & 0 deletions docs/local-development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Local development configuration

Typed helpers for running the Aegis SDK against a **local Stellar standalone /
Quickstart** network. This is for developer machines and CI sandboxes — not a
substitute for testnet integration testing.

> **Compliance note:** Local whitelist / KYC behaviour reflects whatever the
> locally deployed Aegis contract reports. It is protocol-level test data only
> and is **not** legal, financial, or regulatory compliance advice.

## Why this exists

`environment: 'local'` alone only fills `rpcUrl` and `networkPassphrase`. Local
work also needs:

* a **deployed contract ID** (no safe default)
* **loopback enforcement** so plain `http://` cannot silently point at a remote host
* optional **env-based** loading for `.env` / CI secrets injection
* a **readiness probe** that says “start Docker” instead of “retry with backoff”

## Quickstart

```bash
# Optional: start Quickstart (Soroban RPC on :8000)
npm run local:up

# Copy env template and set your deployed contract ID
cp .env.example .env
```

```typescript
import { createLocalClient, checkLocalNetwork } from '@aegis/sdk';
import { Keypair } from '@stellar/stellar-sdk';

const client = createLocalClient({
contractId: process.env.AEGIS_CONTRACT_ID!,
// Or inject the whole map explicitly (preferred in tests / browsers):
env: process.env,
keypair: Keypair.random(), // ephemeral local signer only
});

const health = await checkLocalNetwork(client);
if (health.status !== 'ready') {
console.error(health.message, health.hint);
}
```

`resolveLocalConfig` / `createLocalClient` never read `process.env` at module
scope. Pass `env: process.env` (Node) or individual fields when you want env
overrides — this keeps the browser bundle free of Node globals.

## Defaults

| Field | Default |
| --- | --- |
| `rpcUrl` | `http://localhost:8000/soroban/rpc` |
| `networkPassphrase` | `Networks.STANDALONE` (`Standalone Network ; February 2017`) |
| `horizonUrl` | `http://localhost:8000` |
| `friendbotUrl` | `http://localhost:8000/friendbot` |
| `contractId` | **required** — no default |

Constants: `LOCAL_DEV_DEFAULTS`, `LOCAL_ENV_KEYS`.

## Environment variables

| Variable | Purpose |
| --- | --- |
| `AEGIS_CONTRACT_ID` | StrKey contract ID from your local deploy |
| `AEGIS_LOCAL_RPC_URL` | Override RPC URL |
| `AEGIS_LOCAL_NETWORK_PASSPHRASE` | Override passphrase (rarely needed) |
| `AEGIS_LOCAL_HORIZON_URL` | Horizon / Quickstart root |
| `AEGIS_LOCAL_FRIENDBOT_URL` | Friendbot URL for funding accounts |
| `AEGIS_LOCAL_SECRET_KEY` | Optional signer seed (`S...`) — ephemeral keys only |

Precedence per field: **explicit option → env → default**.

Whitespace-only values are treated as unset.

## Loopback / local-host policy

Plain `http://` RPC URLs are accepted only when the host is:

* loopback — `localhost`, `127.0.0.0/8`, `::1`, `0.0.0.0`
* or Docker Desktop’s `host.docker.internal` (SDK container → host Quickstart)

Anything else throws `LocalConfigError` with code `NON_LOOPBACK_RPC_URL` unless
you pass `allowNonLoopbackRpc: true` (trusted private networks only).

This is **enforcement**, not a soft warning. Generic
`new AegisClient({ environment: 'local', rpcUrl: 'http://remote...' })` still
goes through `resolveClientConfig` and does not apply this guard — use
`createLocalClient` / `resolveLocalConfig` for local workflows.

## Readiness probe

```typescript
const health = await checkLocalNetwork({
contractId: process.env.AEGIS_CONTRACT_ID!,
env: process.env,
});

// health.status: 'ready' | 'unavailable' | 'misconfigured'
```

| Status | Meaning | Typical hint |
| --- | --- | --- |
| `ready` | `getHealth` succeeded; passphrase matches when available | — |
| `unavailable` | Connection refused / timeout on local RPC | `npm run local:up` |
| `misconfigured` | Reachable but wrong passphrase / unexpected RPC error | Check `--local` / RPC path |

A refused connection is **not** marked retryable — restarting a stopped
container requires operator action, not backoff.

## Edge cases and failure codes

| Code | When |
| --- | --- |
| `MISSING_CONTRACT_ID` | No `contractId` option and no `AEGIS_CONTRACT_ID` |
| `INVALID_CONTRACT_ID` | Value is not a StrKey contract ID (e.g. `"C..."`) |
| `INVALID_RPC_URL` | Unparseable or non-http(s) URL |
| `NON_LOOPBACK_RPC_URL` | Insecure remote http without opt-in |
| `INVALID_SECRET_KEY` | `AEGIS_LOCAL_SECRET_KEY` / `secretKey` is not a valid seed |
| `INVALID_NETWORK_PASSPHRASE` | Empty passphrase override |
| `LOCAL_NETWORK_UNAVAILABLE` | Reserved for thrown readiness failures (probe returns status instead) |
| `LOCAL_NETWORK_MISCONFIGURED` | Reserved for thrown readiness failures (probe returns status instead) |

Errors never echo secret keys or credential-bearing URL userinfo.

## Security assumptions

1. **Local keys are disposable.** Generate with `Keypair.random()`. Never reuse
testnet/mainnet secrets in `.env`.
2. **`.env` is not committed.** See `.env.example` and `.gitignore`.
3. **HTTP is loopback-scoped** via `resolveLocalConfig`. Do not disable that
guard to point at public infrastructure.
4. **Friendbot / Horizon URLs** in defaults are public local endpoints — they
are not production funding rails.
5. **Do not `JSON.stringify` a `ResolvedLocalDevConfig` that includes a
`keypair`** into GitHub issues — Keypair objects can serialise seed bytes.
Share `checkLocalNetwork` output or redact first.

## Docker Compose

```bash
npm run local:up # docker compose -f docker-compose.local.yml up -d
npm run local:down
```

The compose file pins the documented Quickstart image and publishes port
`8000`. After it is healthy, deploy the Aegis contract with `stellar-cli`
against the standalone network, then set `AEGIS_CONTRACT_ID`.

> Image tags move. For shared CI, pin a digest in your fork’s compose override.

## Troubleshooting

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| `status: 'unavailable'` | Container not running / wrong port | `npm run local:up`, check `:8000` |
| Passphrase mismatch | Quickstart not started with `--local` | Recreate the container from `docker-compose.local.yml` |
| HTTP 404 from RPC | Path differs across Quickstart versions | Try `/rpc` vs `/soroban/rpc` via `AEGIS_LOCAL_RPC_URL` |
| Unfunded account | Need Friendbot | `GET {friendbotUrl}?addr=G...` |
| `NON_LOOPBACK_RPC_URL` | Remote http URL | Use localhost or set `allowNonLoopbackRpc: true` deliberately |

## Reviewer checklist

- [ ] Local helpers live in `src/config/local.ts` (not ad-hoc in examples).
- [ ] Loopback / `host.docker.internal` policy is enforced by default.
- [ ] Missing contract ID fails with a typed error and deploy hint.
- [ ] Secret keys never appear in error messages or returned plain fields.
- [ ] Tests cover precedence, rejection matrix, and readiness-unavailable.
- [ ] Docs state that local compliance checks are not legal advice.
- [ ] README / `docs/environments.md` link here.
- [ ] Change stays compatible with `environment: 'local'` on `AegisClient`.

## Related

* [Environment presets](./environments.md)
* [Network failures](./network-failures.md)
* [Testing utilities](./testing.md)
* [API reference](./api-reference.md)
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"test:compat": "node scripts/check-compat.mjs",
"check": "npm run build && npm test -- --runInBand && npm run test:compat",
"verify": "npm run lint && npm run format && npm run build && npm test -- --runInBand && npm run test:compat",
"local:up": "docker compose -f docker-compose.local.yml up -d",
"local:down": "docker compose -f docker-compose.local.yml down",
"lint": "eslint src --ext .ts",
"format": "prettier --write \"src/**/*.ts\"",
"prepare": "npm run build"
Expand Down
3 changes: 2 additions & 1 deletion src/config/environments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export const AEGIS_ENVIRONMENTS: Record<AegisEnvironmentName, AegisEnvironmentPr
rpcUrl: 'http://localhost:8000/soroban/rpc',
networkPassphrase: Networks.STANDALONE,
available: true,
description: 'Local standalone network (e.g. Stellar Quickstart Docker image).',
description:
'Local standalone network (e.g. Stellar Quickstart). Prefer createLocalClient / resolveLocalConfig for loopback enforcement, env loading, and readiness checks — see docs/local-development.md.',
},
mainnet: {
name: 'mainnet',
Expand Down
Loading
Loading