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
135 changes: 135 additions & 0 deletions docs/GRAPHQL_API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# GraphQL API Design

> Status: design only. This workspace does not have a GraphQL runtime installed, so this SDL is not currently served or executable. Implementing it requires an approved GraphQL server dependency.

## Endpoint and transport

The proposed endpoint is `POST /api/graphql`. Queries and mutations use JSON request/response bodies. Subscriptions use GraphQL over Server-Sent Events (SSE), allowing the same HTTP route to stream `InvoiceUpdate` events. Authenticate with a signed StellarSplit bearer API key. Apply the existing API-key scope policy: read scope for queries and subscriptions, write scope for mutations.

## Schema

```graphql
scalar BigInt
scalar DateTime

type Query {
invoice(id: ID!): Invoice
invoices(
publicKey: String!
first: Int = 20
after: String
search: String
): InvoiceConnection!
}

type Mutation {
"Refresh on-chain state and publish an update; does not submit a transaction."
refreshInvoice(id: ID!): Invoice!
}

type Subscription {
invoiceUpdated(id: ID!): InvoiceUpdate!
}

type InvoiceConnection {
nodes: [Invoice!]!
pageInfo: PageInfo!
}

type PageInfo {
endCursor: String
hasNextPage: Boolean!
}

type InvoiceUpdate {
invoice: Invoice!
reason: InvoiceUpdateReason!
updatedAt: DateTime!
}

enum InvoiceUpdateReason {
REFRESHED
PAYMENT_CONFIRMED
}

type Invoice {
id: ID!
status: String!
creator: String!
funded: BigInt!
deadline: DateTime!
recipients: [Recipient!]!
payments(first: Int = 10, after: String): PaymentConnection!
}

type Recipient {
address: String!
amount: BigInt!
}

type PaymentConnection {
nodes: [Payment!]!
pageInfo: PageInfo!
}

type Payment {
payer: String!
amount: BigInt!
timestamp: DateTime
}
```

Invoice and payment `BigInt` values should serialize as decimal strings to avoid precision loss. Invoice IDs and cursors must be validated as positive safe integers; `first` is limited to 1-50. Stellar public keys must be validated before any RPC scan.

## Operation examples

```graphql
query AccountInvoices($publicKey: String!, $after: String) {
invoices(publicKey: $publicKey, first: 20, after: $after) {
nodes { id status funded deadline }
pageInfo { endCursor hasNextPage }
}
}
```

```graphql
mutation Refresh($id: ID!) {
refreshInvoice(id: $id) { id status funded }
}
```

```graphql
subscription InvoiceChanges($id: ID!) {
invoiceUpdated(id: $id) {
reason
updatedAt
invoice { id status funded }
}
}
```

## Resolver boundaries

- `Query.invoice` reads through the existing `splitClient.getInvoice` path and returns `null` for a missing ID.
- `Query.invoices` delegates to `fetchInvoicesForAddress`, preserving the existing cursor scan and ownership semantics.
- `Invoice.payments` slices the already-loaded invoice payment list, avoiding an additional chain read.
- `Mutation.refreshInvoice` performs a fresh chain read, invalidates the invoice cache, and publishes an update event. It never signs or submits a blockchain transaction.
- `Subscription.invoiceUpdated` filters events by invoice ID and removes its listener when the client disconnects.
- The payment-confirmed flow should publish `PAYMENT_CONFIRMED` only after verifying the transaction on-chain; webhook-supplied invoice/payment payloads alone are not authoritative.

## Caching and query optimization

- Reuse the existing process-local blockchain TTL cache for invoice reads, with a short invoice TTL and explicit invalidation in `refreshInvoice`.
- Create a request-scoped DataLoader for invoice IDs to coalesce duplicate field lookups and preserve request-level authorization context.
- Use the SDK's already-loaded invoice object for nested recipient/payment fields instead of fetching those fields independently.
- Bound `first` to 50, cap GraphQL document size and depth, and enforce a maximum query complexity score before resolver execution. Reject over-budget queries with a structured GraphQL error.
- Do not persist-cache responses containing authorization-sensitive data. Any shared cache must include the normalized API-key identity and query variables in its key.
- Process-local cache and pub/sub do not cross serverless instances. Production deployments requiring cross-instance freshness need shared cache and pub/sub infrastructure; SSE connections must be hosted on an adapter that supports long-lived streams.

## Response and errors

Use standard GraphQL `data` and `errors` fields. Include stable error extensions such as `code` (`BAD_USER_INPUT`, `UNAUTHENTICATED`, `FORBIDDEN`, `NOT_FOUND`, `RATE_LIMITED`, `UPSTREAM_ERROR`) and a request ID. Avoid returning raw SDK, RPC, or credential details to clients.

## Enabling implementation

After approving a GraphQL runtime dependency, implement the SDL with an executable schema, add the `/api/graphql` route with POST and SSE handling, wire query/mutation/subscription resolvers to the existing invoice services, add scope enforcement and request complexity limits, then publish this schema in the interactive API documentation. Do not expose blockchain-writing mutations unless the client supplies a wallet-signed transaction that is independently validated before submission.
90 changes: 90 additions & 0 deletions docs/REST_API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# StellarSplit REST API

The versioned REST API is rooted at `/api/v1`. Version 1 provides authenticated invoice and payment-history reads. Invoice creation, funding, release, and refund are Stellar contract operations and must be prepared and signed by the user's wallet through the StellarSplit SDK; the REST server never holds or impersonates wallet keys.

The interactive reference is available at `/api-docs`, with its machine-readable OpenAPI 3.1 contract at `/openapi.json`.

## Authentication

Send a signed StellarSplit API key as a bearer token. Keys can be created in the app's API key settings.

```http
Authorization: Bearer sk_read_<id>.<signature>
```

Both read- and write-scoped keys can access v1 read endpoints. Missing or invalid keys receive `401 UNAUTHORIZED`. In production, set `API_KEY_SIGNING_SECRET` to a high-entropy secret; API authentication returns `503 CONFIGURATION_ERROR` rather than accepting keys when it is unset.

## Endpoints

| Method | Path | Description |
| --- | --- | --- |
| `GET` | `/api/v1/invoices` | List invoices created by or addressed to a Stellar account |
| `GET` | `/api/v1/invoices/{id}` | Get one invoice by its contract ID |
| `GET` | `/api/v1/invoices/{id}/history` | Get paginated payments for an invoice |

### List invoices

```http
GET /api/v1/invoices?publicKey=G...&limit=20
```

`publicKey` is required and must be a Stellar public key. Optional parameters are `cursor` (exclusive invoice-ID cursor), `limit` (1-50, default 20), and `q` (prefix search on title or memo, at most 100 characters). The response contains `data.invoices` and `data.nextCursor`; pass the returned cursor to fetch the next page.

### Get an invoice

```http
GET /api/v1/invoices/25
```

Invoice IDs must be positive decimal integers. Missing invoices return `404 NOT_FOUND`.

### Get payment history

```http
GET /api/v1/invoices/25/history?cursor=0&limit=10
```

`cursor` is a zero-based payment offset (default 0). `limit` is 1-50 (default 10). The response contains `data.payments` and `data.nextCursor`, which is `null` when the history is exhausted.

## Responses

Successful responses use a shared envelope. SDK `bigint` values, including funded and payment amounts, are serialized as base-10 strings to preserve precision.

```json
{
"data": { "invoices": [], "nextCursor": null },
"meta": { "apiVersion": "v1", "requestId": "1b18a217-d138-4ea2-a44a-1ec09e289a53" }
}
```

Errors include a stable code, message, optional validation details, and request ID:

```json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "One or more query parameters are invalid.",
"details": { "fieldErrors": { "publicKey": ["Must be a valid Stellar public key"] }, "formErrors": [] },
"requestId": "1b18a217-d138-4ea2-a44a-1ec09e289a53"
},
"meta": { "apiVersion": "v1", "requestId": "1b18a217-d138-4ea2-a44a-1ec09e289a53" }
}
```

Error codes: `VALIDATION_ERROR` (400), `UNAUTHORIZED` (401), `NOT_FOUND` (404), `RATE_LIMITED` (429), `UPSTREAM_ERROR` (502), and `CONFIGURATION_ERROR` (503).

## Rate limiting

The default limit is 120 requests per API key per 60-second window. Responses include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. A `429` also includes `Retry-After` in seconds. Counters are currently in-memory and process-local; multi-instance deployments need a shared rate-limit store (for example Redis) for a global quota.

## Versioning

The major API version is in the URL (`/api/v1`). Breaking changes require a new path such as `/api/v2`; compatible additions may be made within v1. The response includes `meta.apiVersion` and the `X-API-Version` header.

## Example

```bash
curl 'https://your-stellarsplit-host/api/v1/invoices?publicKey=G...&limit=20' \
-H 'Authorization: Bearer sk_read_<id>.<signature>' \
-H 'Accept: application/json'
```
Loading