diff --git a/README.md b/README.md index d5b206f..3e3e2e0 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,16 @@ > [!WARNING] > **Beta.** Public API may change between minor versions until v1. Pin to an exact version in production. -First-party MPP payment method for EVM-settled payments, verified via QuickNode RPC. Gate any HTTP endpoint behind a stablecoin (or native-coin) payment — agents pay with one signature, the server verifies on-chain, and the request is forwarded. Built for the [Machine Payments Protocol](https://github.com/tempoxyz/mpp-specs). +SDK for extending the MPP protocol with EVM-settled payments, verified via Quicknode RPC. Gate any HTTP endpoint behind a stablecoin (or native-coin) payment — agents pay with one signature, the server verifies on-chain, and the request is forwarded. Built for the [Machine Payments Protocol](https://github.com/tempoxyz/mpp-specs). -Implements IETF [`draft-evm-charge-00`](https://github.com/tempoxyz/mpp-specs/blob/2f6bfcee6f9e448d2ded15dc350dc92967e17513/specs/methods/evm/draft-evm-charge-00.md) +Implements and expands on the [`draft-evm-charge-00`](https://github.com/tempoxyz/mpp-specs/blob/2f6bfcee6f9e448d2ded15dc350dc92967e17513/specs/methods/evm/draft-evm-charge-00.md) spec with all three non-trivial credential types: -| Type | Binding | Gas | UX | -|---|---|---|---| -| `permit2` (RECOMMENDED) | Strong (EIP-712 witness) | Server pays | One signature, any ERC-20 | -| `authorization` | Strong (on-chain nonce) | Server pays | One signature, USDC / EIP-3009 tokens | -| `hash` | Weakest (post-hoc receipt match) | Client pays | Client broadcasts + waits | +| Type | Binding | Gas | UX | +| ----------------------- | -------------------------------- | ----------- | ------------------------------------- | +| `permit2` (RECOMMENDED) | Strong (EIP-712 witness) | Server pays | One signature, any ERC-20 | +| `authorization` | Strong (on-chain nonce) | Server pays | One signature, USDC / EIP-3009 tokens | +| `hash` | Weakest (post-hoc receipt match) | Client pays | Client broadcasts + waits | ## Contents @@ -45,41 +45,41 @@ npm install @quicknode/mpp mppx viem ## Server — accept payments ```ts -import { Mppx, evm } from '@quicknode/mpp/server' +import { Mppx, evm } from "@quicknode/mpp/server"; const mppx = Mppx.create({ methods: [ evm.charge({ - recipient: '0xMerchantWallet', - chain: 'base', + recipient: "0xMerchantWallet", + chain: "base", submitter: { privateKey: process.env.SUBMITTER_PK! }, }), ], secretKey: process.env.MPP_SECRET_KEY!, -}) +}); // mppx.evm.charge({ amount: '0.01', decimals: 6 })(request) → 402 challenge or verified receipt ``` -No `rpcUrl`? The SDK uses QuickNode's shared public endpoint for the chosen chain. Good for local dev and low-volume workloads. When you start seeing `QuickNodeRateLimitError`, upgrade at [quicknode.com](https://www.quicknode.com/?utm_source=mpp-sdk) and pass your dedicated endpoint via `rpcUrl`. +No `rpcUrl`? The SDK uses Quicknode's shared public endpoint for the chosen chain. Good for local dev and low-volume workloads. When you start seeing `QuicknodeRateLimitError`, upgrade at [quicknode.com](https://www.quicknode.com/?utm_source=mpp-sdk) and pass your dedicated endpoint via `rpcUrl`. Scope accepted types per-server: ```ts evm.charge({ recipient, - chain: 'base', + chain: "base", rpcUrl, // optional override; omit to use public endpoint - credentialTypes: ['permit2', 'authorization'], // drop 'hash' if you don't want client-paid flows + credentialTypes: ["permit2", "authorization"], // drop 'hash' if you don't want client-paid flows submitter: { privateKey: SUBMITTER_PK }, -}) +}); ``` ## Client — pay for content ```ts -import { Mppx, evm } from '@quicknode/mpp/client' -import { privateKeyToAccount } from 'viem/accounts' +import { Mppx, evm } from "@quicknode/mpp/client"; +import { privateKeyToAccount } from "viem/accounts"; const { fetch } = Mppx.create({ methods: [ @@ -88,10 +88,10 @@ const { fetch } = Mppx.create({ // rpcUrl only needed if you want to allow the 'hash' credential path }), ], -}) +}); // Auto-handles 402 → pay → retry -const res = await fetch('https://api.merchant.com/premium') +const res = await fetch("https://api.merchant.com/premium"); ``` Set client preference order: @@ -99,51 +99,51 @@ Set client preference order: ```ts evm.charge({ account, - prefer: ['authorization', 'permit2'], // skip 'hash' entirely -}) + prefer: ["authorization", "permit2"], // skip 'hash' entirely +}); ``` ## Rate limits -The default public RPC is rate-limited per IP. When the limit is exceeded, the SDK throws `QuickNodeRateLimitError`: +The default public RPC is rate-limited per IP. When the limit is exceeded, the SDK throws `QuicknodeRateLimitError`: ```ts -import { QuickNodeRateLimitError } from '@quicknode/mpp/server' +import { QuicknodeRateLimitError } from "@quicknode/mpp/server"; try { - await mppx.evm.charge(/* ... */) + await mppx.evm.charge(/* ... */); } catch (err) { - if (err instanceof QuickNodeRateLimitError) { - console.error(`Rate limited on ${err.chain}. Upgrade: ${err.upgradeUrl}`) + if (err instanceof QuicknodeRateLimitError) { + console.error(`Rate limited on ${err.chain}. Upgrade: ${err.upgradeUrl}`); } } ``` -To avoid the limit entirely, pass your own `rpcUrl` from any QuickNode plan. +To avoid the limit entirely, pass your own `rpcUrl` from any Quicknode plan. ## Configuration ### `evm.charge` (server) -| Option | Required | Default | Notes | -|---|---|---|---| -| `recipient` | ✓ | — | Merchant wallet (receives USDC) | -| `chain` | ✓ | — | `'base' \| 'ethereum' \| 'arbitrum' \| 'polygon' \| 'optimism' \| 'avalanche' \| 'linea' \| 'unichain' \| 'base-sepolia'` | -| `rpcUrl` | — | — | Defaults to QuickNode public endpoint for the chain. Rate-limited per IP. | -| `submitter` | when `credentialTypes` contains `permit2`/`authorization` | — | `{ privateKey }` or `{ account }` | -| `credentialTypes` | | per-token allowed set | Draft-ordered preference list | -| `token` | | `'USDC'` | Curated symbol: `USDC \| EURC \| WETH \| USDT`. Mutually exclusive with `customToken`. | -| `customToken` | | — | Caller-supplied `{ address, decimals, symbol?, name?, version?, credentialTypes? }`. Use for any ERC-20 by address, or for native (zero-address). See below. | -| `confirmations` | | per-chain default | Block-depth check for `hash` credential | -| `store` | | `Store.memory()` | Any mppx `AtomicStore` (Cloudflare KV, Redis, Upstash) | +| Option | Required | Default | Notes | +| ----------------- | --------------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `recipient` | ✓ | — | Merchant wallet (receives USDC) | +| `chain` | ✓ | — | `'base' \| 'ethereum' \| 'arbitrum' \| 'polygon' \| 'optimism' \| 'avalanche' \| 'linea' \| 'unichain' \| 'base-sepolia'` | +| `rpcUrl` | — | — | Defaults to Quicknode public endpoint for the chain. Rate-limited per IP. | +| `submitter` | when `credentialTypes` contains `permit2`/`authorization` | — | `{ privateKey }` or `{ account }` | +| `credentialTypes` | | per-token allowed set | Draft-ordered preference list | +| `token` | | `'USDC'` | Curated symbol: `USDC \| EURC \| WETH \| USDT`. Mutually exclusive with `customToken`. | +| `customToken` | | — | Caller-supplied `{ address, decimals, symbol?, name?, version?, credentialTypes? }`. Use for any ERC-20 by address, or for native (zero-address). See below. | +| `confirmations` | | per-chain default | Block-depth check for `hash` credential | +| `store` | | `Store.memory()` | Any mppx `AtomicStore` (Cloudflare KV, Redis, Upstash) | ### `evm.charge` (client) -| Option | Required | Notes | -|---|---|---| -| `account` / `privateKey` | one of | Viem `Account` or raw `0x...` hex | -| `rpcUrl` | only if `hash` is chosen | Used to broadcast the ERC-20 transfer | -| `prefer` | | `['permit2','authorization','hash']` by default | +| Option | Required | Notes | +| ------------------------ | ------------------------ | ----------------------------------------------- | +| `account` / `privateKey` | one of | Viem `Account` or raw `0x...` hex | +| `rpcUrl` | only if `hash` is chosen | Used to broadcast the ERC-20 transfer | +| `prefer` | | `['permit2','authorization','hash']` by default | ### Permit2 one-time approval @@ -153,10 +153,10 @@ Before the agent can use `permit2`, it must approve Permit2 on each token: // One-time, from the agent's wallet: await walletClient.writeContract({ address: USDC_ADDRESS, - abi: parseAbi(['function approve(address,uint256)']), - functionName: 'approve', - args: ['0x000000000022D473030F116dDEE9F6B43aC78BA3', 2n ** 256n - 1n], -}) + abi: parseAbi(["function approve(address,uint256)"]), + functionName: "approve", + args: ["0x000000000022D473030F116dDEE9F6B43aC78BA3", 2n ** 256n - 1n], +}); ``` ### Custom tokens & native settlement @@ -166,46 +166,46 @@ in the chain's native coin (ETH / MATIC / AVAX / …): ```ts // Any ERC-20 by address — e.g. DAI on mainnet -import { evm } from '@quicknode/mpp/server' +import { evm } from "@quicknode/mpp/server"; evm.charge({ - chain: 'ethereum', + chain: "ethereum", recipient, submitter: { privateKey: SUBMITTER_PK }, customToken: { - address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI + address: "0x6B175474E89094C44Da98b954EedeAC495271d0F", // DAI decimals: 18, - symbol: 'DAI', + symbol: "DAI", }, -}) +}); ``` ```ts // Native chain coin — set address to NATIVE_TOKEN_ADDRESS (zero address) -import { evm, NATIVE_TOKEN_ADDRESS } from '@quicknode/mpp/server' +import { evm, NATIVE_TOKEN_ADDRESS } from "@quicknode/mpp/server"; evm.charge({ - chain: 'base', + chain: "base", recipient, customToken: { address: NATIVE_TOKEN_ADDRESS, decimals: 18, - symbol: 'ETH', + symbol: "ETH", }, // No `submitter` needed — native settlement only supports the `hash` // credential, which the client broadcasts itself. -}) +}); ``` `customToken` fields: -| Field | Required | Notes | -|---|---|---| -| `address` | ✓ | ERC-20 contract address. Use `NATIVE_TOKEN_ADDRESS` for the chain's native coin. | -| `decimals` | ✓ | 18 for native ETH / MATIC / AVAX. | -| `symbol` | | Display only. | -| `name`, `version` | | EIP-712 domain values. Pass these for `authorization` (EIP-3009) when the token's on-chain `name()` / `version()` reverts or differs from its EIP-712 domain. | -| `credentialTypes` | | Defaults: `['permit2','hash']` for ERC-20, `['hash']` for native. | +| Field | Required | Notes | +| ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `address` | ✓ | ERC-20 contract address. Use `NATIVE_TOKEN_ADDRESS` for the chain's native coin. | +| `decimals` | ✓ | 18 for native ETH / MATIC / AVAX. | +| `symbol` | | Display only. | +| `name`, `version` | | EIP-712 domain values. Pass these for `authorization` (EIP-3009) when the token's on-chain `name()` / `version()` reverts or differs from its EIP-712 domain. | +| `credentialTypes` | | Defaults: `['permit2','hash']` for ERC-20, `['hash']` for native. | Defaults intentionally exclude `authorization` for custom ERC-20s: only Circle FiatTokens (USDC, EURC) implement EIP-3009 reliably. Opt in by passing diff --git a/package.json b/package.json index 64f1e41..aee2b7e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@quicknode/mpp", "version": "0.2.0", - "description": "QuickNode payment methods for MPP (Machine Payments Protocol)", + "description": "Quicknode payment methods for MPP (Machine Payments Protocol)", "license": "MIT", "type": "module", "sideEffects": false, @@ -94,4 +94,4 @@ "usdc", "agent" ] -} +} \ No newline at end of file diff --git a/scripts/live-sepolia.ts b/scripts/live-sepolia.ts index a4e390f..ca26e3e 100644 --- a/scripts/live-sepolia.ts +++ b/scripts/live-sepolia.ts @@ -69,7 +69,7 @@ const payer = privateKeyToAccount(PAYER_PK) const submitter = SUBMITTER_PK ? privateKeyToAccount(SUBMITTER_PK) : undefined const EFFECTIVE_RPC = useDefaultRpc ? defaultRpcUrl('base-sepolia') : (RPC_URL as string) -console.log(`▶ RPC: ${useDefaultRpc ? 'QuickNode public (default)' : 'custom'}`) +console.log(`▶ RPC: ${useDefaultRpc ? 'Quicknode public (default)' : 'custom'}`) const CHAIN_ID = CHAIN_IDS['base-sepolia'] const TOKEN = USDC_CONTRACTS['base-sepolia'] diff --git a/scripts/rate-limit-demo.ts b/scripts/rate-limit-demo.ts index 2ebdc7b..e6ed377 100644 --- a/scripts/rate-limit-demo.ts +++ b/scripts/rate-limit-demo.ts @@ -1,6 +1,6 @@ /** * Fires requests in a loop against the SDK's default public RPC until - * it hits a 429, then prints the resulting QuickNodeRateLimitError to + * it hits a 429, then prints the resulting QuicknodeRateLimitError to * validate the upgrade CTA copy. * * Usage: npx tsx scripts/rate-limit-demo.ts [chain] @@ -11,7 +11,7 @@ import { createPublicClient } from 'viem' import { type SupportedChain, defaultRpcUrl } from '../src/constants.js' -import { QuickNodeRateLimitError } from '../src/errors.js' +import { QuicknodeRateLimitError } from '../src/errors.js' import { getViemChain } from '../src/internal/chain.js' import { defaultTransport } from '../src/internal/transport.js' @@ -30,7 +30,7 @@ while (true) { try { await client.getChainId() } catch (err) { - if (err instanceof QuickNodeRateLimitError) { + if (err instanceof QuicknodeRateLimitError) { const elapsed = ((Date.now() - start) / 1000).toFixed(1) console.log(`\n✖ Rate-limited after ${i} requests in ${elapsed}s\n`) console.log('Error:', err.message) diff --git a/src/__tests__/exports.test.ts b/src/__tests__/exports.test.ts index 0f0d09c..d75e0f1 100644 --- a/src/__tests__/exports.test.ts +++ b/src/__tests__/exports.test.ts @@ -4,14 +4,14 @@ import * as client from '../client/index.js' import * as root from '../index.js' import * as server from '../server/index.js' -test('QuickNodeRateLimitError exported from root', () => { - assert.equal(typeof root.QuickNodeRateLimitError, 'function') +test('QuicknodeRateLimitError exported from root', () => { + assert.equal(typeof root.QuicknodeRateLimitError, 'function') }) -test('QuickNodeRateLimitError exported from server', () => { - assert.equal(typeof server.QuickNodeRateLimitError, 'function') +test('QuicknodeRateLimitError exported from server', () => { + assert.equal(typeof server.QuicknodeRateLimitError, 'function') }) -test('QuickNodeRateLimitError exported from client', () => { - assert.equal(typeof client.QuickNodeRateLimitError, 'function') +test('QuicknodeRateLimitError exported from client', () => { + assert.equal(typeof client.QuicknodeRateLimitError, 'function') }) diff --git a/src/client/index.ts b/src/client/index.ts index 57b477f..6110319 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -4,4 +4,4 @@ export { NATIVE_TOKEN_ADDRESS } from '../constants.js' export { charge } export const evm = { charge } export { Mppx } from 'mppx/client' -export { QuickNodeRateLimitError } from '../errors.js' +export { QuicknodeRateLimitError } from '../errors.js' diff --git a/src/constants.ts b/src/constants.ts index 93d3d1d..05db3a7 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -178,7 +178,7 @@ export const TRANSFER_EVENT_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' as const /** - * Pre-provisioned QuickNode endpoint used when the SDK is configured without + * Pre-provisioned Quicknode endpoint used when the SDK is configured without * an explicit rpcUrl. Rate-limited per IP. See README for upgrade path. */ export const PUBLIC_RPC_PREFIX = 'dimensional-red-surf' @@ -212,7 +212,7 @@ export const CHAIN_PATH_SUFFIXES: Partial> = { } /** - * Known QuickNode path suffixes for chains not yet in `SupportedChain`. + * Known Quicknode path suffixes for chains not yet in `SupportedChain`. * Keep these wired up so adding the chain later is a one-line change in * `CHAIN_PATH_SUFFIXES`. Not referenced by runtime code today. */ diff --git a/src/errors.test.ts b/src/errors.test.ts index 47fe511..4033248 100644 --- a/src/errors.test.ts +++ b/src/errors.test.ts @@ -1,36 +1,36 @@ import assert from 'node:assert/strict' import { test } from 'node:test' -import { QuickNodeRateLimitError } from './errors.js' +import { QuicknodeRateLimitError } from './errors.js' -test('QuickNodeRateLimitError has canonical code and chain', () => { - const err = new QuickNodeRateLimitError('base') +test('QuicknodeRateLimitError has canonical code and chain', () => { + const err = new QuicknodeRateLimitError('base') assert.equal(err.code, 'QUICKNODE_RATE_LIMITED') assert.equal(err.chain, 'base') - assert.equal(err.name, 'QuickNodeRateLimitError') + assert.equal(err.name, 'QuicknodeRateLimitError') }) -test('QuickNodeRateLimitError exposes upgrade URL with UTM', () => { - const err = new QuickNodeRateLimitError('optimism') +test('QuicknodeRateLimitError exposes upgrade URL with UTM', () => { + const err = new QuicknodeRateLimitError('optimism') assert.match(err.upgradeUrl, /^https:\/\/www\.quicknode\.com/) assert.match(err.upgradeUrl, /utm_source=mpp-sdk/) assert.match(err.message, /optimism/) assert.match(err.message, /quicknode\.com/) }) -test('QuickNodeRateLimitError records retryAfter when provided', () => { - const err = new QuickNodeRateLimitError('base', 30) +test('QuicknodeRateLimitError records retryAfter when provided', () => { + const err = new QuicknodeRateLimitError('base', 30) assert.equal(err.retryAfterSeconds, 30) assert.match(err.message, /retry after 30s/) }) -test('QuickNodeRateLimitError omits retryAfter clause when unset', () => { - const err = new QuickNodeRateLimitError('base') +test('QuicknodeRateLimitError omits retryAfter clause when unset', () => { + const err = new QuicknodeRateLimitError('base') assert.equal(err.retryAfterSeconds, undefined) assert.doesNotMatch(err.message, /retry after/) }) -test('QuickNodeRateLimitError is an instance of Error', () => { - const err = new QuickNodeRateLimitError('base') +test('QuicknodeRateLimitError is an instance of Error', () => { + const err = new QuicknodeRateLimitError('base') assert.ok(err instanceof Error) - assert.ok(err instanceof QuickNodeRateLimitError) + assert.ok(err instanceof QuicknodeRateLimitError) }) diff --git a/src/errors.ts b/src/errors.ts index 56f905a..129fda1 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -8,7 +8,7 @@ const UPGRADE_URL = 'https://www.quicknode.com/?utm_source=mpp-sdk&utm_medium=ra * Only emitted when the SDK is using its default transport — requests through a * user-supplied `rpcUrl` surface the underlying viem `HttpRequestError` unchanged. */ -export class QuickNodeRateLimitError extends Error { +export class QuicknodeRateLimitError extends Error { readonly code = 'QUICKNODE_RATE_LIMITED' as const readonly upgradeUrl = UPGRADE_URL readonly chain: SupportedChain @@ -17,10 +17,10 @@ export class QuickNodeRateLimitError extends Error { constructor(chain: SupportedChain, retryAfterSeconds?: number) { const suffix = retryAfterSeconds !== undefined ? ` (retry after ${retryAfterSeconds}s)` : '' super( - `QuickNode public RPC rate limit hit on ${chain}. ` + + `Quicknode public RPC rate limit hit on ${chain}. ` + `Upgrade to a dedicated endpoint at ${UPGRADE_URL}${suffix}`, ) - this.name = 'QuickNodeRateLimitError' + this.name = 'QuicknodeRateLimitError' this.chain = chain this.retryAfterSeconds = retryAfterSeconds } diff --git a/src/index.ts b/src/index.ts index fc63be1..2c1832e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ export { TOKEN_CREDENTIAL_TYPES, TOKEN_DECIMALS, } from './constants.js' -export { QuickNodeRateLimitError } from './errors.js' +export { QuicknodeRateLimitError } from './errors.js' export { charge } from './Methods.js' export type { AuthorizationPayload, diff --git a/src/internal/transport.test.ts b/src/internal/transport.test.ts index 607abff..e6af590 100644 --- a/src/internal/transport.test.ts +++ b/src/internal/transport.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict' import { afterEach, beforeEach, test } from 'node:test' import { createPublicClient } from 'viem' -import { QuickNodeRateLimitError } from '../errors.js' +import { QuicknodeRateLimitError } from '../errors.js' import { getViemChain } from './chain.js' import { defaultTransport, logDefaultTransportOnce, resetDefaultTransportLog } from './transport.js' @@ -48,7 +48,7 @@ test('defaultTransport attaches telemetry headers on every request', async () => assert.match(headers.get('user-agent') ?? '', /^@quicknode\/mpp\/.+\(chain=base\)$/) }) -test('defaultTransport converts 429 into QuickNodeRateLimitError', async () => { +test('defaultTransport converts 429 into QuicknodeRateLimitError', async () => { globalThis.fetch = mockFetch( new Response('Too Many Requests', { status: 429, @@ -64,7 +64,7 @@ test('defaultTransport converts 429 into QuickNodeRateLimitError', async () => { await assert.rejects( () => client.getChainId(), (err: unknown) => { - assert.ok(err instanceof QuickNodeRateLimitError) + assert.ok(err instanceof QuicknodeRateLimitError) assert.equal(err.chain, 'optimism') assert.equal(err.retryAfterSeconds, 42) return true @@ -88,7 +88,7 @@ test('defaultTransport passes 500s through unchanged', async () => { () => client.getChainId(), (err: unknown) => { assert.ok(err instanceof Error) - assert.ok(!(err instanceof QuickNodeRateLimitError)) + assert.ok(!(err instanceof QuicknodeRateLimitError)) return true }, ) @@ -104,7 +104,7 @@ test('defaultTransport handles 429 without Retry-After header', async () => { await assert.rejects( () => client.getChainId(), (err: unknown) => { - assert.ok(err instanceof QuickNodeRateLimitError) + assert.ok(err instanceof QuicknodeRateLimitError) assert.equal(err.retryAfterSeconds, undefined) return true }, diff --git a/src/internal/transport.ts b/src/internal/transport.ts index 5b96d18..62d528a 100644 --- a/src/internal/transport.ts +++ b/src/internal/transport.ts @@ -1,15 +1,15 @@ import { HttpRequestError, type HttpTransport, http } from 'viem' import type { SupportedChain } from '../constants.js' -import { QuickNodeRateLimitError } from '../errors.js' +import { QuicknodeRateLimitError } from '../errors.js' import { version } from './version.js' /** * Constructs a viem `http()` transport targeting the SDK's default public RPC. * * Two concerns beyond `http()` defaults: - * 1. Telemetry headers (`x-qn-client`, `x-qn-client-chain`) so QuickNode can + * 1. Telemetry headers (`x-qn-client`, `x-qn-client-chain`) so Quicknode can * separate MPP SDK traffic from generic public-RPC abuse. - * 2. 429 → `QuickNodeRateLimitError` transform with an actionable upgrade CTA. + * 2. 429 → `QuicknodeRateLimitError` transform with an actionable upgrade CTA. * * Do NOT use for user-supplied `rpcUrl` values — user endpoints must not receive * injected headers and their rate-limit semantics are opaque to us. @@ -21,7 +21,7 @@ export function defaultTransport(url: string, chain: SupportedChain): HttpTransp 'x-qn-client': `@quicknode/mpp/${version}`, 'x-qn-client-chain': chain, // Standard HTTP signal mirroring x-qn-client so any log pipeline that - // keys off User-Agent (QuickNode default aggregation, CDN logs, + // keys off User-Agent (Quicknode default aggregation, CDN logs, // upstream proxies) can still identify SDK traffic even when custom // headers get stripped. Browser fetch silently drops this header — // fine; x-qn-* still carries the signal there. @@ -29,7 +29,7 @@ export function defaultTransport(url: string, chain: SupportedChain): HttpTransp }, }, // Disable viem's built-in retry (default 3 with backoff) so 429s surface - // immediately for us to convert to QuickNodeRateLimitError before the caller + // immediately for us to convert to QuicknodeRateLimitError before the caller // sees them. Side effect: transient non-429 errors (DNS, connection reset, // occasional 5xx) on the default transport also surface without retry. // Callers needing resilience should pass their own rpcUrl. @@ -46,7 +46,7 @@ export function defaultTransport(url: string, chain: SupportedChain): HttpTransp if (err instanceof HttpRequestError && err.status === 429) { const retryAfter = err.headers?.get('retry-after') const parsed = retryAfter ? Number.parseInt(retryAfter, 10) : Number.NaN - throw new QuickNodeRateLimitError(chain, Number.isFinite(parsed) ? parsed : undefined) + throw new QuicknodeRateLimitError(chain, Number.isFinite(parsed) ? parsed : undefined) } throw err } @@ -69,7 +69,7 @@ export function logDefaultTransportOnce(chain: SupportedChain): void { if (loggedChains.has(chain)) return loggedChains.add(chain) console.info( - `[@quicknode/mpp] Using QuickNode public RPC for ${chain}.\n` + + `[@quicknode/mpp] Using Quicknode public RPC for ${chain}.\n` + ` Shared endpoint — rate-limited per IP.\n` + ` Upgrade: https://www.quicknode.com/?utm_source=mpp-sdk`, ) diff --git a/src/server/index.ts b/src/server/index.ts index 4f1fbb9..bb33d5f 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,7 +1,7 @@ import { charge } from './Charge.js' export { NATIVE_TOKEN_ADDRESS } from '../constants.js' -export { QuickNodeRateLimitError } from '../errors.js' +export { QuicknodeRateLimitError } from '../errors.js' export type { CustomTokenConfig } from '../types.js' export { charge } export const evm = { charge } diff --git a/src/server/rpc.test.ts b/src/server/rpc.test.ts index 8379412..50b07fd 100644 --- a/src/server/rpc.test.ts +++ b/src/server/rpc.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict' import { afterEach, beforeEach, test } from 'node:test' -import { QuickNodeRateLimitError } from '../errors.js' +import { QuicknodeRateLimitError } from '../errors.js' import { getPublicClient } from './rpc.js' type FetchFn = typeof globalThis.fetch @@ -25,7 +25,7 @@ afterEach(() => { globalThis.fetch = originalFetch }) -test('user-supplied rpcUrl: 429 surfaces as viem HttpRequestError, NOT QuickNodeRateLimitError', async () => { +test('user-supplied rpcUrl: 429 surfaces as viem HttpRequestError, NOT QuicknodeRateLimitError', async () => { globalThis.fetch = mockFetch(new Response('Too Many Requests', { status: 429 })) const client = getPublicClient({ @@ -38,7 +38,7 @@ test('user-supplied rpcUrl: 429 surfaces as viem HttpRequestError, NOT QuickNode () => client.getChainId(), (err: unknown) => { assert.ok(err instanceof Error, 'is an Error') - assert.ok(!(err instanceof QuickNodeRateLimitError), 'is NOT QuickNodeRateLimitError') + assert.ok(!(err instanceof QuicknodeRateLimitError), 'is NOT QuicknodeRateLimitError') return true }, ) diff --git a/src/types.ts b/src/types.ts index 97f8638..6910cee 100644 --- a/src/types.ts +++ b/src/types.ts @@ -78,7 +78,7 @@ export type ServerParameters = { /** Merchant wallet that receives the USDC transfer. */ recipient: Address /** - * RPC endpoint URL. If omitted, defaults to QuickNode's public shared endpoint + * RPC endpoint URL. If omitted, defaults to Quicknode's public shared endpoint * for the given `chain`. The shared endpoint is rate-limited per IP; upgrade * at https://www.quicknode.com for a dedicated endpoint. */