nozomi-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/README.md +163 -0
- package/dist/index.cjs +166 -0
- package/dist/index.d.cts +50 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +138 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Nozomi SDK
|
|
2
|
+
|
|
3
|
+
Find the fastest Nozomi endpoints for optimal Solana transaction submission.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install nozomi-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Basic Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { findFastestEndpoints } from 'nozomi-sdk';
|
|
17
|
+
|
|
18
|
+
// Find the 2 fastest regional endpoints + auto-routed fallback
|
|
19
|
+
const endpoints = await findFastestEndpoints();
|
|
20
|
+
|
|
21
|
+
console.log(endpoints);
|
|
22
|
+
// [
|
|
23
|
+
// { url: 'https://pit1.nozomi.temporal.xyz', region: 'pittsburgh', minTime: 12.5, ... },
|
|
24
|
+
// { url: 'https://ewr1.nozomi.temporal.xyz', region: 'newark', minTime: 15.2, ... },
|
|
25
|
+
// { url: 'https://nozomi.temporal.xyz', region: 'auto', minTime: 18.0, ... }
|
|
26
|
+
// ]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Find Single Fastest
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
const [fastest] = await findFastestEndpoints({ topCount: 1 });
|
|
33
|
+
console.log(`Fastest: ${fastest.url} (${fastest.minTime.toFixed(2)}ms)`);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### With Solana Web3.js
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { Connection, Keypair, Transaction } from '@solana/web3.js';
|
|
40
|
+
import { findFastestEndpoints } from 'nozomi-sdk';
|
|
41
|
+
|
|
42
|
+
const [fastest] = await findFastestEndpoints({ topCount: 1 });
|
|
43
|
+
|
|
44
|
+
const API_KEY = process.env.NOZOMI_API_KEY;
|
|
45
|
+
const connection = new Connection(`${fastest.url}/?c=${API_KEY}`, 'confirmed');
|
|
46
|
+
|
|
47
|
+
// Send transaction via Nozomi
|
|
48
|
+
const signature = await connection.sendRawTransaction(signedTx, {
|
|
49
|
+
skipPreflight: true,
|
|
50
|
+
maxRetries: 0
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Configuration Options
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
const results = await findFastestEndpoints({
|
|
58
|
+
// Number of measurement pings per endpoint (default: 5, max: 20)
|
|
59
|
+
pingCount: 10,
|
|
60
|
+
|
|
61
|
+
// Number of warmup pings before measurement (default: 2, max: 5)
|
|
62
|
+
warmupCount: 2,
|
|
63
|
+
|
|
64
|
+
// Number of top endpoints to return (default: 2, max: 10)
|
|
65
|
+
topCount: 3,
|
|
66
|
+
|
|
67
|
+
// Timeout per ping in ms (default: 5000, min: 1000, max: 30000)
|
|
68
|
+
timeout: 3000,
|
|
69
|
+
|
|
70
|
+
// Include auto-routed endpoint in results (default: true)
|
|
71
|
+
includeAutoRouted: true,
|
|
72
|
+
|
|
73
|
+
// Custom ping endpoint path (default: '/ping')
|
|
74
|
+
endpoint: '/ping',
|
|
75
|
+
|
|
76
|
+
// Custom endpoints URL (default: GitHub raw URL)
|
|
77
|
+
endpointsUrl: 'https://example.com/endpoints.json',
|
|
78
|
+
|
|
79
|
+
// Custom endpoint configs (skips remote fetch)
|
|
80
|
+
endpoints: [
|
|
81
|
+
{ url: 'https://custom.xyz', region: 'custom', type: 'direct' }
|
|
82
|
+
],
|
|
83
|
+
|
|
84
|
+
// Callback for each endpoint result (useful for progress)
|
|
85
|
+
onResult: (result) => {
|
|
86
|
+
console.log(`${result.url}: ${result.minTime}ms`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Fallback Strategy
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
const endpoints = await findFastestEndpoints({ topCount: 3 });
|
|
95
|
+
|
|
96
|
+
for (const endpoint of endpoints) {
|
|
97
|
+
try {
|
|
98
|
+
const connection = new Connection(`${endpoint.url}/?c=${API_KEY}`);
|
|
99
|
+
const sig = await connection.sendRawTransaction(tx);
|
|
100
|
+
console.log(`Success via ${endpoint.url}`);
|
|
101
|
+
break;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.warn(`Failed on ${endpoint.url}, trying next...`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## API Reference
|
|
109
|
+
|
|
110
|
+
### `findFastestEndpoints(options?)`
|
|
111
|
+
|
|
112
|
+
Returns a promise that resolves to an array of `EndpointResult` objects.
|
|
113
|
+
|
|
114
|
+
**Never throws** - always returns at least one endpoint (the auto-routed fallback).
|
|
115
|
+
|
|
116
|
+
#### Options
|
|
117
|
+
|
|
118
|
+
| Option | Type | Default | Description |
|
|
119
|
+
|--------|------|---------|-------------|
|
|
120
|
+
| `pingCount` | number | 5 | Number of measurement pings (1-20) |
|
|
121
|
+
| `warmupCount` | number | 2 | Number of warmup pings (0-5) |
|
|
122
|
+
| `topCount` | number | 2 | Number of top results to return (1-10) |
|
|
123
|
+
| `timeout` | number | 5000 | Timeout per ping in ms (1000-30000) |
|
|
124
|
+
| `includeAutoRouted` | boolean | true | Include auto-routed endpoint |
|
|
125
|
+
| `endpoint` | string | '/ping' | Ping endpoint path |
|
|
126
|
+
| `endpointsUrl` | string | GitHub URL | URL to fetch endpoint configs |
|
|
127
|
+
| `endpoints` | EndpointConfig[] | - | Custom endpoint configs |
|
|
128
|
+
| `onResult` | function | - | Callback for each result |
|
|
129
|
+
|
|
130
|
+
#### Result Type
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
interface EndpointResult {
|
|
134
|
+
url: string; // Endpoint URL
|
|
135
|
+
region: string; // Region identifier
|
|
136
|
+
minTime: number; // Minimum ping time (ms)
|
|
137
|
+
times?: number[]; // All measurement times
|
|
138
|
+
warmupTimes?: number[]; // Warmup times
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Constants
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import {
|
|
146
|
+
NOZOMI_ENDPOINTS, // Hardcoded fallback endpoints
|
|
147
|
+
NOZOMI_AUTO_ENDPOINT, // Auto-routed endpoint URL
|
|
148
|
+
NOZOMI_ENDPOINTS_URL // Default endpoints JSON URL
|
|
149
|
+
} from 'nozomi-sdk';
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Features
|
|
153
|
+
|
|
154
|
+
- **Zero dependencies** - works in Node.js and browsers
|
|
155
|
+
- **Never throws** - always returns valid results with fallbacks
|
|
156
|
+
- **Region deduplication** - returns only the fastest endpoint per region
|
|
157
|
+
- **Warmup pings** - accounts for TLS/TCP connection setup
|
|
158
|
+
- **Remote config** - fetches latest endpoints from GitHub with fallback
|
|
159
|
+
- **Fully typed** - complete TypeScript definitions
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
NOZOMI_AUTO_ENDPOINT: () => NOZOMI_AUTO_ENDPOINT,
|
|
24
|
+
NOZOMI_ENDPOINTS: () => NOZOMI_ENDPOINTS,
|
|
25
|
+
NOZOMI_ENDPOINTS_URL: () => NOZOMI_ENDPOINTS_URL,
|
|
26
|
+
findFastestEndpoints: () => findFastestEndpoints
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
var NOZOMI_ENDPOINTS_URL = "https://raw.githubusercontent.com/temporalxyz/nozomi-sdk/main/endpoints.json";
|
|
30
|
+
var NOZOMI_AUTO_ENDPOINT = "https://nozomi.temporal.xyz";
|
|
31
|
+
var NOZOMI_ENDPOINTS = [
|
|
32
|
+
{ url: NOZOMI_AUTO_ENDPOINT, region: "auto", type: "auto" },
|
|
33
|
+
{ url: "https://pit1.nozomi.temporal.xyz", region: "pittsburgh", type: "direct" },
|
|
34
|
+
{ url: "https://tyo1.nozomi.temporal.xyz", region: "tokyo", type: "direct" },
|
|
35
|
+
{ url: "https://sgp1.nozomi.temporal.xyz", region: "singapore", type: "direct" },
|
|
36
|
+
{ url: "https://ewr1.nozomi.temporal.xyz", region: "newark", type: "direct" },
|
|
37
|
+
{ url: "https://ams1.nozomi.temporal.xyz", region: "amsterdam", type: "direct" },
|
|
38
|
+
{ url: "https://fra2.nozomi.temporal.xyz", region: "frankfurt", type: "direct" },
|
|
39
|
+
{ url: "https://ash1.nozomi.temporal.xyz", region: "ashburn", type: "direct" },
|
|
40
|
+
{ url: "https://lax1.nozomi.temporal.xyz", region: "los-angeles", type: "direct" },
|
|
41
|
+
{ url: "https://lon1.nozomi.temporal.xyz", region: "london", type: "direct" },
|
|
42
|
+
{ url: "https://pit.nozomi.temporal.xyz", region: "pittsburgh", type: "cloudflare" },
|
|
43
|
+
{ url: "https://tyo.nozomi.temporal.xyz", region: "tokyo", type: "cloudflare" },
|
|
44
|
+
{ url: "https://sgp.nozomi.temporal.xyz", region: "singapore", type: "cloudflare" },
|
|
45
|
+
{ url: "https://ewr.nozomi.temporal.xyz", region: "newark", type: "cloudflare" },
|
|
46
|
+
{ url: "https://ams.nozomi.temporal.xyz", region: "amsterdam", type: "cloudflare" },
|
|
47
|
+
{ url: "https://fra.nozomi.temporal.xyz", region: "frankfurt", type: "cloudflare" },
|
|
48
|
+
{ url: "https://ash.nozomi.temporal.xyz", region: "ashburn", type: "cloudflare" },
|
|
49
|
+
{ url: "https://lax.nozomi.temporal.xyz", region: "los-angeles", type: "cloudflare" },
|
|
50
|
+
{ url: "https://lon.nozomi.temporal.xyz", region: "london", type: "cloudflare" }
|
|
51
|
+
];
|
|
52
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
53
|
+
async function fetchEndpointsFromUrl(url, timeout, retries) {
|
|
54
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
55
|
+
if (attempt > 0) await sleep(attempt * 500);
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(url, { signal: controller.signal, cache: "no-store" });
|
|
60
|
+
clearTimeout(timeoutId);
|
|
61
|
+
if (!response.ok) continue;
|
|
62
|
+
const manifest = await response.json();
|
|
63
|
+
if (manifest?.endpoints && Array.isArray(manifest.endpoints)) {
|
|
64
|
+
const valid = manifest.endpoints.filter(
|
|
65
|
+
(e) => e?.url && typeof e.url === "string" && e.url.startsWith("https://") && e.region
|
|
66
|
+
);
|
|
67
|
+
if (valid.length > 0) return valid;
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
clearTimeout(timeoutId);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
async function getEndpointConfigs(endpointsUrl) {
|
|
76
|
+
const url = endpointsUrl || NOZOMI_ENDPOINTS_URL;
|
|
77
|
+
const remote = await fetchEndpointsFromUrl(url, 3e3, 2);
|
|
78
|
+
return remote && remote.length > 0 ? remote : [...NOZOMI_ENDPOINTS];
|
|
79
|
+
}
|
|
80
|
+
async function measurePing(url, endpoint, timeout) {
|
|
81
|
+
const pingUrl = url.replace(/\/+$/, "") + endpoint;
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
84
|
+
const start = performance.now();
|
|
85
|
+
try {
|
|
86
|
+
const response = await fetch(pingUrl, { method: "GET", signal: controller.signal, cache: "no-store" });
|
|
87
|
+
clearTimeout(timeoutId);
|
|
88
|
+
return response.ok ? performance.now() - start : Infinity;
|
|
89
|
+
} catch {
|
|
90
|
+
clearTimeout(timeoutId);
|
|
91
|
+
return Infinity;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function pingEndpoint(config, pingCount, warmupCount, endpoint, timeout) {
|
|
95
|
+
const warmupTimes = [];
|
|
96
|
+
for (let i = 0; i < warmupCount; i++) {
|
|
97
|
+
warmupTimes.push(await measurePing(config.url, endpoint, timeout));
|
|
98
|
+
}
|
|
99
|
+
const times = [];
|
|
100
|
+
for (let i = 0; i < pingCount; i++) {
|
|
101
|
+
times.push(await measurePing(config.url, endpoint, timeout));
|
|
102
|
+
}
|
|
103
|
+
const minTime = times.length > 0 ? Math.min(...times) : Infinity;
|
|
104
|
+
return { url: config.url, region: config.region, minTime, times, warmupTimes };
|
|
105
|
+
}
|
|
106
|
+
async function findFastestEndpoints(options = {}) {
|
|
107
|
+
try {
|
|
108
|
+
let configs = options.endpoints || await getEndpointConfigs(options.endpointsUrl);
|
|
109
|
+
if (!Array.isArray(configs) || configs.length === 0) configs = [...NOZOMI_ENDPOINTS];
|
|
110
|
+
const pingCount = Math.max(1, Math.min(20, options.pingCount ?? 5));
|
|
111
|
+
const topCount = Math.max(1, Math.min(10, options.topCount ?? 2));
|
|
112
|
+
const timeout = Math.max(1e3, Math.min(3e4, options.timeout ?? 5e3));
|
|
113
|
+
const warmupCount = Math.max(0, Math.min(5, options.warmupCount ?? 2));
|
|
114
|
+
const endpoint = options.endpoint ?? "/ping";
|
|
115
|
+
const includeAutoRouted = options.includeAutoRouted ?? true;
|
|
116
|
+
const results = await Promise.all(
|
|
117
|
+
configs.map(async (config) => {
|
|
118
|
+
const result = await pingEndpoint(config, pingCount, warmupCount, endpoint, timeout);
|
|
119
|
+
try {
|
|
120
|
+
options.onResult?.(result);
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
const validResults = results.filter((r) => r.minTime !== Infinity && isFinite(r.minTime)).sort((a, b) => a.minTime - b.minTime);
|
|
127
|
+
let topResults;
|
|
128
|
+
if (includeAutoRouted) {
|
|
129
|
+
const nonAutoResults = validResults.filter((r) => r.region !== "auto");
|
|
130
|
+
const seenRegions = /* @__PURE__ */ new Set();
|
|
131
|
+
const deduped = [];
|
|
132
|
+
for (const result of nonAutoResults) {
|
|
133
|
+
if (!seenRegions.has(result.region)) {
|
|
134
|
+
seenRegions.add(result.region);
|
|
135
|
+
deduped.push(result);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
topResults = deduped.slice(0, topCount);
|
|
139
|
+
const autoResult = validResults.find((r) => r.region === "auto");
|
|
140
|
+
topResults.push(autoResult ?? { url: NOZOMI_AUTO_ENDPOINT, region: "auto", minTime: Infinity, times: [], warmupTimes: [] });
|
|
141
|
+
} else {
|
|
142
|
+
const seenRegions = /* @__PURE__ */ new Set();
|
|
143
|
+
const deduped = [];
|
|
144
|
+
for (const result of validResults) {
|
|
145
|
+
if (!seenRegions.has(result.region)) {
|
|
146
|
+
seenRegions.add(result.region);
|
|
147
|
+
deduped.push(result);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
topResults = deduped.slice(0, topCount);
|
|
151
|
+
}
|
|
152
|
+
if (topResults.length === 0) {
|
|
153
|
+
topResults = [{ url: NOZOMI_AUTO_ENDPOINT, region: "auto", minTime: Infinity, times: [], warmupTimes: [] }];
|
|
154
|
+
}
|
|
155
|
+
return topResults;
|
|
156
|
+
} catch {
|
|
157
|
+
return [{ url: NOZOMI_AUTO_ENDPOINT, region: "auto", minTime: Infinity, times: [], warmupTimes: [] }];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
161
|
+
0 && (module.exports = {
|
|
162
|
+
NOZOMI_AUTO_ENDPOINT,
|
|
163
|
+
NOZOMI_ENDPOINTS,
|
|
164
|
+
NOZOMI_ENDPOINTS_URL,
|
|
165
|
+
findFastestEndpoints
|
|
166
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nozomi SDK - Endpoint Discovery
|
|
3
|
+
*
|
|
4
|
+
* Find the fastest Nozomi endpoints for optimal transaction submission.
|
|
5
|
+
*/
|
|
6
|
+
/** Remote endpoints JSON structure */
|
|
7
|
+
interface EndpointConfig {
|
|
8
|
+
url: string;
|
|
9
|
+
region: string;
|
|
10
|
+
type: 'auto' | 'direct' | 'cloudflare';
|
|
11
|
+
}
|
|
12
|
+
interface EndpointsManifest {
|
|
13
|
+
version: number;
|
|
14
|
+
updated: string;
|
|
15
|
+
endpoints: EndpointConfig[];
|
|
16
|
+
}
|
|
17
|
+
/** Default GitHub raw URL for remote endpoints */
|
|
18
|
+
declare const NOZOMI_ENDPOINTS_URL = "https://raw.githubusercontent.com/temporalxyz/nozomi-sdk/main/endpoints.json";
|
|
19
|
+
/** Auto-routed endpoint (always included as fallback by default) */
|
|
20
|
+
declare const NOZOMI_AUTO_ENDPOINT = "https://nozomi.temporal.xyz";
|
|
21
|
+
/** Hardcoded fallback endpoints with regions */
|
|
22
|
+
declare const NOZOMI_ENDPOINTS: EndpointConfig[];
|
|
23
|
+
interface EndpointResult {
|
|
24
|
+
url: string;
|
|
25
|
+
region: string;
|
|
26
|
+
minTime: number;
|
|
27
|
+
times?: number[];
|
|
28
|
+
warmupTimes?: number[];
|
|
29
|
+
}
|
|
30
|
+
interface FindFastestOptions {
|
|
31
|
+
endpoints?: EndpointConfig[];
|
|
32
|
+
endpointsUrl?: string;
|
|
33
|
+
pingCount?: number;
|
|
34
|
+
topCount?: number;
|
|
35
|
+
timeout?: number;
|
|
36
|
+
endpoint?: string;
|
|
37
|
+
warmupCount?: number;
|
|
38
|
+
includeAutoRouted?: boolean;
|
|
39
|
+
onResult?: (result: EndpointResult) => void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Find the fastest Nozomi endpoints.
|
|
43
|
+
*
|
|
44
|
+
* NEVER THROWS - always returns at least the auto-routed endpoint.
|
|
45
|
+
*
|
|
46
|
+
* By default returns [2 fastest regional endpoints, auto-routed endpoint].
|
|
47
|
+
*/
|
|
48
|
+
declare function findFastestEndpoints(options?: FindFastestOptions): Promise<EndpointResult[]>;
|
|
49
|
+
|
|
50
|
+
export { type EndpointConfig, type EndpointResult, type EndpointsManifest, type FindFastestOptions, NOZOMI_AUTO_ENDPOINT, NOZOMI_ENDPOINTS, NOZOMI_ENDPOINTS_URL, findFastestEndpoints };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nozomi SDK - Endpoint Discovery
|
|
3
|
+
*
|
|
4
|
+
* Find the fastest Nozomi endpoints for optimal transaction submission.
|
|
5
|
+
*/
|
|
6
|
+
/** Remote endpoints JSON structure */
|
|
7
|
+
interface EndpointConfig {
|
|
8
|
+
url: string;
|
|
9
|
+
region: string;
|
|
10
|
+
type: 'auto' | 'direct' | 'cloudflare';
|
|
11
|
+
}
|
|
12
|
+
interface EndpointsManifest {
|
|
13
|
+
version: number;
|
|
14
|
+
updated: string;
|
|
15
|
+
endpoints: EndpointConfig[];
|
|
16
|
+
}
|
|
17
|
+
/** Default GitHub raw URL for remote endpoints */
|
|
18
|
+
declare const NOZOMI_ENDPOINTS_URL = "https://raw.githubusercontent.com/temporalxyz/nozomi-sdk/main/endpoints.json";
|
|
19
|
+
/** Auto-routed endpoint (always included as fallback by default) */
|
|
20
|
+
declare const NOZOMI_AUTO_ENDPOINT = "https://nozomi.temporal.xyz";
|
|
21
|
+
/** Hardcoded fallback endpoints with regions */
|
|
22
|
+
declare const NOZOMI_ENDPOINTS: EndpointConfig[];
|
|
23
|
+
interface EndpointResult {
|
|
24
|
+
url: string;
|
|
25
|
+
region: string;
|
|
26
|
+
minTime: number;
|
|
27
|
+
times?: number[];
|
|
28
|
+
warmupTimes?: number[];
|
|
29
|
+
}
|
|
30
|
+
interface FindFastestOptions {
|
|
31
|
+
endpoints?: EndpointConfig[];
|
|
32
|
+
endpointsUrl?: string;
|
|
33
|
+
pingCount?: number;
|
|
34
|
+
topCount?: number;
|
|
35
|
+
timeout?: number;
|
|
36
|
+
endpoint?: string;
|
|
37
|
+
warmupCount?: number;
|
|
38
|
+
includeAutoRouted?: boolean;
|
|
39
|
+
onResult?: (result: EndpointResult) => void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Find the fastest Nozomi endpoints.
|
|
43
|
+
*
|
|
44
|
+
* NEVER THROWS - always returns at least the auto-routed endpoint.
|
|
45
|
+
*
|
|
46
|
+
* By default returns [2 fastest regional endpoints, auto-routed endpoint].
|
|
47
|
+
*/
|
|
48
|
+
declare function findFastestEndpoints(options?: FindFastestOptions): Promise<EndpointResult[]>;
|
|
49
|
+
|
|
50
|
+
export { type EndpointConfig, type EndpointResult, type EndpointsManifest, type FindFastestOptions, NOZOMI_AUTO_ENDPOINT, NOZOMI_ENDPOINTS, NOZOMI_ENDPOINTS_URL, findFastestEndpoints };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var NOZOMI_ENDPOINTS_URL = "https://raw.githubusercontent.com/temporalxyz/nozomi-sdk/main/endpoints.json";
|
|
3
|
+
var NOZOMI_AUTO_ENDPOINT = "https://nozomi.temporal.xyz";
|
|
4
|
+
var NOZOMI_ENDPOINTS = [
|
|
5
|
+
{ url: NOZOMI_AUTO_ENDPOINT, region: "auto", type: "auto" },
|
|
6
|
+
{ url: "https://pit1.nozomi.temporal.xyz", region: "pittsburgh", type: "direct" },
|
|
7
|
+
{ url: "https://tyo1.nozomi.temporal.xyz", region: "tokyo", type: "direct" },
|
|
8
|
+
{ url: "https://sgp1.nozomi.temporal.xyz", region: "singapore", type: "direct" },
|
|
9
|
+
{ url: "https://ewr1.nozomi.temporal.xyz", region: "newark", type: "direct" },
|
|
10
|
+
{ url: "https://ams1.nozomi.temporal.xyz", region: "amsterdam", type: "direct" },
|
|
11
|
+
{ url: "https://fra2.nozomi.temporal.xyz", region: "frankfurt", type: "direct" },
|
|
12
|
+
{ url: "https://ash1.nozomi.temporal.xyz", region: "ashburn", type: "direct" },
|
|
13
|
+
{ url: "https://lax1.nozomi.temporal.xyz", region: "los-angeles", type: "direct" },
|
|
14
|
+
{ url: "https://lon1.nozomi.temporal.xyz", region: "london", type: "direct" },
|
|
15
|
+
{ url: "https://pit.nozomi.temporal.xyz", region: "pittsburgh", type: "cloudflare" },
|
|
16
|
+
{ url: "https://tyo.nozomi.temporal.xyz", region: "tokyo", type: "cloudflare" },
|
|
17
|
+
{ url: "https://sgp.nozomi.temporal.xyz", region: "singapore", type: "cloudflare" },
|
|
18
|
+
{ url: "https://ewr.nozomi.temporal.xyz", region: "newark", type: "cloudflare" },
|
|
19
|
+
{ url: "https://ams.nozomi.temporal.xyz", region: "amsterdam", type: "cloudflare" },
|
|
20
|
+
{ url: "https://fra.nozomi.temporal.xyz", region: "frankfurt", type: "cloudflare" },
|
|
21
|
+
{ url: "https://ash.nozomi.temporal.xyz", region: "ashburn", type: "cloudflare" },
|
|
22
|
+
{ url: "https://lax.nozomi.temporal.xyz", region: "los-angeles", type: "cloudflare" },
|
|
23
|
+
{ url: "https://lon.nozomi.temporal.xyz", region: "london", type: "cloudflare" }
|
|
24
|
+
];
|
|
25
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
26
|
+
async function fetchEndpointsFromUrl(url, timeout, retries) {
|
|
27
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
28
|
+
if (attempt > 0) await sleep(attempt * 500);
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(url, { signal: controller.signal, cache: "no-store" });
|
|
33
|
+
clearTimeout(timeoutId);
|
|
34
|
+
if (!response.ok) continue;
|
|
35
|
+
const manifest = await response.json();
|
|
36
|
+
if (manifest?.endpoints && Array.isArray(manifest.endpoints)) {
|
|
37
|
+
const valid = manifest.endpoints.filter(
|
|
38
|
+
(e) => e?.url && typeof e.url === "string" && e.url.startsWith("https://") && e.region
|
|
39
|
+
);
|
|
40
|
+
if (valid.length > 0) return valid;
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
clearTimeout(timeoutId);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
async function getEndpointConfigs(endpointsUrl) {
|
|
49
|
+
const url = endpointsUrl || NOZOMI_ENDPOINTS_URL;
|
|
50
|
+
const remote = await fetchEndpointsFromUrl(url, 3e3, 2);
|
|
51
|
+
return remote && remote.length > 0 ? remote : [...NOZOMI_ENDPOINTS];
|
|
52
|
+
}
|
|
53
|
+
async function measurePing(url, endpoint, timeout) {
|
|
54
|
+
const pingUrl = url.replace(/\/+$/, "") + endpoint;
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
57
|
+
const start = performance.now();
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(pingUrl, { method: "GET", signal: controller.signal, cache: "no-store" });
|
|
60
|
+
clearTimeout(timeoutId);
|
|
61
|
+
return response.ok ? performance.now() - start : Infinity;
|
|
62
|
+
} catch {
|
|
63
|
+
clearTimeout(timeoutId);
|
|
64
|
+
return Infinity;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function pingEndpoint(config, pingCount, warmupCount, endpoint, timeout) {
|
|
68
|
+
const warmupTimes = [];
|
|
69
|
+
for (let i = 0; i < warmupCount; i++) {
|
|
70
|
+
warmupTimes.push(await measurePing(config.url, endpoint, timeout));
|
|
71
|
+
}
|
|
72
|
+
const times = [];
|
|
73
|
+
for (let i = 0; i < pingCount; i++) {
|
|
74
|
+
times.push(await measurePing(config.url, endpoint, timeout));
|
|
75
|
+
}
|
|
76
|
+
const minTime = times.length > 0 ? Math.min(...times) : Infinity;
|
|
77
|
+
return { url: config.url, region: config.region, minTime, times, warmupTimes };
|
|
78
|
+
}
|
|
79
|
+
async function findFastestEndpoints(options = {}) {
|
|
80
|
+
try {
|
|
81
|
+
let configs = options.endpoints || await getEndpointConfigs(options.endpointsUrl);
|
|
82
|
+
if (!Array.isArray(configs) || configs.length === 0) configs = [...NOZOMI_ENDPOINTS];
|
|
83
|
+
const pingCount = Math.max(1, Math.min(20, options.pingCount ?? 5));
|
|
84
|
+
const topCount = Math.max(1, Math.min(10, options.topCount ?? 2));
|
|
85
|
+
const timeout = Math.max(1e3, Math.min(3e4, options.timeout ?? 5e3));
|
|
86
|
+
const warmupCount = Math.max(0, Math.min(5, options.warmupCount ?? 2));
|
|
87
|
+
const endpoint = options.endpoint ?? "/ping";
|
|
88
|
+
const includeAutoRouted = options.includeAutoRouted ?? true;
|
|
89
|
+
const results = await Promise.all(
|
|
90
|
+
configs.map(async (config) => {
|
|
91
|
+
const result = await pingEndpoint(config, pingCount, warmupCount, endpoint, timeout);
|
|
92
|
+
try {
|
|
93
|
+
options.onResult?.(result);
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
const validResults = results.filter((r) => r.minTime !== Infinity && isFinite(r.minTime)).sort((a, b) => a.minTime - b.minTime);
|
|
100
|
+
let topResults;
|
|
101
|
+
if (includeAutoRouted) {
|
|
102
|
+
const nonAutoResults = validResults.filter((r) => r.region !== "auto");
|
|
103
|
+
const seenRegions = /* @__PURE__ */ new Set();
|
|
104
|
+
const deduped = [];
|
|
105
|
+
for (const result of nonAutoResults) {
|
|
106
|
+
if (!seenRegions.has(result.region)) {
|
|
107
|
+
seenRegions.add(result.region);
|
|
108
|
+
deduped.push(result);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
topResults = deduped.slice(0, topCount);
|
|
112
|
+
const autoResult = validResults.find((r) => r.region === "auto");
|
|
113
|
+
topResults.push(autoResult ?? { url: NOZOMI_AUTO_ENDPOINT, region: "auto", minTime: Infinity, times: [], warmupTimes: [] });
|
|
114
|
+
} else {
|
|
115
|
+
const seenRegions = /* @__PURE__ */ new Set();
|
|
116
|
+
const deduped = [];
|
|
117
|
+
for (const result of validResults) {
|
|
118
|
+
if (!seenRegions.has(result.region)) {
|
|
119
|
+
seenRegions.add(result.region);
|
|
120
|
+
deduped.push(result);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
topResults = deduped.slice(0, topCount);
|
|
124
|
+
}
|
|
125
|
+
if (topResults.length === 0) {
|
|
126
|
+
topResults = [{ url: NOZOMI_AUTO_ENDPOINT, region: "auto", minTime: Infinity, times: [], warmupTimes: [] }];
|
|
127
|
+
}
|
|
128
|
+
return topResults;
|
|
129
|
+
} catch {
|
|
130
|
+
return [{ url: NOZOMI_AUTO_ENDPOINT, region: "auto", minTime: Infinity, times: [], warmupTimes: [] }];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export {
|
|
134
|
+
NOZOMI_AUTO_ENDPOINT,
|
|
135
|
+
NOZOMI_ENDPOINTS,
|
|
136
|
+
NOZOMI_ENDPOINTS_URL,
|
|
137
|
+
findFastestEndpoints
|
|
138
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nozomi-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Find the fastest Nozomi endpoints for optimal transaction submission",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"browser": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"nozomi",
|
|
28
|
+
"temporal",
|
|
29
|
+
"solana",
|
|
30
|
+
"blockchain",
|
|
31
|
+
"transactions",
|
|
32
|
+
"latency"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^25.0.3",
|
|
37
|
+
"tsup": "^8.0.0",
|
|
38
|
+
"typescript": "^5.0.0",
|
|
39
|
+
"vitest": "^2.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|