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
9 changes: 9 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Vendored agent/skill docs and long-form docs are not part of the source
# formatting contract.
/.agents
/.claude
/docs

# Generated / build output
/dist
/coverage
70 changes: 35 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ The server validates NFC payment requests against the `payment-request.v1` contr

## Tech stack

| Layer | Technology |
|-------|------------|
| Framework | NestJS 11 |
| Language | TypeScript 5.7 (strict) |
| ORM | Prisma + PostgreSQL |
| Database hosting | Supabase |
| Session auth | Supabase Auth (JWT) |
| Payment auth | WebAuthn (passkeys) |
| Blockchain | `@stellar/stellar-sdk` (Horizon + RPC) |
| Validation | `class-validator`, `class-transformer` |
| Config | `@nestjs/config` + Joi |
| API docs | `@nestjs/swagger` |
| Tests | Jest + Supertest |
| Layer | Technology |
| ---------------- | -------------------------------------- |
| Framework | NestJS 11 |
| Language | TypeScript 5.7 (strict) |
| ORM | Prisma + PostgreSQL |
| Database hosting | Supabase |
| Session auth | Supabase Auth (JWT) |
| Payment auth | WebAuthn (passkeys) |
| Blockchain | `@stellar/stellar-sdk` (Horizon + RPC) |
| Validation | `class-validator`, `class-transformer` |
| Config | `@nestjs/config` + Joi |
| API docs | `@nestjs/swagger` |
| Tests | Jest + Supertest |

## Prerequisites

Expand Down Expand Up @@ -49,19 +49,19 @@ The API is versioned under `/v1`. Swagger UI is available at `/docs` when the se

## Scripts

| Command | Description |
|---------|-------------|
| `npm run start:dev` | Start with hot reload |
| `npm run start:prod` | Run compiled build |
| `npm run build` | Compile TypeScript |
| `npm run lint` | Run ESLint |
| `npm test` | Unit tests |
| `npm run test:e2e` | End-to-end tests |
| `npm run test:cov` | Coverage report |
| `npm run prisma:generate` | Generate Prisma client |
| `npm run prisma:migrate` | Create/apply migrations |
| `npm run prisma:studio` | Open Prisma Studio |
| `npm run prisma:seed` | Seed development data |
| Command | Description |
| ------------------------- | ----------------------- |
| `npm run start:dev` | Start with hot reload |
| `npm run start:prod` | Run compiled build |
| `npm run build` | Compile TypeScript |
| `npm run lint` | Run ESLint |
| `npm test` | Unit tests |
| `npm run test:e2e` | End-to-end tests |
| `npm run test:cov` | Coverage report |
| `npm run prisma:generate` | Generate Prisma client |
| `npm run prisma:migrate` | Create/apply migrations |
| `npm run prisma:studio` | Open Prisma Studio |
| `npm run prisma:seed` | Seed development data |

## Project structure

Expand Down Expand Up @@ -91,18 +91,18 @@ ding-server/

## Documentation

| Document | Description |
|----------|-------------|
| [docs/ding-payments.md](./docs/ding-payments.md) | Product vision and UX flows |
| [docs/server-build-plan.md](./docs/server-build-plan.md) | Full server build plan (SRV tasks) |
| [docs/server-build-plan-consolidated.md](./docs/server-build-plan-consolidated.md) | Consolidated task reference |
| Document | Description |
| ---------------------------------------------------------------------------------- | ---------------------------------- |
| [docs/ding-payments.md](./docs/ding-payments.md) | Product vision and UX flows |
| [docs/server-build-plan.md](./docs/server-build-plan.md) | Full server build plan (SRV tasks) |
| [docs/server-build-plan-consolidated.md](./docs/server-build-plan-consolidated.md) | Consolidated task reference |

## Supported assets (MVP)

| Asset | Network | Notes |
|-------|---------|-------|
| XLM | Stellar testnet | Native asset |
| USDC | Stellar testnet | Issuer via `STELLAR_USDC_ISSUER` in `.env` |
| Asset | Network | Notes |
| ----- | --------------- | ------------------------------------------ |
| XLM | Stellar testnet | Native asset |
| USDC | Stellar testnet | Issuer via `STELLAR_USDC_ISSUER` in `.env` |

## Environment variables

Expand Down
165 changes: 165 additions & 0 deletions docs/payment-request.v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# payment-request.v1

`payment-request.v1` is the canonical NFC payment request contract for Ding
mobile and server flows. Producers must emit this exact shape, and consumers
must reject invalid payloads with deterministic error codes.

## Required Fields

| Field | Type | Rule |
| ----------- | ------ | ----------------------------------------------------------------------------------------------------- |
| `type` | string | Must be `payment-request`. |
| `version` | number | Must be `1`. |
| `recipient` | string | Stellar public key in `G...` StrKey form: `^G[A-Z2-7]{55}$`. |
| `asset` | string | MVP supports only `XLM` and `USDC`. |
| `amount` | string | Positive decimal string with at most 7 decimal places. |
| `timestamp` | string | ISO 8601 UTC date-time within 5 minutes of server time. |
| `expiresAt` | string | ISO 8601 UTC date-time after `timestamp`, in the future, and no more than 24 hours after `timestamp`. |

## Optional Fields

| Field | Type | Rule |
| ----------- | ------ | ----------------------------------------------------------- |
| `memo` | string | Optional user-facing memo, up to 280 characters. |
| `requestId` | string | Optional idempotency or trace identifier, 1-128 characters. |
| `metadata` | object | Optional JSON object for non-critical integration context. |

Unknown fields are rejected. Amounts are strings so NFC producers do not lose
precision through JSON number parsing.

## Valid Payload

```json
{
"type": "payment-request",
"version": 1,
"recipient": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"asset": "XLM",
"amount": "12.3456789",
"timestamp": "2026-05-29T12:00:00.000Z",
"expiresAt": "2026-05-29T12:15:00.000Z",
"memo": "Coffee",
"requestId": "req_123",
"metadata": {
"table": 7
}
}
```

## Common Invalid Payloads

Unsupported asset:

```json
{
"type": "payment-request",
"version": 1,
"recipient": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"asset": "BTC",
"amount": "12.50",
"timestamp": "2026-05-29T12:00:00.000Z",
"expiresAt": "2026-05-29T12:15:00.000Z"
}
```

Error:

```json
{
"code": "PAYMENT_REQUEST_ASSET_UNSUPPORTED",
"message": "asset must be one of: XLM, USDC.",
"field": "asset"
}
```

Invalid amount:

```json
{
"type": "payment-request",
"version": 1,
"recipient": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"asset": "USDC",
"amount": "1.12345678",
"timestamp": "2026-05-29T12:00:00.000Z",
"expiresAt": "2026-05-29T12:15:00.000Z"
}
```

Error:

```json
{
"code": "PAYMENT_REQUEST_AMOUNT_INVALID",
"message": "amount must be a positive decimal string with at most 7 decimal places.",
"field": "amount"
}
```

Expiration before timestamp:

```json
{
"type": "payment-request",
"version": 1,
"recipient": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"asset": "XLM",
"amount": "12.50",
"timestamp": "2026-05-29T12:00:00.000Z",
"expiresAt": "2026-05-29T11:59:59.000Z"
}
```

Errors:

```json
[
{
"code": "PAYMENT_REQUEST_EXPIRES_AT_OUT_OF_WINDOW",
"message": "expiresAt must be in the future.",
"field": "expiresAt"
},
{
"code": "PAYMENT_REQUEST_EXPIRES_AT_OUT_OF_WINDOW",
"message": "expiresAt must be after timestamp.",
"field": "expiresAt"
}
]
```

Invalid Stellar recipient:

```json
{
"type": "payment-request",
"version": 1,
"recipient": "not-stellar",
"asset": "XLM",
"amount": "12.50",
"timestamp": "2026-05-29T12:00:00.000Z",
"expiresAt": "2026-05-29T12:15:00.000Z"
}
```

Error:

```json
{
"code": "PAYMENT_REQUEST_RECIPIENT_INVALID",
"message": "recipient must be a Stellar public key starting with G.",
"field": "recipient"
}
```

## Versioning Strategy

`type` identifies the protocol family and `version` identifies the exact
contract. Consumers should dispatch validation by `(type, version)`.

Backward-compatible v1 changes may clarify documentation, add optional fields
that old consumers can ignore only after the unknown-field rule is intentionally
revised, or add examples without changing validation behavior.

Breaking changes require a new version, such as `version: 2`. A future
`payment-request.v2` validator should live beside v1, keep v1 tests intact, and
allow clients and servers to negotiate or route by version during migration.
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default tseslint.config(
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
"prettier/prettier": ["error", { endOfLine: "auto" }],
'prettier/prettier': ['error', { endOfLine: 'auto' }],
},
},
);
Loading
Loading