diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3bbf73d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,82 @@ +# AGENTS.md — @wezzcoetzee/grvt + +Deno-first TypeScript SDK for the GRVT crypto derivatives exchange. Dual-published to JSR and NPM. + +## Tech Stack + +- **Runtime**: Deno (1.40+) +- **Language**: TypeScript (strict, no `any`, explicit return types) +- **Dependencies**: `@noble/hashes`, `@paulmillr/micro-eth-signer`, `@valibot/valibot` +- **Publishing**: JSR (`npx jsr publish`) + NPM (via `dnt` build in `scripts/build_npm.ts`) + +## Module Dependency Order + +``` +types → config → transport → signing → raw → ccxt +``` + +Each module has a `mod.ts` barrel export. Root `src/mod.ts` re-exports everything flat. + +## Key Commands + +```bash +deno fmt # Format (120 char line width) +deno lint # Lint (recommended + jsr tags, custom plugins in .dev/) +deno check src/mod.ts # Type check +deno test -A # Run all tests +``` + +## Testing Conventions + +- Pattern: `Deno.test("suite", async (t) => { await t.step("case", ...) })` +- Assertions: `assertEquals`, `assertExists`, `assertRejects`, `assertThrows` from `jsr:@std/assert@1` +- HTTP tests mock `globalThis.fetch` directly +- Integration tests need `GRVT_PRIVATE_KEY` and `GRVT_API_KEY` env vars (testnet) + +## Code Conventions + +- Private fields use `#` syntax, not `private` keyword +- Top-level functions use `function` declarations, not arrow syntax +- PascalCase for types/interfaces/enums/classes; camelCase for functions/methods +- Error messages: start uppercase, no trailing period, no contractions +- Internal files prefixed with `_` (e.g., `_base.ts`, `_privateKeySigner.ts`) +- Minimize dependencies — prefer small, auditable packages +- All timestamps: unix nanoseconds as strings. All prices: 9-decimal fixed-point as strings. + +## Architecture Overview + +Two client tiers: + +- **`GrvtRawClient`** (`src/raw/`) — 1:1 API mapping, returns raw response types +- **`GrvtClient`** (`src/ccxt/`) — CCXT-style ergonomic wrapper, handles signing/markets + +Transport layer (`src/transport/`): + +- `HttpTransport` — cookie-based auth, auto-refresh +- `WebSocketTransport` — pub/sub with auto-resubscribe, 30s keepalive + +Signing (`src/signing/`): + +- EIP-712 typed data signing for orders +- Wallet-agnostic: supports viem, ethers v5/v6, or built-in `PrivateKeySigner` +- Chain IDs: PROD=325, TESTNET=326, DEV/STG=327 + +## CI/CD + +| Workflow | Trigger | Purpose | +| ------------------- | ------- | -------------------------------------- | +| `deno_fmt_lint.yml` | PR | Format + lint check | +| `deno_tests.yml` | PR | Full test suite + coverage (Coveralls) | +| `publish_jsr.yml` | Release | Publish to JSR | +| `publish_npm.yml` | Release | Build via dnt + publish to NPM | + +## Adding New Functionality + +See [CONTRIBUTING.md](CONTRIBUTING.md) for step-by-step guides on adding API methods, transport changes, and config +updates. + +## Related Docs + +- [ARCHITECTURE.md](ARCHITECTURE.md) — detailed technical architecture +- [docs/SECURITY.md](docs/SECURITY.md) — auth model and signing +- [docs/QUALITY_SCORE.md](docs/QUALITY_SCORE.md) — quality standards diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..c9cfd76 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,155 @@ +# Architecture — @wezzcoetzee/grvt + +## Overview + +TypeScript SDK providing two client tiers for the GRVT crypto derivatives exchange. Runs on Deno, cross-published to JSR +and NPM. + +## Directory Structure + +``` +src/ +├── mod.ts # Root barrel — flat re-exports from all modules +├── _base.ts # GrvtError base class +├── types/ # Enums (Currency, Kind, TimeInForce, OrderStatus, etc.) +├── config/ +│ ├── environment.ts # Per-env URLs + chain IDs (PROD=325, TESTNET=326) +│ └── endpoints.ts # REST paths, WS stream names, endpoint resolution +├── transport/ +│ ├── _base.ts # IRequestTransport / ISubscriptionTransport interfaces +│ ├── http/mod.ts # HttpTransport — REST with cookie auth +│ └── websocket/mod.ts # WebSocketTransport — pub/sub with feed builders +├── signing/ +│ ├── eip712.ts # EIP-712 domain, order types, price multiplier (1e9) +│ ├── orderSigning.ts # signOrder, buildCreateOrderPayload +│ ├── _abstractWallet.ts # Wallet union (viem, ethers v5/v6) +│ └── _privateKeySigner.ts # Built-in signer (micro-eth-signer, no viem/ethers) +├── raw/ +│ ├── client.ts # GrvtRawClient — 25+ methods, 1:1 API mapping +│ └── types.ts # All request/response interfaces +└── ccxt/ + ├── client.ts # GrvtClient — CCXT-style wrapper with signing + ├── types.ts # Order types, interval maps + └── utils.ts # parseSymbol, ns/ms conversion +``` + +## Module Graph + +``` +types ─→ config ─→ transport ─→ signing ─→ raw ─→ ccxt + │ + GrvtClient uses + GrvtRawClient + signOrder +``` + +## Export Strategy + +Each module has `mod.ts` barrel files. Root `src/mod.ts` re-exports everything into a flat namespace. Consumers can also +import from subpaths: + +```ts +import { GrvtClient } from "@wezzcoetzee/grvt"; // everything +import { GrvtRawClient } from "@wezzcoetzee/grvt/raw"; // just raw client +import { HttpTransport } from "@wezzcoetzee/grvt/transport"; +``` + +## Two-Client Design + +### GrvtRawClient (`src/raw/client.ts`) + +Thin wrapper over `IRequestTransport`. Each method maps 1:1 to a GRVT REST endpoint. Returns raw API response types with +snake_case fields. No signing, no market loading — caller manages everything. + +**Use when**: you want full control, custom transports, or only need market data. + +### GrvtClient (`src/ccxt/client.ts`) + +CCXT-style ergonomic client. Wraps `GrvtRawClient` and adds: + +- Automatic market loading and instrument resolution +- Order signing via `signOrder()` with wallet abstraction +- Symbol parsing (`BTC_USDT_Perp` → kind/base/quote) +- Convenience methods (`createLimitOrder`, `createMarketOrder`) +- Input validation with `GrvtInvalidOrder` errors + +**Use when**: you want a batteries-included trading client. + +## Transport Layer + +### HTTP (`src/transport/http/mod.ts`) + +- Implements `IRequestTransport` +- Auth: POST to `/auth/api_key/login` with API key, caches `gravity` cookie +- Auto-refreshes cookie 5 seconds before expiry +- Configurable timeout (default 10s) via `AbortSignal.any()` +- Optional custom endpoints per `GrvtEndpointType` (edge/trade/market) + +### WebSocket (`src/transport/websocket/mod.ts`) + +- Implements `ISubscriptionTransport` +- Feed builders: `buildTickerFeed`, `buildOrderbookFeed`, `buildTradeFeed`, `buildCandleFeed`, `buildAccountFeed` +- Auto-resubscribe on reconnect (configurable) +- 30-second keepalive ping +- Stream names auto-prefixed with `v1.` + +## Signing Flow + +1. Caller provides `OrderParams` with human-readable values (price as number, size as number) +2. `signOrder()` converts to fixed-point integers (price × 1e9, size × 10^baseDecimals) +3. Builds EIP-712 typed data with domain `{ name: "GRVT Exchange", version: "0", chainId }` +4. Signs via `AbstractWallet` dispatch (detects viem/ethers v5/ethers v6 by structural typing) +5. Returns `SignedOrder` with split signature (r, s, v) +6. `buildCreateOrderPayload()` converts to snake_case for the REST API + +## Wallet Abstraction + +Structural typing — no imports from viem or ethers: + +| Type | Detection | Method | +| -------------------------- | ------------------------------------------- | --------------------------------------- | +| `AbstractViemLocalAccount` | has `.address` string | `.signTypedData(params)` | +| `AbstractEthersV6Signer` | has `.signTypedData` (not `_signTypedData`) | `.signTypedData(domain, types, value)` | +| `AbstractEthersV5Signer` | has `._signTypedData` | `._signTypedData(domain, types, value)` | + +Built-in `PrivateKeySigner` uses `@paulmillr/micro-eth-signer` — zero dependency on viem/ethers. + +## Environment Configuration + +| Environment | Chain ID | Edge Domain | +| ----------- | -------- | ---------------------------- | +| PROD | 325 | `edge.grvt.io` | +| TESTNET | 326 | `edge.testnet.grvt.io` | +| DEV | 327 | `edge.dev.gravitymarkets.io` | +| STG | 327 | `edge.stg.gravitymarkets.io` | + +Each env has three endpoint types: edge (auth/graphql), trade data, and market data — each with RPC and WS URLs. + +## Error Hierarchy + +``` +GrvtError +├── TransportError +│ ├── HttpRequestError (response?, body?) +│ └── WebSocketRequestError +├── ApiRequestError (code?, status?, response?) +└── AbstractWalletError + +GrvtInvalidOrder (separate hierarchy — order validation errors) +``` + +## CI/CD Pipeline + +PRs run format check, lint, doc check, and tests with coverage (uploaded to Coveralls). + +Releases trigger dual publishing: + +- **JSR**: `npx jsr publish` +- **NPM**: `dnt` build (`scripts/build_npm.ts`) → `npm publish` with semver tag detection (alpha/beta/rc → named tags, + stable → latest) + +## Data Conventions + +- All timestamps: **unix nanoseconds as strings** (not milliseconds, not numbers) +- All prices: **9-decimal fixed-point as strings** (multiply by `PRICE_MULTIPLIER = 1_000_000_000n`) +- Contract sizes: **variable decimal fixed-point** (multiply by `10^baseDecimals`) +- Symbols: `BASE_QUOTE_Kind` format (e.g., `BTC_USDT_Perp`, `ETH_USDT_Call_20Oct23_2800`) diff --git a/docs/QUALITY_SCORE.md b/docs/QUALITY_SCORE.md new file mode 100644 index 0000000..5aa3a94 --- /dev/null +++ b/docs/QUALITY_SCORE.md @@ -0,0 +1,60 @@ +# Quality Standards + +## TypeScript Strictness + +- **No `any`**: Use explicit types everywhere. Valibot lint plugin enforces `v.unknown()` over `v.any()`. +- **Explicit return types**: `explicit-function-return-type` and `explicit-module-boundary-types` lint rules enabled. +- **JSR compatibility**: `jsr` lint tag enabled. Published types must not use slow types (opaque/inferred exports). + +## Lint Rules + +### Deno Built-in + +`recommended` + `jsr` tags plus: `eqeqeq`, `no-console`, `no-eval`, `no-throw-literal`, `no-self-compare`, +`default-param-last`, `no-inferrable-types`, `no-non-null-asserted-optional-chain`, `no-useless-rename`, +`no-sparse-arrays`. + +### Custom Plugins (`.dev/`) + +**`deno_style_lint_plugin.ts`** — Deno Style Guide enforcement: + +- `prefer-private-field` — use `#field` not `private field` +- `no-top-level-arrow-syntax` — use `function` declarations at module level +- `naming-convention` — PascalCase types, camelCase functions, CONSTANT_CASE constants +- `error-message` — uppercase start, no trailing period, no contractions + +**`valibot_lint_plugin.ts`** — Valibot schema rules: + +- `no-explicit-any` — use `v.unknown()` instead of `v.any()` + +## Formatting + +- `deno fmt` with 120-character line width +- Enforced in CI (`deno fmt --check`) + +## Testing + +- All tests run with `deno test -A` +- Coverage uploaded to Coveralls via CI +- Test pattern: nested `Deno.test` → `t.step()` for grouping +- HTTP tests mock `globalThis.fetch` directly +- Signing tests use deterministic keys for reproducible assertions + +## CI Enforcement + +Every PR must pass: + +1. `deno fmt --check` +2. `deno lint` +3. `deno check --doc` (type-checks doc examples) +4. `deno test -A --coverage` + +## Dependency Policy + +Minimize dependencies. Current production deps: + +- `@noble/hashes` — cryptographic hashing +- `@paulmillr/micro-eth-signer` — EIP-712 signing +- `@valibot/valibot` — schema validation + +All are small, auditable, single-purpose packages from trusted authors in the crypto ecosystem. diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..7851644 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,122 @@ +# Security — Auth & Signing + +## API Key Authentication + +GRVT uses cookie-based sessions, not bearer tokens. + +### Flow + +1. Client POSTs API key to `/auth/api_key/login` on the edge endpoint +2. Server returns a `gravity` cookie with an expiration timestamp +3. All subsequent authenticated requests include this cookie +4. `HttpTransport` auto-refreshes the cookie 5 seconds before expiry + +### Configuration + +```ts +const transport = new HttpTransport({ + env: GrvtEnv.TESTNET, + apiKey: "your-api-key", +}); +``` + +Authenticated endpoints (trading, account, transfers) set `requiresAuth: true` in request options. The transport handles +login transparently. + +## EIP-712 Order Signing + +Orders are signed using [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed structured data. + +### Domain + +```ts +{ + name: "GRVT Exchange", + version: "0", + chainId: CHAIN_IDS[env], // PROD=325, TESTNET=326, DEV/STG=327 + verifyingContract: "0x0000000000000000000000000000000000000000" +} +``` + +No verifying contract — the zero address is used as a placeholder. + +### Order Types + +``` +Order { + subAccountID: uint64 + isMarket: bool + timeInForce: uint8 (GTT=1, AON=2, IOC=3, FOK=4) + postOnly: bool + reduceOnly: bool + legs: OrderLeg[] + nonce: uint32 + expiration: int64 +} + +OrderLeg { + assetID: uint256 (instrument hash) + contractSize: uint64 (size × 10^baseDecimals) + limitPrice: uint64 (price × 1e9) + isBuyingContract: bool +} +``` + +### Signing Flow + +1. `signOrder()` receives human-readable `OrderParams` +2. Converts price to fixed-point (× `PRICE_MULTIPLIER` = 1e9) +3. Converts size to fixed-point (× `10^baseDecimals` from instrument metadata) +4. Generates random nonce (uint32) and expiration (default 30 days, nanosecond timestamp) +5. Signs EIP-712 typed data via wallet abstraction +6. Splits 65-byte signature into `{ r, s, v }` +7. `buildCreateOrderPayload()` converts to snake_case for the API + +## Wallet Support + +Three wallet types supported via structural typing (no library imports required): + +| Wallet | Library | Detection | +| -------------------------- | --------- | ----------------------- | +| `AbstractViemLocalAccount` | viem | has `.address` property | +| `AbstractEthersV6Signer` | ethers v6 | has `.signTypedData()` | +| `AbstractEthersV5Signer` | ethers v5 | has `._signTypedData()` | + +### Built-in PrivateKeySigner + +For standalone use without viem or ethers: + +```ts +import { PrivateKeySigner } from "@wezzcoetzee/grvt/signing"; + +const signer = new PrivateKeySigner("0x..."); +``` + +Uses `@paulmillr/micro-eth-signer` internally. Private key stored as a `#privateKey` class field (truly private, not +accessible via reflection). + +## Environment Isolation + +Chain IDs prevent cross-environment replay attacks: + +| Environment | Chain ID | +| ----------- | -------- | +| PROD | 325 | +| TESTNET | 326 | +| DEV / STG | 327 | + +An order signed for testnet (chainId=326) will be rejected by prod (chainId=325) because the EIP-712 domain differs. + +## Private Key Handling + +- `PrivateKeySigner` stores keys in `#privateKey` (ES2022 private field) +- Keys are never logged, serialized, or exposed via public API +- The `GrvtClient` accepts either a hex string (creates `PrivateKeySigner`) or an `AbstractWallet` instance +- For production use, prefer external wallet signers (viem/ethers) that integrate with hardware wallets or KMS + +## Related + +- [ARCHITECTURE.md](../ARCHITECTURE.md) — transport and signing architecture +- `src/signing/eip712.ts` — EIP-712 domain and type definitions +- `src/signing/orderSigning.ts` — signing implementation +- `src/signing/_abstractWallet.ts` — wallet type detection and dispatch