@le-space/ui 0.8.0 → 0.9.1
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/package.json +3 -3
- package/react/index.d.ts +11 -0
- package/react/index.js +333 -46
- package/shared/index.d.ts +12 -1
- package/shared/index.js +332 -46
- package/svelte/SponsorRelayFab.svelte +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@le-space/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Floating Aleph Relay Button UI components for Svelte and React.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"url": "https://github.com/NiKrause/relay-button/issues"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@le-space/browser": "0.
|
|
48
|
-
"@le-space/core": "0.
|
|
47
|
+
"@le-space/browser": "0.9.1",
|
|
48
|
+
"@le-space/core": "0.9.1",
|
|
49
49
|
"viem": "^2.53.1"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
package/react/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
|
|
3
|
+
type CrnSource = 'list' | 'aggregate';
|
|
4
|
+
type CrnSourcePreference = 'auto' | 'aggregate' | 'list';
|
|
5
|
+
|
|
3
6
|
type MessageStatus = 'processed' | 'pending' | 'rejected' | 'removing' | 'removed' | 'unknown';
|
|
4
7
|
interface BalanceResponse {
|
|
5
8
|
address: string;
|
|
@@ -222,6 +225,12 @@ interface SponsorRelayProps {
|
|
|
222
225
|
apiHost?: string;
|
|
223
226
|
apiHosts?: string | readonly string[];
|
|
224
227
|
crnListUrl?: string;
|
|
228
|
+
/**
|
|
229
|
+
* Which CRN source to read. `auto` falls back to the corechannel aggregate
|
|
230
|
+
* when crns.json is down, `aggregate` skips crns.json, `list` disables the
|
|
231
|
+
* fallback. `localStorage.LE_SPACE_CRN_SOURCE` overrides this at runtime.
|
|
232
|
+
*/
|
|
233
|
+
crnSource?: CrnSourcePreference;
|
|
225
234
|
schedulerApiHost?: string;
|
|
226
235
|
twoN6ApiHost?: string;
|
|
227
236
|
}
|
|
@@ -346,6 +355,8 @@ interface SponsorRelayState {
|
|
|
346
355
|
pricingSummary: SponsorRelayPricingSummary;
|
|
347
356
|
balance: BalanceResponse | null;
|
|
348
357
|
crns: Crn[];
|
|
358
|
+
/** Which source the current `crns` came from; `null` until the first load. */
|
|
359
|
+
crnSource: CrnSource | null;
|
|
349
360
|
selectedCrn: Crn | null;
|
|
350
361
|
crnOptions: Array<{
|
|
351
362
|
hash: string;
|
package/react/index.js
CHANGED
|
@@ -43,7 +43,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
|
|
|
43
43
|
var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
|
|
44
44
|
|
|
45
45
|
// src/shared/package-version.ts
|
|
46
|
-
var UI_PACKAGE_VERSION = "0.
|
|
46
|
+
var UI_PACKAGE_VERSION = "0.9.1";
|
|
47
47
|
|
|
48
48
|
// src/shared/format.ts
|
|
49
49
|
function shortHash(value, head = 8, tail = 6) {
|
|
@@ -141,6 +141,8 @@ var DEFAULT_ALEPH_API_HOSTS = [
|
|
|
141
141
|
];
|
|
142
142
|
var DEFAULT_ALEPH_API_HOST = DEFAULT_ALEPH_API_HOSTS[0] ?? "https://api2.aleph.im";
|
|
143
143
|
var DEFAULT_CRN_LIST_URL = "https://crns-list.aleph.sh/crns.json";
|
|
144
|
+
var DEFAULT_CRN_AGGREGATE_ADDRESS = "0xa1B3bb7d2332383D96b7796B908fB7f7F3c2Be10";
|
|
145
|
+
var DEFAULT_CRN_AGGREGATE_KEY = "corechannel";
|
|
144
146
|
var DEFAULT_ALEPH_SCHEDULER_API_HOST = "https://scheduler.api.aleph.cloud";
|
|
145
147
|
var DEFAULT_2N6_API_HOST = "https://api.2n6.me";
|
|
146
148
|
function normalizeMessageStatus(status) {
|
|
@@ -195,7 +197,7 @@ async function fetchBalance(address, apiHost = DEFAULT_ALEPH_API_HOST) {
|
|
|
195
197
|
if (!response.ok) throw new Error(`Balance request failed: ${response.status}`);
|
|
196
198
|
return await response.json();
|
|
197
199
|
}
|
|
198
|
-
async function
|
|
200
|
+
async function fetchCrnsFromList(url = DEFAULT_CRN_LIST_URL) {
|
|
199
201
|
const requestUrl = new URL(url);
|
|
200
202
|
requestUrl.searchParams.set("filter_inactive", "true");
|
|
201
203
|
const response = await fetchWithTimeout(requestUrl, { cache: "no-cache" });
|
|
@@ -203,6 +205,130 @@ async function fetchCrns(url = DEFAULT_CRN_LIST_URL) {
|
|
|
203
205
|
const payload = await response.json();
|
|
204
206
|
return payload.crns ?? [];
|
|
205
207
|
}
|
|
208
|
+
function asFiniteNumber(value) {
|
|
209
|
+
if (value == null || value === "") return null;
|
|
210
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
211
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
212
|
+
}
|
|
213
|
+
function normalizeCrnAddress(value) {
|
|
214
|
+
const address = asString(value);
|
|
215
|
+
if (!address) return null;
|
|
216
|
+
const withScheme = /^https?:\/\//i.test(address) ? address : `https://${address}`;
|
|
217
|
+
try {
|
|
218
|
+
return new URL(withScheme).toString().replace(/\/+$/, "");
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function mapResourceNodesToCrns(resourceNodes) {
|
|
224
|
+
if (!Array.isArray(resourceNodes)) return [];
|
|
225
|
+
const crns = [];
|
|
226
|
+
for (const node of resourceNodes) {
|
|
227
|
+
if (!node || typeof node !== "object") continue;
|
|
228
|
+
const entry = node;
|
|
229
|
+
const hash = asString(entry.hash);
|
|
230
|
+
const address = normalizeCrnAddress(entry.address);
|
|
231
|
+
const type = asString(entry.type)?.toLowerCase();
|
|
232
|
+
const status = asString(entry.status)?.toLowerCase();
|
|
233
|
+
if (!hash || !address) continue;
|
|
234
|
+
if (entry.inactive_since != null || entry.locked === true) continue;
|
|
235
|
+
if (type && type !== "compute") continue;
|
|
236
|
+
if (status && status !== "linked" && status !== "active") continue;
|
|
237
|
+
if (!asString(entry.parent)) continue;
|
|
238
|
+
const score = asFiniteNumber(entry.score) ?? asFiniteNumber(entry.scoreV1);
|
|
239
|
+
const performance = asFiniteNumber(entry.performance);
|
|
240
|
+
const decentralization = asFiniteNumber(entry.decentralization);
|
|
241
|
+
crns.push({
|
|
242
|
+
hash,
|
|
243
|
+
address,
|
|
244
|
+
name: asString(entry.name) ?? hash,
|
|
245
|
+
...score == null ? {} : { score },
|
|
246
|
+
...performance == null ? {} : { performance },
|
|
247
|
+
...decentralization == null ? {} : { decentralization }
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return crns;
|
|
251
|
+
}
|
|
252
|
+
async function fetchCrnsFromAggregate(apiHosts = DEFAULT_ALEPH_API_HOSTS, address = DEFAULT_CRN_AGGREGATE_ADDRESS) {
|
|
253
|
+
const hosts = apiHosts.length > 0 ? apiHosts : DEFAULT_ALEPH_API_HOSTS;
|
|
254
|
+
let lastError = null;
|
|
255
|
+
for (const apiHost of hosts) {
|
|
256
|
+
try {
|
|
257
|
+
const url = new URL(`/api/v0/aggregates/${address}.json`, apiHost);
|
|
258
|
+
url.searchParams.set("keys", DEFAULT_CRN_AGGREGATE_KEY);
|
|
259
|
+
const response = await fetchWithTimeout(url, { cache: "no-cache" });
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
lastError = new Error(`CRN aggregate request failed: ${response.status}`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const payload = await response.json();
|
|
265
|
+
const crns = mapResourceNodesToCrns(payload?.data?.corechannel?.resource_nodes);
|
|
266
|
+
if (crns.length > 0) return crns;
|
|
267
|
+
lastError = new Error("CRN aggregate contained no deployable resource nodes");
|
|
268
|
+
} catch (error) {
|
|
269
|
+
lastError = error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "CRN aggregate request failed"));
|
|
273
|
+
}
|
|
274
|
+
async function fetchCrnsWithSource(url = DEFAULT_CRN_LIST_URL, options = {}) {
|
|
275
|
+
const apiHosts = options.apiHosts ?? DEFAULT_ALEPH_API_HOSTS;
|
|
276
|
+
const source = options.source ?? "auto";
|
|
277
|
+
if (source === "aggregate") {
|
|
278
|
+
return {
|
|
279
|
+
crns: await fetchCrnsFromAggregate(apiHosts, options.aggregateAddress),
|
|
280
|
+
source: "aggregate"
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
let listError;
|
|
284
|
+
try {
|
|
285
|
+
const crns = await fetchCrnsFromList(url);
|
|
286
|
+
if (crns.length > 0) return { crns, source: "list" };
|
|
287
|
+
listError = new Error("CRN list returned no entries");
|
|
288
|
+
} catch (error) {
|
|
289
|
+
listError = error instanceof Error ? error : new Error(String(error));
|
|
290
|
+
}
|
|
291
|
+
if (source === "list") throw listError;
|
|
292
|
+
return {
|
|
293
|
+
crns: await fetchCrnsFromAggregate(apiHosts, options.aggregateAddress),
|
|
294
|
+
source: "aggregate",
|
|
295
|
+
listError
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
async function fetchCrns(url = DEFAULT_CRN_LIST_URL, options = {}) {
|
|
299
|
+
return (await fetchCrnsWithSource(url, options)).crns;
|
|
300
|
+
}
|
|
301
|
+
async function probeCrnAvailability(crn) {
|
|
302
|
+
const address = String(crn.address ?? "").trim();
|
|
303
|
+
if (!address) return "unreachable";
|
|
304
|
+
const lookup = await fetchCrnExecutionMap(address);
|
|
305
|
+
if (lookup.payload !== null) return "reachable";
|
|
306
|
+
return lookup.blocked ? "unknown" : "unreachable";
|
|
307
|
+
}
|
|
308
|
+
async function filterReachableCrns(crns, options = {}) {
|
|
309
|
+
const limit = Math.max(1, Number(options.limit) || 5);
|
|
310
|
+
const probed = crns.slice(0, limit);
|
|
311
|
+
if (probed.length === 0) return [...crns];
|
|
312
|
+
const verdicts = await Promise.all(
|
|
313
|
+
probed.map(async (crn) => {
|
|
314
|
+
try {
|
|
315
|
+
return { crn, verdict: await probeCrnAvailability(crn) };
|
|
316
|
+
} catch {
|
|
317
|
+
return { crn, verdict: "unknown" };
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
const kept = verdicts.filter((entry) => entry.verdict !== "unreachable").map((entry) => entry.crn);
|
|
322
|
+
if (kept.length === 0) {
|
|
323
|
+
options.onDiagnostic?.(`No CRN answered its executions endpoint (${probed.length} probed); keeping the unverified order`);
|
|
324
|
+
return [...crns];
|
|
325
|
+
}
|
|
326
|
+
const dropped = probed.length - kept.length;
|
|
327
|
+
if (dropped > 0) {
|
|
328
|
+
options.onDiagnostic?.(`Skipping ${dropped} unreachable CRN${dropped === 1 ? "" : "s"} of ${probed.length} probed`);
|
|
329
|
+
}
|
|
330
|
+
return [...kept, ...crns.slice(limit)];
|
|
331
|
+
}
|
|
206
332
|
async function fetchInstances(address, apiHost = DEFAULT_ALEPH_API_HOST) {
|
|
207
333
|
const url = new URL("/api/v0/messages.json", apiHost);
|
|
208
334
|
url.searchParams.set("msgTypes", "INSTANCE");
|
|
@@ -857,18 +983,23 @@ function createAlephBrowserClient(options = {}) {
|
|
|
857
983
|
const apiHosts = options.apiHosts === void 0 && options.apiHost === void 0 ? [...DEFAULT_ALEPH_API_HOSTS] : normalizeApiHosts(options.apiHosts, options.apiHost ?? DEFAULT_ALEPH_API_HOST);
|
|
858
984
|
const apiHost = apiHosts[0] ?? DEFAULT_ALEPH_API_HOST;
|
|
859
985
|
const crnListUrl = options.crnListUrl ?? DEFAULT_CRN_LIST_URL;
|
|
986
|
+
const crnSource = options.crnSource ?? "auto";
|
|
860
987
|
const schedulerApiHost = options.schedulerApiHost ?? DEFAULT_ALEPH_SCHEDULER_API_HOST;
|
|
861
988
|
const twoN6ApiHost = options.twoN6ApiHost ?? DEFAULT_2N6_API_HOST;
|
|
862
989
|
return {
|
|
863
990
|
apiHost,
|
|
864
991
|
apiHosts,
|
|
865
992
|
crnListUrl,
|
|
993
|
+
crnSource,
|
|
866
994
|
schedulerApiHost,
|
|
867
995
|
fetchBalance(address) {
|
|
868
996
|
return withApiHostFallback(apiHosts, (host) => fetchBalance(address, host));
|
|
869
997
|
},
|
|
870
998
|
fetchCrns() {
|
|
871
|
-
return fetchCrns(crnListUrl);
|
|
999
|
+
return fetchCrns(crnListUrl, { apiHosts, source: crnSource });
|
|
1000
|
+
},
|
|
1001
|
+
fetchCrnsWithSource(source) {
|
|
1002
|
+
return fetchCrnsWithSource(crnListUrl, { apiHosts, source: source ?? crnSource });
|
|
872
1003
|
},
|
|
873
1004
|
fetchInstances(address) {
|
|
874
1005
|
return withApiHostFallback(apiHosts, (host) => fetchInstances(address, host));
|
|
@@ -943,6 +1074,15 @@ var DEFAULT_ALEPH_API_HOST2 = "https://api.aleph.im";
|
|
|
943
1074
|
|
|
944
1075
|
// ../core/src/crns.ts
|
|
945
1076
|
var DEFAULT_CRN_LIST_URL2 = "https://crns-list.aleph.sh/crns.json";
|
|
1077
|
+
var DEFAULT_CRN_AGGREGATE_ADDRESS2 = "0xa1B3bb7d2332383D96b7796B908fB7f7F3c2Be10";
|
|
1078
|
+
var DEFAULT_CRN_AGGREGATE_KEY2 = "corechannel";
|
|
1079
|
+
var DEFAULT_CRN_AGGREGATE_API_HOSTS = [
|
|
1080
|
+
"https://api2.aleph.im",
|
|
1081
|
+
"https://api.aleph.im"
|
|
1082
|
+
];
|
|
1083
|
+
function asString2(value) {
|
|
1084
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1085
|
+
}
|
|
946
1086
|
function compatibleCrns(crns, excludedHashes = []) {
|
|
947
1087
|
const excluded = new Set(excludedHashes.filter(Boolean));
|
|
948
1088
|
return [...crns].filter((crn) => {
|
|
@@ -976,7 +1116,7 @@ function filterDeployableCrns(crns, options = {}) {
|
|
|
976
1116
|
compatibleCrns(crns, options.excludedHashes).filter((crn) => hasCrnCapacity(crn, options.spec))
|
|
977
1117
|
);
|
|
978
1118
|
}
|
|
979
|
-
async function
|
|
1119
|
+
async function fetchCrnsFromList2(options) {
|
|
980
1120
|
const requestUrl = new URL(options.url ?? DEFAULT_CRN_LIST_URL2);
|
|
981
1121
|
requestUrl.searchParams.set("filter_inactive", "true");
|
|
982
1122
|
const response = await options.fetch(requestUrl.toString(), {
|
|
@@ -988,6 +1128,101 @@ async function fetchCrns2(options) {
|
|
|
988
1128
|
const payload = await response.json();
|
|
989
1129
|
return Array.isArray(payload?.crns) ? payload.crns : [];
|
|
990
1130
|
}
|
|
1131
|
+
function normalizeCrnAddress2(value) {
|
|
1132
|
+
const address = asString2(value);
|
|
1133
|
+
if (!address) return null;
|
|
1134
|
+
const withScheme = /^https?:\/\//i.test(address) ? address : `https://${address}`;
|
|
1135
|
+
try {
|
|
1136
|
+
return new URL(withScheme).toString().replace(/\/+$/, "");
|
|
1137
|
+
} catch {
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
function aggregateScore(node) {
|
|
1142
|
+
for (const candidate of [node.score, node.scoreV1]) {
|
|
1143
|
+
if (candidate == null || candidate === "") continue;
|
|
1144
|
+
const numeric = typeof candidate === "number" ? candidate : Number(candidate);
|
|
1145
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
1146
|
+
}
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
function isDeployableResourceNode(node) {
|
|
1150
|
+
if (!asString2(node.hash)) return false;
|
|
1151
|
+
if (!normalizeCrnAddress2(node.address)) return false;
|
|
1152
|
+
if (node.inactive_since != null) return false;
|
|
1153
|
+
if (node.locked === true) return false;
|
|
1154
|
+
const type = asString2(node.type)?.toLowerCase();
|
|
1155
|
+
if (type && type !== "compute") return false;
|
|
1156
|
+
const status = asString2(node.status)?.toLowerCase();
|
|
1157
|
+
if (status && status !== "linked" && status !== "active") return false;
|
|
1158
|
+
return Boolean(asString2(node.parent));
|
|
1159
|
+
}
|
|
1160
|
+
function mapResourceNodesToCrns2(resourceNodes) {
|
|
1161
|
+
if (!Array.isArray(resourceNodes)) return [];
|
|
1162
|
+
return resourceNodes.filter((node) => Boolean(node) && typeof node === "object").filter(isDeployableResourceNode).map((node) => {
|
|
1163
|
+
const record = {
|
|
1164
|
+
hash: asString2(node.hash),
|
|
1165
|
+
address: normalizeCrnAddress2(node.address)
|
|
1166
|
+
};
|
|
1167
|
+
const name = asString2(node.name);
|
|
1168
|
+
if (name) record.name = name;
|
|
1169
|
+
const score = aggregateScore(node);
|
|
1170
|
+
if (score != null) record.score = score;
|
|
1171
|
+
return record;
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
async function fetchCrnsFromAggregate2(options) {
|
|
1175
|
+
const apiHosts = options.apiHosts?.length ? options.apiHosts : DEFAULT_CRN_AGGREGATE_API_HOSTS;
|
|
1176
|
+
const address = options.address ?? DEFAULT_CRN_AGGREGATE_ADDRESS2;
|
|
1177
|
+
const aggregateKey = options.aggregateKey ?? DEFAULT_CRN_AGGREGATE_KEY2;
|
|
1178
|
+
let lastError = null;
|
|
1179
|
+
for (const apiHost of apiHosts) {
|
|
1180
|
+
try {
|
|
1181
|
+
const url = new URL(`/api/v0/aggregates/${address}.json`, apiHost);
|
|
1182
|
+
url.searchParams.set("keys", aggregateKey);
|
|
1183
|
+
const response = await options.fetch(url.toString(), { cache: "no-cache" });
|
|
1184
|
+
if (!response.ok) {
|
|
1185
|
+
lastError = new Error(`CRN aggregate request failed: ${response.status}`);
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
const payload = await response.json();
|
|
1189
|
+
const crns = mapResourceNodesToCrns2(payload?.data?.corechannel?.resource_nodes);
|
|
1190
|
+
if (crns.length > 0) return crns;
|
|
1191
|
+
lastError = new Error("CRN aggregate contained no deployable resource nodes");
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
lastError = error;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "CRN aggregate request failed"));
|
|
1197
|
+
}
|
|
1198
|
+
async function fetchCrnsWithSource2(options) {
|
|
1199
|
+
const aggregateOptions = {
|
|
1200
|
+
fetch: options.fetch,
|
|
1201
|
+
apiHosts: options.aggregateApiHosts,
|
|
1202
|
+
address: options.aggregateAddress
|
|
1203
|
+
};
|
|
1204
|
+
if (options.preferAggregate) {
|
|
1205
|
+
return { crns: await fetchCrnsFromAggregate2(aggregateOptions), source: "aggregate" };
|
|
1206
|
+
}
|
|
1207
|
+
let listError;
|
|
1208
|
+
try {
|
|
1209
|
+
const crns = await fetchCrnsFromList2({ url: options.url, fetch: options.fetch });
|
|
1210
|
+
if (crns.length > 0) return { crns, source: "list" };
|
|
1211
|
+
listError = new Error("CRN list returned no entries");
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
listError = error instanceof Error ? error : new Error(String(error));
|
|
1214
|
+
}
|
|
1215
|
+
options.onDiagnostic?.(`CRN list unavailable (${listError.message}); falling back to the ${DEFAULT_CRN_AGGREGATE_KEY2} aggregate`);
|
|
1216
|
+
try {
|
|
1217
|
+
return { crns: await fetchCrnsFromAggregate2(aggregateOptions), source: "aggregate", listError };
|
|
1218
|
+
} catch (aggregateError) {
|
|
1219
|
+
const message = aggregateError instanceof Error ? aggregateError.message : String(aggregateError);
|
|
1220
|
+
throw new Error(`${listError.message}; CRN aggregate fallback failed: ${message}`);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
async function fetchCrns2(options) {
|
|
1224
|
+
return (await fetchCrnsWithSource2(options)).crns;
|
|
1225
|
+
}
|
|
991
1226
|
|
|
992
1227
|
// ../core/src/port-forwarding.ts
|
|
993
1228
|
var DEFAULT_INSTANCE_PORT_FORWARDS = [
|
|
@@ -1077,7 +1312,7 @@ async function fetchPortForwardAggregate(address, options) {
|
|
|
1077
1312
|
}
|
|
1078
1313
|
|
|
1079
1314
|
// ../core/src/aleph-normalizers.ts
|
|
1080
|
-
function
|
|
1315
|
+
function asString3(value) {
|
|
1081
1316
|
return typeof value === "string" && value.trim() ? value : null;
|
|
1082
1317
|
}
|
|
1083
1318
|
function asNumber2(value) {
|
|
@@ -1092,7 +1327,7 @@ function normalizeMessageStatus2(status) {
|
|
|
1092
1327
|
return "unknown";
|
|
1093
1328
|
}
|
|
1094
1329
|
function normalizeProxyUrl2(value) {
|
|
1095
|
-
const stringValue =
|
|
1330
|
+
const stringValue = asString3(value);
|
|
1096
1331
|
if (!stringValue) return null;
|
|
1097
1332
|
if (/^https?:\/\//i.test(stringValue)) return stringValue;
|
|
1098
1333
|
return `https://${stringValue}`;
|
|
@@ -1187,22 +1422,22 @@ function normalizeExecution2(item, crnUrl) {
|
|
|
1187
1422
|
version: "v2",
|
|
1188
1423
|
running: typeof v2Item.running === "boolean" ? v2Item.running : void 0,
|
|
1189
1424
|
networking: {
|
|
1190
|
-
ipv4_network:
|
|
1191
|
-
host_ipv4:
|
|
1192
|
-
ipv6_network:
|
|
1193
|
-
ipv6_ip:
|
|
1194
|
-
ipv4_ip:
|
|
1425
|
+
ipv4_network: asString3(networking.ipv4_network),
|
|
1426
|
+
host_ipv4: asString3(networking.host_ipv4),
|
|
1427
|
+
ipv6_network: asString3(networking.ipv6_network),
|
|
1428
|
+
ipv6_ip: asString3(networking.ipv6_ip),
|
|
1429
|
+
ipv4_ip: asString3(networking.ipv4_ip),
|
|
1195
1430
|
proxy_url: extractProxyUrl2(v2Item, networking),
|
|
1196
1431
|
mapped_ports: mappedPorts2
|
|
1197
1432
|
},
|
|
1198
1433
|
status: v2Item.status ? {
|
|
1199
|
-
defined_at:
|
|
1200
|
-
preparing_at:
|
|
1201
|
-
prepared_at:
|
|
1202
|
-
starting_at:
|
|
1203
|
-
started_at:
|
|
1204
|
-
stopping_at:
|
|
1205
|
-
stopped_at:
|
|
1434
|
+
defined_at: asString3(v2Item.status.defined_at),
|
|
1435
|
+
preparing_at: asString3(v2Item.status.preparing_at),
|
|
1436
|
+
prepared_at: asString3(v2Item.status.prepared_at),
|
|
1437
|
+
starting_at: asString3(v2Item.status.starting_at),
|
|
1438
|
+
started_at: asString3(v2Item.status.started_at),
|
|
1439
|
+
stopping_at: asString3(v2Item.status.stopping_at),
|
|
1440
|
+
stopped_at: asString3(v2Item.status.stopped_at)
|
|
1206
1441
|
} : null
|
|
1207
1442
|
};
|
|
1208
1443
|
}
|
|
@@ -1210,8 +1445,8 @@ function normalizeExecution2(item, crnUrl) {
|
|
|
1210
1445
|
crnUrl,
|
|
1211
1446
|
version: "v1",
|
|
1212
1447
|
networking: {
|
|
1213
|
-
ipv4: networking && "ipv4" in networking ?
|
|
1214
|
-
ipv6: networking && "ipv6" in networking ?
|
|
1448
|
+
ipv4: networking && "ipv4" in networking ? asString3(networking.ipv4) : null,
|
|
1449
|
+
ipv6: networking && "ipv6" in networking ? asString3(networking.ipv6) : null
|
|
1215
1450
|
},
|
|
1216
1451
|
status: null
|
|
1217
1452
|
};
|
|
@@ -1373,7 +1608,7 @@ async function broadcastAlephMessage2(message, options) {
|
|
|
1373
1608
|
// ../core/src/runtime.ts
|
|
1374
1609
|
var DEFAULT_SCHEDULER_ALLOCATION_URL = "https://scheduler.api.aleph.cloud/api/v0/allocation";
|
|
1375
1610
|
var DEFAULT_TWO_N_SIX_HASH_URL = "https://api.2n6.me/api/hash";
|
|
1376
|
-
function
|
|
1611
|
+
function asString4(value) {
|
|
1377
1612
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1378
1613
|
}
|
|
1379
1614
|
function asNumber3(value) {
|
|
@@ -1415,17 +1650,17 @@ async function fetchSchedulerAllocation2(itemHash, options) {
|
|
|
1415
1650
|
const node = payload?.node;
|
|
1416
1651
|
return {
|
|
1417
1652
|
source: "scheduler",
|
|
1418
|
-
crnHash:
|
|
1419
|
-
crnUrl:
|
|
1653
|
+
crnHash: asString4(node?.node_id),
|
|
1654
|
+
crnUrl: asString4(node?.url),
|
|
1420
1655
|
node: node ? {
|
|
1421
|
-
node_id:
|
|
1422
|
-
url:
|
|
1423
|
-
ipv6:
|
|
1656
|
+
node_id: asString4(node.node_id) ?? void 0,
|
|
1657
|
+
url: asString4(node.url) ?? void 0,
|
|
1658
|
+
ipv6: asString4(node.ipv6),
|
|
1424
1659
|
supports_ipv6: typeof node.supports_ipv6 === "boolean" ? node.supports_ipv6 : void 0
|
|
1425
1660
|
} : null,
|
|
1426
|
-
vmIpv6:
|
|
1661
|
+
vmIpv6: asString4(payload?.vm_ipv6),
|
|
1427
1662
|
period: payload?.period ? {
|
|
1428
|
-
start_timestamp:
|
|
1663
|
+
start_timestamp: asString4(payload.period.start_timestamp) ?? void 0,
|
|
1429
1664
|
duration_seconds: asNumber3(payload.period.duration_seconds) ?? void 0
|
|
1430
1665
|
} : null
|
|
1431
1666
|
};
|
|
@@ -1440,7 +1675,7 @@ async function fetch2n6WebAccessUrl2(itemHash, options) {
|
|
|
1440
1675
|
return {
|
|
1441
1676
|
url: normalizeProxyUrl2(payload?.url ?? payload?.subdomain),
|
|
1442
1677
|
active: typeof payload?.active === "boolean" ? payload.active : null,
|
|
1443
|
-
subdomain:
|
|
1678
|
+
subdomain: asString4(payload?.subdomain)
|
|
1444
1679
|
};
|
|
1445
1680
|
}
|
|
1446
1681
|
async function fetchCrnExecutionMap2(crnUrl, options) {
|
|
@@ -1660,7 +1895,7 @@ function utf8Hex(value) {
|
|
|
1660
1895
|
function normalizeHexSignature(signature) {
|
|
1661
1896
|
return signature.startsWith("0x") ? signature : `0x${signature}`;
|
|
1662
1897
|
}
|
|
1663
|
-
function
|
|
1898
|
+
function asString5(value) {
|
|
1664
1899
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1665
1900
|
}
|
|
1666
1901
|
function trimTrailingSlashes(value) {
|
|
@@ -1737,14 +1972,14 @@ async function fetchInstanceNodeHash(args) {
|
|
|
1737
1972
|
apiHost: args.apiHost
|
|
1738
1973
|
});
|
|
1739
1974
|
const payload = envelope;
|
|
1740
|
-
return
|
|
1975
|
+
return asString5(payload?.content?.requirements?.node?.node_hash) ?? asString5(payload?.message?.content?.requirements?.node?.node_hash) ?? null;
|
|
1741
1976
|
}
|
|
1742
1977
|
async function resolveInstanceCrn(args) {
|
|
1743
|
-
const providedCrnUrl =
|
|
1978
|
+
const providedCrnUrl = asString5(args.crnUrl);
|
|
1744
1979
|
if (providedCrnUrl) {
|
|
1745
1980
|
return {
|
|
1746
1981
|
crnUrl: trimTrailingSlashes(providedCrnUrl),
|
|
1747
|
-
crnHash:
|
|
1982
|
+
crnHash: asString5(args.crnHash),
|
|
1748
1983
|
source: "provided"
|
|
1749
1984
|
};
|
|
1750
1985
|
}
|
|
@@ -1755,11 +1990,11 @@ async function resolveInstanceCrn(args) {
|
|
|
1755
1990
|
if (allocation?.crnUrl) {
|
|
1756
1991
|
return {
|
|
1757
1992
|
crnUrl: trimTrailingSlashes(allocation.crnUrl),
|
|
1758
|
-
crnHash:
|
|
1993
|
+
crnHash: asString5(allocation.crnHash),
|
|
1759
1994
|
source: "scheduler"
|
|
1760
1995
|
};
|
|
1761
1996
|
}
|
|
1762
|
-
const manualCrnHash =
|
|
1997
|
+
const manualCrnHash = asString5(args.crnHash) ?? await fetchInstanceNodeHash({
|
|
1763
1998
|
instanceHash: args.instanceHash,
|
|
1764
1999
|
fetch: args.fetch,
|
|
1765
2000
|
apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST2
|
|
@@ -3825,6 +4060,7 @@ function defaultState(props = {}) {
|
|
|
3825
4060
|
},
|
|
3826
4061
|
balance: null,
|
|
3827
4062
|
crns: [],
|
|
4063
|
+
crnSource: null,
|
|
3828
4064
|
selectedCrn: null,
|
|
3829
4065
|
crnOptions: [],
|
|
3830
4066
|
crnPinnedByUser: false,
|
|
@@ -3854,6 +4090,17 @@ function isDebugEnabled(props) {
|
|
|
3854
4090
|
return false;
|
|
3855
4091
|
}
|
|
3856
4092
|
}
|
|
4093
|
+
function crnSourcePreference(props) {
|
|
4094
|
+
const fromStorage = (() => {
|
|
4095
|
+
try {
|
|
4096
|
+
return globalThis.localStorage?.getItem("LE_SPACE_CRN_SOURCE") ?? null;
|
|
4097
|
+
} catch {
|
|
4098
|
+
return null;
|
|
4099
|
+
}
|
|
4100
|
+
})();
|
|
4101
|
+
const raw = (fromStorage ?? props.crnSource ?? "auto").trim().toLowerCase();
|
|
4102
|
+
return raw === "aggregate" || raw === "list" ? raw : "auto";
|
|
4103
|
+
}
|
|
3857
4104
|
async function sha256Hex(payload) {
|
|
3858
4105
|
const digest = await globalThis.crypto.subtle.digest(
|
|
3859
4106
|
"SHA-256",
|
|
@@ -3869,6 +4116,17 @@ function hostnameOfUrl(value) {
|
|
|
3869
4116
|
return null;
|
|
3870
4117
|
}
|
|
3871
4118
|
}
|
|
4119
|
+
var INSTANCE_ADDRESS_TIMEOUT_MS = 1e4;
|
|
4120
|
+
function describeInstanceAddressFailure(error, sourceUrl) {
|
|
4121
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
4122
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
4123
|
+
return `${sourceUrl} did not answer within ${INSTANCE_ADDRESS_TIMEOUT_MS / 1e3}s. Press Refresh to try again.`;
|
|
4124
|
+
}
|
|
4125
|
+
if (error instanceof TypeError || /failed to fetch|load failed|networkerror/i.test(raw)) {
|
|
4126
|
+
return `${sourceUrl} closed the connection without answering. Relays built before rootfs 0.8.0 do not serve an address document, and their proxy hangs up instead of returning 404 \u2014 redeploying from a current rootfs fixes it. (${raw})`;
|
|
4127
|
+
}
|
|
4128
|
+
return raw;
|
|
4129
|
+
}
|
|
3872
4130
|
function emptyInstanceAddresses() {
|
|
3873
4131
|
return {
|
|
3874
4132
|
status: "idle",
|
|
@@ -4230,6 +4488,7 @@ var SponsorRelayController = class {
|
|
|
4230
4488
|
apiHost: props.apiHost,
|
|
4231
4489
|
apiHosts: props.apiHosts,
|
|
4232
4490
|
crnListUrl: props.crnListUrl,
|
|
4491
|
+
crnSource: crnSourcePreference(props),
|
|
4233
4492
|
schedulerApiHost: props.schedulerApiHost,
|
|
4234
4493
|
twoN6ApiHost: props.twoN6ApiHost
|
|
4235
4494
|
});
|
|
@@ -5452,10 +5711,18 @@ var SponsorRelayController = class {
|
|
|
5452
5711
|
statusText: currentProfile === "ucan-store" ? "Refreshing service deployment data" : "Refreshing relay deployment data"
|
|
5453
5712
|
});
|
|
5454
5713
|
try {
|
|
5455
|
-
const [pricingSummary,
|
|
5714
|
+
const [pricingSummary, crnList] = await Promise.all([
|
|
5456
5715
|
fetchInstancePricing(this.client.apiHost),
|
|
5457
|
-
this.client.
|
|
5716
|
+
this.client.fetchCrnsWithSource(crnSourcePreference(this.props))
|
|
5458
5717
|
]);
|
|
5718
|
+
const { crns, source: crnSource } = crnList;
|
|
5719
|
+
if (crnList.listError) {
|
|
5720
|
+
this.trace("crns:fallback", {
|
|
5721
|
+
source: crnSource,
|
|
5722
|
+
count: crns.length,
|
|
5723
|
+
listError: crnList.listError.message
|
|
5724
|
+
});
|
|
5725
|
+
}
|
|
5459
5726
|
const manifestState = await resolveManifest({
|
|
5460
5727
|
manifestUrl: this.state.manifestUrl,
|
|
5461
5728
|
manifestJson: this.state.manifestJson
|
|
@@ -5605,6 +5872,7 @@ var SponsorRelayController = class {
|
|
|
5605
5872
|
},
|
|
5606
5873
|
balance,
|
|
5607
5874
|
crns,
|
|
5875
|
+
crnSource,
|
|
5608
5876
|
instances,
|
|
5609
5877
|
bootstrapRegistrations,
|
|
5610
5878
|
orphanBootstrapRegistrations,
|
|
@@ -5682,10 +5950,21 @@ var SponsorRelayController = class {
|
|
|
5682
5950
|
}
|
|
5683
5951
|
});
|
|
5684
5952
|
const preferredCrnHash = this.state.selectedCrn?.hash ?? null;
|
|
5685
|
-
const
|
|
5953
|
+
const rankedCrns = [
|
|
5686
5954
|
...compatibleCrns2.filter((crn) => crn.hash === preferredCrnHash),
|
|
5687
5955
|
...compatibleCrns2.filter((crn) => crn.hash !== preferredCrnHash)
|
|
5688
5956
|
].slice(0, 5);
|
|
5957
|
+
const orderedCrns = this.state.crnSource === "aggregate" ? await filterReachableCrns(rankedCrns, {
|
|
5958
|
+
limit: rankedCrns.length,
|
|
5959
|
+
onDiagnostic: (message) => this.trace("crns:probe", { message })
|
|
5960
|
+
}).then(
|
|
5961
|
+
(reachable) => preferredCrnHash && !reachable.some((crn) => crn.hash === preferredCrnHash) ? [
|
|
5962
|
+
...rankedCrns.filter(
|
|
5963
|
+
(crn) => crn.hash === preferredCrnHash
|
|
5964
|
+
),
|
|
5965
|
+
...reachable
|
|
5966
|
+
] : reachable
|
|
5967
|
+
) : rankedCrns;
|
|
5689
5968
|
if (orderedCrns.length === 0) {
|
|
5690
5969
|
throw new Error(
|
|
5691
5970
|
"No compatible CRN is currently available for this tier."
|
|
@@ -6220,14 +6499,21 @@ var SponsorRelayController = class {
|
|
|
6220
6499
|
sourceUrl,
|
|
6221
6500
|
error: null
|
|
6222
6501
|
});
|
|
6502
|
+
const abort = new AbortController();
|
|
6503
|
+
const timeout = globalThis.setTimeout(
|
|
6504
|
+
() => abort.abort(),
|
|
6505
|
+
INSTANCE_ADDRESS_TIMEOUT_MS
|
|
6506
|
+
);
|
|
6223
6507
|
try {
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6508
|
+
let response;
|
|
6509
|
+
try {
|
|
6510
|
+
response = await fetch(sourceUrl, {
|
|
6511
|
+
cache: "no-cache",
|
|
6512
|
+
signal: abort.signal
|
|
6513
|
+
});
|
|
6514
|
+
} finally {
|
|
6515
|
+
globalThis.clearTimeout(timeout);
|
|
6516
|
+
}
|
|
6231
6517
|
if (response.status === 404) {
|
|
6232
6518
|
this.patchInstanceAddresses(instanceHash, {
|
|
6233
6519
|
...emptyInstanceAddresses(),
|
|
@@ -6254,7 +6540,7 @@ var SponsorRelayController = class {
|
|
|
6254
6540
|
...this.state.instanceAddresses[instanceHash] ?? emptyInstanceAddresses(),
|
|
6255
6541
|
status: "error",
|
|
6256
6542
|
sourceUrl,
|
|
6257
|
-
error: error
|
|
6543
|
+
error: describeInstanceAddressFailure(error, sourceUrl)
|
|
6258
6544
|
});
|
|
6259
6545
|
}
|
|
6260
6546
|
}
|
|
@@ -6485,6 +6771,7 @@ function useSponsorRelayController(props) {
|
|
|
6485
6771
|
props.apiHost,
|
|
6486
6772
|
props.apiHosts,
|
|
6487
6773
|
props.crnListUrl,
|
|
6774
|
+
props.crnSource,
|
|
6488
6775
|
props.debug,
|
|
6489
6776
|
props.instanceName,
|
|
6490
6777
|
props.manifestJson,
|
package/shared/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
type CrnSource = 'list' | 'aggregate';
|
|
2
|
+
type CrnSourcePreference = 'auto' | 'aggregate' | 'list';
|
|
3
|
+
|
|
1
4
|
type MessageStatus = 'processed' | 'pending' | 'rejected' | 'removing' | 'removed' | 'unknown';
|
|
2
5
|
interface BalanceResponse {
|
|
3
6
|
address: string;
|
|
@@ -220,6 +223,12 @@ interface SponsorRelayProps {
|
|
|
220
223
|
apiHost?: string;
|
|
221
224
|
apiHosts?: string | readonly string[];
|
|
222
225
|
crnListUrl?: string;
|
|
226
|
+
/**
|
|
227
|
+
* Which CRN source to read. `auto` falls back to the corechannel aggregate
|
|
228
|
+
* when crns.json is down, `aggregate` skips crns.json, `list` disables the
|
|
229
|
+
* fallback. `localStorage.LE_SPACE_CRN_SOURCE` overrides this at runtime.
|
|
230
|
+
*/
|
|
231
|
+
crnSource?: CrnSourcePreference;
|
|
223
232
|
schedulerApiHost?: string;
|
|
224
233
|
twoN6ApiHost?: string;
|
|
225
234
|
}
|
|
@@ -344,6 +353,8 @@ interface SponsorRelayState {
|
|
|
344
353
|
pricingSummary: SponsorRelayPricingSummary;
|
|
345
354
|
balance: BalanceResponse | null;
|
|
346
355
|
crns: Crn[];
|
|
356
|
+
/** Which source the current `crns` came from; `null` until the first load. */
|
|
357
|
+
crnSource: CrnSource | null;
|
|
347
358
|
selectedCrn: Crn | null;
|
|
348
359
|
crnOptions: Array<{
|
|
349
360
|
hash: string;
|
|
@@ -386,7 +397,7 @@ declare const STALE_INSTANCE_ALLOCATION_COOLDOWN_MS: number;
|
|
|
386
397
|
declare const RECENT_INSTANCE_RUNTIME_GRACE_MS: number;
|
|
387
398
|
declare const DEPLOYMENT_PENDING_WARNING_MS: number;
|
|
388
399
|
|
|
389
|
-
declare const UI_PACKAGE_VERSION = "0.
|
|
400
|
+
declare const UI_PACKAGE_VERSION = "0.9.1";
|
|
390
401
|
|
|
391
402
|
interface RootfsRequiredPortForward {
|
|
392
403
|
port: number;
|
package/shared/index.js
CHANGED
|
@@ -42,7 +42,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
|
|
|
42
42
|
var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
|
|
43
43
|
|
|
44
44
|
// src/shared/package-version.ts
|
|
45
|
-
var UI_PACKAGE_VERSION = "0.
|
|
45
|
+
var UI_PACKAGE_VERSION = "0.9.1";
|
|
46
46
|
|
|
47
47
|
// src/shared/format.ts
|
|
48
48
|
function shortHash(value, head = 8, tail = 6) {
|
|
@@ -140,6 +140,8 @@ var DEFAULT_ALEPH_API_HOSTS = [
|
|
|
140
140
|
];
|
|
141
141
|
var DEFAULT_ALEPH_API_HOST = DEFAULT_ALEPH_API_HOSTS[0] ?? "https://api2.aleph.im";
|
|
142
142
|
var DEFAULT_CRN_LIST_URL = "https://crns-list.aleph.sh/crns.json";
|
|
143
|
+
var DEFAULT_CRN_AGGREGATE_ADDRESS = "0xa1B3bb7d2332383D96b7796B908fB7f7F3c2Be10";
|
|
144
|
+
var DEFAULT_CRN_AGGREGATE_KEY = "corechannel";
|
|
143
145
|
var DEFAULT_ALEPH_SCHEDULER_API_HOST = "https://scheduler.api.aleph.cloud";
|
|
144
146
|
var DEFAULT_2N6_API_HOST = "https://api.2n6.me";
|
|
145
147
|
function normalizeMessageStatus(status) {
|
|
@@ -194,7 +196,7 @@ async function fetchBalance(address, apiHost = DEFAULT_ALEPH_API_HOST) {
|
|
|
194
196
|
if (!response.ok) throw new Error(`Balance request failed: ${response.status}`);
|
|
195
197
|
return await response.json();
|
|
196
198
|
}
|
|
197
|
-
async function
|
|
199
|
+
async function fetchCrnsFromList(url = DEFAULT_CRN_LIST_URL) {
|
|
198
200
|
const requestUrl = new URL(url);
|
|
199
201
|
requestUrl.searchParams.set("filter_inactive", "true");
|
|
200
202
|
const response = await fetchWithTimeout(requestUrl, { cache: "no-cache" });
|
|
@@ -202,6 +204,130 @@ async function fetchCrns(url = DEFAULT_CRN_LIST_URL) {
|
|
|
202
204
|
const payload = await response.json();
|
|
203
205
|
return payload.crns ?? [];
|
|
204
206
|
}
|
|
207
|
+
function asFiniteNumber(value) {
|
|
208
|
+
if (value == null || value === "") return null;
|
|
209
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
210
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
211
|
+
}
|
|
212
|
+
function normalizeCrnAddress(value) {
|
|
213
|
+
const address = asString(value);
|
|
214
|
+
if (!address) return null;
|
|
215
|
+
const withScheme = /^https?:\/\//i.test(address) ? address : `https://${address}`;
|
|
216
|
+
try {
|
|
217
|
+
return new URL(withScheme).toString().replace(/\/+$/, "");
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function mapResourceNodesToCrns(resourceNodes) {
|
|
223
|
+
if (!Array.isArray(resourceNodes)) return [];
|
|
224
|
+
const crns = [];
|
|
225
|
+
for (const node of resourceNodes) {
|
|
226
|
+
if (!node || typeof node !== "object") continue;
|
|
227
|
+
const entry = node;
|
|
228
|
+
const hash = asString(entry.hash);
|
|
229
|
+
const address = normalizeCrnAddress(entry.address);
|
|
230
|
+
const type = asString(entry.type)?.toLowerCase();
|
|
231
|
+
const status = asString(entry.status)?.toLowerCase();
|
|
232
|
+
if (!hash || !address) continue;
|
|
233
|
+
if (entry.inactive_since != null || entry.locked === true) continue;
|
|
234
|
+
if (type && type !== "compute") continue;
|
|
235
|
+
if (status && status !== "linked" && status !== "active") continue;
|
|
236
|
+
if (!asString(entry.parent)) continue;
|
|
237
|
+
const score = asFiniteNumber(entry.score) ?? asFiniteNumber(entry.scoreV1);
|
|
238
|
+
const performance = asFiniteNumber(entry.performance);
|
|
239
|
+
const decentralization = asFiniteNumber(entry.decentralization);
|
|
240
|
+
crns.push({
|
|
241
|
+
hash,
|
|
242
|
+
address,
|
|
243
|
+
name: asString(entry.name) ?? hash,
|
|
244
|
+
...score == null ? {} : { score },
|
|
245
|
+
...performance == null ? {} : { performance },
|
|
246
|
+
...decentralization == null ? {} : { decentralization }
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return crns;
|
|
250
|
+
}
|
|
251
|
+
async function fetchCrnsFromAggregate(apiHosts = DEFAULT_ALEPH_API_HOSTS, address = DEFAULT_CRN_AGGREGATE_ADDRESS) {
|
|
252
|
+
const hosts = apiHosts.length > 0 ? apiHosts : DEFAULT_ALEPH_API_HOSTS;
|
|
253
|
+
let lastError = null;
|
|
254
|
+
for (const apiHost of hosts) {
|
|
255
|
+
try {
|
|
256
|
+
const url = new URL(`/api/v0/aggregates/${address}.json`, apiHost);
|
|
257
|
+
url.searchParams.set("keys", DEFAULT_CRN_AGGREGATE_KEY);
|
|
258
|
+
const response = await fetchWithTimeout(url, { cache: "no-cache" });
|
|
259
|
+
if (!response.ok) {
|
|
260
|
+
lastError = new Error(`CRN aggregate request failed: ${response.status}`);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const payload = await response.json();
|
|
264
|
+
const crns = mapResourceNodesToCrns(payload?.data?.corechannel?.resource_nodes);
|
|
265
|
+
if (crns.length > 0) return crns;
|
|
266
|
+
lastError = new Error("CRN aggregate contained no deployable resource nodes");
|
|
267
|
+
} catch (error) {
|
|
268
|
+
lastError = error;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "CRN aggregate request failed"));
|
|
272
|
+
}
|
|
273
|
+
async function fetchCrnsWithSource(url = DEFAULT_CRN_LIST_URL, options = {}) {
|
|
274
|
+
const apiHosts = options.apiHosts ?? DEFAULT_ALEPH_API_HOSTS;
|
|
275
|
+
const source = options.source ?? "auto";
|
|
276
|
+
if (source === "aggregate") {
|
|
277
|
+
return {
|
|
278
|
+
crns: await fetchCrnsFromAggregate(apiHosts, options.aggregateAddress),
|
|
279
|
+
source: "aggregate"
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
let listError;
|
|
283
|
+
try {
|
|
284
|
+
const crns = await fetchCrnsFromList(url);
|
|
285
|
+
if (crns.length > 0) return { crns, source: "list" };
|
|
286
|
+
listError = new Error("CRN list returned no entries");
|
|
287
|
+
} catch (error) {
|
|
288
|
+
listError = error instanceof Error ? error : new Error(String(error));
|
|
289
|
+
}
|
|
290
|
+
if (source === "list") throw listError;
|
|
291
|
+
return {
|
|
292
|
+
crns: await fetchCrnsFromAggregate(apiHosts, options.aggregateAddress),
|
|
293
|
+
source: "aggregate",
|
|
294
|
+
listError
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
async function fetchCrns(url = DEFAULT_CRN_LIST_URL, options = {}) {
|
|
298
|
+
return (await fetchCrnsWithSource(url, options)).crns;
|
|
299
|
+
}
|
|
300
|
+
async function probeCrnAvailability(crn) {
|
|
301
|
+
const address = String(crn.address ?? "").trim();
|
|
302
|
+
if (!address) return "unreachable";
|
|
303
|
+
const lookup = await fetchCrnExecutionMap(address);
|
|
304
|
+
if (lookup.payload !== null) return "reachable";
|
|
305
|
+
return lookup.blocked ? "unknown" : "unreachable";
|
|
306
|
+
}
|
|
307
|
+
async function filterReachableCrns(crns, options = {}) {
|
|
308
|
+
const limit = Math.max(1, Number(options.limit) || 5);
|
|
309
|
+
const probed = crns.slice(0, limit);
|
|
310
|
+
if (probed.length === 0) return [...crns];
|
|
311
|
+
const verdicts = await Promise.all(
|
|
312
|
+
probed.map(async (crn) => {
|
|
313
|
+
try {
|
|
314
|
+
return { crn, verdict: await probeCrnAvailability(crn) };
|
|
315
|
+
} catch {
|
|
316
|
+
return { crn, verdict: "unknown" };
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
);
|
|
320
|
+
const kept = verdicts.filter((entry) => entry.verdict !== "unreachable").map((entry) => entry.crn);
|
|
321
|
+
if (kept.length === 0) {
|
|
322
|
+
options.onDiagnostic?.(`No CRN answered its executions endpoint (${probed.length} probed); keeping the unverified order`);
|
|
323
|
+
return [...crns];
|
|
324
|
+
}
|
|
325
|
+
const dropped = probed.length - kept.length;
|
|
326
|
+
if (dropped > 0) {
|
|
327
|
+
options.onDiagnostic?.(`Skipping ${dropped} unreachable CRN${dropped === 1 ? "" : "s"} of ${probed.length} probed`);
|
|
328
|
+
}
|
|
329
|
+
return [...kept, ...crns.slice(limit)];
|
|
330
|
+
}
|
|
205
331
|
async function fetchInstances(address, apiHost = DEFAULT_ALEPH_API_HOST) {
|
|
206
332
|
const url = new URL("/api/v0/messages.json", apiHost);
|
|
207
333
|
url.searchParams.set("msgTypes", "INSTANCE");
|
|
@@ -856,18 +982,23 @@ function createAlephBrowserClient(options = {}) {
|
|
|
856
982
|
const apiHosts = options.apiHosts === void 0 && options.apiHost === void 0 ? [...DEFAULT_ALEPH_API_HOSTS] : normalizeApiHosts(options.apiHosts, options.apiHost ?? DEFAULT_ALEPH_API_HOST);
|
|
857
983
|
const apiHost = apiHosts[0] ?? DEFAULT_ALEPH_API_HOST;
|
|
858
984
|
const crnListUrl = options.crnListUrl ?? DEFAULT_CRN_LIST_URL;
|
|
985
|
+
const crnSource = options.crnSource ?? "auto";
|
|
859
986
|
const schedulerApiHost = options.schedulerApiHost ?? DEFAULT_ALEPH_SCHEDULER_API_HOST;
|
|
860
987
|
const twoN6ApiHost = options.twoN6ApiHost ?? DEFAULT_2N6_API_HOST;
|
|
861
988
|
return {
|
|
862
989
|
apiHost,
|
|
863
990
|
apiHosts,
|
|
864
991
|
crnListUrl,
|
|
992
|
+
crnSource,
|
|
865
993
|
schedulerApiHost,
|
|
866
994
|
fetchBalance(address) {
|
|
867
995
|
return withApiHostFallback(apiHosts, (host) => fetchBalance(address, host));
|
|
868
996
|
},
|
|
869
997
|
fetchCrns() {
|
|
870
|
-
return fetchCrns(crnListUrl);
|
|
998
|
+
return fetchCrns(crnListUrl, { apiHosts, source: crnSource });
|
|
999
|
+
},
|
|
1000
|
+
fetchCrnsWithSource(source) {
|
|
1001
|
+
return fetchCrnsWithSource(crnListUrl, { apiHosts, source: source ?? crnSource });
|
|
871
1002
|
},
|
|
872
1003
|
fetchInstances(address) {
|
|
873
1004
|
return withApiHostFallback(apiHosts, (host) => fetchInstances(address, host));
|
|
@@ -942,6 +1073,15 @@ var DEFAULT_ALEPH_API_HOST2 = "https://api.aleph.im";
|
|
|
942
1073
|
|
|
943
1074
|
// ../core/src/crns.ts
|
|
944
1075
|
var DEFAULT_CRN_LIST_URL2 = "https://crns-list.aleph.sh/crns.json";
|
|
1076
|
+
var DEFAULT_CRN_AGGREGATE_ADDRESS2 = "0xa1B3bb7d2332383D96b7796B908fB7f7F3c2Be10";
|
|
1077
|
+
var DEFAULT_CRN_AGGREGATE_KEY2 = "corechannel";
|
|
1078
|
+
var DEFAULT_CRN_AGGREGATE_API_HOSTS = [
|
|
1079
|
+
"https://api2.aleph.im",
|
|
1080
|
+
"https://api.aleph.im"
|
|
1081
|
+
];
|
|
1082
|
+
function asString2(value) {
|
|
1083
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1084
|
+
}
|
|
945
1085
|
function compatibleCrns(crns, excludedHashes = []) {
|
|
946
1086
|
const excluded = new Set(excludedHashes.filter(Boolean));
|
|
947
1087
|
return [...crns].filter((crn) => {
|
|
@@ -975,7 +1115,7 @@ function filterDeployableCrns(crns, options = {}) {
|
|
|
975
1115
|
compatibleCrns(crns, options.excludedHashes).filter((crn) => hasCrnCapacity(crn, options.spec))
|
|
976
1116
|
);
|
|
977
1117
|
}
|
|
978
|
-
async function
|
|
1118
|
+
async function fetchCrnsFromList2(options) {
|
|
979
1119
|
const requestUrl = new URL(options.url ?? DEFAULT_CRN_LIST_URL2);
|
|
980
1120
|
requestUrl.searchParams.set("filter_inactive", "true");
|
|
981
1121
|
const response = await options.fetch(requestUrl.toString(), {
|
|
@@ -987,6 +1127,101 @@ async function fetchCrns2(options) {
|
|
|
987
1127
|
const payload = await response.json();
|
|
988
1128
|
return Array.isArray(payload?.crns) ? payload.crns : [];
|
|
989
1129
|
}
|
|
1130
|
+
function normalizeCrnAddress2(value) {
|
|
1131
|
+
const address = asString2(value);
|
|
1132
|
+
if (!address) return null;
|
|
1133
|
+
const withScheme = /^https?:\/\//i.test(address) ? address : `https://${address}`;
|
|
1134
|
+
try {
|
|
1135
|
+
return new URL(withScheme).toString().replace(/\/+$/, "");
|
|
1136
|
+
} catch {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
function aggregateScore(node) {
|
|
1141
|
+
for (const candidate of [node.score, node.scoreV1]) {
|
|
1142
|
+
if (candidate == null || candidate === "") continue;
|
|
1143
|
+
const numeric = typeof candidate === "number" ? candidate : Number(candidate);
|
|
1144
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
1145
|
+
}
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
function isDeployableResourceNode(node) {
|
|
1149
|
+
if (!asString2(node.hash)) return false;
|
|
1150
|
+
if (!normalizeCrnAddress2(node.address)) return false;
|
|
1151
|
+
if (node.inactive_since != null) return false;
|
|
1152
|
+
if (node.locked === true) return false;
|
|
1153
|
+
const type = asString2(node.type)?.toLowerCase();
|
|
1154
|
+
if (type && type !== "compute") return false;
|
|
1155
|
+
const status = asString2(node.status)?.toLowerCase();
|
|
1156
|
+
if (status && status !== "linked" && status !== "active") return false;
|
|
1157
|
+
return Boolean(asString2(node.parent));
|
|
1158
|
+
}
|
|
1159
|
+
function mapResourceNodesToCrns2(resourceNodes) {
|
|
1160
|
+
if (!Array.isArray(resourceNodes)) return [];
|
|
1161
|
+
return resourceNodes.filter((node) => Boolean(node) && typeof node === "object").filter(isDeployableResourceNode).map((node) => {
|
|
1162
|
+
const record = {
|
|
1163
|
+
hash: asString2(node.hash),
|
|
1164
|
+
address: normalizeCrnAddress2(node.address)
|
|
1165
|
+
};
|
|
1166
|
+
const name = asString2(node.name);
|
|
1167
|
+
if (name) record.name = name;
|
|
1168
|
+
const score = aggregateScore(node);
|
|
1169
|
+
if (score != null) record.score = score;
|
|
1170
|
+
return record;
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
async function fetchCrnsFromAggregate2(options) {
|
|
1174
|
+
const apiHosts = options.apiHosts?.length ? options.apiHosts : DEFAULT_CRN_AGGREGATE_API_HOSTS;
|
|
1175
|
+
const address = options.address ?? DEFAULT_CRN_AGGREGATE_ADDRESS2;
|
|
1176
|
+
const aggregateKey = options.aggregateKey ?? DEFAULT_CRN_AGGREGATE_KEY2;
|
|
1177
|
+
let lastError = null;
|
|
1178
|
+
for (const apiHost of apiHosts) {
|
|
1179
|
+
try {
|
|
1180
|
+
const url = new URL(`/api/v0/aggregates/${address}.json`, apiHost);
|
|
1181
|
+
url.searchParams.set("keys", aggregateKey);
|
|
1182
|
+
const response = await options.fetch(url.toString(), { cache: "no-cache" });
|
|
1183
|
+
if (!response.ok) {
|
|
1184
|
+
lastError = new Error(`CRN aggregate request failed: ${response.status}`);
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
const payload = await response.json();
|
|
1188
|
+
const crns = mapResourceNodesToCrns2(payload?.data?.corechannel?.resource_nodes);
|
|
1189
|
+
if (crns.length > 0) return crns;
|
|
1190
|
+
lastError = new Error("CRN aggregate contained no deployable resource nodes");
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
lastError = error;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "CRN aggregate request failed"));
|
|
1196
|
+
}
|
|
1197
|
+
async function fetchCrnsWithSource2(options) {
|
|
1198
|
+
const aggregateOptions = {
|
|
1199
|
+
fetch: options.fetch,
|
|
1200
|
+
apiHosts: options.aggregateApiHosts,
|
|
1201
|
+
address: options.aggregateAddress
|
|
1202
|
+
};
|
|
1203
|
+
if (options.preferAggregate) {
|
|
1204
|
+
return { crns: await fetchCrnsFromAggregate2(aggregateOptions), source: "aggregate" };
|
|
1205
|
+
}
|
|
1206
|
+
let listError;
|
|
1207
|
+
try {
|
|
1208
|
+
const crns = await fetchCrnsFromList2({ url: options.url, fetch: options.fetch });
|
|
1209
|
+
if (crns.length > 0) return { crns, source: "list" };
|
|
1210
|
+
listError = new Error("CRN list returned no entries");
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
listError = error instanceof Error ? error : new Error(String(error));
|
|
1213
|
+
}
|
|
1214
|
+
options.onDiagnostic?.(`CRN list unavailable (${listError.message}); falling back to the ${DEFAULT_CRN_AGGREGATE_KEY2} aggregate`);
|
|
1215
|
+
try {
|
|
1216
|
+
return { crns: await fetchCrnsFromAggregate2(aggregateOptions), source: "aggregate", listError };
|
|
1217
|
+
} catch (aggregateError) {
|
|
1218
|
+
const message = aggregateError instanceof Error ? aggregateError.message : String(aggregateError);
|
|
1219
|
+
throw new Error(`${listError.message}; CRN aggregate fallback failed: ${message}`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
async function fetchCrns2(options) {
|
|
1223
|
+
return (await fetchCrnsWithSource2(options)).crns;
|
|
1224
|
+
}
|
|
990
1225
|
|
|
991
1226
|
// ../core/src/port-forwarding.ts
|
|
992
1227
|
var DEFAULT_INSTANCE_PORT_FORWARDS = [
|
|
@@ -1076,7 +1311,7 @@ async function fetchPortForwardAggregate(address, options) {
|
|
|
1076
1311
|
}
|
|
1077
1312
|
|
|
1078
1313
|
// ../core/src/aleph-normalizers.ts
|
|
1079
|
-
function
|
|
1314
|
+
function asString3(value) {
|
|
1080
1315
|
return typeof value === "string" && value.trim() ? value : null;
|
|
1081
1316
|
}
|
|
1082
1317
|
function asNumber2(value) {
|
|
@@ -1091,7 +1326,7 @@ function normalizeMessageStatus2(status) {
|
|
|
1091
1326
|
return "unknown";
|
|
1092
1327
|
}
|
|
1093
1328
|
function normalizeProxyUrl2(value) {
|
|
1094
|
-
const stringValue =
|
|
1329
|
+
const stringValue = asString3(value);
|
|
1095
1330
|
if (!stringValue) return null;
|
|
1096
1331
|
if (/^https?:\/\//i.test(stringValue)) return stringValue;
|
|
1097
1332
|
return `https://${stringValue}`;
|
|
@@ -1186,22 +1421,22 @@ function normalizeExecution2(item, crnUrl) {
|
|
|
1186
1421
|
version: "v2",
|
|
1187
1422
|
running: typeof v2Item.running === "boolean" ? v2Item.running : void 0,
|
|
1188
1423
|
networking: {
|
|
1189
|
-
ipv4_network:
|
|
1190
|
-
host_ipv4:
|
|
1191
|
-
ipv6_network:
|
|
1192
|
-
ipv6_ip:
|
|
1193
|
-
ipv4_ip:
|
|
1424
|
+
ipv4_network: asString3(networking.ipv4_network),
|
|
1425
|
+
host_ipv4: asString3(networking.host_ipv4),
|
|
1426
|
+
ipv6_network: asString3(networking.ipv6_network),
|
|
1427
|
+
ipv6_ip: asString3(networking.ipv6_ip),
|
|
1428
|
+
ipv4_ip: asString3(networking.ipv4_ip),
|
|
1194
1429
|
proxy_url: extractProxyUrl2(v2Item, networking),
|
|
1195
1430
|
mapped_ports: mappedPorts2
|
|
1196
1431
|
},
|
|
1197
1432
|
status: v2Item.status ? {
|
|
1198
|
-
defined_at:
|
|
1199
|
-
preparing_at:
|
|
1200
|
-
prepared_at:
|
|
1201
|
-
starting_at:
|
|
1202
|
-
started_at:
|
|
1203
|
-
stopping_at:
|
|
1204
|
-
stopped_at:
|
|
1433
|
+
defined_at: asString3(v2Item.status.defined_at),
|
|
1434
|
+
preparing_at: asString3(v2Item.status.preparing_at),
|
|
1435
|
+
prepared_at: asString3(v2Item.status.prepared_at),
|
|
1436
|
+
starting_at: asString3(v2Item.status.starting_at),
|
|
1437
|
+
started_at: asString3(v2Item.status.started_at),
|
|
1438
|
+
stopping_at: asString3(v2Item.status.stopping_at),
|
|
1439
|
+
stopped_at: asString3(v2Item.status.stopped_at)
|
|
1205
1440
|
} : null
|
|
1206
1441
|
};
|
|
1207
1442
|
}
|
|
@@ -1209,8 +1444,8 @@ function normalizeExecution2(item, crnUrl) {
|
|
|
1209
1444
|
crnUrl,
|
|
1210
1445
|
version: "v1",
|
|
1211
1446
|
networking: {
|
|
1212
|
-
ipv4: networking && "ipv4" in networking ?
|
|
1213
|
-
ipv6: networking && "ipv6" in networking ?
|
|
1447
|
+
ipv4: networking && "ipv4" in networking ? asString3(networking.ipv4) : null,
|
|
1448
|
+
ipv6: networking && "ipv6" in networking ? asString3(networking.ipv6) : null
|
|
1214
1449
|
},
|
|
1215
1450
|
status: null
|
|
1216
1451
|
};
|
|
@@ -1372,7 +1607,7 @@ async function broadcastAlephMessage2(message, options) {
|
|
|
1372
1607
|
// ../core/src/runtime.ts
|
|
1373
1608
|
var DEFAULT_SCHEDULER_ALLOCATION_URL = "https://scheduler.api.aleph.cloud/api/v0/allocation";
|
|
1374
1609
|
var DEFAULT_TWO_N_SIX_HASH_URL = "https://api.2n6.me/api/hash";
|
|
1375
|
-
function
|
|
1610
|
+
function asString4(value) {
|
|
1376
1611
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1377
1612
|
}
|
|
1378
1613
|
function asNumber3(value) {
|
|
@@ -1414,17 +1649,17 @@ async function fetchSchedulerAllocation2(itemHash, options) {
|
|
|
1414
1649
|
const node = payload?.node;
|
|
1415
1650
|
return {
|
|
1416
1651
|
source: "scheduler",
|
|
1417
|
-
crnHash:
|
|
1418
|
-
crnUrl:
|
|
1652
|
+
crnHash: asString4(node?.node_id),
|
|
1653
|
+
crnUrl: asString4(node?.url),
|
|
1419
1654
|
node: node ? {
|
|
1420
|
-
node_id:
|
|
1421
|
-
url:
|
|
1422
|
-
ipv6:
|
|
1655
|
+
node_id: asString4(node.node_id) ?? void 0,
|
|
1656
|
+
url: asString4(node.url) ?? void 0,
|
|
1657
|
+
ipv6: asString4(node.ipv6),
|
|
1423
1658
|
supports_ipv6: typeof node.supports_ipv6 === "boolean" ? node.supports_ipv6 : void 0
|
|
1424
1659
|
} : null,
|
|
1425
|
-
vmIpv6:
|
|
1660
|
+
vmIpv6: asString4(payload?.vm_ipv6),
|
|
1426
1661
|
period: payload?.period ? {
|
|
1427
|
-
start_timestamp:
|
|
1662
|
+
start_timestamp: asString4(payload.period.start_timestamp) ?? void 0,
|
|
1428
1663
|
duration_seconds: asNumber3(payload.period.duration_seconds) ?? void 0
|
|
1429
1664
|
} : null
|
|
1430
1665
|
};
|
|
@@ -1439,7 +1674,7 @@ async function fetch2n6WebAccessUrl2(itemHash, options) {
|
|
|
1439
1674
|
return {
|
|
1440
1675
|
url: normalizeProxyUrl2(payload?.url ?? payload?.subdomain),
|
|
1441
1676
|
active: typeof payload?.active === "boolean" ? payload.active : null,
|
|
1442
|
-
subdomain:
|
|
1677
|
+
subdomain: asString4(payload?.subdomain)
|
|
1443
1678
|
};
|
|
1444
1679
|
}
|
|
1445
1680
|
async function fetchCrnExecutionMap2(crnUrl, options) {
|
|
@@ -1659,7 +1894,7 @@ function utf8Hex(value) {
|
|
|
1659
1894
|
function normalizeHexSignature(signature) {
|
|
1660
1895
|
return signature.startsWith("0x") ? signature : `0x${signature}`;
|
|
1661
1896
|
}
|
|
1662
|
-
function
|
|
1897
|
+
function asString5(value) {
|
|
1663
1898
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1664
1899
|
}
|
|
1665
1900
|
function trimTrailingSlashes(value) {
|
|
@@ -1736,14 +1971,14 @@ async function fetchInstanceNodeHash(args) {
|
|
|
1736
1971
|
apiHost: args.apiHost
|
|
1737
1972
|
});
|
|
1738
1973
|
const payload = envelope;
|
|
1739
|
-
return
|
|
1974
|
+
return asString5(payload?.content?.requirements?.node?.node_hash) ?? asString5(payload?.message?.content?.requirements?.node?.node_hash) ?? null;
|
|
1740
1975
|
}
|
|
1741
1976
|
async function resolveInstanceCrn(args) {
|
|
1742
|
-
const providedCrnUrl =
|
|
1977
|
+
const providedCrnUrl = asString5(args.crnUrl);
|
|
1743
1978
|
if (providedCrnUrl) {
|
|
1744
1979
|
return {
|
|
1745
1980
|
crnUrl: trimTrailingSlashes(providedCrnUrl),
|
|
1746
|
-
crnHash:
|
|
1981
|
+
crnHash: asString5(args.crnHash),
|
|
1747
1982
|
source: "provided"
|
|
1748
1983
|
};
|
|
1749
1984
|
}
|
|
@@ -1754,11 +1989,11 @@ async function resolveInstanceCrn(args) {
|
|
|
1754
1989
|
if (allocation?.crnUrl) {
|
|
1755
1990
|
return {
|
|
1756
1991
|
crnUrl: trimTrailingSlashes(allocation.crnUrl),
|
|
1757
|
-
crnHash:
|
|
1992
|
+
crnHash: asString5(allocation.crnHash),
|
|
1758
1993
|
source: "scheduler"
|
|
1759
1994
|
};
|
|
1760
1995
|
}
|
|
1761
|
-
const manualCrnHash =
|
|
1996
|
+
const manualCrnHash = asString5(args.crnHash) ?? await fetchInstanceNodeHash({
|
|
1762
1997
|
instanceHash: args.instanceHash,
|
|
1763
1998
|
fetch: args.fetch,
|
|
1764
1999
|
apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST2
|
|
@@ -3824,6 +4059,7 @@ function defaultState(props = {}) {
|
|
|
3824
4059
|
},
|
|
3825
4060
|
balance: null,
|
|
3826
4061
|
crns: [],
|
|
4062
|
+
crnSource: null,
|
|
3827
4063
|
selectedCrn: null,
|
|
3828
4064
|
crnOptions: [],
|
|
3829
4065
|
crnPinnedByUser: false,
|
|
@@ -3853,6 +4089,17 @@ function isDebugEnabled(props) {
|
|
|
3853
4089
|
return false;
|
|
3854
4090
|
}
|
|
3855
4091
|
}
|
|
4092
|
+
function crnSourcePreference(props) {
|
|
4093
|
+
const fromStorage = (() => {
|
|
4094
|
+
try {
|
|
4095
|
+
return globalThis.localStorage?.getItem("LE_SPACE_CRN_SOURCE") ?? null;
|
|
4096
|
+
} catch {
|
|
4097
|
+
return null;
|
|
4098
|
+
}
|
|
4099
|
+
})();
|
|
4100
|
+
const raw = (fromStorage ?? props.crnSource ?? "auto").trim().toLowerCase();
|
|
4101
|
+
return raw === "aggregate" || raw === "list" ? raw : "auto";
|
|
4102
|
+
}
|
|
3856
4103
|
async function sha256Hex(payload) {
|
|
3857
4104
|
const digest = await globalThis.crypto.subtle.digest(
|
|
3858
4105
|
"SHA-256",
|
|
@@ -3868,6 +4115,17 @@ function hostnameOfUrl(value) {
|
|
|
3868
4115
|
return null;
|
|
3869
4116
|
}
|
|
3870
4117
|
}
|
|
4118
|
+
var INSTANCE_ADDRESS_TIMEOUT_MS = 1e4;
|
|
4119
|
+
function describeInstanceAddressFailure(error, sourceUrl) {
|
|
4120
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
4121
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
4122
|
+
return `${sourceUrl} did not answer within ${INSTANCE_ADDRESS_TIMEOUT_MS / 1e3}s. Press Refresh to try again.`;
|
|
4123
|
+
}
|
|
4124
|
+
if (error instanceof TypeError || /failed to fetch|load failed|networkerror/i.test(raw)) {
|
|
4125
|
+
return `${sourceUrl} closed the connection without answering. Relays built before rootfs 0.8.0 do not serve an address document, and their proxy hangs up instead of returning 404 \u2014 redeploying from a current rootfs fixes it. (${raw})`;
|
|
4126
|
+
}
|
|
4127
|
+
return raw;
|
|
4128
|
+
}
|
|
3871
4129
|
function emptyInstanceAddresses() {
|
|
3872
4130
|
return {
|
|
3873
4131
|
status: "idle",
|
|
@@ -4229,6 +4487,7 @@ var SponsorRelayController = class {
|
|
|
4229
4487
|
apiHost: props.apiHost,
|
|
4230
4488
|
apiHosts: props.apiHosts,
|
|
4231
4489
|
crnListUrl: props.crnListUrl,
|
|
4490
|
+
crnSource: crnSourcePreference(props),
|
|
4232
4491
|
schedulerApiHost: props.schedulerApiHost,
|
|
4233
4492
|
twoN6ApiHost: props.twoN6ApiHost
|
|
4234
4493
|
});
|
|
@@ -5451,10 +5710,18 @@ var SponsorRelayController = class {
|
|
|
5451
5710
|
statusText: currentProfile === "ucan-store" ? "Refreshing service deployment data" : "Refreshing relay deployment data"
|
|
5452
5711
|
});
|
|
5453
5712
|
try {
|
|
5454
|
-
const [pricingSummary,
|
|
5713
|
+
const [pricingSummary, crnList] = await Promise.all([
|
|
5455
5714
|
fetchInstancePricing(this.client.apiHost),
|
|
5456
|
-
this.client.
|
|
5715
|
+
this.client.fetchCrnsWithSource(crnSourcePreference(this.props))
|
|
5457
5716
|
]);
|
|
5717
|
+
const { crns, source: crnSource } = crnList;
|
|
5718
|
+
if (crnList.listError) {
|
|
5719
|
+
this.trace("crns:fallback", {
|
|
5720
|
+
source: crnSource,
|
|
5721
|
+
count: crns.length,
|
|
5722
|
+
listError: crnList.listError.message
|
|
5723
|
+
});
|
|
5724
|
+
}
|
|
5458
5725
|
const manifestState = await resolveManifest({
|
|
5459
5726
|
manifestUrl: this.state.manifestUrl,
|
|
5460
5727
|
manifestJson: this.state.manifestJson
|
|
@@ -5604,6 +5871,7 @@ var SponsorRelayController = class {
|
|
|
5604
5871
|
},
|
|
5605
5872
|
balance,
|
|
5606
5873
|
crns,
|
|
5874
|
+
crnSource,
|
|
5607
5875
|
instances,
|
|
5608
5876
|
bootstrapRegistrations,
|
|
5609
5877
|
orphanBootstrapRegistrations,
|
|
@@ -5681,10 +5949,21 @@ var SponsorRelayController = class {
|
|
|
5681
5949
|
}
|
|
5682
5950
|
});
|
|
5683
5951
|
const preferredCrnHash = this.state.selectedCrn?.hash ?? null;
|
|
5684
|
-
const
|
|
5952
|
+
const rankedCrns = [
|
|
5685
5953
|
...compatibleCrns2.filter((crn) => crn.hash === preferredCrnHash),
|
|
5686
5954
|
...compatibleCrns2.filter((crn) => crn.hash !== preferredCrnHash)
|
|
5687
5955
|
].slice(0, 5);
|
|
5956
|
+
const orderedCrns = this.state.crnSource === "aggregate" ? await filterReachableCrns(rankedCrns, {
|
|
5957
|
+
limit: rankedCrns.length,
|
|
5958
|
+
onDiagnostic: (message) => this.trace("crns:probe", { message })
|
|
5959
|
+
}).then(
|
|
5960
|
+
(reachable) => preferredCrnHash && !reachable.some((crn) => crn.hash === preferredCrnHash) ? [
|
|
5961
|
+
...rankedCrns.filter(
|
|
5962
|
+
(crn) => crn.hash === preferredCrnHash
|
|
5963
|
+
),
|
|
5964
|
+
...reachable
|
|
5965
|
+
] : reachable
|
|
5966
|
+
) : rankedCrns;
|
|
5688
5967
|
if (orderedCrns.length === 0) {
|
|
5689
5968
|
throw new Error(
|
|
5690
5969
|
"No compatible CRN is currently available for this tier."
|
|
@@ -6219,14 +6498,21 @@ var SponsorRelayController = class {
|
|
|
6219
6498
|
sourceUrl,
|
|
6220
6499
|
error: null
|
|
6221
6500
|
});
|
|
6501
|
+
const abort = new AbortController();
|
|
6502
|
+
const timeout = globalThis.setTimeout(
|
|
6503
|
+
() => abort.abort(),
|
|
6504
|
+
INSTANCE_ADDRESS_TIMEOUT_MS
|
|
6505
|
+
);
|
|
6222
6506
|
try {
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6507
|
+
let response;
|
|
6508
|
+
try {
|
|
6509
|
+
response = await fetch(sourceUrl, {
|
|
6510
|
+
cache: "no-cache",
|
|
6511
|
+
signal: abort.signal
|
|
6512
|
+
});
|
|
6513
|
+
} finally {
|
|
6514
|
+
globalThis.clearTimeout(timeout);
|
|
6515
|
+
}
|
|
6230
6516
|
if (response.status === 404) {
|
|
6231
6517
|
this.patchInstanceAddresses(instanceHash, {
|
|
6232
6518
|
...emptyInstanceAddresses(),
|
|
@@ -6253,7 +6539,7 @@ var SponsorRelayController = class {
|
|
|
6253
6539
|
...this.state.instanceAddresses[instanceHash] ?? emptyInstanceAddresses(),
|
|
6254
6540
|
status: "error",
|
|
6255
6541
|
sourceUrl,
|
|
6256
|
-
error: error
|
|
6542
|
+
error: describeInstanceAddressFailure(error, sourceUrl)
|
|
6257
6543
|
});
|
|
6258
6544
|
}
|
|
6259
6545
|
}
|
|
@@ -20,6 +20,7 @@ export let version = ''
|
|
|
20
20
|
export let apiHost = undefined
|
|
21
21
|
export let apiHosts = undefined
|
|
22
22
|
export let crnListUrl = undefined
|
|
23
|
+
export let crnSource = undefined
|
|
23
24
|
export let schedulerApiHost = undefined
|
|
24
25
|
export let twoN6ApiHost = undefined
|
|
25
26
|
|
|
@@ -35,6 +36,7 @@ export let apiHosts = undefined
|
|
|
35
36
|
apiHost,
|
|
36
37
|
apiHosts,
|
|
37
38
|
crnListUrl,
|
|
39
|
+
crnSource,
|
|
38
40
|
schedulerApiHost,
|
|
39
41
|
twoN6ApiHost
|
|
40
42
|
})
|