diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e92f9b..f57d5f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,20 +22,30 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 with: standalone: true env: - # The repo .npmrc sets ignore-scripts=true (supply-chain hardening), but the - # standalone pnpm (@pnpm/exe) needs its preinstall (setup.js picks the platform - # binary) or the pnpm bin is a stub that fails with "This: not found". Re-enable - # scripts for this bootstrap only; the project installs below still honor .npmrc. + # The standalone pnpm (@pnpm/exe) needs its preinstall (setup.js picks the platform + # binary) or the pnpm bin is a stub that fails with "This: not found". The action + # installs it with `npm ci` in its own directory and lockfile, where the repo .npmrc + # (ignore-scripts=true) is probably not read; this step-level override makes sure + # scripts run for that bootstrap whatever npm config applies. It is scoped to this + # step: the project installs below still honor the repo .npmrc. npm_config_ignore_scripts: 'false' - - uses: actions/setup-node@v4 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 cache: 'pnpm' + # The top CHANGELOG.md heading must be exactly `## ` (publish.yml takes + # the GitHub Release notes from that line). Into release/** a `## (unreleased)` + # heading is accepted too, so a heading still marked "(unreleased)" can never reach main. + # The target is the PR's base branch, or the pushed branch (main) when publish.yml calls this. + - name: Check the CHANGELOG.md heading + env: + TARGET_BRANCH: ${{ github.base_ref || github.ref_name }} + run: node scripts/check-changelog-heading.mjs - run: pnpm install --frozen-lockfile - run: pnpm run lint # Enforce the coverage thresholds (vitest.config.ts), not just that tests pass. @@ -76,21 +86,23 @@ jobs: - run: pnpm dlx publint - run: pnpm dlx @arethetypeswrong/cli --pack . - # Prove the package's advertised floor (engines: ">=18"). Vitest 5 needs Node >=22.12, so this - # job does not run the suite — it builds, smoke-imports the CJS entry and runs a plain-Node - # runtime smoke (a real AbortSignal.timeout() abort -> GlassnodeNetworkError timedOut) on Node 18. - compat-node18: + # Prove the package's advertised floor (engines: ">=22.0.0"). The main job runs on Node 24, and + # Vitest 5 needs Node >=22.12, so this job does not run the suite — it builds, smoke-imports the + # CJS entry and runs a plain-Node runtime smoke (a real AbortSignal.timeout() abort -> + # GlassnodeNetworkError timedOut) on the floor major. The job name is version-neutral so the + # rulesets' required check survives the next floor bump; only node-version changes then. + compat-node-floor: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 with: standalone: true env: npm_config_ignore_scripts: 'false' - - uses: actions/setup-node@v4 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 18 + node-version: 22 cache: 'pnpm' - run: pnpm install --frozen-lockfile - run: pnpm run build diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c4c3ddc..8c4d1bc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,24 +19,26 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 with: standalone: true env: - # The repo .npmrc sets ignore-scripts=true (supply-chain hardening), but the - # standalone pnpm (@pnpm/exe) needs its preinstall (setup.js picks the platform - # binary) or the pnpm bin is a stub that fails with "This: not found". Re-enable - # scripts for this bootstrap only; the project installs below still honor .npmrc. + # The standalone pnpm (@pnpm/exe) needs its preinstall (setup.js picks the platform + # binary) or the pnpm bin is a stub that fails with "This: not found". The action + # installs it with `npm ci` in its own directory and lockfile, where the repo .npmrc + # (ignore-scripts=true) is probably not read; this step-level override makes sure + # scripts run for that bootstrap whatever npm config applies. It is scoped to this + # step: the project installs below still honor the repo .npmrc. npm_config_ignore_scripts: 'false' - - uses: actions/setup-node@v4 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 cache: 'pnpm' - run: pnpm install --frozen-lockfile - run: pnpm run docs - - uses: actions/configure-pages@v6 - - uses: actions/upload-pages-artifact@v5 + - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: api-docs @@ -51,4 +53,4 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - id: deployment - uses: actions/deploy-pages@v5 + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6742f4e..77c2f6d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,6 +9,20 @@ name: Publish Package # GitHub Release from the CHANGELOG section # A merge that does not bump `version`, or a re-run after a successful release, skips the # publish (and the approval) with a notice in the job summary. +# +# Concurrency (the `npm-publish` group on the publish job): an in-progress publish is never +# cancelled, but GitHub keeps at most ONE pending run per group. A newer merge's publish job +# replaces (cancels) an older one still waiting in the queue or for its approval, so that older +# version is never published. This happened on 2026-09-29 to 0.29.5 and 0.30.0 (#33). Hence: +# - Merge release PRs one at a time: wait for each publish to finish before the next merge. +# - The `release` job warns in the summary about any CHANGELOG version between the last +# published one and this one that is missing on npm (scripts/release-plan.mjs). +# - To recover, re-run the cancelled version's workflow run (its commit still carries that +# version). If a higher version is already `latest`, it is published under the +# `backport-.` dist-tag instead, so `latest` never moves backwards; a +# prerelease goes out under `next`. Its GitHub Release is then not marked "Latest". +# A re-run uses the workflow of its own commit: one older than this guard publishes with npm's +# default tag, so check `npm dist-tag ls` afterwards (CONTRIBUTING.md "Releases"). on: push: branches: @@ -31,12 +45,13 @@ jobs: action: ${{ steps.state.outputs.action }} version: ${{ steps.state.outputs.version }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 # Fails the job (rather than publishing) if npm cannot be queried; only a 404 means - # "not published". + # "not published". Also picks the dist-tag and warns about skipped (never published) + # CHANGELOG versions in the job summary. - id: state run: scripts/release-state.sh @@ -57,37 +72,39 @@ jobs: contents: write # push the v tag and create the GitHub Release id-token: write # npm Trusted Publishing (OIDC) and provenance steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 with: standalone: true env: - # The repo .npmrc sets ignore-scripts=true (supply-chain hardening), but the - # standalone pnpm (@pnpm/exe) needs its preinstall (setup.js picks the platform - # binary) or the pnpm bin is a stub that fails with "This: not found". Re-enable - # scripts for this bootstrap only; the project installs below still honor .npmrc. + # The standalone pnpm (@pnpm/exe) needs its preinstall (setup.js picks the platform + # binary) or the pnpm bin is a stub that fails with "This: not found". The action + # installs it with `npm ci` in its own directory and lockfile, where the repo .npmrc + # (ignore-scripts=true) is probably not read; this step-level override makes sure + # scripts run for that bootstrap whatever npm config applies. It is scoped to this + # step: the project installs below still honor the repo .npmrc. npm_config_ignore_scripts: 'false' - - uses: actions/setup-node@v4 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 - # registry-url is required so npm knows the registry for the OIDC exchange. - # npm CLI (>= 11.5.1) detects the OIDC environment and authenticates with it - # BEFORE falling back to the setup-node placeholder token, so no NODE_AUTH_TOKEN - # is set on the publish step. (pnpm publish does NOT do this — it uses the - # placeholder and 404s — which is why we publish with npm below.) + # registry-url is required so npm knows the registry for the OIDC exchange. No cache + # here: this job holds id-token: write and contents: write, and setup-node's docs advise + # against restoring a cache in privileged release jobs (cache poisoning). registry-url: 'https://registry.npmjs.org' - cache: 'pnpm' - # setup-node's registry-url writes an .npmrc with `always-auth=true` and a placeholder - # `_authToken`, which forces npm to send the fake token instead of doing the OIDC - # exchange (→ E404). Overwrite it with just the registry so npm has no token and must - # authenticate via OIDC trusted publishing. - - name: Clean npmrc so npm uses OIDC (not the placeholder token) + # With registry-url, setup-node v7 writes a user .npmrc (NPM_CONFIG_USERCONFIG) containing + # `//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}` and `registry=…`, and no longer + # exports a placeholder NODE_AUTH_TOKEN (v4–v6 did). We set no NODE_AUTH_TOKEN: npm must + # authenticate via OIDC trusted publishing, not a configured token. The `_authToken` line + # still names a token (an unset variable) for the registry, which can pre-empt or break the + # OIDC path, so overwrite the file with just the registry. We publish with npm (>= 11.5.1 + # does the OIDC exchange), not pnpm publish, which does not and 404s. + - name: Clean npmrc so npm uses OIDC (no configured token) run: printf 'registry=https://registry.npmjs.org/\n' > "$NPM_CONFIG_USERCONFIG" # npm >= 11.5.1 performs the OIDC exchange. - run: npm install -g npm@latest - run: npm --version # Re-check right before publishing: the approval may have waited while another run - # published this version. + # published this version, or moved `latest` (which decides the dist-tag). - id: state run: scripts/release-state.sh - if: steps.state.outputs.action == 'publish' @@ -103,12 +120,18 @@ jobs: # provenance (publishConfig.provenance). Requires a Trusted Publisher configured for this # package on npmjs.com: repo glassnode/glassnode-api-ts-client, workflow publish.yml, # environment npm. + # The dist-tag is always explicit: `latest`, or `backport-.` for a version + # lower than the current `latest` (a re-run of a cancelled older release) and `next` for a + # prerelease, so `latest` never moves backwards (scripts/release-plan.mjs). - if: steps.state.outputs.action == 'publish' - run: npm publish + env: + DIST_TAG: ${{ steps.state.outputs.dist_tag }} + run: npm publish --tag "$DIST_TAG" - if: steps.state.outputs.action == 'publish' env: VERSION: ${{ steps.state.outputs.version }} - run: echo "Published \`glassnode-api@$VERSION\` to npm." >> "$GITHUB_STEP_SUMMARY" + DIST_TAG: ${{ steps.state.outputs.dist_tag }} + run: echo "Published \`glassnode-api@$VERSION\` to npm under the \`$DIST_TAG\` dist-tag." >> "$GITHUB_STEP_SUMMARY" # Tag the published commit. An existing tag on this commit is fine (re-run); one on another # commit is left alone with a warning, and the GitHub Release is then skipped. - name: Push the v tag @@ -145,13 +168,16 @@ jobs: exit 1 fi # The release notes are the package.json version's section of CHANGELOG.md (from its - # `## ` heading to the next `## ` heading). + # `## ` heading to the next `## ` heading). Only a version published as npm + # `latest` becomes the repository's "Latest" GitHub Release; a backport does not, and a + # prerelease is marked as one. - name: Create the GitHub Release if: steps.tag.outputs.release == 'true' env: GH_TOKEN: ${{ github.token }} TAG: ${{ steps.state.outputs.tag }} VERSION: ${{ steps.state.outputs.version }} + DIST_TAG: ${{ steps.state.outputs.dist_tag }} run: | if gh release view "$TAG" > /dev/null 2> view.err; then echo "GitHub Release \`$TAG\` already exists." >> "$GITHUB_STEP_SUMMARY" @@ -170,5 +196,8 @@ jobs: echo "::warning::CHANGELOG.md has no '## $VERSION' section; the release links to it instead." echo "See [CHANGELOG.md](https://github.com/$GITHUB_REPOSITORY/blob/$TAG/CHANGELOG.md)." > notes.md fi - gh release create "$TAG" --verify-tag --title "$TAG" --notes-file notes.md + flags=() + if [ "$DIST_TAG" != latest ]; then flags+=(--latest=false); fi + case "${VERSION%%+*}" in *-*) flags+=(--prerelease) ;; esac + gh release create "$TAG" --verify-tag --title "$TAG" --notes-file notes.md "${flags[@]}" echo "Created GitHub Release \`$TAG\`." >> "$GITHUB_STEP_SUMMARY" diff --git a/CHANGELOG.md b/CHANGELOG.md index 616370e..e8585a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,183 @@ # Changelog +## 1.0.0 + +1.0.0 is the first stable release: from here on the public API follows semver, as the README's +[Stability and versioning](https://github.com/glassnode/glassnode-api-ts-client#stability-and-versioning) +section defines. Breaking changes: deprecated exports removed, lenient `MetricMetadata` fields, +`modified` as unix seconds, no `browser` field, `maxRetries` defaulting to `2` and Node.js >= 22; +each has migration notes below. + +It also contains 0.29.4–0.30.1. Of those, only 0.30.1 reached npm: 0.29.4, 0.29.5 and 0.30.0 were +never published and have no GitHub Release. If you are upgrading from 0.29.3 or earlier, read their +sections in the full +[CHANGELOG](https://github.com/glassnode/glassnode-api-ts-client/blob/main/CHANGELOG.md) too +(notably 0.30.0's `BulkEntry['v']` type change). + +### Breaking changes + +- Removed the deprecated exports `MetricTierSchema`, `MetricTier`, `MetricDataTypeSchema`, + `MetricDataType` and `FetchFn` (#23). None of them was used by the client; they were deprecated + with a promise of removal in 1.0. Migration: + - `MetricTier` / `MetricTierSchema`: use `MetricMetadata['tier']`, a `number` (e.g. `2`). The + string enum (`'free' | 'tier1' | …`) never matched the API's `tier` field. + - `MetricDataType` / `MetricDataTypeSchema`: define your own enum if you need these values + (`'average' | 'sum' | 'count' | 'percentage' | 'ratio'`); no response was ever validated + against them. + - `FetchFn`: use `GlassnodeFetch`, the type of the `fetch` config option, or `typeof fetch` + (which `FetchFn` was an alias of; a standard `fetch` still fits the option). +- `MetricMetadata` (from `getMetricMetadata()` / `MetricMetadataSchema`): `refs`, `queried` and + `parameters` are now optional, in line with the lenient response policy (0.12.0) (#25). A response + that omits any of them now parses instead of rejecting the whole call; only `path` and `tier` + stay required. An absent field is reported as sent, `undefined`, with no `{}` default. Their + types become `… | undefined`, which breaks code that reads them directly (e.g. + `meta.refs.docs` or `Object.keys(meta.parameters)`) under `strictNullChecks`. The recorded API + responses still carry all three, so runtime values do not change today. + + **Migration:** read these fields with `?.` and a fallback where you need one, e.g. + `meta.refs?.docs`, `meta.queried?.a`, `meta.parameters?.a ?? []`, + `Object.entries(meta.parameters ?? {})`. + +- `MetricMetadata.modified` is now unix **seconds** (`number | undefined`), passed through as the + API sends it, like every other time field (`timerange.min`/`max`, the time series and bulk `t`, + `callMetric()` results). It used to be the one field converted to a `Date`. The schema is now + `z.number().optional()`; README "Timestamps", the schema comments and + `examples/ex.metadata.validation.ts` are updated. No `toDate` helper is added: + `new Date(t * 1000)` already covers every field (#22). + - **Behaviour change:** `modified: 0` now passes through as `0`; it used to become `undefined` + ("no modification time recorded"). Absent is still `undefined`. + - **Migration:** convert explicitly where you need a `Date`: + + ```ts + const modified = meta.modified !== undefined ? new Date(meta.modified * 1000) : undefined; + ``` + + To keep treating `0` as "not recorded", use `meta.modified || undefined` (or check for `0`) + before converting. + +- `package.json` no longer has a top-level `browser` field; `unpkg` and `jsdelivr` fields point at + `dist/glassnode-api.umd.min.js` instead (#37). `exports` gets no `browser` condition, so + `exports`-aware tools (Node, webpack 5, Vite, esbuild, Rollup, TypeScript) resolve exactly as + before: the tree-shakeable ESM build (`import`) or CommonJS (`require`). Both bundles still ship + at the same paths. + - **CDNs:** the bare `https://unpkg.com/glassnode-api` URL now serves the UMD bundle. unpkg + ignores `browser`, so it used to serve the CommonJS `main` (`dist/index.js`), which fails in a + ` ``` +The bare package URL (`https://unpkg.com/glassnode-api`, `https://cdn.jsdelivr.net/npm/glassnode-api`) +serves the UMD bundle, via the package's `unpkg` and `jsdelivr` fields. Pin a version in production +(e.g. `glassnode-api@1.0.0`). Bundlers (webpack, Vite, esbuild, Rollup) do not use these bundles: +they resolve `glassnode-api` through `exports` to the tree-shakeable ESM build, which shares `zod` +with your app. + The bundles also work unchanged where CORS does not apply, such as browser extensions with host permissions for `api.glassnode.com` (from the background script), or Deno, Bun and other non-browser runtimes. If you do call Glassnode directly from a browser context, keep the default @@ -721,6 +745,43 @@ cp .env.example .env # add GLASSNODE_API_KEY (or the X402_* variables for the x npx ts-node ex.metadata.validation.ts ``` +## Stability and versioning + +From 1.0.0 the package follows [semver](https://semver.org/). + +- **Public API:** exactly what `glassnode-api` and `glassnode-api/x402` export: the classes and + their methods, config and per-call option names, the error classes and their fields, the exported + constants, types and Zod schemas. +- **Minor releases** may add new optional config options, methods or exports, new optional fields + in response schemas, and newly accepted input values. +- **Major releases** are needed to: + - remove or rename an export, method or option, or change a default's behaviour; + - tighten a response schema (make a field required, narrow its type, reject values it accepts + today); + - widen an output type that code already reads: make an existing field optional or nullable, or + add a member to an exported union or enum that a response or event can carry (e.g. + `GlassnodeRetryReason`, `ExternalIdSource`). Both break code under `strictNullChecks` or with + an exhaustive `switch`; + - reject an input that used to be accepted. A minor or patch does so only as a bug fix, for input + that could never have worked (e.g. 0.30.1 rejecting a `callBulkMetric()` path ending in + `/bulk`, which always failed with a 404); + - raise the minimum Node.js version (currently 22); + - move to a new major of Zod or of an optional peer dependency (`@x402/fetch`, `@x402/evm`, + `viem`). The exported schemas are Zod 4 objects. +- **Response parsing:** new object properties the API adds are ignored (stripped), so they never + fail a call. Map-like fields are the exception: in an asset's `external_ids`, a metric's + `descriptors.description` and `parameters`, the stats' `resolution` or a time series point's `o`, + a new key must have the map's value type, or the call fails with a `GlassnodeValidationError`. +- **TypeScript:** no minimum version is pinned or tested in CI. The 1.0.0 typings type-check with + TypeScript 5.4 or later (Zod 4's typings use `NoInfer`), and Zod itself is tested on 5.5+. +- **Browser bundles:** the client's own code is compiled to ES2015, but the bundled Zod keeps its + ES2020 syntax (`?.`, `??`), so the bundles need an ES2020 browser. +- **Not covered:** internal modules (anything the two entry points do not export), the wording of + error messages (match on the error class and its fields instead), an error's `.cause` (the + original underlying error, whose shape is not ours), the text of the server-provided `detail`, + the `logger` output (except that a failing hook is reported as `'Hook failed:', error`), + and the API's own data (which metrics, assets and values Glassnode returns). + ## Development ```bash @@ -734,7 +795,7 @@ pnpm exec tsc -p tsconfig.examples.json # type-check the examples ``` Developing needs Node.js 24 (see `.nvmrc`; Vitest needs Node >= 22.12). The published package -itself supports Node.js >= 18. +itself supports Node.js >= 22. See [CONTRIBUTING.md](./CONTRIBUTING.md) for how to open a pull request: the full local check list, the version and changelog rules, and how releases work. diff --git a/examples/ex.metadata.validation.ts b/examples/ex.metadata.validation.ts index 72ee7ba..0073377 100644 --- a/examples/ex.metadata.validation.ts +++ b/examples/ex.metadata.validation.ts @@ -84,7 +84,9 @@ async function fetchExchangeBalanceMetadata() { console.log('✅ Exchange balance metadata:'); console.log(` Path: ${metadata.path}`); console.log(` Tier: ${metadata.tier}`); - console.log(` Modified: ${metadata.modified?.toISOString()}`); + const modified = + metadata.modified !== undefined ? new Date(metadata.modified * 1000).toISOString() : 'N/A'; + console.log(` Modified: ${modified}`); console.log(` Bulk supported: ${metadata.bulk_supported ?? 'N/A'}`); // Display time range @@ -97,18 +99,18 @@ async function fetchExchangeBalanceMetadata() { // Display available parameters console.log('\nAvailable parameters:'); // Only print the first 3 values per parameter - Object.entries(metadata.parameters).forEach(([key, values]) => { + Object.entries(metadata.parameters ?? {}).forEach(([key, values]) => { console.log(` ${key}: ${values.slice(0, 3).join(', ')}`); }); // Display documentation links and metric variants - if (metadata.refs.docs) { + if (metadata.refs?.docs) { console.log(`\nDocumentation: ${metadata.refs.docs}`); } - if (metadata.refs.studio) { + if (metadata.refs?.studio) { console.log(`Studio: ${metadata.refs.studio}`); } - if (metadata.refs.metric_variant) { + if (metadata.refs?.metric_variant) { console.log('Metric variants:'); Object.entries(metadata.refs.metric_variant).forEach(([key, value]) => { if (value) console.log(` ${key}: ${value}`); @@ -134,7 +136,7 @@ async function fetchExchangeBalanceWithParams() { // Display queried parameters console.log('\nQueried parameters:'); - Object.entries(metadata.queried).forEach(([key, value]) => { + Object.entries(metadata.queried ?? {}).forEach(([key, value]) => { console.log(` ${key}: ${value}`); }); } catch (error) { diff --git a/examples/ex.metric.dump.ts b/examples/ex.metric.dump.ts index fca95a7..38c7a14 100644 --- a/examples/ex.metric.dump.ts +++ b/examples/ex.metric.dump.ts @@ -12,7 +12,7 @@ async function runExamples() { const metricList = await api.getMetricList(); for (const metric of metricList.slice(0, 10)) { const metricMetadata = await api.getMetricMetadata(metric); - const params = metricMetadata.parameters; + const params = metricMetadata.parameters ?? {}; // create params object from params array const paramsObject: Record = {}; for (const param of Object.keys(params)) { diff --git a/examples/package-lock.json b/examples/package-lock.json index e1f3729..5041472 100644 --- a/examples/package-lock.json +++ b/examples/package-lock.json @@ -8,10 +8,10 @@ "name": "glassnode-api-examples", "version": "1.0.0", "dependencies": { - "@x402/evm": "^2.18.0", - "@x402/fetch": "^2.18.0", - "dotenv": "^16.5.0", - "viem": "^2.48.11" + "@x402/evm": "^2.27.0", + "@x402/fetch": "^2.27.0", + "dotenv": "^16.6.1", + "viem": "^2.56.8" }, "devDependencies": { "ts-node": "^10.9.2" @@ -179,32 +179,32 @@ } }, "node_modules/@x402/core": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/@x402/core/-/core-2.26.0.tgz", - "integrity": "sha512-8dlXip9u0JjyELzzFz48vntuMajvUjA8IYpitfaW423m53bR49amZDIn3n4gXRYoNEKzGB9Rtn8at35WHz61cA==", + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@x402/core/-/core-2.27.0.tgz", + "integrity": "sha512-UbxCKnoTsg5nQX9fYER3g9tt5l80jHm1IZOmDczQRqE0cwskls5r+VAAj0iUQfHVshnpv0qo/rLFI7UntqL96g==", "license": "Apache-2.0", "dependencies": { "zod": "^3.24.2" } }, "node_modules/@x402/evm": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/@x402/evm/-/evm-2.26.0.tgz", - "integrity": "sha512-dcDxdWsEqyqmr0BsTXbvDNbL2KCjSvqcQV6VtSu7eTeDmi6RfI/T4IPSst5iI8M2Ys34Wc48AikJepuE7Bojgw==", + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@x402/evm/-/evm-2.27.0.tgz", + "integrity": "sha512-WKVFq5VaSrDJfo6lHNG7/4hT0iZdpsmd53W/a+gZqmE1CiVT8wyRpwIPue4f/YKqNfgHHhZ00AcfPho0xLr2eQ==", "license": "Apache-2.0", "dependencies": { - "@x402/core": "~2.26.0", + "@x402/core": "~2.27.0", "viem": "^2.48.11", "zod": "^3.24.2" } }, "node_modules/@x402/fetch": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/@x402/fetch/-/fetch-2.26.0.tgz", - "integrity": "sha512-pXwFGaWkc76+3ZGKfi9Xjay4h3KfdnbiRMwPC+J2+XGQDP1OVQvyvdyVEDuEtdrVPDYVP6rqq6f+hq1aaFql8g==", + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@x402/fetch/-/fetch-2.27.0.tgz", + "integrity": "sha512-lHJsvBLGRTVw2tVBKyiNfnjrTqqXbks6QKAuFMUGlyBgm6ErcZKaW804jqXUpGOSt+UPHscbbkLclEWHrk5gcQ==", "license": "Apache-2.0", "dependencies": { - "@x402/core": "~2.26.0" + "@x402/core": "~2.27.0" } }, "node_modules/abitype": { @@ -279,9 +279,9 @@ } }, "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -422,9 +422,9 @@ "license": "MIT" }, "node_modules/viem": { - "version": "2.56.7", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.56.7.tgz", - "integrity": "sha512-8YXhttIOKikDnTLj4byvGQmF43Gi0FtEfxUWd6OqSJiuz9bxNYNGmzlS8ZaNiYSKUEfs5vmNprvwPIki9Lk4YA==", + "version": "2.56.8", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.56.8.tgz", + "integrity": "sha512-vD1DsnYxySMml5/02m/+boWQYfZtWug2VYzm6+hdVT+VDmULBZfpoh9gPDAENdMMBmQKW/2QJ/1DuZaslBJDAg==", "funding": [ { "type": "github", diff --git a/examples/package.json b/examples/package.json index 216688e..5473812 100644 --- a/examples/package.json +++ b/examples/package.json @@ -3,10 +3,10 @@ "version": "1.0.0", "private": true, "dependencies": { - "@x402/evm": "^2.18.0", - "@x402/fetch": "^2.18.0", - "dotenv": "^16.5.0", - "viem": "^2.48.11" + "@x402/evm": "^2.27.0", + "@x402/fetch": "^2.27.0", + "dotenv": "^16.6.1", + "viem": "^2.56.8" }, "devDependencies": { "ts-node": "^10.9.2" diff --git a/package.json b/package.json index 7846f4b..0a0eb5e 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "glassnode-api", - "version": "0.30.1", + "version": "1.0.0", "type": "commonjs", "description": "TypeScript client for the Glassnode API (Node.js and browsers)", "main": "dist/index.js", "types": "./dist/index.d.ts", - "browser": "dist/glassnode-api.umd.min.js", + "unpkg": "dist/glassnode-api.umd.min.js", + "jsdelivr": "dist/glassnode-api.umd.min.js", "module": "./dist/esm/index.js", "files": [ "dist/**/*.js", @@ -71,7 +72,7 @@ "packageManager": "pnpm@10.32.1", "homepage": "https://github.com/glassnode/glassnode-api-ts-client#readme", "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "sideEffects": false, "exports": { @@ -100,7 +101,6 @@ "publishConfig": { "registry": "https://registry.npmjs.org/", "access": "public", - "tag": "latest", "provenance": true }, "dependencies": { @@ -128,22 +128,22 @@ "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", - "@types/node": "^18.19.130", - "@vitest/coverage-v8": "^5.0.0", - "@x402/evm": "^2.25.0", - "@x402/fetch": "^2.25.0", - "eslint": "^10.10.0", + "@types/node": "^22.20.4", + "@vitest/coverage-v8": "^5.0.1", + "@x402/evm": "^2.27.0", + "@x402/fetch": "^2.27.0", + "eslint": "^10.11.0", "husky": "^9.1.7", "lint-staged": "^17.5.1", - "prettier": "^3.9.6", - "rollup": "^4.63.2", + "prettier": "^3.9.8", + "rollup": "^4.63.4", "tslib": "^2.8.1", "typedoc": "^0.28.20", "typedoc-plugin-zod": "^1.4.3", "typescript": "^6.0.3", - "typescript-eslint": "^8.70.0", - "viem": "^2.56.5", - "vitest": "^5.0.0" + "typescript-eslint": "^8.70.1", + "viem": "^2.56.8", + "vitest": "^5.0.1" }, "lint-staged": { "*.{js,ts,mjs}": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d6abd9a..347f64b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,34 +22,34 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.10.0) + version: 10.0.1(eslint@10.11.0) '@rollup/plugin-commonjs': specifier: ^29.0.3 - version: 29.0.3(rollup@4.63.2) + version: 29.0.3(rollup@4.63.4) '@rollup/plugin-node-resolve': specifier: ^16.0.3 - version: 16.0.3(rollup@4.63.2) + version: 16.0.3(rollup@4.63.4) '@rollup/plugin-terser': specifier: ^1.0.0 - version: 1.0.0(rollup@4.63.2) + version: 1.0.0(rollup@4.63.4) '@rollup/plugin-typescript': specifier: ^12.3.0 - version: 12.3.0(rollup@4.63.2)(tslib@2.8.1)(typescript@6.0.3) + version: 12.3.0(rollup@4.63.4)(tslib@2.8.1)(typescript@6.0.3) '@types/node': - specifier: ^18.19.130 - version: 18.19.130 + specifier: ^22.20.4 + version: 22.20.4 '@vitest/coverage-v8': - specifier: ^5.0.0 - version: 5.0.0(vitest@5.0.0) + specifier: ^5.0.1 + version: 5.0.1(vitest@5.0.1) '@x402/evm': - specifier: ^2.25.0 - version: 2.25.0(typescript@6.0.3) + specifier: ^2.27.0 + version: 2.27.0(typescript@6.0.3) '@x402/fetch': - specifier: ^2.25.0 - version: 2.25.0 + specifier: ^2.27.0 + version: 2.27.0 eslint: - specifier: ^10.10.0 - version: 10.10.0 + specifier: ^10.11.0 + version: 10.11.0 husky: specifier: ^9.1.7 version: 9.1.7 @@ -57,11 +57,11 @@ importers: specifier: ^17.5.1 version: 17.5.1 prettier: - specifier: ^3.9.6 - version: 3.9.6 + specifier: ^3.9.8 + version: 3.9.8 rollup: - specifier: ^4.63.2 - version: 4.63.2 + specifier: ^4.63.4 + version: 4.63.4 tslib: specifier: ^2.8.1 version: 2.8.1 @@ -75,14 +75,14 @@ importers: specifier: ^6.0.3 version: 6.0.3 typescript-eslint: - specifier: ^8.70.0 - version: 8.70.0(eslint@10.10.0)(typescript@6.0.3) + specifier: ^8.70.1 + version: 8.70.1(eslint@10.11.0)(typescript@6.0.3) viem: - specifier: ^2.56.5 - version: 2.56.5(typescript@6.0.3)(zod@4.6.5) + specifier: ^2.56.8 + version: 2.56.8(typescript@6.0.3)(zod@4.6.5) vitest: - specifier: ^5.0.0 - version: 5.0.0(@types/node@18.19.130)(@vitest/coverage-v8@5.0.0)(vite@8.1.4(@types/node@18.19.130)(terser@5.46.0)(yaml@2.9.0)) + specifier: ^5.0.1 + version: 5.0.1(@types/node@22.20.4)(@vitest/coverage-v8@5.0.1)(vite@8.1.4(@types/node@22.20.4)(terser@5.46.0)(yaml@2.9.0)) packages: @@ -390,141 +390,141 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.63.2': - resolution: {integrity: sha512-Xa6RDoWa+hNiX6PgsljlH6W75RaONx3y6PVlbLhkEWW+GaPQ3dP5gwbL/erAzQHWwkvW5UxdD5l87Qx2FAQ/4A==} + '@rollup/rollup-android-arm-eabi@4.63.4': + resolution: {integrity: sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.63.2': - resolution: {integrity: sha512-vNASxsghMfQ5s+v3PrpnJd+ryL/26lxCCaGI+sDJ7VzmHiYXIrrVltsDhaawxLM1WcoMU2oYlbPHLaYQtBzhcg==} + '@rollup/rollup-android-arm64@4.63.4': + resolution: {integrity: sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.63.2': - resolution: {integrity: sha512-0dWDjmlrpZAgjPD/aPzUDhBW8APLRjAni5bOrM76wiiZm+E+KTMVKNhAzaTBohz8UyO2fKNAl0+fygbe2HZXOA==} + '@rollup/rollup-darwin-arm64@4.63.4': + resolution: {integrity: sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.63.2': - resolution: {integrity: sha512-N58uktcwzk3+qT4KHEuNdIxX1N01RWrkfVoml69EAbSaNDL+sbNVLx2RMl4Qd23lpA0fgPvyh5hHb4weD5WKmg==} + '@rollup/rollup-darwin-x64@4.63.4': + resolution: {integrity: sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.63.2': - resolution: {integrity: sha512-HWF2zH8EAp2scWRpt2PGe6iUGz7zi04waXsdRr3zb4DWCk2ImIo5FZu0jjmD53nP/DGSvnW0e7/1ToCNZs2lZw==} + '@rollup/rollup-freebsd-arm64@4.63.4': + resolution: {integrity: sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.63.2': - resolution: {integrity: sha512-MkvcwHMnzPSMOQEwB6wHnLzmc+hT8BGc5bW/Mhmjjgx3wbj6VBnlc47XsK74kD0K9MikFfXpQqyz4NUXaUW62A==} + '@rollup/rollup-freebsd-x64@4.63.4': + resolution: {integrity: sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.63.2': - resolution: {integrity: sha512-xe1bCKPJaKsD0tfd7Rb6bGfUogJTpKbTEEthsfdb7hTfTRNJVQTdirabQx0o6ERVba/smkM720soMY+0QnrlSQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': + resolution: {integrity: sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.63.2': - resolution: {integrity: sha512-yOM7LdK0p6gk6+Q773OEwtlsikT1TL3yMmYsTtRlDRPha5vV2DC5x7LqRWDr6f3cSYNMKVqxzffXv8ivxNBIFQ==} + '@rollup/rollup-linux-arm-musleabihf@4.63.4': + resolution: {integrity: sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.63.2': - resolution: {integrity: sha512-qiWuJJV3DybA2IfzvRimeKXGrGuVPv1zobSY/26KnP3HbV0VcNb3ECzgvtbvF3xjSMkcooou6HASXZuLdjnhpQ==} + '@rollup/rollup-linux-arm64-gnu@4.63.4': + resolution: {integrity: sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.63.2': - resolution: {integrity: sha512-akcZquRzCY/KpUoZAMBhGf7oi4LmXq1BzRA5CPAC3rkUf28Y/sAYV3jSL+JKd7cwEyFvR5G0XVZ0gaMedP+60A==} + '@rollup/rollup-linux-arm64-musl@4.63.4': + resolution: {integrity: sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.63.2': - resolution: {integrity: sha512-fNwYHrPyYyxauPzX/cpYw8Z7LQpp+DGA0KCoswA0aVFBpmdMil9XgjB8V3Ny64Ihu797+GKcuJqnsOKEmor7fA==} + '@rollup/rollup-linux-loong64-gnu@4.63.4': + resolution: {integrity: sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.63.2': - resolution: {integrity: sha512-XfvsgzR7DZqREdst7K1Mj3ilSUM5xLAHJcIMDFPKdxTs9q5VHOT8aMA+a683fqBu7DQl8+Sd9HCsQYL8EMY9qA==} + '@rollup/rollup-linux-loong64-musl@4.63.4': + resolution: {integrity: sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.63.2': - resolution: {integrity: sha512-Pp7gVZggEFlbcuztay+/U0gVG9S1XAh8i7I1Re/htbAzo43P5wHZHw6pTyzotISqlKohoh9RpIfnOz3RbemK1w==} + '@rollup/rollup-linux-ppc64-gnu@4.63.4': + resolution: {integrity: sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.63.2': - resolution: {integrity: sha512-zkgL2xff6i7u5hau/m6FGeS8gRkLEdgLw522WGmdWWlLd9btmNl3S80mcEjtGq+kvgUekQ3+BOYLLLcPlS2LIA==} + '@rollup/rollup-linux-ppc64-musl@4.63.4': + resolution: {integrity: sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.63.2': - resolution: {integrity: sha512-qOheJomrkVCbbHFJ7L3J97cnhfogKqguAQphv26+3ZsAQIF1L19b+dArl//s8rjJHJLz9byykyM8NBP4nmSa1g==} + '@rollup/rollup-linux-riscv64-gnu@4.63.4': + resolution: {integrity: sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.63.2': - resolution: {integrity: sha512-XlxLD54wQhH3FciCgMofxBw27NzUe818gJH410qWvc41UT0ZFcgxVjyX5/EK8MPTupjeVWqN5oy+9pCA9mqfCA==} + '@rollup/rollup-linux-riscv64-musl@4.63.4': + resolution: {integrity: sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.63.2': - resolution: {integrity: sha512-vdryWeRb2bLJZf0Fv/W8se6nvsHe2PkTCxV0meheK3nQE+G90VCJcke51Miy1yQRsfm2uqIyjXOu4wmUzbTtkQ==} + '@rollup/rollup-linux-s390x-gnu@4.63.4': + resolution: {integrity: sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.63.2': - resolution: {integrity: sha512-bcq2h2pkKmH2po4cZV8VWzO4lL40STyu/nLoFpYMQp9C2tCVNTdcVv86MwSsn3D5s1FBe2Ty1atqvVAUTMimNg==} + '@rollup/rollup-linux-x64-gnu@4.63.4': + resolution: {integrity: sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.63.2': - resolution: {integrity: sha512-EGoo5DMVMRkTId8fuTDaoxVlR5ZTsKULUezRjd9gCw5eeY+DjCvDpZAOlNUvKPGX+7rS1RWx6j+yOpNPx0cUgQ==} + '@rollup/rollup-linux-x64-musl@4.63.4': + resolution: {integrity: sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.63.2': - resolution: {integrity: sha512-MErl12k7BFHZG1TI9QF/3lSSZARzq9KgNy/FjnqFMCkv+N4RSSzoUCA5h2mqHX4Mox3WaTVKblyzhQ1zRb2ZuQ==} + '@rollup/rollup-openbsd-x64@4.63.4': + resolution: {integrity: sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.63.2': - resolution: {integrity: sha512-ILs8k07Wh4p0PsNY4wYLEaXZKMOpVhrG5QDB0yHhGhuzOfDlnyHN6sflL4El/MpUP1y8uY2lUZrv4oBS6pTT3g==} + '@rollup/rollup-openharmony-arm64@4.63.4': + resolution: {integrity: sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.63.2': - resolution: {integrity: sha512-hKgB3nz/TKD3Wv78XEsyXzQsNjvhOHmwKQTvXADGOyU/cIClZDO7DsoggbdmJDPGp5V80tA3Vfv61PaKTLH3LA==} + '@rollup/rollup-win32-arm64-msvc@4.63.4': + resolution: {integrity: sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.63.2': - resolution: {integrity: sha512-T4wf1mudIDxN8Q/CWIBJC1u5gQUc+r5mPvlwoSbIvNkyVTP2TAFeobEmst5AQ4gMyAz4sSByVdoTDfvTmGK/8g==} + '@rollup/rollup-win32-ia32-msvc@4.63.4': + resolution: {integrity: sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.63.2': - resolution: {integrity: sha512-tC3IY7qoaD9Ll3/8WJQn49j5V2f/NuI9S41NOE2iM5MPs3sPIvOkVToLcz/7Bz4pyF7PSvrtwu8I/pUrGOSecQ==} + '@rollup/rollup-win32-x64-gnu@4.63.4': + resolution: {integrity: sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.63.2': - resolution: {integrity: sha512-6NHnk/K3eq2ZFYcU1X8g67s9qIJRCOTT92gwLMVBp08dB2uuuwI1/Q/empzL2Bfr2f2WRLJVwpp90RmacQyFkw==} + '@rollup/rollup-win32-x64-msvc@4.63.4': + resolution: {integrity: sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==} cpu: [x64] os: [win32] @@ -576,8 +576,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@18.19.130': - resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@22.20.4': + resolution: {integrity: sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==} '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -585,70 +585,70 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@typescript-eslint/eslint-plugin@8.70.0': - resolution: {integrity: sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==} + '@typescript-eslint/eslint-plugin@8.70.1': + resolution: {integrity: sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.70.0 + '@typescript-eslint/parser': ^8.70.1 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.70.0': - resolution: {integrity: sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==} + '@typescript-eslint/parser@8.70.1': + resolution: {integrity: sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.70.0': - resolution: {integrity: sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==} + '@typescript-eslint/project-service@8.70.1': + resolution: {integrity: sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.70.0': - resolution: {integrity: sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==} + '@typescript-eslint/scope-manager@8.70.1': + resolution: {integrity: sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.70.0': - resolution: {integrity: sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==} + '@typescript-eslint/tsconfig-utils@8.70.1': + resolution: {integrity: sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.70.0': - resolution: {integrity: sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==} + '@typescript-eslint/type-utils@8.70.1': + resolution: {integrity: sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.70.0': - resolution: {integrity: sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==} + '@typescript-eslint/types@8.70.1': + resolution: {integrity: sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.70.0': - resolution: {integrity: sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==} + '@typescript-eslint/typescript-estree@8.70.1': + resolution: {integrity: sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.70.0': - resolution: {integrity: sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==} + '@typescript-eslint/utils@8.70.1': + resolution: {integrity: sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.70.0': - resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==} + '@typescript-eslint/visitor-keys@8.70.1': + resolution: {integrity: sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitest/coverage-v8@5.0.0': - resolution: {integrity: sha512-toMg6PZGCIa/lQNCDoASrfb1ly4hsUKXFtFYC9kD4t78o5Y6LyNJU7AENt8eHPr3quYdxaxK7hj2mnbFfUk9NA==} + '@vitest/coverage-v8@5.0.1': + resolution: {integrity: sha512-FRC8ACiudC3dI6MTplzRSYWHDRnIv2IPfbzs4FdoJNsMal/35sWV8hwIfV8ZcqzSPy+uXHeMVONt9CEqtOU17w==} peerDependencies: - '@vitest/browser': 5.0.0 - vitest: 5.0.0 + '@vitest/browser': 5.0.1 + vitest: 5.0.1 peerDependenciesMeta: '@vitest/browser': optional: true @@ -661,8 +661,8 @@ packages: resolution: {integrity: sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==} engines: {node: '>=22'} - '@vitest/mocker@5.0.0': - resolution: {integrity: sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==} + '@vitest/mocker@5.0.1': + resolution: {integrity: sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -672,17 +672,17 @@ packages: vite: optional: true - '@vitest/spy@5.0.0': - resolution: {integrity: sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==} + '@vitest/spy@5.0.1': + resolution: {integrity: sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==} - '@x402/core@2.25.0': - resolution: {integrity: sha512-5Ys0XYz3FKutxVKoXC46R/XPT/oaAbuj7ahzrlVHwQxZJPH9u6u91IOh0ztz+7G/zYGsaXR8l3ety205QtEnUw==} + '@x402/core@2.27.0': + resolution: {integrity: sha512-UbxCKnoTsg5nQX9fYER3g9tt5l80jHm1IZOmDczQRqE0cwskls5r+VAAj0iUQfHVshnpv0qo/rLFI7UntqL96g==} - '@x402/evm@2.25.0': - resolution: {integrity: sha512-EmnL4MyGW8weEUlq0qC1uJUeoBmtp9GWGwyPl+qkC0CYItRHn748puhYAxK8GsrofscQrt63HsFCB0Gp3iEROA==} + '@x402/evm@2.27.0': + resolution: {integrity: sha512-WKVFq5VaSrDJfo6lHNG7/4hT0iZdpsmd53W/a+gZqmE1CiVT8wyRpwIPue4f/YKqNfgHHhZ00AcfPho0xLr2eQ==} - '@x402/fetch@2.25.0': - resolution: {integrity: sha512-kpmxnMTjxZJD5r4yVhRDtCrBVfGhum+XwrUUUdffi3Y91eAm9ZiWCDNHLtahOS2kmuzTR2NPbbIoohZQJmQsxg==} + '@x402/fetch@2.27.0': + resolution: {integrity: sha512-lHJsvBLGRTVw2tVBKyiNfnjrTqqXbks6QKAuFMUGlyBgm6ErcZKaW804jqXUpGOSt+UPHscbbkLclEWHrk5gcQ==} abitype@1.2.3: resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} @@ -789,8 +789,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.10.0: - resolution: {integrity: sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==} + eslint@10.11.0: + resolution: {integrity: sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -1053,10 +1053,6 @@ packages: mdurl@2.1.0: resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==} - minimatch@10.2.4: - resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} - engines: {node: 18 || 20 || >=22} - minimatch@10.2.6: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} @@ -1080,8 +1076,8 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - ox@0.14.44: - resolution: {integrity: sha512-O54qXXHEk4ySamMSyBpI0AeBegS5frxU6+LA6Jb9Sf6kMOxcTNaxYmwZfpOu22DIQsdKfgQxHq8EsAGaoBQScA==} + ox@0.14.45: + resolution: {integrity: sha512-jpsQ+p0JZh9mKCxxzxz0d5qFHZZg2/O9xW18IpEC/dNXXPVqmDJ0c//9RMz0CILkbSGPA42+cbU0fC/ghwJ03w==} peerDependencies: typescript: '>=5.4.0' peerDependenciesMeta: @@ -1122,8 +1118,8 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier@3.9.6: - resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + prettier@3.9.8: + resolution: {integrity: sha512-WRFq3Wn3WId7LLROfMLdH7xaFr2jR62wU8nLO6rQUOLOxNZUviyJQs1M0iIhLexSFy+L+w0ch66wtoO2jRjG0A==} engines: {node: '>=14'} hasBin: true @@ -1149,8 +1145,8 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rollup@4.63.2: - resolution: {integrity: sha512-l5eyksV4tPBj6lJyEa37YzIOCSOV7lkZzEHUdpjWZbtD7wTcFYmEYXSgm5bT4vV+dZLb9rBG1W9GROOG4NS4Ew==} + rollup@4.63.4: + resolution: {integrity: sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1253,8 +1249,8 @@ packages: peerDependencies: typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x - typescript-eslint@8.70.0: - resolution: {integrity: sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==} + typescript-eslint@8.70.1: + resolution: {integrity: sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -1268,14 +1264,14 @@ packages: uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - viem@2.56.5: - resolution: {integrity: sha512-GuEf/oee0PHgy4D6JIA8u0rY1g+sM5nF+p7HyMHO3vjlHC5G/ljfrgnUOnrES9m5Ny/P3RwWCHjDuN2T6HUWzQ==} + viem@2.56.8: + resolution: {integrity: sha512-vD1DsnYxySMml5/02m/+boWQYfZtWug2VYzm6+hdVT+VDmULBZfpoh9gPDAENdMMBmQKW/2QJ/1DuZaslBJDAg==} peerDependencies: typescript: '>=5.0.4' peerDependenciesMeta: @@ -1325,20 +1321,20 @@ packages: yaml: optional: true - vitest@5.0.0: - resolution: {integrity: sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==} + vitest@5.0.1: + resolution: {integrity: sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==} engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 5.0.0 - '@vitest/browser-preview': 5.0.0 + '@vitest/browser-playwright': 5.0.1 + '@vitest/browser-preview': 5.0.1 '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 - '@vitest/coverage-istanbul': 5.0.0 - '@vitest/coverage-v8': 5.0.0 - '@vitest/ui': 5.0.0 + '@vitest/coverage-istanbul': 5.0.1 + '@vitest/coverage-v8': 5.0.1 + '@vitest/ui': 5.0.1 happy-dom: '*' jsdom: '*' vite: ^6.4.0 || ^7.0.0 || ^8.0.0 @@ -1454,9 +1450,9 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.10.0)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.11.0)': dependencies: - eslint: 10.10.0 + eslint: 10.11.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -1477,9 +1473,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.10.0)': + '@eslint/js@10.0.1(eslint@10.11.0)': optionalDependencies: - eslint: 10.10.0 + eslint: 10.11.0 '@eslint/object-schema@3.0.5': {} @@ -1612,9 +1608,9 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} - '@rollup/plugin-commonjs@29.0.3(rollup@4.63.2)': + '@rollup/plugin-commonjs@29.0.3(rollup@4.63.4)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.63.2) + '@rollup/pluginutils': 5.3.0(rollup@4.63.4) commondir: 1.0.1 estree-walker: 2.0.2 fdir: 6.5.0(picomatch@4.0.5) @@ -1622,116 +1618,116 @@ snapshots: magic-string: 0.30.21 picomatch: 4.0.5 optionalDependencies: - rollup: 4.63.2 + rollup: 4.63.4 - '@rollup/plugin-node-resolve@16.0.3(rollup@4.63.2)': + '@rollup/plugin-node-resolve@16.0.3(rollup@4.63.4)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.63.2) + '@rollup/pluginutils': 5.3.0(rollup@4.63.4) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.11 optionalDependencies: - rollup: 4.63.2 + rollup: 4.63.4 - '@rollup/plugin-terser@1.0.0(rollup@4.63.2)': + '@rollup/plugin-terser@1.0.0(rollup@4.63.4)': dependencies: serialize-javascript: 7.0.7 smob: 1.6.1 terser: 5.46.0 optionalDependencies: - rollup: 4.63.2 + rollup: 4.63.4 - '@rollup/plugin-typescript@12.3.0(rollup@4.63.2)(tslib@2.8.1)(typescript@6.0.3)': + '@rollup/plugin-typescript@12.3.0(rollup@4.63.4)(tslib@2.8.1)(typescript@6.0.3)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.63.2) + '@rollup/pluginutils': 5.3.0(rollup@4.63.4) resolve: 1.22.11 typescript: 6.0.3 optionalDependencies: - rollup: 4.63.2 + rollup: 4.63.4 tslib: 2.8.1 - '@rollup/pluginutils@5.3.0(rollup@4.63.2)': + '@rollup/pluginutils@5.3.0(rollup@4.63.4)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.5 optionalDependencies: - rollup: 4.63.2 + rollup: 4.63.4 - '@rollup/rollup-android-arm-eabi@4.63.2': + '@rollup/rollup-android-arm-eabi@4.63.4': optional: true - '@rollup/rollup-android-arm64@4.63.2': + '@rollup/rollup-android-arm64@4.63.4': optional: true - '@rollup/rollup-darwin-arm64@4.63.2': + '@rollup/rollup-darwin-arm64@4.63.4': optional: true - '@rollup/rollup-darwin-x64@4.63.2': + '@rollup/rollup-darwin-x64@4.63.4': optional: true - '@rollup/rollup-freebsd-arm64@4.63.2': + '@rollup/rollup-freebsd-arm64@4.63.4': optional: true - '@rollup/rollup-freebsd-x64@4.63.2': + '@rollup/rollup-freebsd-x64@4.63.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.63.2': + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.63.2': + '@rollup/rollup-linux-arm-musleabihf@4.63.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.63.2': + '@rollup/rollup-linux-arm64-gnu@4.63.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.63.2': + '@rollup/rollup-linux-arm64-musl@4.63.4': optional: true - '@rollup/rollup-linux-loong64-gnu@4.63.2': + '@rollup/rollup-linux-loong64-gnu@4.63.4': optional: true - '@rollup/rollup-linux-loong64-musl@4.63.2': + '@rollup/rollup-linux-loong64-musl@4.63.4': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.63.2': + '@rollup/rollup-linux-ppc64-gnu@4.63.4': optional: true - '@rollup/rollup-linux-ppc64-musl@4.63.2': + '@rollup/rollup-linux-ppc64-musl@4.63.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.63.2': + '@rollup/rollup-linux-riscv64-gnu@4.63.4': optional: true - '@rollup/rollup-linux-riscv64-musl@4.63.2': + '@rollup/rollup-linux-riscv64-musl@4.63.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.63.2': + '@rollup/rollup-linux-s390x-gnu@4.63.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.63.2': + '@rollup/rollup-linux-x64-gnu@4.63.4': optional: true - '@rollup/rollup-linux-x64-musl@4.63.2': + '@rollup/rollup-linux-x64-musl@4.63.4': optional: true - '@rollup/rollup-openbsd-x64@4.63.2': + '@rollup/rollup-openbsd-x64@4.63.4': optional: true - '@rollup/rollup-openharmony-arm64@4.63.2': + '@rollup/rollup-openharmony-arm64@4.63.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.63.2': + '@rollup/rollup-win32-arm64-msvc@4.63.4': optional: true - '@rollup/rollup-win32-ia32-msvc@4.63.2': + '@rollup/rollup-win32-ia32-msvc@4.63.4': optional: true - '@rollup/rollup-win32-x64-gnu@4.63.2': + '@rollup/rollup-win32-x64-gnu@4.63.4': optional: true - '@rollup/rollup-win32-x64-msvc@4.63.2': + '@rollup/rollup-win32-x64-msvc@4.63.4': optional: true '@scure/base@1.2.6': {} @@ -1791,23 +1787,23 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@18.19.130': + '@types/node@22.20.4': dependencies: - undici-types: 5.26.5 + undici-types: 6.21.0 '@types/resolve@1.20.2': {} '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.10.0)(typescript@6.0.3))(eslint@10.10.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.11.0)(typescript@6.0.3))(eslint@10.11.0)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.70.0(eslint@10.10.0)(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.70.0 - '@typescript-eslint/type-utils': 8.70.0(eslint@10.10.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.70.0(eslint@10.10.0)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.70.0 - eslint: 10.10.0 + '@typescript-eslint/parser': 8.70.1(eslint@10.11.0)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.70.1 + '@typescript-eslint/type-utils': 8.70.1(eslint@10.11.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.70.1(eslint@10.11.0)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.70.1 + eslint: 10.11.0 ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -1815,58 +1811,58 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.70.0(eslint@10.10.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.70.1(eslint@10.11.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.70.0 - '@typescript-eslint/types': 8.70.0 - '@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.70.0 + '@typescript-eslint/scope-manager': 8.70.1 + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/typescript-estree': 8.70.1(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.70.1 debug: 4.4.3 - eslint: 10.10.0 + eslint: 10.11.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.70.0(typescript@6.0.3)': + '@typescript-eslint/project-service@8.70.1(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3) - '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@6.0.3) + '@typescript-eslint/types': 8.70.1 debug: 4.4.3 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.70.0': + '@typescript-eslint/scope-manager@8.70.1': dependencies: - '@typescript-eslint/types': 8.70.0 - '@typescript-eslint/visitor-keys': 8.70.0 + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/visitor-keys': 8.70.1 - '@typescript-eslint/tsconfig-utils@8.70.0(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.70.1(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.70.0(eslint@10.10.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.70.1(eslint@10.11.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.70.0 - '@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.70.0(eslint@10.10.0)(typescript@6.0.3) + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/typescript-estree': 8.70.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.70.1(eslint@10.11.0)(typescript@6.0.3) debug: 4.4.3 - eslint: 10.10.0 + eslint: 10.11.0 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.70.0': {} + '@typescript-eslint/types@8.70.1': {} - '@typescript-eslint/typescript-estree@8.70.0(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.70.1(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.70.0(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3) - '@typescript-eslint/types': 8.70.0 - '@typescript-eslint/visitor-keys': 8.70.0 + '@typescript-eslint/project-service': 8.70.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@6.0.3) + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/visitor-keys': 8.70.1 debug: 4.4.3 - minimatch: 10.2.4 + minimatch: 10.2.6 semver: 7.7.4 tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -1874,23 +1870,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.70.0(eslint@10.10.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.70.1(eslint@10.11.0)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.10.0) - '@typescript-eslint/scope-manager': 8.70.0 - '@typescript-eslint/types': 8.70.0 - '@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3) - eslint: 10.10.0 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.11.0) + '@typescript-eslint/scope-manager': 8.70.1 + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/typescript-estree': 8.70.1(typescript@6.0.3) + eslint: 10.11.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.70.0': + '@typescript-eslint/visitor-keys@8.70.1': dependencies: - '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/types': 8.70.1 eslint-visitor-keys: 5.0.1 - '@vitest/coverage-v8@5.0.0(vitest@5.0.0)': + '@vitest/coverage-v8@5.0.1(vitest@5.0.1)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/istanbul-lib-coverage': 1.0.1 @@ -1900,7 +1896,7 @@ snapshots: obug: 2.2.1 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 5.0.0(@types/node@18.19.130)(@vitest/coverage-v8@5.0.0)(vite@8.1.4(@types/node@18.19.130)(terser@5.46.0)(yaml@2.9.0)) + vitest: 5.0.1(@types/node@22.20.4)(@vitest/coverage-v8@5.0.1)(vite@8.1.4(@types/node@22.20.4)(terser@5.46.0)(yaml@2.9.0)) '@vitest/istanbul-lib-coverage@1.0.1': {} @@ -1908,34 +1904,34 @@ snapshots: dependencies: '@vitest/istanbul-lib-coverage': 1.0.1 - '@vitest/mocker@5.0.0(vite@8.1.4(@types/node@18.19.130)(terser@5.46.0)(yaml@2.9.0))': + '@vitest/mocker@5.0.1(vite@8.1.4(@types/node@22.20.4)(terser@5.46.0)(yaml@2.9.0))': dependencies: '@jridgewell/trace-mapping': 0.3.31 - '@vitest/spy': 5.0.0 + '@vitest/spy': 5.0.1 estree-walker: 3.0.3 magic-string: 1.3.1 optionalDependencies: - vite: 8.1.4(@types/node@18.19.130)(terser@5.46.0)(yaml@2.9.0) + vite: 8.1.4(@types/node@22.20.4)(terser@5.46.0)(yaml@2.9.0) - '@vitest/spy@5.0.0': {} + '@vitest/spy@5.0.1': {} - '@x402/core@2.25.0': + '@x402/core@2.27.0': dependencies: zod: 3.25.76 - '@x402/evm@2.25.0(typescript@6.0.3)': + '@x402/evm@2.27.0(typescript@6.0.3)': dependencies: - '@x402/core': 2.25.0 - viem: 2.56.5(typescript@6.0.3)(zod@3.25.76) + '@x402/core': 2.27.0 + viem: 2.56.8(typescript@6.0.3)(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: - bufferutil - typescript - utf-8-validate - '@x402/fetch@2.25.0': + '@x402/fetch@2.27.0': dependencies: - '@x402/core': 2.25.0 + '@x402/core': 2.27.0 abitype@1.2.3(typescript@6.0.3)(zod@3.25.76): optionalDependencies: @@ -2025,9 +2021,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.10.0: + eslint@10.11.0: dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.10.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.11.0) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.7.0 @@ -2273,10 +2269,6 @@ snapshots: mdurl@2.1.0: {} - minimatch@10.2.4: - dependencies: - brace-expansion: 5.0.12 - minimatch@10.2.6: dependencies: brace-expansion: 5.0.12 @@ -2298,7 +2290,7 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - ox@0.14.44(typescript@6.0.3)(zod@3.25.76): + ox@0.14.45(typescript@6.0.3)(zod@3.25.76): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/ciphers': 1.3.0 @@ -2313,7 +2305,7 @@ snapshots: transitivePeerDependencies: - zod - ox@0.14.44(typescript@6.0.3)(zod@4.6.5): + ox@0.14.45(typescript@6.0.3)(zod@4.6.5): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/ciphers': 1.3.0 @@ -2354,7 +2346,7 @@ snapshots: prelude-ls@1.2.1: {} - prettier@3.9.6: {} + prettier@3.9.8: {} punycode.js@2.3.1: {} @@ -2391,36 +2383,36 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.1.5 '@rolldown/binding-win32-x64-msvc': 1.1.5 - rollup@4.63.2: + rollup@4.63.4: dependencies: '@types/estree': 1.0.9 optionalDependencies: '@napi-rs/lzma-linux-x64-gnu': 1.5.1 - '@rollup/rollup-android-arm-eabi': 4.63.2 - '@rollup/rollup-android-arm64': 4.63.2 - '@rollup/rollup-darwin-arm64': 4.63.2 - '@rollup/rollup-darwin-x64': 4.63.2 - '@rollup/rollup-freebsd-arm64': 4.63.2 - '@rollup/rollup-freebsd-x64': 4.63.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.63.2 - '@rollup/rollup-linux-arm-musleabihf': 4.63.2 - '@rollup/rollup-linux-arm64-gnu': 4.63.2 - '@rollup/rollup-linux-arm64-musl': 4.63.2 - '@rollup/rollup-linux-loong64-gnu': 4.63.2 - '@rollup/rollup-linux-loong64-musl': 4.63.2 - '@rollup/rollup-linux-ppc64-gnu': 4.63.2 - '@rollup/rollup-linux-ppc64-musl': 4.63.2 - '@rollup/rollup-linux-riscv64-gnu': 4.63.2 - '@rollup/rollup-linux-riscv64-musl': 4.63.2 - '@rollup/rollup-linux-s390x-gnu': 4.63.2 - '@rollup/rollup-linux-x64-gnu': 4.63.2 - '@rollup/rollup-linux-x64-musl': 4.63.2 - '@rollup/rollup-openbsd-x64': 4.63.2 - '@rollup/rollup-openharmony-arm64': 4.63.2 - '@rollup/rollup-win32-arm64-msvc': 4.63.2 - '@rollup/rollup-win32-ia32-msvc': 4.63.2 - '@rollup/rollup-win32-x64-gnu': 4.63.2 - '@rollup/rollup-win32-x64-msvc': 4.63.2 + '@rollup/rollup-android-arm-eabi': 4.63.4 + '@rollup/rollup-android-arm64': 4.63.4 + '@rollup/rollup-darwin-arm64': 4.63.4 + '@rollup/rollup-darwin-x64': 4.63.4 + '@rollup/rollup-freebsd-arm64': 4.63.4 + '@rollup/rollup-freebsd-x64': 4.63.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.4 + '@rollup/rollup-linux-arm-musleabihf': 4.63.4 + '@rollup/rollup-linux-arm64-gnu': 4.63.4 + '@rollup/rollup-linux-arm64-musl': 4.63.4 + '@rollup/rollup-linux-loong64-gnu': 4.63.4 + '@rollup/rollup-linux-loong64-musl': 4.63.4 + '@rollup/rollup-linux-ppc64-gnu': 4.63.4 + '@rollup/rollup-linux-ppc64-musl': 4.63.4 + '@rollup/rollup-linux-riscv64-gnu': 4.63.4 + '@rollup/rollup-linux-riscv64-musl': 4.63.4 + '@rollup/rollup-linux-s390x-gnu': 4.63.4 + '@rollup/rollup-linux-x64-gnu': 4.63.4 + '@rollup/rollup-linux-x64-musl': 4.63.4 + '@rollup/rollup-openbsd-x64': 4.63.4 + '@rollup/rollup-openharmony-arm64': 4.63.4 + '@rollup/rollup-win32-arm64-msvc': 4.63.4 + '@rollup/rollup-win32-ia32-msvc': 4.63.4 + '@rollup/rollup-win32-x64-gnu': 4.63.4 + '@rollup/rollup-win32-x64-msvc': 4.63.4 fsevents: 2.3.3 semver@7.7.4: {} @@ -2497,13 +2489,13 @@ snapshots: typescript: 6.0.3 yaml: 2.9.0 - typescript-eslint@8.70.0(eslint@10.10.0)(typescript@6.0.3): + typescript-eslint@8.70.1(eslint@10.11.0)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.10.0)(typescript@6.0.3))(eslint@10.10.0)(typescript@6.0.3) - '@typescript-eslint/parser': 8.70.0(eslint@10.10.0)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.70.0(eslint@10.10.0)(typescript@6.0.3) - eslint: 10.10.0 + '@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.11.0)(typescript@6.0.3))(eslint@10.11.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.70.1(eslint@10.11.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.70.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.70.1(eslint@10.11.0)(typescript@6.0.3) + eslint: 10.11.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -2512,13 +2504,13 @@ snapshots: uc.micro@2.1.0: {} - undici-types@5.26.5: {} + undici-types@6.21.0: {} uri-js@4.4.1: dependencies: punycode: 2.3.1 - viem@2.56.5(typescript@6.0.3)(zod@3.25.76): + viem@2.56.8(typescript@6.0.3)(zod@3.25.76): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 @@ -2526,7 +2518,7 @@ snapshots: '@scure/bip39': 1.6.0 abitype: 1.2.3(typescript@6.0.3)(zod@3.25.76) isows: 1.0.7(ws@8.21.0) - ox: 0.14.44(typescript@6.0.3)(zod@3.25.76) + ox: 0.14.45(typescript@6.0.3)(zod@3.25.76) ws: 8.21.0 optionalDependencies: typescript: 6.0.3 @@ -2535,7 +2527,7 @@ snapshots: - utf-8-validate - zod - viem@2.56.5(typescript@6.0.3)(zod@4.6.5): + viem@2.56.8(typescript@6.0.3)(zod@4.6.5): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 @@ -2543,7 +2535,7 @@ snapshots: '@scure/bip39': 1.6.0 abitype: 1.2.3(typescript@6.0.3)(zod@4.6.5) isows: 1.0.7(ws@8.21.0) - ox: 0.14.44(typescript@6.0.3)(zod@4.6.5) + ox: 0.14.45(typescript@6.0.3)(zod@4.6.5) ws: 8.21.0 optionalDependencies: typescript: 6.0.3 @@ -2552,7 +2544,7 @@ snapshots: - utf-8-validate - zod - vite@8.1.4(@types/node@18.19.130)(terser@5.46.0)(yaml@2.9.0): + vite@8.1.4(@types/node@22.20.4)(terser@5.46.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.5 @@ -2560,15 +2552,15 @@ snapshots: rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 18.19.130 + '@types/node': 22.20.4 fsevents: 2.3.3 terser: 5.46.0 yaml: 2.9.0 - vitest@5.0.0(@types/node@18.19.130)(@vitest/coverage-v8@5.0.0)(vite@8.1.4(@types/node@18.19.130)(terser@5.46.0)(yaml@2.9.0)): + vitest@5.0.1(@types/node@22.20.4)(@vitest/coverage-v8@5.0.1)(vite@8.1.4(@types/node@22.20.4)(terser@5.46.0)(yaml@2.9.0)): dependencies: '@types/chai': 5.2.3 - '@vitest/mocker': 5.0.0(vite@8.1.4(@types/node@18.19.130)(terser@5.46.0)(yaml@2.9.0)) + '@vitest/mocker': 5.0.1(vite@8.1.4(@types/node@22.20.4)(terser@5.46.0)(yaml@2.9.0)) chai: 6.2.2 es-module-lexer: 2.3.2 expect-type: 1.4.0 @@ -2579,11 +2571,11 @@ snapshots: tinybench: 6.1.4 tinyexec: 1.3.0 tinyglobby: 0.2.17 - vite: 8.1.4(@types/node@18.19.130)(terser@5.46.0)(yaml@2.9.0) + vite: 8.1.4(@types/node@22.20.4)(terser@5.46.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 18.19.130 - '@vitest/coverage-v8': 5.0.0(vitest@5.0.0) + '@types/node': 22.20.4 + '@vitest/coverage-v8': 5.0.1(vitest@5.0.1) transitivePeerDependencies: - msw diff --git a/scripts/check-changelog-heading.mjs b/scripts/check-changelog-heading.mjs new file mode 100644 index 0000000..6ea6308 --- /dev/null +++ b/scripts/check-changelog-heading.mjs @@ -0,0 +1,43 @@ +// CI check (.github/workflows/ci.yml): the top `## ` heading of CHANGELOG.md must name the release. +// publish.yml takes the GitHub Release notes from the line that equals `## `, +// so a heading with a suffix (e.g. still "(unreleased)") would release without notes. +// +// - Target `main` (and anything that is not `release/**`): exactly `## `. +// - Target `release/**`: that, or `## (unreleased)`. PRs into a release branch carry no +// version bump (package.json still has the last published version), so the unreleased heading is +// not compared with package.json. Only the last one, the release prep (into the release branch or +// the final PR to `main`), bumps the version and renames the heading to `## `. +// +// Usage: node scripts/check-changelog-heading.mjs [target-branch] +// The target defaults to $TARGET_BRANCH, then `main`. Env CHANGELOG and PACKAGE_JSON override +// the file paths (for tests). Exits 1 with a GitHub `::error::` annotation on failure. +/* global process, console */ +import { readFileSync } from 'node:fs'; +import { parseSemver } from './release-plan.mjs'; + +/** Returns null when `heading` is acceptable for `target`, or the reason it is not. */ +function checkHeading(heading, version, target) { + const release = /^release\//.test(target); + const expected = `## ${version}`; + if (heading === null) return `CHANGELOG.md has no "## " heading; expected "${expected}".`; + if (heading === expected) return null; + const unreleased = /^## (\S+) \(unreleased\)$/.exec(heading); + if (release && unreleased && parseSemver(unreleased[1])) return null; + const allowed = release ? `"${expected}" or "## (unreleased)"` : `"${expected}"`; + const hint = + unreleased && !release + ? ' A heading still marked "(unreleased)" cannot reach main: bump package.json and rename it.' + : ''; + return `The top CHANGELOG.md heading is "${heading}"; into ${target} it must be ${allowed} (package.json version ${version}).${hint}`; +} + +const target = process.argv[2] || process.env.TARGET_BRANCH || 'main'; +const changelog = readFileSync(process.env.CHANGELOG || 'CHANGELOG.md', 'utf8'); +const { version } = JSON.parse(readFileSync(process.env.PACKAGE_JSON || 'package.json', 'utf8')); +const heading = changelog.split(/\r?\n/).find((line) => line.startsWith('## ')) ?? null; +const error = checkHeading(heading, version, target); +if (error) { + console.error(`::error file=CHANGELOG.md::${error}`); + process.exit(1); +} +console.log(`CHANGELOG.md heading "${heading}" is valid for a PR into ${target}.`); diff --git a/scripts/release-plan.d.mts b/scripts/release-plan.d.mts new file mode 100644 index 0000000..bc3a7df --- /dev/null +++ b/scripts/release-plan.d.mts @@ -0,0 +1,16 @@ +// Types for scripts/release-plan.mjs, so test/release-scripts.spec.ts type-checks its import. +export interface SemVer { + major: bigint; + minor: bigint; + patch: bigint; + prerelease: string[]; +} +export function parseSemver(version: string): SemVer | null; +export function compareSemver(a: string, b: string): -1 | 0 | 1; +export function distTagFor(version: string, latest: string | null): string; +export function changelogVersions(changelog: string): string[]; +export function skippedVersions(version: string, published: string[], changelog: string): string[]; +export function parseNpmView( + raw: string, + rc: string | number +): { latest: string | null; versions: string[] }; diff --git a/scripts/release-plan.mjs b/scripts/release-plan.mjs new file mode 100644 index 0000000..a3e3448 --- /dev/null +++ b/scripts/release-plan.mjs @@ -0,0 +1,157 @@ +// Release planning for .github/workflows/publish.yml, called by scripts/release-state.sh. It only +// computes; it never publishes, tags or talks to npm itself (the caller passes the `npm view` +// output in). Plain Node, no dependencies. Tested by test/release-scripts.spec.ts. +// +// CLI: node scripts/release-plan.mjs +// Env: +// VERSION the version being released (package.json `version`) +// NPM_VIEW stdout of `npm view versions dist-tags --json` +// NPM_RC that command's exit code +// CHANGELOG path of the changelog (default CHANGELOG.md) +// Prints, one per line: +// latest= +// dist_tag= +// skipped= +// Exits 1 (message on stderr) when VERSION is not semver or npm could not be queried: only an +// E404 means "package not on npm yet"; any other failure must fail the job, never publish. +/* global process, console */ +import { readFileSync } from 'node:fs'; +import { pathToFileURL } from 'node:url'; + +// The official SemVer 2.0.0 regex (semver.org), with named groups. +const SEMVER = + /^(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:-(?(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; + +/** Parses a SemVer 2.0.0 version, or returns null. Numbers are BigInt: no precision limit. */ +export function parseSemver(version) { + const m = SEMVER.exec(String(version)); + if (!m) return null; + const { major, minor, patch, prerelease } = m.groups; + return { + major: BigInt(major), + minor: BigInt(minor), + patch: BigInt(patch), + prerelease: prerelease ? prerelease.split('.') : [], + }; +} + +function cmp(a, b) { + return a < b ? -1 : a > b ? 1 : 0; +} + +/** + * SemVer 2.0.0 precedence: -1, 0 or 1. Build metadata is ignored; a prerelease sorts before its + * release; prerelease identifiers compare numerically when both are numeric, numeric before + * alphanumeric, otherwise ASCII order, and a shorter prefix sorts first. + */ +export function compareSemver(a, b) { + const x = parseSemver(a); + const y = parseSemver(b); + if (!x) throw new Error(`not a semver version: ${a}`); + if (!y) throw new Error(`not a semver version: ${b}`); + const core = cmp(x.major, y.major) || cmp(x.minor, y.minor) || cmp(x.patch, y.patch); + if (core) return core; + if (!x.prerelease.length && !y.prerelease.length) return 0; + if (!x.prerelease.length) return 1; // 1.0.0 > 1.0.0-rc.1 + if (!y.prerelease.length) return -1; + for (let i = 0; i < Math.max(x.prerelease.length, y.prerelease.length); i++) { + const p = x.prerelease[i]; + const q = y.prerelease[i]; + if (p === undefined) return -1; + if (q === undefined) return 1; + const pn = /^\d+$/.test(p); + const qn = /^\d+$/.test(q); + const c = pn && qn ? cmp(BigInt(p), BigInt(q)) : pn ? -1 : qn ? 1 : cmp(p, q); + if (c) return c; + } + return 0; +} + +/** + * The dist-tag to publish `version` under, given the current `latest` (null when the package or + * the tag does not exist), so `latest` never moves backwards or onto a prerelease: + * - lower than `latest`: `backport-.` (e.g. a cancelled 0.29.5 re-run after 0.30.1 + * is out). It names the release line, and it cannot be read as a semver range, which npm + * rejects as a tag name (a `v0.29`-style tag would be one). + * - a prerelease, not lower: `next`, npm's conventional prerelease tag. + * - otherwise (higher, equal, no `latest` yet): `latest`. + */ +export function distTagFor(version, latest) { + const v = parseSemver(version); + if (!v) throw new Error(`not a semver version: ${version}`); + if (latest && compareSemver(version, latest) < 0) return `backport-${v.major}.${v.minor}`; + if (v.prerelease.length) return 'next'; + return 'latest'; +} + +/** The released versions in a changelog: every `## ` heading, exactly (no suffix). */ +export function changelogVersions(changelog) { + const out = []; + for (const line of changelog.split(/\r?\n/)) { + const m = /^## (\S+)$/.exec(line); + if (m && parseSemver(m[1])) out.push(m[1]); + } + return out; +} + +/** + * CHANGELOG versions that were never published: above the highest npm version below `version` + * (the last one published before it) and below `version`, and not on npm. These are releases + * whose publish run was cancelled or never approved. Sorted ascending. + */ +export function skippedVersions(version, published, changelog) { + const onNpm = new Set(published); + const before = published.filter((p) => parseSemver(p) && compareSemver(p, version) < 0); + const last = before.sort(compareSemver).at(-1) ?? null; + return changelogVersions(changelog) + .filter((v) => !onNpm.has(v)) + .filter((v) => compareSemver(v, version) < 0 && (!last || compareSemver(v, last) > 0)) + .sort(compareSemver); +} + +/** + * Interprets `npm view versions dist-tags --json`: `{ latest, versions }`, with + * `{ latest: null, versions: [] }` for an E404. Throws on any other failure. + */ +export function parseNpmView(raw, rc) { + let data; + try { + data = JSON.parse(raw); + } catch { + throw new Error(`npm view exited ${rc} with unparseable output`); + } + if (String(rc) !== '0') { + const code = data && data.error && data.error.code; + if (code === 'E404') return { latest: null, versions: [] }; + throw new Error(`npm view exited ${rc} with ${code || 'no error code'}`); + } + if (!data || typeof data !== 'object') { + throw new Error(`npm view exited 0 but returned ${JSON.stringify(data)}`); + } + // One published version prints a bare string instead of an array. + const versions = [].concat(data.versions ?? []); + const latest = (data['dist-tags'] && data['dist-tags'].latest) || null; + if (!versions.every((v) => typeof v === 'string') || (latest && !parseSemver(latest))) { + throw new Error(`npm view exited 0 but returned ${JSON.stringify(data)}`); + } + return { latest, versions }; +} + +function main() { + const { VERSION: version = '', NPM_VIEW: raw = '', NPM_RC: rc = '' } = process.env; + if (!parseSemver(version)) throw new Error(`package.json version is not semver: ${version}`); + const { latest, versions } = parseNpmView(raw, rc); + const changelog = readFileSync(process.env.CHANGELOG || 'CHANGELOG.md', 'utf8'); + console.log(`latest=${latest ?? ''}`); + console.log(`dist_tag=${distTagFor(version, latest)}`); + console.log(`skipped=${skippedVersions(version, versions, changelog).join(' ')}`); +} + +if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { + try { + main(); + } catch (err) { + console.error(err.message); + process.exit(1); + } +} diff --git a/scripts/release-state.sh b/scripts/release-state.sh index c4b5aff..77a0f0c 100755 --- a/scripts/release-state.sh +++ b/scripts/release-state.sh @@ -11,6 +11,15 @@ # so only (re)create the tag and the GitHub Release # action=skip the version is on npm from another commit (a merge that did not bump # `version`, or a later re-run): nothing to do +# dist_tag= the npm dist-tag to publish under (scripts/release-plan.mjs): `latest`, or +# `backport-.` when the version is lower than the current +# `latest` (a re-run of a cancelled older release), or `next` for a prerelease, +# so `latest` never moves backwards +# latest= the current `latest` dist-tag (empty when the package is not on npm) +# +# It also warns (annotation and job summary) about skipped releases: CHANGELOG versions between +# the last published version and this one that are not on npm (their publish run was cancelled +# by a later merge or never approved). # # `npm view` failure modes: only an E404 means "not published". Any other failure (network, # registry error, unparseable output) exits non-zero, so the job fails instead of publishing. @@ -59,6 +68,26 @@ state=$( ' ) +# The package's versions and dist-tags, for the dist-tag and the skipped-release check. Same +# failure policy: an E404 (package never published) is fine, any other failure fails the job. +set +e +pkg_view=$(npm view "$name" versions dist-tags --json --prefer-online 2>/dev/null) +pkg_rc=$? +set -e +plan=$(NPM_VIEW="$pkg_view" NPM_RC="$pkg_rc" VERSION="$version" node "$(dirname "$0")/release-plan.mjs") || { + echo "::error::Could not plan the release of $name@$version (see above)" >&2 + exit 1 +} +dist_tag=$(sed -n 's/^dist_tag=//p' <<<"$plan") +latest=$(sed -n 's/^latest=//p' <<<"$plan") +skipped=$(sed -n 's/^skipped=//p' <<<"$plan") +# Belt and braces: never hand `npm publish --tag` an empty or unexpected tag (an empty one would +# fail the publish, a wrong one could move `latest`). +if ! [[ "$dist_tag" =~ ^(latest|next|backport-[0-9]+\.[0-9]+)$ ]]; then + echo "::error::Unexpected dist-tag '$dist_tag' for $name@$version from release-plan.mjs" >&2 + exit 1 +fi + case "$state" in missing) action='publish' @@ -82,10 +111,28 @@ esac echo "version=$version" echo "tag=$tag" echo "action=$action" + echo "dist_tag=$dist_tag" + echo "latest=$latest" } >>"$out" echo "### Release: $action" >>"$summary" echo "$msg" >>"$summary" +if [ "$dist_tag" != latest ]; then + echo "" >>"$summary" + if [ "$dist_tag" = next ]; then + echo "\`$version\` is a prerelease: it goes out under the \`next\` dist-tag, \`latest\` stays at \`${latest:-none}\`." >>"$summary" + else + echo "\`$version\` is lower than \`latest\` (\`$latest\`): it goes out under the \`$dist_tag\` dist-tag and \`latest\` does not move." >>"$summary" + fi +fi +if [ -n "$skipped" ]; then + echo "::warning::CHANGELOG versions never published to npm (publish run cancelled or not approved): $skipped" + { + echo "" + echo "> [!WARNING]" + echo "> These CHANGELOG versions, between the last published version and \`$version\`, are not on npm (their publish run was cancelled by a later merge or never approved): \`${skipped// /\`, \`}\`. See CONTRIBUTING.md \"Releases\" to recover." + } >>"$summary" +fi if [ "$action" = publish ]; then echo "::notice::$name@$version is not on npm: will publish" else diff --git a/scripts/smoke-timeout.mjs b/scripts/smoke-timeout.mjs index cd3a1e7..3bbc8c8 100644 --- a/scripts/smoke-timeout.mjs +++ b/scripts/smoke-timeout.mjs @@ -1,4 +1,4 @@ -// Runtime smoke for the published CJS build (dist/), run on the Node floor (18) in CI where Vitest +// Runtime smoke for the published CJS build (dist/), run on the Node floor (22) in CI where Vitest // cannot run. Proves a real `AbortSignal.timeout()` abort surfaces as a GlassnodeNetworkError // with `timedOut === true` on this runtime, and that a per-call `signal` combined with that // timeout (the client's own AbortSignal.any() stand-in) cancels as a GlassnodeAbortError. Not published (package.json `files` is dist-only). @@ -19,7 +19,14 @@ function hangingFetch(_url, init) { }); } -const api = new GlassnodeAPI({ apiKey: 'smoke-key', fetch: hangingFetch, timeout: 20 }); +// A single attempt (`maxRetries: 0`, the default is 2): the smoke checks how one timeout abort +// surfaces, and retry backoff (up to 1 s + 2 s) would only slow it down against the watchdog. +const api = new GlassnodeAPI({ + apiKey: 'smoke-key', + fetch: hangingFetch, + timeout: 20, + maxRetries: 0, +}); // AbortSignal.timeout()'s timer does not keep Node alive (a real fetch's socket would), so hold // the event loop open with a watchdog that also fails the smoke if the abort never happens. diff --git a/src/glassnode-api.ts b/src/glassnode-api.ts index 4a5d451..419ce1c 100644 --- a/src/glassnode-api.ts +++ b/src/glassnode-api.ts @@ -5,6 +5,8 @@ import { GlassnodeFetch, DEFAULT_API_URL, X402_API_URL, + DEFAULT_MAX_RETRIES, + DEFAULT_X402_MAX_RETRIES, } from './types/config.js'; import type { z, ZodType, ZodError } from 'zod'; import { @@ -406,7 +408,7 @@ function metricSchema(options: unknown): ZodType | undefined { /** * An AbortSignal that aborts (with the same `reason`) as soon as either input signal aborts, and a * `dispose` that removes the listeners it added. A small stand-in for `AbortSignal.any()`, which - * only exists from Node 20.3 (this package supports Node >= 18). Calling `dispose` once the + * Node >= 22 has but older browsers the bundles run in do not. Calling `dispose` once the * attempt is over keeps a long-lived caller signal from accumulating listeners across calls. */ function combineSignals( @@ -544,7 +546,11 @@ export class GlassnodeAPI { this.logger = validatedConfig.logger; this.hooks = validatedConfig.hooks; this.fetchFn = validatedConfig.fetch ?? globalThis.fetch; - this.maxRetries = validatedConfig.maxRetries; + // The default depends on `x402`, so it is applied here rather than in the schema (which stays + // a plain object schema): a retry through a caller-supplied payment fetch could pay twice. + this.maxRetries = + validatedConfig.maxRetries ?? + (validatedConfig.x402 ? DEFAULT_X402_MAX_RETRIES : DEFAULT_MAX_RETRIES); this.retryDelay = validatedConfig.retryDelay; this.maxRetryDelay = validatedConfig.maxRetryDelay; this.timeout = validatedConfig.timeout; diff --git a/src/types/config.ts b/src/types/config.ts index e167ca1..bbefe1b 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -7,16 +7,6 @@ import type { GlassnodeHooks } from './hooks.js'; */ export type Logger = (message: string, ...args: unknown[]) => void; -/** - * The standard `fetch` type (`typeof fetch`). - * - * @deprecated The `fetch` config option is typed as {@link GlassnodeFetch}, the call the client - * actually makes, which also accepts string-only custom fetches. `FetchFn` still means - * `typeof fetch` (and a `FetchFn` value is still accepted as the option); use `GlassnodeFetch` to - * type a custom fetch for the client. - */ -export type FetchFn = typeof fetch; - /** * Type of the `fetch` config option: the call the client makes. It is only ever called with a * string URL, as `fetch(url)` or `fetch(url, init)`, and must resolve to a standard `Response`. @@ -33,6 +23,21 @@ export const DEFAULT_API_URL = 'https://api.glassnode.com'; export const X402_API_URL = 'https://x402.glassnode.com'; // A testnet/staging x402 endpoint is not hardcoded here — pass its URL via the `apiUrl` config option. +/** + * Default `maxRetries` when `x402` is off: up to 3 attempts for a `429`/`5xx` or a transport + * failure. Applied by the `GlassnodeAPI` constructor when `maxRetries` is not set; + * {@link GlassnodeConfigSchema} leaves an unset `maxRetries` as `undefined`. + */ +export const DEFAULT_MAX_RETRIES = 2; + +/** + * Default `maxRetries` when `x402` is on: no retries. The client cannot tell whether a + * caller-supplied payment fetch refuses to retry after a signed payment was sent, so it never + * retries a paid call unless the caller opts in with an explicit `maxRetries` (safe with the + * fetch from `createX402Fetch`, which only lets unpaid attempts be retried). + */ +export const DEFAULT_X402_MAX_RETRIES = 0; + /** * Largest timer delay (ms) every runtime supports: 2^31 - 1 (~24.8 days). Larger delays overflow * `setTimeout` (Node fires them after 1 ms) and make `AbortSignal.timeout()` throw a RangeError. @@ -116,9 +121,18 @@ export const GlassnodeConfigSchema = z /** * Maximum number of retries for retryable failures: a `429`/`5xx` response, or a transport - * failure (`GlassnodeNetworkError`, including a per-attempt `timeout`). Default 0 (no retries). + * failure (`GlassnodeNetworkError`, including a per-attempt `timeout`). Default 2 + * ({@link DEFAULT_MAX_RETRIES}, up to 3 attempts); `0` disables retries. The schema leaves an + * unset value `undefined`; the `GlassnodeAPI` constructor applies the default, which depends + * on `x402`. + * + * In `x402` mode the default is 0 ({@link DEFAULT_X402_MAX_RETRIES}): the client cannot tell + * whether the `fetch` it was given refuses to retry after a signed payment was sent. The fetch from `createX402Fetch` does + * (such a failure becomes a never-retried `GlassnodePaymentError`), so with it an explicit + * `maxRetries` only ever retries unpaid requests; with a bare x402 wrapper, a retry after a + * paid `5xx` or a timeout would sign a new payment and could pay twice. */ - maxRetries: z.number().int().nonnegative().default(0), + maxRetries: z.number().int().nonnegative().optional(), /** Base delay in milliseconds between retries (doubles each attempt, then full jitter). */ retryDelay: timerMs().default(1000), diff --git a/src/types/metadata.ts b/src/types/metadata.ts index e6c0562..af772b0 100644 --- a/src/types/metadata.ts +++ b/src/types/metadata.ts @@ -2,8 +2,7 @@ * Metadata response types * * Timestamps: the API sends every point in time as unix **seconds** (a number). The schemas - * pass them through unchanged — with one exception, `MetricMetadata.modified`, which is - * converted to a JS `Date`. Convert the others yourself with `new Date(seconds * 1000)`. + * pass them all through unchanged; convert one yourself with `new Date(seconds * 1000)`. * Durations are not timestamps: the lag percentiles of {@link MetricStatsResponse} are lengths of * time in their entry's `unit` (e.g. seconds) and must not be passed to `new Date()`. */ @@ -146,43 +145,6 @@ export const AssetMetadataResponseSchema = z.array(AssetMetadataSchema); */ export type AssetMetadataResponse = z.infer; -/** - * Metric tier schema - * - * @deprecated Not used by any schema or client method, and it does not match the API: - * the `tier` field on metric metadata is a number (e.g. `2`), not one of these strings. - * Use `MetricMetadata['tier']` (a `number`) to type it instead. This export will be - * removed in the next major release (1.0). - */ -export const MetricTierSchema = z.enum(['free', 'tier1', 'tier2', 'tier3', 'tier4', 'tier5']); - -/** - * Metric tier type - * - * @deprecated Does not match the API: the `tier` field on metric metadata is a number - * (e.g. `2`), not one of these strings. Use `MetricMetadata['tier']` (a `number`) instead. - * This export will be removed in the next major release (1.0). - */ -export type MetricTier = z.infer; - -/** - * Metric data type schema - * - * @deprecated Not used by any schema or client method and not validated against any - * API response. If you need these values, define your own enum. This export will be - * removed in the next major release (1.0). - */ -export const MetricDataTypeSchema = z.enum(['average', 'sum', 'count', 'percentage', 'ratio']); - -/** - * Metric data type - * - * @deprecated Not used by any schema or client method and not validated against any - * API response. If you need these values, define your own type. This export will be - * removed in the next major release (1.0). - */ -export type MetricDataType = z.infer; - /** * Metric descriptors schema (human-readable names, tags, descriptions) */ @@ -211,23 +173,16 @@ export const MetricMetadataSchema = z.object({ /** * Access tier required for this metric, as a non-negative integer (e.g. `2`). - * Type it as `MetricMetadata['tier']`; the deprecated string-based `MetricTier` - * does not describe this field. + * Type it as `MetricMetadata['tier']`. */ tier: z.number().int().nonnegative(), /** - * When the metric's metadata was last updated, as a JS `Date`. - * - * The API sends unix seconds; the schema converts them to a `Date` (the only time field - * that is converted — see `timerange` and the time series/bulk `t`, which stay numbers). - * `undefined` when the field is absent **or `0`**: `0` is treated as "no modification time - * recorded", not as 1970-01-01. + * When the metric's metadata was last updated, in unix seconds (as sent by the API, like + * every other time field). `undefined` when the API omits it. Convert with + * `new Date(modified * 1000)`. */ - modified: z - .number() - .optional() - .transform((val) => (val ? new Date(val * 1000) : undefined)), + modified: z.number().optional(), /** * Whether this is a point-in-time metric @@ -257,29 +212,36 @@ export const MetricMetadataSchema = z.object({ .optional(), /** - * Reference links for this metric + * Reference links for this metric (docs, Studio, variant paths). Optional: `undefined` when the + * API omits it, so read it with `?.` (e.g. `meta.refs?.docs`). */ - refs: z.object({ - docs: z.string().optional(), - studio: z.string().optional(), - metric_variant: z - .object({ - base: z.string().optional(), - bulk: z.string().optional(), - pit: z.string().optional(), - }) - .optional(), - }), + refs: z + .object({ + docs: z.string().optional(), + studio: z.string().optional(), + metric_variant: z + .object({ + base: z.string().optional(), + bulk: z.string().optional(), + pit: z.string().optional(), + }) + .optional(), + }) + .optional(), /** - * Queried parameters for the metric + * The request's parameters as the API echoes them back (e.g. `{ path: '/market/price_usd_close' }`, + * plus `a` when an asset was passed). Optional: `undefined` when the API omits it, so read it + * with `?.`. */ - queried: z.record(z.string(), z.any()), + queried: z.record(z.string(), z.any()).optional(), /** - * List of all allowed parameters and their values for the metric + * All allowed parameters of the metric and their values, keyed by parameter name (e.g. + * `{ a: ['BTC', 'ETH', …], i: ['24h', …] }`). Optional, with no `{}` default: `undefined` when + * the API omits it, so read it with `?.` (e.g. `meta.parameters?.a ?? []`). */ - parameters: z.record(z.string(), z.array(z.string())), + parameters: z.record(z.string(), z.array(z.string())).optional(), /** * Default values of the parameters that have one, keyed by parameter name (e.g. diff --git a/src/x402.ts b/src/x402.ts index ce4a476..ae9a05c 100644 --- a/src/x402.ts +++ b/src/x402.ts @@ -103,7 +103,9 @@ export function createMaxAmountPolicy(maxAtomic: bigint) { * * A rejection of, or a non-2xx answer to, the *unpaid* request (no payment signed yet) is passed * through unchanged, so the client still reports and retries it (`GlassnodeNetworkError`, or - * `GlassnodeApiError` for `429`/`5xx`). A `402` the server returns to the paid request (it + * `GlassnodeApiError` for `429`/`5xx`) when retries are enabled: in `x402` mode `maxRetries` + * defaults to 0, and an explicit value is safe with this fetch because only unpaid attempts are + * ever retried. A `402` the server returns to the paid request (it * refused the payment) is not an error here either; the client reports it as `GlassnodeApiError` * (status 402), which it never retries. */ diff --git a/test/call-options.spec.ts b/test/call-options.spec.ts index 8e17e33..016cd32 100644 --- a/test/call-options.spec.ts +++ b/test/call-options.spec.ts @@ -163,7 +163,12 @@ describe('per-call options: signal', () => { }); it('a caller abort is distinguishable from a timeout', async () => { - const api = new GlassnodeAPI({ apiKey: API_KEY, fetch: hangingFetch(), timeout: 20 }); + const api = new GlassnodeAPI({ + apiKey: API_KEY, + fetch: hangingFetch(), + timeout: 20, + maxRetries: 0, + }); const timeoutErr = await caught(api.getMetricList()); expect(timeoutErr).toBeInstanceOf(GlassnodeNetworkError); @@ -206,7 +211,12 @@ describe('per-call options: timeout', () => { }); it('a shorter per-call timeout overrides a long config timeout', async () => { - const api = new GlassnodeAPI({ apiKey: API_KEY, fetch: hangingFetch(), timeout: 60_000 }); + const api = new GlassnodeAPI({ + apiKey: API_KEY, + fetch: hangingFetch(), + timeout: 60_000, + maxRetries: 0, + }); const started = Date.now(); const err = await caught(api.getMetricList({ timeout: 20 })); expect((err as GlassnodeNetworkError).timedOut).toBe(true); @@ -225,7 +235,7 @@ describe('per-call options: timeout', () => { }); }) ); - const api = new GlassnodeAPI({ apiKey: API_KEY, fetch: fetchFn, timeout: 20 }); + const api = new GlassnodeAPI({ apiKey: API_KEY, fetch: fetchFn, timeout: 20, maxRetries: 0 }); await expect(api.getMetricList()).rejects.toBeInstanceOf(GlassnodeNetworkError); await expect(api.getMetricList({ timeout: 5_000 })).resolves.toEqual(mockMetricListResponse); }); @@ -252,7 +262,7 @@ describe('per-call options: signal + timeout', () => { }); it('the timeout still fires through the combined signal as timedOut', async () => { - const api = new GlassnodeAPI({ apiKey: API_KEY, fetch: hangingFetch() }); + const api = new GlassnodeAPI({ apiKey: API_KEY, fetch: hangingFetch(), maxRetries: 0 }); const controller = new AbortController(); const err = await caught(api.getMetricList({ signal: controller.signal, timeout: 20 })); expect(err).toBeInstanceOf(GlassnodeNetworkError); diff --git a/test/config.spec.ts b/test/config.spec.ts index 6fa8269..45d20f3 100644 --- a/test/config.spec.ts +++ b/test/config.spec.ts @@ -3,7 +3,8 @@ import { GlassnodeConfigSchema, DEFAULT_API_URL, X402_API_URL, - type FetchFn, + DEFAULT_MAX_RETRIES, + DEFAULT_X402_MAX_RETRIES, type GlassnodeConfig, type GlassnodeFetch, type Logger, @@ -38,6 +39,56 @@ describe('GlassnodeConfigSchema', () => { expect(parsed.apiUrl).toBeUndefined(); }); + it('stays a plain object schema (.shape, .pick, .extend, .partial still work)', () => { + expect(GlassnodeConfigSchema.shape).toBeDefined(); + expect(GlassnodeConfigSchema.shape.maxRetries).toBeDefined(); + expect(typeof GlassnodeConfigSchema.pick).toBe('function'); + expect(typeof GlassnodeConfigSchema.extend).toBe('function'); + expect(typeof GlassnodeConfigSchema.partial).toBe('function'); + }); + + describe('maxRetries default', () => { + const fetchFn = (async () => new Response()) as unknown as typeof fetch; + // The resolved value lives on the client; the schema only validates. + const resolved = (config: GlassnodeConfig) => + // @ts-expect-error: reading a private property + new GlassnodeAPI(config).maxRetries as number; + + it('exports the documented defaults: 2, and 0 in x402 mode', () => { + expect(DEFAULT_MAX_RETRIES).toBe(2); + expect(DEFAULT_X402_MAX_RETRIES).toBe(0); + }); + + it('leaves an unset maxRetries undefined in the parsed config', () => { + expect(GlassnodeConfigSchema.parse({ apiKey: 'k' }).maxRetries).toBeUndefined(); + }); + + it('the client defaults maxRetries to 2', () => { + expect(resolved({ apiKey: 'k' })).toBe(2); + }); + + it('the client defaults maxRetries to 0 in x402 mode (a retry could pay twice with a fetch that is not from createX402Fetch)', () => { + expect(resolved({ x402: true, fetch: fetchFn })).toBe(0); + // Even with an API key alongside x402. + expect(resolved({ x402: true, apiKey: 'k', fetch: fetchFn })).toBe(0); + }); + + it('keeps an explicit maxRetries in either mode, including 0', () => { + expect(resolved({ apiKey: 'k', maxRetries: 0 })).toBe(0); + expect(resolved({ apiKey: 'k', maxRetries: 5 })).toBe(5); + expect(resolved({ x402: true, fetch: fetchFn, maxRetries: 3 })).toBe(3); + expect(GlassnodeConfigSchema.parse({ apiKey: 'k', maxRetries: 5 }).maxRetries).toBe(5); + }); + + it('still rejects a negative or non-integer maxRetries', () => { + for (const maxRetries of [-1, 1.5]) { + const result = GlassnodeConfigSchema.safeParse({ apiKey: 'k', maxRetries }); + expect(result.success).toBe(false); + expect(result.error?.issues[0].path).toEqual(['maxRetries']); + } + }); + }); + describe('timer bounds (timeout, retryDelay, maxRetryDelay)', () => { // Timers only take a 32-bit signed delay; larger values overflow (Node fires them after 1 ms) // and `AbortSignal.timeout()` throws a RangeError above 2^32 - 1. @@ -84,9 +135,8 @@ describe('logger and fetch options', () => { expectTypeOf().toEqualTypeOf< (input: string, init?: RequestInit) => Promise >(); - // The deprecated `FetchFn` keeps its meaning (`typeof fetch`) and still fits the option. - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toExtend(); + // The standard `fetch` type still fits the option. + expectTypeOf().toExtend(); }); it('contextually types inline logger and fetch callbacks', () => { diff --git a/test/contract.spec.ts b/test/contract.spec.ts index 99a78fa..9df6896 100644 --- a/test/contract.spec.ts +++ b/test/contract.spec.ts @@ -5,7 +5,7 @@ * * - the call succeeds, i.e. the recorded response passes the library's Zod schema; * - the result has the documented shape and the recorded values (timestamps stay unix seconds, - * except `MetricMetadata.modified`, which becomes a `Date`); + * `MetricMetadata.modified` included); * - the request URL the client builds matches the recorded endpoint and query, plus the `f=json` * the client adds to metric data calls and the (query-string) API key; * - the fields the schemas strip from the real response are exactly the known, unmodelled ones @@ -156,23 +156,29 @@ async function metricMetadataCase( expect(result.path).toBe(metadataPath(entry)); expect(Number.isInteger(result.tier)).toBe(true); expect(result.tier).toBe(body.tier); - // The one converted time field: unix seconds → Date. - expect(result.modified).toBeInstanceOf(Date); - expect(result.modified!.getTime()).toBe(body.modified * 1000); + // `modified` stays unix seconds (a number), like every other time field. + expect(result.modified).toBe(body.modified); + expect(typeof result.modified).toBe('number'); // `timerange` stays unix seconds (numbers). expect(result.timerange).toEqual(body.timerange); expect(typeof result.timerange!.min).toBe('number'); expect(result.timerange!.min).toBeLessThan(result.timerange!.max); expect(result.bulk_supported).toBe(body.bulk_supported); + // `refs`, `queried` and `parameters` are optional in the schema, but the recorded responses + // all carry them. Checked before any dereference below, so a re-recorded response without them + // fails with a clear assertion: that is a change in the recorded API response, not a schema bug. + expect(result.refs).toBeDefined(); + expect(result.queried).toBeDefined(); + expect(result.parameters).toBeDefined(); expect(result.parameters).toEqual(body.parameters); // Only sent for a metric with a defaulted parameter; `undefined` (not `{}`) otherwise. expect(result.parameters_defaults).toEqual(body.parameters_defaults); expect(result.queried).toEqual(body.queried); - expect(result.queried.path).toBe(metadataPath(entry)); + expect(result.queried!.path).toBe(metadataPath(entry)); expect(result.refs).toEqual(body.refs); expect(result.descriptors).toEqual(body.descriptors); expect(result.descriptors?.name).toEqual(expect.any(String)); - expect(result.parameters.f).toContain('json'); + expect(result.parameters!.f).toContain('json'); return result; } @@ -279,8 +285,8 @@ const CASES: Record = { 'metric-metadata-price-usd-close': async (api, entry, raw) => { const result = await metricMetadataCase(api, entry, raw); expect(result.tier).toBe(1); - expect(result.parameters.a).toEqual(expect.arrayContaining(['BTC', 'ETH', 'SOL'])); - expect(result.parameters.a.length).toBeGreaterThan(100); + expect(result.parameters!.a).toEqual(expect.arrayContaining(['BTC', 'ETH', 'SOL'])); + expect(result.parameters!.a.length).toBeGreaterThan(100); expect(result.queried).toEqual({ path: '/market/price_usd_close' }); expect(result.parameters_defaults).toBeUndefined(); return result; @@ -290,8 +296,8 @@ const CASES: Record = { 'metric-metadata-balance-exchanges-btc': async (api, entry, raw) => { const result = await metricMetadataCase(api, entry, raw); expect(result.tier).toBe(2); - expect(result.parameters.a).toEqual(['BTC']); - expect(result.parameters.e).toEqual(expect.arrayContaining(['aggregated', 'binance'])); + expect(result.parameters!.a).toEqual(['BTC']); + expect(result.parameters!.e).toEqual(expect.arrayContaining(['aggregated', 'binance'])); expect(result.parameters_defaults).toEqual({ e: ['aggregated'] }); expectTypeOf(result.parameters_defaults).toEqualTypeOf | undefined>(); expect(result.queried).toEqual({ a: 'BTC', path: '/distribution/balance_exchanges' }); @@ -302,8 +308,8 @@ const CASES: Record = { 'metric-metadata-us-spot-etf-balances-all': async (api, entry, raw) => { const result = await metricMetadataCase(api, entry, raw); expect(result.tier).toBe(1); - expect(result.parameters.a).toEqual(expect.arrayContaining(['BTC', 'ETH'])); - expect(result.parameters.i).toEqual(['24h', '1w', '1month']); + expect(result.parameters!.a).toEqual(expect.arrayContaining(['BTC', 'ETH'])); + expect(result.parameters!.i).toEqual(['24h', '1w', '1month']); expect(result.queried).toEqual({ path: '/institutions/us_spot_etf_balances_all' }); return result; }, diff --git a/test/errors.spec.ts b/test/errors.spec.ts index cd72e29..b4880c1 100644 --- a/test/errors.spec.ts +++ b/test/errors.spec.ts @@ -83,7 +83,9 @@ describe('client failure paths', () => { it('network failure -> GlassnodeNetworkError with timedOut=false and cause', async () => { const cause = new TypeError('Failed to fetch'); - const err = await caught(api(vi.fn().mockRejectedValue(cause)).getMetricList()); + const err = await caught( + api(vi.fn().mockRejectedValue(cause), { maxRetries: 0 }).getMetricList() + ); expect(err).toBeInstanceOf(GlassnodeNetworkError); expect(err).toBeInstanceOf(GlassnodeError); const e = err as GlassnodeNetworkError; @@ -95,7 +97,7 @@ describe('client failure paths', () => { it('timeout -> GlassnodeNetworkError with timedOut=true', async () => { const cause = new DOMException('The operation was aborted due to timeout', 'TimeoutError'); const err = await caught( - api(vi.fn().mockRejectedValue(cause), { timeout: 10 }).getMetricList() + api(vi.fn().mockRejectedValue(cause), { timeout: 10, maxRetries: 0 }).getMetricList() ); expect(err).toBeInstanceOf(GlassnodeNetworkError); expect((err as GlassnodeNetworkError).timedOut).toBe(true); @@ -110,14 +112,16 @@ describe('client failure paths', () => { init?.signal?.addEventListener('abort', () => reject(init.signal!.reason)); }) ); - const err = await caught(api(fetchFn, { timeout: 5 }).getMetricList()); + const err = await caught(api(fetchFn, { timeout: 5, maxRetries: 0 }).getMetricList()); expect(err).toBeInstanceOf(GlassnodeNetworkError); expect((err as GlassnodeNetworkError).timedOut).toBe(true); }); it('a non-timeout abort is not reported as timedOut', async () => { const cause = new DOMException('The operation was aborted', 'AbortError'); - const err = await caught(api(vi.fn().mockRejectedValue(cause)).getMetricList()); + const err = await caught( + api(vi.fn().mockRejectedValue(cause), { maxRetries: 0 }).getMetricList() + ); expect(err).toBeInstanceOf(GlassnodeNetworkError); expect((err as GlassnodeNetworkError).timedOut).toBe(false); }); @@ -250,7 +254,7 @@ describe('transport rejection classification', () => { it('a non-Error object named TimeoutError -> timedOut=true, message and cause kept', async () => { const cause = { name: 'TimeoutError', message: 'The operation timed out' }; const err = await caught( - api(vi.fn().mockRejectedValue(cause), { timeout: 10 }).getMetricList() + api(vi.fn().mockRejectedValue(cause), { timeout: 10, maxRetries: 0 }).getMetricList() ); expect(err).toBeInstanceOf(GlassnodeNetworkError); const e = err as GlassnodeNetworkError; @@ -261,7 +265,7 @@ describe('transport rejection classification', () => { it('a non-Error TimeoutError without a message falls back to its name', async () => { const err = await caught( - api(vi.fn().mockRejectedValue({ name: 'TimeoutError' })).getMetricList() + api(vi.fn().mockRejectedValue({ name: 'TimeoutError' }), { maxRetries: 0 }).getMetricList() ); expect((err as GlassnodeNetworkError).timedOut).toBe(true); expect((err as GlassnodeNetworkError).message).toBe('Glassnode API error: TimeoutError'); @@ -342,7 +346,9 @@ describe('transport rejection classification', () => { it('never puts the API key in the error message', async () => { const leaky = `request to https://api.glassnode.com/v1/x?api_key=${API_KEY}&a=BTC failed`; for (const cause of [new TypeError(leaky), { name: 'TimeoutError', message: leaky }]) { - const err = await caught(api(vi.fn().mockRejectedValue(cause)).getMetricList()); + const err = await caught( + api(vi.fn().mockRejectedValue(cause), { maxRetries: 0 }).getMetricList() + ); expect((err as Error).message).not.toContain(API_KEY); expect((err as Error).message).toContain('api_key=***'); } diff --git a/test/glassnode-api.spec.ts b/test/glassnode-api.spec.ts index 808a6f6..f48a6e3 100644 --- a/test/glassnode-api.spec.ts +++ b/test/glassnode-api.spec.ts @@ -1,6 +1,7 @@ -import { describe, it, expect, vi, type Mock } from 'vitest'; +import { afterEach, describe, it, expect, expectTypeOf, vi, type Mock } from 'vitest'; import { GlassnodeAPI } from '../src/glassnode-api'; import { GlassnodeApiError, GlassnodeValidationError } from '../src/errors'; +import { MetricMetadataSchema, type MetricMetadata } from '../src/types/metadata'; import { API_KEY, DEFAULT_API_URL, @@ -26,8 +27,10 @@ function createMockFetch(response: Partial) { return vi.fn().mockResolvedValue(response); } +// A single attempt: the tests built on this helper check what one request or one failure +// produces. Retries (on by default) are covered by the 'retry logic' and 'default retries' tests. function createApi(fetchFn: Mock) { - return new GlassnodeAPI({ apiKey: API_KEY, fetch: fetchFn }); + return new GlassnodeAPI({ apiKey: API_KEY, fetch: fetchFn, maxRetries: 0 }); } describe('GlassnodeAPI', () => { @@ -130,7 +133,12 @@ describe('GlassnodeAPI', () => { Object.assign(new Error('The operation was aborted'), { name: 'TimeoutError' }) ); - const api = new GlassnodeAPI({ apiKey: API_KEY, fetch: fetchFn, timeout: 10 }); + const api = new GlassnodeAPI({ + apiKey: API_KEY, + fetch: fetchFn, + timeout: 10, + maxRetries: 0, + }); await expect(api.getMetricList()).rejects.toThrow( 'Glassnode API error: The operation was aborted' @@ -268,7 +276,12 @@ describe('GlassnodeAPI', () => { const fetchFn = vi .fn() .mockRejectedValue(new Error(`connect failed (X-Api-Key: ${API_KEY})`)); - const api = new GlassnodeAPI({ apiKey: API_KEY, fetch: fetchFn, apiKeyLocation }); + const api = new GlassnodeAPI({ + apiKey: API_KEY, + fetch: fetchFn, + apiKeyLocation, + maxRetries: 0, + }); const err = await api.getMetricList().catch((e: unknown) => e); expect((err as Error).message).toBe('Glassnode API error: connect failed (X-Api-Key: ***)'); @@ -419,8 +432,7 @@ describe('GlassnodeAPI', () => { expect(result).toEqual(mockMetricMetadataResponse); expect(result.path).toBe('/distribution/balance_exchanges'); expect(result.tier).toBe(2); - expect(result.modified).toBeInstanceOf(Date); - expect(result.modified!.getTime()).toBe(mockRawMetricMetadataResponse.modified! * 1000); + expect(result.modified).toBe(mockRawMetricMetadataResponse.modified); }); it('leaves parameters_defaults undefined when the API omits it', async () => { @@ -453,6 +465,38 @@ describe('GlassnodeAPI', () => { } }); + it('requires only path and tier: refs, queried and parameters are optional', () => { + const result = MetricMetadataSchema.safeParse({ path: '/x', tier: 1 }); + + expect(result.success).toBe(true); + expect(result.data).toEqual({ path: '/x', tier: 1 }); + // The fields' types admit `undefined` (read them with `?.`). + expectTypeOf().toExtend(); + expectTypeOf().toExtend(); + expectTypeOf().toEqualTypeOf< + Record | undefined + >(); + + const missing = MetricMetadataSchema.safeParse({}); + expect(missing.success).toBe(false); + expect(missing.error!.issues.map((i) => i.path.join('.')).sort()).toEqual(['path', 'tier']); + }); + + it('leaves refs, queried and parameters undefined (not {}) when the API omits them', async () => { + const raw: Record = { ...mockRawMetricMetadataResponse }; + delete raw.refs; + delete raw.queried; + delete raw.parameters; + const fetchFn = createMockFetch({ ok: true, json: vi.fn().mockResolvedValue(raw) }); + + const result = await createApi(fetchFn).getMetricMetadata('/distribution/balance_exchanges'); + + expect(result.path).toBe('/distribution/balance_exchanges'); + expect(result).not.toHaveProperty('refs'); + expect(result).not.toHaveProperty('queried'); + expect(result).not.toHaveProperty('parameters'); + }); + it('rejects parameters_defaults whose values are not string arrays', async () => { const fetchFn = createMockFetch({ ok: true, @@ -1010,6 +1054,67 @@ describe('GlassnodeAPI', () => { }); }); + describe('default retries (maxRetries: 2)', () => { + const unavailable = () => + vi.fn().mockResolvedValue({ ok: false, status: 503, statusText: 'Service Unavailable' }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('retries a 503 twice (3 attempts) and then throws, with the default config', async () => { + // Full jitter picks a delay in [0, base]; pin it to 0 so the default 1000 ms base does not + // slow the test down. Everything else is the default config. + vi.spyOn(Math, 'random').mockReturnValue(0); + const fetchFn = unavailable(); + const api = new GlassnodeAPI({ apiKey: API_KEY, fetch: fetchFn }); + + const error = await api.getMetricList().catch((e: unknown) => e); + expect(error).toBeInstanceOf(GlassnodeApiError); + expect((error as GlassnodeApiError).status).toBe(503); + expect(fetchFn).toHaveBeenCalledTimes(3); + }); + + it('retries a network error by default and succeeds', async () => { + const fetchFn = vi + .fn() + .mockRejectedValueOnce(new TypeError('Failed to fetch')) + .mockRejectedValueOnce(new TypeError('Failed to fetch')) + .mockResolvedValueOnce({ + ok: true, + json: vi.fn().mockResolvedValue(mockMetricListResponse), + }); + const api = new GlassnodeAPI({ apiKey: API_KEY, fetch: fetchFn, retryDelay: 1 }); + + await expect(api.getMetricList()).resolves.toEqual(mockMetricListResponse); + expect(fetchFn).toHaveBeenCalledTimes(3); + }); + + it('maxRetries: 0 makes a single attempt', async () => { + const fetchFn = unavailable(); + const api = new GlassnodeAPI({ apiKey: API_KEY, fetch: fetchFn, maxRetries: 0 }); + + await expect(api.getMetricList()).rejects.toThrow(GlassnodeApiError); + expect(fetchFn).toHaveBeenCalledTimes(1); + }); + + it('makes a single attempt by default in x402 mode', async () => { + const fetchFn = unavailable(); + const api = new GlassnodeAPI({ x402: true, fetch: fetchFn, retryDelay: 1 }); + + await expect(api.getMetricList()).rejects.toThrow(GlassnodeApiError); + expect(fetchFn).toHaveBeenCalledTimes(1); + }); + + it('honours an explicit maxRetries in x402 mode', async () => { + const fetchFn = unavailable(); + const api = new GlassnodeAPI({ x402: true, fetch: fetchFn, maxRetries: 2, retryDelay: 1 }); + + await expect(api.getMetricList()).rejects.toThrow(GlassnodeApiError); + expect(fetchFn).toHaveBeenCalledTimes(3); + }); + }); + describe('x402 mode', () => { const okFetch = () => vi.fn().mockResolvedValue({ diff --git a/test/hooks.spec.ts b/test/hooks.spec.ts index 37586a0..d671a13 100644 --- a/test/hooks.spec.ts +++ b/test/hooks.spec.ts @@ -103,7 +103,8 @@ describe('observability hooks: event sequence and payloads', () => { endpoint: ENDPOINT, url: 'https://api.glassnode.com/v1/metadata/metrics?api_key=***', attempt: 1, - maxAttempts: 1, + // The default maxRetries (2): up to 3 attempts, even though the first one succeeds. + maxAttempts: 3, }); expect(res).toEqual({ ...req, diff --git a/test/redaction.spec.ts b/test/redaction.spec.ts index aa13a70..ef6d717 100644 --- a/test/redaction.spec.ts +++ b/test/redaction.spec.ts @@ -78,8 +78,15 @@ describe('redactSecrets', () => { describe('GlassnodeApiError never carries the API key', () => { for (const apiKeyLocation of LOCATIONS) { describe(`apiKeyLocation: ${apiKeyLocation}`, () => { + // One attempt: these tests are about the error a single failure produces (the retried + // case has its own test below). const api = (fetchFn: unknown) => - new GlassnodeAPI({ apiKey: KEY, apiKeyLocation, fetch: fetchFn as typeof fetch }); + new GlassnodeAPI({ + apiKey: KEY, + apiKeyLocation, + fetch: fetchFn as typeof fetch, + maxRetries: 0, + }); it('masks a server JSON message echoing the request URL and the raw key', async () => { const url = `https://api.glassnode.com/v1/metadata/metrics?api_key=${encodeURIComponent(KEY)}`; diff --git a/test/release-scripts.spec.ts b/test/release-scripts.spec.ts new file mode 100644 index 0000000..31ca0fd --- /dev/null +++ b/test/release-scripts.spec.ts @@ -0,0 +1,410 @@ +// Tests for the release tooling in scripts/ (publish.yml and ci.yml run it; it is not shipped): +// release-plan.mjs (semver, dist-tag, skipped versions), release-state.sh end to end with a fake +// `npm` on PATH, and check-changelog-heading.mjs. +import { spawnSync } from 'node:child_process'; +import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { afterEach, beforeAll, describe, expect, it } from 'vitest'; + +type Plan = typeof import('../scripts/release-plan.mjs'); +let plan: Plan; +beforeAll(async () => { + plan = await import('../scripts/release-plan.mjs'); +}); + +const scripts = resolve(__dirname, '../scripts'); +const dirs: string[] = []; +function tempDir(): string { + const dir = mkdtempSync(join(tmpdir(), 'release-scripts-')); + dirs.push(dir); + return dir; +} +afterEach(() => { + for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); +}); + +const CHANGELOG = [ + '# Changelog', + '', + '## 0.30.1', + '', + '- fix', + '', + '## 0.30.0', + '', + '## 0.29.5', + '', + '## 0.29.4', + '', + '## 0.29.3', + '', +].join('\n'); + +describe('compareSemver', () => { + it('orders versions by SemVer 2.0.0 precedence', () => { + // The precedence example from semver.org, plus numeric (not string) core comparison. + const ordered = [ + '0.9.0', + '0.10.0', + '1.0.0-alpha', + '1.0.0-alpha.1', + '1.0.0-alpha.beta', + '1.0.0-beta', + '1.0.0-beta.2', + '1.0.0-beta.11', + '1.0.0-rc.1', + '1.0.0', + '1.0.1', + '1.1.0', + '2.0.0', + ]; + for (let i = 0; i < ordered.length; i++) { + for (let j = 0; j < ordered.length; j++) { + expect(plan.compareSemver(ordered[i], ordered[j])).toBe(Math.sign(i - j)); + } + } + }); + + it('ignores build metadata and rejects non-semver input', () => { + expect(plan.compareSemver('1.0.0+build.1', '1.0.0+build.2')).toBe(0); + expect(plan.compareSemver('1.0.0-rc.1+b', '1.0.0')).toBe(-1); + expect(() => plan.compareSemver('v1.0.0', '1.0.0')).toThrow(/not a semver/); + expect(() => plan.compareSemver('1.0', '1.0.0')).toThrow(/not a semver/); + expect(plan.parseSemver('01.0.0')).toBeNull(); + }); +}); + +describe('distTagFor', () => { + it.each([ + ['older than latest', '0.29.5', '0.30.1', 'backport-0.29'], + ['older patch in the same line', '0.30.0', '0.30.1', 'backport-0.30'], + ['older major', '0.30.2', '1.0.0', 'backport-0.30'], + ['newer than latest', '1.0.0', '0.30.1', 'latest'], + ['equal to latest (re-run)', '0.30.1', '0.30.1', 'latest'], + ['no latest (first publish)', '0.1.0', null, 'latest'], + ['prerelease above latest', '1.0.0-rc.1', '0.30.1', 'next'], + ['prerelease with no latest', '1.0.0-rc.1', null, 'next'], + ['prerelease below latest', '1.0.0-rc.1', '1.0.0', 'backport-1.0'], + ['release above a prerelease latest', '1.0.0', '1.0.0-rc.1', 'latest'], + ])('%s', (_, version, latest, expected) => { + expect(plan.distTagFor(version, latest)).toBe(expected); + }); + + it('never picks a tag npm would read as a semver range', () => { + // npm rejects dist-tags that parse as a range; `v0.29`-style names would. + expect(plan.distTagFor('0.29.5', '0.30.1')).not.toMatch(/^v?\d/); + }); +}); + +describe('skippedVersions', () => { + it('lists CHANGELOG versions between the last published one and this one missing on npm', () => { + // 2026-09-29: the 0.29.5 and 0.30.0 publish runs were cancelled by the 0.30.1 merge. + const published = ['0.29.2', '0.29.3', '0.29.4']; + expect(plan.skippedVersions('0.30.1', published, CHANGELOG)).toEqual(['0.29.5', '0.30.0']); + // Still reported on a re-run after 0.30.1 is out: "last" is the last one below this version. + expect(plan.skippedVersions('0.30.1', [...published, '0.30.1'], CHANGELOG)).toEqual([ + '0.29.5', + '0.30.0', + ]); + }); + + it('is empty when nothing was skipped, and ignores unreleased headings', () => { + const changelog = `## 1.0.0 (unreleased)\n\n${CHANGELOG}`; + const all = ['0.29.3', '0.29.4', '0.29.5', '0.30.0']; + expect(plan.skippedVersions('0.30.1', all, changelog)).toEqual([]); + expect(plan.changelogVersions(changelog)).toEqual([ + '0.30.1', + '0.30.0', + '0.29.5', + '0.29.4', + '0.29.3', + ]); + }); + + it('lists every earlier CHANGELOG version when nothing is published yet', () => { + expect(plan.skippedVersions('0.29.5', [], CHANGELOG)).toEqual(['0.29.3', '0.29.4']); + }); +}); + +describe('parseNpmView', () => { + it('reads versions and the latest dist-tag', () => { + const raw = JSON.stringify({ versions: ['0.1.0', '0.2.0'], 'dist-tags': { latest: '0.2.0' } }); + expect(plan.parseNpmView(raw, 0)).toEqual({ latest: '0.2.0', versions: ['0.1.0', '0.2.0'] }); + }); + + it('accepts a single version printed as a string and a missing latest tag', () => { + const raw = JSON.stringify({ versions: '0.1.0', 'dist-tags': { next: '0.1.0' } }); + expect(plan.parseNpmView(raw, '0')).toEqual({ latest: null, versions: ['0.1.0'] }); + }); + + it('treats E404 as "never published" and fails on anything else', () => { + const e404 = JSON.stringify({ error: { code: 'E404' } }); + expect(plan.parseNpmView(e404, 1)).toEqual({ latest: null, versions: [] }); + const timeout = JSON.stringify({ error: { code: 'ETIMEDOUT' } }); + expect(() => plan.parseNpmView(timeout, 1)).toThrow(/ETIMEDOUT/); + expect(() => plan.parseNpmView('', 1)).toThrow(/unparseable/); + expect(() => plan.parseNpmView('"0.1.0"', 0)).toThrow(/returned/); + const badLatest = JSON.stringify({ versions: [], 'dist-tags': { latest: 'nope' } }); + expect(() => plan.parseNpmView(badLatest, 0)).toThrow(/returned/); + }); +}); + +describe('release-state.sh (fake npm)', () => { + const SHA = 'a'.repeat(40); + + interface Npm { + /** `npm view glassnode-api@ version gitHead --json`: stdout and exit code. */ + version: [string, number]; + /** `npm view glassnode-api versions dist-tags --json`: stdout and exit code. */ + pkg: [string, number]; + } + const e404: [string, number] = [JSON.stringify({ error: { code: 'E404' } }), 1]; + const pkg = (versions: string[], latest?: string): [string, number] => [ + JSON.stringify({ versions, 'dist-tags': latest ? { latest } : {} }), + 0, + ]; + + /** + * Runs release-state.sh in a temp repo with a fake `npm`. `plan` replaces release-plan.mjs (the + * script is copied next to a fake one, since it runs `$(dirname "$0")/release-plan.mjs`). + */ + function run(version: string, npm: Npm, changelog = CHANGELOG, plan?: string) { + const dir = tempDir(); + let script = join(scripts, 'release-state.sh'); + if (plan !== undefined) { + mkdirSync(join(dir, 'scripts')); + script = join(dir, 'scripts', 'release-state.sh'); + writeFileSync(script, readFileSync(join(scripts, 'release-state.sh'))); + writeFileSync(join(dir, 'scripts', 'release-plan.mjs'), plan); + } + writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'glassnode-api', version })); + writeFileSync(join(dir, 'CHANGELOG.md'), changelog); + const fakeNpm = join(dir, 'bin', 'npm'); + mkdirSync(join(dir, 'bin')); + // $2 is the package spec: `@` for the version query, `` otherwise. + writeFileSync( + fakeNpm, + [ + '#!/bin/sh', + 'echo "$*" >> "$FAKE_NPM_LOG"', + 'case "$2" in', + ' *@*) printf "%s" "$FAKE_VERSION_OUT"; exit "$FAKE_VERSION_RC" ;;', + ' *) printf "%s" "$FAKE_PKG_OUT"; exit "$FAKE_PKG_RC" ;;', + 'esac', + '', + ].join('\n') + ); + chmodSync(fakeNpm, 0o755); + const output = join(dir, 'output'); + const summary = join(dir, 'summary'); + const npmLog = join(dir, 'npm.log'); + writeFileSync(npmLog, ''); + writeFileSync(output, ''); + writeFileSync(summary, ''); + const res = spawnSync('bash', [script], { + cwd: dir, + encoding: 'utf8', + env: { + ...process.env, + PATH: `${join(dir, 'bin')}:${process.env.PATH}`, + GITHUB_SHA: SHA, + GITHUB_OUTPUT: output, + GITHUB_STEP_SUMMARY: summary, + FAKE_VERSION_OUT: npm.version[0], + FAKE_VERSION_RC: String(npm.version[1]), + FAKE_PKG_OUT: npm.pkg[0], + FAKE_PKG_RC: String(npm.pkg[1]), + FAKE_NPM_LOG: npmLog, + }, + }); + const outputs = Object.fromEntries( + readFileSync(output, 'utf8') + .split('\n') + .filter(Boolean) + .map((line) => line.split(/=(.*)/s).slice(0, 2)) + ); + const npmCalls = readFileSync(npmLog, 'utf8').split('\n').filter(Boolean); + return { ...res, outputs, npmCalls, summary: readFileSync(summary, 'utf8') }; + } + + it('queries npm with exactly the expected arguments', () => { + const r = run('0.30.1', { version: e404, pkg: pkg(['0.30.0'], '0.30.0') }); + expect(r.status).toBe(0); + // --prefer-online: npm's cache must not hide a just-published version or a moved `latest`. + expect(r.npmCalls).toEqual([ + 'view glassnode-api@0.30.1 version gitHead --json --prefer-online', + 'view glassnode-api versions dist-tags --json --prefer-online', + ]); + }); + + it.each([ + ['empty', 'dist_tag='], + ['missing', ''], + ['unexpected', 'dist_tag=beta'], + ['a semver range', 'dist_tag=v0.29'], + ['with trailing text', 'dist_tag=latest x'], + ])('fails, writing no outputs, on a %s dist-tag from release-plan.mjs', (_, line) => { + const plan = `console.log('latest=0.30.0');\nconsole.log(${JSON.stringify(line)});\nconsole.log('skipped=');\n`; + const r = run('0.30.1', { version: e404, pkg: pkg(['0.30.0'], '0.30.0') }, CHANGELOG, plan); + expect(r.status).not.toBe(0); + expect(r.stderr).toContain('Unexpected dist-tag'); + expect(r.outputs).toEqual({}); + }); + + it('accepts every dist-tag shape release-plan.mjs produces', () => { + for (const tag of ['latest', 'next', 'backport-0.29', 'backport-12.345']) { + const plan = `console.log('latest=0.30.0');\nconsole.log('dist_tag=${tag}');\nconsole.log('skipped=');\n`; + const r = run('0.30.1', { version: e404, pkg: pkg(['0.30.0'], '0.30.0') }, CHANGELOG, plan); + expect(r.status, r.stderr).toBe(0); + expect(r.outputs.dist_tag).toBe(tag); + } + }); + + it('publishes a new version above latest under `latest`', () => { + const r = run('0.30.1', { + version: e404, + pkg: pkg(['0.29.4', '0.29.5', '0.30.0'], '0.30.0'), + }); + expect(r.status).toBe(0); + expect(r.outputs).toEqual({ + version: '0.30.1', + tag: 'v0.30.1', + action: 'publish', + dist_tag: 'latest', + latest: '0.30.0', + }); + expect(r.summary).not.toContain('WARNING'); + }); + + it('publishes a version below latest under a backport tag and warns about skipped ones', () => { + // Re-run of the cancelled 0.30.0 after 0.30.1 is out; 0.29.5 is still missing too. + const r = run('0.30.0', { version: e404, pkg: pkg(['0.29.4', '0.30.1'], '0.30.1') }); + expect(r.status).toBe(0); + expect(r.outputs).toMatchObject({ action: 'publish', dist_tag: 'backport-0.30' }); + expect(r.summary).toContain('`latest` does not move'); + expect(r.summary).toContain('`0.29.5`'); + expect(r.stdout).toContain('::warning::'); + }); + + it('warns about the versions skipped before a new release', () => { + const r = run('0.30.1', { version: e404, pkg: pkg(['0.29.3', '0.29.4'], '0.29.4') }); + expect(r.outputs).toMatchObject({ action: 'publish', dist_tag: 'latest' }); + expect(r.summary).toContain('`0.29.5`, `0.30.0`'); + }); + + it('only (re)tags a version published from this commit, keeping `latest`', () => { + const view = JSON.stringify({ version: '0.30.1', gitHead: SHA }); + const r = run('0.30.1', { + version: [view, 0], + pkg: pkg(['0.29.5', '0.30.0', '0.30.1'], '0.30.1'), + }); + expect(r.status).toBe(0); + expect(r.outputs).toMatchObject({ action: 'tag-only', dist_tag: 'latest', latest: '0.30.1' }); + }); + + it('skips a version published from another commit', () => { + const view = JSON.stringify({ version: '0.30.1', gitHead: 'b'.repeat(40) }); + const r = run('0.30.1', { version: [view, 0], pkg: pkg(['0.30.1'], '0.30.1') }); + expect(r.status).toBe(0); + expect(r.outputs).toMatchObject({ action: 'skip' }); + }); + + it('publishes the first version of a package that is not on npm under `latest`', () => { + const r = run('0.29.3', { version: e404, pkg: e404 }, '## 0.29.3\n'); + expect(r.status).toBe(0); + expect(r.outputs).toMatchObject({ action: 'publish', dist_tag: 'latest', latest: '' }); + }); + + it('publishes a prerelease under `next`', () => { + const r = run('1.0.0-rc.1', { version: e404, pkg: pkg(['0.30.1'], '0.30.1') }); + expect(r.status).toBe(0); + expect(r.outputs).toMatchObject({ action: 'publish', dist_tag: 'next' }); + expect(r.summary).toContain('prerelease'); + }); + + it('fails, never publishes, when npm cannot be queried', () => { + const timeout: [string, number] = [JSON.stringify({ error: { code: 'ETIMEDOUT' } }), 1]; + const pkgFails = run('0.30.1', { version: e404, pkg: timeout }); + expect(pkgFails.status).not.toBe(0); + expect(pkgFails.stderr).toContain('ETIMEDOUT'); + expect(pkgFails.outputs).toEqual({}); + const versionFails = run('0.30.1', { version: timeout, pkg: pkg(['0.30.0'], '0.30.0') }); + expect(versionFails.status).not.toBe(0); + expect(versionFails.outputs).toEqual({}); + }); +}); + +describe('check-changelog-heading.mjs', () => { + function check(changelog: string, version: string, target: string) { + const dir = tempDir(); + writeFileSync(join(dir, 'CHANGELOG.md'), changelog); + writeFileSync(join(dir, 'package.json'), JSON.stringify({ version })); + return spawnSync('node', [join(scripts, 'check-changelog-heading.mjs'), target], { + encoding: 'utf8', + env: { + ...process.env, + CHANGELOG: join(dir, 'CHANGELOG.md'), + PACKAGE_JSON: join(dir, 'package.json'), + }, + }); + } + const log = (heading: string) => `# Changelog\n\n${heading}\n\n- change\n\n## 0.30.0\n`; + + it.each([ + ['main, exact version', log('## 0.30.1'), '0.30.1', 'main'], + ['release branch, exact version', log('## 1.0.0'), '1.0.0', 'release/1.0'], + [ + 'release branch, unreleased (package.json not compared)', + log('## 1.0.0 (unreleased)'), + '0.30.1', + 'release/1.0', + ], + ])('passes: %s', (_, changelog, version, target) => { + const r = check(changelog, version, target); + expect(r.stderr).toBe(''); + expect(r.status).toBe(0); + }); + + it.each([ + [ + 'main, unreleased heading', + log('## 1.0.0 (unreleased)'), + '1.0.0', + 'main', + /cannot reach main/, + ], + ['main, version mismatch', log('## 0.30.2'), '0.30.1', 'main', /must be "## 0.30.1"/], + [ + 'release branch, version mismatch', + log('## 0.30.2'), + '0.30.1', + 'release/1.0', + /"## 0.30.1" or/, + ], + [ + 'release branch, unreleased non-semver', + log('## 1.0 (unreleased)'), + '0.30.1', + 'release/1.0', + /must be/, + ], + ['release branch, other suffix', log('## 1.0.0 (draft)'), '0.30.1', 'release/1.0', /must be/], + ['trailing whitespace', log('## 0.30.1 '), '0.30.1', 'main', /must be/], + ['no heading', '# Changelog\n', '0.30.1', 'main', /no "## " heading/], + ])('fails: %s', (_, changelog, version, target, message) => { + const r = check(changelog, version, target); + expect(r.status).toBe(1); + expect(r.stderr).toMatch(/^::error file=CHANGELOG.md::/); + expect(r.stderr).toMatch(message); + }); + + it('accepts the repository CHANGELOG.md for its own branch rules', () => { + // release/1.0 carries "## 1.0.0 (unreleased)" with package.json still at 0.30.x. + const r = spawnSync('node', [join(scripts, 'check-changelog-heading.mjs'), 'release/1.0'], { + cwd: resolve(__dirname, '..'), + encoding: 'utf8', + }); + expect(r.status).toBe(0); + }); +}); diff --git a/test/timestamps.spec.ts b/test/timestamps.spec.ts index 08e522a..dfb98fd 100644 --- a/test/timestamps.spec.ts +++ b/test/timestamps.spec.ts @@ -4,18 +4,17 @@ import { mockRawMetricMetadataResponse } from './mocks/metadata.mock'; /** * Pins the documented timestamp representation (see README "Timestamps"): - * `MetricMetadata.modified` is a `Date`; every other time field stays unix seconds. + * every time field, `MetricMetadata.modified` included, stays unix seconds (a number). */ describe('timestamp representation', () => { describe('MetricMetadata.modified', () => { - it('converts unix seconds into a Date', () => { + it('keeps unix seconds as a number', () => { const result = MetricMetadataSchema.parse({ ...mockRawMetricMetadataResponse, modified: 1733829848, }); - expect(result.modified).toBeInstanceOf(Date); - expect(result.modified!.getTime()).toBe(1733829848 * 1000); - expect(result.modified!.toISOString()).toBe('2024-12-10T11:24:08.000Z'); + expect(result.modified).toBe(1733829848); + expect(typeof result.modified).toBe('number'); }); it('is undefined when the field is absent', () => { @@ -25,9 +24,9 @@ describe('timestamp representation', () => { expect(result.modified).toBeUndefined(); }); - it('is undefined (not the 1970 epoch) when the API sends 0', () => { + it('passes 0 through as 0', () => { const result = MetricMetadataSchema.parse({ ...mockRawMetricMetadataResponse, modified: 0 }); - expect(result.modified).toBeUndefined(); + expect(result.modified).toBe(0); }); }); diff --git a/tsconfig.browser.json b/tsconfig.browser.json index a602afe..ab02fae 100644 --- a/tsconfig.browser.json +++ b/tsconfig.browser.json @@ -2,8 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "target": "ES2015", - "module": "ES2015", - "moduleResolution": "node", + "module": "ESNext", + "moduleResolution": "bundler", "lib": ["ES2022", "DOM"], "declaration": false },