blue-js-sdk 2.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/CHANGELOG.md +446 -0
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/ai-path/ADMIN-ELEVATION.md +116 -0
- package/ai-path/AI-MANIFESTO.md +185 -0
- package/ai-path/BREAKING.md +74 -0
- package/ai-path/CHECKLIST.md +619 -0
- package/ai-path/CONNECTION-STEPS.md +724 -0
- package/ai-path/DECISION-TREE.md +378 -0
- package/ai-path/DEPENDENCIES.md +459 -0
- package/ai-path/E2E-FLOW.md +1555 -0
- package/ai-path/FAILURES.md +403 -0
- package/ai-path/GUIDE.md +1217 -0
- package/ai-path/README.md +558 -0
- package/ai-path/SPLIT-TUNNEL.md +266 -0
- package/ai-path/cli.js +535 -0
- package/ai-path/connect.js +884 -0
- package/ai-path/discover.js +178 -0
- package/ai-path/environment.js +266 -0
- package/ai-path/errors.js +86 -0
- package/ai-path/examples/autonomous-agent.mjs +220 -0
- package/ai-path/examples/multi-region.mjs +174 -0
- package/ai-path/examples/one-shot.mjs +31 -0
- package/ai-path/index.js +60 -0
- package/ai-path/pricing.js +136 -0
- package/ai-path/recommend.js +413 -0
- package/ai-path/run-admin.vbs +25 -0
- package/ai-path/setup.js +291 -0
- package/ai-path/wallet.js +137 -0
- package/app-helpers.js +363 -0
- package/app-settings.js +95 -0
- package/app-types.js +267 -0
- package/audit.js +847 -0
- package/batch.js +293 -0
- package/bin/setup.js +376 -0
- package/chain/authz.js +109 -0
- package/chain/broadcast.js +472 -0
- package/chain/client.js +160 -0
- package/chain/fee-grants.js +305 -0
- package/chain/index.js +891 -0
- package/chain/lcd.js +313 -0
- package/chain/queries.js +547 -0
- package/chain/rpc.js +408 -0
- package/chain/wallet.js +141 -0
- package/cli/config.js +143 -0
- package/cli/index.js +463 -0
- package/cli/output.js +182 -0
- package/cli.js +491 -0
- package/client/index.js +251 -0
- package/client.js +271 -0
- package/config/index.js +255 -0
- package/connection/connect.js +849 -0
- package/connection/disconnect.js +180 -0
- package/connection/discovery.js +321 -0
- package/connection/index.js +76 -0
- package/connection/proxy.js +148 -0
- package/connection/resilience.js +428 -0
- package/connection/security.js +232 -0
- package/connection/state.js +369 -0
- package/connection/tunnel.js +691 -0
- package/consumer.js +132 -0
- package/cosmjs-setup.js +1884 -0
- package/defaults.js +366 -0
- package/disk-cache.js +107 -0
- package/dist/client.d.ts +108 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +400 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/errors/index.js +112 -0
- package/errors.js +218 -0
- package/examples/README.md +64 -0
- package/examples/connect-direct.mjs +106 -0
- package/examples/connect-plan.mjs +125 -0
- package/examples/error-handling.mjs +109 -0
- package/examples/query-nodes.mjs +94 -0
- package/examples/wallet-basics.mjs +61 -0
- package/generated/amino/amino.ts +9 -0
- package/generated/cosmos/base/v1beta1/coin.ts +365 -0
- package/generated/cosmos_proto/cosmos.ts +323 -0
- package/generated/gogoproto/gogo.ts +9 -0
- package/generated/google/protobuf/descriptor.ts +7601 -0
- package/generated/google/protobuf/duration.ts +208 -0
- package/generated/google/protobuf/timestamp.ts +238 -0
- package/generated/sentinel/lease/v1/events.ts +924 -0
- package/generated/sentinel/lease/v1/lease.ts +292 -0
- package/generated/sentinel/lease/v1/msg.ts +949 -0
- package/generated/sentinel/lease/v1/params.ts +164 -0
- package/generated/sentinel/node/v3/events.ts +881 -0
- package/generated/sentinel/node/v3/msg.ts +1002 -0
- package/generated/sentinel/node/v3/node.ts +263 -0
- package/generated/sentinel/node/v3/params.ts +183 -0
- package/generated/sentinel/plan/v3/events.ts +675 -0
- package/generated/sentinel/plan/v3/msg.ts +1191 -0
- package/generated/sentinel/plan/v3/plan.ts +283 -0
- package/generated/sentinel/provider/v2/events.ts +171 -0
- package/generated/sentinel/provider/v2/msg.ts +480 -0
- package/generated/sentinel/provider/v2/params.ts +131 -0
- package/generated/sentinel/provider/v2/provider.ts +246 -0
- package/generated/sentinel/session/v3/events.ts +480 -0
- package/generated/sentinel/session/v3/msg.ts +616 -0
- package/generated/sentinel/session/v3/params.ts +260 -0
- package/generated/sentinel/session/v3/proof.ts +180 -0
- package/generated/sentinel/session/v3/session.ts +384 -0
- package/generated/sentinel/subscription/v3/events.ts +1181 -0
- package/generated/sentinel/subscription/v3/msg.ts +1305 -0
- package/generated/sentinel/subscription/v3/params.ts +167 -0
- package/generated/sentinel/subscription/v3/subscription.ts +315 -0
- package/generated/sentinel/types/v1/bandwidth.ts +124 -0
- package/generated/sentinel/types/v1/price.ts +149 -0
- package/generated/sentinel/types/v1/renewal.ts +87 -0
- package/generated/sentinel/types/v1/status.ts +54 -0
- package/generated/typeRegistry.ts +27 -0
- package/index.js +486 -0
- package/node-connect.js +3015 -0
- package/operator.js +134 -0
- package/package.json +113 -0
- package/plan-operations.js +199 -0
- package/preflight.js +352 -0
- package/pricing/index.js +262 -0
- package/proto/amino/amino.proto +84 -0
- package/proto/cosmos/base/v1beta1/coin.proto +61 -0
- package/proto/cosmos_proto/cosmos.proto +112 -0
- package/proto/gogoproto/gogo.proto +145 -0
- package/proto/google/api/annotations.proto +31 -0
- package/proto/google/api/http.proto +370 -0
- package/proto/google/protobuf/any.proto +106 -0
- package/proto/google/protobuf/duration.proto +115 -0
- package/proto/google/protobuf/timestamp.proto +145 -0
- package/proto/sentinel/lease/v1/events.proto +52 -0
- package/proto/sentinel/lease/v1/genesis.proto +15 -0
- package/proto/sentinel/lease/v1/lease.proto +25 -0
- package/proto/sentinel/lease/v1/msg.proto +62 -0
- package/proto/sentinel/lease/v1/params.proto +17 -0
- package/proto/sentinel/node/v3/events.proto +50 -0
- package/proto/sentinel/node/v3/genesis.proto +15 -0
- package/proto/sentinel/node/v3/msg.proto +63 -0
- package/proto/sentinel/node/v3/node.proto +27 -0
- package/proto/sentinel/node/v3/params.proto +21 -0
- package/proto/sentinel/node/v3/querier.proto +63 -0
- package/proto/sentinel/plan/v3/events.proto +41 -0
- package/proto/sentinel/plan/v3/genesis.proto +21 -0
- package/proto/sentinel/plan/v3/msg.proto +83 -0
- package/proto/sentinel/plan/v3/plan.proto +32 -0
- package/proto/sentinel/plan/v3/querier.proto +53 -0
- package/proto/sentinel/provider/v2/events.proto +16 -0
- package/proto/sentinel/provider/v2/genesis.proto +15 -0
- package/proto/sentinel/provider/v2/msg.proto +35 -0
- package/proto/sentinel/provider/v2/params.proto +17 -0
- package/proto/sentinel/provider/v2/provider.proto +24 -0
- package/proto/sentinel/provider/v3/genesis.proto +15 -0
- package/proto/sentinel/provider/v3/params.proto +13 -0
- package/proto/sentinel/session/v3/events.proto +30 -0
- package/proto/sentinel/session/v3/genesis.proto +15 -0
- package/proto/sentinel/session/v3/msg.proto +50 -0
- package/proto/sentinel/session/v3/params.proto +25 -0
- package/proto/sentinel/session/v3/proof.proto +25 -0
- package/proto/sentinel/session/v3/querier.proto +100 -0
- package/proto/sentinel/session/v3/session.proto +50 -0
- package/proto/sentinel/subscription/v2/allocation.proto +21 -0
- package/proto/sentinel/subscription/v2/payout.proto +22 -0
- package/proto/sentinel/subscription/v3/events.proto +65 -0
- package/proto/sentinel/subscription/v3/genesis.proto +17 -0
- package/proto/sentinel/subscription/v3/msg.proto +83 -0
- package/proto/sentinel/subscription/v3/params.proto +21 -0
- package/proto/sentinel/subscription/v3/subscription.proto +33 -0
- package/proto/sentinel/types/v1/bandwidth.proto +19 -0
- package/proto/sentinel/types/v1/price.proto +21 -0
- package/proto/sentinel/types/v1/renewal.proto +21 -0
- package/proto/sentinel/types/v1/status.proto +16 -0
- package/protocol/encoding.js +341 -0
- package/protocol/events.js +361 -0
- package/protocol/handshake.js +297 -0
- package/protocol/index.js +15 -0
- package/protocol/messages.js +346 -0
- package/protocol/plans.js +199 -0
- package/protocol/v2ray.js +268 -0
- package/protocol/v3.js +723 -0
- package/protocol/wireguard.js +125 -0
- package/security/index.js +132 -0
- package/session-manager.js +329 -0
- package/session-tracker.js +80 -0
- package/setup.js +376 -0
- package/speedtest/index.js +528 -0
- package/speedtest.js +567 -0
- package/src/client.ts +502 -0
- package/src/index.ts +20 -0
- package/state/index.js +347 -0
- package/state.js +516 -0
- package/test-all-chain-ops.js +493 -0
- package/test-all-logic.js +199 -0
- package/test-all-msg-types.js +292 -0
- package/test-every-connection.js +208 -0
- package/test-feegrant-connect.js +98 -0
- package/test-logic.js +148 -0
- package/test-mainnet.js +176 -0
- package/test-plan-lifecycle.js +335 -0
- package/tls-trust.js +132 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +34 -0
- package/types/chain.d.ts +746 -0
- package/types/connection.d.ts +425 -0
- package/types/errors.d.ts +174 -0
- package/types/index.d.ts +1380 -0
- package/types/nodes.d.ts +187 -0
- package/types/pricing.d.ts +156 -0
- package/types/protocol.d.ts +332 -0
- package/types/session.d.ts +236 -0
- package/types/settings.d.ts +192 -0
- package/v3protocol.js +1053 -0
- package/wallet/index.js +153 -0
- package/wireguard.js +307 -0
package/chain/lcd.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentinel SDK — Chain / LCD Module
|
|
3
|
+
*
|
|
4
|
+
* LCD (REST) query helpers: single query, paginated query, auto-paginating query,
|
|
5
|
+
* defensive pagination (handles Sentinel's broken pagination), endpoint health checks.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { lcd, lcdQuery, lcdQueryAll, lcdPaginatedSafe } from './chain/lcd.js';
|
|
9
|
+
* const data = await lcd('https://lcd.sentinel.co', '/sentinel/node/v3/nodes?status=1');
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import axios from 'axios';
|
|
13
|
+
import { publicEndpointAgent } from '../tls-trust.js';
|
|
14
|
+
import { LCD_ENDPOINTS, tryWithFallback } from '../defaults.js';
|
|
15
|
+
import { ChainError, ErrorCodes } from '../errors.js';
|
|
16
|
+
|
|
17
|
+
// Re-export for convenience (used by other chain modules)
|
|
18
|
+
export { LCD_ENDPOINTS };
|
|
19
|
+
|
|
20
|
+
// ─── LCD Query Helper ────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Query a Sentinel LCD REST endpoint.
|
|
24
|
+
* Checks both HTTP status AND gRPC error codes in response body.
|
|
25
|
+
* Uses CA-validated HTTPS for LCD public infrastructure (valid CA certs).
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* const data = await lcd('https://lcd.sentinel.co', '/sentinel/node/v3/nodes?status=1');
|
|
29
|
+
*/
|
|
30
|
+
export async function lcd(baseUrl, path) {
|
|
31
|
+
// Accept Endpoint objects ({ url, name }) or bare strings
|
|
32
|
+
const base = typeof baseUrl === 'object' ? baseUrl.url : baseUrl;
|
|
33
|
+
const url = `${base}${path}`;
|
|
34
|
+
const res = await axios.get(url, { httpsAgent: publicEndpointAgent, timeout: 15000 });
|
|
35
|
+
const data = res.data;
|
|
36
|
+
if (data?.code && data.code !== 0) {
|
|
37
|
+
throw new ChainError(ErrorCodes.LCD_ERROR, `LCD ${path}: code=${data.code} ${data.message || ''}`, { path, code: data.code, message: data.message });
|
|
38
|
+
}
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── LCD Query Helpers (v25b) ────────────────────────────────────────────────
|
|
43
|
+
// General-purpose LCD query with timeout, retry, error wrapping, and pagination.
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Single LCD query with timeout, single retry on network error, and ChainError wrapping.
|
|
47
|
+
* Uses the fallback endpoint list if no lcdUrl is provided.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} path - LCD path (e.g. '/sentinel/node/v3/nodes?status=1')
|
|
50
|
+
* @param {object} [opts]
|
|
51
|
+
* @param {string} [opts.lcdUrl] - Specific LCD endpoint (or uses fallback chain)
|
|
52
|
+
* @param {number} [opts.timeout] - Request timeout in ms (default: 15000)
|
|
53
|
+
* @returns {Promise<any>} Parsed JSON response
|
|
54
|
+
*/
|
|
55
|
+
export async function lcdQuery(path, opts = {}) {
|
|
56
|
+
const timeout = opts.timeout || 15000;
|
|
57
|
+
const doQuery = async (baseUrl) => {
|
|
58
|
+
try {
|
|
59
|
+
return await lcd(baseUrl, path);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
// Single retry on network error
|
|
62
|
+
if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.message?.includes('timeout')) {
|
|
63
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
64
|
+
return await lcd(baseUrl, path);
|
|
65
|
+
}
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (opts.lcdUrl) {
|
|
71
|
+
return doQuery(opts.lcdUrl);
|
|
72
|
+
}
|
|
73
|
+
const { result } = await tryWithFallback(LCD_ENDPOINTS, doQuery, `LCD ${path}`);
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Auto-paginating LCD query. Fetches all pages via next_key, returns all results + chain total.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} basePath - LCD path without pagination params (e.g. '/sentinel/node/v3/nodes?status=1')
|
|
81
|
+
* @param {object} [opts]
|
|
82
|
+
* @param {string} [opts.lcdUrl] - Specific LCD endpoint (or uses fallback chain)
|
|
83
|
+
* @param {number} [opts.limit] - Page size (default: 200)
|
|
84
|
+
* @param {number} [opts.timeout] - Per-page timeout (default: 15000)
|
|
85
|
+
* @param {string} [opts.dataKey] - Key for the results array in response (default: auto-detect first array)
|
|
86
|
+
* @returns {Promise<{ items: any[], total: number|null }>}
|
|
87
|
+
*/
|
|
88
|
+
export async function lcdQueryAll(basePath, opts = {}) {
|
|
89
|
+
const limit = opts.limit || 200;
|
|
90
|
+
const dataKey = opts.dataKey || null;
|
|
91
|
+
|
|
92
|
+
const fetchAll = async (baseUrl) => {
|
|
93
|
+
let allItems = [];
|
|
94
|
+
let nextKey = null;
|
|
95
|
+
let chainTotal = null;
|
|
96
|
+
let isFirst = true;
|
|
97
|
+
do {
|
|
98
|
+
const sep = basePath.includes('?') ? '&' : '?';
|
|
99
|
+
let url = `${basePath}${sep}pagination.limit=${limit}`;
|
|
100
|
+
if (isFirst) url += '&pagination.count_total=true';
|
|
101
|
+
if (nextKey) url += `&pagination.key=${encodeURIComponent(nextKey)}`;
|
|
102
|
+
const data = await lcd(baseUrl, url);
|
|
103
|
+
if (isFirst && data.pagination?.total) {
|
|
104
|
+
chainTotal = parseInt(data.pagination.total, 10);
|
|
105
|
+
}
|
|
106
|
+
// Auto-detect data key: first array property that isn't 'pagination'
|
|
107
|
+
const key = dataKey || Object.keys(data).find(k => k !== 'pagination' && Array.isArray(data[k]));
|
|
108
|
+
const pageItems = key ? (data[key] || []) : [];
|
|
109
|
+
allItems = allItems.concat(pageItems);
|
|
110
|
+
nextKey = data.pagination?.next_key || null;
|
|
111
|
+
isFirst = false;
|
|
112
|
+
} while (nextKey);
|
|
113
|
+
|
|
114
|
+
if (chainTotal && allItems.length !== chainTotal) {
|
|
115
|
+
console.warn(`[lcdQueryAll] Pagination mismatch: got ${allItems.length}, chain reports ${chainTotal}`);
|
|
116
|
+
}
|
|
117
|
+
return { items: allItems, total: chainTotal };
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
if (opts.lcdUrl) {
|
|
121
|
+
return fetchAll(opts.lcdUrl);
|
|
122
|
+
}
|
|
123
|
+
const { result } = await tryWithFallback(LCD_ENDPOINTS, fetchAll, `LCD paginated ${basePath}`);
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── v26c: Defensive Pagination ──────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Paginated LCD query that handles Sentinel's broken pagination.
|
|
131
|
+
* Tries next_key first. If next_key is null but we got exactly `limit` results
|
|
132
|
+
* (suggesting truncation), falls back to a single large request.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} lcdUrl - LCD base URL
|
|
135
|
+
* @param {string} path - Endpoint path (e.g. '/sentinel/node/v3/plans/36/nodes')
|
|
136
|
+
* @param {string} itemsKey - Response array key ('nodes', 'subscriptions', 'sessions')
|
|
137
|
+
* @param {object} [opts]
|
|
138
|
+
* @param {number} [opts.limit=500] - Page size for paginated requests
|
|
139
|
+
* @param {number} [opts.fallbackLimit=5000] - Single-request limit if pagination broken
|
|
140
|
+
* @returns {Promise<{ items: any[], total: number }>}
|
|
141
|
+
*/
|
|
142
|
+
export async function lcdPaginatedSafe(lcdUrl, path, itemsKey, opts = {}) {
|
|
143
|
+
const limit = opts.limit || 500;
|
|
144
|
+
const fallbackLimit = opts.fallbackLimit || 5000;
|
|
145
|
+
const baseLcd = lcdUrl || LCD_ENDPOINTS[0].url;
|
|
146
|
+
const sep = path.includes('?') ? '&' : '?';
|
|
147
|
+
|
|
148
|
+
const firstPage = await lcd(baseLcd, `${path}${sep}pagination.limit=${limit}`);
|
|
149
|
+
const firstItems = firstPage[itemsKey] || [];
|
|
150
|
+
const nextKey = firstPage.pagination?.next_key;
|
|
151
|
+
|
|
152
|
+
// Fewer than limit = that's everything
|
|
153
|
+
if (firstItems.length < limit) {
|
|
154
|
+
return { items: firstItems, total: firstItems.length };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// next_key exists = pagination works, follow it
|
|
158
|
+
if (nextKey) {
|
|
159
|
+
let allItems = [...firstItems];
|
|
160
|
+
let key = nextKey;
|
|
161
|
+
while (key) {
|
|
162
|
+
const page = await lcd(baseLcd, `${path}${sep}pagination.limit=${limit}&pagination.key=${encodeURIComponent(key)}`);
|
|
163
|
+
allItems.push(...(page[itemsKey] || []));
|
|
164
|
+
key = page.pagination?.next_key || null;
|
|
165
|
+
}
|
|
166
|
+
return { items: allItems, total: allItems.length };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// next_key null but hit limit = broken pagination. Single large request.
|
|
170
|
+
const fullData = await lcd(baseLcd, `${path}${sep}pagination.limit=${fallbackLimit}`);
|
|
171
|
+
const allItems = fullData[itemsKey] || [];
|
|
172
|
+
return { items: allItems, total: allItems.length };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Display & Formatting Helpers ───────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Format a micro-denom (udvpn) amount as a human-readable P2P string.
|
|
179
|
+
*
|
|
180
|
+
* @param {number|string} udvpn - Amount in micro-denom (1 P2P = 1,000,000 udvpn)
|
|
181
|
+
* @param {number} [decimals=2] - Decimal places to show
|
|
182
|
+
* @returns {string} e.g., "0.04 P2P", "47.69 P2P"
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* formatDvpn(40152030); // "40.15 P2P"
|
|
186
|
+
* formatDvpn('1000000', 0); // "1 P2P"
|
|
187
|
+
* formatDvpn(500000, 4); // "0.5000 P2P"
|
|
188
|
+
*/
|
|
189
|
+
export function formatDvpn(udvpn, decimals = 2) {
|
|
190
|
+
const val = Number(udvpn) / 1_000_000;
|
|
191
|
+
if (isNaN(val)) return '? P2P';
|
|
192
|
+
return `${val.toFixed(decimals)} P2P`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Alias for formatDvpn — uses the current P2P token name. */
|
|
196
|
+
export const formatP2P = formatDvpn;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Filter a node list by country, service type, and/or max price.
|
|
200
|
+
* Works with results from listNodes(), enrichNodes(), or fetchAllNodes().
|
|
201
|
+
*
|
|
202
|
+
* @param {Array} nodes - Array of node objects
|
|
203
|
+
* @param {object} criteria
|
|
204
|
+
* @param {string} [criteria.country] - Country name (case-insensitive partial match)
|
|
205
|
+
* @param {string} [criteria.serviceType] - 'wireguard' or 'v2ray'
|
|
206
|
+
* @param {number} [criteria.maxPriceDvpn] - Maximum GB price in P2P (e.g., 0.1)
|
|
207
|
+
* @param {number} [criteria.minScore] - Minimum quality score (0-100)
|
|
208
|
+
* @returns {Array} Filtered nodes
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* const cheap = filterNodes(nodes, { maxPriceDvpn: 0.05, serviceType: 'v2ray' });
|
|
212
|
+
* const german = filterNodes(nodes, { country: 'Germany' });
|
|
213
|
+
*/
|
|
214
|
+
export function filterNodes(nodes, criteria = {}) {
|
|
215
|
+
if (!Array.isArray(nodes)) return [];
|
|
216
|
+
return nodes.filter(node => {
|
|
217
|
+
if (criteria.country) {
|
|
218
|
+
const c = (node.country || node.location?.country || '').toLowerCase();
|
|
219
|
+
if (!c.includes(criteria.country.toLowerCase())) return false;
|
|
220
|
+
}
|
|
221
|
+
if (criteria.serviceType) {
|
|
222
|
+
const t = node.serviceType || node.type || '';
|
|
223
|
+
if (t !== criteria.serviceType) return false;
|
|
224
|
+
}
|
|
225
|
+
if (criteria.maxPriceDvpn != null) {
|
|
226
|
+
const prices = node.gigabytePrices || node.gigabyte_prices || [];
|
|
227
|
+
const entry = prices.find(p => p.denom === 'udvpn');
|
|
228
|
+
if (entry) {
|
|
229
|
+
const dvpn = parseInt(entry.quote_value || entry.base_value || entry.amount || '0', 10) / 1_000_000;
|
|
230
|
+
if (dvpn > criteria.maxPriceDvpn) return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (criteria.minScore != null && node.qualityScore != null) {
|
|
234
|
+
if (node.qualityScore < criteria.minScore) return false;
|
|
235
|
+
}
|
|
236
|
+
return true;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get P2P price in USD from CoinGecko (cached for 5 minutes).
|
|
242
|
+
*/
|
|
243
|
+
let _dvpnPrice = null;
|
|
244
|
+
let _dvpnPriceAt = 0;
|
|
245
|
+
export async function getDvpnPrice() {
|
|
246
|
+
if (_dvpnPrice && Date.now() - _dvpnPriceAt < 300_000) return _dvpnPrice;
|
|
247
|
+
try {
|
|
248
|
+
const res = await axios.get('https://api.coingecko.com/api/v3/simple/price?ids=sentinel&vs_currencies=usd', { timeout: 10000 });
|
|
249
|
+
_dvpnPrice = res.data?.sentinel?.usd || null;
|
|
250
|
+
_dvpnPriceAt = Date.now();
|
|
251
|
+
} catch { /* keep old value */ }
|
|
252
|
+
return _dvpnPrice;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── v26c: Display Helpers ───────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Truncate an address for display. Works with sent1, sentprov1, sentnode1.
|
|
259
|
+
* @param {string} addr
|
|
260
|
+
* @param {number} [prefixLen=12]
|
|
261
|
+
* @param {number} [suffixLen=6]
|
|
262
|
+
* @returns {string}
|
|
263
|
+
*/
|
|
264
|
+
export function shortAddress(addr, prefixLen = 12, suffixLen = 6) {
|
|
265
|
+
if (!addr || addr.length <= prefixLen + suffixLen + 3) return addr || '';
|
|
266
|
+
return `${addr.slice(0, prefixLen)}...${addr.slice(-suffixLen)}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Format subscription expiry as relative time.
|
|
271
|
+
* @param {object} subscription - LCD subscription object (or any object with inactive_at)
|
|
272
|
+
* @returns {string} e.g. "23d left", "4h left", "expired"
|
|
273
|
+
*/
|
|
274
|
+
export function formatSubscriptionExpiry(subscription) {
|
|
275
|
+
const iso = subscription?.inactive_at || subscription?.status_at;
|
|
276
|
+
if (!iso) return 'unknown';
|
|
277
|
+
const diff = new Date(iso).getTime() - Date.now();
|
|
278
|
+
if (diff < 0) return 'expired';
|
|
279
|
+
const days = Math.floor(diff / 86400000);
|
|
280
|
+
if (days > 1) return `${days}d left`;
|
|
281
|
+
const hrs = Math.floor(diff / 3600000);
|
|
282
|
+
if (hrs > 0) return `${hrs}h left`;
|
|
283
|
+
const mins = Math.floor(diff / 60000);
|
|
284
|
+
if (mins > 0) return `${mins}m left`;
|
|
285
|
+
return '<1m left';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Format byte count for display.
|
|
290
|
+
* @param {number} bytes
|
|
291
|
+
* @returns {string} e.g. '1.5 GB', '340 MB', '1.2 KB'
|
|
292
|
+
*/
|
|
293
|
+
export function formatBytes(bytes) {
|
|
294
|
+
if (bytes == null || isNaN(bytes)) return '0 B';
|
|
295
|
+
if (bytes < 1024) return bytes + ' B';
|
|
296
|
+
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
297
|
+
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
|
|
298
|
+
return (bytes / 1073741824).toFixed(2) + ' GB';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Parse chain duration string (has "s" suffix).
|
|
303
|
+
* @param {string} durationStr - e.g. '557817.727815887s'
|
|
304
|
+
* @returns {{ seconds: number, hours: number, minutes: number, formatted: string }}
|
|
305
|
+
*/
|
|
306
|
+
export function parseChainDuration(durationStr) {
|
|
307
|
+
const seconds = parseFloat(String(durationStr).replace(/s$/i, '')) || 0;
|
|
308
|
+
const hours = Math.floor(seconds / 3600);
|
|
309
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
310
|
+
const secs = Math.floor(seconds % 60);
|
|
311
|
+
const formatted = hours > 0 ? `${hours}h ${minutes}m` : minutes > 0 ? `${minutes}m ${secs}s` : `${secs}s`;
|
|
312
|
+
return { seconds, hours, minutes, formatted };
|
|
313
|
+
}
|