diff --git a/README.md b/README.md index 0433dae..76cf7b8 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ TypeScript SDK for [HashLock](https://hashlock.tech) — institutional OTC trading with HTLC atomic settlement on Ethereum and Bitcoin. +> 📐 **Architecture:** how this SDK is layered and how it connects to the Hashlock Markets +> backend — [`docs/architecture/ARCHITECTURE.md`](./docs/architecture/ARCHITECTURE.md) +> ([Русский](./docs/architecture/ARCHITECTURE.ru.md)). + ## Install ```bash diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md new file mode 100644 index 0000000..22c4f37 --- /dev/null +++ b/docs/architecture/ARCHITECTURE.md @@ -0,0 +1,303 @@ + + +# hashlock-sdk — Architecture + +> **The authoritative architecture reference for this repository.** It explains what +> every part is, how the parts connect, how a call flows from your code to the Hashlock +> Markets backend and back, and the reasoning behind the design — verified against `main` +> (counts reflect the code as of 2026-05-30). +> +> This is the **TypeScript client SDK** that wraps the Hashlock Markets GraphQL API. For +> the system it talks to, read the master doc: +> [**hashlock-markets / ARCHITECTURE.md**](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md). +> +> Every non-obvious claim points at a `path:line` you can open. If a number here ever +> disagrees with the code, the code wins — please fix the doc. + +--- + +## 1. What it is, and the core idea + +`@hashlock-tech/sdk` is a **thin, fully-typed TypeScript client** over the Hashlock Markets +GraphQL surface. It lets a program — a market-maker bot, an institutional desk integration, +an AI agent — drive the **same RFQ → quote → trade → HTLC-settlement** lifecycle that the web +app drives, without hand-writing GraphQL strings or guessing field shapes. + +The whole package is one public class, `HashLock` (`src/hashlock.ts:66`), exposing **21 +async methods** (`src/hashlock.ts`, one per GraphQL operation) plus `setAccessToken`. Every +method is a typed wrapper that holds an inline GraphQL document and delegates transport to a +private `GraphQLClient` (`src/client.ts:17`). + +### Why it is built this way (the load-bearing decisions) + +| Decision | Why | +|---|---| +| **Zero runtime dependencies** | `package.json` lists only `devDependencies` (`package.json:35-39`). The SDK uses the platform `globalThis.fetch` (`src/client.ts:29`), so it runs unchanged in Node ≥18, Deno, browsers, and edge runtimes with no dependency tree to audit. | +| **Facade over a hidden transport** | `HashLock` is the only surface consumers touch; `GraphQLClient` is **not exported** (`src/index.ts:1-10`) — retry, timeout, and auth are implementation details that can change without a breaking API. | +| **GraphQL documents inlined per method** | Each method owns its query/mutation string (e.g. `src/hashlock.ts:98-104`). There is no codegen step and no schema dependency, so the SDK ships as plain `.ts` and stays readable. The cost: field lists are maintained by hand and must track the backend SDL. | +| **Mutations never retried on 5xx** | `mutate()` passes `retryOn5xx = false` (`src/client.ts:56-61`) because trade/HTLC mutations are **not idempotent** — a retried `fundHTLC` could double-record. Only `query()` retries server errors (`src/client.ts:45-50`). | +| **Typed error hierarchy** | Callers branch on `AuthError` / `GraphQLError` / `NetworkError` (`src/errors.ts`) instead of parsing strings, so token-refresh vs. retry vs. surface-to-user logic is unambiguous. | +| **Experimental fields warn, never silently drop** | Agent-layer fields are accepted at the type surface but not yet wired to the backend; the SDK emits a one-time `console.warn` (`src/experimental.ts:49`) rather than letting a caller assume the field reached the server. | + +--- + +## 2. System at a glance + +```mermaid +graph TB + subgraph "Your code" + APP["Bot / desk integration / AI agent"] + end + + subgraph "hashlock-sdk (this repo)" + FACADE["HashLock facade
src/hashlock.ts — 21 methods"] + CLIENT["GraphQLClient (private)
src/client.ts — retry · timeout · auth"] + TYPES["types.ts · errors.ts
principal.ts · experimental.ts"] + FACADE --> CLIENT + FACADE -. "compile-time" .-> TYPES + end + + subgraph "Hashlock Markets (separate repo)" + GW["api-gateway :4000
/graphql (Apollo Federation)"] + TRADE["trade-service
RFQ · quotes · trades · HTLC"] + AUTH["auth-service
JWT / SIWE"] + end + + APP -->|"new HashLock(config)"| FACADE + CLIENT -->|"HTTP POST + Bearer JWT"| GW + GW --> TRADE & AUTH +``` + +**Reading it:** your code constructs one `HashLock` (`src/hashlock.ts:69`) with an endpoint +and a JWT. Every method call becomes an HTTP `POST` to the backend's **`/graphql`** entry +with an `Authorization: Bearer ` header (`src/client.ts:80-89`). The SDK holds **no +chain logic, no keys, and no on-chain state** — it records on-chain actions the caller has +already taken (e.g. `fundHTLC` records a tx hash you broadcast yourself) and reads +backend-derived state back. The settlement authority lives entirely in hashlock-markets +(see [master doc §4.1, chain-watcher](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#41-backend-services)). + +--- + +## 3. Package layout + +`git ls-files` is 20 files; the seven that *are* the SDK live in `src/`: + +| File | Role | +|---|---| +| `src/index.ts` | Public barrel — exports `HashLock`, `MAINNET_ENDPOINT`, the error classes, all types, and the KYC helpers (`src/index.ts:1-11`). The **only** module a consumer imports. | +| `src/hashlock.ts` | The `HashLock` facade class — 21 GraphQL-backed methods grouped RFQ / quotes / trades / HTLC-EVM / HTLC-Bitcoin (`src/hashlock.ts:66`). | +| `src/client.ts` | `GraphQLClient` — private low-level transport: retry with exponential backoff, timeout via `AbortController`, error normalization (`src/client.ts:17`). Not exported. | +| `src/errors.ts` | Error hierarchy: `HashLockError` base + `GraphQLError`, `NetworkError`, `AuthError` (`src/errors.ts:4,18,31,41`). | +| `src/types.ts` | All domain objects, enums, and mutation input/result interfaces — a hand-maintained mirror of the backend GraphQL SDL (`src/types.ts`). | +| `src/principal.ts` | **Experimental** KYC-tier + principal-attestation types and the `meetsKycTier` helper (`src/principal.ts:62`). | +| `src/experimental.ts` | The one-time experimental-field warning machinery (`src/experimental.ts:49`). | + +Build & test scaffolding: `tsup.config.ts` (ESM+CJS dual build, `.d.ts`, `target: es2022` — +`tsup.config.ts:5-11`), `vitest.config.ts`, `src/__tests__/hashlock.test.ts`, +`.github/workflows/ci.yml`. + +--- + +## 4. The layered architecture + +Three layers, strictly one-directional: **facade → transport → (types/errors)**. + +### 4.1 The facade (`HashLock`) +A method-per-operation class. Each method (a) optionally warns on experimental fields, (b) +calls `client.query` or `client.mutate` with an inline GraphQL document and the typed input, +(c) returns the typed payload. Example — `createRFQ` (`src/hashlock.ts:96-106`): + +```mermaid +sequenceDiagram + participant App as Caller + participant HL as HashLock.createRFQ + participant GC as GraphQLClient.mutate + participant BE as Hashlock Markets /graphql + + App->>HL: createRFQ({ baseToken, quoteToken, side, amount }) + HL->>HL: warnIfExperimental('createRFQ', input) + HL->>GC: mutate(CREATE_RFQ_DOC, input) + GC->>BE: POST { query, variables } + Bearer JWT + BE-->>GC: { data: { createRFQ } } | { errors: [...] } + GC-->>HL: data.createRFQ (or throws typed error) + HL-->>App: RFQ +``` + +The method groups (`src/hashlock.ts`): + +| Group | Methods | Lines | +|---|---|---| +| RFQ | `createRFQ`, `getRFQ`, `listRFQs`, `cancelRFQ` | `:96,111,126,141` | +| Quotes | `submitQuote`, `acceptQuote`, `getQuotes` | `:166,181,195` | +| Trades | `getTrade`, `listTrades`, `confirmDirectTrade`, `acceptTrade`, `cancelTrade`, `confirmSettlementWallets` | `:212,226,241,255,267,279` | +| HTLC — EVM | `fundHTLC`, `claimHTLC`, `refundHTLC`, `getHTLCStatus`, `getHTLCs` | `:308,332,354,368,384` | +| HTLC — Bitcoin | `prepareBitcoinHTLC`, `buildBitcoinClaimPSBT`, `broadcastBitcoinTx` | `:414,429,443` | + +### 4.2 The transport (`GraphQLClient`) +The private engine that every method funnels through (`src/client.ts:63-149`). Its +responsibilities: + +- **Auth** — sets `Authorization: Bearer ` when a token is present + (`src/client.ts:80-82`); `setAccessToken` swaps it live (`src/client.ts:36`). +- **Timeout** — an `AbortController` aborts after `timeout` ms (default `30_000`, + `src/client.ts:4,72-73`). +- **Retry policy** — up to `retries` attempts (default `3`, `src/client.ts:5`) with + exponential backoff `1000 × 2^attempt` ms (`src/client.ts:151-154`). Retries fire on + network/timeout errors always, and on **5xx only for queries** (`src/client.ts:99-107`). +- **Error mapping** — `401/403` → `AuthError` (never retried, `src/client.ts:94-96`); GraphQL + `errors[]` → `GraphQLError` (`src/client.ts:112-117`); empty `data` → `GraphQLError` + (`src/client.ts:119-121`); everything else → `NetworkError` (`src/client.ts:141-144`). + +### 4.3 Errors & types +`errors.ts` gives every failure a `code` and a class so callers branch cleanly +(`README.md:207-223`). `types.ts` is the compile-time contract: enums (`Side`, `RFQStatus`, +`QuoteStatus`, `HTLCRole`, `TradeStatus`, `HTLCStatus` — `src/types.ts:9-45`), domain objects +(`RFQ`, `Quote`, `Trade`, `HTLC` — `src/types.ts:49-125`), and input/result interfaces. These +types are a **hand-maintained projection** of the backend SDL, not generated from it — the +trade-off chosen with the "GraphQL inlined per method" decision in §1. + +--- + +## 5. End-to-end flows + +### 5.1 RFQ → quote → accept → trade +The SDK mirrors the backend lifecycle (see +[master doc §5.1](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#51-rfq--quote--accept--trade)). +A taker calls `createRFQ` (`src/hashlock.ts:96`); makers `submitQuote` (`:166`); the taker +`acceptQuote` (`:181`), whose payload carries the newborn `trade { id status }` +(`src/hashlock.ts:185`). A direct OTC path skips RFQ: `confirmDirectTrade` (`:241`). + +### 5.2 HTLC settlement (the SDK *records*, it does not *sign*) +This is the most important boundary to internalize: **the SDK never holds keys or builds +signed on-chain transactions for EVM**. The caller broadcasts the lock/claim/refund tx with +their own wallet (ethers/viem), then tells the backend about it: + +```mermaid +sequenceDiagram + actor Caller + participant Wallet as Caller's wallet (viem/ethers) + participant SDK as HashLock + participant BE as Hashlock Markets + participant CW as chain-watcher (hashlock-markets) + + Caller->>Wallet: send HTLC newContract() tx on-chain + Wallet-->>Caller: txHash + Caller->>SDK: fundHTLC({ tradeId, txHash, role, hashlock, timelock }) + SDK->>BE: mutation fundHTLC(...) + Note over BE,CW: chain-watcher independently observes the same tx
and is the real authority for trade state + Caller->>Wallet: send withdraw(contractId, preimage) tx + Caller->>SDK: claimHTLC({ tradeId, txHash, preimage }) + SDK->>BE: mutation claimHTLC(...) +``` + +`fundHTLC` (`src/hashlock.ts:308`), `claimHTLC` (`:332`), `refundHTLC` (`:354`), +`getHTLCStatus` (`:368`). The backend's chain-watcher is the sole on-chain authority — the +SDK's `fundHTLC` is a hint/record, **not** what advances the trade +([master doc §5.2 / §5.6](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#52-htlc-settlement-on-one-evm-chain)). + +### 5.3 Bitcoin & cross-chain +Bitcoin HTLCs need no deployed contract — `prepareBitcoinHTLC` (`src/hashlock.ts:414`) asks +the backend for a **P2WSH address + redeem script** (`BitcoinHTLCPrepareResult`, +`src/types.ts:254-266`); the caller funds it, then `buildBitcoinClaimPSBT` (`:429`) returns an +**unsigned PSBT** the caller signs with Xverse/Leather/UniSat, and `broadcastBitcoinTx` (`:443`) +relays the signed hex. A cross-chain ETH↔BTC swap composes these — both legs share one +SHA-256 hashlock so one preimage unlocks both (`README.md:181-205`, +[master doc §5.3](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#53-cross-chain-atomic-swap--preimage-relay)). +Cross-chain RFQs additionally set `baseChain` / `quoteChain` (`src/types.ts:144-175`), which +the backend resolves against its `ChainRegistry` (the allowed ids are enumerated in +`RFQChainId`, `src/types.ts:136-142`). + +### 5.4 Authentication +The SDK is a **bearer-token consumer, not an auth provider** — it does not perform SIWE. The +caller obtains a JWT (web login or the SIWE flow in auth-service) and passes it as +`accessToken` (`src/types.ts:326-337`); `setAccessToken` refreshes it after rotation +(`src/hashlock.ts:74`). A `401/403` surfaces as `AuthError` so the caller can refresh and +retry (`src/client.ts:94-96`, +[master doc §5.5](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#55-authentication--human-siwe-and-agent-otk)). + +--- + +## 6. The experimental agent / attestation layer + +A forward-looking type surface for the **agent economy**: an order can carry a +`PrincipalAttestation` (an opaque binding to a KYC'd entity — `principalId`, `principalType`, +`tier`, rotating `blindId`, `proof`, validity window; `src/principal.ts:26-41`) and an +`AgentInstance` (`src/principal.ts:43-52`), with a `KycTier` lattice +`NONE < BASIC < STANDARD < ENHANCED < INSTITUTIONAL` (`src/principal.ts:17-22`) and the +`meetsKycTier` comparator (`src/principal.ts:62-64`). + +> **Honest status flag (not invented — stated in the code):** these agent-layer input +> fields (`attestation`, `agentInstance`, `minCounterpartyTier`, `hideIdentity` on +> `CreateRFQInput` / `SubmitQuoteInput` / `FundHTLCInput`) are **accepted at the type surface +> but NOT yet sent to the backend** — a no-op at the network layer until the Cayman GraphQL +> schema accepts `PrincipalAttestationInput` (`src/principal.ts:10-15`, `src/types.ts:163-175`). +> To prevent silent confusion, `warnIfExperimental` (`src/experimental.ts:49-65`) emits a +> one-time `console.warn` per `method.field` the first time one is set; suppress with +> `HASHLOCK_SDK_SILENCE_EXPERIMENTAL=1` (`src/experimental.ts:15-19`). The `principal.ts` +> shapes are a deliberate **duplicate** of `@hashlock-tech/intent-schema` kept in sync by hand +> so the SDK has no runtime dependency (`src/principal.ts:1-9`). + +--- + +## 7. Build, test & CI + +- **Build** — `tsup` emits ESM (`dist/index.js`) + CJS (`dist/index.cjs`) + `.d.ts` for both, + `target: es2022`, sourcemaps on (`tsup.config.ts`, `package.json:6-21`). +- **Lint** — `pnpm lint` is `tsc --noEmit` (`package.json:32`); there is no ESLint step. +- **Test** — `vitest run` over `src/__tests__/hashlock.test.ts` (~30 cases): per-method + happy paths with a mocked `fetch`, the error-mapping matrix (`GraphQLError`/`AuthError`/ + `NetworkError`), header injection, cross-chain variable forwarding + (`hashlock.test.ts:40-61`), and the experimental-warning behaviour (`:387-456`). +- **CI** — `.github/workflows/ci.yml` runs lint → test → build on Node **18, 20, 22** + (`ci.yml:13-29`) for every push/PR to `main`. + +--- + +## 8. How this repo connects to hashlock-markets + +This SDK is one of the sibling repos catalogued in the master doc's +[§3 "repositories and how they connect"](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#3-the-repositories-and-how-they-connect) +— its row reads *"hashlock-sdk → TypeScript SDK over the GraphQL/MCP surface."* Concretely: + +| Connection | Detail | +|---|---| +| **Entry point** | `MAINNET_ENDPOINT = 'https://hashlock.markets/graphql'` (`src/hashlock.ts:44`) targets the **api-gateway** `/graphql` — the Apollo Federation gateway ([master doc §2/§4.1](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#2-system-at-a-glance)). It deliberately **does not** use `/api/graphql` (the web app's SSR proxy that reads the httpOnly cookie and would reject SDK Bearer auth — `src/hashlock.ts:30-43`). | +| **Operation mapping** | Each SDK method names a real trade-service resolver: `createRFQ`/`submitQuote`/`acceptQuote` → [master §5.1](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#51-rfq--quote--accept--trade); `fundHTLC`/`claimHTLC`/`refundHTLC` → [§5.2](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#52-htlc-settlement-on-one-evm-chain). | +| **Auth model** | Consumes the JWT minted by auth-service's login/SIWE flow ([master §5.5](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#55-authentication--human-siwe-and-agent-otk)). | +| **Settlement authority** | The SDK **records** on-chain actions; hashlock-markets' **chain-watcher** is the sole authority that advances trade state ([master §5.6](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#56-event-sourcing--chain-watcher-reconciliation-the-spine)). | +| **Mainnet contracts** | The Ethereum HTLC addresses in `README.md:244-250` match the deployments in [master §7](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#7-the-chain-layer). | +| **Agent economy** | The experimental principal/attestation layer (§6) mirrors `@hashlock-tech/intent-schema` and anticipates the MCP/agent surface ([master §8](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#8-the-agent--mcp-surface)). | + +> **Known doc drift (flagged, not fixed here):** the canonical endpoint is the exported +> `MAINNET_ENDPOINT` (`src/hashlock.ts:44`). Several *prose examples* still show the +> superseded DigitalOcean IP `http://142.93.106.129/graphql` (`README.md:23,44,229`, +> `.env.example:2`) — the code comment marks that host as compromised on 2026-04-22 and "never +> restore" (`src/hashlock.ts:40-42`). Treat `MAINNET_ENDPOINT` as truth. Likewise the package +> is `@hashlock-tech/sdk` (`package.json:2`) though some README snippets still import +> `@hashlock/sdk`. `CHANGELOG.md` tops out at `0.1.4` while `package.json` is `0.2.0` +> (`package.json:3`) — the `0.2.0` cross-chain entry is not yet written up. These are +> documentation lags, not code defects; correcting them is out of scope for this +> architecture PR. + +--- + +## 9. Glossary & where to read next + +| Term | Meaning | +|---|---| +| **Facade** | the single `HashLock` class consumers use; hides the transport | +| **HTLC** | Hash-Time-Locked Contract — lock vs a hash; claim reveals the preimage; refund after a timelock | +| **PSBT** | Partially Signed Bitcoin Transaction — the unsigned tx the SDK returns for the caller to sign | +| **Preimage** | the 32-byte secret; `sha256(preimage) = hashlock` on every chain | +| **RFQ** | Request-For-Quote — the sealed-bid quote workflow | +| **SIWE** | Sign-In-With-Ethereum (EIP-4361) — how the JWT this SDK consumes is minted (done outside the SDK) | +| **Principal attestation** | experimental opaque binding of an order to a KYC'd entity, without leaking identity | + +**Read next:** +- The facade — `src/hashlock.ts` (start at `createRFQ`, `:96`). +- The transport & retry/error semantics — `src/client.ts`. +- The full system this SDK talks to — [**hashlock-markets / ARCHITECTURE.md**](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md). + +--- + +*For the Russian version see [`ARCHITECTURE.ru.md`](./ARCHITECTURE.ru.md).* diff --git a/docs/architecture/ARCHITECTURE.ru.md b/docs/architecture/ARCHITECTURE.ru.md new file mode 100644 index 0000000..4b9cd4d --- /dev/null +++ b/docs/architecture/ARCHITECTURE.ru.md @@ -0,0 +1,308 @@ + + +# hashlock-sdk — Архитектура + +> **Авторитетный архитектурный справочник по этому репозиторию.** Документ объясняет, что +> представляет собой каждая часть, как части связаны, как вызов проходит от вашего кода к +> бэкенду Hashlock Markets и обратно, и какая логика стоит за архитектурой — всё выверено по +> ветке `main` (числа соответствуют коду на 2026-05-30). +> +> Это **клиентский TypeScript-SDK**, оборачивающий GraphQL API Hashlock Markets. Про систему, +> с которой он общается, читайте мастер-документ: +> [**hashlock-markets / ARCHITECTURE.md**](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md). +> +> Каждое неочевидное утверждение указывает на `path:line`, который можно открыть. Если число +> здесь когда-либо разойдётся с кодом — прав код, исправьте документ. + +--- + +## 1. Что это такое и центральная идея + +`@hashlock-tech/sdk` — это **тонкий, полностью типизированный TypeScript-клиент** поверх +GraphQL-поверхности Hashlock Markets. Он позволяет программе — маркет-мейкер-боту, интеграции +институционального деска, ИИ-агенту — управлять **тем же жизненным циклом RFQ → котировка → +сделка → HTLC-сеттлмент**, который ведёт веб-приложение, не выписывая GraphQL-строки руками и +не угадывая формы полей. + +Весь пакет — это один публичный класс `HashLock` (`src/hashlock.ts:66`), предоставляющий **21 +async-метод** (`src/hashlock.ts`, по одному на GraphQL-операцию) плюс `setAccessToken`. Каждый +метод — это типизированная обёртка, которая держит встроенный GraphQL-документ и делегирует +транспорт приватному `GraphQLClient` (`src/client.ts:17`). + +### Почему сделано именно так (несущие решения) + +| Решение | Зачем | +|---|---| +| **Ноль рантайм-зависимостей** | В `package.json` есть только `devDependencies` (`package.json:35-39`). SDK использует платформенный `globalThis.fetch` (`src/client.ts:29`), поэтому работает без изменений в Node ≥18, Deno, браузерах и edge-рантаймах — нет дерева зависимостей для аудита. | +| **Фасад над скрытым транспортом** | `HashLock` — единственная поверхность, к которой прикасается потребитель; `GraphQLClient` **не экспортируется** (`src/index.ts:1-10`) — retry, timeout и auth являются деталями реализации, которые можно менять без ломки API. | +| **GraphQL-документы встроены в каждый метод** | Каждый метод владеет своей query/mutation-строкой (например, `src/hashlock.ts:98-104`). Нет шага codegen и нет зависимости от схемы, поэтому SDK поставляется как обычный `.ts` и остаётся читаемым. Цена: списки полей поддерживаются вручную и должны следовать за SDL бэкенда. | +| **Мутации никогда не ретраятся на 5xx** | `mutate()` передаёт `retryOn5xx = false` (`src/client.ts:56-61`), потому что мутации trade/HTLC **не идемпотентны** — повторный `fundHTLC` мог бы записать дважды. Серверные ошибки ретраит только `query()` (`src/client.ts:45-50`). | +| **Типизированная иерархия ошибок** | Вызывающий код ветвится по `AuthError` / `GraphQLError` / `NetworkError` (`src/errors.ts`), а не парсит строки, поэтому логика «обновить токен / повторить / показать пользователю» однозначна. | +| **Экспериментальные поля предупреждают, а не молча отбрасываются** | Поля агентного слоя принимаются на уровне типов, но пока не передаются на бэкенд; SDK выдаёт одноразовый `console.warn` (`src/experimental.ts:49`), а не позволяет вызывающему думать, что поле дошло до сервера. | + +--- + +## 2. Система с высоты птичьего полёта + +```mermaid +graph TB + subgraph "Your code" + APP["Bot / desk integration / AI agent"] + end + + subgraph "hashlock-sdk (this repo)" + FACADE["HashLock facade
src/hashlock.ts — 21 methods"] + CLIENT["GraphQLClient (private)
src/client.ts — retry · timeout · auth"] + TYPES["types.ts · errors.ts
principal.ts · experimental.ts"] + FACADE --> CLIENT + FACADE -. "compile-time" .-> TYPES + end + + subgraph "Hashlock Markets (separate repo)" + GW["api-gateway :4000
/graphql (Apollo Federation)"] + TRADE["trade-service
RFQ · quotes · trades · HTLC"] + AUTH["auth-service
JWT / SIWE"] + end + + APP -->|"new HashLock(config)"| FACADE + CLIENT -->|"HTTP POST + Bearer JWT"| GW + GW --> TRADE & AUTH +``` + +**Как читать:** ваш код создаёт один `HashLock` (`src/hashlock.ts:69`) с эндпоинтом и JWT. +Каждый вызов метода превращается в HTTP `POST` на **`/graphql`** бэкенда с заголовком +`Authorization: Bearer ` (`src/client.ts:80-89`). SDK не держит **никакой чейн-логики, +никаких ключей и никакого он-чейн состояния** — он лишь регистрирует уже совершённые +вызывающим он-чейн действия (например, `fundHTLC` записывает tx-хеш, который вы сами +отправили) и читает обратно состояние, выведенное бэкендом. Авторитет сеттлмента целиком +живёт в hashlock-markets (см. [мастер-документ §4.1, chain-watcher](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#41-backend-services)). + +--- + +## 3. Структура пакета + +`git ls-files` — это 20 файлов; семь, которые *и есть* SDK, живут в `src/`: + +| Файл | Роль | +|---|---| +| `src/index.ts` | Публичный barrel — экспортирует `HashLock`, `MAINNET_ENDPOINT`, классы ошибок, все типы и KYC-хелперы (`src/index.ts:1-11`). **Единственный** модуль, который импортирует потребитель. | +| `src/hashlock.ts` | Класс-фасад `HashLock` — 21 метод поверх GraphQL, сгруппированные RFQ / котировки / сделки / HTLC-EVM / HTLC-Bitcoin (`src/hashlock.ts:66`). | +| `src/client.ts` | `GraphQLClient` — приватный низкоуровневый транспорт: retry с экспоненциальной задержкой, timeout через `AbortController`, нормализация ошибок (`src/client.ts:17`). Не экспортируется. | +| `src/errors.ts` | Иерархия ошибок: база `HashLockError` + `GraphQLError`, `NetworkError`, `AuthError` (`src/errors.ts:4,18,31,41`). | +| `src/types.ts` | Все доменные объекты, перечисления и интерфейсы input/result мутаций — поддерживаемое вручную зеркало GraphQL-SDL бэкенда (`src/types.ts`). | +| `src/principal.ts` | **Экспериментальные** типы KYC-тиров + principal-аттестаций и хелпер `meetsKycTier` (`src/principal.ts:62`). | +| `src/experimental.ts` | Механика одноразового предупреждения об экспериментальных полях (`src/experimental.ts:49`). | + +Сборка и тесты: `tsup.config.ts` (двойная сборка ESM+CJS, `.d.ts`, `target: es2022` — +`tsup.config.ts:5-11`), `vitest.config.ts`, `src/__tests__/hashlock.test.ts`, +`.github/workflows/ci.yml`. + +--- + +## 4. Слоистая архитектура + +Три слоя, строго однонаправленно: **фасад → транспорт → (типы/ошибки)**. + +### 4.1 Фасад (`HashLock`) +Класс «метод на операцию». Каждый метод (a) опционально предупреждает об экспериментальных +полях, (b) вызывает `client.query` или `client.mutate` со встроенным GraphQL-документом и +типизированным входом, (c) возвращает типизированный payload. Пример — `createRFQ` +(`src/hashlock.ts:96-106`): + +```mermaid +sequenceDiagram + participant App as Caller + participant HL as HashLock.createRFQ + participant GC as GraphQLClient.mutate + participant BE as Hashlock Markets /graphql + + App->>HL: createRFQ({ baseToken, quoteToken, side, amount }) + HL->>HL: warnIfExperimental('createRFQ', input) + HL->>GC: mutate(CREATE_RFQ_DOC, input) + GC->>BE: POST { query, variables } + Bearer JWT + BE-->>GC: { data: { createRFQ } } | { errors: [...] } + GC-->>HL: data.createRFQ (or throws typed error) + HL-->>App: RFQ +``` + +Группы методов (`src/hashlock.ts`): + +| Группа | Методы | Строки | +|---|---|---| +| RFQ | `createRFQ`, `getRFQ`, `listRFQs`, `cancelRFQ` | `:96,111,126,141` | +| Котировки | `submitQuote`, `acceptQuote`, `getQuotes` | `:166,181,195` | +| Сделки | `getTrade`, `listTrades`, `confirmDirectTrade`, `acceptTrade`, `cancelTrade`, `confirmSettlementWallets` | `:212,226,241,255,267,279` | +| HTLC — EVM | `fundHTLC`, `claimHTLC`, `refundHTLC`, `getHTLCStatus`, `getHTLCs` | `:308,332,354,368,384` | +| HTLC — Bitcoin | `prepareBitcoinHTLC`, `buildBitcoinClaimPSBT`, `broadcastBitcoinTx` | `:414,429,443` | + +### 4.2 Транспорт (`GraphQLClient`) +Приватный движок, через который проходит каждый метод (`src/client.ts:63-149`). Его +обязанности: + +- **Auth** — устанавливает `Authorization: Bearer ` при наличии токена + (`src/client.ts:80-82`); `setAccessToken` меняет его на лету (`src/client.ts:36`). +- **Timeout** — `AbortController` прерывает запрос через `timeout` мс (по умолчанию `30_000`, + `src/client.ts:4,72-73`). +- **Политика retry** — до `retries` попыток (по умолчанию `3`, `src/client.ts:5`) с + экспоненциальной задержкой `1000 × 2^attempt` мс (`src/client.ts:151-154`). Retry срабатывает + на сетевые/таймаут-ошибки всегда, а на **5xx — только для запросов** (`src/client.ts:99-107`). +- **Отображение ошибок** — `401/403` → `AuthError` (никогда не ретраится, `src/client.ts:94-96`); + GraphQL `errors[]` → `GraphQLError` (`src/client.ts:112-117`); пустой `data` → `GraphQLError` + (`src/client.ts:119-121`); всё прочее → `NetworkError` (`src/client.ts:141-144`). + +### 4.3 Ошибки и типы +`errors.ts` даёт каждому сбою `code` и класс, чтобы вызывающий код ветвился чисто +(`README.md:207-223`). `types.ts` — это контракт уровня компиляции: перечисления (`Side`, +`RFQStatus`, `QuoteStatus`, `HTLCRole`, `TradeStatus`, `HTLCStatus` — `src/types.ts:9-45`), +доменные объекты (`RFQ`, `Quote`, `Trade`, `HTLC` — `src/types.ts:49-125`) и интерфейсы +input/result. Эти типы — **поддерживаемая вручную проекция** SDL бэкенда, а не сгенерированы из +него — компромисс, выбранный вместе с решением «GraphQL встроен в каждый метод» из §1. + +--- + +## 5. Сквозные потоки данных + +### 5.1 RFQ → котировка → принятие → сделка +SDK зеркалит жизненный цикл бэкенда (см. +[мастер-документ §5.1](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#51-rfq--quote--accept--trade)). +Тейкер вызывает `createRFQ` (`src/hashlock.ts:96`); мейкеры — `submitQuote` (`:166`); тейкер — +`acceptQuote` (`:181`), чей payload несёт только что рождённую сделку `trade { id status }` +(`src/hashlock.ts:185`). Прямой OTC-путь минует RFQ: `confirmDirectTrade` (`:241`). + +### 5.2 HTLC-сеттлмент (SDK *регистрирует*, но не *подписывает*) +Это важнейшая граница для усвоения: **SDK никогда не держит ключи и не строит подписанные +он-чейн транзакции для EVM**. Вызывающий отправляет транзакцию lock/claim/refund своим +кошельком (ethers/viem), а затем сообщает об этом бэкенду: + +```mermaid +sequenceDiagram + actor Caller + participant Wallet as Caller's wallet (viem/ethers) + participant SDK as HashLock + participant BE as Hashlock Markets + participant CW as chain-watcher (hashlock-markets) + + Caller->>Wallet: send HTLC newContract() tx on-chain + Wallet-->>Caller: txHash + Caller->>SDK: fundHTLC({ tradeId, txHash, role, hashlock, timelock }) + SDK->>BE: mutation fundHTLC(...) + Note over BE,CW: chain-watcher independently observes the same tx
and is the real authority for trade state + Caller->>Wallet: send withdraw(contractId, preimage) tx + Caller->>SDK: claimHTLC({ tradeId, txHash, preimage }) + SDK->>BE: mutation claimHTLC(...) +``` + +`fundHTLC` (`src/hashlock.ts:308`), `claimHTLC` (`:332`), `refundHTLC` (`:354`), +`getHTLCStatus` (`:368`). chain-watcher бэкенда — единственный авторитет по он-чейн состоянию; +`fundHTLC` в SDK — это подсказка/запись, **а не** то, что двигает сделку +([мастер-документ §5.2 / §5.6](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#52-htlc-settlement-on-one-evm-chain)). + +### 5.3 Bitcoin и кросс-чейн +Bitcoin-HTLC не требуют задеплоенного контракта — `prepareBitcoinHTLC` (`src/hashlock.ts:414`) +запрашивает у бэкенда **P2WSH-адрес + redeem-скрипт** (`BitcoinHTLCPrepareResult`, +`src/types.ts:254-266`); вызывающий фондирует его, затем `buildBitcoinClaimPSBT` (`:429`) +возвращает **неподписанный PSBT**, который вызывающий подписывает в Xverse/Leather/UniSat, а +`broadcastBitcoinTx` (`:443`) ретранслирует подписанный hex. Кросс-чейн своп ETH↔BTC +комбинирует эти вызовы — обе ноги делят один SHA-256 hashlock, поэтому один прообраз +разблокирует обе (`README.md:181-205`, +[мастер-документ §5.3](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#53-cross-chain-atomic-swap--preimage-relay)). +Кросс-чейн RFQ дополнительно задают `baseChain` / `quoteChain` (`src/types.ts:144-175`), +которые бэкенд резолвит по своему `ChainRegistry` (разрешённые id перечислены в `RFQChainId`, +`src/types.ts:136-142`). + +### 5.4 Аутентификация +SDK — это **потребитель bearer-токена, а не провайдер auth** — он не выполняет SIWE. +Вызывающий получает JWT (вход через веб или SIWE-поток в auth-service) и передаёт его как +`accessToken` (`src/types.ts:326-337`); `setAccessToken` обновляет его после ротации +(`src/hashlock.ts:74`). `401/403` всплывает как `AuthError`, чтобы вызывающий мог обновить +токен и повторить (`src/client.ts:94-96`, +[мастер-документ §5.5](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#55-authentication--human-siwe-and-agent-otk)). + +--- + +## 6. Экспериментальный слой агента / аттестации + +Заглядывающая вперёд поверхность типов для **экономики агентов**: ордер может нести +`PrincipalAttestation` (непрозрачную привязку к KYC'd-сущности — `principalId`, `principalType`, +`tier`, ротируемый `blindId`, `proof`, окно валидности; `src/principal.ts:26-41`) и +`AgentInstance` (`src/principal.ts:43-52`), с решёткой `KycTier` +`NONE < BASIC < STANDARD < ENHANCED < INSTITUTIONAL` (`src/principal.ts:17-22`) и компаратором +`meetsKycTier` (`src/principal.ts:62-64`). + +> **Честный флаг статуса (не выдуман — заявлен в коде):** эти входные поля агентного слоя +> (`attestation`, `agentInstance`, `minCounterpartyTier`, `hideIdentity` на `CreateRFQInput` / +> `SubmitQuoteInput` / `FundHTLCInput`) **принимаются на уровне типов, но пока НЕ отправляются +> на бэкенд** — no-op на сетевом уровне, пока GraphQL-схема Cayman не примет +> `PrincipalAttestationInput` (`src/principal.ts:10-15`, `src/types.ts:163-175`). Чтобы избежать +> молчаливой путаницы, `warnIfExperimental` (`src/experimental.ts:49-65`) выдаёт одноразовый +> `console.warn` на каждое `method.field` при первой установке; подавляется через +> `HASHLOCK_SDK_SILENCE_EXPERIMENTAL=1` (`src/experimental.ts:15-19`). Формы из `principal.ts` +> — намеренный **дубликат** `@hashlock-tech/intent-schema`, синхронизируемый вручную, чтобы у +> SDK не было рантайм-зависимости (`src/principal.ts:1-9`). + +--- + +## 7. Сборка, тесты и CI + +- **Сборка** — `tsup` выдаёт ESM (`dist/index.js`) + CJS (`dist/index.cjs`) + `.d.ts` для + обоих, `target: es2022`, sourcemaps включены (`tsup.config.ts`, `package.json:6-21`). +- **Линт** — `pnpm lint` — это `tsc --noEmit` (`package.json:32`); шага ESLint нет. +- **Тесты** — `vitest run` по `src/__tests__/hashlock.test.ts` (~30 кейсов): happy-path для + каждого метода с замоканным `fetch`, матрица отображения ошибок (`GraphQLError`/`AuthError`/ + `NetworkError`), инъекция заголовков, проброс кросс-чейн переменных (`hashlock.test.ts:40-61`) + и поведение экспериментального предупреждения (`:387-456`). +- **CI** — `.github/workflows/ci.yml` запускает lint → test → build на Node **18, 20, 22** + (`ci.yml:13-29`) на каждый push/PR в `main`. + +--- + +## 8. Как этот репозиторий связан с hashlock-markets + +Этот SDK — один из соседних репозиториев, каталогизированных в +[§3 «репозитории и как они связаны»](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#3-the-repositories-and-how-they-connect) +мастер-документа — его строка гласит: *«hashlock-sdk → TypeScript-SDK поверх GraphQL/MCP-поверхности»*. +Конкретно: + +| Связь | Деталь | +|---|---| +| **Точка входа** | `MAINNET_ENDPOINT = 'https://hashlock.markets/graphql'` (`src/hashlock.ts:44`) нацелен на **api-gateway** `/graphql` — Federation-шлюз Apollo ([мастер-документ §2/§4.1](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#2-system-at-a-glance)). Он намеренно **не** использует `/api/graphql` (SSR-прокси веб-приложения, который читает httpOnly-куку и отверг бы Bearer-auth SDK — `src/hashlock.ts:30-43`). | +| **Отображение операций** | Каждый метод SDK называет реальный резолвер trade-service: `createRFQ`/`submitQuote`/`acceptQuote` → [мастер §5.1](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#51-rfq--quote--accept--trade); `fundHTLC`/`claimHTLC`/`refundHTLC` → [§5.2](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#52-htlc-settlement-on-one-evm-chain). | +| **Модель auth** | Потребляет JWT, выпущенный потоком login/SIWE auth-service ([мастер §5.5](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#55-authentication--human-siwe-and-agent-otk)). | +| **Авторитет сеттлмента** | SDK **регистрирует** он-чейн действия; **chain-watcher** в hashlock-markets — единственный авторитет, продвигающий состояние сделки ([мастер §5.6](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#56-event-sourcing--chain-watcher-reconciliation-the-spine)). | +| **Mainnet-контракты** | Адреса Ethereum-HTLC в `README.md:244-250` совпадают с деплоями в [мастер §7](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#7-the-chain-layer). | +| **Экономика агентов** | Экспериментальный слой principal/аттестаций (§6) зеркалит `@hashlock-tech/intent-schema` и предвосхищает MCP/агентную поверхность ([мастер §8](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md#8-the-agent--mcp-surface)). | + +> **Известный дрейф документации (отмечен, но здесь не исправляется):** канонический эндпоинт +> — это экспортируемый `MAINNET_ENDPOINT` (`src/hashlock.ts:44`). Несколько *примеров в прозе* +> всё ещё показывают устаревший DigitalOcean-IP `http://142.93.106.129/graphql` +> (`README.md:23,44,229`, `.env.example:2`) — комментарий в коде помечает этот хост как +> скомпрометированный 2026-04-22 и «никогда не восстанавливать» (`src/hashlock.ts:40-42`). +> Считайте истиной `MAINNET_ENDPOINT`. Аналогично пакет называется `@hashlock-tech/sdk` +> (`package.json:2`), хотя некоторые сниппеты в README всё ещё импортируют `@hashlock/sdk`. +> `CHANGELOG.md` доходит до `0.1.4`, тогда как `package.json` — это `0.2.0` (`package.json:3`) +> — запись о кросс-чейн `0.2.0` ещё не написана. Это запаздывание документации, а не дефекты +> кода; их исправление вне рамок этого архитектурного PR. + +--- + +## 9. Глоссарий и что читать дальше + +| Термин | Значение | +|---|---| +| **Facade (фасад)** | единственный класс `HashLock`, которым пользуется потребитель; скрывает транспорт | +| **HTLC** | Hash-Time-Locked Contract — блокировка под хеш; claim раскрывает прообраз; refund после таймлока | +| **PSBT** | Partially Signed Bitcoin Transaction — неподписанная транзакция, которую SDK возвращает для подписи вызывающим | +| **Preimage (прообраз)** | 32-байтный секрет; `sha256(preimage) = hashlock` на каждом чейне | +| **RFQ** | Request-For-Quote — процесс закрытых котировок | +| **SIWE** | Sign-In-With-Ethereum (EIP-4361) — как выпускается JWT, который потребляет этот SDK (делается вне SDK) | +| **Principal attestation** | экспериментальная непрозрачная привязка ордера к KYC'd-сущности без раскрытия личности | + +**Что читать дальше:** +- Фасад — `src/hashlock.ts` (начните с `createRFQ`, `:96`). +- Транспорт и семантика retry/ошибок — `src/client.ts`. +- Полная система, с которой общается этот SDK — + [**hashlock-markets / ARCHITECTURE.md**](https://github.com/Hashlock-Tech/hashlock-markets/blob/main/docs/architecture/ARCHITECTURE.md). + +--- + +*Английская версия — [`ARCHITECTURE.md`](./ARCHITECTURE.md).*