@direct.dev/sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +6 -0
- package/README.md +339 -0
- package/lib/index.cjs +5 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.mjs +5 -0
- package/package.json +56 -0
package/LICENSE.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# @direct.dev/sdk
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<p>
|
|
5
|
+
<a href="https://direct.dev/">
|
|
6
|
+
<img src="https://s6.imgcdn.dev/YVbIIy.png" alt="Direct.dev logo" width="125" />
|
|
7
|
+
</a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p>
|
|
11
|
+
<a href="https://www.npmjs.com/package/@direct.dev/sdk"><img alt="NPM Version" src="https://img.shields.io/npm/v/%40direct.dev%2Fsdk?style=for-the-badge&labelColor=555&color=00BCB1"></a>
|
|
12
|
+
<a href="https://direct.dev/terms"><img alt="License" src="https://img.shields.io/badge/license-Direct.dev%20Terms-blue?style=for-the-badge&labelColor=555&color=00BCB1"></a>
|
|
13
|
+
</p>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
A drop-in `fetch` wrapper for [Direct.dev](https://direct.dev/) RPC endpoints. Your existing code keeps working — Direct.dev quietly takes over JSON-RPC calls to its own URLs and serves them faster, with caching, sync, and failover built in. Everything else passes through to native `fetch` untouched.
|
|
17
|
+
|
|
18
|
+
## Highlights
|
|
19
|
+
|
|
20
|
+
**Sub-millisecond responses**<br />
|
|
21
|
+
Popular requests are answered from a local cache; the rest are routed through the Direct.dev edge.
|
|
22
|
+
|
|
23
|
+
**Less bandwidth**<br />
|
|
24
|
+
Direct Sync streams only what changed since your last call, instead of resending the full payload every time.
|
|
25
|
+
|
|
26
|
+
**Compact wire format**<br />
|
|
27
|
+
Direct Wire is a binary protocol tuned for Web3 traffic — smaller frames and faster decode than JSON-RPC.
|
|
28
|
+
|
|
29
|
+
**Built-in failover**<br />
|
|
30
|
+
Calls reroute through a healthy path automatically when an upstream is unavailable. No retry logic to write.
|
|
31
|
+
|
|
32
|
+
**Drop-in**<br />
|
|
33
|
+
Plugs into native `fetch`, so viem, ethers, and your own code work unchanged. Only Direct.dev URLs are intercepted.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npm install @direct.dev/sdk
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
Call `install()` once at app startup and you're done:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { install } from "@direct.dev/sdk";
|
|
47
|
+
|
|
48
|
+
install();
|
|
49
|
+
|
|
50
|
+
const res = await fetch("https://rpc.direct.dev/v1/<projectId>.<token>/ethereum", {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "Content-Type": "application/json" },
|
|
53
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_blockNumber", params: [] }),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const { result } = await res.json();
|
|
57
|
+
// "0x134a1c0"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
That's it. Every call to a Direct.dev RPC URL now goes through the SDK; everything else still hits native `fetch`.
|
|
61
|
+
|
|
62
|
+
### With options
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
install({
|
|
66
|
+
logging: { client: "warn", requests: "summary" },
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Without touching globals
|
|
71
|
+
|
|
72
|
+
If you'd rather not wrap `globalThis.fetch` (e.g. inside a library, or to scope to a single instance), use `createFetch()`:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { createFetch } from "@direct.dev/sdk";
|
|
76
|
+
|
|
77
|
+
const directFetch = createFetch({
|
|
78
|
+
logging: { client: "info" },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const res = await directFetch("https://rpc.direct.dev/v1/<projectId>.<token>/ethereum", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "Content-Type": "application/json" },
|
|
84
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_blockNumber", params: [] }),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const { result } = await res.json();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
> Pick one — `install()` or `createFetch()`. Don't mix them.
|
|
91
|
+
|
|
92
|
+
## Using with viem
|
|
93
|
+
|
|
94
|
+
With `install()`, viem (and anything else that reaches for `fetch`) just works:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { install } from "@direct.dev/sdk";
|
|
98
|
+
import { createPublicClient, http } from "viem";
|
|
99
|
+
import { mainnet } from "viem/chains";
|
|
100
|
+
|
|
101
|
+
install();
|
|
102
|
+
|
|
103
|
+
const client = createPublicClient({
|
|
104
|
+
chain: mainnet,
|
|
105
|
+
transport: http("https://rpc.direct.dev/v1/<projectId>.<token>/ethereum"),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await client.getBlockNumber();
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
If you'd rather keep the global `fetch` untouched, pass `createFetch()` directly:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { createPublicClient, http } from "viem";
|
|
115
|
+
import { mainnet } from "viem/chains";
|
|
116
|
+
import { createFetch } from "@direct.dev/sdk";
|
|
117
|
+
|
|
118
|
+
const directFetch = createFetch({ logging: { client: "info" } });
|
|
119
|
+
|
|
120
|
+
const client = createPublicClient({
|
|
121
|
+
chain: mainnet,
|
|
122
|
+
transport: http("https://rpc.direct.dev/v1/<projectId>.<token>/ethereum", {
|
|
123
|
+
fetch: directFetch,
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Preload
|
|
129
|
+
|
|
130
|
+
Warm endpoints at startup so the first request resolves locally.
|
|
131
|
+
|
|
132
|
+
In the browser, prefer an HTML preload link. It lets the browser start fetching the warmup payload before your JS bundle even loads:
|
|
133
|
+
|
|
134
|
+
```html
|
|
135
|
+
<link
|
|
136
|
+
rel="preload"
|
|
137
|
+
as="fetch"
|
|
138
|
+
crossorigin="anonymous"
|
|
139
|
+
href="https://rpc.direct.dev/v1/<projectId>.<token>/ethereum"
|
|
140
|
+
/>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
On the server, or in environments without preload links, pass `preload` to `install()`:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
install({
|
|
147
|
+
preload: [
|
|
148
|
+
"https://rpc.direct.dev/v1/<projectId>.<token>/ethereum",
|
|
149
|
+
"https://rpc.direct.dev/v1/<projectId>.<token>/sonic",
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Configuration
|
|
155
|
+
|
|
156
|
+
| Option | Type | Default | Description |
|
|
157
|
+
| --- | --- | --- | --- |
|
|
158
|
+
| `logging.client` | `"silent" \| "error" \| "warn" \| "info" \| "debug"` | `"warn"` | Operational / lifecycle log verbosity. |
|
|
159
|
+
| `logging.requests` | `"off" \| "summary" \| "debug"` | `"off"` | Per-request tracing. |
|
|
160
|
+
| `logging.onEvent` | `(event) => void` | — | Structured event sink. Replaces console output when set. |
|
|
161
|
+
| `fetch` | `typeof fetch` | `globalThis.fetch` | Underlying fetch to wrap. |
|
|
162
|
+
| `createWebSocket` | `(url) => WebSocket` | `globalThis.WebSocket` | WebSocket factory for Direct Wire sync. |
|
|
163
|
+
| `preload` | `string[]` | auto from `<link rel="preload">` | Endpoint URLs to warm at startup. In the browser, the SDK already picks up `<link rel="preload">` tags automatically — use this option for SSR / non-browser runtimes, or to add endpoints not declared in the HTML. |
|
|
164
|
+
|
|
165
|
+
### `fetch`
|
|
166
|
+
|
|
167
|
+
Wrap a specific implementation instead of `globalThis.fetch`. Useful in tests, polyfilled environments, or when you want a `createFetch()` instance that bypasses an existing `install()`:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
const directFetch = createFetch({
|
|
171
|
+
fetch: window.fetch.bind(window),
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### `createWebSocket`
|
|
176
|
+
|
|
177
|
+
Provide your own constructor for the Direct Wire sync connection. Handy for instrumentation, tests, or runtimes where `globalThis.WebSocket` isn't what you want:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
install({
|
|
181
|
+
createWebSocket: (url) => new WebSocket(url),
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Logging
|
|
186
|
+
|
|
187
|
+
The `logging` option covers three independent concerns:
|
|
188
|
+
|
|
189
|
+
- **`logging.client`** — operational logs (preload, sync, client phases).
|
|
190
|
+
- **`logging.requests`** — per-request tracing.
|
|
191
|
+
- **`logging.onEvent`** — a structured sink. When set, events go here instead of the console.
|
|
192
|
+
|
|
193
|
+
`logging.client` and `logging.requests` don't gate each other — `requests: "summary"` still logs request summaries when `client` is `"warn"`.
|
|
194
|
+
|
|
195
|
+
### `logging.client`
|
|
196
|
+
|
|
197
|
+
| Level | What's emitted |
|
|
198
|
+
| --- | --- |
|
|
199
|
+
| `silent` | Nothing. |
|
|
200
|
+
| `error` | Errors only. `[direct:ERROR] Batch contains duplicate request IDs { ids: [1, 1] }` |
|
|
201
|
+
| `warn` | Errors + warnings. `[direct:WARN] Request failed { input: ..., err: ... }` |
|
|
202
|
+
| `info` | Reserved for sparse operational summaries. No default emissions yet. |
|
|
203
|
+
| `debug` | Full lifecycle: preload, sync, client phases. |
|
|
204
|
+
|
|
205
|
+
Example debug output:
|
|
206
|
+
|
|
207
|
+
```text
|
|
208
|
+
[direct:DEBUG] preload.phase {
|
|
209
|
+
phase: "applied",
|
|
210
|
+
client_instance_id: "client_ethereum_f47ac10b_1",
|
|
211
|
+
network_id: "ethereum"
|
|
212
|
+
}
|
|
213
|
+
[direct:DEBUG] client.phase {
|
|
214
|
+
phase: "created",
|
|
215
|
+
client_instance_id: "client_ethereum_f47ac10b_1",
|
|
216
|
+
network_id: "ethereum"
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### `logging.requests`
|
|
221
|
+
|
|
222
|
+
| Level | What's emitted |
|
|
223
|
+
| --- | --- |
|
|
224
|
+
| `off` | No request tracing. |
|
|
225
|
+
| `summary` | One concise resolution log per handled request. |
|
|
226
|
+
| `debug` | `request_resolution` + `request_pipeline_resolution` + `request_handled` + `block.state_probe`. |
|
|
227
|
+
|
|
228
|
+
Summary example:
|
|
229
|
+
|
|
230
|
+
```text
|
|
231
|
+
[direct:REQUEST] request_resolution {
|
|
232
|
+
method: "eth_blockNumber",
|
|
233
|
+
latency_ms: 8,
|
|
234
|
+
stage_type: "cache",
|
|
235
|
+
stage_name: "local_memory_cache"
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Debug example (a single request):
|
|
240
|
+
|
|
241
|
+
```text
|
|
242
|
+
[direct:REQUEST_DEBUG] request_pipeline_resolution {
|
|
243
|
+
method: "eth_call",
|
|
244
|
+
latency_ms: 12,
|
|
245
|
+
stage_type: "cache",
|
|
246
|
+
stage_name: "continuum_cache"
|
|
247
|
+
}
|
|
248
|
+
[direct:REQUEST_DEBUG] request_handled {
|
|
249
|
+
req: { jsonrpc: "2.0", id: 1, method: "eth_call", params: [...] },
|
|
250
|
+
res: { jsonrpc: "2.0", id: 1, result: "0x1" },
|
|
251
|
+
latency_ms: 12
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### `logging.onEvent`
|
|
256
|
+
|
|
257
|
+
Get events as structured objects — useful for dashboards, telemetry, or piping into your own observability stack:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
install({
|
|
261
|
+
logging: {
|
|
262
|
+
client: "debug",
|
|
263
|
+
requests: "debug",
|
|
264
|
+
onEvent(event) {
|
|
265
|
+
dashboard.push(event);
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Event shape:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
type DirectLogEvent = {
|
|
275
|
+
level: "debug" | "info" | "warn" | "error";
|
|
276
|
+
name: string;
|
|
277
|
+
value: Record<string, unknown>;
|
|
278
|
+
timestamp: number;
|
|
279
|
+
};
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Stable public events** — always safe to consume:
|
|
283
|
+
|
|
284
|
+
- `request_resolution`
|
|
285
|
+
- `preload.phase`
|
|
286
|
+
- `preload.clock_sync`
|
|
287
|
+
- `sync.rebuild`
|
|
288
|
+
- `client.phase`
|
|
289
|
+
|
|
290
|
+
**Debug-only diagnostics** — only emitted when the relevant log level is active:
|
|
291
|
+
|
|
292
|
+
- `request_pipeline_resolution`
|
|
293
|
+
- `request_handled`
|
|
294
|
+
- `block.state_probe`
|
|
295
|
+
|
|
296
|
+
Typed helper unions live in `@direct.dev/client`: `KnownDirectLogEvent`, `StableDirectLogEvent`, `DebugDirectLogEvent`.
|
|
297
|
+
|
|
298
|
+
## Inspecting the live client
|
|
299
|
+
|
|
300
|
+
The SDK intercepts a special JSON-RPC method, `direct_client_status`, and answers it locally without touching the network. Useful for debugging your integration:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
const res = await fetch("https://rpc.direct.dev/v1/<projectId>.<token>/ethereum", {
|
|
304
|
+
method: "POST",
|
|
305
|
+
headers: { "Content-Type": "application/json" },
|
|
306
|
+
body: JSON.stringify({
|
|
307
|
+
jsonrpc: "2.0",
|
|
308
|
+
id: 1,
|
|
309
|
+
method: "direct_client_status",
|
|
310
|
+
params: [],
|
|
311
|
+
}),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const { result } = await res.json();
|
|
315
|
+
// {
|
|
316
|
+
// initialized: true,
|
|
317
|
+
// projectId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
|
318
|
+
// projectToken: "abc123",
|
|
319
|
+
// networkId: "ethereum",
|
|
320
|
+
// baseUrl: "https://rpc.direct.dev",
|
|
321
|
+
// logging: { client: "warn", requests: "off" }
|
|
322
|
+
// }
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## How it works
|
|
326
|
+
|
|
327
|
+
The SDK only takes over POST requests whose body is JSON-RPC and whose URL matches a Direct.dev RPC endpoint:
|
|
328
|
+
|
|
329
|
+
- `https://rpc.direct.dev/v1/<projectId>.<token>/<networkId>`
|
|
330
|
+
- `https://staging.rpc.direct.dev/v1/<projectId>.<token>/<networkId>`
|
|
331
|
+
- `https://prod.rpc.direct.dev/v1/<projectId>.<token>/<networkId>`
|
|
332
|
+
|
|
333
|
+
Everything else — REST APIs, image loads, third-party APIs — passes straight through to native `fetch`.
|
|
334
|
+
|
|
335
|
+
## License
|
|
336
|
+
|
|
337
|
+
Provided under the [Direct.dev Terms and Conditions](https://direct.dev/terms). Use of this package requires agreement to those terms.
|
|
338
|
+
|
|
339
|
+
For questions, reach out at [info@direct.dev](mailto:info@direct.dev).
|