diff --git a/.github/workflows/snippets.yml b/.github/workflows/snippets.yml index a0b13f1..2b09c8a 100644 --- a/.github/workflows/snippets.yml +++ b/.github/workflows/snippets.yml @@ -69,6 +69,9 @@ jobs: - name: Check snippets run: pnpm run check:snippets + - name: Check integration guides + run: pnpm run check:integration-guides + - name: Check placeholders run: pnpm run check:placeholders diff --git a/docs.json b/docs.json index b367b72..a12be78 100644 --- a/docs.json +++ b/docs.json @@ -162,7 +162,10 @@ "guides/integrations/phoenix", "guides/integrations/react-native", "guides/integrations/reflector", - "guides/integrations/soroswap" + "guides/integrations/solid", + "guides/integrations/soroswap", + "guides/integrations/svelte", + "guides/integrations/vue" ] }, { diff --git a/guides/integrations/_meta.json b/guides/integrations/_meta.json index 665da62..bb784fb 100644 --- a/guides/integrations/_meta.json +++ b/guides/integrations/_meta.json @@ -6,5 +6,8 @@ "phoenix": "Phoenix DEX", "react-native": "React Native", "reflector": "Reflector Oracle", - "soroswap": "Soroswap" + "solid": "Solid", + "soroswap": "Soroswap", + "svelte": "Svelte", + "vue": "Vue" } diff --git a/guides/integrations/solid.mdx b/guides/integrations/solid.mdx new file mode 100644 index 0000000..1173615 --- /dev/null +++ b/guides/integrations/solid.mdx @@ -0,0 +1,209 @@ +--- +title: "Solid Integration" +description: "Receive and scan stealth payments in Solid and SolidStart with @wraith-protocol/sdk" +--- + +The Wraith SDK is framework-agnostic. This guide wraps it in Solid signals to poll for incoming stealth payments, with proper teardown, request cancellation, and SolidStart SSR handling. + +The flow is the same everywhere: connect a `WraithAgent`, call `scanPayments()` on an interval, and render the returned `Payment[]`. Getting the lifecycle right matters, because a leaked interval keeps scanning after a component unmounts, and a stale scan can overwrite fresher results. + +## Installation + +```bash +npm install @wraith-protocol/sdk +``` + +## 1. Create the receiver + +Put the framework-agnostic logic in a plain module so it is easy to unit test. `scan()` aborts any request that is still in flight before starting a new one, and `stop()` clears the interval and aborts the active request. + +```ts +import { Wraith, type Payment, type WraithAgent } from "@wraith-protocol/sdk"; + +export function createWraithClient(apiKey: string): Wraith { + return new Wraith({ apiKey }); +} + +export function connectAgent(client: Wraith, wallet: string): Promise { + return client.getAgentByWallet(wallet); +} + +export interface ReceiveOptions { + intervalMs?: number; + onPayments: (payments: Payment[]) => void; + onError?: (error: Error) => void; +} + +export class PaymentReceiver { + private readonly agent: WraithAgent; + private readonly options: ReceiveOptions; + private controller: AbortController | null = null; + private timer: ReturnType | null = null; + + constructor(agent: WraithAgent, options: ReceiveOptions) { + this.agent = agent; + this.options = options; + } + + async scan(): Promise { + this.controller?.abort(); + const controller = new AbortController(); + this.controller = controller; + + try { + const payments = await this.agent.scanPayments(); + if (controller.signal.aborted) return []; + this.options.onPayments(payments); + return payments; + } catch (error) { + if (controller.signal.aborted) return []; + this.options.onError?.(error as Error); + return []; + } finally { + if (this.controller === controller) this.controller = null; + } + } + + start(intervalMs = this.options.intervalMs ?? 30_000): void { + this.stop(); + void this.scan(); + this.timer = setInterval(() => void this.scan(), intervalMs); + } + + stop(): void { + if (this.timer !== null) { + clearInterval(this.timer); + this.timer = null; + } + this.controller?.abort(); + this.controller = null; + } +} +``` + +`scanPayments()` is not cancellable at the network layer, so the receiver uses an `AbortController` as a guard: when a scan is superseded or the component unmounts, its result is discarded instead of being applied. This is what stops a slow request from clobbering newer state. + +## 2. Wrap it in a signal + +`onMount` runs only in the browser, so the receiver is never started during SSR. `onCleanup` runs when the owning scope is disposed, which stops the receiver and aborts any in-flight scan. Returning `stop` means the same code runs whether the component unmounts normally or Solid tears the scope down early. + +```ts no-check +import { createSignal, onCleanup, onMount, type Accessor } from "solid-js"; +import { createWraithClient, PaymentReceiver } from "./wraith"; +import type { Payment } from "@wraith-protocol/sdk"; + +export function createPaymentReceiver( + apiKey: string, + agentId: string, +): { + payments: Accessor; + error: Accessor; + scanning: Accessor; + refresh: () => Promise; +} { + const [payments, setPayments] = createSignal([]); + const [error, setError] = createSignal(null); + const [scanning, setScanning] = createSignal(false); + let receiver: PaymentReceiver | null = null; + + onMount(() => { + const agent = createWraithClient(apiKey).agent(agentId); + receiver = new PaymentReceiver(agent, { + intervalMs: 30_000, + onPayments: (incoming) => { + setPayments(incoming); + setError(null); + }, + onError: (err) => setError(err.message), + }); + receiver.start(); + }); + + onCleanup(() => { + receiver?.stop(); + receiver = null; + }); + + const refresh = async () => { + if (!receiver) return; + setScanning(true); + try { + await receiver.scan(); + } finally { + setScanning(false); + } + }; + + return { payments, error, scanning, refresh }; +} +``` + +## 3. Render the signal in a component + +```tsx no-check +import { For, Show } from "solid-js"; +import { createPaymentReceiver } from "./wraith-receiver"; + +export function PaymentList(props: { apiKey: string; agentId: string }) { + const { payments, error, scanning, refresh } = + createPaymentReceiver(props.apiKey, props.agentId); + + return ( +
+ + + + {(message) =>

{message()}

} +
+ +
    + + {(payment) => ( +
  • + {payment.stealthAddress} — {payment.balance} +
  • + )} +
    +
+
+ ); +} +``` + +## SSR and hydration + +SolidStart renders components on the server. Two rules keep the composable safe: + +- **Never touch the client during setup.** Instantiate the `Wraith` client inside `onMount`, which Solid does not call on the server. If you create a client outside a component, guard it with `isServer` from `solid-js`. +- **Start from a deterministic initial state.** `payments` starts as an empty array, so server markup and the first client render match and Solid does not warn about a hydration mismatch. + +```ts no-check +import { isServer } from "solid-js"; +import { createWraithClient } from "./wraith"; + +export const wraith = isServer + ? null + : createWraithClient(import.meta.env.VITE_WRAITH_API_KEY); +``` + +## Full example + +The composable and component above are the complete flow. Use one receiver per agent, then read `payments()` wherever the data is needed. + +## Receive a payment + +To fund the agent so it has something to scan, send a stealth payment to its meta-address. The agent's address for a chain is on `agent.info`. + +```ts +import { Chain, Wraith } from "@wraith-protocol/sdk"; + +export async function showMetaAddress(apiKey: string): Promise { + const wraith = new Wraith({ apiKey }); + const agent = await wraith.getAgentByName("alice"); + return agent.info.metaAddresses[Chain.Horizen]; +} +``` + +Anyone can now send to that meta-address, and the receiver in your Solid component picks it up on the next scan. diff --git a/guides/integrations/svelte.mdx b/guides/integrations/svelte.mdx new file mode 100644 index 0000000..2d918ed --- /dev/null +++ b/guides/integrations/svelte.mdx @@ -0,0 +1,239 @@ +--- +title: "Svelte Integration" +description: "Receive and scan stealth payments in Svelte and SvelteKit with @wraith-protocol/sdk" +--- + +The Wraith SDK is framework-agnostic. This guide wraps it in Svelte 5 runes to poll for incoming stealth payments, with proper teardown, request cancellation, and SvelteKit SSR handling. + +The flow is the same everywhere: connect a `WraithAgent`, call `scanPayments()` on an interval, and render the returned `Payment[]`. Getting the lifecycle right matters, because a leaked interval keeps scanning after a component unmounts, and a stale scan can overwrite fresher results. + +## Installation + +```bash +npm install @wraith-protocol/sdk +``` + +## 1. Create the receiver + +Put the framework-agnostic logic in a plain module so it is easy to unit test. `scan()` aborts any request that is still in flight before starting a new one, and `stop()` clears the interval and aborts the active request. + +```ts +import { Wraith, type Payment, type WraithAgent } from "@wraith-protocol/sdk"; + +export function createWraithClient(apiKey: string): Wraith { + return new Wraith({ apiKey }); +} + +export function connectAgent(client: Wraith, wallet: string): Promise { + return client.getAgentByWallet(wallet); +} + +export interface ReceiveOptions { + intervalMs?: number; + onPayments: (payments: Payment[]) => void; + onError?: (error: Error) => void; +} + +export class PaymentReceiver { + private readonly agent: WraithAgent; + private readonly options: ReceiveOptions; + private controller: AbortController | null = null; + private timer: ReturnType | null = null; + + constructor(agent: WraithAgent, options: ReceiveOptions) { + this.agent = agent; + this.options = options; + } + + async scan(): Promise { + this.controller?.abort(); + const controller = new AbortController(); + this.controller = controller; + + try { + const payments = await this.agent.scanPayments(); + if (controller.signal.aborted) return []; + this.options.onPayments(payments); + return payments; + } catch (error) { + if (controller.signal.aborted) return []; + this.options.onError?.(error as Error); + return []; + } finally { + if (this.controller === controller) this.controller = null; + } + } + + start(intervalMs = this.options.intervalMs ?? 30_000): void { + this.stop(); + void this.scan(); + this.timer = setInterval(() => void this.scan(), intervalMs); + } + + stop(): void { + if (this.timer !== null) { + clearInterval(this.timer); + this.timer = null; + } + this.controller?.abort(); + this.controller = null; + } +} +``` + +`scanPayments()` is not cancellable at the network layer, so the receiver uses an `AbortController` as a guard: when a scan is superseded or the component unmounts, its result is discarded instead of being applied. This is what stops a slow request from clobbering newer state. + +## 2. Wrap it in a rune + +Runes only work inside `.svelte` files. An `$effect` can return a teardown function, which Svelte calls on unmount and before re-running the effect. Stopping the receiver there covers both cases. + +```svelte + +``` + +## 3. Manual refresh + +Trigger an on-demand scan with the same receiver. Because `scan()` aborts the previous request, hammering the button cannot stack duplicate scans. + +```svelte + +``` + +## SSR and hydration + +SvelteKit renders components on the server by default. Two rules keep the receiver safe: + +- **Instantiate the client in the browser only.** Guard the effect with `if (!browser) return;` from `$app/environment`. `$effect` already skips the server, but the guard documents the intent and protects any code you move into component setup. +- **Start from a deterministic initial state.** Initialise `payments` to `[]` and only fill it after mount. Server-rendered markup and the first client render must match, or Svelte warns about a hydration mismatch. + +For a plain Vite + Svelte app (no SSR), the `browser` guard is unnecessary, but the `$effect` teardown is still required. + +## Full example + +```svelte + + +
+ + + {#if error} +

{error}

+ {/if} + +
    + {#each payments as payment (payment.stealthAddress)} +
  • + {payment.stealthAddress} — {payment.balance} +
  • + {/each} +
+
+``` + +## Receive a payment + +To fund the agent so it has something to scan, send a stealth payment to its meta-address. The agent's address for a chain is on `agent.info`. + +```ts +import { Chain, Wraith } from "@wraith-protocol/sdk"; + +export async function showMetaAddress(apiKey: string): Promise { + const wraith = new Wraith({ apiKey }); + const agent = await wraith.getAgentByName("alice"); + return agent.info.metaAddresses[Chain.Horizen]; +} +``` + +Anyone can now send to that meta-address, and the receiver in your Svelte component picks it up on the next scan. diff --git a/guides/integrations/vue.mdx b/guides/integrations/vue.mdx new file mode 100644 index 0000000..21c4da4 --- /dev/null +++ b/guides/integrations/vue.mdx @@ -0,0 +1,198 @@ +--- +title: "Vue Integration" +description: "Receive and scan stealth payments in Vue 3 with @wraith-protocol/sdk" +--- + +The Wraith SDK is framework-agnostic. This guide wraps it in a Vue 3 composable to poll for incoming stealth payments, with proper teardown, request cancellation, and SSR handling for Nuxt and plain Vite. + +The flow is the same everywhere: connect a `WraithAgent`, call `scanPayments()` on an interval, and render the returned `Payment[]`. Getting the lifecycle right matters, because a leaked interval keeps scanning after a component unmounts, and a stale scan can overwrite fresher results. + +## Installation + +```bash +npm install @wraith-protocol/sdk +``` + +## 1. Create the receiver + +Put the framework-agnostic logic in a plain module so it is easy to unit test. `scan()` aborts any request that is still in flight before starting a new one, and `stop()` clears the interval and aborts the active request. + +```ts +import { Wraith, type Payment, type WraithAgent } from "@wraith-protocol/sdk"; + +export function createWraithClient(apiKey: string): Wraith { + return new Wraith({ apiKey }); +} + +export function connectAgent(client: Wraith, wallet: string): Promise { + return client.getAgentByWallet(wallet); +} + +export interface ReceiveOptions { + intervalMs?: number; + onPayments: (payments: Payment[]) => void; + onError?: (error: Error) => void; +} + +export class PaymentReceiver { + private readonly agent: WraithAgent; + private readonly options: ReceiveOptions; + private controller: AbortController | null = null; + private timer: ReturnType | null = null; + + constructor(agent: WraithAgent, options: ReceiveOptions) { + this.agent = agent; + this.options = options; + } + + async scan(): Promise { + this.controller?.abort(); + const controller = new AbortController(); + this.controller = controller; + + try { + const payments = await this.agent.scanPayments(); + if (controller.signal.aborted) return []; + this.options.onPayments(payments); + return payments; + } catch (error) { + if (controller.signal.aborted) return []; + this.options.onError?.(error as Error); + return []; + } finally { + if (this.controller === controller) this.controller = null; + } + } + + start(intervalMs = this.options.intervalMs ?? 30_000): void { + this.stop(); + void this.scan(); + this.timer = setInterval(() => void this.scan(), intervalMs); + } + + stop(): void { + if (this.timer !== null) { + clearInterval(this.timer); + this.timer = null; + } + this.controller?.abort(); + this.controller = null; + } +} +``` + +`scanPayments()` is not cancellable at the network layer, so the receiver uses an `AbortController` as a guard: when a scan is superseded or the component unmounts, its result is discarded instead of being applied. This is what stops a slow request from clobbering newer state. + +## 2. Wrap it in a composable + +`onMounted` runs only in the browser, so the receiver is never started during SSR. `onUnmounted` stops it, which aborts any in-flight scan and clears the polling interval. + +```ts no-check +import { onMounted, onUnmounted, ref, shallowRef } from "vue"; +import { createWraithClient, PaymentReceiver } from "./wraith"; +import type { Payment } from "@wraith-protocol/sdk"; + +export function usePaymentReceiver(apiKey: string, agentId: string) { + const payments = ref([]); + const error = ref(null); + const scanning = ref(false); + const receiver = shallowRef(null); + + onMounted(() => { + const agent = createWraithClient(apiKey).agent(agentId); + const instance = new PaymentReceiver(agent, { + intervalMs: 30_000, + onPayments: (incoming) => { + payments.value = incoming; + error.value = null; + }, + onError: (err) => { + error.value = err.message; + }, + }); + + receiver.value = instance; + instance.start(); + }); + + onUnmounted(() => { + receiver.value?.stop(); + receiver.value = null; + }); + + async function refresh() { + if (!receiver.value) return; + scanning.value = true; + try { + await receiver.value.scan(); + } finally { + scanning.value = false; + } + } + + return { payments, error, scanning, refresh }; +} +``` + +## 3. Use it in a component + +```vue + + + +``` + +## SSR and hydration + +Vue 3 and Nuxt render components on the server. Two rules keep the composable safe: + +- **Never touch the client during setup.** Instantiate the `Wraith` client inside `onMounted`, which Vue does not call on the server. If you also create a top-level client for other features, guard it with Nuxt's `import.meta.client` (or `import.meta.env.SSR` in plain Vite). +- **Start from a deterministic initial state.** `payments` starts as an empty ref, so server markup and the first client render match and Vue does not warn about a hydration mismatch. + +```ts no-check +const wraith = import.meta.client + ? createWraithClient(useRuntimeConfig().public.wraithApiKey) + : null; +``` + +## Receive a payment + +To fund the agent so it has something to scan, send a stealth payment to its meta-address. The agent's address for a chain is on `agent.info`. + +```ts +import { Chain, Wraith } from "@wraith-protocol/sdk"; + +export async function showMetaAddress(apiKey: string): Promise { + const wraith = new Wraith({ apiKey }); + const agent = await wraith.getAgentByName("alice"); + return agent.info.metaAddresses[Chain.Horizen]; +} +``` + +Anyone can now send to that meta-address, and the composable picks it up on the next scan. diff --git a/package.json b/package.json index d34b957..61f971d 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "check:placeholders": "tsx scripts/check-placeholders.ts", "check:snippets": "tsx scripts/check-snippets.ts && tsx scripts/check-api-examples.ts", "check:api-examples": "tsx scripts/check-api-examples.ts", + "check:integration-guides": "tsx scripts/check-integration-guides.ts", "check:snippets-extended": "tsx scripts/check-snippets-extended.ts && npm run check:python-quickstart", "check:python-quickstart": "tsx scripts/python-quickstart/generate-vectors.ts --check && python3 scripts/python-quickstart/run_quickstart.py --bootstrap", "generate:python-quickstart-vectors": "tsx scripts/python-quickstart/generate-vectors.ts", @@ -24,7 +25,7 @@ "test:playground": "playwright test --config scripts/playground/tests/playwright.config.ts", "mint:validate": "mint validate", "mint:broken-links": "mint broken-links", - "test": "npm run check:snippets && npm run check:nav-coverage && npm run check:contract-registry && npm run check:compatibility-matrix && npm run test:redirects-and-anchors" + "test": "npm run check:snippets && npm run check:integration-guides && npm run check:nav-coverage && npm run check:contract-registry && npm run check:compatibility-matrix && npm run test:redirects-and-anchors" }, "dependencies": { "@solana/web3.js": "^1.95.0", diff --git a/scripts/check-integration-guides.ts b/scripts/check-integration-guides.ts new file mode 100644 index 0000000..87f1ca3 --- /dev/null +++ b/scripts/check-integration-guides.ts @@ -0,0 +1,208 @@ +/** + * check-integration-guides.ts + * + * Type-checks the framework integration guides (Svelte, Vue, Solid) against the + * installed `@wraith-protocol/sdk`. The generic snippet checker + * (scripts/check-snippets.ts) wraps every fence in `// @ts-nocheck`, so it only + * catches syntax errors. These guides document the receive/scan flow call by + * call, so a drifting SDK signature should fail the build, not ship. + * + * Every `ts`/`typescript` fence in a guide is compiled unless it is marked + * `no-check` (framework glue that imports packages the docs repo does not + * install). Each fence becomes its own module, so fences may repeat imports. + * + * CI installs the published `@wraith-protocol/sdk` tarball before running the + * Snippet checks workflow, so this validates the guides against the exact + * package developers will install. + * + * Usage + * pnpm run check:integration-guides + */ + +import { spawn } from "node:child_process"; +import { mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import process from "node:process"; + +const repoRoot = process.cwd(); + +const guides = [ + "guides/integrations/svelte.mdx", + "guides/integrations/vue.mdx", + "guides/integrations/solid.mdx", +]; + +const checkableLanguages = new Set(["ts", "typescript"]); + +type Fence = { + lang: string; + attrs: string; + code: string; + /** 1-based MDX line of the opening fence. */ + line: number; +}; + +type Module = { + name: string; + /** Repo-relative guide this module was generated from. */ + file: string; + /** 1-based MDX line of the fence's opening delimiter. */ + fenceLine: number; + source: string; + /** Number of source lines inside the fence. */ + lines: number; +}; + +async function main() { + const modules: Module[] = []; + const errors: string[] = []; + + for (const [fileIndex, file] of guides.entries()) { + const markdown = await readFile(path.join(repoRoot, file), "utf8"); + const fences = parseFences(markdown).filter( + (fence) => checkableLanguages.has(fence.lang) && !/\bno-check\b/.test(fence.attrs), + ); + + if (fences.length === 0) { + errors.push( + `${file}: no checked TypeScript fences. Add the SDK-facing code without a \`no-check\` marker.`, + ); + continue; + } + + fences.forEach((fence, fenceIndex) => { + const name = `guide-${fileIndex}-${fenceIndex}.ts`; + const body = fence.code.replace(/\s+$/, ""); + modules.push({ + name, + file, + fenceLine: fence.line, + source: `// ${file}:${fence.line}\n${body}\nexport {};\n`, + lines: body.split("\n").length, + }); + }); + } + + if (errors.length > 0) fail(errors); + + const tmp = await mkdtemp(path.join(tmpdir(), "wraith-integration-guides-")); + try { + await writeFile(path.join(tmp, "package.json"), JSON.stringify({ type: "module" }), "utf8"); + await symlink(path.join(repoRoot, "node_modules"), path.join(tmp, "node_modules"), "dir"); + await writeFile( + path.join(tmp, "tsconfig.json"), + JSON.stringify(createTsConfig(modules.map((module) => module.name)), null, 2), + "utf8", + ); + for (const module of modules) { + await writeFile(path.join(tmp, module.name), module.source, "utf8"); + } + + const tsc = path.join(repoRoot, "node_modules", "typescript", "bin", "tsc"); + const result = await run( + process.execPath, + [tsc, "--project", "tsconfig.json", "--pretty", "false"], + tmp, + ); + + if (result.exitCode !== 0) { + fail([`Type check failed:\n\n${mapLocations(result.output.trim(), modules)}`]); + } + } finally { + await rm(tmp, { force: true, recursive: true }); + } + + console.log( + [ + `Guides checked : ${guides.length}`, + `Fences compiled: ${modules.length}`, + `SDK : @wraith-protocol/sdk`, + "", + "Integration guide check passed.", + ].join("\n"), + ); +} + +/** Collects fenced code blocks. Mirrors scripts/check-api-examples.ts. */ +function parseFences(markdown: string): Fence[] { + const fences: Fence[] = []; + const lines = markdown.split("\n"); + let open: { lang: string; attrs: string; line: number; body: string[] } | null = null; + + lines.forEach((line, index) => { + if (open === null) { + const match = /^```([A-Za-z0-9_-]*)([^\n]*)$/.exec(line); + if (match) { + open = { lang: match[1].toLowerCase(), attrs: match[2] ?? "", line: index + 1, body: [] }; + } + return; + } + if (/^```\s*$/.test(line)) { + fences.push({ lang: open.lang, attrs: open.attrs, code: open.body.join("\n"), line: open.line }); + open = null; + return; + } + open.body.push(line); + }); + + return fences; +} + +function createTsConfig(files: string[]) { + return { + compilerOptions: { + target: "ES2022", + module: "NodeNext", + moduleResolution: "NodeNext", + lib: ["ES2022", "DOM"], + types: ["node"], + strict: true, + skipLibCheck: true, + esModuleInterop: true, + noEmit: true, + }, + files, + }; +} + +/** Rewrites `guide-0-1.ts(12,5)` to the MDX location that produced the line. */ +function mapLocations(text: string, modules: Module[]) { + const byName = new Map(modules.map((module) => [module.name, module])); + + return text.replace(/(guide-\d+-\d+\.ts)\((\d+),(\d+)\)/g, (_, name: string, line, column) => { + const module = byName.get(name); + if (!module) return `${name}(${line},${column})`; + const generated = Number(line); + // Generated line 1 is the source-mapping comment; source line 1 is fenceLine + 1. + const mdxLine = module.fenceLine + (generated - 1); + return `${module.file}:${mdxLine}:${column}`; + }); +} + +function run(command: string, args: string[], cwd: string) { + return new Promise<{ exitCode: number; output: string }>((resolve) => { + const child = spawn(command, args, { cwd, env: process.env, shell: false }); + let output = ""; + + child.stdout.on("data", (chunk) => { + output += chunk.toString(); + }); + child.stderr.on("data", (chunk) => { + output += chunk.toString(); + }); + child.on("close", (exitCode) => { + resolve({ exitCode: exitCode ?? 1, output }); + }); + }); +} + +function fail(errors: string[]): never { + console.error(`Integration guide check failed:\n\n${errors.join("\n\n")}`); + process.exit(1); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +});