diff --git a/.github/workflows/sdk-ci.yml b/.github/workflows/sdk-ci.yml new file mode 100644 index 0000000..68da8ce --- /dev/null +++ b/.github/workflows/sdk-ci.yml @@ -0,0 +1,68 @@ +name: SDK CI + +on: + pull_request: + paths: + - "packages/sdk-typescript/**" + - ".github/workflows/sdk-ci.yml" + push: + branches: [main] + paths: + - "packages/sdk-typescript/**" + - ".github/workflows/sdk-ci.yml" + +permissions: + contents: read + +defaults: + run: + working-directory: packages/sdk-typescript + +jobs: + build-and-test: + runs-on: gha-runner-scale-set-standard + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + fetch-depth: 0 + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "22" + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Typecheck + run: npm run typecheck + + - name: Test + run: npm test + + - name: Require Changeset + run: npm run verify:changeset + if: github.event_name == 'pull_request' + - name: Verify package (browser imports + bundle + pack) + run: npm run verify:package + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + continue-on-error: true + with: + bun-version: "1.x" + + - name: Setup Deno + uses: denoland/setup-deno@4606d5cc6fb3f673efd4f594850e3f4b3e9d29cd # v2 + continue-on-error: true + with: + deno-version: v2.x + + - name: Verify multi-runtime (Node, Bun, Deno) + run: npm run verify:runtimes + continue-on-error: true diff --git a/.github/workflows/sdk-release.yml b/.github/workflows/sdk-release.yml new file mode 100644 index 0000000..cdc4488 --- /dev/null +++ b/.github/workflows/sdk-release.yml @@ -0,0 +1,55 @@ +name: SDK Release + +on: + push: + branches: [main] + paths: + - "packages/sdk-typescript/package.json" + - "packages/sdk-typescript/.changeset/**" + +permissions: + contents: write + pull-requests: write + id-token: write + +defaults: + run: + working-directory: packages/sdk-typescript + +jobs: + release: + runs-on: gha-runner-scale-set-standard + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "22" + registry-url: "https://registry.npmjs.org" + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Test + run: npm test + + - name: Verify package + run: npm run verify:package + + - name: Create Release Pull Request or Publish + uses: changesets/action@c8bada60c408975afd1a20b3db81d6eee6789308 # v1.4.9 + with: + version: npx --no-install changeset version + publish: npm publish --tag beta --provenance + cwd: packages/sdk-typescript + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/sdk-smoke.yml b/.github/workflows/sdk-smoke.yml new file mode 100644 index 0000000..9c638bb --- /dev/null +++ b/.github/workflows/sdk-smoke.yml @@ -0,0 +1,74 @@ +name: SDK Smoke Test + +on: + workflow_dispatch: + inputs: + environment: + description: "Target environment" + required: true + default: "sandbox" + type: choice + options: + - sandbox + - production + market-type: + description: "Market type to smoke test" + required: true + default: "market-data" + type: choice + options: + - market-data + - prediction-markets + +permissions: + contents: read + +defaults: + run: + working-directory: packages/sdk-typescript + +jobs: + smoke: + runs-on: gha-runner-scale-set-standard + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "22" + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Run smoke test + run: | + GEMINI_SMOKE_ENV=${{ inputs.environment }} \ + node scripts/smoke-sandbox.mjs --market-type=${{ inputs.market-type }} + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GEMINI_API_SECRET: ${{ secrets.GEMINI_API_SECRET }} + + - name: Verify REST operations + if: inputs.environment == 'sandbox' + run: | + GEMINI_SMOKE_ENV=sandbox \ + node scripts/verify-sandbox-rest.mjs + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GEMINI_API_SECRET: ${{ secrets.GEMINI_API_SECRET }} + + - name: Verify market data + if: inputs.market-type == 'market-data' + run: | + GEMINI_MD_ENV=${{ inputs.environment }} \ + node scripts/verify-market-data-live.mjs + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GEMINI_API_SECRET: ${{ secrets.GEMINI_API_SECRET }} diff --git a/.gitignore b/.gitignore index 91e7345..802c6f0 100644 --- a/.gitignore +++ b/.gitignore @@ -49,12 +49,10 @@ venv/ tmp/ .tmp/ -# SDK generation +# SDK generation (auto-generated — sdk-typescript is committed) scripts/rest.yaml packages/sdk-go/ packages/sdk-python/ -packages/sdk-typescript/ - # AI .claude/ \ No newline at end of file diff --git a/packages/sdk-typescript/.changeset/README.md b/packages/sdk-typescript/.changeset/README.md new file mode 100644 index 0000000..654c6d4 --- /dev/null +++ b/packages/sdk-typescript/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets). + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md). diff --git a/packages/sdk-typescript/.changeset/config.json b/packages/sdk-typescript/.changeset/config.json new file mode 100644 index 0000000..4c9ccf0 --- /dev/null +++ b/packages/sdk-typescript/.changeset/config.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch" +} diff --git a/packages/sdk-typescript/.changeset/initial-gemini-markets.md b/packages/sdk-typescript/.changeset/initial-gemini-markets.md new file mode 100644 index 0000000..28468ed --- /dev/null +++ b/packages/sdk-typescript/.changeset/initial-gemini-markets.md @@ -0,0 +1,5 @@ +--- +"@gemini-markets/sdk": minor +--- + +Initial public release of the Gemini Markets TypeScript SDK. diff --git a/packages/sdk-typescript/.changeset/pre.json b/packages/sdk-typescript/.changeset/pre.json new file mode 100644 index 0000000..7ae3bd2 --- /dev/null +++ b/packages/sdk-typescript/.changeset/pre.json @@ -0,0 +1,8 @@ +{ + "mode": "pre", + "tag": "beta", + "initialVersions": { + "@gemini-markets/sdk": "0.0.0" + }, + "changesets": [] +} diff --git a/packages/sdk-typescript/.gitignore b/packages/sdk-typescript/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/packages/sdk-typescript/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/packages/sdk-typescript/LICENSE b/packages/sdk-typescript/LICENSE new file mode 100644 index 0000000..cf14887 --- /dev/null +++ b/packages/sdk-typescript/LICENSE @@ -0,0 +1,194 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship made available under + the License, as indicated by a copyright notice that is included in + or attached to the work (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and derivative works thereof. + + "Contribution" shall mean, as submitted to the Licensor for inclusion + in the Work by the copyright owner or by an individual or Legal Entity + authorized to submit on behalf of the copyright owner. For the purposes + of this definition, "submitted" means any form of electronic, verbal, + or written communication sent to the Licensor or its representatives, + including but not limited to communication on electronic mailing lists, + source code control systems, and issue tracking systems that are managed + by, or on behalf of, the Licensor for the purpose of discussing and + improving the Work, but excluding communication that is conspicuously + marked or designated in writing by the copyright owner as "Not a + Contribution." + + "Contributor" shall mean Licensor and any Legal Entity on behalf of + whom a Contribution has been received by the Licensor and incorporated + within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a cross-claim + or counterclaim in a lawsuit) alleging that the Work or any + Contribution embodied within the Work constitutes direct or contributory + patent infringement, then any patent licenses granted to You under + this License for that Work shall terminate as of the date such + litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative + Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, You must include a readable copy of the + attribution notices contained within such NOTICE file, in + at least one of the following places: within a NOTICE text + file distributed as part of the Derivative Works; within + the Source form or documentation, if provided along with the + Derivative Works; or, within a display generated by the + Derivative Works, if and wherever such third-party notices + normally appear. The contents of the NOTICE file are for + informational purposes only and do not modify the License. + You may add Your own attribution notices within Derivative + Works that You distribute, alongside or in addition to the + NOTICE text from the Work, provided that such additional + attribution notices cannot be construed as modifying the License. + + You may add Your own license statement for Your modifications and + may provide additional grant of rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the + Contribution, and to permit persons to whom the Contribution is + furnished to do so. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or reproducing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or exemplary damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or all other + commercial damages or losses), even if such Contributor has been + advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format in question. It may also be + included in a separate file called "LICENSE" accompanying the + work, or embedded in the source code comments. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/sdk-typescript/README.md b/packages/sdk-typescript/README.md new file mode 100644 index 0000000..f06ffc7 --- /dev/null +++ b/packages/sdk-typescript/README.md @@ -0,0 +1,113 @@ +# Gemini TypeScript SDK + +```ts +// Server — HMAC authenticated +import { createClient, HmacAuth } from "@gemini-markets/sdk/server"; + +const gemini = await createClient({ + env: "sandbox", + timeoutMs: 30_000, + maxRetries: 3, + auth: new HmacAuth({ + apiKey: process.env.GEMINI_API_KEY!, + apiSecret: process.env.GEMINI_API_SECRET!, + }), +}); + +const events = await gemini.predictions.listEvents({ status: ["active"] }); +const controller = new AbortController(); +const symbols = await gemini.marketData.listSymbols({ signal: controller.signal }); +const fundingReport = await gemini.marketData.getFundingAmountReportFile({ symbol: "BTCGUSDPERP" }); +const book = gemini.orderBook("GEMI-PREDICTION-SYMBOL"); +``` + +```ts +// Browser — public data, no config needed +import { createClient } from "@gemini-markets/sdk/browser"; + +const gemini = createClient(); +const ticker = await gemini.marketData.getTicker({ symbol: "BTCUSD" }); +``` + +`GeminiMarkets` is the primary facade. Its service namespaces are +`predictions`, `marketData`, `trading`, `margin`, `perpetuals`, +`accountServices`, `clearingInstant`, and `websocket`. + +Endpoint methods and response types are generated from the API specifications +and exposed through the package's type declarations and IDE completion. Set +`OAuthAuth({ env: "sandbox", ... })` when the facade uses `env: "sandbox"`. + +Order placement checks the current Prediction Markets terms first. Call +`gemini.predictions.acceptTerms()` only after showing the terms to the user and +receiving explicit consent. OAuth uses the same `auth` option via `OAuthAuth`. +Market Data file responses return raw `bytes` plus response metadata; use +`contentType` to distinguish XLSX from CSV. + +Only generated GET operations retry automatically, and only for transient +network failures or 429/502/503/504 responses. Mutating operations never retry. +Offset pagination is not snapshot-consistent while records are changing; use its +`maxItems` ceiling to bound a traversal, and provide `dedupeKey` when duplicate +records must fail loudly during a drifting traversal. + +REST methods and WebSocket requests/streams accept an optional final +`RequestOptions` argument with `signal` and `timeoutMs`; stream `close()` waits +for the exchange unsubscribe acknowledgement. + +WebSocket streams expose `state`, `lastError`, `malformedFrameCount`, and +`resubscribed`/`subscriptionError` events. Listener registration accepts +`{ signal }` for automatic removal. Public streams share one session and invoke +listeners synchronously, so callbacks should stay short; partial-depth streams +use isolated sessions by design. + +Unit tests use injected transports and do not prove live API availability. Live +sandbox checks are separate, credentialed, and manual. Authenticated WebSocket +use requires a Node/server environment or a proxy because browser WebSocket +clients cannot set custom upgrade headers. + +For applications that need an explicit REST liveness call, use the stopped +heartbeat handle: + +```ts +const heartbeat = gemini.createHeartbeat({ intervalMs: 15_000 }); +heartbeat.start(); +// ... +heartbeat.stop(); +``` + +There is no hidden heartbeat timer. WebSocket liveness checks are separately +opt-in through `webSocketLiveness`, and inbound frames are bounded by +`webSocketMaxMessageSizeBytes`. `LiveOrderBook.spread()` and `.mid()` return +floating-point values intended for display; do not use them for exact execution +decisions without decimal handling. + +## Diagnostics and safe errors + +Diagnostics are silent by default. Inject `onDiagnostic` to collect structured +events across REST, WebSocket, and order-book operations, or inject +`new ConsoleLogger({ minLevel: "debug" })` through `logger` for opt-in console +output: + +```ts +import { ConsoleLogger } from "@gemini-markets/sdk/server"; +import { createClient } from "@gemini-markets/sdk/server"; + +const gemini = await createClient({ + onDiagnostic: (event) => supportLogger.write(event), + logger: new ConsoleLogger({ minLevel: "warn" }), +}); +``` + +When constructing `OAuthAuth`, pass the same `onDiagnostic` callback and +`logger` there to include token exchange and refresh events in that same sink. + +Events include safe response metadata such as the endpoint, method, local +correlation ID, exchange request ID, status, retry count, content type, and +allowlisted rate-limit headers. WebSocket events also identify `control`, +`stream`, `reconnect`, or `mutation` traffic. Frame bodies, request bodies, +credentials, signatures, tokens, and private response bodies are not included. + +Use `serializeError(error)` for logs, telemetry, and evidence. It omits raw +error bodies by default while retaining stable `code`/`category`, response +metadata, and safe operation context. Raw bodies are available only through the +explicit debug option `serializeError(error, { includeRawBody: true })`; treat +that result as sensitive and never send it to a default logger or telemetry sink. diff --git a/packages/sdk-typescript/package-lock.json b/packages/sdk-typescript/package-lock.json new file mode 100644 index 0000000..00998a3 --- /dev/null +++ b/packages/sdk-typescript/package-lock.json @@ -0,0 +1,6510 @@ +{ + "name": "@gemini-markets/sdk", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@gemini-markets/sdk", + "version": "0.0.0", + "license": "Apache-2.0", + "devDependencies": { + "@asyncapi/modelina": "5.10.1", + "@changesets/cli": "^2.31.1", + "@types/node": "^22", + "@types/ws": "^8.18.1", + "esbuild": "^0.28.2", + "miniflare": "^3.20250718.3", + "openapi-typescript": "7.13.0", + "tsx": "^4", + "typescript": "^5.7", + "ws": "^8.21.1", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ws": ">=8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + } + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", + "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.1.tgz", + "integrity": "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "11.7.2", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.2" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.7.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz", + "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@asyncapi/avro-schema-parser": { + "version": "3.0.24", + "resolved": "https://registry.npmjs.org/@asyncapi/avro-schema-parser/-/avro-schema-parser-3.0.24.tgz", + "integrity": "sha512-YMyr2S2heMrWHRyECknjHeejlZl5exUSv9nD1gTejAT13fSf0PqIRydZ9ZuoglCLBg55AeehypR2zLIBu/9kHQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@asyncapi/parser": "^3.1.0", + "@types/json-schema": "^7.0.11", + "avsc": "^5.7.6" + } + }, + "node_modules/@asyncapi/modelina": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/@asyncapi/modelina/-/modelina-5.10.1.tgz", + "integrity": "sha512-mvk77+ls2ia+w3uQftJ7s6/Yid4lO+1IgbTkJ94mGSV9Qqk1n+ln5dz2snccARI5ubdy3ofKb3QP2Dq/OGeH8A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.1.0", + "@apidevtools/swagger-parser": "^10.1.0", + "@asyncapi/multi-parser": "^2.2.0", + "@asyncapi/parser": "^3.4.0", + "alterschema": "^1.1.2", + "change-case": "^4.1.2", + "fast-xml-parser": "^5.3.0", + "js-yaml": "^4.1.0", + "openapi-types": "^12.1.3", + "typescript-json-schema": "^0.58.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@asyncapi/modelina/node_modules/change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/@asyncapi/modelina/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@asyncapi/multi-parser": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@asyncapi/multi-parser/-/multi-parser-2.4.0.tgz", + "integrity": "sha512-odtze8N+nGDuzitYB4PlArsFBhEL601ahvR/NL0FVKXWOVz9FTO4IIwtLToJT0PuNaAZAwwVeE5mAmgERaXvOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@asyncapi/avro-schema-parser": "^3.0.3", + "@asyncapi/openapi-schema-parser": "^3.0.4", + "@asyncapi/parser": "*", + "@asyncapi/protobuf-schema-parser": "^3.8.2", + "parserapiv1": "npm:@asyncapi/parser@^2.1.0", + "parserapiv2": "npm:@asyncapi/parser@3.0.0-next-major-spec.8" + }, + "peerDependencies": { + "@asyncapi/raml-dt-schema-parser": "^4.0.4" + }, + "peerDependenciesMeta": { + "@asyncapi/raml-dt-schema-parser": { + "optional": true + } + } + }, + "node_modules/@asyncapi/openapi-schema-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@asyncapi/openapi-schema-parser/-/openapi-schema-parser-3.1.0.tgz", + "integrity": "sha512-YblYFErE6ixLTz+MNddzB/EW+EgRN5ubHM7LTZNGjUFYpyFOLWQVrV1ErrnCTZJ7Blr81GZmhpDpjpI8IfwiYQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@openapi-contrib/openapi-schema-to-json-schema": "~3.2.0", + "ajv": "^8.11.0", + "ajv-errors": "^3.0.0", + "ajv-formats": "^2.1.1" + }, + "peerDependencies": { + "@asyncapi/parser": "^3.6.2" + } + }, + "node_modules/@asyncapi/parser": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@asyncapi/parser/-/parser-3.6.3.tgz", + "integrity": "sha512-MUC8xIUMcS2qNvqrqyx/ie0txu3d/OdIsrXs7UCzawdyR6P07gh35DpOqPz/z57s1UA3vERVpcheZYl3h8cVtw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@asyncapi/specs": "^6.11.1", + "@openapi-contrib/openapi-schema-to-json-schema": "~3.2.0", + "@stoplight/json": "3.21.0", + "@stoplight/json-ref-readers": "^1.2.2", + "@stoplight/json-ref-resolver": "^3.1.5", + "@stoplight/spectral-core": "^1.18.3", + "@stoplight/spectral-functions": "^1.7.2", + "@stoplight/spectral-parsers": "^1.0.2", + "@stoplight/spectral-ref-resolver": "^1.0.3", + "@stoplight/types": "^13.12.0", + "@types/json-schema": "^7.0.11", + "@types/urijs": "^1.19.19", + "ajv": "^8.18.0", + "ajv-errors": "^3.0.0", + "ajv-formats": "^2.1.1", + "avsc": "^5.7.5", + "js-yaml": "^4.3.1", + "jsonpath-plus": "^10.0.7", + "node-fetch": "2.6.7" + } + }, + "node_modules/@asyncapi/parser/node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@asyncapi/protobuf-schema-parser": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/@asyncapi/protobuf-schema-parser/-/protobuf-schema-parser-3.8.3.tgz", + "integrity": "sha512-r2zro1a/vBz9X9vZIYABWSAdtQTth/vzFYol0dNbzcIe7wl+LrucCzGz8/G97wtGriryKOBe9/T6JYvXSCxI8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@asyncapi/parser": "^3.6.2", + "@types/protocol-buffers-schema": "^3.4.3", + "protobufjs": "^8.7.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@asyncapi/specs": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-6.11.1.tgz", + "integrity": "sha512-A3WBLqAKGoJ2+6FWFtpjBlCQ1oFCcs4GxF7zsIGvNqp/klGUHjlA3aAcZ9XMMpLGE8zPeYDz2x9FmO6DSuKraQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.11" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@changesets/apply-release-plan": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.1.1.tgz", + "integrity": "sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/config": "^3.1.4", + "@changesets/get-version-range-type": "^0.4.0", + "@changesets/git": "^3.0.4", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "detect-indent": "^6.0.0", + "fs-extra": "^7.0.1", + "lodash.startcase": "^4.4.0", + "outdent": "^0.5.0", + "prettier": "^2.7.1", + "resolve-from": "^5.0.0", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/assemble-release-plan": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.10.tgz", + "integrity": "sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.4", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/changelog-git": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.1.tgz", + "integrity": "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0" + } + }, + "node_modules/@changesets/cli": { + "version": "2.31.1", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.31.1.tgz", + "integrity": "sha512-uO05WTcRBwuVOJVSW8Cmpqw6q0WDL53ajGCMyszutvOe5toOnunbpM4jZzf+qxBOz7i0AzopZ8diBuewjmF40w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/apply-release-plan": "^7.1.1", + "@changesets/assemble-release-plan": "^6.0.10", + "@changesets/changelog-git": "^0.2.1", + "@changesets/config": "^3.1.4", + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.4", + "@changesets/get-release-plan": "^4.0.16", + "@changesets/git": "^3.0.4", + "@changesets/logger": "^0.1.1", + "@changesets/pre": "^2.0.2", + "@changesets/read": "^0.6.7", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@changesets/write": "^0.4.0", + "@inquirer/external-editor": "^1.0.2", + "@manypkg/get-packages": "^1.1.3", + "ansi-colors": "^4.1.3", + "enquirer": "^2.4.1", + "fs-extra": "^7.0.1", + "mri": "^1.2.0", + "package-manager-detector": "^0.2.0", + "picocolors": "^1.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.5.3", + "spawndamnit": "^3.0.1", + "term-size": "^2.1.0" + }, + "bin": { + "changeset": "bin.js" + } + }, + "node_modules/@changesets/config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.4.tgz", + "integrity": "sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.4", + "@changesets/logger": "^0.1.1", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1", + "micromatch": "^4.0.8" + } + }, + "node_modules/@changesets/errors": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz", + "integrity": "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==", + "dev": true, + "license": "MIT", + "dependencies": { + "extendable-error": "^0.1.5" + } + }, + "node_modules/@changesets/get-dependents-graph": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.4.tgz", + "integrity": "sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "picocolors": "^1.1.0", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/get-release-plan": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.16.tgz", + "integrity": "sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/assemble-release-plan": "^6.0.10", + "@changesets/config": "^3.1.4", + "@changesets/pre": "^2.0.2", + "@changesets/read": "^0.6.7", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3" + } + }, + "node_modules/@changesets/get-version-range-type": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz", + "integrity": "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@changesets/git": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-3.0.4.tgz", + "integrity": "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@manypkg/get-packages": "^1.1.3", + "is-subdir": "^1.1.1", + "micromatch": "^4.0.8", + "spawndamnit": "^3.0.1" + } + }, + "node_modules/@changesets/logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.1.tgz", + "integrity": "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.0" + } + }, + "node_modules/@changesets/parse": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.3.tgz", + "integrity": "sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "js-yaml": "^4.1.1" + } + }, + "node_modules/@changesets/pre": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-2.0.2.tgz", + "integrity": "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1" + } + }, + "node_modules/@changesets/read": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.7.tgz", + "integrity": "sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/git": "^3.0.4", + "@changesets/logger": "^0.1.1", + "@changesets/parse": "^0.4.3", + "@changesets/types": "^6.1.0", + "fs-extra": "^7.0.1", + "p-filter": "^2.1.0", + "picocolors": "^1.1.0" + } + }, + "node_modules/@changesets/should-skip-package": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-0.1.2.tgz", + "integrity": "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3" + } + }, + "node_modules/@changesets/types": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-6.1.0.tgz", + "integrity": "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@changesets/write": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.4.0.tgz", + "integrity": "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "fs-extra": "^7.0.1", + "human-id": "^4.1.1", + "prettier": "^2.7.1" + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250718.0.tgz", + "integrity": "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250718.0.tgz", + "integrity": "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250718.0.tgz", + "integrity": "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250718.0.tgz", + "integrity": "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250718.0.tgz", + "integrity": "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@hyperjump/json": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@hyperjump/json/-/json-0.1.0.tgz", + "integrity": "sha512-jWsAOHjweWhi0UEBCN57YZzyTt76Z6Fm/OJXOfNBJbEZt569AcTRsjv6Dqj5t4gQhW9td72oquiyaVp9oHbhBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hyperjump/json-pointer": "^0.9.2", + "moo": "^0.5.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jdesrosiers" + } + }, + "node_modules/@hyperjump/json-pointer": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@hyperjump/json-pointer/-/json-pointer-0.9.8.tgz", + "integrity": "sha512-6D6okhpH5VOS3oSYUtxu8nClsOcp59aC+sS06/tCxEta4T5Gk1yaycLiCkG8kE9eh+9AJUHsvQEJJrWBfLOjvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "just-curry-it": "^5.3.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jdesrosiers" + } + }, + "node_modules/@hyperjump/json-schema": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@hyperjump/json-schema/-/json-schema-0.23.5.tgz", + "integrity": "sha512-gb1jOT6+BlZBR9Nc/tMGDt757YM7rjS71Dml3+TBYebdGOZlSrTzTfVAUfGzOlsceB3gP4K9b7HzAwEGMWmexQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@hyperjump/json-schema-core": "^0.28.0", + "fastest-stable-stringify": "^2.0.2", + "just-curry-it": "^5.3.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jdesrosiers" + } + }, + "node_modules/@hyperjump/json-schema-core": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/@hyperjump/json-schema-core/-/json-schema-core-0.28.5.tgz", + "integrity": "sha512-+f5P3oHYCQru3s+Ha+E10rIyEvyK0Hfa2oj3+cDoGaVMbT4Jg5TgCoIM7B5rl3t3KRA7EOmrLjKFGeLi5yd1pg==", + "deprecated": "This package was rolled into @hyperjump/json-schema as of v1.0.0", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@hyperjump/json": "^0.1.0", + "@hyperjump/json-pointer": "^0.9.4", + "@hyperjump/pact": "^0.2.3", + "content-type": "^1.0.4", + "node-fetch": "^2.6.5", + "pubsub-js": "^1.9.4", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jdesrosiers" + } + }, + "node_modules/@hyperjump/pact": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@hyperjump/pact/-/pact-0.2.5.tgz", + "integrity": "sha512-93m7gLf40EI8svsKrdPc+KkLsngwX/2ld08xwc0PFioxJSxnfkx1BUHNJVjhG386UUYP6mNe+ZtmIiDXDJ4TQg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "just-curry-it": "^3.1.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jdesrosiers" + } + }, + "node_modules/@hyperjump/pact/node_modules/just-curry-it": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/just-curry-it/-/just-curry-it-3.2.1.tgz", + "integrity": "sha512-Q8206k8pTY7krW32cdmPsP+DqqLgWx/hYPSj9/+7SYqSqz7UuwPbfSe07lQtvuuaVyiSJveXk0E5RydOuWwsEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "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.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/ternary": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/ternary/-/ternary-1.1.4.tgz", + "integrity": "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@manypkg/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@types/node": "^12.7.1", + "find-up": "^4.1.0", + "fs-extra": "^8.1.0" + } + }, + "node_modules/@manypkg/find-root/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@manypkg/find-root/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@manypkg/get-packages": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz", + "integrity": "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@changesets/types": "^4.0.1", + "@manypkg/find-root": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "^11.0.0", + "read-yaml-file": "^1.1.0" + } + }, + "node_modules/@manypkg/get-packages/node_modules/@changesets/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz", + "integrity": "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@manypkg/get-packages/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@nodable/entities": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", + "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@openapi-contrib/openapi-schema-to-json-schema": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@openapi-contrib/openapi-schema-to-json-schema/-/openapi-schema-to-json-schema-3.2.0.tgz", + "integrity": "sha512-Gj6C0JwCr8arj0sYuslWXUBSP/KnUlEGnPW4qxlXvAl543oaNQgMgIgkQUA6vs5BCCvwTEiL8m/wdWzfl4UvSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/config": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", + "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.17", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.17.tgz", + "integrity": "sha512-wsV2keCt6B806XpSdezbWZ9aFJYf14YVh+XQf0ESt7M90yqVuxH9//PxvtC70sgj9OCkRM3nRaLfu4MsGQZRig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "8.11.2", + "@redocly/config": "0.22.0", + "colorette": "1.4.0", + "https-proxy-agent": "7.0.6", + "js-levenshtein": "1.1.6", + "js-yaml": "4.2.0", + "minimatch": "5.1.9", + "pluralize": "8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@stoplight/better-ajv-errors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", + "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": "^12.20 || >= 14.13" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@stoplight/json": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.21.0.tgz", + "integrity": "sha512-5O0apqJ/t4sIevXCO3SBN9AHCEKKR/Zb4gaj7wYe5863jme9g02Q0n/GhM7ZCALkL+vGPTe4ZzTETP8TFtsw3g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/ordered-object-literal": "^1.0.3", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^13.6.0", + "jsonc-parser": "~2.2.1", + "lodash": "^4.17.21", + "safe-stable-stringify": "^1.1" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json-ref-readers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@stoplight/json-ref-readers/-/json-ref-readers-1.2.2.tgz", + "integrity": "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-fetch": "^2.6.0", + "tslib": "^1.14.1" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json-ref-resolver": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.6.tgz", + "integrity": "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "^3.21.0", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^12.3.0 || ^13.0.0", + "@types/urijs": "^1.19.19", + "dependency-graph": "~0.11.0", + "fast-memoize": "^2.5.2", + "immer": "^9.0.6", + "lodash": "^4.17.21", + "tslib": "^2.6.0", + "urijs": "^1.19.11" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json-ref-resolver/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@stoplight/ordered-object-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.5.tgz", + "integrity": "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/path": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@stoplight/path/-/path-1.3.2.tgz", + "integrity": "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/spectral-core": { + "version": "1.23.1", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-core/-/spectral-core-1.23.1.tgz", + "integrity": "sha512-VLC8OhpO/pMJKb6IHhurxJjXO1qB56Ng1unIb8b+hNxdw0+SEcASvmR+RpjfHYX/jv/DfSaA1x8QhFBJBmqBOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "^1.4.0", + "@stoplight/better-ajv-errors": "1.0.3", + "@stoplight/json": "~3.21.0", + "@stoplight/path": "1.3.2", + "@stoplight/spectral-parsers": "^1.0.0", + "@stoplight/spectral-ref-resolver": "^1.0.4", + "@stoplight/spectral-runtime": "^1.1.2", + "@stoplight/types": "~13.6.0", + "@types/es-aggregate-error": "^1.0.2", + "@types/json-schema": "^7.0.11", + "ajv": "^8.18.0", + "ajv-errors": "~3.0.0", + "ajv-formats": "~2.1.1", + "es-aggregate-error": "^1.0.7", + "expr-eval-fork": "^3.0.1", + "jsonpath-plus": "^10.3.0", + "lodash": "^4.18.1", + "lodash.topath": "^4.5.2", + "minimatch": "^3.1.4", + "nimma": "0.2.3", + "pony-cause": "^1.1.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/@stoplight/types": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.6.0.tgz", + "integrity": "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@stoplight/spectral-core/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@stoplight/spectral-formats": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-formats/-/spectral-formats-1.8.5.tgz", + "integrity": "sha512-xaC0rCH0p7/bzNJsz+JgLSj+Cp6uwYGWpePQxdLkF2G6a8Zyp3OyS7umkGYNiimEwKrOjvCNNTFJpeuiENZSBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "^1.4.0", + "@stoplight/json": "^3.17.0", + "@stoplight/spectral-core": "^1.23.0", + "@types/json-schema": "^7.0.7", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-formats/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@stoplight/spectral-functions": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-functions/-/spectral-functions-1.10.5.tgz", + "integrity": "sha512-vDCd0NJ93715bcUpZZ5vNHiyxd4cgHF6tuXsDiXOXKAByg+I1fR5/dMijEo6Ce1Lz95a+RZ22JKYhF1YuzVvuA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "^1.4.0", + "@stoplight/better-ajv-errors": "1.0.3", + "@stoplight/json": "^3.17.1", + "@stoplight/spectral-core": "^1.23.0", + "@stoplight/spectral-formats": "^1.8.1", + "@stoplight/spectral-runtime": "^1.1.2", + "ajv": "^8.18.0", + "ajv-draft-04": "~1.0.0", + "ajv-errors": "~3.0.0", + "ajv-formats": "~2.1.1", + "lodash": "^4.18.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-functions/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@stoplight/spectral-parsers": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-parsers/-/spectral-parsers-1.0.5.tgz", + "integrity": "sha512-ANDTp2IHWGvsQDAY85/jQi9ZrF4mRrA5bciNHX+PUxPr4DwS6iv4h+FVWJMVwcEYdpyoIdyL+SRmHdJfQEPmwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "~3.21.0", + "@stoplight/types": "^14.1.1", + "@stoplight/yaml": "~4.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-parsers/node_modules/@stoplight/types": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", + "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/spectral-parsers/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@stoplight/spectral-ref-resolver": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-ref-resolver/-/spectral-ref-resolver-1.0.5.tgz", + "integrity": "sha512-gj3TieX5a9zMW29z3mBlAtDOCgN3GEc1VgZnCVlr5irmR4Qi5LuECuFItAq4pTn5Zu+sW5bqutsCH7D4PkpyAA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json-ref-readers": "1.2.2", + "@stoplight/json-ref-resolver": "~3.1.6", + "@stoplight/spectral-runtime": "^1.1.2", + "dependency-graph": "0.11.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-ref-resolver/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@stoplight/spectral-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-runtime/-/spectral-runtime-1.1.6.tgz", + "integrity": "sha512-Y8rEDyMN4bSMJCrDs2shdcVHYyCnH3FvXRP4dBhha4Z8iJv+JPp7KqOV/hwVB/hWFC209upiwj2oDmLfR0qCDg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/json": "^3.20.1", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^13.6.0", + "lodash": "^4.18.1", + "node-fetch": "^2.7.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^16.20 || ^18.18 || >= 20.17" + } + }, + "node_modules/@stoplight/spectral-runtime/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@stoplight/spectral-runtime/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@stoplight/types": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.20.0.tgz", + "integrity": "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.3.0.tgz", + "integrity": "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/ordered-object-literal": "^1.0.5", + "@stoplight/types": "^14.1.1", + "@stoplight/yaml-ast-parser": "0.0.50", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=10.8" + } + }, + "node_modules/@stoplight/yaml-ast-parser": { + "version": "0.0.50", + "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.50.tgz", + "integrity": "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@stoplight/yaml/node_modules/@stoplight/types": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", + "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, + "node_modules/@stoplight/yaml/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/es-aggregate-error": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.6.tgz", + "integrity": "sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/protocol-buffers-schema": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/protocol-buffers-schema/-/protocol-buffers-schema-3.4.3.tgz", + "integrity": "sha512-8cCg6BiIj4jS0LXUFq3sndmd46yyPLYqMzvXLcTM1MRubh3sfZlQiehoCjGDxSHTqGSjjx8EtVNryIAl0njQWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/urijs": { + "version": "1.19.26", + "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.26.tgz", + "integrity": "sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", + "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.0.1" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/alterschema": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/alterschema/-/alterschema-1.1.3.tgz", + "integrity": "sha512-VqKTk8lX8LHVRvSOgEZDGPeEYOvrSOjlX/1PAi4el7ac8acC6/6a99HuVjfU6N1tNrHV5dU0sQDmuOjRvBf/Sw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@hyperjump/json-schema": "^0.23.5", + "json-e": "^4.4.3", + "lodash": "^4.17.21", + "object-hash": "^3.0.0" + }, + "bin": { + "alterschema": "bindings/node/cli.js" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/as-table": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", + "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "printable-characters": "^1.0.42" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "dev": true, + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/avsc": { + "version": "5.7.9", + "resolved": "https://registry.npmjs.org/avsc/-/avsc-5.7.9.tgz", + "integrity": "sha512-yOA4wFeI7ET3v32Di/sUybQ+ttP20JHSW3mxLuNGeO0uD6PPcvLrIQXSvy/rhJOWU5JrYh7U4OHplWMmtAtjMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.11" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/better-path-resolve": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz", + "integrity": "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-windows": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camel-case/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/capital-case/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + } + }, + "node_modules/constant-case/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-case/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-aggregate-error": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.14.tgz", + "integrity": "sha512-3YxX6rVb07B5TV11AV5wsL7nQCHXNwoHPsQC8S4AmBiqYhyNCJ5BRKXkXyDJvs8QzXN20NgRtxe3dEEQD9NLHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "globalthis": "^1.0.4", + "has-property-descriptors": "^1.0.2", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expr-eval-fork": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/expr-eval-fork/-/expr-eval-fork-3.0.3.tgz", + "integrity": "sha512-BhC+hbc5lIVjygr840n5DEkW3MQq7H9o+mc1/N7Z5uIiCFVyESLL5DIE7LNq4CYUNxy+XjA+3jRrL/h0Kt2xcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/extendable-error": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", + "integrity": "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-memoize": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", + "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-builder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz", + "integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz", + "integrity": "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^3.0.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.1", + "xml-naming": "^0.3.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastest-stable-stringify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", + "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-source": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", + "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "data-uri-to-buffer": "^2.0.0", + "source-map": "^0.6.1" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/header-case/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-id": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.2.0.tgz", + "integrity": "sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==", + "dev": true, + "license": "MIT", + "bin": { + "human-id": "dist/cli.js" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-subdir": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz", + "integrity": "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "better-path-resolve": "1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unsafe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz", + "integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/json-e": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/json-e/-/json-e-4.8.2.tgz", + "integrity": "sha512-0EzcoDOkNdiAG66q9d1FGHaWJbFy6Sz2Tf9U4kXyV+nMEbA3NUnpiQBdJm3qTqa9js7fATq35/fRYT+RHZcvNw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "json-stable-stringify-without-jsonify": "^1.0.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/json-schema-migrate": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json-schema-migrate/-/json-schema-migrate-0.2.0.tgz", + "integrity": "sha512-dq4/oHWmtw/+0ytnXsDqVn+VsVweTEmzm5jLgguPn9BjSzn6/q58ZiZx3BHiQyJs612f0T5Z+MrUEUUY5DHsRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^5.0.0" + } + }, + "node_modules/json-schema-migrate/node_modules/ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha512-Ajr4IcMXq/2QmMkEmSvxqfLN5zGmJ92gHXAeOXq1OekoH2rfDNsgdDoL2f7QaRCy7G/E6TpxBVdRuNraMztGHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "node_modules/json-schema-migrate/node_modules/fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha512-fueX787WZKCV0Is4/T2cyAdM4+x1S3MXXOAhavE1ys/W42SHAPacLTQhucja22QBYrfGw50M2sRiXPtTGv9Ymw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-migrate/node_modules/json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha512-4JD/Ivzg7PoW8NzdrBSr3UFwC9mHgvI7Z6z3QGBsSHgKaRTUDmyZAAKJo2UbG1kUVfS9WS8bi36N49U1xw43DA==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonc-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz", + "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpath-plus": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.4.0.tgz", + "integrity": "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.3.0", + "@jsep-plugin/regex": "^1.0.4", + "jsep": "^1.4.0" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/just-curry-it": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/just-curry-it/-/just-curry-it-5.3.0.tgz", + "integrity": "sha512-silMIRiFjUWlfaDhkgSzpuAyQ6EX/o09Eu8ZBfmFwQMbax7+LQzeIU2CBrICT6Ne4l86ITCGvUCBpCubWYy0Yw==", + "dev": true, + "license": "MIT" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.topath": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", + "integrity": "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lower-case/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/miniflare": { + "version": "3.20250718.3", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20250718.3.tgz", + "integrity": "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "8.14.0", + "acorn-walk": "8.3.2", + "exit-hook": "2.2.1", + "glob-to-regexp": "0.4.1", + "stoppable": "1.1.0", + "undici": "^5.28.5", + "workerd": "1.20250718.0", + "ws": "8.18.0", + "youch": "3.3.4", + "zod": "3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/miniflare/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/moo": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.3.tgz", + "integrity": "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/nimma": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/nimma/-/nimma-0.2.3.tgz", + "integrity": "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsep-plugin/regex": "^1.0.1", + "@jsep-plugin/ternary": "^1.0.2", + "astring": "^1.8.1", + "jsep": "^1.2.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + }, + "optionalDependencies": { + "jsonpath-plus": "^6.0.1 || ^10.1.0", + "lodash.topath": "^4.5.2" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/no-case/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/openapi-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz", + "integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.34.6", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.2.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/outdent": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz", + "integrity": "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-filter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", + "integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-map": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-manager-detector": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz", + "integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^0.2.7" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/param-case/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parserapiv1": { + "name": "@asyncapi/parser", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@asyncapi/parser/-/parser-2.1.2.tgz", + "integrity": "sha512-2pHKnr2P8EujcrvZo4x4zNwsEIAg5vb1ZEhl2+OH0YBg8EYH/Xx73XZ+bbwLaYIg1gvFjm29jNB9UL3CMeDU5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@asyncapi/specs": "^5.1.0", + "@openapi-contrib/openapi-schema-to-json-schema": "~3.2.0", + "@stoplight/json": "^3.20.2", + "@stoplight/json-ref-readers": "^1.2.2", + "@stoplight/json-ref-resolver": "^3.1.5", + "@stoplight/spectral-core": "^1.16.1", + "@stoplight/spectral-functions": "^1.7.2", + "@stoplight/spectral-parsers": "^1.0.2", + "@stoplight/spectral-ref-resolver": "^1.0.3", + "@stoplight/types": "^13.12.0", + "@types/json-schema": "^7.0.11", + "@types/urijs": "^1.19.19", + "ajv": "^8.11.0", + "ajv-errors": "^3.0.0", + "ajv-formats": "^2.1.1", + "avsc": "^5.7.5", + "js-yaml": "^4.1.0", + "jsonpath-plus": "^7.2.0", + "node-fetch": "2.6.7" + } + }, + "node_modules/parserapiv1/node_modules/@asyncapi/specs": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-5.1.0.tgz", + "integrity": "sha512-yffhETqehkim43luMnPKOwzY0D0YtU4bKpORIXIaid6p5Y5kDLrMGJaEPkNieQp03HMjhjFrnUPtT8kvqe0+aQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.11" + } + }, + "node_modules/parserapiv1/node_modules/jsonpath-plus": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", + "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/parserapiv2": { + "name": "@asyncapi/parser", + "version": "3.0.0-next-major-spec.8", + "resolved": "https://registry.npmjs.org/@asyncapi/parser/-/parser-3.0.0-next-major-spec.8.tgz", + "integrity": "sha512-d8ebYM08BCsx3Q4AeLke6naU/NrcAXFEVpS6b3EWcKRdUDce+v0X5k9aDH+YXWCaQApEF28UzcxhlSOJvhIFgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@asyncapi/specs": "^6.0.0-next-major-spec.9", + "@openapi-contrib/openapi-schema-to-json-schema": "~3.2.0", + "@stoplight/json-ref-resolver": "^3.1.5", + "@stoplight/spectral-core": "^1.16.1", + "@stoplight/spectral-functions": "^1.7.2", + "@stoplight/spectral-parsers": "^1.0.2", + "@types/json-schema": "^7.0.11", + "@types/urijs": "^1.19.19", + "ajv": "^8.11.0", + "ajv-errors": "^3.0.0", + "ajv-formats": "^2.1.1", + "avsc": "^5.7.5", + "js-yaml": "^4.1.0", + "jsonpath-plus": "^7.2.0", + "node-fetch": "2.6.7", + "ramldt2jsonschema": "^1.2.3", + "webapi-parser": "^0.5.0" + } + }, + "node_modules/parserapiv2/node_modules/jsonpath-plus": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", + "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/pascal-case/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-case/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/path-equal": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/path-equal/-/path-equal-1.2.5.tgz", + "integrity": "sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pony-cause": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz", + "integrity": "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g==", + "dev": true, + "license": "0BSD", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/protobufjs": { + "version": "8.7.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.7.2.tgz", + "integrity": "sha512-oTVHV+oelUBtiu5iTuTNNZ0eLYsXSMxry4cgr30mayNkgIZL6qZ0IOQVPuSWGcyAaXKl/XgqwWHIC3a0khYVBA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/pubsub-js": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.9.5.tgz", + "integrity": "sha512-5MZ0I9i5JWVO7SizvOviKvZU2qaBbl2KQX150FAA+fJBwYpwOUId7aNygURWSdPzlsA/xZ/InUKXqBbzM0czTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/ramldt2jsonschema": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/ramldt2jsonschema/-/ramldt2jsonschema-1.2.3.tgz", + "integrity": "sha512-+wLDAV2NNv9NkfEUOYStaDu/6RYgYXeC1zLtXE+dMU/jDfjpN4iJnBGycDwFTFaIQGosOQhxph7fEX6Mpwxdug==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "commander": "^5.0.0", + "js-yaml": "^3.14.0", + "json-schema-migrate": "^0.2.0", + "webapi-parser": "^0.5.0" + }, + "bin": { + "dt2js": "bin/dt2js.js", + "js2dt": "bin/js2dt.js" + } + }, + "node_modules/ramldt2jsonschema/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/ramldt2jsonschema/node_modules/js-yaml": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/read-yaml-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz", + "integrity": "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.5", + "js-yaml": "^3.6.1", + "pify": "^4.0.1", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/read-yaml-file/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/read-yaml-file/node_modules/js-yaml": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz", + "integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/sentence-case/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/snake-case/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawndamnit": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spawndamnit/-/spawndamnit-3.0.1.tgz", + "integrity": "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "cross-spawn": "^7.0.5", + "signal-exit": "^4.0.1" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stacktracey": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.2.0.tgz", + "integrity": "sha512-ETyQEz+CzXiLjEbyJqpbp+/T79RQD/6wqFucRBIlVNZfYq2Ay7wbretD4cxpbymZlaPWx58aIhPEY1Cr8DlVvg==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "as-table": "^1.0.36", + "get-source": "^2.0.12" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strnum": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz", + "integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/typescript-json-schema": { + "version": "0.58.1", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.58.1.tgz", + "integrity": "sha512-EcmquhfGEmEJOAezLZC6CzY0rPNzfXuky+Z3zoXULEEncW8e13aAjmC2r8ppT1bvvDekJj1TJ4xVhOdkjYtkUA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@types/json-schema": "^7.0.9", + "@types/node": "^16.9.2", + "glob": "^7.1.7", + "path-equal": "^1.2.5", + "safe-stable-stringify": "^2.2.0", + "ts-node": "^10.9.1", + "typescript": "~4.9.5", + "yargs": "^17.1.1" + }, + "bin": { + "typescript-json-schema": "bin/typescript-json-schema" + } + }, + "node_modules/typescript-json-schema/node_modules/@types/node": { + "version": "16.18.126", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", + "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript-json-schema/node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/typescript-json-schema/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/upper-case-first/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/upper-case/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/webapi-parser": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/webapi-parser/-/webapi-parser-0.5.0.tgz", + "integrity": "sha512-fPt6XuMqLSvBz8exwX4QE1UT+pROLHa00EMDCdO0ybICduwQ1V4f7AWX4pNOpCp+x+0FjczEsOxtQU0d8L3QKw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ajv": "6.5.2" + } + }, + "node_modules/webapi-parser/node_modules/ajv": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.2.tgz", + "integrity": "sha512-hOs7GfvI6tUI1LfZddH82ky6mOMyTuY0mk7kE2pWpmhhUSkumzaTO5vbVwij39MdwPQWCV4Zv57Eo06NtL/GVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.1" + } + }, + "node_modules/webapi-parser/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "dev": true, + "license": "MIT" + }, + "node_modules/webapi-parser/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/workerd": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250718.0.tgz", + "integrity": "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20250718.0", + "@cloudflare/workerd-darwin-arm64": "1.20250718.0", + "@cloudflare/workerd-linux-64": "1.20250718.0", + "@cloudflare/workerd-linux-arm64": "1.20250718.0", + "@cloudflare/workerd-windows-64": "1.20250718.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/youch": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", + "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie": "^0.7.1", + "mustache": "^4.2.0", + "stacktracey": "^2.1.8" + } + }, + "node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/packages/sdk-typescript/package.json b/packages/sdk-typescript/package.json new file mode 100644 index 0000000..ba25809 --- /dev/null +++ b/packages/sdk-typescript/package.json @@ -0,0 +1,84 @@ +{ + "name": "@gemini-markets/sdk", + "version": "0.0.0", + "description": "Gemini exchange TypeScript SDK — browser and server entry points with HMAC, OAuth (PKCE), REST, and WebSocket support.", + "type": "module", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/gemini/developer-platform.git", + "directory": "packages/sdk-typescript" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "engines": { + "node": ">=18" + }, + "sideEffects": false, + "exports": { + "./browser": { + "types": "./dist/browser/index.d.ts", + "import": "./dist/browser/index.js" + }, + "./server": { + "types": "./dist/server/index.d.ts", + "import": "./dist/server/index.js" + } + }, + "files": [ + "dist/" + ], + "scripts": { + "build": "tsc", + "typecheck": "tsc -p tsconfig.typecheck.json", + "test": "tsx --test \"src/**/*.test.ts\" \"scripts/**/*.test.mjs\"", + "smoke:sandbox": "npm run build && GEMINI_SMOKE_ENV=sandbox node --env-file=.env.sandbox scripts/smoke-sandbox.mjs --market-type=market-data", + "smoke:sandbox:market-data": "npm run build && GEMINI_SMOKE_ENV=sandbox node --env-file=.env.sandbox scripts/smoke-sandbox.mjs --market-type=market-data", + "smoke:sandbox:prediction-markets": "npm run build && GEMINI_SMOKE_ENV=sandbox node --env-file=.env.sandbox scripts/smoke-sandbox.mjs --market-type=prediction-markets", + "smoke:sandbox:oauth": "npm run build && GEMINI_OAUTH_ENV=sandbox node --env-file=.env.sandbox scripts/smoke-oauth.mjs", + "smoke:qa": "npm run build && node --use-system-ca --env-file=.env.qa --import ./scripts/qa-bootstrap.mjs scripts/smoke-sandbox.mjs --market-type=market-data", + "smoke:qa:market-data": "npm run build && node --use-system-ca --env-file=.env.qa --import ./scripts/qa-bootstrap.mjs scripts/smoke-sandbox.mjs --market-type=market-data", + "smoke:qa:prediction-markets": "npm run build && node --use-system-ca --env-file=.env.qa --import ./scripts/qa-bootstrap.mjs scripts/smoke-sandbox.mjs --market-type=prediction-markets", + "smoke:oauth": "npm run build && GEMINI_OAUTH_ENV=sandbox node --env-file=.env.sandbox scripts/smoke-oauth.mjs", + "verify:sandbox:rest": "npm run build && GEMINI_SMOKE_ENV=sandbox node --env-file=.env.sandbox scripts/verify-sandbox-rest.mjs", + "verify:sandbox:market-data": "npm run build && GEMINI_MD_ENV=sandbox node --env-file=.env.sandbox scripts/verify-market-data-live.mjs", + "smoke:prod:market-data": "npm run build && GEMINI_SMOKE_ENV=production node --env-file=.env scripts/smoke-sandbox.mjs --market-type=market-data", + "smoke:prod:prediction-markets": "npm run build && GEMINI_SMOKE_ENV=production node --env-file=.env scripts/smoke-sandbox.mjs --market-type=prediction-markets", + "smoke:prod:oauth": "npm run build && GEMINI_OAUTH_ENV=production node --env-file=.env scripts/smoke-oauth.mjs", + "verify:prod:rest": "npm run build && GEMINI_SMOKE_ENV=production node --env-file=.env scripts/verify-sandbox-rest.mjs", + "verify:prod:market-data": "npm run build && GEMINI_MD_ENV=production node --env-file=.env scripts/verify-market-data-live.mjs", + "verify:browser": "npm run build && node scripts/verify-browser-imports.mjs", + "verify:bundle": "npm run build && node scripts/verify-browser-bundle.mjs", + "verify:package": "npm run build && node scripts/verify-browser-imports.mjs && node scripts/verify-browser-bundle.mjs && node scripts/verify-package.mjs", + "verify:changeset": "node scripts/verify-changeset.mjs", + "verify:market-data:live": "npm run build && node scripts/verify-market-data-live.mjs", + "verify:market-data:qa": "npm run build && node --use-system-ca --env-file=.env.qa --import ./scripts/qa-bootstrap.mjs scripts/verify-market-data-live.mjs", + "regenerate:websocket-types": "node scripts/generate-ws-types.mjs src/generated/websocket", + "regenerate": "node scripts/generate-prediction-markets.mjs && node scripts/generate-rest-modules.mjs && node scripts/generate-rest-ownership.mjs && npm run regenerate:websocket-types", + "dev": "tsx", + "prepublishOnly": "npm run build && npm test" + }, + "devDependencies": { + "@asyncapi/modelina": "5.10.1", + "@changesets/cli": "^2.31.1", + "@types/node": "^22", + "@types/ws": "^8.18.1", + "esbuild": "^0.28.2", + "miniflare": "^3.20250718.3", + "openapi-typescript": "7.13.0", + "tsx": "^4", + "typescript": "^5.7", + "ws": "^8.21.1", + "yaml": "2.9.0" + }, + "peerDependencies": { + "ws": ">=8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + } + } +} diff --git a/packages/sdk-typescript/scripts/generate-market-data.mjs b/packages/sdk-typescript/scripts/generate-market-data.mjs new file mode 100644 index 0000000..973d86d --- /dev/null +++ b/packages/sdk-typescript/scripts/generate-market-data.mjs @@ -0,0 +1,58 @@ +import { existsSync } from "node:fs"; +import { writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + discoverOperationInventory, + generateOpenApiRestTypes, + loadOpenApiDocument, + renderRestClient, +} from "./openapi-rest-generator.mjs"; +import { ownedOperationsForModule } from "./rest-operation-ownership.mjs"; + +const PUBLISHED_SPEC_URL = "https://developer.gemini.com/specs/openapi/rest.yaml"; +const BANNER = "// Generated from rest.yaml#Market Data. Do not edit.\n\n"; +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const localSpec = resolve(scriptDir, "../../../apis/rest.yaml"); +const specPath = process.argv[2] ?? (existsSync(localSpec) ? localSpec : PUBLISHED_SPEC_URL); +const outputDir = resolve(process.argv[3] ?? resolve(scriptDir, "../src/generated/market-data")); + +const document = await loadOpenApiDocument(specPath); +const ownedOperations = ownedOperationsForModule( + discoverOperationInventory(document, { spec: "rest" }), + { module: "marketData", spec: "rest" }, +); +const { operations } = await generateOpenApiRestTypes({ + specPath, + outputDir, + banner: BANNER, + includeOperationIds: ownedOperations.map((operation) => operation.operationId), + operationResponseModes: Object.fromEntries(ownedOperations.map((operation) => [ + operation.operationId, + operation.responseMode, + ])), + fileResponseImportPath: "../../core/http.js", + operationsConstName: "MARKET_DATA_OPERATIONS", + operationIdTypeName: "MarketDataOperationId", + operationTypesName: "MarketDataOperationTypes", + operationNamespace: "marketData", +}); +const methodNames = new Map(ownedOperations.map((operation) => [operation.operationId, operation.methodName])); + +await writeFile(resolve(outputDir, "rest.ts"), renderRestClient( + operations.map((operation) => ({ + ...operation, + methodName: methodNames.get(operation.operationId), + })), + { + banner: BANNER, + className: "MarketDataRest", + operationsConstName: "MARKET_DATA_OPERATIONS", + operationTypesName: "MarketDataOperationTypes", + operationsImportPath: "./operations.js", + transportImportPath: "../../core/http.js", + executorImportPath: "../../core/rest-operation.js", + deadlineImportPath: "../../core/deadline.js", + }, +)); diff --git a/packages/sdk-typescript/scripts/generate-prediction-markets.mjs b/packages/sdk-typescript/scripts/generate-prediction-markets.mjs new file mode 100644 index 0000000..91c4790 --- /dev/null +++ b/packages/sdk-typescript/scripts/generate-prediction-markets.mjs @@ -0,0 +1,60 @@ +import { existsSync } from "node:fs"; +import { writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + discoverOperationInventory, + generateOpenApiRestTypes, + loadOpenApiDocument, + renderRestClient, +} from "./openapi-rest-generator.mjs"; +import { ownedOperationsForModule } from "./rest-operation-ownership.mjs"; + +const PUBLISHED_SPEC_URL = "https://developer.gemini.com/specs/openapi/prediction-markets.yaml"; +const BANNER = "// Generated from prediction-markets.yaml. Do not edit.\n\n"; +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const localSpec = resolve(scriptDir, "../../../apis/prediction-markets.yaml"); +const specPath = process.argv[2] ?? (existsSync(localSpec) ? localSpec : PUBLISHED_SPEC_URL); +const outputDir = resolve(process.argv[3] ?? resolve(scriptDir, "../src/generated")); + +const document = await loadOpenApiDocument(specPath); +const inventory = discoverOperationInventory(document, { spec: "predictionMarkets" }); +const ownedOperations = ownedOperationsForModule( + inventory, + { module: "predictionMarkets", spec: "predictionMarkets" }, +); +const { operations } = await generateOpenApiRestTypes({ + specPath, + outputDir, + banner: BANNER, + includeOperationIds: ownedOperations.length > 0 + ? ownedOperations.map((operation) => operation.operationId) + : undefined, + operationResponseModes: Object.fromEntries(ownedOperations.map((operation) => [ + operation.operationId, + operation.responseMode, + ])), + operationsConstName: "PREDICTION_MARKET_OPERATIONS", + operationIdTypeName: "PredictionMarketOperationId", + operationTypesName: "PredictionMarketOperationTypes", + operationNamespace: "predictionMarkets", +}); +const methodNames = new Map(ownedOperations.map((operation) => [operation.operationId, operation.methodName])); + +await writeFile(resolve(outputDir, "rest.ts"), renderRestClient( + operations.map((operation) => ({ + ...operation, + methodName: methodNames.get(operation.operationId), + })), + { + banner: BANNER, + className: "PredictionMarketsRest", + operationsConstName: "PREDICTION_MARKET_OPERATIONS", + operationTypesName: "PredictionMarketOperationTypes", + operationsImportPath: "./operations.js", + transportImportPath: "../core/http.js", + executorImportPath: "../core/rest-operation.js", + deadlineImportPath: "../core/deadline.js", + }, +)); diff --git a/packages/sdk-typescript/scripts/generate-rest-modules.mjs b/packages/sdk-typescript/scripts/generate-rest-modules.mjs new file mode 100644 index 0000000..b298c4f --- /dev/null +++ b/packages/sdk-typescript/scripts/generate-rest-modules.mjs @@ -0,0 +1,120 @@ +import { existsSync } from "node:fs"; +import { writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + discoverOperationInventory, + generateOpenApiRestTypes, + loadOpenApiDocument, + renderRestClient, +} from "./openapi-rest-generator.mjs"; +import { ownedOperationsForModule } from "./rest-operation-ownership.mjs"; + +const PUBLISHED_SPEC_URL = "https://developer.gemini.com/specs/openapi/rest.yaml"; +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const localSpec = resolve(scriptDir, "../../../apis/rest.yaml"); +const specPath = process.argv[2] + ?? (existsSync(localSpec) ? localSpec : PUBLISHED_SPEC_URL); +const baseOutputDir = resolve(process.argv[3] ?? resolve(scriptDir, "../src/generated")); + +const modules = [ + { + module: "marketData", + output: "market-data", + banner: "// Generated from rest.yaml#Market Data. Do not edit.\n\n", + className: "MarketDataRest", + constName: "MARKET_DATA_OPERATIONS", + operationIdTypeName: "MarketDataOperationId", + operationTypesName: "MarketDataOperationTypes", + writeModels: true, + }, + { + module: "trading", + output: "trading", + banner: "// Generated from rest.yaml#Trading. Do not edit.\n\n", + className: "TradingRest", + constName: "TRADING_OPERATIONS", + operationIdTypeName: "TradingOperationId", + operationTypesName: "TradingOperationTypes", + }, + { + module: "margin", + output: "margin", + banner: "// Generated from rest.yaml#Margin. Do not edit.\n\n", + className: "MarginRest", + constName: "MARGIN_OPERATIONS", + operationIdTypeName: "MarginOperationId", + operationTypesName: "MarginOperationTypes", + }, + { + module: "perpetuals", + output: "perpetuals", + banner: "// Generated from rest.yaml#Perpetuals. Do not edit.\n\n", + className: "PerpetualsRest", + constName: "PERPETUALS_OPERATIONS", + operationIdTypeName: "PerpetualsOperationId", + operationTypesName: "PerpetualsOperationTypes", + }, + { + module: "accountServices", + output: "account-services", + banner: "// Generated from rest.yaml#Account Services. Do not edit.\n\n", + className: "AccountServicesRest", + constName: "ACCOUNT_SERVICES_OPERATIONS", + operationIdTypeName: "AccountServicesOperationId", + operationTypesName: "AccountServicesOperationTypes", + }, + { + module: "clearingInstant", + output: "clearing-instant", + banner: "// Generated from rest.yaml#Clearing & Instant. Do not edit.\n\n", + className: "ClearingInstantRest", + constName: "CLEARING_INSTANT_OPERATIONS", + operationIdTypeName: "ClearingInstantOperationId", + operationTypesName: "ClearingInstantOperationTypes", + }, +]; + +const document = await loadOpenApiDocument(specPath); +const inventory = discoverOperationInventory(document, { spec: "rest" }); + +for (const config of modules) { + const outputDir = resolve(baseOutputDir, config.output); + const ownedOperations = ownedOperationsForModule(inventory, { module: config.module, spec: "rest" }); + const { operations } = await generateOpenApiRestTypes({ + specPath, + outputDir, + banner: config.banner, + includeOperationIds: ownedOperations.map((operation) => operation.operationId), + operationResponseModes: Object.fromEntries(ownedOperations.map((operation) => [ + operation.operationId, + operation.responseMode, + ])), + fileResponseImportPath: "../../core/http.js", + modelsImportPath: config.writeModels ? "./models.js" : "../market-data/models.js", + writeModels: config.writeModels === true, + operationsConstName: config.constName, + operationIdTypeName: config.operationIdTypeName, + operationTypesName: config.operationTypesName, + operationNamespace: config.module, + }); + const methodNames = new Map(ownedOperations.map((operation) => [operation.operationId, operation.methodName])); + + await writeFile(resolve(outputDir, "rest.ts"), renderRestClient( + operations.map((operation) => ({ + ...operation, + methodName: methodNames.get(operation.operationId), + })), + { + banner: config.banner, + className: config.className, + operationsConstName: config.constName, + operationTypesName: config.operationTypesName, + operationsImportPath: "./operations.js", + transportImportPath: "../../core/http.js", + executorImportPath: "../../core/rest-operation.js", + deadlineImportPath: "../../core/deadline.js", + }, + )); +} diff --git a/packages/sdk-typescript/scripts/generate-rest-ownership.mjs b/packages/sdk-typescript/scripts/generate-rest-ownership.mjs new file mode 100644 index 0000000..6e76382 --- /dev/null +++ b/packages/sdk-typescript/scripts/generate-rest-ownership.mjs @@ -0,0 +1,19 @@ +import { existsSync } from "node:fs"; +import { writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { discoverOperationInventory, loadOpenApiDocument } from "./openapi-rest-generator.mjs"; +import { createRestOperationOwnershipReport } from "./rest-operation-ownership.mjs"; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const localPm = resolve(scriptDir, "../../../apis/prediction-markets.yaml"); +const localRest = resolve(scriptDir, "../../../apis/rest.yaml"); +const specs = [ + ["predictionMarkets", existsSync(localPm) ? localPm : "https://developer.gemini.com/specs/openapi/prediction-markets.yaml"], + ["rest", existsSync(localRest) ? localRest : "https://developer.gemini.com/specs/openapi/rest.yaml"], +]; +const operations = (await Promise.all(specs.map(async ([spec, specPath]) => + discoverOperationInventory(await loadOpenApiDocument(specPath), { spec })))).flat(); +const snapshot = createRestOperationOwnershipReport(operations); +await writeFile(resolve(scriptDir, "rest-operation-ownership.snapshot.json"), `${JSON.stringify(snapshot, null, 2)}\n`); diff --git a/packages/sdk-typescript/scripts/generate-ws-types.mjs b/packages/sdk-typescript/scripts/generate-ws-types.mjs new file mode 100644 index 0000000..12f05a6 --- /dev/null +++ b/packages/sdk-typescript/scripts/generate-ws-types.mjs @@ -0,0 +1,132 @@ +/* global console, process */ + +// Generates TypeScript types for the WebSocket API. +// +// Usage: node scripts/generate-ws-types.mjs [outputDir] [specPath] +// outputDir defaults to src/generated/websocket +// specPath defaults to fetching from https://developer.gemini.com/specs/asyncapi/websocket.yaml +// +// Why a script instead of `asyncapi generate models`: the Gemini WS protocol +// uses case-distinct single-letter keys (e vs E, u vs U). Modelina's default +// camelCase naming convention lowercases them, collapsing e/E and u/U into one +// property and silently dropping a field. We override the property-key naming +// formatter to identity so keys are emitted verbatim. The CLI can't pass that +// override, so we drive Modelina as a library here. + +import { + TypeScriptGenerator, + typeScriptDefaultPropertyKeyConstraints, +} from "@asyncapi/modelina"; +import { parse } from "yaml"; +import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve, join } from "node:path"; + +const PUBLISHED_SPEC_URL = "https://developer.gemini.com/specs/asyncapi/websocket.yaml"; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = resolve(here, ".."); +const outDirs = [process.argv[2] ?? join(root, "src", "generated", "websocket")].map(d => resolve(root, d)); + +async function loadSpec() { + const specPath = process.argv[3]; + if (specPath) { + if (specPath.startsWith("http://") || specPath.startsWith("https://")) { + const response = await fetch(specPath); + if (!response.ok) throw new Error(`Failed to fetch spec: ${response.status}`); + return parse(await response.text()); + } + return parse(readFileSync(resolve(specPath), "utf8")); + } + // Try local apis/ first (when running inside api-docs), then fetch published + const localPath = resolve(root, "../../apis/websocket.yaml"); + try { + return parse(readFileSync(localPath, "utf8")); + } catch (err) { + if (err?.code !== "ENOENT") throw err; // rethrow parse/permission errors + console.log(`Fetching spec from ${PUBLISHED_SPEC_URL}`); + const response = await fetch(PUBLISHED_SPEC_URL); + if (!response.ok) throw new Error(`Failed to fetch spec: ${response.status}`); + return parse(await response.text()); + } +} + +const doc = await loadSpec(); +const defaultTypeMapping = TypeScriptGenerator.defaultOptions.typeMapping; + +const generator = new TypeScriptGenerator({ + modelType: "interface", + typeMapping: { + ...defaultTypeMapping, + Integer(context) { + if (context.constrainedModel.options.format === "int64") { + const type = "number | bigint"; + return context.constrainedModel.options.isNullable + ? `${type} | null` + : type; + } + + return defaultTypeMapping.Integer(context); + }, + }, + constraints: { + propertyKey: typeScriptDefaultPropertyKeyConstraints({ + // Keep wire keys verbatim (e/E, u/U). See header comment. + NAMING_FORMATTER: (name) => name, + // TS interface property keys may be reserved words as-is; don't let + // Modelina rename `status` -> `reserved_status` etc. + NO_RESERVED_KEYWORDS: (name) => name, + }), + }, +}); + +const models = await generator.generate(doc); + +// Guard: the whole reason this script exists is that Modelina's default naming +// drops case-distinct keys. Fail loudly if DepthUpdate ever loses one again. +const depthUpdate = models.find((m) => m.modelName === "DepthUpdate")?.result ?? ""; +for (const key of ["e", "E", "s", "U", "u", "b", "a"]) { + if (!new RegExp(`^\\s*${key}[?:]`, "m").test(depthUpdate)) { + throw new Error( + `generate-ws-types: DepthUpdate is missing wire key "${key}" — naming override regressed.`, + ); + } +} +if (!/^\s*E:\s*number \| bigint;/m.test(depthUpdate)) { + throw new Error( + "generate-ws-types: DepthUpdate.E must stay widened for nanosecond int64 timestamps.", + ); +} + +const banner = + "// GENERATED by scripts/generate-ws-types.mjs from websocket.yaml — DO NOT EDIT.\n" + + "// Regenerate: yarn ws:generate\n\n"; +function refineKnownMethodLiterals(source) { + return source + .replace( + /(export interface SubscribeRequest \{[\s\S]*?\n\s*method: )string(;)/, + '$1"SUBSCRIBE" | "subscribe"$2', + ) + .replace( + /(export interface UnsubscribeRequest \{[\s\S]*?\n\s*method: )string(;)/, + '$1"UNSUBSCRIBE" | "unsubscribe"$2', + ) + .replace( + /(export interface ListSubscriptionsRequest \{[\s\S]*?\n\s*method: )string(;)/, + '$1"LIST_SUBSCRIPTIONS" | "list_subscriptions"$2', + ); +} +// Modelina's library output doesn't prefix declarations with `export`; add it +// so the barrel file exports every type. +const body = refineKnownMethodLiterals( + models + .map((m) => m.result) + .join("\n\n") + .replace(/^(interface |enum |type )/gm, "export $1"), +); + +for (const outDir of outDirs) { + mkdirSync(outDir, { recursive: true }); + writeFileSync(join(outDir, "index.ts"), banner + body + "\n"); + console.log(`Wrote ${models.length} model(s) to ${join(outDir, "index.ts")}`); +} diff --git a/packages/sdk-typescript/scripts/openapi-rest-generator.mjs b/packages/sdk-typescript/scripts/openapi-rest-generator.mjs new file mode 100644 index 0000000..c5fca5c --- /dev/null +++ b/packages/sdk-typescript/scripts/openapi-rest-generator.mjs @@ -0,0 +1,567 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +import openapiTS, { astToString } from "openapi-typescript"; +import ts from "typescript"; +import { parse } from "yaml"; + +const HTTP_METHODS = ["get", "post", "put", "patch", "delete"]; +const RESERVED_HEADERS = new Set(["accept", "authorization", "content-length", "content-type", "cache-control"]); + +export async function loadOpenApiDocument(specPathOrUrl) { + if (specPathOrUrl.startsWith("http://") || specPathOrUrl.startsWith("https://")) { + console.log(`Fetching spec from ${specPathOrUrl}`); + const response = await fetch(specPathOrUrl); + if (!response.ok) throw new Error(`Failed to fetch spec: ${response.status}`); + return parse(await response.text()); + } + return parse(await readFile(specPathOrUrl, "utf8")); +} + +function createResolver(document) { + function resolveRef(ref) { + if (!ref.startsWith("#/")) throw new Error(`Only local references are supported: ${ref}`); + const resolved = ref + .slice(2) + .split("/") + .map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~")) + .reduce((current, part) => current?.[part], document); + if (resolved === undefined) throw new Error(`Reference not found: ${ref}`); + return resolved; + } + + return { + resolveRef, + dereference(candidate) { + return candidate?.$ref ? resolveRef(candidate.$ref) : candidate; + }, + }; +} + +function operationAccess(security) { + if (!security || security.length === 0) return "public"; + return security.some((requirement) => Object.keys(requirement).length === 0) + ? "public" + : "authenticated"; +} + +function parameterShape(schema, dereference, activeRefs = new Set()) { + if (!schema) return undefined; + if (schema.$ref) { + if (activeRefs.has(schema.$ref)) return undefined; + return parameterShape( + dereference(schema), + dereference, + new Set(activeRefs).add(schema.$ref), + ); + } + if (["boolean", "integer", "number", "string"].includes(schema.type)) return "scalar"; + if (schema.type === "array") { + return parameterShape(schema.items, dereference, activeRefs) === "scalar" ? "array" : undefined; + } + if (schema.type === "object") { + const properties = Object.values(schema.properties ?? {}); + const additionalProperties = schema.additionalProperties; + const hasScalarProperties = properties.length > 0 && properties.every((property) => + parameterShape(property, dereference, activeRefs) === "scalar"); + const hasScalarAdditionalProperties = additionalProperties && additionalProperties !== true && + parameterShape(additionalProperties, dereference, activeRefs) === "scalar"; + return hasScalarProperties || hasScalarAdditionalProperties ? "object" : undefined; + } + const branches = [...(schema.oneOf ?? []), ...(schema.anyOf ?? []), ...(schema.allOf ?? [])]; + const shapes = branches.map((branch) => parameterShape(branch, dereference, activeRefs)); + return shapes.length > 0 && shapes.every((shape) => shape === shapes[0]) ? shapes[0] : undefined; +} + +function operationParameters(pathItem, operation, dereference) { + const parameters = new Map(); + for (const candidate of [...(pathItem.parameters ?? []), ...(operation.parameters ?? [])]) { + const parameter = dereference(candidate); + if (parameter.in !== "path" && parameter.in !== "query") continue; + if (parameter.content) { + throw new Error(`${parameter.in} parameter ${parameter.name} uses unsupported content serialization`); + } + const style = parameter.style ?? (parameter.in === "query" ? "form" : "simple"); + const explode = parameter.explode ?? style === "form"; + const shape = parameterShape(parameter.schema, dereference); + const allowedShapes = parameter.in === "path" + ? style === "simple" && !explode ? ["scalar"] : [] + : style === "form" ? ["scalar", "array", "object"] + : (style === "spaceDelimited" || style === "pipeDelimited") && !explode ? ["array"] + : style === "deepObject" && explode ? ["object"] : []; + if (allowedShapes.length === 0) { + throw new Error(`${parameter.in} parameter ${parameter.name} uses unsupported style ${style} with explode=${explode}`); + } + if (!allowedShapes.includes(shape)) { + throw new Error(`${parameter.in} parameter ${parameter.name} has an unsupported schema for ${style} serialization`); + } + const metadata = { + name: parameter.name, + in: parameter.in, + required: parameter.in === "path" ? true : Boolean(parameter.required), + style, + explode, + }; + if (parameter.in === "query") { + metadata.shape = shape; + metadata.allowReserved = Boolean(parameter.allowReserved); + } + parameters.set(`${parameter.in}:${parameter.name}`, metadata); + } + return [...parameters.values()]; +} + +function operationHeaders(pathItem, operation, dereference) { + const headers = new Map(); + for (const candidate of [...(pathItem.parameters ?? []), ...(operation.parameters ?? [])]) { + const parameter = dereference(candidate); + if (parameter.in !== "header") continue; + const normalized = parameter.name.toLowerCase(); + if (normalized.startsWith("x-gemini-") || RESERVED_HEADERS.has(normalized)) continue; + headers.set(normalized, { + name: parameter.name, + in: "header", + required: Boolean(parameter.required), + explode: false, + }); + } + return [...headers.values()]; +} + +function int64Paths(schema, resolveRef, initialPath = [], isRequest = false) { + const paths = new Map(); + + function isStringSchema(candidate, activeRefs) { + if (!candidate) return false; + if (candidate.$ref) { + if (activeRefs.has(candidate.$ref)) return false; + return isStringSchema(resolveRef(candidate.$ref), new Set(activeRefs).add(candidate.$ref)); + } + return candidate.type === "string"; + } + + function walk(candidate, path, activeRefs, allowString = false) { + if (!candidate) return; + if (candidate.$ref) { + if (activeRefs.has(candidate.$ref)) return; + const nextRefs = new Set(activeRefs).add(candidate.$ref); + walk(resolveRef(candidate.$ref), path, nextRefs, allowString); + return; + } + if (candidate.type === "integer" && candidate.format === "int64" && (isRequest || !allowString)) { + const key = JSON.stringify(path); + const current = paths.get(key) ?? { path, allowString: false, unsigned: false }; + current.allowString ||= allowString; + current.unsigned ||= isRequest && candidate["x-unsigned-int64"] === true; + paths.set(key, current); + } + if (candidate.type === "array") walk(candidate.items, [...path, "*"], activeRefs, allowString); + for (const [name, property] of Object.entries(candidate.properties ?? {})) { + walk(property, [...path, name], activeRefs, allowString); + } + const branches = [ + ...(candidate.oneOf ?? []), + ...(candidate.anyOf ?? []), + ...(candidate.allOf ?? []), + ]; + const branchAllowsString = branches.some((branch) => isStringSchema(branch, activeRefs)); + for (const branch of branches) { + walk(branch, path, activeRefs, allowString || branchAllowsString); + } + } + + walk(schema, initialPath, new Set()); + return [...paths.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0) + .map(([, descriptor]) => isRequest + ? { + path: descriptor.path, + ...(descriptor.allowString ? { allowString: true } : {}), + ...(descriptor.unsigned ? { unsigned: true } : {}), + } + : descriptor.path); +} + +function requestInt64Paths(pathItem, operation, resolveRef) { + const paths = { body: [], path: [], query: [] }; + const bodySchema = operation.requestBody?.content?.["application/json"]?.schema; + if (bodySchema) paths.body = int64Paths(bodySchema, resolveRef, [], true); + for (const candidate of [...(pathItem.parameters ?? []), ...(operation.parameters ?? [])]) { + const parameter = candidate.$ref ? resolveRef(candidate.$ref) : candidate; + if (parameter.in !== "path" && parameter.in !== "query") continue; + paths[parameter.in].push(...int64Paths(parameter.schema, resolveRef, [parameter.name], true)); + } + for (const location of ["body", "path", "query"]) { + paths[location].sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))); + } + return paths; +} + +function successResponses(operation, dereference) { + return Object.entries(operation.responses ?? {}).flatMap(([status, response]) => { + if (!/^2\d\d$/.test(status)) return []; + const resolved = dereference(response); + return [{ + status: Number(status), + content: Object.fromEntries(Object.entries(resolved.content ?? {}).sort(([left], [right]) => + left < right ? -1 : left > right ? 1 : 0)), + }]; + }); +} + +function successResponse(operation, operationId, dereference, responseMode = "json") { + const responses = successResponses(operation, dereference); + if (responseMode === "json") { + if (responses.length === 0) { + throw new Error(`${operationId} must have exactly one 2xx application/json response`); + } + const jsonResponses = responses.map((response) => { + const contentTypes = Object.keys(response.content); + const json = response.content["application/json"]; + if (contentTypes.length !== 1 || !json) { + throw new Error(`${operationId} must have only application/json 2xx responses`); + } + if (!json.schema) { + throw new Error(`${operationId} 2xx application/json response must define a schema`); + } + return { ...response, schema: json.schema }; + }); + const [response, ...alternatives] = jsonResponses; + if (!alternatives.every(({ schema }) => JSON.stringify(schema) === JSON.stringify(response.schema))) { + throw new Error(`${operationId} 2xx application/json responses must use the same schema`); + } + return { statuses: responses.map(({ status }) => status), contentTypes: ["application/json"], schema: response.schema }; + } + if (responses.length === 0) { + throw new Error(`${operationId} must have at least one 2xx file response`); + } + const fileContentTypes = new Set(); + for (const response of responses) { + const contentTypes = Object.keys(response.content); + const responseFileTypes = contentTypes.filter((contentType) => contentType !== "application/json"); + if (responseFileTypes.length === 0 || responseFileTypes.length !== contentTypes.length) { + throw new Error(`${operationId} must have only file 2xx responses`); + } + for (const contentType of responseFileTypes) fileContentTypes.add(contentType); + } + return { statuses: responses.map(({ status }) => status), contentTypes: [...fileContentTypes], schema: undefined }; +} + +function inferResponseMode(operation, dereference) { + const responses = successResponses(operation, dereference); + if (responses.length > 0 && responses.every((response) => { + const contentTypes = Object.keys(response.content); + return contentTypes.length === 1 && contentTypes[0] === "application/json"; + })) return "json"; + if (responses.length > 0 && responses.every((response) => { + const contentTypes = Object.keys(response.content); + return contentTypes.length > 0 && contentTypes.every((contentType) => contentType !== "application/json"); + })) return "file"; + return undefined; +} + +function responseModeFor(operation, operationId, dereference, options) { + if (options.operationResponseModes?.[operationId] && + options.operationResponseModes[operationId] !== "json" && + options.operationResponseModes[operationId] !== "file") { + throw new Error(`${operationId} responseMode must be json or file`); + } + const responseMode = options.operationResponseModes?.[operationId] ?? inferResponseMode(operation, dereference); + if (responseMode !== "json" && responseMode !== "file") { + throw new Error(`${operationId} response contract is unsupported or ambiguous`); + } + return responseMode; +} + +function assertJsonSchema(response, operationId) { + if (!response.schema) { + throw new Error(`${operationId} 2xx application/json response must define a schema`); + } +} + +function shouldInclude(operation, operationId, options) { + if (options.excludeOperationIds?.includes(operationId)) return false; + const byId = options.includeOperationIds?.includes(operationId) ?? false; + const byTag = operation.tags?.some((tag) => options.includeTags?.includes(tag)) ?? false; + return options.includeOperationIds || options.includeTags ? byId || byTag : true; +} + +export function discoverOperationInventory(document, { spec }) { + const { dereference } = createResolver(document); + const seen = new Set(); + const operations = []; + for (const [path, rawPathItem] of Object.entries(document.paths ?? {})) { + const pathItem = dereference(rawPathItem); + for (const method of HTTP_METHODS) { + const operation = pathItem[method] && dereference(pathItem[method]); + if (!operation) continue; + const operationId = operation.operationId; + if (!operationId) throw new Error(`${method.toUpperCase()} ${path} is missing operationId`); + if (seen.has(operationId)) throw new Error(`Repeated operationId: ${operationId}`); + seen.add(operationId); + operations.push({ + spec, + operationId, + method, + path, + tags: operation.tags ?? [], + successResponses: Object.entries(operation.responses ?? {}).flatMap(([status, response]) => { + if (!/^2\d\d$/.test(status)) return []; + return [{ status: Number(status), contentTypes: Object.keys(dereference(response).content ?? {}).sort() }]; + }), + }); + } + } + return operations.sort((left, right) => + left.operationId < right.operationId ? -1 : left.operationId > right.operationId ? 1 : 0); +} + +export function discoverOperations(document, options = {}) { + const { resolveRef, dereference } = createResolver(document); + const requestedTags = new Set(options.includeTags ?? []); + const matchedTags = new Set(); + const seen = new Set(); + const found = new Set(); + const operations = []; + for (const [path, rawPathItem] of Object.entries(document.paths ?? {})) { + const pathItem = dereference(rawPathItem); + for (const method of HTTP_METHODS) { + const operation = pathItem[method] && dereference(pathItem[method]); + if (!operation) continue; + const operationId = operation.operationId; + if (!operationId) throw new Error(`${method.toUpperCase()} ${path} is missing operationId`); + if (seen.has(operationId)) throw new Error(`Repeated operationId: ${operationId}`); + seen.add(operationId); + for (const tag of operation.tags ?? []) { + if (requestedTags.has(tag)) matchedTags.add(tag); + } + if (!shouldInclude(operation, operationId, options)) continue; + found.add(operationId); + const responseMode = responseModeFor(operation, operationId, dereference, options); + const response = successResponse(operation, operationId, dereference, responseMode); + if (responseMode === "json") assertJsonSchema(response, operationId); + operations.push({ + operationId, + metadata: { + responseMode, + ...(options.operationNamespace ? { operation: `${options.operationNamespace}.${operationId}` } : {}), + method, + path, + access: operationAccess(operation.security ?? document.security), + parameters: operationParameters(pathItem, operation, dereference), + headers: operationHeaders(pathItem, operation, dereference), + requestBody: Boolean(operation.requestBody), + requestBodyRequired: Boolean(operation.requestBody?.required), + successStatuses: response.statuses, + responseContentTypes: response.contentTypes, + responseInt64Paths: responseMode === "json" ? int64Paths(response.schema, resolveRef) : [], + requestInt64Paths: requestInt64Paths(pathItem, operation, resolveRef), + // GET is the generated SDK's explicit safe-read policy; every mutation is false. + retryable: method === "get", + }, + }); + } + } + for (const tag of requestedTags) { + if (!matchedTags.has(tag)) throw new Error(`REST tag not found: ${tag}`); + } + for (const operationId of options.includeOperationIds ?? []) { + if (!found.has(operationId) && !options.excludeOperationIds?.includes(operationId)) { + throw new Error(`REST operation not found: ${operationId}`); + } + } + return operations.sort((left, right) => + left.operationId < right.operationId ? -1 : left.operationId > right.operationId ? 1 : 0); +} + +export function renderOperations(operations, options) { + const registry = operations + .map(({ operationId, metadata }) => ` ${JSON.stringify(operationId)}: ${JSON.stringify(metadata)},`) + .join("\n"); + const usesCallerJsonBody = operations.some(({ metadata }) => + metadata.access === "authenticated" && metadata.requestBody); + const headersType = (operationId, metadata) => { + if (metadata.headers.length === 0) return "never"; + const names = metadata.headers.map((header) => JSON.stringify(header.name)).join(" | "); + return `Pick>, ${names}>`; + }; + const inputType = (type) => `Int64Input<${type}>`; + const bodyType = (operationId, metadata) => { + if (!metadata.requestBody) return "never"; + const jsonBody = inputType(`JsonBody`); + return metadata.access === "authenticated" ? `CallerJsonBody<${jsonBody}>` : jsonBody; + }; + const typeMap = operations + .map(({ operationId, metadata }) => ` ${JSON.stringify(operationId)}: {\n` + + ` path: ${inputType(`ParameterAt`)};\n` + + ` query: ${inputType(`ParameterAt`)};\n` + + ` headers: ${headersType(operationId, metadata)};\n` + + ` body: ${bodyType(operationId, metadata)};\n` + + ` response: ${metadata.responseMode === "file" + ? "RestFileResponse" + : `JsonResponse`};\n` + + " };") + .join("\n"); + const fileImport = operations.some(({ metadata }) => metadata.responseMode === "file") + ? `import type { RestFileResponse } from ${JSON.stringify(options.fileResponseImportPath ?? "../core/http.js")};\n` + : ""; + const callerJsonBodyTypes = usesCallerJsonBody + ? `type StripTransportFields = T extends object ? Omit : T;\n\n` + + `type CallerJsonBody = StripTransportFields;\n\n` + : ""; + + return `${options.banner}${fileImport}import type { operations as OpenApiOperations } from ${JSON.stringify(options.modelsImportPath ?? "./models.js")};\n\n` + + `type ParameterAt =\n` + + ` O extends { parameters: infer P }\n` + + ` ? Location extends keyof P ? P[Location] : never\n` + + ` : never;\n\n` + + `type Int64Input =\n` + + ` T extends bigint ? bigint | number :\n` + + ` T extends readonly (infer Item)[] ? Int64Input[] :\n` + + ` T extends object ? { [K in keyof T]: Int64Input } : T;\n\n` + + `type JsonBody =\n` + + ` NonNullable extends\n` + + ` { content: { "application/json": infer Body } }\n` + + ` ? Required extends true ? Body : Body | undefined\n` + + ` : never;\n\n` + + callerJsonBodyTypes + + `type JsonResponse =\n` + + ` O extends { responses: infer R }\n` + + ` ? Status extends keyof R\n` + + ` ? R[Status] extends { content: { "application/json": infer Body } } ? Body : never\n` + + ` : never\n` + + ` : never;\n\n` + + `export const ${options.operationsConstName} = {\n${registry}\n} as const;\n\n` + + `export type ${options.operationIdTypeName} = keyof typeof ${options.operationsConstName};\n\n` + + `export type ${options.operationTypesName} = {\n${typeMap}\n};\n`; +} + +const IDENTIFIER = /^[A-Za-z_$][\w$]*$/u; + +function inputBuckets(metadata, operationTypesName, operationId) { + const pathParameters = metadata.parameters.filter((parameter) => parameter.in === "path"); + const queryParameters = metadata.parameters.filter((parameter) => parameter.in === "query"); + const queryRequired = queryParameters.some((parameter) => parameter.required); + const headersRequired = metadata.headers.some((header) => header.required); + return [ + pathParameters.length > 0 && { + name: "path", + required: true, + type: `${operationTypesName}[${JSON.stringify(operationId)}]["path"]`, + }, + queryParameters.length > 0 && { + name: "query", + required: queryRequired, + type: `${operationTypesName}[${JSON.stringify(operationId)}]["query"]`, + }, + metadata.headers.length > 0 && { + name: "headers", + required: headersRequired, + type: `${operationTypesName}[${JSON.stringify(operationId)}]["headers"]`, + }, + metadata.requestBody && { + name: "body", + required: metadata.requestBodyRequired, + type: `${operationTypesName}[${JSON.stringify(operationId)}]["body"]`, + }, + ].filter(Boolean); +} + +function usesPositionalBuckets(buckets) { + if (buckets.length <= 1) return true; + const bucketNames = buckets.map((bucket) => bucket.name).sort().join(","); + return bucketNames === "headers,path" || + bucketNames === "headers,query" || + bucketNames === "path,query"; +} + +function renderMethodInput(buckets, positional) { + if (buckets.length === 0) return ""; + if (positional) { + return buckets + .map((bucket) => `${bucket.name}${bucket.required ? "" : "?"}: ${bucket.type}`) + .join(", "); + } + const optional = buckets.every((bucket) => !bucket.required); + const fields = buckets + .map((bucket) => ` ${bucket.name}${bucket.required ? "" : "?"}: ${bucket.type};`) + .join("\n"); + return `input${optional ? "?" : ""}: {\n${fields}\n }`; +} + +function renderOperationInput(buckets, positional) { + if (buckets.length === 0) return ""; + const optionalInput = !positional && buckets.every((bucket) => !bucket.required); + const lines = buckets.map((bucket) => { + if (positional) return `${bucket.name},`; + return `${bucket.name}: input${optionalInput ? "?." : "."}${bucket.name},`; + }); + return `, {\n ${lines.join("\n ")}\n }`; +} + +export function renderRestClient(operations, options) { + const deadlineImportPath = options.deadlineImportPath ?? "../core/deadline.js"; + const seen = new Set(); + const methods = operations.map(({ operationId, methodName = operationId, metadata }) => { + if (!IDENTIFIER.test(methodName)) throw new Error(`Invalid methodName in ${options.className}: ${methodName}`); + if (seen.has(methodName)) throw new Error(`Duplicate methodName in ${options.className}: ${methodName}`); + seen.add(methodName); + const buckets = inputBuckets(metadata, options.operationTypesName, operationId); + const positional = usesPositionalBuckets(buckets); + const methodInput = renderMethodInput(buckets, positional); + const operationInput = renderOperationInput(buckets, positional); + const operationCall = operationInput ? `${operationInput}, requestOptions` : `, {}, requestOptions`; + const returnType = `Promise<${options.operationTypesName}[${JSON.stringify(operationId)}]["response"]>`; + const withOptions = `${methodName}(${methodInput}${methodInput ? ", " : ""}requestOptions?: RequestOptions)`; + const original = `${methodName}(${methodInput})`; + return ` ${withOptions}: ${returnType};\n` + + ` ${original}: ${returnType};\n` + + ` ${withOptions}: ${returnType} {\n` + + ` const operation = ${options.operationsConstName}[${JSON.stringify(operationId)}];\n` + + ` return executeRestOperation<${options.operationTypesName}[${JSON.stringify(operationId)}]>(this.transport, operation${operationCall});\n` + + ` }`; + }).join("\n\n"); + + return `${options.banner}import type { HttpTransport } from ${JSON.stringify(options.transportImportPath)};\n` + + `import type { RequestOptions } from ${JSON.stringify(deadlineImportPath)};\n` + + `import { executeRestOperation } from ${JSON.stringify(options.executorImportPath)};\n\n` + + `import {\n` + + ` ${options.operationsConstName},\n` + + ` type ${options.operationTypesName},\n` + + `} from ${JSON.stringify(options.operationsImportPath)};\n\n` + + `export class ${options.className} {\n` + + ` constructor(private readonly transport: HttpTransport) {}\n\n` + + `${methods}\n` + + `}\n`; +} + +export async function renderModels(document, banner) { + const BIGINT = ts.factory.createKeywordTypeNode(ts.SyntaxKind.BigIntKeyword); + const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); + const ast = await openapiTS(document, { + silent: true, + transform(schema) { + if (schema.type === "integer" && schema.format === "int64") { + return schema.nullable ? ts.factory.createUnionTypeNode([BIGINT, NULL]) : BIGINT; + } + }, + }); + return `${banner}${astToString(ast).trimEnd()}\n`; +} + +export async function generateOpenApiRestTypes(options) { + const specPath = options.specPath.startsWith("http://") || options.specPath.startsWith("https://") + ? options.specPath + : resolve(options.specPath); + const outputDir = resolve(options.outputDir); + const document = await loadOpenApiDocument(specPath); + const operations = discoverOperations(document, options); + await mkdir(outputDir, { recursive: true }); + const writes = [ + writeFile(resolve(outputDir, "operations.ts"), renderOperations(operations, options)), + ]; + if (options.writeModels !== false) { + writes.push(writeFile(resolve(outputDir, "models.ts"), await renderModels(document, options.banner))); + } + await Promise.all(writes); + return { document, operations }; +} diff --git a/packages/sdk-typescript/scripts/openapi-rest-generator.test.mjs b/packages/sdk-typescript/scripts/openapi-rest-generator.test.mjs new file mode 100644 index 0000000..64b910f --- /dev/null +++ b/packages/sdk-typescript/scripts/openapi-rest-generator.test.mjs @@ -0,0 +1,561 @@ +import assert from "node:assert/strict"; +import { execFile as execFileCallback } from "node:child_process"; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { promisify } from "node:util"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +import { generateOpenApiRestTypes, renderRestClient } from "./openapi-rest-generator.mjs"; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const sdkDir = resolve(scriptDir, ".."); +const localRestSpec = resolve(sdkDir, "../../apis/rest.yaml"); +const localPmSpec = resolve(sdkDir, "../../apis/prediction-markets.yaml"); +const restSpecPath = existsSync(localRestSpec) ? localRestSpec : "https://developer.gemini.com/specs/openapi/rest.yaml"; +const predictionMarketsSpecPath = existsSync(localPmSpec) ? localPmSpec : "https://developer.gemini.com/specs/openapi/prediction-markets.yaml"; +const execFile = promisify(execFileCallback); + +/** Read a spec as raw text — works with both local files and URLs. */ +async function readSpecText(pathOrUrl) { + if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) { + const res = await fetch(pathOrUrl); + if (!res.ok) throw new Error(`Failed to fetch spec: ${res.status}`); + return res.text(); + } + return readFileSync(pathOrUrl, "utf8"); +} + +function operationIds(source) { + const registry = source.match(/export const \w+ = \{\n(?[\s\S]*?)\n\} as const;/)?.groups?.body; + assert(registry, "generated operations registry not found"); + return [...registry.matchAll(/^ "([^"]+)":/gm)].map(([, id]) => id); +} + +test("generator can emit Market Data operations by tag including file downloads", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "market-data-generator-")); + t.after(() => rmSync(directory, { recursive: true, force: true })); + + await generateOpenApiRestTypes({ + specPath: restSpecPath, + outputDir: directory, + banner: "// Generated from apis/rest.yaml. Do not edit.\n\n", + includeTags: ["Market Data"], + operationResponseModes: { getFundingAmountReportFile: "file" }, + operationsConstName: "MARKET_DATA_OPERATIONS", + operationIdTypeName: "MarketDataOperationId", + operationTypesName: "MarketDataOperationTypes", + }); + + const operations = readFileSync(join(directory, "operations.ts"), "utf8"); + const models = readFileSync(join(directory, "models.ts"), "utf8"); + const ids = operationIds(operations); + + assert.match(models, /Generated from apis\/rest\.yaml/); + assert.equal(ids.length, 15); + assert(ids.includes("getTicker")); + assert(ids.includes("listDerivativeCandles")); + assert(ids.includes("getFundingAmountReportFile")); + assert(!ids.includes("createNewOrder")); + assert.match(operations, /export const MARKET_DATA_OPERATIONS/); + assert.match(operations, /export type MarketDataOperationTypes/); + assert.match(operations, /import type \{ RestFileResponse \} from "\.\.\/core\/http\.js";/); + assert.match(operations, /"getFundingAmountReportFile": \{[\s\S]*"responseContentTypes":\["application\/vnd\.openxmlformats-officedocument\.spreadsheetml\.sheet","text\/csv"\]/); + assert.match(operations, /response: RestFileResponse;/); +}); + +test("generator fails loudly when a success response mixes json and file content without override", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "rest-generator-mixed-response-")); + const specPath = join(directory, "mixed.yaml"); + t.after(() => rmSync(directory, { recursive: true, force: true })); + writeFileSync(specPath, `openapi: 3.0.3 +info: { title: mixed, version: 1 } +paths: + /v1/items: + get: + operationId: listItems + responses: + "200": + description: ok + content: + application/json: { schema: { type: object } } + text/csv: { schema: { type: string, format: binary } } +`); + + await assert.rejects( + generateOpenApiRestTypes({ + specPath, + outputDir: directory, + banner: "// generated\n", + operationsConstName: "OPERATIONS", + operationIdTypeName: "OperationId", + operationTypesName: "OperationTypes", + }), + /listItems response contract is unsupported or ambiguous/, + ); +}); + +test("generator fails loudly when a requested tag matches no operations", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "market-data-generator-missing-tag-")); + t.after(() => rmSync(directory, { recursive: true, force: true })); + + await assert.rejects( + generateOpenApiRestTypes({ + specPath: restSpecPath, + outputDir: directory, + banner: "// Generated from apis/rest.yaml. Do not edit.\n\n", + includeTags: ["Market Datas"], + operationsConstName: "MARKET_DATA_OPERATIONS", + operationIdTypeName: "MarketDataOperationId", + operationTypesName: "MarketDataOperationTypes", + }), + /REST tag not found: Market Datas/, + ); +}); + +test("generator supports matching JSON schemas across multiple success statuses", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "rest-generator-multiple-json-successes-")); + const specPath = join(directory, "multiple-json-successes.yaml"); + t.after(() => rmSync(directory, { recursive: true, force: true })); + writeFileSync(specPath, `openapi: 3.0.3 +info: { title: ambiguous, version: 1 } +paths: + /v1/items: + get: + operationId: listItems + responses: + "200": { description: ok, content: { application/json: { schema: { type: object } } } } + "201": { description: created, content: { application/json: { schema: { type: object } } } } +`); + + await generateOpenApiRestTypes({ + specPath, + outputDir: directory, + banner: "// generated\n", + operationsConstName: "OPERATIONS", + operationIdTypeName: "OperationId", + operationTypesName: "OperationTypes", + }); + + const operations = readFileSync(join(directory, "operations.ts"), "utf8"); + assert.match(operations, /"listItems": \{"responseMode":"json"[\s\S]*"successStatuses":\[200,201\]/); + assert.match(operations, /response: JsonResponse;/); +}); + +test("generator emits all compatible file success statuses and media types", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "rest-generator-multiple-file-successes-")); + const specPath = join(directory, "multiple-file-successes.yaml"); + t.after(() => rmSync(directory, { recursive: true, force: true })); + writeFileSync(specPath, `openapi: 3.0.3 +info: { title: files, version: 1 } +paths: + /v1/report: + get: + operationId: getReport + responses: + "200": { description: ok, content: { application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: { schema: { type: string, format: binary } } } } + "206": { description: partial, content: { text/csv: { schema: { type: string, format: binary } } } } +`); + + await generateOpenApiRestTypes({ + specPath, + outputDir: directory, + banner: "// generated\n", + operationsConstName: "OPERATIONS", + operationIdTypeName: "OperationId", + operationTypesName: "OperationTypes", + }); + + const operations = readFileSync(join(directory, "operations.ts"), "utf8"); + assert.match(operations, /"getReport": \{"responseMode":"file"[\s\S]*"successStatuses":\[200,206\]/); + assert.match(operations, /"responseContentTypes":\["application\/vnd\.openxmlformats-officedocument\.spreadsheetml\.sheet","text\/csv"\]/); +}); + +test("generator emits request int64 metadata and bigint-compatible input types", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "rest-generator-request-int64-")); + const specPath = join(directory, "request-int64.yaml"); + t.after(() => rmSync(directory, { recursive: true, force: true })); + writeFileSync(specPath, `openapi: 3.0.3 +info: { title: request-int64, version: 1 } +paths: + /v1/items/{itemId}: + post: + operationId: updateItem + parameters: + - name: itemId + in: path + required: true + schema: { type: integer, format: int64 } + - name: since + in: query + required: false + schema: { type: integer, format: int64 } + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [orderId] + properties: + orderId: { type: integer, format: int64, x-unsigned-int64: true } + legacyId: + oneOf: + - { type: integer, format: int64 } + - { type: string } + responses: + "200": { description: ok, content: { application/json: { schema: { type: object } } } } +`); + + await generateOpenApiRestTypes({ + specPath, + outputDir: directory, + banner: "// generated\n", + operationsConstName: "OPERATIONS", + operationIdTypeName: "OperationId", + operationTypesName: "OperationTypes", + }); + + const operations = readFileSync(join(directory, "operations.ts"), "utf8"); + assert.match(operations, /requestInt64Paths/); + assert.match(operations, /"body":\[\{"path":\["legacyId"\],"allowString":true\},\{"path":\["orderId"\],"unsigned":true\}\]/); + assert.match(operations, /"path":\[\{"path":\["itemId"\]\}\]/); + assert.match(operations, /"query":\[\{"path":\["since"\]\}\]/); + assert.match(operations, /type Int64Input/); + assert.match(operations, /bigint \| number/); +}); + +test("generator does not normalize response int64 fields with a string variant", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "rest-generator-response-int64-string-")); + const specPath = join(directory, "response-int64-string.yaml"); + t.after(() => rmSync(directory, { recursive: true, force: true })); + writeFileSync(specPath, `openapi: 3.0.3 +info: { title: response-int64-string, version: 1 } +paths: + /v1/items: + get: + operationId: listItems + responses: + "200": + description: ok + content: + application/json: + schema: + type: array + items: + type: object + properties: + timestamp: + oneOf: + - { type: integer, format: int64 } + - { type: string, format: date-time } +`); + + await generateOpenApiRestTypes({ + specPath, + outputDir: directory, + banner: "// generated\n", + operationsConstName: "OPERATIONS", + operationIdTypeName: "OperationId", + operationTypesName: "OperationTypes", + }); + + const operations = readFileSync(join(directory, "operations.ts"), "utf8"); + assert.match(operations, /responseInt64Paths":\[\]/); +}); + +test("generator rejects unsupported parameter styles and object shapes", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "rest-generator-parameter-contract-")); + t.after(() => rmSync(directory, { recursive: true, force: true })); + const spec = (parameter) => `openapi: 3.0.3 +info: { title: invalid, version: 1 } +paths: + /v1/items: + get: + operationId: listItems + parameters: + - ${parameter} + responses: + "200": { description: ok, content: { application/json: { schema: { type: object } } } } +`; + const options = (specPath) => ({ + specPath, + outputDir: directory, + banner: "// generated\n", + operationsConstName: "OPERATIONS", + operationIdTypeName: "OperationId", + operationTypesName: "OperationTypes", + }); + const stylePath = join(directory, "unsupported-style.yaml"); + writeFileSync(stylePath, spec("{ name: filter, in: query, style: deepObject, explode: true, schema: { type: string } }")); + await assert.rejects(generateOpenApiRestTypes(options(stylePath)), /query parameter filter has an unsupported schema/); + + const objectPath = join(directory, "unsupported-object.yaml"); + writeFileSync(objectPath, spec("{ name: filter, in: query, style: form, explode: true, schema: { type: object } }")); + await assert.rejects(generateOpenApiRestTypes(options(objectPath)), /query parameter filter has an unsupported schema/); +}); + +test("generator emits metadata for every supported query serialization style", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "rest-generator-query-styles-")); + const specPath = join(directory, "query-styles.yaml"); + t.after(() => rmSync(directory, { recursive: true, force: true })); + writeFileSync(specPath, `openapi: 3.0.3 +info: { title: query styles, version: 1 } +paths: + /v1/items: + get: + operationId: listItems + parameters: + - { name: formDefault, in: query, schema: { type: string } } + - { name: formArray, in: query, schema: { type: array, items: { type: string } }, style: form, explode: false } + - { name: formObject, in: query, schema: { type: object, properties: { status: { type: string } } }, style: form, explode: true, allowReserved: true } + - { name: spaceArray, in: query, schema: { type: array, items: { type: string } }, style: spaceDelimited } + - { name: pipeArray, in: query, schema: { type: array, items: { type: string } }, style: pipeDelimited } + - { name: deepObject, in: query, schema: { type: object, properties: { status: { type: string } } }, style: deepObject, explode: true } + responses: + "200": { description: ok, content: { application/json: { schema: { type: object } } } } +`); + + await generateOpenApiRestTypes({ + specPath, + outputDir: directory, + banner: "// generated\n", + operationsConstName: "OPERATIONS", + operationIdTypeName: "OperationId", + operationTypesName: "OperationTypes", + }); + + const operations = readFileSync(join(directory, "operations.ts"), "utf8"); + assert.match(operations, /"formDefault","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false/); + assert.match(operations, /"formArray","in":"query","required":false,"style":"form","explode":false,"shape":"array","allowReserved":false/); + assert.match(operations, /"formObject","in":"query","required":false,"style":"form","explode":true,"shape":"object","allowReserved":true/); + assert.match(operations, /"spaceArray","in":"query","required":false,"style":"spaceDelimited","explode":false,"shape":"array","allowReserved":false/); + assert.match(operations, /"pipeArray","in":"query","required":false,"style":"pipeDelimited","explode":false,"shape":"array","allowReserved":false/); + assert.match(operations, /"deepObject","in":"query","required":false,"style":"deepObject","explode":true,"shape":"object","allowReserved":false/); +}); + +test("generator rejects invalid query serialization combinations", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "rest-generator-invalid-query-styles-")); + t.after(() => rmSync(directory, { recursive: true, force: true })); + const spec = (parameter) => `openapi: 3.0.3 +info: { title: invalid query styles, version: 1 } +paths: + /v1/items: + get: + operationId: listItems + parameters: + - ${parameter} + responses: + "200": { description: ok, content: { application/json: { schema: { type: object } } } } +`; + const options = (specPath) => ({ + specPath, + outputDir: directory, + banner: "// generated\n", + operationsConstName: "OPERATIONS", + operationIdTypeName: "OperationId", + operationTypesName: "OperationTypes", + }); + const cases = [ + ["spaceDelimited scalar", "{ name: value, in: query, style: spaceDelimited, schema: { type: string } }", /query parameter value has an unsupported schema/], + ["pipeDelimited exploded", "{ name: value, in: query, style: pipeDelimited, explode: true, schema: { type: array, items: { type: string } } }", /query parameter value uses unsupported style/], + ["deepObject not exploded", "{ name: value, in: query, style: deepObject, schema: { type: object, properties: { status: { type: string } } } }", /query parameter value uses unsupported style/], + ["deepObject array", "{ name: value, in: query, style: deepObject, explode: true, schema: { type: array, items: { type: string } } }", /query parameter value has an unsupported schema/], + ["nested object", "{ name: value, in: query, style: deepObject, explode: true, schema: { type: object, properties: { nested: { type: object } } } }", /query parameter value has an unsupported schema/], + ["content parameter", "{ name: value, in: query, content: { application/json: { schema: { type: string } } } }", /uses unsupported content serialization/], + ]; + for (const [name, parameter, error] of cases) { + const specPath = join(directory, `${name.replaceAll(" ", "-")}.yaml`); + writeFileSync(specPath, spec(parameter)); + await assert.rejects(generateOpenApiRestTypes(options(specPath)), error); + } +}); + +test("generator rejects multiple JSON success responses with different schemas", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "rest-generator-mismatched-json-successes-")); + const specPath = join(directory, "mismatched-json-successes.yaml"); + t.after(() => rmSync(directory, { recursive: true, force: true })); + writeFileSync(specPath, `openapi: 3.0.3 +info: { title: mismatched, version: 1 } +paths: + /v1/items: + get: + operationId: listItems + responses: + "200": { description: ok, content: { application/json: { schema: { type: object } } } } + "201": { description: created, content: { application/json: { schema: { type: string } } } } +`); + + await assert.rejects( + generateOpenApiRestTypes({ + specPath, + outputDir: directory, + banner: "// generated\n", + operationResponseModes: { listItems: "json" }, + operationsConstName: "OPERATIONS", + operationIdTypeName: "OperationId", + operationTypesName: "OperationTypes", + }), + /listItems 2xx application\/json responses must use the same schema/, + ); +}); + +test("generator json override rejects a mixed-content success response", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "rest-generator-json-override-")); + const specPath = join(directory, "mixed.yaml"); + t.after(() => rmSync(directory, { recursive: true, force: true })); + writeFileSync(specPath, `openapi: 3.0.3 +info: { title: mixed, version: 1 } +paths: + /v1/items: + get: + operationId: listItems + responses: + "200": + description: ok + content: + application/json: { schema: { type: object } } + text/csv: { schema: { type: string, format: binary } } +`); + + await assert.rejects( + generateOpenApiRestTypes({ + specPath, + outputDir: directory, + banner: "// generated\n", + operationResponseModes: { listItems: "json" }, + operationsConstName: "OPERATIONS", + operationIdTypeName: "OperationId", + operationTypesName: "OperationTypes", + }), + /listItems must have only application\/json 2xx responses/, + ); +}); + +test("generator retains caller-owned header parameters and excludes transport headers", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "rest-generator-headers-")); + const specPath = join(directory, "headers.yaml"); + t.after(() => rmSync(directory, { recursive: true, force: true })); + writeFileSync(specPath, `openapi: 3.0.3 +info: { title: headers, version: 1 } +paths: + /v1/items: + get: + operationId: listItems + parameters: + - { name: X-Request-Id, in: header, required: true, schema: { type: string } } + - { name: Authorization, in: header, required: true, schema: { type: string } } + - { name: X-GEMINI-APIKEY, in: header, required: true, schema: { type: string } } + - { name: Content-Type, in: header, required: true, schema: { type: string } } + responses: + "200": { description: ok, content: { application/json: { schema: { type: object } } } } +`); + + await generateOpenApiRestTypes({ + specPath, + outputDir: directory, + banner: "// generated\n", + operationsConstName: "OPERATIONS", + operationIdTypeName: "OperationId", + operationTypesName: "OperationTypes", + }); + + const operations = readFileSync(join(directory, "operations.ts"), "utf8"); + assert.match(operations, /"headers":\[\{"name":"X-Request-Id","in":"header","required":true,"explode":false}\]/); + assert.match(operations, /headers: Pick>, "X-Request-Id">;/); + assert.doesNotMatch(operations, /Authorization|X-GEMINI-APIKEY|Content-Type/); +}); + +test("Prediction Markets REST wrappers delegate through executeRestOperation", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "prediction-markets-rest-generator-")); + t.after(() => rmSync(directory, { recursive: true, force: true })); + + await execFile(process.execPath, [join(scriptDir, "generate-prediction-markets.mjs"), predictionMarketsSpecPath, directory]); + + const rest = readFileSync(join(directory, "rest.ts"), "utf8"); + assert.match(rest, /import \{ executeRestOperation \} from "\.\.\/core\/rest-operation\.js";/); + assert.match(rest, /return executeRestOperation { + const directory = mkdtempSync(join(tmpdir(), "prediction-markets-rest-headers-")); + const specPath = join(directory, "prediction-markets.yaml"); + t.after(() => rmSync(directory, { recursive: true, force: true })); + writeFileSync( + specPath, + (await readSpecText(predictionMarketsSpecPath)).replace( + " parameters:\n - name: status", + " parameters:\n - name: X-Request-Id\n in: header\n required: false\n schema:\n type: string\n - name: status", + ), + ); + + await execFile(process.execPath, [join(scriptDir, "generate-prediction-markets.mjs"), specPath, directory]); + + const rest = readFileSync(join(directory, "rest.ts"), "utf8"); + assert.match(rest, /listEvents\(query\?: [^,]+, headers\?: PredictionMarketOperationTypes\["listEvents"\]\["headers"\]\)/); + assert.match(rest, /query,\n headers,/); +}); + +test("generic REST client renderer emits callable module methods", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "market-data-rest-client-")); + t.after(() => rmSync(directory, { recursive: true, force: true })); + + const { operations } = await generateOpenApiRestTypes({ + specPath: restSpecPath, + outputDir: directory, + banner: "// generated\n", + includeOperationIds: ["getCurrentOrderBook", "listSymbols"], + operationsConstName: "MARKET_DATA_OPERATIONS", + operationIdTypeName: "MarketDataOperationId", + operationTypesName: "MarketDataOperationTypes", + }); + + const rest = renderRestClient(operations, { + banner: "// generated\n", + className: "MarketDataRest", + operationsConstName: "MARKET_DATA_OPERATIONS", + operationTypesName: "MarketDataOperationTypes", + operationsImportPath: "./operations.js", + transportImportPath: "../../core/http.js", + executorImportPath: "../../core/rest-operation.js", + }); + + assert.match(rest, /export class MarketDataRest/); + assert.match(rest, /listSymbols\(\): Promise/); + assert.match(rest, /getCurrentOrderBook\(path: MarketDataOperationTypes\["getCurrentOrderBook"\]\["path"\], query\?: MarketDataOperationTypes\["getCurrentOrderBook"\]\["query"\]\)/); + assert.match(rest, /return executeRestOperation\(this\.transport, operation, \{\n path,\n query,/); + assert.doesNotMatch(rest, /this\.transport\.(?:requestPublic|request)\(/); +}); + +test("generic REST client renderer rejects duplicate public method names", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "market-data-rest-collision-")); + t.after(() => rmSync(directory, { recursive: true, force: true })); + + const { operations } = await generateOpenApiRestTypes({ + specPath: restSpecPath, + outputDir: directory, + banner: "// generated\n", + includeOperationIds: ["getTicker", "getTickerV2"], + operationsConstName: "MARKET_DATA_OPERATIONS", + operationIdTypeName: "MarketDataOperationId", + operationTypesName: "MarketDataOperationTypes", + }); + + assert.throws( + () => renderRestClient( + operations.map((operation) => ({ ...operation, methodName: "getTicker" })), + { + banner: "// generated\n", + className: "MarketDataRest", + operationsConstName: "MARKET_DATA_OPERATIONS", + operationTypesName: "MarketDataOperationTypes", + operationsImportPath: "./operations.js", + transportImportPath: "../../core/http.js", + executorImportPath: "../../core/rest-operation.js", + }, + ), + /Duplicate methodName in MarketDataRest: getTicker/, + ); +}); diff --git a/packages/sdk-typescript/scripts/qa-bootstrap.mjs b/packages/sdk-typescript/scripts/qa-bootstrap.mjs new file mode 100644 index 0000000..3eb5a75 --- /dev/null +++ b/packages/sdk-typescript/scripts/qa-bootstrap.mjs @@ -0,0 +1,23 @@ +import { rewriteRestUrl, rewriteWebSocketUrl } from "./qa-routing.mjs"; + +const qaRestUrl = process.env.GEMINI_QA_REST_URL; +const qaWebSocketUrl = process.env.GEMINI_QA_WEBSOCKET_URL; +const environment = process.env.GEMINI_QA_PROTOCOL ?? "production"; +if (!qaRestUrl || !qaWebSocketUrl) { + throw new Error("GEMINI_QA_REST_URL and GEMINI_QA_WEBSOCKET_URL are required"); +} +if (environment !== "production" && environment !== "sandbox") { + throw new Error("GEMINI_QA_PROTOCOL must be production or sandbox"); +} +process.env.GEMINI_SMOKE_ENV = environment; +process.env.GEMINI_SMOKE_LABEL = "QA"; + +const nativeFetch = globalThis.fetch.bind(globalThis); +globalThis.fetch = (url, init) => nativeFetch(rewriteRestUrl(url, qaRestUrl, environment), init); + +const NativeWebSocket = globalThis.WebSocket; +globalThis.WebSocket = new Proxy(NativeWebSocket, { + construct(Target, args) { + return Reflect.construct(Target, [rewriteWebSocketUrl(args[0], qaWebSocketUrl, environment), ...args.slice(1)]); + }, +}); diff --git a/packages/sdk-typescript/scripts/qa-routing.mjs b/packages/sdk-typescript/scripts/qa-routing.mjs new file mode 100644 index 0000000..49256e1 --- /dev/null +++ b/packages/sdk-typescript/scripts/qa-routing.mjs @@ -0,0 +1,36 @@ +const REST_ORIGIN = { + production: "https://api.gemini.com", + sandbox: "https://api.sandbox.gemini.com", +}; +const WEBSOCKET_ORIGIN = { + production: "wss://ws.gemini.com", + sandbox: "wss://ws.sandbox.gemini.com", +}; + +function qaUrl(value, protocol, label) { + const url = new URL(value); + if (url.protocol !== protocol) throw new Error(`${label} must use ${protocol.slice(0, -1)}`); + if (url.username || url.password) throw new Error(`${label} must not contain credentials`); + if (url.search || url.hash) throw new Error(`${label} must not contain a query or fragment`); + return url; +} + +export function rewriteRestUrl(requestUrl, qaRestUrl, environment = "sandbox") { + const request = new URL(requestUrl); + if (request.origin !== REST_ORIGIN[environment]) { + throw new Error(`refusing to reroute unexpected REST origin ${request.origin}`); + } + const target = qaUrl(qaRestUrl, "https:", "QA REST URL"); + if (target.pathname !== "/") throw new Error("QA REST URL must be an origin without a path"); + return `${target.origin}${request.pathname}${request.search}`; +} + +export function rewriteWebSocketUrl(requestUrl, qaWebSocketUrl, environment = "sandbox") { + const request = new URL(requestUrl); + if (request.origin !== WEBSOCKET_ORIGIN[environment]) { + throw new Error(`refusing to reroute unexpected WebSocket origin ${request.origin}`); + } + const target = qaUrl(qaWebSocketUrl, "wss:", "QA WebSocket URL"); + target.search = request.search; + return target.href; +} diff --git a/packages/sdk-typescript/scripts/qa-routing.test.mjs b/packages/sdk-typescript/scripts/qa-routing.test.mjs new file mode 100644 index 0000000..2100869 --- /dev/null +++ b/packages/sdk-typescript/scripts/qa-routing.test.mjs @@ -0,0 +1,62 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { rewriteRestUrl, rewriteWebSocketUrl } from "./qa-routing.mjs"; + +test("QA REST routing changes only the sandbox origin", () => { + assert.equal( + rewriteRestUrl( + "https://api.sandbox.gemini.com/v1/prediction-markets/events?status=active", + "https://api.qa100.aurora7.net", + ), + "https://api.qa100.aurora7.net/v1/prediction-markets/events?status=active", + ); +}); + +test("QA routing can preserve production protocol behavior while replacing its origins", () => { + assert.equal( + rewriteRestUrl( + "https://api.gemini.com/v1/prediction-markets/events", + "https://api.qa100.aurora7.net", + "production", + ), + "https://api.qa100.aurora7.net/v1/prediction-markets/events", + ); + assert.equal( + rewriteWebSocketUrl( + "wss://ws.gemini.com/?snapshot=-1", + "wss://api.qa100.aurora7.net/feed", + "production", + ), + "wss://api.qa100.aurora7.net/feed?snapshot=-1", + ); +}); + +test("QA WebSocket routing retains its configured path and the SDK snapshot query", () => { + assert.equal( + rewriteWebSocketUrl( + "wss://ws.sandbox.gemini.com/?snapshot=-1", + "wss://feed.qa100.aurora7.net/prediction-markets", + ), + "wss://feed.qa100.aurora7.net/prediction-markets?snapshot=-1", + ); +}); + +test("QA routing rejects non-TLS targets and unexpected source origins", () => { + assert.throws( + () => rewriteRestUrl("https://api.sandbox.gemini.com/v1/test", "http://localhost:3000"), + /QA REST URL must use https/, + ); + assert.throws( + () => rewriteWebSocketUrl("wss://ws.sandbox.gemini.com", "ws://localhost:3000"), + /QA WebSocket URL must use wss/, + ); + assert.throws( + () => rewriteRestUrl("https://api.gemini.com/v1/test", "https://api.qa100.aurora7.net"), + /refusing to reroute unexpected REST origin/, + ); + assert.throws( + () => rewriteWebSocketUrl("wss://ws.gemini.com", "wss://feed.qa100.aurora7.net"), + /refusing to reroute unexpected WebSocket origin/, + ); +}); diff --git a/packages/sdk-typescript/scripts/rest-operation-ownership.mjs b/packages/sdk-typescript/scripts/rest-operation-ownership.mjs new file mode 100644 index 0000000..dc97033 --- /dev/null +++ b/packages/sdk-typescript/scripts/rest-operation-ownership.mjs @@ -0,0 +1,118 @@ +export const REST_OPERATION_OWNERSHIP = { + modules: [ + { id: "predictionMarkets", specs: ["predictionMarkets"], tags: ["Combos", "Markets", "Positions", "Rewards", "Terms", "Trading", "Volume"] }, + { id: "marketData", tags: ["Market Data"] }, + { id: "trading", tags: ["Orders", "Session"] }, + { id: "margin", tags: ["Margin Trading"] }, + { id: "perpetuals", tags: ["Derivatives"] }, + { id: "accountServices", tags: ["Account Administration", "Fund Management", "OAuth", "Staking"] }, + { id: "clearingInstant", tags: ["Clearing", "Instant"] }, + ], + operationOverrides: { + "rest:getFundingAmountReportFile": { responseMode: "file" }, + "rest:getFundingPaymentReportFile": { responseMode: "file" }, + }, +}; + +function operationKey(operation) { + return `${operation.spec}:${operation.operationId}`; +} + +function isJsonResponse(operation) { + return operation.successResponses.length > 0 && + operation.successResponses.every((response) => + response.contentTypes.length === 1 && response.contentTypes[0] === "application/json"); +} + +function isFileResponse(operation) { + return operation.successResponses.length === 1 && + operation.successResponses[0].contentTypes.length > 0 && + !operation.successResponses[0].contentTypes.includes("application/json"); +} + +function responseModeFor(key, operation, override = {}) { + if (override.responseMode && override.responseMode !== "json" && override.responseMode !== "file") { + throw new Error(`${key} responseMode must be json or file`); + } + if (override.responseMode === "json" && !isJsonResponse(operation)) { + throw new Error(`${key} must have exactly one 2xx application/json response`); + } + if (override.responseMode === "file" && !isFileResponse(operation)) { + throw new Error(`${key} must have exactly one 2xx file response`); + } + if (override.responseMode) return override.responseMode; + if (isJsonResponse(operation)) return "json"; + if (isFileResponse(operation)) return "file"; + return undefined; +} + +function moduleOwnsOperation(module, operation) { + return (!module.specs || module.specs.includes(operation.spec)) && + module.tags.some((tag) => operation.tags.includes(tag)); +} + +export function validateRestOperationOwnership(operations, manifest = REST_OPERATION_OWNERSHIP) { + const operationsByKey = new Map(operations.map((operation) => [operationKey(operation), operation])); + const owned = []; + + for (const module of manifest.modules) { + for (const tag of module.tags) { + if (!operations.some((operation) => moduleOwnsOperation({ ...module, tags: [tag] }, operation))) { + throw new Error(`REST tag not found: ${tag}`); + } + } + } + for (const key of Object.keys(manifest.operationOverrides ?? {})) { + if (!operationsByKey.has(key)) throw new Error(`REST operation not found: ${key}`); + responseModeFor(key, operationsByKey.get(key), manifest.operationOverrides[key]); + } + + for (const operation of operations) { + const key = operationKey(operation); + const modules = manifest.modules.filter((module) => moduleOwnsOperation(module, operation)); + if (modules.length === 0) throw new Error(`Unowned REST operation: ${key}`); + if (modules.length > 1) throw new Error(`Duplicate REST operation ownership: ${key}`); + const override = manifest.operationOverrides?.[key] ?? {}; + const responseMode = responseModeFor(key, operation, override); + if (!responseMode) { + throw new Error(`${key} must have exactly one 2xx application/json response or an explicit file override`); + } + owned.push({ ...operation, module: modules[0].id, methodName: override.methodName ?? operation.operationId, responseMode }); + } + + const methodNames = new Set(); + for (const operation of owned) { + const key = `${operation.module}:${operation.methodName}`; + if (methodNames.has(key)) throw new Error(`Duplicate methodName in ${operation.module}: ${operation.methodName}`); + methodNames.add(key); + } + return owned; +} + +export function ownedOperationsForModule(operations, { module, spec }, manifest = REST_OPERATION_OWNERSHIP) { + const selectedModule = manifest.modules.find((candidate) => candidate.id === module); + if (!selectedModule) throw new Error(`REST module not found: ${module}`); + const owned = []; + for (const operation of operations) { + if (operation.spec !== spec || !moduleOwnsOperation(selectedModule, operation)) continue; + const key = operationKey(operation); + const override = manifest.operationOverrides?.[key] ?? {}; + const responseMode = responseModeFor(key, operation, override); + if (responseMode) { + owned.push({ ...operation, module, methodName: override.methodName ?? operation.operationId, responseMode }); + } + } + return owned; +} + +export function createRestOperationOwnershipReport(operations, manifest = REST_OPERATION_OWNERSHIP) { + return validateRestOperationOwnership(operations, manifest) + .map(({ spec, module, operationId, methodName, responseMode, method, path, tags }) => ({ + spec, module, operationId, methodName, responseMode, method, path, tags, + })) + .sort((left, right) => { + const leftKey = `${left.spec}:${left.module}:${left.operationId}`; + const rightKey = `${right.spec}:${right.module}:${right.operationId}`; + return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0; + }); +} diff --git a/packages/sdk-typescript/scripts/rest-operation-ownership.snapshot.json b/packages/sdk-typescript/scripts/rest-operation-ownership.snapshot.json new file mode 100644 index 0000000..93bdf9a --- /dev/null +++ b/packages/sdk-typescript/scripts/rest-operation-ownership.snapshot.json @@ -0,0 +1,1262 @@ +[ + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "acceptPredictionMarketsTerms", + "methodName": "acceptPredictionMarketsTerms", + "responseMode": "json", + "method": "post", + "path": "/v1/prediction-markets/terms/accept", + "tags": [ + "Terms" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "cancelOrder", + "methodName": "cancelOrder", + "responseMode": "json", + "method": "post", + "path": "/v1/prediction-markets/order/cancel", + "tags": [ + "Trading" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "cancelOrderBatch", + "methodName": "cancelOrderBatch", + "responseMode": "json", + "method": "post", + "path": "/v1/prediction-markets/order/batch/cancel", + "tags": [ + "Trading" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "createCombo", + "methodName": "createCombo", + "responseMode": "json", + "method": "post", + "path": "/v1/prediction-markets/combos", + "tags": [ + "Combos" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getActiveOrders", + "methodName": "getActiveOrders", + "responseMode": "json", + "method": "post", + "path": "/v1/prediction-markets/orders/active", + "tags": [ + "Positions" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getCategories", + "methodName": "getCategories", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/categories", + "tags": [ + "Markets" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getComboByInstrumentSymbol", + "methodName": "getComboByInstrumentSymbol", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/combos/{instrumentSymbol}", + "tags": [ + "Combos" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getEvent", + "methodName": "getEvent", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/events/{eventTicker}", + "tags": [ + "Markets" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getEventStrike", + "methodName": "getEventStrike", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/events/{eventTicker}/strike", + "tags": [ + "Markets" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getLiquidityRewardsConfig", + "methodName": "getLiquidityRewardsConfig", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/liquidity-rewards/config", + "tags": [ + "Rewards" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getLiquidityRewardsDailySummary", + "methodName": "getLiquidityRewardsDailySummary", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/liquidity-rewards/summary/daily", + "tags": [ + "Rewards" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getLiquidityRewardsLifetimeSummary", + "methodName": "getLiquidityRewardsLifetimeSummary", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/liquidity-rewards/summary/total", + "tags": [ + "Rewards" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getMakerRebateLifetimeSummary", + "methodName": "getMakerRebateLifetimeSummary", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/maker-rebate/summary/total", + "tags": [ + "Rewards" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getMakerRebateRates", + "methodName": "getMakerRebateRates", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/maker-rebate/rates", + "tags": [ + "Rewards" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getOrderHistory", + "methodName": "getOrderHistory", + "responseMode": "json", + "method": "post", + "path": "/v1/prediction-markets/orders/history", + "tags": [ + "Positions" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getPositions", + "methodName": "getPositions", + "responseMode": "json", + "method": "post", + "path": "/v1/prediction-markets/positions", + "tags": [ + "Positions" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getPredictionMarketDailyVolume", + "methodName": "getPredictionMarketDailyVolume", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/volume/{date}", + "tags": [ + "Volume" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getPredictionMarketHourlyVolume", + "methodName": "getPredictionMarketHourlyVolume", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/volume/{date}/hourly", + "tags": [ + "Volume" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getPredictionMarketsTerms", + "methodName": "getPredictionMarketsTerms", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/terms", + "tags": [ + "Terms" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getPredictionMarketsTermsStatus", + "methodName": "getPredictionMarketsTermsStatus", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/terms/status", + "tags": [ + "Terms" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getSettledPositions", + "methodName": "getSettledPositions", + "responseMode": "json", + "method": "post", + "path": "/v1/prediction-markets/positions/settled", + "tags": [ + "Positions" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "getVolumeMetrics", + "methodName": "getVolumeMetrics", + "responseMode": "json", + "method": "post", + "path": "/v1/prediction-markets/metrics/volume", + "tags": [ + "Positions" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "listCombos", + "methodName": "listCombos", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/combos", + "tags": [ + "Combos" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "listEvents", + "methodName": "listEvents", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/events", + "tags": [ + "Markets" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "listLiquidityRewardsEvents", + "methodName": "listLiquidityRewardsEvents", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/liquidity-rewards/events", + "tags": [ + "Rewards" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "listMakerRebatePayouts", + "methodName": "listMakerRebatePayouts", + "responseMode": "json", + "method": "post", + "path": "/v1/prediction-markets/maker-rebate/payouts", + "tags": [ + "Rewards" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "listNewlyListedEvents", + "methodName": "listNewlyListedEvents", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/events/newly-listed", + "tags": [ + "Markets" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "listRecentlySettledEvents", + "methodName": "listRecentlySettledEvents", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/events/recently-settled", + "tags": [ + "Markets" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "listUpcomingEvents", + "methodName": "listUpcomingEvents", + "responseMode": "json", + "method": "get", + "path": "/v1/prediction-markets/events/upcoming", + "tags": [ + "Markets" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "placeOrder", + "methodName": "placeOrder", + "responseMode": "json", + "method": "post", + "path": "/v1/prediction-markets/order", + "tags": [ + "Trading" + ] + }, + { + "spec": "predictionMarkets", + "module": "predictionMarkets", + "operationId": "placeOrderBatch", + "methodName": "placeOrderBatch", + "responseMode": "json", + "method": "post", + "path": "/v1/prediction-markets/order/batch", + "tags": [ + "Trading" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "addBank", + "methodName": "addBank", + "responseMode": "json", + "method": "post", + "path": "/v1/payments/addbank", + "tags": [ + "Fund Management" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "addBankCAD", + "methodName": "addBankCAD", + "responseMode": "json", + "method": "post", + "path": "/v1/payments/addbank/cad", + "tags": [ + "Fund Management" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "createNewAccount", + "methodName": "createNewAccount", + "responseMode": "json", + "method": "post", + "path": "/v1/account/create", + "tags": [ + "Account Administration" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "createNewApprovedAddress", + "methodName": "createNewApprovedAddress", + "responseMode": "json", + "method": "post", + "path": "/v1/approvedAddresses/{network}/request", + "tags": [ + "Fund Management" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "createNewDepositAddress", + "methodName": "createNewDepositAddress", + "responseMode": "json", + "method": "post", + "path": "/v1/deposit/{network}/newAddress", + "tags": [ + "Fund Management" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "getAccountDetail", + "methodName": "getAccountDetail", + "responseMode": "json", + "method": "post", + "path": "/v1/account", + "tags": [ + "Account Administration" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "getAvailableBalances", + "methodName": "getAvailableBalances", + "responseMode": "json", + "method": "post", + "path": "/v1/balances", + "tags": [ + "Fund Management" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "getGasFeeEstimation", + "methodName": "getGasFeeEstimation", + "responseMode": "json", + "method": "post", + "path": "/v2/withdraw/{network}/{ticker}/feeEstimate", + "tags": [ + "Fund Management" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "getNotionalBalances", + "methodName": "getNotionalBalances", + "responseMode": "json", + "method": "post", + "path": "/v1/notionalbalances/{currency}", + "tags": [ + "Fund Management" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "getRoles", + "methodName": "getRoles", + "responseMode": "json", + "method": "post", + "path": "/v1/roles", + "tags": [ + "Account Administration" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "getTransactionHistory", + "methodName": "getTransactionHistory", + "responseMode": "json", + "method": "post", + "path": "/v1/transactions", + "tags": [ + "Fund Management" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "listAccountsInGroup", + "methodName": "listAccountsInGroup", + "responseMode": "json", + "method": "post", + "path": "/v1/account/list", + "tags": [ + "Account Administration" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "listApprovedAddresses", + "methodName": "listApprovedAddresses", + "responseMode": "json", + "method": "post", + "path": "/v1/approvedAddresses/account/{network}", + "tags": [ + "Fund Management" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "listCustodyFeeTransfers", + "methodName": "listCustodyFeeTransfers", + "responseMode": "json", + "method": "post", + "path": "/v1/custodyaccountfees", + "tags": [ + "Fund Management" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "listDepositAddresses", + "methodName": "listDepositAddresses", + "responseMode": "json", + "method": "post", + "path": "/v1/addresses/{network}", + "tags": [ + "Fund Management" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "listPastTransfers", + "methodName": "listPastTransfers", + "responseMode": "json", + "method": "post", + "path": "/v2/transfers", + "tags": [ + "Fund Management" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "listPaymentMethods", + "methodName": "listPaymentMethods", + "responseMode": "json", + "method": "post", + "path": "/v1/payments/methods", + "tags": [ + "Fund Management" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "listStakingBalances", + "methodName": "listStakingBalances", + "responseMode": "json", + "method": "post", + "path": "/v1/balances/staking", + "tags": [ + "Staking" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "listStakingEventHistory", + "methodName": "listStakingEventHistory", + "responseMode": "json", + "method": "post", + "path": "/v1/staking/history", + "tags": [ + "Staking" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "listStakingRates", + "methodName": "listStakingRates", + "responseMode": "json", + "method": "get", + "path": "/v1/staking/rates", + "tags": [ + "Staking" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "listStakingRewards", + "methodName": "listStakingRewards", + "responseMode": "json", + "method": "post", + "path": "/v1/staking/rewards", + "tags": [ + "Staking" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "removeApprovedAddress", + "methodName": "removeApprovedAddress", + "responseMode": "json", + "method": "post", + "path": "/v1/approvedAddresses/{network}/remove", + "tags": [ + "Fund Management" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "renameAccount", + "methodName": "renameAccount", + "responseMode": "json", + "method": "post", + "path": "/v1/account/rename", + "tags": [ + "Account Administration" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "revokeOAuthToken", + "methodName": "revokeOAuthToken", + "responseMode": "json", + "method": "post", + "path": "/v1/oauth/revokeByToken", + "tags": [ + "OAuth" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "stakeCryptoFunds", + "methodName": "stakeCryptoFunds", + "responseMode": "json", + "method": "post", + "path": "/v1/staking/stake", + "tags": [ + "Staking" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "transferBetweenAccounts", + "methodName": "transferBetweenAccounts", + "responseMode": "json", + "method": "post", + "path": "/v1/account/transfer/{currency}", + "tags": [ + "Fund Management" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "unstakeCryptoFunds", + "methodName": "unstakeCryptoFunds", + "responseMode": "json", + "method": "post", + "path": "/v1/staking/unstake", + "tags": [ + "Staking" + ] + }, + { + "spec": "rest", + "module": "accountServices", + "operationId": "withdrawCryptoFunds", + "methodName": "withdrawCryptoFunds", + "responseMode": "json", + "method": "post", + "path": "/v2/withdraw/{network}/{ticker}", + "tags": [ + "Fund Management" + ] + }, + { + "spec": "rest", + "module": "clearingInstant", + "operationId": "cancelClearingOrder", + "methodName": "cancelClearingOrder", + "responseMode": "json", + "method": "post", + "path": "/v1/clearing/cancel", + "tags": [ + "Clearing" + ] + }, + { + "spec": "rest", + "module": "clearingInstant", + "operationId": "confirmClearingOrder", + "methodName": "confirmClearingOrder", + "responseMode": "json", + "method": "post", + "path": "/v1/clearing/confirm", + "tags": [ + "Clearing" + ] + }, + { + "spec": "rest", + "module": "clearingInstant", + "operationId": "createNewBrokerOrder", + "methodName": "createNewBrokerOrder", + "responseMode": "json", + "method": "post", + "path": "/v1/clearing/broker/new", + "tags": [ + "Clearing" + ] + }, + { + "spec": "rest", + "module": "clearingInstant", + "operationId": "createNewClearingOrder", + "methodName": "createNewClearingOrder", + "responseMode": "json", + "method": "post", + "path": "/v1/clearing/new", + "tags": [ + "Clearing" + ] + }, + { + "spec": "rest", + "module": "clearingInstant", + "operationId": "executeInstantOrder", + "methodName": "executeInstantOrder", + "responseMode": "json", + "method": "post", + "path": "/v1/instant/execute", + "tags": [ + "Instant" + ] + }, + { + "spec": "rest", + "module": "clearingInstant", + "operationId": "getClearingOrder", + "methodName": "getClearingOrder", + "responseMode": "json", + "method": "post", + "path": "/v1/clearing/status", + "tags": [ + "Clearing" + ] + }, + { + "spec": "rest", + "module": "clearingInstant", + "operationId": "getInstantQuote", + "methodName": "getInstantQuote", + "responseMode": "json", + "method": "post", + "path": "/v1/instant/quote", + "tags": [ + "Instant" + ] + }, + { + "spec": "rest", + "module": "clearingInstant", + "operationId": "listClearingBrokers", + "methodName": "listClearingBrokers", + "responseMode": "json", + "method": "post", + "path": "/v1/clearing/broker/list", + "tags": [ + "Clearing" + ] + }, + { + "spec": "rest", + "module": "clearingInstant", + "operationId": "listClearingOrders", + "methodName": "listClearingOrders", + "responseMode": "json", + "method": "post", + "path": "/v1/clearing/list", + "tags": [ + "Clearing" + ] + }, + { + "spec": "rest", + "module": "clearingInstant", + "operationId": "listClearingTrades", + "methodName": "listClearingTrades", + "responseMode": "json", + "method": "post", + "path": "/v1/clearing/trades", + "tags": [ + "Clearing" + ] + }, + { + "spec": "rest", + "module": "margin", + "operationId": "getMarginAccount", + "methodName": "getMarginAccount", + "responseMode": "json", + "method": "post", + "path": "/v1/margin/account", + "tags": [ + "Margin Trading" + ] + }, + { + "spec": "rest", + "module": "margin", + "operationId": "getMarginRates", + "methodName": "getMarginRates", + "responseMode": "json", + "method": "post", + "path": "/v1/margin/rates", + "tags": [ + "Margin Trading" + ] + }, + { + "spec": "rest", + "module": "margin", + "operationId": "previewMarginOrder", + "methodName": "previewMarginOrder", + "responseMode": "json", + "method": "post", + "path": "/v1/margin/order/preview", + "tags": [ + "Margin Trading" + ] + }, + { + "spec": "rest", + "module": "marketData", + "operationId": "getAssetsForNetwork", + "methodName": "getAssetsForNetwork", + "responseMode": "json", + "method": "get", + "path": "/v2/networks/{network}/assets", + "tags": [ + "Market Data" + ] + }, + { + "spec": "rest", + "module": "marketData", + "operationId": "getCurrentOrderBook", + "methodName": "getCurrentOrderBook", + "responseMode": "json", + "method": "get", + "path": "/v1/book/{symbol}", + "tags": [ + "Market Data" + ] + }, + { + "spec": "rest", + "module": "marketData", + "operationId": "getFXRate", + "methodName": "getFXRate", + "responseMode": "json", + "method": "get", + "path": "/v2/fxrate/{symbol}/{timestamp}", + "tags": [ + "Market Data" + ] + }, + { + "spec": "rest", + "module": "marketData", + "operationId": "getFundingAmount", + "methodName": "getFundingAmount", + "responseMode": "json", + "method": "get", + "path": "/v1/fundingamount/{symbol}", + "tags": [ + "Market Data" + ] + }, + { + "spec": "rest", + "module": "marketData", + "operationId": "getFundingAmountReportFile", + "methodName": "getFundingAmountReportFile", + "responseMode": "file", + "method": "get", + "path": "/v1/fundingamountreport/records.xlsx", + "tags": [ + "Market Data" + ] + }, + { + "spec": "rest", + "module": "marketData", + "operationId": "getSymbolDetails", + "methodName": "getSymbolDetails", + "responseMode": "json", + "method": "get", + "path": "/v1/symbols/details/{symbol}", + "tags": [ + "Market Data" + ] + }, + { + "spec": "rest", + "module": "marketData", + "operationId": "getTicker", + "methodName": "getTicker", + "responseMode": "json", + "method": "get", + "path": "/v1/pubticker/{symbol}", + "tags": [ + "Market Data" + ] + }, + { + "spec": "rest", + "module": "marketData", + "operationId": "getTickerV2", + "methodName": "getTickerV2", + "responseMode": "json", + "method": "get", + "path": "/v2/ticker/{symbol}", + "tags": [ + "Market Data" + ] + }, + { + "spec": "rest", + "module": "marketData", + "operationId": "getTokenNetworkV2", + "methodName": "getTokenNetworkV2", + "responseMode": "json", + "method": "get", + "path": "/v2/network/{token}", + "tags": [ + "Market Data" + ] + }, + { + "spec": "rest", + "module": "marketData", + "operationId": "listCandles", + "methodName": "listCandles", + "responseMode": "json", + "method": "get", + "path": "/v2/candles/{symbol}/{time_frame}", + "tags": [ + "Market Data" + ] + }, + { + "spec": "rest", + "module": "marketData", + "operationId": "listDerivativeCandles", + "methodName": "listDerivativeCandles", + "responseMode": "json", + "method": "get", + "path": "/v2/derivatives/candles/{symbol}/{time_frame}", + "tags": [ + "Market Data" + ] + }, + { + "spec": "rest", + "module": "marketData", + "operationId": "listFeePromos", + "methodName": "listFeePromos", + "responseMode": "json", + "method": "get", + "path": "/v1/feepromos", + "tags": [ + "Market Data" + ] + }, + { + "spec": "rest", + "module": "marketData", + "operationId": "listPrices", + "methodName": "listPrices", + "responseMode": "json", + "method": "get", + "path": "/v1/pricefeed", + "tags": [ + "Market Data" + ] + }, + { + "spec": "rest", + "module": "marketData", + "operationId": "listSymbols", + "methodName": "listSymbols", + "responseMode": "json", + "method": "get", + "path": "/v1/symbols", + "tags": [ + "Market Data" + ] + }, + { + "spec": "rest", + "module": "marketData", + "operationId": "listTrades", + "methodName": "listTrades", + "responseMode": "json", + "method": "get", + "path": "/v1/trades/{symbol}", + "tags": [ + "Market Data" + ] + }, + { + "spec": "rest", + "module": "perpetuals", + "operationId": "getAccountMargin", + "methodName": "getAccountMargin", + "responseMode": "json", + "method": "post", + "path": "/v1/margin", + "tags": [ + "Derivatives" + ] + }, + { + "spec": "rest", + "module": "perpetuals", + "operationId": "getFundingPaymentReportFile", + "methodName": "getFundingPaymentReportFile", + "responseMode": "file", + "method": "get", + "path": "/v1/perpetuals/fundingpaymentreport/records.xlsx", + "tags": [ + "Derivatives" + ] + }, + { + "spec": "rest", + "module": "perpetuals", + "operationId": "getFundingPaymentReportJson", + "methodName": "getFundingPaymentReportJson", + "responseMode": "json", + "method": "post", + "path": "/v1/perpetuals/fundingpaymentreport/records.json", + "tags": [ + "Derivatives" + ] + }, + { + "spec": "rest", + "module": "perpetuals", + "operationId": "getOpenPositions", + "methodName": "getOpenPositions", + "responseMode": "json", + "method": "post", + "path": "/v1/positions", + "tags": [ + "Derivatives" + ] + }, + { + "spec": "rest", + "module": "perpetuals", + "operationId": "getRiskStats", + "methodName": "getRiskStats", + "responseMode": "json", + "method": "get", + "path": "/v1/riskstats/{symbol}", + "tags": [ + "Derivatives" + ] + }, + { + "spec": "rest", + "module": "perpetuals", + "operationId": "listFundingPayments", + "methodName": "listFundingPayments", + "responseMode": "json", + "method": "post", + "path": "/v1/perpetuals/fundingPayment", + "tags": [ + "Derivatives" + ] + }, + { + "spec": "rest", + "module": "trading", + "operationId": "cancelAllActiveOrders", + "methodName": "cancelAllActiveOrders", + "responseMode": "json", + "method": "post", + "path": "/v1/order/cancel/all", + "tags": [ + "Orders" + ] + }, + { + "spec": "rest", + "module": "trading", + "operationId": "cancelAllSessionOrders", + "methodName": "cancelAllSessionOrders", + "responseMode": "json", + "method": "post", + "path": "/v1/order/cancel/session", + "tags": [ + "Orders" + ] + }, + { + "spec": "rest", + "module": "trading", + "operationId": "cancelOrder", + "methodName": "cancelOrder", + "responseMode": "json", + "method": "post", + "path": "/v1/order/cancel", + "tags": [ + "Orders" + ] + }, + { + "spec": "rest", + "module": "trading", + "operationId": "createNewOrder", + "methodName": "createNewOrder", + "responseMode": "json", + "method": "post", + "path": "/v1/order/new", + "tags": [ + "Orders" + ] + }, + { + "spec": "rest", + "module": "trading", + "operationId": "getNotionalTradingVolume", + "methodName": "getNotionalTradingVolume", + "responseMode": "json", + "method": "post", + "path": "/v1/notionalvolume", + "tags": [ + "Orders" + ] + }, + { + "spec": "rest", + "module": "trading", + "operationId": "getOrderStatus", + "methodName": "getOrderStatus", + "responseMode": "json", + "method": "post", + "path": "/v1/order/status", + "tags": [ + "Orders" + ] + }, + { + "spec": "rest", + "module": "trading", + "operationId": "getTradingVolume", + "methodName": "getTradingVolume", + "responseMode": "json", + "method": "post", + "path": "/v1/tradevolume", + "tags": [ + "Orders" + ] + }, + { + "spec": "rest", + "module": "trading", + "operationId": "listActiveOrders", + "methodName": "listActiveOrders", + "responseMode": "json", + "method": "post", + "path": "/v1/orders", + "tags": [ + "Orders" + ] + }, + { + "spec": "rest", + "module": "trading", + "operationId": "listPastOrders", + "methodName": "listPastOrders", + "responseMode": "json", + "method": "post", + "path": "/v1/orders/history", + "tags": [ + "Orders" + ] + }, + { + "spec": "rest", + "module": "trading", + "operationId": "listPastTrades", + "methodName": "listPastTrades", + "responseMode": "json", + "method": "post", + "path": "/v1/mytrades", + "tags": [ + "Orders" + ] + }, + { + "spec": "rest", + "module": "trading", + "operationId": "sendHeartbeat", + "methodName": "sendHeartbeat", + "responseMode": "json", + "method": "post", + "path": "/v1/heartbeat", + "tags": [ + "Session" + ] + }, + { + "spec": "rest", + "module": "trading", + "operationId": "wrapOrder", + "methodName": "wrapOrder", + "responseMode": "json", + "method": "post", + "path": "/v1/wrap/{symbol}", + "tags": [ + "Orders" + ] + } +] diff --git a/packages/sdk-typescript/scripts/rest-operation-ownership.test.mjs b/packages/sdk-typescript/scripts/rest-operation-ownership.test.mjs new file mode 100644 index 0000000..0bf4695 --- /dev/null +++ b/packages/sdk-typescript/scripts/rest-operation-ownership.test.mjs @@ -0,0 +1,212 @@ +import assert from "node:assert/strict"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +import { discoverOperationInventory, loadOpenApiDocument } from "./openapi-rest-generator.mjs"; +import { + REST_OPERATION_OWNERSHIP, + createRestOperationOwnershipReport, + validateRestOperationOwnership, +} from "./rest-operation-ownership.mjs"; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const sdkDir = resolve(scriptDir, ".."); +const snapshotPath = resolve(scriptDir, "rest-operation-ownership.snapshot.json"); +const localPmSpec = resolve(sdkDir, "../../apis/prediction-markets.yaml"); +const localRestSpec = resolve(sdkDir, "../../apis/rest.yaml"); +const pmSpecPath = existsSync(localPmSpec) ? localPmSpec : "https://developer.gemini.com/specs/openapi/prediction-markets.yaml"; +const restSpecPath = existsSync(localRestSpec) ? localRestSpec : "https://developer.gemini.com/specs/openapi/rest.yaml"; + +async function realOperations() { + return (await Promise.all([ + ["predictionMarkets", pmSpecPath], + ["rest", restSpecPath], + ].map(async ([spec, specPath]) => discoverOperationInventory(await loadOpenApiDocument(specPath), { spec })))).flat(); +} + +function operation(overrides = {}) { + return { + spec: "rest", + operationId: "getTicker", + method: "get", + path: "/v1/pubticker/{symbol}", + tags: ["Market Data"], + successResponses: [{ status: 200, contentTypes: ["application/json"] }], + ...overrides, + }; +} + +function manifest(overrides = {}) { + return { + modules: [{ id: "marketData", tags: ["Market Data"] }], + operationOverrides: {}, + ...overrides, + }; +} + +test("real specs validate to exactly 105 owned operations", async () => { + assert.equal(validateRestOperationOwnership(await realOperations()).length, 105); +}); + +test("generated real-spec report matches the ownership snapshot", async () => { + const report = createRestOperationOwnershipReport(await realOperations()); + assert.deepEqual(JSON.parse(readFileSync(snapshotPath, "utf8")), report); +}); + +test("unowned operation fails", () => { + assert.throws( + () => validateRestOperationOwnership([operation()], manifest({ modules: [] })), + /Unowned REST operation: rest:getTicker/, + ); +}); + +test("duplicate ownership fails", () => { + assert.throws( + () => validateRestOperationOwnership([operation()], manifest({ + modules: [ + { id: "marketData", tags: ["Market Data"] }, + { id: "trading", tags: ["Market Data"] }, + ], + })), + /Duplicate REST operation ownership: rest:getTicker/, + ); +}); + +test("stale tag fails", () => { + assert.throws( + () => validateRestOperationOwnership([operation()], manifest({ modules: [{ id: "marketData", tags: ["Stale"] }] })), + /REST tag not found: Stale/, + ); +}); + +test("stale operation id fails", () => { + assert.throws( + () => validateRestOperationOwnership([operation()], manifest({ + operationOverrides: { "rest:doesNotExist": { responseMode: "file" } }, + })), + /REST operation not found: rest:doesNotExist/, + ); +}); + +test("missing operationId fails", () => { + assert.throws( + () => discoverOperationInventory({ paths: { "/v1/test": { get: { responses: {} } } } }, { spec: "rest" }), + /GET \/v1\/test is missing operationId/, + ); +}); + +test("duplicate methodName within a module fails", () => { + assert.throws( + () => validateRestOperationOwnership([ + operation(), + operation({ operationId: "getTickerV2", path: "/v2/ticker/{symbol}" }), + ], manifest({ + operationOverrides: { + "rest:getTicker": { methodName: "ticker" }, + "rest:getTickerV2": { methodName: "ticker" }, + }, + })), + /Duplicate methodName in marketData: ticker/, + ); +}); + +test("non-JSON 2xx response without an explicit override uses file mode", () => { + const [owned] = validateRestOperationOwnership([ + operation({ operationId: "download", successResponses: [{ status: 200, contentTypes: ["application/pdf"] }] }), + ], manifest()); + + assert.equal(owned.responseMode, "file"); +}); + +test("mixed JSON and file response without an explicit override fails", () => { + assert.throws( + () => validateRestOperationOwnership([ + operation({ successResponses: [{ status: 200, contentTypes: ["application/json", "text/csv"] }] }), + ], manifest()), + /rest:getTicker must have exactly one 2xx application\/json response or an explicit file override/, + ); +}); + +test("explicit json override fails for mixed JSON and file content", () => { + assert.throws( + () => validateRestOperationOwnership([ + operation({ successResponses: [{ status: 200, contentTypes: ["application/json", "text/csv"] }] }), + ], manifest({ + operationOverrides: { "rest:getTicker": { responseMode: "json" } }, + })), + /rest:getTicker must have exactly one 2xx application\/json response/, + ); +}); + +test("spec-scoped tags do not duplicate-own operations in another spec", () => { + const owned = validateRestOperationOwnership([ + operation({ spec: "predictionMarkets", operationId: "placeOrder", tags: ["Trading"] }), + operation({ tags: ["Trading"] }), + ], manifest({ + modules: [ + { id: "predictionMarkets", specs: ["predictionMarkets"], tags: ["Trading"] }, + { id: "trading", specs: ["rest"], tags: ["Trading"] }, + ], + })); + + assert.equal(owned.find(({ spec }) => spec === "rest").module, "trading"); +}); + +test("response overrides allow json and file modes", () => { + const [json] = validateRestOperationOwnership([operation()], manifest({ + operationOverrides: { "rest:getTicker": { responseMode: "json" } }, + })); + const [file] = validateRestOperationOwnership([ + operation({ operationId: "download", successResponses: [{ status: 200, contentTypes: ["text/csv"] }] }), + ], manifest({ + operationOverrides: { "rest:download": { responseMode: "file" } }, + })); + + assert.equal(json.responseMode, "json"); + assert.equal(file.responseMode, "file"); +}); + +test("invalid response override mode fails", () => { + assert.throws( + () => validateRestOperationOwnership([operation()], manifest({ + operationOverrides: { "rest:getTicker": { responseMode: "stream" } }, + })), + /rest:getTicker responseMode must be json or file/, + ); +}); + +test("multiple JSON 2xx responses use json mode", () => { + const [owned] = validateRestOperationOwnership([ + operation({ + successResponses: [ + { status: 200, contentTypes: ["application/json"] }, + { status: 201, contentTypes: ["application/json"] }, + ], + }), + ], manifest()); + + assert.equal(owned.responseMode, "json"); +}); + +test("explicit json override on multiple JSON 2xx responses uses json mode", () => { + const [owned] = validateRestOperationOwnership([ + operation({ + successResponses: [ + { status: 200, contentTypes: ["application/json"] }, + { status: 201, contentTypes: ["application/json"] }, + ], + }), + ], manifest({ + operationOverrides: { "rest:getTicker": { responseMode: "json" } }, + })); + + assert.equal(owned.responseMode, "json"); +}); + +test("production manifest owns all required module ids", () => { + assert.deepEqual(REST_OPERATION_OWNERSHIP.modules.map(({ id }) => id), [ + "predictionMarkets", "marketData", "trading", "margin", "perpetuals", "accountServices", "clearingInstant", + ]); +}); diff --git a/packages/sdk-typescript/scripts/smoke-oauth.mjs b/packages/sdk-typescript/scripts/smoke-oauth.mjs new file mode 100644 index 0000000..75c63ac --- /dev/null +++ b/packages/sdk-typescript/scripts/smoke-oauth.mjs @@ -0,0 +1,73 @@ +import assert from "node:assert/strict"; +import { createServer } from "node:http"; + +class MemoryStore { + record; + load() { return Promise.resolve(this.record); } + save(tokens) { this.record = tokens; return Promise.resolve(); } + clear() { this.record = undefined; return Promise.resolve(); } + runExclusive(operation) { return operation(); } +} + +async function callbackAt(redirectUri) { + const target = new URL(redirectUri); + let resolveCallback; + const callback = new Promise((resolve) => { resolveCallback = resolve; }); + const server = createServer((request, response) => { + const url = new URL(request.url, redirectUri); + response.end("Authorization received. Return to the terminal."); + resolveCallback(url); + }); + await new Promise((resolve, reject) => server.listen(Number(target.port), target.hostname, resolve).once("error", reject)); + return { server, callback: Promise.race([callback, new Promise((_, reject) => setTimeout(() => reject(new Error("OAuth callback timeout")), 120_000))]) }; +} + +const { GeminiMarkets, HttpTransport, OAuthAuth, PredictionMarketsRest } = await import("../dist/server/index.js"); +const clientId = process.env.GEMINI_OAUTH_CLIENT_ID; +const redirectUri = process.env.GEMINI_OAUTH_REDIRECT_URI; +if (!clientId || !redirectUri) throw new Error("GEMINI_OAUTH_CLIENT_ID and GEMINI_OAUTH_REDIRECT_URI are required"); +const environment = process.env.GEMINI_OAUTH_ENV ?? "sandbox"; +if (environment !== "production" && environment !== "sandbox") throw new Error("GEMINI_OAUTH_ENV must be production or sandbox"); +const client = process.env.GEMINI_OAUTH_CLIENT_SECRET + ? { type: "confidential", clientId, clientSecret: process.env.GEMINI_OAUTH_CLIENT_SECRET, redirectUri } + : { type: "public", clientId, redirectUri }; +const store = new MemoryStore(); +const auth = new OAuthAuth({ client, env: environment, tokenStore: store }); +const listener = await callbackAt(redirectUri); +let facade; +try { + const authorization = await auth.beginAuthorization((process.env.GEMINI_OAUTH_SCOPES ?? "orders").split(",")); + console.log("Open this authorization URL:", authorization.url); + await auth.completeAuthorization(await listener.callback, authorization.transaction); + facade = new GeminiMarkets({ env: environment, auth }); + await facade.predictions.listEvents({ status: ["active"], limit: 1 }); + await facade.predictions.getPredictionMarketsTermsStatus(); + await facade.predictions.getPositions({ limit: 1 }); + await facade.predictions.getLiquidityRewardsLifetimeSummary(); + + let inspected = false; + const inspectingTransport = new HttpTransport({ env: environment, auth, fetchImpl: async (url, init) => { + assert(init.headers.Authorization?.startsWith("Bearer ")); + assert.equal(init.headers["X-GEMINI-APIKEY"], undefined); + assert.equal(init.headers["X-GEMINI-SIGNATURE"], undefined); + const payload = JSON.parse(Buffer.from(init.headers["X-GEMINI-PAYLOAD"], "base64").toString("utf8")); + assert.equal("nonce" in payload, false); + inspected = true; + return fetch(url, init); + } }); + await new PredictionMarketsRest(inspectingTransport).getPositions({ limit: 1 }); + assert(inspected); + + const previousRefresh = store.record.refreshToken; + const refreshAuth = new OAuthAuth({ client, env: environment, tokenStore: store, refreshSkewMs: Number.MAX_SAFE_INTEGER }); + const refreshTransport = new HttpTransport({ env: environment, auth: refreshAuth }); + await new PredictionMarketsRest(refreshTransport).getPositions({ limit: 1 }); + assert.notEqual(store.record.refreshToken, previousRefresh, "refresh token did not rotate"); + await refreshAuth.revoke(refreshTransport); + assert.equal(await store.load(), undefined); + await assert.rejects(new PredictionMarketsRest(refreshTransport).getPositions({ limit: 1 }), /complete authorization first/i); + console.log(`${environment} OAuth smoke passed: authorization, Bearer request, rotation, revocation`); +} finally { + facade?.close(); + await new Promise((resolve) => listener.server.close(resolve)); +} diff --git a/packages/sdk-typescript/scripts/smoke-sandbox.mjs b/packages/sdk-typescript/scripts/smoke-sandbox.mjs new file mode 100644 index 0000000..b920f3c --- /dev/null +++ b/packages/sdk-typescript/scripts/smoke-sandbox.mjs @@ -0,0 +1,105 @@ +import assert from "node:assert/strict"; +import { pathToFileURL } from "node:url"; + +export async function placeWithCleanup(predictions, order, verify = async () => {}) { + let orderId; + try { + const placed = await predictions.placeOrder(order); + orderId = placed.orderId; + await verify(placed); + } finally { + if (orderId !== undefined) await predictions.cancelOrder({ orderId }); + } +} + +export function smokeEnvironment(value = "sandbox") { + if (value !== "production" && value !== "sandbox") throw new Error("smoke environment must be production or sandbox"); + return value; +} + +export function smokeMarketType(args = process.argv.slice(2)) { + const value = args.find((arg) => arg.startsWith("--market-type="))?.slice("--market-type=".length); + if (value !== "market-data" && value !== "prediction-markets") { + throw new Error("pass --market-type=market-data or --market-type=prediction-markets"); + } + return value; +} + +export function predictionMarketSymbols(response) { + const events = Array.isArray(response) ? response : response?.data ?? response?.events ?? []; + return [...new Set(events.flatMap((event) => [...(event?.markets ?? []), ...(event?.contracts ?? [])]) + .map((market) => typeof market === "string" ? market : market?.symbol ?? market?.instrumentSymbol) + .filter(Boolean))]; +} + +export function marketDataSymbols(response) { + const symbols = Array.isArray(response) ? response : response?.data ?? response?.symbols ?? []; + return [...new Set(symbols + .map((entry) => typeof entry === "string" ? entry : entry?.symbol ?? entry?.pair) + .filter((symbol) => typeof symbol === "string" && symbol.toUpperCase().includes("BTC")))]; +} + +async function liveSnapshot(client, symbols) { + for (const symbol of symbols) { + console.log(`Sandbox order book: trying ${symbol} (10s timeout)`); + const book = client.orderBook(symbol); + try { + await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error("live book timeout")), 10_000); + book.on("update", () => { clearTimeout(timer); resolve(); }); + book.on("error", (error) => { clearTimeout(timer); reject(error); }); + }); + assert(book.snapshot().bids.length + book.snapshot().asks.length > 0, "empty live book"); + console.log(`Sandbox order book: received update for ${symbol}`); + return book; + } catch (error) { + book.close(); + if (error.message !== "live book timeout") throw error; + console.log(`Sandbox order book: ${symbol} timed out; trying next`); + } + } + throw new Error("no live book updates for the selected symbols"); +} + +async function main() { + const { createClient, HmacAuth } = await import("../dist/server/index.js"); + const environment = smokeEnvironment(process.env.GEMINI_SMOKE_ENV); + const marketType = smokeMarketType(); + const label = process.env.GEMINI_SMOKE_LABEL ?? environment; + const apiKey = process.env.GEMINI_API_KEY; + const apiSecret = process.env.GEMINI_API_SECRET; + if (!apiKey || !apiSecret) throw new Error("GEMINI_API_KEY and GEMINI_API_SECRET are required"); + const accept = process.argv.includes("--accept-terms"); + const place = process.argv.includes("--place-order"); + const client = await createClient({ env: environment, auth: new HmacAuth({ apiKey, apiSecret }) }); + let book; + try { + const events = marketType === "prediction-markets" + ? await client.predictions.listEvents({ status: ["active"], limit: 10 }) + : undefined; + const symbols = marketType === "prediction-markets" + ? predictionMarketSymbols(events) + : marketDataSymbols(await client.marketData.listSymbols()); + if (!symbols.length) throw new Error(`No active ${marketType} symbol was discovered`); + console.log(`Sandbox order book: type=${marketType}, discovered ${symbols.length} symbol(s)`); + book = await liveSnapshot(client, symbols); + if (marketType === "prediction-markets") { + const terms = await client.predictions.getPredictionMarketsTermsStatus(); + if (accept && !terms.hasAcceptedLatest) await client.predictions.acceptTerms(); + const positions = await client.predictions.getPositions({ limit: 1 }); + const rebates = await client.predictions.getMakerRebateLifetimeSummary(); + const rewards = await client.predictions.getLiquidityRewardsLifetimeSummary(); + console.log(`${label} read-only smoke passed`, { events: events.data?.length ?? events.events?.length ?? 0, positions: positions.positions?.length ?? 0, rebates: Boolean(rebates), rewards: Boolean(rewards) }); + } else { + console.log(`${label} market-data order-book smoke passed`); + } + if (place) { + if (marketType !== "prediction-markets") throw new Error("--place-order requires --market-type=prediction-markets"); + for (const name of ["GEMINI_PM_SIDE", "GEMINI_PM_OUTCOME", "GEMINI_PM_QUANTITY", "GEMINI_PM_PRICE"]) if (!process.env[name]) throw new Error(`${name} is required with --place-order`); + await placeWithCleanup(client.predictions, { symbol: symbols[0], orderType: "limit", side: process.env.GEMINI_PM_SIDE, outcome: process.env.GEMINI_PM_OUTCOME, quantity: process.env.GEMINI_PM_QUANTITY, price: process.env.GEMINI_PM_PRICE, makerOrCancel: false }); + console.log(`${label} order placed and cancelled`); + } + } finally { book?.close(); client.close(); } +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) await main(); diff --git a/packages/sdk-typescript/scripts/smoke-sandbox.test.mjs b/packages/sdk-typescript/scripts/smoke-sandbox.test.mjs new file mode 100644 index 0000000..ff283e6 --- /dev/null +++ b/packages/sdk-typescript/scripts/smoke-sandbox.test.mjs @@ -0,0 +1,36 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { marketDataSymbols, placeWithCleanup, predictionMarketSymbols, smokeEnvironment, smokeMarketType } from "./smoke-sandbox.mjs"; + +test("smoke environment defaults to sandbox and permits the internal QA production profile", () => { + assert.equal(smokeEnvironment(), "sandbox"); + assert.equal(smokeEnvironment("production"), "production"); + assert.throws(() => smokeEnvironment("qa"), /production or sandbox/); +}); + +test("requires the order-book market type from the caller", () => { + assert.equal(smokeMarketType(["--market-type=market-data"]), "market-data"); + assert.equal(smokeMarketType(["--market-type=prediction-markets"]), "prediction-markets"); + assert.throws(() => smokeMarketType([]), /pass --market-type/); +}); + +test("discovers prediction-market symbols from active markets", () => { + assert.deepEqual( + predictionMarketSymbols({ data: [{ symbol: "EVENT-SYMBOL", markets: [{ symbol: "GEMI-TEST" }] }] }), + ["GEMI-TEST"], + ); +}); + +test("discovers market-data symbols from the symbols response", () => { + assert.deepEqual( + marketDataSymbols({ data: ["btcusd", { pair: "ethusd" }, { pair: "btcgusdperp" }] }), + ["btcusd", "btcgusdperp"], + ); +}); + +test("successful placement always attempts cancellation after a later failure", async () => { + const calls = []; + const predictions = { async placeOrder() { calls.push("place"); return { orderId: 7n }; }, async cancelOrder({ orderId }) { calls.push(`cancel:${orderId}`); } }; + await assert.rejects(placeWithCleanup(predictions, {}, async () => { throw new Error("later failure"); }), /later failure/); + assert.deepEqual(calls, ["place", "cancel:7"]); +}); diff --git a/packages/sdk-typescript/scripts/verify-browser-bundle.mjs b/packages/sdk-typescript/scripts/verify-browser-bundle.mjs new file mode 100644 index 0000000..f7df990 --- /dev/null +++ b/packages/sdk-typescript/scripts/verify-browser-bundle.mjs @@ -0,0 +1,74 @@ +/** + * Bundle the browser entry point with esbuild and verify the output contains + * no Node-only references. This catches dynamic imports, tree-shaking failures, + * and issues the static import scanner (verify-browser-imports.mjs) misses. + */ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { createRequire } from "node:module"; + +const FORBIDDEN_PATTERNS = [ + /\brequire\s*\(\s*["']node:/g, + /\brequire\s*\(\s*["']ws["']\s*\)/g, + /\bfrom\s+["']node:/g, + /\bimport\s*\(\s*["']node:/g, + /\bimport\s*\(\s*["']ws["']\s*\)/g, +]; + +const temp = mkdtempSync(join(tmpdir(), "gemini-browser-bundle-")); +const outfile = join(temp, "browser-bundle.js"); + +try { + const entry = resolve("dist/browser/index.js"); + + const moduleRequire = createRequire(import.meta.url); + const esbuild = moduleRequire.resolve("esbuild/bin/esbuild"); + + execFileSync( + process.execPath, + [esbuild, + entry, + "--bundle", + "--platform=browser", + "--format=esm", + "--target=es2022", + `--outfile=${outfile}`, + // Don't error on missing externals — we want to see if they leak into output + "--log-level=warning", + ], + { stdio: "pipe", encoding: "utf8" }, + ); + + const bundle = readFileSync(outfile, "utf8"); + const violations = []; + + for (const pattern of FORBIDDEN_PATTERNS) { + let match; + // Reset lastIndex for global patterns + pattern.lastIndex = 0; + while ((match = pattern.exec(bundle)) !== null) { + // Find the line for context + const before = bundle.slice(Math.max(0, match.index - 60), match.index); + violations.push(`${match[0]} (near: …${before.split("\n").pop()}…)`); + } + } + + // Also check for HmacAuth class in the bundle + if (/\bHmacAuth\b/.test(bundle)) { + violations.push("HmacAuth reference found in browser bundle"); + } + + if (violations.length > 0) { + console.error("Browser bundle contains forbidden references:"); + for (const v of violations) console.error(` ${v}`); + process.exit(1); + } + + const sizeKb = (Buffer.byteLength(bundle) / 1024).toFixed(1); + console.log(`browser bundle clean: ${sizeKb} KB, 0 forbidden references`); +} finally { + rmSync(temp, { recursive: true, force: true }); +} diff --git a/packages/sdk-typescript/scripts/verify-browser-imports.mjs b/packages/sdk-typescript/scripts/verify-browser-imports.mjs new file mode 100644 index 0000000..5b1f4ba --- /dev/null +++ b/packages/sdk-typescript/scripts/verify-browser-imports.mjs @@ -0,0 +1,73 @@ +/** + * Verify the browser entry point's import graph contains no Node-only modules. + * Recursively follows every import starting from dist/browser/index.js and fails + * if any resolved file imports node:*, ws, or other server-only specifiers. + */ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; + +const FORBIDDEN = [ + /^node:/, + /^ws$/, +]; + +const root = resolve("dist"); +const entry = join(root, "browser", "index.js"); +const visited = new Set(); +const violations = []; + +function scan(filePath) { + const resolved = resolve(filePath); + if (visited.has(resolved)) return; + visited.add(resolved); + + const source = readFileSync(resolved, "utf8"); + // Match both static imports and re-exports: import ... from "X" / export ... from "X" + const importPattern = /(?:import|export)\s+.*?\s+from\s+["']([^"']+)["']/g; + let match; + while ((match = importPattern.exec(source)) !== null) { + const specifier = match[1]; + + // Check against forbidden patterns + for (const pattern of FORBIDDEN) { + if (pattern.test(specifier)) { + violations.push({ file: resolved.replace(root + "/", "dist/"), specifier }); + } + } + + // Follow relative imports into the dist tree + if (specifier.startsWith(".")) { + const target = resolve(dirname(resolved), specifier); + // Try .js directly, or as-is if it already resolves + for (const candidate of [target, target + ".js"]) { + try { + readFileSync(candidate); + scan(candidate); + break; + } catch { + // not found, try next candidate + } + } + } + } +} + +scan(entry); + +if (violations.length > 0) { + console.error("Browser entry point import graph contains forbidden specifiers:"); + for (const { file, specifier } of violations) { + console.error(` ${file} → ${specifier}`); + } + process.exit(1); +} + +// Also verify no HmacAuth reference in the browser dist output +const browserFiles = [...visited]; +for (const filePath of browserFiles) { + const source = readFileSync(filePath, "utf8"); + assert(!source.includes("HmacAuth"), `${filePath.replace(root + "/", "dist/")} references HmacAuth`); +} + +console.log(`browser import graph clean: ${visited.size} files scanned, 0 forbidden imports`); diff --git a/packages/sdk-typescript/scripts/verify-changeset.mjs b/packages/sdk-typescript/scripts/verify-changeset.mjs new file mode 100644 index 0000000..11e1894 --- /dev/null +++ b/packages/sdk-typescript/scripts/verify-changeset.mjs @@ -0,0 +1,49 @@ +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; + +if (process.env.GITHUB_EVENT_NAME !== "pull_request") { + console.log("Changeset enforcement applies to pull requests only"); + process.exit(0); +} + +const event = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")); +const pullRequest = event.pull_request; + +if (pullRequest.title === "Version Packages") { + console.log("Version Packages pull request does not require a new Changeset"); + process.exit(0); +} + +const changedFiles = execFileSync( + "git", + ["diff", "--name-only", `${pullRequest.base.sha}...${pullRequest.head.sha}`], + { encoding: "utf8" }, +) + .trim() + .split("\n") + .filter(Boolean); + +const sdkChanges = changedFiles.filter( + (file) => + file.startsWith("packages/sdk-typescript/") && + !file.startsWith("packages/sdk-typescript/.changeset/"), +); +const changesets = changedFiles.filter( + (file) => + /^packages\/sdk-typescript\/\.changeset\/[^/]+\.md$/.test(file) && + !file.endsWith("/README.md"), +); + +if (sdkChanges.length > 0 && changesets.length === 0) { + console.error( + "SDK changes require a Changeset in packages/sdk-typescript/.changeset/*.md", + ); + console.error(`Changed SDK files: ${sdkChanges.join(", ")}`); + process.exit(1); +} + +console.log( + sdkChanges.length === 0 + ? "No release-worthy SDK changes detected" + : `Changeset found for ${sdkChanges.length} SDK change(s)`, +); diff --git a/packages/sdk-typescript/scripts/verify-market-data-live.mjs b/packages/sdk-typescript/scripts/verify-market-data-live.mjs new file mode 100644 index 0000000..d87612a --- /dev/null +++ b/packages/sdk-typescript/scripts/verify-market-data-live.mjs @@ -0,0 +1,271 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, isAbsolute, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const PREREQUISITE = "Prerequisite missing: @gemini-markets/sdk GeminiMarkets.marketData facade is not available in the packed package."; +export const OPERATIONS = [ + ["listSymbols", "public JSON"], + ["getSymbolDetails", "public JSON"], + ["getTicker", "public JSON"], + ["getTickerV2", "public JSON"], + ["getCurrentOrderBook", "public JSON"], + ["listTrades", "public JSON"], + ["listPrices", "public JSON"], + ["listCandles", "public JSON"], + ["listDerivativeCandles", "public JSON"], + ["listFeePromos", "public JSON"], + ["getFundingAmount", "public JSON"], + ["getAssetsForNetwork", "authenticated JSON"], + ["getTokenNetworkV2", "authenticated JSON"], + ["getFXRate", "authenticated JSON"], + ["getFundingAmountReportFile", "public file"], +]; +const AUTHENTICATED = new Set(["getAssetsForNetwork", "getTokenNetworkV2", "getFXRate"]); + +export function redact(value, secrets = []) { + if (Array.isArray(value)) return value.map((item) => redact(item, secrets)); + if (value && typeof value === "object") { + return Object.fromEntries(Object.entries(value).map(([key, item]) => { + const sensitive = /authorization|auth|api[-_]?key|api[-_]?secret|signature|payload/i.test(key); + return sensitive ? [key, "[REDACTED]"] : [key, redact(item, secrets)]; + })); + } + if (typeof value !== "string") return value; + let redacted = value; + for (const secret of secrets) if (secret) redacted = redacted.replaceAll(secret, "[REDACTED]"); + return redacted + .replace(/\b(authorization|auth|x-gemini-(?:api[-_]?key|apikey|payload|signature)|api[-_]?key|api[-_]?secret|signature|payload)(\s*[:=]\s*)(?:[a-z]+\s+)?[^\s,&|]+/gi, "$1$2[REDACTED]") + .replace(/\b[a-z]+\s+[^\s,&|]+/gi, (match) => /^\s*(?:bearer|token|basic|hmac)\s+/i.test(match) ? match.replace(/\S+$/, "[REDACTED]") : match); +} + +export function verifyFileEvidence(response) { + const responseHeaders = response?.headers instanceof Headers + ? Object.fromEntries(response.headers) + : response?.headers ?? {}; + const headers = Object.fromEntries(Object.entries(responseHeaders).map(([key, value]) => [key.toLowerCase(), value])); + const bytes = response?.bytes ?? response?.body ?? response; + const byteLength = bytes?.byteLength; + if (byteLength === undefined) throw new Error("file response has no byteLength"); + if (!Number.isInteger(byteLength) || byteLength <= 0) throw new Error("file response has empty bytes"); + const contentType = response?.contentType ?? headers["content-type"]; + if (!contentType) throw new Error("file response has no content type"); + const contentDisposition = response?.contentDisposition ?? headers["content-disposition"]; + if (!contentDisposition) throw new Error("file response has no content disposition"); + return { byteLength, contentType, contentDisposition }; +} + +export function exitCodeFor(operations) { + if (operations.every(({ status }) => status === "passed")) return 0; + return 1; +} + +export function forwardedNodeExecArgs(execArgv, root) { + const forwarded = []; + const importSpecifier = (specifier) => { + if (specifier.startsWith(".") || isAbsolute(specifier)) { + return pathToFileURL(resolve(root, specifier)).href; + } + return specifier; + }; + for (let index = 0; index < execArgv.length; index += 1) { + const option = execArgv[index]; + if (option === "--use-system-ca") forwarded.push(option); + else if (option.startsWith("--env-file=")) forwarded.push(`--env-file=${resolve(root, option.slice("--env-file=".length))}`); + else if (option === "--env-file" && execArgv[index + 1]) forwarded.push(option, resolve(root, execArgv[++index])); + else if (option === "--import" && execArgv[index + 1]) forwarded.push(option, importSpecifier(execArgv[++index])); + else if (option.startsWith("--import=")) forwarded.push(`--import=${importSpecifier(option.slice("--import=".length))}`); + } + return forwarded; +} + +function verificationFollowUp(operation, message) { + return { + title: `${operation} live verification ${message}`, + body: `Operation: ${operation}\nResult: ${message}\nRe-run npm run verify:market-data:live after the SDK facade and endpoint configuration are available.`, + }; +} + +function operationResult(name, kind, status, message, evidence) { + return { + name, + kind, + status, + ...(message ? { message } : {}), + ...(evidence ? { evidence: redact(evidence) } : {}), + ...(status === "passed" ? {} : { followUp: verificationFollowUp(name, message) }), + }; +} + +function writeReports(reportDir, operations, secrets) { + mkdirSync(reportDir, { recursive: true }); + const redactedOperations = redact(operations, secrets); + const counts = Object.fromEntries( + ["passed", "failed", "blocked"].map((status) => [ + status, + redactedOperations.filter((operation) => operation.status === status).length, + ]), + ); + writeFileSync( + join(reportDir, "results.json"), + `${JSON.stringify({ generatedAt: new Date().toISOString(), operations: redactedOperations, counts }, null, 2)}\n`, + ); + writeFileSync( + join(reportDir, "summary.md"), + `# Market Data live verification\n\n${counts.passed} passed, ${counts.failed} failed, ${counts.blocked} blocked.\n\n| Operation | Status | Detail |\n| --- | --- | --- |\n${redactedOperations.map((operation) => `| ${operation.name} | ${operation.status} | ${operation.message ?? ""} |`).join("\n")}\n`, + ); + return { operations: redactedOperations, counts, reportDir }; +} + +function firstSymbol(symbols) { + return Array.isArray(symbols) ? symbols.find((symbol) => typeof symbol === "string") : undefined; +} + +function networks(networkResponse) { + return networkResponse?.network ?? networkResponse?.networks ?? []; +} + +async function responseBytes(response) { + if (response instanceof Response) return { bytes: new Uint8Array(await response.arrayBuffer()), headers: response.headers }; + return response; +} + +export async function runVerification({ reportDir, env = process.env, loadSdk = () => import(process.argv.includes("--consumer") ? "@gemini-markets/sdk/server" : "../dist/server/index.js") } = {}) { + const results = new Map(OPERATIONS.map(([name, kind]) => [name, operationResult(name, kind, "blocked", "Verification did not run.")])); + const secrets = [env.GEMINI_API_KEY, env.GEMINI_API_SECRET]; + const safeError = (error) => redact(error?.message ?? String(error), secrets); + let sdk; + try { + sdk = await loadSdk(); + } catch (error) { + for (const [name, kind] of OPERATIONS) results.set(name, operationResult(name, kind, "blocked", `Unable to import @gemini-markets/sdk: ${safeError(error)}`)); + return writeReports(reportDir, [...results.values()], secrets); + } + const publicClient = new sdk.GeminiMarkets({ env: env.GEMINI_MD_ENV ?? "production" }); + if (!publicClient.marketData) { + publicClient.close?.(); + return writeReports(reportDir, OPERATIONS.map(([name, kind]) => operationResult(name, kind, "blocked", PREREQUISITE)), secrets); + } + const call = async (client, name, args = []) => { + if (typeof client.marketData[name] !== "function") throw new Error("method is not available on GeminiMarkets.marketData"); + return client.marketData[name](...args); + }; + let symbols; + try { + symbols = await call(publicClient, "listSymbols"); + results.set("listSymbols", operationResult("listSymbols", "public JSON", "passed")); + } catch (error) { + results.set("listSymbols", operationResult("listSymbols", "public JSON", "failed", safeError(error))); + } + const symbol = env.GEMINI_MD_SYMBOL ?? firstSymbol(symbols); + const derivativeSymbol = env.GEMINI_MD_DERIVATIVE_SYMBOL ?? (Array.isArray(symbols) + ? symbols.find((candidate) => typeof candidate === "string" && candidate.toUpperCase().includes("PERP")) + : undefined); + const publicCalls = [ + ["getSymbolDetails", [{ symbol }], Boolean(symbol), "Required symbol discovery returned no usable symbol."], + ["getTicker", [{ symbol }], Boolean(symbol), "Required symbol discovery returned no usable symbol."], + ["getTickerV2", [{ symbol }], Boolean(symbol), "Required symbol discovery returned no usable symbol."], + ["getCurrentOrderBook", [{ symbol }, { limit_bids: 1, limit_asks: 1 }], Boolean(symbol), "Required symbol discovery returned no usable symbol."], + ["listTrades", [{ symbol }, { limit_trades: 1 }], Boolean(symbol), "Required symbol discovery returned no usable symbol."], + ["listPrices", [], true], + ["listCandles", [{ symbol, time_frame: "1m" }], Boolean(symbol), "Required symbol discovery returned no usable symbol."], + ["listDerivativeCandles", [{ symbol: derivativeSymbol, time_frame: "1m" }], Boolean(derivativeSymbol), "Required derivative symbol discovery returned no usable symbol."], + ["listFeePromos", [], true], + ["getFundingAmount", [{ symbol: derivativeSymbol }], Boolean(derivativeSymbol), "Required derivative symbol discovery returned no usable symbol."], + ["getFundingAmountReportFile", [{ symbol: derivativeSymbol }], Boolean(derivativeSymbol), "Required derivative symbol discovery returned no usable symbol."], + ]; + for (const [name, args, canRun, missingReason] of publicCalls) { + const category = name === "getFundingAmountReportFile" ? "public file" : "public JSON"; + if (!canRun) { + results.set(name, operationResult(name, category, "blocked", missingReason)); + continue; + } + try { + const response = await call(publicClient, name, args); + const evidence = name === "getFundingAmountReportFile" ? verifyFileEvidence(await responseBytes(response)) : undefined; + results.set(name, operationResult(name, category, "passed", undefined, evidence)); + } catch (error) { + const message = safeError(error); + const status = message.includes("method is not available") ? "blocked" : "failed"; + results.set(name, operationResult(name, category, status, message)); + } + } + const key = env.GEMINI_API_KEY; + const secret = env.GEMINI_API_SECRET; + if (!key || !secret) { + for (const name of AUTHENTICATED) results.set(name, operationResult(name, "authenticated JSON", "blocked", "GEMINI_API_KEY and GEMINI_API_SECRET are required for authenticated verification.")); + } else { + const authenticatedClient = new sdk.GeminiMarkets({ env: env.GEMINI_MD_ENV ?? "production", auth: new sdk.HmacAuth({ apiKey: key, apiSecret: secret }) }); + try { + let network; + try { + const tokenNetworks = await call(authenticatedClient, "getTokenNetworkV2", [{ token: env.GEMINI_MD_TOKEN ?? "USDC" }]); + results.set("getTokenNetworkV2", operationResult("getTokenNetworkV2", "authenticated JSON", "passed")); + network = env.GEMINI_MD_NETWORK ?? firstSymbol(networks(tokenNetworks)); + } catch (error) { + results.set("getTokenNetworkV2", operationResult("getTokenNetworkV2", "authenticated JSON", "failed", safeError(error))); + results.set("getAssetsForNetwork", operationResult("getAssetsForNetwork", "authenticated JSON", "failed", "Token network discovery failed.")); + } + if (results.get("getAssetsForNetwork").status === "blocked") { + if (!network) { + results.set("getAssetsForNetwork", operationResult("getAssetsForNetwork", "authenticated JSON", "blocked", "Token network discovery returned no usable network.")); + } else { + try { + await call(authenticatedClient, "getAssetsForNetwork", [{ network }]); + results.set("getAssetsForNetwork", operationResult("getAssetsForNetwork", "authenticated JSON", "passed")); + } catch (error) { + results.set("getAssetsForNetwork", operationResult("getAssetsForNetwork", "authenticated JSON", "failed", safeError(error))); + } + } + } + try { + await call(authenticatedClient, "getFXRate", [{ + symbol: env.GEMINI_MD_FX_SYMBOL ?? "EURUSD", + timestamp: env.GEMINI_MD_FX_TIMESTAMP ?? Date.now(), + }]); + results.set("getFXRate", operationResult("getFXRate", "authenticated JSON", "passed")); + } catch (error) { + results.set("getFXRate", operationResult("getFXRate", "authenticated JSON", "failed", safeError(error))); + } + } finally { + authenticatedClient.close?.(); + } + } + publicClient.close?.(); + return writeReports(reportDir, OPERATIONS.map(([name]) => results.get(name)), secrets); +} + +function runPackedVerifier() { + const root = dirname(dirname(fileURLToPath(import.meta.url))); + const tempDir = mkdtempSync(join(tmpdir(), "gemini-sdk-market-data-")); + const reportDir = join(root, ".market-data-verification", new Date().toISOString().replace(/[:.]/g, "-")); + try { + const packed = JSON.parse(execFileSync("npm", ["pack", "--json", "--pack-destination", tempDir, "--cache", join(tempDir, ".npm")], { cwd: root, encoding: "utf8" }))[0]; + writeFileSync(join(tempDir, "package.json"), JSON.stringify({ type: "module", dependencies: { "@gemini-markets/sdk": `file:./${packed.filename}` } })); + execFileSync("npm", ["install", "--ignore-scripts", "--no-package-lock", "--cache", join(tempDir, ".npm")], { cwd: tempDir, stdio: "inherit" }); + cpSync(fileURLToPath(import.meta.url), join(tempDir, "verify-market-data-live.mjs")); + const verificationProcess = spawnSync( + process.execPath, + [...forwardedNodeExecArgs(process.execArgv, root), "verify-market-data-live.mjs", "--consumer", "--report-dir", reportDir], + { cwd: tempDir, stdio: "inherit", env: process.env }, + ); + if (verificationProcess.error) throw verificationProcess.error; + return verificationProcess.status ?? 1; + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } +} + +async function main() { + if (process.argv.includes("--consumer")) { + const reportDir = process.argv[process.argv.indexOf("--report-dir") + 1]; + const verification = await runVerification({ reportDir }); + console.log(`Market Data live verification report: ${verification.reportDir}`); + process.exitCode = exitCodeFor(verification.operations); + return; + } + process.exitCode = runPackedVerifier(); +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) await main(); diff --git a/packages/sdk-typescript/scripts/verify-market-data-live.test.mjs b/packages/sdk-typescript/scripts/verify-market-data-live.test.mjs new file mode 100644 index 0000000..d9bdf4f --- /dev/null +++ b/packages/sdk-typescript/scripts/verify-market-data-live.test.mjs @@ -0,0 +1,157 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +import { exitCodeFor, forwardedNodeExecArgs, OPERATIONS, redact, runVerification, verifyFileEvidence } from "./verify-market-data-live.mjs"; + +test("operation list covers the generated Market Data contract and file exception", () => { + const operationsSource = readFileSync(new URL("../src/generated/market-data/operations.ts", import.meta.url), "utf8"); + const generatedNames = [...operationsSource.matchAll(/^ "([^"]+)": \{/gm)].map((match) => match[1]); + assert.deepEqual( + new Set(OPERATIONS.map(([name]) => name)), + new Set([...generatedNames, "getFundingAmountReportFile"]), + ); +}); + +test("rewrites QA Node options for the packed consumer", () => { + const root = join("/tmp", "sdk-package"); + assert.deepEqual(forwardedNodeExecArgs(["--use-system-ca", "--env-file=.env.qa", "--import", "./scripts/qa-bootstrap.mjs"], root), [ + "--use-system-ca", + `--env-file=${join(root, ".env.qa")}`, + "--import", + new URL(`file://${join(root, "scripts", "qa-bootstrap.mjs")}`).href, + ]); +}); + +test("does not add Node options in live mode", () => { + assert.deepEqual(forwardedNodeExecArgs([], "/tmp/sdk-package"), []); +}); + +test("redact removes credentials and request authentication fields", () => { + const value = redact({ authorization: "Bearer secret", auth: "Token secret", "X-GEMINI-APIKEY": "key", "X-GEMINI-SIGNATURE": "signature", payload: "payload", nested: "apiSecret=secret" }); + assert.deepEqual(value, { authorization: "[REDACTED]", auth: "[REDACTED]", "X-GEMINI-APIKEY": "[REDACTED]", "X-GEMINI-SIGNATURE": "[REDACTED]", payload: "[REDACTED]", nested: "apiSecret=[REDACTED]" }); +}); + +test("redact removes colon-form authentication strings and supplied secrets", () => { + const value = redact("Authorization: Token bearer-secret auth: Basic basic-secret X-GEMINI-PAYLOAD: payload-secret X-GEMINI-SIGNATURE: signature-secret X-GEMINI-APIKEY: key-secret direct-secret", ["direct-secret"]); + assert.doesNotMatch(value, /bearer-secret|basic-secret|payload-secret|signature-secret|key-secret|direct-secret/); + assert.match(value, /\[REDACTED\]/); +}); + +test("missing marketData blocks all operations and writes reports", async () => { + const reportDir = mkdtempSync(join(tmpdir(), "market-data-test-")); + try { + const results = await runVerification({ reportDir, loadSdk: async () => ({ GeminiMarkets: class { close() {} } }) }); + assert.equal(results.operations.length, 15); + assert(results.operations.every((operation) => operation.status === "blocked" && operation.message === "Prerequisite missing: @gemini-markets/sdk GeminiMarkets.marketData facade is not available in the packed package.")); + assert.equal(JSON.parse(readFileSync(join(reportDir, "results.json"))).operations.length, 15); + assert.match(readFileSync(join(reportDir, "summary.md"), "utf8"), /15 blocked/); + } finally { rmSync(reportDir, { recursive: true, force: true }); } +}); + +test("missing credentials block exactly authenticated operations", async () => { + const reportDir = mkdtempSync(join(tmpdir(), "market-data-test-")); + const methods = Object.fromEntries(["listSymbols", "getSymbolDetails", "getTicker", "getTickerV2", "getCurrentOrderBook", "listTrades", "listPrices", "listCandles", "listDerivativeCandles", "listFeePromos", "getFundingAmount", "getFundingAmountReportFile"].map((name) => [name, async () => name === "listSymbols" ? ["btcusd", "btcgusdperp"] : name === "getFundingAmountReportFile" ? { bytes: new Uint8Array([1]), contentType: "application/octet-stream", contentDisposition: "attachment; filename=report.xlsx" } : {}])); + try { + const results = await runVerification({ reportDir, env: {}, loadSdk: async () => ({ GeminiMarkets: class { constructor() { this.marketData = methods; } close() {} } }) }); + assert.deepEqual(results.operations.filter((operation) => operation.status === "blocked").map((operation) => operation.name), ["getAssetsForNetwork", "getTokenNetworkV2", "getFXRate"]); + } finally { rmSync(reportDir, { recursive: true, force: true }); } +}); + +test("missing facade methods are blocked, not failed", async () => { + const reportDir = mkdtempSync(join(tmpdir(), "market-data-test-")); + const methods = { async listSymbols() { return ["btcusd", "btcgusdperp"]; } }; + try { + const results = await runVerification({ reportDir, env: {}, loadSdk: async () => ({ GeminiMarkets: class { constructor() { this.marketData = methods; } close() {} } }) }); + assert.equal(results.operations.find((operation) => operation.name === "getTicker").status, "blocked"); + } finally { rmSync(reportDir, { recursive: true, force: true }); } +}); + +test("uses generated Market Data arguments and runs FX when token lookup fails", async () => { + const reportDir = mkdtempSync(join(tmpdir(), "market-data-test-")); + const calls = []; + const originalNow = Date.now; + const methods = Object.fromEntries(["listSymbols", "getSymbolDetails", "getTicker", "getTickerV2", "getCurrentOrderBook", "listTrades", "listPrices", "listCandles", "listDerivativeCandles", "listFeePromos", "getFundingAmount", "getFundingAmountReportFile", "getAssetsForNetwork", "getTokenNetworkV2", "getFXRate"].map((name) => [name, async (...args) => { + calls.push([name, args]); + if (name === "listSymbols") return ["btcusd", "btcgusdperp"]; + if (name === "getTokenNetworkV2") throw new Error("token lookup failed"); + if (name === "getFundingAmountReportFile") return { bytes: new Uint8Array([1]), contentType: "application/octet-stream", contentDisposition: "attachment; filename=report.xlsx" }; + return {}; + }])); + try { + Date.now = () => 1770000000000; + await runVerification({ reportDir, env: { GEMINI_API_KEY: "key-secret", GEMINI_API_SECRET: "secret-value", GEMINI_MD_DERIVATIVE_SYMBOL: "btcgusdperp" }, loadSdk: async () => ({ GeminiMarkets: class { constructor() { this.marketData = methods; } close() {} }, HmacAuth: class {} }) }); + assert.deepEqual(Object.fromEntries(calls), { + listSymbols: [], getSymbolDetails: [{ symbol: "btcusd" }], getTicker: [{ symbol: "btcusd" }], getTickerV2: [{ symbol: "btcusd" }], getCurrentOrderBook: [{ symbol: "btcusd" }, { limit_bids: 1, limit_asks: 1 }], listTrades: [{ symbol: "btcusd" }, { limit_trades: 1 }], listPrices: [], listCandles: [{ symbol: "btcusd", time_frame: "1m" }], listDerivativeCandles: [{ symbol: "btcgusdperp", time_frame: "1m" }], listFeePromos: [], getFundingAmount: [{ symbol: "btcgusdperp" }], getFundingAmountReportFile: [{ symbol: "btcgusdperp" }], getTokenNetworkV2: [{ token: "USDC" }], getFXRate: [{ symbol: "EURUSD", timestamp: 1770000000000 }], + }); + assert(!calls.some(([name]) => name === "getAssetsForNetwork")); + } finally { Date.now = originalNow; rmSync(reportDir, { recursive: true, force: true }); } +}); + +test("discovers lowercase derivative symbols by default", async () => { + const reportDir = mkdtempSync(join(tmpdir(), "market-data-test-")); + const calls = []; + const methods = Object.fromEntries(["listSymbols", "getSymbolDetails", "getTicker", "getTickerV2", "getCurrentOrderBook", "listTrades", "listPrices", "listCandles", "listDerivativeCandles", "listFeePromos", "getFundingAmount", "getFundingAmountReportFile"].map((name) => [name, async (...args) => { + calls.push([name, args]); + if (name === "listSymbols") return ["btcusd", "avaxgusdperp"]; + if (name === "getFundingAmountReportFile") return { bytes: new Uint8Array([1]), contentType: "application/octet-stream", contentDisposition: "attachment; filename=report.xlsx" }; + return {}; + }])); + try { + const results = await runVerification({ reportDir, env: {}, loadSdk: async () => ({ GeminiMarkets: class { constructor() { this.marketData = methods; } close() {} } }) }); + assert.deepEqual(calls.find(([name]) => name === "listDerivativeCandles"), ["listDerivativeCandles", [{ symbol: "avaxgusdperp", time_frame: "1m" }]]); + assert.deepEqual(calls.find(([name]) => name === "getFundingAmount"), ["getFundingAmount", [{ symbol: "avaxgusdperp" }]]); + assert.deepEqual(calls.find(([name]) => name === "getFundingAmountReportFile"), ["getFundingAmountReportFile", [{ symbol: "avaxgusdperp" }]]); + assert.equal(results.operations.find((operation) => operation.name === "listDerivativeCandles").status, "passed"); + assert.equal(results.operations.find((operation) => operation.name === "getFundingAmountReportFile").status, "passed"); + } finally { rmSync(reportDir, { recursive: true, force: true }); } +}); + +test("symbol discovery failure still exercises symbol-free public operations", async () => { + const reportDir = mkdtempSync(join(tmpdir(), "market-data-test-")); + const calls = []; + const methods = { + async listSymbols() { calls.push("listSymbols"); throw new Error("symbols unavailable"); }, + async listPrices() { calls.push("listPrices"); return {}; }, + async listFeePromos() { calls.push("listFeePromos"); return {}; }, + }; + try { + const results = await runVerification({ reportDir, env: {}, loadSdk: async () => ({ GeminiMarkets: class { constructor() { this.marketData = methods; } close() {} } }) }); + assert.deepEqual(calls, ["listSymbols", "listPrices", "listFeePromos"]); + assert.equal(results.operations.find((operation) => operation.name === "listPrices").status, "passed"); + assert.equal(results.operations.find((operation) => operation.name === "listFeePromos").status, "passed"); + } finally { rmSync(reportDir, { recursive: true, force: true }); } +}); + +test("reports redact authentication headers and environment secrets", async () => { + const reportDir = mkdtempSync(join(tmpdir(), "market-data-test-")); + const secret = "direct-secret"; + const methods = Object.fromEntries(["listSymbols", "getSymbolDetails", "getTicker", "getTickerV2", "getCurrentOrderBook", "listTrades", "listPrices", "listCandles", "listDerivativeCandles", "listFeePromos", "getFundingAmount", "getFundingAmountReportFile"].map((name) => [name, async () => { + if (name === "listSymbols") return ["btcusd", "btcgusdperp"]; + throw new Error(`Authorization: Bearer bearer-secret X-GEMINI-PAYLOAD: payload-secret X-GEMINI-SIGNATURE: signature-secret X-GEMINI-APIKEY: key-secret ${secret}`); + }])); + try { + await runVerification({ reportDir, env: { GEMINI_API_KEY: "key-secret", GEMINI_API_SECRET: secret }, loadSdk: async () => ({ GeminiMarkets: class { constructor() { this.marketData = methods; } close() {} }, HmacAuth: class {} }) }); + for (const report of [readFileSync(join(reportDir, "results.json"), "utf8"), readFileSync(join(reportDir, "summary.md"), "utf8")]) { + assert.doesNotMatch(report, /bearer-secret|payload-secret|signature-secret|key-secret|direct-secret/); + assert.match(report, /\[REDACTED\]/); + } + } finally { rmSync(reportDir, { recursive: true, force: true }); } +}); + +test("file evidence rejects empty bytes and retains only metadata", async () => { + assert.throws(() => verifyFileEvidence({ bytes: new Uint8Array(), headers: { "content-type": "application/vnd.ms-excel", "content-disposition": "attachment; filename=report.xlsx" } }), /empty/); + assert.throws(() => verifyFileEvidence({ body: new ReadableStream(), headers: { "content-type": "application/vnd.ms-excel", "content-disposition": "attachment; filename=report.xlsx" } }), /byteLength/); + assert.throws(() => verifyFileEvidence({ bytes: new Uint8Array([1]), headers: { "content-type": "application/vnd.ms-excel" } }), /content disposition/); + assert.deepEqual(verifyFileEvidence({ bytes: new Uint8Array([1]), contentType: "application/vnd.ms-excel", contentDisposition: "attachment; filename=report.xlsx" }), { byteLength: 1, contentType: "application/vnd.ms-excel", contentDisposition: "attachment; filename=report.xlsx" }); + assert.deepEqual(verifyFileEvidence({ bytes: new Uint8Array([1]), headers: { "Content-Type": "application/vnd.ms-excel", "Content-Disposition": "attachment; filename=report.xlsx" } }), { byteLength: 1, contentType: "application/vnd.ms-excel", contentDisposition: "attachment; filename=report.xlsx" }); + assert.deepEqual(verifyFileEvidence({ bytes: new Uint8Array([1]), headers: { "content-type": "application/vnd.ms-excel", "content-disposition": "attachment; filename=report.xlsx" } }), { byteLength: 1, contentType: "application/vnd.ms-excel", contentDisposition: "attachment; filename=report.xlsx" }); +}); + +test("exit aggregation fails for blocked or failed operations", () => { + assert.equal(exitCodeFor([{ status: "passed" }]), 0); + assert.equal(exitCodeFor([{ status: "blocked" }]), 1); + assert.equal(exitCodeFor([{ status: "failed" }]), 1); +}); diff --git a/packages/sdk-typescript/scripts/verify-multi-runtime.mjs b/packages/sdk-typescript/scripts/verify-multi-runtime.mjs new file mode 100644 index 0000000..1487a0c --- /dev/null +++ b/packages/sdk-typescript/scripts/verify-multi-runtime.mjs @@ -0,0 +1,249 @@ +/** + * Verify the SDK works across Node, Bun, Deno, and Cloudflare Workers. + * + * Packs the SDK tarball, installs it in a temp directory, then runs a minimal + * consumer script under each runtime. Each consumer imports from both entry + * points (where applicable), constructs core classes, and exercises Web Crypto. + */ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +// Deno: prefer PATH (CI uses setup-deno), fall back to ~/.deno/bin (local install) +function findDeno() { + try { execFileSync("deno", ["--version"], { stdio: "pipe" }); return "deno"; } catch {} + const local = join(process.env.HOME, ".deno", "bin", "deno"); + try { execFileSync(local, ["--version"], { stdio: "pipe" }); return local; } catch {} + return null; +} +const DENO = process.env.DENO_BIN || findDeno(); + +// --- Consumer scripts --- + +const BROWSER_CONSUMER = ` +import { + GeminiMarkets, + GeminiWebSocket, + WsSession, + BrowserOAuthAuth, + MarketDataRest, + HttpTransport, +} from "@gemini-markets/sdk/browser"; + +// Verify core construction works +const client = new GeminiMarkets({ env: "sandbox" }); +assert(client.marketData instanceof MarketDataRest, "marketData must be MarketDataRest"); +assert(typeof client.websocket.trades === "function", "websocket.trades must be a function"); + +// Verify BrowserOAuthAuth exists and is constructible (type-level restriction only) +assert(typeof BrowserOAuthAuth === "function", "BrowserOAuthAuth must be exported"); + +// Verify WebSocket classes exist +const ws = new GeminiWebSocket({ url: "wss://example.test" }); +const session = new WsSession({ url: "wss://example.test" }); + +// Verify HttpTransport exists +assert(typeof HttpTransport === "function", "HttpTransport must be exported"); + +client.close(); +ws.close(); +session.close(); +console.log("browser entry: OK"); +`; + +const SERVER_CONSUMER = ` +import { + HmacAuth, + createClient, + OAuthAuth, + serverSocketFactory, + initServerWebSocket, +} from "@gemini-markets/sdk/server"; + +// Verify HMAC auth constructs and uses Web Crypto +const auth = new HmacAuth({ apiKey: "test-key", apiSecret: "test-secret" }); +const headers = await auth.credentialHeaders("dGVzdA=="); +assert(headers["X-GEMINI-APIKEY"] === "test-key", "HMAC must set API key header"); +assert(typeof headers["X-GEMINI-SIGNATURE"] === "string" && headers["X-GEMINI-SIGNATURE"].length > 0, "HMAC must produce signature"); + +// Verify OAuthAuth is re-exported from server +assert(typeof OAuthAuth === "function", "OAuthAuth must be re-exported from server"); + +// Verify server-only exports exist +assert(typeof serverSocketFactory === "function", "serverSocketFactory must be exported"); +assert(typeof initServerWebSocket === "function", "initServerWebSocket must be exported"); + +// Verify createClient works (REST-only, skip ws init) +const client = await createClient({ env: "sandbox", skipWsInit: true }); +assert(typeof client.marketData === "object", "server client must have marketData"); +client.close(); + +console.log("server entry: OK"); +`; + +// Workers can't use server entry (ws dep). Test browser entry only. +const WORKER_CONSUMER = ` +import { + GeminiMarkets, + BrowserOAuthAuth, + MarketDataRest, + HttpTransport, +} from "@gemini-markets/sdk/browser"; + +export default { + async fetch() { + const client = new GeminiMarkets({ env: "sandbox" }); + const checks = [ + client.marketData instanceof MarketDataRest, + typeof client.websocket.trades === "function", + typeof BrowserOAuthAuth === "function", + typeof HttpTransport === "function", + ]; + client.close(); + const ok = checks.every(Boolean); + return new Response(ok ? "OK" : "FAIL: " + JSON.stringify(checks), { + status: ok ? 200 : 500, + }); + } +}; +`; + +// --- Helpers --- + +function run(label, fn) { + try { + fn(); + console.log(`✔ ${label}`); + } catch (error) { + console.error(`✖ ${label}`); + console.error(error.message || error); + if (error.stderr) console.error(error.stderr.toString()); + process.exitCode = 1; + } +} + +function hasRuntime(name, binary) { + try { + execFileSync(binary, ["--version"], { stdio: "pipe" }); + return true; + } catch { + return false; + } +} + +// --- Main --- + +const temp = mkdtempSync(join(tmpdir(), "gemini-multi-runtime-")); +try { + // Pack the SDK + const packed = JSON.parse( + execFileSync("npm", ["pack", "--json", "--pack-destination", temp, "--cache", join(temp, ".npm")], { + encoding: "utf8", + cwd: resolve("."), + }), + )[0]; + + // Set up a consumer project with the packed tarball + writeFileSync( + join(temp, "package.json"), + JSON.stringify({ type: "module", dependencies: { "@gemini-markets/sdk": `file:./${packed.filename}` } }), + ); + execFileSync("npm", ["install", "--ignore-scripts", "--no-package-lock", "--cache", join(temp, ".npm")], { + cwd: temp, + stdio: "pipe", + }); + + // Write consumer scripts + writeFileSync(join(temp, "browser-consumer.mjs"), `import assert from "node:assert/strict";\n${BROWSER_CONSUMER}`); + writeFileSync(join(temp, "server-consumer.mjs"), `import assert from "node:assert/strict";\n${SERVER_CONSUMER}`); + + // --- Node --- + run("Node (browser entry)", () => { + execFileSync("node", ["browser-consumer.mjs"], { cwd: temp, stdio: "pipe" }); + }); + run("Node (server entry)", () => { + execFileSync("node", ["server-consumer.mjs"], { cwd: temp, stdio: "pipe" }); + }); + + // --- Bun --- + if (hasRuntime("Bun", "bun")) { + run("Bun (browser entry)", () => { + execFileSync("bun", ["run", "browser-consumer.mjs"], { cwd: temp, stdio: "pipe" }); + }); + run("Bun (server entry)", () => { + execFileSync("bun", ["run", "server-consumer.mjs"], { cwd: temp, stdio: "pipe" }); + }); + } else { + console.log("⊘ Bun: not installed, skipping"); + } + + // --- Deno --- + if (DENO && hasRuntime("Deno", DENO)) { + // Deno 2.x with file: specifiers needs --node-modules-dir=manual + run("Deno (browser entry)", () => { + execFileSync(DENO, ["run", "--allow-all", "--node-modules-dir=manual", "browser-consumer.mjs"], { + cwd: temp, + stdio: "pipe", + }); + }); + run("Deno (server entry)", () => { + execFileSync(DENO, ["run", "--allow-all", "--node-modules-dir=manual", "server-consumer.mjs"], { + cwd: temp, + stdio: "pipe", + }); + }); + } else { + console.log("⊘ Deno: not installed, skipping"); + } + + // --- Cloudflare Workers (via Miniflare) --- + await (async () => { + try { + const esbuildBin = join(process.cwd(), "node_modules", ".bin", "esbuild"); + + // Write the worker source (no node:assert — use Response status) + writeFileSync(join(temp, "worker-src.mjs"), WORKER_CONSUMER); + + // Bundle for workers with esbuild + execFileSync(esbuildBin, [ + join(temp, "worker-src.mjs"), + "--bundle", + "--platform=browser", + "--format=esm", + "--target=es2022", + `--outfile=${join(temp, "worker-bundle.mjs")}`, + ], { stdio: "pipe" }); + + // Read the bundle and pass as inline script to avoid workerd path issues + const bundleScript = readFileSync(join(temp, "worker-bundle.mjs"), "utf8"); + + const { Miniflare } = await import("miniflare"); + const mf = new Miniflare({ + modules: true, + script: bundleScript, + compatibilityDate: "2024-01-01", + }); + + try { + const response = await mf.dispatchFetch("http://localhost/"); + const text = await response.text(); + assert.equal(response.status, 200, `Worker returned ${response.status}: ${text}`); + console.log("✔ Cloudflare Workers (browser entry via Miniflare)"); + } finally { + await mf.dispose(); + } + } catch (error) { + console.error("✖ Cloudflare Workers (browser entry via Miniflare)"); + console.error(error.message || error); + process.exitCode = 1; + } + })(); + + if (!process.exitCode) { + console.log("\nAll multi-runtime checks passed."); + } +} finally { + rmSync(temp, { recursive: true, force: true }); +} diff --git a/packages/sdk-typescript/scripts/verify-package.mjs b/packages/sdk-typescript/scripts/verify-package.mjs new file mode 100644 index 0000000..4b1ce98 --- /dev/null +++ b/packages/sdk-typescript/scripts/verify-package.mjs @@ -0,0 +1,310 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { createRequire } from "node:module"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const moduleRequire = createRequire(import.meta.url); +const tscPath = moduleRequire.resolve("typescript/bin/tsc"); +const temp = mkdtempSync(join(tmpdir(), "gemini-sdk-consumer-")); +try { + const packed = JSON.parse(execFileSync("npm", ["pack", "--json", "--pack-destination", temp, "--cache", join(temp, ".npm")], { encoding: "utf8" }))[0]; + const paths = new Set(packed.files.map(({ path }) => path)); + const generatedModules = [ + "trading", + "margin", + "perpetuals", + "account-services", + "clearing-instant", + ]; + for (const path of [ + "package.json", + "README.md", + "dist/browser/index.js", + "dist/browser/index.d.ts", + "dist/server/index.js", + "dist/server/index.d.ts", + "dist/server/ws-factory.js", + "dist/server/ws-factory.d.ts", + "dist/websocket.js", + "dist/websocket.d.ts", + "dist/ws-session.js", + "dist/ws-session.d.ts", + "dist/websocket-types.d.ts", + "dist/generated/websocket/index.d.ts", + "dist/prediction-markets.js", + "dist/generated/rest.d.ts", + "dist/generated/market-data/models.d.ts", + "dist/generated/market-data/rest.js", + "dist/generated/market-data/rest.d.ts", + "dist/generated/market-data/operations.js", + "dist/generated/market-data/operations.d.ts", + ]) assert(paths.has(path), `package missing ${path}`); + for (const moduleName of generatedModules) { + for (const filename of ["rest.js", "rest.d.ts", "operations.js", "operations.d.ts"]) { + const path = `dist/generated/${moduleName}/${filename}`; + assert(paths.has(path), `package missing ${path}`); + } + } + assert([...paths].every((path) => !path.startsWith("src/") && !path.includes("test")), "package contains source or tests"); + + writeFileSync(join(temp, "package.json"), '{"type":"module","dependencies":{"@gemini-markets/sdk":"file:./' + packed.filename + '"}}'); + execFileSync("npm", ["install", "--ignore-scripts", "--no-package-lock", "--cache", join(temp, ".npm")], { cwd: temp, stdio: "inherit" }); + writeFileSync(join(temp, "consumer.mjs"), ` +import { + GeminiMarkets, + GeminiWebSocket, + WsSession, + MarketDataClient, + MarketDataRest, + TradingClient, + TradingRest, + MarginClient, + MarginRest, + PerpetualsClient, + PerpetualsRest, + AccountServicesClient, + AccountServicesRest, + ClearingInstantClient, + ClearingInstantRest, + MARKET_DATA_OPERATIONS, + MARGIN_OPERATIONS, + TRADING_OPERATIONS, + PERPETUALS_OPERATIONS, + ACCOUNT_SERVICES_OPERATIONS, + CLEARING_INSTANT_OPERATIONS, +} from "@gemini-markets/sdk/browser"; + +import { + HmacAuth, + createClient, +} from "@gemini-markets/sdk/server"; + +const sdk = new GeminiMarkets({ env: "sandbox" }); +const websocket = new GeminiWebSocket({ url: "wss://example.test" }); +const session = new WsSession({ url: "wss://example.test" }); +if ( + !sdk.predictions || + !sdk.marketData || + !sdk.trading || + !sdk.margin || + !sdk.perpetuals || + !sdk.accountServices || + !sdk.clearingInstant || + !sdk.websocket || + typeof sdk.orderBook !== "function" || + typeof sdk.websocket.trades !== "function" || + typeof sdk.websocket.ping !== "function" || + MarketDataClient !== MarketDataRest || + TradingClient !== TradingRest || + MarginClient !== MarginRest || + PerpetualsClient !== PerpetualsRest || + AccountServicesClient !== AccountServicesRest || + ClearingInstantClient !== ClearingInstantRest || + MARKET_DATA_OPERATIONS.getFundingAmountReportFile.responseMode !== "file" || + TRADING_OPERATIONS.createNewOrder.path !== "/v1/order/new" || + MARGIN_OPERATIONS.previewMarginOrder.path !== "/v1/margin/order/preview" || + PERPETUALS_OPERATIONS.getFundingPaymentReportFile.responseMode !== "file" || + ACCOUNT_SERVICES_OPERATIONS.listStakingRates.access !== "public" || + CLEARING_INSTANT_OPERATIONS.createNewClearingOrder.path !== "/v1/clearing/new" || + CLEARING_INSTANT_OPERATIONS.getInstantQuote.path !== "/v1/instant/quote" +) throw new Error("missing package exports"); +if (typeof HmacAuth !== "function" || typeof createClient !== "function") throw new Error("missing server exports"); +const serverClient = await createClient({ env: "sandbox" }); +serverClient.close(); +sdk.close(); +websocket.close(); +session.close(); +`); + execFileSync("node", ["consumer.mjs"], { cwd: temp, stdio: "inherit" }); + writeFileSync(join(temp, "consumer.ts"), ` +import { + ACCOUNT_SERVICES_OPERATIONS, + CLEARING_INSTANT_OPERATIONS, + GeminiWebSocket, + GeminiMarkets, + MARKET_DATA_OPERATIONS, + MARGIN_OPERATIONS, + PERPETUALS_OPERATIONS, + TRADING_OPERATIONS, + WsSession, + type BalanceUpdate, + type BookTicker, + type ContractStatus, + type DepthUpdate, + type GeminiWebSocketOptions, + type LiveOrderBook, + type OrderActionResponse, + type OrderBookSnapshot, + type OrderUpdate, + type PositionReport, + type RfqPrivateDelivery, + type RfqPublicEvent, + type RfqSubmitQuoteParams, + type RfqSubmitQuoteResponse, + type SuccessResponse, + type Trade, + type AccountServicesOperationTypes, + type ClearingInstantOperationTypes, + type MarketDataOperationTypes, + type MarginOperationTypes, + type PerpetualsOperationTypes, + type TradingOperationTypes, + type WebSocketStream, + type WsSessionOptions, + type WsSubscription, +} from "@gemini-markets/sdk/browser"; + +import { + HmacAuth, + createClient, +} from "@gemini-markets/sdk/server"; + +const sdk = new GeminiMarkets({ + env: "sandbox", + auth: new HmacAuth({ apiKey: "key", apiSecret: "secret" }), +}); +const book: LiveOrderBook = sdk.orderBook("btcusd"); +sdk.predictions.listEvents(); +sdk.predictions.getPositions(); +sdk.marketData.getTicker({ symbol: "btcusd" }); +sdk.marketData.getCurrentOrderBook({ symbol: "btcusd" }, { limit_bids: 1 }); +sdk.marketData.getAssetsForNetwork({ network: "ethereum" }); +sdk.marketData.getFundingAmountReportFile({ symbol: "BTCGUSDPERP" }).then((file) => { + const contentType: string | undefined = file.contentType; + const firstByte: number | undefined = file.bytes[0]; + void contentType; + void firstByte; +}); +sdk.trading.createNewOrder({ + symbol: "btcusd", + amount: "1", + price: "100", + side: "buy", + type: "exchange limit", +}); +sdk.trading.wrapOrder({ + path: { symbol: "btcusd" }, + body: { amount: "1" }, +}); +sdk.margin.previewMarginOrder({ + symbol: "btcusd", + side: "buy", + type: "limit", + amount: "1", + price: "100", +}); +sdk.perpetuals.getRiskStats({ symbol: "BTCGUSDPERP" }); +sdk.perpetuals.getFundingPaymentReportFile({ + query: { fromDate: "2026-01-01", numRows: 1 }, + body: { account: "primary" }, +}); +sdk.accountServices.listStakingRates(); +sdk.accountServices.withdrawCryptoFunds({ + path: { network: "ethereum", ticker: "eth" }, + body: { address: "0xabc", amount: "1" }, +}); +sdk.clearingInstant.createNewClearingOrder({ + symbol: "btcusd", + amount: "1", + price: "100", + side: "buy", +}); +sdk.clearingInstant.getInstantQuote({ + side: "buy", + symbol: "btcusd", + totalSpend: "100", +}); +const trades: WebSocketStream = sdk.websocket.trades("btcusd"); +const ticker: WebSocketStream = sdk.websocket.bookTicker("btcusd"); +const depthUpdates: WebSocketStream = sdk.websocket.depthUpdates("btcusd"); +const depth: WebSocketStream = sdk.websocket.depth("btcusd", { levels: 20 }); +const contractStatus: WebSocketStream = sdk.websocket.contractStatus(); +const rfqs: WebSocketStream = sdk.websocket.rfqs(); +const orders: WebSocketStream = sdk.websocket.orders({ scope: "session" }); +const balances: WebSocketStream = sdk.websocket.balances(); +const positions: WebSocketStream = sdk.websocket.positions(); +const deliveries: WebSocketStream = sdk.websocket.rfqDeliveries({ scope: "account" }); + +const ping: Promise = sdk.websocket.ping(); +const time: Promise = sdk.websocket.time(); +const conninfo: Promise = sdk.websocket.conninfo(); +const placed: Promise = sdk.websocket.placeOrder({ + symbol: "btcusd", + side: "BUY", + type: "LIMIT", + timeInForce: "GTC", + quantity: "1", + price: "100", +}); +const quoteParams: RfqSubmitQuoteParams = { rfqId: "rfq-1", price: "100", quantity: "1" }; +const quote: Promise = sdk.websocket.rfq.submitQuote(quoteParams); + +const websocketOptions: GeminiWebSocketOptions = { url: "wss://example.test" }; +const directWebSocket = new GeminiWebSocket(websocketOptions); +const sessionOptions: WsSessionOptions = { url: "wss://example.test" }; +const directSession = new WsSession(sessionOptions); +const subscription: WsSubscription = directSession.subscribe(["btcusd@trade"]); +void [book, trades, ticker, depthUpdates, depth, contractStatus, rfqs, orders, balances, positions, deliveries, ping, time, conninfo, placed, quote, directWebSocket, directSession, subscription]; +const serverClient: Promise = createClient({ env: "sandbox" }); +void serverClient; + +const tickerPath: MarketDataOperationTypes["getTicker"]["path"] = { symbol: "btcusd" }; +const reportQuery: MarketDataOperationTypes["getFundingAmountReportFile"]["query"] = { symbol: "BTCGUSDPERP", numRows: 1 }; +const wrapPath: TradingOperationTypes["wrapOrder"]["path"] = { symbol: "btcusd" }; +const riskPath: PerpetualsOperationTypes["getRiskStats"]["path"] = { symbol: "BTCGUSDPERP" }; +const withdrawPath: AccountServicesOperationTypes["withdrawCryptoFunds"]["path"] = { network: "ethereum", ticker: "eth" }; +const instantQuoteBody: ClearingInstantOperationTypes["getInstantQuote"]["body"] = { + side: "buy", + symbol: "btcusd", + totalSpend: "100", +}; +type TransportFieldKeys = "request" | "nonce"; +type AssertNoTransportFields = Extract extends never ? true : never; +const callerBodyTypesStripTransportFields: [ + AssertNoTransportFields, + AssertNoTransportFields, + AssertNoTransportFields, + AssertNoTransportFields, + AssertNoTransportFields, +] = [true, true, true, true, true]; +void callerBodyTypesStripTransportFields; +if ( + MARKET_DATA_OPERATIONS.getTicker.path !== "/v1/pubticker/{symbol}" || + MARKET_DATA_OPERATIONS.getFundingAmountReportFile.responseMode !== "file" || + TRADING_OPERATIONS.createNewOrder.method !== "post" || + MARGIN_OPERATIONS.previewMarginOrder.method !== "post" || + PERPETUALS_OPERATIONS.getRiskStats.access !== "public" || + ACCOUNT_SERVICES_OPERATIONS.listStakingRates.access !== "public" || + CLEARING_INSTANT_OPERATIONS.createNewClearingOrder.path !== "/v1/clearing/new" || + CLEARING_INSTANT_OPERATIONS.getInstantQuote.path !== "/v1/instant/quote" || + tickerPath.symbol !== "btcusd" || + reportQuery.symbol !== "BTCGUSDPERP" || + wrapPath.symbol !== "btcusd" || + riskPath.symbol !== "BTCGUSDPERP" || + withdrawPath.ticker !== "eth" || + instantQuoteBody.totalSpend !== "100" || + !ticker.ready +) throw new Error("missing generated REST contracts"); +`); + writeFileSync(join(temp, "tsconfig.json"), '{"compilerOptions":{"module":"NodeNext","moduleResolution":"NodeNext","strict":true,"noEmit":true},"include":["consumer.ts"]}'); + execFileSync(process.execPath, [tscPath, "-p", join(temp, "tsconfig.json")], { cwd: temp, stdio: "inherit" }); + + // Negative type test: HmacAuth must NOT be importable from the browser entry point. + writeFileSync(join(temp, "negative.ts"), `import { HmacAuth } from "@gemini-markets/sdk/browser";\nvoid HmacAuth;\n`); + writeFileSync(join(temp, "tsconfig.negative.json"), '{"compilerOptions":{"module":"NodeNext","moduleResolution":"NodeNext","strict":true,"noEmit":true},"include":["negative.ts"]}'); + let negativePassed = false; + try { + execFileSync(process.execPath, [tscPath, "-p", join(temp, "tsconfig.negative.json")], { cwd: temp, stdio: "pipe" }); + negativePassed = true; + } catch { + // Expected: tsc should fail because HmacAuth is not exported from browser + } + assert(!negativePassed, "HmacAuth must not be importable from @gemini-markets/sdk/browser"); + + JSON.parse(readFileSync(join(temp, "node_modules", "@gemini-markets", "sdk", "package.json"), "utf8")); + console.log(`verified ${packed.entryCount} packed entries in an isolated consumer`); +} finally { + rmSync(temp, { recursive: true, force: true }); +} diff --git a/packages/sdk-typescript/scripts/verify-sandbox-rest.mjs b/packages/sdk-typescript/scripts/verify-sandbox-rest.mjs new file mode 100644 index 0000000..d7b0779 --- /dev/null +++ b/packages/sdk-typescript/scripts/verify-sandbox-rest.mjs @@ -0,0 +1,396 @@ +import { readFileSync } from "node:fs"; +import { pathToFileURL } from "node:url"; +import { redact, verifyFileEvidence } from "./verify-market-data-live.mjs"; + +const INVENTORY = JSON.parse(readFileSync(new URL("./rest-operation-ownership.snapshot.json", import.meta.url), "utf8")); +const MISSING_FIXTURE = Symbol("missing fixture"); + +function account(context) { + return context.env.GEMINI_ACCOUNT || "primary"; +} + +function reportDates(context) { + return { + fromDate: context.env.GEMINI_REPORT_FROM_DATE || "2026-01-01", + toDate: context.env.GEMINI_REPORT_TO_DATE || "2026-01-31", + }; +} + +function preferredSymbol(symbols, derivative = false) { + const candidates = symbols.filter((symbol) => derivative === symbol.toUpperCase().includes("PERP")); + const bitcoin = candidates.find((symbol) => symbol.toLowerCase() === (derivative ? "btcgusdperp" : "btcusd")); + return bitcoin ?? firstString(candidates); +} + +export function priceSymbols(response) { + const source = response?.data ?? response; + if (Array.isArray(source)) return source + .map((entry) => typeof entry === "string" ? entry : entry?.symbol ?? entry?.pair) + .filter(Boolean); + if (!source || typeof source !== "object") return []; + return Object.keys(source).filter((key) => key !== "data" && key !== "timestamp"); +} + +const QUERY_FIXTURES = { + "marketData:getCurrentOrderBook": (context) => ({ limit_bids: 1, limit_asks: 1 }), + "marketData:listTrades": () => ({ limit_trades: 1 }), + "marketData:getFundingAmountReportFile": (context) => ({ symbol: context.derivativeSymbol, numRows: 1 }), + "predictionMarkets:listEvents": () => ({ status: ["active"], limit: 10 }), + "predictionMarkets:getPositions": () => ({ limit: 10 }), + "predictionMarkets:getSettledPositions": () => ({ limit: 10 }), + "predictionMarkets:listMakerRebatePayouts": () => ({ limit: 10 }), + "predictionMarkets:getLiquidityRewardsDailySummary": (context) => ({ dateFrom: "2026-05-01", dateTo: "2026-05-07" }), + "predictionMarkets:getLiquidityRewardsLifetimeSummary": () => ({ dateFrom: "2026-04-01", dateTo: "2026-05-01" }), + "predictionMarkets:getMakerRebateLifetimeSummary": () => ({ dateFrom: "2026-04-01", dateTo: "2026-05-01" }), + "perpetuals:listFundingPayments": (context) => ({ + since: BigInt(context.timestamp - 86_400_000), + to: BigInt(context.timestamp), + }), + "perpetuals:getFundingPaymentReportFile": (context) => ({ ...reportDates(context), numRows: 1 }), + "perpetuals:getFundingPaymentReportJson": (context) => ({ ...reportDates(context), numRows: 1 }), +}; + +const BODY_FIXTURES = { + "predictionMarkets:getVolumeMetrics": (context) => ({ + eventTicker: context.eventTicker, + startTime: context.timestamp - 86_400_000, + endTime: context.timestamp, + }), + "trading:getOrderStatus": (context) => { + if (context.env.GEMINI_ORDER_ID) return { order_id: context.env.GEMINI_ORDER_ID, account: account(context) }; + if (context.env.GEMINI_CLIENT_ORDER_ID) return { client_order_id: context.env.GEMINI_CLIENT_ORDER_ID, account: account(context) }; + return MISSING_FIXTURE; + }, + "accountServices:getAccountDetail": (context) => ({ account: account(context) }), + "accountServices:getAvailableBalances": (context) => ({ account: account(context), showPendingBalances: false }), + "accountServices:getGasFeeEstimation": (context) => context.env.GEMINI_MD_ADDRESS + ? { address: context.env.GEMINI_MD_ADDRESS, amount: "1", account: account(context) } + : MISSING_FIXTURE, + "accountServices:getNotionalBalances": (context) => ({ account: account(context) }), + "accountServices:getRoles": () => ({}), + "accountServices:getTransactionHistory": (context) => ({ timestamp_nanos: BigInt(context.timestamp) * 1_000_000n, limit: 1 }), + "accountServices:listAccountsInGroup": () => ({ limit_accounts: 1 }), + "accountServices:listApprovedAddresses": (context) => ({ account: account(context) }), + "accountServices:listCustodyFeeTransfers": (context) => ({ limit_transfers: 1, account: account(context) }), + "accountServices:listDepositAddresses": (context) => ({ timestamp: String(context.timestamp), account: account(context) }), + "accountServices:listPastTransfers": (context) => ({ currency: "eth", network: context.network, limit_transfers: 1, account: account(context) }), + "accountServices:listPaymentMethods": (context) => ({ account: account(context) }), + "accountServices:listStakingBalances": (context) => ({ account: account(context) }), + "accountServices:listStakingEventHistory": (context) => ({ account: account(context), limit: 1, sortAsc: false }), + "accountServices:listStakingRewards": (context) => ({ account: account(context), since: "2026-01-01T00:00:00.000Z" }), + "clearingInstant:getClearingOrder": (context) => context.env.GEMINI_CLEARING_ID + ? { clearing_id: context.env.GEMINI_CLEARING_ID, account: account(context) } + : MISSING_FIXTURE, + "clearingInstant:getInstantQuote": (context) => context.env.GEMINI_INSTANT_SYMBOL + ? { + side: "buy", + symbol: context.env.GEMINI_INSTANT_SYMBOL, + totalSpend: context.env.GEMINI_INSTANT_TOTAL_SPEND ?? "1", + account: account(context), + } + : MISSING_FIXTURE, + "clearingInstant:listClearingBrokers": (context) => context.env.GEMINI_CLEARING_SYMBOL + ? { symbol: context.env.GEMINI_CLEARING_SYMBOL, limit_orders: 1, account: account(context) } + : MISSING_FIXTURE, + "clearingInstant:listClearingOrders": (context) => context.env.GEMINI_CLEARING_SYMBOL + ? { symbol: context.env.GEMINI_CLEARING_SYMBOL, limit_orders: 1, account: account(context) } + : MISSING_FIXTURE, + "clearingInstant:listClearingTrades": (context) => context.env.GEMINI_CLEARING_SYMBOL + ? { symbol: context.env.GEMINI_CLEARING_SYMBOL, limit_per_account: 1, account: account(context) } + : MISSING_FIXTURE, + "margin:getMarginAccount": (context) => ({ account: account(context) }), + "margin:getMarginRates": (context) => ({ account: account(context) }), + "margin:previewMarginOrder": (context) => ({ + symbol: context.symbol, + side: "buy", + type: "limit", + amount: "0.5", + price: "100", + account: account(context), + }), + "perpetuals:getAccountMargin": (context) => ({ account: account(context), symbol: context.derivativeSymbol }), + "perpetuals:getFundingPaymentReportFile": (context) => ({ account: account(context) }), + "perpetuals:getFundingPaymentReportJson": (context) => ({ account: account(context) }), + "perpetuals:getOpenPositions": (context) => ({ account: account(context) }), + "perpetuals:listFundingPayments": (context) => ({ account: account(context) }), + "trading:getNotionalTradingVolume": (context) => ({ account: account(context) }), + "trading:getTradingVolume": (context) => ({ account: account(context) }), + "trading:listActiveOrders": (context) => ({ account: account(context) }), + "trading:listPastOrders": (context) => ({ symbol: context.symbol, limit_orders: 1, timestamp: String(context.timestamp), account: account(context) }), + "trading:listPastTrades": (context) => ({ symbol: context.symbol, limit_trades: 1, timestamp: String(context.timestamp), account: account(context) }), +}; + +const WRAPPED_INPUTS = new Set([ + "accountServices:getGasFeeEstimation", + "accountServices:getNotionalBalances", + "accountServices:listApprovedAddresses", + "accountServices:listDepositAddresses", + "perpetuals:getFundingPaymentReportFile", + "perpetuals:getFundingPaymentReportJson", + "perpetuals:listFundingPayments", +]); + +function operationKey(operation) { + return `${operation.module}:${operation.methodName}`; +} + +function facadeName(operation) { + return operation.module === "predictionMarkets" ? "predictions" : operation.module; +} + +export function isReadOnlyOperation(operation) { + return /^(?:get|list)/u.test(operation.methodName ?? ""); +} + +function requireFixture(value, name) { + if (value === undefined || value === null || value === "" || value === MISSING_FIXTURE) { + throw new Error(`missing runtime fixture: ${name}`); + } + return value; +} + +function pathInput(operation, context) { + const names = [...operation.path.matchAll(/\{([^}]+)\}/gu)].map((match) => match[1]); + if (names.length === 0) return undefined; + const values = {}; + for (const name of names) { + let value; + if (name === "symbol") { + value = operation.module === "perpetuals" ? context.derivativeSymbol : context.symbol; + if (operation.methodName === "getFundingAmount") value = context.derivativeSymbol; + if (operation.methodName === "getFXRate") value = context.env.GEMINI_MD_FX_SYMBOL; + } else if (name === "time_frame") { + value = context.env.GEMINI_MD_TIMEFRAME ?? "1m"; + } else if (name === "eventTicker") { + value = context.eventTicker; + } else if (name === "instrumentSymbol") { + value = context.comboSymbol; + } else if (name === "date") { + value = context.date; + } else if (name === "network") { + value = context.network; + } else if (name === "token") { + value = context.token; + } else if (name === "ticker") { + value = context.env.GEMINI_MD_TICKER; + } else if (name === "currency") { + value = context.currency; + } else if (name === "timestamp") { + value = context.timestamp; + } + values[name] = requireFixture(value, name); + } + return values; +} + +function queryInput(operation, context) { + const fixture = QUERY_FIXTURES[operationKey(operation)]; + return fixture ? fixture(context) : undefined; +} + +function bodyInput(operation, context) { + const fixture = BODY_FIXTURES[operationKey(operation)]; + if (fixture) return fixture(context); + return operation.method.toLowerCase() === "post" ? {} : undefined; +} + +export function operationArgs(operation, context) { + const key = operationKey(operation); + const path = pathInput(operation, context); + const query = queryInput(operation, context); + const body = bodyInput(operation, context); + + if (body === MISSING_FIXTURE) { + throw new Error(`missing runtime fixture for ${key}`); + } + if (WRAPPED_INPUTS.has(key)) { + const input = {}; + if (path !== undefined) input.path = path; + if (query !== undefined) input.query = query; + if (body !== undefined) input.body = body; + return [input]; + } + if (path !== undefined) return query === undefined ? [path] : [path, query]; + if (query !== undefined) return [query]; + if (body !== undefined) return [body]; + return []; +} + +function responseArray(response, keys = []) { + if (Array.isArray(response)) return response; + for (const key of keys) if (Array.isArray(response?.[key])) return response[key]; + return []; +} + +function firstString(values) { + return values.find((value) => typeof value === "string" && value.length > 0); +} + +function eventTickerFrom(response) { + const events = responseArray(response, ["data", "events"]); + const event = events.find((candidate) => candidate && typeof candidate === "object"); + return event?.eventTicker ?? event?.event_ticker ?? event?.ticker; +} + +function instrumentSymbolFrom(response) { + const events = responseArray(response, ["data", "events"]); + for (const event of events) { + for (const market of [...(event?.markets ?? []), ...(event?.contracts ?? [])]) { + const symbol = typeof market === "string" ? market : market?.symbol ?? market?.instrumentSymbol; + if (symbol) return symbol; + } + } + return undefined; +} + +function comboSymbolFrom(response) { + const combos = responseArray(response, ["data", "combos"]); + for (const combo of combos) { + const symbol = typeof combo === "string" ? combo : combo?.instrumentSymbol ?? combo?.symbol; + if (symbol) return symbol; + } + return undefined; +} + +function updateContext(operation, response, context) { + const key = operationKey(operation); + if (key === "marketData:listSymbols") { + const symbols = responseArray(response, ["data", "symbols"]); + context.symbols = symbols.filter((symbol) => typeof symbol === "string"); + context.symbol = context.env.GEMINI_MD_SYMBOL || preferredSymbol(context.symbols) || context.symbol; + context.derivativeSymbol = context.env.GEMINI_MD_DERIVATIVE_SYMBOL || preferredSymbol(context.symbols, true) || context.derivativeSymbol; + } else if (key === "marketData:listPrices") { + const symbols = priceSymbols(response); + context.symbol = context.env.GEMINI_MD_SYMBOL || preferredSymbol(symbols) || context.symbol; + context.derivativeSymbol = context.env.GEMINI_MD_DERIVATIVE_SYMBOL || preferredSymbol(symbols, true) || context.derivativeSymbol; + } else if (key === "predictionMarkets:listEvents") { + context.eventTicker = context.env.GEMINI_PM_EVENT_TICKER || context.eventTicker || eventTickerFrom(response); + context.instrumentSymbol = context.env.GEMINI_PM_SYMBOL || context.instrumentSymbol || instrumentSymbolFrom(response); + } else if (key === "predictionMarkets:listCombos") { + context.comboSymbol = context.env.GEMINI_PM_COMBO_SYMBOL || context.comboSymbol || comboSymbolFrom(response); + } +} + +function operationResult(operation, status, message) { + return { + module: operation.module, + methodName: operation.methodName, + method: operation.method, + path: operation.path, + status, + ...(message ? { message } : {}), + }; +} + +function safeMessage(error, env) { + const details = [ + error?.message ?? String(error), + error?.reason ? `reason=${error.reason}` : undefined, + error?.code ? `code=${error.code}` : undefined, + error?.serverCode ? `serverCode=${error.serverCode}` : undefined, + ].filter(Boolean).join("; "); + return redact(details, [env.GEMINI_API_KEY, env.GEMINI_API_SECRET]); +} + +function blockedMessage(message) { + return /auth|credential|permission|forbidden|unauthorized|status 401|status 403|missing runtime fixture|method is not available|invalidapikey|service_unavailable|endpointnotfound|not_found|no data|maintenance|sandboxunsupportednetwork|accountnotoftype/iu.test(message); +} + +async function responseEvidence(operation, response) { + if (operation.responseMode !== "file") return undefined; + return verifyFileEvidence(response); +} + +export function exitCodeFor(operations) { + return operations.some((operation) => operation.status === "failed" || operation.status === "blocked") ? 1 : 0; +} + +export async function runVerification({ + operations = INVENTORY, + env = process.env, + loadSdk = () => import("../dist/server/index.js"), + log = console.log, +} = {}) { + const context = { + env, + date: env.GEMINI_PM_DATE ?? new Date().toISOString().slice(0, 10), + timestamp: Date.now(), + symbol: env.GEMINI_MD_SYMBOL || undefined, + derivativeSymbol: env.GEMINI_MD_DERIVATIVE_SYMBOL || undefined, + eventTicker: env.GEMINI_PM_EVENT_TICKER || undefined, + comboSymbol: env.GEMINI_PM_COMBO_SYMBOL || undefined, + instrumentSymbol: env.GEMINI_PM_SYMBOL || undefined, + instantSymbol: env.GEMINI_INSTANT_SYMBOL || env.GEMINI_MD_SYMBOL || undefined, + network: env.GEMINI_MD_NETWORK, + token: env.GEMINI_MD_TOKEN, + currency: env.GEMINI_MD_CURRENCY || "USD", + }; + const results = new Map(operations.map((operation) => [operationKey(operation), operationResult(operation, "skipped", "write or control operation"),])); + const eligible = operations.filter(isReadOnlyOperation); + const ordered = [ + ...eligible.filter((operation) => ["marketData:listSymbols", "marketData:listPrices", "predictionMarkets:listEvents", "predictionMarkets:listCombos"].includes(operationKey(operation))), + ...eligible.filter((operation) => !["marketData:listSymbols", "marketData:listPrices", "predictionMarkets:listEvents", "predictionMarkets:listCombos"].includes(operationKey(operation))), + ]; + let sdk; + try { + sdk = await loadSdk(); + } catch (error) { + for (const operation of eligible) results.set(operationKey(operation), operationResult(operation, "blocked", `SDK import failed: ${safeMessage(error, env)}`)); + return { operations: [...results.values()], counts: countResults(results) }; + } + + const auth = env.GEMINI_API_KEY && env.GEMINI_API_SECRET && sdk.HmacAuth + ? new sdk.HmacAuth({ apiKey: env.GEMINI_API_KEY, apiSecret: env.GEMINI_API_SECRET, nonceMode: env.GEMINI_NONCE_MODE }) + : undefined; + let client; + try { + client = new sdk.GeminiMarkets({ env: env.GEMINI_SMOKE_ENV ?? "sandbox", auth }); + log(`Sandbox REST: checking ${eligible.length} read-only operation(s)`); + for (const operation of ordered) { + const key = operationKey(operation); + log(`Sandbox REST: ${key}`); + try { + const facade = client[facadeName(operation)]; + if (!facade || typeof facade[operation.methodName] !== "function") { + throw new Error("method is not available on GeminiMarkets facade"); + } + const response = await facade[operation.methodName](...operationArgs(operation, context)); + const evidence = await responseEvidence(operation, response); + updateContext(operation, response, context); + results.set(key, { ...operationResult(operation, "passed"), ...(evidence ? { evidence } : {}) }); + log(`Sandbox REST: ${key} passed`); + } catch (error) { + const message = safeMessage(error, env); + results.set(key, operationResult(operation, blockedMessage(message) ? "blocked" : "failed", message)); + log(`Sandbox REST: ${key} ${results.get(key).status} - ${message}`); + } + } + } catch (error) { + const message = safeMessage(error, env); + for (const operation of eligible) { + if (results.get(operationKey(operation)).status === "skipped") { + results.set(operationKey(operation), operationResult(operation, "blocked", message)); + } + } + } finally { + client?.close?.(); + } + const orderedResults = operations.map((operation) => results.get(operationKey(operation))); + const counts = countResults(new Map(orderedResults.map((operation) => [operationKey(operation), operation]))); + log(`Sandbox REST: ${counts.passed} passed, ${counts.blocked} blocked, ${counts.failed} failed, ${counts.skipped} skipped`); + return { operations: orderedResults, counts }; +} + +function countResults(results) { + return Object.fromEntries(["passed", "blocked", "failed", "skipped"].map((status) => [ + status, + [...results.values()].filter((operation) => operation.status === status).length, + ])); +} + +async function main() { + const result = await runVerification(); + process.exitCode = exitCodeFor(result.operations); +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) await main(); diff --git a/packages/sdk-typescript/scripts/verify-sandbox-rest.test.mjs b/packages/sdk-typescript/scripts/verify-sandbox-rest.test.mjs new file mode 100644 index 0000000..76b73a4 --- /dev/null +++ b/packages/sdk-typescript/scripts/verify-sandbox-rest.test.mjs @@ -0,0 +1,143 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + exitCodeFor, + isReadOnlyOperation, + operationArgs, + priceSymbols, + runVerification, +} from "./verify-sandbox-rest.mjs"; + +test("only get/list REST operations are eligible for the read-only verifier", () => { + assert.equal(isReadOnlyOperation({ methodName: "getPositions" }), true); + assert.equal(isReadOnlyOperation({ methodName: "listSymbols" }), true); + assert.equal(isReadOnlyOperation({ methodName: "createNewOrder" }), false); + assert.equal(isReadOnlyOperation({ methodName: "sendHeartbeat" }), false); +}); + +test("builds dynamic arguments for market-data and prediction-market calls", () => { + const context = { + env: {}, + date: "2026-08-06", + timestamp: 1_700_000_000_000, + eventTicker: "FED260806", + symbol: "btcusd", + derivativeSymbol: "BTCGUSDPERP", + }; + + assert.deepEqual( + operationArgs({ module: "marketData", methodName: "getCurrentOrderBook", method: "get", path: "/v1/book/{symbol}" }, context), + [{ symbol: "btcusd" }, { limit_bids: 1, limit_asks: 1 }], + ); + assert.deepEqual( + operationArgs({ module: "predictionMarkets", methodName: "getVolumeMetrics", method: "post", path: "/v1/prediction-markets/metrics/volume" }, context), + [{ eventTicker: "FED260806", startTime: context.timestamp - 86_400_000, endTime: context.timestamp }], + ); + assert.deepEqual( + operationArgs({ module: "accountServices", methodName: "getAvailableBalances", method: "post", path: "/v1/balances" }, context), + [{ account: "primary", showPendingBalances: false }], + ); + assert.deepEqual( + operationArgs({ module: "trading", methodName: "listPastOrders", method: "post", path: "/v1/orders/history" }, context), + [{ symbol: "btcusd", limit_orders: 1, timestamp: "1700000000000", account: "primary" }], + ); +}); + +test("read-only verification never invokes skipped write operations", async () => { + const calls = []; + const facade = new Proxy({}, { + get: (_target, method) => async (...args) => { + calls.push([method, args]); + return method === "listSymbols" ? ["btcusd"] : []; + }, + }); + const result = await runVerification({ + operations: [ + { module: "marketData", methodName: "listSymbols", method: "get", path: "/v1/symbols" }, + { module: "trading", methodName: "createNewOrder", method: "post", path: "/v1/order/new" }, + ], + env: {}, + loadSdk: async () => ({ + GeminiMarkets: class { + marketData = facade; + trading = facade; + close() {} + }, + }), + log: () => {}, + }); + + assert.deepEqual(calls.map(([method]) => method), ["listSymbols"]); + assert.equal(result.operations.find((operation) => operation.methodName === "createNewOrder").status, "skipped"); +}); + +test("routes prediction-market inventory entries to the predictions facade", async () => { + const result = await runVerification({ + operations: [{ module: "predictionMarkets", methodName: "listEvents", method: "get", path: "/v1/prediction-markets/events" }], + env: {}, + loadSdk: async () => ({ + GeminiMarkets: class { + predictions = { async listEvents() { return []; } }; + close() {} + }, + }), + log: () => {}, + }); + + assert.equal(result.operations[0].status, "passed"); +}); + +test("passes the configured nonce mode to HmacAuth", async () => { + let authOptions; + await runVerification({ + operations: [{ module: "marketData", methodName: "listSymbols", method: "get", path: "/v1/symbols" }], + env: { GEMINI_API_KEY: "key", GEMINI_API_SECRET: "secret", GEMINI_NONCE_MODE: "time-based" }, + loadSdk: async () => ({ + HmacAuth: class { constructor(options) { authOptions = options; } }, + GeminiMarkets: class { + marketData = { async listSymbols() { return ["btcusd"]; } }; + close() {} + }, + }), + log: () => {}, + }); + + assert.equal(authOptions.nonceMode, "time-based"); +}); + +test("uses a bigint for timestamp_nanos fixtures", () => { + const args = operationArgs( + { module: "accountServices", methodName: "getTransactionHistory", method: "post", path: "/v1/transactions" }, + { env: {}, date: "2026-08-06", timestamp: 1_700_000_000_000 }, + ); + + assert.equal(typeof args[0].timestamp_nanos, "bigint"); +}); + +test("discovers price-feed pairs as market-data symbols", () => { + assert.deepEqual(priceSymbols([{ pair: "btcusd" }, { pair: "ethusd" }]), ["btcusd", "ethusd"]); +}); + +test("logs the server reason when an API request fails", async () => { + const logs = []; + const result = await runVerification({ + operations: [{ module: "accountServices", methodName: "getRoles", method: "post", path: "/v1/roles" }], + env: {}, + loadSdk: async () => ({ + GeminiMarkets: class { + accountServices = { async getRoles() { throw Object.assign(new Error("HTTP 400"), { reason: "MissingRole" }); } }; + close() {} + }, + }), + log: (message) => logs.push(message), + }); + + assert.equal(result.operations[0].status, "failed"); + assert(logs.some((message) => message.includes("MissingRole"))); +}); + +test("failed or blocked read-only operations fail the verifier, skipped writes do not", () => { + assert.equal(exitCodeFor([{ status: "passed" }, { status: "skipped" }]), 0); + assert.equal(exitCodeFor([{ status: "blocked" }, { status: "skipped" }]), 1); + assert.equal(exitCodeFor([{ status: "failed" }]), 1); +}); diff --git a/packages/sdk-typescript/scripts/websocket-types.test.mjs b/packages/sdk-typescript/scripts/websocket-types.test.mjs new file mode 100644 index 0000000..b854de6 --- /dev/null +++ b/packages/sdk-typescript/scripts/websocket-types.test.mjs @@ -0,0 +1,82 @@ +import assert from "node:assert/strict"; +import { execFile as execFileCallback } from "node:child_process"; +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import test from "node:test"; +import { promisify } from "node:util"; +import { fileURLToPath } from "node:url"; + +const execFile = promisify(execFileCallback); +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const sdkDir = resolve(scriptDir, ".."); +const repoRoot = resolve(sdkDir, "../.."); +const generatorPath = resolve(sdkDir, "scripts/generate-ws-types.mjs"); +const localWsSpec = resolve(repoRoot, "apis/websocket.yaml"); +const specPath = existsSync(localWsSpec) ? localWsSpec : "https://developer.gemini.com/specs/asyncapi/websocket.yaml"; +const sdkGeneratedPath = resolve(sdkDir, "src/generated/websocket/index.ts"); + +function declarationNames(source) { + return [...source.matchAll(/^export (?:interface|enum|type) (\w+)/gm)].map(([, name]) => name); +} + +function interfaceBlock(source, name) { + const start = source.indexOf(`export interface ${name} {`); + assert.notEqual(start, -1, `${name} interface not found`); + const end = source.indexOf("\n}", start); + assert.notEqual(end, -1, `${name} interface end not found`); + return source.slice(start, end + 2); +} + +test("generated WebSocket types match the AsyncAPI generator output", async (t) => { + const directory = mkdtempSync(join(tmpdir(), "ws-types-")); + t.after(() => rmSync(directory, { recursive: true, force: true })); + await execFile(process.execPath, [generatorPath, directory, specPath]); + + const fresh = readFileSync(resolve(directory, "index.ts"), "utf8"); + const sdkGenerated = readFileSync(sdkGeneratedPath, "utf8"); + + assert.equal(sdkGenerated, fresh, "SDK generated WebSocket types are stale"); +}); + +test("generated WebSocket types cover documented request, response, and stream messages", () => { + const source = readFileSync(sdkGeneratedPath, "utf8"); + const names = declarationNames(source); + + assert.equal(names.length, 59); + for (const name of [ + "DepthUpdate", + "BookTicker", + "Trade", + "OrderUpdate", + "BalanceUpdate", + "PositionReport", + "ContractStatus", + "RfqPublicEvent", + "RfqPrivateDelivery", + "OrderPlaceRequest", + "RfqConfirmQuoteResponse", + ]) { + assert(names.includes(name), `${name} is missing from generated WebSocket types`); + } +}); + +test("generated WebSocket types preserve wire keys and widened int64 fields", () => { + const source = readFileSync(sdkGeneratedPath, "utf8"); + const depthUpdate = interfaceBlock(source, "DepthUpdate"); + + for (const key of ["e", "E", "s", "U", "u", "b", "a"]) { + assert(depthUpdate.split("\n").some((line) => line.trimStart().startsWith(`${key}:`)), `${key} wire key is missing`); + } + assert.match(depthUpdate, /^\s*E: number \| bigint;/m); + assert.match(depthUpdate, /^\s*U: number \| bigint;/m); + assert.match(depthUpdate, /^\s*u: number \| bigint;/m); +}); + +test("generated control-plane request method literals stay narrowed", () => { + const source = readFileSync(sdkGeneratedPath, "utf8"); + + assert.match(interfaceBlock(source, "SubscribeRequest"), /^\s*method: "SUBSCRIBE" \| "subscribe";/m); + assert.match(interfaceBlock(source, "UnsubscribeRequest"), /^\s*method: "UNSUBSCRIBE" \| "unsubscribe";/m); + assert.match(interfaceBlock(source, "ListSubscriptionsRequest"), /^\s*method: "LIST_SUBSCRIPTIONS" \| "list_subscriptions";/m); +}); diff --git a/packages/sdk-typescript/src/auth/hmac.ts b/packages/sdk-typescript/src/auth/hmac.ts new file mode 100644 index 0000000..d1ff11c --- /dev/null +++ b/packages/sdk-typescript/src/auth/hmac.ts @@ -0,0 +1,86 @@ +import { toHex } from "../core/encoding.js"; + +import type { AuthStrategy } from "../core/http.js"; +import { SdkError } from "../errors.js"; + +export type HmacNonceMode = "monotonic" | "time-based"; + +export interface HmacAuthOptions { + apiKey: string; + apiSecret: string; + nonceMode?: HmacNonceMode; + now?: () => number; +} + +export class HmacAuth implements AuthStrategy { + readonly #apiKey: string; + readonly #keyPromise: Promise; + readonly #nonceMode: HmacNonceMode; + readonly #now: () => number; + #lastNonce?: bigint; + #signQueue: Promise = Promise.resolve(); + + constructor(options: HmacAuthOptions) { + if (!options || typeof options !== "object") { + throw new SdkError("options are required"); + } + if (typeof options.apiKey !== "string" || options.apiKey.length === 0) { + throw new SdkError("apiKey is required"); + } + if (typeof options.apiSecret !== "string" || options.apiSecret.length === 0) { + throw new SdkError("apiSecret is required"); + } + if (options.nonceMode !== undefined && !["monotonic", "time-based"].includes(options.nonceMode)) { + throw new SdkError("nonceMode must be monotonic or time-based"); + } + if (options.now !== undefined && typeof options.now !== "function") { + throw new SdkError("now must be a function"); + } + this.#apiKey = options.apiKey; + this.#keyPromise = crypto.subtle.importKey( + "raw", + new TextEncoder().encode(options.apiSecret), + { name: "HMAC", hash: "SHA-384" }, + false, + ["sign"], + ); + this.#nonceMode = options.nonceMode ?? "monotonic"; + this.#now = options.now ?? Date.now; + } + + nextNonce(): string { + const now = this.#now(); + if (!Number.isSafeInteger(now) || now < 0) { + throw new SdkError("nonce clock must return a non-negative safe integer timestamp"); + } + if (this.#nonceMode === "time-based") { + return Math.floor(now / 1000).toString(); + } + + const candidate = BigInt(Math.trunc(now)); + this.#lastNonce = this.#lastNonce === undefined || candidate > this.#lastNonce + ? candidate + : this.#lastNonce + 1n; + return this.#lastNonce.toString(); + } + + async credentialHeaders(payloadBase64: string): Promise> { + // Serialize signing so that requests dispatched in nonce order arrive at the + // server in nonce order. Without this, concurrent await crypto.subtle.sign() + // calls can resolve out of order, causing the server to reject valid nonces. + const result = this.#signQueue.then(async () => { + const key = await this.#keyPromise; + const signature = await crypto.subtle.sign( + "HMAC", + key, + new TextEncoder().encode(payloadBase64), + ); + return { + "X-GEMINI-APIKEY": this.#apiKey, + "X-GEMINI-SIGNATURE": toHex(new Uint8Array(signature)), + }; + }); + this.#signQueue = result.then(() => {}, () => {}); + return result; + } +} diff --git a/packages/sdk-typescript/src/auth/oauth.ts b/packages/sdk-typescript/src/auth/oauth.ts new file mode 100644 index 0000000..0acddd9 --- /dev/null +++ b/packages/sdk-typescript/src/auth/oauth.ts @@ -0,0 +1,417 @@ + +import type { AuthStrategy, FetchLike, HttpTransport } from "../core/http.js"; +import { DEFAULT_TIMEOUT_MS, deadline, type RequestOptions, withSignal } from "../core/deadline.js"; +import { + OAuthAuthorizationError, + OAuthStateError, + OAuthTokenError, + SdkError, + serializeError, +} from "../errors.js"; +import { createResponseMetadata, type DiagnosticListener, type ResponseMetadata } from "../diagnostics.js"; +import { emitDiagnostic, type Logger, NOOP_LOGGER } from "../logging.js"; +import { ENVIRONMENT_URLS, type Environment } from "../core/environment.js"; +import { toBase64Url } from "../core/encoding.js"; +const REVOKE_PATH = "/v1/oauth/revokeByToken"; +const DEFAULT_REFRESH_SKEW_MS = 60_000; + +export type OAuthClient = + | { type: "public"; clientId: string; redirectUri: string } + | { type: "confidential"; clientId: string; clientSecret: string; redirectUri: string }; + +export interface OAuthTokens { + accessToken: string; + refreshToken: string; + tokenType: "bearer"; + scope: string; + /** Absolute Unix time in milliseconds. */ + expiresAt: number; +} + +/** + * Caller-owned token persistence. runExclusive must serialize operations across + * every OAuthAuth instance and process backed by this store; single-use refresh + * token rotation depends on that shared exclusion. + */ +export interface OAuthTokenStore { + load(): Promise; + save(tokens: OAuthTokens): Promise; + clear(): Promise; + runExclusive(operation: () => Promise): Promise; +} + +export interface OAuthAuthorizationTransaction { + state: string; + /** Present only for public clients; keep it private until the callback. */ + codeVerifier?: string; +} + +export interface OAuthAuthorizationRequest { + url: string; + transaction: OAuthAuthorizationTransaction; +} + +export interface OAuthAuthOptions { + client: OAuthClient; + tokenStore: OAuthTokenStore; + /** OAuth environment. Defaults to production. */ + env?: Environment; + fetchImpl?: FetchLike; + now?: () => number; + randomBytes?: (size: number) => Uint8Array; + /** Refresh this many milliseconds before expiry. Defaults to 60 seconds. */ + refreshSkewMs?: number; + /** End-to-end timeout for token exchange and refresh. Defaults to 30 seconds. */ + timeoutMs?: number; + /** Receives safe OAuth lifecycle diagnostics. Defaults to silent. */ + logger?: Logger; + onDiagnostic?: DiagnosticListener; +} + +type TokenEndpointResponse = { + access_token?: unknown; + refresh_token?: unknown; + token_type?: unknown; + scope?: unknown; + expires_in?: unknown; + error?: unknown; + error_description?: unknown; +}; + +function requiredString(value: unknown, name: string): string { + if (typeof value !== "string" || value.length === 0) { + throw new SdkError(`${name} is required`); + } + return value; +} + +function validateStoredTokens(tokens: unknown): OAuthTokens | undefined { + if (tokens === undefined) { + return undefined; + } + if (tokens === null || typeof tokens !== "object" || Array.isArray(tokens)) { + throw new SdkError("stored OAuth tokens must be an object"); + } + const record = tokens as Record; + const accessToken = requiredString(record.accessToken, "stored OAuth accessToken"); + const refreshToken = requiredString(record.refreshToken, "stored OAuth refreshToken"); + if (record.tokenType !== "bearer") { + throw new SdkError("stored OAuth tokenType must be bearer"); + } + if (typeof record.scope !== "string") { + throw new SdkError("stored OAuth scope must be a string"); + } + const expiresAt = record.expiresAt; + if (typeof expiresAt !== "number" || !Number.isSafeInteger(expiresAt) || expiresAt < 0) { + throw new SdkError("stored OAuth expiresAt must be a non-negative safe integer"); + } + return { + accessToken, + refreshToken, + tokenType: "bearer", + scope: record.scope, + expiresAt, + }; +} + + +export class OAuthAuth implements AuthStrategy { + readonly #client: OAuthClient; + readonly #tokenStore: OAuthTokenStore; + readonly #fetchImpl: FetchLike; + readonly #now: () => number; + readonly #randomBytes: (size: number) => Uint8Array; + readonly #refreshSkewMs: number; + readonly #timeoutMs: number; + readonly #logger: Logger; + readonly #onDiagnostic?: DiagnosticListener; + readonly #authorizationUrl: string; + readonly #tokenUrl: string; + #revocationAccessToken?: string; + + constructor(options: OAuthAuthOptions) { + if (!options || typeof options !== "object") { + throw new SdkError("options are required"); + } + if (!options.client || !["public", "confidential"].includes(options.client.type)) { + throw new SdkError("client must be public or confidential"); + } + requiredString(options.client.clientId, "clientId"); + requiredString(options.client.redirectUri, "redirectUri"); + if (options.client.type === "confidential") { + requiredString(options.client.clientSecret, "clientSecret"); + } + if (!options.tokenStore || typeof options.tokenStore.load !== "function" || + typeof options.tokenStore.save !== "function" || + typeof options.tokenStore.clear !== "function" || + typeof options.tokenStore.runExclusive !== "function") { + throw new SdkError("tokenStore must implement load, save, clear, and runExclusive"); + } + const skew = options.refreshSkewMs ?? DEFAULT_REFRESH_SKEW_MS; + if (!Number.isFinite(skew) || skew < 0) { + throw new SdkError("refreshSkewMs must be a finite non-negative number"); + } + this.#client = { ...options.client }; + this.#tokenStore = options.tokenStore; + const environment = ENVIRONMENT_URLS[options.env ?? "production"]; + this.#authorizationUrl = environment.oauthAuthorization; + this.#tokenUrl = environment.oauthToken; + this.#fetchImpl = options.fetchImpl ?? + ((url, init) => fetch(url, init) as ReturnType); + this.#now = options.now ?? Date.now; + this.#randomBytes = options.randomBytes ?? ((size: number) => crypto.getRandomValues(new Uint8Array(size))); + this.#refreshSkewMs = skew; + this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; + this.#logger = options.logger ?? NOOP_LOGGER; + this.#onDiagnostic = options.onDiagnostic; + if (!Number.isFinite(this.#timeoutMs) || this.#timeoutMs <= 0) { + throw new SdkError("timeoutMs must be a finite positive number"); + } + } + + #emit( + level: "debug" | "info" | "warn" | "error", + name: string, + response: ResponseMetadata, + error?: unknown, + ): void { + emitDiagnostic({ + level, + component: "oauth", + name, + response, + ...(error ? { error: serializeError(error) } : {}), + }, this.#logger, this.#onDiagnostic); + } + + async beginAuthorization(scopes: string[]): Promise { + if (!Array.isArray(scopes) || scopes.length === 0 || + scopes.some((scope) => typeof scope !== "string" || scope.length === 0)) { + throw new SdkError("scopes must contain at least one non-empty scope"); + } + const state = toBase64Url(this.#randomBytes(32)); + const params = new URLSearchParams({ + client_id: this.#client.clientId, + response_type: "code", + redirect_uri: this.#client.redirectUri, + state, + scope: scopes.join(","), + }); + const transaction: OAuthAuthorizationTransaction = { state }; + + if (this.#client.type === "public") { + const codeVerifier = toBase64Url(this.#randomBytes(64)); + if (!/^[A-Za-z0-9._~-]{43,128}$/.test(codeVerifier)) { + throw new SdkError("generated PKCE verifier must be 43-128 unreserved characters"); + } + const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier)); + const challenge = toBase64Url(new Uint8Array(hash)); + transaction.codeVerifier = codeVerifier; + params.set("code_challenge", challenge); + params.set("code_challenge_method", "S256"); + } + + return { url: `${this.#authorizationUrl}?${params}`, transaction }; + } + + async completeAuthorization( + callback: string | URL, + transaction: OAuthAuthorizationTransaction, + options: RequestOptions = {}, + ): Promise { + const url = callback instanceof URL ? callback : new URL(callback); + const returnedState = url.searchParams.get("state"); + if (!returnedState) { + throw new OAuthStateError("OAuth callback is missing state"); + } + if (!transaction?.state || returnedState !== transaction.state) { + throw new OAuthStateError("OAuth callback state does not match the authorization request"); + } + const callbackError = url.searchParams.get("error"); + if (callbackError) { + throw new OAuthAuthorizationError( + callbackError, + url.searchParams.get("error_description") ?? undefined, + ); + } + const code = url.searchParams.get("code"); + if (!code) { + throw new OAuthAuthorizationError("invalid_response", "OAuth callback is missing code"); + } + + const body: Record = { + client_id: this.#client.clientId, + code, + redirect_uri: this.#client.redirectUri, + grant_type: "authorization_code", + }; + if (this.#client.type === "public") { + if (!transaction.codeVerifier || + !/^[A-Za-z0-9._~-]{43,128}$/.test(transaction.codeVerifier)) { + throw new SdkError("public OAuth transaction is missing a valid PKCE verifier"); + } + body.code_verifier = transaction.codeVerifier; + } else { + body.client_secret = this.#client.clientSecret; + } + + return this.#tokenStore.runExclusive(async () => { + const tokens = await this.#tokenRequest(body, options); + await this.#tokenStore.save(tokens); + return tokens; + }); + } + + nextNonce(): undefined { + return undefined; + } + + async credentialHeaders(_payloadBase64: string, options: RequestOptions = {}): Promise> { + const accessToken = this.#revocationAccessToken ?? (await this.#validTokens(options)).accessToken; + return { Authorization: `Bearer ${accessToken}` }; + } + + async revoke(transport: HttpTransport, options: RequestOptions = {}): Promise { + if (!transport.isAuthenticatedWith(this)) { + throw new SdkError("revoke transport must use the same OAuthAuth instance"); + } + await this.#validTokens(options); + await this.#tokenStore.runExclusive(async () => { + const current = validateStoredTokens(await this.#tokenStore.load()); + if (!current) { + throw new SdkError("OAuth tokens are unavailable; complete authorization first"); + } + this.#revocationAccessToken = current.accessToken; + try { + await transport.request({ method: "POST", path: REVOKE_PATH, ...options }); + await this.#tokenStore.clear(); + } finally { + this.#revocationAccessToken = undefined; + } + }); + } + + async #validTokens(options: RequestOptions = {}): Promise { + const tokens = validateStoredTokens(await this.#tokenStore.load()); + if (!tokens) { + throw new SdkError("OAuth tokens are unavailable; complete authorization first"); + } + if (this.#isValid(tokens)) { + return tokens; + } + + return this.#tokenStore.runExclusive(async () => { + const current = validateStoredTokens(await this.#tokenStore.load()); + if (!current) { + throw new SdkError("OAuth tokens are unavailable; complete authorization first"); + } + return this.#isValid(current) ? current : this.#refresh(current, options); + }); + } + + #isValid(tokens: OAuthTokens): boolean { + return tokens.expiresAt > this.#now() + this.#refreshSkewMs; + } + + async #refresh(current: OAuthTokens, options: RequestOptions = {}): Promise { + const body: Record = { + client_id: this.#client.clientId, + refresh_token: current.refreshToken, + grant_type: "refresh_token", + }; + if (this.#client.type === "confidential") { + body.client_secret = this.#client.clientSecret; + } + + try { + const tokens = await this.#tokenRequest(body, options); + await this.#tokenStore.save(tokens); + return tokens; + } catch (error) { + if (error instanceof OAuthTokenError && error.error === "invalid_grant") { + await this.#tokenStore.clear(); + } + throw error; + } + } + + async #tokenRequest(body: Record, options: RequestOptions = {}): Promise { + const execution = deadline(options, this.#timeoutMs); + const correlationId = crypto.randomUUID(); + const metadata = (status?: number, response?: { headers?: { get(name: string): string | null } }): ResponseMetadata => + createResponseMetadata({ endpoint: this.#tokenUrl, method: "POST", correlationId, status, retryCount: 0, headers: response?.headers }); + const eventName = body.grant_type === "refresh_token" ? "token.refresh" : "token.exchange"; + this.#emit("debug", "token.request.start", metadata()); + let response: Awaited>; + let text: string; + try { + response = await withSignal(this.#fetchImpl(this.#tokenUrl, { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + body: JSON.stringify(body), + signal: execution.signal, + }), execution.signal); + text = await withSignal(response.text(), execution.signal); + } catch (cause) { + const error = cause instanceof SdkError + ? cause + : new SdkError("OAuth token request failed", { cause, metadata: metadata() }); + this.#emit("error", "token.request.failure", metadata(), error); + throw error; + } finally { + execution.cleanup(); + } + + let parsed: TokenEndpointResponse; + try { + parsed = JSON.parse(text) as TokenEndpointResponse; + } catch (cause) { + const error = new SdkError("OAuth token endpoint returned unparseable JSON", { cause, metadata: metadata(response.status, response) }); + this.#emit("error", "token.response.failure", metadata(response.status, response), error); + throw error; + } + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + const error = new SdkError("OAuth token endpoint returned an invalid response object", { metadata: metadata(response.status, response) }); + this.#emit("error", "token.response.failure", metadata(response.status, response), error); + throw error; + } + if (typeof parsed.error === "string" || response.status < 200 || response.status >= 300) { + const error = new OAuthTokenError({ + status: response.status, + error: typeof parsed.error === "string" ? parsed.error : "token_endpoint_error", + errorDescription: typeof parsed.error_description === "string" + ? parsed.error_description + : undefined, + body: parsed, + metadata: metadata(response.status, response), + }); + this.#emit("error", "token.request.failure", metadata(response.status, response), error); + throw error; + } + + const accessToken = requiredString(parsed.access_token, "access_token"); + const refreshToken = requiredString(parsed.refresh_token, "refresh_token"); + const tokenType = requiredString(parsed.token_type, "token_type").toLowerCase(); + if (tokenType !== "bearer") { + throw new SdkError(`unsupported OAuth token_type ${tokenType}`); + } + if (typeof parsed.expires_in !== "number" || !Number.isInteger(parsed.expires_in) || + parsed.expires_in <= 0) { + throw new SdkError("expires_in must be a positive integer"); + } + const expiresAt = this.#now() + parsed.expires_in * 1000; + if (!Number.isSafeInteger(expiresAt)) { + throw new SdkError("OAuth expiration is out of range"); + } + + const tokens: OAuthTokens = { + accessToken, + refreshToken, + tokenType: "bearer", + scope: typeof parsed.scope === "string" ? parsed.scope : "", + expiresAt, + }; + this.#emit("info", eventName, metadata(response.status, response)); + return tokens; + } +} diff --git a/packages/sdk-typescript/src/browser/index.ts b/packages/sdk-typescript/src/browser/index.ts new file mode 100644 index 0000000..219a99a --- /dev/null +++ b/packages/sdk-typescript/src/browser/index.ts @@ -0,0 +1,155 @@ +// Browser entry point — no HMAC, no Node dependencies, no server-only auth. + +// --- Core --- +export * from "../logging.js"; +export * from "../diagnostics.js"; +export * from "../errors.js"; +export * from "../orderbook.js"; +export * from "../json.js"; +export type { Environment } from "../core/environment.js"; +export type { RequestOptions } from "../core/deadline.js"; +export { + HttpTransport, +} from "../core/http.js"; +export type { + AuthStrategy, + FetchLike, + HttpMethod, + HttpTransportOptions, + RestFileResponse, + RestQueryParameter, + RestResponseContract, + RestResponseMode, +} from "../core/http.js"; + +// --- Auth (OAuth only — no HMAC, no confidential client) --- + +import { OAuthAuth as _OAuthAuth } from "../auth/oauth.js"; +import type { OAuthAuthOptions as _FullOAuthAuthOptions } from "../auth/oauth.js"; + +/** Browser OAuth client — public clients only (no client secret). */ +export type BrowserOAuthClient = { type: "public"; clientId: string; redirectUri: string }; + +/** Browser-safe OAuth options — restricts client to public (PKCE) only. */ +export type BrowserOAuthAuthOptions = Omit<_FullOAuthAuthOptions, "client"> & { + client: BrowserOAuthClient; +}; + +/** + * Browser-safe OAuthAuth — only accepts public clients (no client secret). + * Use `gemini-markets/server` for confidential OAuth flows. + */ +export class BrowserOAuthAuth extends _OAuthAuth { + constructor(options: BrowserOAuthAuthOptions) { + super(options); + } +} + +export { + type OAuthAuthorizationRequest, + type OAuthAuthorizationTransaction, + type OAuthTokens, + type OAuthTokenStore, +} from "../auth/oauth.js"; + +// --- WebSocket --- +export * from "../websocket-types.js"; +export { + GeminiWebSocket, + type GeminiWebSocketOptions, + type DepthIntervalMs, + type DepthSnapshotOptions, + type DepthUpdatesOptions, + type PartialDepthLevel, + type PartialDepthOptions, + type WebSocketAccountIntervalOptions, + type WebSocketCancelAllOptions, + type WebSocketOrderPlaceParams, + type WebSocketScopeOptions, + type WebSocketStream, + type WebSocketStreamState, +} from "../websocket.js"; +export { WsSession, type WsSessionOptions, type WsSubscription } from "../ws-session.js"; +export { ManagedHeartbeat, type ManagedHeartbeatOptions } from "../heartbeat.js"; + +// --- Client --- +export type { GeminiMarketsOptions, BookEvent, BookDelta, LiveOrderBook } from "../types/client.js"; +export { GeminiMarkets } from "../gemini-markets.js"; + +// --- Generated REST clients --- +export { MarketDataRest, MarketDataRest as MarketDataClient } from "../generated/market-data/rest.js"; +export { TradingRest, TradingRest as TradingClient } from "../generated/trading/rest.js"; +export { MarginRest, MarginRest as MarginClient } from "../generated/margin/rest.js"; +export { PerpetualsRest, PerpetualsRest as PerpetualsClient } from "../generated/perpetuals/rest.js"; +export { AccountServicesRest, AccountServicesRest as AccountServicesClient } from "../generated/account-services/rest.js"; +export { ClearingInstantRest, ClearingInstantRest as ClearingInstantClient } from "../generated/clearing-instant/rest.js"; +export { PredictionMarketsRest } from "../generated/rest.js"; +export { PredictionMarkets } from "../prediction-markets.js"; + +// --- Generated types & operations --- +export type { + paths as PredictionMarketsPaths, + components as PredictionMarketsComponents, + operations as PredictionMarketsOpenApiOperations, +} from "../generated/models.js"; +export { + PREDICTION_MARKET_OPERATIONS, + type PredictionMarketOperationId, + type PredictionMarketOperationTypes, +} from "../generated/operations.js"; +export { + MARKET_DATA_OPERATIONS, + type MarketDataOperationId, + type MarketDataOperationTypes, +} from "../generated/market-data/operations.js"; +export { + TRADING_OPERATIONS, + type TradingOperationId, + type TradingOperationTypes, +} from "../generated/trading/operations.js"; +export { + MARGIN_OPERATIONS, + type MarginOperationId, + type MarginOperationTypes, +} from "../generated/margin/operations.js"; +export { + PERPETUALS_OPERATIONS, + type PerpetualsOperationId, + type PerpetualsOperationTypes, +} from "../generated/perpetuals/operations.js"; +export { + ACCOUNT_SERVICES_OPERATIONS, + type AccountServicesOperationId, + type AccountServicesOperationTypes, +} from "../generated/account-services/operations.js"; +export { + CLEARING_INSTANT_OPERATIONS, + type ClearingInstantOperationId, + type ClearingInstantOperationTypes, +} from "../generated/clearing-instant/operations.js"; + +// --- Browser createClient --- + +import { GeminiMarkets } from "../gemini-markets.js"; +import type { GeminiMarketsOptions } from "../types/client.js"; + +/** Browser-safe options — same as GeminiMarketsOptions (auth is optional, typically OAuthAuth). */ +export type BrowserClientOptions = GeminiMarketsOptions; + +/** + * Create a Gemini Markets client with browser-safe defaults. + * Uses native `fetch` and `WebSocket` — no Node dependencies. + * + * For public market data, no options are needed: + * ```ts + * const client = createClient(); + * ``` + * + * For authenticated access, pass an OAuthAuth instance: + * ```ts + * const client = createClient({ auth: oauthAuth }); + * ``` + */ +export function createClient(options?: BrowserClientOptions): GeminiMarkets { + return new GeminiMarkets(options); +} diff --git a/packages/sdk-typescript/src/core/deadline.ts b/packages/sdk-typescript/src/core/deadline.ts new file mode 100644 index 0000000..a669033 --- /dev/null +++ b/packages/sdk-typescript/src/core/deadline.ts @@ -0,0 +1,43 @@ +import { RequestAbortedError, RequestTimeoutError, SdkError } from "../errors.js"; + +export const DEFAULT_TIMEOUT_MS = 30_000; + +/** Controls one bounded SDK operation. A timeout covers retries and response reads. */ +export interface RequestOptions { + signal?: AbortSignal; + timeoutMs?: number; +} + +export function deadline(options: RequestOptions = {}, defaultTimeoutMs = DEFAULT_TIMEOUT_MS): { + signal: AbortSignal; + cleanup(): void; +} { + const timeoutMs = options.timeoutMs ?? defaultTimeoutMs; + if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new SdkError("timeoutMs must be a finite positive number"); + const controller = new AbortController(); + const abort = () => controller.abort(new RequestAbortedError("request was aborted")); + if (options.signal?.aborted) abort(); + else options.signal?.addEventListener("abort", abort, { once: true }); + const timer = setTimeout(() => controller.abort(new RequestTimeoutError(`request exceeded ${timeoutMs}ms deadline`)), timeoutMs); + return { signal: controller.signal, cleanup: () => { clearTimeout(timer); options.signal?.removeEventListener("abort", abort); } }; +} + +export async function withSignal(promise: Promise, signal: AbortSignal): Promise { + if (signal.aborted) throw signal.reason instanceof SdkError ? signal.reason : new RequestAbortedError("request was aborted"); + return new Promise((resolve, reject) => { + const abort = () => { + cleanup(); + reject(signal.reason instanceof SdkError ? signal.reason : new RequestAbortedError("request was aborted")); + }; + const cleanup = () => signal.removeEventListener("abort", abort); + signal.addEventListener("abort", abort, { once: true }); + void promise.then( + (value) => { cleanup(); resolve(value); }, + (error) => { cleanup(); reject(error); }, + ); + }); +} + +export async function sleepWithSignal(ms: number, signal: AbortSignal): Promise { + await withSignal(new Promise((resolve) => setTimeout(resolve, ms)), signal); +} diff --git a/packages/sdk-typescript/src/core/encoding.ts b/packages/sdk-typescript/src/core/encoding.ts new file mode 100644 index 0000000..2372b9e --- /dev/null +++ b/packages/sdk-typescript/src/core/encoding.ts @@ -0,0 +1,53 @@ +const encoder = new TextEncoder(); + +/** Encode a UTF-8 string to standard base64. */ +export function toBase64(text: string): string { + const bytes = encoder.encode(text); + return btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join("")); +} + +/** Encode a Uint8Array to URL-safe base64 (no padding). */ +export function toBase64Url(bytes: Uint8Array): string { + const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join(""); + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} + +const decoder = new TextDecoder(); + +/** Decode a standard base64 string to a UTF-8 string. */ +export function fromBase64(encoded: string): string { + return decoder.decode(fromBase64Bytes(encoded)); +} + +/** Decode a standard base64 string to bytes. */ +export function fromBase64Bytes(encoded: string): Uint8Array { + const binary = atob(encoded); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return bytes; +} + +/** Decode a URL-safe base64 string to bytes. */ +export function fromBase64Url(encoded: string): Uint8Array { + const padded = encoded.replace(/-/g, "+").replace(/_/g, "/"); + return fromBase64Bytes(padded); +} + +/** Return the UTF-8 byte length of a string without allocating the full buffer. */ +export function utf8ByteLength(text: string): number { + return new Blob([text]).size; +} + +/** Encode bytes as lowercase hex. */ +export function toHex(bytes: Uint8Array): string { + return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); +} + +/** Compute HMAC-SHA-384 and return the hex digest. */ +export async function hmacSha384Hex(secret: string, data: string): Promise { + const key = await crypto.subtle.importKey( + "raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-384" }, false, ["sign"], + ); + const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(data)); + return toHex(new Uint8Array(sig)); +} diff --git a/packages/sdk-typescript/src/core/environment.ts b/packages/sdk-typescript/src/core/environment.ts new file mode 100644 index 0000000..3936e66 --- /dev/null +++ b/packages/sdk-typescript/src/core/environment.ts @@ -0,0 +1,16 @@ +export const ENVIRONMENT_URLS = { + production: { + rest: "https://api.gemini.com", + websocket: "wss://ws.gemini.com", + oauthAuthorization: "https://exchange.gemini.com/auth", + oauthToken: "https://exchange.gemini.com/auth/token", + }, + sandbox: { + rest: "https://api.sandbox.gemini.com", + websocket: "wss://ws.sandbox.gemini.com", + oauthAuthorization: "https://exchange.sandbox.gemini.com/auth", + oauthToken: "https://exchange.sandbox.gemini.com/auth/token", + }, +} as const; + +export type Environment = keyof typeof ENVIRONMENT_URLS; diff --git a/packages/sdk-typescript/src/core/http.ts b/packages/sdk-typescript/src/core/http.ts new file mode 100644 index 0000000..a270cd0 --- /dev/null +++ b/packages/sdk-typescript/src/core/http.ts @@ -0,0 +1,769 @@ +import { toBase64 } from "./encoding.js"; + +import { + AcceptTermsRequired, + ApiError, + EndpointMismatch, + InsufficientFunds, + InvalidNonce, + InvalidRequest, + InvalidSignature, + MissingNonce, + MissingRole, + NotFoundError, + RateLimitError, + SdkError, + ServiceUnavailable, + classifyServerError, + serializeError, +} from "../errors.js"; +import { DEFAULT_TIMEOUT_MS, deadline, sleepWithSignal, type RequestOptions, withSignal } from "./deadline.js"; +import { + type Int64Path, + normalizeInt64Paths, + parseLosslessJson, +} from "../json.js"; +import { emitDiagnostic, type Logger, NOOP_LOGGER } from "../logging.js"; +import { createResponseMetadata, type DiagnosticListener, type OperationContext, type ResponseMetadata } from "../diagnostics.js"; +import { ENVIRONMENT_URLS, type Environment } from "./environment.js"; + +type ApiErrorOptions = ConstructorParameters[0]; +type ApiErrorCtor = new (options: ApiErrorOptions) => ApiError; + +// Reason code (normalized: lowercased, non-alphanumerics stripped) -> error type. +// Normalizing absorbs the casing/spacing drift the exchange emits ("RateLimit", +// "RATE_LIMIT", "Rate Limit" all collapse to "ratelimit"). +const REASON_CLASS: Record = { + invalidnonce: InvalidNonce, + missingnonce: MissingNonce, + invalidsignature: InvalidSignature, + missingrole: MissingRole, + accepttermsrequired: AcceptTermsRequired, + termsnotaccepted: AcceptTermsRequired, + predictionmarketstermsmustbeacceptedbeforeplacingorders: AcceptTermsRequired, + insufficientfunds: InsufficientFunds, + ratelimit: RateLimitError, +}; +const RETRYABLE_STATUS_CODES: readonly number[] = [429, 502, 503, 504]; + +const DEFAULT_BACKOFF_BASE_MS = 500; +const DEFAULT_BACKOFF_CAP_MS = 30_000; +const DEFAULT_BACKOFF_FACTOR = 2; +const MAX_SETTIMEOUT_MS = 2_147_483_647; +const MAX_PAGE_SIZE = 500; +const DEFAULT_PAGE_SIZE = 50; +const DEFAULT_MAX_RETRIES = 5; + +// HTTP status -> error type, used when the reason code is absent or unrecognized. +function statusClass(status: number): ApiErrorCtor | undefined { + if (status === 400) return InvalidRequest; + if (status === 403) return MissingRole; + if (status === 404) return NotFoundError; + if (status === 406) return InsufficientFunds; + if (status === 429) return RateLimitError; + if (status >= 500) return ServiceUnavailable; + return undefined; +} + +// Map a non-2xx response to a typed error. Status is the primary key (always +// present); the body refines the specific reason. Unmapped -> generic ApiError. +function mapError( + status: number, + body: unknown, + metadata?: ResponseMetadata, + operationContext?: OperationContext, +): ApiError { + const classification = classifyServerError(body, status); + const rawReason = classification.reason; + const norm = rawReason?.toLowerCase().replace(/[^a-z0-9]/g, ""); + const ctor = (norm && REASON_CLASS[norm]) || statusClass(status) || ApiError; + return new ctor({ + status, + reason: rawReason, + body, + metadata, + operationContext, + code: classification.code, + category: classification.category, + serverCode: classification.serverCode, + authorizationContext: classification.authorizationContext, + }); +} + +export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; +export type RestResponseMode = "json" | "file"; +export type RestResponseContract = { + successStatuses: readonly number[]; + responseContentTypes: readonly string[]; +}; +export type RestQueryParameter = { + name: string; + in: string; + required: boolean; + style: string; + explode: boolean; + shape?: "scalar" | "array" | "object"; + allowReserved?: boolean; +}; +export type RestFileResponse = { + bytes: Uint8Array; + contentType?: string; + contentDisposition?: string; +}; + +type FetchResponse = { + status: number; + headers?: { get(name: string): string | null }; + text(): Promise; + arrayBuffer?(): Promise; +}; + +/** The minimal slice of `fetch` this transport depends on. Native `fetch` satisfies it. */ +export type FetchLike = ( + url: string, + init: { method: HttpMethod; headers: Record; body?: string; signal?: AbortSignal }, +) => Promise; + +/** Produces credentials for a private request. */ +export interface AuthStrategy { + /** A strictly increasing nonce per credential, or undefined when the scheme does not use one. */ + nextNonce(): string | undefined; + /** Credential headers for a request whose private payload is `payloadBase64`. */ + credentialHeaders(payloadBase64: string, options?: RequestOptions): Promise>; +} + +const rawJSON = (JSON as typeof JSON & { rawJSON(source: string): unknown }).rawJSON; + +function mediaType(value: string | null | undefined): string | undefined { + return value?.split(";", 1)[0]?.trim().toLowerCase() || undefined; +} + +function validateResponseContract( + status: number, + headers: { get(name: string): string | null } | undefined, + contract: RestResponseContract, + path: string, + metadata?: ResponseMetadata, +): void { + if (!contract.successStatuses.includes(status)) { + throw new SdkError(`unexpected success status ${status} for ${path}`, { metadata }); + } + const actual = mediaType(headers?.get("content-type")); + const expected = contract.responseContentTypes.map((value) => mediaType(value)); + if (!actual || !expected.includes(actual)) { + throw new SdkError(`unexpected success content type ${actual ?? "missing"} for ${path}`, { metadata }); + } +} + +const RESERVED_QUERY_ESCAPE = /%(?:21|23|24|26|27|28|29|2A|2B|2C|2F|3A|3B|3D|3F|40|5B|5D)/gi; +const COMPONENT_ESCAPE = /[!'()*]/g; + +function encodeQueryValue(value: unknown, allowReserved: boolean): string { + const encoded = encodeURIComponent(String(value)).replace(COMPONENT_ESCAPE, (character) => + `%${character.charCodeAt(0).toString(16).toUpperCase()}`); + return allowReserved ? encoded.replace(RESERVED_QUERY_ESCAPE, decodeURIComponent) : encoded; +} + +function appendQueryPair( + parts: string[], + name: string, + value: unknown, + allowReserved = false, +): void { + parts.push(`${encodeQueryValue(name, false)}=${encodeQueryValue(value, allowReserved)}`); +} + +function appendEncodedQueryPair(parts: string[], name: string, encodedValue: string): void { + parts.push(`${encodeQueryValue(name, false)}=${encodedValue}`); +} + +function encodedQueryValues(values: readonly unknown[], delimiter: string, allowReserved: boolean): string { + return values + .filter((value) => value !== undefined) + .map((value) => encodeQueryValue(value, allowReserved)) + .join(delimiter); +} + +function withDeclaredQuery( + path: string, + query: Record, + parameters: readonly RestQueryParameter[], +): string { + const parts: string[] = []; + for (const parameter of parameters) { + const value = query[parameter.name]; + if (value === undefined) continue; + const allowReserved = Boolean(parameter.allowReserved); + if (parameter.style === "form") { + if (Array.isArray(value)) { + if (parameter.explode) { + for (const item of value) { + if (item !== undefined) appendQueryPair(parts, parameter.name, item, allowReserved); + } + } else { + appendEncodedQueryPair(parts, parameter.name, encodedQueryValues(value, ",", allowReserved)); + } + } else if (value !== null && typeof value === "object") { + const entries = Object.entries(value).filter(([, item]) => item !== undefined); + if (parameter.explode) { + for (const [name, item] of entries) appendQueryPair(parts, name, item, allowReserved); + } else { + const flattened = entries.flatMap(([name, item]) => [name, item]); + appendEncodedQueryPair(parts, parameter.name, encodedQueryValues(flattened, ",", allowReserved)); + } + } else { + appendQueryPair(parts, parameter.name, value, allowReserved); + } + } else if (parameter.style === "spaceDelimited" || parameter.style === "pipeDelimited") { + const delimiter = parameter.style === "spaceDelimited" ? "%20" : "%7C"; + if (!Array.isArray(value)) throw new SdkError(`${parameter.name} must be an array for ${parameter.style} serialization`); + appendEncodedQueryPair(parts, parameter.name, encodedQueryValues(value as unknown[], delimiter, allowReserved)); + } else if (parameter.style === "deepObject" && value !== null && typeof value === "object") { + for (const [name, item] of Object.entries(value)) { + if (item !== undefined) appendQueryPair(parts, `${parameter.name}[${name}]`, item, allowReserved); + } + } else { + throw new SdkError(`unsupported query parameter serialization for ${parameter.name}`); + } + } + return parts.length > 0 ? `${path}?${parts.join("&")}` : path; +} + +function withQuery( + path: string, + query?: Record, + parameters?: readonly RestQueryParameter[], +): string { + if (!query) return path; + if (parameters) return withDeclaredQuery(path, query, parameters); + const qs = new URLSearchParams(); + for (const [key, value] of Object.entries(query)) { + if (Array.isArray(value)) { + for (const item of value) { + if (item !== undefined) qs.append(key, String(item)); + } + } else if (value !== undefined) { + qs.append(key, String(value)); + } + } + const encoded = qs.toString(); + return encoded ? `${path}?${encoded}` : path; +} + +export interface HttpTransportOptions { + env: Environment; + auth?: AuthStrategy; + fetchImpl?: FetchLike; + logger?: Logger; + onDiagnostic?: DiagnosticListener; + /** Max client-side retries for generated safe reads. Default 5. */ + maxRetries?: number; + /** Transient-read backoff tuning. Defaults: base 500ms, cap 30s, factor 2. */ + backoff?: { baseMs?: number; capMs?: number; factor?: number }; + /** Default end-to-end request deadline. Defaults to 30 seconds. */ + timeoutMs?: number; + /** Clock used for Retry-After HTTP dates. */ + now?: () => number; + // Injectable so tests make jitter and waits deterministic; production uses the defaults. + random?: () => number; + sleep?: (ms: number) => Promise; +} + +export class HttpTransport { + private readonly baseUrl: string; + private readonly auth?: AuthStrategy; + private readonly fetchImpl: FetchLike; + private readonly logger: Logger; + private readonly onDiagnostic?: DiagnosticListener; + private readonly maxRetries: number; + private readonly baseMs: number; + private readonly capMs: number; + private readonly factor: number; + private readonly random: () => number; + private readonly sleep: (ms: number) => Promise; + private readonly timeoutMs: number; + private readonly now: () => number; + + constructor(options: HttpTransportOptions) { + this.baseUrl = ENVIRONMENT_URLS[options.env].rest; + this.auth = options.auth; + this.fetchImpl = options.fetchImpl ?? ((url, init) => fetch(url, init) as ReturnType); + this.logger = options.logger ?? NOOP_LOGGER; + this.onDiagnostic = options.onDiagnostic; + const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES; + if (!Number.isInteger(maxRetries) || maxRetries < 0) { + throw new SdkError("maxRetries must be a finite non-negative integer"); + } + this.maxRetries = maxRetries; + this.baseMs = options.backoff?.baseMs ?? DEFAULT_BACKOFF_BASE_MS; + this.capMs = options.backoff?.capMs ?? DEFAULT_BACKOFF_CAP_MS; + this.factor = options.backoff?.factor ?? DEFAULT_BACKOFF_FACTOR; + if (![this.baseMs, this.capMs, this.factor].every(Number.isFinite) || this.baseMs < 0 || this.capMs < 0 || this.factor < 1) throw new SdkError("backoff values must be finite (base/cap >= 0, factor >= 1)"); + this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; + if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) throw new SdkError("timeoutMs must be a finite positive number"); + this.now = options.now ?? Date.now; + this.random = options.random ?? Math.random; + this.sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))); + } + + /** Whether this transport uses the exact auth strategy instance supplied by the caller. */ + isAuthenticatedWith(auth: AuthStrategy): boolean { + return this.auth === auth; + } + + // Equal-jitter backoff for retry attempt N (0-based): half fixed, half random, + // capped — the same shape WsTransport uses, so many clients throttled at once + // don't retry in lockstep. No Retry-After is documented, so this is client-side. + private backoffDelay(attempt: number): number { + const raw = Math.min(this.capMs, this.baseMs * this.factor ** attempt); + return raw / 2 + this.random() * (raw / 2); + } + + private retryAfterDelay(value: string | null | undefined, attempt: number): number { + const trimmed = value?.trim(); + if (trimmed && /^\d+$/.test(trimmed)) { + const seconds = Number(trimmed); + if (Number.isSafeInteger(seconds)) return Math.min(seconds * 1000, MAX_SETTIMEOUT_MS); + } + if (trimmed) { + const date = Date.parse(trimmed); + if (Number.isFinite(date)) return Math.min(Math.max(0, date - this.now()), MAX_SETTIMEOUT_MS); + } + return this.backoffDelay(attempt); + } + + private isTransient(cause: unknown): boolean { + if (cause instanceof TypeError) return true; + if (!cause || typeof cause !== "object") return false; + const error = cause as { code?: unknown; name?: unknown }; + return ["ECONNRESET", "ECONNREFUSED", "ECONNABORTED", "ETIMEDOUT", "EPIPE"].includes(String(error.code)) || + ["AbortError", "NetworkError"].includes(String(error.name)); + } + + /** Signed private request. Shapes the payload envelope and merges auth headers. */ + async request(options: { + method: HttpMethod; + path: string; + params?: Record; + query?: Record; + queryParameters?: readonly RestQueryParameter[]; + headers?: Record; + responseInt64Paths?: readonly Int64Path[]; + responseMode?: RestResponseMode; + responseContract?: RestResponseContract; + retryable?: boolean; + operationContext?: OperationContext; + } & RequestOptions): Promise { + const { method, path, params } = options; + if (!this.auth) { + throw new SdkError("private request requires an injected AuthStrategy"); + } + if ( + (params && Object.hasOwn(params, "nonce")) || + (options.query && Object.hasOwn(options.query, "nonce")) + ) { + throw new SdkError("nonce is reserved for the AuthStrategy"); + } + const reservedCallerHeader = Object.keys(options.headers ?? {}).find((name) => { + const normalized = name.toLowerCase(); + return normalized.startsWith("x-gemini-") || ["authorization", "content-length", "content-type", "cache-control", ...(options.responseContract ? ["accept"] : [])].includes(normalized); + }); + if (reservedCallerHeader) { + throw new SdkError(`private request header ${reservedCallerHeader} is reserved for transport or authentication`); + } + const auth = this.auth; + const stableHeaders = { ...options.headers }; + // A retry refreshes authentication only; caller mutation must never change + // the trading instruction between attempts. + const stableParams = structuredClone(params); + + // Build the signed request afresh each attempt: a retry gets a new nonce and + // signature, so the exchange never sees a reused nonce (-> InvalidNonce). + const build = async () => { + const payload: Record = { request: path, ...stableParams }; + // A params key must never override the endpoint the payload is signed for. + if (payload.request !== path) { + throw new EndpointMismatch(path, payload.request); + } + const nonce = auth.nextNonce(); + if (nonce !== undefined) { + if (!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(nonce)) { + throw new SdkError("AuthStrategy returned an invalid nonce"); + } + payload.nonce = rawJSON(nonce); + } + const json = JSON.stringify(payload, (_key, value) => + typeof value === "bigint" + ? rawJSON(value.toString()) + : value, + ); + const b64 = toBase64(json); + const credentials = await auth.credentialHeaders(b64, { signal: options.signal }); + const reservedHeader = Object.keys(credentials).find((name) => + ["content-length", "content-type", "cache-control", "x-gemini-payload"].includes( + name.toLowerCase(), + ), + ); + if (reservedHeader) { + throw new SdkError(`AuthStrategy returned reserved header ${reservedHeader}`); + } + // Auth headers spread FIRST so the fixed envelope headers always win — a + // buggy strategy can never clobber X-GEMINI-PAYLOAD or the content headers. + // (Content-Length is belt-and-suspenders: undici recomputes it for the + // empty body, but the Gemini private-REST convention documents it.) + const headers: Record = { + ...stableHeaders, + ...credentials, + ...(options.responseContract + ? { Accept: options.responseContract.responseContentTypes.join(", ") } + : {}), + "Content-Length": "0", + "Content-Type": "text/plain", + "Cache-Control": "no-cache", + "X-GEMINI-PAYLOAD": b64, + }; + return headers; + }; + + return this.send( + method, + withQuery(path, options.query, options.queryParameters), + build, + options.responseInt64Paths, + options.responseMode, + options.responseContract, + options.retryable, + options, + options.operationContext, + ); + } + + /** + * Unsigned public request (market data). No auth, no payload envelope — query + * params go in the URL. Shares the same parse, error mapping and 429 backoff. + */ + async requestPublic(options: { + method: HttpMethod; + path: string; + query?: Record; + queryParameters?: readonly RestQueryParameter[]; + headers?: Record; + responseInt64Paths?: readonly Int64Path[]; + responseMode?: RestResponseMode; + responseContract?: RestResponseContract; + retryable?: boolean; + operationContext?: OperationContext; + } & RequestOptions): Promise { + if (options.responseContract && Object.keys(options.headers ?? {}).some((name) => name.toLowerCase() === "accept")) { + throw new SdkError("Accept is reserved by the REST operation contract"); + } + const stableHeaders = { + ...options.headers, + ...(options.responseContract ? { Accept: options.responseContract.responseContentTypes.join(", ") } : {}), + }; + return this.send( + options.method, + withQuery(options.path, options.query, options.queryParameters), + async () => stableHeaders, + options.responseInt64Paths, + options.responseMode, + options.responseContract, + options.retryable, + options, + options.operationContext, + ); + } + + /** + * Walk an offset-paginated endpoint, yielding each item across pages. The API + * has no cursors: pages advance by incrementing `offset` by `limit` until a + * short page (fewer than `limit` items) signals the end. Use `itemsKey` for + * documented object envelopes such as `{ orders, pagination }`. `limit` + * defaults to 50 and is clamped to the documented max of 500. Public pages + * use query parameters; private pages default to the signed payload but can + * select query parameters for endpoints that document them there. Offset + * pagination is not snapshot-consistent; provide `dedupeKey` when drift + * must fail loudly instead of yielding the same logical record twice. + */ + async *paginate(options: { + method: HttpMethod; + path: string; + params?: Record; + limit?: number; + /** Endpoint-specific limit ceiling. Defaults to the API-wide maximum of 500. */ + maxLimit?: number; + /** Top-level array field for endpoints that return an object envelope. */ + itemsKey?: string; + visibility?: "private" | "public"; + parameterLocation?: "payload" | "query"; + responseInt64Paths?: readonly Int64Path[]; + maxItems?: number; + dedupeKey?: (item: unknown) => string; + retryable?: boolean; + } & RequestOptions): AsyncGenerator { + for (const [name, value] of [["limit", options.limit], ["maxLimit", options.maxLimit]] as const) { + if (value !== undefined && (!Number.isInteger(value) || value <= 0)) { + throw new SdkError(`${name} must be a finite positive integer`); + } + } + const maxLimit = Math.min(Math.max(options.maxLimit ?? MAX_PAGE_SIZE, 1), MAX_PAGE_SIZE); + const limit = Math.min(Math.max(options.limit ?? DEFAULT_PAGE_SIZE, 1), maxLimit); + if (options.maxItems !== undefined && (!Number.isInteger(options.maxItems) || options.maxItems <= 0)) throw new SdkError("maxItems must be a finite positive integer"); + const execution = deadline(options, this.timeoutMs); + let yielded = 0; + let offset = 0; + const seen = options.dedupeKey ? new Set() : undefined; + try { for (;;) { + const pageLimit = options.maxItems === undefined ? limit : Math.min(limit, options.maxItems - yielded); + if (pageLimit <= 0) return; + const params = { ...options.params, limit: pageLimit, offset }; + const page = options.visibility === "public" + ? await this.requestPublic({ + method: options.method, + path: options.path, + query: params, + responseInt64Paths: options.responseInt64Paths, + retryable: options.retryable, signal: execution.signal, timeoutMs: this.timeoutMs, + }) + : options.parameterLocation === "query" + ? await this.request({ + method: options.method, + path: options.path, + query: params, + responseInt64Paths: options.responseInt64Paths, + retryable: options.retryable, signal: execution.signal, timeoutMs: this.timeoutMs, + }) + : await this.request({ + method: options.method, + path: options.path, + params, + responseInt64Paths: options.responseInt64Paths, + retryable: options.retryable, signal: execution.signal, timeoutMs: this.timeoutMs, + }); + const items = Array.isArray(page) + ? page + : options.itemsKey && page !== null && typeof page === "object" + ? (page as Record)[options.itemsKey] + : undefined; + if (!Array.isArray(items)) { + const endpoint = options.path.split("?", 1)[0] ?? options.path; + throw new SdkError(`paginate expected an array page from ${endpoint}`); + } + for (const item of items) { + if (seen) { + const key = options.dedupeKey?.(item); + if (typeof key !== "string") throw new SdkError("dedupeKey must return a string"); + if (seen.has(key)) throw new SdkError(`paginate detected duplicate item key ${key}`); + seen.add(key); + } + yield item; + yielded++; + if (yielded === options.maxItems) return; + } + if (items.length < pageLimit) return; // a short page is the last page + offset += pageLimit; + } } finally { execution.cleanup(); } + } + + // Send with bounded safe-read retry. `buildHeaders` runs per attempt so each + // retry is freshly signed. Mutations and non-transient errors return once. + private async send( + method: HttpMethod, + path: string, + buildHeaders: () => Promise>, + responseInt64Paths: readonly Int64Path[] = [], + responseMode: RestResponseMode = "json", + responseContract?: RestResponseContract, + retryable = false, + requestOptions: RequestOptions = {}, + operationContext?: OperationContext, + ): Promise { + const endpoint = path.split("?", 1)[0] ?? path; + if (responseMode !== "json" && responseMode !== "file") { + throw new SdkError(`unsupported response mode ${responseMode} for ${endpoint}`); + } + const canRetry = retryable && method === "GET"; + const correlationId = crypto.randomUUID(); + const responseMetadata = ( + status: number | undefined, + retryCount: number, + response?: { headers?: { get(name: string): string | null } }, + ): ResponseMetadata => createResponseMetadata({ + endpoint, + method, + correlationId, + status, + retryCount, + headers: response?.headers, + }); + const emit = ( + level: "debug" | "info" | "warn" | "error", + name: string, + response?: ResponseMetadata, + metadata?: Record, + error?: unknown, + ): void => emitDiagnostic({ + level, + component: "rest", + name, + response, + operationContext, + metadata, + ...(error ? { error: serializeError(error) } : {}), + }, this.logger, this.onDiagnostic); + emit("debug", "request.start", responseMetadata(undefined, 0), { + operation: operationContext?.operation, + }); + const execution = deadline(requestOptions, this.timeoutMs); + try { for (let attempt = 0; ; attempt++) { + let headers: Record; + try { + headers = await withSignal(buildHeaders(), execution.signal); + } catch (cause) { + emit("error", "request.failure", responseMetadata(undefined, attempt), undefined, cause); + throw cause; + } + let response: Awaited>; + try { + response = await withSignal( + this.fetchImpl(`${this.baseUrl}${path}`, { method, headers, signal: execution.signal }), + execution.signal, + ); + } catch (cause) { + if (cause instanceof SdkError) { + emit("error", "transport.failure", responseMetadata(undefined, attempt), undefined, cause); + throw cause; + } + if (canRetry && attempt < this.maxRetries && this.isTransient(cause)) { + const delay = this.backoffDelay(attempt); + emit("warn", "request.retry", responseMetadata(undefined, attempt), { attempt, delayMs: delay }); + await sleepWithSignal(delay, execution.signal); + continue; + } + const error = new SdkError(`HTTP request failed for ${endpoint}`, { + cause, + metadata: responseMetadata(undefined, attempt), + operationContext, + }); + emit("error", "transport.failure", error.metadata, undefined, error); + throw error; + } + + const isSuccess = response.status >= 200 && response.status < 300; + if (isSuccess && responseContract) { + try { + validateResponseContract( + response.status, + response.headers, + responseContract, + endpoint, + responseMetadata(response.status, attempt, response), + ); + } catch (cause) { + emit("error", "response.failure", responseMetadata(response.status, attempt, response), undefined, cause); + throw cause; + } + } + + if (isSuccess && responseMode === "file") { + if (!response.arrayBuffer) { + const error = new SdkError(`file response from ${endpoint} cannot be read as bytes`, { + metadata: responseMetadata(response.status, attempt, response), + operationContext, + }); + emit("error", "response.failure", error.metadata, undefined, error); + throw error; + } + try { + const fileResponse = { + bytes: new Uint8Array(await withSignal(response.arrayBuffer(), execution.signal)), + contentType: response.headers?.get("content-type") ?? undefined, + contentDisposition: response.headers?.get("content-disposition") ?? undefined, + } satisfies RestFileResponse; + emit("info", "request.end", responseMetadata(response.status, attempt, response)); + return fileResponse; + } catch (cause) { + if (cause instanceof SdkError) throw cause; + if (canRetry && attempt < this.maxRetries && this.isTransient(cause)) { await sleepWithSignal(this.backoffDelay(attempt), execution.signal); continue; } + const error = new SdkError(`HTTP request failed for ${endpoint}`, { + cause, + metadata: responseMetadata(response.status, attempt, response), + operationContext, + }); + emit("error", "transport.failure", error.metadata, undefined, error); + throw error; + } + } + + let text: string; + try { + text = await withSignal(response.text(), execution.signal); + } catch (cause) { + if (cause instanceof SdkError) throw cause; + if (canRetry && RETRYABLE_STATUS_CODES.includes(response.status) && attempt < this.maxRetries && this.isTransient(cause)) { + const delay = this.retryAfterDelay(response.headers?.get("retry-after"), attempt); + emit("warn", "request.retry", responseMetadata(response.status, attempt, response), { attempt, delayMs: delay }); + await sleepWithSignal(delay, execution.signal); + continue; + } + if (canRetry && attempt < this.maxRetries && this.isTransient(cause)) { await sleepWithSignal(this.backoffDelay(attempt), execution.signal); continue; } + const error = new SdkError(`HTTP request failed for ${endpoint}`, { + cause, + metadata: responseMetadata(response.status, attempt, response), + operationContext, + }); + emit("error", "transport.failure", error.metadata, undefined, error); + throw error; + } + + if (canRetry && RETRYABLE_STATUS_CODES.includes(response.status) && attempt < this.maxRetries) { + const delay = this.retryAfterDelay(response.headers?.get("retry-after"), attempt); + emit("warn", "request.retry", responseMetadata(response.status, attempt, response), { attempt, delayMs: delay }); + await withSignal(this.sleep(delay), execution.signal); + continue; + } + + // Parse the body, but never let a non-JSON body (a proxy/LB HTML error + // page, an empty 429) escape as a raw SyntaxError — that would strip the + // HTTP status and defeat error mapping. Empty -> undefined; unparseable on + // an error status -> map by status, keeping the raw text as the message. + let body: unknown; + try { + body = text ? parseLosslessJson(text) : undefined; + } catch (cause) { + if (isSuccess) { + // A 2xx that isn't JSON is a protocol violation — fail loud, typed. + const error = new SdkError(`unparseable success response from ${endpoint}`, { + cause, + metadata: responseMetadata(response.status, attempt, response), + operationContext, + }); + emit("error", "response.failure", error.metadata, undefined, error); + throw error; + } + body = text; + } + + if (isSuccess) { + let normalizedResponse: unknown; + try { + normalizedResponse = normalizeInt64Paths(body, responseInt64Paths); + } catch (cause) { + emit("error", "response.failure", responseMetadata(response.status, attempt, response), undefined, cause); + throw cause; + } + emit("info", "request.end", responseMetadata(response.status, attempt, response)); + return normalizedResponse; + } + + const apiError = mapError( + response.status, + body, + responseMetadata(response.status, attempt, response), + operationContext, + ); + emit("error", "api.error", apiError.metadata, undefined, apiError); + throw apiError; + } } finally { execution.cleanup(); } + } +} diff --git a/packages/sdk-typescript/src/core/request-validation.ts b/packages/sdk-typescript/src/core/request-validation.ts new file mode 100644 index 0000000..b13204a --- /dev/null +++ b/packages/sdk-typescript/src/core/request-validation.ts @@ -0,0 +1,331 @@ +import { ValidationError } from "../errors.js"; + +type RequestBody = Record; +type Validator = (operation: string, body: RequestBody) => void; + +const DECIMAL = /^(?:\d+\.?\d*|\.\d+)$/u; +const INTEGER_ID = /^\d+$/u; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu; +const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu; +const hasField = (body: RequestBody, field: string): boolean => Object.prototype.hasOwnProperty.call(body, field); +const isBoolean = (value: unknown): boolean => typeof value === "boolean"; +const isFiniteNumber = (value: unknown): boolean => typeof value === "number" && Number.isFinite(value); +const isString = (value: unknown): value is string => typeof value === "string"; + +function fail(operation: string, field: string, rule: string, message: string): never { + throw new ValidationError({ operation, field, rule, message }); +} + +function objectBody(operation: string, value: unknown): RequestBody { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + return fail(operation, "body", "type", "request body must be a non-null JSON object"); + } + return value as RequestBody; +} + +function required(body: RequestBody, operation: string, field: string): unknown { + if (!hasField(body, field) || body[field] === undefined || body[field] === null) { + return fail(operation, field, "required", `${field} is required`); + } + return body[field]; +} + +function optional(body: RequestBody, operation: string, field: string, check: (value: unknown) => boolean): void { + if (hasField(body, field) && body[field] !== undefined && !check(body[field])) { + fail(operation, field, "type", `${field} has an invalid type or format`); + } +} + +function stringField(body: RequestBody, operation: string, field: string, requiredField = false): void { + const value = requiredField ? required(body, operation, field) : body[field]; + if ((requiredField || hasField(body, field)) && !isString(value)) { + fail(operation, field, "type", `${field} must be a string`); + } +} + +function decimal(value: unknown): boolean { + return typeof value === "string" && DECIMAL.test(value); +} + +function decimalField(body: RequestBody, operation: string, field: string, requiredField = false): void { + const value = requiredField ? required(body, operation, field) : body[field]; + if ((requiredField || hasField(body, field)) && !decimal(value)) { + fail(operation, field, "format", `${field} must be a quoted decimal string`); + } +} + +function booleanField(body: RequestBody, operation: string, field: string): void { + optional(body, operation, field, isBoolean); +} + +function enumField(body: RequestBody, operation: string, field: string, values: readonly string[], requiredField = false): void { + const value = requiredField ? required(body, operation, field) : body[field]; + if ((requiredField || hasField(body, field)) && (!isString(value) || !values.includes(value))) { + fail(operation, field, "enum", `${field} must be one of: ${values.join(", ")}`); + } +} + +function identifier(value: unknown): boolean { + return (typeof value === "bigint" && value >= 0n) || + (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) || + (typeof value === "string" && INTEGER_ID.test(value)); +} + +function identifierField(body: RequestBody, operation: string, field: string): void { + if (!identifier(required(body, operation, field))) { + fail(operation, field, "format", `${field} must be a non-negative safe integer, bigint, or numeric string`); + } +} + +function uuidField(body: RequestBody, operation: string, field: string, version4 = false): void { + const pattern = version4 ? UUID_V4 : UUID; + if (hasField(body, field) && body[field] !== undefined && !(isString(body[field]) && pattern.test(body[field] as string))) { + fail(operation, field, "format", `${field} must be a ${version4 ? "UUIDv4" : "UUID"} string`); + } +} + +function arrayField(body: RequestBody, operation: string, field: string, min: number, max: number, validateItem: (value: unknown, path: string) => void): void { + const value = required(body, operation, field); + if (!Array.isArray(value)) fail(operation, field, "type", `${field} must be an array`); + if (value.length < min || value.length > max) fail(operation, field, "bounds", `${field} must contain ${min}-${max} items`); + value.forEach((entry, index) => validateItem(entry, `${field}[${index}]`)); +} + +function nestedObject(operation: string, value: unknown, field: string): RequestBody { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + return fail(operation, field, "type", `${field} must be an object`); + } + return value as RequestBody; +} + +function atMostOne(value: unknown): boolean { + if (!decimal(value)) return false; + const [whole, fraction = ""] = (value as string).split("."); + const normalizedWhole = whole.replace(/^0+(?=\d)/u, ""); + return normalizedWhole === "0" || (normalizedWhole === "1" && /^0*$/u.test(fraction)); +} + +function compareDecimals(left: string, right: string): number { + const normalize = (value: string): { whole: string; fraction: string } => { + const [whole = "0", fraction = ""] = value.split("."); + return { + whole: whole.replace(/^0+(?=\d)/u, "") || "0", + fraction: fraction.replace(/0+$/u, ""), + }; + }; + const a = normalize(left); + const b = normalize(right); + if (a.whole.length !== b.whole.length) return a.whole.length < b.whole.length ? -1 : 1; + if (a.whole !== b.whole) return a.whole < b.whole ? -1 : 1; + const width = Math.max(a.fraction.length, b.fraction.length); + const aFraction = a.fraction.padEnd(width, "0"); + const bFraction = b.fraction.padEnd(width, "0"); + return aFraction === bFraction ? 0 : aFraction < bFraction ? -1 : 1; +} + +function tradingOrder(operation: string, body: RequestBody): void { + stringField(body, operation, "symbol", true); + decimalField(body, operation, "amount", true); + decimalField(body, operation, "price", true); + enumField(body, operation, "side", ["buy", "sell"], true); + enumField(body, operation, "type", ["exchange limit", "exchange stop limit", "exchange market"], true); + optional(body, operation, "client_order_id", isString); + optional(body, operation, "account", isString); + optional(body, operation, "stop_price", decimal); + booleanField(body, operation, "margin_order"); + if (hasField(body, "options")) { + const options = body.options; + if (!Array.isArray(options) || options.length > 1 || options.some((value) => + typeof value !== "string" || !["maker-or-cancel", "immediate-or-cancel", "fill-or-kill"].includes(value))) { + fail(operation, "options", "bounds", "options must contain at most one supported execution option"); + } + } + const stop = body.type === "exchange stop limit"; + if (stop && (!hasField(body, "stop_price") || body.stop_price === undefined || body.stop_price === null)) fail(operation, "stop_price", "conditional", "stop_price is required for stop-limit orders"); + if (!stop && hasField(body, "stop_price")) fail(operation, "stop_price", "conditional", "stop_price is only valid for stop-limit orders"); + if (stop && Array.isArray(body.options) && body.options.length > 0) fail(operation, "options", "exclusive", "options cannot be used with stop-limit orders"); + if (stop && body.side === "buy" && compareDecimals(body.stop_price as string, body.price as string) >= 0) { + fail(operation, "stop_price", "relationship", "stop_price must be less than price for buy stop-limit orders"); + } + if (stop && body.side === "sell" && compareDecimals(body.stop_price as string, body.price as string) <= 0) { + fail(operation, "stop_price", "relationship", "stop_price must be greater than price for sell stop-limit orders"); + } +} + +function predictionOrder(operation: string, body: RequestBody, prefix = ""): void { + try { + stringField(body, operation, "symbol", true); + enumField(body, operation, "orderType", ["limit", "stop-limit"], true); + enumField(body, operation, "side", ["buy", "sell"], true); + decimalField(body, operation, "quantity", true); + decimalField(body, operation, "price", true); + enumField(body, operation, "outcome", ["yes", "no"], true); + optional(body, operation, "stopPrice", decimal); + enumField(body, operation, "timeInForce", ["good-til-cancel", "immediate-or-cancel", "fill-or-kill"]); + booleanField(body, operation, "makerOrCancel"); + if (body.orderType === "stop-limit" && !hasField(body, "stopPrice")) fail(operation, "stopPrice", "conditional", "stopPrice is required for stop-limit orders"); + if (body.orderType !== "stop-limit" && hasField(body, "stopPrice")) fail(operation, "stopPrice", "conditional", "stopPrice is only valid for stop-limit orders"); + if (!atMostOne(body.price)) fail(operation, "price", "bounds", "price must be between 0 and 1"); + if (hasField(body, "stopPrice") && !atMostOne(body.stopPrice)) fail(operation, "stopPrice", "bounds", "stopPrice must be between 0 and 1"); + } catch (error) { + if (prefix && error instanceof ValidationError) { + throw new ValidationError({ operation: error.operation, field: `${prefix}${error.field}`, rule: error.rule, message: `${prefix}${error.message}` }); + } + throw error; + } +} + +function validateFields(operation: string, body: RequestBody, fields: Record boolean>): void { + for (const [field, check] of Object.entries(fields)) { + if (hasField(body, field) && !check(body[field])) fail(operation, field, "type", `${field} has an invalid type or format`); + } +} + +function requiredStrings(body: RequestBody, operation: string, fields: readonly string[]): void { + for (const field of fields) stringField(body, operation, field, true); +} + +function requiredDecimals(body: RequestBody, operation: string, fields: readonly string[]): void { + for (const field of fields) decimalField(body, operation, field, true); +} + +const validators: Record = { + "trading.createNewOrder": tradingOrder, + "trading.getOrderStatus": (operation, body) => { + const hasOrderId = hasField(body, "order_id") && body.order_id !== undefined; + const hasClientOrderId = hasField(body, "client_order_id") && body.client_order_id !== undefined; + if (hasOrderId === hasClientOrderId) { + fail( + operation, + "order_id", + hasOrderId ? "exclusive" : "required", + "exactly one of order_id or client_order_id is required", + ); + } + if (hasOrderId) identifierField(body, operation, "order_id"); + else stringField(body, operation, "client_order_id", true); + booleanField(body, operation, "include_trades"); + optional(body, operation, "account", isString); + }, + "trading.cancelOrder": (operation, body) => identifierField(body, operation, "order_id"), + "trading.cancelAllActiveOrders": () => undefined, + "trading.cancelAllSessionOrders": () => undefined, + "trading.wrapOrder": (operation, body) => { + decimalField(body, operation, "amount", true); + enumField(body, operation, "side", ["buy", "sell"]); + validateFields(operation, body, { client_order_id: isString, account: isString }); + }, + "accountServices.createNewDepositAddress": (operation, body) => validateFields(operation, body, { + label: isString, + legacy: isBoolean, + account: isString, + }), + "accountServices.withdrawCryptoFunds": (operation, body) => { + stringField(body, operation, "address", true); + decimalField(body, operation, "amount", true); + validateFields(operation, body, { memo: isString }); + uuidField(body, operation, "clientTransferId"); + }, + "clearingInstant.createNewClearingOrder": (operation, body) => { + requiredStrings(body, operation, ["symbol"]); + requiredDecimals(body, operation, ["amount", "price"]); + enumField(body, operation, "side", ["buy", "sell"], true); + validateFields(operation, body, { + counterparty_id: isString, + expires_in_hrs: isFiniteNumber, + account: isString, + }); + }, + "clearingInstant.cancelClearingOrder": (operation, body) => stringField(body, operation, "clearing_id", true), + "clearingInstant.confirmClearingOrder": (operation, body) => { + requiredStrings(body, operation, ["clearing_id", "symbol"]); + requiredDecimals(body, operation, ["amount", "price"]); + enumField(body, operation, "side", ["buy", "sell"], true); + }, + "clearingInstant.createNewBrokerOrder": (operation, body) => { + requiredStrings(body, operation, ["source_counterparty_id", "target_counterparty_id", "symbol"]); + requiredDecimals(body, operation, ["amount", "price"]); + enumField(body, operation, "side", ["buy", "sell"], true); + const expires = required(body, operation, "expires_in_hrs"); + if (!isFiniteNumber(expires)) fail(operation, "expires_in_hrs", "type", "expires_in_hrs must be a finite number"); + }, + "clearingInstant.executeInstantOrder": (operation, body) => { + requiredStrings(body, operation, ["symbol", "quantity", "price", "fee"]); + enumField(body, operation, "side", ["buy", "sell"], true); + const quoteId = required(body, operation, "quoteId"); + if (!identifier(quoteId)) fail(operation, "quoteId", "format", "quoteId must be a safe integer, bigint, or numeric string"); + }, + "accountServices.addBank": (operation, body) => { + requiredStrings(body, operation, ["accountnumber", "routing", "name"]); + enumField(body, operation, "type", ["checking", "savings"], true); + }, + "accountServices.addBankCAD": (operation, body) => { + for (const field of ["swiftcode", "accountNumber", "name"]) stringField(body, operation, field, true); + enumField(body, operation, "type", ["checking", "savings"], true); + validateFields(operation, body, { institutionNumber: isString, branchnnumber: isString }); + }, + "accountServices.createNewApprovedAddress": (operation, body) => requiredStrings(body, operation, ["address", "label"]), + "accountServices.removeApprovedAddress": (operation, body) => stringField(body, operation, "address", true), + "accountServices.createNewAccount": (operation, body) => { + stringField(body, operation, "name", true); + enumField(body, operation, "type", ["exchange", "custody"]); + }, + "accountServices.renameAccount": (operation, body) => validateFields(operation, body, { + account: isString, + newName: isString, + newAccount: isString, + }), + "accountServices.transferBetweenAccounts": (operation, body) => { + stringField(body, operation, "sourceAccount", true); + stringField(body, operation, "targetAccount", true); + decimalField(body, operation, "amount", true); + uuidField(body, operation, "clientTransferId", true); + validateFields(operation, body, { withdrawalId: isString }); + }, + "accountServices.revokeOAuthToken": () => undefined, + "accountServices.stakeCryptoFunds": (operation, body) => { + requiredStrings(body, operation, ["providerId", "currency"]); + decimalField(body, operation, "amount", true); + }, + "accountServices.unstakeCryptoFunds": (operation, body) => { + requiredStrings(body, operation, ["providerId", "currency"]); + decimalField(body, operation, "amount", true); + }, + "predictionMarkets.placeOrder": predictionOrder, + "predictionMarkets.placeOrderBatch": (operation, body) => arrayField( + body, + operation, + "orders", + 1, + 20, + (value, path) => predictionOrder(operation, nestedObject(operation, value, path), `${path}.`), + ), + "predictionMarkets.cancelOrder": (operation, body) => identifierField(body, operation, "orderId"), + "predictionMarkets.cancelOrderBatch": (operation, body) => arrayField( + body, + operation, + "orderIds", + 1, + 20, + (value, path) => { + if (!identifier(value)) fail(operation, path, "format", `${path} must be a non-negative order identifier`); + }, + ), + "predictionMarkets.createCombo": (operation, body) => arrayField( + body, + operation, + "legs", + 2, + 6, + (value, path) => { + const leg = nestedObject(operation, value, path); + stringField(leg, operation, "contractId", true); + enumField(leg, operation, "requiredOutcome", ["Yes", "No"], true); + }, + ), +}; + +export function validateRequestBody(operation: string | undefined, body: unknown): void { + if (!operation || !validators[operation]) return; + validators[operation](operation, objectBody(operation, body)); +} diff --git a/packages/sdk-typescript/src/core/rest-operation.ts b/packages/sdk-typescript/src/core/rest-operation.ts new file mode 100644 index 0000000..9a454b6 --- /dev/null +++ b/packages/sdk-typescript/src/core/rest-operation.ts @@ -0,0 +1,253 @@ +import type { HttpMethod, HttpTransport, RestQueryParameter, RestResponseMode } from "./http.js"; +import type { RequestOptions } from "./deadline.js"; +import { SdkError } from "../errors.js"; +import { validateInt64RequestPaths, type Int64Path, type RequestInt64Path } from "../json.js"; +import { validateRequestBody } from "./request-validation.js"; + +type OperationCallTypes = { + path: unknown; + query: unknown; + headers?: unknown; + body: unknown; + response: unknown; +}; + +type RestOperation = { + operation?: string; + method: string; + path: string; + access: string; + parameters: readonly RestQueryParameter[]; + headers?: readonly { name: string; required: boolean }[]; + requestBody: boolean; + requestBodyRequired: boolean; + successStatuses: readonly number[]; + responseMode: RestResponseMode; + responseContentTypes: readonly string[]; + responseInt64Paths: readonly Int64Path[]; + requestInt64Paths?: { + body: readonly RequestInt64Path[]; + path: readonly RequestInt64Path[]; + query: readonly RequestInt64Path[]; + }; + retryable?: boolean; +}; + +const PCHAR_ESCAPE = /%(?:21|24|26|27|28|29|2A|2B|2C|3A|3B|3D|40)/gi; + +function encodePathSegment(value: unknown): string { + return encodeURIComponent(String(value)).replace(PCHAR_ESCAPE, decodeURIComponent); +} + +function renderPath(operation: RestOperation, pathInput: unknown): string { + let path = operation.path; + const pathValues = (pathInput ?? {}) as Record; + for (const parameter of operation.parameters) { + if (parameter.in !== "path") continue; + if (parameter.style !== "simple" || parameter.explode) { + throw new SdkError(`unsupported path parameter serialization for ${parameter.name} in ${operation.path}`); + } + const parameterValue = pathValues[parameter.name]; + if (parameterValue === undefined) { + throw new SdkError(`missing path parameter ${parameter.name} for ${operation.path}`); + } + if (parameterValue !== null && typeof parameterValue === "object") { + throw new SdkError(`path parameter ${parameter.name} must be a scalar for ${operation.path}`); + } + path = path.replaceAll(`{${parameter.name}}`, encodePathSegment(parameterValue)); + } + if (/{[^}]+}/.test(path)) { + throw new SdkError(`unresolved path parameter in ${operation.path}`); + } + return path; +} + +function isScalar(value: unknown): boolean { + return value === undefined || (value !== null && typeof value !== "object"); +} + +function validateQueryValue(parameter: RestQueryParameter, value: unknown, path: string): void { + if (value === null) { + throw new SdkError(`query parameter ${parameter.name} cannot be null for ${path}`); + } + if (!parameter.shape) { + throw new SdkError(`query parameter ${parameter.name} is missing generated shape metadata for ${path}`); + } + if (parameter.style === "form") { + const valid = parameter.shape === "scalar" + ? isScalar(value) + : parameter.shape === "array" + ? Array.isArray(value) && value.every((item) => isScalar(item)) + : value !== null && typeof value === "object" && !Array.isArray(value) && + Object.values(value).every((item) => isScalar(item)); + if (!valid) throw new SdkError(`query parameter ${parameter.name} must match its generated ${parameter.shape} shape for ${path}`); + return; + } + if (parameter.style === "spaceDelimited" || parameter.style === "pipeDelimited") { + if (parameter.shape !== "array" || !Array.isArray(value) || !value.every((item) => isScalar(item))) { + throw new SdkError(`query parameter ${parameter.name} must be an array of scalars for ${path}`); + } + return; + } + if (parameter.style === "deepObject") { + if (parameter.shape !== "object" || value === null || typeof value !== "object" || Array.isArray(value) || + !Object.values(value).every((item) => isScalar(item))) { + throw new SdkError(`query parameter ${parameter.name} must be a shallow scalar object for ${path}`); + } + return; + } + throw new SdkError(`unsupported query parameter serialization for ${parameter.name} in ${path}`); +} + +function renderQuery(operation: RestOperation, queryInput: unknown): Record | undefined { + const queryParameters = operation.parameters.filter((parameter) => parameter.in === "query"); + if (queryInput === undefined) { + if (queryParameters.some((parameter) => parameter.required)) { + const parameter = queryParameters.find((candidate) => candidate.required); + throw new SdkError(`missing query parameter ${parameter?.name} for ${operation.path}`); + } + return queryParameters.length === 0 ? undefined : {}; + } + if (queryInput === null || typeof queryInput !== "object" || Array.isArray(queryInput)) { + throw new SdkError(`query input must be an object for ${operation.path}`); + } + const queryValues = queryInput as Record; + const declared = new Set(queryParameters.map((parameter) => parameter.name)); + for (const name of Object.keys(queryValues)) { + if (!declared.has(name)) throw new SdkError(`unexpected query parameter ${name} for ${operation.path}`); + } + for (const parameter of queryParameters) { + const parameterValue = queryValues[parameter.name]; + if (parameter.required && parameterValue === undefined) { + throw new SdkError(`missing query parameter ${parameter.name} for ${operation.path}`); + } + if (parameterValue !== undefined) validateQueryValue(parameter, parameterValue, operation.path); + } + return queryValues; +} + +function renderHeaders(operation: RestOperation, headersInput: unknown): Record | undefined { + const declared = operation.headers ?? []; + if (headersInput === undefined) { + const required = declared.find((header) => header.required); + if (required) throw new SdkError(`missing header ${required.name} for ${operation.path}`); + return undefined; + } + if (headersInput === null || typeof headersInput !== "object" || Array.isArray(headersInput)) { + throw new SdkError(`headers input must be an object for ${operation.path}`); + } + const headerValues = headersInput as Record; + const byName = new Map(declared.map((header) => [header.name.toLowerCase(), header])); + for (const name of Object.keys(headerValues)) { + if (!byName.has(name.toLowerCase())) throw new SdkError(`unexpected header ${name} for ${operation.path}`); + if (headerValues[name] === null) throw new SdkError(`header ${name} cannot be null for ${operation.path}`); + } + for (const header of declared) { + const suppliedName = Object.keys(headerValues).find((name) => name.toLowerCase() === header.name.toLowerCase()); + const headerValue = suppliedName === undefined ? undefined : headerValues[suppliedName]; + if (header.required && headerValue === undefined) throw new SdkError(`missing header ${header.name} for ${operation.path}`); + } + return Object.fromEntries(Object.entries(headerValues).filter(([, value]) => value !== undefined)) as Record; +} + +function methodFor(operation: RestOperation): HttpMethod { + const method = operation.method.toUpperCase(); + if (method === "GET" || method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE") { + return method; + } + throw new SdkError(`unsupported REST method ${operation.method} for ${operation.path}`); +} + +function operationContextFor(operation: RestOperation, body: unknown): { + operation: string; + clientOrderId?: string; + clientOrderIds?: readonly string[]; +} { + const context: { + operation: string; + clientOrderId?: string; + clientOrderIds?: readonly string[]; + } = { operation: operation.operation ?? operation.path }; + if (body === null || typeof body !== "object" || Array.isArray(body)) return context; + const request = body as Record; + const clientOrderId = typeof request.clientOrderId === "string" + ? request.clientOrderId + : typeof request.client_order_id === "string" + ? request.client_order_id + : undefined; + if (clientOrderId !== undefined) { + context.clientOrderId = clientOrderId; + } + if (Array.isArray(request.orders)) { + const clientOrderIds = request.orders + .filter((item): item is Record => item !== null && typeof item === "object" && !Array.isArray(item)) + .map((item) => typeof item.clientOrderId === "string" ? item.clientOrderId : item.client_order_id) + .filter((value): value is string => typeof value === "string"); + if (clientOrderIds.length > 0) context.clientOrderIds = clientOrderIds; + } + return context; +} + +export async function executeRestOperation( + transport: HttpTransport, + operation: RestOperation, + input: { path?: T["path"]; query?: T["query"]; headers?: T["headers"]; body?: T["body"] } = {}, + requestOptions: RequestOptions = {}, +): Promise { + if (operation.responseMode !== "json" && operation.responseMode !== "file") { + throw new SdkError(`unsupported REST response mode ${operation.responseMode} for ${operation.path}`); + } + if (operation.requestBodyRequired && input.body === undefined) { + throw new SdkError(`request body is required for ${operation.path}`); + } + validateRequestBody(operation.operation, input.body); + if (operation.requestInt64Paths) { + const operationName = operation.operation ?? operation.path; + validateInt64RequestPaths(input.path, operation.requestInt64Paths.path, operationName); + validateInt64RequestPaths(input.query, operation.requestInt64Paths.query, operationName); + validateInt64RequestPaths(input.body, operation.requestInt64Paths.body, operationName); + } + if (operation.access === "public" && operation.requestBody) { + throw new SdkError(`public REST operation cannot send a body for ${operation.path}`); + } + const suppliedHeaders = input.headers as Record | undefined; + if (Object.keys(suppliedHeaders ?? {}).some((name) => name.toLowerCase() === "accept")) { + throw new SdkError(`Accept is reserved by the REST operation contract for ${operation.path}`); + } + const reservedHeader = Object.keys(suppliedHeaders ?? {}).find((name) => { + const normalized = name.toLowerCase(); + return normalized.startsWith("x-gemini-") || + ["authorization", "content-length", "content-type", "cache-control"].includes(normalized); + }); + if (reservedHeader) { + throw new SdkError(`header ${reservedHeader} is reserved by the REST operation contract for ${operation.path}`); + } + const headers = renderHeaders(operation, suppliedHeaders); + const query = renderQuery(operation, input.query); + const request = { + method: methodFor(operation), + path: renderPath(operation, input.path), + query, + queryParameters: operation.parameters.filter((parameter) => parameter.in === "query"), + headers, + responseInt64Paths: operation.responseInt64Paths, + responseMode: operation.responseMode, + responseContract: { + successStatuses: operation.successStatuses, + responseContentTypes: operation.responseContentTypes, + }, + operationContext: operationContextFor(operation, input.body), + retryable: operation.retryable === true, + ...requestOptions, + }; + if (operation.access === "public") { + return transport.requestPublic(request) as T["response"]; + } + if (operation.access === "authenticated") { + return transport.request({ + ...request, + params: operation.requestBody ? input.body as Record | undefined : undefined, + }) as T["response"]; + } + throw new SdkError(`unsupported REST access mode ${operation.access} for ${operation.path}`); +} diff --git a/packages/sdk-typescript/src/core/typed-emitter.ts b/packages/sdk-typescript/src/core/typed-emitter.ts new file mode 100644 index 0000000..e15b8f3 --- /dev/null +++ b/packages/sdk-typescript/src/core/typed-emitter.ts @@ -0,0 +1,129 @@ +// any[] required: unknown[] rejects typed callbacks due to contravariance (TS function parameter bivariance) +type Listener = (...args: any[]) => void; // eslint-disable-line @typescript-eslint/no-explicit-any + +interface Registration { + original: Listener; // the callback the caller passed + actual: Listener; // what's in the _listeners array (same for on, wrapper for once) +} + +/** + * Minimal typed event emitter that replaces `node:events` EventEmitter. + * Preserves the public surface used by the SDK and consumers: `on`, `off`, + * `once`, `addListener`, `removeListener`, `emit`, `listenerCount`, + * `removeAllListeners`, and `eventNames`. + * + * `off()` removes the most recently registered listener matching the callback, + * regardless of whether it was added via `on()` or `once()`, matching Node + * EventEmitter semantics. + */ +export class TypedEmitter> { + // The actual listener functions invoked by emit(). + private _listeners = new Map(); + // Registrations in insertion order — off() scans from the end to find the + // most recent match, exactly like Node's EventEmitter. + private _registrations = new Map(); + + on(event: E, fn: Events[E]): this { + let list = this._listeners.get(event); + if (!list) { + list = []; + this._listeners.set(event, list); + } + list.push(fn); + let regs = this._registrations.get(event); + if (!regs) { + regs = []; + this._registrations.set(event, regs); + } + regs.push({ original: fn, actual: fn }); + return this; + } + + addListener(event: E, fn: Events[E]): this { + return this.on(event, fn); + } + + off(event: E, fn: Events[E]): this { + const regs = this._registrations.get(event); + if (!regs) return this; + // Scan from the end to remove the most recent registration (Node semantics). + for (let i = regs.length - 1; i >= 0; i--) { + if (regs[i]!.original === fn) { + const removed = regs.splice(i, 1)[0]!; + const list = this._listeners.get(event); + if (list) { + const idx = list.indexOf(removed.actual); + if (idx >= 0) list.splice(idx, 1); + } + return this; + } + } + return this; + } + + removeListener(event: E, fn: Events[E]): this { + return this.off(event, fn); + } + + once(event: E, fn: Events[E]): this { + const wrapper = ((...args: Parameters) => { + this._removeByActual(event, wrapper); + fn(...args); + }) as Events[E]; + let list = this._listeners.get(event); + if (!list) { + list = []; + this._listeners.set(event, list); + } + list.push(wrapper); + let regs = this._registrations.get(event); + if (!regs) { + regs = []; + this._registrations.set(event, regs); + } + regs.push({ original: fn, actual: wrapper }); + return this; + } + + /** Remove a specific registration by its actual (wrapper) identity. */ + private _removeByActual(event: keyof Events, actual: Listener): void { + const regs = this._registrations.get(event); + if (!regs) return; + for (let i = regs.length - 1; i >= 0; i--) { + if (regs[i]!.actual === actual) { + regs.splice(i, 1); + break; + } + } + const list = this._listeners.get(event); + if (list) { + const idx = list.indexOf(actual); + if (idx >= 0) list.splice(idx, 1); + } + } + + emit(event: E, ...args: Parameters): void { + const list = this._listeners.get(event); + if (!list) return; + for (const fn of [...list]) fn(...args); + } + + listenerCount(event: keyof Events): number { + return this._listeners.get(event)?.length ?? 0; + } + + eventNames(): Array { + return [...this._listeners.keys()].filter((e) => (this._listeners.get(e)?.length ?? 0) > 0); + } + + removeAllListeners(event?: keyof Events): this { + if (event !== undefined) { + this._listeners.delete(event); + this._registrations.delete(event); + } else { + this._listeners.clear(); + this._registrations.clear(); + } + return this; + } +} diff --git a/packages/sdk-typescript/src/diagnostics.ts b/packages/sdk-typescript/src/diagnostics.ts new file mode 100644 index 0000000..ca1dc7c --- /dev/null +++ b/packages/sdk-typescript/src/diagnostics.ts @@ -0,0 +1,168 @@ +import type { HttpMethod } from "./core/http.js"; + +export type LogLevel = "debug" | "info" | "warn" | "error"; + +export type ErrorCategory = + | "validation" + | "authentication" + | "authorization" + | "not_found" + | "funds" + | "rate_limit" + | "service_unavailable" + | "unknown"; + +export type StableErrorCode = + | "invalid_request" + | "invalid_input" + | "authentication_failed" + | "authorization_failed" + | "terms_required" + | "terms_not_found" + | "order_not_found" + | "not_found" + | "insufficient_funds" + | "rate_limited" + | "program_unavailable" + | "service_unavailable" + | "unknown"; + +export type ResponseMetadata = { + endpoint: string; + method: HttpMethod; + correlationId: string; + exchangeRequestId?: string; + status?: number; + retryCount: number; + contentType?: string; + rateLimit?: { + limit?: string; + remaining?: string; + reset?: string; + retryAfter?: string; + }; +}; + +type ResponseHeaders = { get(name: string): string | null }; + +export function createResponseMetadata(options: { + endpoint: string; + method: HttpMethod; + correlationId: string; + status?: number; + retryCount: number; + headers?: ResponseHeaders; +}): ResponseMetadata { + const header = (...names: string[]): string | undefined => { + for (const name of names) { + const value = options.headers?.get(name); + if (value) return value; + } + return undefined; + }; + const rateLimit = Object.fromEntries(Object.entries({ + limit: header("x-ratelimit-limit", "x-rate-limit-limit"), + remaining: header("x-ratelimit-remaining", "x-rate-limit-remaining"), + reset: header("x-ratelimit-reset", "x-rate-limit-reset"), + retryAfter: header("retry-after", "x-ratelimit-retry-after", "x-rate-limit-retry-after"), + }).filter(([, value]) => value !== undefined)) as ResponseMetadata["rateLimit"]; + return { + endpoint: options.endpoint, + method: options.method, + correlationId: options.correlationId, + ...(options.status === undefined ? {} : { status: options.status }), + retryCount: options.retryCount, + exchangeRequestId: header("x-gemini-request-id", "x-request-id", "request-id"), + contentType: header("content-type")?.split(";", 1)[0]?.trim().toLowerCase(), + ...(Object.keys(rateLimit ?? {}).length > 0 ? { rateLimit } : {}), + }; +} + +export type OperationContext = { + operation: string; + clientOrderId?: string; + clientOrderIds?: readonly string[]; +}; + +export type AuthorizationContext = { + requiredRole?: string; + scope?: string; +}; + +export type SerializedError = { + name: string; + message: string; + cause?: SerializedError; + status?: number; + reason?: string; + code?: StableErrorCode; + serverCode?: string | number; + category?: ErrorCategory; + opened?: boolean; + closeCode?: number; + closeReason?: string; + metadata?: ResponseMetadata; + operationContext?: OperationContext; + authorizationContext?: AuthorizationContext; + body?: unknown; +}; + +export type DiagnosticEvent = { + level: LogLevel; + component: "rest" | "oauth" | "websocket" | "order_book"; + name: string; + traffic?: "control" | "stream" | "reconnect" | "mutation"; + response?: ResponseMetadata; + operationContext?: OperationContext; + metadata?: Readonly>; + error?: SerializedError; +}; + +export type DiagnosticListener = (event: DiagnosticEvent) => void; + +/** Remove credentials, query parameters, and fragments from a diagnostic URL. */ +export function sanitizeDiagnosticUrl(value: string): string { + try { + const url = new URL(value); + return `${url.protocol}//${url.host}${url.pathname}`; + } catch { + return "[REDACTED]"; + } +} + +const SENSITIVE_KEY = /authorization|auth|api[-_]?key|api[-_]?secret|signature|payload|token|secret|password|private[-_]?key|bank|payment|account|routing|swift|iban|address|street|city|state|postal|zip|transfer|transaction|body/i; +const SENSITIVE_STRING = /\b(authorization|auth|x-gemini-(?:api[-_]?key|apikey|payload|signature)|api[-_]?key|api[-_]?secret|signature|payload|access[-_]?token|refresh[-_]?token)(\s*[:=]\s*)(?:[a-z]+\s+)?[^\s,&|]+/gi; + +function redactString(value: string, secrets: readonly string[]): string { + let redacted = value; + for (const secret of secrets) { + if (secret) redacted = redacted.replaceAll(secret, "[REDACTED]"); + } + return redacted.replace(SENSITIVE_STRING, "$1$2[REDACTED]"); +} + +/** Clone and redact diagnostic data without mutating the caller's value. */ +export function redactDiagnosticValue( + value: unknown, + secrets: readonly string[] = [], + seen = new WeakSet(), + depth = 0, +): unknown { + if (typeof value === "string") return redactString(value, secrets); + if (typeof value === "bigint") return value.toString(); + if (value === null || typeof value !== "object") return value; + if (depth > 12) return "[REDACTED]"; + if (seen.has(value)) return "[CIRCULAR]"; + seen.add(value); + + if (Array.isArray(value)) { + return value.map((item) => redactDiagnosticValue(item, secrets, seen, depth + 1)); + } + + return Object.fromEntries(Object.entries(value).map(([key, item]) => + key === "authorizationContext" + ? [key, redactDiagnosticValue(item, secrets, seen, depth + 1)] + : SENSITIVE_KEY.test(key) + ? [key, "[REDACTED]"] + : [key, redactDiagnosticValue(item, secrets, seen, depth + 1)])); +} diff --git a/packages/sdk-typescript/src/errors.ts b/packages/sdk-typescript/src/errors.ts new file mode 100644 index 0000000..1a19b84 --- /dev/null +++ b/packages/sdk-typescript/src/errors.ts @@ -0,0 +1,466 @@ +import { + type AuthorizationContext, + type ErrorCategory, + type OperationContext, + type ResponseMetadata, + redactDiagnosticValue, + type SerializedError, + type StableErrorCode, +} from "./diagnostics.js"; +const rawErrorBodies = new WeakMap(); + +function retainRawErrorBody(error: Error, body: unknown): void { + if (body !== undefined) rawErrorBodies.set(error, body); +} + +export class SdkError extends Error { + readonly metadata?: ResponseMetadata; + readonly operationContext?: OperationContext; + + constructor(message: string, options?: { + cause?: unknown; + metadata?: ResponseMetadata; + operationContext?: OperationContext; + }) { + super(message, options); + this.name = "SdkError"; + this.metadata = options?.metadata; + this.operationContext = options?.operationContext; + } + + toJSON(): SerializedError { + return serializeError(this); + } +} + +/** A caller-owned request body does not match the documented input shape. */ +export class ValidationError extends SdkError { + readonly operation: string; + readonly field: string; + readonly rule: string; + + constructor(options: { operation: string; field: string; rule: string; message: string }) { + super(options.message); + this.name = "ValidationError"; + this.operation = options.operation; + this.field = options.field; + this.rule = options.rule; + } +} + +/** The caller cancelled an SDK operation before it completed. */ +export class RequestAbortedError extends SdkError { + constructor(message = "request was aborted") { super(message); this.name = "RequestAbortedError"; } +} + +/** An SDK operation exceeded its configured end-to-end deadline. */ +export class RequestTimeoutError extends SdkError { + constructor(message: string) { super(message); this.name = "RequestTimeoutError"; } +} + +/** The OAuth callback is missing the state value or belongs to another authorization flow. */ +export class OAuthStateError extends SdkError { + constructor(message: string) { + super(message); + this.name = "OAuthStateError"; + } +} + +/** An error returned through the browser authorization callback. */ +export class OAuthAuthorizationError extends SdkError { + readonly error: string; + readonly errorDescription?: string; + + constructor(error: string, errorDescription?: string) { + super("OAuth authorization failed"); + this.name = "OAuthAuthorizationError"; + this.error = error; + this.errorDescription = errorDescription; + } +} + +/** An RFC 6749 error response from Gemini's OAuth token endpoint. */ +export class OAuthTokenError extends SdkError { + readonly status: number; + readonly error: string; + readonly errorDescription?: string; + readonly category: ErrorCategory = "authentication"; + readonly code: StableErrorCode = "authentication_failed"; + + constructor(options: { + status: number; + error: string; + errorDescription?: string; + body?: unknown; + metadata?: ResponseMetadata; + }) { + super("OAuth token request failed", { metadata: options.metadata }); + this.name = "OAuthTokenError"; + this.status = options.status; + this.error = options.error; + this.errorDescription = options.errorDescription; + retainRawErrorBody(this, options.body); + } +} + +/** + * The WebSocket connection failed to open, or dropped mid-stream. + * `options.cause`, when present, holds the underlying network error. + */ +export class ConnectionError extends SdkError { + readonly opened?: boolean; + readonly closeCode?: number; + readonly closeReason?: string; + + constructor(message: string, options?: { + cause?: unknown; + opened?: boolean; + closeCode?: number; + closeReason?: string; + }) { + super(message, options); + this.name = "ConnectionError"; + this.opened = options?.opened; + this.closeCode = options?.closeCode; + this.closeReason = options?.closeReason; + } +} + +/** A non-success response to a WebSocket method request. */ +export class WebSocketRequestError extends SdkError { + readonly status: number; + readonly reason?: string; + readonly code: StableErrorCode; + readonly serverCode?: string | number; + readonly category: ErrorCategory; + readonly authorizationContext?: AuthorizationContext; + + constructor(options: { status: number; body: unknown; message?: string; operationContext?: OperationContext }) { + const classification = classifyServerError(options.body, options.status); + super(options.message ?? `WebSocket request failed with status ${options.status}`, { + operationContext: options.operationContext, + }); + this.name = "WebSocketRequestError"; + this.status = options.status; + this.reason = classification.reason; + this.code = classification.code; + this.serverCode = classification.serverCode; + this.category = classification.category; + this.authorizationContext = classification.authorizationContext; + retainRawErrorBody(this, options.body); + } +} + +/** + * A private REST payload's `request` field did not match the endpoint being + * called — a build-time invariant violation (e.g. a params key clobbering + * `request`). Thrown before the request is sent, never silently corrected. + */ +export class EndpointMismatch extends SdkError { + constructor(expected: string, actual: unknown) { + super(`payload.request must be "${expected}", got ${typeof actual}`); + this.name = "EndpointMismatch"; + } +} + +/** + * A non-2xx REST response. The base class for every API-reported failure; catch + * it to handle any HTTP error at once, or catch a specific subclass below. + * - status: the HTTP status code + * - reason: the error code from the body (`reason` or `error` field), verbatim + * Raw response bodies are available only through `serializeError(error, { includeRawBody: true })`. + */ +export class ApiError extends SdkError { + readonly status: number; + readonly reason?: string; + readonly code: StableErrorCode; + readonly serverCode?: string | number; + readonly category: ErrorCategory; + readonly authorizationContext?: AuthorizationContext; + + constructor(options: { + status: number; + reason?: string; + message?: string; + body?: unknown; + metadata?: ResponseMetadata; + operationContext?: OperationContext; + code?: StableErrorCode; + serverCode?: string | number; + category?: ErrorCategory; + authorizationContext?: AuthorizationContext; + }) { + const classification = classifyServerError({ error: options.reason, code: options.serverCode }, options.status); + super(options.message ?? `HTTP ${options.status}`, { + metadata: options.metadata, + operationContext: options.operationContext, + }); + this.name = "ApiError"; + this.status = options.status; + this.reason = options.reason; + retainRawErrorBody(this, options.body); + this.code = options.code ?? classification.code; + this.serverCode = options.serverCode ?? classification.serverCode; + this.category = options.category ?? classification.category; + this.authorizationContext = options.authorizationContext ?? classification.authorizationContext; + } +} + +export type ServerErrorClassification = { + reason?: string; + code: StableErrorCode; + category: ErrorCategory; + serverCode?: string | number; + authorizationContext?: AuthorizationContext; +}; + +function normalized(value: string | undefined): string | undefined { + return value?.toLowerCase().replace(/[^a-z0-9]/g, ""); +} + +function reasonClassification(reason: string | undefined): Pick | undefined { + switch (normalized(reason)) { + case "invalidinput": + case "badrequest": + case "invalidrequest": + return { code: "invalid_input", category: "validation" }; + case "invalidnonce": + case "missingnonce": + case "invalidsignature": + return { code: "authentication_failed", category: "authentication" }; + case "missingrole": + case "forbidden": + return { code: "authorization_failed", category: "authorization" }; + case "accepttermsrequired": + case "termsnotaccepted": + case "predictionmarketstermsmustbeacceptedbeforeplacingorders": + return { code: "terms_required", category: "authorization" }; + case "termsnotfound": + return { code: "terms_not_found", category: "not_found" }; + case "ordernotfound": + return { code: "order_not_found", category: "not_found" }; + case "notfound": + return { code: "not_found", category: "not_found" }; + case "insufficientfunds": + return { code: "insufficient_funds", category: "funds" }; + case "ratelimit": + case "ratelimited": + return { code: "rate_limited", category: "rate_limit" }; + case "programunavailable": + return { code: "program_unavailable", category: "service_unavailable" }; + case "serviceunavailable": + case "internalerror": + return { code: "service_unavailable", category: "service_unavailable" }; + default: + return undefined; + } +} + +function statusClassification(status: number): Pick { + if (status === 400) return { code: "invalid_request", category: "validation" }; + if (status === 401) return { code: "authentication_failed", category: "authentication" }; + if (status === 403) return { code: "authorization_failed", category: "authorization" }; + if (status === 404) return { code: "not_found", category: "not_found" }; + if (status === 406) return { code: "insufficient_funds", category: "funds" }; + if (status === 429) return { code: "rate_limited", category: "rate_limit" }; + if (status >= 500) return { code: "service_unavailable", category: "service_unavailable" }; + return { code: "unknown", category: "unknown" }; +} + +/** Classify an exchange error without retaining or returning its raw body. */ +export function classifyServerError(body: unknown, status?: number): ServerErrorClassification { + const record = body !== null && typeof body === "object" && !Array.isArray(body) + ? body as Record + : {}; + const nestedError = record.error !== null && typeof record.error === "object" && !Array.isArray(record.error) + ? record.error as Record + : undefined; + const reason = typeof record.reason === "string" + ? record.reason + : typeof record.error === "string" + ? record.error + : typeof nestedError?.msg === "string" + ? nestedError.msg + : typeof nestedError?.reason === "string" + ? nestedError.reason + : typeof record.code === "string" + ? record.code + : undefined; + const serverCode = typeof record.code === "string" || typeof record.code === "number" + ? record.code + : typeof nestedError?.code === "string" || typeof nestedError?.code === "number" + ? nestedError.code + : undefined; + const requiredRole = typeof record.requiredRole === "string" + ? record.requiredRole + : typeof record.required_role === "string" + ? record.required_role + : undefined; + const scope = typeof record.accountScope === "string" + ? record.accountScope + : typeof record.account_scope === "string" + ? record.account_scope + : typeof record.scope === "string" + ? record.scope + : undefined; + const authorizationContext = requiredRole !== undefined || scope !== undefined + ? { + ...(requiredRole !== undefined ? { requiredRole } : {}), + ...(scope !== undefined ? { scope } : {}), + } + : undefined; + return { + reason, + ...(reasonClassification(reason) ?? statusClassification(status ?? 0)), + ...(serverCode ? { serverCode } : {}), + ...(authorizationContext ? { authorizationContext } : {}), + }; +} + +export type SerializeErrorOptions = { includeRawBody?: boolean }; + +/** Serialize an SDK error for logs, telemetry, or evidence without raw secrets. */ +export function serializeError(error: unknown, options?: SerializeErrorOptions): SerializedError { + if (!(error instanceof Error)) { + return { name: "Error", message: "Unknown error" }; + } + + const candidate = error as Error & { + status?: number; + reason?: string; + code?: StableErrorCode; + serverCode?: string | number; + category?: ErrorCategory; + opened?: boolean; + closeCode?: number; + closeReason?: string; + cause?: unknown; + metadata?: ResponseMetadata; + operationContext?: OperationContext; + authorizationContext?: AuthorizationContext; + }; + const safeReason = candidate.reason !== undefined && reasonClassification(candidate.reason) !== undefined + ? candidate.reason + : undefined; + const safeServerCode = candidate.serverCode !== undefined && ( + typeof candidate.serverCode === "number" || reasonClassification(String(candidate.serverCode)) !== undefined + ) ? candidate.serverCode : undefined; + const serialized: SerializedError = { + name: error.name, + message: redactDiagnosticValue(error.message) as string, + ...(candidate.cause instanceof Error + ? { cause: serializeError(candidate.cause) } + : candidate.cause !== undefined + ? { cause: { name: "Cause", message: redactDiagnosticValue(String(candidate.cause)) as string } } + : {}), + ...(candidate.status !== undefined ? { status: candidate.status } : {}), + ...(safeReason !== undefined ? { reason: safeReason } : {}), + ...(candidate.code !== undefined ? { code: candidate.code } : {}), + ...(safeServerCode !== undefined ? { serverCode: safeServerCode } : {}), + ...(candidate.category !== undefined ? { category: candidate.category } : {}), + ...(candidate.opened !== undefined ? { opened: candidate.opened } : {}), + ...(candidate.closeCode !== undefined ? { closeCode: candidate.closeCode } : {}), + ...(candidate.closeReason !== undefined ? { closeReason: redactDiagnosticValue(candidate.closeReason) as string } : {}), + ...(candidate.metadata ? { metadata: candidate.metadata } : {}), + ...(candidate.operationContext ? { operationContext: candidate.operationContext } : {}), + ...(candidate.authorizationContext ? { authorizationContext: candidate.authorizationContext } : {}), + }; + const body = rawErrorBodies.get(error); + if (options?.includeRawBody && body !== undefined) serialized.body = body; + return serialized; +} + +type ApiErrorOptions = ConstructorParameters[0]; + +/** 400: the request was malformed or rejected (generic 4xx default). */ +export class InvalidRequest extends ApiError { + constructor(options: ApiErrorOptions) { + super(options); + this.name = "InvalidRequest"; + } +} +/** The nonce was reused or did not strictly increase. */ +export class InvalidNonce extends ApiError { + constructor(options: ApiErrorOptions) { + super(options); + this.name = "InvalidNonce"; + } +} +/** The payload omitted the required nonce. */ +export class MissingNonce extends ApiError { + constructor(options: ApiErrorOptions) { + super(options); + this.name = "MissingNonce"; + } +} +/** The request signature did not verify against the payload. */ +export class InvalidSignature extends ApiError { + constructor(options: ApiErrorOptions) { + super(options); + this.name = "InvalidSignature"; + } +} +/** The API key lacks a role required for this endpoint (403 default). */ +export class MissingRole extends ApiError { + constructor(options: ApiErrorOptions) { + super(options); + this.name = "MissingRole"; + } +} +/** The caller must accept the current terms before using this endpoint. */ +export class AcceptTermsRequired extends ApiError { + constructor(options: ApiErrorOptions) { + super(options); + this.name = "AcceptTermsRequired"; + } +} +/** 404: the referenced resource does not exist. */ +export class NotFoundError extends ApiError { + constructor(options: ApiErrorOptions) { + super(options); + this.name = "NotFoundError"; + } +} +/** 406: the account lacks the funds/quantity to satisfy the request. */ +export class InsufficientFunds extends ApiError { + constructor(options: ApiErrorOptions) { + super(options); + this.name = "InsufficientFunds"; + } +} +/** 429: rate limit exceeded. Thrown after client-side retries are exhausted. */ +export class RateLimitError extends ApiError { + constructor(options: ApiErrorOptions) { + super(options); + this.name = "RateLimitError"; + } +} +/** 5xx: the exchange is unavailable or errored internally. */ +export class ServiceUnavailable extends ApiError { + constructor(options: ApiErrorOptions) { + super(options); + this.name = "ServiceUnavailable"; + } +} + +/** + * The local order book hit a gap in the update sequence, so it can no longer be + * trusted and must be discarded and rebuilt from a fresh snapshot. + * + * The two ids describe the gap (for logging/debugging): + * - lastUpdateId: the last update the book had applied + * - firstUpdateId: the first id of the diff that skipped ahead + */ +export class ResyncRequiredError extends SdkError { + readonly lastUpdateId: bigint; + readonly firstUpdateId: bigint; + + constructor(lastUpdateId: bigint, firstUpdateId: bigint) { + super( + `Order book gap: had update ${lastUpdateId}, next diff started at ${firstUpdateId}`, + ); + this.name = "ResyncRequiredError"; + this.lastUpdateId = lastUpdateId; + this.firstUpdateId = firstUpdateId; + } +} diff --git a/packages/sdk-typescript/src/gemini-markets.ts b/packages/sdk-typescript/src/gemini-markets.ts new file mode 100644 index 0000000..f72b97c --- /dev/null +++ b/packages/sdk-typescript/src/gemini-markets.ts @@ -0,0 +1,99 @@ +import { NOOP_LOGGER } from "./logging.js"; +import type { GeminiMarketsOptions, LiveOrderBook as LiveOrderBookContract } from "./types/client.js"; +import type { RequestOptions } from "./core/deadline.js"; +import { HttpTransport, type FetchLike } from "./core/http.js"; +import { GeminiWebSocket } from "./websocket.js"; +import { PredictionMarkets } from "./prediction-markets.js"; +import { AccountServicesRest } from "./generated/account-services/rest.js"; +import { ClearingInstantRest } from "./generated/clearing-instant/rest.js"; +import { MarginRest } from "./generated/margin/rest.js"; +import { MarketDataRest } from "./generated/market-data/rest.js"; +import { PerpetualsRest } from "./generated/perpetuals/rest.js"; +import { TradingRest } from "./generated/trading/rest.js"; +import { ManagedHeartbeat } from "./heartbeat.js"; +import { ENVIRONMENT_URLS } from "./core/environment.js"; + +const DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000; + +interface RuntimeOptions extends GeminiMarketsOptions { + fetchImpl?: FetchLike; +} + +export class GeminiMarkets { + readonly predictions: PredictionMarkets; + readonly marketData: MarketDataRest; + readonly trading: TradingRest; + readonly margin: MarginRest; + readonly perpetuals: PerpetualsRest; + readonly accountServices: AccountServicesRest; + readonly clearingInstant: ClearingInstantRest; + readonly websocket: GeminiWebSocket; + + constructor(options?: GeminiMarketsOptions) { + const settings = (options ?? {}) as RuntimeOptions; + const env = settings.env ?? "production"; + const logger = settings.logger ?? NOOP_LOGGER; + const restTransport = new HttpTransport({ + env, + auth: settings.auth, + logger, + onDiagnostic: settings.onDiagnostic, + fetchImpl: settings.fetch ?? settings.fetchImpl, + maxRetries: settings.maxRetries, + backoff: settings.backoff, + timeoutMs: settings.timeoutMs, + }); + + this.predictions = new PredictionMarkets(restTransport); + this.marketData = new MarketDataRest(restTransport); + this.trading = new TradingRest(restTransport); + this.margin = new MarginRest(restTransport); + this.perpetuals = new PerpetualsRest(restTransport); + this.accountServices = new AccountServicesRest(restTransport); + this.clearingInstant = new ClearingInstantRest(restTransport); + const websocketUrl = ENVIRONMENT_URLS[env].websocket; + this.websocket = new GeminiWebSocket({ + url: websocketUrl, + snapshotUrl: `${websocketUrl}?snapshot=-1`, + auth: settings.auth, + logger, + onDiagnostic: settings.onDiagnostic, + socketFactory: settings.webSocketFactory, + snapshotStream: env === "sandbox", + timeoutMs: settings.timeoutMs, + liveness: settings.webSocketLiveness, + maxMessageSizeBytes: settings.webSocketMaxMessageSizeBytes, + }); + } + + orderBook(symbol: string, options?: RequestOptions): LiveOrderBookContract { + return this.websocket.orderBook(symbol, options); + } + + /** Create a stopped heartbeat handle; call start() explicitly to begin sending. */ + createHeartbeat(options?: { + intervalMs?: number; + onError?: (error: unknown) => void; + requestOptions?: RequestOptions; + }): ManagedHeartbeat { + return new ManagedHeartbeat({ + intervalMs: options?.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS, + onError: options?.onError, + requestOptions: options?.requestOptions, + beat: (requestOptions) => this.trading.sendHeartbeat({}, requestOptions), + }); + } + + /** @deprecated Use `createHeartbeat()` — this alias will be removed in a future release. */ + startHeartbeat(options?: { + intervalMs?: number; + onError?: (error: unknown) => void; + requestOptions?: RequestOptions; + }): ManagedHeartbeat { + return this.createHeartbeat(options); + } + + close(): void { + this.websocket.close(); + } +} diff --git a/packages/sdk-typescript/src/generated/account-services/operations.ts b/packages/sdk-typescript/src/generated/account-services/operations.ts new file mode 100644 index 0000000..a689425 --- /dev/null +++ b/packages/sdk-typescript/src/generated/account-services/operations.ts @@ -0,0 +1,262 @@ +// Generated from rest.yaml#Account Services. Do not edit. + +import type { operations as OpenApiOperations } from "../market-data/models.js"; + +type ParameterAt = + O extends { parameters: infer P } + ? Location extends keyof P ? P[Location] : never + : never; + +type Int64Input = + T extends bigint ? bigint | number : + T extends readonly (infer Item)[] ? Int64Input[] : + T extends object ? { [K in keyof T]: Int64Input } : T; + +type JsonBody = + NonNullable extends + { content: { "application/json": infer Body } } + ? Required extends true ? Body : Body | undefined + : never; + +type StripTransportFields = T extends object ? Omit : T; + +type CallerJsonBody = StripTransportFields; + +type JsonResponse = + O extends { responses: infer R } + ? Status extends keyof R + ? R[Status] extends { content: { "application/json": infer Body } } ? Body : never + : never + : never; + +export const ACCOUNT_SERVICES_OPERATIONS = { + "addBank": {"responseMode":"json","operation":"accountServices.addBank","method":"post","path":"/v1/payments/addbank","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "addBankCAD": {"responseMode":"json","operation":"accountServices.addBankCAD","method":"post","path":"/v1/payments/addbank/cad","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "createNewAccount": {"responseMode":"json","operation":"accountServices.createNewAccount","method":"post","path":"/v1/account/create","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "createNewApprovedAddress": {"responseMode":"json","operation":"accountServices.createNewApprovedAddress","method":"post","path":"/v1/approvedAddresses/{network}/request","access":"authenticated","parameters":[{"name":"network","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "createNewDepositAddress": {"responseMode":"json","operation":"accountServices.createNewDepositAddress","method":"post","path":"/v1/deposit/{network}/newAddress","access":"authenticated","parameters":[{"name":"network","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "getAccountDetail": {"responseMode":"json","operation":"accountServices.getAccountDetail","method":"post","path":"/v1/account","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "getAvailableBalances": {"responseMode":"json","operation":"accountServices.getAvailableBalances","method":"post","path":"/v1/balances","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "getGasFeeEstimation": {"responseMode":"json","operation":"accountServices.getGasFeeEstimation","method":"post","path":"/v2/withdraw/{network}/{ticker}/feeEstimate","access":"authenticated","parameters":[{"name":"network","in":"path","required":true,"style":"simple","explode":false},{"name":"ticker","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "getNotionalBalances": {"responseMode":"json","operation":"accountServices.getNotionalBalances","method":"post","path":"/v1/notionalbalances/{currency}","access":"authenticated","parameters":[{"name":"currency","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "getRoles": {"responseMode":"json","operation":"accountServices.getRoles","method":"post","path":"/v1/roles","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "getTransactionHistory": {"responseMode":"json","operation":"accountServices.getTransactionHistory","method":"post","path":"/v1/transactions","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["results","*","advanceEid"],["results","*","correlationId"],["results","*","eid"],["results","*","orderId"],["results","*","pendingEid"],["results","*","tid"],["results","*","withdrawalEid"]],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true},{"path":["timestamp_nanos"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "listAccountsInGroup": {"responseMode":"json","operation":"accountServices.listAccountsInGroup","method":"post","path":"/v1/account/list","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true},{"path":["timestamp"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "listApprovedAddresses": {"responseMode":"json","operation":"accountServices.listApprovedAddresses","method":"post","path":"/v1/approvedAddresses/account/{network}","access":"authenticated","parameters":[{"name":"network","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "listCustodyFeeTransfers": {"responseMode":"json","operation":"accountServices.listCustodyFeeTransfers","method":"post","path":"/v1/custodyaccountfees","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true},{"path":["timestamp"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "listDepositAddresses": {"responseMode":"json","operation":"accountServices.listDepositAddresses","method":"post","path":"/v1/addresses/{network}","access":"authenticated","parameters":[{"name":"network","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true},{"path":["timestamp"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "listPastTransfers": {"responseMode":"json","operation":"accountServices.listPastTransfers","method":"post","path":"/v2/transfers","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["*","eid"]],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true},{"path":["timestamp"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "listPaymentMethods": {"responseMode":"json","operation":"accountServices.listPaymentMethods","method":"post","path":"/v1/payments/methods","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "listStakingBalances": {"responseMode":"json","operation":"accountServices.listStakingBalances","method":"post","path":"/v1/balances/staking","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "listStakingEventHistory": {"responseMode":"json","operation":"accountServices.listStakingEventHistory","method":"post","path":"/v1/staking/history","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true},{"path":["since"],"allowString":true},{"path":["until"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "listStakingRates": {"responseMode":"json","operation":"accountServices.listStakingRates","method":"get","path":"/v1/staking/rates","access":"public","parameters":[],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "listStakingRewards": {"responseMode":"json","operation":"accountServices.listStakingRewards","method":"post","path":"/v1/staking/rewards","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "removeApprovedAddress": {"responseMode":"json","operation":"accountServices.removeApprovedAddress","method":"post","path":"/v1/approvedAddresses/{network}/remove","access":"authenticated","parameters":[{"name":"network","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "renameAccount": {"responseMode":"json","operation":"accountServices.renameAccount","method":"post","path":"/v1/account/rename","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "revokeOAuthToken": {"responseMode":"json","operation":"accountServices.revokeOAuthToken","method":"post","path":"/v1/oauth/revokeByToken","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":false}, + "stakeCryptoFunds": {"responseMode":"json","operation":"accountServices.stakeCryptoFunds","method":"post","path":"/v1/staking/stake","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "transferBetweenAccounts": {"responseMode":"json","operation":"accountServices.transferBetweenAccounts","method":"post","path":"/v1/account/transfer/{currency}","access":"authenticated","parameters":[{"name":"currency","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "unstakeCryptoFunds": {"responseMode":"json","operation":"accountServices.unstakeCryptoFunds","method":"post","path":"/v1/staking/unstake","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "withdrawCryptoFunds": {"responseMode":"json","operation":"accountServices.withdrawCryptoFunds","method":"post","path":"/v2/withdraw/{network}/{ticker}","access":"authenticated","parameters":[{"name":"network","in":"path","required":true,"style":"simple","explode":false},{"name":"ticker","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":false}, +} as const; + +export type AccountServicesOperationId = keyof typeof ACCOUNT_SERVICES_OPERATIONS; + +export type AccountServicesOperationTypes = { + "addBank": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "addBankCAD": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "createNewAccount": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "createNewApprovedAddress": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "createNewDepositAddress": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getAccountDetail": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getAvailableBalances": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getGasFeeEstimation": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getNotionalBalances": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getRoles": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getTransactionHistory": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "listAccountsInGroup": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "listApprovedAddresses": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "listCustodyFeeTransfers": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "listDepositAddresses": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "listPastTransfers": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "listPaymentMethods": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "listStakingBalances": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "listStakingEventHistory": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "listStakingRates": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "listStakingRewards": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "removeApprovedAddress": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "renameAccount": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "revokeOAuthToken": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "stakeCryptoFunds": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "transferBetweenAccounts": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "unstakeCryptoFunds": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "withdrawCryptoFunds": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; +}; diff --git a/packages/sdk-typescript/src/generated/account-services/rest.ts b/packages/sdk-typescript/src/generated/account-services/rest.ts new file mode 100644 index 0000000..6a7f307 --- /dev/null +++ b/packages/sdk-typescript/src/generated/account-services/rest.ts @@ -0,0 +1,354 @@ +// Generated from rest.yaml#Account Services. Do not edit. + +import type { HttpTransport } from "../../core/http.js"; +import type { RequestOptions } from "../../core/deadline.js"; +import { executeRestOperation } from "../../core/rest-operation.js"; + +import { + ACCOUNT_SERVICES_OPERATIONS, + type AccountServicesOperationTypes, +} from "./operations.js"; + +export class AccountServicesRest { + constructor(private readonly transport: HttpTransport) {} + + addBank(body: AccountServicesOperationTypes["addBank"]["body"], requestOptions?: RequestOptions): Promise; + addBank(body: AccountServicesOperationTypes["addBank"]["body"]): Promise; + addBank(body: AccountServicesOperationTypes["addBank"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["addBank"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + addBankCAD(body: AccountServicesOperationTypes["addBankCAD"]["body"], requestOptions?: RequestOptions): Promise; + addBankCAD(body: AccountServicesOperationTypes["addBankCAD"]["body"]): Promise; + addBankCAD(body: AccountServicesOperationTypes["addBankCAD"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["addBankCAD"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + createNewAccount(body: AccountServicesOperationTypes["createNewAccount"]["body"], requestOptions?: RequestOptions): Promise; + createNewAccount(body: AccountServicesOperationTypes["createNewAccount"]["body"]): Promise; + createNewAccount(body: AccountServicesOperationTypes["createNewAccount"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["createNewAccount"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + createNewApprovedAddress(input: { + path: AccountServicesOperationTypes["createNewApprovedAddress"]["path"]; + body: AccountServicesOperationTypes["createNewApprovedAddress"]["body"]; + }, requestOptions?: RequestOptions): Promise; + createNewApprovedAddress(input: { + path: AccountServicesOperationTypes["createNewApprovedAddress"]["path"]; + body: AccountServicesOperationTypes["createNewApprovedAddress"]["body"]; + }): Promise; + createNewApprovedAddress(input: { + path: AccountServicesOperationTypes["createNewApprovedAddress"]["path"]; + body: AccountServicesOperationTypes["createNewApprovedAddress"]["body"]; + }, requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["createNewApprovedAddress"]; + return executeRestOperation(this.transport, operation, { + path: input.path, + body: input.body, + }, requestOptions); + } + + createNewDepositAddress(input: { + path: AccountServicesOperationTypes["createNewDepositAddress"]["path"]; + body: AccountServicesOperationTypes["createNewDepositAddress"]["body"]; + }, requestOptions?: RequestOptions): Promise; + createNewDepositAddress(input: { + path: AccountServicesOperationTypes["createNewDepositAddress"]["path"]; + body: AccountServicesOperationTypes["createNewDepositAddress"]["body"]; + }): Promise; + createNewDepositAddress(input: { + path: AccountServicesOperationTypes["createNewDepositAddress"]["path"]; + body: AccountServicesOperationTypes["createNewDepositAddress"]["body"]; + }, requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["createNewDepositAddress"]; + return executeRestOperation(this.transport, operation, { + path: input.path, + body: input.body, + }, requestOptions); + } + + getAccountDetail(body: AccountServicesOperationTypes["getAccountDetail"]["body"], requestOptions?: RequestOptions): Promise; + getAccountDetail(body: AccountServicesOperationTypes["getAccountDetail"]["body"]): Promise; + getAccountDetail(body: AccountServicesOperationTypes["getAccountDetail"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["getAccountDetail"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + getAvailableBalances(body: AccountServicesOperationTypes["getAvailableBalances"]["body"], requestOptions?: RequestOptions): Promise; + getAvailableBalances(body: AccountServicesOperationTypes["getAvailableBalances"]["body"]): Promise; + getAvailableBalances(body: AccountServicesOperationTypes["getAvailableBalances"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["getAvailableBalances"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + getGasFeeEstimation(input: { + path: AccountServicesOperationTypes["getGasFeeEstimation"]["path"]; + body: AccountServicesOperationTypes["getGasFeeEstimation"]["body"]; + }, requestOptions?: RequestOptions): Promise; + getGasFeeEstimation(input: { + path: AccountServicesOperationTypes["getGasFeeEstimation"]["path"]; + body: AccountServicesOperationTypes["getGasFeeEstimation"]["body"]; + }): Promise; + getGasFeeEstimation(input: { + path: AccountServicesOperationTypes["getGasFeeEstimation"]["path"]; + body: AccountServicesOperationTypes["getGasFeeEstimation"]["body"]; + }, requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["getGasFeeEstimation"]; + return executeRestOperation(this.transport, operation, { + path: input.path, + body: input.body, + }, requestOptions); + } + + getNotionalBalances(input: { + path: AccountServicesOperationTypes["getNotionalBalances"]["path"]; + body: AccountServicesOperationTypes["getNotionalBalances"]["body"]; + }, requestOptions?: RequestOptions): Promise; + getNotionalBalances(input: { + path: AccountServicesOperationTypes["getNotionalBalances"]["path"]; + body: AccountServicesOperationTypes["getNotionalBalances"]["body"]; + }): Promise; + getNotionalBalances(input: { + path: AccountServicesOperationTypes["getNotionalBalances"]["path"]; + body: AccountServicesOperationTypes["getNotionalBalances"]["body"]; + }, requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["getNotionalBalances"]; + return executeRestOperation(this.transport, operation, { + path: input.path, + body: input.body, + }, requestOptions); + } + + getRoles(body: AccountServicesOperationTypes["getRoles"]["body"], requestOptions?: RequestOptions): Promise; + getRoles(body: AccountServicesOperationTypes["getRoles"]["body"]): Promise; + getRoles(body: AccountServicesOperationTypes["getRoles"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["getRoles"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + getTransactionHistory(body: AccountServicesOperationTypes["getTransactionHistory"]["body"], requestOptions?: RequestOptions): Promise; + getTransactionHistory(body: AccountServicesOperationTypes["getTransactionHistory"]["body"]): Promise; + getTransactionHistory(body: AccountServicesOperationTypes["getTransactionHistory"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["getTransactionHistory"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + listAccountsInGroup(body: AccountServicesOperationTypes["listAccountsInGroup"]["body"], requestOptions?: RequestOptions): Promise; + listAccountsInGroup(body: AccountServicesOperationTypes["listAccountsInGroup"]["body"]): Promise; + listAccountsInGroup(body: AccountServicesOperationTypes["listAccountsInGroup"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["listAccountsInGroup"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + listApprovedAddresses(input: { + path: AccountServicesOperationTypes["listApprovedAddresses"]["path"]; + body: AccountServicesOperationTypes["listApprovedAddresses"]["body"]; + }, requestOptions?: RequestOptions): Promise; + listApprovedAddresses(input: { + path: AccountServicesOperationTypes["listApprovedAddresses"]["path"]; + body: AccountServicesOperationTypes["listApprovedAddresses"]["body"]; + }): Promise; + listApprovedAddresses(input: { + path: AccountServicesOperationTypes["listApprovedAddresses"]["path"]; + body: AccountServicesOperationTypes["listApprovedAddresses"]["body"]; + }, requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["listApprovedAddresses"]; + return executeRestOperation(this.transport, operation, { + path: input.path, + body: input.body, + }, requestOptions); + } + + listCustodyFeeTransfers(body: AccountServicesOperationTypes["listCustodyFeeTransfers"]["body"], requestOptions?: RequestOptions): Promise; + listCustodyFeeTransfers(body: AccountServicesOperationTypes["listCustodyFeeTransfers"]["body"]): Promise; + listCustodyFeeTransfers(body: AccountServicesOperationTypes["listCustodyFeeTransfers"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["listCustodyFeeTransfers"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + listDepositAddresses(input: { + path: AccountServicesOperationTypes["listDepositAddresses"]["path"]; + body: AccountServicesOperationTypes["listDepositAddresses"]["body"]; + }, requestOptions?: RequestOptions): Promise; + listDepositAddresses(input: { + path: AccountServicesOperationTypes["listDepositAddresses"]["path"]; + body: AccountServicesOperationTypes["listDepositAddresses"]["body"]; + }): Promise; + listDepositAddresses(input: { + path: AccountServicesOperationTypes["listDepositAddresses"]["path"]; + body: AccountServicesOperationTypes["listDepositAddresses"]["body"]; + }, requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["listDepositAddresses"]; + return executeRestOperation(this.transport, operation, { + path: input.path, + body: input.body, + }, requestOptions); + } + + listPastTransfers(body: AccountServicesOperationTypes["listPastTransfers"]["body"], requestOptions?: RequestOptions): Promise; + listPastTransfers(body: AccountServicesOperationTypes["listPastTransfers"]["body"]): Promise; + listPastTransfers(body: AccountServicesOperationTypes["listPastTransfers"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["listPastTransfers"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + listPaymentMethods(body: AccountServicesOperationTypes["listPaymentMethods"]["body"], requestOptions?: RequestOptions): Promise; + listPaymentMethods(body: AccountServicesOperationTypes["listPaymentMethods"]["body"]): Promise; + listPaymentMethods(body: AccountServicesOperationTypes["listPaymentMethods"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["listPaymentMethods"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + listStakingBalances(body: AccountServicesOperationTypes["listStakingBalances"]["body"], requestOptions?: RequestOptions): Promise; + listStakingBalances(body: AccountServicesOperationTypes["listStakingBalances"]["body"]): Promise; + listStakingBalances(body: AccountServicesOperationTypes["listStakingBalances"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["listStakingBalances"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + listStakingEventHistory(body: AccountServicesOperationTypes["listStakingEventHistory"]["body"], requestOptions?: RequestOptions): Promise; + listStakingEventHistory(body: AccountServicesOperationTypes["listStakingEventHistory"]["body"]): Promise; + listStakingEventHistory(body: AccountServicesOperationTypes["listStakingEventHistory"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["listStakingEventHistory"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + listStakingRates(requestOptions?: RequestOptions): Promise; + listStakingRates(): Promise; + listStakingRates(requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["listStakingRates"]; + return executeRestOperation(this.transport, operation, {}, requestOptions); + } + + listStakingRewards(body: AccountServicesOperationTypes["listStakingRewards"]["body"], requestOptions?: RequestOptions): Promise; + listStakingRewards(body: AccountServicesOperationTypes["listStakingRewards"]["body"]): Promise; + listStakingRewards(body: AccountServicesOperationTypes["listStakingRewards"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["listStakingRewards"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + removeApprovedAddress(input: { + path: AccountServicesOperationTypes["removeApprovedAddress"]["path"]; + body: AccountServicesOperationTypes["removeApprovedAddress"]["body"]; + }, requestOptions?: RequestOptions): Promise; + removeApprovedAddress(input: { + path: AccountServicesOperationTypes["removeApprovedAddress"]["path"]; + body: AccountServicesOperationTypes["removeApprovedAddress"]["body"]; + }): Promise; + removeApprovedAddress(input: { + path: AccountServicesOperationTypes["removeApprovedAddress"]["path"]; + body: AccountServicesOperationTypes["removeApprovedAddress"]["body"]; + }, requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["removeApprovedAddress"]; + return executeRestOperation(this.transport, operation, { + path: input.path, + body: input.body, + }, requestOptions); + } + + renameAccount(body: AccountServicesOperationTypes["renameAccount"]["body"], requestOptions?: RequestOptions): Promise; + renameAccount(body: AccountServicesOperationTypes["renameAccount"]["body"]): Promise; + renameAccount(body: AccountServicesOperationTypes["renameAccount"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["renameAccount"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + revokeOAuthToken(body: AccountServicesOperationTypes["revokeOAuthToken"]["body"], requestOptions?: RequestOptions): Promise; + revokeOAuthToken(body: AccountServicesOperationTypes["revokeOAuthToken"]["body"]): Promise; + revokeOAuthToken(body: AccountServicesOperationTypes["revokeOAuthToken"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["revokeOAuthToken"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + stakeCryptoFunds(body: AccountServicesOperationTypes["stakeCryptoFunds"]["body"], requestOptions?: RequestOptions): Promise; + stakeCryptoFunds(body: AccountServicesOperationTypes["stakeCryptoFunds"]["body"]): Promise; + stakeCryptoFunds(body: AccountServicesOperationTypes["stakeCryptoFunds"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["stakeCryptoFunds"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + transferBetweenAccounts(input: { + path: AccountServicesOperationTypes["transferBetweenAccounts"]["path"]; + body: AccountServicesOperationTypes["transferBetweenAccounts"]["body"]; + }, requestOptions?: RequestOptions): Promise; + transferBetweenAccounts(input: { + path: AccountServicesOperationTypes["transferBetweenAccounts"]["path"]; + body: AccountServicesOperationTypes["transferBetweenAccounts"]["body"]; + }): Promise; + transferBetweenAccounts(input: { + path: AccountServicesOperationTypes["transferBetweenAccounts"]["path"]; + body: AccountServicesOperationTypes["transferBetweenAccounts"]["body"]; + }, requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["transferBetweenAccounts"]; + return executeRestOperation(this.transport, operation, { + path: input.path, + body: input.body, + }, requestOptions); + } + + unstakeCryptoFunds(body: AccountServicesOperationTypes["unstakeCryptoFunds"]["body"], requestOptions?: RequestOptions): Promise; + unstakeCryptoFunds(body: AccountServicesOperationTypes["unstakeCryptoFunds"]["body"]): Promise; + unstakeCryptoFunds(body: AccountServicesOperationTypes["unstakeCryptoFunds"]["body"], requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["unstakeCryptoFunds"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + withdrawCryptoFunds(input: { + path: AccountServicesOperationTypes["withdrawCryptoFunds"]["path"]; + body: AccountServicesOperationTypes["withdrawCryptoFunds"]["body"]; + }, requestOptions?: RequestOptions): Promise; + withdrawCryptoFunds(input: { + path: AccountServicesOperationTypes["withdrawCryptoFunds"]["path"]; + body: AccountServicesOperationTypes["withdrawCryptoFunds"]["body"]; + }): Promise; + withdrawCryptoFunds(input: { + path: AccountServicesOperationTypes["withdrawCryptoFunds"]["path"]; + body: AccountServicesOperationTypes["withdrawCryptoFunds"]["body"]; + }, requestOptions?: RequestOptions): Promise { + const operation = ACCOUNT_SERVICES_OPERATIONS["withdrawCryptoFunds"]; + return executeRestOperation(this.transport, operation, { + path: input.path, + body: input.body, + }, requestOptions); + } +} diff --git a/packages/sdk-typescript/src/generated/clearing-instant/operations.ts b/packages/sdk-typescript/src/generated/clearing-instant/operations.ts new file mode 100644 index 0000000..cbc438d --- /dev/null +++ b/packages/sdk-typescript/src/generated/clearing-instant/operations.ts @@ -0,0 +1,118 @@ +// Generated from rest.yaml#Clearing & Instant. Do not edit. + +import type { operations as OpenApiOperations } from "../market-data/models.js"; + +type ParameterAt = + O extends { parameters: infer P } + ? Location extends keyof P ? P[Location] : never + : never; + +type Int64Input = + T extends bigint ? bigint | number : + T extends readonly (infer Item)[] ? Int64Input[] : + T extends object ? { [K in keyof T]: Int64Input } : T; + +type JsonBody = + NonNullable extends + { content: { "application/json": infer Body } } + ? Required extends true ? Body : Body | undefined + : never; + +type StripTransportFields = T extends object ? Omit : T; + +type CallerJsonBody = StripTransportFields; + +type JsonResponse = + O extends { responses: infer R } + ? Status extends keyof R + ? R[Status] extends { content: { "application/json": infer Body } } ? Body : never + : never + : never; + +export const CLEARING_INSTANT_OPERATIONS = { + "cancelClearingOrder": {"responseMode":"json","operation":"clearingInstant.cancelClearingOrder","method":"post","path":"/v1/clearing/cancel","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "confirmClearingOrder": {"responseMode":"json","operation":"clearingInstant.confirmClearingOrder","method":"post","path":"/v1/clearing/confirm","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "createNewBrokerOrder": {"responseMode":"json","operation":"clearingInstant.createNewBrokerOrder","method":"post","path":"/v1/clearing/broker/new","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "createNewClearingOrder": {"responseMode":"json","operation":"clearingInstant.createNewClearingOrder","method":"post","path":"/v1/clearing/new","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "executeInstantOrder": {"responseMode":"json","operation":"clearingInstant.executeInstantOrder","method":"post","path":"/v1/instant/execute","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "getClearingOrder": {"responseMode":"json","operation":"clearingInstant.getClearingOrder","method":"post","path":"/v1/clearing/status","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "getInstantQuote": {"responseMode":"json","operation":"clearingInstant.getInstantQuote","method":"post","path":"/v1/instant/quote","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "listClearingBrokers": {"responseMode":"json","operation":"clearingInstant.listClearingBrokers","method":"post","path":"/v1/clearing/broker/list","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["expiration_end"],"allowString":true},{"path":["expiration_start"],"allowString":true},{"path":["nonce"],"allowString":true},{"path":["submission_end"],"allowString":true},{"path":["submission_start"],"allowString":true},{"path":["timestamp"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "listClearingOrders": {"responseMode":"json","operation":"clearingInstant.listClearingOrders","method":"post","path":"/v1/clearing/list","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["expiration_end"],"allowString":true},{"path":["expiration_start"],"allowString":true},{"path":["nonce"],"allowString":true},{"path":["submission_end"],"allowString":true},{"path":["submission_start"],"allowString":true},{"path":["timestamp"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "listClearingTrades": {"responseMode":"json","operation":"clearingInstant.listClearingTrades","method":"post","path":"/v1/clearing/trades","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true},{"path":["timestamp"],"allowString":true}],"path":[],"query":[]},"retryable":false}, +} as const; + +export type ClearingInstantOperationId = keyof typeof CLEARING_INSTANT_OPERATIONS; + +export type ClearingInstantOperationTypes = { + "cancelClearingOrder": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "confirmClearingOrder": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "createNewBrokerOrder": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "createNewClearingOrder": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "executeInstantOrder": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getClearingOrder": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getInstantQuote": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "listClearingBrokers": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "listClearingOrders": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "listClearingTrades": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; +}; diff --git a/packages/sdk-typescript/src/generated/clearing-instant/rest.ts b/packages/sdk-typescript/src/generated/clearing-instant/rest.ts new file mode 100644 index 0000000..3ff886f --- /dev/null +++ b/packages/sdk-typescript/src/generated/clearing-instant/rest.ts @@ -0,0 +1,104 @@ +// Generated from rest.yaml#Clearing & Instant. Do not edit. + +import type { HttpTransport } from "../../core/http.js"; +import type { RequestOptions } from "../../core/deadline.js"; +import { executeRestOperation } from "../../core/rest-operation.js"; + +import { + CLEARING_INSTANT_OPERATIONS, + type ClearingInstantOperationTypes, +} from "./operations.js"; + +export class ClearingInstantRest { + constructor(private readonly transport: HttpTransport) {} + + cancelClearingOrder(body: ClearingInstantOperationTypes["cancelClearingOrder"]["body"], requestOptions?: RequestOptions): Promise; + cancelClearingOrder(body: ClearingInstantOperationTypes["cancelClearingOrder"]["body"]): Promise; + cancelClearingOrder(body: ClearingInstantOperationTypes["cancelClearingOrder"]["body"], requestOptions?: RequestOptions): Promise { + const operation = CLEARING_INSTANT_OPERATIONS["cancelClearingOrder"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + confirmClearingOrder(body: ClearingInstantOperationTypes["confirmClearingOrder"]["body"], requestOptions?: RequestOptions): Promise; + confirmClearingOrder(body: ClearingInstantOperationTypes["confirmClearingOrder"]["body"]): Promise; + confirmClearingOrder(body: ClearingInstantOperationTypes["confirmClearingOrder"]["body"], requestOptions?: RequestOptions): Promise { + const operation = CLEARING_INSTANT_OPERATIONS["confirmClearingOrder"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + createNewBrokerOrder(body: ClearingInstantOperationTypes["createNewBrokerOrder"]["body"], requestOptions?: RequestOptions): Promise; + createNewBrokerOrder(body: ClearingInstantOperationTypes["createNewBrokerOrder"]["body"]): Promise; + createNewBrokerOrder(body: ClearingInstantOperationTypes["createNewBrokerOrder"]["body"], requestOptions?: RequestOptions): Promise { + const operation = CLEARING_INSTANT_OPERATIONS["createNewBrokerOrder"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + createNewClearingOrder(body: ClearingInstantOperationTypes["createNewClearingOrder"]["body"], requestOptions?: RequestOptions): Promise; + createNewClearingOrder(body: ClearingInstantOperationTypes["createNewClearingOrder"]["body"]): Promise; + createNewClearingOrder(body: ClearingInstantOperationTypes["createNewClearingOrder"]["body"], requestOptions?: RequestOptions): Promise { + const operation = CLEARING_INSTANT_OPERATIONS["createNewClearingOrder"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + executeInstantOrder(body: ClearingInstantOperationTypes["executeInstantOrder"]["body"], requestOptions?: RequestOptions): Promise; + executeInstantOrder(body: ClearingInstantOperationTypes["executeInstantOrder"]["body"]): Promise; + executeInstantOrder(body: ClearingInstantOperationTypes["executeInstantOrder"]["body"], requestOptions?: RequestOptions): Promise { + const operation = CLEARING_INSTANT_OPERATIONS["executeInstantOrder"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + getClearingOrder(body: ClearingInstantOperationTypes["getClearingOrder"]["body"], requestOptions?: RequestOptions): Promise; + getClearingOrder(body: ClearingInstantOperationTypes["getClearingOrder"]["body"]): Promise; + getClearingOrder(body: ClearingInstantOperationTypes["getClearingOrder"]["body"], requestOptions?: RequestOptions): Promise { + const operation = CLEARING_INSTANT_OPERATIONS["getClearingOrder"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + getInstantQuote(body: ClearingInstantOperationTypes["getInstantQuote"]["body"], requestOptions?: RequestOptions): Promise; + getInstantQuote(body: ClearingInstantOperationTypes["getInstantQuote"]["body"]): Promise; + getInstantQuote(body: ClearingInstantOperationTypes["getInstantQuote"]["body"], requestOptions?: RequestOptions): Promise { + const operation = CLEARING_INSTANT_OPERATIONS["getInstantQuote"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + listClearingBrokers(body: ClearingInstantOperationTypes["listClearingBrokers"]["body"], requestOptions?: RequestOptions): Promise; + listClearingBrokers(body: ClearingInstantOperationTypes["listClearingBrokers"]["body"]): Promise; + listClearingBrokers(body: ClearingInstantOperationTypes["listClearingBrokers"]["body"], requestOptions?: RequestOptions): Promise { + const operation = CLEARING_INSTANT_OPERATIONS["listClearingBrokers"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + listClearingOrders(body: ClearingInstantOperationTypes["listClearingOrders"]["body"], requestOptions?: RequestOptions): Promise; + listClearingOrders(body: ClearingInstantOperationTypes["listClearingOrders"]["body"]): Promise; + listClearingOrders(body: ClearingInstantOperationTypes["listClearingOrders"]["body"], requestOptions?: RequestOptions): Promise { + const operation = CLEARING_INSTANT_OPERATIONS["listClearingOrders"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + listClearingTrades(body: ClearingInstantOperationTypes["listClearingTrades"]["body"], requestOptions?: RequestOptions): Promise; + listClearingTrades(body: ClearingInstantOperationTypes["listClearingTrades"]["body"]): Promise; + listClearingTrades(body: ClearingInstantOperationTypes["listClearingTrades"]["body"], requestOptions?: RequestOptions): Promise { + const operation = CLEARING_INSTANT_OPERATIONS["listClearingTrades"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } +} diff --git a/packages/sdk-typescript/src/generated/margin/operations.ts b/packages/sdk-typescript/src/generated/margin/operations.ts new file mode 100644 index 0000000..36512a2 --- /dev/null +++ b/packages/sdk-typescript/src/generated/margin/operations.ts @@ -0,0 +1,62 @@ +// Generated from rest.yaml#Margin. Do not edit. + +import type { operations as OpenApiOperations } from "../market-data/models.js"; + +type ParameterAt = + O extends { parameters: infer P } + ? Location extends keyof P ? P[Location] : never + : never; + +type Int64Input = + T extends bigint ? bigint | number : + T extends readonly (infer Item)[] ? Int64Input[] : + T extends object ? { [K in keyof T]: Int64Input } : T; + +type JsonBody = + NonNullable extends + { content: { "application/json": infer Body } } + ? Required extends true ? Body : Body | undefined + : never; + +type StripTransportFields = T extends object ? Omit : T; + +type CallerJsonBody = StripTransportFields; + +type JsonResponse = + O extends { responses: infer R } + ? Status extends keyof R + ? R[Status] extends { content: { "application/json": infer Body } } ? Body : never + : never + : never; + +export const MARGIN_OPERATIONS = { + "getMarginAccount": {"responseMode":"json","operation":"margin.getMarginAccount","method":"post","path":"/v1/margin/account","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "getMarginRates": {"responseMode":"json","operation":"margin.getMarginRates","method":"post","path":"/v1/margin/rates","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["rates","*","lastUpdated"]],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "previewMarginOrder": {"responseMode":"json","operation":"margin.previewMarginOrder","method":"post","path":"/v1/margin/order/preview","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, +} as const; + +export type MarginOperationId = keyof typeof MARGIN_OPERATIONS; + +export type MarginOperationTypes = { + "getMarginAccount": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getMarginRates": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "previewMarginOrder": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; +}; diff --git a/packages/sdk-typescript/src/generated/margin/rest.ts b/packages/sdk-typescript/src/generated/margin/rest.ts new file mode 100644 index 0000000..9c4242a --- /dev/null +++ b/packages/sdk-typescript/src/generated/margin/rest.ts @@ -0,0 +1,41 @@ +// Generated from rest.yaml#Margin. Do not edit. + +import type { HttpTransport } from "../../core/http.js"; +import type { RequestOptions } from "../../core/deadline.js"; +import { executeRestOperation } from "../../core/rest-operation.js"; + +import { + MARGIN_OPERATIONS, + type MarginOperationTypes, +} from "./operations.js"; + +export class MarginRest { + constructor(private readonly transport: HttpTransport) {} + + getMarginAccount(body: MarginOperationTypes["getMarginAccount"]["body"], requestOptions?: RequestOptions): Promise; + getMarginAccount(body: MarginOperationTypes["getMarginAccount"]["body"]): Promise; + getMarginAccount(body: MarginOperationTypes["getMarginAccount"]["body"], requestOptions?: RequestOptions): Promise { + const operation = MARGIN_OPERATIONS["getMarginAccount"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + getMarginRates(body: MarginOperationTypes["getMarginRates"]["body"], requestOptions?: RequestOptions): Promise; + getMarginRates(body: MarginOperationTypes["getMarginRates"]["body"]): Promise; + getMarginRates(body: MarginOperationTypes["getMarginRates"]["body"], requestOptions?: RequestOptions): Promise { + const operation = MARGIN_OPERATIONS["getMarginRates"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + previewMarginOrder(body: MarginOperationTypes["previewMarginOrder"]["body"], requestOptions?: RequestOptions): Promise; + previewMarginOrder(body: MarginOperationTypes["previewMarginOrder"]["body"]): Promise; + previewMarginOrder(body: MarginOperationTypes["previewMarginOrder"]["body"], requestOptions?: RequestOptions): Promise { + const operation = MARGIN_OPERATIONS["previewMarginOrder"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } +} diff --git a/packages/sdk-typescript/src/generated/market-data/models.ts b/packages/sdk-typescript/src/generated/market-data/models.ts new file mode 100644 index 0000000..45837c9 --- /dev/null +++ b/packages/sdk-typescript/src/generated/market-data/models.ts @@ -0,0 +1,9438 @@ +// Generated from rest.yaml#Market Data. Do not edit. + +export interface paths { + "/v1/symbols": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Symbols + * @description This endpoint retrieves all available symbols for trading. + */ + get: operations["listSymbols"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/symbols/details/{symbol}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Symbol Details + * @description This endpoint retrieves extra detail on supported symbols, such as minimum order size, tick size, quote increment and more. + */ + get: operations["getSymbolDetails"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/networks/{network}/assets": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Assets for Network + * @description This endpoint retrieves the enabled assets (tokens) available on a specified blockchain network, filtered by your account's access permissions. + * + * This authenticated endpoint returns only the assets where your account has deposit and withdraw access enabled on the specified network. + * + * Use this endpoint to discover all tokens that support deposits and withdrawals on a particular blockchain network. + * + * The `assets` field in the response is always an array, sorted alphabetically, containing one or more enabled asset codes. + * + * ### Roles + * The API key you use to access this endpoint must have the Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + */ + get: operations["getAssetsForNetwork"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/network/{token}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Network + * @description + * + * This endpoint retrieves the associated network(s) for a requested token, filtered by your account's access permissions. + * + * This authenticated endpoint returns only the networks where your account has both deposit and withdraw access enabled. This supports the multinetwork deposit and withdrawal flow. + * + * Many tokens are available on multiple blockchain networks. For example, USDC is available on Optimism, Solana, Base, Arbitrum, Avalanche, and Ethereum. Use this endpoint to discover which networks your account can deposit to and withdraw from for a given token. + * + * The `network` field in the response is always an array, which may contain one or more supported networks. + * + * ### Roles + * The API key you use to access this endpoint must have the Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + */ + get: operations["getTokenNetworkV2"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/pubticker/{symbol}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Ticker + * @description This endpoint retrieves information about recent trading activity for the symbol. + * + * + */ + get: operations["getTicker"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/feepromos": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Fee Promos + * @description This endpoint retrieves symbols that currently have fee promos. + */ + get: operations["listFeePromos"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/book/{symbol}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Current Order Book + * @description This will return the current order book as two arrays (bids / asks). + * + * + */ + get: operations["getCurrentOrderBook"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/trades/{symbol}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Trades + * @description + * + * This will return the trades that have executed since the specified timestamp. Timestamps are either seconds or milliseconds since the epoch (1970-01-01). See the [Data Types](/data-types) section about `timestamp` for information on this. + * + * Each request will show at most 500 records. + * + * If no `since` or `timestamp` is specified, then it will show the most recent trades; otherwise, it will show the most recent trades that occurred after that timestamp. + */ + get: operations["listTrades"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/pricefeed": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List Prices */ + get: operations["listPrices"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/fundingamount/{symbol}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Funding Amount */ + get: operations["getFundingAmount"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/fundingamountreport/records.xlsx": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Funding Amount Report File + * @description ### Examples + * - `symbol=BTCGUSDPERP&fromDate=2024-04-10&toDate=2024-04-25&numRows=1000`
+ * Compare and obtain the minimum records between (2024-04-10 to 2024-04-25) and 1000. If (2024-04-10 to 2024-04-25) contains 360 records. Then fetch the minimum between 360 and 1000 records only. + * + * - `symbol=BTCGUSDPERP&numRows=2024-04-10&toDate=2024-04-25`
+ * If (2024-04-10 to 2024-04-25) contains 360 records. Then fetch 360 records only. + * + * - `symbol=BTCGUSDPERP&numRows=1000`
+ * Fetch maximum 1000 records starting from Now to a historical date + * + * - `symbol=BTCGUSDPERP`
+ * Fetch maximum 8760 records starting from Now to a historical date + */ + get: operations["getFundingAmountReportFile"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/order/new": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create New Order + * @description If you wish orders to be automatically cancelled when your session ends, see the [require heartbeat](/authentication/api-key#require-heartbeat) section, or manually send the [cancel all session orders](/rest/orders#cancel-all-session-orders) message. + * + * + * + * ### Roles + * The API key you use to access this endpoint must have the Trader role assigned. See Roles for more information. + * + * The OAuth scope must have `orders:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + * + * + * ### Margin Orders + * Set `margin_order: true` to place an order using borrowed funds on a margin-enabled account. This allows you to trade with leverage beyond your available balance. + * + * **Important**: Margin trading amplifies both gains and losses. Monitor your account using the [Margin Account Summary](/rest/margin-trading#get-margin-account-summary) endpoint and preview order impacts with [Order Preview](/rest/margin-trading#preview-margin-order-impact) before placing margin orders. + * + * ### Stop-Limit Orders + * A Stop-Limit order is an order type that allows for order placement when a price reaches a specified level. Stop-Limit orders take in both a `price` and and a `stop_price` as parameters. The `stop_price` is the price that triggers the order to be placed on the continous live order book at the `price`. For buy orders, the `stop_price` must be below the `price` while sell orders require the `stop_price` to be greater than the `price`. + * + * + * ### What about market orders? + * The API doesn't directly support market orders because they provide you with no price protection. + * + * Instead, use the “immediate-or-cancel” order execution option, coupled with an aggressive limit price (i.e. very high for a buy order or very low for a sell order), to achieve the same result. + * + * ### Order execution options + * Note that `options` is an array. If you omit `options` or provide an empty array, your order will be a standard limit order - it will immediately fill against any open orders at an equal or better price, then the remainder of the order will be posted to the order book. + * + * If you specify more than one option (or an unsupported option) in the `options` array, the exchange will reject your order. + * + * No `options` can be applied to stop-limit orders at this time. + * + * The available limit order options are: + * + * | Option | Description | + * |--------|-------------| + * | `"maker-or-cancel"` | This order will only add liquidity to the order book.

If any part of the order could be filled immediately, the whole order will instead be canceled before any execution occurs.

If that happens, the response back from the API will indicate that the order has already been canceled (`"is_cancelled": true` in JSON).

*Note: some other exchanges call this option "post-only".* | + * | `"immediate-or-cancel"` | This order will only remove liquidity from the order book.

It will fill whatever part of the order it can immediately, then cancel any remaining amount so that no part of the order is added to the order book.

If the order doesn't fully fill immediately, the response back from the API will indicate that the order has already been canceled (`"is_cancelled": true` in JSON). | + * | `"fill-or-kill"` | This order will only remove liquidity from the order book.

It will fill the entire order immediately or cancel.

If the order doesn't fully fill immediately, the response back from the API will indicate that the order has already been canceled (`"is_cancelled": true` in JSON). | + */ + post: operations["createNewOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/order/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Cancel Order + * @description This will cancel an order. If the order is already canceled, the message will succeed but have no effect. + * + * + * + * ### Roles + * The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles) for more information. + * + * The OAuth scope must have `orders:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + * + * ### All Cancellation Reasons + * Under unique circumstances, orders may be automatically cancelled by the exchange. These scenarios are detailed in the table below: + * + * | Cancel Reason | Description | + * |---------------|-------------| + * | `MakerOrCancelWouldTake` | Occurs when the "maker-or-cancel" execution option is included in the order request and any part of the requested order could be filled immediately. | + * | `ExceedsPriceLimits` | Occurs when there is not sufficient liquidity on the order book to support the entered trade. Orders will be automatically cancelled when liquidity conditions are such that the order would move price +/- 5%. | + * | `SelfCrossPrevented` | Occurs when a user enters a bid that is higher than that user's lowest open ask or enters an ask that is lower than their highest open bid on the same pair. | + * | `ImmediateOrCancelWouldPost` | Occurs when the "immediate-or-cancel" execution option is included in the order request and the requested order cannot be fully filled immediately. This type of cancellation will only cancel the unfulfilled part of any impacted order. | + * | `FillOrKillWouldNotFill` | Occurs when the "fill-or-kill" execution option is included in the new order request and the entire order cannot be filled immediately.

Unlike "immediate-or-cancel" orders, this execution option will result in the entire order being cancelled rather than just the unfulfilled portion. | + * | `Requested` | Cancelled via user request to /v1/order/cancel endpoint. | + * | `MarketClosed` | Occurs when an order is placed for a trading pair that is currently closed. | + * | `TradingClosed` | Occurs when an order is placed while the exchange is closed for trading. | + */ + post: operations["cancelOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/order/cancel/all": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Cancel All Active Orders + * @description This will cancel all outstanding orders created by all [sessions](/authentication/api-key#sessions) owned by this account, including interactive orders placed through the UI. + * + * + * + * Typically [Cancel All Session Orders](/rest/orders#cancel-all-session-orders) is preferable, so that only orders related to the current connected session are cancelled. + * + * ### Roles + * The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `orders:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["cancelAllActiveOrders"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/order/cancel/session": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Cancel All Session Orders + * @description This will cancel all orders opened by this [session](/authentication/api-key#sessions). + * + * This will have the same effect as [heartbeat](/authentication/api-key#require-heartbeat) expiration if "Require Heartbeat" is selected for the session. + * + * ### Roles + * The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `orders:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["cancelAllSessionOrders"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/order/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Order Status + * @description + * + * ### Roles + * The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `orders:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["getOrderStatus"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/orders": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List Active Orders + * @description + * + * ### Roles + * The API key you use to access this endpoint must have the Trader or Auditor role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `orders:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["listActiveOrders"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/orders/history": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List Past Orders + * @description This API retrieves (closed) orders history for an account. + * + * ### Roles + * The API key you use to access this endpoint must have the Trader or Auditor role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `history:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + * + * + * ### How to retrieve your order history + * + * To retrieve your full order history walking backwards, + * + * 1. Initial request: `POST` to https://api.gemini.com/v1/orders/history with a JSON payload including a `timestamp` key with value `0` and a `limit_orders` key with value `500` + * 2. When you receive the list of orders, it will be sorted by `timestamp` descending - so the first element in the list will have the highest `timestamp` value. For this example, say that value is `X`. + * 3. Create a second `POST` request with a JSON payload including a `timestamp` key with value `X+1` and a `limit_orders` key with value `500`. + * 4. Take the first element of the list returned with highest `timestamp` value `Y` and create a third `POST` request with a JSON payload including a `timestamp` key with value `Y+1` and a `limit_orders` key with value `500`. + * 5. Continue creating `POST` requests and retrieving orders until an empty list is returned. + * + * ### Break Types + * + * In the rare event that a trade has been reversed (broken), the trade that is broken will have this flag set. The field will contain one of these values + * + * |Value|Description| + * |--- |--- | + * |manual|The trade was reversed manually. This means that all fees, proceeds, and debits associated with the trade have been credited or debited to the account seperately. That means that this reported trade must be included for order for the account balance to be correct.| + * |full|The trade was fully broken. The reported trade should not be accounted for. It will be as though the transfer of fund associated with the trade had simply not happened.| + */ + post: operations["listPastOrders"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/mytrades": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List Past Trades + * @description + * + * ### Roles + * The API key you use to access this endpoint must have the Trader or Auditor role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `history:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + * + * ### How to retrieve your trade history + * + * To retrieve your full trade history walking backwards, + * + * 1. Initial request: `POST` to https://api.gemini.com/v1/mytrades with a JSON payload including a `timestamp` key with value 0 and a `limit_trades` key with value `500` + * 2. When you receive the list of trades, it will be sorted by `timestamp` descending - so the first element in the list will have the highest `timestamp` value. For this example, say that value is `X`. + * 3. Create a second `POST` request with a JSON payload including a `timestamp` key with value `X+1` and a `limit_trades` key with value `500`. + * 4. Take the first element of the list returned with highest `timestamp` value `Y` and create a third `POST` request with a JSON payload including a `timestamp` key with value `Y+1` and a `limit_trades` key with value `500`. + * 5. Continue creating `POST` requests and retrieving trades until an empty list is returned. + * + * ### Break Types + * + * In the rare event that a trade has been reversed (broken), the trade that is broken will have this flag set. The field will contain one of these values + * + * |Value|Description| + * |--- |--- | + * |manual|The trade was reversed manually. This means that all fees, proceeds, and debits associated with the trade have been credited or debited to the account seperately. That means that this reported trade must be included for order for the account balance to be correct.| + * |full|The trade was fully broken. The reported trade should not be accounted for. It will be as though the transfer of fund associated with the trade had simply not happened.| + */ + post: operations["listPastTrades"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/tradevolume": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Trading Volume + * @description ### Roles + * The API key you use to access this endpoint must have the Trader or Auditor role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `history:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["getTradingVolume"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/balances": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Available Balances + * @description + * + * This will show the available balances in the supported currencies + * + * + * + * ### Roles + * The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `balances:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["getAvailableBalances"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/notionalvolume": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Notional Trading Volume + * @description ### Roles + * The API key you use to access this endpoint must have the Trader or Auditor role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `history:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["getNotionalTradingVolume"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/margin/account": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Margin Account Summary + * @description Retrieves comprehensive margin account information including collateral, leverage, buying/selling power, and liquidation risk. + * + * This endpoint provides real-time margin statistics for spot margin trading accounts, helping you monitor your account health and manage risk. + * + * ### Roles + * The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `balances:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + * + * ### Account Type + * This endpoint is only available for margin trading accounts. Standard exchange accounts will receive an error. + */ + post: operations["getMarginAccount"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/margin/rates": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Margin Interest Rates + * @description Retrieves current margin interest rates for all borrowable assets. + * + * Returns hourly, daily, and annual borrow rates for each currency that can be borrowed on margin. Interest is charged on borrowed amounts at the hourly rate and compounds over time. + * + * ### Roles + * The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `balances:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + * + * ### Account Type + * This endpoint is only available for margin trading accounts. + */ + post: operations["getMarginRates"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/margin/order/preview": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Preview Margin Order Impact + * @description Previews the margin impact of a hypothetical spot order without actually placing it. + * + * Returns both pre-order and post-order margin risk statistics, allowing you to understand how an order would affect your margin account before execution. This is useful for risk management and planning trades. + * + * ### Roles + * The API key you use to access this endpoint must have the Trader or Auditor role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `orders:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + * + * ### Account Type + * This endpoint is only available for margin trading accounts. + */ + post: operations["previewMarginOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/heartbeat": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Heartbeat + * @description This will prevent a [session](/authentication/api-key#private-api-invocation) from timing out and canceling orders if the [require heartbeat](/authentication/api-key#require-heartbeat) flag has been set. Note that this is only required if no other private API requests have been made. The arrival of any message resets the heartbeat timer. + */ + post: operations["sendHeartbeat"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/wrap/{symbol}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Wrap Order + * @description ### Roles + * The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `orders:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["wrapOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/notionalbalances/{currency}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Notional Balances + * @description + * + * This will show the available balances in the supported currencies as well as the notional value in the currency specified. + * + * + * + * ### Roles + * The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `balances:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["getNotionalBalances"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/addresses/{network}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List Deposit Addresses + * @description + * + * ### Roles + * The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `addresses:read` or `addresses:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["listDepositAddresses"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/deposit/{network}/newAddress": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create New Deposit Address + * @description + * + * ### Roles + * The API key you use to access this endpoint must have the Fund Manager role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `addresses:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["createNewDepositAddress"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/transfers": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List Past Transfers + * @description + * + * This endpoint shows deposits and withdrawals in supported currencies with full multichain (multi-network) support. It returns accurate status information for transfers on **all supported networks** including Solana, Arbitrum, Optimism, Base, Avalanche, and Ethereum. + * + * Each transfer in the response includes a `network` field identifying the blockchain network, along with network-specific `feeAmount`, `feeCurrency`, and `txHash` values. + * + * This endpoint does not currently show cancelled advances, returned outgoing wires or ACH transactions, or other exceptional transaction circumstances. + * + * Fiat transfers between non-derivative and derivatives accounts are prohibited. + * + * ### Roles + * The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `history:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["listPastTransfers"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/custodyaccountfees": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List Custody Fee Transfers + * @description + * + * This endpoint shows Custody fee records in the supported currencies. + * + * ### Roles + * The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `history:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["listCustodyFeeTransfers"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/withdraw/{network}/{ticker}/feeEstimate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Gas Fee Estimation + * @description + * + * API users will not be aware of the transfer fees before starting the withdrawal process. This endpoint allows you to find out the estimated gas fees before you start a withdrawal. It requires specifying the blockchain network and ticker, which is useful for tokens that exist on multiple networks (e.g. USDC on Ethereum vs Solana). + * + * ### Roles + * The API key you use to access this endpoint can have the Trader, Fund Manager, Auditor, WealthManager or Administrator role assigned. See [Roles](#roles) for more information. + */ + post: operations["getGasFeeEstimation"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/withdraw/{network}/{ticker}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Withdraw Crypto Funds + * @description + * + * Withdraw cryptocurrency funds to an approved address, with explicit network selection. + * + * The key improvement over v1 is the explicit `network` path parameter, which allows you to specify exactly which blockchain network to use for the withdrawal. This is especially important for tokens available on multiple networks (e.g., USDC on Ethereum, Solana, Base, Arbitrum, etc.). + * + * Before you can withdraw cryptocurrency funds to an approved address, you need three things: + * + * 1. You must have an approved address list for your account + * 2. The address you want to withdraw funds to needs to already be on that approved address list + * 3. An API key with the Fund Manager role added + * + * If you would like to withdraw via API to addresses that are not on your approved address list, please reach out to trading@gemini.com. We can enable this feature for you provided a set of approved IP addresses. This functionality is only available for exchange accounts. Pre-approved IP addresses and addresses added to your approved address list are required to enable withdrawal APIs for custody accounts. + * + * Use the [Get Network](/rest/market-data#get-network) endpoint to discover which networks support withdrawals for a given token. + * + * See [Roles](/roles#roles) for more information on how to add the Fund Manager role to the API key you want to use. + * + * ### Roles + * The API key you use to access this endpoint must have the Fund Manager role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `crypto:send` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["withdrawCryptoFunds"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/clearing/new": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create New Clearing Order + * @description ### Roles + * The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `clearing:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["createNewClearingOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/clearing/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Clearing Order + * @description ### Roles + * The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `clearing:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["getClearingOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/clearing/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Cancel Clearing Order + * @description ### Roles + * The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `clearing:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["cancelClearingOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/clearing/confirm": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Confirm Clearing Order + * @description ### Roles + * The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `clearing:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["confirmClearingOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/clearing/list": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List Clearing Orders + * @description ### Roles + * The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `clearing:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["listClearingOrders"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/clearing/broker/list": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List Clearing Brokers + * @description ### Roles + * The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `clearing:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["listClearingBrokers"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/clearing/broker/new": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create New Broker Order + * @description Gemini Clearing also allows for brokers to facilitate trades between two Gemini customers. A broker can submit a new Gemini Clearing order that must then be confirmed by each counterparty before settlement. + */ + post: operations["createNewBrokerOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/clearing/trades": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List Clearing Trades + * @description ### Roles + * The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `clearing:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["listClearingTrades"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/instant/quote": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Instant Quote + * @description ### Roles + * The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `orders:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["getInstantQuote"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/instant/execute": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Execute Instant Order + * @description ### Roles + * The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `orders:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["executeInstantOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/payments/addbank": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Add Bank + * @description The add bank API allows for banking information to be sent in via API. However, for the bank to be verified, you must still send in a wire for any amount from the bank account. + * + * ### Roles + * This API requires the FundManager role. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `banks:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["addBank"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/payments/addbank/cad": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Add Bank CAD + * @description The add bank API allows for CAD banking information to be sent in via API. However, for the bank to be verified, you must still send in a wire for any amount from the bank account. + * + * ### Roles + * This API requires the FundManager role. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `banks:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["addBankCAD"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/payments/methods": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List Payment Methods + * @description The payments methods API will return data on balances in the account and linked banks. + * + * ### Roles + * The API key you use to access this endpoint can be either a Master or Account level key with any role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `banks:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["listPaymentMethods"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/account": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Account Detail + * @description The account API will return detail about the specific account requested such as users, country codes, etc. + * + * ### Roles + * The API key you use to access this endpoint can be either a Master or Account level key with any role assigned. See [Roles](/roles#roles) for more information. + */ + post: operations["getAccountDetail"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/approvedAddresses/account/{network}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List Approved Addresses + * @description Allows viewing of Approved Address list. + * + * ### Roles + * This API can accept any role. See [Roles](/roles#roles) for more information. + */ + post: operations["listApprovedAddresses"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/approvedAddresses/{network}/request": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create New Approved Address + * @description Allows for creation of an approved withdrawal address. Once the request is made, the 7 day waiting period will begin. Please note that all approved address requests are subject to the 7 day waiting period. + * + * If you add an address using an account-scoped API key, then the address will be added to your account specific approved address list. If you use a master-scoped API key, the address will be added to your group-level approved address list unless you specify an account. + * + * This endpoint is subject to additional security constraints and is only accessible via API keys which have configured Trusted IP controls. + * + * Please reach out to trading@gemini.com if you have any questions about approved addresses. + * + * ### Roles + * This API requires the FundManager role. See [Roles](/roles#roles) for more information. + */ + post: operations["createNewApprovedAddress"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/approvedAddresses/{network}/remove": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Remove Approved Address + * @description Allows for removal of active or time-pending addresses from the Approved Address list. Addresses that are pending approval from another user on the account cannot be removed via API. + * + * ### Roles + * This API requires the FundManager role. See [Roles](/roles#roles) for more information. + */ + post: operations["removeApprovedAddress"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/account/create": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create New Account + * @description A Master API key can create a new exchange account within the group. This API will return the name of your new account for use with the account parameter in when using Master API keys to perform account level functions. Please see the [example](/account-admin-endpoints#using-master-api-keys). + * + * ### Roles + * The API key you use to access this endpoint must be a Master level key and have the Administrator role assigned. See [Roles](/roles#roles) for more information. + */ + post: operations["createNewAccount"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/account/rename": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Rename Account + * @description A Master or Account level API key can rename an account within the group. + * + * ### Roles + * The API key you use to access this endpoint can be either a Master or Account level API key and must have the Administrator role assigned. See [Roles](/roles#roles) for more information. + */ + post: operations["renameAccount"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/account/list": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List Accounts in Group + * @description A Master API key can be used to get the accounts within the group. A maximum of 500 accounts can be listed in a single API call. + * + * ### Roles + * The API key you use to access this endpoint must be a Master level key. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `account:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["listAccountsInGroup"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/account/transfer/{currency}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Transfer Between Accounts + * @description This API allows you to execute an internal transfer between any two accounts within your Master Group. In the scenario of exchange account to exchange account there will be no activity on a blockchain network. All other combinations will result in a movement of funds on a blockchain network. + * + * Gemini Custody account withdrawals will not occur until the daily custody run occurs. In the case of funds moving from a Gemini Custody account to a Gemini Exchange account, the exchange account will get a precredit for the amount to be received. The exchange account will be able to trade these funds but will be unable to withdraw until the funds are processed on the blockchain and received. + * + * Gemini Custody accounts request withdrawals to approved addresses in all cases and require the request to come from an approved IP address. Please reach out to trading@gemini.com to enable API withdrawals for custody accounts. + * + * Gemini Custody accounts do not support fiat currency transfers. + * + * Fiat transfers between non-derivative and derivatives accounts are prohibited. + * + * ### Roles + * The API key you use to access this endpoint must be a Master level key and have the Fund Manager role assigned. See [Roles](/roles#roles) for more information. + */ + post: operations["transferBetweenAccounts"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transactions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Transaction History + * @description + * + * This endpoint shows trade detail and transactions. There is a `continuation_token` that is a pagination token used for subsequent requests. + * + * ### Roles + * The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned and have the master account scope. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `history:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["getTransactionHistory"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/oauth/revokeByToken": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Revoke OAuth Token + * @description The `access_token` may be revoked at any time by using `v1/oauth/revokeByToken`. Once a token is revoked or expires, it can no longer be used to make requests. + * + * This endpoint is only available using an `access_token` and will revoke the token used to make the request. + */ + post: operations["revokeOAuthToken"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/balances/staking": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List Staking Balances + * @description This will show the available balance in Staking as well as the available balance for withdrawal. + * + * ### Roles + * The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + */ + post: operations["listStakingBalances"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/staking/stake": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Stake Crypto Funds + * @description Initiates Staking deposits. + * + * ### Roles + * The API key you use to access this endpoint must have the Trader, Fund Manager or Trader role assigned. See [Roles](/roles#roles) for more information. + */ + post: operations["stakeCryptoFunds"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/staking/history": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List Staking Event History + * @description This will show all staking deposits, redemptions and interest accruals. + * + * ### Roles + * The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + * + * ### How to iterate through all transactions: + * To retrieve your full Staking history walking backwards, + * + * 1. Initial request: `POST` to https://api.gemini.com/v1/staking/history with a JSON payload including `sortAsc` set to `false` and a limit key with value `500`. + * 2. When you receive the list of Staking transactions, they will be sorted by `datetime` descending - so the last element in the list will have the lowest `timestamp` value. For this example, say that value is `X`. + * 3. Create a second `POST` request with a JSON payload including a `until` timestamp key with value `X-1`, `sortAsc` set to `false`, and a limit key with value `500`. + * 4. Take the last element of the list returned with lowest `datetime` value `Y` and create a third `POST` request with a JSON payload including a `until` timestamp key with value `Y-1`, `sortAsc` set to false, and a `limit` key with value `500`. + * 5. Continue creating `POST` requests and retrieving Staking transactions until an empty list is returned. + */ + post: operations["listStakingEventHistory"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/staking/rates": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Staking Rates + * @description This will return the current Gemini Staking interest rates (in bps). When including the specific asset(s) in the request, the response will include the specific assets' (e.g. `eth`, `matic`) Staking rate. When not including the specific asset in the request, the response will include all Staking rates. + */ + get: operations["listStakingRates"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/staking/rewards": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List Staking Rewards + * @description This will show the historical Staking reward payments and accrual. + * + * ### Roles + * The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + */ + post: operations["listStakingRewards"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/staking/unstake": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Unstake Crypto Funds + * @description Initiates Staking withdrawals. + * + * ### Roles + * The API key you use to access this endpoint must have the Trader, Fund Manager or Trader role assigned. See [Roles](/roles#roles) for more information. + */ + post: operations["unstakeCryptoFunds"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/roles": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Roles Endpoint + * @description The `v1/roles` endpoint will return a string of the role of the current API key. The response fields will be different for account-level and master-level API keys. + */ + post: operations["getRoles"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/margin": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Account Margin + * @description ### Roles + * The API key you use to access this endpoint must have the Trader or Auditor role assigned. See Roles for more information. + * + * The OAuth scope must have `orders:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["getAccountMargin"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/perpetuals/fundingPayment": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List Funding Payments + * @description + * + * ### Roles + * The API key you use to access this endpoint must have the Trader or Auditor role assigned. See Roles for more information. + * + * The OAuth scope must have `orders:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["listFundingPayments"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/perpetuals/fundingpaymentreport/records.xlsx": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Funding Payment Report File + * @description ### Roles + * The API key you use to access this endpoint must have the Trader or Auditor role assigned. See Roles for more information. + * + * The OAuth scope must have `orders:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + * + * ### Examples + * - `&fromDate=2024-04-10&toDate=2024-04-25&numRows=1000`
+ * Compare and obtain the minimum records between (2024-04-10 to 2024-04-25) and 1000. If (2024-04-10 to 2024-04-25) contains 360 records. Then fetch the minimum between 360 and 1000 records only. + * + * - `&numRows=2024-04-10&toDate=2024-04-25`
+ * If (2024-04-10 to 2024-04-25) contains 360 records. Then fetch 360 records only. + * + * - `&numRows=1000`
+ * Fetch maximum 1000 records starting from Now to a historical date + * + * - ``
+ * Fetch maximum 8760 records starting from Now to a historical date + */ + get: operations["getFundingPaymentReportFile"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/perpetuals/fundingpaymentreport/records.json": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Funding Payment Report JSON + * @description This endpoint retrieves funding payment report in JSON format. + * + * ### Examples + * - `&fromDate=2024-04-10&toDate=2024-04-25&numRows=1000`
+ * Compare and obtain the minimum records between (2024-04-10 to 2024-04-25) and 1000. If (2024-04-10 to 2024-04-25) contains 360 records. Then fetch the minimum between 360 and 1000 records only. + * + * - `&numRows=2024-04-10&toDate=2024-04-25`
+ * If (2024-04-10 to 2024-04-25) contains 360 records. Then fetch 360 records only. + * + * - `&numRows=1000`
+ * Fetch maximum 1000 records starting from Now to a historical date + * + * - ``
+ * Fetch maximum 8760 records starting from Now to a historical date + */ + post: operations["getFundingPaymentReportJson"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/positions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Open Positions + * @description ### Roles + * The API key you use to access this endpoint must have the Trader or Auditor role assigned. See [Roles](/roles#roles) for more information. + * + * The OAuth scope must have `orders:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + */ + post: operations["getOpenPositions"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/riskstats/{symbol}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Risk Stats */ + get: operations["getRiskStats"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/ticker/{symbol}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Ticker V2 + * @description This endpoint retrieves information about recent trading activity for the provided symbol. + */ + get: operations["getTickerV2"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/candles/{symbol}/{time_frame}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Candles + * @description This endpoint retrieves time-intervaled data for the provided symbol. + */ + get: operations["listCandles"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/derivatives/candles/{symbol}/{time_frame}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Derivative Candles + * @description This endpoint retrieves time-intervaled data for the provided perpetual symbol. + */ + get: operations["listDerivativeCandles"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/fxrate/{symbol}/{timestamp}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * FX Rate + * @description We have a growing international institutional customer base. When pulling market data for charting, it can be useful to have access to our FX rate for the relevant currency at that time. + * + * Please note, Gemini does not offer foreign exchange services. This endpoint is for historical reference only and does not provide any guarantee of future exchange rates. + * + * **Roles** + * The API key you use to access this endpoint must have the Auditor role assigned. See Roles for more information. + * + * **Supported Pairs** + * + * ` + * [AUDUSD, CADUSD, COPUSD, EURUSD, CHFUSD, HKDUSD, NZDUSD, GBPUSD, BRLUSD, INRUSD, SGDUSD, KRWUSD, JPYUSD, CNYUSD] + * ` + */ + get: operations["getFXRate"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** @description timestamp */ + TimestampType: string | bigint; + /** @description The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + Nonce: components["schemas"]["TimestampType"] | number; + ErrorResponse: { + /** @description Error */ + result?: string; + /** @description A short description */ + reason?: string; + /** @description Detailed error message */ + message?: string; + }; + SymbolDetails: { + /** + * @description The requested symbol. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols) + * @example BTCUSD + */ + symbol?: string; + /** + * @description CCY1 or the top currency. (i.e `BTC` in `BTCUSD`) + * @example BTC + */ + base_currency?: string; + /** + * @description CCY2 or the quote currency. (i.e `USD` in `BTCUSD`) + * @example USD + */ + quote_currency?: string; + /** + * Format: decimal + * @description The number of decimal places in the `base_currency`. (i.e `1e-8`) + * @example 1e-8 + */ + tick_size?: number; + /** + * Format: decimal + * @description The number of decimal places in the `quote_currency` (i.e `0.01`) + * @example 0.01 + */ + quote_increment?: number; + /** + * @description The minimum order size in `base_currency` units (i.e `0.00001`) + * @example 0.00001 + */ + min_order_size?: string; + /** + * @description Status of the current order book. Can be `open`, `closed`, `cancel_only`, `post_only`, `limit_only`. + * @example open + */ + status?: string; + /** + * @description When `True`, symbol can be wrapped using this endpoint: + * `POST https://api.gemini.com/v1/wrap/:symbol` + * @example false + */ + wrap_enabled?: boolean; + /** + * @description Instrument type `spot` / `swap` -- where `swap` signifies `perpetual swap`. + * @example spot + */ + product_type?: string; + /** + * @description `vanilla` / `linear` / `inverse` where `vanilla` is for spot + * while `linear` is for perpetual swap and `inverse` is a special case perpetual swap where the perpetual contract will be settled in base currency. + * @example vanilla + */ + contract_type?: string; + /** + * @description CCY2 or the quote currency for spot instrument (i.e. `USD` in `BTCUSD`) + * Or collateral currency of the contract in case of perpetual swap instrument. + * @example USD + */ + contract_price_currency?: string; + }; + Ticker: { + /** + * Format: decimal + * @description The highest bid currently available + * @example 977.59 + */ + bid?: string; + /** + * Format: decimal + * @description The lowest ask currently available + * @example 977.35 + */ + ask?: string; + /** + * Format: decimal + * @description The price of the last executed trade + * @example 977.65 + */ + last?: string; + /** @description Information about the 24 hour volume on the exchange. See properties below */ + volume?: { + /** + * @description The end of the 24-hour period over which volume was measured. [timestamp (ms)](/rest/~schemas#timestamp-type) + * @example 1483018200000 + */ + timestamp?: components["schemas"]["TimestampType"]; + /** + * Format: decimal + * @description The volume denominated in the price currency + * @example 2210.505328803 + */ + price_symbol?: string; + /** + * Format: decimal + * @description The volume denominated in the quantity currency + * @example 2135477.463379586263 + */ + quantity_symbol?: string; + }; + }; + OrderBook: { + /** @description The bid price levels currently on the book. These are offers to buy at a given price. */ + bids?: components["schemas"]["OrderBookEntry"][]; + /** @description The ask price levels currently on the book. These are offers to sell at a given price. */ + asks?: components["schemas"]["OrderBookEntry"][]; + }; + OrderBookEntry: { + /** + * Format: decimal + * @description The price + */ + price?: string; + /** + * Format: decimal + * @description The total quantity remaining at the price + */ + amount?: string; + /** @description **DO NOT USE** - this field is included for compatibility reasons only and is just populated with a dummy value. */ + timestamp?: string; + }; + Trade: { + /** + * @description The time that the trade was executed + * @example 1547146811 + */ + timestamp?: components["schemas"]["TimestampType"]; + /** + * @description The time that the trade was executed in milliseconds + * @example 1547146811357 + */ + timestampms?: components["schemas"]["TimestampType"]; + /** + * Format: int64 + * @description The trade ID number + * @example 5335307668 + */ + tid?: bigint; + /** + * Format: decimal + * @description The price the trade was executed at + * @example 3610.85 + */ + price?: string; + /** + * Format: decimal + * @description The amount that was traded + * @example 0.27413495 + */ + amount?: string; + /** + * @description Will always be "gemini" + * @example gemini + */ + exchange?: string; + /** + * @description - `buy` means that an ask was removed from the book by an incoming buy order. + * - `sell` means that a bid was removed from the book by an incoming sell order. + * @example buy + * @enum {string} + */ + type?: "buy" | "sell"; + /** + * @description Whether the trade was broken or not. Broken trades will not be displayed by default; use the `include_breaks` to display them. + * @example false + */ + broken?: boolean; + }; + Heartbeat: { + /** @description The literal string `/v1/heartbeat` */ + request?: string; + /** @description The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + nonce?: string | bigint; + }; + NewOrderRequest: { + /** + * @description The literal string "/v1/order/new" + * @example /v1/order/new + */ + request: string; + /** @description The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + nonce: number; + /** @description *Recommended*. A [client-specified order id](/client-order-id) */ + client_order_id?: string; + /** + * @description The [symbol](/market-data/symbols-and-minimums) for the new order + * @example BTCUSD + */ + symbol: string; + /** + * @description Quoted decimal amount to purchase + * @example 5 + */ + amount: string; + /** + * @description Quoted decimal amount to spend per unit + * @example 3633.00 + */ + price: string; + /** + * @example buy + * @enum {string} + */ + side: "buy" | "sell"; + /** + * @description The order type. "exchange limit" for all order types except for stop-limit orders. "exchange stop limit" for stop-limit orders. + * @example exchange limit + * @enum {string} + */ + type: "exchange limit" | "exchange stop limit" | "exchange market"; + /** + * @description An optional array containing at most one supported order execution option. See Order execution options for details. + * @example [ + * "maker-or-cancel" + * ] + */ + options?: ("maker-or-cancel" | "immediate-or-cancel" | "fill-or-kill")[]; + /** @description The price to trigger a stop-limit order. Only available for stop-limit orders. */ + stop_price?: string; + /** + * @description Set to `true` to place this order on a margin account using borrowed funds. Defaults to `false`. Only available for margin-enabled accounts. See [Margin Trading](/margin/account-summary) for details. + * @example false + */ + margin_order?: boolean; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts. */ + account?: string; + }; + CancelOrderRequest: { + /** + * @description The literal string "/v1/order/cancel" + * @example /v1/order/cancel + */ + request: string; + /** The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + nonce: components["schemas"]["TimestampType"]; + /** + * Format: int64 + * @description The order ID given by `/order/new` + * @example 106817811 + */ + order_id: bigint; + /** + * @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to cancel the order. Only available for exchange accounts. + * @example primary + */ + account?: string; + }; + CancelAllOrdersRequest: { + /** + * @description The literal string "/v1/order/cancel/all" + * @example /v1/order/cancel/all + */ + request: string; + /** The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + nonce: components["schemas"]["TimestampType"]; + /** + * @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to cancel the orders. Only available for exchange accounts. + * @example primary + */ + account?: string; + }; + CancelAllOrdersBySessionRequest: { + /** + * @description The literal string "/v1/order/cancel/session" + * @example /v1/order/cancel/session + */ + request: string; + /** The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + nonce: components["schemas"]["TimestampType"]; + /** + * @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to cancel the orders. Only available for exchange accounts. + * @example primary + */ + account?: string; + }; + OrderStatusRequest: { + /** + * @description The API endpoint path + * @example /v1/order/status + */ + request: string; + /** The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + nonce: components["schemas"]["TimestampType"]; + /** + * @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts. + * @example primary + */ + account?: string; + /** + * Format: int64 + * @description The order id to get information on. The `order_id` represents a whole number and is transmitted as an unsigned 64-bit integer in JSON format. `order_id` cannot be used in combination with `client_order_id`. + * @example 123456789012345 + */ + order_id: bigint; + /** @description The `client_order_id` used when placing the order. `client_order_id` cannot be used in combination with `order_id` */ + client_order_id?: string; + /** @description Either `True` or `False`. If `True` the endpoint will return individual trade details of all fills from the order. */ + include_trades?: boolean; + }; + MyTradesRequest: { + /** + * @description The API endpoint path + * @example /v1/mytrades + */ + request: string; + /** The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + nonce: components["schemas"]["TimestampType"]; + /** + * @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts. + * @example primary + */ + account?: string; + /** + * @description The [symbol](/market-data/symbols-and-minimums) to retrieve trades for + * @example btcusd + */ + symbol?: string; + /** + * @description The maximum number of trades to return. Default is 50, max is 500. + * @example 50 + */ + limit_trades?: number; + /** + * @description Only return trades on or after this timestamp. See [Data Types: Timestamps](/rest/~schemas#timestamp-type) for more information. If not present, will show the most recent orders. + * @example 1591084414000 + */ + timestamp?: components["schemas"]["TimestampType"]; + }; + /** Limit Order Response */ + LimitOrderResponse: { + order_id?: string; + id?: string; + symbol?: string; + exchange?: string; + avg_execution_price?: string; + /** @enum {string} */ + side?: "buy" | "sell"; + /** @enum {string} */ + type?: "exchange limit" | "exchange stop limit" | "exchange market"; + timestamp?: components["schemas"]["TimestampType"]; + timestampms?: components["schemas"]["TimestampType"]; + is_live?: boolean; + is_cancelled?: boolean; + is_hidden?: boolean; + was_forced?: boolean; + executed_amount?: string; + /** Format: double */ + remaining_amount?: string; + client_order_id?: string; + options?: string[]; + /** Format: double */ + price?: string; + /** Format: double */ + original_amount?: string; + }; + /** Stop-Limit Order Response */ + StopLimitOrderResponse: { + order_id?: string; + id?: string; + symbol?: string; + exchange?: string; + avg_execution_price?: string; + /** @enum {string} */ + side?: "buy" | "sell"; + /** @enum {string} */ + type?: "exchange stop limit"; + timestamp?: components["schemas"]["TimestampType"]; + timestampms?: components["schemas"]["TimestampType"]; + is_live?: boolean; + is_cancelled?: boolean; + is_hidden?: boolean; + was_forced?: boolean; + executed_amount?: string; + options?: string[]; + /** Format: double */ + stop_price?: string; + /** Format: double */ + price?: string; + /** Format: double */ + original_amount?: string; + }; + CancelOrderResponse: { + /** Format: integer */ + order_id?: string; + /** Format: integer */ + id?: string; + symbol?: string; + exchange?: string; + /** Format: double */ + avg_execution_price?: string; + /** @enum {string} */ + side?: "buy" | "sell"; + /** @enum {string} */ + type?: "exchange limit" | "exchange stop limit" | "exchange market"; + timestamp?: components["schemas"]["TimestampType"]; + timestampms?: components["schemas"]["TimestampType"]; + is_live?: boolean; + is_cancelled?: boolean; + is_hidden?: boolean; + was_forced?: boolean; + /** Format: double */ + executed_amount?: string; + /** Format: double */ + remaining_amount?: string; + /** @enum {string} */ + reason?: "MakerOrCancelWouldTake" | "ExceedsPriceLimits" | "SelfCrossPrevented" | "ImmediateOrCancelWouldPost" | "FillOrKillWouldNotFill" | "Requested" | "MarketClosed" | "TradingClosed"; + options?: string[]; + /** Format: double */ + price?: string; + /** Format: double */ + original_amount?: string; + }; + Order: { + /** + * Format: integer + * @description The order id + */ + order_id?: string; + /** + * Format: integer + * @description An optional [client-specified order id](/client-order-id#client-order-id) + */ + client_order_id?: string; + /** @description The [symbol](/market-data/symbols-and-minimums#symbols-and-minimums) of the order */ + symbol?: string; + /** @description Will always be "gemini" */ + exchange?: string; + /** + * Format: decimal + * @description The price the order was issued at + */ + price?: string; + /** + * Format: decimal + * @description The average price at which this order as been executed so far. 0 if the order has not been executed at all. + */ + avg_execution_price?: string; + /** @enum {string} */ + side?: "buy" | "sell"; + /** + * @description Description of the order + * @enum {string} + */ + type?: "exchange limit" | "exchange stop limit" | "exchange market"; + /** @description An array containing at most one supported order execution option. See [Order execution options](/rest/orders#create-new-order) for details. */ + options?: string[]; + /** @description The timestamp the order was submitted. Note that for compatibility reasons, this is returned as a string. We recommend using the timestampms field instead. */ + timestamp?: components["schemas"]["TimestampType"]; + /** @description The timestamp the order was submitted in milliseconds. */ + timestampms?: components["schemas"]["TimestampType"]; + /** @description `true` if the order is active on the book (has remaining quantity and has not been canceled) */ + is_live?: boolean; + /** @description `true` if the order has been canceled. Note the spelling, "cancelled" instead of "canceled". This is for compatibility reasons. */ + is_cancelled?: boolean; + /** @description Populated with the reason your order was canceled, if available. */ + reason?: string; + /** @description Will always be `false`. */ + was_forced?: boolean; + /** + * Format: decimal + * @description The amount of the order that has been filled. + */ + executed_amount?: string; + /** + * Format: decimal + * @description The amount of the order that has not been filled. + */ + remaining_amount?: string; + /** + * Format: decimal + * @description The originally submitted amount of the order. + */ + original_amount?: string; + /** @description Will always return `false`. */ + is_hidden?: boolean; + /** @description Contains an array of JSON objects with trade details. */ + trades?: { + /** + * Format: decimal + * @description The price that the execution happened at + */ + price?: string; + /** + * Format: decimal + * @description The quantity that was executed + */ + amount?: string; + /** @description The time that the trade happened in epoch seconds */ + timestamp?: components["schemas"]["TimestampType"]; + /** @description The time that the trade happened in milliseconds */ + timestampms?: components["schemas"]["TimestampType"]; + /** + * @description Will be either "Buy" or "Sell", indicating the side of the original order + * @example Buy + * @enum {string} + */ + type?: "Buy" | "Sell"; + /** @description If `true`, this order was the taker in the trade */ + aggressor?: boolean; + /** + * @description Currency that the fee was paid in + * @example USD + */ + fee_currency?: string; + /** + * Format: decimal + * @description The amount charged + * @example 1.23 + */ + fee_amount?: string; + /** + * @description Unique identifier for the trade + * @example 17379712930 + */ + tid?: number; + /** + * @description The order that this trade executed against + * @example 123456789 + */ + order_id?: string; + /** + * @description Will always be "gemini" + * @example gemini + */ + exchange?: string; + /** @description Will only be present if the trade is broken. See `Break Types` below for more information. */ + break?: string; + }[]; + }; + CancelAllResult: { + /** @example ok */ + result?: string; + /** @description cancelledOrders/cancelRejects with IDs of both */ + details?: { + cancelledOrders?: number[]; + cancelRejects?: number[]; + }; + }; + MyTrade: { + /** @example 9100 */ + price?: string; + /** @example 1.5 */ + amount?: string; + /** @example 1591084414 */ + timestamp?: components["schemas"]["TimestampType"]; + /** @example 1591084414622 */ + timestampms?: components["schemas"]["TimestampType"]; + /** + * @example Buy + * @enum {string} + */ + type?: "Buy" | "Sell"; + /** @example true */ + aggressor?: boolean; + /** @example USD */ + fee_currency?: string; + /** @example 13.65 */ + fee_amount?: string; + /** + * Format: int64 + * @example 123456789 + */ + tid?: bigint; + /** @example 123456789 */ + order_id?: string; + client_order_id?: string; + /** @example gemini */ + exchange?: string; + /** @example false */ + is_auction_fill?: boolean; + /** + * @example + * @enum {string} + */ + break?: "" | "trade correct"; + }; + TradeVolume: { + /** @example btcusd */ + symbol?: string; + /** @example BTC */ + base_currency?: string; + /** @example USD */ + quote_currency?: string; + /** @example USD */ + notional_currency?: string; + /** @example 2020-06-02 */ + data_date?: string; + /** @example 10.5 */ + total_volume_base?: string; + /** @example 1.2 */ + maker_buy_sell_ratio?: string; + /** @example 5.5 */ + buy_maker_base?: string; + /** @example 50050 */ + buy_maker_notional?: string; + /** @example 10 */ + buy_maker_count?: number; + /** @example 5 */ + sell_maker_base?: string; + /** @example 45500 */ + sell_maker_notional?: string; + /** @example 8 */ + sell_maker_count?: number; + /** @example 8.5 */ + buy_taker_base?: string; + /** @example 77350 */ + buy_taker_notional?: string; + /** @example 15 */ + buy_taker_count?: number; + /** @example 7.5 */ + sell_taker_base?: string; + /** @example 68250 */ + sell_taker_notional?: string; + /** @example 12 */ + sell_taker_count?: number; + }; + Balance: { + /** + * @example exchange + * @enum {string} + */ + type?: "exchange"; + /** + * @description The currency symbol + * @example BTC + */ + currency?: string; + /** + * @description The confirmed balance for the currency (also referred to as `confirmedBalance`). For crypto withdrawals, this value is **not** reduced until the withdrawal has been confirmed on the blockchain. This delay protects against blockchain reorganizations. Use the `available` field instead if you need balances that immediately reflect holds. + * @example 10.5 + */ + amount?: number; + /** + * @description The amount available for trading. This value is reduced **immediately** when an order hold or withdrawal hold is placed, making it the recommended field for tracking real-time spendable balances. + * @example 9 + */ + available?: number; + /** + * @description The amount available for withdrawal + * @example 9 + */ + availableForWithdrawal?: number; + /** + * @description The amount pending withdrawal + * @example 1 + */ + pendingWithdrawal?: number; + /** + * @description The amount pending deposit + * @example 0.5 + */ + pendingDeposit?: number; + /** + * Format: date-time + * @description Server-side monotonically increasing clock value as an ISO 8601 timestamp. Clients can use this value to detect and filter out stale responses that may occur due to load balancing or potential stale servers. + * @example 2024-03-16T00:00:00.000000Z + */ + _timestamp?: string; + }; + NotionalVolume: { + /** + * Format: date + * @example 2020-06-02 + */ + date?: string; + /** @example 1591084414622 */ + last_updated_ms?: number; + /** @example 25 */ + web_maker_fee_bps?: number; + /** @example 35 */ + web_taker_fee_bps?: number; + /** @example 25 */ + web_auction_fee_bps?: number; + /** @example 10 */ + api_maker_fee_bps?: number; + /** @example 35 */ + api_taker_fee_bps?: number; + /** @example 20 */ + api_auction_fee_bps?: number; + /** @example 10 */ + fix_maker_fee_bps?: number; + /** @example 35 */ + fix_taker_fee_bps?: number; + /** @example 20 */ + fix_auction_fee_bps?: number; + /** @example 1000000 */ + notional_30d_volume?: string; + notional_1d_volume?: { + /** @description UTC date in `yyyy-MM-dd` format */ + date?: string; + /** + * Format: decimal + * @description Notional volume value in USD for this single day + */ + notional_volume?: string; + }[]; + /** @example 750000 */ + api_notional_30d_volume?: string; + fee_tier?: { + /** @example 0bps */ + tier?: string; + /** @example 0 */ + api_maker_fee_bps?: number; + /** @example 10 */ + api_taker_fee_bps?: number; + }; + }; + NotionalBalance: { + /** @description Currency code, see symbols and minimums */ + currency?: string; + /** @description The current balance */ + amount?: string; + /** @description Amount, in notional */ + amountNotional?: string; + /** @description The amount that is available to trade */ + available?: string; + /** @description Available, in notional */ + availableNotional?: string; + /** @description The amount that is available to withdraw */ + availableForWithdrawal?: string; + /** @description AvailableForWithdrawal, in notional */ + availableForWithdrawalNotional?: string; + }; + Address: { + /** @description String representation of the cryptocurrency address */ + address?: string; + /** @description Creation date of the address */ + timestamp?: components["schemas"]["TimestampType"]; + /** @description If you provided a label when creating the address, it will be echoed back here */ + label?: string; + /** @description It would be present if applicable, it will be present for cosmos address */ + memo?: string; + /** @description The blockchain network for the address */ + network?: string; + }; + Transfer: { + /** @enum {string} */ + type?: "Deposit" | "Withdrawal"; + /** @enum {string} */ + status?: "Complete" | "Pending"; + /** @description The timestamp in milliseconds */ + timestampms?: components["schemas"]["TimestampType"]; + /** + * Format: int64 + * @description The transfer ID + */ + eid?: bigint; + /** @description The currency transferred */ + currency?: string; + /** @description The amount transferred */ + amount?: string; + /** @description The transaction hash if applicable */ + txHash?: string; + }; + V2Transfer: { + /** + * @description The type of the transfer + * @enum {string} + */ + type?: "Deposit" | "Withdrawal" | "Reward" | "AdminDebit" | "AdminCredit"; + /** + * @description The status of the transfer + * @enum {string} + */ + status?: "Complete" | "Pending" | "Advanced"; + /** @description The timestamp in milliseconds */ + timestampms?: components["schemas"]["TimestampType"]; + /** + * Format: int64 + * @description The transfer event ID + */ + eid?: bigint; + /** @description The currency transferred */ + currency?: string; + /** @description The amount transferred */ + amount?: string; + /** @description The blockchain network the transfer was executed on (e.g., `ethereum`, `solana`, `arbitrum`, `optimism`, `base`, `avalanche`). Not present for fiat or administrative transfers. */ + network?: string; + /** @description The fee charged for the transfer */ + feeAmount?: string; + /** @description The currency in which the fee was charged */ + feeCurrency?: string; + /** @description The on-chain transaction hash, if applicable */ + txHash?: string; + /** @description The transfer method (e.g., `ACH`, `CreditCard`) */ + method?: string; + /** @description The destination address for withdrawals */ + destination?: string; + /** @description The unique withdrawal identifier */ + withdrawalId?: string; + /** @description The output index for withdrawals */ + outputIdx?: number; + /** @description The purpose or reason for administrative transfers */ + purpose?: string; + }; + InstantQuote: { + /** @description Unique ID for the quote. This is used in the execution of the order */ + quoteId?: number; + /** @description Number of milliseconds until this quote price expires. Once expired, you will need to request a new quote */ + maxAgeMs?: number; + /** @description The symbol passed in the quote request */ + pair?: string; + /** @description The quoted price of the asset. This will not change when attempting execution */ + price?: string; + /** @description The currency in which the order is priced. Matches `CCY2` in the symbol */ + priceCurrency?: string; + /** + * @description Either "buy" or "sell" + * @enum {string} + */ + side?: "buy" | "sell"; + /** @description The quantity of the asset to be bought or sold */ + quantity?: string; + /** @description The currency label for the `quantity` field. Matches `CCY1` in the symbol */ + quantityCurrency?: string; + /** @description The fee quantity to be taken for the order upon execution */ + fee?: string; + /** @description The currency label for the order */ + feeCurrency?: string; + /** @description The deposit fee quantity. Will be applied if a debit card is used for the order. Will return 0 if there is no `depositFee` */ + depositFee?: string; + /** @description Currency in which `depositFee` is taken */ + depositFeeCurrency?: string; + /** @description Total quantity to spend for the order. Will be the sum inclusive of all fees and amount to be traded. */ + totalSpend?: string; + /** @description Currency of the `totalSpend` to be spent on the order */ + totalSpendCurrency?: string; + }; + ClearingOrder: { + /** @description The clearing ID */ + clearing_id?: string; + /** @description The trading pair */ + symbol?: string; + /** @description The order price */ + price?: string; + /** @description The order amount */ + amount?: string; + /** @enum {string} */ + side?: "buy" | "sell"; + /** @description The order status */ + status?: string; + /** @description The timestamp */ + timestamp?: components["schemas"]["TimestampType"]; + /** @description The timestamp in milliseconds */ + timestampms?: number; + /** @description Whether the order is confirmed */ + is_confirmed?: boolean; + }; + Account: { + /** @description The account name */ + name?: string; + /** @description The account ID */ + account_id?: string; + /** @description Whether the account is the default account */ + is_default?: boolean; + /** @description The creation date */ + created?: string; + }; + Transaction: { + /** @description The account. */ + account?: string; + /** @description The quantity that was executed. */ + amount?: string; + /** @description The client order ID, if defined. Otherwise an empty string. */ + clientOrderId?: string; + /** @description The price that the execution happened at. */ + price?: string; + /** @description The time that the trade happened in milliseconds. */ + timestampms?: components["schemas"]["TimestampType"]; + /** @description Indicating the side of the original order. */ + side?: string; + /** @description If true, this order was the taker in the trade. */ + isAggressor?: boolean; + /** @description The symbol that the trade was for */ + feeAssetCode?: string; + /** @description The fee amount charged */ + feeAmount?: string; + /** + * Format: int64 + * @description The order that this trade executed against. + */ + orderId?: bigint; + /** @description Will always be "gemini". */ + exchange?: string; + /** @description True if the trade was a auction trade and not an on-exchange trade. */ + isAuctionFill?: boolean; + /** @description True if the trade was a clearing trade and not an on-exchange trade. */ + isClearingFill?: boolean; + /** + * Format: int64 + * @description The trade ID. + */ + tid?: bigint; + /** @description The symbol that the trade was for. */ + symbol?: string; + } | { + /** @description The time that the trade happened in milliseconds. */ + timestampms?: components["schemas"]["TimestampType"]; + /** @description The account you are transferring from. */ + source?: string; + /** @description The account you are transferring to. */ + destination?: string; + /** @description The operation reason. */ + operationReason?: string; + /** @description The status of the transfer. */ + status?: string; + /** + * Format: int64 + * @description Transfer event id. + */ + eid?: bigint; + /** @description Currency code, see symbols */ + currency?: string; + /** @description The quantity that was transferred. */ + amount?: string; + /** @description Type of transfer method. */ + method?: string; + /** + * Format: int64 + * @description Correlation ID. + */ + correlationId?: bigint; + /** @description Transfer type. */ + transferType?: string; + /** @description Bank ID. */ + bankId?: string; + /** @description Purpose. */ + purpose?: string; + /** @description Supplies the transaction hash when available. */ + transactionHash?: string; + /** @description Transfer ID. */ + transferId?: string; + /** @description Withdrawal ID. */ + withdrawalId?: string; + /** @description Client Transfer ID. Client transfer ID is an optional client-supplied unique identifier for each withdrawal or internal transfer. */ + clientTransferId?: string; + /** + * Format: int64 + * @description Deposit advance event ID. + */ + advanceEid?: bigint; + /** + * Format: int64 + * @description Pending event ID. + */ + pendingEid?: bigint; + /** + * Format: int64 + * @description Withdrawal event ID. + */ + withdrawalEid?: bigint; + /** @description Fee ID. */ + feeId?: string; + }; + RevokeOauthTokenResponse: { + /** @description A message that indicates the token has been revoked for the account */ + message?: string; + }; + NetworkToken: { + /** @description The requested token identifier. */ + token?: string; + /** + * @description Array of supported blockchain networks for the token. Many tokens (especially stablecoins like USDC, USDT) are available on multiple networks. + * + * Supported networks include: `bitcoin`, `ethereum`, `solana`, `optimism`, `arbitrum`, `base`, `monad`, `avalanche`, `litecoin`, `bitcoincash`, `dogecoin`, `zcash`, `filecoin`, `tezos`, `polkadot`, `cosmos`, `xrpl`, `linea`, and more. + * @example [ + * "optimism", + * "solana", + * "base", + * "arbitrum", + * "monad", + * "avalanche", + * "ethereum" + * ] + */ + network?: string[]; + }; + NetworkAssets: { + /** + * @description The blockchain network identifier. + * @example ethereum + */ + network?: string; + /** + * @description Alphabetically sorted array of enabled asset/token codes available on this network. Assets include both exchange-tradable and custody-supported tokens. + * @example [ + * "AAVE", + * "BAT", + * "DAI", + * "ETH", + * "LINK", + * "MATIC", + * "UNI", + * "USDC", + * "USDT", + * "WBTC" + * ] + */ + assets?: string[]; + }; + FeePromos: { + /** @description Symbols that currently have fee promos */ + symbols?: string[]; + }; + PriceFeedResponse: { + /** @description Trading pair symbol. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols) */ + pair?: string; + /** @description Current price of the pair on the Gemini order book */ + price?: string; + /** @description 24 hour change in price of the pair on the Gemini order book */ + percentChange24h?: string; + }[]; + ApprovedAddress: { + /** @description The network of the approved address. Network can be `bitcoin`, `ethereum`, `bitcoincash`, `litecoin`, `zcash`, `filecoin`, `dogecoin`, `tezos`, `solana`, `polkadot`, `avalanche`, `cosmos`, or `xrpl` */ + network?: string; + /** @description Will return the scope of the address as either "account" or "group" */ + scope?: string; + /** @description The label assigned to the address */ + label?: string; + /** @description The status of the address that will return as "active", "pending-time" or "pending-mua". The remaining time is exactly 7 days after the initial request. "pending-mua" is for multi-user accounts and will require another administator or fund manager on the account to approve the address. */ + status?: string; + /** @description UTC timestamp in millisecond of when the address was created. */ + createdAt?: string; + /** @description The address on the approved address list. */ + address?: string; + }; + OpenPosition: { + /** @description The [symbol](/market-data/symbols-and-minimums) of the order. */ + symbol?: string; + /** @description The type of instrument. Either "spot" or "perp". */ + instrument_type?: string; + /** + * Format: decimal + * @description The position size. Value will be negative for shorts. + */ + quantity?: string; + /** + * Format: decimal + * @description The value of position; calculated as (`quantity` * `mark_price`). Value will be negative for shorts. + */ + notional_value?: string; + /** + * Format: decimal + * @description The current P&L that has been realised from the position. + */ + realised_pnl?: string; + /** + * Format: decimal + * @description Current Mark to Market value of the positions. + */ + unrealised_pnl?: string; + /** + * Format: decimal + * @description The average price of the current position. + */ + average_cost?: string; + /** + * Format: decimal + * @description The current Mark Price for the Asset or the position. + */ + mark_price?: string; + }; + FundingAmountResponse: { + /** @description The requested symbol. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols) */ + symbol?: string; + /** @description UTC date time in format `yyyy-MM-ddThh:mm:ss.SSSZ` format */ + fundingDateTime?: string; + /** + * Format: long + * @description Current funding amount Epoc time. + */ + fundingTimestampMilliSecs?: number; + /** + * Format: long + * @description Next funding amount Epoc time. + */ + nextFundingTimestamp?: number; + /** + * Format: decimal + * @description The dollar amount for a Long 1 position held in the symbol for funding period (1 hour) + */ + amount?: number; + /** + * Format: decimal + * @description The estimated dollar amount for a Long 1 position held in the symbol for next funding period (1 hour) + */ + estimatedFundingAmount?: number; + }; + StakingBalance: { + /** + * @description Will always be "Staking" + * @example Staking + */ + type?: string; + /** + * @description Currency code, see symbols and minimums + * @example MATIC + */ + currency?: string; + /** + * Format: decimal + * @description The current Staking balance + * @example 10 + */ + balance?: number; + /** + * Format: decimal + * @description The amount that is available to trade + * @example 0 + */ + available?: number; + /** + * Format: decimal + * @description The Staking amount that is available to redeem to exchange account + * @example 10 + */ + availableForWithdrawal?: number; + balanceByProvider?: { + [key: string]: { + /** + * Format: decimal + * @description The current Staking balance per providerId + * @example 10 + */ + balance?: number; + }; + }; + }; + StakingDeposit: { + /** + * @description A unique identifier for the staking transaction + * @example 65QN4XM5 + */ + transactionId?: string; + /** + * @description Provider Id, in uuid4 format + * @example 62b21e17-2534-4b9f-afcf-b7edb609dd8d + */ + providerId?: string; + /** + * @description Currency code, see [symbols](/market-data/symbols-and-minimums) + * @example MATIC + */ + currency?: string; + /** + * Format: decimal + * @description The amount deposited + * @example 30 + */ + amount?: number; + /** + * Format: decimal + * @description The total accrual + */ + accrualTotal?: number; + /** @description A JSON object including one or many rates. If more than one rate it would be an array of rates. */ + rates?: { + /** + * @description Staking interest rate in bps (Expressed as a simple rate. Interest on Staking balances compounds daily. In mobile and web applications, APYs are derived from this rate and rounded to 1/10th of a percent.) + * @example 540 + */ + rate?: number; + }; + }; + StakingTransaction: { + /** + * @description A unique identifier for the staking transaction + * @example MPZ7LDD8 + */ + transactionId?: string; + /** + * @description Can be any one of the following - Deposit, Redeem, Interest, RedeemPayment, AdminRedeem, AdminCreditAdjustment, AdminDebitAdjustment + * @example Redeem + * @enum {string} + */ + transactionType?: "Deposit" | "Redeem" | "Interest" | "RedeemPayment" | "AdminRedeem" | "AdminCreditAdjustment" | "AdminDebitAdjustment"; + /** + * @description Currency code + * @example MATIC + */ + amountCurrency?: string; + /** + * Format: decimal + * @description The amount that is defined by the transactionType above + * @example 20 + */ + amount?: number; + /** + * @description A supported three-letter fiat currency code, e.g. usd + * @example USD + */ + priceCurrency?: string; + /** + * Format: decimal + * @description Current market price of the underlying token at the time of the reward + * @example 0.1 + */ + priceAmount?: number; + /** + * @description The time of the transaction in milliseconds + * @example 1667418560153 + */ + dateTime?: components["schemas"]["TimestampType"]; + }; + StakingHistory: { + /** + * @description Provider Id, in uuid4 format + * @example 62b21e17-2534-4b9f-afcf-b7edb609dd8d + */ + providerId?: string; + transactions?: components["schemas"]["StakingTransaction"][]; + }; + StakingRate: { + /** + * @description Provider Id, in uuid4 format + * @example 62bb4d27-a9c8-4493-a737-d4fa33994f1f + */ + providerId?: string; + /** + * @description Staking interest rate in bps (Expressed as a simple rate. Interest on Staking balances compounds daily. In mobile and web applications, APYs are derived from this rate and rounded to 1/10th of a percent.) + * @example 429.386 + */ + rate?: number; + /** + * Format: decimal + * @description Staking interest APY (Expressed as a percentage derived from the rate and rounded to 1/10th of a percent.) + * @example 4.39 + */ + apyPct?: number; + /** + * Format: decimal + * @description `rate` expressed as a percentage + * @example 4.29386 + */ + ratePct?: number; + /** + * @description Maximum new amount in USD notional of this crypto that can participate in Gemini Staking per account per month + * @example 500000 + */ + depositUsdLimit?: number; + }; + /** @description Currency Symbol Keys */ + StakingRateProvider: { + currency_symbol?: components["schemas"]["StakingRate"]; + }; + /** @description Provider UUID Keys */ + StakingRateResponse: { + provider_uuid?: components["schemas"]["StakingRateProvider"]; + }; + StakingRewardPeriod: { + /** + * @description Provider Id, in uuid4 format + * @example 62b21e17-2534-4b9f-afcf-b7edb609dd8d + */ + providerId?: string; + /** + * @description Currency code, see [symbols](/market-data/symbols-and-minimums) + * @example MATIC + */ + currency?: string; + /** + * Format: decimal + * @description Staking reward rate expressed as an APY at time of accrual. Interest on Staking balances compounds daily based on the simple rate which is available from `/v1/staking/rates/` + * @example 5.75 + */ + apyPct?: number; + /** + * Format: decimal + * @description Rate expressed as a percentage + * @example 5.592369 + */ + ratePct?: number; + /** + * @description Number of accruals in the specific aggregate, typically one per day. If the rate is adjusted, new accruals are added. + * @example 1 + */ + numberOfAccruals?: number; + /** + * Format: decimal + * @description The total accrual + * @example 0.0065678 + */ + accrualTotal?: number; + /** + * @description Time of first accrual. In iso datetime with timezone format + * @example 2022-08-23T20:00:00.000Z + */ + firstAccrualAt?: string; + /** + * @description Time of last accrual. In iso datetime with timezone format + * @example 2022-08-23T20:00:00.000Z + */ + lastAccrualAt?: string; + }; + StakingRewards: { + /** + * @description Provider Id, in uuid4 format + * @example 62b21e17-2534-4b9f-afcf-b7edb609dd8d + */ + providerId?: string; + /** + * @description Currency code, see [symbols](/market-data/symbols-and-minimums) + * @example MATIC + */ + currency?: string; + /** + * Format: decimal + * @description The total accrual + * @example 0.103994 + */ + accrualTotal?: number; + /** @description Array of JSON objects with period accrual information */ + ratePeriods?: components["schemas"]["StakingRewardPeriod"][]; + }; + /** @description Currency Symbol Keys */ + StakingRewardsProvider: { + currency_symbol?: components["schemas"]["StakingRewards"]; + }; + /** @description Provider UUID Keys */ + StakingRewardsResponse: { + provider_uuid?: components["schemas"]["StakingRewardsProvider"]; + }; + StakingWithdrawal: { + /** + * @description A unique identifier for the staking transaction + * @example MPZ7LDD8 + */ + transactionId?: string; + /** + * Format: decimal + * @description The amount deposited + * @example 20 + */ + amount?: number; + /** + * Format: decimal + * @description The amount redeemed successfully + * @example 20 + */ + amountPaidSoFar?: number; + /** + * Format: decimal + * @description The amount pending to be redeemed + * @example 0 + */ + amountRemaining?: number; + /** + * @description Currency code + * @example MATIC + */ + currency?: string; + /** + * @description In ISO datetime with timezone format + * @example 2022-11-02T19:49:20.153Z + */ + requestInitiated?: string; + }; + FeeEstimateRequest: { + /** + * @description The string `/v1/withdraw/{currencyCodeLowerCase}/feeEstimate` where `:currencyCodeLowerCase` is replaced with the currency code of a supported crypto-currency, e.g. `eth`, `aave`, etc. See [Symbols and minimums](/market-data/symbols-and-minimums) + * @example /v1/withdraw/eth/feeEstimate + */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** + * @description Standard string format of cryptocurrency address + * @example 0x31c2105b8dea834167f32f7ea7d877812e059230 + */ + address: string; + /** + * @description Quoted decimal amount to withdraw + * @example 0.01 + */ + amount: string; + /** + * @description The name of the account within the subaccount group. + * @example primary + */ + account: string; + }; + FeeEstimateResponse: { + /** + * @description Currency code, see [symbols](/market-data/symbols-and-minimums). + * @example ETH + */ + currency?: string; + /** + * @description The estimated gas fee + * @example {currency: 'ETH', value: '0'} + */ + fee?: string; + /** + * @description Value that shows if an override on the customer's account for free withdrawals exists + * @example false + */ + isOverride?: boolean; + /** + * @description Total nunber of allowable fee-free withdrawals + * @example 1 + */ + monthlyLimit?: number; + /** + * @description Total number of allowable fee-free withdrawals left to use + * @example 1 + */ + monthlyRemaining?: number; + }; + FeeEstimateV2Request: { + /** + * @description The string `/v2/withdraw/{network}/{ticker}/feeEstimate` where `{network}` is the blockchain network (e.g. `ethereum`, `bitcoin`, `solana`) and `{ticker}` is the currency code (e.g. `eth`, `btc`, `sol`). See [Symbols and minimums](/market-data/symbols-and-minimums) + * @example /v2/withdraw/ethereum/eth/feeEstimate + */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** + * @description Standard string format of the destination cryptocurrency address + * @example 0x31c2105b8dea834167f32f7ea7d877812e059230 + */ + address: string; + /** + * @description Quoted decimal amount to withdraw + * @example 0.01 + */ + amount: string; + /** + * @description Required for Master API keys. The name of the account within the subaccount group. + * @example primary + */ + account?: string; + /** @description It would be present if applicable, it will be present for cosmos address. */ + memo?: string; + }; + FeeEstimateV2Response: { + /** + * @description Currency code, see [symbols](/market-data/symbols-and-minimums). + * @example ETH + */ + currency?: string; + /** + * Format: decimal + * @description The estimated withdrawal fee as a decimal amount + * @example 0.001 + */ + fee?: number; + /** + * @description Whether an override on the customer's account for free withdrawals exists + * @example false + */ + isOverride?: boolean; + /** + * @description Total number of allowable fee-free withdrawals + * @example 1 + */ + monthlyLimit?: number; + /** + * @description Total number of allowable fee-free withdrawals remaining + * @example 1 + */ + monthlyRemaining?: number; + }; + RoleResponse: { + /** @description `True` if the Auditor role is assigned to the API keys. `False` otherwise. */ + isAuditor: boolean; + /** @description `True` if the Fund Manager role is assigned to the API keys. `False` otherwise. */ + isFundManager: boolean; + /** @description `True` if the Trader role is assigned to the API keys. `False` otherwise. */ + isTrader: boolean; + /** @description _Only returned for master-level API keys_. The Gemini clearing counterparty ID associated with the API key making the request. */ + counterparty_id?: string; + /** @description _Only returned for master-level API keys_.`True` if the Administrator role is assigned to the API keys. `False` otherwise. */ + isAccountAdmin?: boolean; + }; + MarginResponse: { + /** + * Format: decimal + * @description The $ equivalent value of all the assets available in the current trading account that can contribute to funding a derivatives position. + */ + margin_assets_value?: string; + /** + * Format: decimal + * @description The $ amount that is being required by the accounts current positions and open orders. + */ + initial_margin?: string; + /** + * Format: decimal + * @description The difference between the `margin_assets_value` and `initial_margin`. + */ + available_margin?: string; + /** + * Format: decimal + * @description The minimum amount of `margin_assets_value` required before the account is moved to liquidation status. + */ + margin_maintenance_limit?: string; + /** + * Format: decimal + * @description The ratio of Notional Value to Margin Assets Value. + */ + leverage?: string; + /** + * Format: decimal + * @description The $ value of the current position. + */ + notional_value?: string; + /** + * Format: decimal + * @description The estimated price for the asset at which liquidation would occur. + */ + estimated_liquidation_price?: string; + /** + * Format: decimal + * @description The contribution to `initial_margin` from open positions. + */ + initial_margin_positions?: string; + /** + * Format: decimal + * @description The contribution to `initial_margin` from open orders. + */ + reserved_margin?: string; + /** + * Format: decimal + * @description The contribution to `initial_margin` from open BUY orders. + */ + reserved_margin_buys?: string; + /** + * Format: decimal + * @description The contribution to `initial_margin` from open SELL orders. + */ + reserved_margin_sells?: string; + /** + * Format: decimal + * @description The amount of that product the account could purchase based on current `initial_margin` and `margin_assets_value`. + */ + buying_power?: string; + /** + * Format: decimal + * @description The amount of that product the account could sell based on current `initial_margin` and `margin_assets_value`. + */ + selling_power?: string; + }; + MoneyAmount: { + /** + * @description The currency code (e.g., "USD", "BTC", "ETH") + * @example USD + */ + currency: string; + /** + * Format: decimal + * @description The amount in the specified currency + * @example 10000.00 + */ + value: string; + }; + LiquidationRisk: { + /** + * Format: decimal + * @description The percentage loss from current value that would trigger liquidation, formatted as decimal (e.g., "0.1550" = 15.50%) + * @example 0.1550 + */ + lossPercentage: string; + /** @description The estimated price at which liquidation would occur (optional, may not be present for all positions) */ + liquidationPrice?: components["schemas"]["MoneyAmount"]; + }; + InterestRateInfo: { + /** + * Format: decimal + * @description The interest rate as a decimal string + * @example 0.00001141552511 + */ + rate: string; + /** + * @description The time interval for the rate (currently only "hour" is supported) + * @example hour + * @enum {string} + */ + interval: "hour"; + }; + MarginAccountSummary: { + /** @description The total value of all assets available in the margin account that can contribute to funding positions */ + marginAssetValue: components["schemas"]["MoneyAmount"]; + /** @description The amount of collateral available for new positions or withdrawals */ + availableCollateral: components["schemas"]["MoneyAmount"]; + /** @description The total value of all open positions */ + notionalValue: components["schemas"]["MoneyAmount"]; + /** @description The total amount currently borrowed across all currencies */ + totalBorrowed: components["schemas"]["MoneyAmount"]; + /** + * Format: decimal + * @description The current leverage ratio (notionalValue / marginAssetValue) + * @example 1.5 + */ + leverage: string; + /** @description The maximum value that can be purchased with available collateral */ + buyingPower: components["schemas"]["MoneyAmount"]; + /** @description The maximum value that can be sold with available collateral */ + sellingPower: components["schemas"]["MoneyAmount"]; + /** @description Liquidation risk information (only present if positions exist) */ + liquidationRisk?: components["schemas"]["LiquidationRisk"]; + /** @description Current interest rate on borrowed amounts (only present if borrows exist) */ + interestRate?: components["schemas"]["InterestRateInfo"]; + /** @description Collateral reserved for open buy orders */ + reservedBuyOrders: components["schemas"]["MoneyAmount"]; + /** @description Collateral reserved for open sell orders */ + reservedSellOrders: components["schemas"]["MoneyAmount"]; + }; + MarginInterestRate: { + /** + * @description The currency code (e.g., "BTC", "ETH", "USD") + * @example BTC + */ + currency: string; + /** + * Format: decimal + * @description The hourly borrow rate as a decimal + * @example 0.00001141552511 + */ + borrowRate: string; + /** + * Format: decimal + * @description The daily borrow rate (hourly rate × 24) + * @example 0.00027397260264 + */ + borrowRateDaily: string; + /** + * Format: decimal + * @description The annualized borrow rate (daily rate × 365) + * @example 0.1 + */ + borrowRateAnnual: string; + /** + * Format: int64 + * @description Unix timestamp in milliseconds when the rate was last updated + * @example 1700000000000 + */ + lastUpdated: bigint; + }; + MarginRatesResponse: { + /** @description Array of interest rates for all borrowable currencies */ + rates: components["schemas"]["MarginInterestRate"][]; + }; + MarginRiskStats: { + /** @description The total value of all assets available in the margin account */ + marginAssetValue: components["schemas"]["MoneyAmount"]; + /** @description The amount of collateral available for new positions */ + availableCollateral: components["schemas"]["MoneyAmount"]; + /** @description The total value of all open positions */ + notionalValue: components["schemas"]["MoneyAmount"]; + /** @description The total amount currently borrowed */ + totalBorrowed: components["schemas"]["MoneyAmount"]; + /** + * Format: decimal + * @description The leverage ratio + * @example 1.5 + */ + leverage: string; + /** @description Collateral reserved for open buy orders */ + reservedBuyOrders: components["schemas"]["MoneyAmount"]; + /** @description Collateral reserved for open sell orders */ + reservedSellOrders: components["schemas"]["MoneyAmount"]; + /** @description The maximum value that can be purchased */ + buyingPower: components["schemas"]["MoneyAmount"]; + /** @description The maximum value that can be sold */ + sellingPower: components["schemas"]["MoneyAmount"]; + /** @description Liquidation risk information (only present if applicable) */ + liquidationRisk?: components["schemas"]["LiquidationRisk"]; + }; + MarginOrderPreview: { + /** @description Margin risk statistics before the order would be executed */ + preorder: components["schemas"]["MarginRiskStats"]; + /** @description Margin risk statistics after the order would be executed */ + postorder: components["schemas"]["MarginRiskStats"]; + }; + Quantity: { + /** @description The currency code of the quantity. */ + currency: string; + /** + * Format: decimal + * @description The value of the quantity. + */ + value: string; + }; + FundingTransfer: { + /** @description Event type */ + eventType: string; + /** @description Time of the funding payment */ + timestamp: components["schemas"]["TimestampType"]; + /** @description Asset symbol */ + assetCode: string; + /** + * @description Credit or Debit + * @enum {string} + */ + action: "Credit" | "Debit"; + /** @description A nested JSON object describing the transaction amount */ + quantity: components["schemas"]["Quantity"]; + /** @description Symbol of the underlying instrument. **Note** that this is only attached to requests from 16th April 2024 onwards. */ + instrumentSymbol?: string; + }; + FundingPayment: { + /** + * @description Event type + * @enum {string} + */ + eventType: "Hourly Funding Transfer"; + hourlyFundingTransfer: components["schemas"]["FundingTransfer"]; + }; + FundingPaymentReportItem: { + /** + * @description Event type + * @enum {string} + */ + eventType: "Hourly Funding Transfer"; + /** @description Time of the funding payment */ + timestamp: components["schemas"]["TimestampType"]; + /** @description Asset symbol */ + assetCode: string; + /** + * @description Credit or Debit + * @enum {string} + */ + action: "Credit" | "Debit"; + /** @description A nested JSON object describing the transaction amount */ + quantity: components["schemas"]["Quantity"]; + /** @description Symbol of the underlying instrument. **Note** that this is only attached to requests from 16th April 2024 onwards. */ + instrumentSymbol?: string; + }; + RiskStatsResponse: { + /** + * @description Contract type for which the symbol data is fetched + * @enum {string} + */ + product_type?: "PerpetualSwapContract"; + /** + * Format: decimal + * @description Current mark price at the time of request + */ + mark_price?: string; + /** + * Format: decimal + * @description Current index price at the time of request + */ + index_price?: string; + /** + * Format: decimal + * @description string representation of decimal value of open interest + */ + open_interest?: string; + /** + * Format: decimal + * @description string representation of decimal value of open interest notional + */ + open_interest_notional?: string; + }; + FxRate: { + /** + * @description The requested currency pair + * @example AUDUSD + */ + fxPair?: string; + /** + * Format: double + * @description The exchange rate + * @example 0.69 + */ + rate?: number; + /** + * @description The timestamp (in Epoch time format) that the requested fxrate has been retrieved for + * @example 1594651859000 + */ + asOf?: components["schemas"]["TimestampType"]; + /** + * @description The market data provider + * @example bcb + */ + provider?: string; + /** + * @description The market for which the retrieved price applies to + * @example Spot + */ + benchmark?: string; + }; + /** + * @example [ + * [ + * 1559755800000, + * 7781.6, + * 7820.23, + * 7776.56, + * 7819.39, + * 34.7624802159 + * ], + * [ + * 1559755800000, + * 7781.6, + * 7829.46, + * 7776.56, + * 7817.28, + * 43.4228281059 + * ] + * ] + */ + Candle: number[]; + CandleResponse: components["schemas"]["Candle"][]; + TickerInfo: { + /** + * @description The trading pair symbol + * @example BTCUSD + */ + symbol?: string; + /** + * Format: decimal + * @description Open price from 24 hours ago + * @example 9121.76 + */ + open?: string; + /** + * Format: decimal + * @description High price from 24 hours ago + * @example 9440.66 + */ + high?: string; + /** + * Format: decimal + * @description Low price from 24 hours ago + * @example 9106.51 + */ + low?: string; + /** + * Format: decimal + * @description Close price (most recent trade) + * @example 9347.66 + */ + close?: string; + /** + * @description Hourly prices descending for past 24 hours + * @example [ + * "9365.1", + * "9386.16", + * "9373.41", + * "9322.56", + * "9268.89", + * "9265.38" + * ] + */ + changes?: string[]; + /** + * Format: decimal + * @description Current best bid + * @example 9345.70 + */ + bid?: string; + /** + * Format: decimal + * @description Current best offer + * @example 9347.67 + */ + ask?: string; + }; + }; + responses: { + /** @description Bad request - malformed request or invalid parameters */ + BadRequest: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "result": "error", + * "reason": "InvalidSignature", + * "message": "Invalid signature for this request" + * } + */ + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Unauthorized - missing or invalid authentication */ + Unauthorized: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "result": "error", + * "reason": "MissingApikeyHeader", + * "message": "Must provide 'X-GEMINI-APIKEY' header" + * } + */ + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description ApiKey fails IP Filtering Check */ + ApiKeyIpFilteringFailure: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "result": "error", + * "reason": "ApiKeyIpFilteringFailure", + * "message": "ApiKey fails IP Filtering Check for some accounts" + * } + */ + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Resource not found */ + NotFound: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "result": "error", + * "reason": "EndpointNotFound", + * "message": "API entry point not found" + * } + */ + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Too many requests - you have exceeded the rate limit */ + TooManyRequests: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "result": "error", + * "reason": "Too Many Requests", + * "message": "Too Many Requests" + * } + */ + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Internal server error */ + InternalError: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "result": "error", + * "reason": "Internal Server Error", + * "message": "Unexpected server error occurred." + * } + */ + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + parameters: { + /** + * @description The timestamp to pull the FX rate for. + * + * Gemini strongly recommends using milliseconds instead of seconds for timestamps. + * @example 1591084414622 + */ + timestampParam: components["schemas"]["TimestampType"]; + /** + * @description Trading pair symbol

+ * + * `BTCUSD`, etc. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols). + */ + symbolParam: string; + /** @description Either a fiat currency, e.g. `usd` or `gbp`, or a supported crypto-currency, e.g. `gusd`, `btc`, `eth`, `aave`, etc. */ + currencyParam: string; + /** @description Can be `bitcoin`, `ethereum`, `bitcoincash`, `litecoin`, `zcash`, `filecoin`, `dogecoin`, `tezos`, `solana`, `polkadot`, `avalanche`, `cosmos`, or `xrpl` */ + networkParam: string; + /** @description Your API key */ + apiKeyAuth: string; + /** @description Base64-encoded JSON payload */ + payloadAuth: string; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + signatureAuth: string; + contentType: string; + contentLength: string; + cacheControl: string; + }; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + listSymbols: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The full list of supported symbols. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example [ + * "aavegusd", + * "aaveusd", + * "aligusd", + * "aliusd", + * "ampgusd", + * "ampusd", + * "ankrgusd", + * "ankrusd", + * "apegusd", + * "apeusd", + * "api3gusd", + * "api3usd", + * "arbgusd", + * "arbusd", + * "atomgusd", + * "atomusd", + * "avaxgusd", + * "avaxgusdperp", + * "avaxusd", + * "axsgusd", + * "axsusd", + * "batgusd", + * "batusd", + * "bchgusd", + * "bchgusdperp", + * "bchusd", + * "bnbgusdperp", + * "bomegusd", + * "bomegusdperp", + * "bomeusd", + * "bonkgusd", + * "bonkgusdperp", + * "bonkusd", + * "btceur", + * "btcgbp", + * "btcgusd", + * "btcgusdperp", + * "btcsgd", + * "btcusd", + * "btcusdt", + * "chillguygusd", + * "chillguyusd", + * "chzgusd", + * "chzusd", + * "compgusd", + * "compusd", + * "crvgusd", + * "crvusd", + * "ctxgusd", + * "ctxusd", + * "cubegusd", + * "cubeusd", + * "daigusd", + * "daiusd", + * "dogebtc", + * "dogeeth", + * "dogegusd", + * "dogegusdperp", + * "dogeusd", + * "dotgusd", + * "dotgusdperp", + * "dotusd", + * "efilfil", + * "elongusd", + * "elonusd", + * "ensgusd", + * "ensusd", + * "ethbtc", + * "etheur", + * "ethgbp", + * "ethgusd", + * "ethgusdperp", + * "ethsgd", + * "ethusd", + * "ethusdt", + * "fetgusd", + * "fetusd", + * "filgusd", + * "filusd", + * "flokigusd", + * "flokigusdperp", + * "flokiusd", + * "ftmgusd", + * "ftmusd", + * "galagusd", + * "galausd", + * "gmtgusd", + * "gmtusd", + * "goatgusd", + * "goatgusdperp", + * "goatusd", + * "grtgusd", + * "grtusd", + * "gusdgbp", + * "gusdsgd", + * "gusdusd", + * "hntgusd", + * "hntusd", + * "hypegusdperp", + * "imxgusd", + * "imxusd", + * "injgusd", + * "injgusdperp", + * "injusd", + * "iotxgusd", + * "iotxusd", + * "ksl2gusdperp", + * "kt5gusdperp", + * "ldogusd", + * "ldousd", + * "linkbtc", + * "linketh", + * "linkgusd", + * "linkgusdperp", + * "linkusd", + * "lptgusd", + * "lptusd", + * "lrcgusd", + * "lrcusd", + * "ltcbtc", + * "ltceth", + * "ltcgusd", + * "ltcgusdperp", + * "ltcusd", + * "managusd", + * "manausd", + * "maskgusd", + * "maskusd", + * "maticgusd", + * "maticusd", + * "mewgusd", + * "mewgusdperp", + * "mewusd", + * "mkrgusd", + * "mkrusd", + * "moodenggusd", + * "moodenggusdperp", + * "moodengusd", + * "opgusd", + * "opgusdperp", + * "opusd", + * "oxtgusd", + * "oxtusd", + * "paxggusd", + * "paxgusd", + * "pepegusd", + * "pepegusdperp", + * "pepeusd", + * "pnutgusd", + * "pnutgusdperp", + * "pnutusd", + * "polgusdperp", + * "popcatgusd", + * "popcatgusdperp", + * "popcatusd", + * "pythgusd", + * "pythgusdperp", + * "pythusd", + * "qntgusd", + * "qntusd", + * "raregusd", + * "rareusd", + * "rengusd", + * "renusd", + * "rlusdusd", + * "rndrgusd", + * "rndrusd", + * "samogusd", + * "samousd", + * "sandgusd", + * "sandusd", + * "shibgusd", + * "shibgusdperp", + * "shibusd", + * "sklgusd", + * "sklusd", + * "solbtc", + * "soleth", + * "solgusd", + * "solgusdperp", + * "solusd", + * "storjgusd", + * "storjusd", + * "sushigusd", + * "sushiusd", + * "trumpgusdperp", + * "umagusd", + * "umausd", + * "unigusd", + * "unigusdperp", + * "uniusd", + * "usdcusd", + * "usdtgusd", + * "usdtusd", + * "wifgusd", + * "wifgusdperp", + * "wifusd", + * "xrpgusd", + * "xrpgusdperp", + * "xrpusd", + * "xtzgusd", + * "xtzusd", + * "yfigusd", + * "yfiusd", + * "zecgusd", + * "zecusd", + * "zrxgusd", + * "zrxusd" + * ] + */ + "application/json": string[]; + }; + }; + 400: components["responses"]["BadRequest"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getSymbolDetails: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description Trading pair symbol

+ * + * `BTCUSD`, etc. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols). + */ + symbol: components["parameters"]["symbolParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Instrument responses examples */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SymbolDetails"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getAssetsForNetwork: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path: { + /** + * @description Blockchain network identifier (lowercase). Supported networks include: `ethereum`, `solana`, `bitcoin`, `optimism`, `arbitrum`, `base`, `monad`, `avalanche`, `litecoin`, `bitcoincash`, `dogecoin`, `zcash`, `filecoin`, `tezos`, `polkadot`, `cosmos`, `xrpl`, `linea`, and more. + * @example ethereum + */ + network: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The response will be a JSON object containing the network name and its supported assets. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NetworkAssets"]; + }; + }; + /** @description The supplied network is not supported or has no enabled assets. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + errorMessage?: string; + }; + }; + }; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getTokenNetworkV2: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path: { + /** + * @description Token identifier. `BTC`, `ETH`, `USDC`, `SOL` etc. See [**symbols and minimums**](/market-data/symbols-and-minimums) + * @example USDC + */ + token: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The response will be a JSON object containing the token and its available networks for the authenticated account. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NetworkToken"]; + }; + }; + 400: components["responses"]["BadRequest"]; + /** @description Returned when the token is not supported or the account has no available networks for the requested token. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example error */ + result?: string; + /** @example UnsupportedNetwork */ + reason?: string; + /** @example UnsupportedNetwork: INVALIDTOKEN */ + message?: string; + }; + }; + }; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getTicker: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description Trading pair symbol

+ * + * `BTCUSD`, etc. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols). + */ + symbol: components["parameters"]["symbolParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The current ticker for the symbol */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "bid": "977.59", + * "ask": "977.35", + * "last": "977.65", + * "volume": { + * "BTC": "2210.505328803", + * "USD": "2135477.463379586263", + * "timestamp": 1483018200000 + * } + * } + */ + "application/json": components["schemas"]["Ticker"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listFeePromos: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The response will be a JSON object */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "symbols": [ + * "PNUTGUSDPERP", + * "WIFGUSDPERP", + * "PYTHGUSDPERP", + * "MEWGUSDPERP", + * "BONKGUSDPERP", + * "BCHGUSDPERP", + * "BTCGUSDPERP", + * "BUSDUSD", + * "POLGUSDPERP", + * "FRAXUSD", + * "OPGUSDPERP", + * "DOTGUSDPERP", + * "TRUMPGUSDPERP", + * "GUSDGBP", + * "USDTUSD", + * "POPCATGUSDPERP", + * "FLOKIGUSDPERP", + * "MOODENGGUSDPERP", + * "LINKGUSDPERP", + * "ETHGUSDPERP", + * "UNIGUSDPERP", + * "MATICGUSDPERP", + * "USDTGUSD", + * "BNBGUSDPERP", + * "MIMUSD", + * "KSL2GUSDPERP", + * "LUSDUSD", + * "SHIBGUSDPERP", + * "AVAXGUSDPERP", + * "BOMEGUSDPERP", + * "USDCUSD", + * "HYPEGUSDPERP", + * "MOGGUSDPERP", + * "KT5GUSDPERP", + * "SOLGUSDPERP", + * "PEPEGUSDPERP", + * "DOGEGUSDPERP", + * "GUSDSGD", + * "INJGUSDPERP", + * "LTCGUSDPERP", + * "XRPGUSDPERP", + * "USTUSD", + * "GOATGUSDPERP", + * "DAIUSD" + * ] + * } + */ + "application/json": components["schemas"]["FeePromos"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getCurrentOrderBook: { + parameters: { + query?: { + /** @description Limit the number of bid (offers to buy) price levels returned. Default is 50. May be 0 to return the full order book on this side. */ + limit_bids?: number; + /** @description Limit the number of ask (offers to sell) price levels returned. Default is 50. May be 0 to return the full order book on this side. */ + limit_asks?: number; + }; + header?: never; + path: { + /** + * @description Trading pair symbol

+ * + * `BTCUSD`, etc. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols). + */ + symbol: components["parameters"]["symbolParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The response will be two arrays. The bids and the asks are grouped by price, so each entry may represent multiple orders at that price. Each element of the array will be a JSON object. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "bids": [ + * { + * "price": "3607.85", + * "amount": "6.643373", + * "timestamp": "1547147541" + * } + * ], + * "asks": [ + * { + * "price": "3607.86", + * "amount": "14.68205084", + * "timestamp": "1547147541" + * } + * ] + * } + */ + "application/json": components["schemas"]["OrderBook"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listTrades: { + parameters: { + query?: { + /** @description Only return trades after this timestamp. See [**Timestamps**](/rest/~schemas#timestamp-type) for more information. If not present, will show the most recent trades. For backwards compatibility, you may also use the alias `since`. With timestamp, there is a 90-day hard limit. */ + timestamp?: components["schemas"]["TimestampType"]; + /** @description Only retuns trades that executed after this tid. since_tid trumps timestamp parameter which has no effect if provided too. You may set since_tid to zero to get the earliest available trade history data. */ + since_tid?: number; + /** @description The maximum number of trades to return. The default is 50. */ + limit_trades?: number; + /** @description Whether to display broken trades. False by default. Can be `1` or `true` to activate */ + include_breaks?: boolean; + }; + header?: never; + path: { + /** + * @description Trading pair symbol

+ * + * `BTCUSD`, etc. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols). + */ + symbol: components["parameters"]["symbolParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The response will be an array of JSON objects, sorted by timestamp, with the newest trade shown first. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "timestamp": 1547146811, + * "timestampms": 1547146811357, + * "tid": 5335307668, + * "price": "3610.85", + * "amount": "0.27413495", + * "exchange": "gemini", + * "type": "buy", + * "broken": true + * } + */ + "application/json": components["schemas"]["Trade"][]; + }; + }; + 400: components["responses"]["BadRequest"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listPrices: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Response is a list of objects, one for each pair. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example [ + * { + * "pair": "BTCUSD", + * "price": "9500.00", + * "percentChange24h": "5.23" + * }, + * { + * "pair": "ETHUSD", + * "price": "257.54", + * "percentChange24h": "4.85" + * }, + * { + * "pair": "BCHUSD", + * "price": "450.10", + * "percentChange24h": "-2.91" + * }, + * { + * "pair": "LTCUSD", + * "price": "79.50", + * "percentChange24h": "7.63" + * } + * ] + */ + "application/json": components["schemas"]["PriceFeedResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getFundingAmount: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description Trading pair symbol

+ * + * `BTCGUSDPERP`, etc. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols). + * @example BTCGUSDPERP + */ + symbol: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The response will be an object */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "symbol": "BTCGUSDPERP", + * "fundingDateTime": "2025-04-22T18:00:00.000Z", + * "fundingTimestampMilliSecs": 1745344800000, + * "nextFundingTimestamp": 1745348400000, + * "fundingAmount": -1.50991, + * "estimatedFundingAmount": -2.10595 + * } + */ + "application/json": components["schemas"]["FundingAmountResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getFundingAmountReportFile: { + parameters: { + query: { + /** + * @description Trading pair symbol

+ * + * `BTCGUSDPERP`, etc. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols). + */ + symbol: string; + /** @description Mandatory if `toDate` is specified, else optional. If empty, will only fetch records by numRows value. */ + fromDate?: string; + /** @description Mandatory if `fromDate` is specified, else optional. If empty, will only fetch records by numRows value. */ + toDate?: string; + /** @description If empty, default value '8760' */ + numRows?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The response will be an excel / csv file. filename=FundingAmount_{SYMBOL}.{xlsx,csv} */ + 200: { + headers: { + "Content-Disposition"?: string; + [name: string]: unknown; + }; + content: { + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": string; + "text/csv": string; + }; + }; + 400: components["responses"]["BadRequest"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + createNewOrder: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NewOrderRequest"]; + }; + }; + responses: { + /** @description Response will be the fields included in Order Status */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LimitOrderResponse"] | components["schemas"]["StopLimitOrderResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + cancelOrder: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CancelOrderRequest"]; + }; + }; + responses: { + /** @description Response will be the fields included in Order Status. If the order was already canceled, then the request will have no effect and the status will be returned. Note the *is_cancelled* node will have a value of 'true' */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CancelOrderResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + cancelAllActiveOrders: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CancelAllOrdersRequest"]; + }; + }; + responses: { + /** @description JSON response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "result": "ok", + * "details": { + * "cancelRejects": [], + * "cancelledOrders": [ + * 330429106, + * 330429079, + * 330429082 + * ] + * } + * } + */ + "application/json": components["schemas"]["CancelAllResult"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + cancelAllSessionOrders: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CancelAllOrdersBySessionRequest"]; + }; + }; + responses: { + /** @description JSON response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "result": "ok", + * "details": { + * "cancelRejects": [ + * 330429345 + * ], + * "cancelledOrders": [] + * } + * } + */ + "application/json": components["schemas"]["CancelAllResult"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getOrderStatus: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OrderStatusRequest"]; + }; + }; + responses: { + /** @description The order status */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Order"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listActiveOrders: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * @description The API endpoint path + * @example /v1/orders + */ + request: string; + /** The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + nonce: components["schemas"]["TimestampType"]; + /** + * @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts. + * @example primary + */ + account?: string; + }; + }; + }; + responses: { + /** @description The active orders */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Order"][]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listPastOrders: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The API endpoint `/v1/orders/history` */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description The symbol to retrieve orders for */ + symbol?: string; + /** + * @description The maximum number of orders to return. Default is 50, max is 500. + * @default 50 + */ + limit_orders?: number; + /** + * #/components/schemas/TimestampType + * @description In iso datetime with timezone format from that date you will get order history + */ + timestamp?: components["schemas"]["TimestampType"]; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Order"][]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listPastTrades: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["MyTradesRequest"]; + }; + }; + responses: { + /** @description The past trades */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MyTrade"][]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getTradingVolume: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * @description The API endpoint path + * @example /v1/tradevolume + */ + request: string; + /** The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + nonce: components["schemas"]["TimestampType"]; + /** + * @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts. + * @example primary + */ + account?: string; + }; + }; + }; + responses: { + /** @description The trade volume */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TradeVolume"][]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getAvailableBalances: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/balances", + * "nonce": "", + * "account": "primary", + * "showPendingBalances": false + * } + */ + "application/json": { + /** + * @description The API endpoint path + * @example /v1/balances + */ + request: string; + /** The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + nonce: components["schemas"]["TimestampType"]; + /** + * @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts. + * @example primary + */ + account: string; + /** + * @description Whether to include pending balances such as in-flight crypto deposits or withdrawals in the balances response. + * + * > **Note:** Setting this field to `true` will result in slower response times due to additional database lookups required to retrieve pending balance information. + * @default false + * @example false + */ + showPendingBalances?: boolean; + }; + }; + }; + responses: { + /** @description The account balances */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Balance"][]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getNotionalTradingVolume: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * @description The API endpoint path + * @example /v1/notionalvolume + */ + request: string; + /** The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + nonce: components["schemas"]["TimestampType"]; + /** + * @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts. + * @example primary + */ + account?: string; + }; + }; + }; + responses: { + /** @description The notional volume */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NotionalVolume"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getMarginAccount: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/margin/account", + * "nonce": "" + * } + */ + "application/json": { + /** @description The literal string "/v1/margin/account" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. */ + account?: string; + }; + }; + }; + responses: { + /** @description Margin account summary with risk statistics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "marginAssetValue": { + * "currency": "USD", + * "value": "10000.00" + * }, + * "availableCollateral": { + * "currency": "USD", + * "value": "8500.00" + * }, + * "notionalValue": { + * "currency": "USD", + * "value": "15000.00" + * }, + * "totalBorrowed": { + * "currency": "USD", + * "value": "5000.00" + * }, + * "leverage": "1.5", + * "buyingPower": { + * "currency": "USD", + * "value": "8500.00" + * }, + * "sellingPower": { + * "currency": "USD", + * "value": "8500.00" + * }, + * "liquidationRisk": { + * "lossPercentage": "0.1550", + * "liquidationPrice": { + * "currency": "USD", + * "value": "50000.00" + * } + * }, + * "interestRate": { + * "rate": "0.00001141552511", + * "interval": "hour" + * }, + * "reservedBuyOrders": { + * "currency": "USD", + * "value": "1000.00" + * }, + * "reservedSellOrders": { + * "currency": "USD", + * "value": "500.00" + * } + * } + */ + "application/json": components["schemas"]["MarginAccountSummary"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getMarginRates: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/margin/rates", + * "nonce": "" + * } + */ + "application/json": { + /** @description The literal string "/v1/margin/rates" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. */ + account?: string; + }; + }; + }; + responses: { + /** @description Current margin interest rates for all borrowable assets */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "rates": [ + * { + * "currency": "BTC", + * "borrowRate": "0.00001141552511", + * "borrowRateDaily": "0.00027397260264", + * "borrowRateAnnual": "0.1", + * "lastUpdated": 1700000000000 + * }, + * { + * "currency": "ETH", + * "borrowRate": "0.00001141552511", + * "borrowRateDaily": "0.00027397260264", + * "borrowRateAnnual": "0.1", + * "lastUpdated": 1700000000000 + * }, + * { + * "currency": "USD", + * "borrowRate": "0.00000913242009", + * "borrowRateDaily": "0.00021917808216", + * "borrowRateAnnual": "0.08", + * "lastUpdated": 1700000000000 + * } + * ] + * } + */ + "application/json": components["schemas"]["MarginRatesResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + previewMarginOrder: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/margin/order/preview" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** + * @description The trading pair symbol (e.g., "btcusd") + * @example btcusd + */ + symbol: string; + /** + * @description The order side + * @example buy + * @enum {string} + */ + side: "buy" | "sell"; + /** + * @description The order type + * @example limit + * @enum {string} + */ + type: "market" | "limit"; + /** + * Format: decimal + * @description The order amount in base currency (required for limit orders and sell market orders) + * @example 0.5 + */ + amount?: string; + /** + * Format: decimal + * @description The limit price (required for limit orders) + * @example 50000.00 + */ + price?: string; + /** + * Format: decimal + * @description Total spend in quote currency (required for buy market orders) + * @example 25000.00 + */ + totalSpend?: string; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. */ + account?: string; + }; + }; + }; + responses: { + /** @description Pre-order and post-order margin risk statistics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "preorder": { + * "marginAssetValue": { + * "currency": "USD", + * "value": "10000.00" + * }, + * "availableCollateral": { + * "currency": "USD", + * "value": "8500.00" + * }, + * "notionalValue": { + * "currency": "USD", + * "value": "15000.00" + * }, + * "totalBorrowed": { + * "currency": "USD", + * "value": "5000.00" + * }, + * "leverage": "1.5", + * "reservedBuyOrders": { + * "currency": "USD", + * "value": "0.00" + * }, + * "reservedSellOrders": { + * "currency": "USD", + * "value": "0.00" + * }, + * "buyingPower": { + * "currency": "USD", + * "value": "8500.00" + * }, + * "sellingPower": { + * "currency": "USD", + * "value": "8500.00" + * } + * }, + * "postorder": { + * "marginAssetValue": { + * "currency": "USD", + * "value": "10000.00" + * }, + * "availableCollateral": { + * "currency": "USD", + * "value": "6000.00" + * }, + * "notionalValue": { + * "currency": "USD", + * "value": "40000.00" + * }, + * "totalBorrowed": { + * "currency": "USD", + * "value": "30000.00" + * }, + * "leverage": "4.0", + * "reservedBuyOrders": { + * "currency": "USD", + * "value": "0.00" + * }, + * "reservedSellOrders": { + * "currency": "USD", + * "value": "0.00" + * }, + * "buyingPower": { + * "currency": "USD", + * "value": "6000.00" + * }, + * "sellingPower": { + * "currency": "USD", + * "value": "6000.00" + * }, + * "liquidationRisk": { + * "lossPercentage": "0.6000", + * "liquidationPrice": { + * "currency": "USD", + * "value": "30000.00" + * } + * } + * } + * } + */ + "application/json": components["schemas"]["MarginOrderPreview"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + sendHeartbeat: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/heartbeat", + * "nonce": "" + * } + */ + "application/json": components["schemas"]["Heartbeat"]; + }; + }; + responses: { + /** @description The heartbeat was received successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "result": "ok" + * } + */ + "application/json": { + /** + * @description ok + * @example ok + */ + result?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + wrapOrder: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path: { + /** + * @description Trading pair symbol

+ * + * `BTCUSD`, etc. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols). + */ + symbol: components["parameters"]["symbolParam"]; + }; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/wrap/GUSDUSD", + * "nonce": "", + * "amount": "1", + * "side": "buy", + * "client_order_id": "4ac6f45f-baf1-40f8-83c5-001e3ea73c7f" + * } + */ + "application/json": { + /** @description The literal string "/v1/wrap/symbol" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description The amount to wrap */ + amount: string; + /** + * @description "buy" or "sell" + * @enum {string} + */ + side?: "buy" | "sell"; + /** @description A client-specified order id */ + client_order_id?: string; + /** @description Required for Master API keys. The name of the account within the subaccount group. */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "orderId": 429135395, + * "pair": "GUSDUSD", + * "price": "1", + * "priceCurrency": "USD", + * "side": "buy", + * "quantity": "1", + * "quantityCurrency": "GUSD", + * "totalSpend": "1", + * "totalSpendCurrency": "USD", + * "fee": "0", + * "feeCurrency": "USD", + * "depositFee": "0", + * "depositFeeCurrency": "USD" + * } + */ + "application/json": { + /** @description The order ID */ + orderId?: string; + /** @description Trading pair symbol */ + pair?: string; + /** @description The price of the order */ + price?: string; + /** @description The currency in which the order is priced */ + priceCurrency?: string; + /** @description Either "buy" or "sell" */ + side?: string; + /** @description The amount that was executed */ + quantity?: string; + /** @description The currency label for the quantity field */ + quantityCurrency?: string; + /** @description Total quantity spent for the order */ + totalSpend?: string; + /** @description Currency of the totalSpend */ + totalSpendCurrency?: string; + /** @description The amount charged */ + fee?: string; + /** @description Currency that the fee was paid in */ + feeCurrency?: string; + /** @description The deposit fee quantity */ + depositFee?: string; + /** @description Currency in which depositFee is taken */ + depositFeeCurrency?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getNotionalBalances: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path: { + /** @description Either a fiat currency, e.g. `usd` or `gbp`, or a supported crypto-currency, e.g. `gusd`, `btc`, `eth`, `aave`, etc. */ + currency: components["parameters"]["currencyParam"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/notionalbalances/currency" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Required for Master API keys. The name of the account within the subaccount group. */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example [ + * { + * "currency": "BTC", + * "amount": "1154.62034001", + * "amountNotional": "10386000.59", + * "available": "1129.10517279", + * "availableNotional": "10161000.71", + * "availableForWithdrawal": "1129.10517279", + * "availableForWithdrawalNotional": "10161000.71" + * }, + * { + * "currency": "USD", + * "amount": "18722.79", + * "amountNotional": "18722.79", + * "available": "14481.62", + * "availableNotional": "14481.62", + * "availableForWithdrawal": "14481.62", + * "availableForWithdrawalNotional": "14481.62" + * }, + * { + * "currency": "ETH", + * "amount": "20124.50369697", + * "amountNotional": "100621.31", + * "available": "20124.50369697", + * "availableNotional": "100621.31", + * "availableForWithdrawal": "20124.50369697", + * "availableForWithdrawalNotional": "100621.31" + * } + * ] + */ + "application/json": components["schemas"]["NotionalBalance"][]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listDepositAddresses: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path: { + /** @description Can be `bitcoin`, `ethereum`, `bitcoincash`, `litecoin`, `zcash`, `filecoin`, `dogecoin`, `tezos`, `solana`, `polkadot`, `avalanche`, `cosmos`, or `xrpl` */ + network: components["parameters"]["networkParam"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/addresses/network" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Only returns addresses created on or after this timestamp */ + timestamp?: components["schemas"]["TimestampType"]; + /** @description Required for Master API keys. The name of the account within the subaccount group. */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Address"][]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + createNewDepositAddress: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path: { + /** @description Can be `bitcoin`, `ethereum`, `bitcoincash`, `litecoin`, `zcash`, `filecoin`, `dogecoin`, `tezos`, `solana`, `polkadot`, `avalanche`, `cosmos`, or `xrpl` */ + network: components["parameters"]["networkParam"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/deposit/network/newAddress" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description A label for the address */ + label?: string; + /** @description Whether to generate a legacy P2SH-P2PKH litecoin address. False by default. */ + legacy?: boolean; + /** @description Required for Master API keys. The name of the account within the subaccount group. */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Address"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listPastTransfers: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v2/transfers" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Currency code, see symbols and minimums */ + currency?: string; + /** @description Filter transfers by blockchain network (e.g., `ethereum`, `solana`, `arbitrum`, `optimism`, `base`, `avalanche`) */ + network?: string; + /** @description Only return transfers after this timestamp */ + timestamp?: components["schemas"]["TimestampType"]; + /** @description The maximum number of transfers to return. The default is 10 and the maximum is 50. */ + limit_transfers?: number; + /** @description Required for Master API keys. The name of the account within the subaccount group. */ + account?: string; + /** @description Whether to display completed deposit advances. True by default. */ + show_completed_deposit_advances?: boolean; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["V2Transfer"][]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listCustodyFeeTransfers: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/custodyaccountfees" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Only return Custody fee records on or after this timestamp */ + timestamp?: components["schemas"]["TimestampType"]; + /** @description The maximum number of Custody fee records to return. The default is 10 and the maximum is 50. */ + limit_transfers?: number; + /** @description Required for Master API keys. The name of the account within the subaccount group. */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description Time of Custody fee record in milliseconds */ + txTime?: number; + /** @description The fee amount charged */ + feeAmount?: string; + /** @description Currency that the fee was paid in */ + feeCurrency?: string; + /** @description Custody fee event id */ + eid?: number; + /** @description Custody fee event type */ + eventType?: string; + }[]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getGasFeeEstimation: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path: { + /** + * @description The blockchain network for the withdrawal (e.g. `ethereum`, `bitcoin`, `solana`) + * @example ethereum + */ + network: string; + /** + * @description The currency code for the withdrawal (e.g. `eth`, `btc`, `sol`, `usdc`) + * @example eth + */ + ticker: string; + }; + cookie?: never; + }; + /** @description Sample payload for ETH fee estimation on Ethereum network */ + requestBody: { + content: { + "application/json": components["schemas"]["FeeEstimateV2Request"]; + }; + }; + responses: { + /** @description Successful fee estimation response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeeEstimateV2Response"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + withdrawCryptoFunds: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path: { + /** @description Can be `bitcoin`, `ethereum`, `bitcoincash`, `litecoin`, `zcash`, `filecoin`, `dogecoin`, `tezos`, `solana`, `polkadot`, `avalanche`, `cosmos`, or `xrpl` */ + network: components["parameters"]["networkParam"]; + /** + * @description The cryptocurrency ticker code (e.g., `btc`, `eth`, `usdc`). See [Symbols and minimums](/market-data/symbols-and-minimums). + * @example eth + */ + ticker: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The destination address for the withdrawal */ + address: string; + /** @description The amount to withdraw */ + amount: string; + /** @description Required for certain networks that use memos (e.g., Solana, XRP, Cosmos). The destination tag or memo for the withdrawal. */ + memo?: string; + /** + * Format: uuid + * @description A unique UUID for idempotent withdrawals. If provided, duplicate requests with the same `clientTransferId` will not create additional withdrawals. + */ + clientTransferId?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description A unique ID for the withdrawal */ + withdrawalId?: string; + /** @description Standard string format of the withdrawal destination address */ + address?: string; + /** @description The withdrawal amount */ + amount?: string; + /** @description The currency code of the withdrawn asset */ + currency?: string; + /** @description The fee charged for the withdrawal */ + fee?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + createNewClearingOrder: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/clearing/new" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description The trading pair */ + symbol: string; + /** @description The amount to trade */ + amount: string; + /** @description The price */ + price: string; + /** + * @description The direction of the trade + * @enum {string} + */ + side: "buy" | "sell"; + /** @description The counterparty ID */ + counterparty_id?: string; + /** @description The number of hours until the order expires */ + expires_in_hrs?: number; + /** @description Required for Master API keys. The name of the account within the subaccount group. */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "result": "AwaitConfirm", + * "clearing_id": "0OQGOZXW" + * } + */ + "application/json": components["schemas"]["ClearingOrder"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getClearingOrder: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/clearing/status" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description The clearing ID */ + clearing_id: string; + /** @description Required for Master API keys. The name of the account within the subaccount group. */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "result": "ok", + * "status": "Confirmed" + * } + */ + "application/json": components["schemas"]["ClearingOrder"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + cancelClearingOrder: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/clearing/cancel" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description The clearing ID */ + clearing_id: string; + /** @description Required for Master API keys. The name of the account within the subaccount group. */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description Status of the cancel operation */ + result?: string; + /** @description Detailed description of the result */ + details?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + confirmClearingOrder: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/clearing/confirm" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description The clearing ID */ + clearing_id: string; + /** @description The trading pair */ + symbol: string; + /** @description The amount to trade */ + amount: string; + /** @description The price */ + price: string; + /** + * @description The direction of the trade + * @enum {string} + */ + side: "buy" | "sell"; + /** @description Required for Master API keys. The name of the account within the subaccount group. */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description Status of the confirmation operation */ + result?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listClearingOrders: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/clearing/list" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Trading pair */ + symbol?: string; + /** @description counterparty_id or counterparty_alias */ + counterparty?: string; + /** + * @description "buy" or "sell" + * @enum {string} + */ + side?: "buy" | "sell"; + /** @description UTC timestamp. Requires `expiration_end` if set */ + expiration_start?: components["schemas"]["TimestampType"]; + /** @description UTC timestamp. Requires `expiration_start` if set */ + expiration_end?: components["schemas"]["TimestampType"]; + /** @description UTC timestamp. Requires `submission_end` if set */ + submission_start?: components["schemas"]["TimestampType"]; + /** @description UTC timestamp. Requires `submission_start` if set */ + submission_end?: components["schemas"]["TimestampType"]; + /** @description Default value false if not set */ + funded?: boolean; + /** @description Filter by status */ + status?: string; + /** @description Only return orders after this timestamp */ + timestamp?: components["schemas"]["TimestampType"]; + /** @description The maximum number of orders to return */ + limit_orders?: number; + /** @description Required for Master API keys. The name of the account within the subaccount group. */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description Status of the operation */ + result?: string; + orders?: { + /** @description A unique identifier for the clearing order */ + clearing_id?: string; + /** @description Only provided if order was submitted with it */ + order_id?: string; + /** @description A symbol that corresponds with a counterparty */ + counterparty_id?: string; + /** @description Counterparty alias */ + counterparty_alias?: string; + /** @description A symbol that corresponds with a broker id */ + broker_id?: string; + /** @description Trading pair */ + symbol?: string; + /** + * @description "buy" or "sell" + * @enum {string} + */ + side?: "buy" | "sell"; + /** + * Format: decimal + * @description The price the clearing order was executed at + */ + price?: number; + /** + * Format: decimal + * @description The amount that was executed + */ + quantity?: number; + /** @description A description of the status of the order */ + status?: string; + /** @description UTC timestamp */ + submission?: components["schemas"]["TimestampType"]; + /** @description UTC timestamp */ + expiration?: components["schemas"]["TimestampType"]; + }[]; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listClearingBrokers: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/clearing/broker/list" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Trading pair */ + symbol?: string; + /** @description UTC timestamp. Requires `expiration_end` if set */ + expiration_start?: components["schemas"]["TimestampType"]; + /** @description UTC timestamp. Requires `expiration_start` if set */ + expiration_end?: components["schemas"]["TimestampType"]; + /** @description UTC timestamp. Requires `submission_end` if set */ + submission_start?: components["schemas"]["TimestampType"]; + /** @description UTC timestamp. Requires `submission_start` if set */ + submission_end?: components["schemas"]["TimestampType"]; + /** @description Default value false if not set */ + funded?: boolean; + /** @description Filter by status */ + status?: string; + /** @description Only return orders after this timestamp */ + timestamp?: components["schemas"]["TimestampType"]; + /** @description The maximum number of orders to return */ + limit_orders?: number; + /** @description Required for Master API keys. The name of the account within the subaccount group. */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description Status of the operation */ + result?: string; + orders?: { + /** @description A unique identifier for the clearing order */ + clearing_id?: string; + /** @description Source counterparty id */ + source_counterparty_id?: string; + /** @description Only provided if order was submitted with it */ + source_order_id?: string; + /** @description Only provided if target counterparty was already set */ + target_counterparty_id?: string; + /** @description Only provided if target counterparty set this field */ + target_order_id?: string; + /** @description Trading pair */ + symbol?: string; + /** + * @description "buy" or "sell" + * @enum {string} + */ + source_side?: "buy" | "sell"; + /** + * Format: decimal + * @description The price the clearing order was executed at + */ + price?: number; + /** + * Format: decimal + * @description The amount that was executed + */ + quantity?: number; + /** @description A description of the status of the order */ + status?: string; + /** @description UTC timestamp */ + submission?: number; + /** @description UTC timestamp */ + expiration?: number; + }[]; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + createNewBrokerOrder: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/clearing/broker/new" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description A symbol that corresponds with the counterparty sourcing the clearing trade */ + source_counterparty_id: string; + /** @description A symbol that corresponds with the counterparty where the clearing trade is targeted */ + target_counterparty_id: string; + /** @description The [symbol](/market-data/symbols-and-minimums) of the order */ + symbol: string; + /** + * Format: decimal + * @description Quoted decimal amount to purchase + */ + amount: string; + /** + * Format: float + * @description The number of hours before the trade expires. Your counterparty will need to confirm the order before this time expires. + */ + expires_in_hrs: number; + /** + * Format: decimal + * @description Quoted decimal amount to spend per unit + */ + price: string; + /** + * @description "buy" or "sell". This side will be assigned to the `source_counterparty_id`. The opposite side will be sent to the `target_counterparty_id` + * @enum {string} + */ + side: "buy" | "sell"; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the broker account on which to place the order. Only available for exchange accounts. */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "result": "AwaitSourceTargetConfirm", + * "clearing_id": "8EM7NVXD" + * } + */ + "application/json": { + /** @description Will return `AwaitSourceTargetConfirm`, meaning the order is waiting for both the source and the target parties to confirm the order */ + result?: string; + /** @description A unique identifier for the clearing order. */ + clearing_id?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listClearingTrades: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/clearing/trades" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Only return transfers on or after this timestamp in nanos */ + timestamp_nanos?: number; + /** @description The maximum number of clearing trades to return. The default is 100 and the maximum is 300. */ + limit_per_account?: number; + /** @description Only required when using a master api-key. The name of the account within the subaccount group. */ + account?: string; + /** @description The trading pair */ + symbol?: string; + /** @description Only return trades after this timestamp */ + timestamp?: components["schemas"]["TimestampType"]; + /** @description The maximum number of trades to return */ + limit_trades?: number; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + results?: { + /** @description A account that corresponds with the counterparty sourcing the clearing trade */ + sourceAccount?: string; + /** @description A account that corresponds with the counterparty where the clearing trade is targeted */ + targetAccount?: string; + /** @description The trading pair of the clearing trade */ + pair?: string; + /** + * @description "buy" or "sell" + * @enum {string} + */ + sourceSide?: "buy" | "sell"; + /** @description The price the clearing order was executed at */ + price?: string; + /** @description The amount that was executed */ + quantity?: string; + /** @description The clearing ID */ + clearingId?: string; + /** @description A description of the status of the order */ + status?: string; + /** @description The time that the clearing trade expires */ + expirationTimeMs?: components["schemas"]["TimestampType"]; + /** @description The time that the clearing trade was created */ + createdMs?: components["schemas"]["TimestampType"]; + /** @description The last time the clearing trade was updated */ + lastUpdatedMs?: components["schemas"]["TimestampType"]; + /** @description Broker trade */ + hasBroker?: boolean; + /** @description Broker was notified */ + wasNotified?: boolean; + }[]; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getInstantQuote: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/instant/quote/" */ + request: string; + /** + * @description "buy" or "sell" + * @enum {string} + */ + side: "buy" | "sell"; + /** @description The [symbol](/market-data/symbols-and-minimums) for the order. Instant includes order books denominated in a [supported currency](https://support.gemini.com/hc/en-us/articles/360000032663-Does-Gemini-support-fiat-currencies-other-than-USD), as `CCY2` */ + symbol: string; + nonce: components["schemas"]["Nonce"]; + /** @description Quoted decimal amount to spend on the order. Must comply with [stated minimums](/market-data/symbols-and-minimums). The `totalSpend` will be `CCY2` in `buy` orders and `CCY1` in `sell` orders. */ + totalSpend: string; + /** @description uuid provided as `bankId` in [Payment Methods API](/fund-management#list-payment-methods) */ + paymentMethodUuid?: string; + /** @description Method used to specify payment method in `buy` order. Can be "AccountBalancePaymentType" to use funds available in USD balance held on Gemini, "BankAccountType" to initial an ACH from a linked bank account, or "CardAccountType" to use a linked debit card to fund the purchase. */ + paymentMethodType?: string; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts. */ + account?: string; + }; + }; + }; + responses: { + /** @description Sample Responses */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["InstantQuote"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + executeInstantOrder: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/instant/execute" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description The symbol for the order. */ + symbol: string; + /** + * @description "buy" or "sell" + * @enum {string} + */ + side: "buy" | "sell"; + /** @description The quantity of the asset bought or sold. quantity must match quantity returned in the quote */ + quantity: string; + /** @description The price from the quote. price must match price returned in the quote */ + price: string; + /** @description The fee for the order. fee must match fee returned in the quote */ + fee: string; + /** @description Unique ID for the quote. quoteId must match quoteId returned in the quote */ + quoteId: number; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts. */ + account?: string; + }; + }; + }; + responses: { + /** @description JSON response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description The ID for the executed order */ + orderId?: number; + /** @description The symbol for the order. */ + pair?: string; + /** @description The price at which the order was executed */ + price?: string; + /** @description The currency in which the order is priced. Matches `CCY2` in the symbol */ + priceCurrency?: string; + /** @description Either "buy" or "sell" */ + side?: string; + /** @description The quantity of the asset bought or sold */ + quantity?: string; + /** @description The currency label for the `quantity` field. */ + quantityCurrency?: string; + /** @description Total quantity to spend for the order. Will be the sum inclusive of all fees and amount to be traded. */ + totalSpend?: string; + /** @description Currency of the `totalSpend` to be spent on the order */ + totalSpendCurrency?: string; + /** @description The fee quantity charged for the order */ + fee?: string; + /** @description The currency label for the fee. */ + feeCurrency?: string; + /** @description The deposit fee quantity. Will be applied if a debit card is used for the order. Will return 0 if there is no `depositFee` */ + depositFee?: string; + /** @description Currency in which `depositFee` is taken */ + depositFeeCurrency?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + addBank: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/payments/addbank", + * "nonce": "", + * "accountnumber": "account-number-string", + * "routing": "routing-number-string", + * "type": "checking", + * "name": "Satoshi Nakamoto Checking" + * } + */ + "application/json": { + /** @description The literal string "/v1/payments/addbank" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Account number of bank account to be added */ + accountnumber: string; + /** @description Routing number of bank account to be added */ + routing: string; + /** + * @description Type of bank account to be added. Accepts `checking` or `savings` + * @enum {string} + */ + type: "checking" | "savings"; + /** @description The name of the bank account as shown on your account statements */ + name: string; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Master API keys can get all account names using the [Get Accounts endpoint](/rest/account-administration#list-accounts-in-group). */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "referenceId": "BankAccountRefId(18428)" + * } + */ + "application/json": { + /** @description Reference ID for the new bank addition request. Once received, send in a wire from the requested bank account to verify it and enable withdrawals to that account. */ + referenceId?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + addBankCAD: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/payments/addbank/cad", + * "nonce": "", + * "swiftcode": "swift-code-string", + * "accountnumber": "account-number-string", + * "institutionnumber": "institution-number-string", + * "branchnumber": "branch-number-string", + * "type": "checking", + * "name": "Satoshi Nakamoto Checking", + * "account": "account-string" + * } + */ + "application/json": { + /** @description The literal string "/v1/payments/addbank/cad" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description The account SWIFT code */ + swiftcode: string; + /** @description Account number of bank account to be added */ + accountNumber: string; + /** @description The institution number of the account - optional but recommended. */ + institutionNumber?: string; + /** @description The branch number - optional but recommended. */ + branchnnumber?: string; + /** + * @description Type of bank account to be added. Accepts `checking` or `savings` + * @enum {string} + */ + type: "checking" | "savings"; + /** @description The name of the bank account as shown on your account statements */ + name: string; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Master API keys can get all account names using the [Get Accounts endpoint](/rest/account-administration#list-accounts-in-group). */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "result": "OK" + * } + */ + "application/json": { + /** @description Status of the request. "OK" indicates the account has been created successfully. */ + result?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listPaymentMethods: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/payments/methods", + * "account": "primary", + * "nonce": "" + * } + */ + "application/json": { + /** @description The literal string "/v1/payments/methods" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Master API keys can get all account names using the [Get Accounts endpoint](/rest/account-administration#list-accounts-in-group). */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "balances": [ + * { + * "type": "exchange", + * "currency": "USD", + * "amount": "50893484.26", + * "available": "50889972.01", + * "availableForWithdrawal": "50889972.01" + * } + * ], + * "banks": [ + * { + * "bank": "Jpmorgan Chase Bank Checking - 1111", + * "bankId": "97631a24-ca40-4277-b3d5-38c37673d029" + * } + * ] + * } + */ + "application/json": { + /** @description Array of JSON objects with available fiat currencies and their balances. */ + balances?: { + /** @description Account type. Will always be `exchange` */ + type?: string; + /** @description Symbol for fiat balance. */ + currency?: string; + /** @description Total account balance for currency. */ + amount?: string; + /** @description Total amount available for trading */ + available?: string; + /** @description Total amount available for withdrawal */ + availableForWithdrawal?: string; + }[]; + /** @description Array of JSON objects with banking information */ + banks?: { + /** @description Name of bank account */ + bank?: string; + /** @description Unique identifier for bank account */ + bankId?: string; + }[]; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getAccountDetail: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/account", + * "account": "primary", + * "nonce": "" + * } + */ + "application/json": { + /** @description The literal string "/v1/account" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Master API keys can get all account names using the [Get Accounts endpoint](/rest/account-administration#list-accounts-in-group). */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "account": { + * "accountName": "Primary", + * "shortName": "primary", + * "type": "exchange", + * "created": "1498245007981" + * }, + * "users": [ + * { + * "name": "Satoshi Nakamoto", + * "lastSignIn": "2020-07-21T13:37:39.453Z", + * "status": "Active", + * "countryCode": "US", + * "isVerified": true + * }, + * { + * "name": "Gemini Support", + * "lastSignIn": "2018-07-11T20:04:36.073Z", + * "status": "Suspended", + * "countryCode": "US", + * "isVerified": false + * } + * ], + * "memo_reference_code": "GEMPJBRDZ", + * "virtual_account_number": "123456" + * } + */ + "application/json": { + /** @description Contains information on the requested account */ + account?: { + /** @description The name of the account provided upon creation. Will default to `Primary` */ + accountName?: string; + /** @description Nickname of the specific account (will take the name given, remove all symbols, replace all " " with "-" and make letters lowercase) */ + shortName?: string; + /** @description The type of account. Will return either `exchange` or `custody` */ + type?: string; + /** @description The timestamp of account creation, displayed as number of milliseconds since 1970-01-01 UTC. This will be transmitted as a JSON number */ + created?: components["schemas"]["TimestampType"]; + }; + /** @description Contains an array of JSON objects with user information for the requested account */ + users?: { + /** @description Full legal name of the user */ + name?: string; + /** @description Timestamp of the last sign for the user. Formatted as yyyy-MM-dd'T'HH:mm:ss.SSS'Z' */ + lastSignIn?: string; + /** @description Returns user status. Will inform of `active` users or otherwise not active */ + status?: string; + /** @description 2 Letter country code indicating residence of user */ + countryCode?: string; + /** @description Returns verification status of user */ + isVerified?: boolean; + }[]; + /** @description Returns wire memo reference code for linked bank account */ + memo_reference_code?: string; + /** @description Virtual account number for the account. Only populated if applicable for the account */ + virtual_account_number?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listApprovedAddresses: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path: { + /** @description Can be `bitcoin`, `ethereum`, `bitcoincash`, `litecoin`, `zcash`, `filecoin`, `dogecoin`, `tezos`, `solana`, `polkadot`, `avalanche`, `cosmos`, or `xrpl` */ + network: components["parameters"]["networkParam"]; + }; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/approvedAddresses/account/ethereum", + * "nonce": "", + * "account": "primary" + * } + */ + "application/json": { + /** @description The literal string "/v1/approvedAddresses/account/:network" where `:network` can be `bitcoin`, `ethereum`, `bitcoincash`, `litecoin`, `zcash`, `filecoin`, `dogecoin`, `tezos`, `solana`, `polkadot`, `avalanche`, `cosmos`, or `xrpl` */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to view the approved address list. */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "approvedAddresses": [ + * { + * "network": "ethereum", + * "scope": "account", + * "label": "api_added_ETH_address", + * "status": "pending-time", + * "createdAt": "1602692572349", + * "address": "0x0000000000000000000000000000000000000000" + * }, + * { + * "network": "ethereum", + * "scope": "group", + * "label": "api_added_ETH_address", + * "status": "pending-time", + * "createdAt": "1602692542296", + * "address": "0x0000000000000000000000000000000000000000" + * }, + * { + * "network": "ethereum", + * "scope": "group", + * "label": "hardware_wallet", + * "status": "active", + * "createdAt": "1602087433270", + * "address": "0xA63123350Acc8F5ee1b1fBd1A6717135e82dBd28" + * }, + * { + * "network": "ethereum", + * "scope": "account", + * "label": "hardware_wallet", + * "status": "active", + * "createdAt": "1602086832986", + * "address": "0xA63123350Acc8F5ee1b1fBd1A6717135e82dBd28" + * } + * ] + * } + */ + "application/json": { + /** @description Array of approved addresses on both the account and group level. */ + approvedAddresses?: components["schemas"]["ApprovedAddress"][]; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + createNewApprovedAddress: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path: { + /** @description Can be `bitcoin`, `ethereum`, `bitcoincash`, `litecoin`, `zcash`, `filecoin`, `dogecoin`, `tezos`, `solana`, `polkadot`, `avalanche`, `cosmos`, or `xrpl` */ + network: components["parameters"]["networkParam"]; + }; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/approvedAddresses/ethereum/request", + * "nonce": "", + * "address": "0x0000000000000000000000000000000000000000", + * "label": "api_added_ETH_address", + * "account": "primary" + * } + */ + "application/json": { + /** @description The literal string "/v1/approvedAddresses/:network/request" where `:network` can be `bitcoin`, `ethereum`, `bitcoincash`, `litecoin`, `zcash`, `filecoin`, `dogecoin`, `tezos`, `solana`, `polkadot`, `avalanche`, `cosmos`, or `xrpl` */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description A string of the address to be added to the approved address list. */ + address: string; + /** @description The label of the approved address. */ + label: string; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to add the approved address. */ + account?: string; + /** @description it would be present if applicable, it will be present for cosmos address. */ + memo?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "message": "Approved address addition is now waiting a 7-day approval hold before activation." + * } + */ + "application/json": { + /** @description Upon successful request, the endpoint will return a string indicating the 7-day approval hold period has begun. */ + message?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + removeApprovedAddress: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path: { + /** @description Can be `bitcoin`, `ethereum`, `bitcoincash`, `litecoin`, `zcash`, `filecoin`, `dogecoin`, `tezos`, `solana`, `polkadot`, `avalanche`, `cosmos`, or `xrpl` */ + network: components["parameters"]["networkParam"]; + }; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/approvedAddresses/ethereum/remove", + * "nonce": "", + * "address": "0x0000000000000000000000000000000000000000" + * } + */ + "application/json": { + /** @description The literal string "/v1/approvedAddresses/:network/remove" where `:network` can be `bitcoin`, `ethereum`, `bitcoincash`, `litecoin`, `zcash`, `filecoin`, `dogecoin`, `tezos`, `solana`, `polkadot`, `avalanche`, `cosmos`, or `xrpl` */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description A string of the address to be removed from the approved address list. */ + address: string; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to remove the approved address. */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "message": "0x0000000000000000000000000000000000000000 removed from group pending-time approved addresses." + * } + */ + "application/json": { + /** @description Upon successful request, the endpoint will return a string indicating the address and whether it was removed from the group-level or account-level approved address list. */ + message?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + createNewAccount: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/account/create" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description A unique name for the new account */ + name: string; + /** @description Either `exchange` or `custody` is accepted. Will generate an exchange account if `exchange` or parameter is missing. Will generate a custody account if `custody`. */ + type?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "account": "my-secondary-account", + * "type": "exchange" + * } + */ + "application/json": { + /** @description Account reference string for use in APIs based off the provided `name` field */ + account?: string; + /** @description Will return the type of account generated. `exchange` if an exchange account was created, `custody` if a custody account was created */ + type?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + renameAccount: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/account/rename". */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Only required when using a master api-key. The shortname of the account within the subaccount group. Master API keys can get all account shortnames from the `account` field returned by the [Get Accounts endpoint](/rest/account-administration#list-accounts-in-group). */ + account?: string; + /** @description A unique name for the new account. If not provided, name will not change. */ + newName?: string; + /** @description A unique shortname for the new account. If not provided, shortname will not change. */ + newAccount?: string; + }; + }; + }; + responses: { + /** @description An element containing the updated name of the account. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "name": "My Exchange Account New Name", + * "account": "my-exchange-account-new-name" + * } + */ + "application/json": { + /** @description New name for the account based off the provided `newName` field. Only returned if `newName` was provided in the request. */ + name?: string; + /** @description New shortname for the account based off the provided `newAccount` field. Only returned if `newAccount` was provided in the request. */ + account?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listAccountsInGroup: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/account/list", + * "nonce": "", + * "limit_accounts": 100, + * "timestamp": 1632485834721 + * } + */ + "application/json": { + /** @description The literal string "/v1/account/list" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description The maximum number of accounts to return. Maximum and default values are both 500. */ + limit_accounts?: number; + /** @description Only return accounts created on or before the supplied timestamp. If not provided, the 500 most recently created accounts are returned. */ + timestamp?: components["schemas"]["TimestampType"]; + }; + }; + }; + responses: { + /** @description The response will be a JSON object containing all accounts within the master group */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example [ + * { + * "name": "Primary", + * "account": "primary", + * "type": "exchange", + * "counterparty_id": "EMONNYXH", + * "created": 1495127793000, + * "status": "open" + * }, + * { + * "name": "My Custody Account", + * "account": "my-custody-account", + * "type": "custody", + * "counterparty_id": null, + * "created": 1565970772000, + * "status": "open" + * }, + * { + * "name": "Other exchange account!", + * "account": "other-exchange-account", + * "type": "exchange", + * "counterparty_id": "EMONNYXK", + * "created": 1565970772000, + * "status": "closed" + * } + * ] + */ + "application/json": { + /** @description The name of the account provided upon creation */ + name?: string; + /** @description Nickname of the specific account (will take the name given, remove all symbols, replace all " " with "-" and make letters lowercase) */ + account?: string; + /** @description Either "exchange" or "custody" depending on type of account */ + type?: string; + /** @description The Gemini clearing counterparty ID associated with the API key making the request. Will return `None` for custody accounts */ + counterparty_id?: string; + /** @description The timestamp of account creation, displayed as number of milliseconds since 1970-01-01 UTC. This will be transmitted as a JSON number */ + created?: components["schemas"]["TimestampType"]; + /** @description Either "open" or "closed" */ + status?: string; + }[]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + transferBetweenAccounts: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path: { + /** @description Either a fiat currency, e.g. `usd` or `gbp`, or a supported crypto-currency, e.g. `gusd`, `btc`, `eth`, `aave`, etc. */ + currency: components["parameters"]["currencyParam"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The string `/v1/account/transfer/:currency` where `:currency` is replaced with either `usd` or a supported crypto-currency, e.g. `gusd`, `btc`, `eth`, `aave`, etc. See [Symbols and minimums](/market-data/symbols-and-minimums). */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Nickname of the account you are transferring from. Use the [Get Accounts endpoint](/rest/account-administration#list-accounts-in-group) to get all account names in the group. */ + sourceAccount: string; + /** @description Nickname of the account you are transferring to. Use the [Get Accounts endpoint](/rest/account-administration#list-accounts-in-group) to get all account names in the group. */ + targetAccount: string; + /** @description Quoted decimal amount to withdraw */ + amount: string; + /** @description A unique identifier for the internal transfer, in uuid4 format */ + clientTransferId?: string; + /** @description Unique ID of the requested withdrawal. */ + withdrawalId?: string; + }; + }; + }; + responses: { + /** @description JSON response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "fromAccount": "my-account", + * "toAccount": "my-other-account", + * "amount": "1", + * "currency": "Bitcoin", + * "uuid": "9c153d64-83ba-4532-a159-ebe3f6797766", + * "message": "Success, transfer completed." + * } + */ + "application/json": { + /** @description Source account where funds are sent from */ + fromAccount?: string; + /** @description Target account to receive funds in the internal transfer */ + toAccount?: string; + /** @description Quantity of assets being transferred */ + amount?: string; + /** @description Fee taken for the transfer. Exchange account to exchange account transfers will always be free and will not be deducted from the free monthly transfer amount for that account. */ + fee?: string; + /** @description Display Name. Can be `Bitcoin`, `Ether`, `Zcash`, `Litecoin`, `Dollar`, etc. */ + currency?: string; + /** @description _Excludes_ exchange to exchange. Unique ID of the requested withdrawal */ + withdrawalId?: string; + /** @description _Only_ for exchange to exchange. Unique ID of the completed transfer */ + uuid?: string; + /** @description Message describing result of withdrawal. Will inform of success, failure, or pending blockchain transaction. */ + message?: string; + /** @description _Only for Ethereum network transfers. Excludes exchange to exchange transfers_. Transaction hash for ethereum network transfer. */ + txHash?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getTransactionHistory: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/transactions", + * "nonce": "", + * "timestamp_nanos": 1630382206000000000, + * "limit": 50, + * "continuation_token": "daccgrp_123421:n712621873886999872349872349:a71289723498273492374978424:m2:iForward" + * } + */ + "application/json": { + /** @description The literal string "/v1/transactions" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Only return transfers on or after this timestamp in nanos. If this is defined, do not define “continuation_token”. */ + timestamp_nanos?: components["schemas"]["TimestampType"]; + /** + * @description The maximum number of transfers to return. The default is 100 and the maximum is 300. + * @default 100 + */ + limit?: number; + /** @description For subsequent requests, use the returned `continuation_token` value for next page. If this is defined, do not define “timestamp_nanos”. */ + continuation_token?: string; + }; + }; + }; + responses: { + /** @description The response will be an array of JSON objects, sorted by trade and transfer as well as a continuationToken to be used in subsequent requests. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description Results will contain either a list of Trade or Transfer responses */ + results?: components["schemas"]["Transaction"][]; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + revokeOAuthToken: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The literal string "/v1/oauth/revokeByToken" */ + request: string; + }; + }; + }; + responses: { + /** @description An object that indicates the access_token has been revoked. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description A message that indicates the token has been revoked for the account */ + message?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listStakingBalances: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/balances/staking", + * "nonce": "", + * "account": "primary" + * } + */ + "application/json": { + /** @description The literal string "/v1/balances/staking" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. */ + account?: string; + }; + }; + }; + responses: { + /** @description The staking balances */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example [ + * { + * "type": "Staking", + * "currency": "MATIC", + * "balance": 10, + * "available": 0, + * "availableForWithdrawal": 10, + * "balanceByProvider": { + * "62b21e17-2534-4b9f-afcf-b7edb609dd8d": { + * "balance": 10 + * } + * } + * }, + * { + * "type": "Staking", + * "currency": "ETH", + * "balance": 3, + * "available": 0, + * "availableForWithdrawal": 3, + * "balanceByProvider": { + * "62b21e17-2534-4b9f-afcf-b7edb609dd8d": { + * "balance": 3 + * } + * } + * } + * ] + */ + "application/json": components["schemas"]["StakingBalance"][]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + stakeCryptoFunds: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "v1/staking/stake", + * "nonce": "", + * "providerId": "62b21e17-2534-4b9f-afcf-b7edb609dd8d", + * "currency": "MATIC", + * "amount": 30 + * } + */ + "application/json": { + /** @description The literal string "v1/staking/stake" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. */ + account?: string; + /** @description Provider Id, in uuid4 format. providerId is accessible from the [Staking rates](#list-staking-rates) response */ + providerId: string; + /** @description Currency code, see [symbols](/market-data/symbols-and-minimums) */ + currency: string; + /** + * Format: decimal + * @description The amount of currency to deposit + */ + amount: string; + }; + }; + }; + responses: { + /** @description The staking deposit transaction */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "transactionId": "65QN4XM5", + * "providerId": "62b21e17-2534-4b9f-afcf-b7edb609dd8d", + * "currency": "MATIC", + * "amount": 30, + * "rates": { + * "rate": 540 + * } + * } + */ + "application/json": components["schemas"]["StakingDeposit"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listStakingEventHistory: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/staking/history", + * "nonce": "", + * "account": "primary", + * "since": "2022-11-01T00:00:00.000Z", + * "until": "2022-11-03T00:00:00.000Z", + * "limit": 50 + * } + */ + "application/json": { + /** @description The literal string "/v1/staking/history" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. */ + account?: string; + /** @description In iso datetime with timezone format. Defaults to the timestamp of the first deposit into Staking. */ + since?: components["schemas"]["TimestampType"]; + /** @description In iso datetime with timezone format, default to current time as of server time */ + until?: components["schemas"]["TimestampType"]; + /** + * @description The maximum number of transactions to return. Default is 50, max is 500. + * @default 50 + */ + limit?: number; + /** @description Borrower Id, in uuid4 format. providerId is accessible from the [Staking rates](#list-staking-rates) response */ + providerId?: string; + /** @description Currency code, see [symbols](/market-data/symbols-and-minimums) */ + currency?: string; + /** + * @description Toggles whether to only return daily interest transactions. Defaults to false. + * @default false + */ + interestOnly?: boolean; + /** + * @description Toggles whether to sort the transactions in ascending order by datetime. Defaults to false. + * @default false + */ + sortAsc?: boolean; + }; + }; + }; + responses: { + /** @description Staking transaction history */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example [ + * { + * "providerId": "62b21e17-2534-4b9f-afcf-b7edb609dd8d", + * "transactions": [ + * { + * "transactionId": "MPZ7LDD8", + * "transactionType": "Redeem", + * "amountCurrency": "MATIC", + * "amount": 20, + * "dateTime": 1667418560153 + * }, + * { + * "transactionId": "65QN4XM5", + * "transactionType": "Deposit", + * "amountCurrency": "MATIC", + * "amount": 30, + * "dateTime": 1667418287795 + * }, + * { + * "transactionId": "YP22OK4P", + * "transactionType": "Deposit", + * "amountCurrency": "ETH", + * "amount": 3, + * "dateTime": 1667397368929 + * }, + * { + * "transactionId": "TQN9OPN", + * "transactionType": "Interest", + * "amountCurrency": "MATIC", + * "amount": 0.01, + * "priceCurrency": "USD", + * "priceAmount": 0.1, + * "dateTime": 1667418287795 + * } + * ] + * } + * ] + */ + "application/json": components["schemas"]["StakingHistory"][]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listStakingRates: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description JSON response with staking rates */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "62bb4d27-a9c8-4493-a737-d4fa33994f1f": { + * "MATIC": { + * "providerId": "62bb4d27-a9c8-4493-a737-d4fa33994f1f", + * "rate": 95.8909, + * "apyPct": 0.96, + * "ratePct": 0.958909, + * "depositUsdLimit": 500000 + * }, + * "ETH": { + * "providerId": "62bb4d27-a9c8-4493-a737-d4fa33994f1f", + * "rate": 228.0197, + * "apyPct": 2.31, + * "ratePct": 2.280197, + * "depositUsdLimit": 500000 + * }, + * "SOL": { + * "providerId": "62bb4d27-a9c8-4493-a737-d4fa33994f1f", + * "rate": 321.5282, + * "apyPct": 3.27, + * "ratePct": 3.215282, + * "depositUsdLimit": 500000 + * } + * } + * } + */ + "application/json": components["schemas"]["StakingRateResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listStakingRewards: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/staking/rewards", + * "nonce": "", + * "since": "2022-08-20T00:00:00.000Z", + * "until": "2022-11-05T00:00:00.000Z", + * "providerId": "62b21e17-2534-4b9f-afcf-b7edb609dd8d", + * "currency": "ETH" + * } + */ + "application/json": { + /** @description The literal string "/v1/staking/rewards" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. */ + account?: string; + /** @description In iso datetime with timezone format */ + since: string; + /** @description In iso datetime with timezone format, default to current time as of server time */ + until?: string; + /** @description Borrower Id, in uuid4 format. providerId is accessible from the [Staking rates](#list-staking-rates) response */ + providerId?: string; + /** @description Currency code, see [symbols](/market-data/symbols-and-minimums) */ + currency?: string; + }; + }; + }; + responses: { + /** @description A nested JSON object, organized by provider, then currency */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "62b21e17-2534-4b9f-afcf-b7edb609dd8d": { + * "MATIC": { + * "providerId": "62b21e17-2534-4b9f-afcf-b7edb609dd8d", + * "currency": "MATIC", + * "accrualTotal": 0.103994, + * "ratePeriods": [ + * { + * "providerId": "62b21e17-2534-4b9f-afcf-b7edb609dd8d", + * "currency": "MATIC", + * "apyPct": 5.75, + * "ratePct": 5.592369, + * "numberOfAccruals": 1, + * "accrualTotal": 0.0065678, + * "firstAccrualAt": "2022-08-23T20:00:00.000Z", + * "lastAccrualAt": "2022-08-23T20:00:00.000Z" + * }, + * { + * "providerId": "62b21e17-2534-4b9f-afcf-b7edb609dd8d", + * "currency": "MATIC", + * "apyPct": 5.2, + * "ratePct": 5.073801, + * "numberOfAccruals": 1, + * "accrualTotal": 0.0037971687995651837, + * "firstAccrualAt": "2022-10-28T20:00:00.000Z", + * "lastAccrualAt": "2022-10-28T20:00:00.000Z" + * } + * ] + * }, + * "ETH": { + * "providerId": "62b21e17-2534-4b9f-afcf-b7edb609dd8d", + * "currency": "ETH", + * "accrualTotal": 0.017999076209977, + * "ratePeriods": [ + * { + * "providerId": "62b21e17-2534-4b9f-afcf-b7edb609dd8d", + * "currency": "ETH", + * "apyPct": 0.66, + * "ratePct": 0.65913408, + * "numberOfAccruals": 1, + * "accrualTotal": 0.00014802170517505, + * "firstAccrualAt": "2022-11-02T20:00:00.000Z", + * "lastAccrualAt": "2022-11-02T20:00:00.000Z" + * } + * ] + * } + * } + * } + */ + "application/json": components["schemas"]["StakingRewardsResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + unstakeCryptoFunds: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "v1/staking/unstake", + * "nonce": "", + * "providerId": "62b21e17-2534-4b9f-afcf-b7edb609dd8d", + * "currency": "MATIC", + * "amount": 20 + * } + */ + "application/json": { + /** @description The literal string "v1/staking/unstake" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. */ + account?: string; + /** @description Provider Id, in uuid4 format. providerId is accessible from the [Staking rates](#list-staking-rates) response */ + providerId: string; + /** @description Currency code, see [symbols](/market-data/symbols-and-minimums) */ + currency: string; + /** + * Format: decimal + * @description The amount of currency to withdraw + */ + amount: string; + }; + }; + }; + responses: { + /** @description The staking withdrawal transaction */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "transactionId": "MPZ7LDD8", + * "amount": 20, + * "amountPaidSoFar": 20, + * "amountRemaining": 0, + * "currency": "MATIC", + * "requestInitiated": "2022-11-02T19:49:20.153Z" + * } + */ + "application/json": components["schemas"]["StakingWithdrawal"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getRoles: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/roles", + * "nonce": "" + * } + */ + "application/json": { + /** + * @description The literal string "/v1/roles" + * @example /v1/roles + */ + request: string; + /** The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + nonce: components["schemas"]["TimestampType"]; + }; + }; + }; + responses: { + /** @description The response will be a JSON object indicating the assigned roles to the set of API keys used to call `/v1/roles`. The `Auditor` role cannot be combined with other roles. `Fund Manager` and `Trader` can be combined. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RoleResponse"]; + }; + }; + }; + }; + getAccountMargin: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/margin", + * "nonce": "", + * "symbol": "BTC-GUSD-PERP" + * } + */ + "application/json": { + /** + * @description The API endpoint path + * @example /v1/margin + */ + request: string; + /** The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + nonce: components["schemas"]["TimestampType"]; + /** + * @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts. + * @example primary + */ + account?: string; + /** @description Trading pair symbol. See [symbols and minimums](/market-data/symbols-and-minimums) */ + symbol: string; + }; + }; + }; + responses: { + /** @description JSON object */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "margin_assets_value": "9800", + * "initial_margin": "6000", + * "available_margin": "3800", + * "margin_maintenance_limit": "5800", + * "leverage": "12.34567", + * "notional_value": "1300", + * "estimated_liquidation_price": "1300", + * "initial_margin_positions": "3500", + * "reserved_margin": "2500", + * "reserved_margin_buys": "1800", + * "reserved_margin_sells": "700", + * "buying_power": "0.19", + * "selling_power": "0.19" + * } + */ + "application/json": components["schemas"]["MarginResponse"]; + }; + }; + }; + }; + listFundingPayments: { + parameters: { + query?: { + /** @description If specified, only return funding payments after this point. Default value is 24h in past. See [**Timestamps**](/rest/~schemas#timestamp-type) for more information */ + since?: components["schemas"]["TimestampType"]; + /** @description If specified, only returns funding payment until this point. Default value is now. See [**Timestamps**](/rest/~schemas#timestamp-type) for more information */ + to?: components["schemas"]["TimestampType"]; + }; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/perpetuals/fundingPayment", + * "nonce": "" + * } + */ + "application/json": { + /** + * @description The API endpoint path + * @example /v1/perpetuals/fundingPayment + */ + request: string; + /** The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + nonce: components["schemas"]["TimestampType"]; + /** + * @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts. + * @example primary + */ + account?: string; + }; + }; + }; + responses: { + /** @description The response will be an array of funding payment objects. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example [ + * { + * "eventType": "Hourly Funding Transfer", + * "hourlyFundingTransfer": { + * "eventType": "Hourly Funding Transfer", + * "timestamp": 1683730803940, + * "assetCode": "GUSD", + * "action": "Debit", + * "quantity": { + * "currency": "GUSD", + * "value": "4.78958" + * } + * } + * }, + * { + * "eventType": "Hourly Funding Transfer", + * "hourlyFundingTransfer": { + * "eventType": "Hourly Funding Transfer", + * "timestamp": 1683734406746, + * "assetCode": "GUSD", + * "action": "Debit", + * "quantity": { + * "currency": "GUSD", + * "value": "4.78958" + * }, + * "instrumentSymbol": "BTCGUSDPERP" + * } + * } + * ] + */ + "application/json": components["schemas"]["FundingPayment"][]; + }; + }; + }; + }; + getFundingPaymentReportFile: { + parameters: { + query?: { + /** @description If empty, will only fetch records by numRows value. */ + fromDate?: string; + /** @description If empty, will only fetch records by numRows value. */ + toDate?: string; + /** @description If empty, default value '8760' */ + numRows?: number; + }; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + /** + * @example { + * "request": "/v1/perpetuals/fundingpaymentreport/records.xlsx?fromDate=2024-04-10&toDate=2024-04-25&numRows=1000", + * "nonce": "" + * } + */ + "application/json": { + /** + * @description The API endpoint path + * @example /v1/perpetuals/fundingpaymentreport/records.xlsx + */ + request: string; + /** The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + nonce: components["schemas"]["TimestampType"]; + /** + * @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts. + * @example primary + */ + account?: string; + }; + }; + }; + responses: { + /** @description XLSX file downloaded containing funding payment report. */ + 200: { + headers: { + "Content-Disposition"?: string; + [name: string]: unknown; + }; + content: { + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": string; + }; + }; + 400: components["responses"]["BadRequest"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getFundingPaymentReportJson: { + parameters: { + query?: { + /** @description If empty, will only fetch records by numRows value. */ + fromDate?: string; + /** @description If empty, will only fetch records by numRows value. */ + toDate?: string; + /** @description If empty, default value '8760' */ + numRows?: number; + }; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/perpetuals/fundingpaymentreport/records.json?fromDate=2024-04-10&toDate=2024-04-25&numRows=1000", + * "nonce": "" + * } + */ + "application/json": { + /** + * @description The API endpoint path + * @example /v1/perpetuals/fundingpaymentreport/records.json?fromDate=2024-04-10&toDate=2024-04-25&numRows=1000 + */ + request: string; + /** The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) */ + nonce: components["schemas"]["TimestampType"]; + /** + * @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts. + * @example primary + */ + account?: string; + }; + }; + }; + responses: { + /** @description JSON response containing funding payment report. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example [ + * { + * "eventType": "Hourly Funding Transfer", + * "timestamp": 1713344403617, + * "assetCode": "GUSD", + * "action": "Credit", + * "quantity": { + * "currency": "GUSD", + * "value": "35.81084" + * }, + * "instrumentSymbol": "BTCGUSDPERP" + * } + * ] + */ + "application/json": components["schemas"]["FundingPaymentReportItem"][]; + }; + }; + 400: components["responses"]["BadRequest"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getOpenPositions: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "request": "/v1/positions", + * "nonce": "", + * "account": "primary" + * } + */ + "application/json": { + /** @description The literal string "/v1/positions" */ + request: string; + nonce: components["schemas"]["Nonce"]; + /** @description Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which the orders were placed. Only available for exchange accounts. */ + account?: string; + }; + }; + }; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example [ + * { + * "symbol": "btcgusdperp", + * "instrument_type": "perp", + * "quantity": "0.2", + * "notional_value": "4000.036", + * "realised_pnl": "1234.5678", + * "unrealised_pnl": "999.946", + * "average_cost": "15000.45", + * "mark_price": "20000.18" + * } + * ] + */ + "application/json": { + openPositions?: components["schemas"]["OpenPosition"][]; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getRiskStats: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description Perps Trading pair symbol

+ * + * `BTCGUSDPERP`, etc. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols). + */ + symbol: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The response will be an json object */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "product_type": "PerpetualSwapContract", + * "mark_price": "30080.00", + * "index_price": "30079.046", + * "open_interest": "14.439", + * "open_interest_notional": "434325.12" + * } + */ + "application/json": components["schemas"]["RiskStatsResponse"]; + }; + }; + }; + }; + getTickerV2: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description Trading pair symbol + * @example BTCUSD + */ + symbol: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "symbol": "BTCUSD", + * "open": "9121.76", + * "high": "9440.66", + * "low": "9106.51", + * "close": "9347.66", + * "changes": [ + * "9365.1", + * "9386.16", + * "9373.41", + * "9322.56", + * "9268.89", + * "9265.38", + * "9245", + * "9231.43", + * "9235.88", + * "9265.8", + * "9295.18", + * "9295.47", + * "9310.82", + * "9335.38", + * "9344.03", + * "9261.09", + * "9265.18", + * "9282.65", + * "9260.01", + * "9225", + * "9159.5", + * "9150.81", + * "9118.6", + * "9148.01" + * ], + * "bid": "9345.70", + * "ask": "9347.67" + * } + */ + "application/json": components["schemas"]["TickerInfo"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listCandles: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description Trading pair symbol + * @example BTCUSD + */ + symbol: string; + /** + * @description Time range for each candle: + * * `1m` - 1 minute + * * `5m` - 5 minutes + * * `15m` - 15 minutes + * * `30m` - 30 minutes + * * `1h` - 1 hour + * * `6h` - 6 hours + * * `1day` - 1 day + * @example 15m + */ + time_frame: "1m" | "5m" | "15m" | "30m" | "1h" | "6h" | "1d"; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The response will be an array of arrays */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example [ + * [ + * 1559755800000, + * 7781.6, + * 7820.23, + * 7776.56, + * 7819.39, + * 34.7624802159 + * ], + * [ + * 1559755800000, + * 7781.6, + * 7829.46, + * 7776.56, + * 7817.28, + * 43.4228281059 + * ] + * ] + */ + "application/json": components["schemas"]["CandleResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + listDerivativeCandles: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description Trading pair symbol. Available only for perpetual pairs like `BTCGUSDPERP` + * @example BTCGUSDPERP + */ + symbol: string; + /** + * @description Time range for each candle. `1m`: 1 minute (only) + * @example 1m + */ + time_frame: "1m"; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The response will be an array of arrays */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example [ + * [ + * 1714126740000, + * 68038, + * 68038, + * 68038, + * 68038, + * 0 + * ], + * [ + * 1714126680000, + * 68038, + * 68038, + * 68038, + * 68038, + * 0 + * ] + * ] + */ + "application/json": components["schemas"]["CandleResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; + getFXRate: { + parameters: { + query?: never; + header: { + /** @description Your API key */ + "X-GEMINI-APIKEY": components["parameters"]["apiKeyAuth"]; + /** @description HEX-encoded HMAC-SHA384 of payload signed with API secret */ + "X-GEMINI-SIGNATURE": components["parameters"]["signatureAuth"]; + /** @description Base64-encoded JSON payload */ + "X-GEMINI-PAYLOAD": components["parameters"]["payloadAuth"]; + "Content-Type"?: components["parameters"]["contentType"]; + "Content-Length"?: components["parameters"]["contentLength"]; + "Cache-Control"?: components["parameters"]["cacheControl"]; + }; + path: { + /** + * @description Trading pair symbol

+ * + * `BTCUSD`, etc. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols). + */ + symbol: components["parameters"]["symbolParam"]; + /** + * @description The timestamp to pull the FX rate for. + * + * Gemini strongly recommends using milliseconds instead of seconds for timestamps. + * @example 1591084414622 + */ + timestamp: components["parameters"]["timestampParam"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "fxPair": "AUDUSD", + * "rate": "0.69", + * "asOf": 1594651859000, + * "provider": "bcb", + * "benchmark": "Spot" + * } + */ + "application/json": components["schemas"]["FxRate"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["ApiKeyIpFilteringFailure"]; + 404: components["responses"]["NotFound"]; + 429: components["responses"]["TooManyRequests"]; + 500: components["responses"]["InternalError"]; + }; + }; +} diff --git a/packages/sdk-typescript/src/generated/market-data/operations.ts b/packages/sdk-typescript/src/generated/market-data/operations.ts new file mode 100644 index 0000000..444800c --- /dev/null +++ b/packages/sdk-typescript/src/generated/market-data/operations.ts @@ -0,0 +1,155 @@ +// Generated from rest.yaml#Market Data. Do not edit. + +import type { RestFileResponse } from "../../core/http.js"; +import type { operations as OpenApiOperations } from "./models.js"; + +type ParameterAt = + O extends { parameters: infer P } + ? Location extends keyof P ? P[Location] : never + : never; + +type Int64Input = + T extends bigint ? bigint | number : + T extends readonly (infer Item)[] ? Int64Input[] : + T extends object ? { [K in keyof T]: Int64Input } : T; + +type JsonBody = + NonNullable extends + { content: { "application/json": infer Body } } + ? Required extends true ? Body : Body | undefined + : never; + +type JsonResponse = + O extends { responses: infer R } + ? Status extends keyof R + ? R[Status] extends { content: { "application/json": infer Body } } ? Body : never + : never + : never; + +export const MARKET_DATA_OPERATIONS = { + "getAssetsForNetwork": {"responseMode":"json","operation":"marketData.getAssetsForNetwork","method":"get","path":"/v2/networks/{network}/assets","access":"authenticated","parameters":[{"name":"network","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getCurrentOrderBook": {"responseMode":"json","operation":"marketData.getCurrentOrderBook","method":"get","path":"/v1/book/{symbol}","access":"public","parameters":[{"name":"symbol","in":"path","required":true,"style":"simple","explode":false},{"name":"limit_bids","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"limit_asks","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getFXRate": {"responseMode":"json","operation":"marketData.getFXRate","method":"get","path":"/v2/fxrate/{symbol}/{timestamp}","access":"authenticated","parameters":[{"name":"symbol","in":"path","required":true,"style":"simple","explode":false},{"name":"timestamp","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[{"path":["timestamp"],"allowString":true}],"query":[]},"retryable":true}, + "getFundingAmount": {"responseMode":"json","operation":"marketData.getFundingAmount","method":"get","path":"/v1/fundingamount/{symbol}","access":"public","parameters":[{"name":"symbol","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getFundingAmountReportFile": {"responseMode":"file","operation":"marketData.getFundingAmountReportFile","method":"get","path":"/v1/fundingamountreport/records.xlsx","access":"public","parameters":[{"name":"symbol","in":"query","required":true,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"fromDate","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"toDate","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"numRows","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet","text/csv"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getSymbolDetails": {"responseMode":"json","operation":"marketData.getSymbolDetails","method":"get","path":"/v1/symbols/details/{symbol}","access":"public","parameters":[{"name":"symbol","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getTicker": {"responseMode":"json","operation":"marketData.getTicker","method":"get","path":"/v1/pubticker/{symbol}","access":"public","parameters":[{"name":"symbol","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getTickerV2": {"responseMode":"json","operation":"marketData.getTickerV2","method":"get","path":"/v2/ticker/{symbol}","access":"public","parameters":[{"name":"symbol","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getTokenNetworkV2": {"responseMode":"json","operation":"marketData.getTokenNetworkV2","method":"get","path":"/v2/network/{token}","access":"authenticated","parameters":[{"name":"token","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "listCandles": {"responseMode":"json","operation":"marketData.listCandles","method":"get","path":"/v2/candles/{symbol}/{time_frame}","access":"public","parameters":[{"name":"symbol","in":"path","required":true,"style":"simple","explode":false},{"name":"time_frame","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "listDerivativeCandles": {"responseMode":"json","operation":"marketData.listDerivativeCandles","method":"get","path":"/v2/derivatives/candles/{symbol}/{time_frame}","access":"public","parameters":[{"name":"symbol","in":"path","required":true,"style":"simple","explode":false},{"name":"time_frame","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "listFeePromos": {"responseMode":"json","operation":"marketData.listFeePromos","method":"get","path":"/v1/feepromos","access":"public","parameters":[],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "listPrices": {"responseMode":"json","operation":"marketData.listPrices","method":"get","path":"/v1/pricefeed","access":"public","parameters":[],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "listSymbols": {"responseMode":"json","operation":"marketData.listSymbols","method":"get","path":"/v1/symbols","access":"public","parameters":[],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "listTrades": {"responseMode":"json","operation":"marketData.listTrades","method":"get","path":"/v1/trades/{symbol}","access":"public","parameters":[{"name":"symbol","in":"path","required":true,"style":"simple","explode":false},{"name":"timestamp","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"since_tid","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"limit_trades","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"include_breaks","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["*","tid"]],"requestInt64Paths":{"body":[],"path":[],"query":[{"path":["timestamp"],"allowString":true}]},"retryable":true}, +} as const; + +export type MarketDataOperationId = keyof typeof MARKET_DATA_OPERATIONS; + +export type MarketDataOperationTypes = { + "getAssetsForNetwork": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getCurrentOrderBook": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getFXRate": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getFundingAmount": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getFundingAmountReportFile": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: RestFileResponse; + }; + "getSymbolDetails": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getTicker": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getTickerV2": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getTokenNetworkV2": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "listCandles": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "listDerivativeCandles": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "listFeePromos": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "listPrices": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "listSymbols": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "listTrades": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; +}; diff --git a/packages/sdk-typescript/src/generated/market-data/rest.ts b/packages/sdk-typescript/src/generated/market-data/rest.ts new file mode 100644 index 0000000..21750c7 --- /dev/null +++ b/packages/sdk-typescript/src/generated/market-data/rest.ts @@ -0,0 +1,145 @@ +// Generated from rest.yaml#Market Data. Do not edit. + +import type { HttpTransport } from "../../core/http.js"; +import type { RequestOptions } from "../../core/deadline.js"; +import { executeRestOperation } from "../../core/rest-operation.js"; + +import { + MARKET_DATA_OPERATIONS, + type MarketDataOperationTypes, +} from "./operations.js"; + +export class MarketDataRest { + constructor(private readonly transport: HttpTransport) {} + + getAssetsForNetwork(path: MarketDataOperationTypes["getAssetsForNetwork"]["path"], requestOptions?: RequestOptions): Promise; + getAssetsForNetwork(path: MarketDataOperationTypes["getAssetsForNetwork"]["path"]): Promise; + getAssetsForNetwork(path: MarketDataOperationTypes["getAssetsForNetwork"]["path"], requestOptions?: RequestOptions): Promise { + const operation = MARKET_DATA_OPERATIONS["getAssetsForNetwork"]; + return executeRestOperation(this.transport, operation, { + path, + }, requestOptions); + } + + getCurrentOrderBook(path: MarketDataOperationTypes["getCurrentOrderBook"]["path"], query?: MarketDataOperationTypes["getCurrentOrderBook"]["query"], requestOptions?: RequestOptions): Promise; + getCurrentOrderBook(path: MarketDataOperationTypes["getCurrentOrderBook"]["path"], query?: MarketDataOperationTypes["getCurrentOrderBook"]["query"]): Promise; + getCurrentOrderBook(path: MarketDataOperationTypes["getCurrentOrderBook"]["path"], query?: MarketDataOperationTypes["getCurrentOrderBook"]["query"], requestOptions?: RequestOptions): Promise { + const operation = MARKET_DATA_OPERATIONS["getCurrentOrderBook"]; + return executeRestOperation(this.transport, operation, { + path, + query, + }, requestOptions); + } + + getFXRate(path: MarketDataOperationTypes["getFXRate"]["path"], requestOptions?: RequestOptions): Promise; + getFXRate(path: MarketDataOperationTypes["getFXRate"]["path"]): Promise; + getFXRate(path: MarketDataOperationTypes["getFXRate"]["path"], requestOptions?: RequestOptions): Promise { + const operation = MARKET_DATA_OPERATIONS["getFXRate"]; + return executeRestOperation(this.transport, operation, { + path, + }, requestOptions); + } + + getFundingAmount(path: MarketDataOperationTypes["getFundingAmount"]["path"], requestOptions?: RequestOptions): Promise; + getFundingAmount(path: MarketDataOperationTypes["getFundingAmount"]["path"]): Promise; + getFundingAmount(path: MarketDataOperationTypes["getFundingAmount"]["path"], requestOptions?: RequestOptions): Promise { + const operation = MARKET_DATA_OPERATIONS["getFundingAmount"]; + return executeRestOperation(this.transport, operation, { + path, + }, requestOptions); + } + + getFundingAmountReportFile(query: MarketDataOperationTypes["getFundingAmountReportFile"]["query"], requestOptions?: RequestOptions): Promise; + getFundingAmountReportFile(query: MarketDataOperationTypes["getFundingAmountReportFile"]["query"]): Promise; + getFundingAmountReportFile(query: MarketDataOperationTypes["getFundingAmountReportFile"]["query"], requestOptions?: RequestOptions): Promise { + const operation = MARKET_DATA_OPERATIONS["getFundingAmountReportFile"]; + return executeRestOperation(this.transport, operation, { + query, + }, requestOptions); + } + + getSymbolDetails(path: MarketDataOperationTypes["getSymbolDetails"]["path"], requestOptions?: RequestOptions): Promise; + getSymbolDetails(path: MarketDataOperationTypes["getSymbolDetails"]["path"]): Promise; + getSymbolDetails(path: MarketDataOperationTypes["getSymbolDetails"]["path"], requestOptions?: RequestOptions): Promise { + const operation = MARKET_DATA_OPERATIONS["getSymbolDetails"]; + return executeRestOperation(this.transport, operation, { + path, + }, requestOptions); + } + + getTicker(path: MarketDataOperationTypes["getTicker"]["path"], requestOptions?: RequestOptions): Promise; + getTicker(path: MarketDataOperationTypes["getTicker"]["path"]): Promise; + getTicker(path: MarketDataOperationTypes["getTicker"]["path"], requestOptions?: RequestOptions): Promise { + const operation = MARKET_DATA_OPERATIONS["getTicker"]; + return executeRestOperation(this.transport, operation, { + path, + }, requestOptions); + } + + getTickerV2(path: MarketDataOperationTypes["getTickerV2"]["path"], requestOptions?: RequestOptions): Promise; + getTickerV2(path: MarketDataOperationTypes["getTickerV2"]["path"]): Promise; + getTickerV2(path: MarketDataOperationTypes["getTickerV2"]["path"], requestOptions?: RequestOptions): Promise { + const operation = MARKET_DATA_OPERATIONS["getTickerV2"]; + return executeRestOperation(this.transport, operation, { + path, + }, requestOptions); + } + + getTokenNetworkV2(path: MarketDataOperationTypes["getTokenNetworkV2"]["path"], requestOptions?: RequestOptions): Promise; + getTokenNetworkV2(path: MarketDataOperationTypes["getTokenNetworkV2"]["path"]): Promise; + getTokenNetworkV2(path: MarketDataOperationTypes["getTokenNetworkV2"]["path"], requestOptions?: RequestOptions): Promise { + const operation = MARKET_DATA_OPERATIONS["getTokenNetworkV2"]; + return executeRestOperation(this.transport, operation, { + path, + }, requestOptions); + } + + listCandles(path: MarketDataOperationTypes["listCandles"]["path"], requestOptions?: RequestOptions): Promise; + listCandles(path: MarketDataOperationTypes["listCandles"]["path"]): Promise; + listCandles(path: MarketDataOperationTypes["listCandles"]["path"], requestOptions?: RequestOptions): Promise { + const operation = MARKET_DATA_OPERATIONS["listCandles"]; + return executeRestOperation(this.transport, operation, { + path, + }, requestOptions); + } + + listDerivativeCandles(path: MarketDataOperationTypes["listDerivativeCandles"]["path"], requestOptions?: RequestOptions): Promise; + listDerivativeCandles(path: MarketDataOperationTypes["listDerivativeCandles"]["path"]): Promise; + listDerivativeCandles(path: MarketDataOperationTypes["listDerivativeCandles"]["path"], requestOptions?: RequestOptions): Promise { + const operation = MARKET_DATA_OPERATIONS["listDerivativeCandles"]; + return executeRestOperation(this.transport, operation, { + path, + }, requestOptions); + } + + listFeePromos(requestOptions?: RequestOptions): Promise; + listFeePromos(): Promise; + listFeePromos(requestOptions?: RequestOptions): Promise { + const operation = MARKET_DATA_OPERATIONS["listFeePromos"]; + return executeRestOperation(this.transport, operation, {}, requestOptions); + } + + listPrices(requestOptions?: RequestOptions): Promise; + listPrices(): Promise; + listPrices(requestOptions?: RequestOptions): Promise { + const operation = MARKET_DATA_OPERATIONS["listPrices"]; + return executeRestOperation(this.transport, operation, {}, requestOptions); + } + + listSymbols(requestOptions?: RequestOptions): Promise; + listSymbols(): Promise; + listSymbols(requestOptions?: RequestOptions): Promise { + const operation = MARKET_DATA_OPERATIONS["listSymbols"]; + return executeRestOperation(this.transport, operation, {}, requestOptions); + } + + listTrades(path: MarketDataOperationTypes["listTrades"]["path"], query?: MarketDataOperationTypes["listTrades"]["query"], requestOptions?: RequestOptions): Promise; + listTrades(path: MarketDataOperationTypes["listTrades"]["path"], query?: MarketDataOperationTypes["listTrades"]["query"]): Promise; + listTrades(path: MarketDataOperationTypes["listTrades"]["path"], query?: MarketDataOperationTypes["listTrades"]["query"], requestOptions?: RequestOptions): Promise { + const operation = MARKET_DATA_OPERATIONS["listTrades"]; + return executeRestOperation(this.transport, operation, { + path, + query, + }, requestOptions); + } +} diff --git a/packages/sdk-typescript/src/generated/models.ts b/packages/sdk-typescript/src/generated/models.ts new file mode 100644 index 0000000..f67ecba --- /dev/null +++ b/packages/sdk-typescript/src/generated/models.ts @@ -0,0 +1,3714 @@ +// Generated from prediction-markets.yaml. Do not edit. + +export interface paths { + "/v1/prediction-markets/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List prediction market events + * @description Returns a paginated list of prediction market events with optional filtering by status, category, sports-market classification, and search text. Repeated values for the same filter use OR semantics; different filters combine with AND semantics. + */ + get: operations["listEvents"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/events/{eventTicker}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get event by ticker + * @description Returns detailed information about a specific prediction market event. + */ + get: operations["getEvent"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/events/{eventTicker}/strike": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get strike price for event + * @description Returns strike price information for a specific prediction market event. + * + * Useful for crypto Up/Down contracts where the strike price becomes available at the start of the observation window (typically ~5 minutes before expiry for 5M contracts). + * + * For Up/Down contracts, the `value` field will be `null` until the strike is captured at `availableAt` time. + */ + get: operations["getEventStrike"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/events/newly-listed": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List newly listed events + * @description Returns a list of prediction market events created in the last 24 hours, sorted by creation date (newest first). Repeated values for the same sports-market filter use OR semantics; different filters combine with AND semantics. + */ + get: operations["listNewlyListedEvents"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/events/recently-settled": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List recently settled events + * @description Returns a list of prediction market events settled in the last 24 hours, sorted by resolution date (most recently settled first). Repeated values for the same sports-market filter use OR semantics; different filters combine with AND semantics. + */ + get: operations["listRecentlySettledEvents"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/events/upcoming": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List upcoming events + * @description Returns a list of approved prediction market events that are not yet active (pre-launch), sorted by start time (soonest first). Repeated values for the same sports-market filter use OR semantics; different filters combine with AND semantics. + */ + get: operations["listUpcomingEvents"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/categories": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List event categories + * @description Returns available prediction market event categories, optionally filtered by event status. + */ + get: operations["getCategories"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/volume/{date}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get daily prediction market trade volume + * @description Returns prediction-market trade volume by category for one completed UTC day. This is a public, unauthenticated endpoint. + * + * `date` must use the `YYYY-MM-DD` UTC calendar-date format. Requests may select one day in the rolling one-year UTC window ending before the current UTC day; the current UTC day is not available. The exact earliest supported date is evaluated for each request. + * + * Prediction-market volume begins at `2025-12-15`. A pre-launch date, or a post-launch date with any missing source hour, returns `404 NOT_FOUND`. The endpoint never synthesizes zero-volume rows for time before launch. + * + * All volume values are non-negative decimal strings. Category rows are flat and ordered with each parent before its descendants. Each category row's `volume` includes trades assigned directly to that category and to all descendant categories. + */ + get: operations["getPredictionMarketDailyVolume"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/volume/{date}/hourly": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get hourly prediction market trade volume + * @description Returns prediction-market trade volume by category and UTC hour for one completed UTC day. This is a public, unauthenticated endpoint. + * + * `date` must use the `YYYY-MM-DD` UTC calendar-date format. Requests may select one day in the rolling one-year UTC window ending before the current UTC day; the current UTC day is not available. The exact earliest supported date is evaluated for each request. + * + * Prediction-market volume begins at `2025-12-15`. A pre-launch date, or a post-launch date with any missing source hour, returns `404 NOT_FOUND`. The endpoint never synthesizes zero-volume rows for time before launch or completed zero-volume hours. + * + * All volume values are non-negative decimal strings. Rows are ordered by UTC hour, then with each category parent before its descendants. Each category row's `volume` includes trades assigned directly to that category and to all descendant categories. + */ + get: operations["getPredictionMarketHourlyVolume"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/terms": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get prediction market terms + * @description Returns the latest Prediction Markets terms content. This endpoint is public so clients can display the terms before asking an authenticated account to accept them. + */ + get: operations["getPredictionMarketsTerms"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/terms/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get prediction market terms status + * @description Returns whether the authenticated account group has accepted the latest Prediction Markets terms. Requires authentication and OrderStatus permission. + */ + get: operations["getPredictionMarketsTermsStatus"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/terms/accept": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Accept prediction market terms + * @description Accepts the latest configured Prediction Markets terms for the authenticated account group. Requires authentication and NewOrder permission. The actor is recorded from the OAuth client when OAuth is used, otherwise from the API key session. + */ + post: operations["acceptPredictionMarketsTerms"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/order": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Place order + * @description Place a new prediction market order. Supports limit and stop-limit order types. Requires authentication and NewOrder permission. + * + * Before sending orders, check `GET /v1/prediction-markets/terms/status`. If `hasAcceptedLatest` is `false`, display `GET /v1/prediction-markets/terms` and call `POST /v1/prediction-markets/terms/accept`, then retry the order. + * + * Validate each order's quantity and price against the instrument-specific `quantityIncrement`, `quantityMinimum`, `priceIncrement`, and `priceMinimum` returned in the contract metadata. Do not assume a fixed quantity or price grid across instruments. + * + * ### Stop-Limit Orders + * A stop-limit order is an order type that allows for order placement when a price reaches a specified level. Stop-limit orders take in both a `price` and a `stopPrice` as parameters. The `stopPrice` is the price that triggers the order to be placed on the continuous live order book at the `price`. For buy orders, the `stopPrice` must be greater than or equal to the last trade price and less than or equal to the `price`; for sell orders, the `stopPrice` must be less than or equal to the last trade price and greater than or equal to the `price`. Both `price` and `stopPrice` must be in the 0-1 range. + */ + post: operations["placeOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/order/batch": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Place a batch of orders + * @description Place between 1 and 20 prediction market orders in one authenticated request. The complete payload is signed once using the standard private REST authentication headers. Each entry accepts the same fields as `POST /v1/prediction-markets/order`. + * + * The operation is synchronous and non-atomic. Gemini validates the entire batch before submitting any orders. If the batch or any entry fails up-front validation, the request fails and no orders are submitted. After validation succeeds, orders are submitted sequentially and each result is returned in request order. An exchange rejection for one order does not stop later orders from being submitted; it appears as an `error` and `message` for that entry in the `200` response. + */ + post: operations["placeOrderBatch"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/order/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Cancel order + * @description Cancel an existing prediction market order. Requires authentication and CancelOrder permission. + */ + post: operations["cancelOrder"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/order/batch/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Cancel a batch of orders + * @description Cancel between 1 and 20 prediction market orders in one authenticated request. The complete payload is signed once using the standard private REST authentication headers. Each order ID may be a JSON integer or a quoted numeric string. + * + * The operation is synchronous and non-atomic. Gemini validates all order IDs before attempting any cancellation. If the batch is empty, contains more than 20 entries, or contains an invalid ID, the request fails and no orders are cancelled. After validation succeeds, cancellations are attempted sequentially and each result is returned in request order. A rejection for one cancellation does not stop later cancellations from being attempted; it appears as an `error` and `message` for that entry in the `200` response. + */ + post: operations["cancelOrderBatch"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/orders/active": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get active orders + * @description Returns a list of currently open (active) orders. Requires authentication. + */ + post: operations["getActiveOrders"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/orders/history": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get order history + * @description Returns historical orders (filled or cancelled) for the authenticated user. Use `status: filled` with `from` and `to` to retrieve fully filled orders in a bounded time window. The range is `[from, to)`: `from` is inclusive and `to` is exclusive. A time-bounded response contains at most `limit` results and ignores `offset`; split high-volume periods into non-overlapping ranges. Use `/orders/active` for open orders. + */ + post: operations["getOrderHistory"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/positions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get positions + * @description Returns current filled positions for the authenticated user. All query parameters are optional; omitting them preserves the legacy unpaginated, unsorted behavior. + */ + post: operations["getPositions"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/positions/settled": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get settled positions + * @description Returns historically settled positions for the authenticated user. Each entry represents a position in a contract that has resolved. + * - `payout` — the amount received from settlement + * - `resolutionSide` — indicates which outcome (`yes` or `no`) won. + * + * This endpoint differs from [Get positions](#operation/getPositions) in that it returns closed positions from settled contracts rather than current open positions. + */ + post: operations["getSettledPositions"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/metrics/volume": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get volume metrics + * @description Returns per-contract share volume metrics for an event, including the authenticated user's taker and maker volumes. + * + * All volumes are in shares (number of contracts traded), not dollar amounts. + * + * - `totalQty` — Total taker volume across all participants for this contract + * - `userAggressorQty` — The authenticated user's taker (aggressor) volume + * - `userRestingQty` — The authenticated user's maker (resting) volume, counted when another order fills against the user's resting limit order + * + * An optional time range can be specified to filter trades within a specific window. + */ + post: operations["getVolumeMetrics"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/combos": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List combo contracts + * @description Returns a paginated list of combo contracts. Each combo includes its full leg breakdown and per-leg resolution status. When `status` is omitted, the endpoint returns only `Active` combos. This Combo Prediction Markets endpoint is not currently enabled in production. + */ + get: operations["listCombos"]; + put?: never; + /** + * Create or retrieve a canonical combo + * @description Creates a combo from two to six underlying contract legs for the authenticated account. The service canonicalizes the complete leg set, so submitting the same legs again returns the existing combo regardless of leg order. The account is derived from the authenticated API key; do not include an account ID in the request. This Combo Prediction Markets endpoint is not currently enabled in production. + * + * Requires signed private REST authentication, the `PredictionsNewOrder` permission, and an unrestricted trading account. A new canonical combo returns `201 Created` with `alreadyExisted: false`; an existing canonical combo returns `200 OK` with `alreadyExisted: true`. + */ + post: operations["createCombo"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/combos/{instrumentSymbol}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get combo by instrument symbol + * @description Returns the full specification of a single combo contract identified by its instrument symbol, including leg breakdown and per-leg resolution status. This Combo Prediction Markets endpoint is not currently enabled in production. + */ + get: operations["getComboByInstrumentSymbol"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/maker-rebate/rates": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get maker-rebate rate schedule + * @description Returns the current Maker Rebate rate rules. Public endpoint; no authentication required. + * + * Each rule defines a `rebate_multiplier_bps` (basis points of the maker fee that is rebated) and an `effective_from` timestamp. An optional `category` scopes the rule to a single market category (omitted rules apply to all categories). An optional `effective_to` marks a rule as superseded. + * + * Returns `503` with `error: "Maker rebate program is not currently available"` when the program is disabled. + */ + get: operations["getMakerRebateRates"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/maker-rebate/payouts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * List maker-rebate payouts + * @description Returns the authenticated account's Maker Rebate payout history. Most recent payout first. + * + * Pagination is read from the `limit` and `offset` query parameters: `limit` is clamped to `[1, 100]` (default 50), `offset` is clamped to `[0, +∞)` (default 0). + * + * Requires authentication with `OrderStatus` permission. Returns `503` when the program is disabled. + */ + post: operations["listMakerRebatePayouts"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/maker-rebate/summary/total": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get maker-rebate lifetime summary + * @description Returns lifetime totals for the authenticated account's Maker Rebate payouts. When both `dateFrom` and `dateTo` are provided, the totals are restricted to payouts paid within that inclusive Eastern Time window. + * + * Either provide both date parameters or omit both. Dates must be in `YYYY-MM-DD` format, `dateTo` must be on or after `dateFrom`, and the range must not exceed 5 years. + * + * Requires authentication with `OrderStatus` permission. Returns `503` when the program is disabled. + */ + get: operations["getMakerRebateLifetimeSummary"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/liquidity-rewards/config": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get liquidity-rewards program config + * @description Returns the Liquidity Rewards program configuration. Public endpoint; no authentication required. + * + * When the program is fully configured the response includes `max_spread_cents`, `min_payout_threshold_usd`, and `enabled: true`. When the program is not yet fully configured, the response collapses to `{ "enabled": false }` only. + * + * Returns `503` when the program is not currently available. + */ + get: operations["getLiquidityRewardsConfig"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/liquidity-rewards/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List liquidity-rewards events + * @description Returns the paginated list of events currently participating in the Liquidity Rewards program. Public endpoint; no authentication required. + * + * `category` accepts a comma-separated list of category names (whitespace trimmed, empty entries dropped). `sort` controls ordering. `limit` is clamped to `[1, 100]` (default 50); `offset` is clamped to `[0, +∞)` (default 0). `last_score_date` is the most recent date for which scoring data has been written, or `null` when no scoring has run yet. + * + * Returns `503` when the program is not currently available. + */ + get: operations["listLiquidityRewardsEvents"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/liquidity-rewards/summary/daily": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get liquidity-rewards daily summary + * @description Returns daily Liquidity Rewards payouts for the authenticated account within the requested date window. Both `dateFrom` and `dateTo` are required and must be in `YYYY-MM-DD` format; `dateTo` must be on or after `dateFrom`. + * + * Each daily entry includes the total USD reward for that day, the payout status (e.g. `PENDING`, `PAID`), the paid-at timestamp (if paid), and per-event score breakdowns showing how the day's reward was distributed across events the account scored on. + * + * Requires authentication with `OrderStatus` permission. Returns `503` when the program is not currently available. + */ + get: operations["getLiquidityRewardsDailySummary"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/prediction-markets/liquidity-rewards/summary/total": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get liquidity-rewards lifetime summary + * @description Returns lifetime totals for the authenticated account's Liquidity Rewards payouts. When both `dateFrom` and `dateTo` are provided, the totals are restricted to payouts paid within that inclusive Eastern Time window. + * + * Either provide both date parameters or omit both. Dates must be in `YYYY-MM-DD` format, `dateTo` must be on or after `dateFrom`, and the range must not exceed 5 years. + * + * Requires authentication with `OrderStatus` permission. Returns `503` when the program is not currently available. + */ + get: operations["getLiquidityRewardsLifetimeSummary"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + Error: { + /** + * @description Error code + * @example InvalidInput + */ + error?: string; + /** + * @description Human-readable error message + * @example orderId is required + */ + message?: string; + }; + PredictionMarketsError: { + /** + * @description Prediction Markets error class + * @example InvalidInput + */ + error: string; + /** + * @description Request field associated with the error, when available + * @example orders + */ + field?: string; + /** + * @description Human-readable error detail, when available + * @example orders must contain between 1 and 20 entries + */ + message?: string; + }; + AuthErrorResponse: { + /** @enum {string} */ + result: "error"; + /** + * @description Authentication or authorization error class + * @example MissingNonce + */ + reason: string; + /** + * @description Human-readable authentication or authorization detail + * @example Must provide unique monotonic increasing 'nonce' field in payload + */ + message: string; + }; + AccountGroupBlockedError: { + /** @enum {string} */ + error: "This account is not permitted to trade prediction markets"; + /** @enum {string} */ + code: "ACCOUNT_GROUP_BLOCKED"; + }; + TermsNotAcceptedError: { + /** @enum {string} */ + error: "TERMS_NOT_ACCEPTED"; + /** @enum {string} */ + message: "Prediction markets terms must be accepted before placing orders"; + }; + RestrictedSellOnlyError: { + /** @enum {string} */ + error: "ACCOUNT_RESTRICTED_SELL_ONLY"; + /** @enum {string} */ + message: "Your account is restricted to selling existing positions; buying is not permitted."; + }; + PredictionMarketsTerms: { + /** + * @description Terms type identifier + * @example PredictionsMarket + */ + termsType: string; + /** + * @description Latest terms version + * @example 3 + */ + version: number; + /** + * @description Terms content to display before acceptance + * @example These are the prediction market terms. + */ + content: string; + /** + * Format: date-time + * @description UTC timestamp when the terms content was last updated + * @example 2026-05-18T17:00:00Z + */ + updatedAt: string; + }; + PredictionMarketsTermsStatus: { + /** + * @description Whether the account group has accepted the latest configured Prediction Markets terms + * @example false + */ + hasAcceptedLatest: boolean; + /** + * @description Latest terms version accepted by the account group, if any + * @example 2 + */ + acceptedVersion?: number | null; + /** + * @description Latest configured Prediction Markets terms version, if available + * @example 3 + */ + latestVersion?: number | null; + }; + AcceptPredictionMarketsTermsResponse: { + /** @example true */ + success: boolean; + }; + /** + * @description Status of a prediction market + * @enum {string} + */ + MarketStatus: "approved" | "active" | "closed" | "under_review" | "settled" | "invalid"; + /** + * @description Type of prediction market + * @enum {string} + */ + MarketType: "binary" | "categorical"; + /** + * @description Sport whose rules give the market's scope and metric their sport-specific meaning. + * @enum {string} + */ + SportsMarketSport: "american_football" | "athletics" | "australian_rules_football" | "baseball" | "basketball" | "boxing" | "chess" | "cricket" | "cycling" | "darts" | "esports" | "golf" | "hockey" | "lacrosse" | "mixed_martial_arts" | "motorsports" | "rugby" | "sailing" | "soccer" | "tennis"; + /** + * @description Conventional sports-market family. `subject`, `scope`, and `metric` provide detail within the family. This classification is independent of the event's structural `type` (`binary` or `categorical`). + * @enum {string} + */ + SportsMarketType: "moneyline" | "spread" | "total" | "prop" | "correct_score" | "to_advance" | "futures" | "other"; + /** + * @description What the market is about. `participant` covers non-player entrants such as drivers and horses. + * @enum {string} + */ + SportsMarketSubject: "contest" | "team" | "player" | "participant" | "other"; + /** + * @description Unit covered by the market. `full_contest` follows the market's official final-result rules; `regulation` covers scheduled regulation play only. Ordinal and range qualifiers are represented separately on `SportsMarketScope`. + * @enum {string} + */ + SportsMarketScopeType: "full_contest" | "regulation" | "half" | "quarter" | "period" | "inning" | "team_innings" | "over" | "powerplay" | "set" | "game" | "round" | "hole" | "match_day" | "session" | "super_over" | "race" | "sprint" | "qualifying" | "practice" | "stage" | "lap" | "series" | "season" | "tournament" | "competition" | "other"; + /** + * @description Statistic measured by the market. Interpret shared metric names using `sportsMarket.sport`. + * @enum {string} + */ + SportsMarketMetric: "aces" | "assists" | "balls_faced" | "birdies" | "blocked_shots" | "blocks" | "bogeys" | "boundaries" | "break_points_won" | "cards" | "catches" | "clean_sheets" | "completed_passes" | "control_time" | "corners" | "defensive_rebounds" | "double_double" | "double_faults" | "doubles" | "eagles" | "earned_runs" | "errors" | "faceoff_wins" | "fairways_hit" | "fantasy_points" | "fastest_lap" | "field_goals_made" | "finishing_position" | "fouls" | "fours" | "free_throws_made" | "fumbles" | "games" | "goals" | "goals_allowed" | "greens_in_regulation" | "grid_position" | "hits" | "hits_allowed" | "hits_runs_rbis" | "holes_in_one" | "home_runs" | "innings_pitched" | "interceptions_thrown" | "kicking_points" | "knockdowns" | "laps_completed" | "laps_led" | "lap_time" | "longest_pass_completion" | "longest_reception" | "longest_rush" | "maiden_overs" | "offensive_rebounds" | "offsides" | "pars" | "passes" | "pass_attempts" | "pass_completions" | "passing_touchdowns" | "passing_yards" | "penalty_minutes" | "pitching_outs_recorded" | "pit_stops" | "points" | "points_assists" | "points_rebounds" | "points_rebounds_assists" | "positions_gained" | "power_play_points" | "putts" | "qualifying_position" | "rebounds" | "rebounds_assists" | "receiving_touchdowns" | "receiving_yards" | "receptions" | "red_cards" | "retirements" | "rounds" | "runs" | "runs_batted_in" | "runs_conceded" | "rush_attempts" | "rushing_touchdowns" | "rushing_yards" | "sacks" | "safety_cars" | "saves" | "sets" | "shots" | "shots_on_goal" | "shots_on_target" | "shutouts" | "significant_strikes" | "singles" | "sixes" | "steals" | "stolen_bases" | "strokes" | "strikeouts" | "submission_attempts" | "tackles" | "takedowns" | "three_pointers_made" | "tiebreaks_won" | "total_bases" | "total_points_won" | "total_strikes" | "touchdowns" | "triples" | "triple_double" | "turnovers" | "walks" | "wickets" | "wins" | "yellow_cards" | "other"; + /** @description Settlement scope. `ordinal` identifies one unit; `start` and `end` identify an inclusive range of units. */ + SportsMarketScope: { + type: components["schemas"]["SportsMarketScopeType"]; + /** + * Format: int32 + * @description Optional ordinal within the scope type, such as half `1` or quarter `4`. + */ + ordinal?: number; + /** + * Format: int32 + * @description Optional inclusive start of a scope range, such as inning `1`. + */ + start?: number; + /** + * Format: int32 + * @description Optional inclusive end of a scope range, such as inning `5`. + */ + end?: number; + }; + /** @description Atomic sports-market classification shared by every contract grouped under the event. Present only for sports events. All fields except `metric` are required together. */ + SportsMarket: { + sport: components["schemas"]["SportsMarketSport"]; + type: components["schemas"]["SportsMarketType"]; + subject: components["schemas"]["SportsMarketSubject"]; + scope: components["schemas"]["SportsMarketScope"]; + metric?: components["schemas"]["SportsMarketMetric"]; + }; + /** + * @description Order type. `stop-limit` orders require a `stopPrice` that triggers a limit order at `price` when the market reaches the trigger. + * @enum {string} + */ + OrderType: "limit" | "stop-limit"; + /** @enum {string} */ + OrderSide: "buy" | "sell"; + /** + * @description The outcome being traded (Yes or No) + * @enum {string} + */ + Outcome: "yes" | "no"; + /** + * @description Order execution behavior: + * - `good-til-cancel` - Order remains active until filled or cancelled (default) + * - `immediate-or-cancel` - Fill immediately or cancel remaining + * - `fill-or-kill` - Fill entire order immediately or cancel + * @default good-til-cancel + * @enum {string} + */ + TimeInForce: "good-til-cancel" | "immediate-or-cancel" | "fill-or-kill"; + /** @enum {string} */ + OrderStatus: "open" | "filled" | "cancelled"; + /** @enum {string} */ + PositionStatus: "active" | "resolved" | "cancelled"; + Pagination: { + /** @example 50 */ + limit?: number; + /** @example 0 */ + offset?: number; + /** @example 100 */ + total?: number; + }; + PaginationSimple: { + limit?: number; + offset?: number; + /** @description Number of items in current response */ + count?: number; + }; + OrderBook: { + bids?: components["schemas"]["OrderBookEntry"][]; + asks?: components["schemas"]["OrderBookEntry"][]; + }; + OrderBookEntry: { + side?: components["schemas"]["OrderSide"]; + /** @example 0.65 */ + price?: string; + /** @example 1000 */ + quantity?: string; + }; + OrderBookDepth: { + bids?: components["schemas"]["OrderBookLevel"][]; + asks?: components["schemas"]["OrderBookLevel"][]; + /** Format: date-time */ + lastUpdateTime?: string; + }; + OrderBookLevel: { + price?: string; + quantity?: string; + orderCount?: number; + }; + /** @description Contract quantity and price validation is instrument-specific. Clients must validate order quantities and prices against the returned increment and minimum fields rather than assuming a fixed grid. */ + Contract: { + id?: string; + /** @description Human-readable label (e.g., "Yes", "No") */ + label?: string; + /** @description Short form label (e.g., ">$90") */ + abbreviatedName?: string | null; + /** @description Rich text description */ + description?: Record; + prices?: components["schemas"]["ContractPrices"]; + totalShares?: string | null; + color?: string | null; + status?: components["schemas"]["MarketStatus"]; + imageUrl?: string | null; + priceHistory?: components["schemas"]["PricePoint"][] | null; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + expiryDate?: string | null; + resolutionSide?: components["schemas"]["Outcome"]; + /** Format: date-time */ + resolvedAt?: string | null; + termsAndConditionsUrl?: string; + ticker?: string; + instrumentSymbol?: string; + /** @description Contract quantity grid from instrument refdata (for example, "0.01"). */ + quantityIncrement?: string | null; + /** @description Minimum contract quantity from instrument refdata (for example, "1.00"). */ + quantityMinimum?: string | null; + /** @description Contract price grid from instrument refdata (for example, "0.0001"). */ + priceIncrement?: string | null; + /** @description Decimal places supported by the instrument's quote asset. */ + quoteAssetPrecision?: number | null; + /** @description Minimum contract price and anchor for the instrument price grid (for example, "0.0001"). */ + priceMinimum?: string | null; + /** Format: date-time */ + effectiveDate?: string | null; + /** + * @description Trading state of the contract + * @enum {string|null} + */ + marketState?: "open" | "closed" | null; + /** @description Display order within the event */ + sortOrder?: number | null; + strike?: components["schemas"]["Strike"]; + /** + * @deprecated + * @description Deprecated: use the event-level `sourceDetails` (`agency` + `index`) instead. Data source identifier for price observation (e.g., "GRR-KAIKO_BTCUSD_60S"). Present for crypto Up/Down contracts. + * @example GRR-KAIKO_BTCUSD_60S + */ + source?: string | null; + /** + * @description The observed settlement price. Only present after the contract is settled. + * @example 87654.32 + */ + settlementValue?: string | null; + }; + /** + * @description Strike or condition inequality type for contract threshold evaluation. - `reference`: Crypto Up/Down reference strike price captured at `availableAt` time. - `above`: Higher/Lower contract threshold. - `spread`: Point, run, or goal handicap spread line. - `over`: Total or prop threshold evaluated as strict greater than (`>`). - `over_or_equal`: Total or prop threshold evaluated as greater than or equal to (`>=`). - `under`: Total or prop threshold evaluated as strict less than (`<`). - `under_or_equal`: Position, rank, or total threshold evaluated as less than or equal to (`<=`). + * @example spread + * @enum {string} + */ + StrikeType: "reference" | "above" | "spread" | "over" | "over_or_equal" | "under" | "under_or_equal"; + /** @description Strike price or contract threshold information for Up/Down crypto contracts and sports prediction market contracts. */ + Strike: { + /** + * @description The strike price value. Null for "reference" type strikes where the value is determined at availableAt time. For sports contracts, this represents the derived numeric strike value (e.g. spread margin, total line, or position/rank threshold). + * @example 87500.00 + */ + value?: string | null; + type?: components["schemas"]["StrikeType"]; + /** + * Format: date-time + * @description When the strike price becomes available + * @example 2026-03-27T19:45:00.000Z + */ + availableAt?: string | null; + }; + /** @description Structured data source information for price observation. Replaces the deprecated flat `source` string on the event and contract. Present for crypto Up/Down events. Both fields are omitted when not available. */ + SourceDetails: { + /** + * @description The data provider / vendor name. + * @example Kaiko + */ + agency?: string | null; + /** + * @description The specific data feed identifier (the value previously carried by the flat `source` field). + * @example GRR-KAIKO_BTCUSD_60S + */ + index?: string | null; + } | null; + PricePoint: { + /** Format: date-time */ + timestamp?: string; + price?: string; + }; + /** @description Current bid/ask pricing for the contract */ + ContractPrices: { + /** @description Buy prices for each outcome */ + buy?: { + /** + * @description Price to buy YES outcome + * @example 0.42 + */ + yes?: string; + /** + * @description Price to buy NO outcome + * @example 0.58 + */ + no?: string; + }; + /** @description Sell prices for each outcome */ + sell?: { + /** + * @description Price to sell YES outcome + * @example 0.42 + */ + yes?: string; + /** + * @description Price to sell NO outcome + * @example 0.58 + */ + no?: string; + }; + /** + * @description Highest buy offer + * @example 0.49 + */ + bestBid?: string | null; + /** + * @description Lowest sell offer + * @example 0.54 + */ + bestAsk?: string | null; + /** + * @description Most recent transaction price + * @example 0.75 + */ + lastTradePrice?: string | null; + } | null; + /** @description A prediction market event containing one or more tradeable contracts */ + Event: { + id?: string; + /** @example Will Bitcoin reach $100k by end of 2028? */ + title?: string; + /** @example bitcoin-100k-2028 */ + slug?: string; + description?: string | null; + imageUrl?: string | null; + type?: components["schemas"]["MarketType"]; + /** @example crypto */ + category?: string; + series?: string | null; + sportsMarket?: components["schemas"]["SportsMarket"]; + /** + * @description The event ticker (e.g., "BTC100K2028") + * @example BTC100K2028 + */ + ticker?: string; + status?: components["schemas"]["MarketStatus"]; + /** Format: date-time */ + resolvedAt?: string | null; + /** Format: date-time */ + createdAt?: string; + /** @description Tradeable contracts within this event */ + contracts?: components["schemas"]["Contract"][]; + contractOrderbooks?: { + [key: string]: components["schemas"]["OrderBook"]; + }; + /** + * @description Total trading volume in USD + * @example 125000.00 + */ + volume?: string; + /** + * @description Total liquidity in USD + * @example 50000.00 + */ + liquidity?: string; + tags?: string[] | null; + /** Format: date-time */ + effectiveDate?: string; + /** Format: date-time */ + expiryDate?: string | null; + subcategory?: components["schemas"]["Subcategory"]; + /** + * @deprecated + * @description Deprecated: use `sourceDetails` (`agency` + `index`) instead. Data source identifier for price observation. Aggregated from contracts for crypto Up/Down events. + * @example GRR-KAIKO_BTCUSD_60S + */ + source?: string | null; + sourceDetails?: components["schemas"]["SourceDetails"]; + settlement?: components["schemas"]["Settlement"]; + }; + /** @description Nested category information for the event */ + Subcategory: { + /** + * @description Category identifier + * @example 35 + */ + id?: number; + /** + * @description URL-friendly category identifier + * @example crypto_solana + */ + slug?: string; + /** + * @description Display name + * @example Solana + */ + name?: string; + /** + * @description Category hierarchy path + * @example [ + * "Crypto", + * "Solana" + * ] + */ + path?: string[]; + } | null; + /** @description Settlement information for resolved events */ + Settlement: { + /** + * @description The observed settlement value (e.g., the price at expiry for crypto contracts) + * @example 87654.32 + */ + value?: string | null; + }; + EventsResponse: { + data?: components["schemas"]["Event"][]; + pagination?: components["schemas"]["Pagination"]; + }; + ContractMetadata: { + contractId?: string; + contractName?: string; + contractTicker?: string; + eventTicker?: string; + eventName?: string; + category?: string; + contractStatus?: string; + /** @description Event type ("binary" or "categorical") */ + eventType?: string; + /** Format: date-time */ + expiryDate?: string | null; + /** Format: date-time */ + resolvedAt?: string | null; + /** @description Winning outcome if resolved ("yes" or "no") */ + resolutionSide?: string | null; + /** @description Parent event ticker for sub-events */ + parentEventTicker?: string | null; + /** + * Format: date-time + * @description Start datetime (ISO 8601) + */ + startTime?: string | null; + }; + ComboLeg: { + /** + * Format: int64 + * @description Internal ID of the parent combo contract + * @example 456 + */ + comboId: bigint; + /** + * @description Zero-based position of this leg in the combo + * @example 0 + */ + legIndex: number; + /** + * @description Internal ID of the underlying single contract, represented as a decimal string + * @example 101 + */ + contractId: string; + /** + * @description The outcome this leg must settle for the combo to settle YES + * @example Yes + * @enum {string} + */ + requiredOutcome: "Yes" | "No"; + /** + * @description The outcome this leg has settled to, if resolved (`"Yes"` or `"No"`). Null while the leg is still active. + * @example null + */ + legOutcome?: string | null; + /** + * Format: date-time + * @description UTC timestamp when this leg resolved. Null while still active. + * @example null + */ + resolvedAt?: string | null; + /** @description Full metadata for the underlying single contract */ + contract?: components["schemas"]["ContractMetadata"] | null; + }; + ComboResponse: { + /** @description Metadata for the combo contract itself (ticker, status, expiry, etc.) */ + contract: components["schemas"]["ContractMetadata"]; + /** @description Ordered list of legs that make up this combo */ + legs: components["schemas"]["ComboLeg"][]; + }; + ListCombosResponse: { + /** @description List of combo contracts matching the query */ + combos: components["schemas"]["ComboResponse"][]; + pagination: components["schemas"]["Pagination"]; + }; + /** @description A canonical combo definition. The authenticated account is derived from the signed request and is not a request field. */ + CreateComboRequest: { + /** @description Two to six distinct underlying contract legs. The service canonicalizes their complete set, so leg order does not create a distinct combo. */ + legs: components["schemas"]["CreateComboLeg"][]; + }; + CreateComboLeg: { + /** + * @description Underlying contract ID as a decimal string. + * @example 101 + */ + contractId: string; + /** + * @description Required settlement outcome for this leg. + * @example Yes + * @enum {string} + */ + requiredOutcome: "Yes" | "No"; + }; + CreateComboResponse: { + combo: components["schemas"]["ComboSummary"]; + /** @description `false` when this request created the canonical combo; `true` when the canonical combo already existed. */ + alreadyExisted: boolean; + }; + ComboSummary: { + /** + * Format: int64 + * @description Internal combo ID. + * @example 456 + */ + id: bigint; + /** + * @description Canonical identity of the complete combo leg set. + * @example 101:Yes|202:No + */ + canonicalLegKey: string; + /** + * Format: int32 + * @description Number of legs in the combo. + * @example 2 + */ + legCount: number; + /** @description Human-readable combo name, when available. */ + displayName?: string; + /** @description Current combo status, when available. */ + status?: string; + /** + * Format: int64 + * @description Associated instrument ID, when available. + */ + instrumentId?: bigint; + /** + * @description Associated instrument symbol, when available. + * @example GEMI-CMB-0526-A7F3B2C1D4E5 + */ + instrumentSymbol?: string; + /** @description Whether the combo has been registered with an instrument symbol. */ + instrumentRegistered: boolean; + /** + * Format: date-time + * @description Latest expiry among the underlying legs, when available. + */ + latestExpiryDate?: string; + /** + * Format: date-time + * @description Creation time, when available. + */ + createdAt?: string; + /** + * Format: date-time + * @description Most recent update time, when available. + */ + updatedAt?: string; + /** @description Canonically ordered combo legs. */ + legs: components["schemas"]["ComboSummaryLeg"][]; + }; + ComboSummaryLeg: { + /** + * Format: int64 + * @description Parent combo ID. + */ + comboId: bigint; + /** + * Format: int32 + * @description Zero-based leg position in canonical order. + */ + legIndex: number; + /** @description Underlying contract ID as a decimal string. */ + contractId: string; + /** + * @description Required settlement outcome for the leg. + * @enum {string} + */ + requiredOutcome: "Yes" | "No"; + /** + * @description Settled outcome for the leg, when resolved. + * @enum {string|null} + */ + legOutcome?: "Yes" | "No" | null; + /** + * Format: date-time + * @description Resolution time for the leg, when resolved. + */ + resolvedAt?: string | null; + /** @description Underlying contract metadata, when available. */ + contract?: components["schemas"]["ContractMetadata"]; + }; + ComboWriteError: { + /** + * @description Error class. + * @example InvalidInput + */ + error: string; + /** + * @description Machine-readable code for validation or missing-leg errors, when available. + * @example COMBO_VALIDATION_ERROR + */ + code?: string; + /** + * @description Human-readable error detail. + * @example a combo needs 2-6 legs + */ + message: string; + }; + OrderRequest: { + /** + * @description Contract instrument symbol + * @example GEMI-FEDJAN26-DN25 + */ + symbol: string; + orderType: components["schemas"]["OrderType"]; + side: components["schemas"]["OrderSide"]; + /** + * Format: decimal + * @description Number of contracts + * @example 100 + */ + quantity: string; + /** + * Format: decimal + * @description Limit price (0-1 range) + * @example 0.65 + */ + price: string; + /** + * Format: decimal + * @description The price to trigger a stop-limit order (0-1 range). Only available for stop-limit orders. See [Stop-Limit Orders](#operation/placeOrder) above for `stopPrice`/`price` constraints. + * @example 0.60 + */ + stopPrice?: string; + outcome: components["schemas"]["Outcome"]; + timeInForce?: components["schemas"]["TimeInForce"]; + /** + * @description Set to `true` to require maker-only behavior. If the order would immediately take liquidity, the order is cancelled instead of filling. + * @default false + */ + makerOrCancel: boolean; + }; + PlaceOrderBatchRequest: { + /** @description Orders to submit. Every entry is validated before any order is submitted. All orders use the account associated with the authenticated request. */ + orders: components["schemas"]["OrderRequest"][]; + }; + /** @description An accepted order returned for one batch entry. */ + BatchOrderResponse: { + /** + * Format: int64 + * @example 12345678 + */ + orderId: bigint; + /** @description Hashed order ID; omitted when unavailable */ + hashOrderId?: string; + /** @description Client-provided order ID; omitted when unavailable */ + clientOrderId?: string; + /** @description Global order ID; omitted when unavailable */ + globalOrderId?: string; + /** @enum {string} */ + status: "open" | "filled" | "cancelled" | "closed"; + symbol: string; + side: components["schemas"]["OrderSide"]; + outcome: components["schemas"]["Outcome"]; + orderType: components["schemas"]["OrderType"]; + /** @enum {string} */ + timeInForce: "good-til-cancel" | "immediate-or-cancel" | "fill-or-kill" | "maker-or-cancel"; + /** @description Original order quantity */ + quantity: string; + /** @description Amount filled so far */ + filledQuantity: string; + /** @description Amount remaining to fill */ + remainingQuantity: string; + /** @description Limit price */ + price: string; + /** @description Stop trigger price; omitted unless populated for a `stop-limit` order */ + stopPrice?: string; + /** @description Average price of fills; omitted when unavailable */ + avgExecutionPrice?: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + /** + * Format: date-time + * @description Cancellation time; omitted unless the order was cancelled + */ + cancelledAt?: string; + contractMetadata?: components["schemas"]["ContractMetadata"]; + /** @description Promotional cash reserved or applied to the order; omitted when unavailable */ + promoCashApplied?: string; + /** @description Cash reserved for the unfilled portion of a resting buy order; omitted when unavailable */ + fundsOnHold?: string; + }; + PlaceOrderBatchSuccessResult: { + order: components["schemas"]["BatchOrderResponse"]; + }; + PlaceOrderBatchErrorResult: { + /** + * @description Error class for a rejected entry + * @example InsufficientFunds + */ + error: string; + /** + * @description Human-readable detail for a rejected entry + * @example Insufficient funds + */ + message: string; + }; + /** @description Exactly one outcome is present. Accepted entries contain `order`; rejected entries contain `error` and `message`. */ + PlaceOrderBatchResult: components["schemas"]["PlaceOrderBatchSuccessResult"] | components["schemas"]["PlaceOrderBatchErrorResult"]; + PlaceOrderBatchResponse: { + /** @description One result for each submitted order, in request order. */ + results: components["schemas"]["PlaceOrderBatchResult"][]; + }; + CancelOrderBatchRequest: { + /** @description Order IDs to cancel. Each ID may be an integer or a quoted numeric string. All IDs are validated before any cancellation is attempted. */ + orderIds: (bigint | string)[]; + }; + CancelOrderBatchSuccessResult: { + /** + * Format: int64 + * @description Order ID from the corresponding request entry. + * @example 12345678 + */ + orderId: bigint; + /** @enum {string} */ + result: "ok"; + }; + CancelOrderBatchErrorResult: { + /** + * Format: int64 + * @description Order ID from the corresponding request entry. + * @example 12345678 + */ + orderId: bigint; + /** + * @description Error class for a rejected cancellation + * @example OrderNotFound + */ + error: string; + /** + * @description Human-readable detail for a rejected cancellation + * @example Order 12345678 not found + */ + message: string; + }; + /** @description Exactly one outcome is present. Successful entries contain `orderId` and `result`; rejected entries contain `orderId`, `error`, and `message`. */ + CancelOrderBatchResult: components["schemas"]["CancelOrderBatchSuccessResult"] | components["schemas"]["CancelOrderBatchErrorResult"]; + CancelOrderBatchResponse: { + /** @description One result for each requested cancellation, in request order. */ + results: components["schemas"]["CancelOrderBatchResult"][]; + }; + OrderResponse: { + /** + * Format: int64 + * @example 12345678 + */ + orderId?: bigint; + hashOrderId?: string | null; + clientOrderId?: string | null; + globalOrderId?: string | null; + status?: components["schemas"]["OrderStatus"]; + symbol?: string; + side?: components["schemas"]["OrderSide"]; + outcome?: components["schemas"]["Outcome"]; + orderType?: components["schemas"]["OrderType"]; + /** @description Original order quantity */ + quantity?: string; + /** @description Amount filled so far */ + filledQuantity?: string; + /** @description Amount remaining to fill */ + remainingQuantity?: string; + /** @description Limit price */ + price?: string; + /** @description Stop trigger price (populated for `stop-limit` orders) */ + stopPrice?: string | null; + /** @description Average price of fills */ + avgExecutionPrice?: string | null; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + cancelledAt?: string | null; + contractMetadata?: components["schemas"]["ContractMetadata"]; + }; + OrdersResponse: { + orders?: components["schemas"]["OrderResponse"][]; + pagination?: components["schemas"]["PaginationSimple"]; + }; + Position: { + symbol?: string; + /** Format: int64 */ + instrumentId?: bigint; + /** @description Total position size */ + totalQuantity?: string; + /** @description Quantity currently on hold from open orders */ + quantityOnHold?: string; + /** @description Average entry price */ + avgPrice?: string; + outcome?: components["schemas"]["Outcome"]; + contractMetadata?: components["schemas"]["ContractMetadata"]; + prices?: components["schemas"]["PositionPrices"]; + /** @description Winning outcome ("yes" or "no") if the contract has resolved */ + resolutionSide?: string | null; + /** @description Whether the position is above the auto-start threshold */ + isAboveAutoStartThreshold?: boolean; + /** @description Whether the market is currently live/active */ + isLive?: boolean; + /** @description Realized profit/loss from sells */ + realizedPl?: string | null; + /** + * @description Mark-to-market value of the position in USD at the current sell price (bestBid for YES, bestAsk for NO). **Absent** from the response when the held outcome has no live sell quote (no liquidity to sell into) — surface a no-liquidity state rather than a price the user cannot transact at. `lastTradePrice` is still returned for display. Treat as `Optional`. + * @example 65.00 + */ + marketValue?: string; + /** + * @description Unrealized P&L in USD (`marketValue - costBasis`). **Absent** whenever `marketValue` is absent. Treat as `Optional`. + * @example 12.50 + */ + unrealizedPnl?: string; + /** + * Format: double + * @description Unrealized P&L as a percentage of cost basis. Expressed as a percent (e.g. `12.5` represents 12.5%, **not** `0.125`); rounded to 4 decimal places. **Absent** when there is no live sell quote, or when cost basis is zero. Treat as `Optional`. + * @example 23.81 + */ + unrealizedPct?: number; + }; + /** @description Current bid/ask/last-trade prices for the contract */ + PositionPrices: { + buy: { + yes?: string | null; + no?: string | null; + }; + sell: { + yes?: string | null; + no?: string | null; + }; + bestBid?: string | null; + bestAsk?: string | null; + lastTradePrice?: string | null; + } | null; + PositionsResponse: { + positions?: components["schemas"]["Position"][]; + /** @description Total number of positions (for pagination) */ + total?: number | null; + }; + /** @description A historically settled position in a resolved prediction market contract. */ + SettledPosition: { + /** + * Format: int64 + * @description Account that held the position + */ + accountId?: bigint; + /** + * Format: int64 + * @description Unique instrument identifier for the contract + */ + instrumentId?: bigint; + /** + * @description Contract instrument symbol + * @example GEMI-FEDJAN26-DN25 + */ + instrumentSymbol?: string; + /** + * @description Signed position held at settlement. Positive values represent a `yes` position; negative values represent a `no` position. + * @example 125 + */ + position?: string; + /** + * @description Absolute quantity held at settlement (unsigned) + * @example 125 + */ + positionQuantity?: string; + outcome?: components["schemas"]["Outcome"]; + /** + * @description Payout received from settlement. `0` when the position lost. + * @example 125.00 + */ + payout?: string; + /** @description The winning outcome of the contract */ + resolutionSide?: components["schemas"]["Outcome"]; + /** + * Format: date-time + * @description Settlement timestamp (ISO 8601) + */ + settledAt?: string; + contractMetadata?: components["schemas"]["ContractMetadata"]; + /** + * @description Total amount spent to enter the position, net of any prior realized P&L from partial sells. Omitted when cost-basis data is not available. + * @example 78.75 + */ + costBasis?: string | null; + /** + * @description Realized profit or loss recorded from sells prior to settlement. Omitted when not available. + * @example 0 + */ + realizedPnl?: string | null; + /** + * @description Net profit for the position, computed as `payout - costBasis + realizedPnl`. Omitted when `costBasis` is not available. + * @example 46.25 + */ + netProfit?: string | null; + }; + SettledPositionsResponse: { + positions?: components["schemas"]["SettledPosition"][]; + /** @description Total number of settled positions across all pages for the current filter set. */ + total?: number | null; + /** @description Sum of `payout` across all settled positions in the filter set. Retained for binary back-compat with the legacy response shape; **field is absent (not `null`) on the unified backend** because computing a roll-up over the full filtered set would require a separate aggregate query (deferred until a partner asks). Play's default `OptionHandlers` omits absent `Option` fields rather than emitting `null`. */ + totalPayout?: string; + /** @description Sum of `costBasis` across all settled positions in the filter set. Retained for binary back-compat; **field is absent (not `null`) on the unified backend** (see `totalPayout`). */ + totalCostBasis?: string; + /** @description Sum of `netProfit` across all settled positions in the filter set. Retained for binary back-compat; **field is absent (not `null`) on the unified backend** (see `totalPayout`). */ + totalNetProfit?: string; + /** @description Cash-outs (early sells before contract resolution) in the same account-scoped time window as the returned page's settled positions. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. `positions[]` pagination is unaffected — `limit`/`offset` continue to scope `positions[]` only. */ + cashOuts?: components["schemas"]["CashedOutPosition"][]; + /** + * @description Sum of `cashOuts[].proceeds` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. + * @example 120.00 + */ + totalCashOutProceeds?: string; + /** + * @description Sum of `cashOuts[].costBasis` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. + * @example 100.00 + */ + totalCashOutCostBasis?: string; + /** + * @description Sum of `cashOuts[].netProfit` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. + * @example 20.00 + */ + totalCashOutNetProfit?: string; + }; + /** @description A qualifying cash-out (early sell before contract resolution) with cost-basis context. Exposed only via the `withCashOuts=true` sibling array on `POST /v1/prediction-markets/positions/settled`. Distinct from `SettledPosition` — cash-outs don't have a `payout` or `resolutionSide` since the contract hadn't resolved when the user sold. */ + CashedOutPosition: { + /** + * Format: int64 + * @description Account that held the position. + * @example 456 + */ + accountId: bigint; + /** + * Format: int64 + * @description Contract instrument ID. + * @example 16789219 + */ + instrumentId: bigint; + /** + * @description Contract instrument symbol. + * @example GEMI-BTC100K-YES + */ + instrumentSymbol: string; + /** + * Format: date-time + * @description Wall-clock timestamp when the cash-out order closed (ISO 8601). + * @example 2026-05-15T14:30:00.000Z + */ + timestamp: string; + /** + * @description Quantity sold (cumulative filled quantity on the cash-out order). + * @example 10 + */ + filledQuantity: string; + /** + * @description Always `sell` for cash-outs. + * @example sell + * @enum {string} + */ + side: "sell"; + /** + * @description Amount received from the sale in USD. For prediction sells, proceeds flow through `cash_balance` rather than `closed_orders.total_spend`, so the value is derived from position-balance snapshots before/after the fill. + * @example 10.50 + */ + proceeds: string; + /** + * @description Cost basis allocated proportionally to the filled quantity (`(costBasisSpend / costBasisPositionBalance) * filledQuantity`). + * @example 10.00 + */ + costBasis: string; + /** + * @description Realized P&L from this cash-out fill (`proceeds - costBasis`). Equals the ledger `realized_pl` delta on the position-balance row pair around the fill; falls back to `0` under transient market-data lag so a missing post-fill snapshot can't poison the page. + * @example 0.50 + */ + netProfit: string; + contractMetadata?: components["schemas"]["ContractMetadata"]; + }; + ContractShareVolume: { + /** + * @description Contract instrument symbol + * @example GEMI-FED260318-CUT25 + */ + symbol?: string; + /** + * @description Total taker volume across all participants (in shares) + * @example 94625 + */ + totalQty?: string; + /** + * @description The authenticated user's taker (aggressor) volume (in shares) + * @example 1 + */ + userAggressorQty?: string | null; + /** + * @description The authenticated user's maker (resting) volume (in shares) + * @example 0 + */ + userRestingQty?: string | null; + }; + VolumeMetricsResponse: { + /** + * @description The event ticker + * @example FED260318 + */ + eventTicker?: string; + contracts?: components["schemas"]["ContractShareVolume"][]; + }; + PredictionMarketVolumeCategory: { + /** + * @description Display-name path from the top-level category to this category. It replaces recursive child nodes. + * @example [ + * "Sports", + * "Football", + * "Pro Football" + * ] + */ + categoryPath: string[]; + /** @description Total volume for this category, including all descendant categories. */ + volume: components["schemas"]["PredictionMarketVolumeDecimal"]; + }; + PredictionMarketHourlyVolumeCategory: { + /** + * Format: date-time + * @description Inclusive UTC start of this hourly period. + * @example 2026-07-20T00:00:00Z + */ + periodStart: string; + /** + * @description Display-name path from the top-level category to this category. It replaces recursive child nodes. + * @example [ + * "Sports", + * "Football", + * "Pro Football" + * ] + */ + categoryPath: string[]; + /** @description Total volume for this category in this hour, including all descendant categories. */ + volume: components["schemas"]["PredictionMarketVolumeDecimal"]; + }; + /** + * @description Non-negative decimal string. Preserve it as a string to avoid floating-point precision loss. + * @example 143567.25 + */ + PredictionMarketVolumeDecimal: string; + MakerRebateRateRule: { + /** + * Format: int64 + * @description Stable identifier for this rate rule. + * @example 12 + */ + id: bigint; + /** + * Format: int32 + * @description Portion of the maker fee that is rebated, in basis points (10000 bps = 100%). + * @example 5000 + */ + rebate_multiplier_bps: number; + /** + * Format: date-time + * @description ISO-8601 timestamp at which this rule becomes effective. Always present; in practice never `null`. + * @example 2026-03-19T00:00:00Z + */ + effective_from: string | null; + /** + * @description Market category this rule applies to. When absent, the rule applies to all categories. + * @example Crypto + */ + category?: string; + /** + * Format: date-time + * @description ISO-8601 timestamp after which this rule is superseded. Omitted when the rule is still current. + * @example 2026-04-19T00:00:00Z + */ + effective_to?: string; + }; + MakerRebateRatesResponse: { + rate_rules: components["schemas"]["MakerRebateRateRule"][]; + }; + MakerRebatePayout: { + /** + * Format: int64 + * @description Stable payout identifier. + * @example 9182 + */ + id: bigint; + /** + * @description Total qualifying maker volume contributing to this payout, in USD. + * @example 12450.00 + */ + total_volume_usd: string; + /** + * @description Total rebate paid, in USD. + * @example 6.23 + */ + total_rebate_usd: string; + /** + * Format: int32 + * @description Number of qualifying maker fills that contributed to the payout. + * @example 187 + */ + total_fill_count: number; + /** + * @description Payout status (e.g. `PENDING`, `PAID`). + * @example PAID + */ + status: string; + /** + * Format: date-time + * @description ISO-8601 timestamp at which the rebate was credited. Always present; `null` for payouts that have not yet been paid. + * @example 2026-05-20T21:00:00Z + */ + paid_at: string | null; + /** + * Format: date-time + * @description ISO-8601 timestamp at which the payout row was created. Always present. + * @example 2026-05-20T20:55:12Z + */ + created_at: string | null; + }; + MakerRebatePayoutsResponse: { + payouts: components["schemas"]["MakerRebatePayout"][]; + }; + MakerRebateLifetimeSummary: { + /** + * @description Sum of `total_rebate_usd` across payouts in the window. + * @example 152.40 + */ + total_earned_usd: string; + /** + * Format: int64 + * @description Sum of qualifying maker fills across payouts in the window. + * @example 4218 + */ + total_fill_count: bigint; + /** + * @description Sum of qualifying maker volume (USD) across payouts in the window. + * @example 304800.00 + */ + total_volume_usd: string; + /** + * Format: int32 + * @description Number of payouts in the window. Always present; `0` when no payouts exist in the window. + * @example 27 + */ + payout_count: number; + /** + * Format: date + * @description Date of the earliest payout in the window, or `null` if no payouts exist. + * @example 2026-03-19 + */ + first_payout_date: string | null; + /** + * Format: date + * @description Date of the most recent payout in the window, or `null` if no payouts exist. + * @example 2026-05-20 + */ + last_payout_date: string | null; + }; + LiquidityRewardsConfig: { + /** + * Format: int32 + * @description Quotes wider than this spread score zero in the scoring algorithm. Only present when `enabled` is `true`. + * @example 10 + */ + max_spread_cents?: number; + /** + * @description Daily reward amounts below this threshold are suppressed (sub-threshold accounts get no row at all). Only present when `enabled` is `true`. + * @example 1.00 + */ + min_payout_threshold_usd?: string; + /** + * @description True when the program is fully configured upstream. When false, the response collapses to `{ "enabled": false }` only. + * @example true + */ + enabled: boolean; + }; + LiquidityRewardEvent: { + /** + * @description Event ticker (e.g. `BTC2605202100`). + * @example BTC2605202100 + */ + event_ticker: string; + /** + * @description Event title. + * @example BTC above $95,000? + */ + title: string; + /** + * @description Market category. + * @example Crypto + */ + category: string; + /** + * @description Daily USD reward pool budgeted for this event. + * @example 500.00 + */ + daily_pool_usd: string; + /** + * @description Whether the pool came from a per-event override or the category default. + * @example event_override + * @enum {string} + */ + pool_source: "event_override" | "category_default" | "unspecified"; + /** + * Format: date-time + * @description ISO-8601 timestamp at which the event ends and stops scoring. `null` when the underlying event has no end timestamp set. + * @example 2026-05-20T21:00:00Z + */ + ends_at: string | null; + /** + * Format: int32 + * @description Number of accounts that met qualifying-maker criteria in the most recent snapshot window for this event. + * @example 14 + */ + qualifying_maker_count: number; + /** + * @description Optional URL for the event icon. Omitted when not configured. + * @example https://example.com/btc.png + */ + icon_url?: string; + }; + LiquidityRewardsEventsResponse: { + events: components["schemas"]["LiquidityRewardEvent"][]; + pagination: components["schemas"]["Pagination"]; + /** + * Format: date + * @description Most recent date for which scoring has been written. `null` when no scoring has run yet. + * @example 2026-05-19 + */ + last_score_date: string | null; + }; + LiquidityEventScore: { + /** + * Format: int64 + * @description Stable event identifier. + * @example 1234567890 + */ + event_id: bigint; + /** + * @description Event title. + * @example BTC above $95,000? + */ + event_name: string; + /** + * @description Market category. + * @example Crypto + */ + category_name: string; + /** + * @description This account's normalized score for the event on the scoring date (0-1 range as a decimal string). + * @example 0.4521 + */ + normalized_score: string; + /** + * Format: int32 + * @description Number of snapshots in which this account had a qualifying quote. + * @example 1180 + */ + snapshot_count: number; + /** + * Format: int32 + * @description Total snapshots taken for the event on the scoring date. + * @example 1440 + */ + total_snapshots: number; + /** + * @description Portion of the day's total reward attributed to this event. + * @example 8.20 + */ + event_reward_usd: string; + }; + LiquidityDailySummary: { + /** + * Format: date + * @description Date the payout applies to (Eastern Time). + * @example 2026-05-07 + */ + payout_date: string; + /** + * @description Total USD reward for the day across all events the account scored on. + * @example 12.45 + */ + total_reward_usd: string; + /** + * @description Status of the day's payout (e.g. `PENDING`, `PAID`, `ZERO_AMOUNT`). + * @example PAID + */ + payout_status: string; + /** + * Format: date-time + * @description ISO-8601 timestamp the day's payout was credited. Always present; `null` if not yet paid. + * @example 2026-05-08T21:00:00Z + */ + paid_at: string | null; + /** @description Per-event score breakdown showing how the day's total was distributed. */ + events: components["schemas"]["LiquidityEventScore"][]; + }; + LiquidityRewardsDailySummaryResponse: { + daily_summaries: components["schemas"]["LiquidityDailySummary"][]; + }; + LiquidityRewardsLifetimeSummary: { + /** + * @description Sum of `total_reward_usd` across daily payouts in the window. + * @example 84.20 + */ + total_earned_usd: string; + /** + * Format: int32 + * @description Number of daily payouts in the window. Always present; `0` when no payouts exist in the window. + * @example 12 + */ + payout_count: number; + /** + * Format: date + * @description Date of the earliest payout in the window, or `null` if no payouts exist. + * @example 2026-05-08 + */ + first_payout_date: string | null; + /** + * Format: date + * @description Date of the most recent payout in the window, or `null` if no payouts exist. + * @example 2026-05-20 + */ + last_payout_date: string | null; + }; + }; + responses: { + /** @description Invalid request parameters */ + BadRequest: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Authentication required or invalid credentials */ + Unauthorized: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + InternalError: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Prediction markets feature is temporarily unavailable */ + ServiceUnavailable: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + parameters: { + /** @description Maximum number of results to return (max 500) */ + Limit: number; + /** @description Number of results to skip for pagination */ + Offset: number; + /** @description Filter by `sportsMarket.sport`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`; valid combinations with no matching events return an empty result. */ + SportFilter: components["schemas"]["SportsMarketSport"][]; + /** @description Filter by `sportsMarket.type`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + SportsMarketTypeFilter: components["schemas"]["SportsMarketType"][]; + /** @description Filter by `sportsMarket.subject`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + SportsMarketSubjectFilter: components["schemas"]["SportsMarketSubject"][]; + /** @description Filter by `sportsMarket.scope.type`. Repeat the parameter to match any supplied value (OR). Ordinal and range qualifiers are not inferred by this filter. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + SportsMarketScopeFilter: components["schemas"]["SportsMarketScopeType"][]; + /** @description Filter by `sportsMarket.metric`. Repeat the parameter to match any supplied value (OR). A metric can be queried without `sport`; use `sport` when its sport-specific meaning matters. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + SportsMarketMetricFilter: components["schemas"]["SportsMarketMetric"][]; + }; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + listEvents: { + parameters: { + query?: { + /** @description Filter by event status (can specify multiple) */ + status?: components["schemas"]["MarketStatus"][]; + /** @description Filter by category (can specify multiple). If omitted, returns events from all categories. */ + category?: string[]; + /** @description Filter by `sportsMarket.sport`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`; valid combinations with no matching events return an empty result. */ + sport?: components["parameters"]["SportFilter"]; + /** @description Filter by `sportsMarket.type`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + sports_market_type?: components["parameters"]["SportsMarketTypeFilter"]; + /** @description Filter by `sportsMarket.subject`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + sports_market_subject?: components["parameters"]["SportsMarketSubjectFilter"]; + /** @description Filter by `sportsMarket.scope.type`. Repeat the parameter to match any supplied value (OR). Ordinal and range qualifiers are not inferred by this filter. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + sports_market_scope?: components["parameters"]["SportsMarketScopeFilter"]; + /** @description Filter by `sportsMarket.metric`. Repeat the parameter to match any supplied value (OR). A metric can be queried without `sport`; use `sport` when its sport-specific meaning matters. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + sports_market_metric?: components["parameters"]["SportsMarketMetricFilter"]; + /** @description Search text to filter events by title */ + search?: string; + /** @description Maximum number of results to return (max 500) */ + limit?: components["parameters"]["Limit"]; + /** @description Number of results to skip for pagination */ + offset?: components["parameters"]["Offset"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EventsResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["ServiceUnavailable"]; + }; + }; + getEvent: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The event ticker symbol (e.g., "BTC100K") */ + eventTicker: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Event"]; + }; + }; + /** @description Event not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["ServiceUnavailable"]; + }; + }; + getEventStrike: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The event ticker symbol (e.g., "BTC05M2603271950") */ + eventTicker: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Strike"]; + }; + }; + /** @description Strike not found (event doesn't exist or doesn't have strike data) */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "Strike not found" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["ServiceUnavailable"]; + }; + }; + listNewlyListedEvents: { + parameters: { + query?: { + /** @description Filter by category (can specify multiple). If omitted, returns events from all categories. */ + category?: string[]; + /** @description Filter by `sportsMarket.sport`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`; valid combinations with no matching events return an empty result. */ + sport?: components["parameters"]["SportFilter"]; + /** @description Filter by `sportsMarket.type`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + sports_market_type?: components["parameters"]["SportsMarketTypeFilter"]; + /** @description Filter by `sportsMarket.subject`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + sports_market_subject?: components["parameters"]["SportsMarketSubjectFilter"]; + /** @description Filter by `sportsMarket.scope.type`. Repeat the parameter to match any supplied value (OR). Ordinal and range qualifiers are not inferred by this filter. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + sports_market_scope?: components["parameters"]["SportsMarketScopeFilter"]; + /** @description Filter by `sportsMarket.metric`. Repeat the parameter to match any supplied value (OR). A metric can be queried without `sport`; use `sport` when its sport-specific meaning matters. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + sports_market_metric?: components["parameters"]["SportsMarketMetricFilter"]; + /** @description Maximum number of results to return (max 500) */ + limit?: components["parameters"]["Limit"]; + /** @description Number of results to skip for pagination */ + offset?: components["parameters"]["Offset"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EventsResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["ServiceUnavailable"]; + }; + }; + listRecentlySettledEvents: { + parameters: { + query?: { + /** @description Filter by category (can specify multiple). If omitted, returns events from all categories. */ + category?: string[]; + /** @description Filter by `sportsMarket.sport`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`; valid combinations with no matching events return an empty result. */ + sport?: components["parameters"]["SportFilter"]; + /** @description Filter by `sportsMarket.type`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + sports_market_type?: components["parameters"]["SportsMarketTypeFilter"]; + /** @description Filter by `sportsMarket.subject`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + sports_market_subject?: components["parameters"]["SportsMarketSubjectFilter"]; + /** @description Filter by `sportsMarket.scope.type`. Repeat the parameter to match any supplied value (OR). Ordinal and range qualifiers are not inferred by this filter. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + sports_market_scope?: components["parameters"]["SportsMarketScopeFilter"]; + /** @description Filter by `sportsMarket.metric`. Repeat the parameter to match any supplied value (OR). A metric can be queried without `sport`; use `sport` when its sport-specific meaning matters. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + sports_market_metric?: components["parameters"]["SportsMarketMetricFilter"]; + /** @description Maximum number of results to return (max 500) */ + limit?: components["parameters"]["Limit"]; + /** @description Number of results to skip for pagination */ + offset?: components["parameters"]["Offset"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EventsResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["ServiceUnavailable"]; + }; + }; + listUpcomingEvents: { + parameters: { + query?: { + /** @description Filter by category (can specify multiple). If omitted, returns events from all categories. */ + category?: string[]; + /** @description Filter by `sportsMarket.sport`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`; valid combinations with no matching events return an empty result. */ + sport?: components["parameters"]["SportFilter"]; + /** @description Filter by `sportsMarket.type`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + sports_market_type?: components["parameters"]["SportsMarketTypeFilter"]; + /** @description Filter by `sportsMarket.subject`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + sports_market_subject?: components["parameters"]["SportsMarketSubjectFilter"]; + /** @description Filter by `sportsMarket.scope.type`. Repeat the parameter to match any supplied value (OR). Ordinal and range qualifiers are not inferred by this filter. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + sports_market_scope?: components["parameters"]["SportsMarketScopeFilter"]; + /** @description Filter by `sportsMarket.metric`. Repeat the parameter to match any supplied value (OR). A metric can be queried without `sport`; use `sport` when its sport-specific meaning matters. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ + sports_market_metric?: components["parameters"]["SportsMarketMetricFilter"]; + /** @description Maximum number of results to return (max 500) */ + limit?: components["parameters"]["Limit"]; + /** @description Number of results to skip for pagination */ + offset?: components["parameters"]["Offset"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EventsResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["ServiceUnavailable"]; + }; + }; + getCategories: { + parameters: { + query?: { + /** @description Filter categories by event status */ + status?: components["schemas"]["MarketStatus"][]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** + * @example [ + * "sports", + * "politics", + * "crypto", + * "entertainment" + * ] + */ + categories?: string[]; + }; + }; + }; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["ServiceUnavailable"]; + }; + }; + getPredictionMarketDailyVolume: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Completed UTC calendar date in `YYYY-MM-DD` format. */ + date: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Category volume for the requested UTC day */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PredictionMarketVolumeCategory"][]; + }; + }; + /** @description Invalid or unsupported date. The message includes the current one-year UTC date bounds. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description No complete volume data is available for the requested date. This includes dates before prediction markets launched and post-launch dates with a missing source hour. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "NOT_FOUND", + * "message": "Prediction market volume data is not available for the requested date" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description The canonical source is invalid or temporarily unavailable. The endpoint does not return a partial result. */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "SERVICE_UNAVAILABLE", + * "message": "Prediction market volume data is temporarily unavailable" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + getPredictionMarketHourlyVolume: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Completed UTC calendar date in `YYYY-MM-DD` format. */ + date: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Hourly category volume for the requested UTC day */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PredictionMarketHourlyVolumeCategory"][]; + }; + }; + /** @description Invalid or unsupported date. The message includes the current one-year UTC date bounds. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description No complete volume data is available for the requested date. This includes dates before prediction markets launched and post-launch dates with a missing source hour. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "NOT_FOUND", + * "message": "Prediction market volume data is not available for the requested date" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description The canonical source is invalid or temporarily unavailable. The endpoint does not return a partial result. */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "SERVICE_UNAVAILABLE", + * "message": "Prediction market volume data is temporarily unavailable" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + getPredictionMarketsTerms: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Latest Prediction Markets terms */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PredictionMarketsTerms"]; + }; + }; + /** @description No Prediction Markets terms are configured */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "TermsNotFound", + * "message": "No terms configured for prediction markets" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + 500: components["responses"]["InternalError"]; + }; + }; + getPredictionMarketsTermsStatus: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Terms acceptance status */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PredictionMarketsTermsStatus"]; + }; + }; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + }; + }; + acceptPredictionMarketsTerms: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Terms accepted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "success": true + * } + */ + "application/json": components["schemas"]["AcceptPredictionMarketsTermsResponse"]; + }; + }; + 401: components["responses"]["Unauthorized"]; + /** @description No Prediction Markets terms are configured */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "TermsNotFound", + * "message": "No terms configured for prediction markets" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + 500: components["responses"]["InternalError"]; + }; + }; + placeOrder: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OrderRequest"]; + }; + }; + responses: { + /** @description Order created successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrderResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + /** @description Order rejected (e.g., insufficient funds) */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["ServiceUnavailable"]; + }; + }; + placeOrderBatch: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PlaceOrderBatchRequest"]; + }; + }; + responses: { + /** @description Batch processed. Results are returned in request order and may contain both successful orders and per-entry errors. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PlaceOrderBatchResponse"]; + }; + }; + /** @description Invalid payload, empty batch, more than 20 entries, or an invalid order. No orders are submitted. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthErrorResponse"] | components["schemas"]["PredictionMarketsError"]; + }; + }; + /** @description Authentication is missing or invalid. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthErrorResponse"]; + }; + }; + /** @description The account is not permitted to place orders or has not accepted the current Prediction Markets terms. No orders are submitted. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthErrorResponse"] | components["schemas"]["AccountGroupBlockedError"] | components["schemas"]["TermsNotAcceptedError"] | components["schemas"]["RestrictedSellOnlyError"]; + }; + }; + /** @description The request nonce conflicts with a previously submitted request. */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthErrorResponse"]; + }; + }; + /** @description An order failed an up-front risk check. No orders are submitted. */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PredictionMarketsError"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PredictionMarketsError"]; + }; + }; + /** @description Prediction markets or batch orders are temporarily unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PredictionMarketsError"]; + }; + }; + }; + }; + cancelOrder: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * Format: int64 + * @description The order ID to cancel + * @example 12345678 + */ + orderId: bigint; + }; + }; + }; + responses: { + /** @description Order cancelled successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example ok */ + result?: string; + /** @example Order 12345678 cancelled successfully */ + message?: string; + }; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + /** @description Order not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Order cannot be cancelled (e.g., already filled) */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["ServiceUnavailable"]; + }; + }; + cancelOrderBatch: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CancelOrderBatchRequest"]; + }; + }; + responses: { + /** @description Batch processed. Results are returned in request order and may contain both successful cancellations and per-entry errors. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CancelOrderBatchResponse"]; + }; + }; + /** @description Invalid payload, empty batch, more than 20 entries, or an invalid order ID. No orders are cancelled. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthErrorResponse"] | components["schemas"]["PredictionMarketsError"]; + }; + }; + /** @description Authentication is missing or invalid. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthErrorResponse"]; + }; + }; + /** @description The account is not permitted to cancel orders. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthErrorResponse"]; + }; + }; + /** @description The request nonce conflicts with a previously submitted request. */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthErrorResponse"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PredictionMarketsError"]; + }; + }; + /** @description Prediction markets or batch orders are temporarily unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PredictionMarketsError"]; + }; + }; + }; + }; + getActiveOrders: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + /** + * @description Filter by contract instrument symbol + * @example GEMI-FEDJAN26-DN25 + */ + symbol?: string; + /** + * @description Maximum number of results to return (default 50, max 100) + * @default 50 + */ + limit?: number; + /** + * @description Number of results to skip for pagination + * @default 0 + */ + offset?: number; + }; + }; + }; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrdersResponse"]; + }; + }; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["ServiceUnavailable"]; + }; + }; + getOrderHistory: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + /** + * @description Filter by order status + * @enum {string} + */ + status?: "filled" | "cancelled"; + /** + * @description Filter by contract instrument symbol + * @example GEMI-FEDJAN26-DN25 + */ + symbol?: string; + /** + * @description Maximum number of results to return. Defaults to 50 and is capped at 1000. + * @default 50 + */ + limit?: number; + /** + * @description Number of results to skip for pagination. Offset is ignored when `from` or `to` is supplied. + * @default 0 + */ + offset?: number; + /** + * Format: int64 + * @description Inclusive start of the order-closed time range, expressed as Unix epoch milliseconds. Use with `to` for a UTC daily window. + * @example 1775001600000 + */ + from?: bigint; + /** + * Format: int64 + * @description Exclusive end of the order-closed time range, expressed as Unix epoch milliseconds. `from` must not be later than `to`. + * @example 1775088000000 + */ + to?: bigint; + }; + }; + }; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrdersResponse"]; + }; + }; + /** @description Invalid status parameter or date range */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["ServiceUnavailable"]; + }; + }; + getPositions: { + parameters: { + query?: { + /** @description Filter positions to a single event ticker (e.g. `FEDJAN26`). Positions on sub-events whose `parentEventTicker` matches the value may also be included. */ + eventTicker?: string; + /** @description Maximum number of positions to return. Clamped to `[1, 1000]` when supplied. Omit for legacy unpaginated behavior. */ + limit?: number; + /** @description Number of positions to skip for pagination. Floor-clamped to `0` when supplied. Ignored when `limit` is omitted (the response is unpaginated). */ + offset?: number; + /** @description Sort order. Accepts `positionValue`, `unrealizedPnl`, or `expiryDate` (case-insensitive), optionally prefixed with `+` (ascending) or `-` (descending). A bare field name uses each field's default direction: `positionValue` and `unrealizedPnl` default to descending; `expiryDate` defaults to ascending (soonest-first). `unrealizedPnl` and `expiryDate` sort NULLS LAST so positions without the sort key sink to the bottom regardless of direction. `instrumentId` ascending is the final tiebreaker for stable pagination across quote ticks. A malformed `sort` value silently falls back to `-positionValue` — no `400` is returned. */ + sort?: "positionValue" | "+positionValue" | "-positionValue" | "unrealizedPnl" | "+unrealizedPnl" | "-unrealizedPnl" | "expiryDate" | "+expiryDate" | "-expiryDate"; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PositionsResponse"]; + }; + }; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["ServiceUnavailable"]; + }; + }; + getSettledPositions: { + parameters: { + query?: { + /** @description Optional event ticker to filter settled positions to a single event (e.g. `FEDJAN26`). If omitted, all settled positions for the account are returned. */ + eventTicker?: string; + /** @description Maximum number of settled positions to return. */ + limit?: number; + /** @description Number of settled positions to skip for pagination. */ + offset?: number; + /** @description Sort order. Accepts `date` or `payout`, optionally prefixed with `+` (ascending) or `-` (descending). A bare field name defaults to descending. `date` ascending is rejected and silently falls back to the default order — settled positions are conceptually ordered most-recent-first. A malformed `sort` value also falls back silently; no `400` is returned. */ + sort?: "date" | "-date" | "payout" | "+payout" | "-payout"; + /** @description Case-insensitive substring filter. Matches against the event name, contract name, event ticker, or any ancestor category name in the contract's category subtree (up to four levels). Whitespace is trimmed; inputs under 3 characters are dropped (GIN trigram lookup floor); inputs over 64 characters are truncated. */ + search?: string; + /** @description Filter to settled positions whose contract's event belongs to the named category (or any of its descendants in the category tree). Whitespace is trimmed; empty values are ignored. */ + category?: string; + /** @description Opt-in flag. When `true`, the response carries new sibling fields (`cashOuts`, `totalCashOutProceeds`, `totalCashOutCostBasis`, `totalCashOutNetProfit`) populated with the qualifying cash-outs in the same account-scoped time window as the returned page's settled positions. When `false` (default) the response shape is byte-identical to the pre-`withCashOuts` contract: the `positions[]` element schema is unchanged regardless of the flag. */ + withCashOuts?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SettledPositionsResponse"]; + }; + }; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["ServiceUnavailable"]; + }; + }; + getVolumeMetrics: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * @description The event ticker symbol + * @example FED260318 + */ + eventTicker: string; + /** + * Format: int64 + * @description Start of time range filter (epoch milliseconds). If omitted, defaults to the earliest contract creation time. + */ + startTime?: bigint; + /** + * Format: int64 + * @description End of time range filter (epoch milliseconds). If omitted, includes all trades up to now. + */ + endTime?: bigint; + }; + }; + }; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["VolumeMetricsResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["ServiceUnavailable"]; + }; + }; + listCombos: { + parameters: { + query?: { + /** @description Filter by combo contract status (for example, `Active`, `Settled`, or `Voided`). Defaults to `Active` when omitted. */ + status?: string; + /** @description Filter to combos that contain a specific underlying contract ID as a leg */ + contractId?: bigint; + /** @description Filter by whether the combo has been registered with an instrument symbol */ + instrumentRegistered?: boolean; + /** @description Maximum number of results to return (max 500) */ + limit?: components["parameters"]["Limit"]; + /** @description Number of results to skip for pagination */ + offset?: components["parameters"]["Offset"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListCombosResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["ServiceUnavailable"]; + }; + }; + createCombo: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateComboRequest"]; + }; + }; + responses: { + /** @description The canonical combo already exists. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreateComboResponse"]; + }; + }; + /** @description A new canonical combo was created. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreateComboResponse"]; + }; + }; + /** @description The request body is malformed or fails combo validation. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "InvalidInput", + * "code": "COMBO_VALIDATION_ERROR", + * "message": "a combo needs 2-6 legs" + * } + */ + "application/json": components["schemas"]["ComboWriteError"]; + }; + }; + /** @description Signed private REST authentication is missing or invalid. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthErrorResponse"]; + }; + }; + /** @description The API key lacks `PredictionsNewOrder`, or the authenticated trading account is restricted. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthErrorResponse"]; + }; + }; + /** @description Combos are unavailable, or an underlying contract in the request cannot be found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ComboWriteError"]; + }; + }; + /** @description An unexpected error occurred while creating the combo. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "InternalError", + * "message": "An unexpected error occurred" + * } + */ + "application/json": components["schemas"]["ComboWriteError"]; + }; + }; + }; + }; + getComboByInstrumentSymbol: { + parameters: { + query?: never; + header?: never; + path: { + /** + * @description The combo contract's instrument symbol (e.g. `GEMI-CMB-0526-A7F3B2C1D4E5`) + * @example GEMI-CMB-0526-A7F3B2C1D4E5 + */ + instrumentSymbol: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ComboResponse"]; + }; + }; + /** @description Combo not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "NOT_FOUND", + * "message": "Combo not found" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["ServiceUnavailable"]; + }; + }; + getMakerRebateRates: { + parameters: { + query?: { + /** @description Filter to rules that apply to this category (e.g. `Crypto`, `Sports`). When omitted, returns all rules. */ + category?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MakerRebateRatesResponse"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "An unexpected error occurred" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Maker rebate program is not currently available */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "Maker rebate program is not currently available" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + listMakerRebatePayouts: { + parameters: { + query?: { + /** @description Maximum number of payouts to return (default 50, clamped to [1, 100]). */ + limit?: number; + /** @description Number of payouts to skip (default 0). */ + offset?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MakerRebatePayoutsResponse"]; + }; + }; + 401: components["responses"]["Unauthorized"]; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "An unexpected error occurred" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Maker rebate program is not currently available */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "Maker rebate program is not currently available" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + getMakerRebateLifetimeSummary: { + parameters: { + query?: { + /** @description Inclusive start of the payout date window (`YYYY-MM-DD`, Eastern Time). Must be provided together with `dateTo`. */ + dateFrom?: string; + /** @description Inclusive end of the payout date window (`YYYY-MM-DD`, Eastern Time). Must be on or after `dateFrom` and within 5 years of it. */ + dateTo?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MakerRebateLifetimeSummary"]; + }; + }; + /** @description Invalid date parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "BAD_REQUEST", + * "message": "date_to must be on or after date_from" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + 401: components["responses"]["Unauthorized"]; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "INTERNAL_ERROR", + * "message": "An unexpected error occurred" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Maker rebate program is not currently available */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "SERVICE_UNAVAILABLE", + * "message": "Maker rebate program is not currently available" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + getLiquidityRewardsConfig: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LiquidityRewardsConfig"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "INTERNAL_ERROR", + * "message": "An unexpected error occurred" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Liquidity rewards program is not currently available */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "SERVICE_UNAVAILABLE", + * "message": "Liquidity rewards program is not currently available" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + listLiquidityRewardsEvents: { + parameters: { + query?: { + /** @description Comma-separated list of category names. Whitespace is trimmed and empty entries are dropped. */ + category?: string; + /** @description Filter events by title substring (case-insensitive). */ + search?: string; + /** @description Sort order for the returned events. Defaults to `daily_pool_desc`. */ + sort?: "daily_pool_desc" | "daily_pool_asc" | "ends_soonest" | "ends_latest" | "title_asc" | "title_desc" | "category_asc" | "category_desc" | "competition_asc" | "competition_desc"; + /** @description Maximum number of events to return (default 50, clamped to [1, 100]). */ + limit?: number; + /** @description Number of events to skip (default 0). */ + offset?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LiquidityRewardsEventsResponse"]; + }; + }; + /** @description Invalid sort or pagination parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "BAD_REQUEST", + * "message": "sort must be one of: daily_pool_desc, daily_pool_asc, ends_soonest, ends_latest, title_asc, title_desc, category_asc, category_desc, competition_asc, competition_desc (got 'foo')" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "INTERNAL_ERROR", + * "message": "An unexpected error occurred" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Liquidity rewards program is not currently available */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "SERVICE_UNAVAILABLE", + * "message": "Liquidity rewards program is not currently available" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + getLiquidityRewardsDailySummary: { + parameters: { + query: { + /** @description Inclusive start of the date window (`YYYY-MM-DD`, Eastern Time). */ + dateFrom: string; + /** @description Inclusive end of the date window (`YYYY-MM-DD`, Eastern Time). Must be on or after `dateFrom`. */ + dateTo: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LiquidityRewardsDailySummaryResponse"]; + }; + }; + /** @description Invalid date parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "date_to must be on or after date_from" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + 401: components["responses"]["Unauthorized"]; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "An unexpected error occurred" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Liquidity rewards program is not currently available */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "Liquidity incentive program is not currently available" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + getLiquidityRewardsLifetimeSummary: { + parameters: { + query?: { + /** @description Inclusive start of the payout date window (`YYYY-MM-DD`, Eastern Time). Must be provided together with `dateTo`. */ + dateFrom?: string; + /** @description Inclusive end of the payout date window (`YYYY-MM-DD`, Eastern Time). Must be on or after `dateFrom` and within 5 years of it. */ + dateTo?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LiquidityRewardsLifetimeSummary"]; + }; + }; + /** @description Invalid date parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "BAD_REQUEST", + * "message": "date_to must be on or after date_from" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + 401: components["responses"]["Unauthorized"]; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "INTERNAL_ERROR", + * "message": "An unexpected error occurred" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Liquidity rewards program is not currently available */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": "SERVICE_UNAVAILABLE", + * "message": "Liquidity rewards program is not currently available" + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; +} diff --git a/packages/sdk-typescript/src/generated/operations.ts b/packages/sdk-typescript/src/generated/operations.ts new file mode 100644 index 0000000..e088808 --- /dev/null +++ b/packages/sdk-typescript/src/generated/operations.ts @@ -0,0 +1,286 @@ +// Generated from prediction-markets.yaml. Do not edit. + +import type { operations as OpenApiOperations } from "./models.js"; + +type ParameterAt = + O extends { parameters: infer P } + ? Location extends keyof P ? P[Location] : never + : never; + +type Int64Input = + T extends bigint ? bigint | number : + T extends readonly (infer Item)[] ? Int64Input[] : + T extends object ? { [K in keyof T]: Int64Input } : T; + +type JsonBody = + NonNullable extends + { content: { "application/json": infer Body } } + ? Required extends true ? Body : Body | undefined + : never; + +type StripTransportFields = T extends object ? Omit : T; + +type CallerJsonBody = StripTransportFields; + +type JsonResponse = + O extends { responses: infer R } + ? Status extends keyof R + ? R[Status] extends { content: { "application/json": infer Body } } ? Body : never + : never + : never; + +export const PREDICTION_MARKET_OPERATIONS = { + "acceptPredictionMarketsTerms": {"responseMode":"json","operation":"predictionMarkets.acceptPredictionMarketsTerms","method":"post","path":"/v1/prediction-markets/terms/accept","access":"authenticated","parameters":[],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":false}, + "cancelOrder": {"responseMode":"json","operation":"predictionMarkets.cancelOrder","method":"post","path":"/v1/prediction-markets/order/cancel","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["orderId"]}],"path":[],"query":[]},"retryable":false}, + "cancelOrderBatch": {"responseMode":"json","operation":"predictionMarkets.cancelOrderBatch","method":"post","path":"/v1/prediction-markets/order/batch/cancel","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["results","*","orderId"]],"requestInt64Paths":{"body":[{"path":["orderIds","*"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "createCombo": {"responseMode":"json","operation":"predictionMarkets.createCombo","method":"post","path":"/v1/prediction-markets/combos","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200,201],"responseContentTypes":["application/json"],"responseInt64Paths":[["combo","id"],["combo","instrumentId"],["combo","legs","*","comboId"]],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":false}, + "getActiveOrders": {"responseMode":"json","operation":"predictionMarkets.getActiveOrders","method":"post","path":"/v1/prediction-markets/orders/active","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["orders","*","orderId"]],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":false}, + "getCategories": {"responseMode":"json","operation":"predictionMarkets.getCategories","method":"get","path":"/v1/prediction-markets/categories","access":"public","parameters":[{"name":"status","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getComboByInstrumentSymbol": {"responseMode":"json","operation":"predictionMarkets.getComboByInstrumentSymbol","method":"get","path":"/v1/prediction-markets/combos/{instrumentSymbol}","access":"public","parameters":[{"name":"instrumentSymbol","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["legs","*","comboId"]],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getEvent": {"responseMode":"json","operation":"predictionMarkets.getEvent","method":"get","path":"/v1/prediction-markets/events/{eventTicker}","access":"public","parameters":[{"name":"eventTicker","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getEventStrike": {"responseMode":"json","operation":"predictionMarkets.getEventStrike","method":"get","path":"/v1/prediction-markets/events/{eventTicker}/strike","access":"public","parameters":[{"name":"eventTicker","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getLiquidityRewardsConfig": {"responseMode":"json","operation":"predictionMarkets.getLiquidityRewardsConfig","method":"get","path":"/v1/prediction-markets/liquidity-rewards/config","access":"public","parameters":[],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getLiquidityRewardsDailySummary": {"responseMode":"json","operation":"predictionMarkets.getLiquidityRewardsDailySummary","method":"get","path":"/v1/prediction-markets/liquidity-rewards/summary/daily","access":"authenticated","parameters":[{"name":"dateFrom","in":"query","required":true,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"dateTo","in":"query","required":true,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["daily_summaries","*","events","*","event_id"]],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getLiquidityRewardsLifetimeSummary": {"responseMode":"json","operation":"predictionMarkets.getLiquidityRewardsLifetimeSummary","method":"get","path":"/v1/prediction-markets/liquidity-rewards/summary/total","access":"authenticated","parameters":[{"name":"dateFrom","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"dateTo","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getMakerRebateLifetimeSummary": {"responseMode":"json","operation":"predictionMarkets.getMakerRebateLifetimeSummary","method":"get","path":"/v1/prediction-markets/maker-rebate/summary/total","access":"authenticated","parameters":[{"name":"dateFrom","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"dateTo","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["total_fill_count"]],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getMakerRebateRates": {"responseMode":"json","operation":"predictionMarkets.getMakerRebateRates","method":"get","path":"/v1/prediction-markets/maker-rebate/rates","access":"public","parameters":[{"name":"category","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["rate_rules","*","id"]],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getOrderHistory": {"responseMode":"json","operation":"predictionMarkets.getOrderHistory","method":"post","path":"/v1/prediction-markets/orders/history","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["orders","*","orderId"]],"requestInt64Paths":{"body":[{"path":["from"]},{"path":["to"]}],"path":[],"query":[]},"retryable":false}, + "getPositions": {"responseMode":"json","operation":"predictionMarkets.getPositions","method":"post","path":"/v1/prediction-markets/positions","access":"authenticated","parameters":[{"name":"eventTicker","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"limit","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"offset","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"sort","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["positions","*","instrumentId"]],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":false}, + "getPredictionMarketDailyVolume": {"responseMode":"json","operation":"predictionMarkets.getPredictionMarketDailyVolume","method":"get","path":"/v1/prediction-markets/volume/{date}","access":"public","parameters":[{"name":"date","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getPredictionMarketHourlyVolume": {"responseMode":"json","operation":"predictionMarkets.getPredictionMarketHourlyVolume","method":"get","path":"/v1/prediction-markets/volume/{date}/hourly","access":"public","parameters":[{"name":"date","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getPredictionMarketsTerms": {"responseMode":"json","operation":"predictionMarkets.getPredictionMarketsTerms","method":"get","path":"/v1/prediction-markets/terms","access":"public","parameters":[],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getPredictionMarketsTermsStatus": {"responseMode":"json","operation":"predictionMarkets.getPredictionMarketsTermsStatus","method":"get","path":"/v1/prediction-markets/terms/status","access":"authenticated","parameters":[],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "getSettledPositions": {"responseMode":"json","operation":"predictionMarkets.getSettledPositions","method":"post","path":"/v1/prediction-markets/positions/settled","access":"authenticated","parameters":[{"name":"eventTicker","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"limit","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"offset","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"sort","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"search","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"category","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"withCashOuts","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["cashOuts","*","accountId"],["cashOuts","*","instrumentId"],["positions","*","accountId"],["positions","*","instrumentId"]],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":false}, + "getVolumeMetrics": {"responseMode":"json","operation":"predictionMarkets.getVolumeMetrics","method":"post","path":"/v1/prediction-markets/metrics/volume","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["endTime"]},{"path":["startTime"]}],"path":[],"query":[]},"retryable":false}, + "listCombos": {"responseMode":"json","operation":"predictionMarkets.listCombos","method":"get","path":"/v1/prediction-markets/combos","access":"public","parameters":[{"name":"status","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"contractId","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"instrumentRegistered","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"limit","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"offset","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["combos","*","legs","*","comboId"]],"requestInt64Paths":{"body":[],"path":[],"query":[{"path":["contractId"]}]},"retryable":true}, + "listEvents": {"responseMode":"json","operation":"predictionMarkets.listEvents","method":"get","path":"/v1/prediction-markets/events","access":"public","parameters":[{"name":"status","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"category","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sport","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sports_market_type","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sports_market_subject","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sports_market_scope","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sports_market_metric","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"search","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"limit","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"offset","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "listLiquidityRewardsEvents": {"responseMode":"json","operation":"predictionMarkets.listLiquidityRewardsEvents","method":"get","path":"/v1/prediction-markets/liquidity-rewards/events","access":"public","parameters":[{"name":"category","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"search","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"sort","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"limit","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"offset","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "listMakerRebatePayouts": {"responseMode":"json","operation":"predictionMarkets.listMakerRebatePayouts","method":"post","path":"/v1/prediction-markets/maker-rebate/payouts","access":"authenticated","parameters":[{"name":"limit","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"offset","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["payouts","*","id"]],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":false}, + "listNewlyListedEvents": {"responseMode":"json","operation":"predictionMarkets.listNewlyListedEvents","method":"get","path":"/v1/prediction-markets/events/newly-listed","access":"public","parameters":[{"name":"category","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sport","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sports_market_type","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sports_market_subject","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sports_market_scope","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sports_market_metric","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"limit","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"offset","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "listRecentlySettledEvents": {"responseMode":"json","operation":"predictionMarkets.listRecentlySettledEvents","method":"get","path":"/v1/prediction-markets/events/recently-settled","access":"public","parameters":[{"name":"category","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sport","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sports_market_type","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sports_market_subject","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sports_market_scope","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sports_market_metric","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"limit","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"offset","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "listUpcomingEvents": {"responseMode":"json","operation":"predictionMarkets.listUpcomingEvents","method":"get","path":"/v1/prediction-markets/events/upcoming","access":"public","parameters":[{"name":"category","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sport","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sports_market_type","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sports_market_subject","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sports_market_scope","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"sports_market_metric","in":"query","required":false,"style":"form","explode":true,"shape":"array","allowReserved":false},{"name":"limit","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"offset","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "placeOrder": {"responseMode":"json","operation":"predictionMarkets.placeOrder","method":"post","path":"/v1/prediction-markets/order","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[201],"responseContentTypes":["application/json"],"responseInt64Paths":[["orderId"]],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":false}, + "placeOrderBatch": {"responseMode":"json","operation":"predictionMarkets.placeOrderBatch","method":"post","path":"/v1/prediction-markets/order/batch","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["results","*","order","orderId"]],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":false}, +} as const; + +export type PredictionMarketOperationId = keyof typeof PREDICTION_MARKET_OPERATIONS; + +export type PredictionMarketOperationTypes = { + "acceptPredictionMarketsTerms": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "cancelOrder": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "cancelOrderBatch": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "createCombo": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getActiveOrders": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getCategories": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getComboByInstrumentSymbol": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getEvent": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getEventStrike": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getLiquidityRewardsConfig": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getLiquidityRewardsDailySummary": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getLiquidityRewardsLifetimeSummary": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getMakerRebateLifetimeSummary": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getMakerRebateRates": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getOrderHistory": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getPositions": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getPredictionMarketDailyVolume": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getPredictionMarketHourlyVolume": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getPredictionMarketsTerms": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getPredictionMarketsTermsStatus": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getSettledPositions": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "getVolumeMetrics": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "listCombos": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "listEvents": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "listLiquidityRewardsEvents": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "listMakerRebatePayouts": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "listNewlyListedEvents": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "listRecentlySettledEvents": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "listUpcomingEvents": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "placeOrder": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "placeOrderBatch": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; +}; diff --git a/packages/sdk-typescript/src/generated/perpetuals/operations.ts b/packages/sdk-typescript/src/generated/perpetuals/operations.ts new file mode 100644 index 0000000..892107b --- /dev/null +++ b/packages/sdk-typescript/src/generated/perpetuals/operations.ts @@ -0,0 +1,87 @@ +// Generated from rest.yaml#Perpetuals. Do not edit. + +import type { RestFileResponse } from "../../core/http.js"; +import type { operations as OpenApiOperations } from "../market-data/models.js"; + +type ParameterAt = + O extends { parameters: infer P } + ? Location extends keyof P ? P[Location] : never + : never; + +type Int64Input = + T extends bigint ? bigint | number : + T extends readonly (infer Item)[] ? Int64Input[] : + T extends object ? { [K in keyof T]: Int64Input } : T; + +type JsonBody = + NonNullable extends + { content: { "application/json": infer Body } } + ? Required extends true ? Body : Body | undefined + : never; + +type StripTransportFields = T extends object ? Omit : T; + +type CallerJsonBody = StripTransportFields; + +type JsonResponse = + O extends { responses: infer R } + ? Status extends keyof R + ? R[Status] extends { content: { "application/json": infer Body } } ? Body : never + : never + : never; + +export const PERPETUALS_OPERATIONS = { + "getAccountMargin": {"responseMode":"json","operation":"perpetuals.getAccountMargin","method":"post","path":"/v1/margin","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "getFundingPaymentReportFile": {"responseMode":"file","operation":"perpetuals.getFundingPaymentReportFile","method":"get","path":"/v1/perpetuals/fundingpaymentreport/records.xlsx","access":"authenticated","parameters":[{"name":"fromDate","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"toDate","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"numRows","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":true,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":true}, + "getFundingPaymentReportJson": {"responseMode":"json","operation":"perpetuals.getFundingPaymentReportJson","method":"post","path":"/v1/perpetuals/fundingpaymentreport/records.json","access":"authenticated","parameters":[{"name":"fromDate","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"toDate","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"numRows","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "getOpenPositions": {"responseMode":"json","operation":"perpetuals.getOpenPositions","method":"post","path":"/v1/positions","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "getRiskStats": {"responseMode":"json","operation":"perpetuals.getRiskStats","method":"get","path":"/v1/riskstats/{symbol}","access":"public","parameters":[{"name":"symbol","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":false,"requestBodyRequired":false,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":true}, + "listFundingPayments": {"responseMode":"json","operation":"perpetuals.listFundingPayments","method":"post","path":"/v1/perpetuals/fundingPayment","access":"authenticated","parameters":[{"name":"since","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false},{"name":"to","in":"query","required":false,"style":"form","explode":true,"shape":"scalar","allowReserved":false}],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[{"path":["since"],"allowString":true},{"path":["to"],"allowString":true}]},"retryable":false}, +} as const; + +export type PerpetualsOperationId = keyof typeof PERPETUALS_OPERATIONS; + +export type PerpetualsOperationTypes = { + "getAccountMargin": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getFundingPaymentReportFile": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: RestFileResponse; + }; + "getFundingPaymentReportJson": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getOpenPositions": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getRiskStats": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: never; + response: JsonResponse; + }; + "listFundingPayments": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; +}; diff --git a/packages/sdk-typescript/src/generated/perpetuals/rest.ts b/packages/sdk-typescript/src/generated/perpetuals/rest.ts new file mode 100644 index 0000000..393d5e9 --- /dev/null +++ b/packages/sdk-typescript/src/generated/perpetuals/rest.ts @@ -0,0 +1,98 @@ +// Generated from rest.yaml#Perpetuals. Do not edit. + +import type { HttpTransport } from "../../core/http.js"; +import type { RequestOptions } from "../../core/deadline.js"; +import { executeRestOperation } from "../../core/rest-operation.js"; + +import { + PERPETUALS_OPERATIONS, + type PerpetualsOperationTypes, +} from "./operations.js"; + +export class PerpetualsRest { + constructor(private readonly transport: HttpTransport) {} + + getAccountMargin(body: PerpetualsOperationTypes["getAccountMargin"]["body"], requestOptions?: RequestOptions): Promise; + getAccountMargin(body: PerpetualsOperationTypes["getAccountMargin"]["body"]): Promise; + getAccountMargin(body: PerpetualsOperationTypes["getAccountMargin"]["body"], requestOptions?: RequestOptions): Promise { + const operation = PERPETUALS_OPERATIONS["getAccountMargin"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + getFundingPaymentReportFile(input?: { + query?: PerpetualsOperationTypes["getFundingPaymentReportFile"]["query"]; + body?: PerpetualsOperationTypes["getFundingPaymentReportFile"]["body"]; + }, requestOptions?: RequestOptions): Promise; + getFundingPaymentReportFile(input?: { + query?: PerpetualsOperationTypes["getFundingPaymentReportFile"]["query"]; + body?: PerpetualsOperationTypes["getFundingPaymentReportFile"]["body"]; + }): Promise; + getFundingPaymentReportFile(input?: { + query?: PerpetualsOperationTypes["getFundingPaymentReportFile"]["query"]; + body?: PerpetualsOperationTypes["getFundingPaymentReportFile"]["body"]; + }, requestOptions?: RequestOptions): Promise { + const operation = PERPETUALS_OPERATIONS["getFundingPaymentReportFile"]; + return executeRestOperation(this.transport, operation, { + query: input?.query, + body: input?.body, + }, requestOptions); + } + + getFundingPaymentReportJson(input: { + query?: PerpetualsOperationTypes["getFundingPaymentReportJson"]["query"]; + body: PerpetualsOperationTypes["getFundingPaymentReportJson"]["body"]; + }, requestOptions?: RequestOptions): Promise; + getFundingPaymentReportJson(input: { + query?: PerpetualsOperationTypes["getFundingPaymentReportJson"]["query"]; + body: PerpetualsOperationTypes["getFundingPaymentReportJson"]["body"]; + }): Promise; + getFundingPaymentReportJson(input: { + query?: PerpetualsOperationTypes["getFundingPaymentReportJson"]["query"]; + body: PerpetualsOperationTypes["getFundingPaymentReportJson"]["body"]; + }, requestOptions?: RequestOptions): Promise { + const operation = PERPETUALS_OPERATIONS["getFundingPaymentReportJson"]; + return executeRestOperation(this.transport, operation, { + query: input.query, + body: input.body, + }, requestOptions); + } + + getOpenPositions(body: PerpetualsOperationTypes["getOpenPositions"]["body"], requestOptions?: RequestOptions): Promise; + getOpenPositions(body: PerpetualsOperationTypes["getOpenPositions"]["body"]): Promise; + getOpenPositions(body: PerpetualsOperationTypes["getOpenPositions"]["body"], requestOptions?: RequestOptions): Promise { + const operation = PERPETUALS_OPERATIONS["getOpenPositions"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + getRiskStats(path: PerpetualsOperationTypes["getRiskStats"]["path"], requestOptions?: RequestOptions): Promise; + getRiskStats(path: PerpetualsOperationTypes["getRiskStats"]["path"]): Promise; + getRiskStats(path: PerpetualsOperationTypes["getRiskStats"]["path"], requestOptions?: RequestOptions): Promise { + const operation = PERPETUALS_OPERATIONS["getRiskStats"]; + return executeRestOperation(this.transport, operation, { + path, + }, requestOptions); + } + + listFundingPayments(input: { + query?: PerpetualsOperationTypes["listFundingPayments"]["query"]; + body: PerpetualsOperationTypes["listFundingPayments"]["body"]; + }, requestOptions?: RequestOptions): Promise; + listFundingPayments(input: { + query?: PerpetualsOperationTypes["listFundingPayments"]["query"]; + body: PerpetualsOperationTypes["listFundingPayments"]["body"]; + }): Promise; + listFundingPayments(input: { + query?: PerpetualsOperationTypes["listFundingPayments"]["query"]; + body: PerpetualsOperationTypes["listFundingPayments"]["body"]; + }, requestOptions?: RequestOptions): Promise { + const operation = PERPETUALS_OPERATIONS["listFundingPayments"]; + return executeRestOperation(this.transport, operation, { + query: input.query, + body: input.body, + }, requestOptions); + } +} diff --git a/packages/sdk-typescript/src/generated/rest.ts b/packages/sdk-typescript/src/generated/rest.ts new file mode 100644 index 0000000..71f02e2 --- /dev/null +++ b/packages/sdk-typescript/src/generated/rest.ts @@ -0,0 +1,285 @@ +// Generated from prediction-markets.yaml. Do not edit. + +import type { HttpTransport } from "../core/http.js"; +import type { RequestOptions } from "../core/deadline.js"; +import { executeRestOperation } from "../core/rest-operation.js"; + +import { + PREDICTION_MARKET_OPERATIONS, + type PredictionMarketOperationTypes, +} from "./operations.js"; + +export class PredictionMarketsRest { + constructor(private readonly transport: HttpTransport) {} + + acceptPredictionMarketsTerms(requestOptions?: RequestOptions): Promise; + acceptPredictionMarketsTerms(): Promise; + acceptPredictionMarketsTerms(requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["acceptPredictionMarketsTerms"]; + return executeRestOperation(this.transport, operation, {}, requestOptions); + } + + cancelOrder(body: PredictionMarketOperationTypes["cancelOrder"]["body"], requestOptions?: RequestOptions): Promise; + cancelOrder(body: PredictionMarketOperationTypes["cancelOrder"]["body"]): Promise; + cancelOrder(body: PredictionMarketOperationTypes["cancelOrder"]["body"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["cancelOrder"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + cancelOrderBatch(body: PredictionMarketOperationTypes["cancelOrderBatch"]["body"], requestOptions?: RequestOptions): Promise; + cancelOrderBatch(body: PredictionMarketOperationTypes["cancelOrderBatch"]["body"]): Promise; + cancelOrderBatch(body: PredictionMarketOperationTypes["cancelOrderBatch"]["body"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["cancelOrderBatch"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + createCombo(body: PredictionMarketOperationTypes["createCombo"]["body"], requestOptions?: RequestOptions): Promise; + createCombo(body: PredictionMarketOperationTypes["createCombo"]["body"]): Promise; + createCombo(body: PredictionMarketOperationTypes["createCombo"]["body"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["createCombo"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + getActiveOrders(body?: PredictionMarketOperationTypes["getActiveOrders"]["body"], requestOptions?: RequestOptions): Promise; + getActiveOrders(body?: PredictionMarketOperationTypes["getActiveOrders"]["body"]): Promise; + getActiveOrders(body?: PredictionMarketOperationTypes["getActiveOrders"]["body"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getActiveOrders"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + getCategories(query?: PredictionMarketOperationTypes["getCategories"]["query"], requestOptions?: RequestOptions): Promise; + getCategories(query?: PredictionMarketOperationTypes["getCategories"]["query"]): Promise; + getCategories(query?: PredictionMarketOperationTypes["getCategories"]["query"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getCategories"]; + return executeRestOperation(this.transport, operation, { + query, + }, requestOptions); + } + + getComboByInstrumentSymbol(path: PredictionMarketOperationTypes["getComboByInstrumentSymbol"]["path"], requestOptions?: RequestOptions): Promise; + getComboByInstrumentSymbol(path: PredictionMarketOperationTypes["getComboByInstrumentSymbol"]["path"]): Promise; + getComboByInstrumentSymbol(path: PredictionMarketOperationTypes["getComboByInstrumentSymbol"]["path"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getComboByInstrumentSymbol"]; + return executeRestOperation(this.transport, operation, { + path, + }, requestOptions); + } + + getEvent(path: PredictionMarketOperationTypes["getEvent"]["path"], requestOptions?: RequestOptions): Promise; + getEvent(path: PredictionMarketOperationTypes["getEvent"]["path"]): Promise; + getEvent(path: PredictionMarketOperationTypes["getEvent"]["path"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getEvent"]; + return executeRestOperation(this.transport, operation, { + path, + }, requestOptions); + } + + getEventStrike(path: PredictionMarketOperationTypes["getEventStrike"]["path"], requestOptions?: RequestOptions): Promise; + getEventStrike(path: PredictionMarketOperationTypes["getEventStrike"]["path"]): Promise; + getEventStrike(path: PredictionMarketOperationTypes["getEventStrike"]["path"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getEventStrike"]; + return executeRestOperation(this.transport, operation, { + path, + }, requestOptions); + } + + getLiquidityRewardsConfig(requestOptions?: RequestOptions): Promise; + getLiquidityRewardsConfig(): Promise; + getLiquidityRewardsConfig(requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getLiquidityRewardsConfig"]; + return executeRestOperation(this.transport, operation, {}, requestOptions); + } + + getLiquidityRewardsDailySummary(query: PredictionMarketOperationTypes["getLiquidityRewardsDailySummary"]["query"], requestOptions?: RequestOptions): Promise; + getLiquidityRewardsDailySummary(query: PredictionMarketOperationTypes["getLiquidityRewardsDailySummary"]["query"]): Promise; + getLiquidityRewardsDailySummary(query: PredictionMarketOperationTypes["getLiquidityRewardsDailySummary"]["query"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getLiquidityRewardsDailySummary"]; + return executeRestOperation(this.transport, operation, { + query, + }, requestOptions); + } + + getLiquidityRewardsLifetimeSummary(query?: PredictionMarketOperationTypes["getLiquidityRewardsLifetimeSummary"]["query"], requestOptions?: RequestOptions): Promise; + getLiquidityRewardsLifetimeSummary(query?: PredictionMarketOperationTypes["getLiquidityRewardsLifetimeSummary"]["query"]): Promise; + getLiquidityRewardsLifetimeSummary(query?: PredictionMarketOperationTypes["getLiquidityRewardsLifetimeSummary"]["query"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getLiquidityRewardsLifetimeSummary"]; + return executeRestOperation(this.transport, operation, { + query, + }, requestOptions); + } + + getMakerRebateLifetimeSummary(query?: PredictionMarketOperationTypes["getMakerRebateLifetimeSummary"]["query"], requestOptions?: RequestOptions): Promise; + getMakerRebateLifetimeSummary(query?: PredictionMarketOperationTypes["getMakerRebateLifetimeSummary"]["query"]): Promise; + getMakerRebateLifetimeSummary(query?: PredictionMarketOperationTypes["getMakerRebateLifetimeSummary"]["query"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getMakerRebateLifetimeSummary"]; + return executeRestOperation(this.transport, operation, { + query, + }, requestOptions); + } + + getMakerRebateRates(query?: PredictionMarketOperationTypes["getMakerRebateRates"]["query"], requestOptions?: RequestOptions): Promise; + getMakerRebateRates(query?: PredictionMarketOperationTypes["getMakerRebateRates"]["query"]): Promise; + getMakerRebateRates(query?: PredictionMarketOperationTypes["getMakerRebateRates"]["query"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getMakerRebateRates"]; + return executeRestOperation(this.transport, operation, { + query, + }, requestOptions); + } + + getOrderHistory(body?: PredictionMarketOperationTypes["getOrderHistory"]["body"], requestOptions?: RequestOptions): Promise; + getOrderHistory(body?: PredictionMarketOperationTypes["getOrderHistory"]["body"]): Promise; + getOrderHistory(body?: PredictionMarketOperationTypes["getOrderHistory"]["body"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getOrderHistory"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + getPositions(query?: PredictionMarketOperationTypes["getPositions"]["query"], requestOptions?: RequestOptions): Promise; + getPositions(query?: PredictionMarketOperationTypes["getPositions"]["query"]): Promise; + getPositions(query?: PredictionMarketOperationTypes["getPositions"]["query"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getPositions"]; + return executeRestOperation(this.transport, operation, { + query, + }, requestOptions); + } + + getPredictionMarketDailyVolume(path: PredictionMarketOperationTypes["getPredictionMarketDailyVolume"]["path"], requestOptions?: RequestOptions): Promise; + getPredictionMarketDailyVolume(path: PredictionMarketOperationTypes["getPredictionMarketDailyVolume"]["path"]): Promise; + getPredictionMarketDailyVolume(path: PredictionMarketOperationTypes["getPredictionMarketDailyVolume"]["path"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getPredictionMarketDailyVolume"]; + return executeRestOperation(this.transport, operation, { + path, + }, requestOptions); + } + + getPredictionMarketHourlyVolume(path: PredictionMarketOperationTypes["getPredictionMarketHourlyVolume"]["path"], requestOptions?: RequestOptions): Promise; + getPredictionMarketHourlyVolume(path: PredictionMarketOperationTypes["getPredictionMarketHourlyVolume"]["path"]): Promise; + getPredictionMarketHourlyVolume(path: PredictionMarketOperationTypes["getPredictionMarketHourlyVolume"]["path"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getPredictionMarketHourlyVolume"]; + return executeRestOperation(this.transport, operation, { + path, + }, requestOptions); + } + + getPredictionMarketsTerms(requestOptions?: RequestOptions): Promise; + getPredictionMarketsTerms(): Promise; + getPredictionMarketsTerms(requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getPredictionMarketsTerms"]; + return executeRestOperation(this.transport, operation, {}, requestOptions); + } + + getPredictionMarketsTermsStatus(requestOptions?: RequestOptions): Promise; + getPredictionMarketsTermsStatus(): Promise; + getPredictionMarketsTermsStatus(requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getPredictionMarketsTermsStatus"]; + return executeRestOperation(this.transport, operation, {}, requestOptions); + } + + getSettledPositions(query?: PredictionMarketOperationTypes["getSettledPositions"]["query"], requestOptions?: RequestOptions): Promise; + getSettledPositions(query?: PredictionMarketOperationTypes["getSettledPositions"]["query"]): Promise; + getSettledPositions(query?: PredictionMarketOperationTypes["getSettledPositions"]["query"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getSettledPositions"]; + return executeRestOperation(this.transport, operation, { + query, + }, requestOptions); + } + + getVolumeMetrics(body: PredictionMarketOperationTypes["getVolumeMetrics"]["body"], requestOptions?: RequestOptions): Promise; + getVolumeMetrics(body: PredictionMarketOperationTypes["getVolumeMetrics"]["body"]): Promise; + getVolumeMetrics(body: PredictionMarketOperationTypes["getVolumeMetrics"]["body"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["getVolumeMetrics"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + listCombos(query?: PredictionMarketOperationTypes["listCombos"]["query"], requestOptions?: RequestOptions): Promise; + listCombos(query?: PredictionMarketOperationTypes["listCombos"]["query"]): Promise; + listCombos(query?: PredictionMarketOperationTypes["listCombos"]["query"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["listCombos"]; + return executeRestOperation(this.transport, operation, { + query, + }, requestOptions); + } + + listEvents(query?: PredictionMarketOperationTypes["listEvents"]["query"], requestOptions?: RequestOptions): Promise; + listEvents(query?: PredictionMarketOperationTypes["listEvents"]["query"]): Promise; + listEvents(query?: PredictionMarketOperationTypes["listEvents"]["query"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["listEvents"]; + return executeRestOperation(this.transport, operation, { + query, + }, requestOptions); + } + + listLiquidityRewardsEvents(query?: PredictionMarketOperationTypes["listLiquidityRewardsEvents"]["query"], requestOptions?: RequestOptions): Promise; + listLiquidityRewardsEvents(query?: PredictionMarketOperationTypes["listLiquidityRewardsEvents"]["query"]): Promise; + listLiquidityRewardsEvents(query?: PredictionMarketOperationTypes["listLiquidityRewardsEvents"]["query"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["listLiquidityRewardsEvents"]; + return executeRestOperation(this.transport, operation, { + query, + }, requestOptions); + } + + listMakerRebatePayouts(query?: PredictionMarketOperationTypes["listMakerRebatePayouts"]["query"], requestOptions?: RequestOptions): Promise; + listMakerRebatePayouts(query?: PredictionMarketOperationTypes["listMakerRebatePayouts"]["query"]): Promise; + listMakerRebatePayouts(query?: PredictionMarketOperationTypes["listMakerRebatePayouts"]["query"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["listMakerRebatePayouts"]; + return executeRestOperation(this.transport, operation, { + query, + }, requestOptions); + } + + listNewlyListedEvents(query?: PredictionMarketOperationTypes["listNewlyListedEvents"]["query"], requestOptions?: RequestOptions): Promise; + listNewlyListedEvents(query?: PredictionMarketOperationTypes["listNewlyListedEvents"]["query"]): Promise; + listNewlyListedEvents(query?: PredictionMarketOperationTypes["listNewlyListedEvents"]["query"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["listNewlyListedEvents"]; + return executeRestOperation(this.transport, operation, { + query, + }, requestOptions); + } + + listRecentlySettledEvents(query?: PredictionMarketOperationTypes["listRecentlySettledEvents"]["query"], requestOptions?: RequestOptions): Promise; + listRecentlySettledEvents(query?: PredictionMarketOperationTypes["listRecentlySettledEvents"]["query"]): Promise; + listRecentlySettledEvents(query?: PredictionMarketOperationTypes["listRecentlySettledEvents"]["query"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["listRecentlySettledEvents"]; + return executeRestOperation(this.transport, operation, { + query, + }, requestOptions); + } + + listUpcomingEvents(query?: PredictionMarketOperationTypes["listUpcomingEvents"]["query"], requestOptions?: RequestOptions): Promise; + listUpcomingEvents(query?: PredictionMarketOperationTypes["listUpcomingEvents"]["query"]): Promise; + listUpcomingEvents(query?: PredictionMarketOperationTypes["listUpcomingEvents"]["query"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["listUpcomingEvents"]; + return executeRestOperation(this.transport, operation, { + query, + }, requestOptions); + } + + placeOrder(body: PredictionMarketOperationTypes["placeOrder"]["body"], requestOptions?: RequestOptions): Promise; + placeOrder(body: PredictionMarketOperationTypes["placeOrder"]["body"]): Promise; + placeOrder(body: PredictionMarketOperationTypes["placeOrder"]["body"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["placeOrder"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + placeOrderBatch(body: PredictionMarketOperationTypes["placeOrderBatch"]["body"], requestOptions?: RequestOptions): Promise; + placeOrderBatch(body: PredictionMarketOperationTypes["placeOrderBatch"]["body"]): Promise; + placeOrderBatch(body: PredictionMarketOperationTypes["placeOrderBatch"]["body"], requestOptions?: RequestOptions): Promise { + const operation = PREDICTION_MARKET_OPERATIONS["placeOrderBatch"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } +} diff --git a/packages/sdk-typescript/src/generated/trading/operations.ts b/packages/sdk-typescript/src/generated/trading/operations.ts new file mode 100644 index 0000000..13ba3d6 --- /dev/null +++ b/packages/sdk-typescript/src/generated/trading/operations.ts @@ -0,0 +1,134 @@ +// Generated from rest.yaml#Trading. Do not edit. + +import type { operations as OpenApiOperations } from "../market-data/models.js"; + +type ParameterAt = + O extends { parameters: infer P } + ? Location extends keyof P ? P[Location] : never + : never; + +type Int64Input = + T extends bigint ? bigint | number : + T extends readonly (infer Item)[] ? Int64Input[] : + T extends object ? { [K in keyof T]: Int64Input } : T; + +type JsonBody = + NonNullable extends + { content: { "application/json": infer Body } } + ? Required extends true ? Body : Body | undefined + : never; + +type StripTransportFields = T extends object ? Omit : T; + +type CallerJsonBody = StripTransportFields; + +type JsonResponse = + O extends { responses: infer R } + ? Status extends keyof R + ? R[Status] extends { content: { "application/json": infer Body } } ? Body : never + : never + : never; + +export const TRADING_OPERATIONS = { + "cancelAllActiveOrders": {"responseMode":"json","operation":"trading.cancelAllActiveOrders","method":"post","path":"/v1/order/cancel/all","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "cancelAllSessionOrders": {"responseMode":"json","operation":"trading.cancelAllSessionOrders","method":"post","path":"/v1/order/cancel/session","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "cancelOrder": {"responseMode":"json","operation":"trading.cancelOrder","method":"post","path":"/v1/order/cancel","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true},{"path":["order_id"],"unsigned":true}],"path":[],"query":[]},"retryable":false}, + "createNewOrder": {"responseMode":"json","operation":"trading.createNewOrder","method":"post","path":"/v1/order/new","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[],"path":[],"query":[]},"retryable":false}, + "getNotionalTradingVolume": {"responseMode":"json","operation":"trading.getNotionalTradingVolume","method":"post","path":"/v1/notionalvolume","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "getOrderStatus": {"responseMode":"json","operation":"trading.getOrderStatus","method":"post","path":"/v1/order/status","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true},{"path":["order_id"],"unsigned":true}],"path":[],"query":[]},"retryable":false}, + "getTradingVolume": {"responseMode":"json","operation":"trading.getTradingVolume","method":"post","path":"/v1/tradevolume","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "listActiveOrders": {"responseMode":"json","operation":"trading.listActiveOrders","method":"post","path":"/v1/orders","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "listPastOrders": {"responseMode":"json","operation":"trading.listPastOrders","method":"post","path":"/v1/orders/history","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true},{"path":["timestamp"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "listPastTrades": {"responseMode":"json","operation":"trading.listPastTrades","method":"post","path":"/v1/mytrades","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[["*","tid"]],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true},{"path":["timestamp"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "sendHeartbeat": {"responseMode":"json","operation":"trading.sendHeartbeat","method":"post","path":"/v1/heartbeat","access":"authenticated","parameters":[],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, + "wrapOrder": {"responseMode":"json","operation":"trading.wrapOrder","method":"post","path":"/v1/wrap/{symbol}","access":"authenticated","parameters":[{"name":"symbol","in":"path","required":true,"style":"simple","explode":false}],"headers":[],"requestBody":true,"requestBodyRequired":true,"successStatuses":[200],"responseContentTypes":["application/json"],"responseInt64Paths":[],"requestInt64Paths":{"body":[{"path":["nonce"],"allowString":true}],"path":[],"query":[]},"retryable":false}, +} as const; + +export type TradingOperationId = keyof typeof TRADING_OPERATIONS; + +export type TradingOperationTypes = { + "cancelAllActiveOrders": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "cancelAllSessionOrders": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "cancelOrder": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "createNewOrder": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getNotionalTradingVolume": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getOrderStatus": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "getTradingVolume": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "listActiveOrders": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "listPastOrders": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "listPastTrades": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "sendHeartbeat": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; + "wrapOrder": { + path: Int64Input>; + query: Int64Input>; + headers: never; + body: CallerJsonBody>>; + response: JsonResponse; + }; +}; diff --git a/packages/sdk-typescript/src/generated/trading/rest.ts b/packages/sdk-typescript/src/generated/trading/rest.ts new file mode 100644 index 0000000..996798b --- /dev/null +++ b/packages/sdk-typescript/src/generated/trading/rest.ts @@ -0,0 +1,132 @@ +// Generated from rest.yaml#Trading. Do not edit. + +import type { HttpTransport } from "../../core/http.js"; +import type { RequestOptions } from "../../core/deadline.js"; +import { executeRestOperation } from "../../core/rest-operation.js"; + +import { + TRADING_OPERATIONS, + type TradingOperationTypes, +} from "./operations.js"; + +export class TradingRest { + constructor(private readonly transport: HttpTransport) {} + + cancelAllActiveOrders(body: TradingOperationTypes["cancelAllActiveOrders"]["body"], requestOptions?: RequestOptions): Promise; + cancelAllActiveOrders(body: TradingOperationTypes["cancelAllActiveOrders"]["body"]): Promise; + cancelAllActiveOrders(body: TradingOperationTypes["cancelAllActiveOrders"]["body"], requestOptions?: RequestOptions): Promise { + const operation = TRADING_OPERATIONS["cancelAllActiveOrders"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + cancelAllSessionOrders(body: TradingOperationTypes["cancelAllSessionOrders"]["body"], requestOptions?: RequestOptions): Promise; + cancelAllSessionOrders(body: TradingOperationTypes["cancelAllSessionOrders"]["body"]): Promise; + cancelAllSessionOrders(body: TradingOperationTypes["cancelAllSessionOrders"]["body"], requestOptions?: RequestOptions): Promise { + const operation = TRADING_OPERATIONS["cancelAllSessionOrders"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + cancelOrder(body: TradingOperationTypes["cancelOrder"]["body"], requestOptions?: RequestOptions): Promise; + cancelOrder(body: TradingOperationTypes["cancelOrder"]["body"]): Promise; + cancelOrder(body: TradingOperationTypes["cancelOrder"]["body"], requestOptions?: RequestOptions): Promise { + const operation = TRADING_OPERATIONS["cancelOrder"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + createNewOrder(body: TradingOperationTypes["createNewOrder"]["body"], requestOptions?: RequestOptions): Promise; + createNewOrder(body: TradingOperationTypes["createNewOrder"]["body"]): Promise; + createNewOrder(body: TradingOperationTypes["createNewOrder"]["body"], requestOptions?: RequestOptions): Promise { + const operation = TRADING_OPERATIONS["createNewOrder"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + getNotionalTradingVolume(body: TradingOperationTypes["getNotionalTradingVolume"]["body"], requestOptions?: RequestOptions): Promise; + getNotionalTradingVolume(body: TradingOperationTypes["getNotionalTradingVolume"]["body"]): Promise; + getNotionalTradingVolume(body: TradingOperationTypes["getNotionalTradingVolume"]["body"], requestOptions?: RequestOptions): Promise { + const operation = TRADING_OPERATIONS["getNotionalTradingVolume"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + getOrderStatus(body: TradingOperationTypes["getOrderStatus"]["body"], requestOptions?: RequestOptions): Promise; + getOrderStatus(body: TradingOperationTypes["getOrderStatus"]["body"]): Promise; + getOrderStatus(body: TradingOperationTypes["getOrderStatus"]["body"], requestOptions?: RequestOptions): Promise { + const operation = TRADING_OPERATIONS["getOrderStatus"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + getTradingVolume(body: TradingOperationTypes["getTradingVolume"]["body"], requestOptions?: RequestOptions): Promise; + getTradingVolume(body: TradingOperationTypes["getTradingVolume"]["body"]): Promise; + getTradingVolume(body: TradingOperationTypes["getTradingVolume"]["body"], requestOptions?: RequestOptions): Promise { + const operation = TRADING_OPERATIONS["getTradingVolume"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + listActiveOrders(body: TradingOperationTypes["listActiveOrders"]["body"], requestOptions?: RequestOptions): Promise; + listActiveOrders(body: TradingOperationTypes["listActiveOrders"]["body"]): Promise; + listActiveOrders(body: TradingOperationTypes["listActiveOrders"]["body"], requestOptions?: RequestOptions): Promise { + const operation = TRADING_OPERATIONS["listActiveOrders"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + listPastOrders(body: TradingOperationTypes["listPastOrders"]["body"], requestOptions?: RequestOptions): Promise; + listPastOrders(body: TradingOperationTypes["listPastOrders"]["body"]): Promise; + listPastOrders(body: TradingOperationTypes["listPastOrders"]["body"], requestOptions?: RequestOptions): Promise { + const operation = TRADING_OPERATIONS["listPastOrders"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + listPastTrades(body: TradingOperationTypes["listPastTrades"]["body"], requestOptions?: RequestOptions): Promise; + listPastTrades(body: TradingOperationTypes["listPastTrades"]["body"]): Promise; + listPastTrades(body: TradingOperationTypes["listPastTrades"]["body"], requestOptions?: RequestOptions): Promise { + const operation = TRADING_OPERATIONS["listPastTrades"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + sendHeartbeat(body: TradingOperationTypes["sendHeartbeat"]["body"], requestOptions?: RequestOptions): Promise; + sendHeartbeat(body: TradingOperationTypes["sendHeartbeat"]["body"]): Promise; + sendHeartbeat(body: TradingOperationTypes["sendHeartbeat"]["body"], requestOptions?: RequestOptions): Promise { + const operation = TRADING_OPERATIONS["sendHeartbeat"]; + return executeRestOperation(this.transport, operation, { + body, + }, requestOptions); + } + + wrapOrder(input: { + path: TradingOperationTypes["wrapOrder"]["path"]; + body: TradingOperationTypes["wrapOrder"]["body"]; + }, requestOptions?: RequestOptions): Promise; + wrapOrder(input: { + path: TradingOperationTypes["wrapOrder"]["path"]; + body: TradingOperationTypes["wrapOrder"]["body"]; + }): Promise; + wrapOrder(input: { + path: TradingOperationTypes["wrapOrder"]["path"]; + body: TradingOperationTypes["wrapOrder"]["body"]; + }, requestOptions?: RequestOptions): Promise { + const operation = TRADING_OPERATIONS["wrapOrder"]; + return executeRestOperation(this.transport, operation, { + path: input.path, + body: input.body, + }, requestOptions); + } +} diff --git a/packages/sdk-typescript/src/generated/websocket/index.ts b/packages/sdk-typescript/src/generated/websocket/index.ts new file mode 100644 index 0000000..636d22d --- /dev/null +++ b/packages/sdk-typescript/src/generated/websocket/index.ts @@ -0,0 +1,463 @@ +// GENERATED by scripts/generate-ws-types.mjs from websocket.yaml — DO NOT EDIT. +// Regenerate: yarn ws:generate + +export type Connection = GenericSuccessResponse | ListSubscriptionsResponse | DepthResponse | OrderActionResponse | RfqSubmitQuoteResponse | RfqWithdrawQuoteResponse | RfqConfirmQuoteResponse | ErrorResponse; + +export interface ConninfoRequest { + id: string | number; + method: 'conninfo'; + params?: Map; + additionalProperties?: Map; +} + +export interface PingRequest { + id: string | number; + method: 'ping'; + params?: Map; + additionalProperties?: Map; +} + +export interface TimeRequest { + id: string | number; + method: 'time'; + params?: Map; + additionalProperties?: Map; +} + +export interface SubscribeRequest { + id: string | number; + method: "SUBSCRIBE" | "subscribe"; + params: string[]; + additionalProperties?: Map; +} + +export interface UnsubscribeRequest { + id: string | number; + method: "UNSUBSCRIBE" | "unsubscribe"; + params: string[]; + additionalProperties?: Map; +} + +export interface ListSubscriptionsRequest { + id: string | number; + method: "LIST_SUBSCRIPTIONS" | "list_subscriptions"; + params?: Map; + additionalProperties?: Map; +} + +export interface DepthRequest { + id: string | number; + method: 'depth'; + params: AnonymousSchema_26; + additionalProperties?: Map; +} + +export interface AnonymousSchema_26 { + symbol: string; + limit?: number; + additionalProperties?: Map; +} + +export interface OrderPlaceRequest { + id: string | number; + method: 'order.place'; + params: OrderPlaceParams; + additionalProperties?: Map; +} + +export interface OrderPlaceParams { + symbol: string; + side: AnonymousSchema_32; + type: AnonymousSchema_33; + timeInForce: AnonymousSchema_34; + price?: string; + stopPrice?: string; + quantity: string; + clientOrderId?: string; + eventOutcome?: AnonymousSchema_36; + additionalProperties?: Map; +} + +export enum AnonymousSchema_32 { + BUY = "BUY", + SELL = "SELL", +} + +export enum AnonymousSchema_33 { + LIMIT = "LIMIT", + MARKET = "MARKET", +} + +export enum AnonymousSchema_34 { + GTC = "GTC", + IOC = "IOC", + FOK = "FOK", + MOC = "MOC", +} + +export enum AnonymousSchema_36 { + YES = "YES", + NO = "NO", +} + +export interface OrderCancelRequest { + id: string | number; + method: 'order.cancel'; + params: OrderCancelParams; + additionalProperties?: Map; +} + +export interface OrderCancelParams { + orderId: string | number; + additionalProperties?: Map; +} + +export interface OrderCancelAllRequest { + id: string | number; + method: 'order.cancel_all'; + params?: Map; + additionalProperties?: Map; +} + +export interface OrderCancelSessionRequest { + id: string | number; + method: 'order.cancel_session'; + params?: Map; + additionalProperties?: Map; +} + +export interface RfqSubmitQuoteRequest { + id: string | number; + method: 'rfq.submit_quote'; + params: RfqSubmitQuoteParams; + additionalProperties?: Map; +} + +export interface RfqSubmitQuoteParams { + rfqId: string; + price: string; + quantity: string; + validUntil?: number | bigint; + additionalProperties?: Map; +} + +export interface RfqWithdrawQuoteRequest { + id: string | number; + method: 'rfq.withdraw_quote'; + params: RfqWithdrawQuoteParams; + additionalProperties?: Map; +} + +export interface RfqWithdrawQuoteParams { + rfqId: string; + quoteId: string; + additionalProperties?: Map; +} + +export interface RfqConfirmQuoteRequest { + id: string | number; + method: 'rfq.confirm_quote'; + params: RfqConfirmQuoteParams; + additionalProperties?: Map; +} + +export interface RfqConfirmQuoteParams { + rfqId: string; + quoteId: string; + confirm: boolean; + additionalProperties?: Map; +} + +export interface GenericSuccessResponse { + id: string | number; + status: number; + result?: any; + additionalProperties?: Map; +} + +export interface ListSubscriptionsResponse { + id: string | number; + status: number; + result?: string[]; + additionalProperties?: Map; +} + +export interface DepthResponse { + id: string | number; + status: number; + result?: OrderBookSnapshot; + additionalProperties?: Map; +} + +export interface OrderBookSnapshot { + lastUpdateId: number | bigint; + bids: string[][]; + asks: string[][]; + additionalProperties?: Map; +} + +export interface OrderActionResponse { + id: string | number; + status: number; + result?: Map; + additionalProperties?: Map; +} + +export interface RfqSubmitQuoteResponse { + id: string | number; + status: number; + result?: AnonymousSchema_74; + additionalProperties?: Map; +} + +export interface AnonymousSchema_74 { + rfqId: string; + quoteId: string; + additionalProperties?: Map; +} + +export interface RfqWithdrawQuoteResponse { + id: string | number; + status: number; + result?: AnonymousSchema_78; + additionalProperties?: Map; +} + +export interface AnonymousSchema_78 { + rfqId: string; + quoteId: string; + additionalProperties?: Map; +} + +export interface RfqConfirmQuoteResponse { + id: string | number; + status: number; + result?: AnonymousSchema_82; + additionalProperties?: Map; +} + +export interface AnonymousSchema_82 { + rfqId: string; + quoteId: string; + confirmed: boolean; + additionalProperties?: Map; +} + +export interface ErrorResponse { + id: string | number; + status: number; + error: WsError; + additionalProperties?: Map; +} + +export interface WsError { + code: number; + msg: string; + additionalProperties?: Map; +} + +export interface BookTicker { + u: number | bigint; + E: number | bigint; + s: string; + b: string; + B: string; + a: string; + A: string; + c?: string; + C?: string; + additionalProperties?: Map; +} + +export interface DepthUpdate { + e: 'depthUpdate'; + E: number | bigint; + s: string; + U: number | bigint; + u: number | bigint; + b: string[][]; + a: string[][]; + additionalProperties?: Map; +} + +export interface Trade { + E: number | bigint; + s: string; + t: number | bigint; + p: string; + q: string; + m: boolean; + additionalProperties?: Map; +} + +export interface OrderUpdate { + e: 'orderUpdate'; + E: number | bigint; + s: string; + i: number | bigint; + c?: string; + S?: AnonymousSchema_114; + o?: AnonymousSchema_115; + X: AnonymousSchema_116; + O?: AnonymousSchema_117; + p?: string; + P?: string; + q?: string; + z?: string; + Z?: string; + L?: string; + t?: number | bigint; + n?: string; + m?: boolean; + r?: string; + T: number | bigint; + additionalProperties?: Map; +} + +export enum AnonymousSchema_114 { + BUY = "BUY", + SELL = "SELL", +} + +export enum AnonymousSchema_115 { + LIMIT = "LIMIT", + MARKET = "MARKET", + STOP_LIMIT = "STOP_LIMIT", + STOP_MARKET = "STOP_MARKET", +} + +export enum AnonymousSchema_116 { + RESERVED_NEW = "NEW", + RESERVED_OPEN = "OPEN", + FILLED = "FILLED", + PARTIALLY_FILLED = "PARTIALLY_FILLED", + CANCELED = "CANCELED", + REJECTED = "REJECTED", + MODIFIED = "MODIFIED", +} + +export enum AnonymousSchema_117 { + YES = "YES", + NO = "NO", +} + +export interface BalanceUpdate { + e: 'balanceUpdate'; + E: number | bigint; + u: number | bigint; + B: Balance[]; + additionalProperties?: Map; +} + +export interface Balance { + a: string; + f: string; + c: string; + additionalProperties?: Map; +} + +export interface PositionReport { + e: 'positionReport'; + E: number | bigint; + u: number | bigint; + A: number | bigint; + P: PositionRow[]; + additionalProperties?: Map; +} + +export interface PositionRow { + t: string; + s: string; + a: NamedAmount[]; + additionalProperties?: Map; +} + +export interface NamedAmount { + t: string; + v: string; + c?: string; + additionalProperties?: Map; +} + +export interface ContractStatus { + e: 'contractStatus'; + E: number | bigint; + s: string; + k: string; + c: string; + i: number | bigint; + p?: string; + o: string; + n: string; + additionalProperties?: Map; +} + +export interface RfqPublicEvent { + e: 'requestForQuote'; + E: number | bigint; + r: string; + s?: string; + l: RfqLeg[]; + n?: string; + q?: string; + f?: string; + S: RfqLifecycleState; + w?: number | bigint; + x?: number | bigint; + c?: number | bigint; + additionalProperties?: Map; +} + +export interface RfqLeg { + c: string; + o: AnonymousSchema_144; + additionalProperties?: Map; +} + +export enum AnonymousSchema_144 { + YES = "YES", + NO = "NO", +} + +export enum RfqLifecycleState { + RESERVED_OPEN = "OPEN", + PENDING_ACCEPTANCE = "PENDING_ACCEPTANCE", + CONFIRMING = "CONFIRMING", + FINALIZING = "FINALIZING", + FINALIZED = "FINALIZED", + CANCELLED = "CANCELLED", + EXPIRED = "EXPIRED", + FAILED = "FAILED", +} + +export interface RfqPrivateDelivery { + e: 'requestForQuote'; + i: string; + E: number | bigint; + r: string; + x: AnonymousSchema_148; + S: RfqLifecycleState; + q?: string; + p?: string; + sz?: string; + qs?: RfqQuoteStatus; + vu?: number | bigint; + additionalProperties?: Map; +} + +export enum AnonymousSchema_148 { + RESERVED_CLOSED = "CLOSED", + ACCEPTED = "ACCEPTED", + CONFIRMED = "CONFIRMED", + DECLINED = "DECLINED", + FINALIZED = "FINALIZED", + FAILED = "FAILED", +} + +export enum RfqQuoteStatus { + ACTIVE = "ACTIVE", + WITHDRAWN = "WITHDRAWN", + EXPIRED = "EXPIRED", + WON = "WON", + LOST = "LOST", +} diff --git a/packages/sdk-typescript/src/heartbeat.ts b/packages/sdk-typescript/src/heartbeat.ts new file mode 100644 index 0000000..7652154 --- /dev/null +++ b/packages/sdk-typescript/src/heartbeat.ts @@ -0,0 +1,60 @@ +import type { RequestOptions } from "./core/deadline.js"; + +export interface ManagedHeartbeatOptions { + intervalMs: number; + beat: (options: RequestOptions) => Promise; + onError?: (error: unknown) => void; + requestOptions?: RequestOptions; +} + +/** Explicitly controlled heartbeat loop. It never starts by construction. */ +export class ManagedHeartbeat { + private readonly intervalMs: number; + private readonly beat: ManagedHeartbeatOptions["beat"]; + private readonly onError?: ManagedHeartbeatOptions["onError"]; + private readonly requestOptions: RequestOptions; + private timer?: ReturnType; + private controller?: AbortController; + private running = false; + + constructor(options: ManagedHeartbeatOptions) { + if (!Number.isFinite(options.intervalMs) || options.intervalMs <= 0) { + throw new Error("heartbeat intervalMs must be a finite positive number"); + } + this.intervalMs = options.intervalMs; + this.beat = options.beat; + this.onError = options.onError; + this.requestOptions = options.requestOptions ?? {}; + } + + start(): void { + if (this.running) return; + this.running = true; + this.controller = new AbortController(); + void this.run(); + } + + stop(): void { + this.running = false; + if (this.timer) clearTimeout(this.timer); + this.timer = undefined; + this.controller?.abort(); + this.controller = undefined; + } + + private async run(): Promise { + const controller = this.controller; + if (!this.running || !controller) return; + try { + const signal = this.requestOptions.signal + ? AbortSignal.any([this.requestOptions.signal, controller.signal]) + : controller.signal; + await this.beat({ ...this.requestOptions, signal }); + } catch (error) { + if (this.running && !controller.signal.aborted) this.onError?.(error); + } + if (this.running && this.controller === controller) { + this.timer = setTimeout(() => { void this.run(); }, this.intervalMs); + } + } +} diff --git a/packages/sdk-typescript/src/json.ts b/packages/sdk-typescript/src/json.ts new file mode 100644 index 0000000..167189b --- /dev/null +++ b/packages/sdk-typescript/src/json.ts @@ -0,0 +1,187 @@ +// Lossless JSON parsing for the wire boundary. +// +// The exchange sends integers that exceed JS safe-int (2^53) — the `E` nanosecond +// timestamp and sequence ids. Plain JSON.parse rounds them to the nearest double +// silently, corrupting ids with no error. This preserves them as bigint using the +// reviver's raw source text (Node 22+ JSON.parse source access), so the exact +// digits survive. +import { SdkError, ValidationError } from "./errors.js"; + +// An integer literal: optional sign, digits, no fraction or exponent. Only these +// can be a precise id; anything with "." or "e" is a float form and stays a number. +const INTEGER_LITERAL = /^-?\d+$/; + +/** + * Parse JSON, preserving integers beyond the safe range as `bigint`. Everything + * else is unchanged: strings (e.g. prices) stay strings, floats and safe integers + * stay `number`. + */ +export function parseLosslessJson(text: string): unknown { + return JSON.parse(text, (_key, value, context?: { source?: string }) => { + if (typeof value !== "number") return value; + + const source = context?.source; + if (source === undefined) { + // Runtime lacks JSON source access (pre-Node-22). We can't recover the exact + // digits, so fail loud rather than hand back a silently-rounded id. + if (Number.isInteger(value) && !Number.isSafeInteger(value)) { + throw new SdkError( + "lossless JSON parsing requires JSON source access (Node 22+)", + ); + } + return value; + } + + if (INTEGER_LITERAL.test(source) && !Number.isSafeInteger(value)) { + return BigInt(source); + } + return value; + }); +} + +export type Int64Path = readonly (string | "*")[]; + +export type RequestInt64Path = { + path: Int64Path; + allowString?: boolean; + unsigned?: boolean; +}; + +const MAX_UNSIGNED_INT64 = 18446744073709551615n; + +function requestFieldPath(path: Int64Path, index: number, displayPath: string): string { + const segment = path[index]; + if (segment === "*") return `${displayPath}[*]`; + return displayPath ? `${displayPath}.${segment}` : segment; +} + +function validateRequestInt64AtPath( + value: unknown, + descriptor: RequestInt64Path, + offset: number, + displayPath: string, + operation: string, +): void { + const { path } = descriptor; + if (value === undefined || (value === null && offset < path.length)) return; + + if (offset === path.length) { + if (typeof value === "bigint") { + if (!descriptor.unsigned || (value >= 0n && value <= MAX_UNSIGNED_INT64)) return; + throw new ValidationError({ + operation, + field: displayPath, + rule: "unsigned-integer", + message: `request field ${displayPath} must be an unsigned 64-bit integer`, + }); + } + if (typeof value === "number" && Number.isSafeInteger(value)) { + if (!descriptor.unsigned || value >= 0) return; + throw new ValidationError({ + operation, + field: displayPath, + rule: "unsigned-integer", + message: `request field ${displayPath} must be an unsigned 64-bit integer`, + }); + } + if (descriptor.allowString && typeof value === "string") return; + throw new ValidationError({ + operation, + field: displayPath, + rule: typeof value === "number" ? "safe-integer" : "type", + message: `request field ${displayPath} must be a bigint or safe integer`, + }); + } + + const segment = path[offset]; + if (segment === "*") { + if (!Array.isArray(value)) { + throw new ValidationError({ + operation, + field: displayPath, + rule: "type", + message: `request field ${displayPath} must be an array`, + }); + } + value.forEach((item, index) => + validateRequestInt64AtPath(item, descriptor, offset + 1, `${displayPath}[${index}]`, operation)); + return; + } + + if (typeof value !== "object" || Array.isArray(value)) { + throw new ValidationError({ + operation, + field: displayPath, + rule: "type", + message: `request field ${displayPath} must be an object`, + }); + } + const record = value as Record; + if (!Object.hasOwn(record, segment)) return; + validateRequestInt64AtPath( + record[segment], + descriptor, + offset + 1, + requestFieldPath(path, offset, displayPath), + operation, + ); +} + +export function validateInt64RequestPaths( + value: unknown, + paths: readonly RequestInt64Path[], + operation: string, +): void { + for (const descriptor of paths) { + validateRequestInt64AtPath(value, descriptor, 0, "", operation); + } +} + +function normalizeAtPath( + value: unknown, + path: Int64Path, + offset: number, + displayPath: string, +): unknown { + if (value === undefined || value === null) return value; + + if (offset === path.length) { + if (typeof value === "bigint") return value; + if (typeof value === "number" && Number.isSafeInteger(value)) return BigInt(value); + if (typeof value === "string" && INTEGER_LITERAL.test(value)) return BigInt(value); + throw new SdkError(`expected int64 at ${displayPath}`); + } + + const segment = path[offset]; + if (segment === "*") { + if (!Array.isArray(value)) throw new SdkError(`expected array at ${displayPath}`); + for (let index = 0; index < value.length; index++) { + value[index] = normalizeAtPath(value[index], path, offset + 1, `${displayPath}[${index}]`); + } + return value; + } + + if (typeof value !== "object" || Array.isArray(value)) { + throw new SdkError(`expected object at ${displayPath}`); + } + const record = value as Record; + if (!Object.hasOwn(record, segment)) return value; + record[segment] = normalizeAtPath( + record[segment], + path, + offset + 1, + displayPath ? `${displayPath}.${segment}` : segment, + ); + return value; +} + +export function normalizeInt64Paths( + value: unknown, + paths: readonly Int64Path[], +): unknown { + let normalized = value; + for (const path of paths) { + normalized = normalizeAtPath(normalized, path, 0, ""); + } + return normalized; +} diff --git a/packages/sdk-typescript/src/live-order-book.ts b/packages/sdk-typescript/src/live-order-book.ts new file mode 100644 index 0000000..844bd8b --- /dev/null +++ b/packages/sdk-typescript/src/live-order-book.ts @@ -0,0 +1,216 @@ +import { TypedEmitter } from "./core/typed-emitter.js"; + +import { OrderBook, normalizePrice, toId, type Level } from "./orderbook.js"; +import { ResyncRequiredError, SdkError, serializeError } from "./errors.js"; +import type { DiagnosticListener } from "./diagnostics.js"; +import { emitDiagnostic, type Logger, NOOP_LOGGER } from "./logging.js"; +import type { BookDelta, BookEvent, LiveOrderBook as LiveOrderBookContract } from "./types/client.js"; +import type { DepthUpdate } from "./websocket-types.js"; + +// Prices must use the same canonical keys as the book engine. +function toDelta(b: string[][], a: string[][]): BookDelta { + const side = (levels: string[][]): Level[] => + levels.map(([price, qty]) => ({ price: normalizePrice(price), qty })); + return { bids: side(b), asks: side(a) }; +} + +// Keep the TypedEmitter wrapper and AbortSignal handler together so both are removed. +interface Registration { + event: keyof LiveOrderBookEvents; + wrapper: (...args: unknown[]) => void; + signal?: AbortSignal; + onAbort?: () => void; +} + +// The facade identifies snapshots after subscription acknowledgement; the book never guesses. +type LiveOrderBookEvents = { + update: (book: LiveOrderBook, delta: BookDelta) => void; + resync: () => void; + error: (error: Error) => void; +}; + +export class LiveOrderBook extends TypedEmitter implements LiveOrderBookContract { + readonly symbol: string; + private readonly logger: Logger; + private readonly book = new OrderBook(); + private live = false; + private resyncSignaled = false; // dedup: one 'resync' per stale period, until a snapshot recovers + private closed = false; // close() is permanent — no later frame may revive a torn-down book + private readonly registrations = new Map<(...args: never[]) => void, Registration[]>(); + private readonly onClose?: () => void; + private readonly onDiagnostic?: DiagnosticListener; + + constructor(symbol: string, options?: { logger?: Logger; onDiagnostic?: DiagnosticListener; onClose?: () => void }) { + super(); + this.symbol = symbol; + this.logger = options?.logger ?? NOOP_LOGGER; + this.onClose = options?.onClose; + this.onDiagnostic = options?.onDiagnostic; + } + + private emitDiagnosticEvent(level: "warn" | "error", name: string, error?: unknown): void { + emitDiagnostic({ + level, + component: "order_book", + name, + traffic: "stream", + metadata: { symbol: this.symbol }, + ...(error ? { error: serializeError(error) } : {}), + }, this.logger, this.onDiagnostic); + } + + /** + * Rebuild from a fresh full-book snapshot. The facade calls this with the frame it has identified + * as the snapshot after correlating the SUBSCRIBE success, so a queued or stale diff is never + * mistaken for it. Returns whether the snapshot was accepted; on success emits one 'update' + * carrying the full book. + */ + applySnapshot(frame: unknown): boolean { + if (this.closed) return false; + const update = this.asDepthUpdate(frame); + if (!update) return false; + let delta: BookDelta | undefined; + try { + if (toId(update.U) !== toId(update.u)) { + throw new SdkError(`snapshot frame must have U == u, got U=${update.U} u=${update.u}`); + } + this.book.applySnapshot({ lastUpdateId: update.u, bids: update.b, asks: update.a }); + this.live = true; + this.resyncSignaled = false; + const snapshot = this.book.snapshot(); + delta = { bids: snapshot.bids, asks: snapshot.asks }; + } catch (error) { + this.fail(error); + return false; + } + this.emit("update", this, delta); // outside try: a throwing listener must not stale the book + return true; + } + + /** + * Apply one incremental diff. Dropped unless the book is live (before the first snapshot, or + * while stale awaiting the facade's re-snapshot) — so a stray/queued frame can't revive it. + */ + ingest(frame: unknown): void { + if (this.closed || !this.live) return; + const update = this.asDepthUpdate(frame); + if (!update) return; + let delta: BookDelta | undefined; + try { + // Stale updates are dropped so delta consumers cannot roll back their local book. + if (this.book.applyDiff(update)) delta = toDelta(update.b, update.a); + } catch (error) { + this.fail(error); + return; + } + if (delta) this.emit("update", this, delta); // outside try (see applySnapshot) + } + + /** Discard the book (gap, malformed frame, or reconnect): go stale, emit 'resync' (deduped). */ + markStale(): void { + this.live = false; + if (!this.resyncSignaled) { + this.resyncSignaled = true; + this.emit("resync"); + } + } + + private asDepthUpdate(frame: unknown): DepthUpdate | undefined { + if (!frame || typeof frame !== "object") return undefined; + const rec = frame as Record; + return rec.e === "depthUpdate" ? (rec as unknown as DepthUpdate) : undefined; + } + + // A rejected frame (gap or malformed) means the book can't be trusted. Go stale BEFORE notifying + // listeners — a throwing 'error'/'resync' listener must not bypass recovery. + private fail(error: unknown): void { + this.markStale(); + if (error instanceof ResyncRequiredError) { + this.emitDiagnosticEvent("warn", "orderbook.resync", error); + } else { + // Wrap non-SDK errors (e.g. a level that isn't a [price, qty] string tuple -> TypeError) so + // the emitted 'error' is always an SdkError, per the LiveOrderBook contract. + this.emitDiagnosticEvent("error", "orderbook.frame.failure", error); + if (this.listenerCount("error") > 0) { + this.emit( + "error", + error instanceof SdkError ? error : new SdkError("malformed depth frame", { cause: error }), + ); + } + } + } + + // Reads expose nothing while stale — a gapped book must never look tradeable. + bestBid(): Level | undefined { + return this.live ? this.book.bestBid() : undefined; + } + bestAsk(): Level | undefined { + return this.live ? this.book.bestAsk() : undefined; + } + topN(side: "bids" | "asks", n: number): Level[] { + return this.live ? this.book.topN(side, n) : []; + } + spread(): number | undefined { + return this.live ? this.book.spread() : undefined; + } + mid(): number | undefined { + return this.live ? this.book.mid() : undefined; + } + snapshot(): { bids: Level[]; asks: Level[] } { + return this.live ? this.book.snapshot() : { bids: [], asks: [] }; + } + + on(event: keyof LiveOrderBookEvents, cb: (...args: never[]) => void, options?: { signal?: AbortSignal }): this { + if (this.closed || options?.signal?.aborted) return this; + const wrapper = (...args: unknown[]): void => (cb as (...a: unknown[]) => void)(...args); + super.on(event, wrapper as LiveOrderBookEvents[typeof event]); + const registration: Registration = { event, wrapper, signal: options?.signal }; + if (options?.signal) { + registration.onAbort = () => this.remove(cb, registration); + options.signal.addEventListener("abort", registration.onAbort, { once: true }); + } + const list = this.registrations.get(cb) ?? []; + list.push(registration); + this.registrations.set(cb, list); + return this; + } + + off(event: BookEvent, cb: (...args: never[]) => void): this { + const registration = this.registrations.get(cb)?.find((candidate) => candidate.event === event); + if (registration) this.remove(cb, registration); + return this; + } + + // Remove the TypedEmitter listener and its AbortSignal handler together. + private remove(cb: (...args: never[]) => void, registration: Registration): void { + super.off(registration.event, registration.wrapper as LiveOrderBookEvents[keyof LiveOrderBookEvents]); + if (registration.signal && registration.onAbort) { + registration.signal.removeEventListener("abort", registration.onAbort); + } + const list = this.registrations.get(cb); + if (!list) return; + const index = list.indexOf(registration); + if (index >= 0) list.splice(index, 1); + if (list.length === 0) this.registrations.delete(cb); + } + + /** True once close() has run. Lets an owner (the facade) avoid handing back a torn-down book. */ + isClosed(): boolean { + return this.closed; + } + + /** Stop this book permanently: go dark, drop listeners, and detach every abort handler. */ + close(): void { + if (this.closed) return; + this.closed = true; + this.live = false; + this.onClose?.(); + for (const list of this.registrations.values()) { + for (const reg of list) { + if (reg.signal && reg.onAbort) reg.signal.removeEventListener("abort", reg.onAbort); + } + } + this.registrations.clear(); + this.removeAllListeners(); + } +} diff --git a/packages/sdk-typescript/src/logging.ts b/packages/sdk-typescript/src/logging.ts new file mode 100644 index 0000000..4a27db0 --- /dev/null +++ b/packages/sdk-typescript/src/logging.ts @@ -0,0 +1,95 @@ +import { + redactDiagnosticValue, + type DiagnosticEvent, + type DiagnosticListener, + type LogLevel, +} from "./diagnostics.js"; + +export type { DiagnosticEvent, DiagnosticListener, LogLevel }; + +/** Severity levels, ordered low to high. */ +const LEVEL_ORDER: Record = { + debug: 10, + info: 20, + warn: 30, + error: 40, +}; + +export interface Logger { + debug(message: string, meta?: Record): void; + info(message: string, meta?: Record): void; + warn(message: string, meta?: Record): void; + error(message: string, meta?: Record): void; +} + +export function emitDiagnostic( + event: DiagnosticEvent, + logger: Logger, + listener?: DiagnosticListener, +): void { + const safeEvent = redactDiagnosticValue(event) as DiagnosticEvent; + try { + listener?.(safeEvent); + } catch { + // Diagnostics are best-effort and must never change the API result. + } + try { + logger[safeEvent.level](safeEvent.name, safeEvent); + } catch { + // A caller logger must not break the operation it observes. + } +} + +/** Writes safe structured events to the console when enabled. */ +export class ConsoleLogger implements Logger { + private readonly threshold: number; + + constructor(options?: { minLevel?: LogLevel }) { + this.threshold = LEVEL_ORDER[options?.minLevel ?? "info"]; + } + + private log( + level: LogLevel, + message: string, + meta?: Record, + ): void { + if (LEVEL_ORDER[level] < this.threshold) return; + const line = `${new Date().toISOString()} [${level.toUpperCase()}] ${message}`; + let consoleWriter = console.log; + if (level === "warn" || level === "error") { + consoleWriter = console.error; + } + + if (meta === undefined) consoleWriter(line); + else consoleWriter(line, redactDiagnosticValue(meta)); + } + + debug(message: string, meta?: Record): void { + this.log("debug", message, meta); + } + info(message: string, meta?: Record): void { + this.log("info", message, meta); + } + warn(message: string, meta?: Record): void { + this.log("warn", message, meta); + } + error(message: string, meta?: Record): void { + this.log("error", message, meta); + } +} + +/** Silent logger singleton — avoids allocating a new instance per consumer. */ +export const NOOP_LOGGER: Logger = { + debug() {}, + info() {}, + warn() {}, + error() {}, +}; + +/** @deprecated Use `NOOP_LOGGER` instead of `new NoopLogger()`. */ +export class NoopLogger implements Logger { + debug(_message: string, _meta?: Record): void {} + info(_message: string, _meta?: Record): void {} + warn(_message: string, _meta?: Record): void {} + error(_message: string, _meta?: Record): void {} +} diff --git a/packages/sdk-typescript/src/orderbook.ts b/packages/sdk-typescript/src/orderbook.ts new file mode 100644 index 0000000..e3c3e71 --- /dev/null +++ b/packages/sdk-typescript/src/orderbook.ts @@ -0,0 +1,226 @@ +import { ResyncRequiredError, SdkError } from "./errors.js"; +import type { DepthUpdate } from "./websocket-types.js"; + +/** + * Engine snapshot input: a full book at a known update id. NOT a wire type — the exchange + * sends no distinct snapshot message. The sequencer (LiveOrderBook) builds this from the + * first `depthUpdate` frame after subscribe (u -> lastUpdateId, b -> bids, a -> asks). + */ +export interface L2Snapshot { + lastUpdateId: number | bigint; + bids: string[][]; + asks: string[][]; +} + +/** One price level: aggregated quantity at a price, both exact decimal strings. */ +export type Level = { price: string; qty: string }; + +// Wire id → bigint. A bigint (from lossless parsing) passes through — it's already exact. +// A number must be a safe integer; otherwise JSON.parse already rounded it, so it has to be +// parsed losslessly upstream (parseLosslessJson) before it reaches the book. +export function toId(n: number | bigint): bigint { + if (typeof n === "bigint") return n; + if (!Number.isSafeInteger(n)) { + throw new SdkError(`update id ${n} is not a safe integer; parse it losslessly upstream (parseLosslessJson)`); + } + return BigInt(n); +} + +// Canonical key for a price string: strip insignificant zeros so equal values map to one +// key ("0.260" → "0.26", ".50" → "0.5", "0100.0" → "100"). Pure string transform — no float, +// the numeric value is preserved. Map keys are therefore canonical, so one price can't become +// two levels under different spellings. +export function normalizePrice(price: string): string { + if (!price.includes(".")) return price.replace(/^0+(?=\d)/, ""); + const dot = price.indexOf("."); + const int = price.slice(0, dot).replace(/^0+(?=\d)/, "") || "0"; + const frac = price.slice(dot + 1).replace(/0+$/, ""); + return frac === "" ? int : `${int}.${frac}`; +} + +// A well-formed non-negative decimal, for validating both prices and quantities ("0", "12", +// "0.50", ".5", "12."). No float: Number() could underflow a tiny value to 0 and accepts +// non-decimal spellings. +const DECIMAL = /^(?:\d+\.?\d*|\.\d+)$/; + +// Reject a malformed level up front. A corrupt frame must NOT be silently skipped: skipping a +// level while still advancing the sequence leaves a stale level that no later gap can repair +// (silent divergence). Validate every level before mutating anything, so a bad diff throws +// without advancing — the next diff then gaps and triggers a resync. +function assertDecimalLevels(changes: string[][]): void { + for (const level of changes) { + const [price, qty] = level; + // Frames are untrusted (cast from `unknown`), so guard the runtime shape, not just the type: + // each level must be a [price, qty] pair of decimal STRINGS. A non-string (e.g. a JSON number) + // would coerce through the regex but then throw mid-mutation in normalizePrice — breaking the + // "validate before mutating" atomicity and leaving a partially-changed book. + if ( + !Array.isArray(level) || // a string is iterable with a .length too — require a real array + level.length !== 2 || + typeof price !== "string" || + typeof qty !== "string" || + !DECIMAL.test(price) || + !DECIMAL.test(qty) + ) { + throw new SdkError( + `malformed depth level (price ${JSON.stringify(price)}, qty ${JSON.stringify(qty)})`, + ); + } + } +} + +// Apply validated changes to one side: a decimal-zero quantity removes the level, a nonzero sets +// it, under a canonical price key. Assumes assertDecimalLevels has already passed. +function applyLevels(side: Map, changes: string[][]): void { + for (const [price, qty] of changes) { + const key = normalizePrice(price); + if (!/[1-9]/.test(qty)) side.delete(key); // valid decimal with no nonzero digit = zero + else side.set(key, qty); + } +} + +// Compare two non-negative decimal-string prices by exact numeric value — no float, so ordering +// stays correct past ~15 sig figs where Number() would collapse distinct prices together. +// Returns <0 / 0 / >0 like a sort comparator. Prices are order-book prices (non-negative). +function compareDecimal(a: string, b: string): number { + const [aInt, aFrac = ""] = a.split("."); + const [bInt, bFrac = ""] = b.split("."); + const ai = aInt.replace(/^0+(?=\d)/, ""); + const bi = bInt.replace(/^0+(?=\d)/, ""); + // Integer part: more digits = larger; same length compares lexically (digits only). + if (ai.length !== bi.length) return ai.length - bi.length; + if (ai !== bi) return ai < bi ? -1 : 1; + // Fractional part: pad to equal length with trailing zeros, then compare lexically. + const len = Math.max(aFrac.length, bFrac.length); + const af = aFrac.padEnd(len, "0"); + const bf = bFrac.padEnd(len, "0"); + if (af !== bf) return af < bf ? -1 : 1; + return 0; +} + +// Sorted, frozen best-first view of a side. Frozen so callers can't corrupt the cache. +function buildView(side: Map, dir: "desc" | "asc"): Level[] { + const levels = [...side.entries()].map(([price, qty]) => Object.freeze({ price, qty })); + levels.sort((a, b) => + dir === "desc" ? compareDecimal(b.price, a.price) : compareDecimal(a.price, b.price), + ); + return Object.freeze(levels) as Level[]; +} + +/** + * Local L2 order book. Prices/quantities are exact decimal strings (money path); ids are bigint. + * Mutate only via applySnapshot/applyDiff — they invalidate the cached sorted views. + * bids/asks return read-only defensive copies of each side (each call copies the side, so it's + * fine for occasional lookups but not a per-tick hot path — use the read methods for that). + * + * Level identity is the CANONICAL price key (see normalizePrice): equal prices written + * differently ("0.50" vs "0.5") map to one level, so a "0" removal can't leave a stale twin. + */ +export class OrderBook { + readonly #bids = new Map(); + readonly #asks = new Map(); + #lastUpdateId = 0n; + + // Defensive copies: a caller can't corrupt book state or bypass cache invalidation. Each call + // copies the whole side (O(n)) — fine for occasional lookups, not a per-tick hot loop. + get bids(): ReadonlyMap { + return new Map(this.#bids); + } + + get asks(): ReadonlyMap { + return new Map(this.#asks); + } + + /** Sequence id of the last update applied. Read-only; advanced only by applySnapshot/applyDiff. */ + get lastUpdateId(): bigint { + return this.#lastUpdateId; + } + + // Cached sorted views, best-first. null = dirty; rebuilt on read, cleared on write. + #sortedBids: Level[] | null = null; + #sortedAsks: Level[] | null = null; + + /** Replace the entire book with a fresh snapshot. */ + applySnapshot(snapshot: L2Snapshot): void { + assertDecimalLevels(snapshot.bids); // validate before clearing — a malformed snapshot must not + assertDecimalLevels(snapshot.asks); // wipe a good book and then throw + this.#bids.clear(); + this.#asks.clear(); + this.#lastUpdateId = toId(snapshot.lastUpdateId); + // A snapshot "0" means "no level" — applyLevels drops it, same as the diff path. + applyLevels(this.#bids, snapshot.bids); + applyLevels(this.#asks, snapshot.asks); + this.#sortedBids = null; + this.#sortedAsks = null; + } + + /** + * Apply one differential update, enforcing sequence order. + * Returns true if the diff was applied, false if it was dropped as stale + * (`u <= lastUpdateId`) — callers must not surface a delta for a dropped frame. + * Throws ResyncRequiredError on a gap. + */ + applyDiff(diff: DepthUpdate): boolean { + const u = toId(diff.u); + if (u <= this.#lastUpdateId) return false; // stale: already covered + const U = toId(diff.U); + // Gemini's Fast WS depth stream overlaps at U == lastUpdateId. Unlike Binance's + // contiguous stream, U > lastUpdateId already indicates a missed frame. + if (U > this.#lastUpdateId) { + throw new ResyncRequiredError(this.#lastUpdateId, U); // gap: resync + } + assertDecimalLevels(diff.b); // validate both sides before mutating — atomic; a bad diff throws + assertDecimalLevels(diff.a); // without advancing, so the next diff gaps → resync + applyLevels(this.#bids, diff.b); + applyLevels(this.#asks, diff.a); + this.#lastUpdateId = u; + if (diff.b.length) this.#sortedBids = null; + if (diff.a.length) this.#sortedAsks = null; + return true; + } + + #bidView(): Level[] { + return (this.#sortedBids ??= buildView(this.#bids, "desc")); + } + + #askView(): Level[] { + return (this.#sortedAsks ??= buildView(this.#asks, "asc")); + } + + /** Highest-price bid level, or undefined if there are no bids. */ + bestBid(): Level | undefined { + return this.#bidView()[0]; + } + + /** Lowest-price ask level, or undefined if there are no asks. */ + bestAsk(): Level | undefined { + return this.#askView()[0]; + } + + /** Top `n` levels of a side, best-first (bids high→low, asks low→high). */ + topN(side: "bids" | "asks", n: number): Level[] { + if (n <= 0) return []; + return (side === "bids" ? this.#bidView() : this.#askView()).slice(0, n); + } + + /** Best ask price minus best bid, or undefined if a side is empty. Float — for display, not exact math. */ + spread(): number | undefined { + const bid = this.bestBid(); + const ask = this.bestAsk(); + if (!bid || !ask) return undefined; + return Number(ask.price) - Number(bid.price); + } + + /** Midpoint of best bid and best ask, or undefined if a side is empty. Float (see spread). */ + mid(): number | undefined { + const bid = this.bestBid(); + const ask = this.bestAsk(); + if (!bid || !ask) return undefined; + return (Number(ask.price) + Number(bid.price)) / 2; + } + + /** The whole book as sorted arrays (bids high→low, asks low→high). */ + snapshot(): { bids: Level[]; asks: Level[] } { + return { bids: this.#bidView().slice(), asks: this.#askView().slice() }; + } +} diff --git a/packages/sdk-typescript/src/prediction-markets.ts b/packages/sdk-typescript/src/prediction-markets.ts new file mode 100644 index 0000000..856eebb --- /dev/null +++ b/packages/sdk-typescript/src/prediction-markets.ts @@ -0,0 +1,35 @@ +import { AcceptTermsRequired } from "./errors.js"; +import { validateRequestBody } from "./core/request-validation.js"; +import { PredictionMarketsRest } from "./generated/rest.js"; +import type { PredictionMarketOperationTypes } from "./generated/operations.js"; +import type { RequestOptions } from "./core/deadline.js"; + +export class PredictionMarkets extends PredictionMarketsRest { + acceptTerms(requestOptions?: RequestOptions) { + return this.acceptPredictionMarketsTerms(requestOptions); + } + + override async placeOrder(body: PredictionMarketOperationTypes["placeOrder"]["body"], requestOptions?: RequestOptions) { + validateRequestBody("predictionMarkets.placeOrder", body); + await this.requireAcceptedTerms(requestOptions); + return super.placeOrder(body, requestOptions); + } + + override async placeOrderBatch(body: PredictionMarketOperationTypes["placeOrderBatch"]["body"], requestOptions?: RequestOptions) { + validateRequestBody("predictionMarkets.placeOrderBatch", body); + await this.requireAcceptedTerms(requestOptions); + return super.placeOrderBatch(body, requestOptions); + } + + private async requireAcceptedTerms(requestOptions?: RequestOptions): Promise { + const status = await this.getPredictionMarketsTermsStatus(requestOptions); + if (!status.hasAcceptedLatest) { + throw new AcceptTermsRequired({ + status: 403, + reason: "AcceptTermsRequired", + message: "Prediction Markets terms must be accepted before placing orders", + body: status, + }); + } + } +} diff --git a/packages/sdk-typescript/src/server/index.ts b/packages/sdk-typescript/src/server/index.ts new file mode 100644 index 0000000..8c01fa3 --- /dev/null +++ b/packages/sdk-typescript/src/server/index.ts @@ -0,0 +1,64 @@ +// Server entry point — everything from browser, plus HMAC auth and ws-backed WebSocket. + +// Re-export everything the browser exposes. +export * from "../browser/index.js"; + +// --- Server-only: HMAC auth --- +export { + HmacAuth, + type HmacAuthOptions, + type HmacNonceMode, +} from "../auth/hmac.js"; + +// --- Server-only: full OAuth (includes confidential client support) --- +export { + OAuthAuth, + type OAuthAuthOptions, + type OAuthClient, +} from "../auth/oauth.js"; + +// --- Server-only: ws-backed WebSocket factory --- +export { serverSocketFactory, initServerWebSocket } from "./ws-factory.js"; + +// --- Server createClient (overrides the browser createClient) --- + +import { GeminiMarkets } from "../gemini-markets.js"; +import type { GeminiMarketsOptions } from "../types/client.js"; +import { initServerWebSocket, serverSocketFactory } from "./ws-factory.js"; + +export interface ServerClientOptions extends GeminiMarketsOptions { + /** + * Skip automatic ws preloading. Set to true if you only use REST + * endpoints and don't need authenticated WebSocket connections. + */ + skipWsInit?: boolean; +} + +/** + * Create a Gemini Markets client with server defaults. + * + * When `auth` is provided and no custom `webSocketFactory` is set, the `ws` + * package is loaded for authenticated WebSocket header support. This only + * happens if you haven't set `skipWsInit: true` — REST-only users can skip it. + * + * ```ts + * // Full server client (REST + authenticated WebSocket) + * const client = await createClient({ + * auth: new HmacAuth({ apiKey, apiSecret }), + * }); + * + * // REST-only server client (no ws dependency needed) + * const client = await createClient({ + * auth: new HmacAuth({ apiKey, apiSecret }), + * skipWsInit: true, + * }); + * ``` + */ +export async function createClient(options?: ServerClientOptions): Promise { + const settings = options ?? {}; + if (settings.auth && !settings.skipWsInit && !settings.webSocketFactory) { + await initServerWebSocket(); + return new GeminiMarkets({ ...settings, webSocketFactory: serverSocketFactory } as GeminiMarketsOptions); + } + return new GeminiMarkets(settings); +} diff --git a/packages/sdk-typescript/src/server/ws-factory.ts b/packages/sdk-typescript/src/server/ws-factory.ts new file mode 100644 index 0000000..b5fd66e --- /dev/null +++ b/packages/sdk-typescript/src/server/ws-factory.ts @@ -0,0 +1,43 @@ +import type { SocketFactory, SocketLike } from "../transport.js"; +import { SdkError } from "../errors.js"; + +type WsConstructor = new (url: string, options: { headers?: Record }) => SocketLike; + +let WsClass: WsConstructor | undefined; + +/** + * Socket factory that uses the `ws` package for header support on WebSocket + * upgrade requests. Required for HMAC-authenticated WebSocket connections. + * + * `ws` is loaded lazily on first socket creation via `initServerWebSocket()`. + * If not yet loaded, throws a clear error with instructions. + */ +export const serverSocketFactory: SocketFactory = (url, options) => { + if (!WsClass) { + throw new SdkError( + "The ws package is required for authenticated WebSocket connections. " + + "Install it (npm install ws) and call await initServerWebSocket() before connecting, " + + "or use createClient() which handles this automatically.", + ); + } + return new WsClass(url, { headers: options.headers }) as SocketLike; +}; + +/** + * Load the `ws` package. Called automatically by `createClient` when auth + * is provided, or call manually before the first authenticated WebSocket. + */ +export async function initServerWebSocket(): Promise { + if (WsClass) return; + try { + // Dynamic import: ws is an optional peer dependency that only exists on + // server runtimes. A static import would break browser and edge bundling. + const mod = await (import("ws") as Promise<{ default: WsConstructor }>); + WsClass = mod.default; + } catch { + throw new SdkError( + "The ws package is required for server WebSocket connections with custom headers. " + + "Install it: npm install ws", + ); + } +} diff --git a/packages/sdk-typescript/src/tests/browser-oauth.test.ts b/packages/sdk-typescript/src/tests/browser-oauth.test.ts new file mode 100644 index 0000000..a8e8f4c --- /dev/null +++ b/packages/sdk-typescript/src/tests/browser-oauth.test.ts @@ -0,0 +1,199 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { + BrowserOAuthAuth, + type BrowserOAuthAuthOptions, + type BrowserOAuthClient, + type OAuthTokenStore, + type OAuthTokens, +} from "../browser/index.js"; + +// --- Helpers (mirrors oauth-auth.test.ts patterns) --- + +class MemoryTokenStore implements OAuthTokenStore { + record?: OAuthTokens; + #tail: Promise = Promise.resolve(); + + constructor(tokens?: OAuthTokens) { + if (tokens) this.record = tokens; + } + async load() { return this.record; } + async save(tokens: OAuthTokens) { this.record = tokens; } + async clear() { this.record = undefined; } + async runExclusive(operation: () => Promise): Promise { + const previous = this.#tail; + let release: () => void = () => undefined; + this.#tail = new Promise((resolve) => { release = resolve; }); + await previous; + try { return await operation(); } + finally { release(); } + } +} + +const validTokens = (overrides: Partial = {}): OAuthTokens => ({ + accessToken: "browser-access-1", + refreshToken: "browser-refresh-1", + tokenType: "bearer", + scope: "orders:create", + expiresAt: 1_800_000_000_000, + ...overrides, +}); + +function jsonResponse(status: number, body: unknown) { + return { status, text: async () => JSON.stringify(body) }; +} + +const publicClient: BrowserOAuthClient = { + type: "public", + clientId: "browser-test-client", + redirectUri: "http://127.0.0.1:51234/callback", +}; + +function browserOptions( + store: OAuthTokenStore, + extra: Record = {}, +): BrowserOAuthAuthOptions { + return { + client: publicClient, + tokenStore: store, + now: () => 1_700_000_000_000, + randomBytes: (size: number) => new Uint8Array(size).fill(7), + ...extra, + }; +} + +// --- Tests --- + +// BrowserOAuthAuth restricts to public clients at the TYPE level. +// Runtime enforcement is not added — the type system is the guard. +// The negative type test in verify-package.mjs verifies this statically. +// See also: item 7 decision — "doesn't exist" is sufficient for JS bypass. + +test("BrowserOAuthAuth constructs with a public client", () => { + const store = new MemoryTokenStore(); + const auth = new BrowserOAuthAuth(browserOptions(store)); + assert(auth instanceof BrowserOAuthAuth); +}); + +test("BrowserOAuthAuth generates PKCE S256 authorization URL", async () => { + const store = new MemoryTokenStore(); + const auth = new BrowserOAuthAuth(browserOptions(store)); + const { url, transaction } = await auth.beginAuthorization(["orders:create", "orders:read"]); + const parsed = new URL(url); + + assert.equal(parsed.searchParams.get("response_type"), "code"); + assert.equal(parsed.searchParams.get("scope"), "orders:create,orders:read"); + assert.equal(parsed.searchParams.get("code_challenge_method"), "S256"); + assert(parsed.searchParams.has("code_challenge"), "missing code_challenge"); + assert(transaction.codeVerifier, "missing PKCE verifier"); + assert(transaction.state, "missing state"); +}); + +test("BrowserOAuthAuth exchanges code with PKCE verifier (no client secret)", async () => { + const store = new MemoryTokenStore(); + let capturedBody: Record | undefined; + const auth = new BrowserOAuthAuth(browserOptions(store, { + fetchImpl: async (_url: string, init: { body?: string }) => { + capturedBody = JSON.parse(init.body ?? "{}"); + return jsonResponse(200, { + access_token: "browser-access-2", + refresh_token: "browser-refresh-2", + token_type: "bearer", + scope: "orders:create,orders:read", + expires_in: 3600, + }); + }, + })); + + const { transaction } = await auth.beginAuthorization(["orders:create", "orders:read"]); + const callback = new URL(publicClient.redirectUri); + callback.searchParams.set("code", "auth-code-123"); + callback.searchParams.set("state", transaction.state); + const tokens = await auth.completeAuthorization(callback, transaction); + + assert(capturedBody, "fetch was not called"); + assert.equal(capturedBody.grant_type, "authorization_code"); + assert.equal(capturedBody.code, "auth-code-123"); + assert.equal(capturedBody.code_verifier, transaction.codeVerifier); + assert.equal("client_secret" in capturedBody, false, "must not send client_secret"); + assert.equal(tokens.accessToken, "browser-access-2"); + assert.equal(tokens.scope, "orders:create,orders:read"); +}); + +test("BrowserOAuthAuth supplies Bearer header for REST requests", async () => { + const store = new MemoryTokenStore(validTokens()); + const auth = new BrowserOAuthAuth(browserOptions(store)); + const headers = await auth.credentialHeaders(""); + + assert.equal(headers.Authorization, "Bearer browser-access-1"); + assert.equal(headers["X-GEMINI-APIKEY"], undefined, "must not include HMAC key"); + assert.equal(headers["X-GEMINI-SIGNATURE"], undefined, "must not include HMAC signature"); +}); + +test("BrowserOAuthAuth nextNonce returns undefined (no HMAC nonce)", () => { + const store = new MemoryTokenStore(validTokens()); + const auth = new BrowserOAuthAuth(browserOptions(store)); + assert.equal(auth.nextNonce(), undefined); +}); + +test("BrowserOAuthAuth refreshes expired token transparently", async () => { + const store = new MemoryTokenStore(validTokens({ expiresAt: 1_700_000_000_000 })); + let refreshed = false; + const auth = new BrowserOAuthAuth(browserOptions(store, { + fetchImpl: async (_url: string, init: { body?: string }) => { + const body = JSON.parse(init.body ?? "{}"); + assert.equal(body.grant_type, "refresh_token"); + assert.equal(body.refresh_token, "browser-refresh-1"); + assert.equal("client_secret" in body, false); + refreshed = true; + return jsonResponse(200, { + access_token: "browser-access-refreshed", + refresh_token: "browser-refresh-refreshed", + token_type: "bearer", + scope: "orders:create", + expires_in: 3600, + }); + }, + })); + + const headers = await auth.credentialHeaders(""); + assert(refreshed, "refresh was not triggered"); + assert.equal(headers.Authorization, "Bearer browser-access-refreshed"); + assert.equal(store.record?.refreshToken, "browser-refresh-refreshed"); +}); + +test("OAuth scope is correctly encoded in authorization URL", async () => { + const store = new MemoryTokenStore(); + const auth = new BrowserOAuthAuth(browserOptions(store)); + + // Single scope + const single = await auth.beginAuthorization(["auditor"]); + assert.equal(new URL(single.url).searchParams.get("scope"), "auditor"); + + // Multiple scopes + const multi = await auth.beginAuthorization(["orders:create", "orders:read", "auditor"]); + assert.equal(new URL(multi.url).searchParams.get("scope"), "orders:create,orders:read,auditor"); +}); + +test("OAuth scope from token exchange is stored in the token record", async () => { + const store = new MemoryTokenStore(); + const auth = new BrowserOAuthAuth(browserOptions(store, { + fetchImpl: async () => jsonResponse(200, { + access_token: "a", + refresh_token: "r", + token_type: "bearer", + scope: "orders:create,auditor", + expires_in: 3600, + }), + })); + + const { transaction } = await auth.beginAuthorization(["orders:create", "auditor"]); + const callback = new URL(publicClient.redirectUri); + callback.searchParams.set("code", "c"); + callback.searchParams.set("state", transaction.state); + const tokens = await auth.completeAuthorization(callback, transaction); + + assert.equal(tokens.scope, "orders:create,auditor"); + assert.equal(store.record?.scope, "orders:create,auditor"); +}); diff --git a/packages/sdk-typescript/src/tests/core-http.test.ts b/packages/sdk-typescript/src/tests/core-http.test.ts new file mode 100644 index 0000000..e903bb6 --- /dev/null +++ b/packages/sdk-typescript/src/tests/core-http.test.ts @@ -0,0 +1,1156 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { type AuthStrategy, type FetchLike, HttpTransport } from "../core/http.js"; +import { + AcceptTermsRequired, + ApiError, + EndpointMismatch, + InsufficientFunds, + InvalidNonce, + InvalidRequest, + InvalidSignature, + MissingNonce, + MissingRole, + NotFoundError, + RateLimitError, + SdkError, + ServiceUnavailable, + RequestAbortedError, + serializeError, +} from "../errors.js"; +import { parseLosslessJson } from "../json.js"; +import { fromBase64 } from "../core/encoding.js"; + +// A stub auth strategy: fixed nonce, credential headers that echo the signed +// base64 so tests can prove the exact payload string reached the signer. +const stubAuth: AuthStrategy = { + nextNonce: () => "1700000000000", + credentialHeaders: async (payloadBase64: string) => ({ + "X-GEMINI-APIKEY": "test-key", + "X-GEMINI-SIGNATURE": `sig(${payloadBase64})`, + }), +}; + +// A fake transport that records the last request and returns a canned response. +function recordingFetch( + response: { status: number; body: string }, +): { fetchImpl: FetchLike; last: () => { url: string; init: Parameters[1] } } { + let captured: { url: string; init: Parameters[1] } | undefined; + const fetchImpl: FetchLike = async (url, init) => { + captured = { url, init }; + return { status: response.status, text: async () => response.body }; + }; + return { + fetchImpl, + last: () => { + if (!captured) throw new Error("fetch was never called"); + return captured; + }, + }; +} + +// A fake transport that returns a queued sequence of responses, one per call, +// and counts calls. The last entry repeats once the queue is drained. +function sequenceFetch( + responses: Array<{ status: number; body: string }>, +): { fetchImpl: FetchLike; calls: () => number } { + let n = 0; + const fetchImpl: FetchLike = async () => { + const r = responses[Math.min(n, responses.length - 1)]; + n++; + return { status: r.status, text: async () => r.body }; + }; + return { fetchImpl, calls: () => n }; +} + +test("private request shapes the Gemini payload envelope", async () => { + const { fetchImpl, last } = recordingFetch({ status: 200, body: '{"result":"ok"}' }); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + await client.request({ + method: "POST", + path: "/v1/prediction-markets/order", + params: { symbol: "BTCUSD", amount: "1.5" }, + }); + + const { url, init } = last(); + assert.equal(url, "https://api.sandbox.gemini.com/v1/prediction-markets/order"); + assert.equal(init.method, "POST"); + + // Fixed private-REST headers. + assert.equal(init.headers["Content-Length"], "0"); + assert.equal(init.headers["Content-Type"], "text/plain"); + assert.equal(init.headers["Cache-Control"], "no-cache"); + assert.equal(init.body, undefined, "private REST parameters belong only in the signed payload"); + + // The payload is base64(JSON) with request + nonce + params. + const b64 = init.headers["X-GEMINI-PAYLOAD"]; + const payload = JSON.parse(fromBase64(b64)); + assert.deepEqual(payload, { + request: "/v1/prediction-markets/order", + nonce: 1700000000000, + symbol: "BTCUSD", + amount: "1.5", + }); + + // Credential headers from the auth strategy are merged, signing that exact b64. + assert.equal(init.headers["X-GEMINI-APIKEY"], "test-key"); + assert.equal(init.headers["X-GEMINI-SIGNATURE"], `sig(${b64})`); +}); + +test("safe reads retry transient responses but mutations do not", async () => { + const reads = sequenceFetch([{ status: 503, body: "{}" }, { status: 200, body: "{}" }]); + const client = new HttpTransport({ env: "sandbox", fetchImpl: reads.fetchImpl, maxRetries: 1, sleep: async () => {} }); + await client.requestPublic({ method: "GET", path: "/v1/symbols", retryable: true }); + assert.equal(reads.calls(), 2); + + const mutations = sequenceFetch([{ status: 503, body: "{}" }, { status: 200, body: "{}" }]); + const mutationClient = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl: mutations.fetchImpl, maxRetries: 1, sleep: async () => {} }); + await assert.rejects(() => mutationClient.request({ method: "POST", path: "/v1/order/new", retryable: false }), ServiceUnavailable); + assert.equal(mutations.calls(), 1); +}); + +test("Retry-After overrides client jitter for a safe read", async () => { + let calls = 0; + let slept = 0; + const client = new HttpTransport({ + env: "sandbox", maxRetries: 1, sleep: async (ms) => { slept = ms; }, + fetchImpl: async () => (++calls === 1 + ? { status: 429, headers: { get: () => "2" }, text: async () => "{}" } + : { status: 200, text: async () => "{}" }), + }); + await client.requestPublic({ method: "GET", path: "/v1/symbols", retryable: true }); + assert.equal(slept, 2000); +}); + +test("aborting a request rejects promptly and passes the signal to fetch", async () => { + const controller = new AbortController(); + let received: AbortSignal | undefined; + const client = new HttpTransport({ env: "sandbox", fetchImpl: async (_url, init) => { + received = init.signal; + return new Promise(() => {}); + } }); + const pending = client.requestPublic({ method: "GET", path: "/v1/symbols", signal: controller.signal }); + await Promise.resolve(); + controller.abort(); + await assert.rejects(pending, RequestAbortedError); + assert.equal(received?.aborted, true); +}); + +test("auth headers cannot override the transport envelope using different casing", async () => { + const auth: AuthStrategy = { + nextNonce: () => "1700000000000", + credentialHeaders: async () => ({ "x-gemini-payload": "evil" }), + }; + const { fetchImpl, last } = recordingFetch({ status: 200, body: "{}" }); + const client = new HttpTransport({ env: "sandbox", auth, fetchImpl }); + + await assert.rejects( + client.request({ method: "POST", path: "/v1/x" }), + /reserved header.*x-gemini-payload/i, + ); + assert.throws(() => last(), /fetch was never called/); +}); + +test("a params key clobbering `request` throws EndpointMismatch before any send", async () => { + const { fetchImpl, last } = recordingFetch({ status: 200, body: "{}" }); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + await assert.rejects( + client.request({ + method: "POST", + path: "/v1/prediction-markets/order", + params: { request: "/v1/prediction-markets/cancel" }, + }), + (err: unknown) => err instanceof EndpointMismatch, + ); + + // The guard fires before the network call. + assert.throws(() => last(), /fetch was never called/); +}); + +test("a bigint request mismatch still throws EndpointMismatch, never a formatter TypeError", async () => { + const { fetchImpl } = recordingFetch({ status: 200, body: "{}" }); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + await assert.rejects( + client.request({ + method: "POST", + path: "/v1/prediction-markets/order", + params: { request: 1n }, + }), + (err: unknown) => err instanceof EndpointMismatch, + ); +}); + +test("caller params cannot override the credential-scoped nonce", async () => { + const { fetchImpl, last } = recordingFetch({ status: 200, body: "{}" }); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + await assert.rejects( + client.request({ method: "POST", path: "/v1/x", params: { nonce: "stale" } }), + /nonce.*reserved/i, + ); + assert.throws(() => last(), /fetch was never called/); +}); + +test("a private request with no AuthStrategy fails loud", async () => { + const { fetchImpl } = recordingFetch({ status: 200, body: "{}" }); + const client = new HttpTransport({ env: "sandbox", fetchImpl }); + + await assert.rejects( + client.request({ method: "POST", path: "/v1/prediction-markets/order" }), + /auth/i, + ); +}); + +test("public request needs no auth, sends no payload, and builds a query string", async () => { + const { fetchImpl, last } = recordingFetch({ status: 200, body: "[]" }); + let nonceCalls = 0; + let authCalls = 0; + const auth: AuthStrategy = { + nextNonce: () => { + nonceCalls++; + return "1700000000000"; + }, + credentialHeaders: async () => { + authCalls++; + return { "X-GEMINI-APIKEY": "must-not-leak" }; + }, + }; + const client = new HttpTransport({ env: "production", auth, fetchImpl }); + + await client.requestPublic({ + method: "GET", + path: "/v1/prediction-markets/events", + query: { limit: 10, category: "sports", status: ["active", "resolved"] }, + }); + + const { url, init } = last(); + assert.equal( + url, + "https://api.gemini.com/v1/prediction-markets/events?limit=10&category=sports&status=active&status=resolved", + ); + assert.equal(init.method, "GET"); + assert.equal("X-GEMINI-PAYLOAD" in init.headers, false); + assert.equal("X-GEMINI-APIKEY" in init.headers, false); + assert.equal(nonceCalls, 0, "public calls never advance private nonce state"); + assert.equal(authCalls, 0, "public calls never invoke the configured auth strategy"); +}); + +test("file response mode returns bytes and content metadata without parsing success text", async () => { + let textCalls = 0; + const fetchImpl: FetchLike = async () => ({ + status: 200, + headers: { + get: (name) => ({ + "content-type": "text/csv", + "content-disposition": "attachment; filename=FundingAmount_BTCGUSDPERP.csv", + })[name.toLowerCase()] ?? null, + }, + async arrayBuffer() { + return new Uint8Array([97, 44, 98]).buffer; + }, + async text() { + textCalls++; + return "not json"; + }, + }); + const client = new HttpTransport({ env: "production", fetchImpl }); + + const result = await client.requestPublic({ + method: "GET", + path: "/v1/fundingamountreport/records.xlsx", + responseMode: "file", + }); + + assert.deepEqual(result, { + bytes: new Uint8Array([97, 44, 98]), + contentType: "text/csv", + contentDisposition: "attachment; filename=FundingAmount_BTCGUSDPERP.csv", + }); + assert.equal(textCalls, 0); +}); + +test("file response mode preserves empty bodies and typed error mapping", async () => { + const emptyFetch: FetchLike = async () => ({ + status: 200, + async arrayBuffer() { return new ArrayBuffer(0); }, + async text() { throw new Error("file success should not read text"); }, + }); + const empty = await new HttpTransport({ env: "production", fetchImpl: emptyFetch }).requestPublic({ + method: "GET", + path: "/v1/report.xlsx", + responseMode: "file", + }); + assert.deepEqual(empty, { bytes: new Uint8Array(0), contentType: undefined, contentDisposition: undefined }); + + const errorFetch: FetchLike = async () => ({ + status: 400, + async arrayBuffer() { throw new Error("error response should read text"); }, + async text() { return '{"reason":"InvalidNonce","message":"bad nonce"}'; }, + }); + await assert.rejects( + new HttpTransport({ env: "production", fetchImpl: errorFetch }).requestPublic({ + method: "GET", + path: "/v1/report.xlsx", + responseMode: "file", + }), + (error: unknown) => error instanceof InvalidNonce, + ); +}); + +test("an auth strategy can omit nonce for OAuth payloads", async () => { + const oauthAuth: AuthStrategy = { + nextNonce: () => undefined, + credentialHeaders: async () => ({ Authorization: "Bearer test-token" }), + }; + const { fetchImpl, last } = recordingFetch({ status: 200, body: "{}" }); + const client = new HttpTransport({ env: "sandbox", auth: oauthAuth, fetchImpl }); + + await client.request({ method: "POST", path: "/v1/x", params: { symbol: "BTCUSD" } }); + + const { init } = last(); + const payload = JSON.parse( + fromBase64(init.headers["X-GEMINI-PAYLOAD"]), + ); + assert.deepEqual(payload, { request: "/v1/x", symbol: "BTCUSD" }); + assert.equal(init.headers.Authorization, "Bearer test-token"); +}); + +test("invalid auth nonces fail as SdkError before fetch", async () => { + const { fetchImpl, last } = recordingFetch({ status: 200, body: "{}" }); + + for (const nonce of ["not-a-number", "01", "1e3"]) { + const auth: AuthStrategy = { + nextNonce: () => nonce, + credentialHeaders: async () => ({}), + }; + const client = new HttpTransport({ env: "sandbox", auth, fetchImpl }); + + await assert.rejects( + client.request({ method: "POST", path: "/v1/x" }), + (error: unknown) => error instanceof SdkError, + ); + } + assert.throws(() => last(), /fetch was never called/); +}); + +test("private request preserves an oversized nonce as an exact JSON number", async () => { + const auth: AuthStrategy = { + nextNonce: () => "9007199254740993", + credentialHeaders: async () => ({}), + }; + const { fetchImpl, last } = recordingFetch({ status: 200, body: "{}" }); + const client = new HttpTransport({ env: "sandbox", auth, fetchImpl }); + + await client.request({ method: "POST", path: "/v1/x" }); + + const json = fromBase64( + last().init.headers["X-GEMINI-PAYLOAD"], + ); + assert.equal(json, '{"request":"/v1/x","nonce":9007199254740993}'); +}); + +test("private request accepts a documented fractional nonce", async () => { + const auth: AuthStrategy = { + nextNonce: () => "1700000000.5", + credentialHeaders: async () => ({}), + }; + const { fetchImpl, last } = recordingFetch({ status: 200, body: "{}" }); + const client = new HttpTransport({ env: "sandbox", auth, fetchImpl }); + + await client.request({ method: "GET", path: "/v1/x" }); + + const json = fromBase64( + last().init.headers["X-GEMINI-PAYLOAD"], + ); + assert.equal(json, '{"request":"/v1/x","nonce":1700000000.5}'); +}); + +test("private request normalizes only schema-declared int64 response fields", async () => { + const body = '{"orderId":42,"count":3,"ratio":0.5,"amount":"100.00000001"}'; + const { fetchImpl } = recordingFetch({ status: 200, body }); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + const res = (await client.request({ + method: "POST", + path: "/v1/x", + responseInt64Paths: [["orderId"]], + })) as { + orderId: bigint; + count: number; + ratio: number; + amount: string; + }; + + assert.equal(res.orderId, 42n); + assert.equal(res.count, 3); + assert.equal(res.ratio, 0.5); + assert.equal(res.amount, "100.00000001"); +}); + +test("public request normalizes schema-declared int64 fields in nested arrays", async () => { + const { fetchImpl } = recordingFetch({ + status: 200, + body: '{"items":[{"instrumentId":7},{"instrumentId":8}]}', + }); + const client = new HttpTransport({ env: "sandbox", fetchImpl }); + + const res = (await client.requestPublic({ + method: "GET", + path: "/v1/instruments", + responseInt64Paths: [["items", "*", "instrumentId"]], + })) as { items: Array<{ instrumentId: bigint }> }; + + assert.deepEqual(res.items.map(({ instrumentId }) => instrumentId), [7n, 8n]); +}); + +test("a response bigint can be sent back as an exact JSON integer request parameter", async () => { + const orderId = 12345678901234567890n; + const { fetchImpl, last } = recordingFetch({ status: 200, body: "{}" }); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + await client.request({ method: "POST", path: "/v1/order/cancel", params: { orderId } }); + + const encoded = fromBase64(last().init.headers["X-GEMINI-PAYLOAD"]); + assert.match(encoded, /"orderId":12345678901234567890/); // numeric token, not a quoted string + assert.equal((parseLosslessJson(encoded) as { orderId: bigint }).orderId, orderId); +}); + +// The error table, driven through BOTH envelopes the repo emits: +// gateway: { result: "error", reason, message } +// PM svc: { error, message } (inconsistent code casing) +const ERROR_CASES: Array<{ + name: string; + status: number; + body: string; + is: new (...args: never[]) => SdkError; + reason?: string; +}> = [ + { + name: "400 + reason InvalidNonce (gateway envelope)", + status: 400, + body: '{"result":"error","reason":"InvalidNonce","message":"bad nonce"}', + is: InvalidNonce, + reason: "InvalidNonce", + }, + { + name: "403 + error MissingRole (PM envelope)", + status: 403, + body: '{"error":"MissingRole","message":"no role"}', + is: MissingRole, + reason: "MissingRole", + }, + { + name: "403 + AcceptTermsRequired", + status: 403, + body: '{"result":"error","reason":"AcceptTermsRequired"}', + is: AcceptTermsRequired, + reason: "AcceptTermsRequired", + }, + { + name: "403 + TERMS_NOT_ACCEPTED PM envelope", + status: 403, + body: '{"error":"TERMS_NOT_ACCEPTED","message":"Prediction markets terms must be accepted before placing orders"}', + is: AcceptTermsRequired, + reason: "TERMS_NOT_ACCEPTED", + }, + { + name: "403 + terms sentence in the error field", + status: 403, + body: '{"error":"Prediction markets terms must be accepted before placing orders"}', + is: AcceptTermsRequired, + reason: "Prediction markets terms must be accepted before placing orders", + }, + { + name: "400 + MissingNonce", + status: 400, + body: '{"result":"error","reason":"MissingNonce"}', + is: MissingNonce, + reason: "MissingNonce", + }, + { + name: "400 + InvalidSignature", + status: 400, + body: '{"result":"error","reason":"InvalidSignature"}', + is: InvalidSignature, + reason: "InvalidSignature", + }, + { + name: "406 + InsufficientFunds", + status: 406, + body: '{"result":"error","reason":"InsufficientFunds"}', + is: InsufficientFunds, + reason: "InsufficientFunds", + }, + { + name: "429 + reason RateLimit maps to RateLimitError", + status: 429, + body: '{"result":"error","reason":"RateLimit"}', + is: RateLimitError, + reason: "RateLimit", + }, + { + name: "400 unknown reason falls back to status default InvalidRequest", + status: 400, + body: '{"error":"UNKNOWN_BAD_REQUEST"}', + is: InvalidRequest, + }, + { + name: "403 unknown reason falls back to status default MissingRole", + status: 403, + body: '{"error":"UNKNOWN_FORBIDDEN"}', + is: MissingRole, + }, + { + name: "404 unknown reason falls back to status default NotFoundError", + status: 404, + body: '{"error":"NOT_FOUND","message":"missing"}', + is: NotFoundError, + }, + { + name: "500 unknown reason falls back to ServiceUnavailable", + status: 500, + body: '{"error":"An unexpected error occurred"}', + is: ServiceUnavailable, + }, + { + name: "402 unmapped status falls back to generic ApiError", + status: 402, + body: '{"error":"Whatever"}', + is: ApiError, + }, +]; + +test("a 429 then 200 retries after a jittered backoff and returns the body", async () => { + const { fetchImpl, calls } = sequenceFetch([ + { status: 429, body: "" }, + { status: 200, body: '{"ok":true}' }, + ]); + const sleeps: number[] = []; + const client = new HttpTransport({ + env: "sandbox", + auth: stubAuth, + fetchImpl, + backoff: { baseMs: 500, factor: 2 }, + random: () => 0, // jitter floor -> delay is exactly raw/2, deterministic + sleep: async (ms) => { + sleeps.push(ms); + }, + }); + + const res = (await client.request({ method: "GET", path: "/v1/x", retryable: true })) as { ok: boolean }; + + assert.equal(res.ok, true); + assert.equal(calls(), 2); // one retry + assert.deepEqual(sleeps, [250]); // backoffDelay(0) = 500/2 +}); + +test("constructor rejects retry counts that cannot provide a finite bound", () => { + for (const maxRetries of [Number.NaN, Number.POSITIVE_INFINITY, -1, 1.5]) { + assert.throws( + () => new HttpTransport({ env: "sandbox", maxRetries }), + (err: unknown) => err instanceof SdkError && /maxRetries.*non-negative integer/i.test(err.message), + ); + } +}); + +test("maxRetries zero surfaces the first 429 without sleeping", async () => { + const { fetchImpl, calls } = sequenceFetch([{ status: 429, body: "" }]); + let sleeps = 0; + const client = new HttpTransport({ + env: "sandbox", + auth: stubAuth, + fetchImpl, + maxRetries: 0, + sleep: async () => { + sleeps++; + }, + }); + + await assert.rejects( + client.request({ method: "GET", path: "/v1/x", retryable: true }), + (err: unknown) => err instanceof RateLimitError, + ); + assert.equal(calls(), 1); + assert.equal(sleeps, 0); +}); + +test("exhausting 429 retries throws RateLimitError after the bounded backoffs", async () => { + const { fetchImpl, calls } = sequenceFetch([{ status: 429, body: "" }]); + const sleeps: number[] = []; + const client = new HttpTransport({ + env: "sandbox", + auth: stubAuth, + fetchImpl, + maxRetries: 2, + backoff: { baseMs: 500, factor: 2 }, + random: () => 0, + sleep: async (ms) => { + sleeps.push(ms); + }, + }); + + await assert.rejects( + client.request({ method: "GET", path: "/v1/x", retryable: true }), + (err: unknown) => err instanceof RateLimitError, + ); + + assert.equal(calls(), 3); // initial + 2 retries + assert.deepEqual(sleeps, [250, 500]); // backoffDelay(0), backoffDelay(1) +}); + +test("429 backoff applies non-floor jitter and caps later attempts", async () => { + const { fetchImpl } = sequenceFetch([ + { status: 429, body: "" }, + { status: 429, body: "" }, + { status: 429, body: "" }, + { status: 200, body: "{}" }, + ]); + const sleeps: number[] = []; + const client = new HttpTransport({ + env: "sandbox", + auth: stubAuth, + fetchImpl, + maxRetries: 3, + backoff: { baseMs: 100, factor: 10, capMs: 1_000 }, + random: () => 0.5, + sleep: async (ms) => { + sleeps.push(ms); + }, + }); + + await client.request({ method: "GET", path: "/v1/x", retryable: true }); + + assert.deepEqual(sleeps, [75, 750, 750]); // 75% equal jitter; raw delay capped at 1,000ms +}); + +// A fake that records the offset/limit of each page request and returns a +// queued page body per call. +function paginatingFetch(pages: string[]): { + fetchImpl: FetchLike; + offsets: number[]; + limits: number[]; +} { + const offsets: number[] = []; + const limits: number[] = []; + let n = 0; + const fetchImpl: FetchLike = async (_url, init) => { + const payload = JSON.parse( + fromBase64(init.headers["X-GEMINI-PAYLOAD"]), + ); + offsets.push(payload.offset); + limits.push(payload.limit); + const body = pages[Math.min(n, pages.length - 1)]; + n++; + return { status: 200, text: async () => body }; + }; + return { fetchImpl, offsets, limits }; +} + +test("paginate walks offsets and stops on a short page", async () => { + const { fetchImpl, offsets } = paginatingFetch([ + '[{"id":1},{"id":2},{"id":3}]', // full page (limit 3) -> keep going + '[{"id":4},{"id":5}]', // short page (< 3) -> stop + ]); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + const ids: bigint[] = []; + for await (const item of client.paginate({ + method: "POST", + path: "/v1/list", + limit: 3, + responseInt64Paths: [["*", "id"]], + })) { + ids.push((item as { id: bigint }).id); + } + + assert.deepEqual(ids, [1n, 2n, 3n, 4n, 5n]); + assert.deepEqual(offsets, [0, 3]); // second page requested at offset = limit +}); + +test("paginate errors omit caller query values", async () => { + const { fetchImpl } = paginatingFetch(["{}"]); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + await assert.rejects(async () => { + for await (const _item of client.paginate({ method: "POST", path: "/v1/list?account=private-account" })) { /* consume */ } + }, (err: unknown) => { + assert.ok(err instanceof SdkError); + assert.equal(err.message.includes("private-account"), false); + assert.equal(err.message.includes("/v1/list"), true); + return true; + }); +}); + +test("paginate stops at maxItems and bounds the final page", async () => { + const { fetchImpl, limits } = paginatingFetch([ + '[{"id":1},{"id":2}]', + '[{"id":3},{"id":4}]', + ]); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + const items: unknown[] = []; + for await (const item of client.paginate({ method: "GET", path: "/v1/items", limit: 2, maxItems: 3, retryable: true })) items.push(item); + assert.deepEqual(items, [{ id: 1 }, { id: 2 }, { id: 3 }]); + assert.deepEqual(limits, [2, 1]); +}); + +test("paginate can fail loudly when offset drift repeats a logical record", async () => { + const { fetchImpl } = paginatingFetch([ + '[{"id":1},{"id":2}]', + '[{"id":2},{"id":3}]', + ]); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + const pending = (async () => { + for await (const _item of client.paginate({ + method: "POST", + path: "/v1/orders", + limit: 2, + dedupeKey: (item) => String((item as { id: number }).id), + })) { /* consume until the duplicate is detected */ } + })(); + + await assert.rejects(pending, /duplicate item key 2/); +}); + +test("paginate unwraps a documented response envelope using its explicit item key", async () => { + const { fetchImpl, offsets } = paginatingFetch([ + '{"orders":[{"id":1},{"id":2}],"pagination":{"limit":2,"offset":0,"total":3}}', + '{"orders":[{"id":3}],"pagination":{"limit":2,"offset":2,"total":3}}', + ]); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + const ids: number[] = []; + for await (const item of client.paginate({ + method: "POST", + path: "/v1/orders/active", + limit: 2, + itemsKey: "orders", + })) { + ids.push((item as { id: number }).id); + } + + assert.deepEqual(ids, [1, 2, 3]); + assert.deepEqual(offsets, [0, 2]); +}); + +test("paginate sends public offsets as query parameters without requiring auth", async () => { + const urls: string[] = []; + const pages = [ + '{"data":[{"id":1},{"id":2}],"pagination":{"limit":2,"offset":0,"total":3}}', + '{"data":[{"id":3}],"pagination":{"limit":2,"offset":2,"total":3}}', + ]; + const fetchImpl: FetchLike = async (url) => { + urls.push(url); + const body = pages[Math.min(urls.length - 1, pages.length - 1)]; + return { status: 200, text: async () => body }; + }; + const client = new HttpTransport({ env: "sandbox", fetchImpl }); + + const ids: number[] = []; + for await (const item of client.paginate({ + method: "GET", + path: "/v1/prediction-markets/events", + params: { status: ["active", "resolved"] }, + limit: 2, + itemsKey: "data", + visibility: "public", + })) { + ids.push((item as { id: number }).id); + } + + assert.deepEqual(ids, [1, 2, 3]); + assert.deepEqual(urls, [ + "https://api.sandbox.gemini.com/v1/prediction-markets/events?status=active&status=resolved&limit=2&offset=0", + "https://api.sandbox.gemini.com/v1/prediction-markets/events?status=active&status=resolved&limit=2&offset=2", + ]); +}); + +test("paginate can send private offsets in the URL while signing the base path", async () => { + const urls: string[] = []; + const payloads: Array> = []; + const pages = [ + JSON.stringify({ payouts: Array.from({ length: 100 }, (_, index) => ({ id: index + 1 })) }), + '{"payouts":[{"id":101}]}', + ]; + const fetchImpl: FetchLike = async (url, init) => { + urls.push(url); + payloads.push( + JSON.parse(fromBase64(init.headers["X-GEMINI-PAYLOAD"])), + ); + const body = pages[Math.min(urls.length - 1, pages.length - 1)]; + return { status: 200, text: async () => body }; + }; + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + const ids: number[] = []; + for await (const item of client.paginate({ + method: "POST", + path: "/v1/prediction-markets/maker-rebate/payouts", + limit: 500, + maxLimit: 100, + itemsKey: "payouts", + parameterLocation: "query", + })) { + ids.push((item as { id: number }).id); + } + + assert.equal(ids.length, 101); + assert.equal(ids.at(-1), 101); + assert.deepEqual(urls, [ + "https://api.sandbox.gemini.com/v1/prediction-markets/maker-rebate/payouts?limit=100&offset=0", + "https://api.sandbox.gemini.com/v1/prediction-markets/maker-rebate/payouts?limit=100&offset=100", + ]); + assert.deepEqual(payloads, [ + { + request: "/v1/prediction-markets/maker-rebate/payouts", + nonce: 1700000000000, + }, + { + request: "/v1/prediction-markets/maker-rebate/payouts", + nonce: 1700000000000, + }, + ]); +}); + +test("private query pagination rejects a caller-owned nonce before fetch", async () => { + const { fetchImpl, last } = recordingFetch({ status: 200, body: "[]" }); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + await assert.rejects( + async () => { + for await (const _ of client.paginate({ + method: "POST", + path: "/v1/prediction-markets/maker-rebate/payouts", + params: { nonce: "stale" }, + parameterLocation: "query", + })) { + // The nonce guard must fail before the first item or network request. + } + }, + /nonce.*reserved/i, + ); + assert.throws(() => last(), /fetch was never called/); +}); + +test("paginate rejects non-finite, fractional, and non-positive limits before fetch", async () => { + const invalidOptions = [ + { limit: Number.NaN }, + { limit: Number.POSITIVE_INFINITY }, + { limit: 1.5 }, + { limit: 0 }, + { maxLimit: Number.NaN }, + { maxLimit: Number.POSITIVE_INFINITY }, + { maxLimit: 1.5 }, + { maxLimit: 0 }, + ]; + + for (const invalid of invalidOptions) { + const fetchImpl: FetchLike = async () => { + throw new Error("fetch should not be called"); + }; + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + await assert.rejects( + async () => { + for await (const _ of client.paginate({ + method: "POST", + path: "/v1/list", + ...invalid, + })) { + // Validation must fail before iteration starts. + } + }, + /limit.*positive integer/i, + ); + } +}); + +test("paginate clamps limit to the documented max of 500", async () => { + const { fetchImpl, limits } = paginatingFetch(["[]"]); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + // eslint-disable-next-line no-empty + for await (const _ of client.paginate({ method: "POST", path: "/v1/list", limit: 1000 })) { + } + + assert.equal(limits[0], 500); +}); + +test("response int64 paths are not applied before typed error mapping", async () => { + const { fetchImpl } = recordingFetch({ + status: 400, + body: '{"reason":"InvalidNonce","orderId":"not-an-int64"}', + }); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + await assert.rejects( + client.request({ + method: "POST", + path: "/v1/x", + responseInt64Paths: [["orderId"]], + }), + (err: unknown) => err instanceof InvalidNonce, + ); +}); + +test("a non-JSON error body still maps by status instead of throwing SyntaxError", async () => { + // A proxy / load balancer 502 returns an HTML page, not JSON. + const { fetchImpl } = recordingFetch({ + status: 502, + body: "Bad Gateway", + }); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl, maxRetries: 0 }); + + await assert.rejects( + client.request({ method: "POST", path: "/v1/x" }), + (err: unknown) => { + assert.ok(err instanceof ServiceUnavailable, `got ${(err as Error).name}`); + assert.equal((err as ServiceUnavailable).status, 502); + assert.equal((err as Error).message.includes("Bad Gateway"), false); + assert.equal("body" in err, false); + assert.equal(JSON.stringify(serializeError(err)).includes("Bad Gateway"), false); + return true; + }, + ); +}); + +test("a fetch failure is wrapped in SdkError with its native cause", async () => { + const cause = new TypeError("DNS lookup failed"); + const fetchImpl: FetchLike = async () => { + throw cause; + }; + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + await assert.rejects( + client.request({ method: "POST", path: "/v1/x" }), + (err: unknown) => { + assert.ok(err instanceof SdkError, `got ${(err as Error).name}`); + assert.equal(err.cause, cause); + assert.match(err.message, /request.*failed/i); + return true; + }, + ); +}); + +test("transport errors keep query values out of their message", async () => { + const client = new HttpTransport({ + env: "sandbox", + auth: stubAuth, + fetchImpl: async () => { throw new Error("network unavailable"); }, + }); + + await assert.rejects(client.request({ method: "POST", path: "/v1/x?account=private-account" }), (err: unknown) => { + assert.ok(err instanceof SdkError); + assert.equal(err.message.includes("private-account"), false); + assert.equal(err.message.includes("/v1/x"), true); + return true; + }); +}); + +test("a response body read failure is wrapped in SdkError with its native cause", async () => { + const cause = new TypeError("response stream aborted"); + const fetchImpl: FetchLike = async () => ({ + status: 200, + text: async () => { + throw cause; + }, + }); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + await assert.rejects( + client.request({ method: "POST", path: "/v1/x" }), + (err: unknown) => { + assert.ok(err instanceof SdkError, `got ${(err as Error).name}`); + assert.equal(err.cause, cause); + assert.match(err.message, /request.*failed/i); + return true; + }, + ); +}); + +test("an existing SdkError from the HTTP transport is rethrown unchanged", async () => { + const expected = new SdkError("transport already classified this failure"); + const fetchImpl: FetchLike = async () => { + throw expected; + }; + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + await assert.rejects( + client.request({ method: "POST", path: "/v1/x" }), + (err: unknown) => err === expected, + ); +}); + +test("a non-JSON success body fails loud as an SdkError, not a raw parse error", async () => { + const { fetchImpl } = recordingFetch({ status: 200, body: "not json at all" }); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + await assert.rejects( + client.request({ method: "POST", path: "/v1/x" }), + (err: unknown) => { + assert.ok(err instanceof SdkError, `got ${(err as Error).name}`); + assert.ok(!(err instanceof ApiError)); // no HTTP semantics — it's a protocol violation + assert.match((err as Error).message, /unparseable/i); + return true; + }, + ); +}); + +test("a retry mints a fresh nonce and re-signs, never reusing the last one", async () => { + let n = 999; + const countingAuth: AuthStrategy = { + nextNonce: () => String(++n), + credentialHeaders: async (b64) => ({ "X-GEMINI-SIGNATURE": `sig(${b64})` }), + }; + const payloads: string[] = []; + const signatures: string[] = []; + let call = 0; + const fetchImpl: FetchLike = async (_url, init) => { + payloads.push(init.headers["X-GEMINI-PAYLOAD"]); + signatures.push(init.headers["X-GEMINI-SIGNATURE"]); + const status = call === 0 ? 429 : 200; + call++; + return { status, text: async () => "{}" }; + }; + const client = new HttpTransport({ + env: "sandbox", + auth: countingAuth, + fetchImpl, + backoff: { baseMs: 1 }, + random: () => 0, + sleep: async () => {}, + }); + + await client.request({ method: "GET", path: "/v1/x", retryable: true }); + + assert.equal(payloads.length, 2); + assert.notEqual(payloads[0], payloads[1]); // different signed bytes + assert.deepEqual(signatures, payloads.map((payload) => `sig(${payload})`)); + const nonces = payloads.map( + (b) => JSON.parse(fromBase64(b)).nonce as number, + ); + assert.deepEqual(nonces, [1000, 1001]); // strictly advanced +}); + +test("a retry snapshots trading params and changes only authentication state", async () => { + const params = { orders: [{ symbol: "BTCUSD", amount: "1" }] }; + const amounts: string[] = []; + let call = 0; + const fetchImpl: FetchLike = async (_url, init) => { + const payload = JSON.parse( + fromBase64(init.headers["X-GEMINI-PAYLOAD"]), + ); + amounts.push(payload.orders[0].amount); + params.orders[0].amount = "999"; // caller mutation must not alter the retry + return { status: call++ === 0 ? 429 : 200, text: async () => "{}" }; + }; + const client = new HttpTransport({ + env: "sandbox", + auth: stubAuth, + fetchImpl, + backoff: { baseMs: 1 }, + random: () => 0, + sleep: async () => {}, + }); + + await client.request({ method: "GET", path: "/v1/batch", params, retryable: true }); + + assert.deepEqual(amounts, ["1", "1"]); +}); + +test("a private retry snapshots caller headers before asynchronous authentication", async () => { + const headers = { "X-Request-Id": "original" }; + let releaseAuth: (() => void) | undefined; + const authReady = new Promise((resolve) => { + releaseAuth = resolve; + }); + const auth: AuthStrategy = { + nextNonce: () => "1700000000000", + credentialHeaders: async () => { + await authReady; + return { "X-GEMINI-APIKEY": "test-key" }; + }, + }; + const sent: string[] = []; + let call = 0; + const client = new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async (_url, init) => { + sent.push(init.headers["X-Request-Id"]); + return { status: call++ === 0 ? 429 : 200, text: async () => "{}" }; + }, + backoff: { baseMs: 1 }, + random: () => 0, + sleep: async () => {}, + }); + + const request = client.request({ method: "GET", path: "/v1/x", headers, retryable: true }); + headers["X-Request-Id"] = "mutated"; + releaseAuth?.(); + await request; + + assert.deepEqual(sent, ["original", "original"]); +}); + +test("a public retry snapshots caller headers", async () => { + const headers = { "X-Request-Id": "original" }; + const sent: string[] = []; + let call = 0; + const client = new HttpTransport({ + env: "sandbox", + fetchImpl: async (_url, init) => { + sent.push(init.headers["X-Request-Id"]); + headers["X-Request-Id"] = "mutated"; + return { status: call++ === 0 ? 429 : 200, text: async () => "{}" }; + }, + backoff: { baseMs: 1 }, + random: () => 0, + sleep: async () => {}, + }); + + await client.requestPublic({ method: "GET", path: "/v1/x", headers, retryable: true }); + + assert.deepEqual(sent, ["original", "original"]); +}); + +test("paginate over an empty first page yields nothing and stops after one fetch", async () => { + const { fetchImpl, offsets } = paginatingFetch(["[]"]); + const client = new HttpTransport({ env: "sandbox", auth: stubAuth, fetchImpl }); + + const items: unknown[] = []; + for await (const item of client.paginate({ method: "POST", path: "/v1/list", limit: 50 })) { + items.push(item); + } + + assert.deepEqual(items, []); + assert.deepEqual(offsets, [0]); // no second fetch +}); + +for (const c of ERROR_CASES) { + test(`error mapping: ${c.name}`, async () => { + const { fetchImpl } = recordingFetch({ status: c.status, body: c.body }); + // No retries in this suite: a 429 should surface immediately for assertion. + const client = new HttpTransport({ + env: "sandbox", + auth: stubAuth, + fetchImpl, + maxRetries: 0, + }); + + await assert.rejects( + client.request({ method: "POST", path: "/v1/x" }), + (err: unknown) => { + assert.ok(err instanceof c.is, `expected ${c.is.name}, got ${(err as Error).name}`); + assert.ok(err instanceof ApiError); + assert.ok(err instanceof SdkError); + assert.equal((err as ApiError).status, c.status); + if (c.reason) assert.equal((err as ApiError).reason, c.reason); + return true; + }, + ); + }); +} diff --git a/packages/sdk-typescript/src/tests/diagnostics.test.ts b/packages/sdk-typescript/src/tests/diagnostics.test.ts new file mode 100644 index 0000000..d34d728 --- /dev/null +++ b/packages/sdk-typescript/src/tests/diagnostics.test.ts @@ -0,0 +1,307 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + ApiError, + classifyServerError, + serializeError, + WebSocketRequestError, +} from "../errors.js"; +import type { DiagnosticEvent } from "../diagnostics.js"; +import { ConsoleLogger, emitDiagnostic, NoopLogger, type Logger } from "../logging.js"; +import { HttpTransport } from "../core/http.js"; + +test("classifies server reasons and preserves the exchange code", () => { + const classification = classifyServerError({ + error: "InvalidInput", + code: "COMBO_VALIDATION_ERROR", + }); + + assert.deepEqual(classification, { + reason: "InvalidInput", + code: "invalid_input", + category: "validation", + serverCode: "COMBO_VALIDATION_ERROR", + }); +}); + +test("classifies server error codes when the response has no reason field", () => { + assert.equal(classifyServerError({ code: "TermsNotFound" }, 404).code, "terms_not_found"); + assert.equal(classifyServerError({ error: "OrderNotFound" }, 404).code, "order_not_found"); + assert.deepEqual(classifyServerError({ error: "ProgramUnavailable" }, 503), { + reason: "ProgramUnavailable", + code: "program_unavailable", + category: "service_unavailable", + }); +}); + +test("authorization classification preserves only explicit role and scope context", () => { + const classification = classifyServerError({ error: "MissingRole", requiredRole: "trader", accountScope: "primary" }); + assert.deepEqual(classification.authorizationContext, { + requiredRole: "trader", + scope: "primary", + }); + assert.deepEqual(serializeError(new ApiError({ status: 403, reason: "MissingRole", authorizationContext: classification.authorizationContext })).authorizationContext, { + requiredRole: "trader", + scope: "primary", + }); + assert.equal("requiredRole" in (classifyServerError({ error: "MissingRole" }).authorizationContext ?? {}), false); +}); + +test("safe error serialization redacts nested sensitive fields and omits the raw body", () => { + const error = new ApiError({ + status: 403, + reason: "MissingRole", + body: { + error: "MissingRole", + bankAccount: { accountNumber: "1234" }, + address: { street: "private" }, + transactionId: "tx-1", + token: "secret-token", + }, + }); + + const serialized = serializeError(error); + assert.equal("body" in serialized, false); + assert.equal(JSON.stringify(serialized).includes("1234"), false); + assert.equal(JSON.stringify(serialized).includes("private"), false); + assert.equal(JSON.stringify(serialized).includes("secret-token"), false); + assert.equal(serialized.category, "authorization"); + assert.equal(serialized.code, "authorization_failed"); +}); + +test("raw error bodies require explicit opt-in", () => { + const error = new ApiError({ + status: 500, + body: { detail: "debug-only" }, + }); + + assert.equal("body" in serializeError(error), false); + assert.equal("details" in serializeError(error), false); + assert.equal("body" in error, false); + assert.deepEqual(serializeError(error, { includeRawBody: true }).body, { + detail: "debug-only", + }); +}); + +test("safe error serialization preserves normalized WebSocket server fields", () => { + const error = new WebSocketRequestError({ + status: 400, + body: { reason: "InvalidNonce", code: "INVALID_NONCE", detail: "debug-only" }, + }); + + assert.equal(serializeError(error).reason, "InvalidNonce"); + assert.equal(serializeError(error).serverCode, "INVALID_NONCE"); + assert.equal((serializeError(error, { includeRawBody: true }).body as { detail?: string }).detail, "debug-only"); +}); + +test("safe error serialization omits unrecognized exchange reasons and codes", () => { + const error = new ApiError({ + status: 403, + reason: "customer-private-reason", + serverCode: "customer-private-code", + }); + + const serialized = JSON.stringify(serializeError(error)); + assert.equal(serialized.includes("customer-private"), false); + assert.equal(error.message, "HTTP 403"); +}); + +test("JSON.stringify uses the safe error representation", () => { + const error = new ApiError({ + status: 500, + body: { token: "secret-token" }, + }); + + const json = JSON.stringify(error); + assert.equal(json.includes("secret-token"), false); + assert.equal(json.includes('"body"'), false); +}); + +test("logger implementations consume typed diagnostic events", () => { + const event: DiagnosticEvent = { + level: "info", + component: "rest", + name: "request.end", + metadata: { endpoint: "/v1/time" }, + }; + const original = console.log; + let output: unknown; + console.log = (...args: unknown[]) => { + output = args; + }; + try { + new ConsoleLogger({ minLevel: "debug" }).info(event.name, event); + new NoopLogger().info(event.name, event); + } finally { + console.log = original; + } + assert.equal(Array.isArray(output), true); + assert.equal(String((output as unknown[])[0]).endsWith("[INFO] request.end"), true); + assert.deepEqual((output as unknown[])[1], event); +}); + +test("ConsoleLogger emits message-only calls", () => { + const originalLog = console.log; + const originalError = console.error; + const output: unknown[][] = []; + console.log = (...args: unknown[]) => output.push(args); + console.error = (...args: unknown[]) => output.push(args); + try { + const logger = new ConsoleLogger({ minLevel: "debug" }); + logger.debug("debug message"); + logger.info("info message"); + logger.warn("warn message"); + logger.error("error message"); + } finally { + console.log = originalLog; + console.error = originalError; + } + + assert.equal(output.length, 4); + assert.equal(output.every((args) => args.length === 1), true); + assert.equal(output.some((args) => String(args[0]).endsWith("[INFO] info message")), true); +}); + +test("diagnostics preserve the public logger message and metadata contract", () => { + let message: string | undefined; + let metadata: Record | undefined; + const logger: Logger = { + debug: () => {}, + info: (value, meta) => { message = value; metadata = meta; }, + warn: () => {}, + error: () => {}, + }; + + emitDiagnostic({ level: "info", component: "rest", name: "request.end" }, logger); + + assert.equal(message, "request.end"); + assert.equal(metadata?.name, "request.end"); +}); + +test("REST diagnostics preserve safe response metadata without changing the result", async () => { + const events: DiagnosticEvent[] = []; + const client = new HttpTransport({ + env: "sandbox", + onDiagnostic: (event) => events.push(event), + fetchImpl: async () => ({ + status: 200, + headers: { + get(name: string) { + return { + "content-type": "application/json; charset=utf-8", + "x-gemini-request-id": "exchange-1", + "x-ratelimit-limit": "10", + "x-ratelimit-remaining": "9", + "x-ratelimit-reset": "123", + }[name.toLowerCase()] ?? null; + }, + }, + text: async () => '{"ok":true}', + }), + }); + + const result = await client.requestPublic({ method: "GET", path: "/v1/time" }); + assert.deepEqual(result, { ok: true }); + const end = events.find((event) => event.name === "request.end"); + assert.equal(end?.level, "info"); + assert.equal(end?.response?.endpoint, "/v1/time"); + assert.equal(end?.response?.method, "GET"); + assert.equal(end?.response?.exchangeRequestId, "exchange-1"); + assert.equal(end?.response?.contentType, "application/json"); + assert.deepEqual(end?.response?.rateLimit, { + limit: "10", + remaining: "9", + reset: "123", + }); + assert.equal(end?.response?.retryCount, 0); +}); + +test("REST retry diagnostics reuse correlation and capture retry guidance", async () => { + const events: DiagnosticEvent[] = []; + let calls = 0; + const delays: number[] = []; + const client = new HttpTransport({ + env: "sandbox", + maxRetries: 1, + sleep: async (delay) => { delays.push(delay); }, + onDiagnostic: (event) => events.push(event), + fetchImpl: async () => { + calls++; + return calls === 1 + ? { + status: 503, + headers: { get: (name: string) => name.toLowerCase() === "retry-after" ? "2" : null }, + text: async () => '{"error":"SERVICE_UNAVAILABLE"}', + } + : { status: 200, headers: { get: () => "application/json" }, text: async () => "{}" }; + }, + }); + + await client.requestPublic({ method: "GET", path: "/v1/time", retryable: true }); + const retry = events.find((event) => event.name === "request.retry"); + const end = events.find((event) => event.name === "request.end"); + assert.equal(retry?.level, "warn"); + assert.equal(retry?.response?.rateLimit?.retryAfter, "2"); + assert.equal(delays[0], 2000); + assert.equal(retry?.response?.correlationId, end?.response?.correlationId); + assert.equal(end?.response?.retryCount, 1); +}); + +test("REST diagnostics carry safe operation context without request data", async () => { + const events: DiagnosticEvent[] = []; + const client = new HttpTransport({ + env: "sandbox", + onDiagnostic: (event) => events.push(event), + fetchImpl: async () => ({ status: 200, headers: { get: () => "application/json" }, text: async () => "{}" }), + }); + + await client.requestPublic({ + method: "POST", + path: "/v1/prediction-markets/order", + operationContext: { + operation: "predictionMarkets.placeOrder", + clientOrderId: "client-1", + }, + }); + + const end = events.find((event) => event.name === "request.end"); + assert.deepEqual(end?.operationContext, { + operation: "predictionMarkets.placeOrder", + clientOrderId: "client-1", + }); + assert.equal(JSON.stringify(end).includes("signed-payload"), false); +}); + +test("API errors retain correlation metadata while diagnostics omit the raw private body", async () => { + const events: DiagnosticEvent[] = []; + const client = new HttpTransport({ + env: "sandbox", + onDiagnostic: (event) => events.push(event), + fetchImpl: async () => ({ + status: 403, + headers: { get: (name: string) => name.toLowerCase() === "x-gemini-request-id" ? "exchange-403" : "application/json" }, + text: async () => JSON.stringify({ error: "MissingRole", bankAccount: "bank-secret", address: "private-address" }), + }), + }); + + await assert.rejects( + client.requestPublic({ + method: "GET", + path: "/v1/account", + operationContext: { operation: "account.list" }, + }), + (error: unknown) => { + assert.ok(error instanceof ApiError); + assert.equal(error.category, "authorization"); + assert.equal(error.code, "authorization_failed"); + assert.equal(error.metadata?.exchangeRequestId, "exchange-403"); + assert.equal(error.operationContext?.operation, "account.list"); + return true; + }, + ); + const apiError = events.find((event) => event.name === "api.error"); + assert.equal(JSON.stringify(apiError).includes("bank-secret"), false); + assert.equal(JSON.stringify(apiError).includes("private-address"), false); + assert.equal("body" in (apiError?.error ?? {}), false); +}); diff --git a/packages/sdk-typescript/src/tests/fake-socket.ts b/packages/sdk-typescript/src/tests/fake-socket.ts new file mode 100644 index 0000000..2238582 --- /dev/null +++ b/packages/sdk-typescript/src/tests/fake-socket.ts @@ -0,0 +1,26 @@ +import type { SocketLike } from "../transport.js"; + +/** + * A fake WebSocket the tests drive by hand: no network, fully synchronous. + * It records what was sent and exposes fire() to simulate the socket firing + * its own events (open/message/close/error), so we control every scenario. + */ +export class FakeSocket implements SocketLike { + sent: string[] = []; + closed = false; + private listeners: Record void)[]> = {}; + + addEventListener(type: string, listener: (ev: unknown) => void): void { + (this.listeners[type] ??= []).push(listener); + } + send(data: string): void { + this.sent.push(data); + } + close(): void { + this.closed = true; + } + + fire(type: string, ev?: unknown): void { + for (const l of this.listeners[type] ?? []) l(ev); + } +} diff --git a/packages/sdk-typescript/src/tests/foundation.test.ts b/packages/sdk-typescript/src/tests/foundation.test.ts new file mode 100644 index 0000000..a2808d1 --- /dev/null +++ b/packages/sdk-typescript/src/tests/foundation.test.ts @@ -0,0 +1,107 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { SdkError, ResyncRequiredError } from "../errors.js"; +import * as sdk from "../server/index.js"; +import type { + PredictionMarketsPaths, + PredictionMarketsComponents, + PredictionMarketsOpenApiOperations, + PredictionMarketOperationTypes, + PredictionMarketOperationId, + MarketDataOperationTypes, + MarketDataOperationId, +} from "../server/index.js"; +import { ConsoleLogger, NoopLogger } from "../logging.js"; +import type { DiagnosticEvent } from "../diagnostics.js"; + +const generatedContractTypes: [ + PredictionMarketsPaths, + PredictionMarketsComponents, + PredictionMarketsOpenApiOperations, + PredictionMarketOperationTypes, + MarketDataOperationTypes, +] | undefined = undefined; +void generatedContractTypes; + +const knownPredictionMarketOperation: PredictionMarketOperationId = "placeOrder"; +// @ts-expect-error Unknown operation IDs must not be accepted. +const unknownPredictionMarketOperation: PredictionMarketOperationId = "unknownOperation"; +void unknownPredictionMarketOperation; +const knownMarketDataOperation: MarketDataOperationId = "getTicker"; +const knownMarketDataFileOperation: MarketDataOperationId = "getFundingAmountReportFile"; +void knownMarketDataFileOperation; + +// Run fn with console.log/error replaced by counters; restore afterward. +function captureConsole(fn: () => void): { logs: number; errors: number } { + const origLog = console.log; + const origError = console.error; + let logs = 0; + let errors = 0; + console.log = () => { + logs++; + }; + console.error = () => { + errors++; + }; + try { + fn(); + } finally { + console.log = origLog; + console.error = origError; + } + return { logs, errors }; +} + +test("ConsoleLogger drops messages below minLevel", () => { + const logger = new ConsoleLogger({ minLevel: "error" }); + const event: DiagnosticEvent = { level: "info", component: "rest", name: "test.info" }; + const errorEvent: DiagnosticEvent = { level: "error", component: "rest", name: "test.error" }; + const { logs, errors } = captureConsole(() => { + logger.info(event.name, event); + logger.error(errorEvent.name, errorEvent); + }); + assert.equal(logs, 0, "info must be dropped when minLevel is error"); + assert.equal(errors, 1, "error must be emitted"); +}); + +test("NoopLogger emits nothing", () => { + const logger = new NoopLogger(); + const event = (level: DiagnosticEvent["level"]): DiagnosticEvent => ({ level, component: "rest", name: `test.${level}` }); + const { logs, errors } = captureConsole(() => { + logger.debug("test.debug", event("debug")); + logger.info("test.info", event("info")); + logger.warn("test.warn", event("warn")); + logger.error("test.error", event("error")); + }); + assert.equal(logs, 0); + assert.equal(errors, 0); +}); + +test("ResyncRequiredError is an SdkError and carries the gap ids", () => { + const err = new ResyncRequiredError(1n, 5n); + assert.ok(err instanceof SdkError); + assert.ok(err instanceof Error); + assert.equal(err.name, "ResyncRequiredError"); + assert.equal(err.lastUpdateId, 1n); + assert.equal(err.firstUpdateId, 5n); +}); + +test("parseLosslessJson is reachable from the package barrel", () => { + // The root-only exports map means anything public must be re-exported from index. + const exported = (sdk as Record).parseLosslessJson; + assert.equal(typeof exported, "function", "parser must be exported from the barrel"); +}); + +test("generated prediction market contracts are reachable from the package barrel", () => { + assert.equal(knownPredictionMarketOperation, "placeOrder"); + assert.equal(sdk.PREDICTION_MARKET_OPERATIONS.placeOrder.method, "post"); + assert.equal(typeof sdk.PredictionMarketsRest, "function"); +}); + +test("generated Market Data contracts are reachable from the package barrel", () => { + assert.equal(knownMarketDataOperation, "getTicker"); + assert.equal(sdk.MARKET_DATA_OPERATIONS.getTicker.method, "get"); + assert.equal(typeof sdk.MarketDataRest, "function"); + assert.equal(typeof sdk.MarketDataClient, "function"); +}); diff --git a/packages/sdk-typescript/src/tests/generated-market-data.test.ts b/packages/sdk-typescript/src/tests/generated-market-data.test.ts new file mode 100644 index 0000000..cc53d3f --- /dev/null +++ b/packages/sdk-typescript/src/tests/generated-market-data.test.ts @@ -0,0 +1,242 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +import type { components } from "../generated/market-data/models.js"; +import type { RestFileResponse } from "../core/http.js"; +import { + MARKET_DATA_OPERATIONS, + type MarketDataOperationTypes, +} from "../generated/market-data/operations.js"; +import { MarketDataRest } from "../generated/market-data/rest.js"; + +type Equal = + (() => T extends A ? 1 : 2) extends + (() => T extends B ? 1 : 2) ? true : false; +type Assert = T; + +type _GetBookPath = Assert< + Equal +>; +type _GetBookQuery = Assert< + Equal< + MarketDataOperationTypes["getCurrentOrderBook"]["query"], + { limit_bids?: number; limit_asks?: number } | undefined + > +>; +type _ListTradesQuery = Assert< + Equal< + MarketDataOperationTypes["listTrades"]["query"], + { + timestamp?: string | bigint | number; + since_tid?: number; + limit_trades?: number; + include_breaks?: boolean; + } | undefined + > +>; +type _ListCandlesPath = Assert< + Equal< + MarketDataOperationTypes["listCandles"]["path"], + { symbol: string; time_frame: "1m" | "5m" | "15m" | "30m" | "1h" | "6h" | "1d" } + > +>; +type _GetFundingAmountPath = Assert< + Equal +>; +type _GetTickerResponse = Assert< + Equal +>; +type _GetBookResponse = Assert< + Equal +>; +type _ListCandlesResponse = Assert< + Equal< + MarketDataOperationTypes["listCandles"]["response"], + components["schemas"]["CandleResponse"] + > +>; +type _GetFundingAmountResponse = Assert< + Equal< + MarketDataOperationTypes["getFundingAmount"]["response"], + components["schemas"]["FundingAmountResponse"] + > +>; +type _GetFXRatePath = Assert< + Equal< + MarketDataOperationTypes["getFXRate"]["path"], + { symbol: string; timestamp: string | bigint | number } + > +>; +type _MarketDataRestMethods = Assert< + Equal< + keyof MarketDataRest, + | "listSymbols" + | "getSymbolDetails" + | "getAssetsForNetwork" + | "getTokenNetworkV2" + | "getTicker" + | "listFeePromos" + | "getCurrentOrderBook" + | "listTrades" + | "listPrices" + | "getFundingAmount" + | "getFundingAmountReportFile" + | "getTickerV2" + | "listCandles" + | "listDerivativeCandles" + | "getFXRate" + > +>; +type _NoMarketDataBodies = Assert< + Equal +>; +type _FundingReportResponse = Assert< + Equal +>; +type _RestMethods = Assert< + Equal< + keyof MarketDataRest, + | "getAssetsForNetwork" + | "getCurrentOrderBook" + | "getFXRate" + | "getFundingAmount" + | "getFundingAmountReportFile" + | "getSymbolDetails" + | "getTicker" + | "getTickerV2" + | "getTokenNetworkV2" + | "listCandles" + | "listDerivativeCandles" + | "listFeePromos" + | "listPrices" + | "listSymbols" + | "listTrades" + > +>; +type _ListSymbolsArgs = Assert, []>>; +type _TickerArgs = Assert< + Equal, [path: MarketDataOperationTypes["getTicker"]["path"]]> +>; +type _BookArgs = Assert< + Equal< + Parameters, + [ + path: MarketDataOperationTypes["getCurrentOrderBook"]["path"], + query?: MarketDataOperationTypes["getCurrentOrderBook"]["query"], + ] + > +>; +type _TradesArgs = Assert< + Equal< + Parameters, + [ + path: MarketDataOperationTypes["listTrades"]["path"], + query?: MarketDataOperationTypes["listTrades"]["query"], + ] + > +>; +type _FundingReportArgs = Assert< + Equal< + Parameters, + [query: MarketDataOperationTypes["getFundingAmountReportFile"]["query"]] + > +>; +type _TickerResult = Assert< + Equal>, MarketDataOperationTypes["getTicker"]["response"]> +>; + +test("generated operation metadata describes every Market Data operation", () => { + const entries = Object.entries(MARKET_DATA_OPERATIONS); + assert.equal(entries.length, 15); + assert.equal(new Set(entries.map(([operationId]) => operationId)).size, 15); + assert.equal(entries.filter(([, operation]) => operation.access === "authenticated").length, 3); + assert.equal(entries.filter(([, operation]) => operation.access === "public").length, 12); + + const metadataKeys = [ + "access", "headers", "method", "operation", "parameters", "path", "requestBody", "requestBodyRequired", + "requestInt64Paths", "responseContentTypes", "responseInt64Paths", "responseMode", "retryable", "successStatuses", + ]; + for (const [, operation] of entries) { + assert.deepEqual(Object.keys(operation).sort(), metadataKeys); + assert.equal(operation.method, "get"); + assert.deepEqual(operation.headers, []); + assert.equal(operation.requestBody, false); + assert.equal(operation.requestBodyRequired, false); + } + + assert.deepEqual(MARKET_DATA_OPERATIONS.getCurrentOrderBook, { + responseMode: "json", + operation: "marketData.getCurrentOrderBook", + method: "get", + path: "/v1/book/{symbol}", + access: "public", + parameters: [ + { name: "symbol", in: "path", required: true, style: "simple", explode: false }, + { name: "limit_bids", in: "query", required: false, style: "form", explode: true, shape: "scalar", allowReserved: false }, + { name: "limit_asks", in: "query", required: false, style: "form", explode: true, shape: "scalar", allowReserved: false }, + ], + headers: [], + requestBody: false, + requestBodyRequired: false, + successStatuses: [200], + responseContentTypes: ["application/json"], + responseInt64Paths: [], + requestInt64Paths: { body: [], path: [], query: [] }, + retryable: true, + }); + assert.deepEqual(MARKET_DATA_OPERATIONS.getFundingAmountReportFile.responseContentTypes, [ + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "text/csv", + ]); + assert.equal(MARKET_DATA_OPERATIONS.getFundingAmountReportFile.responseMode, "file"); + assert.deepEqual(MARKET_DATA_OPERATIONS.listTrades.parameters, [ + { name: "symbol", in: "path", required: true, style: "simple", explode: false }, + { name: "timestamp", in: "query", required: false, style: "form", explode: true, shape: "scalar", allowReserved: false }, + { name: "since_tid", in: "query", required: false, style: "form", explode: true, shape: "scalar", allowReserved: false }, + { name: "limit_trades", in: "query", required: false, style: "form", explode: true, shape: "scalar", allowReserved: false }, + { name: "include_breaks", in: "query", required: false, style: "form", explode: true, shape: "scalar", allowReserved: false }, + ]); + assert.deepEqual(MARKET_DATA_OPERATIONS.listCandles.parameters, [ + { name: "symbol", in: "path", required: true, style: "simple", explode: false }, + { name: "time_frame", in: "path", required: true, style: "simple", explode: false }, + ]); + assert.deepEqual(MARKET_DATA_OPERATIONS.listCandles.responseInt64Paths, []); + assert.deepEqual(MARKET_DATA_OPERATIONS.listDerivativeCandles.responseInt64Paths, []); + assert.deepEqual(MARKET_DATA_OPERATIONS.getFundingAmount.parameters, [ + { name: "symbol", in: "path", required: true, style: "simple", explode: false }, + ]); + assert.equal(MARKET_DATA_OPERATIONS.getAssetsForNetwork.access, "authenticated"); + assert.equal(MARKET_DATA_OPERATIONS.getTokenNetworkV2.access, "authenticated"); + assert.equal(MARKET_DATA_OPERATIONS.getFXRate.access, "authenticated"); +}); + +test("Market Data generator output is deterministic and matches committed files", (t) => { + const sdkDir = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); + const generatorPath = join(sdkDir, "scripts/generate-market-data.mjs"); + const localSpec = resolve(sdkDir, "../../apis/rest.yaml"); + const specPath = existsSync(localSpec) ? localSpec : "https://developer.gemini.com/specs/openapi/rest.yaml"; + const generatedDir = join(sdkDir, "src/generated/market-data"); + const first = mkdtempSync(join(tmpdir(), "market-data-generator-first-")); + const second = mkdtempSync(join(tmpdir(), "market-data-generator-second-")); + + t.after(() => { + rmSync(first, { recursive: true, force: true }); + rmSync(second, { recursive: true, force: true }); + }); + + execFileSync(process.execPath, [generatorPath, specPath, first]); + execFileSync(process.execPath, [generatorPath, specPath, second]); + + for (const filename of ["models.ts", "operations.ts", "rest.ts"]) { + const firstBytes = readFileSync(join(first, filename)); + const secondBytes = readFileSync(join(second, filename)); + const committedBytes = readFileSync(join(generatedDir, filename)); + assert.deepEqual(firstBytes, secondBytes); + assert.deepEqual(firstBytes, committedBytes); + } +}); diff --git a/packages/sdk-typescript/src/tests/generated-prediction-markets.test.ts b/packages/sdk-typescript/src/tests/generated-prediction-markets.test.ts new file mode 100644 index 0000000..dea4527 --- /dev/null +++ b/packages/sdk-typescript/src/tests/generated-prediction-markets.test.ts @@ -0,0 +1,633 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { parse } from "yaml"; + +import type { components } from "../generated/models.js"; +import { + PREDICTION_MARKET_OPERATIONS, + type PredictionMarketOperationTypes, +} from "../generated/operations.js"; +import { PredictionMarketsRest } from "../generated/rest.js"; + +type Equal = + (() => T extends A ? 1 : 2) extends + (() => T extends B ? 1 : 2) ? true : false; +type Assert = T; + +type OrderResponse = components["schemas"]["OrderResponse"]; +type Position = components["schemas"]["Position"]; +type Batch = components["schemas"]["PlaceOrderBatchRequest"]; +type Reward = components["schemas"]["MakerRebatePayout"]; +type RewardSummary = components["schemas"]["MakerRebateLifetimeSummary"]; +type RewardEvent = components["schemas"]["LiquidityRewardEvent"]; +type ComboLeg = components["schemas"]["ComboLeg"]; +type ComboSummaryLeg = components["schemas"]["ComboSummaryLeg"]; +type MakerRate = components["schemas"]["MakerRebateRateRule"]; +type RewardsConfig = components["schemas"]["LiquidityRewardsConfig"]; +type Event = components["schemas"]["Event"]; +type SportsMarket = components["schemas"]["SportsMarket"]; +type SportsMarketScope = components["schemas"]["SportsMarketScope"]; + +type _OrderId = Assert>; +type _OrderPrice = Assert>; +type _Pct = Assert>; +type _BatchArray = Assert>; +type _RewardId = Assert>; +type _RewardMoney = Assert>; +type _PaidAt = Assert>; +type _IconUrl = Assert>; +type _ComboId = Assert>; +type _ComboContractId = Assert>; +type _ComboSummaryLegContractId = Assert>; +type _ComboSummaryLegOutcome = Assert< + Equal +>; +type _ComboSummaryLegResolvedAt = Assert< + Equal +>; +type _MakerRateBps = Assert>; +type _RewardsThreshold = Assert< + Equal +>; +type _RewardPool = Assert>; +type _NoRewardEventAliases = Assert< + Equal, never> +>; +type _MarketStatus = Assert< + Equal< + components["schemas"]["MarketStatus"], + "approved" | "active" | "closed" | "under_review" | "settled" | "invalid" + > +>; +type _SportsMarket = Assert< + Equal< + SportsMarket, + { + sport: components["schemas"]["SportsMarketSport"]; + type: components["schemas"]["SportsMarketType"]; + subject: components["schemas"]["SportsMarketSubject"]; + scope: SportsMarketScope; + metric?: components["schemas"]["SportsMarketMetric"]; + } + > +>; +type _SportsMarketScope = Assert< + Equal< + SportsMarketScope, + { + type: components["schemas"]["SportsMarketScopeType"]; + ordinal?: number; + start?: number; + end?: number; + } + > +>; +type _EventSportsMarket = Assert>; +type _NoRewardAliases = Assert< + Equal< + Extract<"totalVolumeUsd" | "totalRebateUsd" | "paidAt" | "createdAt", keyof Reward>, + never + > +>; +type _NoRewardSummaryAliases = Assert< + Equal, never> +>; +type _PlaceOrderBody = Assert< + Equal +>; +type _PlaceOrderResponse = Assert< + Equal< + PredictionMarketOperationTypes["placeOrder"]["response"], + components["schemas"]["OrderResponse"] + > +>; +type _CancelOrderBody = Assert< + Equal +>; +type _CancelOrderResponse = Assert< + Equal< + PredictionMarketOperationTypes["cancelOrder"]["response"], + { result?: string; message?: string } + > +>; +type _GetActiveOrdersBody = Assert< + Equal< + PredictionMarketOperationTypes["getActiveOrders"]["body"], + { symbol?: string; limit?: number; offset?: number } | undefined + > +>; +type _GetOrderHistoryBody = Assert< + Equal< + PredictionMarketOperationTypes["getOrderHistory"]["body"], + { + status?: "filled" | "cancelled"; + symbol?: string; + limit?: number; + offset?: number; + from?: bigint | number; + to?: bigint | number; + } | undefined + > +>; +type _NoRequestBody = Assert< + Equal +>; +type _GetEventPath = Assert< + Equal +>; +type _ListEventsQuery = Assert< + Equal< + PredictionMarketOperationTypes["listEvents"]["query"], + | { + status?: components["schemas"]["MarketStatus"][]; + category?: string[]; + sport?: components["schemas"]["SportsMarketSport"][]; + sports_market_type?: components["schemas"]["SportsMarketType"][]; + sports_market_subject?: components["schemas"]["SportsMarketSubject"][]; + sports_market_scope?: components["schemas"]["SportsMarketScopeType"][]; + sports_market_metric?: components["schemas"]["SportsMarketMetric"][]; + search?: string; + limit?: number; + offset?: number; + } + | undefined + > +>; +type _RestMethods = Assert< + Equal< + keyof PredictionMarketsRest, + | "getComboByInstrumentSymbol" + | "listEvents" + | "getEvent" + | "getEventStrike" + | "listNewlyListedEvents" + | "listRecentlySettledEvents" + | "listUpcomingEvents" + | "getCategories" + | "getPredictionMarketDailyVolume" + | "getPredictionMarketHourlyVolume" + | "getPredictionMarketsTerms" + | "getLiquidityRewardsConfig" + | "getMakerRebateRates" + | "listCombos" + | "listLiquidityRewardsEvents" + | "getPredictionMarketsTermsStatus" + | "acceptPredictionMarketsTerms" + | "createCombo" + | "placeOrder" + | "placeOrderBatch" + | "cancelOrder" + | "cancelOrderBatch" + | "getActiveOrders" + | "getOrderHistory" + | "getPositions" + | "getSettledPositions" + | "getVolumeMetrics" + | "listMakerRebatePayouts" + | "getMakerRebateLifetimeSummary" + | "getLiquidityRewardsDailySummary" + | "getLiquidityRewardsLifetimeSummary" + > +>; +type _TermsStatusArgs = Assert< + Equal, []> +>; +type _AcceptTermsArgs = Assert< + Equal, []> +>; +type _PlaceOrderArgs = Assert< + Equal< + Parameters, + [body: PredictionMarketOperationTypes["placeOrder"]["body"]] + > +>; +type _CreateComboArgs = Assert< + Equal< + Parameters, + [body: PredictionMarketOperationTypes["createCombo"]["body"]] + > +>; +type _CreateComboResult = Assert< + Equal< + Awaited>, + PredictionMarketOperationTypes["createCombo"]["response"] + > +>; +type _PlaceBatchArgs = Assert< + Equal< + Parameters, + [body: PredictionMarketOperationTypes["placeOrderBatch"]["body"]] + > +>; +type _CancelOrderArgs = Assert< + Equal< + Parameters, + [body: PredictionMarketOperationTypes["cancelOrder"]["body"]] + > +>; +type _CancelBatchArgs = Assert< + Equal< + Parameters, + [body: PredictionMarketOperationTypes["cancelOrderBatch"]["body"]] + > +>; +type _ActiveOrdersArgs = Assert< + Equal< + Parameters, + [body?: PredictionMarketOperationTypes["getActiveOrders"]["body"]] + > +>; +type _OrderHistoryResult = Assert< + Equal< + Awaited>, + PredictionMarketOperationTypes["getOrderHistory"]["response"] + > +>; +type _PositionsArgs = Assert< + Equal< + Parameters, + [query?: PredictionMarketOperationTypes["getPositions"]["query"]] + > +>; +type _SettledPositionsArgs = Assert< + Equal< + Parameters, + [query?: PredictionMarketOperationTypes["getSettledPositions"]["query"]] + > +>; +type _VolumeMetricsArgs = Assert< + Equal< + Parameters, + [body: PredictionMarketOperationTypes["getVolumeMetrics"]["body"]] + > +>; +type _PositionsResult = Assert< + Equal< + Awaited>, + PredictionMarketOperationTypes["getPositions"]["response"] + > +>; +type _SettledPositionsResult = Assert< + Equal< + Awaited>, + PredictionMarketOperationTypes["getSettledPositions"]["response"] + > +>; +type _VolumeMetricsResult = Assert< + Equal< + Awaited>, + PredictionMarketOperationTypes["getVolumeMetrics"]["response"] + > +>; +type _MakerRebatePayoutsArgs = Assert< + Equal< + Parameters, + [query?: PredictionMarketOperationTypes["listMakerRebatePayouts"]["query"]] + > +>; +type _MakerRebatePayoutsResult = Assert< + Equal< + Awaited>, + PredictionMarketOperationTypes["listMakerRebatePayouts"]["response"] + > +>; +type _MakerRebateLifetimeArgs = Assert< + Equal< + Parameters, + [query?: PredictionMarketOperationTypes["getMakerRebateLifetimeSummary"]["query"]] + > +>; +type _MakerRebateLifetimeResult = Assert< + Equal< + Awaited>, + PredictionMarketOperationTypes["getMakerRebateLifetimeSummary"]["response"] + > +>; +type _LiquidityRewardsDailyArgs = Assert< + Equal< + Parameters, + [query: PredictionMarketOperationTypes["getLiquidityRewardsDailySummary"]["query"]] + > +>; +type _LiquidityRewardsDailyResult = Assert< + Equal< + Awaited>, + PredictionMarketOperationTypes["getLiquidityRewardsDailySummary"]["response"] + > +>; +type _LiquidityRewardsLifetimeArgs = Assert< + Equal< + Parameters, + [query?: PredictionMarketOperationTypes["getLiquidityRewardsLifetimeSummary"]["query"]] + > +>; +type _LiquidityRewardsLifetimeResult = Assert< + Equal< + Awaited>, + PredictionMarketOperationTypes["getLiquidityRewardsLifetimeSummary"]["response"] + > +>; +type _ListEventsInput = Assert< + Equal< + Parameters[0], + PredictionMarketOperationTypes["listEvents"]["query"] + > +>; +type _GetEventInput = Assert< + Equal< + Parameters, + [path: PredictionMarketOperationTypes["getEvent"]["path"]] + > +>; +type _GetEventResult = Assert< + Equal< + Awaited>, + PredictionMarketOperationTypes["getEvent"]["response"] + > +>; +type _StrikeMoney = Assert< + Equal< + PredictionMarketOperationTypes["getEventStrike"]["response"]["value"], + string | null | undefined + > +>; +type _TermsInput = Assert< + Equal, []> +>; +type _PredictionMarketDailyVolumeInput = Assert< + Equal< + Parameters, + [path: PredictionMarketOperationTypes["getPredictionMarketDailyVolume"]["path"]] + > +>; +type _PredictionMarketDailyVolumeResult = Assert< + Equal< + Awaited>, + PredictionMarketOperationTypes["getPredictionMarketDailyVolume"]["response"] + > +>; +type _PredictionMarketHourlyVolumeInput = Assert< + Equal< + Parameters, + [path: PredictionMarketOperationTypes["getPredictionMarketHourlyVolume"]["path"]] + > +>; +type _PredictionMarketHourlyVolumeResult = Assert< + Equal< + Awaited>, + PredictionMarketOperationTypes["getPredictionMarketHourlyVolume"]["response"] + > +>; +type _ListCombosResult = Assert< + Equal< + Awaited>, + PredictionMarketOperationTypes["listCombos"]["response"] + > +>; +type _ComboInput = Assert< + Equal< + Parameters[0], + PredictionMarketOperationTypes["getComboByInstrumentSymbol"]["path"] + > +>; +type _MakerRatesResult = Assert< + Equal< + Awaited>, + PredictionMarketOperationTypes["getMakerRebateRates"]["response"] + > +>; +type _RewardsConfigArgs = Assert< + Equal, []> +>; +type _RewardsEventsResult = Assert< + Equal< + Awaited>, + PredictionMarketOperationTypes["listLiquidityRewardsEvents"]["response"] + > +>; + +test("generated operation metadata describes every prediction-market operation", () => { + const entries = Object.entries(PREDICTION_MARKET_OPERATIONS); + assert.equal(entries.length, 31); + assert.equal(new Set(entries.map(([operationId]) => operationId)).size, 31); + assert.equal(entries.filter(([, operation]) => operation.access === "public").length, 15); + assert.equal(entries.filter(([, operation]) => operation.access === "authenticated").length, 16); + + const metadataKeys = [ + "access", "headers", "method", "operation", "parameters", "path", "requestBody", "requestBodyRequired", + "requestInt64Paths", "responseContentTypes", "responseInt64Paths", "responseMode", "retryable", "successStatuses", + ]; + for (const [, operation] of entries) assert.deepEqual(Object.keys(operation).sort(), metadataKeys); + + assert.deepEqual(PREDICTION_MARKET_OPERATIONS.listEvents, { + responseMode: "json", + operation: "predictionMarkets.listEvents", + method: "get", + path: "/v1/prediction-markets/events", + access: "public", + parameters: [ + { name: "status", in: "query", required: false, style: "form", explode: true, shape: "array", allowReserved: false }, + { name: "category", in: "query", required: false, style: "form", explode: true, shape: "array", allowReserved: false }, + { name: "sport", in: "query", required: false, style: "form", explode: true, shape: "array", allowReserved: false }, + { name: "sports_market_type", in: "query", required: false, style: "form", explode: true, shape: "array", allowReserved: false }, + { name: "sports_market_subject", in: "query", required: false, style: "form", explode: true, shape: "array", allowReserved: false }, + { name: "sports_market_scope", in: "query", required: false, style: "form", explode: true, shape: "array", allowReserved: false }, + { name: "sports_market_metric", in: "query", required: false, style: "form", explode: true, shape: "array", allowReserved: false }, + { name: "search", in: "query", required: false, style: "form", explode: true, shape: "scalar", allowReserved: false }, + { name: "limit", in: "query", required: false, style: "form", explode: true, shape: "scalar", allowReserved: false }, + { name: "offset", in: "query", required: false, style: "form", explode: true, shape: "scalar", allowReserved: false }, + ], + headers: [], + requestBody: false, + requestBodyRequired: false, + successStatuses: [200], + responseContentTypes: ["application/json"], + responseInt64Paths: [], + requestInt64Paths: { body: [], path: [], query: [] }, + retryable: true, + }); + assert.deepEqual(PREDICTION_MARKET_OPERATIONS.getComboByInstrumentSymbol.parameters, [ + { name: "instrumentSymbol", in: "path", required: true, style: "simple", explode: false }, + ]); + assert.deepEqual(PREDICTION_MARKET_OPERATIONS.getPredictionMarketDailyVolume, { + responseMode: "json", + operation: "predictionMarkets.getPredictionMarketDailyVolume", + method: "get", + path: "/v1/prediction-markets/volume/{date}", + access: "public", + parameters: [{ name: "date", in: "path", required: true, style: "simple", explode: false }], + headers: [], + requestBody: false, + requestBodyRequired: false, + successStatuses: [200], + responseContentTypes: ["application/json"], + responseInt64Paths: [], + requestInt64Paths: { body: [], path: [], query: [] }, + retryable: true, + }); + assert.deepEqual(PREDICTION_MARKET_OPERATIONS.getPredictionMarketHourlyVolume, { + responseMode: "json", + operation: "predictionMarkets.getPredictionMarketHourlyVolume", + method: "get", + path: "/v1/prediction-markets/volume/{date}/hourly", + access: "public", + parameters: [{ name: "date", in: "path", required: true, style: "simple", explode: false }], + headers: [], + requestBody: false, + requestBodyRequired: false, + successStatuses: [200], + responseContentTypes: ["application/json"], + responseInt64Paths: [], + requestInt64Paths: { body: [], path: [], query: [] }, + retryable: true, + }); + assert.deepEqual(PREDICTION_MARKET_OPERATIONS.placeOrder, { + responseMode: "json", + operation: "predictionMarkets.placeOrder", + method: "post", + path: "/v1/prediction-markets/order", + access: "authenticated", + parameters: [], + headers: [], + requestBody: true, + requestBodyRequired: true, + successStatuses: [201], + responseContentTypes: ["application/json"], + responseInt64Paths: [["orderId"]], + requestInt64Paths: { body: [], path: [], query: [] }, + retryable: false, + }); + assert.deepEqual(PREDICTION_MARKET_OPERATIONS.cancelOrder, { + responseMode: "json", + operation: "predictionMarkets.cancelOrder", + method: "post", + path: "/v1/prediction-markets/order/cancel", + access: "authenticated", + parameters: [], + headers: [], + requestBody: true, + requestBodyRequired: true, + successStatuses: [200], + responseContentTypes: ["application/json"], + responseInt64Paths: [], + requestInt64Paths: { body: [{ path: ["orderId"] }], path: [], query: [] }, + retryable: false, + }); + assert.deepEqual(PREDICTION_MARKET_OPERATIONS.listCombos.responseInt64Paths, [ + ["combos", "*", "legs", "*", "comboId"], + ]); + assert.deepEqual(PREDICTION_MARKET_OPERATIONS.createCombo, { + responseMode: "json", + operation: "predictionMarkets.createCombo", + method: "post", + path: "/v1/prediction-markets/combos", + access: "authenticated", + parameters: [], + headers: [], + requestBody: true, + requestBodyRequired: true, + successStatuses: [200, 201], + responseContentTypes: ["application/json"], + responseInt64Paths: [["combo", "id"], ["combo", "instrumentId"], ["combo", "legs", "*", "comboId"]], + requestInt64Paths: { body: [], path: [], query: [] }, + retryable: false, + }); + assert.equal(PREDICTION_MARKET_OPERATIONS.getActiveOrders.requestBody, true); + assert.equal(PREDICTION_MARKET_OPERATIONS.getActiveOrders.requestBodyRequired, false); + assert.equal(PREDICTION_MARKET_OPERATIONS.getOrderHistory.requestBodyRequired, false); + + assert.doesNotMatch( + JSON.stringify(PREDICTION_MARKET_OPERATIONS), + /hmac|oauth|signature|apiKey/i, + ); +}); + +test("OpenAPI, operation manifest, and REST wrappers contain the same 31 unique operations", async () => { + const sdkDir = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); + const localSpec = resolve(sdkDir, "../../apis/prediction-markets.yaml"); + const specText = existsSync(localSpec) + ? readFileSync(localSpec, "utf8") + : await fetch("https://developer.gemini.com/specs/openapi/prediction-markets.yaml").then((r) => r.text()); + const document = parse(specText) as { + paths?: Record>; + }; + const methods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"]; + const specIds = Object.values(document.paths ?? {}).flatMap((pathItem) => + methods.flatMap((method) => { + const operationId = pathItem[method]?.operationId; + if (operationId === undefined) return []; + assert.equal(typeof operationId, "string"); + return [operationId as string]; + }) + ); + const manifestIds = Object.keys(PREDICTION_MARKET_OPERATIONS); + const wrapperIds = Object.getOwnPropertyNames(PredictionMarketsRest.prototype) + .filter((name) => name !== "constructor"); + + assert.equal(specIds.length, 31); + assert.equal(new Set(specIds).size, specIds.length); + const expectedIds = [...specIds].sort(); + assert.deepEqual([...manifestIds].sort(), expectedIds); + assert.deepEqual([...wrapperIds].sort(), expectedIds); +}); + +test("generator rejects multiple 2xx JSON responses even when one omits its schema", (t) => { + const sdkDir = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); + const directory = mkdtempSync(join(tmpdir(), "pm-generator-invalid-")); + const specPath = join(directory, "spec.yaml"); + t.after(() => rmSync(directory, { recursive: true, force: true })); + writeFileSync(specPath, `openapi: 3.0.3 +info: + title: test + version: 1.0.0 +paths: + /test: + get: + operationId: testOperation + responses: + "200": + description: first + content: + application/json: + schema: + type: object + "201": + description: second + content: + application/json: {} +`); + assert.throws(() => execFileSync(process.execPath, [ + join(sdkDir, "scripts/generate-prediction-markets.mjs"), specPath, join(directory, "output"), + ])); +}); + +test("generator output is deterministic and matches committed files", (t) => { + const sdkDir = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); + const generatorPath = join(sdkDir, "scripts/generate-prediction-markets.mjs"); + const localSpec = resolve(sdkDir, "../../apis/prediction-markets.yaml"); + const specPath = existsSync(localSpec) ? localSpec : "https://developer.gemini.com/specs/openapi/prediction-markets.yaml"; + const generatedDir = join(sdkDir, "src/generated"); + const first = mkdtempSync(join(tmpdir(), "pm-generator-first-")); + const second = mkdtempSync(join(tmpdir(), "pm-generator-second-")); + + t.after(() => { + rmSync(first, { recursive: true, force: true }); + rmSync(second, { recursive: true, force: true }); + }); + + execFileSync(process.execPath, [generatorPath, specPath, first]); + execFileSync(process.execPath, [generatorPath, specPath, second]); + + for (const filename of ["models.ts", "operations.ts", "rest.ts"]) { + const firstBytes = readFileSync(join(first, filename)); + const secondBytes = readFileSync(join(second, filename)); + const committedBytes = readFileSync(join(generatedDir, filename)); + assert.deepEqual(firstBytes, secondBytes); + assert.deepEqual(firstBytes, committedBytes); + } +}); diff --git a/packages/sdk-typescript/src/tests/generated-rest-surfaces.test.ts b/packages/sdk-typescript/src/tests/generated-rest-surfaces.test.ts new file mode 100644 index 0000000..65830d1 --- /dev/null +++ b/packages/sdk-typescript/src/tests/generated-rest-surfaces.test.ts @@ -0,0 +1,1030 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { fromBase64, hmacSha384Hex } from "../core/encoding.js"; + +import { + ACCOUNT_SERVICES_OPERATIONS, + AccountServicesClient, + AccountServicesRest, + CLEARING_INSTANT_OPERATIONS, + ClearingInstantClient, + ClearingInstantRest, + GeminiMarkets, + HmacAuth, + MARGIN_OPERATIONS, + MarginClient, + MarginRest, + PERPETUALS_OPERATIONS, + PerpetualsClient, + PerpetualsRest, + TRADING_OPERATIONS, + TradingClient, + TradingRest, + parseLosslessJson, + type AccountServicesOperationTypes, + type ClearingInstantOperationTypes, + type FetchLike, + type MarginOperationTypes, + type PerpetualsOperationTypes, + type RestFileResponse, + type TradingOperationTypes, +} from "../server/index.js"; +import type { components } from "../generated/market-data/models.js"; + +type Equal = + (() => T extends A ? 1 : 2) extends + (() => T extends B ? 1 : 2) ? true : false; +type Assert = T; +type TransportFieldKeys = "request" | "nonce"; + +type _GeneratedFacadeClients = Assert< + Equal< + [ + GeminiMarkets["trading"], + GeminiMarkets["margin"], + GeminiMarkets["perpetuals"], + GeminiMarkets["accountServices"], + GeminiMarkets["clearingInstant"], + ], + [TradingRest, MarginRest, PerpetualsRest, AccountServicesRest, ClearingInstantRest] + > +>; +type _TradingOrderBody = Assert< + Equal< + TradingOperationTypes["createNewOrder"]["body"], + Omit + > +>; +type _TradingWrapPath = Assert< + Equal +>; +type _TradingWrapBodyNoTransportFields = Assert< + Equal, never> +>; +type _MarginPreviewBodyNoTransportFields = Assert< + Equal, never> +>; +type _MarginPreviewResponse = Assert< + Equal +>; +type _PerpetualsRiskStatsPath = Assert< + Equal +>; +type _PerpetualsReportFileResponse = Assert< + Equal +>; +type _PerpetualsReportBodyNoTransportFields = Assert< + Equal< + Extract, + never + > +>; +type _AccountStakingRatesBody = Assert< + Equal +>; +type _AccountWithdrawPath = Assert< + Equal< + AccountServicesOperationTypes["withdrawCryptoFunds"]["path"], + { network: components["parameters"]["networkParam"]; ticker: string } + > +>; +type _ClearingOrderResponse = Assert< + Equal +>; +type _ClearingOrderBodyNoTransportFields = Assert< + Equal< + Extract, + never + > +>; +type _InstantQuoteResponse = Assert< + Equal +>; +type _InstantQuoteBodyNoTransportFields = Assert< + Equal, never> +>; + +type Request = { + url: string; + init: Parameters[1]; +}; + +type GeneratedOperationMetadata = { + responseMode: string; + operation: string; + method: string; + path: string; + access: string; + parameters: readonly { name: string; in: string; required: boolean; style: string; explode: boolean }[]; + headers: readonly unknown[]; + requestBody: boolean; + requestBodyRequired: boolean; + successStatuses: readonly number[]; + responseContentTypes: readonly string[]; + responseInt64Paths: readonly unknown[]; + requestInt64Paths: { + body: readonly unknown[]; + path: readonly unknown[]; + query: readonly unknown[]; + }; + retryable: boolean; +}; + +const metadataKeys = [ + "access", + "headers", + "method", + "operation", + "parameters", + "path", + "requestBody", + "requestBodyRequired", + "requestInt64Paths", + "responseContentTypes", + "responseInt64Paths", + "responseMode", + "retryable", + "successStatuses", +]; + +test("generated trading metadata preserves unsigned order IDs", () => { + assert.deepEqual(TRADING_OPERATIONS.cancelOrder.requestInt64Paths.body, [ + { path: ["nonce"], allowString: true }, + { path: ["order_id"], unsigned: true }, + ]); + assert.deepEqual(TRADING_OPERATIONS.getOrderStatus.requestInt64Paths.body, [ + { path: ["nonce"], allowString: true }, + { path: ["order_id"], unsigned: true }, + ]); +}); + +function assertModuleMetadata( + operations: Record, + expectedCount: number, +): void { + const entries = Object.entries(operations); + assert.equal(entries.length, expectedCount); + assert.equal(new Set(entries.map(([operationId]) => operationId)).size, expectedCount); + for (const [, operation] of entries) { + assert.deepEqual(Object.keys(operation).sort(), metadataKeys); + assert.equal(operation.successStatuses.length > 0, true); + assert.equal(operation.responseContentTypes.length > 0, true); + } +} + +function testClient(): { sdk: GeminiMarkets; requests: Request[]; fileBytes: Uint8Array } { + const requests: Request[] = []; + const fileBytes = new Uint8Array([1, 2, 3, 4]); + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const fetchImpl: FetchLike = async (url, init) => { + const pathname = new URL(url).pathname; + requests.push({ url, init }); + if (pathname === "/v1/margin/rates") { + return { + status: 200, + headers: { get: (name: string) => name.toLowerCase() === "content-type" ? "application/json" : null }, + async text() { return '{"rates":[]}'; }, + }; + } + if ( + pathname === "/v1/perpetuals/fundingPayment" || + pathname === "/v1/perpetuals/fundingpaymentreport/records.json" || + pathname === "/v1/account/list" || + pathname === "/v1/balances" || + pathname === "/v1/addresses/ethereum" || + pathname === "/v1/orders" || + pathname === "/v1/orders/history" || + pathname === "/v1/mytrades" || + pathname === "/v1/staking/history" || + pathname === "/v1/tradevolume" || + pathname === "/v2/transfers" + ) { + return { + status: 200, + headers: { get: (name: string) => name.toLowerCase() === "content-type" ? "application/json" : null }, + async text() { return "[]"; }, + }; + } + if (pathname === "/v1/transactions") { + return { + status: 200, + headers: { get: (name: string) => name.toLowerCase() === "content-type" ? "application/json" : null }, + async text() { return '{"results":[]}'; }, + }; + } + if (pathname.endsWith(".xlsx")) { + return { + status: 200, + headers: { + get(name: string) { + const headers: Record = { + "content-disposition": "attachment; filename=funding-payment-report.xlsx", + "content-type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }; + return headers[name.toLowerCase()] ?? null; + }, + }, + async text() { throw new Error("file smoke response should not be read as text"); }, + async arrayBuffer() { + return fileBytes.buffer.slice( + fileBytes.byteOffset, + fileBytes.byteOffset + fileBytes.byteLength, + ); + }, + }; + } + return { + status: 200, + headers: { get: (name: string) => name.toLowerCase() === "content-type" ? "application/json" : null }, + async text() { return "{}"; }, + }; + }; + const sdk = new GeminiMarkets({ + env: "sandbox", + auth, + fetchImpl, + } as never); + return { sdk, requests, fileBytes }; +} + +function payload(request: Request): Record { + return parseLosslessJson( + fromBase64(request.init.headers["X-GEMINI-PAYLOAD"]!), + ) as Record; +} + +async function assertSigned(request: Request): Promise { + const encoded = request.init.headers["X-GEMINI-PAYLOAD"]!; + assert.equal(request.init.headers["X-GEMINI-APIKEY"], "key"); + assert.equal( + request.init.headers["X-GEMINI-SIGNATURE"], + await hmacSha384Hex("secret", encoded), + ); + assert.equal(request.init.headers["Content-Length"], "0"); + assert.equal(request.init.headers["Content-Type"], "text/plain"); + assert.equal(request.init.body, undefined); +} + +test("generated REST operation metadata covers the new module surfaces", () => { + assertModuleMetadata(TRADING_OPERATIONS, 12); + assertModuleMetadata(MARGIN_OPERATIONS, 3); + assertModuleMetadata(PERPETUALS_OPERATIONS, 6); + assertModuleMetadata(ACCOUNT_SERVICES_OPERATIONS, 28); + assertModuleMetadata(CLEARING_INSTANT_OPERATIONS, 10); + + assert.equal(TRADING_OPERATIONS.wrapOrder.path, "/v1/wrap/{symbol}"); + assert.deepEqual(TRADING_OPERATIONS.wrapOrder.parameters, [ + { name: "symbol", in: "path", required: true, style: "simple", explode: false }, + ]); + assert.equal(MARGIN_OPERATIONS.previewMarginOrder.path, "/v1/margin/order/preview"); + assert.equal(PERPETUALS_OPERATIONS.getRiskStats.access, "public"); + assert.equal(PERPETUALS_OPERATIONS.getFundingPaymentReportFile.method, "get"); + assert.equal(PERPETUALS_OPERATIONS.getFundingPaymentReportFile.requestBodyRequired, false); + assert.equal(PERPETUALS_OPERATIONS.getFundingPaymentReportFile.responseMode, "file"); + assert.equal(ACCOUNT_SERVICES_OPERATIONS.listStakingRates.access, "public"); + assert.equal(ACCOUNT_SERVICES_OPERATIONS.withdrawCryptoFunds.path, "/v2/withdraw/{network}/{ticker}"); + assert.equal(CLEARING_INSTANT_OPERATIONS.createNewClearingOrder.path, "/v1/clearing/new"); + assert.equal(CLEARING_INSTANT_OPERATIONS.getInstantQuote.path, "/v1/instant/quote"); +}); + +test("package barrel exports generated REST client aliases", () => { + assert.equal(TradingClient, TradingRest); + assert.equal(MarginClient, MarginRest); + assert.equal(PerpetualsClient, PerpetualsRest); + assert.equal(AccountServicesClient, AccountServicesRest); + assert.equal(ClearingInstantClient, ClearingInstantRest); + + const { sdk } = testClient(); + assert.equal(typeof sdk.trading.createNewOrder, "function"); + assert.equal(typeof sdk.margin.previewMarginOrder, "function"); + assert.equal(typeof sdk.perpetuals.getRiskStats, "function"); + assert.equal(typeof sdk.accountServices.listStakingRates, "function"); + assert.equal(typeof sdk.clearingInstant.getInstantQuote, "function"); + sdk.close(); +}); + +test("Trading wrappers shape signed requests without using the network", async () => { + const { sdk, requests } = testClient(); + + await sdk.trading.cancelAllActiveOrders({ account: "primary" }); + await sdk.trading.cancelAllSessionOrders({ account: "primary" }); + await sdk.trading.cancelOrder({ order_id: 123, account: "primary" }); + await sdk.trading.createNewOrder({ + symbol: "btcusd", + amount: "1", + price: "100", + side: "buy", + type: "exchange limit", + client_order_id: "codex-no-network", + account: "primary", + }); + await sdk.trading.getNotionalTradingVolume({ account: "primary" }); + await sdk.trading.getOrderStatus({ + order_id: 123, + include_trades: true, + account: "primary", + }); + await sdk.trading.getTradingVolume({ account: "primary" }); + await sdk.trading.listActiveOrders({ account: "primary" }); + await sdk.trading.listPastOrders({ + symbol: "btcusd", + limit_orders: 10, + timestamp: "1700000000000", + account: "primary", + }); + await sdk.trading.listPastTrades({ + symbol: "btcusd", + limit_trades: 10, + timestamp: "1700000000000", + account: "primary", + }); + await sdk.trading.sendHeartbeat({}); + await sdk.trading.wrapOrder({ + path: { symbol: "GUSDUSD" }, + body: { + amount: "1", + side: "buy", + client_order_id: "codex-wrap-no-network", + account: "primary", + }, + }); + + assert.deepEqual(requests.map(({ init }) => init.method), Array(12).fill("POST")); + assert.deepEqual(requests.map(({ url }) => new URL(url).pathname), [ + "/v1/order/cancel/all", + "/v1/order/cancel/session", + "/v1/order/cancel", + "/v1/order/new", + "/v1/notionalvolume", + "/v1/order/status", + "/v1/tradevolume", + "/v1/orders", + "/v1/orders/history", + "/v1/mytrades", + "/v1/heartbeat", + "/v1/wrap/GUSDUSD", + ]); + assert.deepEqual(requests.map(({ url }) => new URL(url).search), Array(12).fill("")); + [ + { request: "/v1/order/cancel/all", nonce: 1000, account: "primary" }, + { request: "/v1/order/cancel/session", nonce: 1001, account: "primary" }, + { request: "/v1/order/cancel", nonce: 1002, order_id: 123, account: "primary" }, + { + request: "/v1/order/new", + nonce: 1003, + symbol: "btcusd", + amount: "1", + price: "100", + side: "buy", + type: "exchange limit", + client_order_id: "codex-no-network", + account: "primary", + }, + { request: "/v1/notionalvolume", nonce: 1004, account: "primary" }, + { + request: "/v1/order/status", + nonce: 1005, + order_id: 123, + include_trades: true, + account: "primary", + }, + { request: "/v1/tradevolume", nonce: 1006, account: "primary" }, + { request: "/v1/orders", nonce: 1007, account: "primary" }, + { + request: "/v1/orders/history", + nonce: 1008, + symbol: "btcusd", + limit_orders: 10, + timestamp: "1700000000000", + account: "primary", + }, + { + request: "/v1/mytrades", + nonce: 1009, + symbol: "btcusd", + limit_trades: 10, + timestamp: "1700000000000", + account: "primary", + }, + { request: "/v1/heartbeat", nonce: 1010 }, + { + request: "/v1/wrap/GUSDUSD", + nonce: 1011, + amount: "1", + side: "buy", + client_order_id: "codex-wrap-no-network", + account: "primary", + }, + ].forEach((expected, index) => assert.deepEqual(payload(requests[index]!), expected)); + for (const request of requests) await assertSigned(request); + sdk.close(); +}); + +test("Margin wrappers shape signed requests without using the network", async () => { + const { sdk, requests } = testClient(); + + await sdk.margin.getMarginAccount({ account: "primary" }); + await sdk.margin.getMarginRates({ account: "primary" }); + await sdk.margin.previewMarginOrder({ + symbol: "btcusd", + side: "buy", + type: "limit", + amount: "0.5", + price: "100", + }); + + assert.deepEqual(requests.map(({ init }) => init.method), ["POST", "POST", "POST"]); + assert.deepEqual(requests.map(({ url }) => new URL(url).pathname), [ + "/v1/margin/account", + "/v1/margin/rates", + "/v1/margin/order/preview", + ]); + assert.deepEqual(payload(requests[0]!), { + request: "/v1/margin/account", + nonce: 1000, + account: "primary", + }); + assert.deepEqual(payload(requests[1]!), { + request: "/v1/margin/rates", + nonce: 1001, + account: "primary", + }); + assert.deepEqual(payload(requests[2]!), { + request: "/v1/margin/order/preview", + nonce: 1002, + symbol: "btcusd", + side: "buy", + type: "limit", + amount: "0.5", + price: "100", + }); + for (const request of requests) await assertSigned(request); + sdk.close(); +}); + +test("Perpetuals wrappers shape public, authenticated JSON, and file requests", async () => { + const { sdk, requests, fileBytes } = testClient(); + + await sdk.perpetuals.getRiskStats({ symbol: "BTCGUSDPERP" }); + await sdk.perpetuals.getAccountMargin({ + account: "primary", + symbol: "BTCGUSDPERP", + }); + await sdk.perpetuals.getOpenPositions({ account: "primary" }); + await sdk.perpetuals.listFundingPayments({ + query: { since: 1700000000000n, to: 1700003600000n }, + body: { account: "primary" }, + }); + await sdk.perpetuals.getFundingPaymentReportJson({ + query: { fromDate: "2026-01-01", toDate: "2026-01-31", numRows: 10 }, + body: { account: "primary" }, + }); + const file = await sdk.perpetuals.getFundingPaymentReportFile({ + query: { fromDate: "2026-01-01", toDate: "2026-01-31", numRows: 10 }, + body: { account: "primary" }, + }); + + assert.deepEqual(requests.map(({ init }) => init.method), [ + "GET", + "POST", + "POST", + "POST", + "POST", + "GET", + ]); + assert.equal(requests[0]?.url, "https://api.sandbox.gemini.com/v1/riskstats/BTCGUSDPERP"); + assert.deepEqual(requests[0]?.init.headers, { Accept: "application/json" }); + assert.equal( + requests[1]?.url, + "https://api.sandbox.gemini.com/v1/margin", + ); + assert.equal( + requests[2]?.url, + "https://api.sandbox.gemini.com/v1/positions", + ); + assert.equal( + requests[3]?.url, + "https://api.sandbox.gemini.com/v1/perpetuals/fundingPayment?since=1700000000000&to=1700003600000", + ); + assert.equal( + requests[4]?.url, + "https://api.sandbox.gemini.com/v1/perpetuals/fundingpaymentreport/records.json?fromDate=2026-01-01&toDate=2026-01-31&numRows=10", + ); + assert.equal( + requests[5]?.url, + "https://api.sandbox.gemini.com/v1/perpetuals/fundingpaymentreport/records.xlsx?fromDate=2026-01-01&toDate=2026-01-31&numRows=10", + ); + assert.deepEqual(payload(requests[1]!), { + request: "/v1/margin", + account: "primary", + symbol: "BTCGUSDPERP", + nonce: 1000, + }); + assert.deepEqual(payload(requests[2]!), { + request: "/v1/positions", + account: "primary", + nonce: 1001, + }); + assert.deepEqual(payload(requests[3]!), { + request: "/v1/perpetuals/fundingPayment", + account: "primary", + nonce: 1002, + }); + assert.deepEqual(payload(requests[4]!), { + request: "/v1/perpetuals/fundingpaymentreport/records.json", + account: "primary", + nonce: 1003, + }); + assert.deepEqual(payload(requests[5]!), { + request: "/v1/perpetuals/fundingpaymentreport/records.xlsx", + account: "primary", + nonce: 1004, + }); + assert.deepEqual(file.bytes, fileBytes); + assert.equal( + file.contentType, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ); + assert.equal(file.contentDisposition, "attachment; filename=funding-payment-report.xlsx"); + for (const request of requests.slice(1)) await assertSigned(request); + sdk.close(); +}); + +test("Account Services wrappers shape public, signed read, and mutation requests", async () => { + const { sdk, requests } = testClient(); + + await sdk.accountServices.addBank({ + accountnumber: "123456789", + routing: "021000021", + type: "checking", + name: "Codex Test", + account: "primary", + }); + await sdk.accountServices.addBankCAD({ + swiftcode: "BOFMCAM2", + accountNumber: "1234567", + type: "checking", + name: "Codex CAD Test", + account: "primary", + }); + await sdk.accountServices.createNewAccount({ + name: "Codex Test Account", + type: "exchange", + }); + await sdk.accountServices.createNewApprovedAddress({ + path: { network: "ethereum" }, + body: { address: "0xabc", label: "codex", account: "primary" }, + }); + await sdk.accountServices.createNewDepositAddress({ + path: { network: "ethereum" }, + body: { label: "codex", account: "primary" }, + }); + await sdk.accountServices.getAccountDetail({ account: "primary" }); + await sdk.accountServices.getAvailableBalances({ + account: "primary", + showPendingBalances: false, + }); + await sdk.accountServices.getGasFeeEstimation({ + path: { network: "ethereum", ticker: "eth" }, + body: { address: "0xabc", amount: "1", account: "primary" }, + }); + await sdk.accountServices.getNotionalBalances({ + path: { currency: "usd" }, + body: { account: "primary" }, + }); + await sdk.accountServices.getRoles({}); + await sdk.accountServices.getTransactionHistory({ limit: 10 }); + await sdk.accountServices.listAccountsInGroup({ limit_accounts: 10 }); + await sdk.accountServices.listApprovedAddresses({ + path: { network: "ethereum" }, + body: { account: "primary" }, + }); + await sdk.accountServices.listCustodyFeeTransfers({ + limit_transfers: 10, + account: "primary", + }); + await sdk.accountServices.listDepositAddresses({ + path: { network: "ethereum" }, + body: { timestamp: "1700000000000", account: "primary" }, + }); + await sdk.accountServices.listPastTransfers({ + currency: "eth", + network: "ethereum", + limit_transfers: 10, + account: "primary", + }); + await sdk.accountServices.listPaymentMethods({ account: "primary" }); + await sdk.accountServices.listStakingBalances({ account: "primary" }); + await sdk.accountServices.listStakingEventHistory({ + account: "primary", + since: "2026-01-01T00:00:00.000Z", + limit: 10, + }); + await sdk.accountServices.listStakingRates(); + await sdk.accountServices.listStakingRewards({ + account: "primary", + since: "2026-01-01T00:00:00.000Z", + }); + await sdk.accountServices.removeApprovedAddress({ + path: { network: "ethereum" }, + body: { address: "0xabc", account: "primary" }, + }); + await sdk.accountServices.renameAccount({ + account: "primary", + newName: "Codex Test Renamed", + newAccount: "codex-test-renamed", + }); + await sdk.accountServices.revokeOAuthToken({}); + await sdk.accountServices.stakeCryptoFunds({ + account: "primary", + providerId: "provider-1", + currency: "eth", + amount: "0.1", + }); + await sdk.accountServices.transferBetweenAccounts({ + path: { currency: "usd" }, + body: { + sourceAccount: "primary", + targetAccount: "secondary", + amount: "1.00", + clientTransferId: "aa97b177-9383-4934-8543-0f91a7a02838", + }, + }); + await sdk.accountServices.unstakeCryptoFunds({ + account: "primary", + providerId: "provider-1", + currency: "eth", + amount: "0.1", + }); + await sdk.accountServices.withdrawCryptoFunds({ + path: { network: "ethereum", ticker: "eth" }, + body: { + address: "0xabc", + amount: "1.25", + clientTransferId: "aa97b177-9383-4934-8543-0f91a7a02839", + }, + }); + + assert.deepEqual(requests.map(({ init }) => init.method), [ + ...Array(19).fill("POST"), + "GET", + ...Array(8).fill("POST"), + ]); + assert.deepEqual(requests.map(({ url }) => new URL(url).pathname), [ + "/v1/payments/addbank", + "/v1/payments/addbank/cad", + "/v1/account/create", + "/v1/approvedAddresses/ethereum/request", + "/v1/deposit/ethereum/newAddress", + "/v1/account", + "/v1/balances", + "/v2/withdraw/ethereum/eth/feeEstimate", + "/v1/notionalbalances/usd", + "/v1/roles", + "/v1/transactions", + "/v1/account/list", + "/v1/approvedAddresses/account/ethereum", + "/v1/custodyaccountfees", + "/v1/addresses/ethereum", + "/v2/transfers", + "/v1/payments/methods", + "/v1/balances/staking", + "/v1/staking/history", + "/v1/staking/rates", + "/v1/staking/rewards", + "/v1/approvedAddresses/ethereum/remove", + "/v1/account/rename", + "/v1/oauth/revokeByToken", + "/v1/staking/stake", + "/v1/account/transfer/usd", + "/v1/staking/unstake", + "/v2/withdraw/ethereum/eth", + ]); + assert.deepEqual(requests.map(({ url }) => new URL(url).search), Array(28).fill("")); + + const expectedPayloads: (Record | undefined)[] = [ + { + request: "/v1/payments/addbank", + nonce: 1000, + accountnumber: "123456789", + routing: "021000021", + type: "checking", + name: "Codex Test", + account: "primary", + }, + { + request: "/v1/payments/addbank/cad", + nonce: 1001, + swiftcode: "BOFMCAM2", + accountNumber: "1234567", + type: "checking", + name: "Codex CAD Test", + account: "primary", + }, + { + request: "/v1/account/create", + nonce: 1002, + name: "Codex Test Account", + type: "exchange", + }, + { + request: "/v1/approvedAddresses/ethereum/request", + nonce: 1003, + address: "0xabc", + label: "codex", + account: "primary", + }, + { + request: "/v1/deposit/ethereum/newAddress", + nonce: 1004, + label: "codex", + account: "primary", + }, + { request: "/v1/account", nonce: 1005, account: "primary" }, + { + request: "/v1/balances", + nonce: 1006, + account: "primary", + showPendingBalances: false, + }, + { + request: "/v2/withdraw/ethereum/eth/feeEstimate", + nonce: 1007, + address: "0xabc", + amount: "1", + account: "primary", + }, + { request: "/v1/notionalbalances/usd", nonce: 1008, account: "primary" }, + { request: "/v1/roles", nonce: 1009 }, + { request: "/v1/transactions", nonce: 1010, limit: 10 }, + { request: "/v1/account/list", nonce: 1011, limit_accounts: 10 }, + { request: "/v1/approvedAddresses/account/ethereum", nonce: 1012, account: "primary" }, + { + request: "/v1/custodyaccountfees", + nonce: 1013, + limit_transfers: 10, + account: "primary", + }, + { + request: "/v1/addresses/ethereum", + nonce: 1014, + timestamp: "1700000000000", + account: "primary", + }, + { + request: "/v2/transfers", + nonce: 1015, + currency: "eth", + network: "ethereum", + limit_transfers: 10, + account: "primary", + }, + { request: "/v1/payments/methods", nonce: 1016, account: "primary" }, + { request: "/v1/balances/staking", nonce: 1017, account: "primary" }, + { + request: "/v1/staking/history", + nonce: 1018, + account: "primary", + since: "2026-01-01T00:00:00.000Z", + limit: 10, + }, + undefined, + { + request: "/v1/staking/rewards", + nonce: 1019, + account: "primary", + since: "2026-01-01T00:00:00.000Z", + }, + { + request: "/v1/approvedAddresses/ethereum/remove", + nonce: 1020, + address: "0xabc", + account: "primary", + }, + { + request: "/v1/account/rename", + nonce: 1021, + account: "primary", + newName: "Codex Test Renamed", + newAccount: "codex-test-renamed", + }, + { request: "/v1/oauth/revokeByToken", nonce: 1022 }, + { + request: "/v1/staking/stake", + nonce: 1023, + account: "primary", + providerId: "provider-1", + currency: "eth", + amount: "0.1", + }, + { + request: "/v1/account/transfer/usd", + nonce: 1024, + sourceAccount: "primary", + targetAccount: "secondary", + amount: "1.00", + clientTransferId: "aa97b177-9383-4934-8543-0f91a7a02838", + }, + { + request: "/v1/staking/unstake", + nonce: 1025, + account: "primary", + providerId: "provider-1", + currency: "eth", + amount: "0.1", + }, + { + request: "/v2/withdraw/ethereum/eth", + nonce: 1026, + address: "0xabc", + amount: "1.25", + clientTransferId: "aa97b177-9383-4934-8543-0f91a7a02839", + }, + ]; + for (const [index, expected] of expectedPayloads.entries()) { + if (expected === undefined) { + assert.equal(requests[index]?.url, "https://api.sandbox.gemini.com/v1/staking/rates"); + assert.deepEqual(requests[index]?.init.headers, { Accept: "application/json" }); + continue; + } + assert.deepEqual(payload(requests[index]!), expected); + await assertSigned(requests[index]!); + } + sdk.close(); +}); + +test("Clearing and Instant wrappers shape signed requests without using the network", async () => { + const { sdk, requests } = testClient(); + + await sdk.clearingInstant.cancelClearingOrder({ + clearing_id: "CLEARING-123", + account: "primary", + }); + await sdk.clearingInstant.confirmClearingOrder({ + clearing_id: "CLEARING-123", + symbol: "btcusd", + amount: "1", + price: "100", + side: "sell", + account: "primary", + }); + await sdk.clearingInstant.createNewBrokerOrder({ + source_counterparty_id: "SOURCE-CP", + target_counterparty_id: "TARGET-CP", + symbol: "ethusd", + amount: "1", + expires_in_hrs: 1, + price: "200", + side: "sell", + account: "primary", + }); + await sdk.clearingInstant.createNewClearingOrder({ + symbol: "btcusd", + amount: "1", + price: "100", + side: "buy", + counterparty_id: "COUNTERPARTY-1", + expires_in_hrs: 24, + account: "primary", + }); + await sdk.clearingInstant.executeInstantOrder({ + symbol: "btcusd", + side: "buy", + quantity: "0.01505181", + price: "6445.07", + fee: "2.9900309233", + quoteId: 1328, + account: "primary", + }); + await sdk.clearingInstant.getClearingOrder({ + clearing_id: "CLEARING-123", + account: "primary", + }); + await sdk.clearingInstant.getInstantQuote({ + side: "buy", + symbol: "btcusd", + totalSpend: "100", + account: "primary", + }); + await sdk.clearingInstant.listClearingBrokers({ + symbol: "btcusd", + limit_orders: 10, + account: "primary", + }); + await sdk.clearingInstant.listClearingOrders({ + symbol: "btcusd", + counterparty: "COUNTERPARTY-1", + limit_orders: 10, + account: "primary", + }); + await sdk.clearingInstant.listClearingTrades({ + symbol: "btcusd", + limit_per_account: 10, + account: "primary", + }); + + assert.deepEqual(requests.map(({ init }) => init.method), Array(10).fill("POST")); + assert.deepEqual(requests.map(({ url }) => new URL(url).pathname), [ + "/v1/clearing/cancel", + "/v1/clearing/confirm", + "/v1/clearing/broker/new", + "/v1/clearing/new", + "/v1/instant/execute", + "/v1/clearing/status", + "/v1/instant/quote", + "/v1/clearing/broker/list", + "/v1/clearing/list", + "/v1/clearing/trades", + ]); + assert.deepEqual(requests.map(({ url }) => new URL(url).search), Array(10).fill("")); + [ + { + request: "/v1/clearing/cancel", + nonce: 1000, + clearing_id: "CLEARING-123", + account: "primary", + }, + { + request: "/v1/clearing/confirm", + nonce: 1001, + clearing_id: "CLEARING-123", + symbol: "btcusd", + amount: "1", + price: "100", + side: "sell", + account: "primary", + }, + { + request: "/v1/clearing/broker/new", + nonce: 1002, + source_counterparty_id: "SOURCE-CP", + target_counterparty_id: "TARGET-CP", + symbol: "ethusd", + amount: "1", + expires_in_hrs: 1, + price: "200", + side: "sell", + account: "primary", + }, + { + request: "/v1/clearing/new", + nonce: 1003, + symbol: "btcusd", + amount: "1", + price: "100", + side: "buy", + counterparty_id: "COUNTERPARTY-1", + expires_in_hrs: 24, + account: "primary", + }, + { + request: "/v1/instant/execute", + nonce: 1004, + symbol: "btcusd", + side: "buy", + quantity: "0.01505181", + price: "6445.07", + fee: "2.9900309233", + quoteId: 1328, + account: "primary", + }, + { + request: "/v1/clearing/status", + nonce: 1005, + clearing_id: "CLEARING-123", + account: "primary", + }, + { + request: "/v1/instant/quote", + nonce: 1006, + side: "buy", + symbol: "btcusd", + totalSpend: "100", + account: "primary", + }, + { + request: "/v1/clearing/broker/list", + nonce: 1007, + symbol: "btcusd", + limit_orders: 10, + account: "primary", + }, + { + request: "/v1/clearing/list", + nonce: 1008, + symbol: "btcusd", + counterparty: "COUNTERPARTY-1", + limit_orders: 10, + account: "primary", + }, + { + request: "/v1/clearing/trades", + nonce: 1009, + symbol: "btcusd", + limit_per_account: 10, + account: "primary", + }, + ].forEach((expected, index) => assert.deepEqual(payload(requests[index]!), expected)); + for (const request of requests) await assertSigned(request); + sdk.close(); +}); diff --git a/packages/sdk-typescript/src/tests/heartbeat.test.ts b/packages/sdk-typescript/src/tests/heartbeat.test.ts new file mode 100644 index 0000000..d3447f1 --- /dev/null +++ b/packages/sdk-typescript/src/tests/heartbeat.test.ts @@ -0,0 +1,70 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { ManagedHeartbeat } from "../heartbeat.js"; + +test("managed heartbeat does not run until start and stops future beats", async (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + let calls = 0; + const heartbeat = new ManagedHeartbeat({ + intervalMs: 100, + beat: async () => { calls++; }, + }); + + assert.equal(calls, 0); + heartbeat.start(); + await Promise.resolve(); + assert.equal(calls, 1); + + heartbeat.stop(); + t.mock.timers.tick(500); + assert.equal(calls, 1); +}); + +test("managed heartbeat reports failed beats without creating unhandled rejections", async () => { + const errors: unknown[] = []; + const heartbeat = new ManagedHeartbeat({ + intervalMs: 100, + beat: async () => { throw new Error("heartbeat failed"); }, + onError: (error) => errors.push(error), + }); + + heartbeat.start(); + await Promise.resolve(); + assert.equal((errors[0] as Error).message, "heartbeat failed"); + heartbeat.stop(); +}); + +test("stopping a heartbeat aborts its in-flight beat", async () => { + let aborted = false; + const heartbeat = new ManagedHeartbeat({ + intervalMs: 100, + beat: ({ signal }) => new Promise((resolve) => { + signal?.addEventListener("abort", () => { aborted = true; resolve(); }, { once: true }); + }), + }); + + heartbeat.start(); + await Promise.resolve(); + heartbeat.stop(); + assert.equal(aborted, true); +}); + +test("managed heartbeat preserves the caller abort signal", async () => { + const external = new AbortController(); + let aborted = false; + const heartbeat = new ManagedHeartbeat({ + intervalMs: 100, + requestOptions: { signal: external.signal }, + beat: ({ signal }) => new Promise((resolve) => { + signal?.addEventListener("abort", () => { aborted = true; resolve(); }, { once: true }); + }), + }); + + heartbeat.start(); + await Promise.resolve(); + external.abort(); + await Promise.resolve(); + assert.equal(aborted, true); + heartbeat.stop(); +}); diff --git a/packages/sdk-typescript/src/tests/hmac-auth.test.ts b/packages/sdk-typescript/src/tests/hmac-auth.test.ts new file mode 100644 index 0000000..6196009 --- /dev/null +++ b/packages/sdk-typescript/src/tests/hmac-auth.test.ts @@ -0,0 +1,187 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { + HmacAuth, + HttpTransport, + SdkError, + type FetchLike, + type HmacAuthOptions, +} from "../server/index.js"; +import { fromBase64 } from "../core/encoding.js"; + +test("HmacAuth signs the exact base64 payload with HMAC-SHA384", async () => { + const auth = new HmacAuth({ + apiKey: "test-key", + apiSecret: "test-secret", + }); + const payload = + "eyJyZXF1ZXN0IjoiL3YxL3ByZWRpY3Rpb24tbWFya2V0cy9vcmRlciIsIm5vbmNlIjoxNzAwMDAwMDAwMDAwfQ=="; + + assert.deepEqual(await auth.credentialHeaders(payload), { + "X-GEMINI-APIKEY": "test-key", + "X-GEMINI-SIGNATURE": + "4a665b714370dde25f1505aa89fc9f79830b9e071dd4f2276ec7913ef52edf2ba9911afbaa47b8a99bb4a4f32f273121", + }); +}); + +test("default nonces strictly increase within one millisecond", () => { + const auth = new HmacAuth({ + apiKey: "key-a", + apiSecret: "secret-a", + now: () => 1_700_000_000_000, + }); + + assert.deepEqual( + [auth.nextNonce(), auth.nextNonce(), auth.nextNonce()], + ["1700000000000", "1700000000001", "1700000000002"], + ); +}); + +test("default nonce follows clock advances and survives clock regression", () => { + let now = 1_700_000_000_000; + const auth = new HmacAuth({ + apiKey: "key-a", + apiSecret: "secret-a", + now: () => now, + }); + + assert.equal(auth.nextNonce(), "1700000000000"); + now += 100; + assert.equal(auth.nextNonce(), "1700000000100"); + now -= 200; + assert.equal(auth.nextNonce(), "1700000000101"); +}); + +test("separate API-key sessions have independent nonce state", () => { + const options = { apiSecret: "secret", now: () => 1_700_000_000_000 }; + const first = new HmacAuth({ ...options, apiKey: "key-a" }); + const second = new HmacAuth({ ...options, apiKey: "key-b" }); + + assert.equal(first.nextNonce(), "1700000000000"); + assert.equal(first.nextNonce(), "1700000000001"); + assert.equal(second.nextNonce(), "1700000000000"); +}); + +test("time-based nonce mode emits epoch seconds for time-based session keys", () => { + const auth = new HmacAuth({ + apiKey: "time-key", + apiSecret: "secret", + nonceMode: "time-based", + now: () => 1_700_000_000_123, + }); + + assert.equal(auth.nextNonce(), "1700000000"); + assert.equal(auth.nextNonce(), "1700000000"); +}); + +test("credentials are not exposed as enumerable object state", () => { + const auth = new HmacAuth({ apiKey: "private-key", apiSecret: "private-secret" }); + + assert.deepEqual(Object.keys(auth), []); + assert.equal(JSON.stringify(auth), "{}"); +}); + +test("HttpTransport sends the exact payload signed by HmacAuth", async () => { + const payload = + "eyJyZXF1ZXN0IjoiL3YxL3ByZWRpY3Rpb24tbWFya2V0cy9vcmRlciIsIm5vbmNlIjoxNzAwMDAwMDAwMDAwfQ=="; + const signature = + "4a665b714370dde25f1505aa89fc9f79830b9e071dd4f2276ec7913ef52edf2ba9911afbaa47b8a99bb4a4f32f273121"; + let headers: Record | undefined; + const fetchImpl: FetchLike = async (_url, init) => { + headers = init.headers; + return { status: 200, text: async () => "{}" }; + }; + const auth = new HmacAuth({ + apiKey: "test-key", + apiSecret: "test-secret", + now: () => 1_700_000_000_000, + }); + const client = new HttpTransport({ env: "sandbox", auth, fetchImpl }); + + await client.request({ method: "POST", path: "/v1/prediction-markets/order" }); + + assert.equal(headers?.["X-GEMINI-PAYLOAD"], payload); + assert.equal(headers?.["X-GEMINI-SIGNATURE"], signature); + assert.equal( + JSON.parse(fromBase64(payload)).nonce, + 1_700_000_000_000, + ); +}); + +test("concurrent requests use unique increasing nonces", async () => { + const nonces: number[] = []; + const fetchImpl: FetchLike = async (_url, init) => { + const payload = JSON.parse( + fromBase64(init.headers["X-GEMINI-PAYLOAD"]), + ); + nonces.push(payload.nonce); + return { status: 200, text: async () => "{}" }; + }; + const auth = new HmacAuth({ + apiKey: "key", + apiSecret: "secret", + now: () => 1_700_000_000_000, + }); + const client = new HttpTransport({ env: "sandbox", auth, fetchImpl }); + + await Promise.all([ + client.request({ method: "POST", path: "/v1/a" }), + client.request({ method: "POST", path: "/v1/b" }), + client.request({ method: "POST", path: "/v1/c" }), + ]); + + assert.deepEqual(nonces, [1_700_000_000_000, 1_700_000_000_001, 1_700_000_000_002]); +}); + +test("HmacAuth rejects missing credentials without echoing credential values", () => { + assert.throws( + () => new HmacAuth({ apiKey: "", apiSecret: "not-for-errors" }), + (error: unknown) => error instanceof SdkError && !error.message.includes("not-for-errors"), + ); + assert.throws( + () => new HmacAuth({ apiKey: "not-for-errors", apiSecret: "" }), + (error: unknown) => error instanceof SdkError && !error.message.includes("not-for-errors"), + ); +}); + +test("HmacAuth rejects an invalid clock before emitting any nonce", () => { + for (const nonceMode of ["monotonic", "time-based"] as const) { + const auth = new HmacAuth({ + apiKey: "key", + apiSecret: "not-for-errors", + nonceMode, + now: () => Number.NaN, + }); + assert.throws( + () => auth.nextNonce(), + (error: unknown) => + error instanceof SdkError && !error.message.includes("not-for-errors"), + ); + } +}); + +test("HmacAuth rejects invalid runtime options with SdkError", () => { + const invalid = [ + null, + undefined, + { apiKey: 1, apiSecret: "secret" }, + { apiKey: "key", apiSecret: 1 }, + { apiKey: "key", apiSecret: "secret", nonceMode: "invalid" }, + { apiKey: "key", apiSecret: "secret", now: 1 }, + ] as unknown as HmacAuthOptions[]; + + for (const options of invalid) { + assert.throws(() => new HmacAuth(options), SdkError); + } +}); + +test("HmacAuth rejects unsafe clock values", () => { + const auth = new HmacAuth({ + apiKey: "key", + apiSecret: "secret", + now: () => Number.MAX_SAFE_INTEGER + 1, + }); + + assert.throws(() => auth.nextNonce(), SdkError); +}); diff --git a/packages/sdk-typescript/src/tests/json.test.ts b/packages/sdk-typescript/src/tests/json.test.ts new file mode 100644 index 0000000..c1514d6 --- /dev/null +++ b/packages/sdk-typescript/src/tests/json.test.ts @@ -0,0 +1,229 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { SdkError } from "../errors.js"; +import { + normalizeInt64Paths, + parseLosslessJson, + validateInt64RequestPaths, + type Int64Path, +} from "../json.js"; + +const obj = (text: string): Record => + parseLosslessJson(text) as Record; + +test("large integer beyond safe range becomes an exact bigint", () => { + const r = obj('{"u":9007199254740993}'); + assert.equal(typeof r.u, "bigint"); + assert.equal(r.u, 9007199254740993n); +}); + +test("19-digit E-style timestamp is preserved exactly as bigint", () => { + const r = obj('{"E":1700000000123456789}'); + assert.equal(typeof r.E, "bigint"); + assert.equal(r.E, 1700000000123456789n); +}); + +test("negative large integer becomes bigint", () => { + const r = obj('{"n":-9007199254740993}'); + assert.equal(typeof r.n, "bigint"); + assert.equal(r.n, -9007199254740993n); +}); + +test("MAX_SAFE_INTEGER stays a number (boundary)", () => { + const r = obj('{"n":9007199254740991}'); + assert.equal(typeof r.n, "number"); + assert.equal(r.n, 9007199254740991); +}); + +test("MAX_SAFE_INTEGER + 2 crosses to bigint (boundary)", () => { + const r = obj('{"n":9007199254740993}'); + assert.equal(typeof r.n, "bigint"); +}); + +test("small integers and zero stay numbers", () => { + const r = obj('{"a":0,"b":42,"c":-7}'); + assert.equal(r.a, 0); + assert.equal(r.b, 42); + assert.equal(r.c, -7); + assert.equal(typeof r.b, "number"); +}); + +test("decimals stay numbers (not integers, never bigint)", () => { + const r = obj('{"p":1.5,"q":0.0000001}'); + assert.equal(typeof r.p, "number"); + assert.equal(r.p, 1.5); + assert.equal(typeof r.q, "number"); + assert.equal(r.q, 0.0000001); +}); + +test("exponent-form numbers stay numbers (not integer literals)", () => { + const r = obj('{"n":1e21}'); + assert.equal(typeof r.n, "number"); + assert.equal(r.n, 1e21); +}); + +test("numeric-looking strings (prices) are left untouched", () => { + const r = obj('{"price":"0.26","qty":"9007199254740993"}'); + assert.equal(r.price, "0.26"); + assert.equal(r.qty, "9007199254740993"); + assert.equal(typeof r.qty, "string"); +}); + +test("a depth-frame shape: ids become bigint, prices stay strings", () => { + // U/u here are deliberately above 2^53 to exercise preservation of large ids. + const r = obj( + '{"U":9007199254740993,"u":9007199254740999,"E":1700000000123456789,' + + '"b":[["0.26","1500"],["0.25","0"]],"a":[["0.27","800"]]}', + ); + assert.equal(typeof r.U, "bigint"); + assert.equal(typeof r.u, "bigint"); + assert.equal(typeof r.E, "bigint"); + assert.deepEqual(r.b, [ + ["0.26", "1500"], + ["0.25", "0"], + ]); + assert.deepEqual(r.a, [["0.27", "800"]]); +}); + +test("large integer inside an array is preserved", () => { + const r = parseLosslessJson("[1, 9007199254740993, 3]") as unknown[]; + assert.equal(r[0], 1); + assert.equal(typeof r[1], "bigint"); + assert.equal(r[1], 9007199254740993n); +}); + +test("regression: plain JSON.parse loses precision where this does not", () => { + const text = '{"u":9007199254740993}'; + // Plain parse silently rounds to 2^53 — the bug this exists to prevent. + assert.equal((JSON.parse(text) as { u: number }).u, 9007199254740992); + // Lossless parse keeps every digit. + assert.equal(obj(text).u, 9007199254740993n); +}); + +test("normalizes only int64 leaves selected by schema paths", () => { + const value = { + orderId: 12345678, + count: 4, + unrealizedPct: 23.81, + price: "0.65", + results: [ + { order: { orderId: 9007199254740993n } }, + { orderId: 42 }, + ], + }; + const paths: readonly Int64Path[] = [ + ["orderId"], + ["results", "*", "order", "orderId"], + ["results", "*", "orderId"], + ]; + + const normalized = normalizeInt64Paths(value, paths); + + assert.deepEqual(normalized, { + orderId: 12345678n, + count: 4, + unrealizedPct: 23.81, + price: "0.65", + results: [ + { order: { orderId: 9007199254740993n } }, + { orderId: 42n }, + ], + }); +}); + +test("leaves missing optional paths and nullable ancestors unchanged", () => { + const value = { missing: {}, nullable: null }; + + assert.deepEqual( + normalizeInt64Paths(value, [["missing", "orderId"], ["nullable", "orderId"]]), + value, + ); +}); + +test("preserves null at an int64 response leaf", () => { + assert.deepEqual( + normalizeInt64Paths({ orderId: null }, [["orderId"]]), + { orderId: null }, + ); +}); + +test("rejects a fractional number at an int64 leaf with its path", () => { + assert.throws( + () => + normalizeInt64Paths( + { results: [{ orderId: 4.2 }] }, + [["results", "*", "orderId"]], + ), + (error) => + error instanceof SdkError && + error.message === "expected int64 at results[0].orderId", + ); +}); + +test("normalizes numeric strings at int64 leaves", () => { + assert.deepEqual( + normalizeInt64Paths({ orderId: "42" }, [["orderId"]]), + { orderId: 42n }, + ); +}); + +test("accepts safe request numbers and schema-approved numeric strings", () => { + const value = { orderId: Number.MAX_SAFE_INTEGER, legacyId: "9007199254740993" }; + assert.doesNotThrow(() => validateInt64RequestPaths( + value, + [ + { path: ["orderId"] }, + { path: ["legacyId"], allowString: true }, + ], + "trading.getOrderStatus", + )); + assert.deepEqual(value, { orderId: Number.MAX_SAFE_INTEGER, legacyId: "9007199254740993" }); +}); + +test("rejects unsafe request numbers with stable validation metadata", () => { + assert.throws( + () => validateInt64RequestPaths( + { orderId: Number.MAX_SAFE_INTEGER + 1 }, + [{ path: ["orderId"] }], + "trading.getOrderStatus", + ), + (error) => + error instanceof SdkError && + error.name === "ValidationError" && + "operation" in error && + error.operation === "trading.getOrderStatus" && + "field" in error && + error.field === "orderId" && + "rule" in error && + error.rule === "safe-integer", + ); +}); + +test("enforces the unsigned int64 request range", () => { + const max = 18446744073709551615n; + assert.doesNotThrow(() => validateInt64RequestPaths( + { orderId: max }, + [{ path: ["orderId"], unsigned: true }], + "trading.getOrderStatus", + )); + + for (const orderId of [-1n, max + 1n, -1]) { + assert.throws( + () => validateInt64RequestPaths( + { orderId }, + [{ path: ["orderId"], unsigned: true }], + "trading.getOrderStatus", + ), + (error) => + error instanceof SdkError && + error.name === "ValidationError" && + "operation" in error && + error.operation === "trading.getOrderStatus" && + "field" in error && + error.field === "orderId" && + "rule" in error && + error.rule === "unsigned-integer", + ); + } +}); diff --git a/packages/sdk-typescript/src/tests/live-order-book.test.ts b/packages/sdk-typescript/src/tests/live-order-book.test.ts new file mode 100644 index 0000000..922b867 --- /dev/null +++ b/packages/sdk-typescript/src/tests/live-order-book.test.ts @@ -0,0 +1,367 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { LiveOrderBook } from "../live-order-book.js"; +import { SdkError } from "../errors.js"; +import type { BookDelta } from "../types/client.js"; +import type { DepthUpdate } from "../websocket-types.js"; + +// A depth frame. The facade routes the fresh snapshot (first frame after a (re)subscribe ack) to +// applySnapshot(), and every other frame to ingest(). A snapshot has U == u; a diff need not. +function frame(over: Partial): DepthUpdate { + return { e: "depthUpdate", E: 0, s: "btcusd", U: 0, u: 0, b: [], a: [], ...over }; +} + +test("applySnapshot builds the book, goes live, and 'update' passes the book + full-book delta", () => { + const book = new LiveOrderBook("btcusd"); + const updates: Array<{ b: unknown; delta: BookDelta }> = []; + book.on("update", (b, delta) => updates.push({ b, delta })); + + book.applySnapshot(frame({ U: 100, u: 100, b: [["0.60", "5"], ["0.59", "3"]], a: [["0.61", "2"]] })); + + assert.equal(updates.length, 1); + assert.equal(updates[0].b, book, "'update' passes the emitting LiveOrderBook (public contract)"); + assert.deepEqual(book.bestBid(), { price: "0.6", qty: "5" }); + assert.deepEqual(book.bestAsk(), { price: "0.61", qty: "2" }); + assert.deepEqual(updates[0].delta, { + bids: [{ price: "0.6", qty: "5" }, { price: "0.59", qty: "3" }], + asks: [{ price: "0.61", qty: "2" }], + }); +}); + +test("a diff after the snapshot applies and 'update' carries only the changed levels", () => { + const book = new LiveOrderBook("btcusd"); + book.applySnapshot(frame({ U: 100, u: 100, b: [["0.60", "5"]], a: [["0.61", "2"]] })); + + const deltas: BookDelta[] = []; + book.on("update", (_b, d) => deltas.push(d)); + book.ingest(frame({ U: 100, u: 101, b: [["0.60", "7"]], a: [] })); + + assert.equal(deltas.length, 1); + assert.deepEqual(deltas[0], { bids: [{ price: "0.6", qty: "7" }], asks: [] }); + assert.deepEqual(book.bestBid(), { price: "0.6", qty: "7" }); +}); + +test("an ask-only diff updates the ask side and the delta carries the changed asks", () => { + const book = new LiveOrderBook("btcusd"); + book.applySnapshot(frame({ U: 100, u: 100, b: [["0.60", "5"]], a: [["0.61", "2"], ["0.62", "9"]] })); + + const deltas: BookDelta[] = []; + book.on("update", (_b, d) => deltas.push(d)); + book.ingest(frame({ U: 100, u: 101, b: [], a: [["0.61", "4"], ["0.62", "0"]] })); // reprice + remove + + assert.equal(deltas.length, 1); + assert.deepEqual( + deltas[0], + { bids: [], asks: [{ price: "0.61", qty: "4" }, { price: "0.62", qty: "0" }] }, + "ask deltas canonicalized, removal (qty 0) preserved", + ); + assert.deepEqual(book.bestAsk(), { price: "0.61", qty: "4" }); + assert.deepEqual(book.topN("asks", 5), [{ price: "0.61", qty: "4" }], "0.62 removed from the book"); +}); + +test("a sequence gap emits 'resync', goes stale, drops stray frames, and rebuilds via applySnapshot", () => { + const book = new LiveOrderBook("btcusd"); + book.applySnapshot(frame({ U: 100, u: 100, b: [["0.60", "5"]], a: [["0.61", "2"]] })); + + let resyncs = 0; + const updates: BookDelta[] = []; + book.on("resync", () => resyncs++); + book.on("update", (_b, d) => updates.push(d)); + + book.ingest(frame({ U: 105, u: 105, b: [["0.60", "9"]], a: [] })); // U skips ahead → gap + assert.equal(resyncs, 1); + assert.equal(updates.length, 0); + assert.equal(book.bestBid(), undefined, "stale"); + + // A stray queued diff — even one with U == u — arriving while stale is dropped by ingest (not live), + // so it can never be mistaken for the recovery snapshot. + book.ingest(frame({ U: 106, u: 106, b: [["9.99", "1"]], a: [] })); + assert.equal(updates.length, 0, "stray frame discarded"); + assert.equal(book.bestBid(), undefined, "still stale"); + + // The facade (post-resubscribe-ack) designates the fresh snapshot; it REPLACES the old book. + book.applySnapshot(frame({ U: 200, u: 200, b: [["0.70", "1"]], a: [["0.71", "1"]] })); + assert.equal(updates.length, 1); + const recovered = { bids: [{ price: "0.7", qty: "1" }], asks: [{ price: "0.71", qty: "1" }] }; + assert.deepEqual(book.snapshot(), recovered, "recovered book is the new snapshot only — no stale levels"); + assert.deepEqual(updates[0], recovered, "recovery 'update' carries the full new book, not a merge"); +}); + +test("markStale() (reconnect) goes stale; applySnapshot rebuilds", () => { + const book = new LiveOrderBook("btcusd"); + book.applySnapshot( + frame({ U: 100, u: 100, b: [["0.60", "5"], ["0.59", "4"]], a: [["0.61", "3"]] }), + ); + + let resyncs = 0; + const updates: BookDelta[] = []; + book.on("resync", () => resyncs++); + book.on("update", (_b, d) => updates.push(d)); + + book.markStale(); + assert.equal(resyncs, 1); + assert.equal(book.bestBid(), undefined, "stale after markStale"); + + book.applySnapshot(frame({ U: 200, u: 200, b: [["0.70", "9"]], a: [["0.71", "2"]] })); + assert.equal(updates.length, 1); + assert.deepEqual( + book.snapshot(), + { bids: [{ price: "0.7", qty: "9" }], asks: [{ price: "0.71", qty: "2" }] }, + "the reconnect snapshot replaces every old level", + ); +}); + +test("a stale diff (u <= lastUpdateId) is dropped and emits no 'update'", () => { + const book = new LiveOrderBook("btcusd"); + book.applySnapshot(frame({ U: 100, u: 100, b: [["0.60", "5"]], a: [] })); + book.ingest(frame({ U: 100, u: 102, b: [["0.60", "9"]], a: [] })); // -> lastU 102 + + const updates: BookDelta[] = []; + book.on("update", (_b, d) => updates.push(d)); + book.ingest(frame({ U: 100, u: 101, b: [["0.60", "5"]], a: [] })); // stale duplicate + + assert.equal(updates.length, 0, "a stale, dropped diff must not emit a delta"); + assert.deepEqual(book.bestBid(), { price: "0.6", qty: "9" }, "book unchanged — still at the u=102 state"); +}); + +test("a malformed level on a live book surfaces an 'error', goes stale, and recovers", () => { + const book = new LiveOrderBook("btcusd"); + book.applySnapshot(frame({ U: 100, u: 100, b: [["0.60", "5"]], a: [["0.61", "2"]] })); + + const errors: unknown[] = []; + let resyncs = 0; + book.on("error", (e) => errors.push(e)); + book.on("resync", () => resyncs++); + + book.ingest(frame({ U: 100, u: 101, b: [["not-a-price", "1"]], a: [] })); + + assert.equal(errors.length, 1, "a malformed level surfaces exactly one 'error'"); + assert.ok(errors[0] instanceof SdkError); + assert.equal(resyncs, 1, "a rejected diff marks the book stale"); + assert.equal(book.bestBid(), undefined, "stale until it rebuilds"); + + book.applySnapshot(frame({ U: 200, u: 200, b: [["0.70", "9"]], a: [] })); + assert.deepEqual(book.bestBid(), { price: "0.7", qty: "9" }); +}); + +test("a malformed level does not crash when no 'error' listener is attached", () => { + const book = new LiveOrderBook("btcusd"); + book.applySnapshot(frame({ U: 100, u: 100, b: [["0.60", "5"]], a: [] })); + + assert.doesNotThrow(() => book.ingest(frame({ U: 100, u: 101, b: [["x", "1"]], a: [] }))); + assert.equal(book.bestBid(), undefined, "went stale, not live-and-wrong"); +}); + +test("a malformed snapshot surfaces 'error' + 'resync' and recovers on a valid one", () => { + const book = new LiveOrderBook("btcusd"); + const errors: unknown[] = []; + let resyncs = 0; + const updates: BookDelta[] = []; + book.on("error", (e) => errors.push(e)); + book.on("resync", () => resyncs++); + book.on("update", (_b, d) => updates.push(d)); + + book.applySnapshot(frame({ U: 100, u: 100, b: [["not-a-price", "1"]], a: [] })); // bad snapshot + assert.equal(errors.length, 1); + assert.ok(errors[0] instanceof SdkError); + assert.equal(resyncs, 1, "a rejected snapshot signals resync so the facade resubscribes"); + assert.equal(updates.length, 0); + assert.equal(book.bestBid(), undefined, "never went live"); + + book.applySnapshot(frame({ U: 200, u: 200, b: [["0.70", "1"]], a: [["0.71", "2"]] })); + assert.equal(updates.length, 1); + assert.deepEqual(book.bestBid(), { price: "0.7", qty: "1" }); +}); + +test("applySnapshot rejects a non-snapshot frame (U != u) and recovers on a real one", () => { + const book = new LiveOrderBook("btcusd"); + const errors: unknown[] = []; + const updates: BookDelta[] = []; + book.on("error", (e) => errors.push(e)); + book.on("update", (_b, d) => updates.push(d)); + + book.applySnapshot(frame({ U: 10, u: 11, b: [["0.60", "5"]], a: [] })); // U != u — not a snapshot + assert.equal(errors.length, 1); + assert.ok(errors[0] instanceof SdkError); + assert.equal(updates.length, 0); + assert.equal(book.bestBid(), undefined, "never went live from a non-snapshot frame"); + + book.applySnapshot(frame({ U: 20, u: 20, b: [["0.60", "5"]], a: [["0.61", "2"]] })); + assert.equal(updates.length, 1); + assert.deepEqual(book.bestBid(), { price: "0.6", qty: "5" }); +}); + +test("a diff removing a level (qty 0) updates the book and the delta preserves the removal", () => { + const book = new LiveOrderBook("btcusd"); + book.applySnapshot(frame({ U: 100, u: 100, b: [["0.60", "5"], ["0.59", "3"]], a: [] })); + + const deltas: BookDelta[] = []; + book.on("update", (_b, d) => deltas.push(d)); + book.ingest(frame({ U: 100, u: 101, b: [["0.60", "0"]], a: [] })); // remove the top bid + + assert.equal(deltas.length, 1); + assert.deepEqual(deltas[0], { bids: [{ price: "0.6", qty: "0" }], asks: [] }, "delta preserves the qty:0 removal"); + assert.deepEqual(book.bestBid(), { price: "0.59", qty: "3" }, "removed level gone; next-best is now top"); +}); + +test("all reads are empty until live, then reflect the book", () => { + const book = new LiveOrderBook("btcusd"); + assert.deepEqual(book.topN("bids", 5), []); + assert.equal(book.spread(), undefined); + assert.equal(book.mid(), undefined); + assert.deepEqual(book.snapshot(), { bids: [], asks: [] }); + + book.applySnapshot(frame({ U: 1, u: 1, b: [["0.60", "5"]], a: [["0.62", "2"]] })); + + assert.deepEqual(book.topN("bids", 5), [{ price: "0.6", qty: "5" }]); + assert.ok(Math.abs(book.spread()! - 0.02) < 1e-9); + assert.ok(Math.abs(book.mid()! - 0.61) < 1e-9); + assert.deepEqual(book.snapshot(), { + bids: [{ price: "0.6", qty: "5" }], + asks: [{ price: "0.62", qty: "2" }], + }); +}); + +test("off() removes exactly one registration; close() removes the rest", () => { + const book = new LiveOrderBook("btcusd"); + let n = 0; + const cb = (): void => { + n++; + }; + book.on("update", cb); // register the SAME cb twice + book.on("update", cb); + + book.applySnapshot(frame({ U: 1, u: 1, b: [["0.6", "1"]], a: [] })); + assert.equal(n, 2, "registered twice → fires twice"); + + book.off("update", cb); // must remove exactly ONE (EventEmitter semantics) + book.ingest(frame({ U: 1, u: 2, b: [["0.6", "1"]], a: [] })); + assert.equal(n, 3, "one registration remains after a single off()"); + + book.close(); + book.ingest(frame({ U: 2, u: 3, b: [["0.6", "1"]], a: [] })); + assert.equal(n, 3, "close() removed the rest"); +}); + +test("a listener bound with an AbortSignal is removed when the signal aborts", () => { + const book = new LiveOrderBook("btcusd"); + const ac = new AbortController(); + let n = 0; + book.on("update", () => n++, { signal: ac.signal }); + + book.applySnapshot(frame({ U: 1, u: 1, b: [["0.6", "1"]], a: [] })); + assert.equal(n, 1); + + ac.abort(); + book.ingest(frame({ U: 1, u: 2, b: [["0.6", "1"]], a: [] })); + assert.equal(n, 1, "aborting the signal removed the listener"); +}); + +test("a listener whose AbortSignal is already aborted is never registered", () => { + const book = new LiveOrderBook("btcusd"); + const ac = new AbortController(); + ac.abort(); + + let n = 0; + book.on("update", () => n++, { signal: ac.signal }); + book.applySnapshot(frame({ U: 1, u: 1, b: [["0.6", "1"]], a: [] })); + + assert.equal(n, 0, "an already-aborted signal must not register the listener"); +}); + +test("the same callback under two signals: aborting one removes only that registration", () => { + const book = new LiveOrderBook("btcusd"); + const a = new AbortController(); + const b = new AbortController(); + let n = 0; + const cb = (): void => { + n++; + }; + book.on("update", cb, { signal: a.signal }); + book.on("update", cb, { signal: b.signal }); + + book.applySnapshot(frame({ U: 1, u: 1, b: [["0.6", "1"]], a: [] })); + assert.equal(n, 2, "registered twice → fires twice"); + + a.abort(); // removes exactly A's registration, leaving B + book.ingest(frame({ U: 1, u: 2, b: [["0.6", "1"]], a: [] })); + assert.equal(n, 3, "aborting A leaves B active (not the other way around)"); + + b.abort(); + book.ingest(frame({ U: 2, u: 3, b: [["0.6", "1"]], a: [] })); + assert.equal(n, 3, "aborting B removes the last registration"); +}); + +test("resync is emitted once per stale period and re-arms after a recovery", () => { + const book = new LiveOrderBook("btcusd"); + book.applySnapshot(frame({ U: 100, u: 100, b: [["0.60", "5"]], a: [] })); + let resyncs = 0; + book.on("resync", () => resyncs++); + + book.ingest(frame({ U: 105, u: 105, b: [["0.60", "9"]], a: [] })); // gap → resync #1 + assert.equal(resyncs, 1); + book.markStale(); // a second stale trigger in the same period must NOT re-emit + assert.equal(resyncs, 1, "no duplicate resync within one stale period"); + + book.applySnapshot(frame({ U: 200, u: 200, b: [["0.70", "1"]], a: [] })); // recover + book.ingest(frame({ U: 205, u: 205, b: [["0.70", "9"]], a: [] })); // later gap → resync #2 + assert.equal(resyncs, 2, "resync re-arms after a successful snapshot"); +}); + +test("a throwing 'update' listener propagates and does not stale the book", () => { + const book = new LiveOrderBook("btcusd"); + let resyncs = 0; + book.on("resync", () => resyncs++); + book.on("update", () => { + throw new Error("consumer boom"); + }); + + // The 'update' emit is outside the protocol try/catch, so a throwing consumer listener must + // propagate to the caller — not be swallowed as a malformed frame (which would falsely resync). + assert.throws(() => book.applySnapshot(frame({ U: 1, u: 1, b: [["0.6", "1"]], a: [] })), /consumer boom/); + assert.equal(resyncs, 0, "a throwing update listener must not trigger resync"); + assert.deepEqual(book.bestBid(), { price: "0.6", qty: "1" }, "book stayed live"); +}); + +test("close() is permanent — a queued snapshot/diff after teardown can't revive the book", () => { + const book = new LiveOrderBook("btcusd"); + book.close(); + + book.applySnapshot(frame({ U: 1, u: 1, b: [["0.6", "1"]], a: [] })); // queued snapshot after close + assert.equal(book.bestBid(), undefined, "closed book stays dark"); + assert.deepEqual(book.snapshot(), { bids: [], asks: [] }); + + book.ingest(frame({ U: 2, u: 3, b: [["0.7", "1"]], a: [] })); + assert.equal(book.bestBid(), undefined, "still dark"); +}); + +test("a frame missing b/a surfaces an SdkError, not a raw TypeError", () => { + const book = new LiveOrderBook("btcusd"); + book.applySnapshot(frame({ U: 1, u: 1, b: [["0.6", "1"]], a: [] })); + + const errors: unknown[] = []; + book.on("error", (e) => errors.push(e)); + book.ingest({ e: "depthUpdate", E: 1, s: "btcusd", U: 1, u: 2 }); // no b/a + + assert.equal(errors.length, 1); + assert.ok(errors[0] instanceof SdkError, "raw errors are wrapped as SdkError"); +}); + +test("bigint sequence ids (past 2^53) drive gap detection", () => { + const book = new LiveOrderBook("btcusd"); + const big = 9007199254740993n; // > Number.MAX_SAFE_INTEGER + book.applySnapshot(frame({ U: big, u: big, b: [["0.6", "1"]], a: [] })); + + const updates: BookDelta[] = []; + let resyncs = 0; + book.on("update", (_b, d) => updates.push(d)); + book.on("resync", () => resyncs++); + + book.ingest(frame({ U: big, u: big + 1n, b: [["0.6", "2"]], a: [] })); + assert.equal(updates.length, 1); + assert.equal(resyncs, 0); + + book.ingest(frame({ U: big + 5n, u: big + 5n, b: [["0.6", "3"]], a: [] })); // gap + assert.equal(resyncs, 1); +}); diff --git a/packages/sdk-typescript/src/tests/market-data-rest.test.ts b/packages/sdk-typescript/src/tests/market-data-rest.test.ts new file mode 100644 index 0000000..ae09cbe --- /dev/null +++ b/packages/sdk-typescript/src/tests/market-data-rest.test.ts @@ -0,0 +1,390 @@ +import assert from "node:assert/strict"; +import { hmacSha384Hex } from "../core/encoding.js"; +import test from "node:test"; + +import { MarketDataRest } from "../generated/market-data/rest.js"; +import { + HmacAuth, + type HttpMethod, + HttpTransport, + OAuthAuth, + parseLosslessJson, + SdkError, +} from "../server/index.js"; +import { fromBase64 } from "../core/encoding.js"; + +type Request = { + url: string; + init: { method: HttpMethod; headers: Record; body?: string }; +}; + +const jsonHeaders = { get: (name: string) => name.toLowerCase() === "content-type" ? "application/json" : null }; + +test("all Market Data wrappers route to their documented REST endpoints", async () => { + const requests: Request[] = []; + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const rest = new MarketDataRest(new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async (url, init) => { + requests.push({ url, init }); + if (url.includes("/v1/trades/")) { + return { status: 200, headers: jsonHeaders, async text() { return "[]"; } }; + } + if (url.includes("records.xlsx")) { + return { + status: 200, + headers: { get: (name: string) => name.toLowerCase() === "content-type" ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" : null }, + async arrayBuffer() { return new Uint8Array([1]).buffer; }, + async text() { throw new Error("file endpoint verification should not read text"); }, + }; + } + return { status: 200, headers: jsonHeaders, async text() { return "{}"; } }; + }, + })); + + const cases: { + name: string; + signed: boolean; + url: string; + call: () => Promise; + }[] = [ + { + name: "listSymbols", + signed: false, + url: "https://api.sandbox.gemini.com/v1/symbols", + call: () => rest.listSymbols(), + }, + { + name: "getSymbolDetails", + signed: false, + url: "https://api.sandbox.gemini.com/v1/symbols/details/BTC%2FUSD", + call: () => rest.getSymbolDetails({ symbol: "BTC/USD" }), + }, + { + name: "getAssetsForNetwork", + signed: true, + url: "https://api.sandbox.gemini.com/v2/networks/base%2Fmain/assets", + call: () => rest.getAssetsForNetwork({ network: "base/main" }), + }, + { + name: "getTokenNetworkV2", + signed: true, + url: "https://api.sandbox.gemini.com/v2/network/USDC", + call: () => rest.getTokenNetworkV2({ token: "USDC" }), + }, + { + name: "getTicker", + signed: false, + url: "https://api.sandbox.gemini.com/v1/pubticker/BTCUSD", + call: () => rest.getTicker({ symbol: "BTCUSD" }), + }, + { + name: "listFeePromos", + signed: false, + url: "https://api.sandbox.gemini.com/v1/feepromos", + call: () => rest.listFeePromos(), + }, + { + name: "getCurrentOrderBook", + signed: false, + url: "https://api.sandbox.gemini.com/v1/book/BTCUSD?limit_bids=1&limit_asks=2", + call: () => rest.getCurrentOrderBook({ symbol: "BTCUSD" }, { limit_bids: 1, limit_asks: 2 }), + }, + { + name: "listTrades", + signed: false, + url: "https://api.sandbox.gemini.com/v1/trades/ETHUSD?timestamp=1700000000000&since_tid=123&include_breaks=true", + call: () => rest.listTrades( + { symbol: "ETHUSD" }, + { timestamp: 1700000000000n, since_tid: 123, include_breaks: true }, + ), + }, + { + name: "listPrices", + signed: false, + url: "https://api.sandbox.gemini.com/v1/pricefeed", + call: () => rest.listPrices(), + }, + { + name: "getFundingAmount", + signed: false, + url: "https://api.sandbox.gemini.com/v1/fundingamount/BTCGUSDPERP", + call: () => rest.getFundingAmount({ symbol: "BTCGUSDPERP" }), + }, + { + name: "getFundingAmountReportFile", + signed: false, + url: "https://api.sandbox.gemini.com/v1/fundingamountreport/records.xlsx?symbol=BTCGUSDPERP&fromDate=2026-01-01&toDate=2026-01-31&numRows=10", + call: () => rest.getFundingAmountReportFile({ + symbol: "BTCGUSDPERP", + fromDate: "2026-01-01", + toDate: "2026-01-31", + numRows: 10, + }), + }, + { + name: "getTickerV2", + signed: false, + url: "https://api.sandbox.gemini.com/v2/ticker/BTCUSD", + call: () => rest.getTickerV2({ symbol: "BTCUSD" }), + }, + { + name: "listCandles", + signed: false, + url: "https://api.sandbox.gemini.com/v2/candles/BTCUSD/1m", + call: () => rest.listCandles({ symbol: "BTCUSD", time_frame: "1m" }), + }, + { + name: "listDerivativeCandles", + signed: false, + url: "https://api.sandbox.gemini.com/v2/derivatives/candles/BTCGUSDPERP/1m", + call: () => rest.listDerivativeCandles({ symbol: "BTCGUSDPERP", time_frame: "1m" }), + }, + { + name: "getFXRate", + signed: true, + url: "https://api.sandbox.gemini.com/v2/fxrate/EURUSD/1591084414622", + call: () => rest.getFXRate({ symbol: "EURUSD", timestamp: 1591084414622n }), + }, + ]; + + for (const [index, route] of cases.entries()) { + await route.call(); + const request = requests[index]!; + assert.equal(request.init.method, "GET", route.name); + assert.equal(request.url, route.url, route.name); + assert.equal(request.init.body, undefined, route.name); + assert.ok(request.init.headers.Accept, route.name); + if (!route.signed) continue; + const encoded = request.init.headers["X-GEMINI-PAYLOAD"]!; + const payload = parseLosslessJson(fromBase64(encoded)) as Record; + assert.equal(payload.request, new URL(route.url).pathname, route.name); + assert.equal(payload.nonce, 1000 + requests.slice(0, index).filter(({ init }) => + "X-GEMINI-PAYLOAD" in init.headers + ).length); + assert.equal(request.init.headers["X-GEMINI-APIKEY"], "key", route.name); + assert.equal( + request.init.headers["X-GEMINI-SIGNATURE"], + await hmacSha384Hex("secret", encoded), + route.name, + ); + } + + assert.equal(requests.length, cases.length); +}); + +test("order book snapshots preserve documented dummy timestamp strings", async () => { + const rest = new MarketDataRest(new HttpTransport({ + env: "sandbox", + fetchImpl: async () => ({ + status: 200, + headers: jsonHeaders, + async text() { + return JSON.stringify({ + asks: [{ price: "3607.86", amount: "14.68205084", timestamp: "1547147541" }], + bids: [{ price: "3607.85", amount: "6.643373", timestamp: "1547147541" }], + }); + }, + }), + })); + + const book = await rest.getCurrentOrderBook({ symbol: "BTCUSD" }); + + assert.equal(book.asks?.[0]?.timestamp, "1547147541"); + assert.equal(book.bids?.[0]?.timestamp, "1547147541"); +}); + +test("candle operations return top-level candle arrays", async () => { + const candles = [[1559755800000, 7781.6, 7820.23, 7776.56, 7819.39, 34.7624802159]]; + const rest = new MarketDataRest(new HttpTransport({ + env: "sandbox", + fetchImpl: async () => ({ + status: 200, + headers: jsonHeaders, + async text() { return JSON.stringify(candles); }, + }), + })); + + assert.deepEqual(await rest.listCandles({ symbol: "BTCUSD", time_frame: "1m" }), candles); + assert.deepEqual(await rest.listDerivativeCandles({ symbol: "BTCGUSDPERP", time_frame: "1m" }), candles); +}); + +test("public Market Data file operations return bytes and response metadata", async () => { + const requests: Request[] = []; + const fileBytes = new Uint8Array([0, 1, 255]); + const rest = new MarketDataRest(new HttpTransport({ + env: "sandbox", + fetchImpl: async (url, init) => { + requests.push({ url, init }); + return { + status: 200, + headers: { + get(name: string) { + const headers: Record = { + "content-disposition": "attachment; filename=report.xlsx", + "content-type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }; + return headers[name.toLowerCase()] ?? null; + }, + }, + async text() { throw new Error("file success responses should not be read as text"); }, + async arrayBuffer() { return fileBytes.buffer.slice(fileBytes.byteOffset, fileBytes.byteOffset + fileBytes.byteLength); }, + }; + }, + })); + + const response = await rest.getFundingAmountReportFile({ symbol: "BTCGUSDPERP" }); + + assert.deepEqual(response.bytes, fileBytes); + assert.equal( + response.contentType, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ); + assert.equal(response.contentDisposition, "attachment; filename=report.xlsx"); + assert.equal( + requests[0]?.url, + "https://api.sandbox.gemini.com/v1/fundingamountreport/records.xlsx?symbol=BTCGUSDPERP", + ); + assert.equal(requests[0]?.init.method, "GET"); + assert.ok(requests[0]?.init.headers.Accept); + assert.equal(requests[0]?.init.body, undefined); +}); + +test("public Market Data file operations preserve CSV bytes without decoding", async () => { + const csv = new TextEncoder().encode("symbol,amount\nBTCGUSDPERP,1.25\n"); + const rest = new MarketDataRest(new HttpTransport({ + env: "sandbox", + fetchImpl: async () => ({ + status: 200, + headers: { + get(name: string) { + const headers: Record = { + "content-disposition": "attachment; filename=report.csv", + "content-type": "text/csv", + }; + return headers[name.toLowerCase()] ?? null; + }, + }, + async text() { throw new Error("csv file responses should not be read as text"); }, + async arrayBuffer() { return csv.buffer.slice(csv.byteOffset, csv.byteOffset + csv.byteLength); }, + }), + })); + + const response = await rest.getFundingAmountReportFile({ symbol: "BTCGUSDPERP" }); + + assert.deepEqual(response.bytes, csv); + assert.equal(response.contentType, "text/csv"); + assert.equal(response.contentDisposition, "attachment; filename=report.csv"); +}); + +test("authenticated Market Data operations use HMAC through the transport", async () => { + const requests: Request[] = []; + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const rest = new MarketDataRest(new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async (url, init) => { + requests.push({ url, init }); + return { status: 200, headers: jsonHeaders, async text() { return "{}"; } }; + }, + })); + + await rest.getAssetsForNetwork({ network: "base/main" }); + await rest.getTokenNetworkV2({ token: "USDC" }); + await rest.getFXRate({ symbol: "EURUSD", timestamp: "2025-04-16T23:07:27.189Z" }); + await rest.getFXRate({ symbol: "EURUSD", timestamp: 1591084414622n }); + + assert.deepEqual(requests.map(({ init }) => init.method), ["GET", "GET", "GET", "GET"]); + assert.deepEqual(requests.map(({ url }) => new URL(url).pathname), [ + "/v2/networks/base%2Fmain/assets", + "/v2/network/USDC", + "/v2/fxrate/EURUSD/2025-04-16T23:07:27.189Z", + "/v2/fxrate/EURUSD/1591084414622", + ]); + const payloads = requests.map(({ init }) => + parseLosslessJson(fromBase64(init.headers["X-GEMINI-PAYLOAD"]!)) as Record + ); + assert.deepEqual(payloads.map((payload) => payload.request), [ + "/v2/networks/base%2Fmain/assets", + "/v2/network/USDC", + "/v2/fxrate/EURUSD/2025-04-16T23:07:27.189Z", + "/v2/fxrate/EURUSD/1591084414622", + ]); + assert.deepEqual(payloads.map((payload) => payload.nonce), [1000, 1001, 1002, 1003]); + for (const { init } of requests) { + const payload = init.headers["X-GEMINI-PAYLOAD"]!; + assert.equal(init.headers["X-GEMINI-APIKEY"], "key"); + assert.equal( + init.headers["X-GEMINI-SIGNATURE"], + await hmacSha384Hex("secret", payload), + ); + } +}); + +test("authenticated Market Data operations fail before fetch without auth", async () => { + let fetches = 0; + const rest = new MarketDataRest(new HttpTransport({ + env: "sandbox", + fetchImpl: async () => { + fetches++; + return { status: 200, headers: jsonHeaders, async text() { return "{}"; } }; + }, + })); + + await assert.rejects( + rest.getFXRate({ symbol: "EURUSD", timestamp: "2025-04-16T23:07:27.189Z" }), + SdkError, + ); + assert.equal(fetches, 0); +}); + +test("authenticated Market Data operations accept OAuth through AuthStrategy", async () => { + const requests: Request[] = []; + const auth = new OAuthAuth({ + client: { type: "public", clientId: "client", redirectUri: "https://example.com/callback" }, + tokenStore: { + async load() { return { accessToken: "access", refreshToken: "refresh", tokenType: "bearer" as const, scope: "auditor", expiresAt: 100_000 }; }, + async save() {}, async clear() {}, async runExclusive(operation: () => Promise) { return operation(); }, + }, + now: () => 1000, + }); + const rest = new MarketDataRest(new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async (url, init) => { + requests.push({ url, init }); + return { status: 200, headers: jsonHeaders, async text() { return "{}"; } }; + }, + })); + + await rest.getAssetsForNetwork({ network: "ethereum" }); + + assert.equal(requests[0]?.init.headers.Authorization, "Bearer access"); + assert.equal(requests[0]?.init.headers["X-GEMINI-APIKEY"], undefined); + const payload = JSON.parse(fromBase64(requests[0]!.init.headers["X-GEMINI-PAYLOAD"]!)); + assert.equal("nonce" in payload, false); +}); + +test("getFXRate preserves generated asOf bigint normalization", async () => { + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const rest = new MarketDataRest(new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async () => ({ + status: 200, + headers: jsonHeaders, + async text() { + return '{"fxPair":"EURUSD","rate":"0.69","asOf":9007199254740993,"provider":"bcb","benchmark":"Spot"}'; + }, + }), + })); + + const rate = await rest.getFXRate({ + symbol: "EURUSD", + timestamp: "2025-04-16T23:07:27.189Z", + }); + + assert.equal(rate.asOf, 9007199254740993n); + assert.equal(rate.rate, "0.69"); +}); diff --git a/packages/sdk-typescript/src/tests/oauth-auth.test.ts b/packages/sdk-typescript/src/tests/oauth-auth.test.ts new file mode 100644 index 0000000..53bf07a --- /dev/null +++ b/packages/sdk-typescript/src/tests/oauth-auth.test.ts @@ -0,0 +1,707 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { + HttpTransport, + OAuthAuth, + OAuthAuthorizationError, + OAuthStateError, + OAuthTokenError, + RequestAbortedError, + SdkError, + serializeError, + type FetchLike, + type OAuthTokenStore, + type OAuthTokens, +} from "../server/index.js"; +import type { DiagnosticEvent } from "../diagnostics.js"; +import { fromBase64, fromBase64Url } from "../core/encoding.js"; + +class MemoryTokenStore implements OAuthTokenStore { + record?: OAuthTokens; + #tail: Promise = Promise.resolve(); + + constructor(tokens?: OAuthTokens) { + if (tokens) { + this.record = tokens; + } + } + + async load() { + return this.record; + } + + async save(tokens: OAuthTokens) { + this.record = tokens; + } + + async clear() { + this.record = undefined; + } + + async runExclusive(operation: () => Promise): Promise { + const previous = this.#tail; + let release: () => void = () => undefined; + this.#tail = new Promise((resolve) => { + release = resolve; + }); + await previous; + try { + return await operation(); + } finally { + release(); + } + } +} + +class NonReentrantTokenStore extends MemoryTokenStore { + #locked = false; + + override async runExclusive(operation: () => Promise): Promise { + if (this.#locked) { + throw new Error("token store lock was re-entered"); + } + this.#locked = true; + try { + return await operation(); + } finally { + this.#locked = false; + } + } +} + +const validTokens = (overrides: Partial = {}): OAuthTokens => ({ + accessToken: "access-1", + refreshToken: "refresh-1", + tokenType: "bearer", + scope: "orders:create", + expiresAt: 1_800_000_000_000, + ...overrides, +}); + +const publicOptions = (store: OAuthTokenStore, extra: Record = {}) => ({ + client: { + type: "public" as const, + clientId: "public-client", + redirectUri: "http://127.0.0.1:51234/callback", + }, + tokenStore: store, + now: () => 1_700_000_000_000, + randomBytes: (size: number) => new Uint8Array(size).fill(7), + ...extra, +}); + +function jsonResponse(status: number, body: unknown) { + return { status, text: async () => JSON.stringify(body) }; +} + +void test("public authorization request generates state and S256 PKCE", async () => { + const auth = new OAuthAuth(publicOptions(new MemoryTokenStore())); + const { url, transaction } = await auth.beginAuthorization(["orders:create", "orders:read"]); + const parsed = new URL(url); + + assert.equal(parsed.origin + parsed.pathname, "https://exchange.gemini.com/auth"); + assert.equal(parsed.searchParams.get("client_id"), "public-client"); + assert.equal(parsed.searchParams.get("response_type"), "code"); + assert.equal(parsed.searchParams.get("scope"), "orders:create,orders:read"); + assert.equal(parsed.searchParams.get("state"), transaction.state); + assert.equal(parsed.searchParams.get("code_challenge_method"), "S256"); + assert.equal(transaction.codeVerifier?.length, 86); + assert.match(transaction.codeVerifier ?? "", /^[A-Za-z0-9._~-]{43,128}$/); + assert.match(parsed.searchParams.get("code_challenge") ?? "", /^[A-Za-z0-9_-]{43}$/); + assert.doesNotMatch(parsed.searchParams.get("code_challenge") ?? "", /=/); +}); + +void test("sandbox OAuth uses sandbox authorization, exchange, and refresh endpoints", async () => { + const store = new MemoryTokenStore(); + let tokenUrl: string | undefined; + const auth = new OAuthAuth({ + ...publicOptions(store), + env: "sandbox", + fetchImpl: async (url) => { + tokenUrl = url; + return jsonResponse(200, { + access_token: "access-2", + refresh_token: "refresh-2", + token_type: "bearer", + scope: "orders:read", + expires_in: 3600, + }); + }, + }); + const { url, transaction } = await auth.beginAuthorization(["orders:read"]); + const callback = new URL("http://127.0.0.1:51234/callback"); + callback.searchParams.set("code", "authorization-code"); + callback.searchParams.set("state", transaction.state); + + assert.equal(new URL(url).origin, "https://exchange.sandbox.gemini.com"); + await auth.completeAuthorization(callback, transaction); + assert.equal(tokenUrl, "https://exchange.sandbox.gemini.com/auth/token"); + + const refreshAuth = new OAuthAuth({ + ...publicOptions(store), + env: "sandbox", + refreshSkewMs: Number.MAX_SAFE_INTEGER, + fetchImpl: async (refreshUrl) => { + tokenUrl = refreshUrl; + return jsonResponse(200, { + access_token: "access-3", + refresh_token: "refresh-3", + token_type: "bearer", + scope: "orders:read", + expires_in: 3600, + }); + }, + }); + await refreshAuth.credentialHeaders(""); + assert.equal(tokenUrl, "https://exchange.sandbox.gemini.com/auth/token"); +}); + +void test("OAuth diagnostics expose safe lifecycle metadata without token values", async () => { + const events: DiagnosticEvent[] = []; + const auth = new OAuthAuth(publicOptions(new MemoryTokenStore(), { + onDiagnostic: (event: DiagnosticEvent) => events.push(event), + fetchImpl: async () => ({ + status: 200, + headers: { get: (name: string) => name.toLowerCase() === "x-gemini-request-id" ? "exchange-1" : "application/json" }, + text: async () => JSON.stringify({ access_token: "access-secret", refresh_token: "refresh-secret", token_type: "bearer", expires_in: 3600 }), + }), + })); + const request = await auth.beginAuthorization(["orders:create"]); + const callback = new URL("https://exchange.gemini.com/callback"); + callback.searchParams.set("code", "auth-code"); + callback.searchParams.set("state", request.transaction.state); + await auth.completeAuthorization(callback, request.transaction); + assert.ok(events.some((event) => event.name === "token.exchange" && event.level === "info")); + assert.equal(JSON.stringify(events).includes("access-secret"), false); + assert.equal(JSON.stringify(events).includes("refresh-secret"), false); + assert.equal(JSON.stringify(events).includes("auth-code"), false); + assert.equal(events.find((event) => event.name === "token.exchange")?.response?.exchangeRequestId, "exchange-1"); +}); + +void test("PKCE S256 derivation matches the RFC 7636 example vector", async () => { + const verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; + const auth = new OAuthAuth({ + ...publicOptions(new MemoryTokenStore()), + randomBytes: (size) => size === 32 + ? new Uint8Array(32).fill(1) + : fromBase64Url(verifier), + }); + + const { url, transaction } = await auth.beginAuthorization(["orders:read"]); + + assert.equal(transaction.codeVerifier, verifier); + assert.equal( + new URL(url).searchParams.get("code_challenge"), + "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + ); +}); + +void test("confidential authorization request uses state without exposing its secret", async () => { + const auth = new OAuthAuth({ + client: { + type: "confidential", + clientId: "server-client", + clientSecret: "server-secret", + redirectUri: "https://client.example/callback", + }, + tokenStore: new MemoryTokenStore(), + randomBytes: (size) => new Uint8Array(size).fill(9), + }); + const { url, transaction } = await auth.beginAuthorization(["orders:read"]); + const parsed = new URL(url); + + assert.equal(transaction.codeVerifier, undefined); + assert.equal(parsed.searchParams.has("code_challenge"), false); + assert.equal(url.includes("server-secret"), false); + assert.deepEqual(Object.keys(auth), []); +}); + +void test("callback rejects missing or mismatched state before token exchange", async () => { + let calls = 0; + const auth = new OAuthAuth(publicOptions(new MemoryTokenStore(), { + fetchImpl: async () => { + calls++; + return jsonResponse(200, {}); + }, + })); + const { transaction } = await auth.beginAuthorization(["orders:read"]); + + await assert.rejects( + auth.completeAuthorization("http://127.0.0.1:51234/callback?code=abc", transaction), + (error: unknown) => error instanceof OAuthStateError, + ); + await assert.rejects( + auth.completeAuthorization( + "http://127.0.0.1:51234/callback?code=abc&state=wrong", + transaction, + ), + (error: unknown) => error instanceof OAuthStateError, + ); + assert.equal(calls, 0); +}); + +void test("callback maps authorization errors after validating state", async () => { + const auth = new OAuthAuth(publicOptions(new MemoryTokenStore())); + const { transaction } = await auth.beginAuthorization(["orders:read"]); + const callback = new URL("http://127.0.0.1:51234/callback"); + callback.searchParams.set("state", transaction.state); + callback.searchParams.set("error", "access_denied"); + callback.searchParams.set("error_description", "User denied access"); + + await assert.rejects( + auth.completeAuthorization(callback, transaction), + (error: unknown) => + error instanceof OAuthAuthorizationError && error.error === "access_denied", + ); +}); + +void test("public code exchange sends the verifier, stores tokens, and omits a client secret", async () => { + const store = new MemoryTokenStore(); + let request: Parameters | undefined; + const auth = new OAuthAuth(publicOptions(store, { + fetchImpl: async (...args: Parameters) => { + request = args; + return jsonResponse(200, { + access_token: "access-2", + refresh_token: "refresh-2", + token_type: "bearer", + scope: "orders:read", + expires_in: 3600, + }); + }, + })); + const { transaction } = await auth.beginAuthorization(["orders:read"]); + const callback = new URL("http://127.0.0.1:51234/callback"); + callback.searchParams.set("code", "authorization-code"); + callback.searchParams.set("state", transaction.state); + + const tokens = await auth.completeAuthorization(callback, transaction); + const body = JSON.parse(request?.[1].body ?? "{}"); + + assert.equal(request?.[0], "https://exchange.gemini.com/auth/token"); + assert.equal(body.code_verifier, transaction.codeVerifier); + assert.equal("client_secret" in body, false); + assert.equal(tokens.expiresAt, 1_700_003_600_000); + assert.deepEqual(store.record, tokens); +}); + +void test("OAuth code exchange forwards caller cancellation to the token fetch", async () => { + const store = new MemoryTokenStore(); + const controller = new AbortController(); + let received: AbortSignal | undefined; + const auth = new OAuthAuth(publicOptions(store, { + fetchImpl: async (_url: string, init: Parameters[1]) => { + received = init.signal; + return new Promise(() => {}); + }, + })); + const { transaction } = await auth.beginAuthorization(["orders:read"]); + const callback = new URL("http://127.0.0.1:51234/callback"); + callback.searchParams.set("code", "authorization-code"); + callback.searchParams.set("state", transaction.state); + + const pending = auth.completeAuthorization(callback, transaction, { signal: controller.signal }); + await Promise.resolve(); + controller.abort(); + + await assert.rejects(pending, RequestAbortedError); + assert.equal(received?.aborted, true); +}); + +void test("confidential code exchange sends its secret and no PKCE verifier", async () => { + const store = new MemoryTokenStore(); + let body: Record = {}; + const auth = new OAuthAuth({ + client: { + type: "confidential", + clientId: "server-client", + clientSecret: "server-secret", + redirectUri: "https://client.example/callback", + }, + tokenStore: store, + now: () => 1_700_000_000_000, + randomBytes: (size) => new Uint8Array(size).fill(8), + fetchImpl: async (_url, init) => { + body = JSON.parse(init.body ?? "{}"); + return jsonResponse(200, { + access_token: "access-2", + refresh_token: "refresh-2", + token_type: "bearer", + scope: "orders:read", + expires_in: 60, + }); + }, + }); + const { transaction } = await auth.beginAuthorization(["orders:read"]); + const callback = new URL("https://client.example/callback"); + callback.searchParams.set("code", "authorization-code"); + callback.searchParams.set("state", transaction.state); + + await auth.completeAuthorization(callback, transaction); + + assert.equal(body.client_secret, "server-secret"); + assert.equal("code_verifier" in body, false); +}); + +void test("OAuth token endpoint errors have a distinct typed envelope", async () => { + const auth = new OAuthAuth(publicOptions(new MemoryTokenStore(), { + fetchImpl: async () => jsonResponse(400, { + error: "invalid_grant", + error_description: "Code is invalid or already used", + }), + })); + const { transaction } = await auth.beginAuthorization(["orders:read"]); + const callback = new URL("http://127.0.0.1:51234/callback"); + callback.searchParams.set("code", "bad-code"); + callback.searchParams.set("state", transaction.state); + + await assert.rejects( + auth.completeAuthorization(callback, transaction), + (error: unknown) => { + if (!(error instanceof OAuthTokenError)) return false; + const safe = serializeError(error); + const debug = serializeError(error, { includeRawBody: true }); + return error.status === 400 && + error.error === "invalid_grant" && + error.errorDescription === "Code is invalid or already used" && + safe.message === "OAuth token request failed" && + !("body" in safe) && + JSON.stringify(safe).includes("Code is invalid or already used") === false && + JSON.stringify(debug.body).includes("Code is invalid or already used"); + }, + ); +}); + +void test("OAuthAuth supplies Bearer auth through HttpTransport without HMAC or nonce", async () => { + let captured: Parameters[1] | undefined; + const auth = new OAuthAuth(publicOptions(new MemoryTokenStore(validTokens()))); + const client = new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async (_url, init) => { + captured = init; + return jsonResponse(200, {}); + }, + }); + + await client.request({ method: "POST", path: "/v1/x", params: { symbol: "GEMI-TEST" } }); + + assert.equal(captured?.headers.Authorization, "Bearer access-1"); + assert.equal("X-GEMINI-APIKEY" in (captured?.headers ?? {}), false); + assert.equal("X-GEMINI-SIGNATURE" in (captured?.headers ?? {}), false); + const payload = JSON.parse( + fromBase64(captured?.headers["X-GEMINI-PAYLOAD"] ?? ""), + ); + assert.deepEqual(payload, { request: "/v1/x", symbol: "GEMI-TEST" }); +}); + +void test("expired access tokens refresh once for concurrent callers and rotate atomically", async () => { + const store = new MemoryTokenStore(validTokens({ expiresAt: 1_700_000_000_000 })); + let refreshCalls = 0; + const auth = new OAuthAuth(publicOptions(store, { + fetchImpl: async (_url: string, init: Parameters[1]) => { + refreshCalls++; + const body = JSON.parse(init.body ?? "{}"); + assert.equal(body.grant_type, "refresh_token"); + assert.equal(body.refresh_token, "refresh-1"); + assert.equal("client_secret" in body, false); + assert.equal("code_verifier" in body, false); + await Promise.resolve(); + return jsonResponse(200, { + access_token: "access-2", + refresh_token: "refresh-2", + token_type: "bearer", + scope: "orders:create", + expires_in: 3600, + }); + }, + })); + + const headers = await Promise.all([ + auth.credentialHeaders("ignored"), + auth.credentialHeaders("ignored"), + auth.credentialHeaders("ignored"), + ]); + + assert.equal(refreshCalls, 1); + assert.deepEqual(headers, Array(3).fill({ Authorization: "Bearer access-2" })); + assert.equal(store.record?.refreshToken, "refresh-2"); +}); + +void test("two OAuthAuth instances consume a single-use refresh token only once", async () => { + const store = new MemoryTokenStore(validTokens({ expiresAt: 1_700_000_000_000 })); + let refreshCalls = 0; + const fetchImpl: FetchLike = async () => { + refreshCalls++; + await Promise.resolve(); + return jsonResponse(200, { + access_token: "shared-access", + refresh_token: "shared-refresh", + token_type: "bearer", + scope: "orders:read", + expires_in: 3600, + }); + }; + const first = new OAuthAuth(publicOptions(store, { fetchImpl })); + const second = new OAuthAuth(publicOptions(store, { fetchImpl })); + + const headers = await Promise.all([ + first.credentialHeaders("ignored"), + second.credentialHeaders("ignored"), + ]); + + assert.equal(refreshCalls, 1); + assert.deepEqual(headers, [ + { Authorization: "Bearer shared-access" }, + { Authorization: "Bearer shared-access" }, + ]); +}); + +void test("invalid_grant retires the rejected refresh token", async () => { + const store = new MemoryTokenStore(validTokens({ expiresAt: 1_700_000_000_000 })); + const auth = new OAuthAuth(publicOptions(store, { + fetchImpl: async () => jsonResponse(400, { + error: "invalid_grant", + error_description: "refresh token is invalid", + }), + })); + + await assert.rejects( + auth.credentialHeaders("ignored"), + (error: unknown) => error instanceof OAuthTokenError && error.error === "invalid_grant", + ); + assert.equal(store.record, undefined); +}); + +void test("transient refresh failure preserves the token record for retry", async () => { + const store = new MemoryTokenStore(validTokens({ expiresAt: 1_700_000_000_000 })); + const before = store.record; + const auth = new OAuthAuth(publicOptions(store, { + fetchImpl: async () => { + throw new Error("temporary network failure"); + }, + })); + + await assert.rejects(auth.credentialHeaders("ignored"), /OAuth token request failed/); + assert.equal(store.record, before); +}); + +void test("confidential refresh authenticates with its client secret", async () => { + const store = new MemoryTokenStore(validTokens({ expiresAt: 1_700_000_000_000 })); + let body: Record = {}; + const auth = new OAuthAuth({ + client: { + type: "confidential", + clientId: "server-client", + clientSecret: "server-secret", + redirectUri: "https://client.example/callback", + }, + tokenStore: store, + now: () => 1_700_000_000_000, + fetchImpl: async (_url, init) => { + body = JSON.parse(init.body ?? "{}"); + return jsonResponse(200, { + access_token: "access-2", + refresh_token: "refresh-2", + token_type: "bearer", + scope: "orders:read", + expires_in: 3600, + }); + }, + }); + + await auth.credentialHeaders("ignored"); + + assert.equal(body.client_secret, "server-secret"); + assert.equal("code_verifier" in body, false); +}); + +void test("revocation uses the matching OAuth transport and clears tokens only after success", async () => { + const store = new MemoryTokenStore(validTokens()); + const auth = new OAuthAuth(publicOptions(store)); + let request: Parameters | undefined; + const transport = new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async (...args) => { + request = args; + return jsonResponse(200, { result: "ok" }); + }, + }); + + await auth.revoke(transport); + + assert.equal(request?.[0], "https://api.sandbox.gemini.com/v1/oauth/revokeByToken"); + assert.equal(request?.[1].headers.Authorization, "Bearer access-1"); + assert.equal(store.record, undefined); +}); + +void test("failed revocation leaves local tokens available", async () => { + const store = new MemoryTokenStore(validTokens()); + const auth = new OAuthAuth(publicOptions(store)); + const transport = new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async () => { + throw new SdkError("revoke failed"); + }, + }); + + await assert.rejects(auth.revoke(transport), /revoke failed/); + assert.notEqual(store.record, undefined); +}); + +void test("revocation uses a freshly rotated token even when its lifetime is within the skew", async () => { + const store = new MemoryTokenStore(validTokens({ expiresAt: 1_700_000_000_000 })); + let refreshCalls = 0; + let revokeCalls = 0; + const auth = new OAuthAuth(publicOptions(store, { + fetchImpl: async () => { + refreshCalls++; + if (refreshCalls > 1) { + throw new Error("revocation refreshed more than once"); + } + return jsonResponse(200, { + access_token: "short-access", + refresh_token: "short-refresh", + token_type: "bearer", + scope: "orders:read", + expires_in: 30, + }); + }, + })); + const transport = new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async (_url, init) => { + revokeCalls++; + assert.equal(init.headers.Authorization, "Bearer short-access"); + return jsonResponse(200, {}); + }, + }); + + await auth.revoke(transport); + + assert.equal(refreshCalls, 1); + assert.equal(revokeCalls, 1); + assert.equal(store.record, undefined); +}); + +void test("revocation does not re-enter the token-store lock when the clock crosses expiry", async () => { + const store = new NonReentrantTokenStore(validTokens({ expiresAt: 1_700_000_000_001 })); + const times = [1_700_000_000_000, 1_700_000_000_000, 1_700_000_000_002]; + const auth = new OAuthAuth(publicOptions(store, { + now: () => times.shift() ?? 1_700_000_000_002, + refreshSkewMs: 0, + })); + const transport = new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async (_url, init) => { + assert.equal(init.headers.Authorization, "Bearer access-1"); + return jsonResponse(200, {}); + }, + }); + + await auth.revoke(transport); + + assert.equal(store.record, undefined); +}); + +void test("revocation cannot clear tokens saved by concurrent authorization", async () => { + const store = new MemoryTokenStore(validTokens()); + let releaseRevoke: () => void = () => undefined; + let announceRevoke: () => void = () => undefined; + const revokeStarted = new Promise((resolve) => { + announceRevoke = resolve; + }); + const auth = new OAuthAuth(publicOptions(store, { + fetchImpl: async () => jsonResponse(200, { + access_token: "replacement-access", + refresh_token: "replacement-refresh", + token_type: "bearer", + scope: "orders:read", + expires_in: 3600, + }), + })); + const transport = new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async () => { + announceRevoke(); + await new Promise((resolve) => { + releaseRevoke = resolve; + }); + return jsonResponse(200, {}); + }, + }); + const { transaction } = await auth.beginAuthorization(["orders:read"]); + const callback = new URL("http://127.0.0.1:51234/callback"); + callback.searchParams.set("code", "replacement-code"); + callback.searchParams.set("state", transaction.state); + + const revocation = auth.revoke(transport); + await revokeStarted; + const replacement = auth.completeAuthorization(callback, transaction); + releaseRevoke(); + await Promise.all([revocation, replacement]); + + assert.equal(store.record?.accessToken, "replacement-access"); +}); + +void test("revocation rejects a transport bound to another OAuthAuth", async () => { + const firstStore = new MemoryTokenStore(validTokens()); + const secondStore = new MemoryTokenStore(validTokens({ accessToken: "other-access" })); + const first = new OAuthAuth(publicOptions(firstStore)); + const second = new OAuthAuth(publicOptions(secondStore)); + let fetchCalls = 0; + const secondTransport = new HttpTransport({ + env: "sandbox", + auth: second, + fetchImpl: async () => { + fetchCalls++; + return jsonResponse(200, {}); + }, + }); + + await assert.rejects(first.revoke(secondTransport), /same OAuthAuth/i); + assert.equal(fetchCalls, 0); + assert.notEqual(firstStore.record, undefined); + assert.notEqual(secondStore.record, undefined); +}); + +void test("malformed persisted tokens fail as SdkError before any network request", async () => { + for (const malformed of [null, 7, [], validTokens({ accessToken: "" })]) { + const store = new MemoryTokenStore(); + store.record = malformed as OAuthTokens; + let fetchCalls = 0; + const auth = new OAuthAuth(publicOptions(store, { + fetchImpl: async () => { + fetchCalls++; + return jsonResponse(200, {}); + }, + })); + + await assert.rejects( + auth.credentialHeaders("ignored"), + (error: unknown) => error instanceof SdkError, + ); + assert.equal(fetchCalls, 0); + } +}); + +void test("missing persisted tokens require authorization before credentials or revocation", async () => { + const store = new MemoryTokenStore(); + const auth = new OAuthAuth(publicOptions(store)); + const transport = new HttpTransport({ env: "sandbox", auth }); + + await assert.rejects( + auth.credentialHeaders("ignored"), + /OAuth tokens are unavailable; complete authorization first/, + ); + await assert.rejects( + auth.revoke(transport), + /OAuth tokens are unavailable; complete authorization first/, + ); +}); diff --git a/packages/sdk-typescript/src/tests/orderbook.test.ts b/packages/sdk-typescript/src/tests/orderbook.test.ts new file mode 100644 index 0000000..914e7c4 --- /dev/null +++ b/packages/sdk-typescript/src/tests/orderbook.test.ts @@ -0,0 +1,341 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { OrderBook } from "../orderbook.js"; +import { ResyncRequiredError, SdkError } from "../errors.js"; +import type { DepthUpdate } from "../websocket-types.js"; + +// A depth diff with the boilerplate fields filled in; override what a test cares about. +function diff(over: Partial): DepthUpdate { + return { e: "depthUpdate", E: 0, s: "TEST", U: 0, u: 0, b: [], a: [], ...over }; +} + +// Book seeded with a known snapshot at lastUpdateId 42. +function seededBook(): OrderBook { + const book = new OrderBook(); + book.applySnapshot({ + lastUpdateId: 42, + bids: [["100.5", "2"], ["100.4", "1"]], + asks: [["101.0", "3"]], + }); + return book; +} + +// Gemini's Fast WS depth updates overlap: a valid continuation has U == lastUpdateId. + +test("applySnapshot populates the book from a snapshot", () => { + const book = new OrderBook(); + book.applySnapshot({ + lastUpdateId: 42, + bids: [ + ["100.5", "2"], + ["100.4", "1"], + ], + asks: [["101.0", "3"]], + }); + + assert.equal(book.lastUpdateId, 42n, "lastUpdateId stored as bigint"); + assert.equal(book.bids.get("100.5"), "2"); + assert.equal(book.bids.get("100.4"), "1"); + assert.equal(book.asks.get("101"), "3", "'101.0' is stored under its canonical key '101'"); +}); + +test("applyDiff applies changed levels and advances lastUpdateId", () => { + const book = seededBook(); // lastUpdateId 42; a continuation shares that boundary (U == 42) + book.applyDiff( + diff({ + U: 42, + u: 45, + b: [["100.5", "5"], ["100.3", "1"]], // update existing + add new + a: [["101.0", "4"]], + }), + ); + + assert.equal(book.lastUpdateId, 45n); + assert.equal(book.bids.get("100.5"), "5"); + assert.equal(book.bids.get("100.3"), "1"); + assert.equal(book.bids.get("100.4"), "1", "untouched level unchanged"); + assert.equal(book.asks.get("101"), "4", "canonical key"); +}); + +test("applyDiff removes a level when quantity is zero, including \"0.00\"", () => { + const book = seededBook(); + book.applyDiff( + diff({ + U: 42, + u: 44, + b: [["100.4", "0"], ["100.5", "0.00"]], // both forms of zero remove + }), + ); + + assert.equal(book.bids.has("100.4"), false, "\"0\" removes the level"); + assert.equal(book.bids.has("100.5"), false, "\"0.00\" also removes the level"); +}); + +test("applyDiff throws ResyncRequiredError on a gap and leaves the book untouched", () => { + const book = seededBook(); // lastUpdateId 42, so a valid continuation has U == 42 + const gapDiff = diff({ U: 45, u: 46, b: [["100.5", "999"]] }); // U skips past 42 → gap + + assert.throws(() => book.applyDiff(gapDiff), ResyncRequiredError); + assert.equal(book.lastUpdateId, 42n, "id not advanced"); + assert.equal(book.bids.get("100.5"), "2", "changes not applied"); +}); + +// Under the overlap convention, U == lastUpdateId + 1 already indicates a missed frame. +test("applyDiff treats U == lastUpdateId + 1 as a gap (overlap: valid next U == lastUpdateId)", () => { + const book = seededBook(); // lastUpdateId 42; a valid continuation shares the boundary (U == 42) + const boundaryGap = diff({ U: 43, u: 43, b: [["100.5", "999"]] }); // U one past last = a skipped frame + assert.throws(() => book.applyDiff(boundaryGap), ResyncRequiredError); + assert.equal(book.lastUpdateId, 42n, "a boundary gap must not advance the id"); + assert.equal(book.bids.get("100.5"), "2", "and must not mutate the book"); +}); + +test("applyDiff accepts U == lastUpdateId as an in-sequence continuation (overlap)", () => { + const book = seededBook(); // lastUpdateId 42 + book.applyDiff(diff({ U: 42, u: 43, b: [["100.5", "5"]] })); // shares boundary id 42 + assert.equal(book.lastUpdateId, 43n, "continuation advances to u"); + assert.equal(book.bids.get("100.5"), "5"); +}); + +test("applyDiff ignores a stale diff already covered by the snapshot", () => { + const book = seededBook(); // lastUpdateId 42 + book.applyDiff(diff({ U: 40, u: 41, b: [["100.5", "999"]] })); // u <= 42, fully stale + + assert.equal(book.lastUpdateId, 42n, "id not regressed"); + assert.equal(book.bids.get("100.5"), "2", "stale change not applied"); +}); + +test("bestBid/bestAsk pick the numerically best price, not the lexical one", () => { + const book = new OrderBook(); + book.applySnapshot({ + lastUpdateId: 1, + bids: [["9", "1"], ["100", "2"], ["99", "3"]], // lexical max is "99"; numeric max is 100 + asks: [["101", "4"], ["9", "5"], ["20", "6"]], // lexical min is "101"; numeric min is 9 + }); + + assert.deepEqual(book.bestBid(), { price: "100", qty: "2" }); + assert.deepEqual(book.bestAsk(), { price: "9", qty: "5" }); +}); + +test("bestBid/bestAsk return undefined when a side is empty", () => { + const book = new OrderBook(); + assert.equal(book.bestBid(), undefined); + assert.equal(book.bestAsk(), undefined); +}); + +test("topN returns levels best-first, numerically sorted and capped at n", () => { + const book = new OrderBook(); + book.applySnapshot({ + lastUpdateId: 1, + bids: [["9", "1"], ["100", "2"], ["99", "3"], ["20", "4"]], + asks: [["101", "5"], ["9", "6"], ["20", "7"]], + }); + + // bids: high→low, numeric (so 100 > 99 > 20 > 9, not lexical) + assert.deepEqual(book.topN("bids", 3), [ + { price: "100", qty: "2" }, + { price: "99", qty: "3" }, + { price: "20", qty: "4" }, + ]); + // asks: low→high + assert.deepEqual(book.topN("asks", 2), [ + { price: "9", qty: "6" }, + { price: "20", qty: "7" }, + ]); + // n larger than the book returns everything, no padding + assert.equal(book.topN("asks", 99).length, 3); +}); + +test("spread and mid compute from best bid/ask", () => { + const book = new OrderBook(); + book.applySnapshot({ lastUpdateId: 1, bids: [["100", "1"]], asks: [["101", "2"]] }); + + assert.equal(book.spread(), 1); + assert.equal(book.mid(), 100.5); +}); + +test("spread and mid are undefined when a side is empty", () => { + const book = new OrderBook(); + book.applySnapshot({ lastUpdateId: 1, bids: [["100", "1"]], asks: [] }); + + assert.equal(book.spread(), undefined); + assert.equal(book.mid(), undefined); +}); + +test("snapshot returns both sides fully sorted, best-first", () => { + const book = new OrderBook(); + book.applySnapshot({ + lastUpdateId: 1, + bids: [["99", "1"], ["100", "2"]], + asks: [["20", "3"], ["9", "4"]], + }); + + assert.deepEqual(book.snapshot(), { + bids: [{ price: "100", qty: "2" }, { price: "99", qty: "1" }], + asks: [{ price: "9", qty: "4" }, { price: "20", qty: "3" }], + }); +}); + +test("reads reflect the latest diff (no stale cached view)", () => { + const book = seededBook(); // bids 100.5(2), 100.4(1); asks 101.0(3); lastUpdateId 42 + // prime the read path first, so a cache (if any) is populated + assert.deepEqual(book.bestBid(), { price: "100.5", qty: "2" }); + assert.equal(book.topN("bids", 5).length, 2); + + // add a higher bid and remove the old best in one diff (continuation: U == 42) + book.applyDiff(diff({ U: 42, u: 43, b: [["100.9", "7"], ["100.5", "0"]] })); + + assert.deepEqual(book.bestBid(), { price: "100.9", qty: "7" }, "best reflects the new level"); + assert.deepEqual(book.topN("bids", 5), [ + { price: "100.9", qty: "7" }, + { price: "100.4", qty: "1" }, + ]); +}); + +test("applyDiff applies an overlapping diff (U < lastUpdateId < u)", () => { + const book = seededBook(); // lastUpdateId 42 + book.applyDiff(diff({ U: 40, u: 44, b: [["100.5", "9"]] })); // overlaps 40..42, extends to 44 + assert.equal(book.lastUpdateId, 44n); + assert.equal(book.bids.get("100.5"), "9"); +}); + +test("applyDiff applies consecutive in-sequence diffs", () => { + const book = seededBook(); // lastUpdateId 42 + book.applyDiff(diff({ U: 42, u: 45, b: [["100.6", "1"]] })); // continuation from 42, advances to 45 + book.applyDiff(diff({ U: 45, u: 47, a: [["101.5", "2"]] })); // continuation from 45, advances to 47 + assert.equal(book.lastUpdateId, 47n); + assert.equal(book.bids.get("100.6"), "1"); + assert.equal(book.asks.get("101.5"), "2"); +}); + +test("applySnapshot/applyDiff throw on ids beyond safe-integer range", () => { + const book = new OrderBook(); + assert.throws( + () => book.applySnapshot({ lastUpdateId: Number.MAX_SAFE_INTEGER + 1, bids: [], asks: [] }), + SdkError, + ); + book.applySnapshot({ lastUpdateId: 1, bids: [], asks: [] }); + assert.throws(() => book.applyDiff(diff({ U: 1, u: Number.MAX_SAFE_INTEGER + 1 })), SdkError); +}); + +test("topN returns [] for n <= 0", () => { + const book = new OrderBook(); + book.applySnapshot({ lastUpdateId: 1, bids: [["100", "1"], ["99", "2"]], asks: [] }); + assert.deepEqual(book.topN("bids", 0), []); + assert.deepEqual(book.topN("bids", -1), []); +}); + +test("canonicalizes price keys so one price can't split into two levels", () => { + const book = new OrderBook(); + book.applySnapshot({ lastUpdateId: 1, bids: [["0.50", "2"]], asks: [] }); + book.applyDiff(diff({ U: 1, u: 2, b: [["0.5", "9"]] })); // same level as "0.50", different spelling + + assert.equal(book.topN("bids", 5).length, 1, "no duplicate level from a different spelling"); + assert.equal(book.bids.get("0.5"), "9", "exposed key is canonical, quantity updated in place"); + + // A "0" in yet another spelling must clear that single level — the divergence bug this prevents. + book.applyDiff(diff({ U: 2, u: 3, b: [["0.500", "0"]] })); + assert.equal(book.topN("bids", 5).length, 0, '"0" removal clears the level regardless of spelling'); +}); + +test("accepts bigint ids beyond safe-integer range (lossless-parse path)", () => { + const book = new OrderBook(); + const big = BigInt(Number.MAX_SAFE_INTEGER) + 10n; // past 2^53 — a number can't hold this exactly + book.applySnapshot({ lastUpdateId: big, bids: [["1", "1"]], asks: [] }); + assert.equal(book.lastUpdateId, big, "bigint snapshot id preserved"); + + book.applyDiff(diff({ U: big, u: big + 2n, b: [["1", "2"]] })); // continuation shares boundary id + assert.equal(book.lastUpdateId, big + 2n, "bigint diff ids applied without rounding"); + assert.equal(book.bids.get("1"), "2"); +}); + +test("orders prices beyond float precision exactly (not via Number)", () => { + const book = new OrderBook(); + // 17 significant digits: two distinct prices that collapse to the SAME double, so a + // Number()-based comparator can't tell them apart, let alone order them. + const lo = "1.00000000000000001"; + const hi = "1.00000000000000002"; + book.applySnapshot({ lastUpdateId: 1, bids: [[lo, "1"], [hi, "2"]], asks: [] }); + + assert.deepEqual(book.bestBid(), { price: hi, qty: "2" }, "higher price is best bid"); + assert.deepEqual(book.topN("bids", 2), [ + { price: hi, qty: "2" }, + { price: lo, qty: "1" }, + ]); +}); + +test("a malformed quantity throws and leaves the book untouched", () => { + const book = seededBook(); // 100.4 -> "1", lastUpdateId 42 + assert.throws(() => book.applyDiff(diff({ U: 42, u: 43, b: [["100.4", "NaN"]] })), SdkError); + assert.equal(book.bids.get("100.4"), "1", "level unchanged — validated before mutating"); + assert.equal(book.lastUpdateId, 42n, "sequence not advanced past a bad diff"); +}); + +test("a malformed price throws and does not enter the book", () => { + const book = new OrderBook(); + book.applySnapshot({ lastUpdateId: 1, bids: [["100", "2"]], asks: [] }); + assert.throws(() => book.applyDiff(diff({ U: 1, u: 2, b: [["abc", "5"]] })), SdkError); + assert.deepEqual(book.bestBid(), { price: "100", qty: "2" }); + assert.equal(book.lastUpdateId, 1n); +}); + +test("a non-string level element is rejected before any mutation (atomic)", () => { + const book = new OrderBook(); + book.applySnapshot({ lastUpdateId: 1, bids: [["100.5", "2"]], asks: [] }); + // Frames are untrusted: a level of JSON numbers (not strings) would coerce through the regex + // and then throw mid-mutation in normalizePrice. Validation must reject it before the good + // first level is applied — otherwise the book is left partially changed. + const bad = diff({ U: 2, u: 2, b: [["100.5", "9"], [1, 2]] as unknown as string[][] }); + assert.throws(() => book.applyDiff(bad), SdkError); + assert.equal(book.bids.get("100.5"), "2", "first level not applied — validation is atomic"); + assert.equal(book.lastUpdateId, 1n, "sequence not advanced"); +}); + +test("a string masquerading as a level is rejected (not read as [char, char])", () => { + const book = new OrderBook(); + book.applySnapshot({ lastUpdateId: 1, bids: [["100.5", "2"]], asks: [] }); + // "12" is iterable with .length === 2, so a length/destructure-only check would read it as + // price "1", qty "2". It must be rejected — a level has to be a real array. + const bad = diff({ U: 2, u: 2, b: ["12"] as unknown as string[][] }); + assert.throws(() => book.applyDiff(bad), SdkError); + assert.equal(book.bids.get("100.5"), "2", "book untouched"); +}); + +test("applySnapshot rejects a non-string level atomically (prior book left intact)", () => { + const book = new OrderBook(); + book.applySnapshot({ lastUpdateId: 1, bids: [["100", "2"]], asks: [] }); + // A replacement snapshot with a good level then numeric elements must be rejected BEFORE the + // book is cleared — otherwise it wipes a good book and applies part of a bad one, then throws. + assert.throws( + () => book.applySnapshot({ lastUpdateId: 2, bids: [["99", "5"], [1, 2]] as unknown as string[][], asks: [] }), + SdkError, + ); + assert.deepEqual(book.bestBid(), { price: "100", qty: "2" }, "prior book intact — validated before clearing"); + assert.equal(book.lastUpdateId, 1n, "id not advanced"); +}); + +test("a malformed diff does not advance the sequence, so the next diff resyncs", () => { + const book = seededBook(); // lastUpdateId 42 + assert.throws(() => book.applyDiff(diff({ U: 42, u: 43, b: [["100.5", "NaN"]] })), SdkError); + assert.equal(book.lastUpdateId, 42n, "bad diff rejected, id unchanged"); + // The bad diff never advanced past 42; the next frame's U (43) now skips the boundary → gap, + // so a dropped update can't persist silently. + assert.throws(() => book.applyDiff(diff({ U: 43, u: 43, b: [["100.5", "9"]] })), ResyncRequiredError); +}); + +test("applySnapshot drops zero levels", () => { + const book = new OrderBook(); + book.applySnapshot({ lastUpdateId: 1, bids: [["100", "0"], ["98", "5"]], asks: [] }); + assert.equal(book.topN("bids", 5).length, 1, "zero level dropped"); + assert.deepEqual(book.bestBid(), { price: "98", qty: "5" }); +}); + +test("applySnapshot throws on a malformed level and leaves the prior book intact", () => { + const book = new OrderBook(); + book.applySnapshot({ lastUpdateId: 1, bids: [["100", "2"]], asks: [] }); + assert.throws( + () => book.applySnapshot({ lastUpdateId: 2, bids: [["99", "NaN"]], asks: [] }), + SdkError, + ); + assert.deepEqual(book.bestBid(), { price: "100", qty: "2" }, "prior book intact — validated before clearing"); +}); diff --git a/packages/sdk-typescript/src/tests/prediction-markets-domain.test.ts b/packages/sdk-typescript/src/tests/prediction-markets-domain.test.ts new file mode 100644 index 0000000..f2c9647 --- /dev/null +++ b/packages/sdk-typescript/src/tests/prediction-markets-domain.test.ts @@ -0,0 +1,53 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { AcceptTermsRequired, GeminiMarkets, HmacAuth, type FetchLike } from "../server/index.js"; + +type Request = { url: string; init: { method: string; headers: Record } }; +const jsonHeaders = { get: (name: string) => name.toLowerCase() === "content-type" ? "application/json" : null }; + +function client(responses: string[], requests: Request[] = []) { + return new GeminiMarkets({ + env: "sandbox", + auth: new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }), + fetchImpl: (async (url, init) => { + requests.push({ url, init }); + return { status: url.endsWith("/order") ? 201 : 200, headers: jsonHeaders, async text() { return responses.shift() ?? "{}"; } }; + }) as FetchLike, + } as ConstructorParameters[0]); +} + +test("facade exposes public prediction discovery without authentication", async () => { + const sdk = new GeminiMarkets({ env: "sandbox", fetchImpl: async () => ({ status: 200, headers: jsonHeaders, async text() { return '{"events":[]}'; } }) } as ConstructorParameters[0]); + assert.deepEqual(await sdk.predictions.listEvents(), { events: [] }); + sdk.close(); +}); + +test("placeOrder checks terms and submits the unchanged order when accepted", async () => { + const requests: Request[] = []; + const sdk = client(['{"hasAcceptedLatest":true}', '{"orderId":9007199254740993}'], requests); + const order = { symbol: "GEMI-X", orderType: "limit" as const, side: "buy" as const, quantity: "1", price: "0.5", outcome: "yes" as const, makerOrCancel: false }; + const original = structuredClone(order); + const result = await sdk.predictions.placeOrder(order); + assert.deepEqual(order, original); + assert.equal(result.orderId, 9007199254740993n); + assert.deepEqual(requests.map(({ url }) => new URL(url).pathname), ["/v1/prediction-markets/terms/status", "/v1/prediction-markets/order"]); + sdk.close(); +}); + +test("placeOrder rejects unaccepted terms without submitting an order", async () => { + const requests: Request[] = []; + const sdk = client(['{"hasAcceptedLatest":false,"acceptedVersion":2,"latestVersion":3}'], requests); + await assert.rejects(() => sdk.predictions.placeOrder({ symbol: "GEMI-X", orderType: "limit", side: "buy", quantity: "1", price: "0.5", outcome: "yes", makerOrCancel: false }), AcceptTermsRequired); + assert.equal(requests.length, 1); + sdk.close(); +}); + +test("batch placement checks terms once and acceptTerms delegates explicitly", async () => { + const requests: Request[] = []; + const sdk = client(['{"hasAcceptedLatest":true}', '{"results":[]}', '{"success":true}'], requests); + await sdk.predictions.placeOrderBatch({ orders: [{ symbol: "GEMI-X", orderType: "limit", side: "buy", quantity: "1", price: "0.5", outcome: "yes", makerOrCancel: false }] }); + assert.deepEqual(await sdk.predictions.acceptTerms(), { success: true }); + assert.deepEqual(requests.map(({ url }) => new URL(url).pathname), ["/v1/prediction-markets/terms/status", "/v1/prediction-markets/order/batch", "/v1/prediction-markets/terms/accept"]); + sdk.close(); +}); diff --git a/packages/sdk-typescript/src/tests/prediction-markets-rest.test.ts b/packages/sdk-typescript/src/tests/prediction-markets-rest.test.ts new file mode 100644 index 0000000..6220da9 --- /dev/null +++ b/packages/sdk-typescript/src/tests/prediction-markets-rest.test.ts @@ -0,0 +1,715 @@ +import assert from "node:assert/strict"; +import { hmacSha384Hex } from "../core/encoding.js"; +import { test } from "node:test"; + +import { PredictionMarketsRest } from "../generated/rest.js"; +import { + AcceptTermsRequired, + EndpointMismatch, + HmacAuth, + type HttpMethod, + HttpTransport, + InsufficientFunds, + InvalidRequest, + MissingRole, + OAuthAuth, + parseLosslessJson, + RateLimitError, + ServiceUnavailable, + SdkError, +} from "../server/index.js"; +import { fromBase64 } from "../core/encoding.js"; + +type Request = { + url: string; + init: { method: HttpMethod; headers: Record; body?: string }; +}; + +const jsonHeaders = { get: (name: string) => name.toLowerCase() === "content-type" ? "application/json" : null }; + +function client( + requests: Request[], + failure?: SdkError, + responses: string[] = [], + statuses: number[] = [], +): PredictionMarketsRest { + const transport = new HttpTransport({ + env: "sandbox", + fetchImpl: async (url, init) => { + requests.push({ url, init }); + if (failure) throw failure; + return { + status: statuses.shift() ?? 200, + headers: jsonHeaders, + async text() { return responses.shift() ?? "{}"; }, + }; + }, + }); + return new PredictionMarketsRest(transport); +} + +void test("listEvents forwards filters and repeated array query parameters", async () => { + const requests: Request[] = []; + await client(requests).listEvents({ + status: ["active", "settled"], + category: ["sports", "crypto"], + sport: ["baseball", "american_football"], + sports_market_type: ["spread", "prop"], + sports_market_subject: ["team", "player"], + sports_market_scope: ["inning", "full_contest"], + sports_market_metric: ["runs", "passing_yards"], + search: "nba finals", + limit: 10, + offset: 20, + }); + + assert.equal( + requests[0]?.url, + "https://api.sandbox.gemini.com/v1/prediction-markets/events" + + "?status=active&status=settled&category=sports&category=crypto" + + "&sport=baseball&sport=american_football" + + "&sports_market_type=spread&sports_market_type=prop" + + "&sports_market_subject=team&sports_market_subject=player" + + "&sports_market_scope=inning&sports_market_scope=full_contest" + + "&sports_market_metric=runs&sports_market_metric=passing_yards" + + "&search=nba%20finals&limit=10&offset=20", + ); +}); + +void test("all 15 public wrappers make unsigned GET requests to their schema paths", async () => { + const requests: Request[] = []; + const rest = client(requests); + + await rest.listEvents(); + await rest.getEvent({ eventTicker: "NBA/260310 LAL-BOS" }); + await rest.getEventStrike({ eventTicker: "BTC/05M" }); + await rest.listNewlyListedEvents(); + await rest.listRecentlySettledEvents(); + await rest.listUpcomingEvents(); + await rest.getCategories(); + await rest.getPredictionMarketDailyVolume({ date: "2026-07-20" }); + await rest.getPredictionMarketHourlyVolume({ date: "2026-07-20" }); + await rest.getPredictionMarketsTerms(); + await rest.getComboByInstrumentSymbol({ instrumentSymbol: "GEMI:A/B" }); + await rest.getLiquidityRewardsConfig(); + await rest.getMakerRebateRates(); + await rest.listCombos(); + await rest.listLiquidityRewardsEvents(); + + assert.deepEqual( + requests.map(({ url }) => new URL(url).pathname), + [ + "/v1/prediction-markets/events", + "/v1/prediction-markets/events/NBA%2F260310%20LAL-BOS", + "/v1/prediction-markets/events/BTC%2F05M/strike", + "/v1/prediction-markets/events/newly-listed", + "/v1/prediction-markets/events/recently-settled", + "/v1/prediction-markets/events/upcoming", + "/v1/prediction-markets/categories", + "/v1/prediction-markets/volume/2026-07-20", + "/v1/prediction-markets/volume/2026-07-20/hourly", + "/v1/prediction-markets/terms", + "/v1/prediction-markets/combos/GEMI:A%2FB", + "/v1/prediction-markets/liquidity-rewards/config", + "/v1/prediction-markets/maker-rebate/rates", + "/v1/prediction-markets/combos", + "/v1/prediction-markets/liquidity-rewards/events", + ], + ); + for (const { init } of requests) { + assert.equal(init.method, "GET"); + assert.deepEqual(init.headers, { Accept: "application/json" }); + assert.equal(init.body, undefined); + } +}); + +void test("T5c wrappers forward combo, maker-rate, and reward-event filters", async () => { + const requests: Request[] = []; + const rest = client(requests); + + await rest.listCombos({ + status: "Active", + contractId: 2n, + instrumentRegistered: true, + limit: 25, + offset: 50, + }); + await rest.getMakerRebateRates({ category: "Sports" }); + await rest.listLiquidityRewardsEvents({ + category: "Sports,Crypto", + search: "final", + sort: "daily_pool_desc", + limit: 10, + offset: 20, + }); + + const [combos, rates, events] = requests.map(({ url }) => new URL(url).searchParams); + assert.deepEqual(Object.fromEntries(combos ?? []), { + status: "Active", + contractId: "2", + instrumentRegistered: "true", + limit: "25", + offset: "50", + }); + assert.deepEqual(Object.fromEntries(rates ?? []), { category: "Sports" }); + assert.deepEqual(Object.fromEntries(events ?? []), { + category: "Sports,Crypto", + search: "final", + sort: "daily_pool_desc", + limit: "10", + offset: "20", + }); +}); + +void test("T5c responses preserve bigint IDs, exact money strings, and snake_case", async () => { + const responses = [ + '{"legs":[{"comboId":9007199254740993,"contractId":9007199254740995}]}', + '{"rate_rules":[{"id":9007199254740997,"rebate_multiplier_bps":5000,"effective_from":"2026-03-19T00:00:00Z"}]}', + '{"enabled":true,"min_payout_threshold_usd":"1.00"}', + '{"events":[{"event_ticker":"BTC2605202100","title":"BTC final","category":"Crypto","daily_pool_usd":"500.00","pool_source":"category_default","ends_at":null,"qualifying_maker_count":14}],"pagination":{},"last_score_date":null}', + ]; + const rest = client([], undefined, responses); + + const combo = await rest.getComboByInstrumentSymbol({ instrumentSymbol: "GEMI-COMBO" }); + const rates = await rest.getMakerRebateRates(); + const config = await rest.getLiquidityRewardsConfig(); + const events = await rest.listLiquidityRewardsEvents(); + + assert.equal(combo.legs?.[0]?.comboId, 9007199254740993n); + assert.equal(combo.legs?.[0]?.contractId, 9007199254740995n); + assert.equal(rates.rate_rules[0]?.id, 9007199254740997n); + assert.equal(rates.rate_rules[0]?.rebate_multiplier_bps, 5000); + assert.equal(config.min_payout_threshold_usd, "1.00"); + assert.equal(events.events[0]?.daily_pool_usd, "500.00"); + assert.equal("dailyPoolUsd" in events.events[0]!, false); +}); + +void test("transport errors pass through generated wrappers unchanged", async () => { + const expected = new SdkError("network unavailable"); + const rest = client([], expected); + + await assert.rejects(rest.getLiquidityRewardsConfig(), (error) => error === expected); +}); + +void test("T5c wrappers preserve mapped non-2xx transport errors", async () => { + const body = '{"error":"MissingRole","message":"OrderStatus required"}'; + const rest = client([], undefined, [body], [403]); + + await assert.rejects(rest.getMakerRebateRates(), (error) => { + assert.ok(error instanceof MissingRole); + assert.equal(error.status, 403); + assert.equal(error.reason, "MissingRole"); + assert.equal(error.message, "HTTP 403"); + assert.equal("body" in error, false); + return true; + }); +}); + +void test("all eight T5d wrappers use authenticated schema methods and paths", async () => { + const requests: Request[] = []; + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const transport = new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async (url, init) => { + requests.push({ url, init }); + return { status: url.endsWith("/order") ? 201 : 200, headers: jsonHeaders, async text() { return "{}"; } }; + }, + }); + const rest = new PredictionMarketsRest(transport); + const order = { + symbol: "GEMI-FEDJAN26-DN25", + orderType: "limit" as const, + side: "buy" as const, + quantity: "10.00", + price: "0.65", + outcome: "yes" as const, + makerOrCancel: false, + }; + const originalOrder = structuredClone(order); + + await rest.getPredictionMarketsTermsStatus(); + await rest.acceptPredictionMarketsTerms(); + await rest.placeOrder(order); + await rest.placeOrderBatch({ orders: [order] }); + await rest.cancelOrder({ orderId: 9007199254740993n }); + await rest.cancelOrderBatch({ orderIds: [9007199254740995n, "9007199254740997"] }); + await rest.getActiveOrders(); + await rest.getOrderHistory({ status: "cancelled", limit: 5 }); + + assert.deepEqual(order, originalOrder); + assert.deepEqual(requests.map(({ init }) => init.method), [ + "GET", "POST", "POST", "POST", "POST", "POST", "POST", "POST", + ]); + assert.deepEqual(requests.map(({ url }) => new URL(url).pathname), [ + "/v1/prediction-markets/terms/status", + "/v1/prediction-markets/terms/accept", + "/v1/prediction-markets/order", + "/v1/prediction-markets/order/batch", + "/v1/prediction-markets/order/cancel", + "/v1/prediction-markets/order/batch/cancel", + "/v1/prediction-markets/orders/active", + "/v1/prediction-markets/orders/history", + ]); + const payloads = requests.map(({ init }) => + parseLosslessJson(fromBase64(init.headers["X-GEMINI-PAYLOAD"]!)) as Record + ); + assert.equal("symbol" in payloads[6]!, false); + assert.deepEqual(payloads[7]?.status, "cancelled"); + assert.deepEqual(payloads[3]?.orders, [order]); + assert.deepEqual(payloads[5]?.orderIds, [9007199254740995n, "9007199254740997"]); + for (const { init } of requests) { + const payload = init.headers["X-GEMINI-PAYLOAD"]!; + assert.equal( + init.headers["X-GEMINI-SIGNATURE"], + await hmacSha384Hex("secret", payload), + ); + } +}); + +void test("T5d responses preserve partial batch results, bigint IDs, strings, and nulls", async () => { + const responses = [ + '{"orderId":9007199254740993,"quantity":"10.00","price":"0.65","stopPrice":null}', + '{"results":[{"order":{"orderId":9007199254740995,"status":"open","symbol":"GEMI-X","side":"buy","outcome":"yes","orderType":"limit","timeInForce":"good-til-cancel","quantity":"1.00","filledQuantity":"0.00","remainingQuantity":"1.00","price":"0.45","createdAt":"2026-07-21T00:00:00Z","updatedAt":"2026-07-21T00:00:00Z"}},{"error":"InsufficientFunds","message":"insufficient funds"}]}', + '{"results":[{"orderId":9007199254740997,"result":"ok"},{"orderId":9007199254740999,"error":"OrderNotFound","message":"missing"}]}', + '{"orders":[{"orderId":9007199254741001,"price":"0.25","avgExecutionPrice":null}]}', + '{"orders":[{"orderId":9007199254741003,"quantity":"3.00","cancelledAt":null}]}', + ]; + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const rest = new PredictionMarketsRest(new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async (url) => ({ + status: url.endsWith("/order") ? 201 : 200, + headers: jsonHeaders, + async text() { return responses.shift()!; }, + }), + })); + const order = { + symbol: "GEMI-X", orderType: "limit" as const, side: "buy" as const, + quantity: "1.00", price: "0.45", outcome: "yes" as const, makerOrCancel: false, + }; + + const placed = await rest.placeOrder(order); + const placedBatch = await rest.placeOrderBatch({ orders: [order] }); + const cancelledBatch = await rest.cancelOrderBatch({ orderIds: [1n, 2n] }); + const active = await rest.getActiveOrders(); + const history = await rest.getOrderHistory(); + + assert.equal(placed.orderId, 9007199254740993n); + assert.equal(placed.quantity, "10.00"); + assert.equal(placed.stopPrice, null); + assert.equal("order" in placedBatch.results[0]!, true); + assert.equal("error" in placedBatch.results[1]!, true); + assert.equal("order" in placedBatch.results[0]! && placedBatch.results[0].order.orderId, 9007199254740995n); + assert.deepEqual(cancelledBatch.results.map((result) => result.orderId), [ + 9007199254740997n, 9007199254740999n, + ]); + assert.equal(active.orders?.[0]?.avgExecutionPrice, null); + assert.equal(history.orders?.[0]?.cancelledAt, null); +}); + +void test("T5d wrappers accept OAuth through the shared AuthStrategy seam", async () => { + const requests: Request[] = []; + const auth = new OAuthAuth({ + client: { type: "public", clientId: "client", redirectUri: "https://example.com/callback" }, + tokenStore: { + async load() { return { accessToken: "access", refreshToken: "refresh", tokenType: "bearer" as const, scope: "orders", expiresAt: 100_000 }; }, + async save() {}, async clear() {}, async runExclusive(operation: () => Promise) { return operation(); }, + }, + now: () => 1000, + }); + const rest = new PredictionMarketsRest(new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async (url, init) => { + requests.push({ url, init }); + return { status: 200, headers: jsonHeaders, async text() { return '{"hasAcceptedLatest":true}'; } }; + }, + })); + + assert.equal((await rest.getPredictionMarketsTermsStatus()).hasAcceptedLatest, true); + assert.equal(requests[0]?.init.headers.Authorization, "Bearer access"); + assert.equal(requests[0]?.init.headers["X-GEMINI-APIKEY"], undefined); + const payload = JSON.parse(fromBase64(requests[0]!.init.headers["X-GEMINI-PAYLOAD"]!)); + assert.equal("nonce" in payload, false); +}); + +void test("T5d wrappers preserve mapped order-management failures", async () => { + const cases = [ + [403, "AcceptTermsRequired", AcceptTermsRequired], + [403, "MissingRole", MissingRole], + [406, "InsufficientFunds", InsufficientFunds], + [429, "RateLimit", RateLimitError], + ] as const; + for (const [status, reason, Expected] of cases) { + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const rest = new PredictionMarketsRest(new HttpTransport({ + env: "sandbox", auth, maxRetries: 0, + fetchImpl: async () => ({ status, async text() { return JSON.stringify({ reason }); } }), + })); + await assert.rejects(rest.cancelOrder({ orderId: 1n }), Expected); + } +}); + +void test("T5d wrappers preserve transport and malformed-response failures", async () => { + const failure = new SdkError("network unavailable"); + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const networkRest = new PredictionMarketsRest(new HttpTransport({ + env: "sandbox", auth, + fetchImpl: async () => { throw failure; }, + })); + await assert.rejects(networkRest.getPredictionMarketsTermsStatus(), (error) => error === failure); + + const malformedRest = new PredictionMarketsRest(new HttpTransport({ + env: "sandbox", auth, + fetchImpl: async () => ({ status: 200, headers: jsonHeaders, async text() { return "not json"; } }), + })); + await assert.rejects(malformedRest.acceptPredictionMarketsTerms(), SdkError); +}); + +void test("generated bodies cannot override the transport request path or nonce", async () => { + let fetches = 0; + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const rest = new PredictionMarketsRest(new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async () => { + fetches++; + return { status: 201, headers: jsonHeaders, async text() { return "{}"; } }; + }, + })); + const order = { + symbol: "GEMI-X", orderType: "limit" as const, side: "buy" as const, + quantity: "1.00", price: "0.45", outcome: "yes" as const, makerOrCancel: false, + }; + + await assert.rejects( + rest.placeOrder({ ...order, request: "/v1/other" } as Parameters[0]), + EndpointMismatch, + ); + await assert.rejects( + rest.placeOrder({ ...order, nonce: 1 } as Parameters[0]), + SdkError, + ); + assert.equal(fetches, 0); +}); + +void test("generated body fields cannot replace authentication headers", async () => { + const requests: Request[] = []; + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const rest = new PredictionMarketsRest(new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async (url, init) => { + requests.push({ url, init }); + return { status: 201, headers: jsonHeaders, async text() { return "{}"; } }; + }, + })); + const body = { + symbol: "GEMI-X", orderType: "limit" as const, side: "buy" as const, + quantity: "1.00", price: "0.45", outcome: "yes" as const, makerOrCancel: false, + Authorization: "attacker", + "X-GEMINI-APIKEY": "attacker", + "X-GEMINI-SIGNATURE": "attacker", + "X-GEMINI-PAYLOAD": "attacker", + }; + const original = structuredClone(body); + + await rest.placeOrder(body); + + assert.deepEqual(body, original); + const headers = requests[0]!.init.headers; + const payload = headers["X-GEMINI-PAYLOAD"]!; + assert.equal(headers.Authorization, undefined); + assert.equal(headers["X-GEMINI-APIKEY"], "key"); + assert.notEqual(payload, "attacker"); + assert.equal( + headers["X-GEMINI-SIGNATURE"], + await hmacSha384Hex("secret", payload), + ); +}); + +void test("T5e wrappers keep position filters in the query and volume fields in the signed body", async () => { + const requests: Request[] = []; + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const rest = new PredictionMarketsRest(new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async (url, init) => { + requests.push({ url, init }); + return { status: 200, headers: jsonHeaders, async text() { return "{}"; } }; + }, + })); + const positionsQuery = { + eventTicker: "FED JAN", + limit: 25, + offset: 50, + sort: "+positionValue" as const, + }; + const settledQuery = { + eventTicker: "FEDJAN26", + limit: 10, + offset: 20, + sort: "-payout" as const, + search: "final four", + category: "Sports", + withCashOuts: false, + }; + const volumeBody = { + eventTicker: "FED260318", + startTime: 9007199254740993n, + endTime: 9007199254740995n, + }; + const originalInputs = structuredClone({ positionsQuery, settledQuery, volumeBody }); + + await rest.getPositions(positionsQuery); + await rest.getSettledPositions(settledQuery); + await rest.getVolumeMetrics(volumeBody); + + assert.deepEqual({ positionsQuery, settledQuery, volumeBody }, originalInputs); + assert.deepEqual(requests.map(({ init }) => init.method), ["POST", "POST", "POST"]); + assert.deepEqual(requests.map(({ url }) => new URL(url).pathname), [ + "/v1/prediction-markets/positions", + "/v1/prediction-markets/positions/settled", + "/v1/prediction-markets/metrics/volume", + ]); + assert.equal( + new URL(requests[0]!.url).search, + "?eventTicker=FED%20JAN&limit=25&offset=50&sort=%2BpositionValue", + ); + assert.equal( + new URL(requests[1]!.url).search, + "?eventTicker=FEDJAN26&limit=10&offset=20&sort=-payout&search=final%20four&category=Sports&withCashOuts=false", + ); + assert.equal(new URL(requests[2]!.url).search, ""); + + const payloads = requests.map(({ init }) => + parseLosslessJson(fromBase64(init.headers["X-GEMINI-PAYLOAD"]!)) as Record + ); + assert.deepEqual(Object.keys(payloads[0]!).sort(), ["nonce", "request"]); + assert.deepEqual(Object.keys(payloads[1]!).sort(), ["nonce", "request"]); + assert.deepEqual(payloads[2], { request: "/v1/prediction-markets/metrics/volume", ...volumeBody, nonce: 1002 }); + for (const { init } of requests) { + const payload = init.headers["X-GEMINI-PAYLOAD"]!; + assert.equal( + init.headers["X-GEMINI-SIGNATURE"], + await hmacSha384Hex("secret", payload), + ); + } +}); + +void test("T5e responses preserve bigint IDs, exact money and volume strings, nulls, and omissions", async () => { + const responses = [ + '{"positions":[{"symbol":"GEMI-X","instrumentId":9007199254740993,"totalQuantity":"10.00","avgPrice":"0.45","realizedPl":null,"prices":null}],"total":1}', + '{"positions":[{"accountId":9007199254740995,"instrumentId":9007199254740997,"position":"-3.00","positionQuantity":"3.00","payout":"0.00","costBasis":null,"realizedPnl":"1.25","netProfit":null}],"total":1,"cashOuts":[{"accountId":9007199254740999,"instrumentId":9007199254741001,"instrumentSymbol":"GEMI-Y","timestamp":"2026-07-21T00:00:00Z","filledQuantity":"2.00","side":"sell","proceeds":"1.50","costBasis":"1.00","netProfit":"0.50"}],"totalCashOutProceeds":"1.50","totalCashOutCostBasis":"1.00","totalCashOutNetProfit":"0.50"}', + '{"eventTicker":"FED260318","contracts":[{"symbol":"GEMI-Z","totalQty":"94625.00","userAggressorQty":null,"userRestingQty":"0.00"}]}', + ]; + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const rest = new PredictionMarketsRest(new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async () => ({ status: 200, headers: jsonHeaders, async text() { return responses.shift()!; } }), + })); + + const active = await rest.getPositions(); + const settled = await rest.getSettledPositions({ withCashOuts: true }); + const volume = await rest.getVolumeMetrics({ eventTicker: "FED260318" }); + + assert.equal(active.positions?.[0]?.instrumentId, 9007199254740993n); + assert.equal(active.positions?.[0]?.totalQuantity, "10.00"); + assert.equal(active.positions?.[0]?.realizedPl, null); + assert.equal("marketValue" in active.positions![0]!, false); + assert.equal(settled.positions?.[0]?.accountId, 9007199254740995n); + assert.equal(settled.positions?.[0]?.instrumentId, 9007199254740997n); + assert.equal(settled.positions?.[0]?.payout, "0.00"); + assert.equal(settled.cashOuts?.[0]?.accountId, 9007199254740999n); + assert.equal(settled.cashOuts?.[0]?.instrumentId, 9007199254741001n); + assert.equal(settled.totalCashOutNetProfit, "0.50"); + assert.equal("totalPayout" in settled, false); + assert.equal(volume.contracts?.[0]?.totalQty, "94625.00"); + assert.equal(volume.contracts?.[0]?.userAggressorQty, null); +}); + +void test("T5e wrappers preserve mapped transport failures", async () => { + const cases = [ + [503, "ServiceUnavailable", ServiceUnavailable, "positions"], + [403, "MissingRole", MissingRole, "settled"], + [400, "InvalidRequest", InvalidRequest, "volume"], + ] as const; + for (const [status, reason, Expected, operation] of cases) { + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const rest = new PredictionMarketsRest(new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async () => ({ status, async text() { return JSON.stringify({ reason }); } }), + })); + const request = operation === "positions" + ? rest.getPositions() + : operation === "settled" + ? rest.getSettledPositions() + : rest.getVolumeMetrics({ eventTicker: "FED260318" }); + await assert.rejects(request, Expected); + } +}); + +void test("T5f wrappers authenticate exact reward methods, paths, and query strings", async () => { + const requests: Request[] = []; + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const rest = new PredictionMarketsRest(new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async (url, init) => { + requests.push({ url, init }); + return { status: 200, headers: jsonHeaders, async text() { return "{}"; } }; + }, + })); + const payoutsQuery = { limit: 100, offset: 200 }; + const dailyQuery = { dateFrom: "2026-05-01", dateTo: "2026-05-07" }; + const lifetimeQuery = { dateFrom: "2026-04-01", dateTo: "2026-05-01" }; + const originalQueries = structuredClone({ payoutsQuery, dailyQuery, lifetimeQuery }); + + await rest.listMakerRebatePayouts(payoutsQuery); + await rest.getMakerRebateLifetimeSummary(); + await rest.getLiquidityRewardsDailySummary(dailyQuery); + await rest.getLiquidityRewardsLifetimeSummary(lifetimeQuery); + + assert.deepEqual({ payoutsQuery, dailyQuery, lifetimeQuery }, originalQueries); + assert.deepEqual(requests.map(({ init }) => init.method), ["POST", "GET", "GET", "GET"]); + assert.deepEqual(requests.map(({ url }) => new URL(url).pathname), [ + "/v1/prediction-markets/maker-rebate/payouts", + "/v1/prediction-markets/maker-rebate/summary/total", + "/v1/prediction-markets/liquidity-rewards/summary/daily", + "/v1/prediction-markets/liquidity-rewards/summary/total", + ]); + assert.deepEqual(requests.map(({ url }) => new URL(url).search), [ + "?limit=100&offset=200", + "", + "?dateFrom=2026-05-01&dateTo=2026-05-07", + "?dateFrom=2026-04-01&dateTo=2026-05-01", + ]); + const payloads = requests.map(({ init }) => + parseLosslessJson(fromBase64(init.headers["X-GEMINI-PAYLOAD"]!)) as Record + ); + assert.deepEqual(payloads, [ + { request: "/v1/prediction-markets/maker-rebate/payouts", nonce: 1000 }, + { request: "/v1/prediction-markets/maker-rebate/summary/total", nonce: 1001 }, + { request: "/v1/prediction-markets/liquidity-rewards/summary/daily", nonce: 1002 }, + { request: "/v1/prediction-markets/liquidity-rewards/summary/total", nonce: 1003 }, + ]); + for (const { init } of requests) { + const payload = init.headers["X-GEMINI-PAYLOAD"]!; + assert.equal( + init.headers["X-GEMINI-SIGNATURE"], + await hmacSha384Hex("secret", payload), + ); + } +}); + +void test("T5f responses preserve reward bigints, decimal strings, snake_case, and nulls", async () => { + const responses = [ + '{"payouts":[{"id":9007199254740993,"total_volume_usd":"12450.00","total_rebate_usd":"6.23","total_fill_count":187,"status":"PENDING","paid_at":null,"created_at":null}]}', + '{"total_earned_usd":"152.40","total_fill_count":9007199254740995,"total_volume_usd":"304800.00","payout_count":27,"first_payout_date":null,"last_payout_date":null}', + '{"daily_summaries":[{"payout_date":"2026-05-07","total_reward_usd":"12.45","payout_status":"PAID","paid_at":null,"events":[{"event_id":9007199254740997,"event_name":"BTC final","category_name":"Crypto","normalized_score":"0.4521","snapshot_count":1180,"total_snapshots":1440,"event_reward_usd":"8.20"}]}]}', + '{"total_earned_usd":"0","payout_count":0,"first_payout_date":null,"last_payout_date":null}', + ]; + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const rest = new PredictionMarketsRest(new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async () => ({ status: 200, headers: jsonHeaders, async text() { return responses.shift()!; } }), + })); + + const payouts = await rest.listMakerRebatePayouts(); + const makerLifetime = await rest.getMakerRebateLifetimeSummary(); + const daily = await rest.getLiquidityRewardsDailySummary({ + dateFrom: "2026-05-01", + dateTo: "2026-05-07", + }); + const rewardsLifetime = await rest.getLiquidityRewardsLifetimeSummary(); + + assert.equal(payouts.payouts[0]?.id, 9007199254740993n); + assert.equal(payouts.payouts[0]?.total_volume_usd, "12450.00"); + assert.equal(payouts.payouts[0]?.total_rebate_usd, "6.23"); + assert.equal(payouts.payouts[0]?.paid_at, null); + assert.equal("totalRebateUsd" in payouts.payouts[0]!, false); + assert.equal(makerLifetime.total_fill_count, 9007199254740995n); + assert.equal(makerLifetime.total_earned_usd, "152.40"); + assert.equal(makerLifetime.first_payout_date, null); + assert.equal(daily.daily_summaries[0]?.total_reward_usd, "12.45"); + assert.equal(daily.daily_summaries[0]?.payout_status, "PAID"); + assert.equal(daily.daily_summaries[0]?.events[0]?.event_id, 9007199254740997n); + assert.equal(daily.daily_summaries[0]?.events[0]?.normalized_score, "0.4521"); + assert.equal(daily.daily_summaries[0]?.events[0]?.event_reward_usd, "8.20"); + assert.equal("dailySummaries" in daily, false); + assert.equal(rewardsLifetime.total_earned_usd, "0"); + assert.equal(rewardsLifetime.last_payout_date, null); +}); + +void test("T5f wrappers accept OAuth through the shared AuthStrategy seam", async () => { + const requests: Request[] = []; + const auth = new OAuthAuth({ + client: { type: "public", clientId: "client", redirectUri: "https://example.com/callback" }, + tokenStore: { + async load() { return { accessToken: "access", refreshToken: "refresh", tokenType: "bearer" as const, scope: "orders", expiresAt: 100_000 }; }, + async save() {}, async clear() {}, async runExclusive(operation: () => Promise) { return operation(); }, + }, + now: () => 1000, + }); + const rest = new PredictionMarketsRest(new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async (url, init) => { + requests.push({ url, init }); + return { status: 200, headers: jsonHeaders, async text() { return '{"daily_summaries":[]}'; } }; + }, + })); + + await rest.getLiquidityRewardsDailySummary({ dateFrom: "2026-05-01", dateTo: "2026-05-07" }); + + assert.equal(requests[0]?.init.headers.Authorization, "Bearer access"); + assert.equal(requests[0]?.init.headers["X-GEMINI-APIKEY"], undefined); + const payload = JSON.parse(fromBase64(requests[0]!.init.headers["X-GEMINI-PAYLOAD"]!)); + assert.equal("nonce" in payload, false); +}); + +void test("T5f wrappers preserve mapped reward failures", async () => { + const cases = [ + [403, "AcceptTermsRequired", AcceptTermsRequired], + [403, "MissingRole", MissingRole], + [400, "InvalidRequest", InvalidRequest], + [429, "RateLimit", RateLimitError], + [503, "ServiceUnavailable", ServiceUnavailable], + ] as const; + for (const [status, reason, Expected] of cases) { + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const rest = new PredictionMarketsRest(new HttpTransport({ + env: "sandbox", + auth, + maxRetries: 0, + fetchImpl: async () => ({ status, async text() { return JSON.stringify({ reason }); } }), + })); + await assert.rejects(rest.getLiquidityRewardsLifetimeSummary(), Expected); + } +}); + +void test("T5f wrappers preserve transport and malformed-response failures", async () => { + const failure = new SdkError("network unavailable"); + const auth = new HmacAuth({ apiKey: "key", apiSecret: "secret", now: () => 1000 }); + const networkRest = new PredictionMarketsRest(new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async () => { throw failure; }, + })); + await assert.rejects(networkRest.listMakerRebatePayouts(), (error) => error === failure); + + const malformedRest = new PredictionMarketsRest(new HttpTransport({ + env: "sandbox", + auth, + fetchImpl: async () => ({ status: 200, headers: jsonHeaders, async text() { return "not json"; } }), + })); + await assert.rejects( + malformedRest.getLiquidityRewardsDailySummary({ dateFrom: "2026-05-01", dateTo: "2026-05-07" }), + SdkError, + ); +}); diff --git a/packages/sdk-typescript/src/tests/request-validation.test.ts b/packages/sdk-typescript/src/tests/request-validation.test.ts new file mode 100644 index 0000000..3edce48 --- /dev/null +++ b/packages/sdk-typescript/src/tests/request-validation.test.ts @@ -0,0 +1,191 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { type AuthStrategy, type FetchLike, HttpTransport } from "../core/http.js"; +import { executeRestOperation } from "../core/rest-operation.js"; +import { SdkError, ValidationError } from "../errors.js"; + +const operation = (name: string) => ({ + method: "post", + path: "/test", + operation: name, + access: "authenticated", + parameters: [], + requestBody: true, + requestBodyRequired: true, + successStatuses: [200], + responseMode: "json" as const, + responseContentTypes: ["application/json"], + responseInt64Paths: [], +}); + +function transportWithCounters() { + let authCalls = 0; + let fetchCalls = 0; + const fetchImpl: FetchLike = async () => { + fetchCalls++; + return { status: 200, headers: { get: () => "application/json" }, async text() { return "{}"; } }; + }; + const http = new HttpTransport({ + env: "sandbox", + auth: { + nextNonce: () => { authCalls++; return auth.nextNonce(); }, + credentialHeaders: async (payload) => { authCalls++; return auth.credentialHeaders(payload); }, + }, + fetchImpl, + }); + return { http, counts: () => ({ authCalls, fetchCalls }) }; +} + +const auth: AuthStrategy = { + nextNonce: () => "1", + credentialHeaders: async () => ({ Authorization: "Bearer token" }), +}; + +test("invalid trading order bodies fail before auth or fetch", async () => { + let authCalls = 0; + let fetchCalls = 0; + const fetchImpl: FetchLike = async () => { + fetchCalls++; + return { + status: 200, + headers: { get: () => "application/json" }, + async text() { return "{}"; }, + }; + }; + const http = new HttpTransport({ + env: "sandbox", + auth: { + nextNonce: () => { authCalls++; return auth.nextNonce(); }, + credentialHeaders: async (payload) => { + authCalls++; + return auth.credentialHeaders(payload); + }, + }, + fetchImpl, + }); + + await assert.rejects( + executeRestOperation<{ path: never; query: never; headers: never; body: unknown; response: unknown }>( + http, + { + method: "post", + path: "/v1/order/new", + operation: "trading.createNewOrder", + access: "authenticated", + parameters: [], + requestBody: true, + requestBodyRequired: true, + successStatuses: [200], + responseMode: "json", + responseContentTypes: ["application/json"], + responseInt64Paths: [], + }, + { body: null }, + ), + (error: unknown) => { + assert.ok(error instanceof SdkError); + assert.equal((error as Error).name, "ValidationError"); + assert.equal((error as { operation?: string }).operation, "trading.createNewOrder"); + assert.equal((error as { field?: string }).field, "body"); + assert.equal((error as { rule?: string }).rule, "type"); + return true; + }, + ); + assert.equal(authCalls, 0); + assert.equal(fetchCalls, 0); +}); + +test("documented field rules reject invalid enums, decimals, conditionals, and bounds", async () => { + const cases = [ + ["trading.createNewOrder", { symbol: "BTCUSD", amount: "1", price: "2", side: "hold", type: "exchange limit" }, "side", "enum"], + ["trading.createNewOrder", { symbol: "BTCUSD", amount: "1e2", price: "2", side: "buy", type: "exchange limit" }, "amount", "format"], + ["trading.createNewOrder", { symbol: "BTCUSD", amount: "1", price: "2", side: "buy", type: "exchange stop limit" }, "stop_price", "conditional"], + ["trading.createNewOrder", { symbol: "BTCUSD", amount: "1", price: "2", side: "buy", type: "exchange stop limit", stop_price: "2", options: ["fill-or-kill"] }, "options", "exclusive"], + ["trading.createNewOrder", { symbol: "BTCUSD", amount: "1", price: "2", side: "buy", type: "exchange stop limit", stop_price: "2" }, "stop_price", "relationship"], + ["trading.createNewOrder", { symbol: "BTCUSD", amount: "1", price: "2", side: "sell", type: "exchange stop limit", stop_price: "1" }, "stop_price", "relationship"], + ["accountServices.transferBetweenAccounts", { sourceAccount: "primary", targetAccount: "custody", amount: "1", clientTransferId: "550e8400-e29b-11d4-a716-446655440000" }, "clientTransferId", "format"], + ["predictionMarkets.placeOrderBatch", { orders: [] }, "orders", "bounds"], + ["predictionMarkets.placeOrderBatch", { orders: [{ symbol: "GEMI-FEDJAN26-DN25", orderType: "limit", side: "buy", quantity: "1", price: "1.1", outcome: "yes" }] }, "orders[0].price", "bounds"], + ] as const; + + for (const [name, body, field, rule] of cases) { + const { http, counts } = transportWithCounters(); + await assert.rejects( + executeRestOperation(http, operation(name), { body }), + (error: unknown) => { + assert.ok(error instanceof ValidationError); + assert.equal(error.operation, name); + assert.equal(error.field, field); + assert.equal(error.rule, rule); + return true; + }, + ); + assert.deepEqual(counts(), { authCalls: 0, fetchCalls: 0 }); + } +}); + +test("order status requires exactly one order identifier before auth or fetch", async () => { + const cases = [ + [{ order_id: 1, client_order_id: "client-1" }, "exclusive"], + [{ include_trades: true }, "required"], + [{ order_id: "not-numeric" }, "format"], + [{ order_id: -1 }, "format"], + ] as const; + + for (const [body, rule] of cases) { + const { http, counts } = transportWithCounters(); + await assert.rejects( + executeRestOperation(http, operation("trading.getOrderStatus"), { body }), + (error: unknown) => { + assert.ok(error instanceof ValidationError); + assert.equal(error.operation, "trading.getOrderStatus"); + assert.equal(error.field, "order_id"); + assert.equal(error.rule, rule); + return true; + }, + ); + assert.deepEqual(counts(), { authCalls: 0, fetchCalls: 0 }); + } + + const { http, counts } = transportWithCounters(); + await executeRestOperation(http, operation("trading.getOrderStatus"), { body: { client_order_id: "client-1" } }); + assert.deepEqual(counts(), { authCalls: 2, fetchCalls: 1 }); +}); + +test("stop-limit prices must follow the documented side relationship", async () => { + const { http, counts } = transportWithCounters(); + for (const body of [ + { symbol: "BTCUSD", amount: "1", price: "100.00", side: "buy", type: "exchange stop limit", stop_price: "99.99" }, + { symbol: "BTCUSD", amount: "1", price: "100.00", side: "sell", type: "exchange stop limit", stop_price: "100.01" }, + { symbol: "BTCUSD", amount: "1", price: "100000000000000000.000000000000000001", side: "buy", type: "exchange stop limit", stop_price: "100000000000000000.000000000000000000" }, + ]) { + await executeRestOperation(http, operation("trading.createNewOrder"), { body }); + } + assert.deepEqual(counts(), { authCalls: 6, fetchCalls: 3 }); +}); + +test("valid prediction order bodies remain unchanged for transport", async () => { + const { http, counts } = transportWithCounters(); + const body = { symbol: "GEMI-FEDJAN26-DN25", orderType: "limit", side: "buy", quantity: "100", price: "0.65", outcome: "yes" }; + await executeRestOperation(http, operation("predictionMarkets.placeOrder"), { body }); + assert.deepEqual(counts(), { authCalls: 2, fetchCalls: 1 }); +}); + +test("documented prediction time-in-force values pass validation", async () => { + for (const timeInForce of ["good-til-cancel", "immediate-or-cancel", "fill-or-kill"] as const) { + const { http, counts } = transportWithCounters(); + await executeRestOperation(http, operation("predictionMarkets.placeOrder"), { + body: { symbol: "GEMI-FEDJAN26-DN25", orderType: "limit", side: "buy", quantity: "100", price: "0.65", outcome: "yes", timeInForce }, + }); + assert.deepEqual(counts(), { authCalls: 2, fetchCalls: 1 }); + } +}); + +test("withdrawals accept any canonical UUID version", async () => { + const { http, counts } = transportWithCounters(); + await executeRestOperation(http, operation("accountServices.withdrawCryptoFunds"), { + body: { address: "0x123", amount: "1", clientTransferId: "550e8400-e29b-11d4-a716-446655440000" }, + }); + assert.deepEqual(counts(), { authCalls: 2, fetchCalls: 1 }); +}); diff --git a/packages/sdk-typescript/src/tests/rest-operation.test.ts b/packages/sdk-typescript/src/tests/rest-operation.test.ts new file mode 100644 index 0000000..aeba8b9 --- /dev/null +++ b/packages/sdk-typescript/src/tests/rest-operation.test.ts @@ -0,0 +1,646 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + type AuthStrategy, + type FetchLike, + type HttpMethod, + HttpTransport, + type RestQueryParameter, +} from "../core/http.js"; +import { SdkError } from "../errors.js"; +import { + PREDICTION_MARKET_OPERATIONS, + type PredictionMarketOperationTypes, +} from "../generated/operations.js"; +import { TRADING_OPERATIONS } from "../generated/trading/operations.js"; +import { executeRestOperation } from "../core/rest-operation.js"; +import type { DiagnosticEvent } from "../diagnostics.js"; +import { fromBase64 } from "../core/encoding.js"; + +type Request = { + url: string; + init: { method: HttpMethod; headers: Record; body?: string }; +}; + +const publicOperation = { + method: "get", + path: "/v1/items/{item}", + access: "public", + parameters: [ + { name: "item", in: "path", required: true, style: "simple", explode: false }, + { name: "tags", in: "query", required: false, style: "form", explode: true, shape: "array" }, + { name: "active", in: "query", required: false, style: "form", explode: true, shape: "scalar" }, + { name: "id", in: "query", required: false, style: "form", explode: true, shape: "scalar" }, + { name: "amount", in: "query", required: false, style: "form", explode: true, shape: "scalar" }, + ], + headers: [{ name: "X-Trace", required: false }], + requestBody: false, + requestBodyRequired: false, + successStatuses: [200], + responseMode: "json", + responseContentTypes: ["application/json"], + responseInt64Paths: [], + retryable: true, +} as const; + +const privateOperation = { + method: "post", + path: "/v1/items/{item}", + access: "authenticated", + parameters: [ + { name: "item", in: "path", required: true, style: "simple", explode: false }, + { name: "active", in: "query", required: false, style: "form", explode: true, shape: "scalar" }, + ], + headers: [{ name: "Idempotency-Key", required: false }], + requestBody: true, + requestBodyRequired: true, + successStatuses: [200], + responseMode: "json", + responseContentTypes: ["application/json"], + responseInt64Paths: [], + retryable: false, +} as const; + +const privateGetOperation = { + ...privateOperation, + method: "get", + requestBody: false, + requestBodyRequired: false, + retryable: true, +} as const; + +function transport(opts: { + auth?: AuthStrategy; + statuses?: number[]; + response?: string; + failure?: SdkError; + contentType?: string | null; + onDiagnostic?: (event: DiagnosticEvent) => void; +} = {}): { transport: HttpTransport; requests: Request[] } { + const requests: Request[] = []; + const fetchImpl: FetchLike = async (url, init) => { + requests.push({ url, init }); + if (opts.failure) throw opts.failure; + return { + status: opts.statuses?.shift() ?? 200, + headers: { get: (name) => name.toLowerCase() === "content-type" ? (opts.contentType ?? "application/json") : null }, + async text() { return opts.response ?? "{}"; }, + }; + }; + return { + transport: new HttpTransport({ + env: "sandbox", + auth: opts.auth, + fetchImpl, + maxRetries: 1, + sleep: async () => {}, + random: () => 0, + onDiagnostic: opts.onDiagnostic, + }), + requests, + }; +} + +test("generated operations expose only safe order context in diagnostics", async () => { + const events: DiagnosticEvent[] = []; + const auth: AuthStrategy = { + nextNonce: () => undefined, + credentialHeaders: async () => ({ Authorization: "Bearer secret" }), + }; + const { transport: http } = transport({ auth, onDiagnostic: (event) => events.push(event) }); + + await executeRestOperation<{ path: { item: string }; query: never; headers: never; body: { clientOrderId: string; token: string }; response: {} }>( + http, + { ...privateOperation, operation: "trading.placeOrder" }, + { path: { item: "order" }, body: { clientOrderId: "client-1", token: "secret-payload" } }, + ); + + const end = events.find((event) => event.name === "request.end"); + assert.deepEqual(end?.operationContext, { + operation: "trading.placeOrder", + clientOrderId: "client-1", + }); + assert.equal(JSON.stringify(end).includes("secret-payload"), false); +}); + +test("generated operation types can be passed to the executor", async () => { + const { transport: http } = transport(); + + await executeRestOperation( + http, + PREDICTION_MARKET_OPERATIONS.getEvent, + { path: { eventTicker: "event" } }, + ); +}); + +test("executor forwards response int64 paths to HttpTransport", async () => { + const { transport: http } = transport({ response: '{"orderId":9007199254740993}' }); + const response = await executeRestOperation<{ + path: never; + query: never; + body: never; + response: { orderId: bigint }; + }>( + http, + { + ...publicOperation, + path: "/v1/order", + parameters: [], + responseInt64Paths: [["orderId"]], + }, + ); + + assert.equal(response.orderId, 9007199254740993n); +}); + +test("public GET is unsigned and preserves query serialization", async () => { + let authCalls = 0; + const { transport: http, requests } = transport({ + auth: { + nextNonce: () => { authCalls++; return "1700000000000"; }, + credentialHeaders: async () => { authCalls++; return {}; }, + }, + }); + + await executeRestOperation<{ path: { item: string }; query: Record; headers: Record; body: never; response: {} }>( + http, + publicOperation, + { path: { item: "a/b:c" }, query: { tags: ["one", "two"], active: true, id: 9007199254740993n, amount: "1.2300" }, headers: { "X-Trace": "trace" } }, + ); + + assert.equal(requests[0]?.url, "https://api.sandbox.gemini.com/v1/items/a%2Fb:c?tags=one&tags=two&active=true&id=9007199254740993&amount=1.2300"); + assert.deepEqual(requests[0]?.init.headers, { Accept: "application/json", "X-Trace": "trace" }); + assert.equal(authCalls, 0); +}); + +function queryOperation(parameters: readonly RestQueryParameter[]) { + return { + ...publicOperation, + path: "/v1/items", + parameters, + headers: [], + } as const; +} + +async function queryUrl(parameters: readonly RestQueryParameter[], query: Record) { + const { transport: http, requests } = transport(); + await executeRestOperation<{ path: never; query: Record; headers: never; body: never; response: {} }>( + http, + queryOperation(parameters), + { query }, + ); + return requests[0]?.url; +} + +test("query serialization follows OpenAPI style and explode metadata", async () => { + assert.equal( + await queryUrl([{ name: "tags", in: "query", required: false, style: "form", explode: false, shape: "array" }], { tags: ["one", "two"] }), + "https://api.sandbox.gemini.com/v1/items?tags=one,two", + ); + assert.equal( + await queryUrl([{ name: "filter", in: "query", required: false, style: "form", explode: false, shape: "object" }], { filter: { status: "open", side: "buy" } }), + "https://api.sandbox.gemini.com/v1/items?filter=status,open,side,buy", + ); + assert.equal( + await queryUrl([{ name: "filter", in: "query", required: false, style: "form", explode: true, shape: "object" }], { filter: { status: "open", side: "buy" } }), + "https://api.sandbox.gemini.com/v1/items?status=open&side=buy", + ); + assert.equal( + await queryUrl([{ name: "tags", in: "query", required: false, style: "spaceDelimited", explode: false, shape: "array" }], { tags: ["one", "two"] }), + "https://api.sandbox.gemini.com/v1/items?tags=one%20two", + ); + assert.equal( + await queryUrl([{ name: "tags", in: "query", required: false, style: "pipeDelimited", explode: false, shape: "array" }], { tags: ["one", "two"] }), + "https://api.sandbox.gemini.com/v1/items?tags=one%7Ctwo", + ); + assert.equal( + await queryUrl([{ name: "filter", in: "query", required: false, style: "deepObject", explode: true, shape: "object" }], { filter: { status: "open" } }), + "https://api.sandbox.gemini.com/v1/items?filter%5Bstatus%5D=open", + ); + assert.equal( + await queryUrl([{ name: "value", in: "query", required: false, style: "form", explode: true, shape: "scalar", allowReserved: true }], { value: "a/b?c" }), + "https://api.sandbox.gemini.com/v1/items?value=a/b?c", + ); +}); + +test("query validation rejects null, unknown fields, and invalid style shapes before dispatch", async () => { + const { transport: http, requests } = transport(); + const operation = queryOperation([{ name: "tags", in: "query", required: false, style: "form", explode: true, shape: "array" }]); + for (const query of [{ tags: null }, { tags: ["one", null] }, { unknown: "value" }, { tags: { nested: { value: "bad" } } }]) { + await assert.rejects( + executeRestOperation<{ path: never; query: Record; headers: never; body: never; response: {} }>(http, operation, { query }), + /null|unexpected query parameter|scalar|array/i, + ); + } + assert.equal(requests.length, 0); +}); + +test("generated scalar query parameters reject object values before dispatch", async () => { + const { transport: http, requests } = transport(); + await assert.rejects( + executeRestOperation( + http, + PREDICTION_MARKET_OPERATIONS.getMakerRebateRates, + { query: { category: { ignored: "500" } } } as never, + ), + /scalar/i, + ); + assert.equal(requests.length, 0); +}); + +test("required query and caller-owned headers are checked case-insensitively", async () => { + const { transport: http, requests } = transport(); + const operation = { + ...queryOperation([{ name: "symbol", in: "query", required: true, style: "form", explode: true, shape: "scalar" }]), + headers: [{ name: "X-Request-Id", required: true }], + } as const; + for (const input of [{ query: {} }, { query: { symbol: null } }, { query: { symbol: "BTCUSD" }, headers: {} }, { query: { symbol: "BTCUSD" }, headers: { "X-Request-Id": null } }]) { + await assert.rejects( + executeRestOperation<{ path: never; query: Record; headers: Record; body: never; response: {} }>(http, operation, input as never), + /missing|required|null/i, + ); + } + await executeRestOperation<{ path: never; query: Record; headers: Record; body: never; response: {} }>( + http, + operation, + { query: { symbol: "BTCUSD" }, headers: { "x-request-id": "request-1" } }, + ); + assert.equal(requests[0]?.url, "https://api.sandbox.gemini.com/v1/items?symbol=BTCUSD"); + assert.equal(requests[0]?.init.headers["x-request-id"], "request-1"); +}); + +test("file operations use the shared executor path for public downloads", async () => { + const bytes = new Uint8Array([1, 2, 3]); + const requests: Request[] = []; + const fetchImpl: FetchLike = async (url, init) => { + requests.push({ url, init }); + return { + status: 200, + headers: { + get: (name) => ({ + "content-type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "content-disposition": "attachment; filename=FundingAmount_BTCGUSDPERP.xlsx", + })[name.toLowerCase()] ?? null, + }, + async arrayBuffer() { return bytes.buffer; }, + async text() { throw new Error("file success should not read text"); }, + }; + }; + const http = new HttpTransport({ env: "sandbox", fetchImpl }); + + const response = await executeRestOperation<{ + path: never; + query: { symbol: string }; + headers: never; + body: never; + response: { bytes: Uint8Array; contentType?: string; contentDisposition?: string }; + }>( + http, + { + ...publicOperation, + path: "/v1/fundingamountreport/records.xlsx", + parameters: [{ name: "symbol", in: "query", required: true, style: "form", explode: true, shape: "scalar" }], + responseMode: "file", + responseContentTypes: [ + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "text/csv", + ], + responseInt64Paths: [], + }, + { query: { symbol: "BTCGUSDPERP" } }, + ); + + assert.deepEqual(response.bytes, bytes); + assert.equal(response.contentType, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + assert.equal(response.contentDisposition, "attachment; filename=FundingAmount_BTCGUSDPERP.xlsx"); + assert.equal(requests[0]?.url, "https://api.sandbox.gemini.com/v1/fundingamountreport/records.xlsx?symbol=BTCGUSDPERP"); +}); + +test("file operations use the shared executor path for authenticated downloads", async () => { + const auth: AuthStrategy = { + nextNonce: () => undefined, + credentialHeaders: async () => ({ Authorization: "Bearer token" }), + }; + const fetchImpl: FetchLike = async () => ({ + status: 200, + headers: { + get: (name) => ({ + "content-type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "content-disposition": "attachment; filename=FundingPayment_Report.xlsx", + })[name.toLowerCase()] ?? null, + }, + async arrayBuffer() { return new Uint8Array([4, 5]).buffer; }, + async text() { throw new Error("file success should not read text"); }, + }); + const http = new HttpTransport({ env: "sandbox", auth, fetchImpl }); + + const response = await executeRestOperation<{ + path: never; + query: { fromDate?: string }; + headers: never; + body: { account?: string }; + response: { bytes: Uint8Array; contentDisposition?: string }; + }>( + http, + { + ...privateOperation, + path: "/v1/perpetuals/fundingpaymentreport/records.xlsx", + parameters: [{ name: "fromDate", in: "query", required: false, style: "form", explode: true, shape: "scalar" }], + responseMode: "file", + responseContentTypes: ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"], + responseInt64Paths: [], + }, + { query: { fromDate: "2024-04-10" }, body: { account: "primary" } }, + ); + + assert.deepEqual(response.bytes, new Uint8Array([4, 5])); + assert.equal(response.contentDisposition, "attachment; filename=FundingPayment_Report.xlsx"); +}); + +test("executor enforces declared statuses and normalized success media types", async () => { + const json = transport({ contentType: "Application/JSON; charset=utf-8" }); + await executeRestOperation<{ path: never; query: never; headers: never; body: never; response: {} }>( + json.transport, + { ...publicOperation, path: "/v1/items", parameters: [] }, + ); + const expected = transport({ statuses: [201] }); + await executeRestOperation<{ path: never; query: never; headers: never; body: never; response: {} }>( + expected.transport, + { ...publicOperation, path: "/v1/items", parameters: [], successStatuses: [201] }, + ); + + const unexpected = transport({ statuses: [200] }); + await assert.rejects( + executeRestOperation<{ path: never; query: never; headers: never; body: never; response: {} }>( + unexpected.transport, + { ...publicOperation, path: "/v1/items", parameters: [], successStatuses: [201] }, + ), + /unexpected success status 200/i, + ); +}); + +test("file operations reject missing and undeclared success media types", async () => { + const operation = { + ...publicOperation, + path: "/v1/items.xlsx", + parameters: [], + responseMode: "file" as const, + responseContentTypes: ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"], + }; + for (const contentType of [null, "text/html", "application/json"]) { + const { transport: http } = transport({ contentType }); + await assert.rejects( + executeRestOperation<{ path: never; query: never; headers: never; body: never; response: { bytes: Uint8Array } }>(http, operation), + /success content type/i, + ); + } +}); + +test("authenticated operations use the injected AuthStrategy boundary", async () => { + for (const credentialHeaders of [ + async () => ({ "X-GEMINI-APIKEY": "key", "X-GEMINI-SIGNATURE": "signature" }), + async () => ({ Authorization: "Bearer token" }), + ]) { + let authCalls = 0; + const { transport: http, requests } = transport({ + auth: { + nextNonce: () => { authCalls++; return undefined; }, + credentialHeaders: async () => { authCalls++; return credentialHeaders(); }, + }, + }); + const body = { price: "1.2300", quantity: 9007199254740993n }; + + await executeRestOperation<{ path: { item: string }; query: never; headers: { "Idempotency-Key": string }; body: typeof body; response: {} }>( + http, + privateOperation, + { path: { item: "item" }, body, headers: { "Idempotency-Key": "request-1" } }, + ); + + assert.equal(authCalls, 2); + assert.equal(requests[0]?.init.headers.Accept, "application/json"); + const payload = fromBase64(requests[0]?.init.headers["X-GEMINI-PAYLOAD"] ?? ""); + assert.equal(payload, '{"request":"/v1/items/item","price":"1.2300","quantity":9007199254740993}'); + assert.equal(requests[0]?.init.headers["Idempotency-Key"], "request-1"); + await executeRestOperation<{ path: { item: string }; query: { active: boolean }; headers: never; body: never; response: {} }>( + http, + privateGetOperation, + { path: { item: "item" }, query: { active: true } }, + ); + assert.equal(requests[1]?.init.method, "GET"); + assert.equal(requests[1]?.url, "https://api.sandbox.gemini.com/v1/items/item?active=true"); + assert.equal(authCalls, 4); + } +}); + +test("executor supports every discovered HTTP method", async () => { + const { transport: http, requests } = transport({ + auth: { nextNonce: () => undefined, credentialHeaders: async () => ({}) }, + }); + + for (const method of ["get", "post", "put", "patch", "delete"] as const) { + await executeRestOperation<{ path: never; query: never; headers: never; body: never; response: {} }>( + http, + { + ...publicOperation, + method, + path: `/v1/${method}`, + parameters: [], + }, + ); + assert.equal(requests.at(-1)?.init.method, method.toUpperCase()); + } +}); + +test("executor accepts safe request int64 numbers and rejects unsafe ones before auth", async () => { + let authCalls = 0; + const requests: Request[] = []; + const http = new HttpTransport({ + env: "sandbox", + auth: { + nextNonce: () => { authCalls++; return undefined; }, + credentialHeaders: async () => { authCalls++; return {}; }, + }, + fetchImpl: async (url, init) => { + requests.push({ url, init }); + return { + status: 200, + headers: { get: () => "application/json" }, + async text() { return "{}"; }, + }; + }, + }); + const operation = { + ...privateOperation, + operation: "trading.getOrderStatus", + path: "/v1/order/status", + parameters: [], + requestInt64Paths: { + body: [{ path: ["order_id"], unsigned: true }], + path: [], + query: [], + }, + }; + + await executeRestOperation<{ path: never; query: never; headers: never; body: { order_id: bigint | number }; response: {} }>( + http, + operation, + { body: { order_id: Number.MAX_SAFE_INTEGER } }, + ); + const payload = fromBase64(requests[0]!.init.headers["X-GEMINI-PAYLOAD"]); + assert.match(payload, /"order_id":9007199254740991/); + + await executeRestOperation<{ path: never; query: never; headers: never; body: { order_id: bigint | number }; response: {} }>( + http, + operation, + { body: { order_id: 18446744073709551615n } }, + ); + const widePayload = fromBase64(requests[1]!.init.headers["X-GEMINI-PAYLOAD"]); + assert.match(widePayload, /"order_id":18446744073709551615/); + + await assert.rejects( + executeRestOperation<{ path: never; query: never; headers: never; body: { order_id: bigint | number }; response: {} }>( + http, + operation, + { body: { order_id: Number.MAX_SAFE_INTEGER + 1 } }, + ), + (error: unknown) => + error instanceof SdkError && + error.name === "ValidationError" && + "operation" in error && + error.operation === "trading.getOrderStatus" && + "field" in error && + error.field === "order_id" && + "rule" in error && + error.rule === "format", + ); + assert.equal(authCalls, 4); + assert.equal(requests.length, 2); +}); + +test("authenticated operations retain transport failures and retry behavior", async () => { + const auth: AuthStrategy = { nextNonce: () => undefined, credentialHeaders: async () => ({}) }; + const retry = transport({ auth, statuses: [429, 200] }); + await executeRestOperation<{ path: { item: string }; query: { active: boolean }; headers: never; body: never; response: {} }>( + retry.transport, + privateGetOperation, + { path: { item: "item" }, query: { active: true } }, + ); + assert.equal(retry.requests.length, 2); + + const failure = new SdkError("transport failure"); + const failing = transport({ auth, failure }); + await assert.rejects( + executeRestOperation<{ path: { item: string }; query: { active: boolean }; headers: never; body: never; response: {} }>( + failing.transport, + privateGetOperation, + { path: { item: "item" }, query: { active: true } }, + ), + (error: unknown) => error === failure, + ); +}); + +test("Trading order placement and cancellation mutations are sent at most once", async () => { + const auth: AuthStrategy = { nextNonce: () => undefined, credentialHeaders: async () => ({}) }; + const mutations = [ + [TRADING_OPERATIONS.createNewOrder, { symbol: "BTCUSD", amount: "1", price: "100", side: "buy", type: "exchange limit" }], + [TRADING_OPERATIONS.cancelAllActiveOrders, {}], + [TRADING_OPERATIONS.cancelAllSessionOrders, {}], + [TRADING_OPERATIONS.cancelOrder, { order_id: 1 }], + ] as const; + + for (const [operation, body] of mutations) { + assert.equal(operation.retryable, false); + const failure = new SdkError("transport failure"); + const failing = transport({ auth, failure }); + await assert.rejects( + executeRestOperation<{ path: never; query: never; headers: never; body: unknown; response: unknown }>( + failing.transport, + operation, + { body }, + ), + (error: unknown) => error === failure, + ); + assert.equal(failing.requests.length, 1); + } +}); + +test("executor rejects invalid descriptor inputs before request dispatch", async () => { + const { transport: unsigned, requests } = transport(); + await assert.rejects( + executeRestOperation<{ path: { item: string }; query: never; headers: never; body: { value: string }; response: {} }>( + unsigned, + privateOperation, + { path: { item: "item" }, body: { value: "exact" } }, + ), + /private request requires an injected AuthStrategy/i, + ); + await assert.rejects( + executeRestOperation<{ path: {}; query: never; headers: never; body: never; response: {} }>( + unsigned, + publicOperation, + ), + /missing path parameter item/i, + ); + await assert.rejects( + executeRestOperation<{ path: { item: string }; query: never; headers: never; body: never; response: {} }>( + unsigned, + { ...publicOperation, responseMode: "xml" as "json" }, + { path: { item: "item" } }, + ), + (error: unknown) => error instanceof SdkError && /response mode/i.test(error.message), + ); + await assert.rejects( + executeRestOperation<{ path: { item: string }; query: never; headers: never; body: { value: string }; response: {} }>( + unsigned, + { ...publicOperation, requestBody: true, requestBodyRequired: true }, + { path: { item: "item" }, body: { value: "exact" } }, + ), + /public.*body/i, + ); + const signed = transport({ + auth: { nextNonce: () => undefined, credentialHeaders: async () => ({}) }, + }); + for (const name of [ + "Authorization", + "X-GEMINI-CUSTOM", + "Content-Length", + "Content-Type", + "Cache-Control", + ]) { + await assert.rejects( + executeRestOperation<{ path: { item: string }; query: never; headers: Record; body: { value: string }; response: {} }>( + signed.transport, + privateOperation, + { path: { item: "item" }, body: { value: "exact" }, headers: { [name]: "caller" } }, + ), + /reserved/i, + ); + } + await assert.rejects( + executeRestOperation<{ path: { item: string }; query: never; headers: never; body: never; response: {} }>( + signed.transport, + privateGetOperation, + { path: { item: "item" }, headers: { Accept: "text/html" } } as never, + ), + /accept.*reserved/i, + ); + await assert.rejects( + executeRestOperation<{ path: { item: string }; query: { required: string }; headers: never; body: never; response: {} }>( + unsigned, + { ...publicOperation, path: "/v1/items", parameters: [{ name: "required", in: "query", required: true, style: "form", explode: true, shape: "scalar" }] }, + { path: { item: "item" } } as never, + ), + /missing query parameter required/i, + ); + await assert.rejects( + executeRestOperation<{ path: { item: string }; query: { tags: unknown[] }; headers: never; body: never; response: {} }>( + unsigned, + { ...publicOperation, path: "/v1/items", parameters: [{ name: "tags", in: "query", required: false, style: "form", explode: true, shape: "array" }] }, + { path: { item: "item" }, query: { tags: [{}] } }, + ), + /generated array shape/i, + ); + assert.equal(requests.length, 0); + assert.equal(signed.requests.length, 0); +}); diff --git a/packages/sdk-typescript/src/tests/transport.test.ts b/packages/sdk-typescript/src/tests/transport.test.ts new file mode 100644 index 0000000..4a9c1cf --- /dev/null +++ b/packages/sdk-typescript/src/tests/transport.test.ts @@ -0,0 +1,748 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { WsTransport, type SocketLike } from "../transport.js"; +import { NoopLogger } from "../logging.js"; +import { ConnectionError, SdkError } from "../errors.js"; +import { FakeSocket } from "./fake-socket.js"; + + +type CapturedSocketFactoryOptions = { headers?: Record }; + +test("connect() resolves once the socket opens and emits 'open'", async () => { + const fake = new FakeSocket(); + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => fake, + }); + + let openEvents = 0; + transport.on("open", () => { + openEvents++; + }); + + const connected = transport.connect(); + fake.fire("open"); + await connected; + + assert.equal(openEvents, 1, "'open' event must fire exactly once"); +}); + +test("WebSocket diagnostics omit URL credentials and query parameters", async () => { + const fake = new FakeSocket(); + const events: unknown[] = []; + const transport = new WsTransport("wss://client:secret@example.test/v1?token=private#fragment", { + logger: new NoopLogger(), + onDiagnostic: (event) => events.push(event), + socketFactory: () => fake, + }); + + const connected = transport.connect(); + fake.fire("open"); + await connected; + + assert.equal(JSON.stringify(events).includes("secret"), false); + assert.equal(JSON.stringify(events).includes("private"), false); + assert.equal((events[0] as { metadata?: { url?: string } }).metadata?.url, "wss://example.test/v1"); +}); + +test("incoming frame is emitted as a lossless-parsed 'message' (big id stays bigint)", async () => { + const fake = new FakeSocket(); + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => fake, + }); + + let received: unknown; + transport.on("message", (msg: unknown) => { + received = msg; + }); + + const connected = transport.connect(); + fake.fire("open"); + await connected; + + // lastUpdateId exceeds 2^53; raw JSON.parse would silently round it. + fake.fire("message", { data: '{"lastUpdateId":9007199254740993,"bids":[]}' }); + + assert.deepEqual(received, { + lastUpdateId: 9007199254740993n, + bids: [], + }); +}); + +test("a malformed frame emits ConnectionError and leaves the socket up", async () => { + const fake = new FakeSocket(); + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => fake, + }); + + const errors: unknown[] = []; + transport.on("error", (err: unknown) => { + errors.push(err); + }); + let goodMessages = 0; + transport.on("message", () => { + goodMessages++; + }); + + const connected = transport.connect(); + fake.fire("open"); + await connected; + + fake.fire("message", { data: "}{ not json" }); + + assert.equal(errors.length, 1, "malformed frame must surface exactly one error"); + assert.ok(errors[0] instanceof ConnectionError, "error must be a ConnectionError"); + assert.equal(fake.closed, false, "one bad frame must not tear down the socket"); + + // A subsequent good frame still flows — the connection survived. + fake.fire("message", { data: '{"ok":true}' }); + assert.equal(goodMessages, 1); +}); + +test("a non-string frame emits a typed error and closes the socket", async () => { + const fake = new FakeSocket(); + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => fake, + }); + const errors: unknown[] = []; + transport.on("error", (err: unknown) => errors.push(err)); + + const connected = transport.connect(); + fake.fire("open"); + await connected; + fake.fire("message", { data: new TextEncoder().encode("{}") }); + + assert.equal(errors.length, 1); + assert.ok(errors[0] instanceof ConnectionError); + assert.match((errors[0] as Error).message, /must be a string/); + assert.equal(fake.closed, true); + transport.close(); +}); + +test("socket close errors expose cause, close metadata, and whether it opened", async () => { + const fake = new FakeSocket(); + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => fake, + backoff: { baseMs: 100, capMs: 100, factor: 2 }, + }); + const errors: unknown[] = []; + transport.on("error", (error) => errors.push(error)); + const connected = transport.connect(); + fake.fire("open"); + await connected; + + fake.fire("close", { code: 4001, reason: "expired" }); + + assert.equal((errors[0] as ConnectionError).opened, true); + assert.equal((errors[0] as ConnectionError).closeCode, 4001); + assert.equal((errors[0] as ConnectionError).closeReason, "expired"); + transport.close(); +}); + +test("socket error events preserve their underlying cause", async () => { + const fake = new FakeSocket(); + const transport = new WsTransport("wss://example.test/v1", { socketFactory: () => fake }); + const errors: unknown[] = []; + transport.on("error", (error) => errors.push(error)); + const connected = transport.connect(); + fake.fire("open"); + await connected; + + const cause = new Error("network reset"); + fake.fire("error", { error: cause }); + + assert.equal((errors[0] as ConnectionError).cause, cause); + transport.close(); +}); + +test("oversized inbound messages are rejected before parsing", async () => { + const fake = new FakeSocket(); + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => fake, + maxMessageSizeBytes: 4, + }); + const messages: unknown[] = []; + transport.on("message", (message) => messages.push(message)); + const connected = transport.connect(); + fake.fire("open"); + await connected; + + fake.fire("message", { data: '{"x":1}' }); + + assert.equal(fake.closed, true); + assert.deepEqual(messages, []); + transport.close(); +}); + +test("a malformed frame does not crash when no 'error' listener is attached", async () => { + const fake = new FakeSocket(); + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => fake, + }); + + // Deliberately attach NO 'error' listener — an ordinary consumer that only + // cares about messages. Node's EventEmitter re-throws an unhandled 'error', + // which would crash the process; a resilient transport must not die on one bad frame. + let goodMessages = 0; + transport.on("message", () => { + goodMessages++; + }); + + const connected = transport.connect(); + fake.fire("open"); + await connected; + + fake.fire("message", { data: "}{ not json" }); // must NOT throw + assert.equal(fake.closed, false, "socket must survive a bad frame"); + + fake.fire("message", { data: '{"ok":true}' }); + assert.equal(goodMessages, 1, "good frames still flow after a bad one"); +}); + +test("subscribe(sub) sends the subscription as a serialized frame", async () => { + const fake = new FakeSocket(); + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => fake, + }); + + const connected = transport.connect(); + fake.fire("open"); + await connected; + + const sub = { type: "subscribe", subscriptions: [{ name: "l2", symbols: ["BTCUSD"] }] }; + transport.subscribe(sub); + + assert.equal(fake.sent.length, 1, "subscribe must send exactly one frame"); + assert.deepEqual(JSON.parse(fake.sent[0]), sub, "frame must be the serialized sub"); +}); + +test("socketFactory receives connection headers", () => { + let received: CapturedSocketFactoryOptions | undefined; + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + headers: { Authorization: "Bearer token" }, + socketFactory: (_url, options) => { + received = options; + return new FakeSocket(); + }, + }); + + void transport.connect(); + + assert.deepEqual(received, { headers: { Authorization: "Bearer token" } }); + transport.close(); +}); + +test("send(frame) fails loud before the socket opens", () => { + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => new FakeSocket(), + }); + + assert.throws(() => transport.send({ id: 1, method: "ping" }), SdkError); +}); + +test("send(frame) sends a one-shot frame that is not replayed on reconnect", async (t) => { + const sockets: FakeSocket[] = []; + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => { + const socket = new FakeSocket(); + sockets.push(socket); + return socket; + }, + }); + t.mock.timers.enable({ apis: ["setTimeout"] }); + + const connected = transport.connect(); + sockets[0].fire("open"); + await connected; + + transport.send({ id: 1, method: "ping" }); + transport.subscribe({ id: 2, method: "SUBSCRIBE", params: ["btcusd@trade"] }); + + sockets[0].fire("close"); + t.mock.timers.tick(0); + sockets[1].fire("open"); + + assert.deepEqual(sockets[0].sent.map((frame) => JSON.parse(frame)), [ + { id: 1, method: "ping" }, + { id: 2, method: "SUBSCRIBE", params: ["btcusd@trade"] }, + ]); + assert.deepEqual(sockets[1].sent.map((frame) => JSON.parse(frame)), [ + { id: 2, method: "SUBSCRIBE", params: ["btcusd@trade"] }, + ]); +}); + +test("reconnect() closes the live socket and replays every durable subscription exactly once", async (t) => { + const sockets: FakeSocket[] = []; + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => { + const socket = new FakeSocket(); + sockets.push(socket); + return socket; + }, + }); + + t.mock.timers.enable({ apis: ["setTimeout"] }); + const connected = transport.connect(); + sockets[0].fire("open"); + await connected; + + const btc = { id: 1, method: "SUBSCRIBE", params: ["btcusd@depth"] }; + const eth = { id: 2, method: "SUBSCRIBE", params: ["ethusd@depth"] }; + transport.subscribe(btc); + transport.subscribe(eth); + + transport.reconnect(); + assert.equal(sockets[0].closed, true, "restart closes the current socket"); + sockets[0].fire("close"); + t.mock.timers.tick(0); + sockets[1].fire("open"); + assert.deepEqual( + sockets[1].sent.map((frame) => JSON.parse(frame)), + [btc, eth], + "every durable subscription replays once on the fresh socket", + ); +}); + +test("reconnect() drops frames from the closing socket before its close event", async () => { + const socket = new FakeSocket(); + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => socket, + }); + const messages: unknown[] = []; + transport.on("message", (frame) => messages.push(frame)); + + const connected = transport.connect(); + socket.fire("open"); + await connected; + + transport.reconnect(); + socket.fire("message", { data: '{"stale":true}' }); + + assert.deepEqual(messages, [], "the socket is stale as soon as restart begins"); +}); + +test("unsubscribe() removes only that durable subscription from reconnect replay", async (t) => { + const sockets: FakeSocket[] = []; + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => { + const socket = new FakeSocket(); + sockets.push(socket); + return socket; + }, + }); + t.mock.timers.enable({ apis: ["setTimeout"] }); + const connected = transport.connect(); + sockets[0].fire("open"); + await connected; + + const btc = { id: 1, method: "SUBSCRIBE", params: ["btcusd@depth"] }; + const eth = { id: 2, method: "SUBSCRIBE", params: ["ethusd@depth"] }; + transport.subscribe(btc); + transport.subscribe(eth); + transport.unsubscribe(btc); + + transport.reconnect(); + sockets[0].fire("close"); + t.mock.timers.tick(0); + sockets[1].fire("open"); + + assert.deepEqual(sockets[1].sent.map((frame) => JSON.parse(frame)), [eth]); +}); + +test("an unexpected drop announces 'reconnecting' and opens a fresh socket immediately", async (t) => { + const sockets: FakeSocket[] = []; + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => { + const s = new FakeSocket(); + sockets.push(s); + return s; + }, + }); + + let reconnecting = 0; + transport.on("reconnecting", () => { + reconnecting++; + }); + + t.mock.timers.enable({ apis: ["setTimeout"] }); + + const connected = transport.connect(); + sockets[0].fire("open"); + await connected; + assert.equal(sockets.length, 1, "one socket so far"); + + sockets[0].fire("close"); // unexpected drop + assert.equal(reconnecting, 1, "drop must announce 'reconnecting'"); + + t.mock.timers.tick(0); // immediate first retry (delay 0) + assert.equal(sockets.length, 2, "must open a fresh socket to reconnect"); + + sockets[1].fire("open"); // connection restored, no throw +}); + +test("remembered subscriptions are replayed on reconnect", async (t) => { + const sockets: FakeSocket[] = []; + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => { + const s = new FakeSocket(); + sockets.push(s); + return s; + }, + }); + + t.mock.timers.enable({ apis: ["setTimeout"] }); + + const connected = transport.connect(); + sockets[0].fire("open"); + await connected; + + const sub = { type: "subscribe", subscriptions: [{ name: "l2", symbols: ["BTCUSD"] }] }; + transport.subscribe(sub); + assert.equal(sockets[0].sent.length, 1, "sent live on the open socket"); + + sockets[0].fire("close"); // drop + t.mock.timers.tick(0); // reconnect + sockets[1].fire("open"); // fresh socket — exchange has forgotten our subs + + assert.equal(sockets[1].sent.length, 1, "sub must be replayed on the new socket"); + assert.deepEqual(JSON.parse(sockets[1].sent[0]), sub); +}); + +// Reconnect a transport whose sockets never open, driving repeated failures. +// random:()=>1 makes equal-jitter contribute its max, so delay === the exact +// base value — lets us assert cadence boundaries precisely. +function makeFlappy(t: { mock: { timers: { enable(o: unknown): void } } }) { + const sockets: FakeSocket[] = []; + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => { + const s = new FakeSocket(); + sockets.push(s); + return s; + }, + backoff: { baseMs: 100, capMs: 400, factor: 2 }, + random: () => 1, + }); + t.mock.timers.enable({ apis: ["setTimeout"] }); + return { sockets, transport }; +} + +test("reconnect backoff grows exponentially and is capped", async (t) => { + const { sockets, transport } = makeFlappy(t); + + const connected = transport.connect(); + sockets[0].fire("open"); + await connected; + + // attempt 0: immediate (0ms) + sockets[0].fire("close"); + t.mock.timers.tick(0); + assert.equal(sockets.length, 2, "first retry is immediate"); + + // attempt 1: base = 100ms + sockets[1].fire("close"); + t.mock.timers.tick(99); + assert.equal(sockets.length, 2, "must still be waiting at 99ms"); + t.mock.timers.tick(1); + assert.equal(sockets.length, 3, "reconnects at 100ms"); + + // attempt 2: 200ms + sockets[2].fire("close"); + t.mock.timers.tick(199); + assert.equal(sockets.length, 3); + t.mock.timers.tick(1); + assert.equal(sockets.length, 4, "reconnects at 200ms"); + + // attempt 3: 400ms (100*2^2) + sockets[3].fire("close"); + t.mock.timers.tick(400); + assert.equal(sockets.length, 5, "reconnects at 400ms"); + + // attempt 4: would be 800ms but capped at 400ms + sockets[4].fire("close"); + t.mock.timers.tick(400); + assert.equal(sockets.length, 6, "delay is capped at 400ms"); +}); + +test("backoff resets to immediate after a successful reconnect", async (t) => { + const { sockets, transport } = makeFlappy(t); + + const connected = transport.connect(); + sockets[0].fire("open"); + await connected; + + // Fail once so the attempt counter escalates past 0. + sockets[0].fire("close"); + t.mock.timers.tick(0); // sockets[1] + sockets[1].fire("close"); + t.mock.timers.tick(100); // sockets[2] at the base delay + assert.equal(sockets.length, 3); + + sockets[2].fire("open"); // SUCCESS — must reset the counter + + // The next drop should be immediate again, not the escalated 200ms. + sockets[2].fire("close"); + t.mock.timers.tick(0); + assert.equal(sockets.length, 4, "a success resets backoff, so the next reconnect is immediate"); +}); + +test("jitter collapses the delay to its floor (raw/2) when random is low", async (t) => { + const sockets: FakeSocket[] = []; + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => { + const s = new FakeSocket(); + sockets.push(s); + return s; + }, + backoff: { baseMs: 100, capMs: 400, factor: 2 }, + random: () => 0, // jitter contributes nothing → delay is the floor, raw/2 + }); + t.mock.timers.enable({ apis: ["setTimeout"] }); + + const connected = transport.connect(); + sockets[0].fire("open"); + await connected; + + sockets[0].fire("close"); + t.mock.timers.tick(0); // sockets[1], immediate + sockets[1].fire("close"); // attempt 1: raw = 100ms, floor = 50ms + + t.mock.timers.tick(49); + assert.equal(sockets.length, 2, "still waiting at 49ms"); + t.mock.timers.tick(1); + assert.equal(sockets.length, 3, "reconnects at 50ms — the jitter floor, not the full 100ms"); +}); + +test("close() emits 'close' and suppresses reconnect", async (t) => { + const sockets: FakeSocket[] = []; + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => { + const s = new FakeSocket(); + sockets.push(s); + return s; + }, + }); + + t.mock.timers.enable({ apis: ["setTimeout"] }); + + let closeEvents = 0; + let reconnecting = 0; + transport.on("close", () => { + closeEvents++; + }); + transport.on("reconnecting", () => { + reconnecting++; + }); + + const connected = transport.connect(); + sockets[0].fire("open"); + await connected; + + transport.close(); + assert.equal(sockets[0].closed, true, "close() must shut the underlying socket"); + + sockets[0].fire("close"); // socket confirms the deliberate closure + + assert.equal(closeEvents, 1, "must emit 'close' on deliberate teardown"); + assert.equal(reconnecting, 0, "must NOT announce reconnecting"); + + t.mock.timers.tick(60_000); // well past any backoff window + assert.equal(sockets.length, 1, "must NOT reconnect after a deliberate close"); +}); + +test("close() during the backoff window cancels the pending reconnect", async (t) => { + const sockets: FakeSocket[] = []; + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => { + const s = new FakeSocket(); + sockets.push(s); + return s; + }, + backoff: { baseMs: 100, capMs: 400, factor: 2 }, + }); + + t.mock.timers.enable({ apis: ["setTimeout"] }); + + let opens = 0; + transport.on("open", () => { + opens++; + }); + + const connected = transport.connect(); + sockets[0].fire("open"); + await connected; + assert.equal(opens, 1); + + sockets[0].fire("close"); // unexpected drop → schedules a reconnect timer + transport.close(); // caller tears down while that timer is still pending + + t.mock.timers.tick(60_000); // fire everything + assert.equal(sockets.length, 1, "a pending reconnect must not fire after close()"); + assert.equal(opens, 1, "no second connection is opened"); +}); + +test("close() during the backoff window still emits 'close'", async (t) => { + const sockets: FakeSocket[] = []; + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => { + const s = new FakeSocket(); + sockets.push(s); + return s; + }, + backoff: { baseMs: 100, capMs: 400, factor: 2 }, + }); + + t.mock.timers.enable({ apis: ["setTimeout"] }); + + let closeEvents = 0; + transport.on("close", () => { + closeEvents++; + }); + + const connected = transport.connect(); + sockets[0].fire("open"); + await connected; + + sockets[0].fire("close"); // drop → reconnect scheduled (socket now dead) + transport.close(); // tear down mid-backoff — no live socket to fire 'close' + + assert.equal(closeEvents, 1, "close() must emit 'close' even with no live socket"); +}); + +test("close() before the socket opens unblocks a pending connect()", { timeout: 2000 }, async () => { + const fake = new FakeSocket(); + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => fake, + }); + + let resolved = false; + const connected = transport.connect().then(() => { + resolved = true; + }); + + transport.close(); // caller shuts down before the first 'open' ever arrives + await connected; // hangs forever (test times out) if connect() never settles + + assert.ok(resolved, "connect() must settle when closed before it opens"); +}); + +test("connect() called twice fails loud instead of starting a second connection", async () => { + const sockets: FakeSocket[] = []; + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => { + const s = new FakeSocket(); + sockets.push(s); + return s; + }, + }); + + const connected = transport.connect(); + sockets[0].fire("open"); + await connected; + + assert.throws(() => transport.connect(), SdkError, "second connect() must throw"); + assert.equal(sockets.length, 1, "no orphaned second socket"); +}); + +test("connect() rejects when its caller aborts", async () => { + const fake = new FakeSocket(); + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => fake, + }); + const controller = new AbortController(); + + const connected = transport.connect({ signal: controller.signal }); + controller.abort(); + + await assert.rejects(connected, /aborted/); + transport.close(); +}); + +test("connect() rejects when its caller deadline expires", async (t) => { + const fake = new FakeSocket(); + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => fake, + }); + t.mock.timers.enable({ apis: ["setTimeout"] }); + + const connected = transport.connect({ timeoutMs: 10 }); + t.mock.timers.tick(10); + + await assert.rejects(connected, /deadline/); + transport.close(); +}); + +test("connect() settles when the socket factory throws synchronously", async () => { + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => { throw new Error("factory failed"); }, + }); + + await assert.rejects(transport.connect(), (error: unknown) => + error instanceof ConnectionError && (error.cause as Error | undefined)?.message === "factory failed", + ); + transport.close(); +}); + +test("late events from a superseded socket are ignored after reconnect", async (t) => { + const sockets: FakeSocket[] = []; + const transport = new WsTransport("wss://example.test/v1", { + logger: new NoopLogger(), + socketFactory: () => { + const s = new FakeSocket(); + sockets.push(s); + return s; + }, + backoff: { baseMs: 100, capMs: 400, factor: 2 }, + }); + + t.mock.timers.enable({ apis: ["setTimeout"] }); + + let messages = 0; + let reconnecting = 0; + transport.on("message", () => { + messages++; + }); + transport.on("reconnecting", () => { + reconnecting++; + }); + + const connected = transport.connect(); + sockets[0].fire("open"); + await connected; + + sockets[0].fire("close"); // drop → reconnect scheduled (reconnecting == 1) + t.mock.timers.tick(0); // sockets[1] created + sockets[1].fire("open"); // reconnected; current socket is now sockets[1] + + // The old, superseded socket coughs up a late frame and a second close. + sockets[0].fire("message", { data: '{"stale":true}' }); + sockets[0].fire("close"); + + assert.equal(messages, 0, "a stale frame from the old socket must not be emitted"); + assert.equal(reconnecting, 1, "the old socket's second close must not schedule another reconnect"); +}); diff --git a/packages/sdk-typescript/src/tests/typed-emitter.test.ts b/packages/sdk-typescript/src/tests/typed-emitter.test.ts new file mode 100644 index 0000000..b01a410 --- /dev/null +++ b/packages/sdk-typescript/src/tests/typed-emitter.test.ts @@ -0,0 +1,143 @@ +import assert from "node:assert/strict"; +import { describe, test } from "node:test"; +import { TypedEmitter } from "../core/typed-emitter.js"; + +type Events = { x: (v: number) => void; y: () => void }; + +describe("TypedEmitter", () => { + test("on() fires on every emit", () => { + const ee = new TypedEmitter(); + const calls: number[] = []; + ee.on("x", (v) => calls.push(v)); + ee.emit("x", 1); + ee.emit("x", 2); + assert.deepStrictEqual(calls, [1, 2]); + }); + + test("once() fires only on first emit", () => { + const ee = new TypedEmitter(); + const calls: number[] = []; + ee.once("x", (v) => calls.push(v)); + ee.emit("x", 1); + ee.emit("x", 2); + assert.deepStrictEqual(calls, [1]); + assert.equal(ee.listenerCount("x"), 0); + }); + + test("off() removes the most recent registration (Node semantics)", () => { + const ee = new TypedEmitter(); + const calls: number[] = []; + const fn = (v: number) => calls.push(v); + ee.on("x", fn); + ee.on("x", fn); + ee.off("x", fn); // removes the second (most recent) + ee.emit("x", 1); + assert.deepStrictEqual(calls, [1]); // first registration still fires + }); + + // The exact bug scenario: once("x", fn); on("x", fn) + // The once wrapper must remove itself, not the later on() registration. + test("once before on with same fn: once self-removes, on survives", () => { + const ee = new TypedEmitter(); + const calls: number[] = []; + const fn = (v: number) => calls.push(v); + ee.once("x", fn); // registration 0: once wrapper + ee.on("x", fn); // registration 1: direct + + // First emit: both fire (once + on), once removes itself + ee.emit("x", 1); + assert.deepStrictEqual(calls, [1, 1]); + assert.equal(ee.listenerCount("x"), 1); // only the on() remains + + // Second emit: only the on() fires + calls.length = 0; + ee.emit("x", 2); + assert.deepStrictEqual(calls, [2]); + }); + + // Reverse order: on("x", fn); once("x", fn) + test("on before once with same fn: once self-removes, on survives", () => { + const ee = new TypedEmitter(); + const calls: number[] = []; + const fn = (v: number) => calls.push(v); + ee.on("x", fn); // registration 0: direct + ee.once("x", fn); // registration 1: once wrapper + + // First emit: both fire, once removes itself + ee.emit("x", 1); + assert.deepStrictEqual(calls, [1, 1]); + assert.equal(ee.listenerCount("x"), 1); + + // Second emit: only the on() fires + calls.length = 0; + ee.emit("x", 2); + assert.deepStrictEqual(calls, [2]); + }); + + // off() with mixed on/once: removes the most recent match by original fn + test("on then once then off: off removes the once (most recent)", () => { + const ee = new TypedEmitter(); + const calls: number[] = []; + const fn = (v: number) => calls.push(v); + ee.on("x", fn); + ee.once("x", fn); + ee.off("x", fn); // removes the once (most recent registration for fn) + assert.equal(ee.listenerCount("x"), 1); + + ee.emit("x", 1); + ee.emit("x", 2); + assert.deepStrictEqual(calls, [1, 2]); // on() persists + }); + + test("once then on then off: off removes the on (most recent)", () => { + const ee = new TypedEmitter(); + const calls: number[] = []; + const fn = (v: number) => calls.push(v); + ee.once("x", fn); + ee.on("x", fn); + ee.off("x", fn); // removes the on (most recent registration for fn) + assert.equal(ee.listenerCount("x"), 1); + + // once fires and self-removes + ee.emit("x", 1); + assert.deepStrictEqual(calls, [1]); + assert.equal(ee.listenerCount("x"), 0); + }); + + test("removeAllListeners clears a specific event", () => { + const ee = new TypedEmitter(); + ee.on("x", () => {}); + ee.on("y", () => {}); + ee.removeAllListeners("x"); + assert.equal(ee.listenerCount("x"), 0); + assert.equal(ee.listenerCount("y"), 1); + }); + + test("removeAllListeners with no arg clears all events", () => { + const ee = new TypedEmitter(); + ee.on("x", () => {}); + ee.on("y", () => {}); + ee.removeAllListeners(); + assert.deepStrictEqual(ee.eventNames(), []); + }); + + test("eventNames returns only events with listeners", () => { + const ee = new TypedEmitter(); + const fn = () => {}; + ee.on("x", fn); + assert.deepStrictEqual(ee.eventNames(), ["x"]); + ee.off("x", fn); + assert.deepStrictEqual(ee.eventNames(), []); + }); + + test("addListener and removeListener are aliases", () => { + const ee = new TypedEmitter(); + const calls: number[] = []; + const fn = (v: number) => calls.push(v); + ee.addListener("x", fn); + ee.emit("x", 1); + ee.removeListener("x", fn); + ee.emit("x", 2); + assert.deepStrictEqual(calls, [1]); + }); +}); diff --git a/packages/sdk-typescript/src/tests/websocket.test.ts b/packages/sdk-typescript/src/tests/websocket.test.ts new file mode 100644 index 0000000..a6fc6db --- /dev/null +++ b/packages/sdk-typescript/src/tests/websocket.test.ts @@ -0,0 +1,905 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { BrowserOAuthAuth, GeminiMarkets, GeminiWebSocket, SdkError, type AuthStrategy, type OAuthTokenStore, type OAuthTokens } from "../browser/index.js"; +import type { DiagnosticEvent } from "../diagnostics.js"; +import type { Logger } from "../logging.js"; +import type { SocketFactoryOptions, SocketLike } from "../transport.js"; +import { FakeSocket } from "./fake-socket.js"; + + +function auth(): AuthStrategy { + return { + nextNonce: () => "1700000000", + credentialHeaders: async (payloadBase64) => ({ + "X-GEMINI-APIKEY": "key", + "X-GEMINI-SIGNATURE": `sig:${payloadBase64}`, + }), + }; +} + +function harness(opts?: { auth?: AuthStrategy; logger?: Logger; onDiagnostic?: (event: DiagnosticEvent) => void; env?: "production" | "sandbox"; timeoutMs?: number }) { + const sockets: FakeSocket[] = []; + const options: SocketFactoryOptions[] = []; + const client = new GeminiMarkets({ + env: opts?.env ?? "sandbox", + auth: opts?.auth, + logger: opts?.logger, + onDiagnostic: opts?.onDiagnostic, + timeoutMs: opts?.timeoutMs, + webSocketFactory: (_url: string, socketOptions: SocketFactoryOptions) => { + const socket = new FakeSocket(); + sockets.push(socket); + options.push(socketOptions); + return socket; + }, + } as never); + return { client, sockets, options }; +} + +test("WebSocket diagnostics classify control and mutation traffic without frames", async () => { + const events: DiagnosticEvent[] = []; + const { client, sockets } = harness({ auth: auth(), onDiagnostic: (event) => events.push(event) }); + const stream = client.websocket.trades("btcusd"); + await flush(); + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await stream.ready; + const order = client.websocket.placeOrder({ + symbol: "btcusd", + side: "BUY", + type: "LIMIT", + quantity: "1", + price: "100", + clientOrderId: "client-1", + } as never).catch(() => undefined); + await flush(); + sockets[0].fire("message", { data: '{"id":2,"status":200}' }); + await order; + + assert.ok(events.some((event) => event.traffic === "control")); + const mutation = events.find((event) => event.name === "ws.request.start" && event.traffic === "mutation"); + assert.deepEqual(mutation?.operationContext, { operation: "order.place", clientOrderId: "client-1" }); + assert.equal(events.some((event) => "body" in event || JSON.stringify(event).includes("X-GEMINI-SIGNATURE")), false); + assert.equal(JSON.stringify(events).includes("btcusd@trade"), false); + client.close(); +}); + +async function flush(): Promise { + await new Promise((resolve) => setImmediate(resolve)); +} + +test("public streams share one underlying WebSocket session", async () => { + const { client, sockets } = harness({ env: "production" }); + const trades = client.websocket.trades("btcusd"); + const ticker = client.websocket.bookTicker("ethusd"); + + sockets[0].fire("open"); + await flush(); + + assert.equal(sockets.length, 1); + assert.deepEqual(sockets[0].sent.map((frame) => JSON.parse(frame)), [ + { id: 1, method: "SUBSCRIBE", params: ["btcusd@trade"] }, + { id: 2, method: "SUBSCRIBE", params: ["ethusd@bookTicker"] }, + ]); + + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + sockets[0].fire("message", { data: '{"id":2,"status":200}' }); + await trades.ready; + await ticker.ready; + client.close(); +}); + +test("generic stream listeners support AbortSignal lifecycle", async () => { + const { client, sockets } = harness(); + const controller = new AbortController(); + const messages: unknown[] = []; + const trades = client.websocket.trades("btcusd"); + trades.on("message", (message) => messages.push(message), { signal: controller.signal }); + + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await trades.ready; + controller.abort(); + sockets[0].fire("message", { data: '{"E":2,"s":"btcusd","t":2,"p":"100","q":"1","m":false}' }); + + assert.deepEqual(messages, []); + client.close(); +}); + +test("stream state exposes a socket failure without an error listener", async () => { + const { client, sockets } = harness(); + const trades = client.websocket.trades("btcusd"); + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await trades.ready; + + sockets[0].fire("error", new Error("socket failed")); + + assert.equal(trades.state, "failed"); + assert.equal((trades.lastError?.cause as Error | undefined)?.message, "socket failed"); + client.close(); +}); + +test("generic streams count and diagnose malformed known frames", async () => { + const events: DiagnosticEvent[] = []; + const { client, sockets } = harness({ onDiagnostic: (event) => events.push(event) }); + const trades = client.websocket.trades("btcusd"); + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await trades.ready; + + sockets[0].fire("message", { data: '{"E":2,"s":"btcusd","t":2,"p":"100","m":false}' }); + + assert.equal(trades.malformedFrameCount, 1); + assert.equal(events.some((event) => event.name === "ws.stream.malformed_frame"), true); + client.close(); +}); + +test("public streams surface successful replay acknowledgement", async (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + const { client, sockets } = harness(); + const events: string[] = []; + const trades = client.websocket.trades("btcusd"); + trades.on("resubscribed", () => events.push("resubscribed")); + + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await trades.ready; + sockets[0].fire("close"); + t.mock.timers.tick(0); + sockets[1].fire("open"); + sockets[1].fire("message", { data: '{"id":1,"status":200}' }); + + assert.deepEqual(events, ["resubscribed"]); + client.close(); +}); + +test("direct GeminiWebSocket order books derive a snapshot URL and preserve query parameters", async () => { + const sockets: FakeSocket[] = []; + const urls: string[] = []; + const websocket = new GeminiWebSocket({ + url: "wss://example.test?foo=bar", + socketFactory: (url, _options) => { + urls.push(url); + const socket = new FakeSocket(); + sockets.push(socket); + return socket; + }, + }); + const book = websocket.orderBook("btcusd"); + + await flush(); + assert.deepEqual(urls, ["wss://example.test/?foo=bar&snapshot=-1"]); + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + sockets[0].fire("message", { data: '{"e":"depthUpdate","E":1,"s":"btcusd","U":1,"u":1,"b":[["100","1"]],"a":[["101","1"]]}' }); + await flush(); + + assert.deepEqual(book.bestBid(), { price: "100", qty: "1" }); + websocket.close(); +}); + +test("orderBook uses a snapshot session separate from public streams", async () => { + const { client, sockets } = harness(); + const trades: unknown[] = []; + const book = client.orderBook("btcusd"); + const tradeStream = client.websocket.trades("ethusd"); + tradeStream.on("message", (trade) => trades.push(trade)); + + await flush(); + sockets[0].fire("open"); + sockets[1].fire("open"); + await flush(); + + assert.equal(sockets.length, 2); + assert.deepEqual(sockets[0].sent.map((frame) => JSON.parse(frame)), [ + { id: 1, method: "SUBSCRIBE", params: ["btcusd@depth20"] }, + ]); + assert.deepEqual(sockets[1].sent.map((frame) => JSON.parse(frame)), [ + { id: 1, method: "SUBSCRIBE", params: ["ethusd@trade"] }, + ]); + + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + sockets[1].fire("message", { data: '{"id":1,"status":200}' }); + await tradeStream.ready; + sockets[0].fire("message", { data: '{"lastUpdateId":1,"symbol":"btcusd","bids":[["100","1"]],"asks":[["101","1"]]}' }); + sockets[1].fire("message", { data: '{"E":2,"s":"ethusd","t":3,"p":"50","q":"2","m":false}' }); + + assert.deepEqual(book.bestBid(), { price: "100", qty: "1" }); + assert.deepEqual(trades, [{ E: 2, s: "ethusd", t: 3, p: "50", q: "2", m: false }]); + client.close(); +}); + +test("order-book reconstruction applies queued diffs after the fresh snapshot", async () => { + const { client, sockets } = harness({ env: "production" }); + const book = client.orderBook("btcusd"); + await flush(); + sockets[0].fire("open"); + await flush(); + + sockets[0].fire("message", { data: '{"e":"depthUpdate","E":1,"s":"btcusd","U":1,"u":1,"b":[["100","1"]],"a":[]}' }); + sockets[0].fire("message", { data: '{"e":"depthUpdate","E":2,"s":"btcusd","U":1,"u":2,"b":[["100","2"]],"a":[]}' }); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await flush(); + + assert.deepEqual(book.bestBid(), { price: "100", qty: "2" }); + client.close(); +}); + +test("a timed-out order-book subscription closes the book and releases its routing entry", async (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + const { client, sockets } = harness({ env: "production", timeoutMs: 10 }); + const errors: Error[] = []; + const book = client.orderBook("btcusd"); + book.on("error", (error) => errors.push(error)); + + await flush(); + sockets[0].fire("open"); + await flush(); + t.mock.timers.tick(10); + await flush(); + + assert.equal(errors.length, 1); + assert.deepEqual(sockets[0].sent.map((frame) => JSON.parse(frame)), [ + { id: 1, method: "SUBSCRIBE", params: ["btcusd@depth"] }, + { id: 2, method: "UNSUBSCRIBE", params: ["btcusd@depth"] }, + ]); + assert.notEqual(client.orderBook("btcusd"), book); + client.close(); +}); + +test("public reconnect does not stale an order book on its separate session", async (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + const { client, sockets } = harness(); + const book = client.orderBook("btcusd"); + const trades = client.websocket.trades("ethusd"); + + await flush(); + sockets[0].fire("open"); + sockets[1].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + sockets[1].fire("message", { data: '{"id":1,"status":200}' }); + await trades.ready; + sockets[0].fire("message", { data: '{"lastUpdateId":1,"symbol":"btcusd","bids":[["100","1"]],"asks":[["101","1"]]}' }); + assert.deepEqual(book.bestBid(), { price: "100", qty: "1" }); + + sockets[1].fire("close"); + t.mock.timers.tick(0); + await flush(); + sockets[2].fire("open"); + await flush(); + sockets[2].fire("message", { data: '{"id":1,"status":200}' }); + await flush(); + sockets[0].fire("message", { data: '{"e":"depthUpdate","E":2,"s":"btcusd","U":2,"u":2,"b":[["100","2"]],"a":[]}' }); + + assert.deepEqual(book.bestBid(), { price: "100", qty: "2" }); + client.close(); +}); + +test("utility methods resolve through request correlation", async () => { + const { client, sockets } = harness(); + const ping = client.websocket.ping(); + + sockets[0].fire("open"); + await flush(); + assert.deepEqual(JSON.parse(sockets[0].sent[0]), { id: 1, method: "ping" }); + + sockets[0].fire("message", { data: '{"id":1,"status":200,"result":{"pong":true}}' }); + assert.deepEqual(await ping, { id: 1, status: 200, result: { pong: true } }); + client.close(); +}); + +test("utility methods send documented public request frames", async () => { + const { client, sockets } = harness(); + const conninfo = client.websocket.conninfo(); + + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await conninfo; + + const time = client.websocket.time(); + await flush(); + sockets[0].fire("message", { data: '{"id":2,"status":200}' }); + await time; + + const subscriptions = client.websocket.listSubscriptions(); + await flush(); + sockets[0].fire("message", { data: '{"id":3,"status":200,"result":["btcusd@trade"]}' }); + assert.deepEqual(await subscriptions, { id: 3, status: 200, result: ["btcusd@trade"] }); + + const depth = client.websocket.depthSnapshot("BTCUSD", { limit: 10 }); + await flush(); + sockets[0].fire("message", { data: '{"id":4,"status":200,"result":{"lastUpdateId":1,"bids":[],"asks":[]}}' }); + await depth; + + assert.deepEqual(sockets[0].sent.map((frame) => JSON.parse(frame)), [ + { id: 1, method: "conninfo" }, + { id: 2, method: "time" }, + { id: 3, method: "LIST_SUBSCRIPTIONS" }, + { id: 4, method: "depth", params: { symbol: "btcusd", limit: 10 } }, + ]); + client.close(); +}); + +test("stream methods subscribe to documented public stream names", async () => { + const { client, sockets } = harness(); + const depthUpdates = client.websocket.depthUpdates("btcusd", { intervalMs: 100 }); + const depth = client.websocket.depth("ethusd", { levels: 20, intervalMs: 100 }); + const contractStatus = client.websocket.contractStatus(); + const rfqs = client.websocket.rfqs(); + + sockets[0].fire("open"); + sockets[1].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + sockets[1].fire("message", { data: '{"id":1,"status":200}' }); + sockets[0].fire("message", { data: '{"id":2,"status":200}' }); + sockets[0].fire("message", { data: '{"id":3,"status":200}' }); + await Promise.all([depthUpdates.ready, depth.ready, contractStatus.ready, rfqs.ready]); + + assert.deepEqual(sockets[0].sent.map((frame) => JSON.parse(frame)), [ + { id: 1, method: "SUBSCRIBE", params: ["btcusd@depth@100ms"] }, + { id: 2, method: "SUBSCRIBE", params: ["contractStatus"] }, + { id: 3, method: "SUBSCRIBE", params: ["requestForQuote"] }, + ]); + assert.deepEqual(sockets[1].sent.map((frame) => JSON.parse(frame)), [ + { id: 1, method: "SUBSCRIBE", params: ["ethusd@depth20@100ms"] }, + ]); + client.close(); +}); + +test("stream frames do not resolve pending utility requests", async () => { + const { client, sockets } = harness(); + const messages: unknown[] = []; + const trades = client.websocket.trades("btcusd"); + trades.on("message", (trade) => messages.push(trade)); + const ping = client.websocket.ping(); + + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await trades.ready; + + sockets[0].fire("message", { data: '{"E":1,"s":"btcusd","t":2,"p":"100","q":"0.1","m":false}' }); + assert.deepEqual(messages, [{ E: 1, s: "btcusd", t: 2, p: "100", q: "0.1", m: false }]); + + sockets[0].fire("message", { data: '{"id":2,"status":200}' }); + await ping; + client.close(); +}); + +test("depth updates use the differential public session", async () => { + const { client, sockets } = harness(); + const stream = client.websocket.depthUpdates("btcusd"); + + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await stream.ready; + client.close(); +}); + +test("closing one stream unsubscribes only that stream", async () => { + const { client, sockets } = harness(); + const trades = client.websocket.trades("btcusd"); + const ticker = client.websocket.bookTicker("ethusd"); + + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + sockets[0].fire("message", { data: '{"id":2,"status":200}' }); + await trades.ready; + await ticker.ready; + + const closed = trades.close(); + await flush(); + assert.deepEqual(JSON.parse(sockets[0].sent[2]), { + id: 3, + method: "UNSUBSCRIBE", + params: ["btcusd@trade"], + }); + sockets[0].fire("message", { data: '{"id":3,"status":200}' }); + await closed; + + assert.equal(trades.state, "closed"); + assert.equal(sockets[0].sent.length, 3); + const tickerClosed = ticker.close(); + await flush(); + sockets[0].fire("message", { data: '{"id":4,"status":200}' }); + await tickerClosed; + client.close(); +}); + +test("typed stream handles route only matching stream frames", async () => { + const { client, sockets } = harness(); + const trades: unknown[] = []; + const tickers: unknown[] = []; + const tradeStream = client.websocket.trades("btcusd"); + const tickerStream = client.websocket.bookTicker("ethusd"); + tradeStream.on("message", (trade) => trades.push(trade)); + tickerStream.on("message", (ticker) => tickers.push(ticker)); + + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + sockets[0].fire("message", { data: '{"id":2,"status":200}' }); + await tradeStream.ready; + await tickerStream.ready; + + sockets[0].fire("message", { data: '{"E":1,"s":"ethusd","t":2,"p":"100","q":"0.1","m":false}' }); + sockets[0].fire("message", { data: '{"u":1,"E":2,"s":"ethusd","b":"99","B":"1","a":"101","A":"2"}' }); + sockets[0].fire("message", { data: '{"E":3,"s":"btcusd","t":4,"p":"200","q":"0.2","m":true}' }); + + assert.deepEqual(trades, [{ E: 3, s: "btcusd", t: 4, p: "200", q: "0.2", m: true }]); + assert.deepEqual(tickers, [{ u: 1, E: 2, s: "ethusd", b: "99", B: "1", a: "101", A: "2" }]); + client.close(); +}); + +test("concurrent partial depth streams isolate symbol-less snapshots", async () => { + const { client, sockets } = harness(); + const btcSnapshots: unknown[] = []; + const ethSnapshots: unknown[] = []; + const btc = client.websocket.depth("btcusd", { levels: 20 }); + const eth = client.websocket.depth("ethusd", { levels: 20 }); + btc.on("message", (snapshot) => btcSnapshots.push(snapshot)); + eth.on("message", (snapshot) => ethSnapshots.push(snapshot)); + + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + sockets[1].fire("open"); + await flush(); + sockets[1].fire("message", { data: '{"id":1,"status":200}' }); + await btc.ready; + await eth.ready; + + sockets[0].fire("message", { data: '{"lastUpdateId":1,"bids":[["100","1"]],"asks":[["101","1"]]}' }); + + assert.deepEqual(btcSnapshots, [{ lastUpdateId: 1, bids: [["100", "1"]], asks: [["101", "1"]] }]); + assert.deepEqual(ethSnapshots, []); + client.close(); +}); + +test("client.close() closes active public streams and the shared session", async () => { + const { client, sockets } = harness(); + const closes: string[] = []; + const trades = client.websocket.trades("btcusd"); + const ticker = client.websocket.bookTicker("ethusd"); + const depth = client.websocket.depth("solusd", { levels: 5 }); + trades.on("close", () => closes.push("trades")); + ticker.on("close", () => closes.push("ticker")); + depth.on("close", () => closes.push("depth")); + + sockets[0].fire("open"); + sockets[1].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + sockets[0].fire("message", { data: '{"id":2,"status":200}' }); + sockets[1].fire("message", { data: '{"id":1,"status":200}' }); + await trades.ready; + await ticker.ready; + await depth.ready; + + client.close(); + + assert.equal(sockets[0].closed, true); + assert.equal(sockets[1].closed, true); + assert.deepEqual(closes.sort(), ["depth", "ticker", "trades"]); +}); + +test("authenticated streams pass upgrade headers and route typed frames", async () => { + const { client, sockets, options } = harness({ auth: auth() }); + const messages: unknown[] = []; + const orders = client.websocket.orders({ scope: "account" }); + const balances = client.websocket.balances({ intervalMs: 1000 }); + const positions = client.websocket.positions({ intervalMs: 0 }); + const rfqs = client.websocket.rfqDeliveries({ scope: "session" }); + orders.on("message", (message) => messages.push(message)); + balances.on("message", (message) => messages.push(message)); + positions.on("message", (message) => messages.push(message)); + rfqs.on("message", (message) => messages.push(message)); + + await flush(); + sockets[0].fire("open"); + await flush(); + for (let id = 1; id <= 4; id++) sockets[0].fire("message", { data: `{"id":${id},"status":200}` }); + await Promise.all([orders.ready, balances.ready, positions.ready, rfqs.ready]); + + assert.deepEqual(options[0].headers, { + "X-GEMINI-APIKEY": "key", + "X-GEMINI-SIGNATURE": "sig:MTcwMDAwMDAwMA==", + "X-GEMINI-NONCE": "1700000000", + "X-GEMINI-PAYLOAD": "MTcwMDAwMDAwMA==", + }); + assert.deepEqual(sockets[0].sent.map((frame) => JSON.parse(frame)), [ + { id: 1, method: "SUBSCRIBE", params: ["orders@account"] }, + { id: 2, method: "SUBSCRIBE", params: ["balances@account@1s"] }, + { id: 3, method: "SUBSCRIBE", params: ["positions@account"] }, + { id: 4, method: "SUBSCRIBE", params: ["requestForQuote@session"] }, + ]); + + sockets[0].fire("message", { data: '{"e":"orderUpdate","E":1,"s":"GEMI-X","i":2,"X":"NEW","T":3}' }); + sockets[0].fire("message", { data: '{"e":"balanceUpdate","E":4,"u":5,"B":[]}' }); + sockets[0].fire("message", { data: '{"e":"positionReport","E":6,"u":7,"A":8,"P":[]}' }); + sockets[0].fire("message", { data: '{"e":"requestForQuote","i":"delivery-1","E":9,"r":"rfq-1","x":"ACCEPTED","S":"CONFIRMING"}' }); + + assert.deepEqual(messages, [ + { e: "orderUpdate", E: 1, s: "GEMI-X", i: 2, X: "NEW", T: 3 }, + { e: "balanceUpdate", E: 4, u: 5, B: [] }, + { e: "positionReport", E: 6, u: 7, A: 8, P: [] }, + { e: "requestForQuote", i: "delivery-1", E: 9, r: "rfq-1", x: "ACCEPTED", S: "CONFIRMING" }, + ]); + client.close(); +}); + +test("authenticated streams and methods fail before sending without auth", async () => { + const { client, sockets } = harness(); + + assert.throws(() => client.websocket.orders({ scope: "account" }), /requires auth/); + await assert.rejects( + client.websocket.placeOrder({ + symbol: "GEMI-X", + side: "BUY", + type: "LIMIT", + timeInForce: "GTC", + quantity: "1", + price: "0.50", + }), + /requires auth/, + ); + assert.equal(sockets.length, 0); + client.close(); +}); + +test("authenticated order and RFQ methods send one-shot request frames", async () => { + const { client, sockets } = harness({ auth: auth() }); + const placed = client.websocket.placeOrder({ + symbol: "GEMI-X", + side: "BUY", + type: "LIMIT", + timeInForce: "GTC", + quantity: "1", + price: "0.50", + }); + + await flush(); + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200,"result":{"orderId":"o1"}}' }); + assert.deepEqual(await placed, { id: 1, status: 200, result: { orderId: "o1" } }); + + const canceled = client.websocket.cancelOrder({ orderId: "o1" }); + await flush(); + sockets[0].fire("message", { data: '{"id":2,"status":200,"result":{"orderId":"o1"}}' }); + await canceled; + + const cancelAll = client.websocket.cancelAllOrders({ confirm: true }); + await flush(); + sockets[0].fire("message", { data: '{"id":3,"status":200}' }); + await cancelAll; + + const cancelSession = client.websocket.cancelSessionOrders({ confirm: true }); + await flush(); + sockets[0].fire("message", { data: '{"id":4,"status":200}' }); + await cancelSession; + + const quote = client.websocket.rfq.submitQuote({ rfqId: "rfq-1", price: "0.55", quantity: "10" }); + await flush(); + sockets[0].fire("message", { data: '{"id":5,"status":200,"result":{"rfqId":"rfq-1","quoteId":"q1"}}' }); + await quote; + + const withdraw = client.websocket.rfq.withdrawQuote({ rfqId: "rfq-1", quoteId: "q1" }); + await flush(); + sockets[0].fire("message", { data: '{"id":6,"status":200,"result":{"rfqId":"rfq-1","quoteId":"q1"}}' }); + await withdraw; + + const confirm = client.websocket.rfq.confirmQuote({ rfqId: "rfq-1", quoteId: "q1", confirm: true }); + await flush(); + sockets[0].fire("message", { data: '{"id":7,"status":200,"result":{"rfqId":"rfq-1","quoteId":"q1","confirmed":true}}' }); + await confirm; + + assert.deepEqual(sockets[0].sent.map((frame) => JSON.parse(frame)), [ + { id: 1, method: "order.place", params: { symbol: "GEMI-X", side: "BUY", type: "LIMIT", timeInForce: "GTC", quantity: "1", price: "0.50" } }, + { id: 2, method: "order.cancel", params: { orderId: "o1" } }, + { id: 3, method: "order.cancel_all" }, + { id: 4, method: "order.cancel_session" }, + { id: 5, method: "rfq.submit_quote", params: { rfqId: "rfq-1", price: "0.55", quantity: "10" } }, + { id: 6, method: "rfq.withdraw_quote", params: { rfqId: "rfq-1", quoteId: "q1" } }, + { id: 7, method: "rfq.confirm_quote", params: { rfqId: "rfq-1", quoteId: "q1", confirm: true } }, + ]); + client.close(); +}); + +test("authenticated request errors reject with SdkError", async () => { + const { client, sockets } = harness({ auth: auth() }); + const request = client.websocket.cancelOrder({ orderId: "o1" }); + + await flush(); + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":400,"error":{"code":-2010,"msg":"rejected"}}' }); + + await assert.rejects(request, SdkError); + client.close(); +}); + +test("mutating requests reject on reconnect and are not replayed", async (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + const { client, sockets } = harness({ auth: auth() }); + const request = client.websocket.cancelOrder({ orderId: "o1" }); + + await flush(); + sockets[0].fire("open"); + await flush(); + sockets[0].fire("close"); + + await assert.rejects(request, /reconnecting/); + t.mock.timers.tick(0); + await flush(); + sockets[1].fire("open"); + assert.deepEqual(sockets[1].sent, []); + client.close(); +}); + +test("public streams replay exactly once after reconnect and ignore superseded frames", async (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + const { client, sockets } = harness(); + const messages: unknown[] = []; + const trades = client.websocket.trades("btcusd"); + trades.on("message", (message) => messages.push(message)); + + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await trades.ready; + + sockets[0].fire("close"); + t.mock.timers.tick(0); + await flush(); + sockets[0].fire("message", { data: '{"E":1,"s":"btcusd","t":1,"p":"90","q":"1","m":false}' }); + sockets[1].fire("open"); + await flush(); + + assert.deepEqual(sockets[1].sent.map((frame) => JSON.parse(frame)), [ + { id: 1, method: "SUBSCRIBE", params: ["btcusd@trade"] }, + ]); + sockets[1].fire("message", { data: '{"E":2,"s":"btcusd","t":2,"p":"100","q":"1","m":false}' }); + + assert.deepEqual(messages, [{ E: 2, s: "btcusd", t: 2, p: "100", q: "1", m: false }]); + client.close(); +}); + +test("authenticated streams replay with fresh credentials without logging secrets", async (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + let nonce = 1700000000; + const logs: string[] = []; + const logger: Logger = { + debug: (_message, meta) => logs.push(JSON.stringify(meta)), + info: (_message, meta) => logs.push(JSON.stringify(meta)), + warn: (_message, meta) => logs.push(JSON.stringify(meta)), + error: (_message, meta) => logs.push(JSON.stringify(meta)), + }; + const authenticated: AuthStrategy = { + nextNonce: () => String(nonce++), + credentialHeaders: async (payloadBase64) => ({ + "X-GEMINI-APIKEY": "key", + "X-GEMINI-SIGNATURE": `sig:${payloadBase64}`, + }), + }; + const { client, sockets, options } = harness({ auth: authenticated, logger }); + const orders = client.websocket.orders({ scope: "session" }); + + await flush(); + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await orders.ready; + + sockets[0].fire("close"); + t.mock.timers.tick(0); + await flush(); + sockets[1].fire("open"); + await flush(); + + assert.equal(options[0].headers?.["X-GEMINI-NONCE"], "1700000000"); + assert.equal(options[1].headers?.["X-GEMINI-NONCE"], "1700000001"); + assert.deepEqual(sockets[1].sent.map((frame) => JSON.parse(frame)), [ + { id: 1, method: "SUBSCRIBE", params: ["orders@session"] }, + ]); + assert.equal(logs.some((entry) => /key|sig:|1700000000|1700000001|X-GEMINI-PAYLOAD/i.test(entry)), false); + client.close(); +}); + +test("ping, time, and conninfo reject on reconnect and are not replayed", async (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + const { client, sockets } = harness(); + const requests = [client.websocket.ping(), client.websocket.time(), client.websocket.conninfo()]; + + sockets[0].fire("open"); + await flush(); + sockets[0].fire("close"); + await Promise.all(requests.map((request) => assert.rejects(request, /reconnecting/))); + + t.mock.timers.tick(0); + await flush(); + sockets[1].fire("open"); + assert.deepEqual(sockets[1].sent, []); + client.close(); +}); + +test("closing one stream detaches its listener before unsubscribe acknowledgement", async () => { + const { client, sockets } = harness(); + const trades: unknown[] = []; + const tickers: unknown[] = []; + const tradeStream = client.websocket.trades("btcusd"); + const tickerStream = client.websocket.bookTicker("ethusd"); + tradeStream.on("message", (message) => trades.push(message)); + tickerStream.on("message", (message) => tickers.push(message)); + + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + sockets[0].fire("message", { data: '{"id":2,"status":200}' }); + await Promise.all([tradeStream.ready, tickerStream.ready]); + + const closed = tradeStream.close(); + await flush(); + sockets[0].fire("message", { data: '{"E":3,"s":"btcusd","t":3,"p":"100","q":"1","m":false}' }); + sockets[0].fire("message", { data: '{"u":4,"E":4,"s":"ethusd","b":"99","B":"1","a":"101","A":"2"}' }); + assert.deepEqual(trades, []); + assert.deepEqual(tickers, [{ u: 4, E: 4, s: "ethusd", b: "99", B: "1", a: "101", A: "2" }]); + + sockets[0].fire("message", { data: '{"id":3,"status":200}' }); + await closed; + client.close(); +}); + +test("malformed stream frames emit errors and later valid frames still arrive", async () => { + const { client, sockets } = harness(); + const errors: Error[] = []; + const messages: unknown[] = []; + const trades = client.websocket.trades("btcusd"); + trades.on("error", (error) => errors.push(error)); + trades.on("message", (message) => messages.push(message)); + + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await trades.ready; + sockets[0].fire("message", { data: "not-json" }); + sockets[0].fire("message", { data: '{"E":2,"s":"btcusd","t":2,"p":"100","q":"1","m":false}' }); + + assert.equal(errors.length, 1); + assert.match(errors[0].message, /malformed WebSocket frame/); + assert.deepEqual(messages, [{ E: 2, s: "btcusd", t: 2, p: "100", q: "1", m: false }]); + client.close(); +}); + +test("broad cancellation methods require explicit confirmation", async () => { + const { client } = harness({ auth: auth() }); + + await assert.rejects(client.websocket.cancelAllOrders({ confirm: false }), /confirm: true/); + await assert.rejects(client.websocket.cancelSessionOrders({ confirm: false }), /confirm: true/); + client.close(); +}); + +test("OAuth token refresh triggers during WebSocket reconnect", async (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + + // Simulate an OAuthAuth-like strategy: first call returns an expired-then-refreshed token, + // second call (reconnect) triggers a refresh and returns a new token. + let credentialCalls = 0; + const oauthLike: AuthStrategy = { + nextNonce: () => undefined, + credentialHeaders: async () => { + credentialCalls++; + if (credentialCalls === 1) { + return { Authorization: "Bearer token-v1" }; + } + // Simulate refresh delay (as OAuthAuth would internally refresh) + await new Promise((r) => setImmediate(r)); + return { Authorization: "Bearer token-v2-refreshed" }; + }, + }; + + const sockets: FakeSocket[] = []; + const capturedOptions: SocketFactoryOptions[] = []; + const client = new GeminiMarkets({ + env: "sandbox", + auth: oauthLike, + webSocketFactory: (_url: string, opts: SocketFactoryOptions) => { + const socket = new FakeSocket(); + sockets.push(socket); + capturedOptions.push(opts); + return socket; + }, + } as never); + + // Initial connection + const orders = client.websocket.orders({ scope: "session" }); + await flush(); + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await orders.ready; + + // Verify first connection used token-v1 + assert.equal(capturedOptions[0].headers?.Authorization, "Bearer token-v1"); + + // Simulate disconnect → reconnect + sockets[0].fire("close"); + t.mock.timers.tick(0); + await flush(); + // Let the async refresh resolve + await flush(); + sockets[1].fire("open"); + await flush(); + + // Verify reconnect used the refreshed token + assert.equal(capturedOptions[1].headers?.Authorization, "Bearer token-v2-refreshed"); + assert.equal(credentialCalls, 2, "credentialHeaders should be called twice (initial + reconnect)"); + + client.close(); +}); + +test("BrowserOAuthAuth Bearer header flows through to WebSocket upgrade", async () => { + // Wire a real BrowserOAuthAuth (with pre-loaded tokens) into GeminiMarkets + // and verify the Bearer header reaches the socket factory. + const tokens: OAuthTokens = { + accessToken: "ws-bearer-token", + refreshToken: "ws-refresh", + tokenType: "bearer", + scope: "orders:create", + expiresAt: 1_800_000_000_000, + }; + const store: OAuthTokenStore = { + load: async () => tokens, + save: async () => {}, + clear: async () => {}, + runExclusive: async (op: () => Promise) => op(), + }; + const oauthAuth = new BrowserOAuthAuth({ + client: { type: "public", clientId: "ws-test", redirectUri: "http://localhost/cb" }, + tokenStore: store, + now: () => 1_700_000_000_000, + }); + + const capturedOptions: SocketFactoryOptions[] = []; + const sockets: FakeSocket[] = []; + const client = new GeminiMarkets({ + env: "sandbox", + auth: oauthAuth, + webSocketFactory: (_url: string, opts: SocketFactoryOptions) => { + const socket = new FakeSocket(); + sockets.push(socket); + capturedOptions.push(opts); + return socket; + }, + } as never); + + // Open an authenticated stream + const orders = client.websocket.orders({ scope: "session" }); + await flush(); + await flush(); // credentialHeaders is async — socket creation may need an extra tick + sockets[0].fire("open"); + await flush(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await orders.ready; + + // Verify the Bearer header made it to the socket upgrade + assert.equal(capturedOptions[0].headers?.Authorization, "Bearer ws-bearer-token"); + // OAuth does not use HMAC headers + assert.equal(capturedOptions[0].headers?.["X-GEMINI-APIKEY"], undefined); + assert.equal(capturedOptions[0].headers?.["X-GEMINI-SIGNATURE"], undefined); + + client.close(); +}); diff --git a/packages/sdk-typescript/src/tests/ws-session.test.ts b/packages/sdk-typescript/src/tests/ws-session.test.ts new file mode 100644 index 0000000..83a1af8 --- /dev/null +++ b/packages/sdk-typescript/src/tests/ws-session.test.ts @@ -0,0 +1,465 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { WsSession } from "../ws-session.js"; +import { serializeError, SdkError, WebSocketRequestError } from "../errors.js"; +import type { AuthStrategy } from "../core/http.js"; +import type { SocketLike, SocketFactoryOptions } from "../transport.js"; +import { FakeSocket } from "./fake-socket.js"; + + +function harness(opts?: { auth?: AuthStrategy; timeoutMs?: number; liveness?: { intervalMs?: number; timeoutMs?: number } }) { + const sockets: FakeSocket[] = []; + const options: SocketFactoryOptions[] = []; + const session = new WsSession({ + url: "wss://example.test", + auth: opts?.auth, + timeoutMs: opts?.timeoutMs, + liveness: opts?.liveness, + socketFactory: (_url, socketOptions) => { + const socket = new FakeSocket(); + sockets.push(socket); + options.push(socketOptions); + return socket; + }, + }); + return { session, sockets, options }; +} + +async function open(session: WsSession, sockets: FakeSocket[]): Promise { + const connected = session.connect(); + if (!sockets[0]) await new Promise((resolve) => setImmediate(resolve)); + sockets[0].fire("open"); + await connected; +} + +test("request() sends one frame with a generated id and resolves the matching response", async () => { + const { session, sockets } = harness(); + const request = session.request({ method: "ping" }); + sockets[0].fire("open"); + await Promise.resolve(); + + assert.deepEqual(JSON.parse(sockets[0].sent[0]), { id: 1, method: "ping" }); + sockets[0].fire("message", { data: '{"id":1,"status":200,"result":{"pong":true}}' }); + + assert.deepEqual(await request, { id: 1, status: 200, result: { pong: true } }); + session.close(); +}); + +test("request() rejects a matching error response", async () => { + const { session, sockets } = harness(); + const request = session.request({ method: "ping" }); + sockets[0].fire("open"); + await Promise.resolve(); + sockets[0].fire("message", { data: '{"id":1,"status":400,"error":{"code":-1002,"msg":"bad"}}' }); + + await assert.rejects(request, SdkError); + session.close(); +}); + +test("request() preserves the full server error payload and normalized fields", async () => { + const { session, sockets } = harness(); + const request = session.request({ method: "ping" }); + sockets[0].fire("open"); + await Promise.resolve(); + const payload = { error: { code: -1002, msg: "bad parameters" }, result: { field: "symbol" } }; + sockets[0].fire("message", { data: JSON.stringify({ id: 1, status: 400, ...payload }) }); + + await assert.rejects(request, (error: unknown) => { + assert.ok(error instanceof WebSocketRequestError); + assert.equal(error.status, 400); + assert.equal(error.reason, "bad parameters"); + assert.equal(error.serverCode, -1002); + assert.equal(serializeError(error).body, undefined); + assert.deepEqual(serializeError(error, { includeRawBody: true }).body, { id: 1, status: 400, ...payload }); + return true; + }); + session.close(); +}); + +test("request() rejects when its deadline expires", async (t) => { + const { session, sockets } = harness(); + t.mock.timers.enable({ apis: ["setTimeout"] }); + const request = session.request({ method: "ping" }, { timeoutMs: 10 }); + sockets[0].fire("open"); + await Promise.resolve(); + + t.mock.timers.tick(10); + + await assert.rejects(request, /deadline/); + session.close(); +}); + +test("subscribe() timeout removes replay state and unsubscribes after the request was sent", async (t) => { + const { session, sockets } = harness(); + t.mock.timers.enable({ apis: ["setTimeout"] }); + const subscription = session.subscribe(["btcusd@trade"], { timeoutMs: 10 }); + sockets[0].fire("open"); + await Promise.resolve(); + + t.mock.timers.tick(10); + + await assert.rejects(subscription.ready, /deadline/); + assert.deepEqual(sockets[0].sent.map((frame) => JSON.parse(frame)), [ + { id: 1, method: "SUBSCRIBE", params: ["btcusd@trade"] }, + { id: 2, method: "UNSUBSCRIBE", params: ["btcusd@trade"] }, + ]); + session.close(); +}); + +test("opt-in liveness watchdog pings and reconnects after a missed response", async (t) => { + const { session, sockets } = harness({ liveness: { intervalMs: 10, timeoutMs: 5 } }); + t.mock.timers.enable({ apis: ["setTimeout"] }); + await open(session, sockets); + + t.mock.timers.tick(10); + await new Promise((resolve) => setImmediate(resolve)); + assert.deepEqual(JSON.parse(sockets[0].sent[0]), { id: 1, method: "ping" }); + + t.mock.timers.tick(5); + await new Promise((resolve) => setImmediate(resolve)); + assert.equal(sockets[0].closed, true); + session.close(); +}); + +test("request() rejects duplicate ids without replacing pending subscriptions", async () => { + const { session, sockets } = harness(); + const sub = session.subscribe(["btcusd@trade"]); + sockets[0].fire("open"); + await Promise.resolve(); + + await assert.rejects(session.request({ id: 1, method: "ping" }), /id 1 is already pending/); + assert.deepEqual(sockets[0].sent.map((frame) => JSON.parse(frame)), [{ + id: 1, + method: "SUBSCRIBE", + params: ["btcusd@trade"], + }]); + + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await sub.ready; + session.close(); +}); + +test("stream frames are emitted as message events and do not resolve pending requests", async () => { + const { session, sockets } = harness(); + const frames: unknown[] = []; + session.on("message", (frame) => frames.push(frame)); + const request = session.request({ method: "ping" }); + sockets[0].fire("open"); + await Promise.resolve(); + + sockets[0].fire("message", { data: '{"e":"trade","s":"btcusd"}' }); + assert.deepEqual(frames, [{ e: "trade", s: "btcusd" }]); + + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await request; + session.close(); +}); + +test("close() rejects pending requests", async () => { + const { session, sockets } = harness(); + const request = session.request({ method: "ping" }); + sockets[0].fire("open"); + await Promise.resolve(); + + session.close(); + + await assert.rejects(request, /WebSocket session closed/); +}); + +test("close() rejects pending durable subscriptions and one-shot requests", async () => { + const { session, sockets } = harness(); + const subscription = session.subscribe(["btcusd@trade"]); + const request = session.request({ method: "ping" }); + sockets[0].fire("open"); + await Promise.resolve(); + + session.close(); + + await assert.rejects(subscription.ready, /WebSocket session closed/); + await assert.rejects(request, /WebSocket session closed/); +}); + +test("reconnecting rejects in-flight method requests", async (t) => { + const { session, sockets } = harness(); + t.mock.timers.enable({ apis: ["setTimeout"] }); + const request = session.request({ method: "ping" }); + sockets[0].fire("open"); + await Promise.resolve(); + + sockets[0].fire("close"); + + await assert.rejects(request, /WebSocket session reconnecting/); + session.close(); +}); + +test("a pending durable subscription survives reconnect and resolves on the replay ack", async (t) => { + const { session, sockets } = harness(); + t.mock.timers.enable({ apis: ["setTimeout"] }); + await open(session, sockets); + + const sub = session.subscribe(["btcusd@trade"]); + await Promise.resolve(); + sockets[0].fire("close"); + t.mock.timers.tick(0); + sockets[1].fire("open"); + + assert.deepEqual(sockets[1].sent.map((frame) => JSON.parse(frame)), [{ + id: 1, + method: "SUBSCRIBE", + params: ["btcusd@trade"], + }]); + sockets[1].fire("message", { data: '{"id":1,"status":200}' }); + + await sub.ready; + session.close(); +}); + +test("replayed subscriptions emit success and rejection separately from initial ready", async (t) => { + const { session, sockets } = harness(); + t.mock.timers.enable({ apis: ["setTimeout"] }); + await open(session, sockets); + + const events: string[] = []; + session.on("resubscribed", () => events.push("resubscribed")); + session.on("subscriptionError", () => events.push("subscriptionError")); + const sub = session.subscribe(["btcusd@trade"]); + await Promise.resolve(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await sub.ready; + + sockets[0].fire("close"); + t.mock.timers.tick(0); + sockets[1].fire("open"); + sockets[1].fire("message", { data: '{"id":1,"status":400,"error":{"code":-1,"msg":"rejected"}}' }); + + assert.deepEqual(events, ["subscriptionError"]); + session.close(); +}); + +test("a rejected replay is not sent on a later reconnect", async (t) => { + const { session, sockets } = harness(); + t.mock.timers.enable({ apis: ["setTimeout"] }); + await open(session, sockets); + + const sub = session.subscribe(["btcusd@trade"]); + await Promise.resolve(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await sub.ready; + + sockets[0].fire("close"); + t.mock.timers.tick(0); + sockets[1].fire("open"); + sockets[1].fire("message", { data: '{"id":1,"status":400,"error":{"code":-1,"msg":"rejected"}}' }); + + sockets[1].fire("close"); + t.mock.timers.tick(0); + sockets[2].fire("open"); + + assert.deepEqual(sockets[2].sent, []); + session.close(); +}); + +test("reconnect rejects method requests while retaining a pending subscription", async (t) => { + const { session, sockets } = harness(); + t.mock.timers.enable({ apis: ["setTimeout"] }); + await open(session, sockets); + + const sub = session.subscribe(["btcusd@trade"]); + const request = session.request({ method: "ping" }); + await Promise.resolve(); + sockets[0].fire("close"); + + await assert.rejects(request, /WebSocket session reconnecting/); + t.mock.timers.tick(0); + sockets[1].fire("open"); + assert.deepEqual(sockets[1].sent.map((frame) => JSON.parse(frame)), [{ + id: 1, + method: "SUBSCRIBE", + params: ["btcusd@trade"], + }]); + sockets[1].fire("message", { data: '{"id":1,"status":200}' }); + await sub.ready; + session.close(); +}); + +test("malformed transport errors reject method requests without destroying pending subscriptions", async () => { + const { session, sockets } = harness(); + await open(session, sockets); + + const sub = session.subscribe(["btcusd@trade"]); + const request = session.request({ method: "ping" }); + await Promise.resolve(); + sockets[0].fire("message", { data: "not-json" }); + + await assert.rejects(request, /malformed WebSocket frame/); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await sub.ready; + session.close(); +}); + +test("subscribe() sends a durable SUBSCRIBE, resolves ready on ack, and replays on reconnect", async (t) => { + const { session, sockets } = harness(); + t.mock.timers.enable({ apis: ["setTimeout"] }); + await open(session, sockets); + + const sub = session.subscribe(["btcusd@trade"]); + await Promise.resolve(); + assert.deepEqual(JSON.parse(sockets[0].sent[0]), { + id: 1, + method: "SUBSCRIBE", + params: ["btcusd@trade"], + }); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await sub.ready; + + sockets[0].fire("close"); + t.mock.timers.tick(0); + sockets[1].fire("open"); + + assert.deepEqual(sockets[1].sent.map((frame) => JSON.parse(frame)), [{ + id: 1, + method: "SUBSCRIBE", + params: ["btcusd@trade"], + }]); + session.close(); +}); + +test("subscription close removes durable replay and sends UNSUBSCRIBE", async (t) => { + const { session, sockets } = harness(); + t.mock.timers.enable({ apis: ["setTimeout"] }); + await open(session, sockets); + + const sub = session.subscribe(["btcusd@trade"]); + await Promise.resolve(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await sub.ready; + + const closed = sub.close(); + await Promise.resolve(); + assert.deepEqual(JSON.parse(sockets[0].sent[1]), { + id: 2, + method: "UNSUBSCRIBE", + params: ["btcusd@trade"], + }); + sockets[0].fire("message", { data: '{"id":2,"status":200}' }); + await closed; + + sockets[0].fire("close"); + t.mock.timers.tick(0); + sockets[1].fire("open"); + + assert.deepEqual(sockets[1].sent, []); + session.close(); +}); + +test("subscription close during reconnect resolves without retaining unsubscribe state", async (t) => { + const { session, sockets } = harness(); + t.mock.timers.enable({ apis: ["setTimeout"] }); + await open(session, sockets); + + const sub = session.subscribe(["btcusd@trade"]); + await Promise.resolve(); + sockets[0].fire("message", { data: '{"id":1,"status":200}' }); + await sub.ready; + sockets[0].fire("close"); + + const closed = sub.close(); + await closed; + t.mock.timers.tick(0); + sockets[1].fire("open"); + + assert.deepEqual(sockets[1].sent, []); + session.close(); +}); + +test("subscription close before subscribe send rejects ready and sends no unsubscribe", async () => { + const { session, sockets } = harness(); + const sub = session.subscribe(["btcusd@trade"]); + + await sub.close(); + + await assert.rejects(sub.ready, /subscription closed before acknowledgement/); + assert.deepEqual(sockets[0].sent, []); + session.close(); +}); + +test("close() before subscribe ack rejects subscription readiness", async () => { + const { session } = harness(); + const sub = session.subscribe(["btcusd@trade"]); + + session.close(); + + await assert.rejects(sub.ready, /WebSocket session closed/); +}); + +test("authenticated reconnects generate fresh upgrade headers", async (t) => { + let nonce = 1700000000; + const auth: AuthStrategy = { + nextNonce: () => String(nonce++), + credentialHeaders: async (payloadBase64) => ({ + "X-GEMINI-APIKEY": "key", + "X-GEMINI-SIGNATURE": `sig:${payloadBase64}`, + }), + }; + const { session, sockets, options } = harness({ auth }); + t.mock.timers.enable({ apis: ["setTimeout"] }); + + await open(session, sockets); + sockets[0].fire("close"); + t.mock.timers.tick(0); + await new Promise((resolve) => setImmediate(resolve)); + + assert.equal(sockets.length, 2); + assert.equal(options[0].headers?.["X-GEMINI-NONCE"], "1700000000"); + assert.equal(options[1].headers?.["X-GEMINI-NONCE"], "1700000001"); + session.close(); +}); + +test("HMAC auth creates WebSocket upgrade headers", async () => { + const auth: AuthStrategy = { + nextNonce: () => "1700000000", + credentialHeaders: async (payloadBase64) => ({ + "X-GEMINI-APIKEY": "key", + "X-GEMINI-SIGNATURE": `sig:${payloadBase64}`, + }), + }; + const { session, sockets, options } = harness({ auth }); + + await open(session, sockets); + + assert.deepEqual(options[0].headers, { + "X-GEMINI-APIKEY": "key", + "X-GEMINI-SIGNATURE": "sig:MTcwMDAwMDAwMA==", + "X-GEMINI-NONCE": "1700000000", + "X-GEMINI-PAYLOAD": "MTcwMDAwMDAwMA==", + }); + session.close(); +}); + +test("OAuth auth creates only Authorization upgrade headers", async () => { + const auth: AuthStrategy = { + nextNonce: () => undefined, + credentialHeaders: async () => ({ Authorization: "Bearer token" }), + }; + const { session, sockets, options } = harness({ auth }); + + await open(session, sockets); + + assert.deepEqual(options[0].headers, { Authorization: "Bearer token" }); + session.close(); +}); + +test("auth header helper rejects transport-controlled credential headers", async () => { + const auth: AuthStrategy = { + nextNonce: () => "1700000000", + credentialHeaders: async () => ({ "X-GEMINI-PAYLOAD": "evil" }), + }; + const session = new WsSession({ + url: "wss://example.test", + auth, + socketFactory: () => new FakeSocket(), + }); + + await assert.rejects(session.connect(), /reserved header X-GEMINI-PAYLOAD/); +}); diff --git a/packages/sdk-typescript/src/transport.ts b/packages/sdk-typescript/src/transport.ts new file mode 100644 index 0000000..e3c67be --- /dev/null +++ b/packages/sdk-typescript/src/transport.ts @@ -0,0 +1,373 @@ +import { TypedEmitter } from "./core/typed-emitter.js"; +import { utf8ByteLength } from "./core/encoding.js"; + +import { ConnectionError, RequestTimeoutError, SdkError, serializeError } from "./errors.js"; +import { DEFAULT_TIMEOUT_MS, deadline, type RequestOptions, withSignal } from "./core/deadline.js"; +import { sanitizeDiagnosticUrl, type DiagnosticListener } from "./diagnostics.js"; +import { parseLosslessJson } from "./json.js"; +import { emitDiagnostic, type Logger, NOOP_LOGGER } from "./logging.js"; + +const DEFAULT_BACKOFF_BASE_MS = 250; +const DEFAULT_BACKOFF_CAP_MS = 30_000; +const DEFAULT_BACKOFF_FACTOR = 2; +const DEFAULT_MAX_MESSAGE_SIZE_BYTES = 1_048_576; + +/** + * The minimal slice of the WebSocket API this transport depends on. Native + * `WebSocket` satisfies it; tests pass a fake implementing just these members. + */ +export interface SocketLike { + addEventListener(type: string, listener: (ev: unknown) => void): void; + send(data: string): void; + close(): void; +} + +/** Produces a socket for a URL. The default uses the native WebSocket global. */ +export interface SocketFactoryOptions { + headers?: Record; +} +export type SocketFactory = (url: string, options: SocketFactoryOptions) => SocketLike; + +export interface WsTransportOptions { + logger?: Logger; + onDiagnostic?: DiagnosticListener; + socketFactory?: SocketFactory; + headers?: Record; + headersFactory?: () => Promise | undefined>; + /** Reconnect backoff tuning. Defaults: base 250ms, cap 30s, factor 2. */ + backoff?: { baseMs?: number; capMs?: number; factor?: number }; + // Injectable so tests can make jitter deterministic; production uses Math.random. + random?: () => number; + /** Bounds the initial socket open; reconnects remain background work. */ + timeoutMs?: number; + /** Rejects and closes frames larger than this many UTF-8 bytes. */ + maxMessageSizeBytes?: number; +} +type WsTransportEvents = { + open: () => void; + message: (frame: unknown) => void; + error: (error: unknown) => void; + reconnecting: (attempt: number) => void; + close: () => void; +}; + + +export class WsTransport extends TypedEmitter { + private readonly url: string; + private readonly logger: Logger; + private readonly onDiagnostic?: DiagnosticListener; + private readonly socketFactory: SocketFactory; + private readonly headers?: Record; + private readonly headersFactory?: () => Promise | undefined>; + private readonly baseMs: number; + private readonly capMs: number; + private readonly factor: number; + private readonly random: () => number; + private readonly timeoutMs: number; + private readonly maxMessageSizeBytes: number; + private reconnectAttempt = 0; + private closedByUser = false; + private socket?: SocketLike; + private reconnectTimer?: ReturnType; + private resolveConnect?: () => void; + private rejectConnect?: (error: unknown) => void; + private connectStarted = false; + private isOpen = false; + private everOpened = false; + private closeEmitted = false; + private firstSocket = true; + private opening = false; + // Every subscription ever made, replayed on each reconnect — a fresh socket is + // a blank slate at the exchange, so the caller's subs must be re-sent. + private readonly subscriptions: unknown[] = []; + + constructor(url: string, options?: WsTransportOptions) { + super(); + this.url = url; + this.logger = options?.logger ?? NOOP_LOGGER; + this.onDiagnostic = options?.onDiagnostic; + this.socketFactory = options?.socketFactory ?? ((socketUrl, socketOptions) => { + if (socketOptions.headers && Object.keys(socketOptions.headers).length > 0) { + throw new SdkError( + "The default WebSocket factory cannot send custom headers. " + + "Pass a socketFactory that supports upgrade headers (e.g. the ws package for Node.js).", + ); + } + return new WebSocket(socketUrl) as SocketLike; + }); + this.headers = options?.headers ? { ...options.headers } : undefined; + this.headersFactory = options?.headersFactory; + this.baseMs = options?.backoff?.baseMs ?? DEFAULT_BACKOFF_BASE_MS; + this.capMs = options?.backoff?.capMs ?? DEFAULT_BACKOFF_CAP_MS; + this.factor = options?.backoff?.factor ?? DEFAULT_BACKOFF_FACTOR; + this.random = options?.random ?? Math.random; + this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS; + this.maxMessageSizeBytes = options?.maxMessageSizeBytes ?? DEFAULT_MAX_MESSAGE_SIZE_BYTES; + if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) { + throw new SdkError("timeoutMs must be a finite positive number"); + } + if (!Number.isFinite(this.maxMessageSizeBytes) || this.maxMessageSizeBytes <= 0) { + throw new SdkError("maxMessageSizeBytes must be a finite positive number"); + } + } + + private emitDiagnosticEvent( + level: "debug" | "info" | "warn" | "error", + name: string, + traffic: "control" | "stream" | "reconnect" | "mutation", + metadata?: Record, + error?: unknown, + ): void { + emitDiagnostic({ + level, + component: "websocket", + name, + traffic, + metadata: { url: sanitizeDiagnosticUrl(this.url), ...metadata }, + ...(error ? { error: serializeError(error) } : {}), + }, this.logger, this.onDiagnostic); + } + + // Delay before reconnect attempt N. Attempt 0 is immediate (most drops are + // transient); later attempts grow exponentially, capped. Equal jitter — half + // fixed, half random — keeps a floor while de-synchronising many clients that + // all dropped at once (e.g. an exchange restart), so they don't reconnect in + // lockstep and re-crash it. + private backoffDelay(attempt: number): number { + if (attempt === 0) return 0; + const raw = Math.min(this.capMs, this.baseMs * this.factor ** (attempt - 1)); + return raw / 2 + this.random() * (raw / 2); + } + + /** + * Open the connection. Resolves on the first 'open'. + * + * The initial connection is bounded by the configured timeout and rejects on + * failure. Reconnects after a drop remain background work; watch the + * 'reconnecting'/'open' events for those. Call once; calling again throws. + */ + connect(options: RequestOptions = {}): Promise { + if (this.connectStarted) { + throw new SdkError("connect() called more than once on the same transport"); + } + this.connectStarted = true; + const connection = new Promise((resolve, reject) => { + this.resolveConnect = resolve; + this.rejectConnect = reject; + void this.openSocket(); + }); + if (options.signal === undefined && options.timeoutMs === undefined) { + const timer = setTimeout(() => this.rejectConnect?.( + new RequestTimeoutError(`WebSocket connection exceeded ${this.timeoutMs}ms deadline`), + ), this.timeoutMs); + const resolve = this.resolveConnect; + const reject = this.rejectConnect; + this.resolveConnect = () => { clearTimeout(timer); resolve?.(); }; + this.rejectConnect = (error) => { clearTimeout(timer); reject?.(error); }; + return connection; + } + const execution = deadline(options, this.timeoutMs); + return withSignal(connection, execution.signal).finally(execution.cleanup); + } + + // Open a socket and wire its lifecycle. Called for the initial connect and + // again for every reconnect, so all connections behave identically. + private async openSocket(): Promise { + // A reconnect timer may fire after the caller tore us down; don't reconnect. + if (this.closedByUser || this.opening) return; + this.opening = true; + + let headers = this.headers; + if (!this.firstSocket && this.headersFactory) { + try { + headers = await this.headersFactory(); + } catch (error) { + this.opening = false; + this.emitDiagnosticEvent("error", "ws.reconnect_headers.failure", "reconnect", { attempt: this.reconnectAttempt }, error); + this.scheduleReconnect(); + return; + } + } + this.firstSocket = false; + if (this.closedByUser) { + this.opening = false; + return; + } + + let socket: SocketLike; + try { + socket = this.socketFactory(this.url, { headers }); + } catch (cause) { + this.opening = false; + const error = new ConnectionError("WebSocket socket creation failed", { + cause, + opened: this.everOpened, + }); + this.rejectConnect?.(error); + this.emitDiagnosticEvent("error", "ws.socket_factory.failure", "control", { attempt: this.reconnectAttempt }, error); + this.scheduleReconnect(); + return; + } + this.opening = false; + this.socket = socket; + + // A superseded socket (replaced on reconnect) can still fire late events; the + // SocketLike seam has no removeEventListener, so ignore anything not from the + // current socket — otherwise a stale frame would be emitted as if it were live. + const isCurrent = () => socket === this.socket; + + socket.addEventListener("open", () => { + if (!isCurrent()) return; + this.emitDiagnosticEvent("info", "ws.open", "control"); + this.isOpen = true; + this.everOpened = true; + this.reconnectAttempt = 0; // a live connection resets the backoff curve + + this.emit("open"); + this.resolveConnect?.(); // idempotent — only the first connect() awaits it + for (const sub of this.subscriptions) this.sendSub(sub); + }); + + socket.addEventListener("message", (event) => { + if (!isCurrent() || !this.isOpen) return; + // Gemini sends text JSON; reject binary frames until the protocol requires decoding. + const frameText = (event as { data: unknown }).data; + if (typeof frameText !== "string") { + const error = new ConnectionError("WebSocket frame must be a string", { + opened: this.everOpened, + }); + this.emitDiagnosticEvent("error", "ws.invalid_frame_type", "stream", undefined, error); + if (this.listenerCount("error") > 0) this.emit("error", error); + socket.close(); + return; + } + if (utf8ByteLength(frameText) > this.maxMessageSizeBytes) { + const error = new ConnectionError("WebSocket message exceeded the configured size limit", { + opened: this.everOpened, + }); + this.emitDiagnosticEvent("warn", "ws.message_too_large", "stream", { + maxMessageSizeBytes: this.maxMessageSizeBytes, + }, error); + if (this.listenerCount("error") > 0) this.emit("error", error); + socket.close(); + return; + } + let parsed: unknown; + try { + parsed = parseLosslessJson(frameText); + } catch (cause) { + // Fail loud: a frame we can't parse must never be silently dropped on a + // trading path. Always log it; the socket itself is still fine, so keep it. + const error = new ConnectionError("malformed WebSocket frame", { + cause, + opened: this.everOpened, + }); + this.emitDiagnosticEvent("error", "ws.malformed_frame", "stream", undefined, error); + // Emit only if someone's listening — avoids swallowing the error when + // no listener is attached. Already logged above, so this stays loud without crashing. + if (this.listenerCount("error") > 0) { + this.emit("error", error); + } + return; + } + this.emit("message", parsed); + }); + + socket.addEventListener("error", (event) => { + if (!isCurrent()) return; + const socketErrorEvent = event as { error?: unknown }; + const error = new ConnectionError("WebSocket socket error", { + cause: event instanceof Error ? event : socketErrorEvent.error, + opened: this.everOpened, + }); + this.emitDiagnosticEvent("error", "ws.socket.failure", "control", undefined, error); + if (this.listenerCount("error") > 0) this.emit("error", error); + }); + + socket.addEventListener("close", (event) => { + if (!isCurrent()) return; + this.isOpen = false; + + // A deliberate close() — don't fight the caller by reconnecting. + if (this.closedByUser) { + this.emitClose(); + return; + } + + const closeEvent = event as { code?: unknown; reason?: unknown }; + const error = new ConnectionError("WebSocket connection closed unexpectedly", { + opened: this.everOpened, + closeCode: typeof closeEvent?.code === "number" ? closeEvent.code : undefined, + closeReason: typeof closeEvent?.reason === "string" ? closeEvent.reason : undefined, + }); + this.emitDiagnosticEvent("error", "ws.close.failure", "reconnect", undefined, error); + if (this.listenerCount("error") > 0) this.emit("error", error); + this.scheduleReconnect(); + }); + } + + private scheduleReconnect(): void { + if (this.closedByUser || this.reconnectTimer) return; + const delay = this.backoffDelay(this.reconnectAttempt); + this.emitDiagnosticEvent("warn", "ws.reconnect", "reconnect", { + attempt: this.reconnectAttempt, + delayMs: delay, + }); + this.emit("reconnecting", this.reconnectAttempt); + this.reconnectAttempt++; + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = undefined; + void this.openSocket(); + }, delay); + } + + /** Send a subscription frame. `sub` is opaque — serialized and sent as-is. */ + subscribe(sub: unknown): void { + this.subscriptions.push(sub); + if (this.isOpen) this.sendSub(sub); + } + + /** Send a one-shot frame. It is not remembered or replayed after reconnect. */ + send(frame: unknown): void { + if (!this.isOpen) { + throw new SdkError("send() called before WebSocket is open"); + } + this.sendSub(frame); + } + + /** Stop replaying a durable subscription. The caller restarts to drop its server-side stream. */ + unsubscribe(sub: unknown): void { + const index = this.subscriptions.indexOf(sub); + if (index >= 0) this.subscriptions.splice(index, 1); + } + + /** Restart the live connection; durable subscriptions replay when the fresh socket opens. */ + reconnect(): void { + if (this.closedByUser || !this.isOpen) return; + this.isOpen = false; // reject frames immediately; close may complete asynchronously + this.socket?.close(); + } + + private sendSub(sub: unknown): void { + this.socket?.send(JSON.stringify(sub)); + } + + /** Deliberately tear down the connection. Suppresses reconnect; emits 'close'. */ + close(): void { + this.closedByUser = true; + this.isOpen = false; + clearTimeout(this.reconnectTimer); // cancel any reconnect scheduled mid-backoff + this.resolveConnect?.(); // unblock a connect() still awaiting a first 'open' + this.socket?.close(); + // Announce teardown now: mid-backoff the socket is already dead and won't fire + // 'close'. If it is live, its 'close' handler also calls emitClose() — deduped. + this.emitClose(); + } + + private emitClose(): void { + if (this.closeEmitted) return; + this.closeEmitted = true; + this.emitDiagnosticEvent("info", "ws.close", "control", { reason: "caller" }); + this.emit("close"); + } +} diff --git a/packages/sdk-typescript/src/types/client.ts b/packages/sdk-typescript/src/types/client.ts new file mode 100644 index 0000000..3a17020 --- /dev/null +++ b/packages/sdk-typescript/src/types/client.ts @@ -0,0 +1,77 @@ +import type { SdkError } from "../errors.js"; +import type { Logger } from "../logging.js"; +import type { Level } from "../orderbook.js"; +import type { AuthStrategy, FetchLike } from "../core/http.js"; +import type { DiagnosticListener } from "../diagnostics.js"; +import type { Environment } from "../core/environment.js"; +import type { SocketFactory } from "../transport.js"; + +/** Client options. All optional — `new GeminiMarkets()` is valid. */ +export interface GeminiMarketsOptions { + /** Venue to connect to. Default "production". */ + env?: Environment; + /** Where SDK logs go. Default: silent (NoopLogger). */ + logger?: Logger; + /** Receives safe structured diagnostics from REST, OAuth, WebSocket, and order books. */ + onDiagnostic?: DiagnosticListener; + /** Authentication used by private Prediction Markets REST methods. */ + auth?: AuthStrategy; + /** End-to-end timeout for REST and WebSocket waits. Default 30 seconds. */ + timeoutMs?: number; + /** Optional application-level WebSocket liveness checks. */ + webSocketLiveness?: { intervalMs?: number; timeoutMs?: number }; + /** Maximum accepted inbound WebSocket message size in UTF-8 bytes. */ + webSocketMaxMessageSizeBytes?: number; + /** Retry count for generated safe REST reads only. Default 5. */ + maxRetries?: number; + /** Backoff tuning for generated safe REST reads only. */ + backoff?: { baseMs?: number; capMs?: number; factor?: number }; + /** Custom fetch implementation for REST instrumentation or routing. */ + fetch?: FetchLike; + /** Custom WebSocket factory for runtimes that need upgrade headers (e.g. ws for Node HMAC auth). */ + webSocketFactory?: SocketFactory; +} + +export type BookEvent = "update" | "resync" | "error"; + +/** + * Levels that changed on the last update (not the whole book); qty "0" = removed. + * Exception: the first "update" after subscribe or a "resync" carries the FULL book + * (a replacement, not an incremental patch) — mirror it as authoritative state. + */ +export interface BookDelta { + bids: Level[]; + asks: Level[]; +} + +/** + * Live, self-healing L2 book for one symbol, returned by orderBook(). Reads are + * safe anytime; the SDK keeps it fresh and heals gaps in the background. + */ +export interface LiveOrderBook { + readonly symbol: string; + + // Reads — current state, best-first. + bestBid(): Level | undefined; + bestAsk(): Level | undefined; + topN(side: "bids" | "asks", n: number): Level[]; + spread(): number | undefined; // float, display-only + mid(): number | undefined; // float, display-only + snapshot(): { bids: Level[]; asks: Level[] }; + + // Events. Pass { signal } to auto-remove on abort, or use off()/close(). + on( + event: "update", + cb: (book: LiveOrderBook, delta: BookDelta) => void, + options?: { signal?: AbortSignal }, + ): void; + /** resync = gap detected, book stale and rebuilding — protect yourself until the next "update". */ + on(event: "resync", cb: () => void, options?: { signal?: AbortSignal }): void; + on(event: "error", cb: (err: SdkError) => void, options?: { signal?: AbortSignal }): void; + + /** Remove a listener — must be the same function reference passed to on(). */ + off(event: BookEvent, cb: (...args: never[]) => void): void; + + /** Stop this book: remove all listeners and release its stream. */ + close(): void; +} diff --git a/packages/sdk-typescript/src/websocket-types.ts b/packages/sdk-typescript/src/websocket-types.ts new file mode 100644 index 0000000..d7acab1 --- /dev/null +++ b/packages/sdk-typescript/src/websocket-types.ts @@ -0,0 +1,2 @@ +export * from "./generated/websocket/index.js"; +export type { GenericSuccessResponse as SuccessResponse } from "./generated/websocket/index.js"; diff --git a/packages/sdk-typescript/src/websocket.ts b/packages/sdk-typescript/src/websocket.ts new file mode 100644 index 0000000..8137994 --- /dev/null +++ b/packages/sdk-typescript/src/websocket.ts @@ -0,0 +1,842 @@ +import { TypedEmitter } from "./core/typed-emitter.js"; + +import type { AuthStrategy } from "./core/http.js"; +import { DEFAULT_TIMEOUT_MS, type RequestOptions } from "./core/deadline.js"; +import { SdkError, serializeError } from "./errors.js"; +import { LiveOrderBook } from "./live-order-book.js"; +import { emitDiagnostic, type Logger, NOOP_LOGGER } from "./logging.js"; +import type { DiagnosticListener, OperationContext } from "./diagnostics.js"; +import type { SocketFactory } from "./transport.js"; +import type { LiveOrderBook as LiveOrderBookContract } from "./types/client.js"; +import type { + BalanceUpdate, + BookTicker, + ContractStatus, + DepthResponse, + DepthUpdate, + GenericSuccessResponse, + ListSubscriptionsResponse, + OrderActionResponse, + OrderCancelParams, + OrderPlaceParams, + OrderUpdate, + OrderBookSnapshot, + PositionReport, + RfqConfirmQuoteParams, + RfqConfirmQuoteResponse, + RfqPrivateDelivery, + RfqPublicEvent, + RfqSubmitQuoteParams, + RfqSubmitQuoteResponse, + RfqWithdrawQuoteParams, + RfqWithdrawQuoteResponse, + Trade, +} from "./websocket-types.js"; +import { WsSession, type WsSubscription } from "./ws-session.js"; + +type StreamEvent = "message" | "error" | "close" | "resubscribed" | "subscriptionError"; +type StreamListener = ((message: T) => void) | ((err: Error) => void) | (() => void); +type FrameMatcher = (frame: unknown) => frame is T; +type StreamRegistration = { + event: StreamEvent; + wrapper: (...args: unknown[]) => void; + signal?: AbortSignal; + onAbort?: () => void; + callback: StreamListener; +}; + +export type WebSocketStreamState = "active" | "reconnecting" | "failed" | "closed"; + +export interface WebSocketStream { + readonly ready: Promise; + readonly state: WebSocketStreamState; + readonly lastError?: Error; + readonly malformedFrameCount: number; + on(event: "message", cb: (message: T) => void, options?: { signal?: AbortSignal }): this; + on(event: "error", cb: (err: Error) => void, options?: { signal?: AbortSignal }): this; + on(event: "close", cb: () => void, options?: { signal?: AbortSignal }): this; + on(event: "resubscribed", cb: () => void, options?: { signal?: AbortSignal }): this; + /** Emits the failed subscription in addition to notifying registered error listeners. */ + on(event: "subscriptionError", cb: (err: Error) => void, options?: { signal?: AbortSignal }): this; + off(event: "message", cb: (message: T) => void): this; + off(event: "error", cb: (err: Error) => void): this; + off(event: "close", cb: () => void): this; + off(event: "resubscribed", cb: () => void): this; + off(event: "subscriptionError", cb: (err: Error) => void): this; + close(options?: RequestOptions): Promise; +} + +export type DepthIntervalMs = 100; +export type PartialDepthLevel = 5 | 10 | 20; + +export interface DepthUpdatesOptions extends RequestOptions { + intervalMs?: DepthIntervalMs; +} + +export interface PartialDepthOptions extends RequestOptions { + levels: PartialDepthLevel; + intervalMs?: DepthIntervalMs; +} + +export interface DepthSnapshotOptions extends RequestOptions { + limit?: number; +} + +export interface WebSocketScopeOptions extends RequestOptions { + scope: "account" | "session"; +} + +export interface WebSocketAccountIntervalOptions extends RequestOptions { + intervalMs?: 0 | 1000; +} + +export type WebSocketOrderPlaceParams = Omit< + OrderPlaceParams, + "side" | "type" | "timeInForce" | "eventOutcome" +> & { + side: "BUY" | "SELL"; + type: "LIMIT" | "MARKET"; + timeInForce: "GTC" | "IOC" | "FOK" | "MOC"; + eventOutcome?: "YES" | "NO"; +}; + +export interface WebSocketCancelAllOptions extends RequestOptions { + confirm: boolean; +} + +export interface GeminiWebSocketOptions { + url: string; + snapshotUrl?: string; + auth?: AuthStrategy; + logger?: Logger; + onDiagnostic?: DiagnosticListener; + socketFactory?: SocketFactory; + snapshotStream?: boolean; + /** Deadline for connection, acknowledgements, and unsubscribe completion. */ + timeoutMs?: number; + /** Optional application-level liveness checks for long-lived sessions. */ + liveness?: { intervalMs?: number; timeoutMs?: number }; + /** Maximum accepted inbound WebSocket message size in UTF-8 bytes. */ + maxMessageSizeBytes?: number; +} + +type BookPhase = "awaitingAck" | "awaitingSnapshot" | "live"; + +interface OrderBookEntry { + book: LiveOrderBook; + phase: BookPhase; + pending: unknown[]; + subscription: WsSubscription; +} + +type StreamEmitterEvents = { + message: (frame: unknown) => void; + error: (error: unknown) => void; + close: () => void; + resubscribed: () => void; + subscriptionError: (error: unknown) => void; +}; + +class PublicWebSocketStream implements WebSocketStream { + readonly ready: Promise; + private readonly emitter = new TypedEmitter(); + private readonly onMessage: (frame: unknown) => void; + private readonly onError: (error: unknown) => void; + private readonly onClose: () => void; + private readonly onReconnecting: () => void; + private readonly onReconnected: (event: { id: string | number }) => void; + private readonly onSubscriptionError: (event: { id: string | number; error: unknown }) => void; + private readonly registrations = new Map, StreamRegistration[]>(); + private streamState: WebSocketStreamState = "active"; + private streamError?: Error; + private malformedFrames = 0; + private closed = false; + + constructor( + private readonly session: WsSession, + private readonly subscription: WsSubscription, + private readonly matcher: FrameMatcher, + private readonly release: () => void, + private readonly symbol: string | undefined, + private readonly onMalformed: (symbol: string, count: number) => void, + ) { + this.ready = subscription.ready; + this.onMessage = (frame) => { + if (this.closed) return; + if (this.matcher(frame)) { + this.emitter.emit("message", frame); + } else if (this.symbol && lowerSymbol(record(frame) ?? {}) === this.symbol) { + this.malformedFrames++; + this.onMalformed(this.symbol, this.malformedFrames); + } + }; + this.onError = (error) => { + if (this.closed) return; + this.streamError = error instanceof Error ? error : new SdkError("WebSocket stream error"); + this.streamState = "failed"; + if (this.emitter.listenerCount("error") > 0) this.emitter.emit("error", this.streamError); + }; + this.onClose = () => { + this.streamState = "closed"; + this.dispose(); + }; + this.onReconnecting = () => { + if (!this.closed) this.streamState = "reconnecting"; + }; + this.onReconnected = (event) => { + if (!this.closed && String(event.id) === String(this.subscription.id)) { + this.streamState = "active"; + this.emitter.emit("resubscribed"); + } + }; + this.onSubscriptionError = (event) => { + if (!this.closed && String(event.id) === String(this.subscription.id)) { + this.onError(event.error); + this.emitter.emit("subscriptionError", this.streamError); + } + }; + session.on("message", this.onMessage); + session.on("error", this.onError); + session.on("reconnecting", this.onReconnecting); + session.on("close", this.onClose); + session.on("resubscribed", this.onReconnected); + session.on("subscriptionError", this.onSubscriptionError); + void this.ready.catch(this.onError); + } + + get state(): WebSocketStreamState { return this.streamState; } + get lastError(): Error | undefined { return this.streamError; } + get malformedFrameCount(): number { return this.malformedFrames; } + + on(event: "message", cb: (message: T) => void, options?: { signal?: AbortSignal }): this; + on(event: "error", cb: (err: Error) => void, options?: { signal?: AbortSignal }): this; + on(event: "close", cb: () => void, options?: { signal?: AbortSignal }): this; + on(event: "resubscribed", cb: () => void, options?: { signal?: AbortSignal }): this; + on(event: "subscriptionError", cb: (err: Error) => void, options?: { signal?: AbortSignal }): this; + on(event: StreamEvent, cb: StreamListener, options?: { signal?: AbortSignal }): this { + if (this.closed || options?.signal?.aborted) return this; + const wrapper = (...args: unknown[]) => (cb as (...values: unknown[]) => void)(...args); + this.emitter.on(event, wrapper); + const registration: StreamRegistration = { event, wrapper, signal: options?.signal, callback: cb }; + if (options?.signal) { + registration.onAbort = () => this.removeRegistration(cb, registration); + options.signal.addEventListener("abort", registration.onAbort, { once: true }); + } + const list = this.registrations.get(cb) ?? []; + list.push(registration); + this.registrations.set(cb, list); + return this; + } + + off(event: "message", cb: (message: T) => void): this; + off(event: "error", cb: (err: Error) => void): this; + off(event: "close", cb: () => void): this; + off(event: "resubscribed", cb: () => void): this; + off(event: "subscriptionError", cb: (err: Error) => void): this; + off(event: StreamEvent, cb: StreamListener): this { + const registration = this.registrations.get(cb)?.find((candidate) => candidate.event === event); + if (registration) this.removeRegistration(cb, registration); + return this; + } + + async close(options?: RequestOptions): Promise { + if (this.closed) return; + this.dispose(); + await this.subscription.close(options); + } + + dispose(): void { + if (this.closed) return; + this.closed = true; + this.streamState = "closed"; + this.session.off("message", this.onMessage); + this.session.off("error", this.onError); + this.session.off("reconnecting", this.onReconnecting); + this.session.off("close", this.onClose); + this.session.off("resubscribed", this.onReconnected); + this.session.off("subscriptionError", this.onSubscriptionError); + this.release(); + for (const list of this.registrations.values()) { + for (const registration of list) registration.signal?.removeEventListener("abort", registration.onAbort!); + } + this.registrations.clear(); + this.emitter.emit("close"); + this.emitter.removeAllListeners(); + } + + private removeRegistration(cb: StreamListener, registration: StreamRegistration): void { + this.emitter.off(registration.event, registration.wrapper); + registration.signal?.removeEventListener("abort", registration.onAbort!); + const list = this.registrations.get(cb); + if (!list) return; + const index = list.indexOf(registration); + if (index >= 0) list.splice(index, 1); + if (list.length === 0) this.registrations.delete(cb); + } +} + +export class GeminiWebSocket { + private readonly url: string; + private readonly snapshotUrl: string; + private readonly auth?: AuthStrategy; + private readonly logger: Logger; + private readonly onDiagnostic?: DiagnosticListener; + private readonly socketFactory?: SocketFactory; + private readonly snapshotStream: boolean; + private readonly timeoutMs: number; + private readonly liveness?: { intervalMs?: number; timeoutMs?: number }; + private readonly maxMessageSizeBytes?: number; + private readonly streams = new Set>(); + private readonly books = new Map(); + private readonly subIdToBook = new Map(); + private session?: WsSession; + private bookSession?: WsSession; + private closed = false; + private bookRoutingAttached = false; + private restartingBooks = false; + private readonly routeBookMessage = (frame: unknown) => this.routeOrderBook(frame); + private readonly prepareBookReconnect = () => { + this.restartingBooks = true; + this.prepareBooksForReconnect(); + }; + private readonly finishBookReconnect = () => { + this.restartingBooks = false; + }; + private readonly logSessionError = (error: unknown) => this.emitDiagnosticEvent("error", "ws.session.failure", "control", undefined, error); + + readonly rfq = { + submitQuote: (params: RfqSubmitQuoteParams, options?: RequestOptions): Promise => + this.authenticatedRequest({ method: "rfq.submit_quote", params }, options), + withdrawQuote: (params: RfqWithdrawQuoteParams, options?: RequestOptions): Promise => + this.authenticatedRequest({ method: "rfq.withdraw_quote", params }, options), + confirmQuote: (params: RfqConfirmQuoteParams, options?: RequestOptions): Promise => + this.authenticatedRequest({ method: "rfq.confirm_quote", params }, options), + }; + + constructor(options: GeminiWebSocketOptions) { + if (!options || typeof options.url !== "string" || options.url.length === 0) { + throw new SdkError("url is required"); + } + this.url = options.url; + this.snapshotUrl = options.snapshotUrl ?? snapshotUrl(options.url); + this.auth = options.auth; + this.logger = options.logger ?? NOOP_LOGGER; + this.onDiagnostic = options.onDiagnostic; + this.socketFactory = options.socketFactory; + this.snapshotStream = options.snapshotStream ?? false; + this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; + this.liveness = options.liveness; + this.maxMessageSizeBytes = options.maxMessageSizeBytes; + if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) { + throw new SdkError("timeoutMs must be a finite positive number"); + } + } + + private emitDiagnosticEvent( + level: "debug" | "info" | "warn" | "error", + name: string, + traffic: "control" | "stream" | "reconnect" | "mutation", + metadata?: Record, + error?: unknown, + operationContext?: OperationContext, + ): void { + emitDiagnostic({ + level, + component: "websocket", + name, + traffic, + metadata, + operationContext, + ...(error ? { error: serializeError(error) } : {}), + }, this.logger, this.onDiagnostic); + } + + bookTicker(symbol: string, options?: RequestOptions): WebSocketStream { + const symbolKey = normalizedSymbol(symbol); + return this.stream(`${symbolKey}@bookTicker`, isBookTickerFor(symbolKey), options); + } + + trades(symbol: string, options?: RequestOptions): WebSocketStream { + const symbolKey = normalizedSymbol(symbol); + return this.stream(`${symbolKey}@trade`, isTradeFor(symbolKey), options); + } + + depthUpdates(symbol: string, options?: DepthUpdatesOptions): WebSocketStream { + const symbolKey = normalizedSymbol(symbol); + return this.stream(`${symbolKey}@depth${intervalSuffix(options?.intervalMs)}`, isDepthUpdateFor(symbolKey), options); + } + + depth(symbol: string, options: PartialDepthOptions): WebSocketStream { + const symbolKey = normalizedSymbol(symbol); + if (!options || ![5, 10, 20].includes(options.levels)) { + throw new SdkError("depth levels must be 5, 10, or 20"); + } + return this.isolatedStream(`${symbolKey}@depth${options.levels}${intervalSuffix(options.intervalMs)}`, isDepthSnapshotFor(symbolKey), options); + } + + contractStatus(options?: RequestOptions): WebSocketStream { + return this.stream("contractStatus", isContractStatus, options); + } + + rfqs(options?: RequestOptions): WebSocketStream { + return this.stream("requestForQuote", isRfqPublicEvent, options); + } + + orders(options: WebSocketScopeOptions): WebSocketStream { + return this.authenticatedStream(`orders@${scope(options)}`, isOrderUpdate, options); + } + + balances(options?: WebSocketAccountIntervalOptions): WebSocketStream { + return this.authenticatedStream(accountIntervalName("balances", options), isBalanceUpdate, options); + } + + positions(options?: WebSocketAccountIntervalOptions): WebSocketStream { + return this.authenticatedStream(accountIntervalName("positions", options), isPositionReport, options); + } + + rfqDeliveries(options: WebSocketScopeOptions): WebSocketStream { + return this.authenticatedStream(`requestForQuote@${scope(options)}`, isRfqPrivateDelivery, options); + } + + orderBook(symbol: string, options?: RequestOptions): LiveOrderBookContract { + if (this.closed) throw new SdkError("orderBook() called on a closed GeminiMarkets client"); + const symbolKey = normalizedSymbol(symbol); + const existing = this.books.get(symbolKey); + if (existing) return existing.book; + + const session = this.ensureBookSession(); + this.ensureBookRouting(session); + + let bookEntry: OrderBookEntry; + const book = new LiveOrderBook(symbolKey, { + logger: this.logger, + onDiagnostic: this.onDiagnostic, + onClose: () => this.releaseBook(symbolKey, bookEntry), + }); + book.on("resync", () => this.restartBooks()); + + const subscription = session.subscribe([`${symbolKey}@${this.snapshotStream ? "depth20" : "depth"}`], options); + bookEntry = { book, phase: "awaitingAck", pending: [], subscription }; + this.books.set(symbolKey, bookEntry); + this.subIdToBook.set(String(subscription.id), symbolKey); + void subscription.ready.then( + () => this.ackBook(subscription.id), + (error) => this.rejectBook(subscription.id, error), + ); + return book; + } + + ping(options?: RequestOptions): Promise { + return this.request({ method: "ping" }, options); + } + + time(options?: RequestOptions): Promise { + return this.request({ method: "time" }, options); + } + + conninfo(options?: RequestOptions): Promise { + return this.request({ method: "conninfo" }, options); + } + + listSubscriptions(options?: RequestOptions): Promise { + return this.request({ method: "LIST_SUBSCRIPTIONS" }, options); + } + + depthSnapshot(symbol: string, options?: DepthSnapshotOptions): Promise { + const symbolKey = normalizedSymbol(symbol); + return this.request({ + method: "depth", + params: options?.limit === undefined ? { symbol: symbolKey } : { symbol: symbolKey, limit: options.limit }, + }, options); + } + + placeOrder(params: WebSocketOrderPlaceParams, options?: RequestOptions): Promise { + return this.authenticatedRequest({ method: "order.place", params }, options); + } + + cancelOrder(params: OrderCancelParams, options?: RequestOptions): Promise { + return this.authenticatedRequest({ method: "order.cancel", params }, options); + } + + async cancelAllOrders(options: WebSocketCancelAllOptions): Promise { + requireConfirmedCancel(options); + return this.authenticatedRequest({ method: "order.cancel_all" }, options); + } + + async cancelSessionOrders(options: WebSocketCancelAllOptions): Promise { + requireConfirmedCancel(options); + return this.authenticatedRequest({ method: "order.cancel_session" }, options); + } + + close(): void { + if (this.closed) return; + this.closed = true; + for (const bookEntry of [...this.books.values()]) bookEntry.book.close(); + this.books.clear(); + this.subIdToBook.clear(); + for (const stream of [...this.streams]) stream.dispose(); + this.streams.clear(); + this.session?.close(); + this.bookSession?.close(); + } + + private stream(name: string, matcher: FrameMatcher, options?: RequestOptions): WebSocketStream { + if (this.closed) throw new SdkError("websocket stream requested on a closed GeminiMarkets client"); + return this.createStream(this.ensureSession(), name, matcher, options); + } + + private createStream( + session: WsSession, + name: string, + matcher: FrameMatcher, + options?: RequestOptions, + closeSessionOnRelease = false, + ): WebSocketStream { + const subscription = session.subscribe([name], options); + let stream: PublicWebSocketStream; + stream = new PublicWebSocketStream(session, subscription, matcher, () => { + this.streams.delete(stream as PublicWebSocketStream); + if (closeSessionOnRelease) session.close(); + }, streamSymbol(name), (symbol, count) => { + this.emitDiagnosticEvent("warn", "ws.stream.malformed_frame", "stream", { symbol, stream: name, count }); + }); + this.streams.add(stream as PublicWebSocketStream); + return stream; + } + + private authenticatedStream(name: string, matcher: FrameMatcher, options?: RequestOptions): WebSocketStream { + this.requireAuth(); + return this.stream(name, matcher, options); + } + + private isolatedStream(name: string, matcher: FrameMatcher, options?: RequestOptions): WebSocketStream { + if (this.closed) throw new SdkError("websocket stream requested on a closed GeminiMarkets client"); + return this.createStream(this.createSession(this.snapshotUrl), name, matcher, options, true); + } + + private request(frame: { method: string; params?: unknown }, options?: RequestOptions): Promise { + if (this.closed) throw new SdkError("websocket request made on a closed GeminiMarkets client"); + return this.ensureSession().request(frame, options); + } + + private async authenticatedRequest(frame: { method: string; params?: unknown }, options?: RequestOptions): Promise { + this.requireAuth(); + return this.request(frame, options); + } + + private requireAuth(): void { + if (!this.auth) throw new SdkError("authenticated WebSocket operation requires auth"); + } + + private ensureSession(): WsSession { + if (!this.session) { + this.session = this.createSession(this.url); + } + return this.session; + } + + private ensureBookSession(): WsSession { + if (!this.bookSession) { + this.bookSession = this.createSession(this.snapshotUrl); + } + return this.bookSession; + } + + private createSession(url: string): WsSession { + return new WsSession({ + url, + auth: this.auth, + logger: this.logger, + onDiagnostic: this.onDiagnostic, + socketFactory: this.socketFactory, + timeoutMs: this.timeoutMs, + liveness: this.liveness, + maxMessageSizeBytes: this.maxMessageSizeBytes, + }); + } + + private ensureBookRouting(session: WsSession): void { + if (this.bookRoutingAttached) return; + this.bookRoutingAttached = true; + this.attachBookRouting(session); + } + + private attachBookRouting(session: WsSession): void { + session.on("message", this.routeBookMessage); + session.on("reconnecting", this.prepareBookReconnect); + session.on("open", this.finishBookReconnect); + session.on("error", this.logSessionError); + } + + private releaseBook(symbolKey: string, bookEntry: OrderBookEntry): void { + if (this.closed || this.books.get(symbolKey) !== bookEntry) return; + this.books.delete(symbolKey); + this.subIdToBook.delete(String(bookEntry.subscription.id)); + void bookEntry.subscription.close().catch((error) => { + this.emitDiagnosticEvent("error", "orderbook.unsubscribe.failure", "control", { symbol: symbolKey }, error); + }); + } + + private prepareBooksForReconnect(): void { + for (const bookEntry of this.books.values()) { + if (bookEntry.book.isClosed()) continue; + bookEntry.phase = "awaitingAck"; + bookEntry.pending = []; + bookEntry.book.markStale(); + } + } + + private restartBooks(): void { + if (this.restartingBooks || !this.bookSession) return; + this.restartingBooks = true; + this.prepareBooksForReconnect(); + this.bookSession.reconnect(); + } + + private routeOrderBook(frame: unknown): void { + if (this.closed) return; + const message = record(frame); + if (!message) return; + + if (message.e === "depthUpdate") { + this.routeDepth(message, frame); + return; + } + const hasSnapshot = this.snapshotStream && + (typeof message.lastUpdateId === "number" || typeof message.lastUpdateId === "bigint") && + typeof message.symbol === "string" && + Array.isArray(message.bids) && + Array.isArray(message.asks); + if (hasSnapshot) { + const snapshot = { + e: "depthUpdate", + E: message.lastUpdateId, + s: message.symbol, + U: message.lastUpdateId, + u: message.lastUpdateId, + b: message.bids, + a: message.asks, + }; + this.routeDepth(snapshot, snapshot); + return; + } + const hasAcknowledgement = typeof message.status === "number" && + (typeof message.id === "string" || typeof message.id === "number"); + if (hasAcknowledgement) { + this.ackOrRejectBook(message as { id: string | number; status: number; error?: unknown }); + } + } + + private routeDepth(message: { s?: unknown }, frame: unknown): void { + if (typeof message.s !== "string") { + this.emitDiagnosticEvent("warn", "orderbook.frame.unroutable", "stream"); + return; + } + const symbolKey = message.s.toLowerCase(); + const bookEntry = this.books.get(symbolKey); + if (!bookEntry) { + this.emitDiagnosticEvent("warn", "orderbook.frame.unsubscribed", "stream", { symbol: symbolKey }); + return; + } + if (bookEntry.phase === "awaitingSnapshot") { + this.activateBook(bookEntry, frame, []); + } else if (bookEntry.phase === "live") { + if (this.snapshotStream) bookEntry.book.applySnapshot(frame); + else bookEntry.book.ingest(frame); + } else { + bookEntry.pending.push(frame); + } + } + + private ackOrRejectBook(frame: { id: string | number; status: number; error?: unknown }): void { + if (frame.error !== undefined || frame.status !== 200) { + this.rejectBook(frame.id, new SdkError(`subscribe rejected with status ${frame.status}`), frame.status); + return; + } + this.ackBook(frame.id); + } + + private ackBook(id: string | number): void { + const key = this.subIdToBook.get(String(id)); + if (key === undefined) return; + const bookEntry = this.books.get(key); + if (!bookEntry || bookEntry.book.isClosed() || bookEntry.phase !== "awaitingAck") return; + if (bookEntry.pending.length === 0) { + bookEntry.phase = "awaitingSnapshot"; + return; + } + const [snapshot, ...diffs] = bookEntry.pending; + this.activateBook(bookEntry, snapshot, diffs); + } + + private rejectBook(id: string | number, error: unknown, status?: number): void { + const key = this.subIdToBook.get(String(id)); + if (key === undefined) return; + const bookEntry = this.books.get(key); + if (!bookEntry || bookEntry.book.isClosed()) return; + this.emitDiagnosticEvent("error", "orderbook.subscribe.failure", "control", { symbol: key, status }, error); + if (bookEntry.book.listenerCount("error") > 0) { + bookEntry.book.emit("error", error instanceof Error ? error : new SdkError(`subscribe rejected for ${key}`)); + } + bookEntry.pending = []; + this.books.delete(key); + this.subIdToBook.delete(String(id)); + void bookEntry.subscription.close().catch((unsubscribeError) => { + this.emitDiagnosticEvent("error", "orderbook.unsubscribe.failure", "control", { symbol: key }, unsubscribeError); + }); + bookEntry.book.close(); + } + + private activateBook(bookEntry: OrderBookEntry, snapshot: unknown, diffs: unknown[]): void { + bookEntry.pending = []; + bookEntry.phase = "live"; + const accepted = bookEntry.book.applySnapshot(snapshot); + if (bookEntry.phase !== "live") return; + if (!accepted) { + bookEntry.phase = "awaitingAck"; + this.restartBooks(); + return; + } + for (const diff of diffs) { + if (this.snapshotStream) bookEntry.book.applySnapshot(diff); + else bookEntry.book.ingest(diff); + if (bookEntry.phase !== "live") return; + } + } +} + +function requireConfirmedCancel(options: WebSocketCancelAllOptions): void { + if (!options || options.confirm !== true) { + throw new SdkError("cancel-all WebSocket methods require confirm: true"); + } +} + +function normalizedSymbol(symbol: string): string { + if (typeof symbol !== "string" || symbol.length === 0) { + throw new SdkError("symbol is required"); + } + return symbol.toLowerCase(); +} + +function streamSymbol(name: string): string | undefined { + const symbol = name.split("@", 1)[0]; + return name.includes("@trade") || name.includes("@bookTicker") || name.includes("@depth") + ? symbol + : undefined; +} + +function snapshotUrl(url: string): string { + try { + const parsed = new URL(url); + parsed.searchParams.set("snapshot", "-1"); + return parsed.toString(); + } catch { + throw new SdkError("url must be a valid WebSocket URL"); + } +} + +function intervalSuffix(intervalMs: DepthIntervalMs | undefined): string { + if (intervalMs === undefined) return ""; + if (intervalMs !== 100) throw new SdkError("only 100ms WebSocket depth intervals are supported"); + return "@100ms"; +} + +function scope(options: WebSocketScopeOptions): "account" | "session" { + if (options?.scope !== "account" && options?.scope !== "session") { + throw new SdkError("scope must be account or session"); + } + return options.scope; +} + +function accountIntervalName(base: "balances" | "positions", options?: WebSocketAccountIntervalOptions): string { + const intervalMs = options?.intervalMs ?? 0; + if (intervalMs !== 0 && intervalMs !== 1000) { + throw new SdkError("intervalMs must be 0 or 1000"); + } + return intervalMs === 1000 ? `${base}@account@1s` : `${base}@account`; +} + +function record(frame: unknown): Record | undefined { + return frame && typeof frame === "object" ? (frame as Record) : undefined; +} + +function lowerSymbol(frame: Record): string | undefined { + return typeof frame.s === "string" ? frame.s.toLowerCase() : undefined; +} + +function isBookTickerFor(symbol: string): FrameMatcher { + return (frame): frame is BookTicker => { + const frameRecord = record(frame); + return !!frameRecord && + lowerSymbol(frameRecord) === symbol && + typeof frameRecord.u !== "undefined" && + typeof frameRecord.b === "string" && + typeof frameRecord.B === "string" && + typeof frameRecord.a === "string" && + typeof frameRecord.A === "string"; + }; +} + +function isTradeFor(symbol: string): FrameMatcher { + return (frame): frame is Trade => { + const frameRecord = record(frame); + return !!frameRecord && + lowerSymbol(frameRecord) === symbol && + typeof frameRecord.t !== "undefined" && + typeof frameRecord.p === "string" && + typeof frameRecord.q === "string" && + typeof frameRecord.m === "boolean"; + }; +} + +function isDepthUpdateFor(symbol: string): FrameMatcher { + return (frame): frame is DepthUpdate => { + const frameRecord = record(frame); + return !!frameRecord && + frameRecord.e === "depthUpdate" && + lowerSymbol(frameRecord) === symbol && + Array.isArray(frameRecord.b) && + Array.isArray(frameRecord.a); + }; +} + +function isDepthSnapshotFor(symbol: string): FrameMatcher { + return (frame): frame is OrderBookSnapshot => { + const frameRecord = record(frame); + if (!frameRecord || + typeof frameRecord.lastUpdateId === "undefined" || + !Array.isArray(frameRecord.bids) || + !Array.isArray(frameRecord.asks)) return false; + return typeof frameRecord.symbol !== "string" || frameRecord.symbol.toLowerCase() === symbol; + }; +} + +function isContractStatus(frame: unknown): frame is ContractStatus { + const message = record(frame); + return !!message && message.e === "contractStatus"; +} + +function isRfqPublicEvent(frame: unknown): frame is RfqPublicEvent { + const message = record(frame); + return !!message && message.e === "requestForQuote" && Array.isArray(message.l); +} + +function isOrderUpdate(frame: unknown): frame is OrderUpdate { + const message = record(frame); + return !!message && message.e === "orderUpdate"; +} + +function isBalanceUpdate(frame: unknown): frame is BalanceUpdate { + const message = record(frame); + return !!message && message.e === "balanceUpdate" && Array.isArray(message.B); +} + +function isPositionReport(frame: unknown): frame is PositionReport { + const message = record(frame); + return !!message && message.e === "positionReport" && Array.isArray(message.P); +} + +function isRfqPrivateDelivery(frame: unknown): frame is RfqPrivateDelivery { + const message = record(frame); + return !!message && message.e === "requestForQuote" && typeof message.i === "string"; +} diff --git a/packages/sdk-typescript/src/ws-session.ts b/packages/sdk-typescript/src/ws-session.ts new file mode 100644 index 0000000..f550a1f --- /dev/null +++ b/packages/sdk-typescript/src/ws-session.ts @@ -0,0 +1,503 @@ +import { TypedEmitter } from "./core/typed-emitter.js"; +import { toBase64 } from "./core/encoding.js"; + +import type { AuthStrategy } from "./core/http.js"; +import { DEFAULT_TIMEOUT_MS, deadline, type RequestOptions, withSignal } from "./core/deadline.js"; +import { + RequestAbortedError, + RequestTimeoutError, + serializeError, + SdkError, + WebSocketRequestError, +} from "./errors.js"; +import type { DiagnosticListener, OperationContext } from "./diagnostics.js"; +import { emitDiagnostic, type Logger, NOOP_LOGGER } from "./logging.js"; +import { WsTransport, type SocketFactory } from "./transport.js"; +import type { + GenericSuccessResponse, + SubscribeRequest, + UnsubscribeRequest, +} from "./websocket-types.js"; + +const DEFAULT_LIVENESS_INTERVAL_MS = 30_000; +const DEFAULT_LIVENESS_TIMEOUT_MS = 10_000; + +type WsMethodFrame = { + id?: string | number; + method: string; + params?: unknown; +}; + +type Pending = { + resolve: (value: unknown) => void; + reject: (reason?: unknown) => void; + durable?: boolean; + replay?: boolean; +}; + +type DurableSubscription = { + frame: SubscribeRequest; + active: boolean; + readySettled: boolean; +}; + +export interface WsSessionOptions { + url: string; + auth?: AuthStrategy; + logger?: Logger; + onDiagnostic?: DiagnosticListener; + socketFactory?: SocketFactory; + timeoutMs?: number; + liveness?: { intervalMs?: number; timeoutMs?: number }; + maxMessageSizeBytes?: number; +} + +export interface WsSubscription { + readonly id: string | number; + readonly ready: Promise; + close(options?: RequestOptions): Promise; +} + +function isNumericNonce(nonce: string): boolean { + return /^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(nonce); +} + +function reservedCredentialHeader(headers: Record): string | undefined { + return Object.keys(headers).find((name) => + ["content-length", "x-gemini-nonce", "x-gemini-payload"].includes(name.toLowerCase()), + ); +} + +async function websocketAuthHeaders(auth: AuthStrategy, options?: RequestOptions): Promise> { + const nonce = auth.nextNonce(); + if (nonce === undefined) { + const headers = await auth.credentialHeaders("", options); + const reserved = reservedCredentialHeader(headers); + if (reserved) throw new SdkError(`AuthStrategy returned reserved header ${reserved}`); + return headers; + } + if (!isNumericNonce(nonce)) { + throw new SdkError("AuthStrategy returned an invalid nonce"); + } + const payloadBase64 = toBase64(nonce); + const headers = await auth.credentialHeaders(payloadBase64, options); + const reserved = reservedCredentialHeader(headers); + if (reserved) throw new SdkError(`AuthStrategy returned reserved header ${reserved}`); + return { + ...headers, + "X-GEMINI-NONCE": nonce, + "X-GEMINI-PAYLOAD": payloadBase64, + }; +} + +type WsSessionEvents = { + open: () => void; + message: (frame: unknown) => void; + error: (error: unknown) => void; + reconnecting: () => void; + close: () => void; + resubscribed: (event: { id: string | number; response: unknown }) => void; + subscriptionError: (event: { id: string | number; error: unknown }) => void; +}; + +export class WsSession extends TypedEmitter { + private readonly url: string; + private readonly auth?: AuthStrategy; + private readonly logger: Logger; + private readonly onDiagnostic?: DiagnosticListener; + private readonly socketFactory?: SocketFactory; + private readonly pending = new Map(); + private readonly subscriptions = new Map(); + private nextId = 1; + private transport?: WsTransport; + private connectPromise?: Promise; + private closed = false; + private transportOpen = false; + private readonly timeoutMs: number; + private readonly liveness?: { intervalMs: number; timeoutMs: number }; + private readonly maxMessageSizeBytes?: number; + private livenessTimer?: ReturnType; + + constructor(options: WsSessionOptions) { + super(); + if (!options || typeof options.url !== "string" || options.url.length === 0) { + throw new SdkError("url is required"); + } + this.url = options.url; + this.auth = options.auth; + this.logger = options.logger ?? NOOP_LOGGER; + this.onDiagnostic = options.onDiagnostic; + this.socketFactory = options.socketFactory; + this.maxMessageSizeBytes = options.maxMessageSizeBytes; + this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; + if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) { + throw new SdkError("timeoutMs must be a finite positive number"); + } + if (options.liveness) { + const intervalMs = options.liveness.intervalMs ?? DEFAULT_LIVENESS_INTERVAL_MS; + const timeoutMs = options.liveness.timeoutMs ?? DEFAULT_LIVENESS_TIMEOUT_MS; + if (!Number.isFinite(intervalMs) || intervalMs <= 0 || !Number.isFinite(timeoutMs) || timeoutMs <= 0) { + throw new SdkError("liveness intervalMs and timeoutMs must be finite positive numbers"); + } + this.liveness = { intervalMs, timeoutMs }; + } + } + + private emitDiagnosticEvent( + level: "debug" | "info" | "warn" | "error", + name: string, + traffic: "control" | "stream" | "reconnect" | "mutation", + metadata?: Record, + error?: unknown, + operationContext?: OperationContext, + ): void { + emitDiagnostic({ + level, + component: "websocket", + name, + traffic, + operationContext, + metadata, + ...(error ? { error: serializeError(error) } : {}), + }, this.logger, this.onDiagnostic); + } + + async connect(options: RequestOptions = {}): Promise { + return this.wait(this.ensureConnected(options), options); + } + + private ensureConnected(options: RequestOptions = {}): Promise { + if (this.closed) throw new SdkError("connect() called on a closed WebSocket session"); + if (this.transportOpen) return Promise.resolve(); + if (this.connectPromise) return this.connectPromise; + const auth = this.auth; + if (!auth) { + this.transport = this.createTransport(undefined); + this.connectPromise = this.transport.connect(); + return this.connectPromise; + } + this.connectPromise = (async () => { + const headers = await websocketAuthHeaders(auth, options); + this.transport = this.createTransport(headers); + await this.transport.connect(); + })(); + return this.connectPromise; + } + + async request(frame: WsMethodFrame, options: RequestOptions = {}): Promise { + if (this.closed) throw new SdkError("request() called on a closed WebSocket session"); + const execution = deadline(options, this.timeoutMs); + const operationContext = operationContextForFrame(frame); + const traffic = isMutationMethod(frame.method) ? "mutation" : "control"; + this.emitDiagnosticEvent("debug", "ws.request.start", traffic, { method: frame.method }, undefined, operationContext); + let id: string | number | undefined; + try { + const connection = this.ensureConnected(options); + await (options.signal || options.timeoutMs !== undefined + ? withSignal(connection, execution.signal) + : connection); + if (this.closed) throw new SdkError("WebSocket session closed"); + id = this.reserveId(frame.id); + const requestFrame = { ...frame, id }; + const pendingRequest = new Promise((resolve, reject) => { + this.pending.set(String(id), { + resolve: (response) => resolve(response as T), + reject, + }); + try { + this.transport?.send(requestFrame); + } catch (error) { + this.pending.delete(String(id)); + reject(error); + } + }); + const response = await withSignal(pendingRequest, execution.signal); + this.emitDiagnosticEvent("info", "ws.request.end", traffic, { + method: frame.method, + status: statusFromFrame(response), + }, undefined, operationContext); + return response; + } catch (error) { + this.emitDiagnosticEvent("error", "ws.request.failure", traffic, { method: frame.method }, error, operationContext); + throw error; + } finally { + if (id !== undefined) this.pending.delete(String(id)); + execution.cleanup(); + } + } + + subscribe(params: string[], options: RequestOptions = {}): WsSubscription { + if (this.closed) throw new SdkError("subscribe() called on a closed WebSocket session"); + const id = this.reserveId(); + const frame: SubscribeRequest = { id, method: "SUBSCRIBE", params }; + this.emitDiagnosticEvent("debug", "ws.subscription.start", "control", { + subscriptionCount: params.length, + }); + let closed = false; + let sent = false; + let rejectReady: (reason?: unknown) => void = () => {}; + const ready = new Promise((resolve, reject) => { + rejectReady = reject; + this.pending.set(String(id), { + resolve: (value) => { + const subscription = this.subscriptions.get(String(id)); + if (subscription) subscription.readySettled = true; + resolve(value as GenericSuccessResponse); + }, + reject, + durable: true, + }); + this.subscriptions.set(String(id), { frame, active: true, readySettled: false }); + const connection = options.signal || options.timeoutMs !== undefined + ? this.wait(this.ensureConnected(options), options) + : this.ensureConnected(options); + void connection.then(() => { + if (closed || this.closed) return; + sent = true; + this.emitDiagnosticEvent("info", "ws.subscription.send", "control", { + subscriptionCount: params.length, + }); + this.transport?.subscribe(frame); + }, (error) => { + this.emitDiagnosticEvent("error", "ws.subscription.failure", "control", { + subscriptionCount: params.length, + }, error); + this.pending.delete(String(id)); + this.subscriptions.delete(String(id)); + reject(error); + }); + }); + + let unsubscribeSent = false; + const sendUnsubscribe = async (closeOptions: RequestOptions = {}): Promise => { + this.transport?.unsubscribe(frame); + if (!sent || !this.transportOpen || unsubscribeSent) return; + unsubscribeSent = true; + const unsubscribe: UnsubscribeRequest = { + method: "UNSUBSCRIBE", + params, + id: this.reserveId(), + }; + await this.request(unsubscribe, closeOptions); + }; + + const boundedReady = this.wait(ready, options).catch((error) => { + closed = true; + if (this.pending.delete(String(id))) { + this.subscriptions.delete(String(id)); + rejectReady(error); + } + if (error instanceof RequestTimeoutError || error instanceof RequestAbortedError) { + void sendUnsubscribe().catch((unsubscribeError) => { + this.emitDiagnosticEvent("error", "ws.subscription.unsubscribe.failure", "control", { + subscriptionCount: params.length, + }, unsubscribeError); + }); + } else { + this.transport?.unsubscribe(frame); + } + throw error; + }); + return { + id, + ready: boundedReady, + close: async (closeOptions = {}) => { + if (closed) return; + closed = true; + if (this.pending.delete(String(id))) { + rejectReady(new SdkError("WebSocket subscription closed before acknowledgement")); + } + this.subscriptions.delete(String(id)); + await sendUnsubscribe(closeOptions); + }, + }; + } + + private async wait(promise: Promise, options: RequestOptions): Promise { + const execution = deadline(options, this.timeoutMs); + try { return await withSignal(promise, execution.signal); } finally { execution.cleanup(); } + } + + close(): void { + if (this.closed) return; + this.closed = true; + this.transportOpen = false; + this.clearLivenessTimer(); + this.rejectAll(new SdkError("WebSocket session closed")); + this.subscriptions.clear(); + this.transport?.close(); + } + + reconnect(): void { + if (this.closed) throw new SdkError("reconnect() called on a closed WebSocket session"); + this.transport?.reconnect(); + } + + private createTransport(headers: Record | undefined): WsTransport { + const auth = this.auth; + const transport = new WsTransport(this.url, { + logger: this.logger, + socketFactory: this.socketFactory, + headers, + onDiagnostic: this.onDiagnostic, + timeoutMs: this.timeoutMs, + maxMessageSizeBytes: this.maxMessageSizeBytes, + headersFactory: auth ? () => websocketAuthHeaders(auth) : undefined, + }); + transport.on("message", (frame: unknown) => this.route(frame)); + transport.on("open", () => { + this.transportOpen = true; + this.connectPromise = Promise.resolve(); + this.scheduleLiveness(); + this.emit("open"); + }); + transport.on("reconnecting", () => { + this.transportOpen = false; + this.clearLivenessTimer(); + this.rejectRequests(new SdkError("WebSocket session reconnecting")); + for (const [key, subscription] of this.subscriptions) { + if (!subscription.active || !subscription.readySettled) continue; + this.pending.set(key, { + resolve: () => {}, + reject: () => { + subscription.active = false; + }, + durable: true, + replay: true, + }); + } + this.emit("reconnecting"); + }); + transport.on("close", () => { + this.transportOpen = false; + this.rejectAll(new SdkError("WebSocket session closed")); + this.emit("close"); + }); + transport.on("error", (transportError: unknown) => { + const error = transportError instanceof Error ? transportError : new SdkError("WebSocket session error"); + // A socket close emits the richer transport error immediately before the + // reconnecting event. Let reconnecting own pending-request rejection; + // standalone malformed/socket errors still reject on the next turn. + queueMicrotask(() => { + if (this.transportOpen) this.rejectRequests(error); + }); + if (this.listenerCount("error") > 0) this.emit("error", transportError); + }); + return transport; + } + + private scheduleLiveness(): void { + if (!this.liveness || this.closed || !this.transportOpen) return; + this.clearLivenessTimer(); + this.livenessTimer = setTimeout(() => { void this.runLiveness(); }, this.liveness.intervalMs); + } + + private clearLivenessTimer(): void { + if (this.livenessTimer) clearTimeout(this.livenessTimer); + this.livenessTimer = undefined; + } + + private async runLiveness(): Promise { + this.livenessTimer = undefined; + if (!this.liveness || this.closed || !this.transportOpen) return; + try { + await this.request({ method: "ping" }, { timeoutMs: this.liveness.timeoutMs }); + } catch (error) { + if (!this.closed && this.transportOpen) { + this.emitDiagnosticEvent("error", "ws.liveness.failure", "reconnect", undefined, error); + this.transport?.reconnect(); + } + return; + } + this.scheduleLiveness(); + } + + private route(frame: unknown): void { + const response = frame as { id?: unknown; status?: unknown; error?: unknown }; + if ((typeof response.id === "string" || typeof response.id === "number") && typeof response.status === "number") { + const key = String(response.id); + const pending = this.pending.get(key); + if (pending) { + this.pending.delete(key); + if (response.error !== undefined || response.status !== 200) { + const error = new WebSocketRequestError({ status: response.status, body: frame }); + pending.reject(error); + const subscription = this.subscriptions.get(key); + if (pending.durable && subscription) { + this.transport?.unsubscribe(subscription.frame); + subscription.active = false; + this.subscriptions.delete(key); + } + if (pending.replay) this.emit("subscriptionError", { id: response.id, error }); + } else { + pending.resolve(frame); + if (pending.replay) { + const subscription = this.subscriptions.get(key); + if (subscription) subscription.active = true; + this.emit("resubscribed", { id: response.id, response: frame }); + } + } + return; + } + return; + } + this.emitDiagnosticEvent("debug", "ws.stream.frame", "stream", streamMetadata(frame)); + this.emit("message", frame); + } + + private rejectAll(error: unknown): void { + for (const pending of this.pending.values()) pending.reject(error); + this.pending.clear(); + } + + private rejectRequests(error: unknown): void { + for (const [key, pending] of this.pending) { + if (pending.durable) continue; + pending.reject(error); + this.pending.delete(key); + } + } + + private reserveId(preferred?: string | number): string | number { + if (preferred !== undefined) { + const key = String(preferred); + if (this.pending.has(key)) throw new SdkError(`WebSocket request id ${key} is already pending`); + return preferred; + } + while (this.pending.has(String(this.nextId))) this.nextId++; + return this.nextId++; + } +} + +function statusFromFrame(message: unknown): number | undefined { + if (message !== null && typeof message === "object" && typeof (message as { status?: unknown }).status === "number") { + return (message as { status: number }).status; + } + return undefined; +} + +function streamMetadata(frame: unknown): Record { + if (frame === null || typeof frame !== "object" || Array.isArray(frame)) return {}; + const message = frame as Record; + return { + ...(typeof message.e === "string" ? { event: message.e } : {}), + ...(typeof message.s === "string" ? { symbol: message.s } : {}), + }; +} + +function isMutationMethod(method: string): boolean { + return /(?:order|quote|cancel|withdraw|transfer|payment|session)/i.test(method); +} + +function operationContextForFrame(frame: WsMethodFrame): OperationContext { + const context: OperationContext = { operation: frame.method }; + if (frame.params === null || typeof frame.params !== "object" || Array.isArray(frame.params)) return context; + const params = frame.params as Record; + const clientOrderId = typeof params.clientOrderId === "string" + ? params.clientOrderId + : typeof params.client_order_id === "string" + ? params.client_order_id + : undefined; + if (clientOrderId !== undefined) context.clientOrderId = clientOrderId; + return context; +} diff --git a/packages/sdk-typescript/tsconfig.json b/packages/sdk-typescript/tsconfig.json new file mode 100644 index 0000000..5d90e9a --- /dev/null +++ b/packages/sdk-typescript/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true + }, + "include": ["src/**/*"], + "exclude": ["src/**/*.test.ts", "src/tests/**"] +} diff --git a/packages/sdk-typescript/tsconfig.typecheck.json b/packages/sdk-typescript/tsconfig.typecheck.json new file mode 100644 index 0000000..18d219e --- /dev/null +++ b/packages/sdk-typescript/tsconfig.typecheck.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src/**/*"], + "exclude": [] +} diff --git a/scripts/config/typescript.yaml b/scripts/config/typescript.yaml index d3b2cf9..42587fa 100644 --- a/scripts/config/typescript.yaml +++ b/scripts/config/typescript.yaml @@ -1,5 +1,5 @@ -npmName: "@gemini/sdk" -npmVersion: "1.0.0" +npmName: "@gemini-markets/sdk" +npmVersion: "0.1.0" supportsES6: true withSeparateModelsAndApi: true modelPackage: models