diff --git a/openapi.json b/openapi.json index e72d6192..81e35ef1 100644 --- a/openapi.json +++ b/openapi.json @@ -596,7 +596,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "XLM" }, { "name": "assetB", @@ -604,7 +605,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "USDC" }, { "name": "amount", @@ -707,7 +709,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "XLM" }, { "name": "assetB", @@ -715,7 +718,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "USDC" }, { "name": "window", @@ -938,7 +942,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "XLM" }, { "name": "assetB", @@ -946,7 +951,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "USDC" }, { "name": "interval", @@ -1396,7 +1402,7 @@ }, "assetB": { "type": "string", - "example": "USDC:GBBD47..." + "example": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" } } } @@ -1680,7 +1686,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "XLM" }, { "name": "assetB", @@ -1688,7 +1695,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "USDC" }, { "name": "window", @@ -1781,7 +1789,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "XLM" }, { "name": "assetB", @@ -1789,7 +1798,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "USDC" }, { "name": "window", diff --git a/openapi.yaml b/openapi.yaml index 8eeb42cb..d39c3d76 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -447,11 +447,13 @@ paths: required: true schema: type: string + example: XLM - name: assetB in: path required: true schema: type: string + example: USDC - name: amount in: query required: false @@ -519,11 +521,13 @@ paths: required: true schema: type: string + example: XLM - name: assetB in: path required: true schema: type: string + example: USDC - name: window in: query required: false @@ -659,11 +663,13 @@ paths: required: true schema: type: string + example: XLM - name: assetB in: path required: true schema: type: string + example: USDC - name: interval in: query required: false @@ -982,7 +988,7 @@ paths: example: XLM assetB: type: string - example: USDC:GBBD47... + example: USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 responses: '201': description: Pair added @@ -1175,11 +1181,13 @@ paths: required: true schema: type: string + example: XLM - name: assetB in: path required: true schema: type: string + example: USDC - name: window in: query required: false @@ -1238,11 +1246,13 @@ paths: required: true schema: type: string + example: XLM - name: assetB in: path required: true schema: type: string + example: USDC - name: window in: query required: false @@ -4114,4 +4124,4 @@ components: type: object properties: message: - type: string + type: string \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c3aa959c..4ad4f273 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3560,6 +3560,7 @@ "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, diff --git a/package.json b/package.json index 5e083709..4dcf112e 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "db:drift-check": "bash scripts/db-drift-check.sh", "db:migration-cycle": "bash scripts/db-migration-cycle.sh", "db:migrate:partitioning": "tsx scripts/migrate-price-points-partitioning.ts", - "seed": "tsx scripts/seed.ts" + "seed": "tsx scripts/seed.ts", + "snippets:gen": "tsx scripts/gen-snippets.ts" }, "dependencies": { "@fastify/compress": "^8.3.1", @@ -75,4 +76,4 @@ "typescript": "^5.0.0", "vitest": "^4.1.5" } -} +} \ No newline at end of file diff --git a/scripts/gen-snippets.ts b/scripts/gen-snippets.ts new file mode 100644 index 00000000..ea943e56 --- /dev/null +++ b/scripts/gen-snippets.ts @@ -0,0 +1,238 @@ +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import YAML from 'js-yaml' + +const ROOT = fileURLToPath(new URL('..', import.meta.url)) +const SPEC_PATH = join(ROOT, 'openapi.yaml') + +// Route groups, matched by path prefix in order. Every GET route must match a +// group or appear in SKIPPED, so a new route cannot be dropped silently. +const GROUPS: { name: string; prefixes: string[] }[] = [ + { name: 'price', prefixes: ['/price/', '/candles', '/book', '/prices/', '/pools', '/pairs'] }, + { name: 'analytics', prefixes: ['/volumes', '/spreads', '/compare', '/screener', '/benchmark', '/basket'] }, + { name: 'service', prefixes: ['/status', '/supported', '/discovery', '/usage', '/venues', '/snapshot'] }, +] + +// GET routes deliberately without a snippet, with the reason. +const SKIPPED: Record = { + '/graphql': 'GraphQL query documents are covered in docs/graphql.md', + '/live': 'Orchestrator liveness probe, not a data API call', + '/ready': 'Orchestrator readiness probe, not a data API call', +} + +// Non-GET operations (POST, DELETE) are not generated. They need request bodies, +// admin keys, or signed payment payloads that cannot be runnable from the spec alone. + +interface Param { + name: string + in: string + required?: boolean + example?: unknown + schema?: { default?: unknown; example?: unknown } + $ref?: string +} +interface Operation { + operationId: string + security?: Record[] + parameters?: Param[] +} +interface Spec { + paths: Record> + components: { parameters: Record } +} + +interface Built { + operationId: string + path: string + query: [string, string][] + gated: boolean + apiKey: boolean +} + +export function loadSpec(): Spec { + return YAML.load(readFileSync(SPEC_PATH, 'utf8')) as Spec +} + +function resolveParam(spec: Spec, p: Param): { param: Param; isNetwork: boolean } { + if (p.$ref) { + const key = p.$ref.split('/').pop() as string + const resolved = spec.components.parameters[key] + if (!resolved) throw new Error(`Unknown parameter ref ${p.$ref}`) + return { param: resolved, isNetwork: key === 'NetworkQuery' } + } + return { param: p, isNetwork: false } +} + +function valueFor(param: Param): unknown { + if (param.example !== undefined) return param.example + if (param.schema?.example !== undefined) return param.schema.example + throw new Error( + `Required parameter "${param.name}" has no example in openapi.yaml. Add one to the spec rather than guessing a value here.`, + ) +} + +function build(spec: Spec, path: string, op: Operation): Built { + let resolvedPath = path + const query: [string, string][] = [] + + for (const raw of op.parameters ?? []) { + const { param, isNetwork } = resolveParam(spec, raw) + if (param.in === 'path') { + const v = valueFor(param) + resolvedPath = resolvedPath.replace(`{${param.name}}`, encodeURIComponent(String(v))) + } else if (param.in === 'query' && (param.required || isNetwork)) { + const v = isNetwork ? param.schema?.default : valueFor(param) + const values = Array.isArray(v) ? v : [v] + for (const x of values) query.push([param.name, String(x)]) + } + } + + const security = op.security ?? [] + return { + operationId: op.operationId, + path: resolvedPath, + query, + gated: security.some((s) => 'x402' in s), + apiKey: security.some((s) => 'apiKeyAuth' in s), + } +} + +function collect(spec: Spec): Map { + const byGroup = new Map(GROUPS.map((g) => [g.name, []])) + for (const [path, methods] of Object.entries(spec.paths)) { + const get = methods.get + if (!get) continue + if (path in SKIPPED) continue + const group = GROUPS.find((g) => g.prefixes.some((p) => path.startsWith(p))) + if (!group) { + throw new Error( + `GET ${path} is not in a snippet group or SKIPPED in scripts/gen-snippets.ts`, + ) + } + byGroup.get(group.name)!.push(build(spec, path, get)) + } + return byGroup +} + +const REQUIRE_ENV_TS = `function requireEnv(name: string): string { + const value = process.env[name] + if (!value) throw new Error(\`Set \${name} before running this snippet\`) + return value +}` + +function renderTs(group: string, ops: Built[]): string { + const needsPay = ops.some((o) => o.gated) + const lines: string[] = [ + '// Generated by scripts/gen-snippets.ts from openapi.yaml. Do not edit by hand.', + `// Run: LENS_BASE_URL= npx tsx snippets/typescript/${group}.ts`, + '', + ] + if (needsPay) { + lines.push( + `import { wrapFetchWithPaymentFromConfig } from '@x402/fetch'`, + `import { ExactStellarScheme } from '@x402/stellar/exact/client'`, + `import { createEd25519Signer } from '@x402/stellar'`, + '', + ) + } + lines.push(REQUIRE_ENV_TS, '', `const BASE_URL = requireEnv('LENS_BASE_URL')`) + if (needsPay) { + lines.push( + `// Requires STELLAR_SECRET (testnet). Payments are signed for stellar:testnet.`, + `const signer = createEd25519Signer(requireEnv('STELLAR_SECRET'), 'stellar:testnet')`, + `const payFetch = wrapFetchWithPaymentFromConfig(fetch, {`, + ` schemes: [{ network: 'stellar:*', client: new ExactStellarScheme(signer) }],`, + `})`, + ) + } + + for (const op of ops) { + const fetchFn = op.gated ? 'payFetch' : 'fetch' + lines.push('') + lines.push(`// ${op.operationId}: GET ${op.path}${op.gated ? ' (x402-gated)' : ''}`) + lines.push('{') + lines.push(` const url = new URL(${JSON.stringify(op.path)}, BASE_URL)`) + if (op.query.length > 0) { + lines.push(` url.search = new URLSearchParams(${JSON.stringify(op.query)}).toString()`) + } + if (op.apiKey) { + lines.push(` const headers: Record = { Authorization: 'Bearer ' + requireEnv('LENS_API_KEY') }`) + } else { + lines.push(` const headers: Record = {}`) + } + lines.push(` const res = await ${fetchFn}(url, { headers })`) + lines.push(` console.log(${JSON.stringify(op.operationId)}, res.status, await res.json())`) + lines.push('}') + } + return lines.join('\n') + '\n' +} + +const REQUIRE_ENV_PY = `def require_env(name): + value = os.environ.get(name) + if not value: + sys.exit(f"Set {name} before running this snippet") + return value` + +function renderPy(group: string, ops: Built[]): string { + const needsPay = ops.some((o) => o.gated) + const needsKey = ops.some((o) => o.apiKey) + const lines: string[] = [ + '# Generated by scripts/gen-snippets.ts from openapi.yaml. Do not edit by hand.', + `# Run: LENS_BASE_URL= python snippets/python/${group}.py (needs: pip install requests)`, + '', + 'import os', + 'import sys', + 'import requests', + '', + REQUIRE_ENV_PY, + '', + 'BASE_URL = require_env("LENS_BASE_URL")', + ] + if (needsPay) { + lines.push( + '# Base64 X-PAYMENT header produced by an x402 signer (see @x402/stellar).', + 'PAYMENT = require_env("LENS_X_PAYMENT")', + ) + } + if (needsKey) { + lines.push('API_KEY = require_env("LENS_API_KEY")') + } + + for (const op of ops) { + let headers = '{}' + if (op.gated) headers = '{"X-PAYMENT": PAYMENT}' + else if (op.apiKey) headers = '{"Authorization": "Bearer " + API_KEY}' + lines.push('') + lines.push(`# ${op.operationId}: GET ${op.path}${op.gated ? ' (x402-gated)' : ''}`) + lines.push( + `res = requests.get(BASE_URL + ${JSON.stringify(op.path)}, params=${JSON.stringify(op.query)}, headers=${headers}, timeout=30)`, + ) + lines.push(`print(${JSON.stringify(op.operationId)}, res.status_code, res.json())`) + } + return lines.join('\n') + '\n' +} + +export function renderSnippets(spec: Spec = loadSpec()): Record { + const files: Record = {} + for (const [group, ops] of collect(spec)) { + if (ops.length === 0) continue + files[`snippets/typescript/${group}.ts`] = renderTs(group, ops) + files[`snippets/python/${group}.py`] = renderPy(group, ops) + } + return files +} + +export function writeSnippets(files: Record): void { + for (const [path, content] of Object.entries(files)) { + const full = join(ROOT, path) + mkdirSync(dirname(full), { recursive: true }) + writeFileSync(full, content) + } +} + +// Run only when invoked directly (`npm run snippets:gen`). +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + writeSnippets(renderSnippets()) + console.log('Generated snippets') +} \ No newline at end of file diff --git a/snippets/python/analytics.py b/snippets/python/analytics.py new file mode 100644 index 00000000..59c85213 --- /dev/null +++ b/snippets/python/analytics.py @@ -0,0 +1,38 @@ +# Generated by scripts/gen-snippets.ts from openapi.yaml. Do not edit by hand. +# Run: LENS_BASE_URL= python snippets/python/analytics.py (needs: pip install requests) + +import os +import sys +import requests + +def require_env(name): + value = os.environ.get(name) + if not value: + sys.exit(f"Set {name} before running this snippet") + return value + +BASE_URL = require_env("LENS_BASE_URL") + +# getVolumes: GET /volumes/XLM +res = requests.get(BASE_URL + "/volumes/XLM", params=[["network","testnet"]], headers={}, timeout=30) +print("getVolumes", res.status_code, res.json()) + +# getSpreads: GET /spreads/XLM +res = requests.get(BASE_URL + "/spreads/XLM", params=[["network","testnet"]], headers={}, timeout=30) +print("getSpreads", res.status_code, res.json()) + +# compareAsset: GET /compare/XLM +res = requests.get(BASE_URL + "/compare/XLM", params=[["network","testnet"]], headers={}, timeout=30) +print("compareAsset", res.status_code, res.json()) + +# screenPairs: GET /screener +res = requests.get(BASE_URL + "/screener", params=[["network","testnet"]], headers={}, timeout=30) +print("screenPairs", res.status_code, res.json()) + +# getBenchmark: GET /benchmark/USDC +res = requests.get(BASE_URL + "/benchmark/USDC", params=[], headers={}, timeout=30) +print("getBenchmark", res.status_code, res.json()) + +# getBasket: GET /basket +res = requests.get(BASE_URL + "/basket", params=[["asset","XLM"],["asset","USDC"],["weight","0.6"],["weight","0.4"]], headers={}, timeout=30) +print("getBasket", res.status_code, res.json()) diff --git a/snippets/python/price.py b/snippets/python/price.py new file mode 100644 index 00000000..cd08d1df --- /dev/null +++ b/snippets/python/price.py @@ -0,0 +1,60 @@ +# Generated by scripts/gen-snippets.ts from openapi.yaml. Do not edit by hand. +# Run: LENS_BASE_URL= python snippets/python/price.py (needs: pip install requests) + +import os +import sys +import requests + +def require_env(name): + value = os.environ.get(name) + if not value: + sys.exit(f"Set {name} before running this snippet") + return value + +BASE_URL = require_env("LENS_BASE_URL") +# Base64 X-PAYMENT header produced by an x402 signer (see @x402/stellar). +PAYMENT = require_env("LENS_X_PAYMENT") + +# getPrice: GET /price/XLM/USDC (x402-gated) +res = requests.get(BASE_URL + "/price/XLM/USDC", params=[["network","testnet"]], headers={"X-PAYMENT": PAYMENT}, timeout=30) +print("getPrice", res.status_code, res.json()) + +# getBestRoute: GET /price/XLM/USDC/route (x402-gated) +res = requests.get(BASE_URL + "/price/XLM/USDC/route", params=[["network","testnet"]], headers={"X-PAYMENT": PAYMENT}, timeout=30) +print("getBestRoute", res.status_code, res.json()) + +# getPriceHistory: GET /price/XLM/USDC/history +res = requests.get(BASE_URL + "/price/XLM/USDC/history", params=[["network","testnet"]], headers={}, timeout=30) +print("getPriceHistory", res.status_code, res.json()) + +# getPools: GET /pools (x402-gated) +res = requests.get(BASE_URL + "/pools", params=[["network","testnet"]], headers={"X-PAYMENT": PAYMENT}, timeout=30) +print("getPools", res.status_code, res.json()) + +# getCandles: GET /candles/XLM/USDC (x402-gated) +res = requests.get(BASE_URL + "/candles/XLM/USDC", params=[], headers={"X-PAYMENT": PAYMENT}, timeout=30) +print("getCandles", res.status_code, res.json()) + +# getPriceSnapshots: GET /prices/history (x402-gated) +res = requests.get(BASE_URL + "/prices/history", params=[["pair","XLM/USDC"],["network","testnet"]], headers={"X-PAYMENT": PAYMENT}, timeout=30) +print("getPriceSnapshots", res.status_code, res.json()) + +# listPairs: GET /pairs +res = requests.get(BASE_URL + "/pairs", params=[["network","testnet"]], headers={}, timeout=30) +print("listPairs", res.status_code, res.json()) + +# getDepth: GET /price/XLM/USDC/depth (x402-gated) +res = requests.get(BASE_URL + "/price/XLM/USDC/depth", params=[["network","testnet"]], headers={"X-PAYMENT": PAYMENT}, timeout=30) +print("getDepth", res.status_code, res.json()) + +# getBook: GET /book/XLM/USDC%3AGBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 +res = requests.get(BASE_URL + "/book/XLM/USDC%3AGBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", params=[["network","testnet"]], headers={}, timeout=30) +print("getBook", res.status_code, res.json()) + +# getTwap: GET /price/twap/XLM/USDC (x402-gated) +res = requests.get(BASE_URL + "/price/twap/XLM/USDC", params=[["network","testnet"]], headers={"X-PAYMENT": PAYMENT}, timeout=30) +print("getTwap", res.status_code, res.json()) + +# getVwap: GET /price/vwap/XLM/USDC (x402-gated) +res = requests.get(BASE_URL + "/price/vwap/XLM/USDC", params=[], headers={"X-PAYMENT": PAYMENT}, timeout=30) +print("getVwap", res.status_code, res.json()) diff --git a/snippets/python/service.py b/snippets/python/service.py new file mode 100644 index 00000000..3ed3bea5 --- /dev/null +++ b/snippets/python/service.py @@ -0,0 +1,39 @@ +# Generated by scripts/gen-snippets.ts from openapi.yaml. Do not edit by hand. +# Run: LENS_BASE_URL= python snippets/python/service.py (needs: pip install requests) + +import os +import sys +import requests + +def require_env(name): + value = os.environ.get(name) + if not value: + sys.exit(f"Set {name} before running this snippet") + return value + +BASE_URL = require_env("LENS_BASE_URL") +API_KEY = require_env("LENS_API_KEY") + +# getVenueRegistry: GET /venues/registry +res = requests.get(BASE_URL + "/venues/registry", params=[["network","testnet"]], headers={}, timeout=30) +print("getVenueRegistry", res.status_code, res.json()) + +# getStatus: GET /status +res = requests.get(BASE_URL + "/status", params=[["network","testnet"]], headers={}, timeout=30) +print("getStatus", res.status_code, res.json()) + +# getSnapshot: GET /snapshot +res = requests.get(BASE_URL + "/snapshot", params=[["network","testnet"]], headers={}, timeout=30) +print("getSnapshot", res.status_code, res.json()) + +# listDiscoveryResources: GET /discovery/resources +res = requests.get(BASE_URL + "/discovery/resources", params=[], headers={}, timeout=30) +print("listDiscoveryResources", res.status_code, res.json()) + +# getMyUsage: GET /usage/me +res = requests.get(BASE_URL + "/usage/me", params=[], headers={"Authorization": "Bearer " + API_KEY}, timeout=30) +print("getMyUsage", res.status_code, res.json()) + +# getSupported: GET /supported +res = requests.get(BASE_URL + "/supported", params=[], headers={}, timeout=30) +print("getSupported", res.status_code, res.json()) diff --git a/snippets/typescript/analytics.ts b/snippets/typescript/analytics.ts new file mode 100644 index 00000000..f028444e --- /dev/null +++ b/snippets/typescript/analytics.ts @@ -0,0 +1,63 @@ +// Generated by scripts/gen-snippets.ts from openapi.yaml. Do not edit by hand. +// Run: LENS_BASE_URL= npx tsx snippets/typescript/analytics.ts + +function requireEnv(name: string): string { + const value = process.env[name] + if (!value) throw new Error(`Set ${name} before running this snippet`) + return value +} + +const BASE_URL = requireEnv('LENS_BASE_URL') + +// getVolumes: GET /volumes/XLM +{ + const url = new URL("/volumes/XLM", BASE_URL) + url.search = new URLSearchParams([["network","testnet"]]).toString() + const headers: Record = {} + const res = await fetch(url, { headers }) + console.log("getVolumes", res.status, await res.json()) +} + +// getSpreads: GET /spreads/XLM +{ + const url = new URL("/spreads/XLM", BASE_URL) + url.search = new URLSearchParams([["network","testnet"]]).toString() + const headers: Record = {} + const res = await fetch(url, { headers }) + console.log("getSpreads", res.status, await res.json()) +} + +// compareAsset: GET /compare/XLM +{ + const url = new URL("/compare/XLM", BASE_URL) + url.search = new URLSearchParams([["network","testnet"]]).toString() + const headers: Record = {} + const res = await fetch(url, { headers }) + console.log("compareAsset", res.status, await res.json()) +} + +// screenPairs: GET /screener +{ + const url = new URL("/screener", BASE_URL) + url.search = new URLSearchParams([["network","testnet"]]).toString() + const headers: Record = {} + const res = await fetch(url, { headers }) + console.log("screenPairs", res.status, await res.json()) +} + +// getBenchmark: GET /benchmark/USDC +{ + const url = new URL("/benchmark/USDC", BASE_URL) + const headers: Record = {} + const res = await fetch(url, { headers }) + console.log("getBenchmark", res.status, await res.json()) +} + +// getBasket: GET /basket +{ + const url = new URL("/basket", BASE_URL) + url.search = new URLSearchParams([["asset","XLM"],["asset","USDC"],["weight","0.6"],["weight","0.4"]]).toString() + const headers: Record = {} + const res = await fetch(url, { headers }) + console.log("getBasket", res.status, await res.json()) +} diff --git a/snippets/typescript/price.ts b/snippets/typescript/price.ts new file mode 100644 index 00000000..25749497 --- /dev/null +++ b/snippets/typescript/price.ts @@ -0,0 +1,116 @@ +// Generated by scripts/gen-snippets.ts from openapi.yaml. Do not edit by hand. +// Run: LENS_BASE_URL= npx tsx snippets/typescript/price.ts + +import { wrapFetchWithPaymentFromConfig } from '@x402/fetch' +import { ExactStellarScheme } from '@x402/stellar/exact/client' +import { createEd25519Signer } from '@x402/stellar' + +function requireEnv(name: string): string { + const value = process.env[name] + if (!value) throw new Error(`Set ${name} before running this snippet`) + return value +} + +const BASE_URL = requireEnv('LENS_BASE_URL') +// Requires STELLAR_SECRET (testnet). Payments are signed for stellar:testnet. +const signer = createEd25519Signer(requireEnv('STELLAR_SECRET'), 'stellar:testnet') +const payFetch = wrapFetchWithPaymentFromConfig(fetch, { + schemes: [{ network: 'stellar:*', client: new ExactStellarScheme(signer) }], +}) + +// getPrice: GET /price/XLM/USDC (x402-gated) +{ + const url = new URL("/price/XLM/USDC", BASE_URL) + url.search = new URLSearchParams([["network","testnet"]]).toString() + const headers: Record = {} + const res = await payFetch(url, { headers }) + console.log("getPrice", res.status, await res.json()) +} + +// getBestRoute: GET /price/XLM/USDC/route (x402-gated) +{ + const url = new URL("/price/XLM/USDC/route", BASE_URL) + url.search = new URLSearchParams([["network","testnet"]]).toString() + const headers: Record = {} + const res = await payFetch(url, { headers }) + console.log("getBestRoute", res.status, await res.json()) +} + +// getPriceHistory: GET /price/XLM/USDC/history +{ + const url = new URL("/price/XLM/USDC/history", BASE_URL) + url.search = new URLSearchParams([["network","testnet"]]).toString() + const headers: Record = {} + const res = await fetch(url, { headers }) + console.log("getPriceHistory", res.status, await res.json()) +} + +// getPools: GET /pools (x402-gated) +{ + const url = new URL("/pools", BASE_URL) + url.search = new URLSearchParams([["network","testnet"]]).toString() + const headers: Record = {} + const res = await payFetch(url, { headers }) + console.log("getPools", res.status, await res.json()) +} + +// getCandles: GET /candles/XLM/USDC (x402-gated) +{ + const url = new URL("/candles/XLM/USDC", BASE_URL) + const headers: Record = {} + const res = await payFetch(url, { headers }) + console.log("getCandles", res.status, await res.json()) +} + +// getPriceSnapshots: GET /prices/history (x402-gated) +{ + const url = new URL("/prices/history", BASE_URL) + url.search = new URLSearchParams([["pair","XLM/USDC"],["network","testnet"]]).toString() + const headers: Record = {} + const res = await payFetch(url, { headers }) + console.log("getPriceSnapshots", res.status, await res.json()) +} + +// listPairs: GET /pairs +{ + const url = new URL("/pairs", BASE_URL) + url.search = new URLSearchParams([["network","testnet"]]).toString() + const headers: Record = {} + const res = await fetch(url, { headers }) + console.log("listPairs", res.status, await res.json()) +} + +// getDepth: GET /price/XLM/USDC/depth (x402-gated) +{ + const url = new URL("/price/XLM/USDC/depth", BASE_URL) + url.search = new URLSearchParams([["network","testnet"]]).toString() + const headers: Record = {} + const res = await payFetch(url, { headers }) + console.log("getDepth", res.status, await res.json()) +} + +// getBook: GET /book/XLM/USDC%3AGBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 +{ + const url = new URL("/book/XLM/USDC%3AGBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", BASE_URL) + url.search = new URLSearchParams([["network","testnet"]]).toString() + const headers: Record = {} + const res = await fetch(url, { headers }) + console.log("getBook", res.status, await res.json()) +} + +// getTwap: GET /price/twap/XLM/USDC (x402-gated) +{ + const url = new URL("/price/twap/XLM/USDC", BASE_URL) + url.search = new URLSearchParams([["network","testnet"]]).toString() + const headers: Record = {} + const res = await payFetch(url, { headers }) + console.log("getTwap", res.status, await res.json()) +} + +// getVwap: GET /price/vwap/XLM/USDC (x402-gated) +{ + const url = new URL("/price/vwap/XLM/USDC", BASE_URL) + const headers: Record = {} + const res = await payFetch(url, { headers }) + console.log("getVwap", res.status, await res.json()) +} diff --git a/snippets/typescript/service.ts b/snippets/typescript/service.ts new file mode 100644 index 00000000..2538d20d --- /dev/null +++ b/snippets/typescript/service.ts @@ -0,0 +1,61 @@ +// Generated by scripts/gen-snippets.ts from openapi.yaml. Do not edit by hand. +// Run: LENS_BASE_URL= npx tsx snippets/typescript/service.ts + +function requireEnv(name: string): string { + const value = process.env[name] + if (!value) throw new Error(`Set ${name} before running this snippet`) + return value +} + +const BASE_URL = requireEnv('LENS_BASE_URL') + +// getVenueRegistry: GET /venues/registry +{ + const url = new URL("/venues/registry", BASE_URL) + url.search = new URLSearchParams([["network","testnet"]]).toString() + const headers: Record = {} + const res = await fetch(url, { headers }) + console.log("getVenueRegistry", res.status, await res.json()) +} + +// getStatus: GET /status +{ + const url = new URL("/status", BASE_URL) + url.search = new URLSearchParams([["network","testnet"]]).toString() + const headers: Record = {} + const res = await fetch(url, { headers }) + console.log("getStatus", res.status, await res.json()) +} + +// getSnapshot: GET /snapshot +{ + const url = new URL("/snapshot", BASE_URL) + url.search = new URLSearchParams([["network","testnet"]]).toString() + const headers: Record = {} + const res = await fetch(url, { headers }) + console.log("getSnapshot", res.status, await res.json()) +} + +// listDiscoveryResources: GET /discovery/resources +{ + const url = new URL("/discovery/resources", BASE_URL) + const headers: Record = {} + const res = await fetch(url, { headers }) + console.log("listDiscoveryResources", res.status, await res.json()) +} + +// getMyUsage: GET /usage/me +{ + const url = new URL("/usage/me", BASE_URL) + const headers: Record = { Authorization: 'Bearer ' + requireEnv('LENS_API_KEY') } + const res = await fetch(url, { headers }) + console.log("getMyUsage", res.status, await res.json()) +} + +// getSupported: GET /supported +{ + const url = new URL("/supported", BASE_URL) + const headers: Record = {} + const res = await fetch(url, { headers }) + console.log("getSupported", res.status, await res.json()) +} diff --git a/tests/snippets.test.ts b/tests/snippets.test.ts new file mode 100644 index 00000000..21be93b4 --- /dev/null +++ b/tests/snippets.test.ts @@ -0,0 +1,26 @@ +import { existsSync, readFileSync } from 'node:fs' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' +import { renderSnippets } from '../scripts/gen-snippets' + +const ROOT = fileURLToPath(new URL('..', import.meta.url)) + +describe('generated snippets', () => { + it('render deterministically', () => { + expect(renderSnippets()).toEqual(renderSnippets()) + }) + + it('committed files match the generator', () => { + for (const [path, content] of Object.entries(renderSnippets())) { + expect(existsSync(join(ROOT, path)), `missing ${path}`).toBe(true) + expect(readFileSync(join(ROOT, path), 'utf8'), path).toBe(content) + } + }) + + it('contain no placeholder hosts or addresses', () => { + for (const content of Object.values(renderSnippets())) { + expect(content).not.toMatch(/example\.com|localhost|REPLACE|your-oracle/) + } + }) +})