diff --git a/.env.example b/.env.example deleted file mode 100644 index 19ef093..0000000 --- a/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -# HashLock SDK Configuration -HASHLOCK_ENDPOINT=http://142.93.106.129/graphql -HASHLOCK_ACCESS_TOKEN=your-jwt-token-here diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ac7f63..0d918aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,29 +1,22 @@ name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - +on: [push, pull_request] jobs: - test: + typescript: runs-on: ubuntu-latest - strategy: - matrix: - node-version: [18, 20, 22] - + defaults: { run: { working-directory: typescript } } steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - version: 9 - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: pnpm - - - run: pnpm install --frozen-lockfile - - run: pnpm lint - - run: pnpm test - - run: pnpm build + with: { node-version: 20 } + - run: npm install + - run: npm run typecheck + - run: npm run build + python: + runs-on: ubuntu-latest + defaults: { run: { working-directory: python } } + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: { python-version: '3.11' } + - run: pip install httpx + - run: python -c "import ast,glob; [ast.parse(open(f).read()) for f in glob.glob('hashlock/*.py')]; print('syntax ok')" diff --git a/.gitignore b/.gitignore index aafcb23..45072b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ node_modules/ dist/ +*.tsbuildinfo +__pycache__/ +*.egg-info/ +build/ +.venv/ .env -coverage/ -*.tgz +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 38cb375..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,208 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -## [0.3.0] - 2026-06-11 - -### Added — Instant Settlement (Lane A) public surface - -First SDK slice of Phase 5.2: agent frameworks call Hashlock's -instant-settlement engine through the SDK ("single engine, two -adapters" — this is the SDK adapter leg). All backend surfaces are -live behind a feature flag; the SDK degrades gracefully when the -flag is off. - -- **Types**: `InstantFill` (state union `committed | fronted | settled | - reimbursed | cancelled`; `amountWei` is a decimal **string** over the - full uint256 range — never converted to `number`), `AgentPolicy` - (`maxLatencyMs` / `maxFeeBps` / `minTrust`), `TrustLevel`, - `InstantTakerResult`, `InstantFillFallbackReason`, - `InstantFillOrphanedError`. `Quote.instantFill` / - `Quote.solverVaultAddr` response fields. -- **Maker**: `submitQuote` accepts `instantFill` + `solverVaultAddr`. -- **Taker**: `requestInstantFill(rfqId, quoteId)`; - `acceptQuote(quoteId, policy?)` — policy is an optional PREFERENCE - that can never fail the accept (SDK sanitizes invalid policies down - to the standard path); flow helper - `requestInstantFillAndAccept(rfqId, quoteId, { policy })` enforcing - the canonical order (fill succeeds → accept) with typed fallbacks: - `INSTANT_FILL_DISABLED` / lane conflict (`extensions.lane`) / - already-requested → automatic standard `acceptQuote`; fill OK + - accept FAIL → `fill_orphaned` result carrying - `InstantFillOrphanedError`, recoverable via the accept-only - `retryAcceptAfterInstantFill(fill)` (never re-requests the fill — - exactly-once per quote). Unexpected errors are thrown, never - swallowed into fallback. -- **Solver**: `markInstantFillFronted(fillId, frontTxHash)`; - `onInstantFillRequested(cb)` (solver-scoped subscription); - `serveInstantFills(front, opts)` = subscribe + auto-mark-fronted. -- **Taker notifications**: `onInstantFillFronted(cb)` (taker-scoped). -- **Subscriptions**: minimal `graphql-transport-ws` client transport - (zero new runtime dependencies). Global `WebSocket` (browsers, - Node >= 22) used automatically; `HashLockConfig.webSocket` / - `wsEndpoint` for Node 18/20 (`ws` package) or custom endpoints. -- **Policy presets**: `policyPresets.instant` (`maxLatencyMs: 3000`) / - `.balanced` (`minTrust: 'med'`) / `.trustless` (`minTrust: 'max'`) - — 1:1 with the design §13.1 human-slider table. -- `GraphQLError.errors[]` entries now preserve the server's - `extensions` (code / flattened metadata) for typed - classification (`classifyInstantFillError`). -- 35 new tests (taker decision table, error classification, policy - sanitization, ws handshake/subscription lifecycle, solver serve loop). - -### Fixed — schema drift (pre-release; 0.3.0 was never tagged/published) - -The initial instant surface was written from the design brief instead of -the real SDL; verification against the main repo's `origin/main` schema -found two confirmed drifts, fixed here BEFORE any release: - -- **Subscription payload types** — `instantFillRequested` / - `instantFillFronted` deliver dedicated event types, NOT `InstantFill`. - The SDK previously selected `id … frontTxHash frontedAt createdAt` on - both, which the server rejects (`Cannot query field "id" on type - "InstantFillRequested"`). Now: `InstantFillRequestedEvent` - (`fillId quoteId rfqId state amountWei createdAt`) and - `InstantFillFrontedEvent` (`fillId quoteId rfqId tradeId state - amountWei frontTxHash frontedAt`), with matching selection constants - `INSTANT_FILL_REQUESTED_FIELDS` / `INSTANT_FILL_FRONTED_FIELDS`. - `onInstantFillRequested` / `onInstantFillFronted` / - `serveInstantFills` callbacks are typed accordingly, and - `serveInstantFills` now fronts via `event.fillId` (the old code read - `fill.id`, which does not exist on the event payload). -- **`minTrust` wire type** — the schema's `AgentPolicyInput.minTrust` - is `Int` (0-100 trust score); the SDK was sending the `TrustLevel` - string raw, which fails GraphQL Int coercion and rejects the ENTIRE - `acceptQuote` (violating the "a policy can never fail the accept" - guarantee). `sanitizeAgentPolicy` now returns the wire shape - (`AgentPolicyWire`, all fields integers): `TrustLevel` maps via - `TRUST_LEVEL_TO_SCORE` (low→0, med→50, max→100 — med passes the - backend's 50 reputation stub since the guard is `minTrust > score`; - max steers trustless); raw 0-100 numbers are accepted, floored and - clamped; `maxFeeBps` clamps to 0-10000 and `maxLatencyMs` to the - 32-bit Int range for coercion safety. -- **`classifyInstantFillError` wire shape** — verified against the - backend pipeline (trade-service `maskTradeError` + gateway - formatter): `HashlockError` serializes as - `extensions: { ...metadata, code, retryable }` — metadata FLATTENED, - HTTP status NEVER serialized. The classifier now reads the lane from - `extensions.lane` (with `extensions.metadata.lane` as a defensive - fallback) and detects already-requested via - `code === 'INVALID_STATE_TRANSITION'` + the "already requested" - message; the dead `extensions.status` / `http.status` / `CONFLICT` - paths were removed. Other `INVALID_STATE_TRANSITION` errors (quote no - longer firm / expired) classify as `null` and are rethrown. - -### Added — permanent drift guard - -- `test/fixtures/schema.graphql` + `schema.subscriptions.graphql`: - vendored authoritative SDL from the main repo (source path, git ref - and commit SHA recorded in the fixture headers). Refresh with - `node scripts/vendor-schema.mjs `. -- `src/__tests__/schema-validate.test.ts`: every operation string the - SDK sends (captured from the real code paths, including the ws - subscribe frames) is `validate()`d against the vendored SDL using the - `graphql` package (devDependency ONLY — runtime stays zero-dependency). -- Known pre-existing legacy drift documented (skipped test, not fixed - to avoid breaking the v0.1.x public shape): `getHTLCStatus` selects - `initiatorHTLC`/`counterpartyHTLC`, but the schema's - `HTLCStatusResult` is flat — use `getHTLCs(tradeId)` instead. - -### Compatibility -- **Backward compatible / additive.** `acceptQuote` without a policy - sends the exact legacy operation; plain quotes are untouched; no - existing export changed shape. -- **Not yet published to npm** (operator decision pending). Publish - with `npm publish --access public` after operator sign-off. - -## [0.2.0] - 2026-04-26 - -### Added -- Cross-chain `createRFQ` — `baseChain` / `quoteChain` inputs - (`RFQChainId`) so cross-chain RFQs resolve `(symbol, chain)` - composite tokens unambiguously. (Entry backfilled in 0.3.0; see git - tag/commit `bb2771f` for details.) - -## [0.1.4] - 2026-04-11 - -### Added -- **Experimental field warning** — when a caller sets any of the - experimental agent-layer fields (`attestation`, `agentInstance`, - `minCounterpartyTier`, `hideIdentity`) on `createRFQ`, - `submitQuote`, or `fundHTLC`, the SDK now emits a one-time - `console.warn` per field/method pair explaining that GraphQL - wire-through to the Cayman backend is not yet implemented and - the field is currently a no-op at the network layer. -- Warning can be suppressed with `HASHLOCK_SDK_SILENCE_EXPERIMENTAL=1` - for call sites that have already acknowledged the experimental - status. -- 2 new tests verifying the warning fires when experimental fields - are set and stays silent when they are not. - -### Why -The v0.1.3 release exposed experimental type surface for agent -flows but dropped the fields silently at the network layer — a -DX hazard, because a caller could set `attestation` and assume -it reached the backend. This warning closes the expectation gap. - -## [0.1.3] - 2026-04-11 - -### Added -- **Principal + attestation type surface (experimental)** — agents operating - under a KYC'd principal can now attach structured metadata to RFQs, - quotes, and HTLC funding calls: - - `PrincipalAttestation` interface: opaque binding from an order to a - KYC'd entity (principalId, principalType, tier, blindId, issuedAt, - expiresAt, proof) without leaking identity to counterparties - - `AgentInstance` interface: autonomous agent instance metadata - (instanceId, strategy, version, spawnedAt) - - `KycTier` enum with `NONE < BASIC < STANDARD < ENHANCED < INSTITUTIONAL` - ordering plus `KYC_TIER_RANK` and `meetsKycTier()` helper - - New optional response fields on `RFQ` (`attestationTier`, - `attestationBlindId`, `minCounterpartyTier`), `Quote` - (`attestationTier`, `attestationBlindId`), and `Trade` - (`initiatorAttestationTier`, `counterpartyAttestationTier`) - - New optional input fields on `CreateRFQInput` (`attestation`, - `agentInstance`, `minCounterpartyTier`, `hideIdentity`), - `SubmitQuoteInput` (`attestation`, `agentInstance`, `hideIdentity`), - and `FundHTLCInput` (`attestation`, `agentInstance`) -- 6 new tests covering the agent principal layer and backward compat - -### Fixed -- Widen `GraphQLClient.execute()` variables parameter to accept - `Record | object | undefined` so typed SDK inputs - (e.g., `CreateRFQInput`) satisfy the signature under strict TypeScript. - This fix was a pre-existing CI lint failure from the v0.1.1 and v0.1.2 - publish commits; v0.1.3 is the first version whose CI lint step is green. - -### Compatibility -- **Backward compatible.** All new fields are optional. Existing human - OTC flows without attestation continue to work unchanged. -- **EXPERIMENTAL.** The new agent-layer fields are accepted at the SDK - type surface today but are not yet sent to the Cayman GraphQL - backend. Wire-through will land in a later release once the backend - schema accepts `PrincipalAttestationInput` and `AgentInstanceInput`. - Passing these fields today is a no-op at the network layer. - -## [0.1.2] - 2026-04-09 - -### Fixed -- Use `/api/graphql` endpoint to bypass CSRF protection. - -## [0.1.1] - 2026-04-09 - -### Fixed -- HTLC query uses `chainId` instead of `chainType` to match backend schema. - -## [0.1.0] - 2026-04-09 - -### Added -- Initial release -- RFQ trading: createRFQ, submitQuote, acceptQuote, listRFQs, cancelRFQ -- Trade management: getTrade, listTrades, acceptTrade, cancelTrade, confirmDirectTrade -- EVM HTLC: fundHTLC, claimHTLC, refundHTLC, getHTLCStatus -- Bitcoin HTLC: prepareBitcoinHTLC, buildBitcoinClaimPSBT, broadcastBitcoinTx -- Settlement wallet management: confirmSettlementWallets -- Full TypeScript types for all inputs and outputs -- Automatic retry with exponential backoff -- Error hierarchy: HashLockError, GraphQLError, NetworkError, AuthError -- ESM + CJS dual build diff --git a/README.md b/README.md index 48d44a3..e473f3f 100644 --- a/README.md +++ b/README.md @@ -1,439 +1,78 @@ -# @hashlock/sdk +# Hashlock Markets SDK -TypeScript SDK for [HashLock](https://hashlock.tech) — institutional OTC trading with HTLC atomic settlement on Ethereum and Bitcoin. +Official TypeScript and Python SDKs for the **Hashlock Markets** developer API — non-custodial +cross-chain atomic swaps (**BTC ↔ EVM / TRON**) over sealed RFQ + HTLC. -> 📐 **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)). +- **Non-custodial.** Settlement endpoints return *unsigned* transactions. You sign with your own key or + HSM; the server never holds your keys. +- **Native, no bridge.** Real BTC ↔ real EVM/TRON assets, settled directly via HTLC — no wrapped tokens, + no bridge, no custody of principal. +- **One API, two roles.** Takers post RFQs; makers quote them; both settle their legs with the same + builders. Plus webhooks and a maker WebSocket feed. -## Install - -```bash -npm install @hashlock/sdk -# or -pnpm add @hashlock/sdk -``` - -## Quick Start - -```ts -import { HashLock } from '@hashlock/sdk'; - -const hl = new HashLock({ - endpoint: 'http://142.93.106.129/graphql', - accessToken: 'your-jwt-token', -}); - -// Create an RFQ to sell 1 ETH for USDT -const rfq = await hl.createRFQ({ - baseToken: 'ETH', - quoteToken: 'USDT', - side: 'SELL', - amount: '1.0', -}); - -console.log(`RFQ created: ${rfq.id}`); -``` - -## Authentication - -Get a JWT token by logging into the HashLock platform, then pass it to the SDK: - -```ts -const hl = new HashLock({ - endpoint: 'http://142.93.106.129/graphql', - accessToken: 'eyJhbGciOiJIUzI1NiIs...', -}); - -// Or update the token later -hl.setAccessToken('new-token'); -``` - -## RFQ Trading - -### Create an RFQ (Request for Quote) - -```ts -const rfq = await hl.createRFQ({ - baseToken: 'BTC', - quoteToken: 'USDT', - side: 'BUY', - amount: '0.5', - expiresIn: 300, // 5 minutes -}); -``` - -### Respond with a Quote - -```ts -const quote = await hl.submitQuote({ - rfqId: rfq.id, - price: '68500.00', - amount: '0.5', -}); -``` - -### Accept a Quote (creates a Trade) - -```ts -const accepted = await hl.acceptQuote(quote.id); -// accepted.trade.id -> trade ready for settlement -``` - -### List & Query - -```ts -const { rfqs, total } = await hl.listRFQs({ status: 'ACTIVE', page: 1 }); -const rfq = await hl.getRFQ('rfq-uuid'); -const quotes = await hl.getQuotes('rfq-uuid'); -``` - -## Instant Settlement (Lane A) - -Solvers (market makers running the instant-settlement flow) can commit to -**fronting the taker's asset immediately** when their quote is accepted, and -get reimbursed when the underlying trade settles. The whole surface is -**feature-flagged on the backend** — when the flag is off, the SDK degrades -gracefully (see *Flag-off behaviour* below). - -> **Money fields:** `InstantFill.amountWei` is the committed amount in the -> asset's smallest on-chain unit (wei / sats / MIST) as a **decimal string** -> covering the full uint256 range. **Never convert it to a JS `number`** -> (precision is lost above 2^53) — use `BigInt(fill.amountWei)`. - -### Taker flow - -The canonical instant path is: `requestInstantFill` **succeeds first**, then -`acceptQuote`. The `requestInstantFillAndAccept` helper enforces this order -inside the SDK and maps every failure mode to a typed result: - -```ts -import { policyPresets } from '@hashlock-tech/sdk'; - -const res = await hl.requestInstantFillAndAccept(rfq.id, quote.id, { - policy: policyPresets.instant, // optional preference (see Policy semantics) -}); - -switch (res.kind) { - case 'instant': - // Fill committed AND quote accepted — solver fronting is on its way. - console.log('instant fill', res.fill.id, BigInt(res.fill.amountWei)); - break; - case 'standard': - // Instant path refused with a typed reason — the SDK already fell - // back to a normal acceptQuote, the trade proceeds on the standard path. - // res.reason: 'disabled' | 'lane_conflict' | 'already_requested' - // res.lane is set for lane_conflict. - console.log('standard path:', res.reason); - break; - case 'fill_orphaned': - // requestInstantFill succeeded but acceptQuote failed — the fill is - // committed server-side. Retry the ACCEPT ONLY (never re-request the - // fill: instant fills are exactly-once per quote and would 409): - const retry = await hl.retryAcceptAfterInstantFill(res.fill); - break; -} -``` - -Decision table implemented by the helper: - -| `requestInstantFill` | `acceptQuote` | Result | -|---|---|---| -| OK | OK | `{ kind: 'instant', fill, quote }` | -| `INSTANT_FILL_DISABLED` | OK (fallback) | `{ kind: 'standard', reason: 'disabled', quote }` | -| lane conflict (`extensions.lane`) | OK (fallback) | `{ kind: 'standard', reason: 'lane_conflict', lane, quote }` | -| already requested (exactly-once) | OK (fallback) | `{ kind: 'standard', reason: 'already_requested', quote }` | -| any other error | — (not attempted) | **thrown** (auth/network/unknown errors are never swallowed) | -| OK | FAIL | `{ kind: 'fill_orphaned', fill, error: InstantFillOrphanedError }` | - -Takers can watch for the fronting payment (payload: -`InstantFillFrontedEvent` — the fill id arrives as `fillId`): - -```ts -const handle = hl.onInstantFillFronted((event) => { - console.log('fronted!', event.fillId, event.frontTxHash); -}); -// later: handle.unsubscribe(); -``` - -### Solver flow - -Submit an instant-fill quote, then serve incoming fill requests: - -```ts -// 1. Commit on the quote (solverVaultAddr is required with instantFill) -const quote = await hl.submitQuote({ - rfqId: rfq.id, - price: '3450.00', - amount: '10.0', - instantFill: true, - solverVaultAddr: '0xYourVault...', -}); - -// 2. Watch for accepted instant fills and front them. The subscription -// payload is InstantFillRequestedEvent — fillId/quoteId/rfqId/state/ -// amountWei/createdAt (NOT the InstantFill mutation type). -// serveInstantFills = subscribe(instantFillRequested) + auto markInstantFillFronted -const handle = hl.serveInstantFills(async (event) => { - const txHash = await vault.front(event.quoteId, BigInt(event.amountWei)); - return txHash; // SDK calls markInstantFillFronted(event.fillId, txHash) for you -}, { - onFronted: (fill) => console.log('fronted', fill.id), - onError: (err, event) => console.error('fronting failed', event?.fillId, err), -}); - -// Or drive the two halves manually: -hl.onInstantFillRequested(async (event) => { - const txHash = await vault.front(event.quoteId, BigInt(event.amountWei)); - await hl.markInstantFillFronted(event.fillId, txHash); -}); -``` - -Subscriptions use the `graphql-transport-ws` protocol. Browsers and -Node >= 22 work out of the box (global `WebSocket`); on Node 18/20 pass an -implementation: `new HashLock({ ..., webSocket: (await import('ws')).default })`. -Streams are scoped server-side by the authenticated user -(`instantFillRequested` → the quote's maker, `instantFillFronted` → the taker). - -### Policy semantics — a preference, not a commitment - -`acceptQuote(quoteId, policy)` takes an optional `AgentPolicy` -(`{ maxLatencyMs?, maxFeeBps?, minTrust? }`). A policy is **routing advice**: -it never causes the accept to fail. The SDK sanitizes it before sending — -invalid fields are dropped, and if nothing valid remains the accept silently -proceeds on the standard path with no policy at all. - -Presets mirror the human speed slider 1:1 (single engine, two adapters): - -| Preset | Policy | Under the hood | +| Package | Language | Install | |---|---|---| -| `policyPresets.instant` | `{ maxLatencyMs: 3000 }` | Lane A/B fronting, 0–1 confs, wide spread | -| `policyPresets.balanced` | `{ minTrust: 'med' }` | Lane A, 2–3 confs (add your own `maxFeeBps`) | -| `policyPresets.trustless` | `{ minTrust: 'max' }` | Lane Z pure HTLC, full confs, tight spread | +| [`typescript/`](./typescript) | TypeScript / JS (Node 18+, browsers, edge) | `npm i @hashlock-tech/sdk` | +| [`python/`](./python) | Python 3.9+ | `pip install hashlock-sdk` | +| [`examples/`](./examples) | Quickstarts + Fireblocks/Copper signing reference | — | -```ts -await hl.acceptQuote(quote.id, { ...policyPresets.balanced, maxFeeBps: 30 }); -``` - -**`minTrust` on the wire:** the schema's `AgentPolicyInput.minTrust` is an -`Int` (a 0–100 solver trust/reputation score), so the SDK converts a -`TrustLevel` string before sending (`TRUST_LEVEL_TO_SCORE`). The backend -guard is `minTrust > solverReputation` (reputation stubbed at 50 until the -reputation oracle lands): - -| `TrustLevel` | Int sent | Effect against the 50 stub | -|---|---|---| -| `'low'` | 0 | never constrains | -| `'med'` | 50 | passes (50 > 50 is false) — Lane A allowed | -| `'max'` | 100 | unmet → steers to the trustless pure-HTLC path | - -You may also pass a raw 0–100 integer directly -(`{ minTrust: 75 }` — floored and clamped). Strings never reach the wire: -an unconverted `'med'` would fail GraphQL `Int` coercion and reject the -**entire** accept, which would break the "a policy can never fail the -accept" guarantee. - -### Flag-off behaviour - -The instant-settlement feature is gated by a backend flag. When it is off: - -- `requestInstantFill` fails with `INSTANT_FILL_DISABLED` → - `requestInstantFillAndAccept` returns `{ kind: 'standard', reason: 'disabled' }` - and the trade completes on the standard path. Nothing throws. -- `submitQuote` with `instantFill: true` is rejected by the backend; plain - quotes are unaffected. -- `policy` on `acceptQuote` remains a no-op preference — accepted and ignored. - -## HTLC Settlement — ETH / ERC-20 - -After a trade is accepted, both parties lock assets in HTLC contracts. - -### Record an HTLC Lock (after on-chain tx) +## Quick start (TypeScript) ```ts -// 1. Send ETH lock tx on-chain via ethers.js / viem -// 2. Record it in HashLock: -const result = await hl.fundHTLC({ - tradeId: 'trade-uuid', - txHash: '0xabc123...', - role: 'INITIATOR', - timelock: Math.floor(Date.now() / 1000) + 3600, - hashlock: '0xdef456...', - chainType: 'evm', -}); -``` - -### Claim an HTLC (reveal preimage) +import { HashlockClient, newSecret } from '@hashlock-tech/sdk'; -```ts -const claimed = await hl.claimHTLC({ - tradeId: 'trade-uuid', - txHash: '0xclaim...', - preimage: '0xsecret...', - chainType: 'evm', -}); -``` +const client = new HashlockClient({ apiKey: process.env.HASHLOCK_API_KEY! }); // hk_test_… / hk_live_… -### Refund (after timelock expiry) +const assets = await client.assets(); +const btc = assets.find((a) => a.symbol === 'BTC')!; +const usdt = assets.find((a) => a.chain === 'ethereum' && a.symbol === 'USDT')!; -```ts -const refunded = await hl.refundHTLC({ - tradeId: 'trade-uuid', - txHash: '0xrefund...', +// Taker: sell BTC for USDT (funds the long leg → is the initiator). +const rfq = await client.createRfq({ + direction: 'sell_base', + baseAssetId: btc.id, + baseAmount: '10000', // sats + quoteAssetId: usdt.id, + ttlSeconds: 3600, }); -``` -### Check HTLC Status - -```ts -const status = await hl.getHTLCStatus('trade-uuid'); -console.log(status?.initiatorHTLC?.status); // 'ACTIVE' -console.log(status?.counterpartyHTLC?.status); // 'PENDING' +// …a maker quotes it, both accept (the initiator passes hashlock = sha256(secret)), +// then each side funds/claims its leg with the unsigned-tx builders. +const { hashlock } = await newSecret(); ``` -## HTLC Settlement — Bitcoin +See [`examples/quickstart.ts`](./examples/quickstart.ts) for the full lifecycle, and +[`examples/fireblocks`](./examples/fireblocks) for signing the unsigned transactions with a custody +provider. -Bitcoin HTLCs use P2WSH scripts (no smart contract deployment needed). +## How a swap works -### Prepare a Bitcoin HTLC +1. **RFQ → quote → accept.** A taker posts an RFQ; a maker quotes it (opening a negotiation thread); both + accept the terms. The **initiator** (whoever funds the long-timelock leg) generates a secret locally and + submits only `hashlock = sha256(secret)`. When both accept, the swap is created. +2. **Addresses.** Each side sets its receive (payout) / refund address per leg (`setSwapAddress`). Bitcoin + uses the compressed pubkey — the server derives the P2WSH. +3. **Fund.** The initiator funds the long leg; the counterparty funds the short leg. Each `buildFund` call + returns an *unsigned* transaction (EVM txs / a Bitcoin payment / TRON txs) — you sign and broadcast. +4. **Claim.** The initiator claims the short leg with the secret — revealing it on-chain. The counterparty + reads the secret and claims the long leg. **Atomic:** both legs settle, or both refund after their + timelocks. -```ts -const btcHtlc = await hl.prepareBitcoinHTLC({ - tradeId: 'trade-uuid', - role: 'INITIATOR', - senderPubKey: '02abc...', // 33-byte compressed pubkey - receiverPubKey: '03def...', - timelock: Math.floor(Date.now() / 1000) + 7200, - amountSats: '100000', // 0.001 BTC -}); - -console.log(`Send BTC to: ${btcHtlc.htlcAddress}`); -// Fund this P2WSH address with your Bitcoin wallet -``` +The same `sha256(secret)` hashlock binds every leg; asymmetric timelocks (long leg ≫ short leg) remove the +free-option/griefing risk. -### Claim a Bitcoin HTLC +## Core surface -```ts -// Build unsigned PSBT -const psbt = await hl.buildBitcoinClaimPSBT({ - tradeId: 'trade-uuid', - htlcId: btcHtlc.htlcId, - preimage: '0xsecret...', - destinationPubKey: '02abc...', - feeRate: 10, // sat/vB -}); +- **Auth:** API key (`Authorization: Bearer hk_…` or `X-Api-Key`), scopes `read | taker | maker`. +- **Market:** `assets()`, `listRfqs()` / `rfqs()` (cursor-paginated), `getRfq()`, `createRfq()`, `quoteRfq()`. +- **Negotiation:** `getThread()`, `proposeTerms()`, `acceptProposal()`, `acceptTerms()`. +- **Swaps:** `listSwaps()` / `swaps()`, `getSwap()`, `setSwapAddress()`. +- **Settlement (unsigned):** `buildFund()`, `buildClaim()`, `buildRefund()`, `broadcast()`. +- **Webhooks:** `createWebhook()`, `listWebhooks()`, `deleteWebhook()`, `pingWebhook()` + `verifyWebhook()`. +- **Maker feed:** `MakerFeed` — stream quotable RFQs and submit quotes over a WebSocket. -// Sign with wallet (Xverse, Leather, UniSat, etc.) -const signedTx = await wallet.signPsbt(psbt.psbtBase64); - -// Broadcast -const broadcast = await hl.broadcastBitcoinTx({ - tradeId: 'trade-uuid', - txHex: signedTx, -}); -console.log(`BTC claimed: ${broadcast.txid}`); -``` - -## Cross-Chain Atomic Swap (ETH ↔ BTC) - -```ts -// Alice (ETH side) locks USDT on Ethereum -await hl.fundHTLC({ - tradeId, txHash: evmTxHash, role: 'INITIATOR', - hashlock, timelock: now + 7200, chainType: 'evm', -}); - -// Bob (BTC side) locks BTC on Bitcoin -const btc = await hl.prepareBitcoinHTLC({ - tradeId, role: 'COUNTERPARTY', - senderPubKey: bobPub, receiverPubKey: alicePub, - timelock: now + 3600, amountSats: '100000', -}); -// Bob funds the P2WSH address, then: -await hl.fundHTLC({ - tradeId, txHash: btcFundingTxid, role: 'COUNTERPARTY', - chainType: 'bitcoin', redeemScript: btc.redeemScript, -}); - -// Alice claims BTC (reveals preimage) -// Bob sees preimage on-chain → claims USDT on Ethereum -// Trade complete! -``` - -## Error Handling - -```ts -import { HashLockError, GraphQLError, AuthError, NetworkError } from '@hashlock/sdk'; - -try { - await hl.getTrade('bad-id'); -} catch (err) { - if (err instanceof AuthError) { - // Token expired — refresh and retry - } else if (err instanceof GraphQLError) { - console.error('API error:', err.errors); - } else if (err instanceof NetworkError) { - console.error('Network issue:', err.message); - } -} -``` - -## Configuration - -```ts -const hl = new HashLock({ - endpoint: 'http://142.93.106.129/graphql', // mainnet - accessToken: 'jwt-token', - timeout: 30000, // 30s (default) - retries: 3, // retry count (default) -}); -``` - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `endpoint` | `string` | — | GraphQL API URL (required) | -| `accessToken` | `string` | — | JWT bearer token | -| `timeout` | `number` | `30000` | Request timeout (ms) | -| `retries` | `number` | `3` | Retry attempts for transient failures | -| `fetch` | `typeof fetch` | `globalThis.fetch` | Custom fetch implementation | - -## Mainnet Contracts (Ethereum) - -| Contract | Address | -|----------|---------| -| HashedTimelockEther | [`0x0CEDC56b17d714dA044954EE26F38e90eC10434A`](https://etherscan.io/address/0x0cedc56b17d714da044954ee26f38e90ec10434a) | -| HashedTimelockEtherFee | [`0xfBAEA1423b5FBeCE89998da6820902fD8f159014`](https://etherscan.io/address/0xfbaea1423b5fbece89998da6820902fd8f159014) | -| HashedTimelockERC20Fee | [`0x4B65490D140Bab3DB828C2386e21646Ed8c4D072`](https://etherscan.io/address/0x4b65490d140bab3db828c2386e21646ed8c4d072) | - -## Development — vendored schema & drift guard - -Every GraphQL operation string the SDK sends is validated in CI against a -**vendored copy of the authoritative trade-service SDL** -(`test/fixtures/schema.graphql` for queries/mutations, -`test/fixtures/schema.subscriptions.graphql` for the graphql-ws -subscription schema) — see `src/__tests__/schema-validate.test.ts`. The -`graphql` package is a **devDependency only**; the published SDK keeps zero -runtime dependencies. - -When the backend schema changes, refresh the fixtures from the main repo's -`origin/main` and re-run the tests: - -```bash -git -C ../Cayman-Hashlock fetch origin -node scripts/vendor-schema.mjs ../Cayman-Hashlock # optional: [path] [git-ref] -pnpm test -``` - -The fixture headers record the source path, git ref and commit SHA they -were vendored from. Never edit the fixtures by hand. +Interactive API reference: `https://api-dev.hashlock.markets/v1/docs` (OpenAPI at `/v1/openapi.json`). ## License -MIT - - -## About Hashlock Markets - -Hashlock Markets (`hashlock.markets`) is operated by Hashlock Corp., a Delaware C-Corporation. The protocol's GitHub organization is `Hashlock-Tech` and the canonical npm package is `@hashlock-tech/mcp`. Hashlock Markets is **not affiliated with Hashlock Pty Ltd** (`hashlock.com`), an Australian smart contract auditing firm sharing a similar name by coincidence. - -For more on the protocol: [hashlock.markets](https://hashlock.markets) · [Documentation](https://hashlock.markets/docs) · [llms.txt](https://hashlock.markets/llms.txt) · [MCP Registry](https://registry.modelcontextprotocol.io) · [All Hashlock-Tech repos](https://github.com/Hashlock-Tech) +MIT — see [LICENSE](./LICENSE). diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md deleted file mode 100644 index 22c4f37..0000000 --- a/docs/architecture/ARCHITECTURE.md +++ /dev/null @@ -1,303 +0,0 @@ - - -# 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 deleted file mode 100644 index 4b9cd4d..0000000 --- a/docs/architecture/ARCHITECTURE.ru.md +++ /dev/null @@ -1,308 +0,0 @@ - - -# 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).* diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..6c1ff33 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,9 @@ +# Examples + +- **`quickstart.ts` / `quickstart.py`** — a full BTC ↔ USDT swap through the developer API. Signing is + yours; the settlement builders return unsigned transactions. +- **`fireblocks/`** — reference for signing those unsigned transactions with a custody provider + (Fireblocks / Copper). Documented pattern, not a runnable integration test. + +All examples target the sandbox (`https://api-dev.hashlock.markets/v1`) by default. Set +`HASHLOCK_API_KEY` (and `HASHLOCK_API_URL` to point at another environment). diff --git a/examples/fireblocks/README.md b/examples/fireblocks/README.md new file mode 100644 index 0000000..e8d9849 --- /dev/null +++ b/examples/fireblocks/README.md @@ -0,0 +1,36 @@ +# Signing with Fireblocks / Copper (reference) + +The Hashlock developer API is **custody-agnostic**: `POST /v1/swaps/:id/legs/:leg/{fund,claim,refund}` +returns *unsigned* transactions, and you sign them with your own key material. This directory shows how to +sign them with an institutional custodian (Fireblocks; Copper follows the same shape). + +> ⚠️ Reference only — documented pattern, not a live-tested integration. Fill in your vault / account IDs +> and asset IDs, and validate against a testnet vault before production. + +## Two signing models + +The settlement build tells you which one applies via its `sign` field: + +| `sign` | Chain | What the build gives you | How to sign | +|----------------|-----------|-----------------------------------------------------------|----------------------------------------------------| +| `evm-tx` | EVM | `txs: [{ to, data, value? }]`, `chainId` | Fireblocks **CONTRACT_CALL** (it signs *and* broadcasts) — or raw-sign + `client.broadcast('evm', rawTx)` | +| `tron-txid` | TRON | `transactions: [{ transaction, txID }]` | **Raw-sign** each `txID` (secp256k1), attach the signature, `client.broadcast('tron', signedTx)` | +| `btc-payment` | Bitcoin | `payTo`, `amountSats` | A normal transfer from your BTC vault to `payTo` | +| `btc-witness` | Bitcoin | `p2wsh`, `redeemHex`, `preimageHex` / `timelockUnix` | **Raw-sign** the BIP-143 sighash, assemble the witness, `client.broadcast('bitcoin', rawHex)` | + +**Custody providers that broadcast for you** (Fireblocks CONTRACT_CALL, a Bitcoin transfer): you do *not* +call `client.broadcast` — the provider submits the transaction and returns the on-chain hash. + +**Raw-signing (HSM / Fireblocks RAW):** you get back a signature, assemble the final transaction yourself, +and relay it with `client.broadcast(chain, signed)`. + +## Ordering & timelocks + +- **TRON** funding is `approve` **then** `fund`; the `fund` transaction's `transferFrom` needs the + `approve` **confirmed first**. TRON transactions expire ~60s after they're built, so submit `approve`, + wait for it to confirm, then **re-fetch** `buildFund` for a fresh `fund` transaction before signing it. +- Fund the **long** leg first, then the counterparty funds the short leg. The initiator claims the short + leg (revealing the secret); the counterparty then claims the long leg. Refund is available per-leg after + its timelock. + +See `sign_evm.ts` for the Fireblocks CONTRACT_CALL flow (EVM `fund` = approve + createSwap). diff --git a/examples/fireblocks/sign_evm.py b/examples/fireblocks/sign_evm.py new file mode 100644 index 0000000..8fc958b --- /dev/null +++ b/examples/fireblocks/sign_evm.py @@ -0,0 +1,47 @@ +"""Reference: sign an EVM settlement build with Fireblocks (CONTRACT_CALL). + +Fireblocks signs AND broadcasts via its own nodes, so you do NOT call client.broadcast for this path. + + pip install hashlock-sdk fireblocks-sdk + +Not live-tested — set your vault account id + asset id and validate on a testnet vault first. +""" +import os +import time + +from fireblocks_sdk import FireblocksSDK, TransferPeerPath, DestinationTransferPeerPath, PeerType + +from hashlock import HashlockClient + +client = HashlockClient(api_key=os.environ["HASHLOCK_API_KEY"]) +fireblocks = FireblocksSDK(os.environ["FIREBLOCKS_API_SECRET"], os.environ["FIREBLOCKS_API_KEY"]) + +VAULT_ACCOUNT_ID = os.environ["FIREBLOCKS_VAULT_ID"] +ASSET_ID = "ETH_TEST5" # Fireblocks asset id for the target chain (Sepolia here) + + +def sign_via_fireblocks(tx: dict) -> str: + """Submit one unsigned EVM tx as a Fireblocks contract call; return the on-chain hash.""" + resp = fireblocks.create_transaction( + operation="CONTRACT_CALL", + asset_id=ASSET_ID, + source=TransferPeerPath(PeerType.VAULT_ACCOUNT, VAULT_ACCOUNT_ID), + destination=DestinationTransferPeerPath(PeerType.ONE_TIME_ADDRESS, one_time_address={"address": tx["to"]}), + amount=str(int(tx.get("value") or "0")), # wei; "0" for approve / createSwap + extra_parameters={"contractCallData": tx["data"]}, + note="Hashlock HTLC settlement", + ) + tx_id = resp["id"] + while True: + t = fireblocks.get_transaction_by_id(tx_id) + if t.get("txHash"): + return t["txHash"] + if t["status"] in ("FAILED", "BLOCKED", "CANCELLED", "REJECTED"): + raise RuntimeError(f"Fireblocks tx {tx_id} {t['status']}: {t.get('subStatus')}") + time.sleep(3) + + +def fund_evm_leg(swap_id: str, leg: str) -> None: + build = client.build_fund(swap_id, leg) # {'sign': 'evm-tx', 'txs': [approve, createSwap], ...} + for tx in build["txs"]: + print("fireblocks broadcast", sign_via_fireblocks(tx)) diff --git a/examples/fireblocks/sign_evm.ts b/examples/fireblocks/sign_evm.ts new file mode 100644 index 0000000..a91a10e --- /dev/null +++ b/examples/fireblocks/sign_evm.ts @@ -0,0 +1,48 @@ +/** + * Reference: sign an EVM settlement build with Fireblocks (CONTRACT_CALL). Fireblocks signs AND broadcasts + * via its own nodes, so you do NOT call client.broadcast for this path — you poll Fireblocks for the hash. + * + * npm i @hashlock-tech/sdk fireblocks-sdk + * + * Not live-tested — set your vault account id + asset id and validate on a testnet vault first. + */ +import { HashlockClient, type EvmBuild } from '@hashlock-tech/sdk'; +import { FireblocksSDK, PeerType, TransactionOperation, TransactionStatus } from 'fireblocks-sdk'; + +const client = new HashlockClient({ apiKey: process.env.HASHLOCK_API_KEY! }); +const fireblocks = new FireblocksSDK(process.env.FIREBLOCKS_API_SECRET!, process.env.FIREBLOCKS_API_KEY!); + +const VAULT_ACCOUNT_ID = process.env.FIREBLOCKS_VAULT_ID!; // your vault account +const ASSET_ID = 'ETH_TEST5'; // Fireblocks asset id for the target chain (Sepolia here) + +/** Submit one unsigned EVM tx as a Fireblocks contract call and wait for the on-chain hash. */ +async function signViaFireblocks(tx: { to: string; data: string; value?: string }): Promise { + const { id } = await fireblocks.createTransaction({ + operation: TransactionOperation.CONTRACT_CALL, + assetId: ASSET_ID, + source: { type: PeerType.VAULT_ACCOUNT, id: VAULT_ACCOUNT_ID }, + destination: { type: PeerType.ONE_TIME_ADDRESS, oneTimeAddress: { address: tx.to } }, + amount: tx.value ? String(BigInt(tx.value)) : '0', // wei; '0' for ERC-20 approve / createSwap + extraParameters: { contractCallData: tx.data }, + note: 'Hashlock HTLC settlement', + }); + + // Poll to completion. Fireblocks broadcasts; the tx hash appears on SUBMITTED/COMPLETED. + for (;;) { + const t = await fireblocks.getTransactionById(id); + if (t.txHash) return t.txHash; + if ([TransactionStatus.FAILED, TransactionStatus.BLOCKED, TransactionStatus.CANCELLED, TransactionStatus.REJECTED].includes(t.status)) { + throw new Error(`Fireblocks tx ${id} ${t.status}: ${t.subStatus}`); + } + await new Promise((r) => setTimeout(r, 3000)); + } +} + +export async function fundEvmLeg(swapId: string, leg: 'a' | 'b') { + const build = (await client.buildFund(swapId, leg)) as EvmBuild; + // EVM fund = [approve, createSwap]; submit in order, waiting for each (approve must confirm before createSwap). + for (const tx of build.txs) { + const hash = await signViaFireblocks(tx); + console.log('fireblocks broadcast', hash); + } +} diff --git a/examples/quickstart.py b/examples/quickstart.py new file mode 100644 index 0000000..edafd9e --- /dev/null +++ b/examples/quickstart.py @@ -0,0 +1,74 @@ +"""Quickstart: a full BTC <-> USDT(EVM) swap driven through the developer API. + + pip install hashlock-sdk + HASHLOCK_API_KEY=hk_test_... python examples/quickstart.py + +Signing is YOURS — the settlement builders return UNSIGNED transactions; sign with your wallet or HSM +(see examples/fireblocks) and either broadcast via client.broadcast(...) or let your custody provider +broadcast. The server never holds your keys. +""" +import os +import time + +from hashlock import HashlockClient, new_secret + + +def sign_and_broadcast(build: dict) -> str: + """Sign an unsigned settlement build with your own key/HSM and return a txid. See examples/fireblocks.""" + raise NotImplementedError("plug in your signer (wallet / Fireblocks / Copper)") + + +def main() -> None: + client = HashlockClient( + api_key=os.environ["HASHLOCK_API_KEY"], + base_url=os.environ.get("HASHLOCK_API_URL", "https://api-dev.hashlock.markets/v1"), + ) + me = client.me() + print("authenticated:", me["userId"], me["scopes"]) + + assets = client.assets() + btc = next(a for a in assets if a["chain"] == "bitcoin" and a["symbol"] == "BTC") + usdt = next(a for a in assets if a["chain"] == "ethereum" and a["symbol"] == "USDT") + + # 1) taker: sell BTC for USDT (the taker funds the long BTC leg -> is the initiator). + rfq = client.create_rfq( + direction="sell_base", + base_asset_id=btc["id"], + base_amount="10000", # sats + quote_asset_id=usdt["id"], + ttl_seconds=3600, + ) + print("rfq", rfq["id"]) + + # 2) maker (a different account) quotes it -> opens a thread: + # thread = maker_client.quote_rfq(rfq["id"], "650000")["thread"] + thread_id = "" + + # 3) accept. The initiator supplies hashlock = sha256(secret). + secret, hashlock = new_secret() # keep `secret` private until you claim your leg + client.accept_terms(thread_id, hashlock=hashlock) + swap = client.accept_terms(thread_id)["swap"] # created once BOTH sides have accepted + + # 4) set receive/refund addresses (Bitcoin uses the compressed pubkey hex). + client.set_swap_address(swap["id"], "ethereum", "0xYourUsdtPayoutAddress") + client.set_swap_address(swap["id"], "bitcoin", "") + + # 5) fund the long (BTC) leg. + sign_and_broadcast(client.build_fund(swap["id"], "b")) + + # 6) after the counterparty funds the USDT leg, claim it with the secret (reveals it on-chain). + sign_and_broadcast(client.build_claim(swap["id"], "a", secret)) + + last = "" + while True: + s = client.get_swap(swap["id"]) + if s["status"] != last: + last = s["status"] + print("status:", last) + if last in ("counterparty_claimed", "refunded"): + break + time.sleep(4) + + +if __name__ == "__main__": + main() diff --git a/examples/quickstart.ts b/examples/quickstart.ts new file mode 100644 index 0000000..cdae6f0 --- /dev/null +++ b/examples/quickstart.ts @@ -0,0 +1,80 @@ +/** + * Quickstart: a full BTC ↔ USDT(EVM) swap driven entirely through the developer API. + * + * npm i @hashlock-tech/sdk + * HASHLOCK_API_KEY=hk_test_… npx tsx examples/quickstart.ts + * + * This shows the API flow end-to-end. Signing is YOURS — the settlement builders return UNSIGNED + * transactions; sign them with your wallet or HSM (see examples/fireblocks) and either broadcast via + * client.broadcast(...) or let your custody provider broadcast. The server never holds your keys. + */ +import { HashlockClient, newSecret } from '@hashlock-tech/sdk'; + +const client = new HashlockClient({ + apiKey: process.env.HASHLOCK_API_KEY!, + baseUrl: process.env.HASHLOCK_API_URL ?? 'https://api-dev.hashlock.markets/v1', +}); + +// Sign an unsigned settlement build with your own key/HSM, then return a txid. +// EVM → sign each tx and broadcast; TRON → sign each txID; Bitcoin → build+sign the P2WSH spend. +// See examples/fireblocks for a custody-provider reference. +declare function signAndBroadcast(build: unknown): Promise; + +async function main() { + const me = await client.me(); + console.log('authenticated:', me.userId, me.scopes); + + const assets = await client.assets(); + const btc = assets.find((a) => a.chain === 'bitcoin' && a.symbol === 'BTC')!; + const usdt = assets.find((a) => a.chain === 'ethereum' && a.symbol === 'USDT')!; + + // 1) taker: create an RFQ to sell BTC for USDT (the taker funds the long BTC leg → is the initiator). + const rfq = await client.createRfq({ + direction: 'sell_base', + baseAssetId: btc.id, + baseAmount: '10000', // sats + quoteAssetId: usdt.id, + ttlSeconds: 3600, + }); + console.log('rfq', rfq.id); + + // 2) maker (a different account/key) quotes it → opens a settlement thread. + // const { thread } = await makerClient.quoteRfq(rfq.id, '650000'); // 0.65 USDT (6 decimals) + const threadId = ''; + + // 3) accept. The initiator (funds the long leg) supplies hashlock = sha256(secret). + const { secret, hashlock } = await newSecret(); // keep `secret` private until you claim your leg + await client.acceptTerms(threadId, { hashlock }); + // …the maker also accepts; when BOTH accept, the swap is created: + const swap = (await client.acceptTerms(threadId)).swap!; // returns the swap once both sides accepted + + // 4) set your receive/refund addresses. Bitcoin uses the compressed pubkey (hex). + await client.setSwapAddress(swap.id, 'ethereum', '0xYourUsdtPayoutAddress'); // taker receives USDT + await client.setSwapAddress(swap.id, 'bitcoin', ''); // taker's BTC refund + + // 5) fund the long (BTC) leg — build → sign → broadcast. + const fundBuild = await client.buildFund(swap.id, 'b'); // leg 'b' = BTC here + await signAndBroadcast(fundBuild); + + // 6) once the counterparty funds the USDT leg (poll getSwap until 'counterparty_funded'), + // claim your USDT leg with the secret — this reveals it on-chain. + const claimBuild = await client.buildClaim(swap.id, 'a', secret); // leg 'a' = USDT here + await signAndBroadcast(claimBuild); + + // The counterparty then claims the BTC leg using the now-public secret. Track it live: + for await (const s of pollSwap(client, swap.id)) { + console.log('status:', s.status); + if (s.status === 'counterparty_claimed' || s.status === 'refunded') break; + } +} + +async function* pollSwap(c: HashlockClient, id: string) { + let last = ''; + for (;;) { + const s = await c.getSwap(id); + if (s.status !== last) { last = s.status; yield s; } + await new Promise((r) => setTimeout(r, 4000)); + } +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 5df6f75..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2421 +0,0 @@ -{ - "name": "@hashlock-tech/sdk", - "version": "0.3.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@hashlock-tech/sdk", - "version": "0.3.0", - "license": "MIT", - "devDependencies": { - "tsup": "^8.0.0", - "typescript": "^5.4.0", - "vitest": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", - "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", - "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", - "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", - "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", - "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", - "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", - "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", - "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", - "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", - "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", - "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", - "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", - "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", - "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", - "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", - "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", - "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", - "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", - "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", - "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", - "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", - "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", - "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", - "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", - "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.9", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/bundle-require": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "load-tsconfig": "^0.2.3" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.18" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fix-dts-default-cjs-exports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", - "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "rollup": "^4.34.8" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/mlly": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", - "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/rollup": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", - "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.2", - "@rollup/rollup-android-arm64": "4.60.2", - "@rollup/rollup-darwin-arm64": "4.60.2", - "@rollup/rollup-darwin-x64": "4.60.2", - "@rollup/rollup-freebsd-arm64": "4.60.2", - "@rollup/rollup-freebsd-x64": "4.60.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", - "@rollup/rollup-linux-arm-musleabihf": "4.60.2", - "@rollup/rollup-linux-arm64-gnu": "4.60.2", - "@rollup/rollup-linux-arm64-musl": "4.60.2", - "@rollup/rollup-linux-loong64-gnu": "4.60.2", - "@rollup/rollup-linux-loong64-musl": "4.60.2", - "@rollup/rollup-linux-ppc64-gnu": "4.60.2", - "@rollup/rollup-linux-ppc64-musl": "4.60.2", - "@rollup/rollup-linux-riscv64-gnu": "4.60.2", - "@rollup/rollup-linux-riscv64-musl": "4.60.2", - "@rollup/rollup-linux-s390x-gnu": "4.60.2", - "@rollup/rollup-linux-x64-gnu": "4.60.2", - "@rollup/rollup-linux-x64-musl": "4.60.2", - "@rollup/rollup-openbsd-x64": "4.60.2", - "@rollup/rollup-openharmony-arm64": "4.60.2", - "@rollup/rollup-win32-arm64-msvc": "4.60.2", - "@rollup/rollup-win32-ia32-msvc": "4.60.2", - "@rollup/rollup-win32-x64-gnu": "4.60.2", - "@rollup/rollup-win32-x64-msvc": "4.60.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tsup": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", - "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-require": "^5.1.0", - "cac": "^6.7.14", - "chokidar": "^4.0.3", - "consola": "^3.4.0", - "debug": "^4.4.0", - "esbuild": "^0.27.0", - "fix-dts-default-cjs-exports": "^1.0.0", - "joycon": "^3.1.1", - "picocolors": "^1.1.1", - "postcss-load-config": "^6.0.1", - "resolve-from": "^5.0.0", - "rollup": "^4.34.8", - "source-map": "^0.7.6", - "sucrase": "^3.35.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.11", - "tree-kill": "^1.2.2" - }, - "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@microsoft/api-extractor": "^7.36.0", - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": ">=4.5.0" - }, - "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "postcss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-node/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 4b2ace2..0000000 --- a/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@hashlock-tech/sdk", - "version": "0.3.0", - "description": "TypeScript SDK for HashLock OTC — HTLC atomic settlement, RFQ trading, and Bitcoin cross-chain swaps", - "license": "MIT", - "type": "module", - "main": "./dist/index.cjs", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - }, - "require": { - "types": "./dist/index.d.cts", - "default": "./dist/index.cjs" - } - } - }, - "files": [ - "dist", - "README.md", - "LICENSE" - ], - "scripts": { - "build": "tsup", - "dev": "tsup --watch", - "test": "vitest run", - "test:watch": "vitest", - "lint": "tsc --noEmit", - "prepublishOnly": "npm run build" - }, - "devDependencies": { - "graphql": "^16", - "tsup": "^8.0.0", - "typescript": "^5.4.0", - "vitest": "^2.0.0" - }, - "keywords": [ - "hashlock", - "htlc", - "otc", - "atomic-swap", - "ethereum", - "bitcoin", - "settlement" - ], - "repository": { - "type": "git", - "url": "https://github.com/Hashlock-Tech/hashlock-sdk" - }, - "engines": { - "node": ">=18" - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 491e864..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,1474 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - devDependencies: - graphql: - specifier: ^16 - version: 16.14.2 - tsup: - specifier: ^8.0.0 - version: 8.5.1(postcss@8.5.9)(typescript@5.9.3) - typescript: - specifier: ^5.4.0 - version: 5.9.3 - vitest: - specifier: ^2.0.0 - version: 2.1.9 - -packages: - - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@rollup/rollup-android-arm-eabi@4.60.1': - resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.60.1': - resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.60.1': - resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.60.1': - resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.60.1': - resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.60.1': - resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.60.1': - resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.60.1': - resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loong64-gnu@4.60.1': - resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-loong64-musl@4.60.1': - resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-ppc64-musl@4.60.1': - resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.60.1': - resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.60.1': - resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.60.1': - resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.60.1': - resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-openbsd-x64@4.60.1': - resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.60.1': - resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.60.1': - resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.60.1': - resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.60.1': - resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.60.1': - resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} - cpu: [x64] - os: [win32] - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@vitest/expect@2.1.9': - resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} - - '@vitest/mocker@2.1.9': - resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@2.1.9': - resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} - - '@vitest/runner@2.1.9': - resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} - - '@vitest/snapshot@2.1.9': - resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} - - '@vitest/spy@2.1.9': - resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} - - '@vitest/utils@2.1.9': - resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} - - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - bundle-require@5.1.0: - resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: '>=0.18' - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - chai@5.3.3: - resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} - engines: {node: '>=18'} - - check-error@2.1.3: - resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} - engines: {node: '>= 16'} - - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - - consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} - engines: {node: '>=18'} - hasBin: true - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} - engines: {node: '>=12.0.0'} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fix-dts-default-cjs-exports@1.0.1: - resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - graphql@16.14.2: - resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} - engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - - joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} - engines: {node: '>=10'} - - lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - mlly@1.8.2: - resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - pathval@2.0.1: - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} - engines: {node: '>= 14.16'} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true - - postcss@8.5.9: - resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} - engines: {node: ^10 || ^12 || >=14} - - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - - rollup@4.60.1: - resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - - sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} - engines: {node: '>=12.0.0'} - - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyrainbow@1.2.0: - resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} - engines: {node: '>=14.0.0'} - - tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} - engines: {node: '>=14.0.0'} - - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - - tsup@8.5.1: - resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - '@microsoft/api-extractor': ^7.36.0 - '@swc/core': ^1 - postcss: ^8.4.12 - typescript: '>=4.5.0' - peerDependenciesMeta: - '@microsoft/api-extractor': - optional: true - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - ufo@1.6.3: - resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} - - vite-node@2.1.9: - resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - - vite@5.4.21: - resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - - vitest@2.1.9: - resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.1.9 - '@vitest/ui': 2.1.9 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - -snapshots: - - '@esbuild/aix-ppc64@0.21.5': - optional: true - - '@esbuild/aix-ppc64@0.27.7': - optional: true - - '@esbuild/android-arm64@0.21.5': - optional: true - - '@esbuild/android-arm64@0.27.7': - optional: true - - '@esbuild/android-arm@0.21.5': - optional: true - - '@esbuild/android-arm@0.27.7': - optional: true - - '@esbuild/android-x64@0.21.5': - optional: true - - '@esbuild/android-x64@0.27.7': - optional: true - - '@esbuild/darwin-arm64@0.21.5': - optional: true - - '@esbuild/darwin-arm64@0.27.7': - optional: true - - '@esbuild/darwin-x64@0.21.5': - optional: true - - '@esbuild/darwin-x64@0.27.7': - optional: true - - '@esbuild/freebsd-arm64@0.21.5': - optional: true - - '@esbuild/freebsd-arm64@0.27.7': - optional: true - - '@esbuild/freebsd-x64@0.21.5': - optional: true - - '@esbuild/freebsd-x64@0.27.7': - optional: true - - '@esbuild/linux-arm64@0.21.5': - optional: true - - '@esbuild/linux-arm64@0.27.7': - optional: true - - '@esbuild/linux-arm@0.21.5': - optional: true - - '@esbuild/linux-arm@0.27.7': - optional: true - - '@esbuild/linux-ia32@0.21.5': - optional: true - - '@esbuild/linux-ia32@0.27.7': - optional: true - - '@esbuild/linux-loong64@0.21.5': - optional: true - - '@esbuild/linux-loong64@0.27.7': - optional: true - - '@esbuild/linux-mips64el@0.21.5': - optional: true - - '@esbuild/linux-mips64el@0.27.7': - optional: true - - '@esbuild/linux-ppc64@0.21.5': - optional: true - - '@esbuild/linux-ppc64@0.27.7': - optional: true - - '@esbuild/linux-riscv64@0.21.5': - optional: true - - '@esbuild/linux-riscv64@0.27.7': - optional: true - - '@esbuild/linux-s390x@0.21.5': - optional: true - - '@esbuild/linux-s390x@0.27.7': - optional: true - - '@esbuild/linux-x64@0.21.5': - optional: true - - '@esbuild/linux-x64@0.27.7': - optional: true - - '@esbuild/netbsd-arm64@0.27.7': - optional: true - - '@esbuild/netbsd-x64@0.21.5': - optional: true - - '@esbuild/netbsd-x64@0.27.7': - optional: true - - '@esbuild/openbsd-arm64@0.27.7': - optional: true - - '@esbuild/openbsd-x64@0.21.5': - optional: true - - '@esbuild/openbsd-x64@0.27.7': - optional: true - - '@esbuild/openharmony-arm64@0.27.7': - optional: true - - '@esbuild/sunos-x64@0.21.5': - optional: true - - '@esbuild/sunos-x64@0.27.7': - optional: true - - '@esbuild/win32-arm64@0.21.5': - optional: true - - '@esbuild/win32-arm64@0.27.7': - optional: true - - '@esbuild/win32-ia32@0.21.5': - optional: true - - '@esbuild/win32-ia32@0.27.7': - optional: true - - '@esbuild/win32-x64@0.21.5': - optional: true - - '@esbuild/win32-x64@0.27.7': - optional: true - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@rollup/rollup-android-arm-eabi@4.60.1': - optional: true - - '@rollup/rollup-android-arm64@4.60.1': - optional: true - - '@rollup/rollup-darwin-arm64@4.60.1': - optional: true - - '@rollup/rollup-darwin-x64@4.60.1': - optional: true - - '@rollup/rollup-freebsd-arm64@4.60.1': - optional: true - - '@rollup/rollup-freebsd-x64@4.60.1': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-x64-musl@4.60.1': - optional: true - - '@rollup/rollup-openbsd-x64@4.60.1': - optional: true - - '@rollup/rollup-openharmony-arm64@4.60.1': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.60.1': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.60.1': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.60.1': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.60.1': - optional: true - - '@types/estree@1.0.8': {} - - '@vitest/expect@2.1.9': - dependencies: - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.3.3 - tinyrainbow: 1.2.0 - - '@vitest/mocker@2.1.9(vite@5.4.21)': - dependencies: - '@vitest/spy': 2.1.9 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 5.4.21 - - '@vitest/pretty-format@2.1.9': - dependencies: - tinyrainbow: 1.2.0 - - '@vitest/runner@2.1.9': - dependencies: - '@vitest/utils': 2.1.9 - pathe: 1.1.2 - - '@vitest/snapshot@2.1.9': - dependencies: - '@vitest/pretty-format': 2.1.9 - magic-string: 0.30.21 - pathe: 1.1.2 - - '@vitest/spy@2.1.9': - dependencies: - tinyspy: 3.0.2 - - '@vitest/utils@2.1.9': - dependencies: - '@vitest/pretty-format': 2.1.9 - loupe: 3.2.1 - tinyrainbow: 1.2.0 - - acorn@8.16.0: {} - - any-promise@1.3.0: {} - - assertion-error@2.0.1: {} - - bundle-require@5.1.0(esbuild@0.27.7): - dependencies: - esbuild: 0.27.7 - load-tsconfig: 0.2.5 - - cac@6.7.14: {} - - chai@5.3.3: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.3 - deep-eql: 5.0.2 - loupe: 3.2.1 - pathval: 2.0.1 - - check-error@2.1.3: {} - - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - - commander@4.1.1: {} - - confbox@0.1.8: {} - - consola@3.4.2: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - deep-eql@5.0.2: {} - - es-module-lexer@1.7.0: {} - - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - - esbuild@0.27.7: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - expect-type@1.3.0: {} - - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - - fix-dts-default-cjs-exports@1.0.1: - dependencies: - magic-string: 0.30.21 - mlly: 1.8.2 - rollup: 4.60.1 - - fsevents@2.3.3: - optional: true - - graphql@16.14.2: {} - - joycon@3.1.1: {} - - lilconfig@3.1.3: {} - - lines-and-columns@1.2.4: {} - - load-tsconfig@0.2.5: {} - - loupe@3.2.1: {} - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - mlly@1.8.2: - dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.3 - - ms@2.1.3: {} - - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - - nanoid@3.3.11: {} - - object-assign@4.1.1: {} - - pathe@1.1.2: {} - - pathe@2.0.3: {} - - pathval@2.0.1: {} - - picocolors@1.1.1: {} - - picomatch@4.0.4: {} - - pirates@4.0.7: {} - - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.2 - pathe: 2.0.3 - - postcss-load-config@6.0.1(postcss@8.5.9): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - postcss: 8.5.9 - - postcss@8.5.9: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - readdirp@4.1.2: {} - - resolve-from@5.0.0: {} - - rollup@4.60.1: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.1 - '@rollup/rollup-android-arm64': 4.60.1 - '@rollup/rollup-darwin-arm64': 4.60.1 - '@rollup/rollup-darwin-x64': 4.60.1 - '@rollup/rollup-freebsd-arm64': 4.60.1 - '@rollup/rollup-freebsd-x64': 4.60.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 - '@rollup/rollup-linux-arm-musleabihf': 4.60.1 - '@rollup/rollup-linux-arm64-gnu': 4.60.1 - '@rollup/rollup-linux-arm64-musl': 4.60.1 - '@rollup/rollup-linux-loong64-gnu': 4.60.1 - '@rollup/rollup-linux-loong64-musl': 4.60.1 - '@rollup/rollup-linux-ppc64-gnu': 4.60.1 - '@rollup/rollup-linux-ppc64-musl': 4.60.1 - '@rollup/rollup-linux-riscv64-gnu': 4.60.1 - '@rollup/rollup-linux-riscv64-musl': 4.60.1 - '@rollup/rollup-linux-s390x-gnu': 4.60.1 - '@rollup/rollup-linux-x64-gnu': 4.60.1 - '@rollup/rollup-linux-x64-musl': 4.60.1 - '@rollup/rollup-openbsd-x64': 4.60.1 - '@rollup/rollup-openharmony-arm64': 4.60.1 - '@rollup/rollup-win32-arm64-msvc': 4.60.1 - '@rollup/rollup-win32-ia32-msvc': 4.60.1 - '@rollup/rollup-win32-x64-gnu': 4.60.1 - '@rollup/rollup-win32-x64-msvc': 4.60.1 - fsevents: 2.3.3 - - siginfo@2.0.0: {} - - source-map-js@1.2.1: {} - - source-map@0.7.6: {} - - stackback@0.0.2: {} - - std-env@3.10.0: {} - - sucrase@3.35.1: - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - commander: 4.1.1 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.7 - tinyglobby: 0.2.16 - ts-interface-checker: 0.1.13 - - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - - tinybench@2.9.0: {} - - tinyexec@0.3.2: {} - - tinyglobby@0.2.16: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - - tinypool@1.1.1: {} - - tinyrainbow@1.2.0: {} - - tinyspy@3.0.2: {} - - tree-kill@1.2.2: {} - - ts-interface-checker@0.1.13: {} - - tsup@8.5.1(postcss@8.5.9)(typescript@5.9.3): - dependencies: - bundle-require: 5.1.0(esbuild@0.27.7) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.3 - esbuild: 0.27.7 - fix-dts-default-cjs-exports: 1.0.1 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.9) - resolve-from: 5.0.0 - rollup: 4.60.1 - source-map: 0.7.6 - sucrase: 3.35.1 - tinyexec: 0.3.2 - tinyglobby: 0.2.16 - tree-kill: 1.2.2 - optionalDependencies: - postcss: 8.5.9 - typescript: 5.9.3 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - - typescript@5.9.3: {} - - ufo@1.6.3: {} - - vite-node@2.1.9: - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 1.1.2 - vite: 5.4.21 - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - vite@5.4.21: - dependencies: - esbuild: 0.21.5 - postcss: 8.5.9 - rollup: 4.60.1 - optionalDependencies: - fsevents: 2.3.3 - - vitest@2.1.9: - dependencies: - '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.21) - '@vitest/pretty-format': 2.1.9 - '@vitest/runner': 2.1.9 - '@vitest/snapshot': 2.1.9 - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.3.3 - debug: 4.4.3 - expect-type: 1.3.0 - magic-string: 0.30.21 - pathe: 1.1.2 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinypool: 1.1.1 - tinyrainbow: 1.2.0 - vite: 5.4.21 - vite-node: 2.1.9 - why-is-node-running: 2.3.0 - transitivePeerDependencies: - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 diff --git a/python/README.md b/python/README.md new file mode 100644 index 0000000..58ef781 --- /dev/null +++ b/python/README.md @@ -0,0 +1,33 @@ +# hashlock-sdk (Python) + +Python SDK for the [Hashlock Markets](https://github.com/Hashlock-Tech/hashlock-sdk) developer API — +non-custodial cross-chain atomic swaps (BTC ↔ EVM/TRON). Python 3.9+, built on `httpx`. + +```bash +pip install hashlock-sdk # add [ws] for the maker WebSocket feed: hashlock-sdk[ws] +``` + +```python +from hashlock import HashlockClient, new_secret, verify_webhook + +client = HashlockClient(api_key=os.environ["HASHLOCK_API_KEY"]) +client.me() + +# cursor pagination — page or auto-iterate +page = client.list_swaps(limit=20) +for swap in client.swaps(): + ... + +# initiator: secret stays private; submit only the hashlock +secret, hashlock = new_secret() +client.accept_terms(thread_id, hashlock=hashlock) + +# webhooks: verify a delivery against the raw body +ok = verify_webhook(secret, raw_body, request.headers.get("x-hashlock-signature"), request.headers.get("x-hashlock-timestamp")) +``` + +Settlement is custody-agnostic: `build_fund` / `build_claim` / `build_refund` return unsigned transactions — +sign with your wallet or HSM (see [`../examples/fireblocks`](../examples/fireblocks)) and `broadcast(...)`. +See the [root README](../README.md) for the full swap lifecycle. + +MIT diff --git a/python/hashlock/__init__.py b/python/hashlock/__init__.py new file mode 100644 index 0000000..8b22809 --- /dev/null +++ b/python/hashlock/__init__.py @@ -0,0 +1,8 @@ +"""Hashlock Markets developer SDK — non-custodial cross-chain atomic swaps (BTC ↔ EVM/TRON).""" +from .client import HashlockClient +from .errors import HashlockError +from .secret import new_secret, sha256_hex +from .webhooks import verify_webhook + +__all__ = ["HashlockClient", "HashlockError", "new_secret", "sha256_hex", "verify_webhook"] +__version__ = "1.0.0" diff --git a/python/hashlock/client.py b/python/hashlock/client.py new file mode 100644 index 0000000..4c9aa40 --- /dev/null +++ b/python/hashlock/client.py @@ -0,0 +1,183 @@ +"""Thin, typed client for the Hashlock Markets developer API (/v1). + +Custody-agnostic: the settlement endpoints return UNSIGNED transactions you sign with your own key/HSM +(see ``examples/``), then hand back to :meth:`HashlockClient.broadcast`. The server never holds your keys. +""" +from __future__ import annotations + +from typing import Any, Iterator, Optional + +import httpx + +from .errors import HashlockError + +DEFAULT_BASE = "https://api-dev.hashlock.markets/v1" + + +class HashlockClient: + def __init__(self, api_key: str, base_url: str = DEFAULT_BASE, timeout: float = 30.0) -> None: + if not api_key: + raise ValueError("api_key is required") + self._base = base_url.rstrip("/") + self._http = httpx.Client(timeout=timeout, headers={"authorization": f"Bearer {api_key}"}) + + # ── context manager ──────────────────────────────────────────────────────── + def __enter__(self) -> "HashlockClient": + return self + + def __exit__(self, *exc: object) -> None: + self.close() + + def close(self) -> None: + self._http.close() + + # ── core ─────────────────────────────────────────────────────────────────── + def me(self) -> dict[str, Any]: + """Verify the key and see its scopes.""" + return self._request("GET", "/me") + + def assets(self) -> list[dict[str, Any]]: + """The asset registry (chain, token|native, decimals, symbol).""" + return self._request("GET", "/assets")["assets"] + + # ── RFQs ───────────────────────────────────────────────────────────────────── + def list_rfqs(self, **params: Any) -> dict[str, Any]: + """One page of the order book: ``{"items": [...], "next_cursor": str | None}``. + + Query params: base_asset_id, quote_asset_id, direction, limit, cursor. + """ + r = self._request("GET", "/rfqs", params=_camel(params)) + return {"items": r.get("rfqs", []), "next_cursor": r.get("nextCursor")} + + def rfqs(self, **params: Any) -> Iterator[dict[str, Any]]: + """Iterate the whole order book, following the cursor automatically.""" + yield from self._paginate(self.list_rfqs, params) + + def get_rfq(self, rfq_id: str) -> dict[str, Any]: + return self._request("GET", f"/rfqs/{rfq_id}")["rfq"] + + def create_rfq( + self, + direction: str, + base_asset_id: str, + base_amount: str, + quote_asset_id: str, + ttl_seconds: int, + ask_amount: Optional[str] = None, + visibility: Optional[str] = None, + target_address: Optional[str] = None, + idempotency_key: Optional[str] = None, + ) -> dict[str, Any]: + """Create an RFQ (requires the ``taker`` scope).""" + body = { + "direction": direction, + "baseAssetId": base_asset_id, + "baseAmount": base_amount, + "quoteAssetId": quote_asset_id, + "ttlSeconds": ttl_seconds, + "askAmount": ask_amount, + "visibility": visibility, + "targetAddress": target_address, + } + return self._request("POST", "/rfqs", json={k: v for k, v in body.items() if v is not None}, idempotency_key=idempotency_key)["rfq"] + + def quote_rfq(self, rfq_id: str, quote_amount: str, idempotency_key: Optional[str] = None) -> dict[str, Any]: + """Quote an RFQ (requires the ``maker`` scope) — opens a settlement thread.""" + return self._request("POST", f"/rfqs/{rfq_id}/quotes", json={"quoteAmount": quote_amount}, idempotency_key=idempotency_key) + + # ── negotiation thread ─────────────────────────────────────────────────────── + def get_thread(self, thread_id: str) -> dict[str, Any]: + return self._request("GET", f"/threads/{thread_id}") + + def propose_terms(self, thread_id: str, quote_amount: str) -> dict[str, Any]: + return self._request("POST", f"/threads/{thread_id}/propose", json={"quoteAmount": quote_amount}) + + def accept_proposal(self, thread_id: str) -> dict[str, Any]: + return self._request("POST", f"/threads/{thread_id}/accept-proposal") + + def accept_terms(self, thread_id: str, hashlock: Optional[str] = None) -> dict[str, Any]: + """Accept the current terms. When BOTH sides accept, the swap is created. The initiator (funds the + long leg) MUST pass ``hashlock`` = sha256(secret) — see :func:`hashlock.secret.new_secret`.""" + return self._request("POST", f"/threads/{thread_id}/accept", json={"hashlock": hashlock} if hashlock else {}) + + # ── swaps ───────────────────────────────────────────────────────────────────── + def list_swaps(self, **params: Any) -> dict[str, Any]: + r = self._request("GET", "/swaps", params=_camel(params)) + return {"items": r.get("swaps", []), "next_cursor": r.get("nextCursor")} + + def swaps(self, **params: Any) -> Iterator[dict[str, Any]]: + yield from self._paginate(self.list_swaps, params) + + def get_swap(self, swap_id: str) -> dict[str, Any]: + return self._request("GET", f"/swaps/{swap_id}")["swap"] + + def set_swap_address(self, swap_id: str, chain: str, address: str) -> dict[str, Any]: + """Set your receive (payout) / refund address for a leg. Bitcoin: the compressed pubkey (hex).""" + return self._request("POST", f"/swaps/{swap_id}/address", json={"chain": chain, "address": address})["swap"] + + # ── settlement builders (UNSIGNED — sign with your own key/HSM, then broadcast) ─ + def build_fund(self, swap_id: str, leg: str) -> dict[str, Any]: + return self._request("POST", f"/swaps/{swap_id}/legs/{leg}/fund") + + def build_claim(self, swap_id: str, leg: str, secret: str) -> dict[str, Any]: + return self._request("POST", f"/swaps/{swap_id}/legs/{leg}/claim", json={"secret": secret}) + + def build_refund(self, swap_id: str, leg: str) -> dict[str, Any]: + return self._request("POST", f"/swaps/{swap_id}/legs/{leg}/refund") + + def broadcast(self, chain: str, signed: Any, idempotency_key: Optional[str] = None) -> dict[str, Any]: + """Relay a client-signed tx. ``chain``: 'evm' (0x raw) | 'tron' (signed obj) | 'bitcoin' (raw hex).""" + return self._request("POST", "/tx/broadcast", json={"chain": chain, "signed": signed}, idempotency_key=idempotency_key) + + # ── webhooks ───────────────────────────────────────────────────────────────── + def list_webhooks(self) -> list[dict[str, Any]]: + return self._request("GET", "/webhooks")["webhooks"] + + def create_webhook(self, url: str, events: Optional[list[str]] = None) -> dict[str, Any]: + """Register a webhook. The returned ``secret`` is shown ONCE — store it to verify deliveries.""" + body: dict[str, Any] = {"url": url} + if events: + body["events"] = events + return self._request("POST", "/webhooks", json=body) + + def delete_webhook(self, webhook_id: str) -> dict[str, Any]: + return self._request("DELETE", f"/webhooks/{webhook_id}") + + def ping_webhook(self, webhook_id: str) -> dict[str, Any]: + return self._request("POST", f"/webhooks/{webhook_id}/ping") + + # ── internals ──────────────────────────────────────────────────────────────── + def _paginate(self, page_fn: Any, params: dict[str, Any]) -> Iterator[dict[str, Any]]: + cursor: Optional[str] = None + while True: + page = page_fn(**{**params, **({"cursor": cursor} if cursor else {})}) + yield from page["items"] + cursor = page["next_cursor"] + if not cursor: + break + + def _request(self, method: str, path: str, *, params: Any = None, json: Any = None, idempotency_key: Optional[str] = None) -> Any: + headers = {} + if idempotency_key: + headers["idempotency-key"] = idempotency_key + resp = self._http.request(method, self._base + path, params=params, json=json, headers=headers or None) + try: + data = resp.json() + except Exception: + data = None + if resp.status_code >= 400: + msg = (data or {}).get("error") if isinstance(data, dict) else None + retry_after = resp.headers.get("retry-after") + raise HashlockError(resp.status_code, msg or f"HTTP {resp.status_code}", data, int(retry_after) if retry_after else None) + return data + + +def _camel(params: dict[str, Any]) -> dict[str, Any]: + """snake_case query params → the API's camelCase; drop Nones.""" + out: dict[str, Any] = {} + for k, v in params.items(): + if v is None: + continue + parts = k.split("_") + out[parts[0] + "".join(p.title() for p in parts[1:])] = v + return out diff --git a/python/hashlock/errors.py b/python/hashlock/errors.py new file mode 100644 index 0000000..60a0e6b --- /dev/null +++ b/python/hashlock/errors.py @@ -0,0 +1,20 @@ +"""Error type raised for non-2xx API responses.""" +from __future__ import annotations + +from typing import Any, Optional + + +class HashlockError(Exception): + def __init__(self, status: int, message: str, body: Any = None, retry_after: Optional[int] = None) -> None: + super().__init__(message) + self.status = status + self.body = body + self.retry_after = retry_after # seconds until the rate-limit window resets (on 429) + + @property + def is_rate_limited(self) -> bool: + return self.status == 429 + + @property + def is_auth_error(self) -> bool: + return self.status in (401, 403) diff --git a/python/hashlock/secret.py b/python/hashlock/secret.py new file mode 100644 index 0000000..5b1a956 --- /dev/null +++ b/python/hashlock/secret.py @@ -0,0 +1,21 @@ +"""The swap secret + its hashlock. + +The INITIATOR (funds the long leg) generates the secret locally, keeps it private, and passes only +``hashlock = sha256(secret)`` to ``accept_terms``. The secret is revealed on-chain when the initiator +claims the counter-leg; keep it until then. +""" +from __future__ import annotations + +import hashlib +import secrets + + +def new_secret() -> tuple[str, str]: + """Return ``(secret_hex, hashlock_hex)`` — a fresh 32-byte secret and its sha256 hashlock (no 0x).""" + secret = secrets.token_bytes(32) + return secret.hex(), hashlib.sha256(secret).hexdigest() + + +def sha256_hex(hex_str: str) -> str: + """sha256(preimage) as hex — verify a preimage against a swap's hashlock.""" + return hashlib.sha256(bytes.fromhex(hex_str[2:] if hex_str.startswith("0x") else hex_str)).hexdigest() diff --git a/python/hashlock/webhooks.py b/python/hashlock/webhooks.py new file mode 100644 index 0000000..9b7fdf9 --- /dev/null +++ b/python/hashlock/webhooks.py @@ -0,0 +1,36 @@ +"""Verify webhook deliveries. + +The server signs each POST with:: + + X-Hashlock-Signature: sha256=HMAC-SHA256(secret, f"{timestamp}.{raw_body}") + +where ``timestamp`` is the ``X-Hashlock-Timestamp`` header. Verify against the RAW request body. +""" +from __future__ import annotations + +import hashlib +import hmac +import time +from typing import Optional + + +def verify_webhook( + secret: str, + raw_body: str, + signature: Optional[str], + timestamp: Optional[str], + tolerance_seconds: int = 300, + now_unix: Optional[int] = None, +) -> bool: + """Return True iff the signature matches and the timestamp is within tolerance (0 disables the check).""" + if not signature or not timestamp: + return False + if tolerance_seconds > 0: + try: + ts = int(timestamp) + except ValueError: + return False + if abs((now_unix if now_unix is not None else int(time.time())) - ts) > tolerance_seconds: + return False + expected = "sha256=" + hmac.new(secret.encode(), f"{timestamp}.{raw_body}".encode(), hashlib.sha256).hexdigest() + return hmac.compare_digest(expected, signature) diff --git a/python/hashlock/ws.py b/python/hashlock/ws.py new file mode 100644 index 0000000..9e3dbce --- /dev/null +++ b/python/hashlock/ws.py @@ -0,0 +1,41 @@ +"""Maker feed over WebSocket (/v1/ws), using the ``websockets`` library (async). + +Authenticate with an API key that has the ``maker`` scope, receive a snapshot of the public order book, +then a live stream of quotable RFQs; submit quotes on the same socket. +""" +from __future__ import annotations + +import json +from typing import Any, AsyncIterator + +try: + import websockets # type: ignore +except ImportError: # pragma: no cover + websockets = None # the maker feed is optional; install `hashlock-sdk[ws]` + +DEFAULT_WS = "wss://api-dev.hashlock.markets/v1/ws" + + +async def maker_feed(api_key: str, url: str = DEFAULT_WS) -> AsyncIterator[dict[str, Any]]: + """Async-iterate feed messages: {'type': 'snapshot'|'ready'|'rfq'|'quoted'|'error', ...}. + + Example:: + + async for msg in maker_feed(api_key): + if msg["type"] == "rfq": + ... # decide whether to quote + """ + if websockets is None: + raise RuntimeError("install `hashlock-sdk[ws]` (the `websockets` package) to use the maker feed") + async with websockets.connect(url) as ws: + await ws.send(json.dumps({"apiKey": api_key})) + async for raw in ws: + try: + yield json.loads(raw) + except json.JSONDecodeError: + continue + + +async def submit_quote(ws: Any, rfq_id: str, quote_amount: str) -> None: + """Submit a quote on an open maker-feed socket.""" + await ws.send(json.dumps({"quote": {"rfqId": rfq_id, "quoteAmount": quote_amount}})) diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..198c334 --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "hashlock-sdk" +version = "1.0.0" +description = "Python SDK for the Hashlock Markets developer API — non-custodial cross-chain atomic swaps (BTC <-> EVM/TRON) over sealed RFQ + HTLC." +readme = "README.md" +license = { text = "MIT" } +requires-python = ">=3.9" +keywords = ["hashlock", "atomic-swap", "htlc", "bitcoin", "cross-chain", "otc", "defi"] +dependencies = ["httpx>=0.24"] + +[project.optional-dependencies] +ws = ["websockets>=12"] + +[project.urls] +Homepage = "https://github.com/Hashlock-Tech/hashlock-sdk" + +[tool.hatch.build.targets.wheel] +packages = ["hashlock"] diff --git a/scripts/vendor-schema.mjs b/scripts/vendor-schema.mjs deleted file mode 100644 index e6722da..0000000 --- a/scripts/vendor-schema.mjs +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env node -/** - * Vendor the authoritative trade-service GraphQL SDL from the main repo - * into test/fixtures/, so the operation-validation test - * (src/__tests__/schema-validate.test.ts) can catch SDK ↔ schema drift. - * - * Usage: - * node scripts/vendor-schema.mjs [path-to-Cayman-Hashlock] [git-ref] - * - * Defaults: ../Cayman-Hashlock, origin/main. Run `git fetch origin` in the - * main repo first so origin/main is current. - * - * The main repo's schema.ts holds TWO template literals: - * 1. `typeDefs` — the federated HTTP schema (queries + mutations) - * 2. `SUBSCRIPTION_SDL` — the standalone graphql-ws schema (subscriptions) - * They are vendored as schema.graphql and schema.subscriptions.graphql. - */ -import { execFileSync } from 'node:child_process'; -import { writeFileSync, mkdirSync } from 'node:fs'; -import { dirname, join, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const repo = resolve(process.argv[2] ?? '../Cayman-Hashlock'); -const ref = process.argv[3] ?? 'origin/main'; -const SCHEMA_PATH = 'backend/services/trade-service/src/schema.ts'; - -const source = execFileSync('git', ['-C', repo, 'show', `${ref}:${SCHEMA_PATH}`], { - encoding: 'utf8', - maxBuffer: 16 * 1024 * 1024, -}); -const sha = execFileSync('git', ['-C', repo, 'rev-parse', ref], { encoding: 'utf8' }).trim(); - -/** Extract the template literal that starts right after `marker`. */ -function extractLiteral(src, marker) { - const at = src.indexOf(marker); - if (at === -1) throw new Error(`marker not found: ${marker}`); - const start = src.indexOf('`', at) + 1; - if (start === 0) throw new Error(`no template literal after: ${marker}`); - // Find the closing UNESCAPED backtick. - let end = start; - for (;;) { - end = src.indexOf('`', end); - if (end === -1) throw new Error(`unterminated literal after: ${marker}`); - if (src[end - 1] !== '\\') break; - end += 1; - } - const raw = src.slice(start, end); - if (raw.includes('${')) throw new Error(`literal after ${marker} has interpolation — extractor cannot vendor it verbatim`); - return raw.replace(/\\`/g, '`'); -} - -function header(which) { - return [ - `# Vendored GraphQL SDL — DO NOT EDIT BY HAND`, - `# Source: Cayman-Hashlock ${SCHEMA_PATH} (${which})`, - `# Ref: ${ref} @ ${sha}`, - `# Vendored: ${new Date().toISOString().slice(0, 10)}`, - `# Refresh: git -C fetch origin && node scripts/vendor-schema.mjs `, - '', - ].join('\n'); -} - -const here = dirname(fileURLToPath(import.meta.url)); -const fixtures = join(here, '..', 'test', 'fixtures'); -mkdirSync(fixtures, { recursive: true }); - -const typeDefs = extractLiteral(source, 'const typeDefs = parse('); -const subscriptionSdl = extractLiteral(source, 'export const SUBSCRIPTION_SDL ='); - -writeFileSync(join(fixtures, 'schema.graphql'), header('typeDefs — federated HTTP schema') + typeDefs); -writeFileSync(join(fixtures, 'schema.subscriptions.graphql'), header('SUBSCRIPTION_SDL — graphql-ws schema') + subscriptionSdl); -console.log(`Vendored ${ref} @ ${sha} → test/fixtures/schema.graphql + schema.subscriptions.graphql`); diff --git a/src/__tests__/hashlock.test.ts b/src/__tests__/hashlock.test.ts deleted file mode 100644 index 72d6322..0000000 --- a/src/__tests__/hashlock.test.ts +++ /dev/null @@ -1,492 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { HashLock } from '../hashlock.js'; -import { GraphQLError, AuthError, NetworkError } from '../errors.js'; -import { meetsKycTier, KYC_TIER_RANK } from '../principal.js'; -import { __resetExperimentalWarningState } from '../experimental.js'; -import type { PrincipalAttestation } from '../principal.js'; - -function mockFetch(data: unknown, status = 200) { - return vi.fn().mockResolvedValue({ - status, - statusText: 'OK', - json: () => Promise.resolve(data), - }); -} - -function createClient(fetchFn: ReturnType) { - return new HashLock({ - endpoint: 'http://localhost:4000/graphql', - accessToken: 'test-token', - fetch: fetchFn as unknown as typeof fetch, - retries: 0, // no retries in tests - }); -} - -describe('HashLock SDK', () => { - // ─── RFQ ───────────────────────────────────────────── - - describe('createRFQ', () => { - it('should create an RFQ and return it', async () => { - const rfq = { id: 'rfq-1', baseToken: 'ETH', quoteToken: 'USDT', side: 'SELL', amount: '10', status: 'ACTIVE', isBlind: false, createdAt: '2026-01-01', userId: 'u1', expiresAt: null, quotesCount: 0 }; - const fetch = mockFetch({ data: { createRFQ: rfq } }); - const hl = createClient(fetch); - - const result = await hl.createRFQ({ baseToken: 'ETH', quoteToken: 'USDT', side: 'SELL', amount: '10' }); - expect(result.id).toBe('rfq-1'); - expect(result.status).toBe('ACTIVE'); - expect(fetch).toHaveBeenCalledTimes(1); - }); - - it('forwards baseChain and quoteChain for cross-chain RFQs (e.g. SUI/sui ↔ ETH/sepolia)', async () => { - const rfq = { id: 'rfq-cross', baseToken: 'SUI', quoteToken: 'ETH', side: 'SELL', amount: '10', status: 'ACTIVE', isBlind: false, createdAt: '2026-01-01', userId: 'u1', expiresAt: null, quotesCount: 0 }; - const fetch = mockFetch({ data: { createRFQ: rfq } }); - const hl = createClient(fetch); - - await hl.createRFQ({ - baseToken: 'SUI', - quoteToken: 'ETH', - side: 'SELL', - amount: '10', - baseChain: 'sui', - quoteChain: 'sepolia', - }); - - const body = JSON.parse(fetch.mock.calls[0][1].body); - expect(body.variables.baseChain).toBe('sui'); - expect(body.variables.quoteChain).toBe('sepolia'); - // Mutation signature must include the new variable types so the - // backend GraphQL parser accepts the operation. - expect(body.query).toContain('$baseChain: String'); - expect(body.query).toContain('$quoteChain: String'); - }); - }); - - describe('getRFQ', () => { - it('should return an RFQ by ID', async () => { - const rfq = { id: 'rfq-1', baseToken: 'ETH', quoteToken: 'USDT', side: 'SELL', amount: '10', status: 'ACTIVE', isBlind: false, createdAt: '2026-01-01', userId: 'u1', expiresAt: null, quotesCount: 0, quotes: [] }; - const fetch = mockFetch({ data: { rfq } }); - const hl = createClient(fetch); - - const result = await hl.getRFQ('rfq-1'); - expect(result?.id).toBe('rfq-1'); - }); - - it('should return null for non-existent RFQ', async () => { - const fetch = mockFetch({ data: { rfq: null } }); - const hl = createClient(fetch); - - const result = await hl.getRFQ('nonexistent'); - expect(result).toBeNull(); - }); - }); - - describe('cancelRFQ', () => { - it('should cancel an RFQ', async () => { - const fetch = mockFetch({ data: { cancelRFQ: { id: 'rfq-1', status: 'CANCELLED' } } }); - const hl = createClient(fetch); - - const result = await hl.cancelRFQ('rfq-1'); - expect(result.status).toBe('CANCELLED'); - }); - }); - - // ─── Quotes ────────────────────────────────────────── - - describe('submitQuote', () => { - it('should submit a quote', async () => { - const quote = { id: 'q-1', rfqId: 'rfq-1', marketMakerId: 'mm-1', price: '3500', amount: '10', status: 'PENDING', createdAt: '2026-01-01', expiresAt: null }; - const fetch = mockFetch({ data: { submitQuote: quote } }); - const hl = createClient(fetch); - - const result = await hl.submitQuote({ rfqId: 'rfq-1', price: '3500', amount: '10' }); - expect(result.price).toBe('3500'); - }); - }); - - describe('acceptQuote', () => { - it('should accept a quote and get trade ref', async () => { - const fetch = mockFetch({ data: { acceptQuote: { id: 'q-1', rfqId: 'rfq-1', status: 'ACCEPTED', trade: { id: 't-1', status: 'PROPOSED' } } } }); - const hl = createClient(fetch); - - const result = await hl.acceptQuote('q-1'); - expect(result.status).toBe('ACCEPTED'); - }); - }); - - // ─── Trades ────────────────────────────────────────── - - describe('getTrade', () => { - it('should return a trade by ID', async () => { - const trade = { id: 't-1', initiatorId: 'u1', counterpartyId: 'u2', baseToken: 'ETH', quoteToken: 'USDT', side: 'SELL', baseAmount: '10', quoteAmount: '35000', price: '3500', status: 'ACCEPTED', createdAt: '2026-01-01' }; - const fetch = mockFetch({ data: { trade } }); - const hl = createClient(fetch); - - const result = await hl.getTrade('t-1'); - expect(result?.status).toBe('ACCEPTED'); - }); - }); - - describe('acceptTrade', () => { - it('should accept a trade', async () => { - const fetch = mockFetch({ data: { acceptTrade: { id: 't-1', status: 'ACCEPTED' } } }); - const hl = createClient(fetch); - - const result = await hl.acceptTrade('t-1'); - expect(result.status).toBe('ACCEPTED'); - }); - }); - - // ─── HTLC (EVM) ───────────────────────────────────── - - describe('fundHTLC', () => { - it('should record an HTLC funding tx', async () => { - const fetch = mockFetch({ data: { fundHTLC: { tradeId: 't-1', txHash: '0xabc', status: 'PENDING' } } }); - const hl = createClient(fetch); - - const result = await hl.fundHTLC({ tradeId: 't-1', txHash: '0xabc', role: 'INITIATOR' }); - expect(result.txHash).toBe('0xabc'); - }); - }); - - describe('claimHTLC', () => { - it('should record an HTLC claim', async () => { - const fetch = mockFetch({ data: { claimHTLC: { tradeId: 't-1', status: 'WITHDRAWN' } } }); - const hl = createClient(fetch); - - const result = await hl.claimHTLC({ tradeId: 't-1', txHash: '0xdef', preimage: '0x1234' }); - expect(result.status).toBe('WITHDRAWN'); - }); - }); - - describe('refundHTLC', () => { - it('should record an HTLC refund', async () => { - const fetch = mockFetch({ data: { refundHTLC: { tradeId: 't-1', status: 'REFUNDED' } } }); - const hl = createClient(fetch); - - const result = await hl.refundHTLC({ tradeId: 't-1', txHash: '0xghi' }); - expect(result.status).toBe('REFUNDED'); - }); - }); - - describe('getHTLCStatus', () => { - it('should return HTLC status with both sides', async () => { - const htlcStatus = { - tradeId: 't-1', status: 'BOTH_LOCKED', - initiatorHTLC: { id: 'h1', tradeId: 't-1', role: 'INITIATOR', status: 'ACTIVE', contractAddress: '0x1', hashlock: '0xh', timelock: 999, amount: '1.0', txHash: '0xa', chainType: 'evm' }, - counterpartyHTLC: { id: 'h2', tradeId: 't-1', role: 'COUNTERPARTY', status: 'ACTIVE', contractAddress: '0x2', hashlock: '0xh', timelock: 888, amount: '3500', txHash: '0xb', chainType: 'evm' }, - }; - const fetch = mockFetch({ data: { htlcStatus } }); - const hl = createClient(fetch); - - const result = await hl.getHTLCStatus('t-1'); - expect(result?.initiatorHTLC?.role).toBe('INITIATOR'); - expect(result?.counterpartyHTLC?.role).toBe('COUNTERPARTY'); - }); - }); - - // ─── HTLC (Bitcoin) ────────────────────────────────── - - describe('prepareBitcoinHTLC', () => { - it('should return P2WSH address and redeem script', async () => { - const btc = { tradeId: 't-1', htlcId: 'bh-1', htlcAddress: 'tb1q...', redeemScript: '6321...', hashlock: '0xabc', preimageHash: '0xdef', timelock: 9999, amountSats: '100000', estimatedClaimFee: 500, estimatedRefundFee: 400, refundPsbt: 'cHNidA==' }; - const fetch = mockFetch({ data: { prepareBitcoinHTLC: btc } }); - const hl = createClient(fetch); - - const result = await hl.prepareBitcoinHTLC({ tradeId: 't-1', role: 'INITIATOR', senderPubKey: '02abc', receiverPubKey: '03def', timelock: 9999, amountSats: '100000' }); - expect(result.htlcAddress).toBe('tb1q...'); - expect(result.amountSats).toBe('100000'); - }); - }); - - describe('broadcastBitcoinTx', () => { - it('should broadcast and return txid', async () => { - const fetch = mockFetch({ data: { broadcastBitcoinTx: { txid: 'abc123', success: true } } }); - const hl = createClient(fetch); - - const result = await hl.broadcastBitcoinTx({ tradeId: 't-1', txHex: '0200000001...' }); - expect(result.success).toBe(true); - expect(result.txid).toBe('abc123'); - }); - }); - - // ─── Error Handling ────────────────────────────────── - - describe('error handling', () => { - it('should throw GraphQLError on GraphQL errors', async () => { - const fetch = mockFetch({ errors: [{ message: 'Trade not found' }] }); - const hl = createClient(fetch); - - await expect(hl.getTrade('bad-id')).rejects.toThrow(GraphQLError); - }); - - it('should throw AuthError on 401', async () => { - const fetch = mockFetch({}, 401); - const hl = createClient(fetch); - - await expect(hl.getTrade('t-1')).rejects.toThrow(AuthError); - }); - - it('should throw NetworkError on 500', async () => { - const fetch = mockFetch({}, 500); - const hl = createClient(fetch); - - await expect(hl.getTrade('t-1')).rejects.toThrow(NetworkError); - }); - - it('should include Authorization header when token is set', async () => { - const fetch = mockFetch({ data: { trade: null } }); - const hl = createClient(fetch); - - await hl.getTrade('t-1'); - - const callArgs = fetch.mock.calls[0]; - expect(callArgs[1].headers['Authorization']).toBe('Bearer test-token'); - }); - - it('should update token via setAccessToken', async () => { - const fetch = mockFetch({ data: { trade: null } }); - const hl = createClient(fetch); - - hl.setAccessToken('new-token'); - await hl.getTrade('t-1'); - - const callArgs = fetch.mock.calls[0]; - expect(callArgs[1].headers['Authorization']).toBe('Bearer new-token'); - }); - }); - - // ─── Principal / Attestation (EXPERIMENTAL) ───────────── - - describe('principal + attestation types', () => { - const validAttestation: PrincipalAttestation = { - principalId: 'pr_acme_001', - principalType: 'INSTITUTION', - tier: 'INSTITUTIONAL', - blindId: 'ag_5g7k92bq', - issuedAt: Math.floor(Date.now() / 1000), - expiresAt: Math.floor(Date.now() / 1000) + 3600, - proof: '0xdeadbeef', - }; - - it('KycTier helpers rank tiers correctly', () => { - expect(KYC_TIER_RANK.NONE).toBe(0); - expect(KYC_TIER_RANK.INSTITUTIONAL).toBe(4); - expect(meetsKycTier('ENHANCED', 'STANDARD')).toBe(true); - expect(meetsKycTier('BASIC', 'ENHANCED')).toBe(false); - }); - - it('createRFQ accepts attestation + agentInstance + tier filters', async () => { - const rfq = { - id: 'rfq-agent', - baseToken: 'ETH', - quoteToken: 'USDT', - side: 'SELL', - amount: '5.0', - status: 'ACTIVE', - isBlind: true, - createdAt: '2026-04-11', - userId: 'u1', - expiresAt: null, - quotesCount: 0, - }; - const fetch = mockFetch({ data: { createRFQ: rfq } }); - const hl = createClient(fetch); - - // Type-level acceptance: the SDK compiles and runs with the - // new fields. GraphQL wire-through is a later release. - const result = await hl.createRFQ({ - baseToken: 'ETH', - quoteToken: 'USDT', - side: 'SELL', - amount: '5.0', - isBlind: true, - attestation: validAttestation, - agentInstance: { - instanceId: 'ag_5g7k92bq', - strategy: 'mm-eth-usdt', - version: '1.0.0', - }, - minCounterpartyTier: 'STANDARD', - hideIdentity: true, - }); - - expect(result.id).toBe('rfq-agent'); - }); - - it('submitQuote accepts attestation + agentInstance + hideIdentity', async () => { - const quote = { - id: 'q-agent', - rfqId: 'rfq-1', - marketMakerId: 'mm-1', - price: '3450', - amount: '1.0', - status: 'PENDING', - createdAt: '2026-04-11', - expiresAt: null, - }; - const fetch = mockFetch({ data: { submitQuote: quote } }); - const hl = createClient(fetch); - - const result = await hl.submitQuote({ - rfqId: 'rfq-1', - price: '3450', - amount: '1.0', - attestation: validAttestation, - agentInstance: { instanceId: 'ag_5g7k92bq' }, - hideIdentity: true, - }); - - expect(result.id).toBe('q-agent'); - }); - - it('fundHTLC accepts attestation + agentInstance', async () => { - const fetch = mockFetch({ - data: { fundHTLC: { tradeId: 't-1', txHash: '0xabc', status: 'PENDING' } }, - }); - const hl = createClient(fetch); - - const result = await hl.fundHTLC({ - tradeId: 't-1', - txHash: '0xabc', - role: 'INITIATOR', - chainType: 'evm', - attestation: validAttestation, - agentInstance: { instanceId: 'ag_5g7k92bq' }, - }); - - expect(result.status).toBe('PENDING'); - }); - - it('existing calls without attestation still work (backward compat)', async () => { - const rfq = { - id: 'rfq-human', - baseToken: 'ETH', - quoteToken: 'USDT', - side: 'BUY', - amount: '1.0', - status: 'ACTIVE', - isBlind: false, - createdAt: '2026-04-11', - userId: 'u1', - expiresAt: null, - quotesCount: 0, - }; - const fetch = mockFetch({ data: { createRFQ: rfq } }); - const hl = createClient(fetch); - - const result = await hl.createRFQ({ - baseToken: 'ETH', - quoteToken: 'USDT', - side: 'BUY', - amount: '1.0', - }); - - expect(result.id).toBe('rfq-human'); - }); - - it('emits experimental warning when attestation is set on createRFQ', async () => { - __resetExperimentalWarningState(); - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const fetch = mockFetch({ - data: { - createRFQ: { - id: 'rfq-w', - baseToken: 'ETH', - quoteToken: 'USDT', - side: 'SELL', - amount: '1', - status: 'ACTIVE', - isBlind: false, - createdAt: '2026-04-11', - userId: 'u1', - expiresAt: null, - quotesCount: 0, - }, - }, - }); - const hl = createClient(fetch); - - await hl.createRFQ({ - baseToken: 'ETH', - quoteToken: 'USDT', - side: 'SELL', - amount: '1', - attestation: validAttestation, - }); - - expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining('EXPERIMENTAL'), - ); - expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining('createRFQ.attestation'), - ); - warnSpy.mockRestore(); - }); - - it('does not emit warning when no experimental fields are set', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const fetch = mockFetch({ - data: { - createRFQ: { - id: 'rfq-plain', - baseToken: 'ETH', - quoteToken: 'USDT', - side: 'BUY', - amount: '1', - status: 'ACTIVE', - isBlind: false, - createdAt: '2026-04-11', - userId: 'u1', - expiresAt: null, - quotesCount: 0, - }, - }, - }); - const hl = createClient(fetch); - - await hl.createRFQ({ - baseToken: 'ETH', - quoteToken: 'USDT', - side: 'BUY', - amount: '1', - }); - - expect(warnSpy).not.toHaveBeenCalled(); - warnSpy.mockRestore(); - }); - - it('parses attestation tier fields from RFQ responses', async () => { - const rfq = { - id: 'rfq-1', - userId: 'u1', - baseToken: 'ETH', - quoteToken: 'USDT', - side: 'SELL', - amount: '5', - status: 'ACTIVE', - isBlind: true, - createdAt: '2026-04-11', - expiresAt: null, - quotesCount: 0, - attestationTier: 'INSTITUTIONAL', - attestationBlindId: 'ag_5g7k92bq', - minCounterpartyTier: 'STANDARD', - }; - const fetch = mockFetch({ data: { createRFQ: rfq } }); - const hl = createClient(fetch); - - const result = await hl.createRFQ({ - baseToken: 'ETH', - quoteToken: 'USDT', - side: 'SELL', - amount: '5', - }); - - // Type is `KycTier | null | undefined`; the JSON response is - // assignable (TS response types are a loose projection). - expect(result.attestationTier).toBe('INSTITUTIONAL'); - expect(result.attestationBlindId).toBe('ag_5g7k92bq'); - expect(result.minCounterpartyTier).toBe('STANDARD'); - }); - }); -}); diff --git a/src/__tests__/instant.test.ts b/src/__tests__/instant.test.ts deleted file mode 100644 index d857a33..0000000 --- a/src/__tests__/instant.test.ts +++ /dev/null @@ -1,697 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import { HashLock } from '../hashlock.js'; -import { GraphQLError, AuthError, NetworkError } from '../errors.js'; -import { - policyPresets, - sanitizeAgentPolicy, - classifyInstantFillError, - InstantFillOrphanedError, - TRUST_LEVEL_TO_SCORE, -} from '../instant.js'; -import type { - InstantFill, - InstantFillRequestedEvent, - InstantFillFrontedEvent, -} from '../instant.js'; -import type { WebSocketConstructor } from '../ws.js'; -import { deriveWsEndpoint } from '../ws.js'; - -// ─── Helpers ───────────────────────────────────────────────── - -type GqlBody = { data?: unknown; errors?: unknown[]; status?: number }; - -/** Mock fetch returning a different GraphQL response per call, in order. */ -function mockFetchSequence(...responses: GqlBody[]) { - const fn = vi.fn(); - for (const r of responses) { - fn.mockResolvedValueOnce({ - status: r.status ?? 200, - statusText: 'OK', - json: () => Promise.resolve({ data: r.data, errors: r.errors }), - }); - } - return fn; -} - -function createClient(fetchFn: ReturnType) { - return new HashLock({ - endpoint: 'http://localhost:4000/graphql', - accessToken: 'test-token', - fetch: fetchFn as unknown as typeof fetch, - retries: 0, - }); -} - -function requestBody(fetchFn: ReturnType, call = 0) { - return JSON.parse(fetchFn.mock.calls[call][1].body) as { - query: string; - variables: Record; - }; -} - -const FILL: InstantFill = { - id: 'fill-1', - quoteId: 'q-1', - tradeId: null, - state: 'committed', - amountWei: '1000000000000000000000000000', // > 2^53 — must stay a string - frontTxHash: null, - frontedAt: null, - createdAt: '2026-06-11T00:00:00Z', -}; - -/** Schema-accurate subscription payloads (InstantFillRequested / - * InstantFillFronted are DIFFERENT types from InstantFill — the fill id - * arrives as `fillId`, Requested has no tradeId/frontTxHash/frontedAt, - * Fronted has no createdAt). */ -const REQUESTED_EVENT: InstantFillRequestedEvent = { - fillId: 'fill-1', - quoteId: 'q-1', - rfqId: 'rfq-1', - state: 'committed', - amountWei: '1000000000000000000000000000', - createdAt: '2026-06-11T00:00:00Z', -}; - -const FRONTED_EVENT: InstantFillFrontedEvent = { - fillId: 'fill-1', - quoteId: 'q-1', - rfqId: 'rfq-1', - tradeId: 't-1', - state: 'fronted', - amountWei: '1000000000000000000000000000', - frontTxHash: '0xfront', - frontedAt: '2026-06-11T00:01:00Z', -}; - -const QUOTE = { id: 'q-1', rfqId: 'rfq-1', status: 'ACCEPTED', trade: { id: 't-1', status: 'PROPOSED' } }; - -// REAL wire shapes (verified against the backend error pipeline): -// trade-service maskTradeError + the gateway formatter serialize a -// HashlockError as extensions: { ...metadata, code, retryable } — -// metadata FLATTENED, HTTP statusCode never serialized. -const DISABLED_ERR = { message: 'instant fill disabled', extensions: { code: 'INSTANT_FILL_DISABLED' } }; -const LANE_ERR = { - message: 'Instant fill unavailable: the settlement router classified this intent into lane Z', - extensions: { code: 'INVALID_INPUT', retryable: false, lane: 'Z' }, -}; -const ALREADY_ERR = { - message: 'Instant fill already requested for this quote', - extensions: { code: 'INVALID_STATE_TRANSITION', retryable: false }, -}; - -// ─── Policy presets + sanitization ─────────────────────────── - -describe('policyPresets', () => { - it('mirrors the design §13.1 preset table 1:1', () => { - expect(policyPresets.instant).toEqual({ maxLatencyMs: 3000 }); - expect(policyPresets.balanced).toEqual({ minTrust: 'med' }); - expect(policyPresets.trustless).toEqual({ minTrust: 'max' }); - }); -}); - -describe('sanitizeAgentPolicy', () => { - it('produces the wire shape — every field an Int (AgentPolicyInput is all Int)', () => { - expect(sanitizeAgentPolicy({ maxLatencyMs: 3000, maxFeeBps: 30, minTrust: 'med' })) - .toEqual({ maxLatencyMs: 3000, maxFeeBps: 30, minTrust: 50 }); - }); - - it('maps TrustLevel strings to the 0-100 Int score (low→0, med→50, max→100)', () => { - expect(TRUST_LEVEL_TO_SCORE).toEqual({ low: 0, med: 50, max: 100 }); - expect(sanitizeAgentPolicy({ minTrust: 'low' })).toEqual({ minTrust: 0 }); - expect(sanitizeAgentPolicy({ minTrust: 'med' })).toEqual({ minTrust: 50 }); - expect(sanitizeAgentPolicy({ minTrust: 'max' })).toEqual({ minTrust: 100 }); - }); - - it('never lets a TrustLevel string reach the wire for any preset', () => { - for (const preset of Object.values(policyPresets)) { - const wire = sanitizeAgentPolicy(preset); - expect(wire).toBeDefined(); - for (const v of Object.values(wire as Record)) { - expect(Number.isInteger(v)).toBe(true); // Int coercion safety - } - } - }); - - it('accepts a raw numeric minTrust, flooring and clamping into 0-100', () => { - expect(sanitizeAgentPolicy({ minTrust: 75 })).toEqual({ minTrust: 75 }); - expect(sanitizeAgentPolicy({ minTrust: 72.9 })).toEqual({ minTrust: 72 }); - expect(sanitizeAgentPolicy({ minTrust: 150 })).toEqual({ minTrust: 100 }); - expect(sanitizeAgentPolicy({ minTrust: -5 })).toEqual({ minTrust: 0 }); - }); - - it('floors/clamps maxLatencyMs and maxFeeBps into Int-safe backend ranges', () => { - expect(sanitizeAgentPolicy({ maxLatencyMs: 2999.9 })).toEqual({ maxLatencyMs: 2999 }); - expect(sanitizeAgentPolicy({ maxLatencyMs: Number.MAX_SAFE_INTEGER })) - .toEqual({ maxLatencyMs: 2_147_483_647 }); // GraphQL Int is 32-bit - expect(sanitizeAgentPolicy({ maxFeeBps: 25.7 })).toEqual({ maxFeeBps: 25 }); - expect(sanitizeAgentPolicy({ maxFeeBps: 20_000 })).toEqual({ maxFeeBps: 10_000 }); - }); - - it('drops unknown keys and wrongly-typed values instead of throwing', () => { - expect(sanitizeAgentPolicy({ - maxLatencyMs: 'fast', // wrong type - maxFeeBps: 25, - minTrust: 'maximum', // not a TrustLevel - surprise: true, // unknown key - })).toEqual({ maxFeeBps: 25 }); - }); - - it('returns undefined when nothing valid remains (→ standard path)', () => { - expect(sanitizeAgentPolicy({ maxLatencyMs: -1, minTrust: 'huge' })).toBeUndefined(); - expect(sanitizeAgentPolicy({ minTrust: NaN })).toBeUndefined(); - expect(sanitizeAgentPolicy('instant')).toBeUndefined(); - expect(sanitizeAgentPolicy(null)).toBeUndefined(); - expect(sanitizeAgentPolicy(undefined)).toBeUndefined(); - }); -}); - -// ─── Maker side: submitQuote instant-fill commitment ───────── - -describe('submitQuote (instant fill)', () => { - it('forwards instantFill + solverVaultAddr and declares them in the operation', async () => { - const fetch = mockFetchSequence({ - data: { submitQuote: { ...QUOTE, instantFill: true, solverVaultAddr: '0xVault' } }, - }); - const hl = createClient(fetch); - - const quote = await hl.submitQuote({ - rfqId: 'rfq-1', - price: '3500', - amount: '10', - instantFill: true, - solverVaultAddr: '0xVault', - }); - - const body = requestBody(fetch); - expect(body.variables.instantFill).toBe(true); - expect(body.variables.solverVaultAddr).toBe('0xVault'); - expect(body.query).toContain('$instantFill: Boolean'); - expect(body.query).toContain('$solverVaultAddr: String'); - expect(body.query).toContain('instantFill solverVaultAddr'); - expect(quote.instantFill).toBe(true); - expect(quote.solverVaultAddr).toBe('0xVault'); - }); - - it('plain quotes keep working without the new fields (backward compat)', async () => { - const fetch = mockFetchSequence({ data: { submitQuote: { ...QUOTE, instantFill: false, solverVaultAddr: null } } }); - const hl = createClient(fetch); - - const quote = await hl.submitQuote({ rfqId: 'rfq-1', price: '3500', amount: '10' }); - const body = requestBody(fetch); - expect('instantFill' in body.variables).toBe(false); // undefined drops in JSON - expect(quote.id).toBe('q-1'); - }); -}); - -// ─── Taker side: acceptQuote policy ────────────────────────── - -describe('acceptQuote (policy)', () => { - it('without policy keeps the legacy operation (no policy variable)', async () => { - const fetch = mockFetchSequence({ data: { acceptQuote: QUOTE } }); - const hl = createClient(fetch); - - await hl.acceptQuote('q-1'); - const body = requestBody(fetch); - expect(body.query).not.toContain('policy'); - expect(body.variables).toEqual({ quoteId: 'q-1' }); - }); - - it('with policy declares $policy: AgentPolicyInput and forwards the WIRE policy (minTrust as Int)', async () => { - const fetch = mockFetchSequence({ data: { acceptQuote: QUOTE } }); - const hl = createClient(fetch); - - await hl.acceptQuote('q-1', { ...policyPresets.balanced, maxFeeBps: 30 }); - const body = requestBody(fetch); - expect(body.query).toContain('$policy: AgentPolicyInput'); - // 'med' must NOT reach the wire — AgentPolicyInput.minTrust is Int, - // a string would fail coercion and reject the ENTIRE accept. - expect(body.variables.policy).toEqual({ minTrust: 50, maxFeeBps: 30 }); - }); - - it('a fully broken policy silently falls back to the standard accept (never errors)', async () => { - const fetch = mockFetchSequence({ data: { acceptQuote: QUOTE } }); - const hl = createClient(fetch); - - const quote = await hl.acceptQuote('q-1', { maxLatencyMs: 'now' } as never); - const body = requestBody(fetch); - expect(body.query).not.toContain('policy'); - expect(quote.status).toBe('ACCEPTED'); - }); -}); - -// ─── requestInstantFill / markInstantFillFronted ───────────── - -describe('requestInstantFill', () => { - it('returns the fill; amountWei stays a decimal string (never number)', async () => { - const fetch = mockFetchSequence({ data: { requestInstantFill: FILL } }); - const hl = createClient(fetch); - - const fill = await hl.requestInstantFill('rfq-1', 'q-1'); - expect(fill.id).toBe('fill-1'); - expect(fill.state).toBe('committed'); - expect(typeof fill.amountWei).toBe('string'); - expect(BigInt(fill.amountWei)).toBe(1000000000000000000000000000n); - - const body = requestBody(fetch); - expect(body.variables).toEqual({ rfqId: 'rfq-1', quoteId: 'q-1' }); - expect(body.query).toContain('requestInstantFill(rfqId: $rfqId, quoteId: $quoteId)'); - }); -}); - -describe('markInstantFillFronted', () => { - it('forwards fillId + frontTxHash and returns the fronted fill', async () => { - const fronted = { ...FILL, state: 'fronted', frontTxHash: '0xfront', frontedAt: '2026-06-11T00:01:00Z' }; - const fetch = mockFetchSequence({ data: { markInstantFillFronted: fronted } }); - const hl = createClient(fetch); - - const fill = await hl.markInstantFillFronted('fill-1', '0xfront'); - expect(fill.state).toBe('fronted'); - expect(fill.frontTxHash).toBe('0xfront'); - - const body = requestBody(fetch); - expect(body.variables).toEqual({ fillId: 'fill-1', frontTxHash: '0xfront' }); - }); -}); - -// ─── Error classification ──────────────────────────────────── - -describe('classifyInstantFillError', () => { - it('INSTANT_FILL_DISABLED code → disabled', () => { - const err = new GraphQLError(DISABLED_ERR.message, [DISABLED_ERR]); - expect(classifyInstantFillError(err)).toEqual({ reason: 'disabled' }); - }); - - it('gateway-masked disabled error (message replaced, code preserved) → disabled', () => { - // In production the gateway masks the message of non-SAFE_CODES errors - // but PRESERVES extensions.code. - const err = new GraphQLError('masked', [{ - message: 'An error occurred. Please try again or contact support.', - extensions: { code: 'INSTANT_FILL_DISABLED' }, - }]); - expect(classifyInstantFillError(err)).toEqual({ reason: 'disabled' }); - }); - - it('INVALID_INPUT with flattened extensions.lane → lane_conflict carrying the lane', () => { - // HashlockError metadata { lane } is FLATTENED into extensions by - // maskTradeError/formatGraphQLError — the real wire shape. - const err = new GraphQLError(LANE_ERR.message, [LANE_ERR]); - expect(classifyInstantFillError(err)).toEqual({ reason: 'lane_conflict', lane: 'Z' }); - }); - - it('nested extensions.metadata.lane (non-flattening transport) still → lane_conflict', () => { - const err = new GraphQLError('lane', [{ - message: 'lane', extensions: { code: 'INVALID_INPUT', metadata: { lane: 'B' } }, - }]); - expect(classifyInstantFillError(err)).toEqual({ reason: 'lane_conflict', lane: 'B' }); - }); - - it('INVALID_STATE_TRANSITION "already requested" → already_requested', () => { - const err = new GraphQLError(ALREADY_ERR.message, [ALREADY_ERR]); - expect(classifyInstantFillError(err)).toEqual({ reason: 'already_requested' }); - }); - - it('other INVALID_STATE_TRANSITION errors (quote no longer firm / expired) → null', () => { - // A dead quote would fail the standard accept too — silently falling - // back would only obscure the real error. - for (const message of ['Quote is no longer firm (status ACCEPTED)', 'Quote has expired']) { - const err = new GraphQLError(message, [{ - message, extensions: { code: 'INVALID_STATE_TRANSITION', retryable: false }, - }]); - expect(classifyInstantFillError(err)).toBeNull(); - } - }); - - it('unrelated GraphQL error → null (must be rethrown by callers)', () => { - const err = new GraphQLError('Quote not found', [{ message: 'Quote not found' }]); - expect(classifyInstantFillError(err)).toBeNull(); - }); - - it('non-GraphQL errors → null', () => { - expect(classifyInstantFillError(new NetworkError('boom'))).toBeNull(); - expect(classifyInstantFillError(new AuthError())).toBeNull(); - expect(classifyInstantFillError('nope')).toBeNull(); - }); -}); - -// ─── Taker flow helper decision table ──────────────────────── - -describe('requestInstantFillAndAccept', () => { - it('fill OK + accept OK → kind instant (canonical order: fill first)', async () => { - const fetch = mockFetchSequence( - { data: { requestInstantFill: FILL } }, - { data: { acceptQuote: QUOTE } }, - ); - const hl = createClient(fetch); - - const res = await hl.requestInstantFillAndAccept('rfq-1', 'q-1'); - expect(res.kind).toBe('instant'); - if (res.kind === 'instant') { - expect(res.fill.id).toBe('fill-1'); - expect(res.quote.status).toBe('ACCEPTED'); - } - // order: call 0 = requestInstantFill, call 1 = acceptQuote - expect(requestBody(fetch, 0).query).toContain('requestInstantFill'); - expect(requestBody(fetch, 1).query).toContain('acceptQuote'); - }); - - it('forwards the policy to acceptQuote on the instant path', async () => { - const fetch = mockFetchSequence( - { data: { requestInstantFill: FILL } }, - { data: { acceptQuote: QUOTE } }, - ); - const hl = createClient(fetch); - - await hl.requestInstantFillAndAccept('rfq-1', 'q-1', { policy: policyPresets.instant }); - expect(requestBody(fetch, 1).variables.policy).toEqual({ maxLatencyMs: 3000 }); - }); - - it('fill DISABLED → standard accept fallback with reason disabled', async () => { - const fetch = mockFetchSequence( - { errors: [DISABLED_ERR] }, - { data: { acceptQuote: QUOTE } }, - ); - const hl = createClient(fetch); - - const res = await hl.requestInstantFillAndAccept('rfq-1', 'q-1'); - expect(res).toMatchObject({ kind: 'standard', reason: 'disabled' }); - expect(fetch).toHaveBeenCalledTimes(2); - expect(requestBody(fetch, 1).query).toContain('acceptQuote'); - }); - - it('fill lane-409 → standard fallback with reason lane_conflict + lane', async () => { - const fetch = mockFetchSequence( - { errors: [LANE_ERR] }, - { data: { acceptQuote: QUOTE } }, - ); - const hl = createClient(fetch); - - const res = await hl.requestInstantFillAndAccept('rfq-1', 'q-1'); - expect(res).toMatchObject({ kind: 'standard', reason: 'lane_conflict', lane: 'Z' }); - }); - - it('fill already-requested 409 → standard fallback with reason already_requested', async () => { - const fetch = mockFetchSequence( - { errors: [ALREADY_ERR] }, - { data: { acceptQuote: QUOTE } }, - ); - const hl = createClient(fetch); - - const res = await hl.requestInstantFillAndAccept('rfq-1', 'q-1'); - expect(res).toMatchObject({ kind: 'standard', reason: 'already_requested' }); - }); - - it('unknown GraphQL error on fill → rethrown, accept NOT attempted', async () => { - const fetch = mockFetchSequence({ errors: [{ message: 'Quote not found' }] }); - const hl = createClient(fetch); - - await expect(hl.requestInstantFillAndAccept('rfq-1', 'q-1')).rejects.toThrow(GraphQLError); - expect(fetch).toHaveBeenCalledTimes(1); - }); - - it('auth error on fill → rethrown as AuthError, no fallback', async () => { - const fetch = mockFetchSequence({ status: 401 }); - const hl = createClient(fetch); - - await expect(hl.requestInstantFillAndAccept('rfq-1', 'q-1')).rejects.toThrow(AuthError); - expect(fetch).toHaveBeenCalledTimes(1); - }); - - it('fill OK + accept FAIL → fill_orphaned with typed InstantFillOrphanedError', async () => { - const fetch = mockFetchSequence( - { data: { requestInstantFill: FILL } }, - { errors: [{ message: 'accept blew up' }] }, - ); - const hl = createClient(fetch); - - const res = await hl.requestInstantFillAndAccept('rfq-1', 'q-1'); - expect(res.kind).toBe('fill_orphaned'); - if (res.kind === 'fill_orphaned') { - expect(res.error).toBeInstanceOf(InstantFillOrphanedError); - expect(res.error.code).toBe('INSTANT_FILL_ORPHANED'); - expect(res.error.fill.id).toBe('fill-1'); - expect(res.error.acceptError.message).toBe('accept blew up'); - expect(res.fill.amountWei).toBe(FILL.amountWei); - } - }); -}); - -describe('retryAcceptAfterInstantFill', () => { - it('accept-only: succeeds without re-requesting the fill', async () => { - const fetch = mockFetchSequence({ data: { acceptQuote: QUOTE } }); - const hl = createClient(fetch); - - const res = await hl.retryAcceptAfterInstantFill(FILL); - expect(res.kind).toBe('instant'); - expect(fetch).toHaveBeenCalledTimes(1); // ONLY acceptQuote, never requestInstantFill - const body = requestBody(fetch); - expect(body.query).toContain('acceptQuote'); - expect(body.variables.quoteId).toBe('q-1'); // taken from fill.quoteId - }); - - it('repeated accept failure stays fill_orphaned (retryable again)', async () => { - const fetch = mockFetchSequence({ errors: [{ message: 'still down' }] }); - const hl = createClient(fetch); - - const res = await hl.retryAcceptAfterInstantFill(FILL, policyPresets.trustless); - expect(res.kind).toBe('fill_orphaned'); - }); -}); - -// ─── Subscriptions (graphql-transport-ws over fake socket) ─── - -type Listener = (event: { data?: unknown; code?: number }) => void; - -class FakeWebSocket { - static instances: FakeWebSocket[] = []; - sent: string[] = []; - closed = false; - closeCode: number | undefined; - private listeners: Record = {}; - - constructor(public url: string, public protocols?: string | string[]) { - FakeWebSocket.instances.push(this); - } - send(data: string): void { this.sent.push(data); } - close(code?: number): void { this.closed = true; this.closeCode = code; } - addEventListener(type: string, listener: Listener): void { - (this.listeners[type] ??= []).push(listener); - } - emit(type: string, event: { data?: unknown; code?: number } = {}): void { - for (const l of this.listeners[type] ?? []) l(event); - } - lastSent(): { type?: string; id?: string; payload?: Record } { - return JSON.parse(this.sent[this.sent.length - 1]); - } -} - -function createWsClient() { - FakeWebSocket.instances = []; - return new HashLock({ - endpoint: 'https://hashlock.markets/graphql', - accessToken: 'test-token', - fetch: vi.fn() as unknown as typeof fetch, - webSocket: FakeWebSocket as unknown as WebSocketConstructor, - }); -} - -describe('deriveWsEndpoint', () => { - it('switches http(s) to ws(s) and leaves ws untouched', () => { - expect(deriveWsEndpoint('https://hashlock.markets/graphql')).toBe('wss://hashlock.markets/graphql'); - expect(deriveWsEndpoint('http://localhost:4000/graphql')).toBe('ws://localhost:4000/graphql'); - expect(deriveWsEndpoint('wss://x/graphql')).toBe('wss://x/graphql'); - }); -}); - -describe('onInstantFillRequested (solver-scoped subscription)', () => { - it('runs the graphql-transport-ws handshake and delivers events', () => { - const hl = createWsClient(); - const events: InstantFillRequestedEvent[] = []; - hl.onInstantFillRequested((e) => events.push(e)); - - const ws = FakeWebSocket.instances[0]; - expect(ws.url).toBe('wss://hashlock.markets/graphql'); - expect(ws.protocols).toBe('graphql-transport-ws'); - - ws.emit('open'); - const init = ws.lastSent(); - expect(init.type).toBe('connection_init'); - expect(init.payload).toEqual({ authorization: 'Bearer test-token' }); - - ws.emit('message', { data: JSON.stringify({ type: 'connection_ack' }) }); - const sub = ws.lastSent(); - expect(sub.type).toBe('subscribe'); - const query = String(sub.payload?.query); - expect(query).toContain('instantFillRequested'); - // InstantFillRequested payload: fillId quoteId rfqId state amountWei createdAt - expect(query).toContain('fillId'); - expect(query).toContain('rfqId'); - expect(query).toContain('amountWei'); - // Fields that do NOT exist on InstantFillRequested must not be selected - // (they made the live server reject the subscription). - expect(query).not.toMatch(/\bid\b/); - expect(query).not.toContain('tradeId'); - expect(query).not.toContain('frontTxHash'); - expect(query).not.toContain('frontedAt'); - - ws.emit('message', { - data: JSON.stringify({ type: 'next', id: '1', payload: { data: { instantFillRequested: REQUESTED_EVENT } } }), - }); - expect(events).toHaveLength(1); - expect(events[0].fillId).toBe('fill-1'); - expect(typeof events[0].amountWei).toBe('string'); - }); - - it('unsubscribe sends complete and closes the socket', () => { - const hl = createWsClient(); - const handle = hl.onInstantFillRequested(() => {}); - const ws = FakeWebSocket.instances[0]; - ws.emit('open'); - ws.emit('message', { data: JSON.stringify({ type: 'connection_ack' }) }); - - handle.unsubscribe(); - expect(ws.lastSent()).toMatchObject({ id: '1', type: 'complete' }); - expect(ws.closed).toBe(true); - }); - - it('protocol error frames surface through onError as GraphQLError', () => { - const hl = createWsClient(); - const errors: Error[] = []; - hl.onInstantFillRequested(() => {}, { onError: (e) => errors.push(e) }); - const ws = FakeWebSocket.instances[0]; - ws.emit('open'); - ws.emit('message', { data: JSON.stringify({ type: 'connection_ack' }) }); - ws.emit('message', { - data: JSON.stringify({ type: 'error', id: '1', payload: [{ message: 'forbidden' }] }), - }); - - expect(errors).toHaveLength(1); - expect(errors[0]).toBeInstanceOf(GraphQLError); - expect(errors[0].message).toBe('forbidden'); - }); - - it('unexpected close surfaces a NetworkError once', () => { - const hl = createWsClient(); - const errors: Error[] = []; - hl.onInstantFillRequested(() => {}, { onError: (e) => errors.push(e) }); - const ws = FakeWebSocket.instances[0]; - ws.emit('open'); - ws.emit('close', { code: 1006 }); - ws.emit('error'); // must not double-report after close - - expect(errors).toHaveLength(1); - expect(errors[0]).toBeInstanceOf(NetworkError); - }); - - it('throws a typed error when no WebSocket implementation exists', () => { - const hl = new HashLock({ - endpoint: 'https://hashlock.markets/graphql', - fetch: vi.fn() as unknown as typeof fetch, - }); - const g = globalThis as { WebSocket?: unknown }; - const saved = g.WebSocket; - delete g.WebSocket; - try { - expect(() => hl.onInstantFillRequested(() => {})).toThrow(/WebSocket/); - } finally { - if (saved !== undefined) g.WebSocket = saved; - } - }); -}); - -describe('onInstantFillFronted (taker-scoped subscription)', () => { - it('subscribes to instantFillFronted and delivers the fronted event', () => { - const hl = createWsClient(); - const events: InstantFillFrontedEvent[] = []; - hl.onInstantFillFronted((e) => events.push(e)); - const ws = FakeWebSocket.instances[0]; - ws.emit('open'); - ws.emit('message', { data: JSON.stringify({ type: 'connection_ack' }) }); - const query = String(ws.lastSent().payload?.query); - expect(query).toContain('instantFillFronted'); - // InstantFillFronted payload: fillId quoteId rfqId tradeId state amountWei frontTxHash frontedAt - expect(query).toContain('fillId'); - expect(query).toContain('tradeId'); - expect(query).toContain('frontTxHash'); - expect(query).toContain('frontedAt'); - // Fields that do NOT exist on InstantFillFronted must not be selected. - expect(query).not.toMatch(/\bid\b/); - expect(query).not.toContain('createdAt'); - - ws.emit('message', { - data: JSON.stringify({ type: 'next', id: '1', payload: { data: { instantFillFronted: FRONTED_EVENT } } }), - }); - expect(events[0].state).toBe('fronted'); - expect(events[0].frontTxHash).toBe('0xfront'); - }); -}); - -describe('serveInstantFills (solver flow helper)', () => { - it('fronts each requested fill and auto-marks it fronted', async () => { - FakeWebSocket.instances = []; - const fetch = mockFetchSequence({ - data: { markInstantFillFronted: { ...FILL, state: 'fronted', frontTxHash: '0xfront' } }, - }); - const hl = new HashLock({ - endpoint: 'https://hashlock.markets/graphql', - accessToken: 'test-token', - fetch: fetch as unknown as typeof fetch, - retries: 0, - webSocket: FakeWebSocket as unknown as WebSocketConstructor, - }); - - const frontedFills: InstantFill[] = []; - let resolveDone: () => void; - const done = new Promise((r) => { resolveDone = r; }); - hl.serveInstantFills( - // The event carries the fill id as `fillId` (no `id` field exists - // on InstantFillRequested). - async (event) => `0xfront-for-${event.fillId}`, - { onFronted: (f) => { frontedFills.push(f); resolveDone(); } }, - ); - - const ws = FakeWebSocket.instances[0]; - ws.emit('open'); - ws.emit('message', { data: JSON.stringify({ type: 'connection_ack' }) }); - ws.emit('message', { - data: JSON.stringify({ type: 'next', id: '1', payload: { data: { instantFillRequested: REQUESTED_EVENT } } }), - }); - await done; - - expect(frontedFills).toHaveLength(1); - expect(frontedFills[0].state).toBe('fronted'); - const body = requestBody(fetch); - expect(body.query).toContain('markInstantFillFronted'); - expect(body.variables).toEqual({ fillId: 'fill-1', frontTxHash: '0xfront-for-fill-1' }); - }); - - it('fronting failure reaches onError with the fill attached, watch continues', async () => { - FakeWebSocket.instances = []; - const hl = new HashLock({ - endpoint: 'https://hashlock.markets/graphql', - accessToken: 'test-token', - fetch: vi.fn() as unknown as typeof fetch, - webSocket: FakeWebSocket as unknown as WebSocketConstructor, - }); - - const seen: Array<{ err: Error; event?: InstantFillRequestedEvent }> = []; - let resolveDone: () => void; - const done = new Promise((r) => { resolveDone = r; }); - hl.serveInstantFills( - async () => { throw new Error('vault empty'); }, - { onError: (err, event) => { seen.push({ err, event }); resolveDone(); } }, - ); - - const ws = FakeWebSocket.instances[0]; - ws.emit('open'); - ws.emit('message', { data: JSON.stringify({ type: 'connection_ack' }) }); - ws.emit('message', { - data: JSON.stringify({ type: 'next', id: '1', payload: { data: { instantFillRequested: REQUESTED_EVENT } } }), - }); - await done; - - expect(seen[0].err.message).toBe('vault empty'); - expect(seen[0].event?.fillId).toBe('fill-1'); - expect(ws.closed).toBe(false); // subscription stays alive - }); -}); diff --git a/src/__tests__/schema-validate.test.ts b/src/__tests__/schema-validate.test.ts deleted file mode 100644 index af25cb1..0000000 --- a/src/__tests__/schema-validate.test.ts +++ /dev/null @@ -1,240 +0,0 @@ -/** - * Schema-drift guard: every GraphQL operation string the SDK sends is - * statically validated against the VENDORED authoritative SDL from the - * main repo (test/fixtures/schema.graphql + schema.subscriptions.graphql, - * refreshed via `node scripts/vendor-schema.mjs `). - * - * This exists because the Instant Settlement surface initially shipped - * with operation strings written from a brief instead of the real SDL — - * two confirmed drifts (subscription payload fields, minTrust type). - * Any future field/arg drift now fails this suite instead of failing at - * runtime against production. - * - * `graphql` is a devDependency ONLY — the published SDK keeps zero - * runtime dependencies. - */ -import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { describe, it, expect, vi } from 'vitest'; -import { - parse, - validate, - buildASTSchema, - Kind, -} from 'graphql'; -import type { DefinitionNode, DocumentNode, GraphQLSchema } from 'graphql'; -import { HashLock } from '../hashlock.js'; -import { policyPresets } from '../instant.js'; -import type { WebSocketConstructor } from '../ws.js'; - -// ─── Vendored schema loading ───────────────────────────────── - -function fixture(name: string): string { - return readFileSync( - fileURLToPath(new URL(`../../test/fixtures/${name}`, import.meta.url)), - 'utf8', - ); -} - -/** - * Build an executable-enough schema from vendored SDL: - * - drop the Apollo Federation `extend schema @link(...)` header - * (gateway-only; the SDK never speaks federation directives); - * - fold `extend type X` extensions in (buildASTSchema accepts a doc - * that defines X and later extends it via concatenated definitions — - * we merge manually to stay graphql-js-version-agnostic); - * - inject a dummy Query root for the subscriptions-only SDL - * (graphql-js refuses to validate against a schema without Query). - */ -function buildVendoredSchema(sdl: string): GraphQLSchema { - const doc = parse(sdl); - const defs = doc.definitions.filter((d) => d.kind !== Kind.SCHEMA_EXTENSION); - - // Merge `extend type X { fields }` into the base `type X` definition. - const merged: DefinitionNode[] = []; - const extensions = defs.filter((d) => d.kind === Kind.OBJECT_TYPE_EXTENSION); - for (const def of defs) { - if (def.kind === Kind.OBJECT_TYPE_EXTENSION) continue; - if (def.kind === Kind.OBJECT_TYPE_DEFINITION) { - const extra = extensions - .filter((e) => e.kind === Kind.OBJECT_TYPE_EXTENSION && e.name.value === def.name.value) - .flatMap((e) => e.fields ?? []); - if (extra.length > 0) { - merged.push({ ...def, fields: [...(def.fields ?? []), ...extra] }); - continue; - } - } - merged.push(def); - } - - const hasQuery = merged.some( - (d) => d.kind === Kind.OBJECT_TYPE_DEFINITION && d.name.value === 'Query', - ); - if (!hasQuery) { - merged.push(...parse('type Query { _sdkSchemaValidationDummy: Boolean }').definitions); - } - - const finalDoc: DocumentNode = { kind: Kind.DOCUMENT, definitions: merged }; - return buildASTSchema(finalDoc); -} - -const httpSchema = buildVendoredSchema(fixture('schema.graphql')); -const wsSchema = buildVendoredSchema(fixture('schema.subscriptions.graphql')); - -function expectValid(schema: GraphQLSchema, operation: string): void { - const errors = validate(schema, parse(operation)); - expect( - errors.map((e) => e.message), - `operation failed schema validation:\n${operation}`, - ).toEqual([]); -} - -// ─── Capture the EXACT operation strings the SDK sends ─────── - -/** GraphQLClient stand-in that records operations and returns hollow data. */ -function capturingClient(captured: string[]) { - const run = (query: string): Promise> => { - captured.push(query); - // Every HashLock method destructures exactly one root field — hand - // back a proxy that has every key. - return Promise.resolve( - new Proxy({}, { get: () => ({}) }) as Record, - ); - }; - return { - query: run, - mutate: run, - setAccessToken: (): void => {}, - getAccessToken: (): string | undefined => 'test-token', - }; -} - -type Listener = (event: { data?: unknown }) => void; - -/** Minimal fake socket: handshakes far enough to capture the subscribe payload. */ -class CaptureWebSocket { - static lastQuery: string | undefined; - private listeners: Record = {}; - constructor(_url: string, _protocols?: string | string[]) { - queueMicrotask(() => { - this.emit('open'); - this.emit('message', { data: JSON.stringify({ type: 'connection_ack' }) }); - }); - } - send(data: string): void { - const msg = JSON.parse(data) as { type?: string; payload?: { query?: string } }; - if (msg.type === 'subscribe' && msg.payload?.query) { - CaptureWebSocket.lastQuery = msg.payload.query; - } - } - close(): void {} - addEventListener(type: string, listener: Listener): void { - (this.listeners[type] ??= []).push(listener); - } - private emit(type: string, event: { data?: unknown } = {}): void { - for (const l of this.listeners[type] ?? []) l(event); - } -} - -function makeSdk(captured: string[]): HashLock { - const hl = new HashLock({ - endpoint: 'http://localhost:4000/graphql', - accessToken: 'test-token', - fetch: vi.fn() as unknown as typeof fetch, - webSocket: CaptureWebSocket as unknown as WebSocketConstructor, - }); - (hl as unknown as { client: ReturnType }).client = - capturingClient(captured); - return hl; -} - -async function captureOp(invoke: (hl: HashLock) => Promise): Promise { - const captured: string[] = []; - await invoke(makeSdk(captured)); - expect(captured).toHaveLength(1); - return captured[0]; -} - -async function captureSubscription(invoke: (hl: HashLock) => void): Promise { - CaptureWebSocket.lastQuery = undefined; - invoke(makeSdk([])); - await new Promise((r) => setTimeout(r, 0)); // let the fake handshake run - expect(CaptureWebSocket.lastQuery).toBeDefined(); - return CaptureWebSocket.lastQuery as string; -} - -// ─── HTTP operations (queries + mutations) ─────────────────── - -const HTTP_OPERATIONS: Array<[name: string, invoke: (hl: HashLock) => Promise]> = [ - ['createRFQ', (hl) => hl.createRFQ({ baseToken: 'ETH', quoteToken: 'USDT', side: 'SELL', amount: '1' })], - ['getRFQ', (hl) => hl.getRFQ('rfq-1')], - ['listRFQs', (hl) => hl.listRFQs()], - ['cancelRFQ', (hl) => hl.cancelRFQ('rfq-1')], - ['submitQuote', (hl) => hl.submitQuote({ rfqId: 'rfq-1', price: '1', amount: '1' })], - ['acceptQuote (no policy)', (hl) => hl.acceptQuote('q-1')], - ['acceptQuote (with policy)', (hl) => hl.acceptQuote('q-1', policyPresets.balanced)], - ['getQuotes', (hl) => hl.getQuotes('rfq-1')], - ['requestInstantFill', (hl) => hl.requestInstantFill('rfq-1', 'q-1')], - ['markInstantFillFronted', (hl) => hl.markInstantFillFronted('fill-1', '0xfront')], - ['getTrade', (hl) => hl.getTrade('t-1')], - ['listTrades', (hl) => hl.listTrades()], - ['confirmDirectTrade', (hl) => hl.confirmDirectTrade({ - counterpartyId: 'u-2', baseToken: 'ETH', quoteToken: 'USDT', side: 'SELL', - baseAmount: '1', price: '1', chainId: '11155111', - })], - ['acceptTrade', (hl) => hl.acceptTrade('t-1')], - ['cancelTrade', (hl) => hl.cancelTrade('t-1')], - ['confirmSettlementWallets', (hl) => hl.confirmSettlementWallets({ - tradeId: 't-1', sendWalletId: 'w-1', receiveWalletId: 'w-2', - })], - ['fundHTLC', (hl) => hl.fundHTLC({ tradeId: 't-1', txHash: '0xabc', role: 'INITIATOR' })], - ['claimHTLC', (hl) => hl.claimHTLC({ tradeId: 't-1', txHash: '0xabc', preimage: '0x01' })], - ['refundHTLC', (hl) => hl.refundHTLC({ tradeId: 't-1', txHash: '0xabc' })], - ['getHTLCs', (hl) => hl.getHTLCs('t-1')], - ['prepareBitcoinHTLC', (hl) => hl.prepareBitcoinHTLC({ - tradeId: 't-1', role: 'INITIATOR', senderPubKey: '02ab', receiverPubKey: '03cd', - timelock: 1700000000, amountSats: '100000', - })], - ['buildBitcoinClaimPSBT', (hl) => hl.buildBitcoinClaimPSBT({ - tradeId: 't-1', htlcId: 'h-1', preimage: '0x01', destinationPubKey: '02ab', - })], - ['broadcastBitcoinTx', (hl) => hl.broadcastBitcoinTx({ tradeId: 't-1', txHex: '0200' })], -]; - -describe('SDK operations validate against the vendored trade-service schema', () => { - describe('HTTP operations (typeDefs schema)', () => { - it.each(HTTP_OPERATIONS)('%s', async (_name, invoke) => { - expectValid(httpSchema, await captureOp(invoke)); - }); - - // KNOWN PRE-EXISTING DRIFT (since v0.1.0, NOT part of the instant - // surface): getHTLCStatus selects nested `initiatorHTLC { … }` / - // `counterpartyHTLC { … }`, but the schema's HTLCStatusResult is a - // FLAT type (tradeId/contractAddress/hashlock/…/chainId). Fixing it - // changes the legacy public return shape, so it is documented here - // and skipped instead of silently "fixed". Use getHTLCs(tradeId) - // for per-role HTLC data until getHTLCStatus is reworked. - it.skip('getHTLCStatus — legacy drift: HTLCStatusResult has no initiatorHTLC/counterpartyHTLC', async () => { - expectValid(httpSchema, await captureOp((hl) => hl.getHTLCStatus('t-1'))); - }); - }); - - describe('subscriptions (graphql-ws schema)', () => { - it('onInstantFillRequested selects exactly the InstantFillRequested payload', async () => { - const op = await captureSubscription((hl) => hl.onInstantFillRequested(() => {})); - expectValid(wsSchema, op); - }); - - it('onInstantFillFronted selects exactly the InstantFillFronted payload', async () => { - const op = await captureSubscription((hl) => hl.onInstantFillFronted(() => {})); - expectValid(wsSchema, op); - }); - - it('serveInstantFills subscribes with the InstantFillRequested payload', async () => { - const op = await captureSubscription((hl) => { - hl.serveInstantFills(async () => '0xfront'); - }); - expectValid(wsSchema, op); - }); - }); -}); diff --git a/src/client.ts b/src/client.ts deleted file mode 100644 index 6851015..0000000 --- a/src/client.ts +++ /dev/null @@ -1,160 +0,0 @@ -import type { HashLockConfig } from './types.js'; -import { GraphQLError, NetworkError, AuthError } from './errors.js'; - -const DEFAULT_TIMEOUT = 30_000; -const DEFAULT_RETRIES = 3; -const RETRY_DELAY_BASE = 1000; - -interface GQLResponse { - data?: T; - errors?: Array<{ message: string; path?: string[]; extensions?: Record }>; -} - -/** - * Low-level GraphQL client with retry logic, timeout, and error normalization. - * Used internally by all SDK methods — not exported to consumers. - */ -export class GraphQLClient { - private endpoint: string; - private accessToken: string | undefined; - private timeout: number; - private retries: number; - private fetchFn: typeof fetch; - - constructor(config: HashLockConfig) { - this.endpoint = config.endpoint; - this.accessToken = config.accessToken; - this.timeout = config.timeout ?? DEFAULT_TIMEOUT; - this.retries = config.retries ?? DEFAULT_RETRIES; - this.fetchFn = config.fetch ?? globalThis.fetch; - - if (!this.fetchFn) { - throw new Error('fetch is not available — pass a custom fetch implementation or use Node.js >= 18'); - } - } - - setAccessToken(token: string): void { - this.accessToken = token; - } - - /** Current bearer token — used by the subscription transport. @internal */ - getAccessToken(): string | undefined { - return this.accessToken; - } - - /** - * Execute a GraphQL query with automatic retries on transient failures. - * Retries on: network errors, 5xx status codes. - * Does NOT retry on: 4xx errors, GraphQL validation errors. - */ - async query( - query: string, - variables?: Record | object, - ): Promise { - return this.execute(query, variables, true); - } - - /** - * Execute a GraphQL mutation. - * Only retries on network errors (not on 5xx — mutations are not idempotent). - */ - async mutate( - query: string, - variables?: Record | object, - ): Promise { - return this.execute(query, variables, false); - } - - private async execute( - query: string, - variables: Record | object | undefined, - retryOn5xx: boolean, - ): Promise { - let lastError: Error | undefined; - - for (let attempt = 0; attempt <= this.retries; attempt++) { - try { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), this.timeout); - - const headers: Record = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }; - - if (this.accessToken) { - headers['Authorization'] = `Bearer ${this.accessToken}`; - } - - const response = await this.fetchFn(this.endpoint, { - method: 'POST', - headers, - body: JSON.stringify({ query, variables }), - signal: controller.signal, - }); - - clearTimeout(timer); - - // Auth errors — don't retry - if (response.status === 401 || response.status === 403) { - throw new AuthError(`HTTP ${response.status}: ${response.statusText}`); - } - - // Server errors — retry only for queries - if (response.status >= 500) { - const msg = `Server error: HTTP ${response.status}`; - if (retryOn5xx && attempt < this.retries) { - lastError = new NetworkError(msg); - await this.delay(attempt); - continue; - } - throw new NetworkError(msg); - } - - const json = (await response.json()) as GQLResponse; - - // GraphQL errors - if (json.errors?.length) { - throw new GraphQLError( - json.errors[0].message, - json.errors, - ); - } - - if (!json.data) { - throw new GraphQLError('No data returned', [{ message: 'Empty response' }]); - } - - return json.data; - } catch (err) { - if (err instanceof AuthError || err instanceof GraphQLError) { - throw err; - } - - // Network/timeout errors — retry - const isAbort = err instanceof DOMException && err.name === 'AbortError'; - const isNetwork = err instanceof TypeError; // fetch network failure - - if ((isAbort || isNetwork) && attempt < this.retries) { - lastError = err instanceof Error ? err : new Error(String(err)); - await this.delay(attempt); - continue; - } - - if (err instanceof NetworkError) throw err; - - throw new NetworkError( - isAbort ? 'Request timed out' : `Network error: ${err instanceof Error ? err.message : err}`, - err instanceof Error ? err : undefined, - ); - } - } - - throw lastError ?? new NetworkError('Request failed after all retries'); - } - - private delay(attempt: number): Promise { - const ms = RETRY_DELAY_BASE * Math.pow(2, attempt); - return new Promise((resolve) => setTimeout(resolve, ms)); - } -} diff --git a/src/errors.ts b/src/errors.ts deleted file mode 100644 index 215e79d..0000000 --- a/src/errors.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Base error class for all HashLock SDK errors. - */ -export class HashLockError extends Error { - constructor( - message: string, - public readonly code: string, - public readonly details?: unknown, - ) { - super(message); - this.name = 'HashLockError'; - } -} - -/** - * GraphQL returned errors in the response. - * - * `extensions` carries the server's structured error data (e.g. - * `code`, `http.status`, `metadata.lane` for instant-fill lane - * conflicts) and is preserved verbatim for typed classification. - */ -export class GraphQLError extends HashLockError { - constructor( - message: string, - public readonly errors: Array<{ - message: string; - path?: string[]; - extensions?: Record; - }>, - ) { - super(message, 'GRAPHQL_ERROR', errors); - this.name = 'GraphQLError'; - } -} - -/** - * Network-level error (timeout, DNS failure, etc.). - */ -export class NetworkError extends HashLockError { - constructor(message: string, public readonly cause?: Error) { - super(message, 'NETWORK_ERROR', cause); - this.name = 'NetworkError'; - } -} - -/** - * Authentication error — token missing or expired. - */ -export class AuthError extends HashLockError { - constructor(message: string = 'Authentication required — set accessToken in config or call setAccessToken()') { - super(message, 'AUTH_ERROR'); - this.name = 'AuthError'; - } -} diff --git a/src/experimental.ts b/src/experimental.ts deleted file mode 100644 index 17e162b..0000000 --- a/src/experimental.ts +++ /dev/null @@ -1,65 +0,0 @@ -// ─── Experimental Field Warning ────────────────────────────── -// -// The agent-layer input fields (attestation, agentInstance, -// minCounterpartyTier, hideIdentity) are accepted by the SDK type -// surface today but are NOT yet sent to the Cayman GraphQL backend. -// Passing them is currently a no-op at the network layer. -// -// To avoid silent confusion, emit a one-time warning per field the -// first time a consumer sets one in a given process. The warning -// can be suppressed with `HASHLOCK_SDK_SILENCE_EXPERIMENTAL=1` in -// the environment, for call sites that have already opted in. - -const warned = new Set(); - -function silenceFlag(): boolean { - // Works in both Node and browser-ish runtimes. - const proc = (globalThis as { process?: { env?: Record } }).process; - return proc?.env?.HASHLOCK_SDK_SILENCE_EXPERIMENTAL === '1'; -} - -function emit(fieldPath: string): void { - if (warned.has(fieldPath)) return; - warned.add(fieldPath); - if (silenceFlag()) return; - - const logger = (globalThis as { console?: { warn?: (...args: unknown[]) => void } }).console; - logger?.warn?.( - `[hashlock-sdk] EXPERIMENTAL: '${fieldPath}' was set but the GraphQL ` + - `wire-through to the Cayman backend is not yet implemented. ` + - `The field is accepted at the type surface but is currently ` + - `a no-op at the network layer. Set HASHLOCK_SDK_SILENCE_EXPERIMENTAL=1 ` + - `to suppress this warning.`, - ); -} - -/** - * Test-only: clear the warning dedup set so each test starts fresh. - * @internal - */ -export function __resetExperimentalWarningState(): void { - warned.clear(); -} - -/** - * Inspect an input object for experimental agent-layer fields and - * warn once per field when any are set. Safe to call on every SDK - * method invocation — deduplication is built in. - */ -export function warnIfExperimental( - methodName: string, - input: Record, -): void { - const experimentalFields = [ - 'attestation', - 'agentInstance', - 'minCounterpartyTier', - 'hideIdentity', - ]; - - for (const field of experimentalFields) { - if (input[field] !== undefined) { - emit(`${methodName}.${field}`); - } - } -} diff --git a/src/hashlock.ts b/src/hashlock.ts deleted file mode 100644 index 0cf557a..0000000 --- a/src/hashlock.ts +++ /dev/null @@ -1,718 +0,0 @@ -import { GraphQLClient } from './client.js'; -import { warnIfExperimental } from './experimental.js'; -import { HashLockError } from './errors.js'; -import { - INSTANT_FILL_FIELDS, - INSTANT_FILL_REQUESTED_FIELDS, - INSTANT_FILL_FRONTED_FIELDS, - InstantFillOrphanedError, - classifyInstantFillError, - sanitizeAgentPolicy, -} from './instant.js'; -import type { - AgentPolicy, - InstantFill, - InstantFillRequestedEvent, - InstantFillFrontedEvent, - InstantTakerResult, -} from './instant.js'; -import { - deriveWsEndpoint, - subscribeOverWebSocket, -} from './ws.js'; -import type { SubscriptionHandle, WebSocketConstructor } from './ws.js'; -import type { - HashLockConfig, - RFQ, - Quote, - Trade, - HTLC, - HTLCStatusResult, - FundHTLCResult, - FundHTLCInput, - ClaimHTLCInput, - RefundHTLCInput, - CreateRFQInput, - SubmitQuoteInput, - ConfirmDirectTradeInput, - ConfirmSettlementWalletsInput, - PrepareBitcoinHTLCInput, - BitcoinHTLCPrepareResult, - BuildBitcoinClaimPSBTInput, - BitcoinClaimPSBTResult, - BroadcastBitcoinTxInput, - BitcoinBroadcastResult, - TradeStatus, - RFQStatus, -} from './types.js'; - -/** - * Canonical Hashlock Markets production endpoint. - * - * Points at api-gateway's /graphql (not /api/graphql — that path is a - * browser-only Next.js SSR proxy that reads the httpOnly api-token cookie - * and ignores the Authorization header, so it rejects every SDK call - * with `Unauthorized — missing api-token`). - * - * /graphql is served directly by the Apollo gateway and accepts - * `Authorization: Bearer `, which matches how this SDK - * constructs its requests (see makeRequest in this file). - * - * Superseded: `http://142.93.106.129/api/graphql` (old DigitalOcean - * droplet IP, compromised 2026-04-22, now unreachable and possibly - * attacker-controlled — never restore). - */ -export const MAINNET_ENDPOINT = 'https://hashlock.markets/graphql'; - -/** - * HashLock SDK — TypeScript client for HashLock OTC trading platform. - * - * @example - * ```ts - * import { HashLock } from '@hashlock-tech/sdk'; - * - * const hl = new HashLock({ - * endpoint: 'https://hashlock.markets/graphql', - * accessToken: 'your-jwt-token', - * }); - * - * const rfq = await hl.createRFQ({ - * baseToken: 'ETH', - * quoteToken: 'USDT', - * side: 'SELL', - * amount: '1.0', - * }); - * ``` - */ -export class HashLock { - private client: GraphQLClient; - private config: HashLockConfig; - - constructor(config: HashLockConfig) { - this.config = config; - this.client = new GraphQLClient(config); - } - - /** Update the access token (e.g., after login or token refresh) */ - setAccessToken(token: string): void { - this.client.setAccessToken(token); - } - - // ─── RFQ ───────────────────────────────────────────────── - - /** - * Create a Request for Quote (RFQ). - * Broadcasts to market makers who can respond with prices. - * - * @example - * ```ts - * const rfq = await hl.createRFQ({ - * baseToken: 'ETH', - * quoteToken: 'USDT', - * side: 'SELL', - * amount: '10.0', - * expiresIn: 300, // 5 minutes - * }); - * console.log(`RFQ created: ${rfq.id}`); - * ``` - */ - async createRFQ(input: CreateRFQInput): Promise { - warnIfExperimental('createRFQ', input as unknown as Record); - const { createRFQ } = await this.client.mutate<{ createRFQ: RFQ }>(` - mutation CreateRFQ($baseToken: String!, $quoteToken: String!, $side: Side!, $amount: String!, $expiresIn: Int, $isBlind: Boolean, $baseChain: String, $quoteChain: String) { - createRFQ(baseToken: $baseToken, quoteToken: $quoteToken, side: $side, amount: $amount, expiresIn: $expiresIn, isBlind: $isBlind, baseChain: $baseChain, quoteChain: $quoteChain) { - id userId baseToken quoteToken side amount isBlind status expiresAt createdAt quotesCount - } - } - `, input); - return createRFQ; - } - - /** - * Get a single RFQ by ID. - */ - async getRFQ(id: string): Promise { - const { rfq } = await this.client.query<{ rfq: RFQ | null }>(` - query GetRFQ($id: ID!) { - rfq(id: $id) { - id userId baseToken quoteToken side amount isBlind status expiresAt createdAt quotesCount - quotes { id rfqId marketMakerId price amount status createdAt } - } - } - `, { id }); - return rfq; - } - - /** - * List RFQs with optional status filter and pagination. - */ - async listRFQs(params?: { status?: RFQStatus; page?: number; pageSize?: number }) { - const { rfqs } = await this.client.query<{ rfqs: { rfqs: RFQ[]; total: number; page: number; pageSize: number } }>(` - query ListRFQs($status: RFQStatus, $page: Int, $pageSize: Int) { - rfqs(status: $status, page: $page, pageSize: $pageSize) { - rfqs { id userId baseToken quoteToken side amount isBlind status expiresAt createdAt quotesCount } - total page pageSize - } - } - `, params); - return rfqs; - } - - /** - * Cancel an active RFQ. - */ - async cancelRFQ(id: string): Promise { - const { cancelRFQ } = await this.client.mutate<{ cancelRFQ: RFQ }>(` - mutation CancelRFQ($id: ID!) { - cancelRFQ(id: $id) { - id status - } - } - `, { id }); - return cancelRFQ; - } - - // ─── Quotes ────────────────────────────────────────────── - - /** - * Submit a price quote in response to an RFQ. - * - * @example - * ```ts - * const quote = await hl.submitQuote({ - * rfqId: 'rfq-uuid', - * price: '3450.00', - * amount: '10.0', - * }); - * ``` - */ - async submitQuote(input: SubmitQuoteInput): Promise { - warnIfExperimental('submitQuote', input as unknown as Record); - const { submitQuote } = await this.client.mutate<{ submitQuote: Quote }>(` - mutation SubmitQuote($rfqId: ID!, $price: String!, $amount: String!, $expiresIn: Int, $instantFill: Boolean, $solverVaultAddr: String) { - submitQuote(rfqId: $rfqId, price: $price, amount: $amount, expiresIn: $expiresIn, instantFill: $instantFill, solverVaultAddr: $solverVaultAddr) { - id rfqId marketMakerId price amount status createdAt expiresAt instantFill solverVaultAddr - } - } - `, input); - return submitQuote; - } - - /** - * Accept a quote — creates a trade from the RFQ flow. - * - * @param policy Optional settlement PREFERENCE (`AgentPolicy`). A - * policy never causes the accept to fail: the SDK sanitizes it - * (invalid fields are dropped; nothing valid left → omitted - * entirely) and the backend treats it as routing advice only. - * Use `policyPresets.instant / .balanced / .trustless` or spread - * your own: `{ ...policyPresets.balanced, maxFeeBps: 30 }`. - * - * WIRE NOTE: the schema's `AgentPolicyInput.minTrust` is `Int` - * (a 0-100 trust score) — a `TrustLevel` string is converted via - * `TRUST_LEVEL_TO_SCORE` (low→0, med→50, max→100) before sending; - * raw 0-100 numbers are floored/clamped. See `sanitizeAgentPolicy`. - */ - async acceptQuote(quoteId: string, policy?: AgentPolicy): Promise { - const sanitized = sanitizeAgentPolicy(policy); - if (sanitized) { - const { acceptQuote } = await this.client.mutate<{ acceptQuote: Quote }>(` - mutation AcceptQuote($quoteId: ID!, $policy: AgentPolicyInput) { - acceptQuote(quoteId: $quoteId, policy: $policy) { - id rfqId status trade { id status } - } - } - `, { quoteId, policy: sanitized }); - return acceptQuote; - } - const { acceptQuote } = await this.client.mutate<{ acceptQuote: Quote }>(` - mutation AcceptQuote($quoteId: ID!) { - acceptQuote(quoteId: $quoteId) { - id rfqId status trade { id status } - } - } - `, { quoteId }); - return acceptQuote; - } - - /** - * Get all quotes for an RFQ. - */ - async getQuotes(rfqId: string): Promise { - const { quotes } = await this.client.query<{ quotes: Quote[] }>(` - query GetQuotes($rfqId: ID!) { - quotes(rfqId: $rfqId) { - id rfqId marketMakerId price amount status createdAt expiresAt - deliveryDelayHours collateralBtcSats isCollateralBacked - } - } - `, { rfqId }); - return quotes; - } - - // ─── Instant Settlement (Lane A) ───────────────────────── - - /** - * Request an instant fill for a quote that carries an instant-fill - * commitment (`quote.instantFill === true`). Taker side. - * - * CANONICAL ORDER: call this FIRST; only after it succeeds call - * `acceptQuote`. Prefer `requestInstantFillAndAccept` which - * enforces the order and maps the typed failure modes for you. - * - * Typed failures (see `classifyInstantFillError`): - * - `INSTANT_FILL_DISABLED` — feature flag off - * - 409 with `extensions.metadata.lane` — lane conflict - * - 409 without lane — fill already requested for this quote - */ - async requestInstantFill(rfqId: string, quoteId: string): Promise { - const { requestInstantFill } = await this.client.mutate<{ requestInstantFill: InstantFill }>(` - mutation RequestInstantFill($rfqId: ID!, $quoteId: ID!) { - requestInstantFill(rfqId: $rfqId, quoteId: $quoteId) { - ${INSTANT_FILL_FIELDS} - } - } - `, { rfqId, quoteId }); - return requestInstantFill; - } - - /** - * Mark an instant fill as fronted — solver side (must be the - * market maker of the underlying quote). Call after the vault's - * fronting payment tx is sent on-chain. - */ - async markInstantFillFronted(fillId: string, frontTxHash: string): Promise { - const { markInstantFillFronted } = await this.client.mutate<{ markInstantFillFronted: InstantFill }>(` - mutation MarkInstantFillFronted($fillId: ID!, $frontTxHash: String!) { - markInstantFillFronted(fillId: $fillId, frontTxHash: $frontTxHash) { - ${INSTANT_FILL_FIELDS} - } - } - `, { fillId, frontTxHash }); - return markInstantFillFronted; - } - - /** - * Taker flow helper — the canonical instant-fill sequence in one - * call, with typed fallbacks (see `InstantTakerResult`): - * - * 1. `requestInstantFill(rfqId, quoteId)` - * - SUCCESS → 2 - * - typed refusal (disabled / lane conflict / already requested) - * → standard `acceptQuote` fallback → `{ kind: 'standard', reason }` - * - any other error (auth/network/validation) → THROWN - * 2. `acceptQuote(quoteId, policy)` - * - SUCCESS → `{ kind: 'instant', fill, quote }` - * - FAILURE → `{ kind: 'fill_orphaned', fill, error }` — the fill - * is committed server-side; recover with - * `retryAcceptAfterInstantFill(result.fill)`. - * - * @example - * ```ts - * const res = await hl.requestInstantFillAndAccept(rfqId, quoteId, { - * policy: policyPresets.instant, - * }); - * if (res.kind === 'instant') console.log('fronting incoming', res.fill.amountWei); - * if (res.kind === 'standard') console.log('standard path:', res.reason); - * if (res.kind === 'fill_orphaned') await hl.retryAcceptAfterInstantFill(res.fill); - * ``` - */ - async requestInstantFillAndAccept( - rfqId: string, - quoteId: string, - options?: { policy?: AgentPolicy }, - ): Promise { - let fill: InstantFill; - try { - fill = await this.requestInstantFill(rfqId, quoteId); - } catch (err) { - const fallback = classifyInstantFillError(err); - if (!fallback) throw err; // unexpected — do not swallow - const quote = await this.acceptQuote(quoteId, options?.policy); - return { kind: 'standard', reason: fallback.reason, lane: fallback.lane, quote }; - } - - try { - const quote = await this.acceptQuote(quoteId, options?.policy); - return { kind: 'instant', fill, quote }; - } catch (err) { - const cause = err instanceof Error ? err : new Error(String(err)); - return { kind: 'fill_orphaned', fill, error: new InstantFillOrphanedError(fill, cause) }; - } - } - - /** - * Accept-only retry for an orphaned instant fill (fill committed, - * accept failed). NEVER re-requests the fill — a second - * `requestInstantFill` would 409 on the exactly-once-per-quote - * guard. Returns the same result union as - * `requestInstantFillAndAccept`. - */ - async retryAcceptAfterInstantFill( - fill: InstantFill, - policy?: AgentPolicy, - ): Promise { - try { - const quote = await this.acceptQuote(fill.quoteId, policy); - return { kind: 'instant', fill, quote }; - } catch (err) { - const cause = err instanceof Error ? err : new Error(String(err)); - return { kind: 'fill_orphaned', fill, error: new InstantFillOrphanedError(fill, cause) }; - } - } - - /** - * Solver side: subscribe to instant-fill requests against YOUR - * quotes (server scopes the stream by the authenticated maker). - * Requires a WebSocket-capable runtime (see `HashLockConfig.webSocket`). - * - * The payload is `InstantFillRequestedEvent` (fillId/quoteId/rfqId/ - * state/amountWei/createdAt) — NOT the `InstantFill` mutation type; - * the fill id arrives as `fillId`. - */ - onInstantFillRequested( - onFill: (event: InstantFillRequestedEvent) => void, - opts?: { onError?: (err: Error) => void; onComplete?: () => void }, - ): SubscriptionHandle { - return this.subscribeInstantFill( - 'instantFillRequested', - INSTANT_FILL_REQUESTED_FIELDS, - onFill, - opts, - ); - } - - /** - * Taker side: subscribe to fronting notifications for YOUR - * instant fills (server scopes the stream by the authenticated taker). - * - * The payload is `InstantFillFrontedEvent` (fillId/quoteId/rfqId/ - * tradeId/state/amountWei/frontTxHash/frontedAt). - */ - onInstantFillFronted( - onFill: (event: InstantFillFrontedEvent) => void, - opts?: { onError?: (err: Error) => void; onComplete?: () => void }, - ): SubscriptionHandle { - return this.subscribeInstantFill( - 'instantFillFronted', - INSTANT_FILL_FRONTED_FIELDS, - onFill, - opts, - ); - } - - /** - * Solver flow helper: watch `instantFillRequested`, front each fill - * via your `front` callback (send the vault payment, return its tx - * hash), then `markInstantFillFronted` automatically. - * - * @example - * ```ts - * const handle = hl.serveInstantFills(async (event) => { - * const txHash = await vault.front(event.quoteId, BigInt(event.amountWei)); - * return txHash; - * }, { onFronted: (f) => console.log('fronted', f.id) }); - * ``` - */ - serveInstantFills( - front: (event: InstantFillRequestedEvent) => Promise, - opts?: { - onFronted?: (fill: InstantFill) => void; - onError?: (err: Error, event?: InstantFillRequestedEvent) => void; - onComplete?: () => void; - }, - ): SubscriptionHandle { - return this.onInstantFillRequested( - (event) => { - void (async () => { - try { - const txHash = await front(event); - const updated = await this.markInstantFillFronted(event.fillId, txHash); - opts?.onFronted?.(updated); - } catch (err) { - const e = err instanceof Error ? err : new Error(String(err)); - opts?.onError?.(e, event); - } - })(); - }, - { onError: (err) => opts?.onError?.(err), onComplete: opts?.onComplete }, - ); - } - - private subscribeInstantFill( - field: 'instantFillRequested' | 'instantFillFronted', - selection: string, - onFill: (event: T) => void, - opts?: { onError?: (err: Error) => void; onComplete?: () => void }, - ): SubscriptionHandle { - const ctor = - this.config.webSocket ?? - (globalThis as { WebSocket?: WebSocketConstructor }).WebSocket; - if (!ctor) { - throw new HashLockError( - 'GraphQL subscriptions need a WebSocket implementation — pass ' + - "`webSocket` in HashLockConfig (e.g. `import WebSocket from 'ws'`) " + - 'or run on a runtime with a global WebSocket (browsers, Node >= 22).', - 'WEBSOCKET_UNAVAILABLE', - ); - } - const opName = field === 'instantFillRequested' ? 'InstantFillRequested' : 'InstantFillFronted'; - return subscribeOverWebSocket>({ - url: this.config.wsEndpoint ?? deriveWsEndpoint(this.config.endpoint), - webSocket: ctor, - token: this.client.getAccessToken(), - query: `subscription ${opName} { ${field} { ${selection} } }`, - onData: (data) => { - const fill = data[field]; - if (fill) onFill(fill); - }, - onError: opts?.onError, - onComplete: opts?.onComplete, - }); - } - - // ─── Trades ────────────────────────────────────────────── - - /** - * Get a single trade by ID. - */ - async getTrade(id: string): Promise { - const { trade } = await this.client.query<{ trade: Trade | null }>(` - query GetTrade($id: ID!) { - trade(id: $id) { - id initiatorId counterpartyId baseToken quoteToken side baseAmount quoteAmount price status createdAt - } - } - `, { id }); - return trade; - } - - /** - * List trades with optional status filter. - */ - async listTrades(params?: { status?: TradeStatus; page?: number; pageSize?: number }) { - const { trades } = await this.client.query<{ trades: { trades: Trade[]; total: number } }>(` - query ListTrades($status: TradeStatus, $page: Int, $pageSize: Int) { - trades(status: $status, page: $page, pageSize: $pageSize) { - trades { id initiatorId counterpartyId baseToken quoteToken side baseAmount quoteAmount price status createdAt } - total - } - } - `, params); - return trades; - } - - /** - * Create a direct trade from 1-on-1 chat (skips RFQ flow). - */ - async confirmDirectTrade(input: ConfirmDirectTradeInput): Promise { - const { confirmDirectTrade } = await this.client.mutate<{ confirmDirectTrade: Trade }>(` - mutation ConfirmDirectTrade($counterpartyId: ID!, $baseToken: String!, $quoteToken: String!, $side: Side!, $baseAmount: String!, $price: String!, $chainId: String!, $broadcastRfqId: ID, $conversationId: ID) { - confirmDirectTrade(counterpartyId: $counterpartyId, baseToken: $baseToken, quoteToken: $quoteToken, side: $side, baseAmount: $baseAmount, price: $price, chainId: $chainId, broadcastRfqId: $broadcastRfqId, conversationId: $conversationId) { - id initiatorId counterpartyId baseToken quoteToken side baseAmount quoteAmount price status createdAt - } - } - `, input); - return confirmDirectTrade; - } - - /** - * Accept a proposed trade. - */ - async acceptTrade(tradeId: string): Promise { - const { acceptTrade } = await this.client.mutate<{ acceptTrade: Trade }>(` - mutation AcceptTrade($tradeId: ID!) { - acceptTrade(tradeId: $tradeId) { id status } - } - `, { tradeId }); - return acceptTrade; - } - - /** - * Cancel a trade. - */ - async cancelTrade(tradeId: string): Promise { - const { cancelTrade } = await this.client.mutate<{ cancelTrade: Trade }>(` - mutation CancelTrade($tradeId: ID!) { - cancelTrade(tradeId: $tradeId) { id status } - } - `, { tradeId }); - return cancelTrade; - } - - /** - * Confirm settlement wallets for a trade. - */ - async confirmSettlementWallets(input: ConfirmSettlementWalletsInput): Promise { - const { confirmSettlementWallets } = await this.client.mutate<{ confirmSettlementWallets: Trade }>(` - mutation ConfirmWallets($tradeId: ID!, $sendWalletId: ID!, $receiveWalletId: ID!) { - confirmSettlementWallets(tradeId: $tradeId, sendWalletId: $sendWalletId, receiveWalletId: $receiveWalletId) { - id status - } - } - `, input); - return confirmSettlementWallets; - } - - // ─── HTLC — EVM (ETH / ERC-20) ────────────────────────── - - /** - * Record an on-chain HTLC funding transaction. - * Called after the user sends an ETH/ERC20 lock tx on-chain. - * - * @example - * ```ts - * // After sending ETH lock tx on-chain via ethers/viem: - * const result = await hl.fundHTLC({ - * tradeId: 'trade-uuid', - * txHash: '0xabc...', - * role: 'INITIATOR', - * timelock: Math.floor(Date.now() / 1000) + 3600, - * hashlock: '0x...', - * }); - * ``` - */ - async fundHTLC(input: FundHTLCInput): Promise { - warnIfExperimental('fundHTLC', input as unknown as Record); - const { fundHTLC } = await this.client.mutate<{ fundHTLC: FundHTLCResult }>(` - mutation FundHTLC($tradeId: ID!, $txHash: String!, $role: HTLCRole!, $timelock: Int, $hashlock: String, $chainType: String, $senderPubKey: String, $receiverPubKey: String, $redeemScript: String, $refundTxHex: String, $preimage: String) { - fundHTLC(tradeId: $tradeId, txHash: $txHash, role: $role, timelock: $timelock, hashlock: $hashlock, chainType: $chainType, senderPubKey: $senderPubKey, receiverPubKey: $receiverPubKey, redeemScript: $redeemScript, refundTxHex: $refundTxHex, preimage: $preimage) { - tradeId txHash status - } - } - `, input); - return fundHTLC; - } - - /** - * Record an on-chain HTLC claim (preimage reveal). - * - * @example - * ```ts - * const result = await hl.claimHTLC({ - * tradeId: 'trade-uuid', - * txHash: '0xdef...', - * preimage: '0x...', - * }); - * ``` - */ - async claimHTLC(input: ClaimHTLCInput): Promise { - const { claimHTLC } = await this.client.mutate<{ claimHTLC: HTLCStatusResult }>(` - mutation ClaimHTLC($tradeId: ID!, $txHash: String!, $preimage: String!, $chainType: String) { - claimHTLC(tradeId: $tradeId, txHash: $txHash, preimage: $preimage, chainType: $chainType) { - tradeId status - } - } - `, input); - return claimHTLC; - } - - /** - * Record an on-chain HTLC refund (after timelock expiry). - * - * @example - * ```ts - * const result = await hl.refundHTLC({ - * tradeId: 'trade-uuid', - * txHash: '0x...', - * }); - * ``` - */ - async refundHTLC(input: RefundHTLCInput): Promise { - const { refundHTLC } = await this.client.mutate<{ refundHTLC: HTLCStatusResult }>(` - mutation RefundHTLC($tradeId: ID!, $txHash: String!, $chainType: String) { - refundHTLC(tradeId: $tradeId, txHash: $txHash, chainType: $chainType) { - tradeId status - } - } - `, input); - return refundHTLC; - } - - /** - * Get HTLC status for a trade (both initiator and counterparty HTLCs). - */ - async getHTLCStatus(tradeId: string): Promise { - const { htlcStatus } = await this.client.query<{ htlcStatus: HTLCStatusResult | null }>(` - query HTLCStatus($tradeId: ID!) { - htlcStatus(tradeId: $tradeId) { - tradeId status - initiatorHTLC { id tradeId role status contractAddress hashlock timelock amount txHash chainId } - counterpartyHTLC { id tradeId role status contractAddress hashlock timelock amount txHash chainId } - } - } - `, { tradeId }); - return htlcStatus; - } - - /** - * Get all HTLCs for a trade. - */ - async getHTLCs(tradeId: string): Promise { - const { htlcs } = await this.client.query<{ htlcs: HTLC[] }>(` - query GetHTLCs($tradeId: ID!) { - htlcs(tradeId: $tradeId) { - id tradeId role status contractAddress hashlock timelock amount txHash chainType preimage - } - } - `, { tradeId }); - return htlcs; - } - - // ─── HTLC — Bitcoin ────────────────────────────────────── - - /** - * Prepare a Bitcoin HTLC. Returns P2WSH address and redeem script. - * The client funds this address, then calls fundHTLC with the txHash. - * - * @example - * ```ts - * const btcHtlc = await hl.prepareBitcoinHTLC({ - * tradeId: 'trade-uuid', - * role: 'INITIATOR', - * senderPubKey: '02abc...', - * receiverPubKey: '03def...', - * timelock: Math.floor(Date.now() / 1000) + 7200, - * amountSats: '100000', // 0.001 BTC - * }); - * console.log(`Fund this address: ${btcHtlc.htlcAddress}`); - * ``` - */ - async prepareBitcoinHTLC(input: PrepareBitcoinHTLCInput): Promise { - const { prepareBitcoinHTLC } = await this.client.mutate<{ prepareBitcoinHTLC: BitcoinHTLCPrepareResult }>(` - mutation PrepareBTCHTLC($tradeId: ID!, $role: HTLCRole!, $senderPubKey: String!, $receiverPubKey: String!, $timelock: Int!, $amountSats: String!) { - prepareBitcoinHTLC(tradeId: $tradeId, role: $role, senderPubKey: $senderPubKey, receiverPubKey: $receiverPubKey, timelock: $timelock, amountSats: $amountSats) { - tradeId htlcId htlcAddress redeemScript hashlock preimageHash timelock amountSats estimatedClaimFee estimatedRefundFee refundPsbt - } - } - `, input); - return prepareBitcoinHTLC; - } - - /** - * Build an unsigned claim PSBT for a Bitcoin HTLC. - * The client signs with their wallet, then broadcasts via broadcastBitcoinTx. - */ - async buildBitcoinClaimPSBT(input: BuildBitcoinClaimPSBTInput): Promise { - const { buildBitcoinClaimPSBT } = await this.client.mutate<{ buildBitcoinClaimPSBT: BitcoinClaimPSBTResult }>(` - mutation BuildClaim($tradeId: ID!, $htlcId: ID!, $preimage: String!, $destinationPubKey: String!, $feeRate: Int) { - buildBitcoinClaimPSBT(tradeId: $tradeId, htlcId: $htlcId, preimage: $preimage, destinationPubKey: $destinationPubKey, feeRate: $feeRate) { - tradeId htlcId psbtBase64 fee utxoTxid utxoVout - } - } - `, input); - return buildBitcoinClaimPSBT; - } - - /** - * Broadcast a signed Bitcoin transaction. - */ - async broadcastBitcoinTx(input: BroadcastBitcoinTxInput): Promise { - const { broadcastBitcoinTx } = await this.client.mutate<{ broadcastBitcoinTx: BitcoinBroadcastResult }>(` - mutation BroadcastBTC($tradeId: ID!, $txHex: String!) { - broadcastBitcoinTx(tradeId: $tradeId, txHex: $txHex) { txid success } - } - `, input); - return broadcastBitcoinTx; - } -} diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 1452145..0000000 --- a/src/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -export { HashLock, MAINNET_ENDPOINT } from './hashlock.js'; -export { HashLockError, GraphQLError, NetworkError, AuthError } from './errors.js'; -export type * from './types.js'; -export { KYC_TIER_RANK, meetsKycTier } from './principal.js'; -export type { - KycTier, - PrincipalType, - PrincipalAttestation, - AgentInstance, -} from './principal.js'; -export { - policyPresets, - sanitizeAgentPolicy, - classifyInstantFillError, - InstantFillOrphanedError, - INSTANT_FILL_FIELDS, - INSTANT_FILL_REQUESTED_FIELDS, - INSTANT_FILL_FRONTED_FIELDS, - TRUST_LEVEL_TO_SCORE, -} from './instant.js'; -export type { - InstantFill, - InstantFillState, - InstantFillRequestedEvent, - InstantFillFrontedEvent, - AgentPolicy, - AgentPolicyWire, - TrustLevel, - PolicyPresetName, - InstantFillFallback, - InstantFillFallbackReason, - InstantTakerResult, -} from './instant.js'; -export { deriveWsEndpoint } from './ws.js'; -export type { - WebSocketLike, - WebSocketConstructor, - SubscriptionHandle, -} from './ws.js'; diff --git a/src/instant.ts b/src/instant.ts deleted file mode 100644 index 065c386..0000000 --- a/src/instant.ts +++ /dev/null @@ -1,377 +0,0 @@ -// ─── Instant Settlement (Lane A) — taker + solver surface ─── -// -// Mirrors the Instant Settlement Phase 1.6 backend surface: -// -// Quote.instantFill / Quote.solverVaultAddr (maker commitment) -// requestInstantFill(rfqId, quoteId) (taker) -// acceptQuote(quoteId, policy) (taker, policy = preference) -// markInstantFillFronted(fillId, frontTxHash) (solver) -// subscription instantFillRequested (solver-scoped) -// subscription instantFillFronted (taker-scoped) -// -// CANONICAL ORDER (taker instant path): -// requestInstantFill SUCCEEDS → then acceptQuote. -// The `requestInstantFillAndAccept` helper enforces this order inside -// the SDK so callers cannot get it backwards. -// -// MONEY FIELDS: `amountWei` is the committed fill amount in the -// asset's smallest on-chain unit (wei / sats / MIST) as a DECIMAL -// STRING. It covers the full uint256 range — NEVER convert it to a -// JavaScript `number` (precision loss above 2^53). Use `BigInt(amountWei)` -// when arithmetic is needed. - -import { GraphQLError } from './errors.js'; -import { HashLockError } from './errors.js'; -import type { Quote } from './types.js'; - -// ─── InstantFill domain object ─────────────────────────────── - -/** - * Lifecycle state of an instant fill. Linear, forward-only: - * `committed → fronted → settled → reimbursed` (with `cancelled` as - * a terminal off-ramp). - */ -export type InstantFillState = - | 'committed' - | 'fronted' - | 'settled' - | 'reimbursed' - | 'cancelled'; - -export interface InstantFill { - id: string; - quoteId: string; - /** The trade materialised from the accepted quote — null until the - * trade exists (e.g. right after `requestInstantFill`, before accept). */ - tradeId: string | null; - state: InstantFillState; - /** - * Committed fill amount in the asset's smallest on-chain unit - * (wei / sats / MIST) as a decimal string — full uint256 range. - * NEVER convert to `number`; use `BigInt(amountWei)` for math. - */ - amountWei: string; - /** Tx hash of the solver's fronting payment (null until fronted). */ - frontTxHash: string | null; - frontedAt: string | null; - createdAt: string; -} - -/** GraphQL selection set for the `InstantFill` type (mutation payloads: - * requestInstantFill / markInstantFillFronted). */ -export const INSTANT_FILL_FIELDS = - 'id quoteId tradeId state amountWei frontTxHash frontedAt createdAt'; - -// ─── Subscription event payloads ───────────────────────────── -// -// The subscription payload types are NOT the InstantFill type — the -// schema defines dedicated event types with different fields: -// -// type InstantFillRequested { fillId quoteId rfqId state amountWei createdAt } -// type InstantFillFronted { fillId quoteId rfqId tradeId state amountWei frontTxHash frontedAt } -// -// (no `id` — the fill id arrives as `fillId`; Requested has no -// tradeId/frontTxHash/frontedAt; Fronted has no createdAt.) -// Kept 1:1 with backend/services/trade-service/src/schema.ts -// SUBSCRIPTION_SDL; guarded by src/__tests__/schema-validate.test.ts -// against the vendored SDL in test/fixtures/. - -/** Selection set for the `InstantFillRequested` subscription payload. */ -export const INSTANT_FILL_REQUESTED_FIELDS = - 'fillId quoteId rfqId state amountWei createdAt'; - -/** Selection set for the `InstantFillFronted` subscription payload. */ -export const INSTANT_FILL_FRONTED_FIELDS = - 'fillId quoteId rfqId tradeId state amountWei frontTxHash frontedAt'; - -/** - * Payload of the solver-scoped `instantFillRequested` subscription: - * a taker requested an instant fill on one of YOUR quotes. - * Use `fillId` (not `id`) when calling `markInstantFillFronted`. - */ -export interface InstantFillRequestedEvent { - fillId: string; - quoteId: string; - rfqId: string; - /** Always 'committed' at request time. */ - state: InstantFillState; - /** Committed amount in the asset's smallest unit (decimal string — - * full uint256 range; never convert to `number`). */ - amountWei: string; - createdAt: string; -} - -/** - * Payload of the taker-scoped `instantFillFronted` subscription: - * the solver marked one of YOUR requested fills as fronted. - */ -export interface InstantFillFrontedEvent { - fillId: string; - quoteId: string; - rfqId: string; - /** Trade materialised from the accepted quote (fronting requires the - * link, but the schema types it nullable). */ - tradeId: string | null; - /** Always 'fronted' at publish time. */ - state: InstantFillState; - /** Committed amount in the asset's smallest unit (decimal string). */ - amountWei: string; - /** EVM tx hash of the solver vault's fronting transfer. */ - frontTxHash: string; - frontedAt: string; -} - -// ─── Agent policy (single engine, two adapters) ────────────── - -/** Trust requirement level — mirrors the design §13.1 preset table. */ -export type TrustLevel = 'low' | 'med' | 'max'; - -/** - * Wire mapping for `TrustLevel` → the schema's `AgentPolicyInput.minTrust: - * Int` (a 0-100 solver trust/reputation score). The backend compares - * `minTrust > solverReputation` (reputation stubbed at 50 until the - * reputation oracle lands): - * - * | TrustLevel | Int sent | Effect vs the 50 stub | - * |------------|----------|-----------------------------------------------| - * | `low` | 0 | never constrains | - * | `med` | 50 | passes the stub (50 > 50 is false) | - * | `max` | 100 | unmet → steers to the trustless pure-HTLC path | - */ -export const TRUST_LEVEL_TO_SCORE: Record = { - low: 0, - med: 50, - max: 100, -}; - -/** - * Declarative settlement preference attached to `acceptQuote`. - * - * SEMANTICS: a policy is a PREFERENCE, not a commitment. The backend - * never rejects an accept because of its policy — a malformed or - * unsatisfiable policy silently falls back to the standard - * settlement path. The SDK reinforces this by sanitizing the policy - * before sending (see `sanitizeAgentPolicy`). - */ -export interface AgentPolicy { - /** Max acceptable settlement latency in milliseconds. */ - maxLatencyMs?: number; - /** Max acceptable total fee in basis points (0-10000). */ - maxFeeBps?: number; - /** - * Minimum solver trust: either a `TrustLevel` preset name or a raw - * 0-100 integer score. Either way the SDK sends an Int on the wire - * (the schema's `AgentPolicyInput.minTrust` is `Int`) — see - * `TRUST_LEVEL_TO_SCORE` for the preset mapping. `'max'`/100 steers - * to the trustless pure-HTLC lane. - */ - minTrust?: TrustLevel | number; -} - -/** - * The exact shape sent as the `policy` GraphQL variable — every field - * an integer, matching `AgentPolicyInput { maxLatencyMs: Int, - * maxFeeBps: Int, minTrust: Int }`. A `TrustLevel` string here would - * fail GraphQL Int coercion and reject the WHOLE acceptQuote, which - * is why `sanitizeAgentPolicy` always converts before send. - */ -export interface AgentPolicyWire { - maxLatencyMs?: number; - maxFeeBps?: number; - minTrust?: number; -} - -/** - * Policy presets — 1:1 with the human speed-slider presets - * (single engine, two adapters; design doc §13.1): - * - * | Preset | Policy | Under the hood | - * |------------|-------------------------|---------------------------------------| - * | instant | `maxLatencyMs: 3000` | Lane A/B fronting, k=0–1 conf, wide spread | - * | balanced | `minTrust: 'med'` | Lane A, k=2–3 confirmations | - * | trustless | `minTrust: 'max'` | Lane Z pure HTLC, full confs, tight spread | - * - * `balanced` intentionally leaves `maxFeeBps` unset (the design table's - * `max_fee=X` is caller-specific) — spread it in: - * `{ ...policyPresets.balanced, maxFeeBps: 30 }`. - */ -export const policyPresets = { - instant: { maxLatencyMs: 3000 }, - balanced: { minTrust: 'med' }, - trustless: { minTrust: 'max' }, -} as const satisfies Record; - -export type PolicyPresetName = keyof typeof policyPresets; - -/** GraphQL `Int` is a signed 32-bit integer — anything above this fails - * wire coercion and would reject the whole mutation. */ -const GRAPHQL_INT_MAX = 2_147_483_647; - -/** Floor to an integer and clamp into [min, max]; non-finite → undefined. */ -function toBoundedInt(v: unknown, min: number, max: number): number | undefined { - if (typeof v !== 'number' || !Number.isFinite(v)) return undefined; - return Math.min(Math.max(Math.floor(v), min), max); -} - -/** - * Reduce an arbitrary value to the wire policy (`AgentPolicyWire` — all - * fields integers), or `undefined` when nothing valid remains. Unknown - * keys and wrongly-typed values are dropped (never thrown) — this is - * what guarantees, at the SDK layer, that a broken policy can never - * fail an `acceptQuote`: - * - * - the schema types every `AgentPolicyInput` field as `Int`, so a - * non-integer (or a `TrustLevel` string) on the wire would fail - * GraphQL coercion and reject the ENTIRE mutation; - * - therefore `minTrust` is converted via `TRUST_LEVEL_TO_SCORE` - * (low→0, med→50, max→100); a raw numeric score is accepted too and - * floored/clamped into the backend's 0-100 range; - * - `maxFeeBps` is floored/clamped into the backend's 0-10000 range, - * `maxLatencyMs` floored and capped at the 32-bit Int maximum. - */ -export function sanitizeAgentPolicy(policy: unknown): AgentPolicyWire | undefined { - if (policy === null || typeof policy !== 'object') return undefined; - const p = policy as Record; - const out: AgentPolicyWire = {}; - - if (typeof p.maxLatencyMs === 'number' && Number.isFinite(p.maxLatencyMs) && p.maxLatencyMs > 0) { - out.maxLatencyMs = toBoundedInt(p.maxLatencyMs, 0, GRAPHQL_INT_MAX); - } - const maxFeeBps = toBoundedInt(p.maxFeeBps, 0, 10_000); - if (maxFeeBps !== undefined) { - out.maxFeeBps = maxFeeBps; - } - if (p.minTrust === 'low' || p.minTrust === 'med' || p.minTrust === 'max') { - out.minTrust = TRUST_LEVEL_TO_SCORE[p.minTrust]; - } else { - const minTrust = toBoundedInt(p.minTrust, 0, 100); - if (minTrust !== undefined) out.minTrust = minTrust; - } - - return Object.keys(out).length > 0 ? out : undefined; -} - -// ─── Typed error classification for requestInstantFill ─────── - -/** - * Why the instant path was unavailable and the SDK fell back to the - * standard accept path: - * - * - `disabled` — feature flag off (`INSTANT_FILL_DISABLED`) - * - `lane_conflict` — the settlement router classified the intent - * into a non-instant lane (`extensions.lane`) - * - `already_requested` — an instant fill already exists for this - * quote (exactly-once per quote) - */ -export type InstantFillFallbackReason = - | 'disabled' - | 'lane_conflict' - | 'already_requested'; - -export interface InstantFillFallback { - reason: InstantFillFallbackReason; - /** The conflicting lane (only for `lane_conflict`). */ - lane?: string; -} - -/** - * Classify a `requestInstantFill` failure into a typed fallback - * reason, or `null` when the error is NOT an expected instant-fill - * refusal (auth, network, validation, …) and must be rethrown - * instead of silently falling back. - * - * WIRE SHAPE (verified against the backend error pipeline): - * `HashlockError` crosses trade-service's yoga `maskTradeError` and the - * gateway formatter as `extensions: { ...metadata, code, retryable }` — - * the metadata is FLATTENED into extensions and the HTTP statusCode is - * NEVER serialized. So on the wire: - * - * - flag off → `extensions.code = 'INSTANT_FILL_DISABLED'` - * (a bare GraphQLError; survives the gateway's - * prod masking, which preserves the code); - * - lane conflict → `extensions.code = 'INVALID_INPUT'` with the - * lane at `extensions.lane` (flattened metadata); - * - already requested → `extensions.code = 'INVALID_STATE_TRANSITION'` - * with message "Instant fill already requested - * for this quote". - * - * Other `INVALID_STATE_TRANSITION` errors (quote no longer firm / - * expired) deliberately classify as `null`: the standard accept would - * fail on the same dead quote, so falling back silently would only - * obscure the real error. - */ -export function classifyInstantFillError(err: unknown): InstantFillFallback | null { - if (!(err instanceof GraphQLError)) return null; - - for (const entry of err.errors) { - const ext = (entry.extensions ?? {}) as Record; - const code = typeof ext.code === 'string' ? ext.code : undefined; - const message = entry.message ?? ''; - - // Feature flag off - if (code === 'INSTANT_FILL_DISABLED' || message.includes('INSTANT_FILL_DISABLED')) { - return { reason: 'disabled' }; - } - - // Lane conflict — HashlockError metadata { lane } is flattened to - // extensions.lane by the error formatter (extensions.metadata.lane - // kept as a defensive fallback for non-flattening transports). - const metadata = (ext.metadata ?? {}) as Record; - const lane = - typeof ext.lane === 'string' ? ext.lane - : typeof metadata.lane === 'string' ? metadata.lane - : undefined; - if (lane !== undefined) { - return { reason: 'lane_conflict', lane }; - } - - // Exactly-once guard: committed fill already exists for the quote. - if (code === 'INVALID_STATE_TRANSITION' && /already requested/i.test(message)) { - return { reason: 'already_requested' }; - } - } - - return null; -} - -// ─── Taker flow result ─────────────────────────────────────── - -/** - * The fill was committed (`requestInstantFill` succeeded) but the - * follow-up `acceptQuote` failed — the fill exists server-side - * without an accepted quote. Recover with - * `retryAcceptAfterInstantFill(result.fill)` (accept-only; never - * re-requests the fill, which would 409 on the exactly-once guard). - */ -export class InstantFillOrphanedError extends HashLockError { - constructor( - public readonly fill: InstantFill, - public readonly acceptError: Error, - ) { - super( - `Instant fill ${fill.id} is committed but acceptQuote failed: ${acceptError.message}. ` + - `Do NOT call requestInstantFill again — retry the accept only ` + - `(retryAcceptAfterInstantFill).`, - 'INSTANT_FILL_ORPHANED', - { fill, acceptError }, - ); - this.name = 'InstantFillOrphanedError'; - } -} - -/** - * Outcome of the taker flow helper `requestInstantFillAndAccept`: - * - * | kind | meaning | - * |-----------------|------------------------------------------------------------| - * | `instant` | fill committed AND quote accepted — instant path complete | - * | `standard` | instant path refused (typed `reason`) → standard accept done | - * | `fill_orphaned` | fill committed but accept failed — see `error.fill` to retry | - * - * Unexpected errors (auth, network, unknown GraphQL) are THROWN, not - * folded into this union — only typed instant-fill refusals fall back. - */ -export type InstantTakerResult = - | { kind: 'instant'; fill: InstantFill; quote: Quote } - | { kind: 'standard'; reason: InstantFillFallbackReason; lane?: string; quote: Quote } - | { kind: 'fill_orphaned'; fill: InstantFill; error: InstantFillOrphanedError }; diff --git a/src/principal.ts b/src/principal.ts deleted file mode 100644 index 7058fab..0000000 --- a/src/principal.ts +++ /dev/null @@ -1,64 +0,0 @@ -// ─── Principal + Attestation Types ─────────────────────────── -// -// Mirror of the canonical types defined in @hashlock-tech/intent-schema. -// Duplicated here as plain TS interfaces (the SDK pattern) so the -// SDK has no runtime dependency on the intent-schema package. -// -// Keep these shapes in sync with: -// @hashlock-tech/intent-schema → src/types/principal.ts -// -// EXPERIMENTAL: these fields are defined at the SDK type surface so -// agents can construct the right shape today. GraphQL wire-through -// to the Cayman backend happens in a later release once the backend -// schema is updated to accept PrincipalAttestationInput and -// AgentInstanceInput. Until then, passing these fields to SDK -// methods is a no-op at the network layer. - -export type KycTier = - | 'NONE' - | 'BASIC' - | 'STANDARD' - | 'ENHANCED' - | 'INSTITUTIONAL'; - -export type PrincipalType = 'HUMAN' | 'INSTITUTION' | 'AGENT'; - -export interface PrincipalAttestation { - /** Opaque identifier (hash) of the KYC'd principal entity */ - principalId: string; - /** Kind of principal backing the intent/order */ - principalType: PrincipalType; - /** Attested compliance tier of the principal */ - tier: KycTier; - /** Rotating pseudonym visible to counterparty (omit for post-match attribution only) */ - blindId?: string; - /** Attestation issuance time (unix seconds) */ - issuedAt: number; - /** Attestation expiration (unix seconds) */ - expiresAt: number; - /** Opaque proof (signature or ZK proof) verified by the HashLock gateway */ - proof: string; -} - -export interface AgentInstance { - /** Stable identifier for the agent instance */ - instanceId: string; - /** Human-readable strategy label (e.g. "mm-eth-usdc") */ - strategy?: string; - /** Agent software version */ - version?: string; - /** Instance spawn time (unix seconds) */ - spawnedAt?: number; -} - -export const KYC_TIER_RANK: Record = { - NONE: 0, - BASIC: 1, - STANDARD: 2, - ENHANCED: 3, - INSTITUTIONAL: 4, -}; - -export function meetsKycTier(actual: KycTier, required: KycTier): boolean { - return KYC_TIER_RANK[actual] >= KYC_TIER_RANK[required]; -} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index e41cae9..0000000 --- a/src/types.ts +++ /dev/null @@ -1,360 +0,0 @@ -import type { - PrincipalAttestation, - AgentInstance, - KycTier, -} from './principal.js'; -import type { WebSocketConstructor } from './ws.js'; - -// ─── Enums ─────────────────────────────────────────────────── - -export type Side = 'BUY' | 'SELL'; - -export type RFQStatus = - | 'ACTIVE' - | 'QUOTES_RECEIVED' - | 'ACCEPTED' - | 'FILLED' - | 'EXPIRED' - | 'CANCELLED'; - -export type QuoteStatus = 'PENDING' | 'ACCEPTED' | 'REJECTED' | 'EXPIRED'; - -export type HTLCRole = 'INITIATOR' | 'COUNTERPARTY'; - -export type TradeStatus = - | 'PROPOSED' - | 'ACCEPTED' - | 'FUNDING' - | 'FUNDED' - | 'INITIATOR_LOCKED' - | 'BOTH_LOCKED' - | 'EXECUTING' - | 'SETTLING' - | 'COMPLETED' - | 'REFUNDED' - | 'FAILED' - | 'CANCELLED' - | 'EXPIRED'; - -export type HTLCStatus = - | 'PENDING' - | 'ACTIVE' - | 'WITHDRAWN' - | 'REFUNDED' - | 'EXPIRED' - | 'INVALIDATED' - | 'UNDER_FUNDED'; - -// ─── Domain Objects ────────────────────────────────────────── - -export interface RFQ { - id: string; - userId: string; - baseToken: string; - quoteToken: string; - side: Side; - amount: string; - isBlind: boolean; - status: RFQStatus; - expiresAt: string | null; - createdAt: string; - quotesCount: number | null; - quotes: Quote[] | null; - /** EXPERIMENTAL: tier the creator attested to (visible, non-leaky) */ - attestationTier?: KycTier | null; - /** EXPERIMENTAL: rotating pseudonym of the creator (blind identity) */ - attestationBlindId?: string | null; - /** EXPERIMENTAL: minimum KYC tier the creator wants in a counterparty */ - minCounterpartyTier?: KycTier | null; -} - -export interface Quote { - id: string; - rfqId: string; - marketMakerId: string; - price: string; - amount: string; - expiresAt: string | null; - status: QuoteStatus; - createdAt: string; - deliveryDelayHours: number | null; - collateralBtcSats: string | null; - isCollateralBacked: boolean; - /** EXPERIMENTAL: tier the market maker attested to */ - attestationTier?: KycTier | null; - /** EXPERIMENTAL: blind identity of the market maker */ - attestationBlindId?: string | null; - /** Solver instant-fill commitment: when true, the maker fronts the - * taker's asset from `solverVaultAddr` immediately on accept and is - * reimbursed at settlement. */ - instantFill?: boolean; - /** On-chain address of the solver vault that fronts the fill. - * Set when `instantFill` is true. Chain-agnostic format - * (EVM 0x-hex, Sui 0x-hex, BTC bech32). */ - solverVaultAddr?: string | null; -} - -export interface Trade { - id: string; - initiatorId: string; - counterpartyId: string; - baseToken: string | null; - quoteToken: string | null; - side: Side | null; - baseAmount: string | null; - quoteAmount: string | null; - price: string; - status: TradeStatus; - createdAt: string; - /** EXPERIMENTAL: initiator's attested compliance tier */ - initiatorAttestationTier?: KycTier | null; - /** EXPERIMENTAL: counterparty's attested compliance tier */ - counterpartyAttestationTier?: KycTier | null; -} - -export interface HTLC { - id: string; - tradeId: string; - role: HTLCRole; - status: HTLCStatus; - contractAddress: string | null; - hashlock: string | null; - timelock: number | null; - amount: string | null; - txHash: string | null; - chainId: number | null; - preimage: string | null; -} - -export interface HTLCStatusResult { - tradeId: string; - status: string; - initiatorHTLC: HTLC | null; - counterpartyHTLC: HTLC | null; -} - -// ─── Mutation Inputs ───────────────────────────────────────── - -/** - * Canonical chain identifier for per-leg RFQ params. Matches the chain `id` - * registered in the backend's `ChainRegistry`. Cross-chain RFQs (e.g. SUI on - * Sui mainnet ↔ ETH on Sepolia) require both `baseChain` and `quoteChain` - * to be set so the backend can resolve `(symbol, chain)` composite tokens - * unambiguously. - */ -export type RFQChainId = - | 'ethereum' - | 'sepolia' - | 'bitcoin' - | 'bitcoin-signet' - | 'sui' - | 'sui-testnet'; - -export interface CreateRFQInput { - /** Base asset symbol (e.g., 'ETH', 'BTC') */ - baseToken: string; - /** Quote asset symbol (e.g., 'USDT', 'USDC') */ - quoteToken: string; - /** BUY or SELL */ - side: Side; - /** Amount in base token units (e.g., '1.5') */ - amount: string; - /** Expiration time in seconds (default: server-configured) */ - expiresIn?: number; - /** Hide counterparty identity in blind auction mode */ - isBlind?: boolean; - /** Chain the base token settles on. Optional — when omitted the backend - * falls back to its single-chain default (env-driven). Set together with - * `quoteChain` to express a cross-chain RFQ. */ - baseChain?: RFQChainId; - /** Chain the quote token settles on. See `baseChain`. */ - quoteChain?: RFQChainId; - /** EXPERIMENTAL — Principal attestation for agent / institution - * flows. The shape is accepted by the SDK today but is not yet - * sent to the Cayman backend. Wire-through will land in a later - * release once the backend accepts PrincipalAttestationInput. */ - attestation?: PrincipalAttestation; - /** EXPERIMENTAL — Agent instance metadata. See `attestation`. */ - agentInstance?: AgentInstance; - /** EXPERIMENTAL — Minimum KYC tier the counterparty must attest to. */ - minCounterpartyTier?: KycTier; - /** EXPERIMENTAL — Hide blind identity in the solver proof. Requires - * `isBlind: true` or a blind auction context. */ - hideIdentity?: boolean; -} - -export interface SubmitQuoteInput { - /** ID of the RFQ to respond to */ - rfqId: string; - /** Price per unit of base token in quote token terms */ - price: string; - /** Amount of base token */ - amount: string; - /** Expiration time in seconds */ - expiresIn?: number; - /** Instant-fill commitment: "if accepted, I front the taker's asset - * immediately from my vault and get reimbursed at settlement". - * Requires `solverVaultAddr`. */ - instantFill?: boolean; - /** On-chain address of the solver vault that fronts the fill. - * Required when `instantFill` is true (backend coherence CHECK). */ - solverVaultAddr?: string; - /** EXPERIMENTAL — Market maker's principal attestation. */ - attestation?: PrincipalAttestation; - /** EXPERIMENTAL — Agent instance metadata. */ - agentInstance?: AgentInstance; - /** EXPERIMENTAL — Hide blind identity from the RFQ creator. */ - hideIdentity?: boolean; -} - -export interface FundHTLCInput { - /** Trade ID */ - tradeId: string; - /** Transaction hash from on-chain HTLC creation */ - txHash: string; - /** Your role in this trade */ - role: HTLCRole; - /** Timelock as Unix timestamp */ - timelock?: number; - /** SHA-256 hashlock (0x-prefixed) */ - hashlock?: string; - /** 'evm', 'bitcoin', or 'sui' */ - chainType?: string; - /** Compressed public key (BTC only) */ - senderPubKey?: string; - /** Compressed public key (BTC only) */ - receiverPubKey?: string; - /** Hex-encoded redeem script (BTC only) */ - redeemScript?: string; - /** Pre-signed refund tx hex (BTC only) */ - refundTxHex?: string; - /** Preimage for initiator (kept encrypted server-side) */ - preimage?: string; - /** EXPERIMENTAL — Principal attestation of the funding party. */ - attestation?: PrincipalAttestation; - /** EXPERIMENTAL — Agent instance metadata. */ - agentInstance?: AgentInstance; -} - -export interface ClaimHTLCInput { - /** Trade ID */ - tradeId: string; - /** Transaction hash of the on-chain claim tx */ - txHash: string; - /** The 32-byte preimage (0x-prefixed hex) */ - preimage: string; - /** Chain type ('evm' | 'bitcoin' | 'sui') */ - chainType?: string; -} - -export interface RefundHTLCInput { - /** Trade ID */ - tradeId: string; - /** Transaction hash of the on-chain refund tx */ - txHash: string; - /** Chain type ('evm' | 'bitcoin' | 'sui') */ - chainType?: string; -} - -export interface PrepareBitcoinHTLCInput { - tradeId: string; - role: HTLCRole; - senderPubKey: string; - receiverPubKey: string; - /** Unix timestamp for HTLC expiry */ - timelock: number; - /** Amount in satoshis */ - amountSats: string; -} - -export interface BitcoinHTLCPrepareResult { - tradeId: string; - htlcId: string; - htlcAddress: string; - redeemScript: string; - hashlock: string; - preimageHash: string | null; - timelock: number; - amountSats: string; - estimatedClaimFee: number; - estimatedRefundFee: number; - refundPsbt: string; -} - -export interface BuildBitcoinClaimPSBTInput { - tradeId: string; - htlcId: string; - /** 32-byte preimage (hex) */ - preimage: string; - /** Claimer's compressed public key */ - destinationPubKey: string; - /** Fee rate in sat/vB (default: 10) */ - feeRate?: number; -} - -export interface BitcoinClaimPSBTResult { - tradeId: string; - htlcId: string; - psbtBase64: string; - fee: number; - utxoTxid: string; - utxoVout: number; -} - -export interface BroadcastBitcoinTxInput { - tradeId: string; - txHex: string; -} - -export interface BitcoinBroadcastResult { - txid: string; - success: boolean; -} - -export interface FundHTLCResult { - tradeId: string; - txHash: string; - status: string; -} - -// ─── Mutation Results ──────────────────────────────────────── - -export interface ConfirmDirectTradeInput { - counterpartyId: string; - baseToken: string; - quoteToken: string; - side: Side; - baseAmount: string; - price: string; - chainId: string; - broadcastRfqId?: string; - conversationId?: string; -} - -export interface ConfirmSettlementWalletsInput { - tradeId: string; - sendWalletId: string; - receiveWalletId: string; -} - -// ─── SDK Config ────────────────────────────────────────────── - -export interface HashLockConfig { - /** GraphQL endpoint URL */ - endpoint: string; - /** JWT access token for authentication */ - accessToken?: string; - /** Request timeout in milliseconds (default: 30000) */ - timeout?: number; - /** Number of retry attempts for failed requests (default: 3) */ - retries?: number; - /** Custom fetch implementation (for Node.js < 18 or testing) */ - fetch?: typeof fetch; - /** WebSocket implementation for GraphQL subscriptions. Defaults to - * `globalThis.WebSocket` (browsers, Node >= 22). On Node 18/20 pass - * the `ws` package's default export. */ - webSocket?: WebSocketConstructor; - /** Subscription endpoint. Defaults to `endpoint` with the scheme - * switched to ws/wss. */ - wsEndpoint?: string; -} diff --git a/src/ws.ts b/src/ws.ts deleted file mode 100644 index 7414874..0000000 --- a/src/ws.ts +++ /dev/null @@ -1,153 +0,0 @@ -// ─── Minimal graphql-transport-ws subscription transport ───── -// -// The SDK keeps zero runtime dependencies, so instead of pulling in -// `graphql-ws` this implements the small client half of the -// graphql-transport-ws protocol (connection_init/ack, subscribe, -// next/error/complete, ping/pong) over an injectable WebSocket -// implementation. -// -// Runtime requirements: -// - Node >= 22 / browsers: global `WebSocket` is used automatically. -// - Node 18/20: pass an implementation via `HashLockConfig.webSocket` -// (e.g. `import WebSocket from 'ws'`). - -import { GraphQLError, NetworkError } from './errors.js'; - -/** Structural WebSocket surface — satisfied by browser WebSocket and `ws`. */ -export interface WebSocketLike { - send(data: string): void; - close(code?: number, reason?: string): void; - addEventListener( - type: 'open' | 'message' | 'close' | 'error', - listener: (event: { data?: unknown; code?: number; reason?: string }) => void, - ): void; -} - -export type WebSocketConstructor = new ( - url: string, - protocols?: string | string[], -) => WebSocketLike; - -/** Handle returned by subscription methods. */ -export interface SubscriptionHandle { - unsubscribe(): void; -} - -export interface WsSubscribeOptions { - url: string; - webSocket: WebSocketConstructor; - /** Bearer token sent as `connection_init` payload `{ authorization }`. */ - token?: string; - query: string; - variables?: Record | object; - onData: (data: T) => void; - onError?: (err: Error) => void; - onComplete?: () => void; -} - -/** Derive the WebSocket endpoint from an http(s) GraphQL endpoint. */ -export function deriveWsEndpoint(endpoint: string): string { - if (endpoint.startsWith('https://')) return `wss://${endpoint.slice('https://'.length)}`; - if (endpoint.startsWith('http://')) return `ws://${endpoint.slice('http://'.length)}`; - return endpoint; // already ws:// or wss:// -} - -const SUBSCRIPTION_ID = '1'; - -interface WsMessage { - type?: string; - id?: string; - payload?: unknown; -} - -/** - * Open one connection per subscription (no multiplexing — solver and - * taker watches are long-lived singletons, simplicity wins) and run - * the graphql-transport-ws handshake. - */ -export function subscribeOverWebSocket(opts: WsSubscribeOptions): SubscriptionHandle { - const ws = new opts.webSocket(opts.url, 'graphql-transport-ws'); - let settled = false; // errored / completed / unsubscribed - - const fail = (err: Error): void => { - if (settled) return; - settled = true; - opts.onError?.(err); - try { ws.close(1000); } catch { /* already closed */ } - }; - - ws.addEventListener('open', () => { - const payload = opts.token ? { authorization: `Bearer ${opts.token}` } : {}; - ws.send(JSON.stringify({ type: 'connection_init', payload })); - }); - - ws.addEventListener('message', (event) => { - let msg: WsMessage; - try { - msg = JSON.parse(String(event.data)) as WsMessage; - } catch { - return; // ignore non-JSON frames - } - - switch (msg.type) { - case 'connection_ack': - ws.send(JSON.stringify({ - id: SUBSCRIPTION_ID, - type: 'subscribe', - payload: { query: opts.query, variables: opts.variables }, - })); - break; - - case 'ping': - ws.send(JSON.stringify({ type: 'pong' })); - break; - - case 'next': { - const payload = (msg.payload ?? {}) as { - data?: T; - errors?: Array<{ message: string; path?: string[]; extensions?: Record }>; - }; - if (payload.errors?.length) { - opts.onError?.(new GraphQLError(payload.errors[0].message, payload.errors)); - } else if (payload.data !== undefined && payload.data !== null) { - opts.onData(payload.data); - } - break; - } - - case 'error': { - const errors = (Array.isArray(msg.payload) ? msg.payload : [{ message: 'Subscription error' }]) as - Array<{ message: string; path?: string[]; extensions?: Record }>; - fail(new GraphQLError(errors[0]?.message ?? 'Subscription error', errors)); - break; - } - - case 'complete': - if (!settled) { - settled = true; - opts.onComplete?.(); - try { ws.close(1000); } catch { /* already closed */ } - } - break; - } - }); - - ws.addEventListener('close', (event) => { - if (!settled) { - fail(new NetworkError(`WebSocket closed before completion (code ${event.code ?? 'unknown'})`)); - } - }); - - ws.addEventListener('error', () => { - fail(new NetworkError('WebSocket transport error')); - }); - - return { - unsubscribe(): void { - if (settled) return; - settled = true; - try { ws.send(JSON.stringify({ id: SUBSCRIPTION_ID, type: 'complete' })); } catch { /* best effort */ } - try { ws.close(1000, 'client unsubscribe'); } catch { /* already closed */ } - }, - }; -} diff --git a/test/fixtures/schema.graphql b/test/fixtures/schema.graphql deleted file mode 100644 index d74be7b..0000000 --- a/test/fixtures/schema.graphql +++ /dev/null @@ -1,864 +0,0 @@ -# Vendored GraphQL SDL — DO NOT EDIT BY HAND -# Source: Cayman-Hashlock backend/services/trade-service/src/schema.ts (typeDefs — federated HTTP schema) -# Ref: origin/main @ 3cbc423522392967db055366a3229e1a3664565b -# Vendored: 2026-06-11 -# Refresh: git -C fetch origin && node scripts/vendor-schema.mjs - - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) - - enum Side { - BUY - SELL - } - - enum RFQStatus { - ACTIVE - QUOTES_RECEIVED - ACCEPTED - FILLED - EXPIRED - CANCELLED - } - - enum QuoteStatus { - PENDING - ACCEPTED - REJECTED - EXPIRED - } - - enum HTLCRole { - INITIATOR - COUNTERPARTY - } - - enum TradeStatus { - PROPOSED - ACCEPTED - FUNDING - FUNDED - INITIATOR_LOCKED - BOTH_LOCKED - EXECUTING - SETTLING - COMPLETED - REFUNDED - FAILED - CANCELLED - EXPIRED - } - - enum HTLCStatus { - PENDING - ACTIVE - WITHDRAWN - REFUNDED - EXPIRED - INVALIDATED - UNDER_FUNDED - } - - enum BlindOrderStatus { - ACTIVE - MATCHED - CANCELLED - EXPIRED - } - - type RFQ { - id: ID! - userId: ID! - baseToken: String! - quoteToken: String! - side: Side! - amount: String! - isBlind: Boolean! - status: RFQStatus! - expiresAt: String - createdAt: String! - quotesCount: Int - quotes: [Quote!] - } - - type RFQPage { - rfqs: [RFQ!]! - total: Int! - page: Int! - pageSize: Int! - } - - type Quote { - id: ID! - rfqId: ID! - marketMakerId: ID! - price: String! - amount: String! - expiresAt: String - status: QuoteStatus! - createdAt: String! - trade: TradeRef - # Collateral-backed counter-offer fields (both populated together or both null) - deliveryDelayHours: Int - collateralBtcSats: String - isCollateralBacked: Boolean! - """Solver instant-fill commitment (Instant Settlement Faz 1.6). When true, - the maker fronts the taker's asset from solverVaultAddr on acceptance and - is reimbursed at settlement. Always false while the instant_fill feature - flag is off.""" - instantFill: Boolean! - """On-chain address of the solver vault backing an instant-fill quote. - Null unless instantFill is true.""" - solverVaultAddr: String - } - - """Solver instant-fill lifecycle record (migrations 115 + 117): exactly one - per quote. State machine: committed -> fronted -> settled -> reimbursed, - with committed -> cancelled as the terminal failure branch (accept failed / - quote left its firm window). settled/reimbursed transitions are wired by - the chain-watcher once the SolverVault is deployed (see - docs/design/INSTANT-FILL-LIFECYCLE.md).""" - type InstantFill { - id: ID! - quoteId: ID! - """Trade materialised from the accepted quote; linked by acceptQuote. - Null until the quote is accepted. Fronting can only be marked on a - trade-linked fill (canonical-sequence guard).""" - tradeId: ID - """One of: committed, fronted, settled, reimbursed, cancelled.""" - state: String! - """Committed fill amount in the asset's smallest on-chain unit - (wei/sat/MIST) as a decimal string — full uint256 range.""" - amountWei: String! - """EVM tx hash of the solver vault's fronting transfer; set when the - solver marks the fill fronted.""" - frontTxHash: String - """ISO timestamp of the committed -> fronted transition.""" - frontedAt: String - createdAt: String! - } - - type TradeRef { - id: ID! - status: TradeStatus! - """Set on collateral-backed trades: ISO timestamp by which seller must deliver""" - deliveryDeadline: String - } - - """Declarative agent settlement policy (Instant Settlement Faz 1.9, - DESIGN §13 — one engine, two adapters). The agent's policy and the human - speed-slider presets are the SAME parameters. The policy is a PREFERENCE, - not a commitment: a malformed policy, a closed instant_fill flag, or an - unavailable instant lane silently degrade to the standard HTLC accept path - (the decision is recorded on the TRADE_CREATED audit event). All fields - optional.""" - input AgentPolicyInput { - """Max settlement latency tolerated, in ms (integer >= 0). Values at or - below the Instant preset threshold (3000ms) request the solver - instant-fill path when the accepted quote carries an instant-fill - commitment and the instant_fill feature flag is on.""" - maxLatencyMs: Int - """Max fee in basis points (0-10000). Currently audit-recorded only — - enforced once the risk-engine fee model (Faz 0.2) lands.""" - maxFeeBps: Int - """Minimum solver trust/reputation score (0-100). Compared against the - platform's solver reputation feed (stubbed at 50 until the reputation - oracle integration). High values steer to the trustless pure-HTLC path.""" - minTrust: Int - } - - type Trade { - id: ID! - initiatorId: ID! - counterpartyId: ID! - baseToken: String - quoteToken: String - side: Side - baseAmount: String - quoteAmount: String - price: String! - status: TradeStatus! - chainId: Int - # Deprecated: use htlcs field for dual-HTLC support - htlcAddress: String - # Deprecated: use htlcs field for dual-HTLC support - hashlock: String - # Deprecated: use htlcs field for dual-HTLC support - timelock: String - htlcs: [HTLC!] - initiatorWalletAddress: String - counterpartyWalletAddress: String - initiatorBtcPubKey: String - counterpartyBtcPubKey: String - createdAt: String! - completedAt: String - } - - type TradePage { - trades: [Trade!]! - total: Int! - page: Int! - pageSize: Int! - } - - type HTLC { - id: ID! - tradeId: ID! - role: String! - contractAddress: String - hashlock: String - timelock: String - sender: String - receiver: String - amount: String - tokenAddress: String - status: HTLCStatus - txHash: String - chainId: Int - """Preimage hex (0x-prefixed). Available after initiator funds. Used for auto-claim.""" - preimage: String - """Bitcoin-only: hex-encoded P2WSH redeem script""" - redeemScript: String - """Bitcoin-only: pre-signed refund transaction hex""" - refundTxHex: String - """Bitcoin-only: sender compressed public key (33 bytes hex)""" - senderPubKey: String - """Bitcoin-only: receiver compressed public key (33 bytes hex)""" - receiverPubKey: String - """Chain type discriminator: evm, bitcoin, or sui""" - chainType: String - createdAt: String - } - - type HTLCStatusResult { - tradeId: ID! - contractAddress: String - hashlock: String - timelock: String - sender: String - receiver: String - amount: String - tokenAddress: String - status: HTLCStatus - redeemed: Boolean - refunded: Boolean - chainId: Int - } - - type BlindOrder { - id: ID! - pseudonym: String! - baseToken: String! - quoteToken: String! - side: Side! - amount: String! - minPrice: String - maxPrice: String - status: BlindOrderStatus! - matchedAt: String - matchedTradeId: ID - createdAt: String! - } - - type BatchCrossingResult { - tradesCreated: Int! - ordersMatched: Int! - batchId: Int! - } - - type ExpireRFQsResult { - rfqsExpired: Int! - quotesExpired: Int! - } - - type BlindOrderPage { - orders: [BlindOrder!]! - total: Int! - page: Int! - pageSize: Int! - } - - """Effective rebate information for a trader, computed from fee_configuration""" - type RebateInfo { - """Effective rebate in basis points (derived from rebate pct columns in fee_configuration)""" - rebateBps: Int! - """Platform fee in basis points""" - platformFeeBps: Int! - """Fee recipient address from active configuration""" - feeRecipient: String! - } - - type FeeConfiguration { - id: Int! - tradeType: String! - feeRecipient: String! - platformFeeBps: Int! - referralRebatePct: Int! - volumeRebatePct: Int! - loyaltyRebatePct: Int! - minFeeUsd: Float - maxFeeUsd: Float - isActive: Boolean! - updatedBy: String! - updatedAt: String! - } - - input UpdateFeeConfigInput { - tradeType: String! - feeRecipient: String - platformFeeBps: Int - referralRebatePct: Int - volumeRebatePct: Int - loyaltyRebatePct: Int - minFeeUsd: Float - maxFeeUsd: Float - isActive: Boolean - } - - type DashboardStats { - activeRfqs: Int! - openTrades: Int! - totalVolume30d: String! - currentTier: String! - } - - type Balance { - token: String! - balance: String! - usdValue: String! - } - - type Portfolio { - balances: [Balance!]! - } - - type EncryptedPreimage { - tradeId: ID! - htlcId: ID! - encryptedPreimage: String! - nonce: String! - ephemeralPublicKeyJwk: String! - senderPublicKeyId: ID - createdAt: String - } - - type McpOtkIssued { - token: String! - expiresAt: String! - } - - type UnsignedEvmTx { - to: String! - data: String! - value: String! - chainId: Int! - gas: String! - } - - """Sui transaction returned by mcpBuildLockCalldata when chainType='sui'. - ptbBytes is the base64-encoded serialized Programmable Transaction Block - the agent passes directly to client.signAndExecuteTransaction (or to - dapp-kit signAndExecute on the web sign page).""" - type UnsignedSuiTx { - ptbBytes: String! - network: String! - } - - """Bitcoin envelope returned by mcpBuild{Lock,Claim,Refund}Calldata when - chainType='bitcoin'. Per .planning/MCP-BITCOIN-PLAN.md §3.1 the backend - never sees the client UTXO set — it describes what the client must - fund (lock) or spend (claim/refund). The agent uses bitcoinjs-lib (or - equivalent) to assemble the actual signed PSBT on its own. - - Field population is action-specific: - - lock : htlcAddress, redeemScript, amountSats, timelock, network. - - claim : outpoint, redeemScript, preimage, network. - - refund : outpoint, redeemScript, network. - """ - type UnsignedBitcoinTx { - """Lock only: P2WSH bech32 address the client must fund.""" - htlcAddress: String - """Every action: HTLC redeem script (hex, no 0x) — witness input.""" - redeemScript: String! - """Lock only: amount in satoshis (u64 as string for safe JSON).""" - amountSats: String - """Lock only: absolute unix timestamp the CLTV branch enforces.""" - timelock: Int - """Every action: network label ('mainnet' | 'signet' | 'testnet').""" - network: String! - """Claim + refund: HTLC UTXO outpoint being spent (':').""" - outpoint: String - """Claim only: SHA-256 preimage hex (no 0x) the witness reveals.""" - preimage: String - } - - """Polling result for BTC lock-tx confirmation status. Returned by - mcpGetConfirmStatus — EVM/Sui agents do not call this because their - confirm fits inside the MCP request budget. States: - - - AWAITING_CONFIRMATION : txid submitted but not yet at required depth - (or no txid registered yet). - - CONFIRMED : tx confirmed at or past required depth. - - FAILED : tx not findable / rejected / reorged out. - - EXPIRED : trade timelock passed before confirmations - reached — caller should move to refund. - """ - type BtcConfirmStatus { - state: String! - confirmationsObserved: Int! - confirmationsRequired: Int! - """Submitted funding-tx txid, or null if mcpSubmitLockTx not called yet.""" - txid: String - """Block height at which the tx was included; null pre-confirmation.""" - blockHeight: Int - } - - """Context returned to the Safe sign page so the browser can compute the - EIP-712 digest, collect the initial owner signature, and submit the proposal - to Safe Transaction Service without a round-trip to the backend.""" - type SafeTxContext { - safeAddress: String! - chainId: Int! - threshold: Int! - nonce: Int! - innerTx: UnsignedEvmTx! - txServiceUrl: String! - } - - """Bitcoin-only: auxiliary one-time-key tokens for the BTC sub-flow - resolvers. Null for non-BTC chains. Each token is one-time-use; the - client MUST pair the right token with the matching resolver per plan - §3.3 OTK purpose table: - registerRefundTx → mcpRegisterPreSignedRefund - submitLockTx → mcpSubmitLockTx - getConfirmStatus → mcpGetConfirmStatus (soft gate)""" - type BtcAuxOtks { - registerRefundTx: String! - submitLockTx: String! - getConfirmStatus: String! - } - - type McpBuildResult { - tradeId: ID - rfqId: ID - """EVM-only unsigned tx. Populated when chainType='evm'; null otherwise. - Kept for backwards compatibility with EVM agents written before - multi-chain dispatch shipped.""" - unsignedTx: UnsignedEvmTx - """Sui-only unsigned PTB. Populated when chainType='sui'; null otherwise.""" - unsignedSuiTx: UnsignedSuiTx - """Bitcoin-only envelope. Populated when chainType='bitcoin'; null otherwise. - Field shape depends on the action (lock vs claim vs refund) — see - UnsignedBitcoinTx.""" - unsignedBitcoinTx: UnsignedBitcoinTx - """Bitcoin-only auxiliary OTKs for the BTC sub-flow resolvers. Null - for non-BTC chains. Issued alongside otkToken (which remains the - confirm-lock gate) so the client can drive register-refund, - submit-lock, and get-status with purpose-matched tokens per - migration 084.""" - btcAuxOtks: BtcAuxOtks - """Discriminator — always matches trades.chain_type. Clients MUST branch - on this before reading any unsigned*Tx field. Nullable in this initial - rollout so existing EVM clients that omit it from their selection set - keep working; will tighten to non-null once all clients migrate.""" - chainType: String - contractId: String - signUrl: String! - otkToken: String! - """Present when walletType='safe'. Browser uses this to compose the Safe - proposal before calling mcpRegisterSafeTx.""" - safeContext: SafeTxContext - """Counterparty lock only — echoes back the derived timelock (initiator - timelock - 30 min gap) so the client does not need to re-derive it.""" - counterpartyTimelock: Int - """Counterparty lock only — the amount (base units) bob is locking.""" - counterpartyAmount: String - """Counterparty lock only — ERC20 token address for bob's lock, or null - for native asset.""" - counterpartyTokenAddress: String - } - - """Lifecycle hint for a pending Safe proposal; returned by mcpConfirmLock - when the proposal has not yet reached its confirmation threshold.""" - type SafeProposalStatus { - safeTxHash: String! - state: String! - confirmations: Int! - required: Int! - executionTxHash: String - } - - type McpConfirmResult { - ok: Boolean! - state: String! - """Set when the caller passed safeTxHash and the proposal is still - pending. ok is false in that case — treat as 'poll later'.""" - safeProposal: SafeProposalStatus - } - - enum McpWalletType { - eoa - safe - } - - type MmStatusEntry { - userId: ID! - lastQuoteAt: String - openQuotes: Int! - fillRate: Float - } - - type MmStatus { - activeMakers: [MmStatusEntry!]! - totalOpenQuotes: Int! - } - - type Query { - rfqs(status: RFQStatus, page: Int, pageSize: Int): RFQPage! - rfq(id: ID!): RFQ - quotes(rfqId: ID!): [Quote!]! - trades(status: TradeStatus, page: Int, pageSize: Int): TradePage! - trade(id: ID!): Trade - htlcStatus(tradeId: ID!): HTLCStatusResult - htlcs(tradeId: ID!): [HTLC!]! - blindOrders(status: BlindOrderStatus, page: Int, pageSize: Int): BlindOrderPage! - dashboardStats: DashboardStats! - portfolio(walletAddress: String): Portfolio! - encryptedPreimage(tradeId: ID!): EncryptedPreimage - """Admin-only: fetch all fee configuration rows ordered by trade_type""" - feeConfigurations: [FeeConfiguration!]! - """Compute the effective rebate bps for a trader based on active fee configuration""" - traderRebateInfo(tradeType: String): RebateInfo - """Get current user's RFQ watch preferences""" - rfqWatchPreferences: RFQWatchPreferences - """Active market maker status (public, no auth required)""" - mmStatus: MmStatus! - - """ - Spot USD price for a token symbol (ETH, BTC, USDC, USDT, ...). - Backed by the same CoinGecko + Redis cache the portfolio resolver - uses, so a 5-minute cache window applies. Returns null when the - symbol is not in the token registry or the upstream feed is down — - callers should treat null as "estimate unavailable", not as zero. - Cephe 3 PR-2. - """ - tokenSpotPrice(symbol: String!): Float - } - - type HTLCTransaction { - tradeId: ID! - htlcId: ID! - contractAddress: String! - txData: String! - chainId: Int! - hashlock: String - timelock: String - amount: String - } - - type FundHTLCResult { - tradeId: ID! - txHash: String! - status: String! - } - - """ - Result of prepareBitcoinHTLC — everything the client needs to fund the HTLC. - The client sends BTC to htlcAddress, then calls fundHTLC with the txHash. - """ - type BitcoinHTLCPrepareResult { - tradeId: ID! - htlcId: ID! - """P2WSH address — send BTC here to fund the HTLC""" - htlcAddress: String! - """Hex-encoded HTLC redeem script""" - redeemScript: String! - """SHA-256 hashlock (0x-prefixed)""" - hashlock: String! - """Preimage hash (SHA-256, 0x-prefixed). Use client-side preimage generation for security. Raw preimage is never returned over the wire.""" - preimageHash: String - """Unix timestamp — HTLC expires after this""" - timelock: Int! - """Amount in satoshis""" - amountSats: String! - """Estimated fee for claim transaction (satoshis)""" - estimatedClaimFee: Int! - """Estimated fee for refund transaction (satoshis)""" - estimatedRefundFee: Int! - """Base64-encoded unsigned refund PSBT template with nLockTime set to the timelock. Uses a placeholder outpoint (zeroed txid, vout 0) — the client must update with the real funding txid:vout before signing.""" - refundPsbt: String! - } - - """Result of broadcastBitcoinTx — confirms the transaction was accepted by the network.""" - type BitcoinBroadcastResult { - """Transaction ID returned by the network""" - txid: String! - success: Boolean! - } - - """Result of buildBitcoinClaimPSBT — unsigned PSBT for claiming a Bitcoin HTLC.""" - type BitcoinClaimPSBTResult { - tradeId: ID! - htlcId: ID! - """Base64-encoded unsigned claim PSBT. Client signs with wallet, then broadcasts via broadcastBitcoinTx.""" - psbtBase64: String! - """Estimated fee in satoshis""" - fee: Int! - """The HTLC UTXO txid being spent""" - utxoTxid: String! - """The HTLC UTXO vout being spent""" - utxoVout: Int! - } - - type Mutation { - createRFQ(baseToken: String!, quoteToken: String!, side: Side!, amount: String!, expiresIn: Int, isBlind: Boolean, chainId: Int, baseChain: String, quoteChain: String): RFQ! - cancelRFQ(id: ID!): RFQ! - submitQuote(rfqId: ID!, price: String!, amount: String!, expiresIn: Int, deliveryDelayHours: Int, collateralBtcSats: String, instantFill: Boolean, solverVaultAddr: String): Quote! - """ - RFQ owner accepts a quote, creating a Trade. The optional policy arg - (Faz 1.9 agent adapter) may additionally request the solver instant-fill - path before the standard accept — see AgentPolicyInput. Omitting it keeps - the historical behavior exactly. - """ - acceptQuote(quoteId: ID!, policy: AgentPolicyInput): Quote! - """ - Taker requests a solver instant fill on a firm instant-fill quote - (Instant Settlement Faz 1.6, Lane A). Gated behind the instant_fill - feature flag; routes through the settlement-core router and only - proceeds when the intent classifies into Lane A. Exactly-once per - quote (uq_instant_fills_quote). - """ - requestInstantFill(rfqId: ID!, quoteId: ID!): InstantFill! - """ - Solver marks its committed instant fill as fronted on-chain (instant-fill - lifecycle). Caller must be the market maker behind the fill's quote. - Requires the fill to be linked to a trade — i.e. the quote was accepted — - so fronting can never run ahead of the canonical fill -> accept sequence. - Atomic single-statement transition committed -> fronted; a second call - 409s instead of overwriting the recorded evidence. frontTxHash is the EVM - tx hash of the vault's fronting transfer (recorded as evidence; on-chain - verification lands with the SolverVault deploy). - """ - markInstantFillFronted(fillId: ID!, frontTxHash: String!): InstantFill! - """ - Create a trade directly from a 1-on-1 chat "Agreed" action. - Skips the RFQ/Quote dance. The caller becomes the counterparty (acceptor), - the original RFQ sender becomes the initiator. Returns a PROPOSED trade - ready to enter the HTLC lifecycle. - """ - confirmDirectTrade(counterpartyId: ID!, baseToken: String!, quoteToken: String!, side: Side!, baseAmount: String!, price: String!, chainId: String!, chainType: String, counterpartyChainType: String, counterpartyChainId: String, broadcastRfqId: ID, deliveryDelayHours: Int, collateralBtcSats: String, conversationId: ID): Trade! - """ - Bulk-expire RFQs whose expires_at has passed. Sweeps all ACTIVE expired - RFQs to status='EXPIRED' and cascades their PENDING quotes. Any - authenticated user can trigger the sweep. - """ - expireRFQs: ExpireRFQsResult! - createBlindOrder(baseToken: String!, quoteToken: String!, side: Side!, amount: String!, minPrice: String, maxPrice: String): BlindOrder! - cancelBlindOrder(id: ID!): BlindOrder! - runBatchCrossing: BatchCrossingResult! - acceptTrade(tradeId: ID!): Trade! - cancelTrade(tradeId: ID!): Trade! - fundHTLC(tradeId: ID!, txHash: String!, role: HTLCRole!, timelock: Int, hashlock: String, chainType: String, senderPubKey: String, receiverPubKey: String, redeemScript: String, refundTxHex: String, preimage: String): FundHTLCResult! - claimHTLC(tradeId: ID!, txHash: String!, preimage: String!, chainType: String): HTLCStatusResult! - refundHTLC(tradeId: ID!, txHash: String!, chainType: String): HTLCStatusResult! - """ - Prepare a Bitcoin HTLC for a trade. Generates the redeem script and P2WSH - address. The client funds this address, then calls fundHTLC with the txHash. - """ - prepareBitcoinHTLC(tradeId: ID!, role: HTLCRole!, senderPubKey: String!, receiverPubKey: String!, timelock: Int!, amountSats: String!, hashlock: String): BitcoinHTLCPrepareResult! - """ - Build an unsigned claim PSBT for a Bitcoin HTLC. Requires the preimage and - the claimer's destination public key. Returns a PSBT for wallet signing. - """ - buildBitcoinClaimPSBT(tradeId: ID!, htlcId: ID!, preimage: String!, destinationPubKey: String!, feeRate: Int): BitcoinClaimPSBTResult! - """ - Broadcast a signed Bitcoin transaction to the network via Blockstream API. - Accepts a fully signed raw transaction hex. - """ - broadcastBitcoinTx(tradeId: ID!, txHex: String!): BitcoinBroadcastResult! - """ - Confirm settlement wallets for a trade. Both parties must confirm before HTLC creation. - Returns the updated trade with wallet confirmation timestamps. - """ - confirmSettlementWallets(tradeId: ID!, sendWalletId: ID!, receiveWalletId: ID!): Trade! - """Admin-only: update fee configuration for a specific trade type""" - updateFeeConfig(input: UpdateFeeConfigInput!): FeeConfiguration! - """Update RFQ watch preferences (opt-in to real-time RFQ broadcast)""" - updateRFQWatchPreferences(enabled: Boolean!, pairs: [String!]): RFQWatchPreferences! - """Internal: issue one-time key (called by mcp-gateway)""" - issueMcpOtk(rfqId: ID, tradeId: ID, purpose: String!, ttlSeconds: Int!): McpOtkIssued! - """MCP: commit hashlock to an RFQ""" - mcpCommitHashlock(rfqId: ID!, hashlock: String!, signature: String!, otkToken: String!): McpConfirmResult! - """MCP: commit HTLC parameters (hashlock/timelock/give amount/counterparty/token) - directly onto a trade row. Used by the direct-trade path (confirmDirectTrade) - where there is no RFQ. The RFQ path populates these fields from the RFQ - row at acceptQuote time and does not call this mutation.""" - submitTradeHashlock( - tradeId: ID! - hashlock: String! - timelock: Int! - giveAmount: String! - counterpartyAddress: String! - tokenAddress: String - signature: String! - otkToken: String - ): McpConfirmResult! - """MCP: build unsigned lock tx for browser wallet. For walletType=safe, - the result includes safeContext and the signUrl routes to the Safe sign - page instead of the immediate broadcast page.""" - mcpBuildLockCalldata(tradeId: ID!, walletType: McpWalletType = eoa, safeAddress: String, chainType: String): McpBuildResult! - """MCP: confirm lock tx was broadcast (EOA) OR a Safe proposal was - submitted (multisig). Provide exactly one of txHash or safeTxHash.""" - mcpConfirmLock(tradeId: ID!, txHash: String, safeTxHash: String, otkToken: String!): McpConfirmResult! - """MCP: build the counterparty mirror-lock calldata. Callable only by - the trade's counterparty, only after status=INITIATOR_LOCKED. Derives - the counterparty timelock as trade.timelock - 30 min. counterpartyAmount - defaults to trade.give_amount for symmetric test swaps; pass an explicit - wei value for real quote-side amounts. counterpartyTokenAddress is null - for native asset.""" - mcpBuildCounterpartyLockCalldata( - tradeId: ID!, - counterpartyAmount: String, - counterpartyTokenAddress: String, - ): McpBuildResult! - """MCP: confirm the counterparty's mirror-lock tx. Transitions the trade - from INITIATOR_LOCKED to BOTH_LOCKED and inserts the counterparty HTLC - row. counterpartyAmount/counterpartyTokenAddress MUST match what the - caller passed to mcpBuildCounterpartyLockCalldata (validator uses them - to re-check the on-chain tx).""" - mcpConfirmCounterpartyLock( - tradeId: ID!, - txHash: String!, - otkToken: String!, - counterpartyAmount: String, - counterpartyTokenAddress: String, - ): McpConfirmResult! - """MCP (Safe multisig): register a safeTxHash returned by the browser - after posting the EIP-712 proposal to Safe Transaction Service. - Flips the matching mcp_wallet_proposals row to AWAITING_APPROVAL.""" - mcpRegisterSafeTx(tradeId: ID!, purpose: String!, safeTxHash: String!, otkToken: String!): McpConfirmResult! - """MCP: build claim calldata for browser wallet. - preimage is required when trade.chain_type='sui' (PTB construction needs it). - EVM agents ignore it and encode withdraw(contractId, preimage) client-side.""" - mcpBuildClaimCalldata(tradeId: ID!, preimage: String): McpBuildResult! - """MCP: confirm claim tx was broadcast""" - mcpConfirmClaim(tradeId: ID!, txHash: String!, otkToken: String!): McpConfirmResult! - """MCP: build refund calldata for browser wallet""" - mcpBuildRefundCalldata(tradeId: ID!): McpBuildResult! - """MCP: confirm refund tx was broadcast""" - mcpConfirmRefund(tradeId: ID!, txHash: String!, otkToken: String!): McpConfirmResult! - """MCP (Bitcoin-only): register a pre-signed refund tx hex BEFORE broadcasting - the funding tx. Persists onto htlcs.refund_tx_hex (inserts the initiator HTLC - row if it does not exist yet). Required per .planning/MCP-BITCOIN-PLAN.md §3.2 - because a stranded funder cannot reclaim BTC without a pre-signed refund - spend. Stored blindly — the agent only hurts itself by signing wrong.""" - mcpRegisterPreSignedRefund(tradeId: ID!, refundTxHex: String!, otkToken: String!): McpConfirmResult! - """MCP (Bitcoin-only): submit the funding-tx txid the agent just broadcast. - Backend does NOT relay the tx itself — the agent owns broadcasting per - .planning/MCP-BITCOIN-PLAN.md §3.1. Persists the txid on the initiator - HTLC row and returns state='AWAITING_CONFIRMATION'; caller polls - mcpGetConfirmStatus until CONFIRMED. Requires mcpRegisterPreSignedRefund - was called first.""" - mcpSubmitLockTx(tradeId: ID!, txid: String!, otkToken: String!): McpConfirmResult! - """MCP (Bitcoin-only): poll the confirmation status of the submitted - funding tx. Queries Esplora directly in C.2 (chain-watcher BTC - subscriber lands in Phase C.3). Returns confirmationsObserved / - confirmationsRequired and a state string — see BtcConfirmStatus.""" - mcpGetConfirmStatus(tradeId: ID!, otkToken: String): BtcConfirmStatus! - """Register a webhook URL to receive new RFQ notifications""" - registerRfqWebhook(url: String!): Boolean! - """Unregister RFQ webhook""" - unregisterRfqWebhook: Boolean! - } - - type RFQWatchPreferences { - enabled: Boolean! - pairs: [String!] - } - - # ============================================================ - # Compute asset class — types required by PR2.1a mutation - # ============================================================ - # These types are duplicated from SUBSCRIPTION_SDL so that the - # federated HTTP schema (built from typeDefs) can resolve - # createComputeCapacityRfq when T6 wires the resolver. - # SUBSCRIPTION_SDL retains its own copy for the graphql-ws server. - - enum AttestationState { - MINTED - LISTED - SOLD - ACTIVE - SETTLED - REFUNDED - } - - type AttestationLeg { - tradeId: ID! - chainType: String! - chainId: String - """ComputeSettlement deployment address""" - contractAddress: String! - """ERC-1155 tokenId of the capacity batch (decimal string)""" - tokenId: String! - periodStart: String! - periodEnd: String! - """checkpoint sequence""" - cpSeq: Int! - state: AttestationState! - cumUptimeBps: Int - cumPerfBps: Int - """65-byte hex when collected""" - buyerSig: String - providerSig: String - monitorSigs: [String!] - settleTxHash: String - refundTxHash: String - """EVM tx hash of the mint, filled by chain-watcher on TokenMinted observed""" - mintTxHash: String - """EVM tx hash of the buy, filled by chain-watcher on Bought observed""" - boughtTxHash: String - """EVM tx hash of ComputeSettlement.activate. NULL until chain-watcher observes Activated. PR2.3.""" - activatedTxHash: String - } - - input CreateComputeCapacityRfqInput { - chainId: Int! - skuHash: String! - regionCode: String! - periodStart: String! - periodEnd: String! - disputeWindow: Int! - termsHash: String! - unitNotional: String! - pAcceptBps: Int! - providerSigner: String! - monitorSigners: [String!]! - monitorThreshold: Int! - quoteToken: String! - } - - type ComputeCapacityRfqPayload { - trade: Trade! - attestationLeg: AttestationLeg! - } - - input AcceptComputeCapacityListingInput { - """Decimal-string tokenId of the LISTED attestation_legs row""" - tokenId: String! - """0x... EVM address of the buyer (must own wallet per assertWalletBelongsToUser)""" - buyerAddress: String! - """65-byte hex signature (0x...) — backend stores opaquely, on-chain contract verifies""" - buyerSignature: String! - } - - type AcceptComputeCapacityListingPayload { - trade: Trade! - attestationLeg: AttestationLeg! - """Operator-actionable next-step text""" - buyInstruction: String! - } - - extend type Mutation { - createComputeCapacityRfq(input: CreateComputeCapacityRfqInput!): ComputeCapacityRfqPayload! - acceptComputeCapacityListing(input: AcceptComputeCapacityListingInput!): AcceptComputeCapacityListingPayload! - } diff --git a/test/fixtures/schema.subscriptions.graphql b/test/fixtures/schema.subscriptions.graphql deleted file mode 100644 index 946e5e4..0000000 --- a/test/fixtures/schema.subscriptions.graphql +++ /dev/null @@ -1,280 +0,0 @@ -# Vendored GraphQL SDL — DO NOT EDIT BY HAND -# Source: Cayman-Hashlock backend/services/trade-service/src/schema.ts (SUBSCRIPTION_SDL — graphql-ws schema) -# Ref: origin/main @ 3cbc423522392967db055366a3229e1a3664565b -# Vendored: 2026-06-11 -# Refresh: git -C fetch origin && node scripts/vendor-schema.mjs - - type TradeUpdate { - tradeId: ID! - status: String! - } - - type HTLCUpdate { - tradeId: ID! - contractAddress: String - htlcStatus: String! - } - - type RFQBroadcast { - rfqId: ID! - baseToken: String! - quoteToken: String! - side: String! - amount: String! - createdAt: String! - } - - type QuoteSubmittedEvent { - quoteId: ID! - rfqId: ID! - marketMakerId: ID! - price: String! - amount: String! - status: String! - expiresAt: String! - createdAt: String! - } - - type MyRfqQuoteNotification { - quoteId: ID! - rfqId: ID! - baseToken: String! - quoteToken: String! - side: String! - price: String! - amount: String! - createdAt: String! - } - - """Day 5 Wave 5 — Ghost Auction Option B: broadcast of new blind orders. - Returns NULL for the originator's own broadcast so creators don't see - their own posts. Carries pseudonym (the standing pii-free alias), - never the creator id. - - Note: `side` is String! (not the Side enum) because this type lives in - SUBSCRIPTION_SDL — a subset parsed standalone for the graphql-ws server, - which does not import the Side enum. The federated schema gets the - enum-typed version via the resolver layer; clients see equivalent - values either way.""" - type BlindOrderBroadcast { - orderId: ID! - pseudonym: String! - baseToken: String! - quoteToken: String! - side: String! - amount: String! - minPrice: String - maxPrice: String - createdAt: String! - } - - """ - User-scoped trade lifecycle event. Fan-out by user_id (initiator OR - counterparty) so a single subscription per session covers every trade - the user is a party to. `kind` discriminates the lifecycle stage so - the bell can render distinct copy + deep-link routes. - """ - type MyTradeUpdate { - tradeId: ID! - """One of: INITIATOR_LOCKED, BOTH_LOCKED, COMPLETED, REFUNDED, EXPIRED, CANCELLED""" - kind: String! - """Tier-2 status for richer UIs (e.g. CROSS_CHAIN_CLAIM_READY).""" - status: String! - role: String! - baseToken: String - quoteToken: String - occurredAt: String! - } - - """ - Instant Settlement Faz 1.6: a taker requested an instant fill on one of - the subscriber's instant-fill quotes. User-scoped to the SOLVER (the - quote's market maker) — the solver must now front the fill from its vault. - """ - type InstantFillRequested { - fillId: ID! - quoteId: ID! - rfqId: ID! - """Always 'committed' at request time.""" - state: String! - """Committed amount in the asset's smallest unit (decimal string).""" - amountWei: String! - createdAt: String! - } - - """ - Instant-fill lifecycle: the solver marked one of the subscriber's requested - fills as fronted — the taker's asset is on its way from the solver vault. - User-scoped to the TAKER (the RFQ owner); mirror of InstantFillRequested - with the parties swapped. - """ - type InstantFillFronted { - fillId: ID! - quoteId: ID! - rfqId: ID! - """Trade materialised from the accepted quote (fronting requires the link).""" - tradeId: ID - """Always 'fronted' at publish time.""" - state: String! - """Committed amount in the asset's smallest unit (decimal string).""" - amountWei: String! - """EVM tx hash of the solver vault's fronting transfer.""" - frontTxHash: String! - frontedAt: String! - } - - type Subscription { - tradeUpdated(tradeId: ID!): TradeUpdate! - htlcUpdated(tradeId: ID!): HTLCUpdate! - """Real-time broadcast of new RFQs to opted-in users""" - rfqCreated: RFQBroadcast - """Live quote landed on the caller's RFQ. Authorized only for the RFQ owner.""" - quoteSubmitted(rfqId: ID!): QuoteSubmittedEvent! - """Cross-page user-scoped fan-out: any quote on any of the caller's RFQs.""" - myRfqQuoteSubmitted: MyRfqQuoteNotification! - """Real-time broadcast of new blind orders for the Ghost Auction terminal + MM bots.""" - blindOrderCreated: BlindOrderBroadcast - """Cross-page user-scoped fan-out: any lifecycle event on any of the caller's trades.""" - myTradeUpdated: MyTradeUpdate! - """Solver-scoped fan-out: a taker requested an instant fill on one of the caller's quotes.""" - instantFillRequested: InstantFillRequested! - """Taker-scoped fan-out: a solver marked one of the caller's requested instant fills as fronted.""" - instantFillFronted: InstantFillFronted! - } - - # ============================================================ - # Compute asset class (PR1 scaffolding — flag OFF) - # ============================================================ - # - # All types below land via the FEATURE_COMPUTE_TRADING-gated path. - # PR1 does NOT wire them into any resolver, query, or mutation. They - # appear in GraphQL introspection but no runtime path produces or - # consumes them. PR2 adds createComputeCapacityRfq, - # acceptComputeCapacityQuote, and the attestation-watcher chain-watcher; - # that's when these types start being served. - # - # See CAYMAN-PR1-KICKOFF.md §4 + §6.2 for the upstream spec; the data - # shape is non-negotiable, only the schema dialect adapts. - - enum InstrumentKind { - TOKEN - COMPUTE_CAPACITY - } - - enum SettlementMode { - HASHLOCK_HTLC # existing token-for-token atomic swap - COLLATERAL_BACKED_DELIVERY # existing forward-contract path - ATTESTATION_LOCK_PERFORMANCE # new — hashlock-compute settlement - } - - enum AttestationState { - MINTED - LISTED - SOLD - ACTIVE - SETTLED - REFUNDED - } - - enum ComputeType { - GPU - CPU - TPU - } - - enum DeliveryMethod { - API_KEY - SSH_ACCESS - DOCKER_JOB_SUBMIT - CUSTOM - } - - enum MeasurementMode { - MONITOR_COMMITTEE - BUYER_SELF_REPORT - NRAS_GPU_ATTESTED - TDX_VM_ATTESTED - ZK_OF_ATTESTATION - } - - """ - Discriminated by 'kind'; both members share chainType + chainId for - ergonomic shared-field reads across asset classes. - """ - interface TradedInstrument { - kind: InstrumentKind! - chainType: String! - chainId: String! - } - - type TokenInstrument implements TradedInstrument { - kind: InstrumentKind! - chainType: String! - chainId: String! - symbol: String! - """null for native gas tokens (ETH, BTC, SUI)""" - contractAddress: String - decimals: Int! - } - - type ComputeCapacityInstrument implements TradedInstrument { - kind: InstrumentKind! - chainType: String! - chainId: String! - computeType: ComputeType! - """Hardware identifier (e.g. 'H100', 'B200', 'Xeon-Platinum-8580')""" - hardwareClass: String - """ComputeSettlement deployment address on the named chain""" - contractAddress: String! - """ERC-1155 tokenId of the capacity batch (256-bit, stored as decimal string)""" - tokenId: String! - """unix seconds as bigint string""" - periodStart: String! - periodEnd: String! - """USDC base units, decimal string""" - unitNotional: String! - attestationLockKeys: AttestationLockKeys! - deliveryMethod: DeliveryMethod! - """opaque method-specific payload (negotiated in chat)""" - endpointSpec: String - measurementMode: MeasurementMode! - } - - type AttestationLockKeys { - """0x-prefixed EVM address""" - providerSigner: String! - monitorSigners: [String!]! - monitorThreshold: Int! - """nullable until buy lands""" - buyerSigner: String - } - - type AttestationLeg { - tradeId: ID! - chainType: String! - chainId: String - """ComputeSettlement deployment address""" - contractAddress: String! - """ERC-1155 tokenId of the capacity batch (decimal string)""" - tokenId: String! - periodStart: String! - periodEnd: String! - """checkpoint sequence""" - cpSeq: Int! - state: AttestationState! - cumUptimeBps: Int - cumPerfBps: Int - """65-byte hex when collected""" - buyerSig: String - providerSig: String - monitorSigs: [String!] - settleTxHash: String - refundTxHash: String - """EVM tx hash of the mint, filled by chain-watcher on TokenMinted observed""" - mintTxHash: String - """EVM tx hash of the buy, filled by chain-watcher on Bought observed""" - boughtTxHash: String - """EVM tx hash of ComputeSettlement.activate. NULL until chain-watcher observes Activated. PR2.3.""" - activatedTxHash: String - } - diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 44786a9..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ES2022", "DOM"], - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "dist", - "rootDir": "src" - }, - "include": ["src"], - "exclude": ["node_modules", "dist", "**/*.test.ts"] -} diff --git a/typescript/README.md b/typescript/README.md new file mode 100644 index 0000000..cce31e5 --- /dev/null +++ b/typescript/README.md @@ -0,0 +1,40 @@ +# @hashlock-tech/sdk + +TypeScript SDK for the [Hashlock Markets](https://github.com/Hashlock-Tech/hashlock-sdk) developer API — +non-custodial cross-chain atomic swaps (BTC ↔ EVM/TRON). Runs on Node 18+, browsers, and edge runtimes +(native `fetch` / `WebSocket`, zero runtime dependencies). + +```bash +npm i @hashlock-tech/sdk +``` + +```ts +import { HashlockClient, newSecret, verifyWebhook, MakerFeed } from '@hashlock-tech/sdk'; + +const client = new HashlockClient({ apiKey: process.env.HASHLOCK_API_KEY! }); +await client.me(); + +// cursor pagination — page or auto-iterate +const page = await client.listSwaps({ limit: 20 }); +for await (const swap of client.swaps()) { /* … */ } + +// webhooks: verify a delivery against the raw body +const ok = await verifyWebhook({ secret, rawBody, signature: req.headers['x-hashlock-signature'], timestamp: req.headers['x-hashlock-timestamp'] }); + +// maker feed: stream RFQs and quote over a socket +const feed = new MakerFeed({ apiKey, onRfq: (rfq) => feed.quote(rfq.id, '650000') }); +await feed.connect(); +``` + +Settlement is custody-agnostic: `buildFund` / `buildClaim` / `buildRefund` return unsigned transactions — +sign with your wallet or HSM (see [`../examples/fireblocks`](../examples/fireblocks)) and `broadcast(...)` +(or let your custody provider broadcast). See the [root README](../README.md) for the full swap lifecycle. + +## Build + +```bash +npm install && npm run build # tsup → dist (esm + d.ts) +npm run typecheck +``` + +MIT diff --git a/typescript/package.json b/typescript/package.json new file mode 100644 index 0000000..4d1fe2c --- /dev/null +++ b/typescript/package.json @@ -0,0 +1,31 @@ +{ + "name": "@hashlock-tech/sdk", + "version": "1.0.0", + "description": "TypeScript SDK for the Hashlock Markets developer API — non-custodial cross-chain atomic swaps (BTC ↔ EVM/TRON) over sealed RFQ + HTLC.", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": ["dist", "README.md"], + "engines": { "node": ">=18" }, + "sideEffects": false, + "scripts": { + "build": "tsup", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "keywords": ["hashlock", "atomic-swap", "htlc", "bitcoin", "cross-chain", "otc", "defi"], + "repository": { "type": "git", "url": "https://github.com/Hashlock-Tech/hashlock-sdk", "directory": "typescript" }, + "devDependencies": { + "tsup": "^8.3.5", + "typescript": "^5.7.2", + "vitest": "^2.1.8" + } +} diff --git a/typescript/src/client.ts b/typescript/src/client.ts new file mode 100644 index 0000000..28f9ae8 --- /dev/null +++ b/typescript/src/client.ts @@ -0,0 +1,190 @@ +import { HashlockError } from './errors.js'; +import type { + Asset, + CreateRfqInput, + LegKey, + Me, + Page, + Rfq, + SettlementBuild, + Swap, + Thread, + Webhook, + WebhookEvent, +} from './types.js'; + +export interface HashlockClientOptions { + /** Your API key: `hk_test_…` (testnet) or `hk_live_…` (mainnet). */ + apiKey: string; + /** API base URL. Default: the public sandbox. Point at your own env / the prod host as needed. */ + baseUrl?: string; + /** Override the fetch implementation (e.g. a custom agent). Defaults to global fetch. */ + fetch?: typeof fetch; +} + +interface RequestOptions { + method?: string; + query?: Record; + body?: unknown; + /** Idempotency-Key — a retried unsafe request with the same key executes at most once. */ + idempotencyKey?: string; +} + +const DEFAULT_BASE = 'https://api-dev.hashlock.markets/v1'; + +/** + * Thin, typed client for the Hashlock Markets developer API (/v1). Custody-agnostic: the settlement + * endpoints return UNSIGNED transactions you sign with your own key/HSM (see the examples), then hand + * back to {@link HashlockClient.broadcast}. The server never holds your keys. + */ +export class HashlockClient { + private readonly apiKey: string; + private readonly baseUrl: string; + private readonly fetchImpl: typeof fetch; + + constructor(opts: HashlockClientOptions) { + if (!opts.apiKey) throw new Error('HashlockClient: apiKey is required'); + this.apiKey = opts.apiKey; + this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE).replace(/\/+$/, ''); + this.fetchImpl = opts.fetch ?? globalThis.fetch; + if (!this.fetchImpl) throw new Error('HashlockClient: no fetch available — pass options.fetch'); + } + + // ── core ─────────────────────────────────────────────────────────────────── + /** Verify the key and see its scopes. */ + me(): Promise { + return this.request('/me'); + } + /** The asset registry (chain, token|native, decimals, symbol). */ + async assets(): Promise { + return (await this.request<{ assets: Asset[] }>('/assets')).assets; + } + + // ── RFQs ───────────────────────────────────────────────────────────────────── + /** One page of the public order book. Pass `cursor`/`limit` to page. */ + listRfqs(params: { baseAssetId?: string; quoteAssetId?: string; direction?: string; limit?: number; cursor?: string } = {}): Promise> { + return this.page('/rfqs', 'rfqs', params); + } + /** Async-iterate the whole order book, following the cursor automatically. */ + rfqs(params: { baseAssetId?: string; quoteAssetId?: string; direction?: string; limit?: number } = {}): AsyncGenerator { + return this.iterate('/rfqs', 'rfqs', params); + } + async getRfq(id: string): Promise { + return (await this.request<{ rfq: Rfq }>(`/rfqs/${id}`)).rfq; + } + /** Create an RFQ (requires the `taker` scope). */ + async createRfq(input: CreateRfqInput, idempotencyKey?: string): Promise { + return (await this.request<{ rfq: Rfq }>('/rfqs', { method: 'POST', body: input, idempotencyKey })).rfq; + } + /** Quote an RFQ (requires the `maker` scope) — opens a settlement thread. */ + quoteRfq(rfqId: string, quoteAmount: string, idempotencyKey?: string): Promise<{ thread: { id: string } }> { + return this.request(`/rfqs/${rfqId}/quotes`, { method: 'POST', body: { quoteAmount }, idempotencyKey }); + } + + // ── negotiation thread ─────────────────────────────────────────────────────── + getThread(id: string): Promise<{ thread: Thread; rfq: Rfq; messages: unknown[] }> { + return this.request(`/threads/${id}`); + } + proposeTerms(threadId: string, quoteAmount: string): Promise<{ thread: Thread }> { + return this.request(`/threads/${threadId}/propose`, { method: 'POST', body: { quoteAmount } }); + } + acceptProposal(threadId: string): Promise<{ thread: Thread }> { + return this.request(`/threads/${threadId}/accept-proposal`, { method: 'POST' }); + } + /** + * Accept the current terms. When BOTH sides have accepted, the swap is created. The initiator (funds + * the long leg) MUST pass `hashlock` = sha256(secret) — see {@link newSecret}. + */ + acceptTerms(threadId: string, opts: { hashlock?: string } = {}): Promise<{ thread: Thread; swap?: Swap }> { + return this.request(`/threads/${threadId}/accept`, { method: 'POST', body: opts }); + } + + // ── swaps ───────────────────────────────────────────────────────────────────── + listSwaps(params: { limit?: number; cursor?: string } = {}): Promise> { + return this.page('/swaps', 'swaps', params); + } + swaps(params: { limit?: number } = {}): AsyncGenerator { + return this.iterate('/swaps', 'swaps', params); + } + async getSwap(id: string): Promise { + return (await this.request<{ swap: Swap }>(`/swaps/${id}`)).swap; + } + /** Set your receive (payout) / refund address for a leg. Bitcoin: the compressed pubkey (hex). */ + async setSwapAddress(swapId: string, chain: string, address: string): Promise { + return (await this.request<{ swap: Swap }>(`/swaps/${swapId}/address`, { method: 'POST', body: { chain, address } })).swap; + } + + // ── settlement builders (UNSIGNED — sign with your own key/HSM, then broadcast) ─ + buildFund(swapId: string, leg: LegKey): Promise { + return this.request(`/swaps/${swapId}/legs/${leg}/fund`, { method: 'POST' }); + } + buildClaim(swapId: string, leg: LegKey, secret: string): Promise { + return this.request(`/swaps/${swapId}/legs/${leg}/claim`, { method: 'POST', body: { secret } }); + } + buildRefund(swapId: string, leg: LegKey): Promise { + return this.request(`/swaps/${swapId}/legs/${leg}/refund`, { method: 'POST' }); + } + /** Relay a client-signed transaction. `chain`: 'evm' (0x raw tx) | 'tron' (signed tx obj) | 'bitcoin' (raw hex). */ + broadcast(chain: 'evm' | 'tron' | 'bitcoin', signed: unknown, idempotencyKey?: string): Promise<{ txid: string }> { + return this.request('/tx/broadcast', { method: 'POST', body: { chain, signed }, idempotencyKey }); + } + + // ── webhooks ───────────────────────────────────────────────────────────────── + async listWebhooks(): Promise { + return (await this.request<{ webhooks: Webhook[] }>('/webhooks')).webhooks; + } + /** Register a webhook. Returns the signing `secret` ONCE — store it to verify deliveries. */ + createWebhook(url: string, events?: WebhookEvent[]): Promise<{ webhook: Webhook; secret: string }> { + return this.request('/webhooks', { method: 'POST', body: { url, events } }); + } + deleteWebhook(id: string): Promise<{ ok: boolean }> { + return this.request(`/webhooks/${id}`, { method: 'DELETE' }); + } + pingWebhook(id: string): Promise<{ ok: boolean; deliveryId: string }> { + return this.request(`/webhooks/${id}/ping`, { method: 'POST' }); + } + + // ── internals ──────────────────────────────────────────────────────────────── + private async page(path: string, key: string, params: Record): Promise> { + const r = await this.request>(path, { query: params }); + return { items: (r[key] as T[]) ?? [], nextCursor: (r.nextCursor as string | null) ?? null }; + } + private async *iterate(path: string, key: string, params: Record): AsyncGenerator { + let cursor: string | null | undefined = undefined; + do { + const p: Page = await this.page(path, key, { ...params, cursor }); + for (const item of p.items) yield item; + cursor = p.nextCursor; + } while (cursor); + } + + async request(path: string, opts: RequestOptions = {}): Promise { + const url = new URL(this.baseUrl + path); + for (const [k, v] of Object.entries(opts.query ?? {})) if (v !== undefined && v !== '') url.searchParams.set(k, String(v)); + const headers: Record = { authorization: `Bearer ${this.apiKey}` }; + if (opts.body !== undefined) headers['content-type'] = 'application/json'; + if (opts.idempotencyKey) headers['idempotency-key'] = opts.idempotencyKey; + + const res = await this.fetchImpl(url.toString(), { + method: opts.method ?? (opts.body !== undefined ? 'POST' : 'GET'), + headers, + body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined, + }); + const text = await res.text(); + const data = text ? safeJson(text) : undefined; + if (!res.ok) { + const msg = (data as { error?: string })?.error ?? `HTTP ${res.status}`; + const retryAfter = res.headers.get('retry-after'); + throw new HashlockError(res.status, msg, data, retryAfter ? Number(retryAfter) : undefined); + } + return data as T; + } +} + +function safeJson(s: string): unknown { + try { + return JSON.parse(s); + } catch { + return s; + } +} diff --git a/typescript/src/errors.ts b/typescript/src/errors.ts new file mode 100644 index 0000000..fc63820 --- /dev/null +++ b/typescript/src/errors.ts @@ -0,0 +1,22 @@ +/** Thrown for any non-2xx API response. Carries the HTTP status and the server's error message. */ +export class HashlockError extends Error { + readonly status: number; + readonly body: unknown; + /** Present on 429 — seconds until the rate-limit window resets. */ + readonly retryAfter?: number; + + constructor(status: number, message: string, body?: unknown, retryAfter?: number) { + super(message); + this.name = 'HashlockError'; + this.status = status; + this.body = body; + this.retryAfter = retryAfter; + } + + get isRateLimited(): boolean { + return this.status === 429; + } + get isAuthError(): boolean { + return this.status === 401 || this.status === 403; + } +} diff --git a/typescript/src/index.ts b/typescript/src/index.ts new file mode 100644 index 0000000..7cc1789 --- /dev/null +++ b/typescript/src/index.ts @@ -0,0 +1,9 @@ +export { HashlockClient } from './client.js'; +export type { HashlockClientOptions } from './client.js'; +export { HashlockError } from './errors.js'; +export { verifyWebhook } from './webhooks.js'; +export type { VerifyOptions } from './webhooks.js'; +export { newSecret, sha256Hex } from './secret.js'; +export { MakerFeed } from './ws.js'; +export type { MakerFeedOptions } from './ws.js'; +export * from './types.js'; diff --git a/typescript/src/secret.ts b/typescript/src/secret.ts new file mode 100644 index 0000000..749ff4e --- /dev/null +++ b/typescript/src/secret.ts @@ -0,0 +1,21 @@ +// The swap secret + its hashlock. The INITIATOR (funds the long leg) generates the secret locally, +// keeps it private, and passes only `hashlock = sha256(secret)` to acceptTerms(). The secret is revealed +// on-chain when the initiator claims the counter-leg; keep it until then. Web Crypto → runs everywhere. + +function toHex(bytes: Uint8Array): string { + return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join(''); +} + +/** A fresh 32-byte secret and its sha256 hashlock, both as hex (no 0x). */ +export async function newSecret(): Promise<{ secret: string; hashlock: string }> { + const secret = crypto.getRandomValues(new Uint8Array(32)); + const hash = await crypto.subtle.digest('SHA-256', secret); + return { secret: toHex(secret), hashlock: toHex(new Uint8Array(hash)) }; +} + +/** sha256(secret) as hex — verify a preimage against a swap's hashlock. */ +export async function sha256Hex(hex: string): Promise { + const bytes = Uint8Array.from((hex.replace(/^0x/, '').match(/.{1,2}/g) ?? []).map((h) => parseInt(h, 16))); + const hash = await crypto.subtle.digest('SHA-256', bytes); + return toHex(new Uint8Array(hash)); +} diff --git a/typescript/src/types.ts b/typescript/src/types.ts new file mode 100644 index 0000000..0eca4ab --- /dev/null +++ b/typescript/src/types.ts @@ -0,0 +1,154 @@ +// Wire types for the Hashlock Markets developer API (/v1). Base-unit amounts are strings (integers in the +// asset's smallest unit — sats for BTC, 1e6 for USDT). Chain families: 'evm' | 'tron' | 'bitcoin'. + +export type Scope = 'read' | 'taker' | 'maker'; +export type Direction = 'sell_base' | 'buy_base'; +export type LegKey = 'a' | 'b'; + +export interface Me { + userId: string; + scopes: Scope[]; +} + +export interface Asset { + id: string; + chain: string; // 'ethereum' | 'tron' | 'bitcoin' | … + symbol: string; + decimals: number; + address: string | null; // token contract; null = native coin + isNative?: boolean; + enabled?: boolean; +} + +export interface Rfq { + id: string; + direction: Direction; + baseAssetId: string; + baseAmount: string; + quoteAssetId: string; + askAmount?: string | null; + status: string; + visibility?: 'public' | 'private'; + targetAddress?: string | null; + creatorId?: string; + createdAt?: string; + expiresAt?: string; +} + +export interface CreateRfqInput { + direction: Direction; + baseAssetId: string; + baseAmount: string; + quoteAssetId: string; + ttlSeconds: number; + askAmount?: string; + visibility?: 'public' | 'private'; + targetAddress?: string; +} + +export interface Thread { + id: string; + rfqId: string; + makerId: string; + takerId: string; + status: string; + currentQuoteAmount?: string | null; + pendingAmount?: string | null; + pendingBy?: string | null; + takerAccepted?: boolean; + makerAccepted?: boolean; + updatedAt?: string; +} + +export interface Swap { + id: string; + threadId: string; + status: string; // agreed → initiator_funded → counterparty_funded → initiator_claimed → counterparty_claimed | refunded + hashlock: string; + makerId: string; + takerId: string; + initiatorUserId: string; + feeAssetId?: string | null; + feeAmount?: string | null; + aChain: string; + aAssetId: string; + aAmount: string; + aHtlcAddress?: string | null; + aRedeemScript?: string | null; + aPayoutAddress?: string | null; + aRefundAddress?: string | null; + aTimelock: string; + aFundTx?: string | null; + aClaimTx?: string | null; + bChain: string; + bAssetId: string; + bAmount: string; + bHtlcAddress?: string | null; + bRedeemScript?: string | null; + bPayoutAddress?: string | null; + bRefundAddress?: string | null; + bTimelock: string; + bFundTx?: string | null; + bClaimTx?: string | null; +} + +/** A page of results plus the opaque cursor for the next page (null = last page). */ +export interface Page { + items: T[]; + nextCursor: string | null; +} + +// ── Settlement builders: chain-specific UNSIGNED material. Sign with your own key/HSM, then broadcast. ── + +/** EVM: unsigned transactions to sign (eip1559) and send raw. */ +export interface EvmBuild { + chain: string; + family: 'evm'; + chainId: number | null; + sign: 'evm-tx'; + txs: Array<{ to: string; data: string; value?: string }>; +} +/** Bitcoin funding: pay this P2WSH exactly `amountSats` from your BTC wallet. */ +export interface BtcPaymentBuild { + chain: string; + family: 'bitcoin'; + sign: 'btc-payment'; + payTo: string; + amountSats: string; +} +/** Bitcoin claim/refund: spend the P2WSH. Claim → witness [sig, preimage, 0x01, redeem]; refund → OP_ELSE after the timelock. */ +export interface BtcWitnessBuild { + chain: string; + family: 'bitcoin'; + sign: 'btc-witness'; + p2wsh: string; + redeemHex: string; + preimageHex?: string; // claim only + timelockUnix?: number; // refund only + note?: string; +} +/** TRON: unsigned transaction objects — sign each by its txID (secp256k1) and broadcast. */ +export interface TronBuild { + chain: string; + family: 'tron'; + sign: 'tron-txid'; + transactions: Array<{ transaction: unknown; txID: string }>; +} +export type SettlementBuild = EvmBuild | BtcPaymentBuild | BtcWitnessBuild | TronBuild; + +export interface Webhook { + id: string; + url: string; + events: string[]; + enabled: boolean; + lastDeliveryAt?: string | null; + lastStatus?: number | null; + createdAt?: string; +} +export type WebhookEvent = + | 'quote.created' + | 'swap.agreed' + | 'swap.funded' + | 'secret.revealed' + | 'swap.settled' + | 'swap.refunded'; diff --git a/typescript/src/webhooks.ts b/typescript/src/webhooks.ts new file mode 100644 index 0000000..ccc77d1 --- /dev/null +++ b/typescript/src/webhooks.ts @@ -0,0 +1,46 @@ +// Verify webhook deliveries. The server signs each POST with +// X-Hashlock-Signature: sha256=HMAC-SHA256(secret, `${timestamp}.${rawBody}`) +// where timestamp is the X-Hashlock-Timestamp header. Verify against the RAW request body (not a +// re-serialised object). Uses Web Crypto so it runs in Node 18+, browsers, and edge runtimes. + +const encoder = new TextEncoder(); + +async function hmacHex(secret: string, message: string): Promise { + const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); + const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(message)); + return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, '0')).join(''); +} + +function timingSafeEqual(a: string, b: string): boolean { + if (a.length !== b.length) return false; + let diff = 0; + for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i); + return diff === 0; +} + +export interface VerifyOptions { + secret: string; + /** The RAW request body string (exactly as received — do not re-serialise). */ + rawBody: string; + /** The X-Hashlock-Signature header value, e.g. `sha256=…`. */ + signature: string | null | undefined; + /** The X-Hashlock-Timestamp header value. */ + timestamp: string | null | undefined; + /** Reject deliveries older than this many seconds (replay guard). Default 300; 0 disables. */ + toleranceSeconds?: number; + /** Current unix time in seconds (for testing). Defaults to Date.now(). */ + nowUnix?: number; +} + +/** Returns true iff the signature matches and the timestamp is within tolerance. */ +export async function verifyWebhook(opts: VerifyOptions): Promise { + if (!opts.signature || !opts.timestamp) return false; + const tolerance = opts.toleranceSeconds ?? 300; + if (tolerance > 0) { + const now = opts.nowUnix ?? Math.floor(Date.now() / 1000); + const ts = Number(opts.timestamp); + if (!Number.isFinite(ts) || Math.abs(now - ts) > tolerance) return false; + } + const expected = `sha256=${await hmacHex(opts.secret, `${opts.timestamp}.${opts.rawBody}`)}`; + return timingSafeEqual(expected, opts.signature); +} diff --git a/typescript/src/ws.ts b/typescript/src/ws.ts new file mode 100644 index 0000000..2efad0f --- /dev/null +++ b/typescript/src/ws.ts @@ -0,0 +1,59 @@ +// Maker feed over WebSocket (/v1/ws). Authenticate with an API key that has the `maker` scope, receive a +// snapshot of the public order book, then a live stream of quotable RFQs; submit quotes on the same socket. +// Uses the global WebSocket (Node 22+, browsers, edge). In older Node, pass one via `options.WebSocket`. + +import type { Rfq } from './types.js'; + +export interface MakerFeedOptions { + apiKey: string; + /** WS URL. Default derives from the REST base: wss://api-dev.hashlock.markets/v1/ws */ + url?: string; + WebSocket?: typeof WebSocket; + onSnapshot?: (rfqs: Rfq[]) => void; + onRfq?: (rfq: Rfq, kind: string) => void; + onQuoted?: (msg: { rfqId: string; threadId: string }) => void; + onError?: (message: string) => void; + onClose?: () => void; +} + +const DEFAULT_WS = 'wss://api-dev.hashlock.markets/v1/ws'; + +export class MakerFeed { + private ws?: WebSocket; + private readonly opts: MakerFeedOptions; + constructor(opts: MakerFeedOptions) { + this.opts = opts; + } + + connect(): Promise { + const Ctor = this.opts.WebSocket ?? (globalThis as { WebSocket?: typeof WebSocket }).WebSocket; + if (!Ctor) throw new Error('MakerFeed: no WebSocket available — pass options.WebSocket'); + const ws = new Ctor(this.opts.url ?? DEFAULT_WS); + this.ws = ws; + return new Promise((resolve, reject) => { + ws.onopen = () => ws.send(JSON.stringify({ apiKey: this.opts.apiKey })); + ws.onerror = () => { this.opts.onError?.('websocket error'); reject(new Error('websocket error')); }; + ws.onclose = () => this.opts.onClose?.(); + ws.onmessage = (ev: MessageEvent) => { + let m: { type?: string; rfqs?: Rfq[]; rfq?: Rfq; kind?: string; rfqId?: string; threadId?: string; error?: string }; + try { m = JSON.parse(String(ev.data)); } catch { return; } + switch (m.type) { + case 'snapshot': this.opts.onSnapshot?.(m.rfqs ?? []); break; + case 'ready': resolve(); break; + case 'rfq': if (m.rfq) this.opts.onRfq?.(m.rfq, m.kind ?? 'created'); break; + case 'quoted': this.opts.onQuoted?.({ rfqId: m.rfqId!, threadId: m.threadId! }); break; + case 'error': this.opts.onError?.(m.error ?? 'error'); break; + } + }; + }); + } + + /** Submit a quote for an RFQ over the socket. */ + quote(rfqId: string, quoteAmount: string): void { + this.ws?.send(JSON.stringify({ quote: { rfqId, quoteAmount } })); + } + + close(): void { + this.ws?.close(); + } +} diff --git a/typescript/tsconfig.json b/typescript/tsconfig.json new file mode 100644 index 0000000..72504a1 --- /dev/null +++ b/typescript/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "declaration": true, + "skipLibCheck": true, + "types": [], + "verbatimModuleSyntax": true, + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/tsup.config.ts b/typescript/tsup.config.ts similarity index 72% rename from tsup.config.ts rename to typescript/tsup.config.ts index 2f7ba2f..9ff9463 100644 --- a/tsup.config.ts +++ b/typescript/tsup.config.ts @@ -2,11 +2,9 @@ import { defineConfig } from 'tsup'; export default defineConfig({ entry: ['src/index.ts'], - format: ['esm', 'cjs'], + format: ['esm'], dts: true, clean: true, - splitting: false, sourcemap: true, - minify: false, target: 'es2022', }); diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index 7382f40..0000000 --- a/vitest.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - }, -});