@le-space/ui 0.1.52
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 +22 -0
- package/package.json +37 -0
- package/react/index.d.ts +315 -0
- package/react/index.js +1856 -0
- package/shared/index.d.ts +339 -0
- package/shared/index.js +1695 -0
- package/styles.css +17 -0
- package/svelte/SponsorRelayFab.svelte +492 -0
- package/svelte/components/AccordionSection.svelte +37 -0
- package/svelte/components/CopyButton.svelte +31 -0
- package/svelte/components/LauncherButton.svelte +44 -0
- package/svelte/components/StatusLed.svelte +50 -0
- package/svelte/index.js +2 -0
- package/svelte/styles/theme.css +17 -0
package/shared/index.js
ADDED
|
@@ -0,0 +1,1695 @@
|
|
|
1
|
+
// src/shared/constants.ts
|
|
2
|
+
var DEFAULT_INSTANCE_NAME = "sponsor-relay";
|
|
3
|
+
var DEFAULT_MANIFEST_URL = "./rootfs-manifest.json";
|
|
4
|
+
var DEFAULT_TIER_ID = "tier-1";
|
|
5
|
+
var ROOTFS_MISSING_STATE = {
|
|
6
|
+
tone: "idle",
|
|
7
|
+
label: "manifest missing",
|
|
8
|
+
detail: "Provide a manifest URL or paste manifest JSON."
|
|
9
|
+
};
|
|
10
|
+
var RELAY_PING_IDLE_STATE = {
|
|
11
|
+
tone: "idle",
|
|
12
|
+
sent: false,
|
|
13
|
+
received: false,
|
|
14
|
+
lastPeerId: null,
|
|
15
|
+
lastLatencyMs: null,
|
|
16
|
+
lastSentAt: null,
|
|
17
|
+
lastReceivedAt: null,
|
|
18
|
+
error: null
|
|
19
|
+
};
|
|
20
|
+
var REFRESH_INTERVAL_MS = 3e4;
|
|
21
|
+
var RELAY_PING_INTERVAL_MS = 2e4;
|
|
22
|
+
|
|
23
|
+
// src/shared/format.ts
|
|
24
|
+
function shortHash(value, head = 8, tail = 6) {
|
|
25
|
+
if (!value) return "-";
|
|
26
|
+
if (value.length <= head + tail + 3) return value;
|
|
27
|
+
return `${value.slice(0, head)}...${value.slice(-tail)}`;
|
|
28
|
+
}
|
|
29
|
+
function formatNumber(value, digits = 0) {
|
|
30
|
+
if (value == null || !Number.isFinite(value)) return "-";
|
|
31
|
+
return new Intl.NumberFormat("en-US", {
|
|
32
|
+
maximumFractionDigits: digits,
|
|
33
|
+
minimumFractionDigits: digits === 0 ? 0 : Math.min(2, digits)
|
|
34
|
+
}).format(value);
|
|
35
|
+
}
|
|
36
|
+
function formatDateTime(value) {
|
|
37
|
+
if (!value) return "-";
|
|
38
|
+
const date = typeof value === "number" ? new Date(value) : new Date(value);
|
|
39
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
40
|
+
return date.toLocaleString();
|
|
41
|
+
}
|
|
42
|
+
function buildSshCommand(hostIpv4, mappedPorts2) {
|
|
43
|
+
if (!hostIpv4) return null;
|
|
44
|
+
const sshPort = mappedPorts2.find((entry) => entry.label.startsWith("22/"))?.hostPort ?? 22;
|
|
45
|
+
return `ssh root@${hostIpv4}${sshPort && sshPort !== 22 ? ` -p ${sshPort}` : ""}`;
|
|
46
|
+
}
|
|
47
|
+
function joinMappedPorts(mappedPorts2) {
|
|
48
|
+
if (mappedPorts2.length === 0) return "-";
|
|
49
|
+
return mappedPorts2.map((entry) => `${entry.label}->${entry.hostPort ?? "?"}`).join(" \xB7 ");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ../browser/src/http.ts
|
|
53
|
+
async function fetchWithTimeout(input, init = {}, timeoutMs = 15e3) {
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const timeout = globalThis.setTimeout(() => controller.abort(), timeoutMs);
|
|
56
|
+
try {
|
|
57
|
+
if (input instanceof URL) {
|
|
58
|
+
const url = new URL(input.toString());
|
|
59
|
+
url.searchParams.set("_ts", String(Date.now()));
|
|
60
|
+
return await fetch(url, {
|
|
61
|
+
...init,
|
|
62
|
+
cache: init.cache ?? "no-store",
|
|
63
|
+
signal: init.signal ?? controller.signal
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (typeof input === "string") {
|
|
67
|
+
let requestInput = input;
|
|
68
|
+
try {
|
|
69
|
+
const url = new URL(input, globalThis.location?.href);
|
|
70
|
+
url.searchParams.set("_ts", String(Date.now()));
|
|
71
|
+
requestInput = url;
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
return await fetch(requestInput, {
|
|
75
|
+
...init,
|
|
76
|
+
cache: init.cache ?? "no-store",
|
|
77
|
+
signal: init.signal ?? controller.signal
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return await fetch(input, {
|
|
81
|
+
...init,
|
|
82
|
+
cache: init.cache ?? "no-store",
|
|
83
|
+
signal: init.signal ?? controller.signal
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
87
|
+
throw new Error(`Request timed out after ${timeoutMs / 1e3}s.`);
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
} finally {
|
|
91
|
+
globalThis.clearTimeout(timeout);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ../browser/src/aleph-api.ts
|
|
96
|
+
var DEFAULT_ALEPH_API_HOST = "https://api2.aleph.im";
|
|
97
|
+
var DEFAULT_CRN_LIST_URL = "https://crns-list.aleph.sh/crns.json";
|
|
98
|
+
var DEFAULT_ALEPH_SCHEDULER_API_HOST = "https://scheduler.api.aleph.cloud";
|
|
99
|
+
var DEFAULT_2N6_API_HOST = "https://api.2n6.me";
|
|
100
|
+
function normalizeMessageStatus(status) {
|
|
101
|
+
if (typeof status !== "string") return "unknown";
|
|
102
|
+
const normalized = status.toLowerCase();
|
|
103
|
+
if (normalized === "processed" || normalized === "pending" || normalized === "rejected") {
|
|
104
|
+
return normalized;
|
|
105
|
+
}
|
|
106
|
+
return "unknown";
|
|
107
|
+
}
|
|
108
|
+
function asString(value) {
|
|
109
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
110
|
+
}
|
|
111
|
+
function asNumber(value) {
|
|
112
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
113
|
+
}
|
|
114
|
+
function isUnconfirmedNetworkError(error) {
|
|
115
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
116
|
+
return error instanceof TypeError || message.includes("Failed to fetch") || message.includes("Request timed out");
|
|
117
|
+
}
|
|
118
|
+
function normalizeProxyUrl(value) {
|
|
119
|
+
const stringValue = asString(value);
|
|
120
|
+
if (!stringValue) return null;
|
|
121
|
+
if (/^https?:\/\//i.test(stringValue)) return stringValue;
|
|
122
|
+
return `https://${stringValue}`;
|
|
123
|
+
}
|
|
124
|
+
function extractProxyUrl(value, networking) {
|
|
125
|
+
const networkingCandidates = [
|
|
126
|
+
networking?.proxy_url,
|
|
127
|
+
networking?.proxyUrl,
|
|
128
|
+
networking?.web_access_url,
|
|
129
|
+
networking?.webAccessUrl,
|
|
130
|
+
networking?.proxy_hostname,
|
|
131
|
+
networking?.proxyHostname,
|
|
132
|
+
networking?.domain,
|
|
133
|
+
networking?.hostname
|
|
134
|
+
];
|
|
135
|
+
for (const candidate of networkingCandidates) {
|
|
136
|
+
const normalized = normalizeProxyUrl(candidate);
|
|
137
|
+
if (normalized) return normalized;
|
|
138
|
+
}
|
|
139
|
+
for (const entry of [value.web_access, value.webAccess]) {
|
|
140
|
+
const normalized = normalizeProxyUrl(entry?.url) ?? normalizeProxyUrl(entry?.proxy_url) ?? normalizeProxyUrl(entry?.hostname) ?? normalizeProxyUrl(entry?.domain);
|
|
141
|
+
if (normalized) return normalized;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
async function fetchBalance(address, apiHost = DEFAULT_ALEPH_API_HOST) {
|
|
146
|
+
const response = await fetchWithTimeout(`${apiHost}/api/v0/addresses/${address}/balance`, {
|
|
147
|
+
cache: "no-cache"
|
|
148
|
+
});
|
|
149
|
+
if (!response.ok) throw new Error(`Balance request failed: ${response.status}`);
|
|
150
|
+
return await response.json();
|
|
151
|
+
}
|
|
152
|
+
async function fetchCrns(url = DEFAULT_CRN_LIST_URL) {
|
|
153
|
+
const requestUrl = new URL(url);
|
|
154
|
+
requestUrl.searchParams.set("filter_inactive", "true");
|
|
155
|
+
const response = await fetchWithTimeout(requestUrl, { cache: "no-cache" });
|
|
156
|
+
if (!response.ok) throw new Error(`CRN list request failed: ${response.status}`);
|
|
157
|
+
const payload = await response.json();
|
|
158
|
+
return payload.crns ?? [];
|
|
159
|
+
}
|
|
160
|
+
async function fetchInstances(address, apiHost = DEFAULT_ALEPH_API_HOST) {
|
|
161
|
+
const url = new URL("/api/v0/messages.json", apiHost);
|
|
162
|
+
url.searchParams.set("msgTypes", "INSTANCE");
|
|
163
|
+
url.searchParams.set("addresses", address);
|
|
164
|
+
url.searchParams.set("message_statuses", "processed,pending,rejected,removing");
|
|
165
|
+
url.searchParams.set("pagination", "100");
|
|
166
|
+
url.searchParams.set("page", "1");
|
|
167
|
+
url.searchParams.set("sortOrder", "-1");
|
|
168
|
+
const response = await fetchWithTimeout(url, { cache: "no-cache" });
|
|
169
|
+
if (!response.ok) throw new Error(`Instance list request failed: ${response.status}`);
|
|
170
|
+
const payload = await response.json();
|
|
171
|
+
return (payload.messages ?? []).map((message) => ({
|
|
172
|
+
...message,
|
|
173
|
+
status: typeof message.status === "string" && message.status.trim() ? message.status : message.confirmed ? "processed" : message.status
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
async function fetch2n6WebAccessUrl(itemHash, twoN6ApiHost = DEFAULT_2N6_API_HOST) {
|
|
177
|
+
const requestUrl = new URL(`/api/hash/${itemHash}`, twoN6ApiHost).toString();
|
|
178
|
+
try {
|
|
179
|
+
const response = await fetchWithTimeout(requestUrl, { cache: "no-cache" });
|
|
180
|
+
if (response.status === 404 || !response.ok) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const payload = await response.json();
|
|
184
|
+
return normalizeProxyUrl(payload.url ?? payload.subdomain);
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function fetchSchedulerAllocation(itemHash, schedulerApiHost = DEFAULT_ALEPH_SCHEDULER_API_HOST) {
|
|
190
|
+
const response = await fetchWithTimeout(`${schedulerApiHost}/api/v0/allocation/${itemHash}`, {
|
|
191
|
+
cache: "no-cache"
|
|
192
|
+
});
|
|
193
|
+
if (response.status === 404) return null;
|
|
194
|
+
if (!response.ok) throw new Error(`Scheduler allocation request failed: ${response.status}`);
|
|
195
|
+
const payload = await response.json();
|
|
196
|
+
const node = payload.node;
|
|
197
|
+
return {
|
|
198
|
+
source: "scheduler",
|
|
199
|
+
crnUrl: asString(node?.url),
|
|
200
|
+
node: node ? {
|
|
201
|
+
node_id: asString(node.node_id) ?? void 0,
|
|
202
|
+
url: asString(node.url) ?? void 0,
|
|
203
|
+
ipv6: asString(node.ipv6),
|
|
204
|
+
supports_ipv6: typeof node.supports_ipv6 === "boolean" ? node.supports_ipv6 : void 0
|
|
205
|
+
} : null,
|
|
206
|
+
vmIpv6: asString(payload.vm_ipv6),
|
|
207
|
+
period: payload.period ? {
|
|
208
|
+
start_timestamp: asString(payload.period.start_timestamp) ?? void 0,
|
|
209
|
+
duration_seconds: asNumber(payload.period.duration_seconds) ?? void 0
|
|
210
|
+
} : null
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async function fetchCrnExecutionMap(crnUrl) {
|
|
214
|
+
const normalizedCrnUrl = crnUrl.replace(/\/+$/, "");
|
|
215
|
+
const v2Url = `${normalizedCrnUrl}/v2/about/executions/list`;
|
|
216
|
+
const v1Url = `${normalizedCrnUrl}/about/executions/list`;
|
|
217
|
+
try {
|
|
218
|
+
const v2Response = await fetchWithTimeout(v2Url, { cache: "no-cache" });
|
|
219
|
+
if (v2Response.ok) {
|
|
220
|
+
const payload = await v2Response.json();
|
|
221
|
+
return {
|
|
222
|
+
payload,
|
|
223
|
+
blocked: false,
|
|
224
|
+
requestUrl: v2Url,
|
|
225
|
+
version: "v2"
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (v2Response.status !== 404) {
|
|
229
|
+
return { payload: null, blocked: false, requestUrl: v2Url, version: "v2" };
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
233
|
+
if (error instanceof TypeError || message.includes("Failed to fetch")) {
|
|
234
|
+
return { payload: null, blocked: true, requestUrl: v2Url, version: "v2" };
|
|
235
|
+
}
|
|
236
|
+
return { payload: null, blocked: false, requestUrl: v2Url, version: "v2" };
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const v1Response = await fetchWithTimeout(v1Url, { cache: "no-cache" });
|
|
240
|
+
if (!v1Response.ok) {
|
|
241
|
+
return { payload: null, blocked: false, requestUrl: v1Url, version: "v1" };
|
|
242
|
+
}
|
|
243
|
+
const payload = await v1Response.json();
|
|
244
|
+
return {
|
|
245
|
+
payload,
|
|
246
|
+
blocked: false,
|
|
247
|
+
requestUrl: v1Url,
|
|
248
|
+
version: "v1"
|
|
249
|
+
};
|
|
250
|
+
} catch (error) {
|
|
251
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
252
|
+
if (error instanceof TypeError || message.includes("Failed to fetch")) {
|
|
253
|
+
return { payload: null, blocked: true, requestUrl: v1Url, version: "v1" };
|
|
254
|
+
}
|
|
255
|
+
return { payload: null, blocked: false, requestUrl: v1Url, version: "v1" };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function normalizeExecution(item, crnUrl) {
|
|
259
|
+
const networking = item.networking ?? null;
|
|
260
|
+
const mappedPorts2 = networking && "mapped_ports" in networking && networking.mapped_ports && typeof networking.mapped_ports === "object" ? Object.fromEntries(
|
|
261
|
+
Object.entries(networking.mapped_ports).map(([port, mapping]) => [
|
|
262
|
+
port,
|
|
263
|
+
{
|
|
264
|
+
host: asNumber(mapping?.host) ?? void 0,
|
|
265
|
+
tcp: typeof mapping?.tcp === "boolean" ? mapping.tcp : void 0,
|
|
266
|
+
udp: typeof mapping?.udp === "boolean" ? mapping.udp : void 0
|
|
267
|
+
}
|
|
268
|
+
])
|
|
269
|
+
) : void 0;
|
|
270
|
+
if (networking && ("host_ipv4" in networking || "ipv6_ip" in networking || "ipv4_network" in networking)) {
|
|
271
|
+
const v2Item = item;
|
|
272
|
+
return {
|
|
273
|
+
crnUrl,
|
|
274
|
+
version: "v2",
|
|
275
|
+
running: typeof v2Item.running === "boolean" ? v2Item.running : void 0,
|
|
276
|
+
networking: {
|
|
277
|
+
ipv4_network: asString(networking.ipv4_network),
|
|
278
|
+
host_ipv4: asString(networking.host_ipv4),
|
|
279
|
+
ipv6_network: asString(networking.ipv6_network),
|
|
280
|
+
ipv6_ip: asString(networking.ipv6_ip),
|
|
281
|
+
ipv4_ip: asString(networking.ipv4_ip),
|
|
282
|
+
proxy_url: extractProxyUrl(v2Item, networking),
|
|
283
|
+
mapped_ports: mappedPorts2
|
|
284
|
+
},
|
|
285
|
+
status: v2Item.status ? {
|
|
286
|
+
defined_at: asString(v2Item.status.defined_at),
|
|
287
|
+
preparing_at: asString(v2Item.status.preparing_at),
|
|
288
|
+
prepared_at: asString(v2Item.status.prepared_at),
|
|
289
|
+
starting_at: asString(v2Item.status.starting_at),
|
|
290
|
+
started_at: asString(v2Item.status.started_at),
|
|
291
|
+
stopping_at: asString(v2Item.status.stopping_at),
|
|
292
|
+
stopped_at: asString(v2Item.status.stopped_at)
|
|
293
|
+
} : null
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
crnUrl,
|
|
298
|
+
version: "v1",
|
|
299
|
+
networking: {
|
|
300
|
+
ipv4: networking && "ipv4" in networking ? asString(networking.ipv4) : null,
|
|
301
|
+
ipv6: networking && "ipv6" in networking ? asString(networking.ipv6) : null
|
|
302
|
+
},
|
|
303
|
+
status: null
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
async function notifyCrnAllocation(crnUrl, itemHash) {
|
|
307
|
+
const normalizedCrnUrl = crnUrl.replace(/\/+$/, "");
|
|
308
|
+
try {
|
|
309
|
+
const response = await fetchWithTimeout(`${normalizedCrnUrl}/control/allocation/notify`, {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers: {
|
|
312
|
+
"content-type": "text/plain;charset=UTF-8"
|
|
313
|
+
},
|
|
314
|
+
body: JSON.stringify({ instance: itemHash }),
|
|
315
|
+
mode: "cors"
|
|
316
|
+
});
|
|
317
|
+
if (!response.ok) {
|
|
318
|
+
const responseText = await response.text().catch(() => "");
|
|
319
|
+
throw new Error(`CRN allocation notify failed: ${response.status}${responseText ? ` ${responseText}` : ""}`);
|
|
320
|
+
}
|
|
321
|
+
return { status: "confirmed" };
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (isUnconfirmedNetworkError(error)) {
|
|
324
|
+
return { status: "unconfirmed" };
|
|
325
|
+
}
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async function configureOrbitdbRelaySetup(args) {
|
|
330
|
+
const targetUrl = `http://${args.hostIpv4}:${args.setupPort}/configure`;
|
|
331
|
+
try {
|
|
332
|
+
const response = await fetchWithTimeout(
|
|
333
|
+
targetUrl,
|
|
334
|
+
{
|
|
335
|
+
method: "POST",
|
|
336
|
+
headers: {
|
|
337
|
+
"content-type": "text/plain;charset=UTF-8"
|
|
338
|
+
},
|
|
339
|
+
body: JSON.stringify({
|
|
340
|
+
public_ipv4: args.hostIpv4,
|
|
341
|
+
public_ipv6: args.publicIpv6 ?? void 0,
|
|
342
|
+
tcp_port: args.tcpPort,
|
|
343
|
+
ws_port: args.wsPort,
|
|
344
|
+
proxy_url: args.proxyUrl ?? void 0,
|
|
345
|
+
metrics_port: args.metricsPort ?? void 0,
|
|
346
|
+
metrics_https_port: args.metricsHttpsPort ?? void 0,
|
|
347
|
+
webrtc_port: args.webrtcPort ?? void 0,
|
|
348
|
+
quic_port: args.quicPort ?? void 0
|
|
349
|
+
}),
|
|
350
|
+
mode: "cors"
|
|
351
|
+
},
|
|
352
|
+
3e4
|
|
353
|
+
);
|
|
354
|
+
if (!response.ok) {
|
|
355
|
+
const responseText = await response.text().catch(() => "");
|
|
356
|
+
throw new Error(`Relay setup request failed: ${response.status}${responseText ? ` ${responseText}` : ""}`);
|
|
357
|
+
}
|
|
358
|
+
return { status: "configured" };
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (isUnconfirmedNetworkError(error)) {
|
|
361
|
+
return { status: "unconfirmed" };
|
|
362
|
+
}
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function messageTypeFromEnvelope(payload) {
|
|
367
|
+
if (!payload) return null;
|
|
368
|
+
const type = payload.type ?? payload.message?.type ?? (Array.isArray(payload.messages) ? payload.messages[0]?.type : void 0);
|
|
369
|
+
return typeof type === "string" ? type.toUpperCase() : null;
|
|
370
|
+
}
|
|
371
|
+
function extractReferenceHashes(details) {
|
|
372
|
+
if (!details || typeof details !== "object" || !("errors" in details)) return [];
|
|
373
|
+
const errors = details.errors;
|
|
374
|
+
if (!Array.isArray(errors)) return [];
|
|
375
|
+
return errors.filter((value) => typeof value === "string");
|
|
376
|
+
}
|
|
377
|
+
function describeRejectedDeployment(payload, references, rootfsRef) {
|
|
378
|
+
const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
|
|
379
|
+
const pendingReferences = references.filter((reference) => reference.status === "pending");
|
|
380
|
+
const missingReferences = references.filter((reference) => reference.status === "missing");
|
|
381
|
+
const rootfsReference = references.find((reference) => reference.itemHash === rootfsRef);
|
|
382
|
+
if (rootfsReference?.status === "pending") {
|
|
383
|
+
return `Aleph rejected this deployment because the referenced rootfs STORE message ${rootfsReference.itemHash} is still pending and cannot yet be used by an instance. Wait for that STORE message to process, then deploy again.`;
|
|
384
|
+
}
|
|
385
|
+
if (pendingReferences.length > 0) {
|
|
386
|
+
return `Aleph rejected this deployment because referenced message(s) are still pending: ${pendingReferences.map((reference) => reference.itemHash).join(", ")}.`;
|
|
387
|
+
}
|
|
388
|
+
if (missingReferences.length > 0) {
|
|
389
|
+
return `Aleph rejected this deployment because referenced message(s) were not found on Aleph: ${missingReferences.map((reference) => reference.itemHash).join(", ")}.`;
|
|
390
|
+
}
|
|
391
|
+
const referencedHashes = extractReferenceHashes(payload.details);
|
|
392
|
+
if (referencedHashes.length > 0) {
|
|
393
|
+
return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}. Referenced message(s): ${referencedHashes.join(", ")}.`;
|
|
394
|
+
}
|
|
395
|
+
return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}.`;
|
|
396
|
+
}
|
|
397
|
+
async function fetchMessageEnvelope(itemHash, apiHost = DEFAULT_ALEPH_API_HOST) {
|
|
398
|
+
const response = await fetchWithTimeout(`${apiHost}/api/v0/messages/${itemHash}`, {
|
|
399
|
+
cache: "no-cache"
|
|
400
|
+
});
|
|
401
|
+
if (response.status === 404) return null;
|
|
402
|
+
if (!response.ok) throw new Error(`Message lookup failed: ${response.status}`);
|
|
403
|
+
return await response.json();
|
|
404
|
+
}
|
|
405
|
+
async function fetchReference(itemHash, apiHost) {
|
|
406
|
+
const payload = await fetchMessageEnvelope(itemHash, apiHost);
|
|
407
|
+
if (!payload) {
|
|
408
|
+
return {
|
|
409
|
+
itemHash,
|
|
410
|
+
status: "missing",
|
|
411
|
+
type: null
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
itemHash,
|
|
416
|
+
status: normalizeMessageStatus(payload.status),
|
|
417
|
+
type: messageTypeFromEnvelope(payload)
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
async function inspectDeploymentResult(itemHash, rootfsRef, apiHost = DEFAULT_ALEPH_API_HOST) {
|
|
421
|
+
const payload = await fetchMessageEnvelope(itemHash, apiHost);
|
|
422
|
+
if (!payload) {
|
|
423
|
+
return {
|
|
424
|
+
status: "unknown",
|
|
425
|
+
errorCode: null,
|
|
426
|
+
details: null,
|
|
427
|
+
rejectionReason: `Deployment message ${itemHash} was not found on Aleph.`,
|
|
428
|
+
references: []
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
const relatedHashes = new Set(rootfsRef ? [rootfsRef] : []);
|
|
432
|
+
for (const referenceHash of extractReferenceHashes(payload.details)) {
|
|
433
|
+
relatedHashes.add(referenceHash);
|
|
434
|
+
}
|
|
435
|
+
const references = await Promise.all(Array.from(relatedHashes).map((hash) => fetchReference(hash, apiHost)));
|
|
436
|
+
const status = normalizeMessageStatus(payload.status);
|
|
437
|
+
const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
|
|
438
|
+
const details = payload.details && typeof payload.details === "object" ? payload.details : null;
|
|
439
|
+
return {
|
|
440
|
+
status,
|
|
441
|
+
errorCode,
|
|
442
|
+
details,
|
|
443
|
+
rejectionReason: status === "rejected" ? describeRejectedDeployment(payload, references, rootfsRef) : null,
|
|
444
|
+
references
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
async function waitForDeploymentResult(itemHash, rootfsRef, apiHost = DEFAULT_ALEPH_API_HOST, attempts = 15, delayMs = 2e3) {
|
|
448
|
+
let lastResult = await inspectDeploymentResult(itemHash, rootfsRef, apiHost);
|
|
449
|
+
for (let attempt = 1; attempt < attempts; attempt += 1) {
|
|
450
|
+
if (lastResult.status === "processed" || lastResult.status === "rejected") {
|
|
451
|
+
return lastResult;
|
|
452
|
+
}
|
|
453
|
+
await new Promise((resolve) => globalThis.setTimeout(resolve, delayMs));
|
|
454
|
+
lastResult = await inspectDeploymentResult(itemHash, rootfsRef, apiHost);
|
|
455
|
+
}
|
|
456
|
+
return lastResult;
|
|
457
|
+
}
|
|
458
|
+
function isInvalidMessageFormatResponse(response, payload) {
|
|
459
|
+
if (response.status !== 422) return false;
|
|
460
|
+
const details = payload.details;
|
|
461
|
+
if (typeof details === "string" && details.includes("InvalidMessageFormat")) return true;
|
|
462
|
+
if (details && typeof details === "object") {
|
|
463
|
+
const detailMessage = details.message;
|
|
464
|
+
if (typeof detailMessage === "string" && detailMessage.includes("InvalidMessageFormat")) return true;
|
|
465
|
+
}
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
async function postBroadcastPayload(body, apiHost) {
|
|
469
|
+
const rawResponse = await fetchWithTimeout(`${apiHost}/api/v0/messages`, {
|
|
470
|
+
method: "POST",
|
|
471
|
+
headers: { "content-type": "application/json" },
|
|
472
|
+
body: JSON.stringify(body)
|
|
473
|
+
});
|
|
474
|
+
const response = await rawResponse.json().catch(() => ({}));
|
|
475
|
+
return {
|
|
476
|
+
response,
|
|
477
|
+
httpStatus: rawResponse.status,
|
|
478
|
+
rawResponse
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
async function broadcastInstanceMessage(message, apiHost = DEFAULT_ALEPH_API_HOST, sync = false) {
|
|
482
|
+
const attempts = [{ sync, message }, { ...message, sync }, { ...message }];
|
|
483
|
+
for (let index = 0; index < attempts.length; index += 1) {
|
|
484
|
+
const result = await postBroadcastPayload(attempts[index], apiHost);
|
|
485
|
+
if (result.rawResponse.ok || result.httpStatus === 202) {
|
|
486
|
+
return {
|
|
487
|
+
response: result.response,
|
|
488
|
+
httpStatus: result.httpStatus
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const canRetry = index < attempts.length - 1 && isInvalidMessageFormatResponse(result.rawResponse, result.response);
|
|
492
|
+
if (!canRetry) {
|
|
493
|
+
throw new Error(`Broadcast failed: ${result.httpStatus} ${JSON.stringify(result.response)}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
throw new Error("Broadcast failed: no compatible request format was accepted");
|
|
497
|
+
}
|
|
498
|
+
async function broadcastAlephMessage(message, apiHost = DEFAULT_ALEPH_API_HOST, sync = false) {
|
|
499
|
+
return broadcastInstanceMessage(message, apiHost, sync);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ../browser/src/rootfs.ts
|
|
503
|
+
var ITEM_HASH_RE = /^[a-fA-F0-9]{64}$/u;
|
|
504
|
+
var DEFAULT_ROOTFS_MANIFEST_URL = "./rootfs-manifest.json";
|
|
505
|
+
var DEFAULT_IPFS_GATEWAY_BASE_URL = "https://ipfs.aleph.cloud/ipfs/";
|
|
506
|
+
function resolveManifestUrl(input, baseUrl) {
|
|
507
|
+
if (input instanceof URL) return input;
|
|
508
|
+
try {
|
|
509
|
+
return new URL(input, baseUrl ? String(baseUrl) : globalThis.location?.href);
|
|
510
|
+
} catch {
|
|
511
|
+
return input;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function validateRootfsManifest(manifest) {
|
|
515
|
+
const errors = [];
|
|
516
|
+
if (!manifest) {
|
|
517
|
+
return { manifest, valid: false, errors: ["Rootfs manifest is missing."] };
|
|
518
|
+
}
|
|
519
|
+
if (!manifest.version) errors.push("Rootfs manifest version is missing.");
|
|
520
|
+
if (manifest.rootfsInstallStrategy != null && manifest.rootfsInstallStrategy !== "thin" && manifest.rootfsInstallStrategy !== "prebaked") {
|
|
521
|
+
errors.push('Rootfs install strategy must be "thin" or "prebaked" when provided.');
|
|
522
|
+
}
|
|
523
|
+
if (manifest.requiresBootstrapNetwork != null && typeof manifest.requiresBootstrapNetwork !== "boolean") {
|
|
524
|
+
errors.push("Rootfs bootstrap network flag must be a boolean when provided.");
|
|
525
|
+
}
|
|
526
|
+
if (manifest.bootstrapSummary != null && !manifest.bootstrapSummary.trim()) {
|
|
527
|
+
errors.push("Rootfs bootstrap summary must be non-empty when provided.");
|
|
528
|
+
}
|
|
529
|
+
if (manifest.requiredPortForwards != null) {
|
|
530
|
+
if (!Array.isArray(manifest.requiredPortForwards)) {
|
|
531
|
+
errors.push("Rootfs required port forwards must be an array when provided.");
|
|
532
|
+
} else {
|
|
533
|
+
manifest.requiredPortForwards.forEach((entry, index) => {
|
|
534
|
+
if (!entry || typeof entry !== "object") {
|
|
535
|
+
errors.push(`Rootfs required port forward #${index + 1} must be an object.`);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (!Number.isInteger(entry.port) || entry.port < 1 || entry.port > 65535) {
|
|
539
|
+
errors.push(`Rootfs required port forward #${index + 1} must use a TCP/UDP port between 1 and 65535.`);
|
|
540
|
+
}
|
|
541
|
+
if (entry.tcp !== true && entry.udp !== true) {
|
|
542
|
+
errors.push(`Rootfs required port forward #${index + 1} must enable TCP or UDP.`);
|
|
543
|
+
}
|
|
544
|
+
if (entry.purpose != null && (typeof entry.purpose !== "string" || !entry.purpose.trim())) {
|
|
545
|
+
errors.push(`Rootfs required port forward #${index + 1} purpose must be non-empty when provided.`);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (!ITEM_HASH_RE.test(manifest.rootfsItemHash || "")) {
|
|
551
|
+
errors.push("Rootfs ItemHash must be a 64 character hex value.");
|
|
552
|
+
}
|
|
553
|
+
if (!Number.isInteger(manifest.rootfsSizeMiB) || manifest.rootfsSizeMiB <= 0) {
|
|
554
|
+
errors.push("Rootfs size must be a positive MiB integer.");
|
|
555
|
+
}
|
|
556
|
+
if (manifest.rootfsSourceSizeBytes != null && (!Number.isInteger(manifest.rootfsSourceSizeBytes) || manifest.rootfsSourceSizeBytes <= 0)) {
|
|
557
|
+
errors.push("Rootfs source size must be a positive byte integer when provided.");
|
|
558
|
+
}
|
|
559
|
+
if (!manifest.createdAt || Number.isNaN(new Date(manifest.createdAt).getTime())) {
|
|
560
|
+
errors.push("Rootfs creation date is missing or invalid.");
|
|
561
|
+
}
|
|
562
|
+
return { manifest, valid: errors.length === 0, errors };
|
|
563
|
+
}
|
|
564
|
+
async function loadRootfsManifest(url = DEFAULT_ROOTFS_MANIFEST_URL, options = {}) {
|
|
565
|
+
const response = await fetchWithTimeout(resolveManifestUrl(url, options.baseUrl), { cache: "no-cache" });
|
|
566
|
+
if (!response.ok) {
|
|
567
|
+
throw new Error(`Rootfs manifest request failed: ${response.status}`);
|
|
568
|
+
}
|
|
569
|
+
return validateRootfsManifest(await response.json());
|
|
570
|
+
}
|
|
571
|
+
async function verifyRootfsExists(itemHash, apiHost = DEFAULT_ALEPH_API_HOST) {
|
|
572
|
+
if (!ITEM_HASH_RE.test(itemHash)) return false;
|
|
573
|
+
const response = await fetchWithTimeout(`${apiHost}/api/v0/messages/${itemHash}`, {
|
|
574
|
+
method: "GET",
|
|
575
|
+
cache: "no-cache"
|
|
576
|
+
});
|
|
577
|
+
if (response.status === 404) return false;
|
|
578
|
+
if (!response.ok) throw new Error(`Rootfs lookup failed: ${response.status}`);
|
|
579
|
+
const payload = await response.json();
|
|
580
|
+
const firstMessage = Array.isArray(payload.messages) ? payload.messages[0] : void 0;
|
|
581
|
+
const type = String(payload.type || payload.message?.type || firstMessage?.type || "").toUpperCase();
|
|
582
|
+
return type === "STORE";
|
|
583
|
+
}
|
|
584
|
+
function normalizeStatus(status) {
|
|
585
|
+
if (typeof status !== "string") return "unknown";
|
|
586
|
+
const normalized = status.toLowerCase();
|
|
587
|
+
if (normalized === "processed" || normalized === "pending" || normalized === "rejected") {
|
|
588
|
+
return normalized;
|
|
589
|
+
}
|
|
590
|
+
return "unknown";
|
|
591
|
+
}
|
|
592
|
+
function parseCidFromPayload(payload) {
|
|
593
|
+
const firstMessage = Array.isArray(payload.messages) && payload.messages[0] && typeof payload.messages[0] === "object" ? payload.messages[0] : null;
|
|
594
|
+
const directContent = firstMessage?.content && typeof firstMessage.content === "object" ? firstMessage.content : null;
|
|
595
|
+
if (typeof directContent?.item_hash === "string") {
|
|
596
|
+
return directContent.item_hash;
|
|
597
|
+
}
|
|
598
|
+
if (typeof firstMessage?.item_content === "string") {
|
|
599
|
+
try {
|
|
600
|
+
const itemContent = JSON.parse(firstMessage.item_content);
|
|
601
|
+
if (typeof itemContent.item_hash === "string") {
|
|
602
|
+
return itemContent.item_hash;
|
|
603
|
+
}
|
|
604
|
+
} catch {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
function parseRejectionReason(payload) {
|
|
611
|
+
const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
|
|
612
|
+
const details = payload.details && typeof payload.details === "object" ? payload.details : null;
|
|
613
|
+
const rawErrors = Array.isArray(details?.errors) ? details.errors : [];
|
|
614
|
+
const firstError = rawErrors[0] && typeof rawErrors[0] === "object" ? rawErrors[0] : null;
|
|
615
|
+
if (firstError) {
|
|
616
|
+
const accountBalance = Number(firstError.account_balance);
|
|
617
|
+
const requiredBalance = Number(firstError.required_balance);
|
|
618
|
+
if (Number.isFinite(accountBalance) && Number.isFinite(requiredBalance)) {
|
|
619
|
+
const shortfall = requiredBalance - accountBalance;
|
|
620
|
+
return {
|
|
621
|
+
rejectionErrorCode: errorCode,
|
|
622
|
+
rejectionReason: shortfall > 0 ? `Rejected by Aleph for insufficient hold balance: ${accountBalance.toFixed(3)} available, ${requiredBalance.toFixed(3)} required, ${shortfall.toFixed(3)} short.` : `Rejected by Aleph for insufficient hold balance: ${accountBalance.toFixed(3)} available, ${requiredBalance.toFixed(3)} required.`
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
rejectionErrorCode: errorCode,
|
|
628
|
+
rejectionReason: errorCode != null ? `Rejected by Aleph (error code ${errorCode}).` : null
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
async function probeGateway(cid, gatewayBaseUrl = DEFAULT_IPFS_GATEWAY_BASE_URL) {
|
|
632
|
+
const gatewayUrl = new URL(cid, gatewayBaseUrl).toString();
|
|
633
|
+
try {
|
|
634
|
+
const response = await fetchWithTimeout(gatewayUrl, { method: "HEAD", cache: "no-store" }, 5e3);
|
|
635
|
+
return {
|
|
636
|
+
gatewayUrl,
|
|
637
|
+
gatewayStatus: response.ok ? "reachable" : "error",
|
|
638
|
+
gatewayError: response.ok ? null : `Gateway responded with ${response.status}.`
|
|
639
|
+
};
|
|
640
|
+
} catch (error) {
|
|
641
|
+
if (error instanceof Error && error.message.includes("timed out")) {
|
|
642
|
+
return {
|
|
643
|
+
gatewayUrl,
|
|
644
|
+
gatewayStatus: "timeout",
|
|
645
|
+
gatewayError: error.message
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
gatewayUrl,
|
|
650
|
+
gatewayStatus: "unavailable",
|
|
651
|
+
gatewayError: error instanceof Error ? error.message : String(error)
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async function resolveRootfsReference(itemHash, apiHost = DEFAULT_ALEPH_API_HOST, gatewayBaseUrl = DEFAULT_IPFS_GATEWAY_BASE_URL) {
|
|
656
|
+
if (!ITEM_HASH_RE.test(itemHash)) return null;
|
|
657
|
+
const response = await fetchWithTimeout(`${apiHost}/api/v0/messages/${itemHash}`, {
|
|
658
|
+
method: "GET",
|
|
659
|
+
cache: "no-cache"
|
|
660
|
+
});
|
|
661
|
+
if (response.status === 404) return null;
|
|
662
|
+
if (!response.ok) throw new Error(`Rootfs lookup failed: ${response.status}`);
|
|
663
|
+
const payload = await response.json();
|
|
664
|
+
const firstMessage = Array.isArray(payload.messages) && payload.messages[0] && typeof payload.messages[0] === "object" ? payload.messages[0] : null;
|
|
665
|
+
const messageObject = payload.message && typeof payload.message === "object" ? payload.message : null;
|
|
666
|
+
const cid = parseCidFromPayload(payload);
|
|
667
|
+
const rejection = normalizeStatus(payload.status) === "rejected" ? parseRejectionReason(payload) : { rejectionErrorCode: null, rejectionReason: null };
|
|
668
|
+
const gateway = cid ? await probeGateway(cid, gatewayBaseUrl) : { gatewayUrl: null, gatewayStatus: "unknown", gatewayError: null };
|
|
669
|
+
return {
|
|
670
|
+
itemHash,
|
|
671
|
+
messageStatus: normalizeStatus(payload.status),
|
|
672
|
+
messageType: String(payload.type || messageObject?.type || firstMessage?.type || "").toUpperCase() || null,
|
|
673
|
+
cid,
|
|
674
|
+
receptionTime: typeof payload.reception_time === "string" ? payload.reception_time : null,
|
|
675
|
+
rejectionErrorCode: rejection.rejectionErrorCode,
|
|
676
|
+
rejectionReason: rejection.rejectionReason,
|
|
677
|
+
gatewayUrl: gateway.gatewayUrl,
|
|
678
|
+
gatewayStatus: gateway.gatewayStatus,
|
|
679
|
+
gatewayError: gateway.gatewayError
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// src/shared/manifest-source.ts
|
|
684
|
+
function resolveManifestSource(args) {
|
|
685
|
+
const trimmed = args.manifestJson.trim();
|
|
686
|
+
if (!trimmed) return null;
|
|
687
|
+
try {
|
|
688
|
+
return validateRootfsManifest(JSON.parse(trimmed));
|
|
689
|
+
} catch (error) {
|
|
690
|
+
return {
|
|
691
|
+
manifest: null,
|
|
692
|
+
valid: false,
|
|
693
|
+
errors: [error instanceof Error ? error.message : String(error)]
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/shared/wallet-controller.ts
|
|
699
|
+
function getEthereumProvider() {
|
|
700
|
+
return globalThis.window?.ethereum ?? null;
|
|
701
|
+
}
|
|
702
|
+
async function connectWallet(provider = getEthereumProvider()) {
|
|
703
|
+
if (!provider) {
|
|
704
|
+
throw new Error("MetaMask provider not found.");
|
|
705
|
+
}
|
|
706
|
+
const accounts = await provider.request({ method: "eth_requestAccounts" });
|
|
707
|
+
const chainId = await provider.request({ method: "eth_chainId" });
|
|
708
|
+
return {
|
|
709
|
+
connected: Boolean(accounts[0]),
|
|
710
|
+
address: accounts[0] ?? null,
|
|
711
|
+
chainId,
|
|
712
|
+
isMetaMask: Boolean(provider.isMetaMask)
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
async function personalSign(address, message, provider = getEthereumProvider()) {
|
|
716
|
+
if (!provider) {
|
|
717
|
+
throw new Error("MetaMask provider not found.");
|
|
718
|
+
}
|
|
719
|
+
const payload = `0x${Array.from(new TextEncoder().encode(message)).map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
720
|
+
return provider.request({
|
|
721
|
+
method: "personal_sign",
|
|
722
|
+
params: [payload, address]
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
function watchWallet(onChange, provider = getEthereumProvider()) {
|
|
726
|
+
if (!provider?.on || !provider?.removeListener) {
|
|
727
|
+
return () => {
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
provider.on("accountsChanged", onChange);
|
|
731
|
+
provider.on("chainChanged", onChange);
|
|
732
|
+
return () => {
|
|
733
|
+
provider.removeListener?.("accountsChanged", onChange);
|
|
734
|
+
provider.removeListener?.("chainChanged", onChange);
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// ../browser/src/client.ts
|
|
739
|
+
function createAlephBrowserClient(options = {}) {
|
|
740
|
+
const apiHost = options.apiHost ?? DEFAULT_ALEPH_API_HOST;
|
|
741
|
+
const crnListUrl = options.crnListUrl ?? DEFAULT_CRN_LIST_URL;
|
|
742
|
+
const schedulerApiHost = options.schedulerApiHost ?? DEFAULT_ALEPH_SCHEDULER_API_HOST;
|
|
743
|
+
const twoN6ApiHost = options.twoN6ApiHost ?? DEFAULT_2N6_API_HOST;
|
|
744
|
+
return {
|
|
745
|
+
apiHost,
|
|
746
|
+
crnListUrl,
|
|
747
|
+
schedulerApiHost,
|
|
748
|
+
fetchBalance(address) {
|
|
749
|
+
return fetchBalance(address, apiHost);
|
|
750
|
+
},
|
|
751
|
+
fetchCrns() {
|
|
752
|
+
return fetchCrns(crnListUrl);
|
|
753
|
+
},
|
|
754
|
+
fetchInstances(address) {
|
|
755
|
+
return fetchInstances(address, apiHost);
|
|
756
|
+
},
|
|
757
|
+
fetch2n6WebAccessUrl(itemHash) {
|
|
758
|
+
return fetch2n6WebAccessUrl(itemHash, twoN6ApiHost);
|
|
759
|
+
},
|
|
760
|
+
fetchMessageEnvelope(itemHash) {
|
|
761
|
+
return fetchMessageEnvelope(itemHash, apiHost);
|
|
762
|
+
},
|
|
763
|
+
fetchSchedulerAllocation(itemHash) {
|
|
764
|
+
return fetchSchedulerAllocation(itemHash, schedulerApiHost);
|
|
765
|
+
},
|
|
766
|
+
fetchCrnExecutionMap(crnUrl) {
|
|
767
|
+
return fetchCrnExecutionMap(crnUrl);
|
|
768
|
+
},
|
|
769
|
+
notifyCrnAllocation(crnUrl, itemHash) {
|
|
770
|
+
return notifyCrnAllocation(crnUrl, itemHash);
|
|
771
|
+
},
|
|
772
|
+
configureOrbitdbRelaySetup(args) {
|
|
773
|
+
return configureOrbitdbRelaySetup(args);
|
|
774
|
+
},
|
|
775
|
+
inspectDeploymentResult(itemHash, rootfsRef) {
|
|
776
|
+
return inspectDeploymentResult(itemHash, rootfsRef, apiHost);
|
|
777
|
+
},
|
|
778
|
+
waitForDeploymentResult(itemHash, rootfsRef, attempts, delayMs) {
|
|
779
|
+
return waitForDeploymentResult(itemHash, rootfsRef, apiHost, attempts, delayMs);
|
|
780
|
+
},
|
|
781
|
+
broadcastInstanceMessage(message, sync) {
|
|
782
|
+
return broadcastInstanceMessage(message, apiHost, sync);
|
|
783
|
+
},
|
|
784
|
+
broadcastAlephMessage(message, sync) {
|
|
785
|
+
return broadcastAlephMessage(message, apiHost, sync);
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// ../browser/src/pricing.ts
|
|
791
|
+
var DEFAULT_ALEPH_AGGREGATE_ADDRESS = "0xFba561a84A537fCaa567bb7A2257e7142701ae2A";
|
|
792
|
+
function parseInstancePricing(payload) {
|
|
793
|
+
const data = payload;
|
|
794
|
+
const pricing = data.data?.pricing ?? data.pricing;
|
|
795
|
+
const instance = pricing?.instance;
|
|
796
|
+
if (!instance?.price?.compute_unit || !instance.compute_unit || !Array.isArray(instance.tiers)) {
|
|
797
|
+
throw new Error("Aleph pricing aggregate does not contain instance pricing.");
|
|
798
|
+
}
|
|
799
|
+
return instance;
|
|
800
|
+
}
|
|
801
|
+
async function fetchInstancePricing(apiHost = DEFAULT_ALEPH_API_HOST, aggregateAddress = DEFAULT_ALEPH_AGGREGATE_ADDRESS) {
|
|
802
|
+
const response = await fetchWithTimeout(`${apiHost}/api/v0/aggregates/${aggregateAddress}.json?keys=pricing`, {
|
|
803
|
+
cache: "no-cache"
|
|
804
|
+
});
|
|
805
|
+
if (!response.ok) {
|
|
806
|
+
throw new Error(`Pricing aggregate request failed: ${response.status}`);
|
|
807
|
+
}
|
|
808
|
+
const payload = await response.json();
|
|
809
|
+
const pricingAggregate = payload.data?.pricing;
|
|
810
|
+
if (!pricingAggregate) {
|
|
811
|
+
throw new Error("Pricing aggregate response did not include a pricing key.");
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
pricing: parseInstancePricing({ pricing: pricingAggregate }),
|
|
815
|
+
fetchedAt: Date.now()
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// ../core/src/constants.ts
|
|
820
|
+
var DEFAULT_ALEPH_CHANNEL = "TEST";
|
|
821
|
+
|
|
822
|
+
// ../core/src/manifests.ts
|
|
823
|
+
var DEFAULT_ALEPH_API_HOST2 = "https://api2.aleph.im";
|
|
824
|
+
|
|
825
|
+
// ../core/src/crns.ts
|
|
826
|
+
function compatibleCrns(crns, excludedHashes = []) {
|
|
827
|
+
const excluded = new Set(excludedHashes.filter(Boolean));
|
|
828
|
+
return [...crns].filter((crn) => {
|
|
829
|
+
if (!crn?.hash || excluded.has(crn.hash)) return false;
|
|
830
|
+
if (crn.qemu_support === false) return false;
|
|
831
|
+
if (crn.system_usage?.active === false) return false;
|
|
832
|
+
return true;
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
function hasCrnCapacity(crn, spec) {
|
|
836
|
+
if (!spec) return true;
|
|
837
|
+
const usage = crn.system_usage;
|
|
838
|
+
if (!usage) return true;
|
|
839
|
+
const cpuOk = usage.cpu?.count == null || usage.cpu.count >= spec.vcpus;
|
|
840
|
+
const memoryOk = usage.mem?.available_kB == null || usage.mem.available_kB >= spec.memoryMiB * 1024;
|
|
841
|
+
const diskOk = usage.disk?.available_kB == null || usage.disk.available_kB >= spec.diskMiB * 1024;
|
|
842
|
+
return cpuOk && memoryOk && diskOk;
|
|
843
|
+
}
|
|
844
|
+
function scoreSortedCrns(crns) {
|
|
845
|
+
return [...crns].sort((left, right) => {
|
|
846
|
+
const rightScore = typeof right.score === "number" ? right.score : Number(right.score ?? Number.NEGATIVE_INFINITY);
|
|
847
|
+
const leftScore = typeof left.score === "number" ? left.score : Number(left.score ?? Number.NEGATIVE_INFINITY);
|
|
848
|
+
if (rightScore !== leftScore) return rightScore - leftScore;
|
|
849
|
+
const leftName = (left.name || left.address || left.hash).toLowerCase();
|
|
850
|
+
const rightName = (right.name || right.address || right.hash).toLowerCase();
|
|
851
|
+
return leftName.localeCompare(rightName);
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
function filterDeployableCrns(crns, options = {}) {
|
|
855
|
+
return scoreSortedCrns(
|
|
856
|
+
compatibleCrns(crns, options.excludedHashes).filter((crn) => hasCrnCapacity(crn, options.spec))
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// ../core/src/aleph-normalizers.ts
|
|
861
|
+
function normalizeMessageStatus2(status) {
|
|
862
|
+
if (typeof status !== "string") return "unknown";
|
|
863
|
+
const normalized = status.toLowerCase();
|
|
864
|
+
if (normalized === "processed" || normalized === "pending" || normalized === "rejected") {
|
|
865
|
+
return normalized;
|
|
866
|
+
}
|
|
867
|
+
return "unknown";
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// ../core/src/broadcast.ts
|
|
871
|
+
function signaturePayload(message) {
|
|
872
|
+
return [message.chain, message.sender, message.type, message.item_hash].join("\n");
|
|
873
|
+
}
|
|
874
|
+
async function signAlephMessage(unsignedMessage, signer) {
|
|
875
|
+
const signature = await signer(unsignedMessage.sender, signaturePayload(unsignedMessage));
|
|
876
|
+
return {
|
|
877
|
+
...unsignedMessage,
|
|
878
|
+
signature: signature.startsWith("0x") ? signature : `0x${signature}`
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
function normalizeBroadcastStatus(httpStatus, responseStatus) {
|
|
882
|
+
if (httpStatus === 202) return "pending";
|
|
883
|
+
return normalizeMessageStatus2(responseStatus);
|
|
884
|
+
}
|
|
885
|
+
function isInvalidMessageFormatResponse2(response, payload) {
|
|
886
|
+
if (response.status !== 422) return false;
|
|
887
|
+
const details = payload?.details;
|
|
888
|
+
if (typeof details === "string" && details.includes("InvalidMessageFormat")) return true;
|
|
889
|
+
if (details && typeof details === "object") {
|
|
890
|
+
const detailMessage = details.message;
|
|
891
|
+
if (typeof detailMessage === "string" && detailMessage.includes("InvalidMessageFormat")) return true;
|
|
892
|
+
}
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
function isRetryableBroadcastFailure(response, payload) {
|
|
896
|
+
if (response.status >= 500) return true;
|
|
897
|
+
const publicationStatus = payload?.publication_status?.status;
|
|
898
|
+
if (typeof publicationStatus === "string" && publicationStatus.toLowerCase() === "error") {
|
|
899
|
+
return true;
|
|
900
|
+
}
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
async function postBroadcastPayload2(body, options) {
|
|
904
|
+
const rawResponse = await options.fetch(`${options.apiHost ?? DEFAULT_ALEPH_API_HOST2}/api/v0/messages`, {
|
|
905
|
+
method: "POST",
|
|
906
|
+
headers: { "content-type": "application/json" },
|
|
907
|
+
body: JSON.stringify(body)
|
|
908
|
+
});
|
|
909
|
+
const response = await rawResponse.json().catch(() => ({}));
|
|
910
|
+
return {
|
|
911
|
+
response,
|
|
912
|
+
httpStatus: rawResponse.status
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
async function broadcastAlephMessage2(message, options) {
|
|
916
|
+
const attempts = [
|
|
917
|
+
{ sync: options.sync ?? false, message },
|
|
918
|
+
{ ...message, sync: options.sync ?? false },
|
|
919
|
+
{ ...message }
|
|
920
|
+
];
|
|
921
|
+
for (let index = 0; index < attempts.length; index += 1) {
|
|
922
|
+
const result = await postBroadcastPayload2(attempts[index], {
|
|
923
|
+
apiHost: options.apiHost,
|
|
924
|
+
fetch: options.fetch
|
|
925
|
+
});
|
|
926
|
+
if (result.httpStatus === 202 || normalizeBroadcastStatus(result.httpStatus, result.response?.message_status) !== "unknown" || result.httpStatus >= 200 && result.httpStatus < 300) {
|
|
927
|
+
return result;
|
|
928
|
+
}
|
|
929
|
+
const canRetry = index < attempts.length - 1 && (isInvalidMessageFormatResponse2({ status: result.httpStatus }, result.response) || isRetryableBroadcastFailure({ status: result.httpStatus }, result.response));
|
|
930
|
+
if (!canRetry) {
|
|
931
|
+
throw new Error(`Broadcast failed: ${result.httpStatus} ${JSON.stringify(result.response ?? {})}`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
throw new Error("Broadcast failed: no compatible request format was accepted");
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// ../core/src/forget.ts
|
|
938
|
+
function asOptionalReason(value) {
|
|
939
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
940
|
+
}
|
|
941
|
+
async function createUnsignedForgetMessage(args) {
|
|
942
|
+
const content = {
|
|
943
|
+
address: args.sender,
|
|
944
|
+
time: args.now ?? Date.now() / 1e3,
|
|
945
|
+
hashes: [...new Set((args.hashes ?? []).filter(Boolean))],
|
|
946
|
+
aggregates: [...new Set((args.aggregates ?? []).filter(Boolean))],
|
|
947
|
+
reason: asOptionalReason(args.reason)
|
|
948
|
+
};
|
|
949
|
+
if (content.hashes.length === 0 && content.aggregates.length === 0) {
|
|
950
|
+
throw new Error("FORGET message requires at least one hash or aggregate key.");
|
|
951
|
+
}
|
|
952
|
+
const itemContent = JSON.stringify(content);
|
|
953
|
+
const itemHash = await args.hasher(itemContent);
|
|
954
|
+
return {
|
|
955
|
+
sender: args.sender,
|
|
956
|
+
chain: "ETH",
|
|
957
|
+
type: "FORGET",
|
|
958
|
+
item_hash: itemHash,
|
|
959
|
+
item_type: "inline",
|
|
960
|
+
item_content: itemContent,
|
|
961
|
+
time: args.now ?? Date.now() / 1e3,
|
|
962
|
+
channel: args.channel ?? DEFAULT_ALEPH_CHANNEL
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
async function forgetAlephMessages(args) {
|
|
966
|
+
const unsignedMessage = await createUnsignedForgetMessage({
|
|
967
|
+
sender: args.sender,
|
|
968
|
+
hashes: args.hashes,
|
|
969
|
+
aggregates: args.aggregates,
|
|
970
|
+
reason: args.reason,
|
|
971
|
+
hasher: args.hasher,
|
|
972
|
+
channel: args.channel,
|
|
973
|
+
now: args.now
|
|
974
|
+
});
|
|
975
|
+
const message = await signAlephMessage(unsignedMessage, args.signer);
|
|
976
|
+
const { response, httpStatus } = await broadcastAlephMessage2(message, {
|
|
977
|
+
apiHost: args.apiHost,
|
|
978
|
+
sync: args.sync,
|
|
979
|
+
fetch: args.fetch
|
|
980
|
+
});
|
|
981
|
+
return {
|
|
982
|
+
sender: args.sender,
|
|
983
|
+
itemHash: message.item_hash,
|
|
984
|
+
response,
|
|
985
|
+
httpStatus,
|
|
986
|
+
status: normalizeBroadcastStatus(httpStatus, response?.message_status)
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// ../core/src/instance-deployment.ts
|
|
991
|
+
var SSH_PUBLIC_KEY_PATTERN = /^(ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|sk-ssh-ed25519@openssh\.com|sk-ecdsa-sha2-nistp256@openssh\.com)\s+[A-Za-z0-9+/]+={0,3}(?:\s+.+)?$/;
|
|
992
|
+
function toFiniteNumber(value) {
|
|
993
|
+
if (typeof value === "number") return value;
|
|
994
|
+
if (typeof value === "string" && value.trim()) return Number(value);
|
|
995
|
+
return Number.NaN;
|
|
996
|
+
}
|
|
997
|
+
function normalizeSshPublicKey(value) {
|
|
998
|
+
return value.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
|
|
999
|
+
}
|
|
1000
|
+
function isValidSshPublicKey(value) {
|
|
1001
|
+
return SSH_PUBLIC_KEY_PATTERN.test(normalizeSshPublicKey(value));
|
|
1002
|
+
}
|
|
1003
|
+
function createReleaseMetadata(name, rootfsVersion, deployer = "shared-aleph-tooling") {
|
|
1004
|
+
return {
|
|
1005
|
+
name,
|
|
1006
|
+
rootfs_version: rootfsVersion,
|
|
1007
|
+
deployer
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function tierSpec(pricing, tier) {
|
|
1011
|
+
return {
|
|
1012
|
+
vcpus: pricing.compute_unit.vcpus * tier.compute_units,
|
|
1013
|
+
memoryMiB: pricing.compute_unit.memory_mib * tier.compute_units,
|
|
1014
|
+
diskMiB: pricing.compute_unit.disk_mib * tier.compute_units
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function buildPaymentQuote(tier, pricing, balance) {
|
|
1018
|
+
const computeUnitPrice = pricing.price.compute_unit;
|
|
1019
|
+
if (!computeUnitPrice) return null;
|
|
1020
|
+
const unitPrice = toFiniteNumber(computeUnitPrice.credit);
|
|
1021
|
+
if (!Number.isFinite(unitPrice)) return null;
|
|
1022
|
+
return {
|
|
1023
|
+
required: unitPrice * tier.compute_units,
|
|
1024
|
+
available: Number(balance.credit_balance ?? 0),
|
|
1025
|
+
computeUnits: tier.compute_units,
|
|
1026
|
+
unitPrice,
|
|
1027
|
+
label: "credits"
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
function createInstanceContent(args) {
|
|
1031
|
+
const sshKey = normalizeSshPublicKey(args.sshPublicKey);
|
|
1032
|
+
if (!sshKey) {
|
|
1033
|
+
throw new Error("An SSH public key is required.");
|
|
1034
|
+
}
|
|
1035
|
+
if (!isValidSshPublicKey(sshKey)) {
|
|
1036
|
+
throw new Error("SSH public key must be a single valid .pub line.");
|
|
1037
|
+
}
|
|
1038
|
+
if (!args.rootfsItemHash || !/^[a-fA-F0-9]{64}$/.test(args.rootfsItemHash)) {
|
|
1039
|
+
throw new Error("rootfsItemHash must be a 64-character Aleph item hash.");
|
|
1040
|
+
}
|
|
1041
|
+
return {
|
|
1042
|
+
address: args.address,
|
|
1043
|
+
time: args.now ?? Date.now() / 1e3,
|
|
1044
|
+
allow_amend: false,
|
|
1045
|
+
metadata: createReleaseMetadata(args.name.trim(), args.rootfsVersion ?? "custom-rootfs", args.deployer),
|
|
1046
|
+
authorized_keys: [sshKey],
|
|
1047
|
+
environment: {
|
|
1048
|
+
internet: true,
|
|
1049
|
+
aleph_api: true,
|
|
1050
|
+
hypervisor: "qemu",
|
|
1051
|
+
reproducible: false,
|
|
1052
|
+
shared_cache: false
|
|
1053
|
+
},
|
|
1054
|
+
resources: {
|
|
1055
|
+
vcpus: Number(args.vcpus),
|
|
1056
|
+
memory: Number(args.memoryMiB),
|
|
1057
|
+
seconds: Number(args.seconds ?? 30)
|
|
1058
|
+
},
|
|
1059
|
+
payment: {
|
|
1060
|
+
type: "credit"
|
|
1061
|
+
},
|
|
1062
|
+
requirements: args.crnHash ? {
|
|
1063
|
+
node: {
|
|
1064
|
+
node_hash: args.crnHash
|
|
1065
|
+
}
|
|
1066
|
+
} : void 0,
|
|
1067
|
+
volumes: [],
|
|
1068
|
+
rootfs: {
|
|
1069
|
+
parent: {
|
|
1070
|
+
ref: args.rootfsItemHash,
|
|
1071
|
+
use_latest: true
|
|
1072
|
+
},
|
|
1073
|
+
persistence: "host",
|
|
1074
|
+
size_mib: Number(args.rootfsSizeMiB)
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
async function createUnsignedInstanceMessage(args) {
|
|
1079
|
+
const itemContent = JSON.stringify(args.content);
|
|
1080
|
+
const itemHash = await args.hasher(itemContent);
|
|
1081
|
+
return {
|
|
1082
|
+
sender: args.sender,
|
|
1083
|
+
chain: "ETH",
|
|
1084
|
+
type: "INSTANCE",
|
|
1085
|
+
item_hash: itemHash,
|
|
1086
|
+
item_type: "inline",
|
|
1087
|
+
item_content: itemContent,
|
|
1088
|
+
time: args.now ?? Date.now() / 1e3,
|
|
1089
|
+
channel: args.channel ?? DEFAULT_ALEPH_CHANNEL
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
async function deployInstance(args) {
|
|
1093
|
+
const unsignedMessage = await createUnsignedInstanceMessage({
|
|
1094
|
+
sender: args.sender,
|
|
1095
|
+
content: args.content,
|
|
1096
|
+
hasher: args.hasher,
|
|
1097
|
+
channel: args.channel,
|
|
1098
|
+
now: args.now
|
|
1099
|
+
});
|
|
1100
|
+
const signature = await args.signer(unsignedMessage.sender, [unsignedMessage.chain, unsignedMessage.sender, unsignedMessage.type, unsignedMessage.item_hash].join("\n"));
|
|
1101
|
+
const message = {
|
|
1102
|
+
...unsignedMessage,
|
|
1103
|
+
signature: signature.startsWith("0x") ? signature : `0x${signature}`
|
|
1104
|
+
};
|
|
1105
|
+
const { response, httpStatus } = await broadcastAlephMessage2(message, {
|
|
1106
|
+
apiHost: args.apiHost,
|
|
1107
|
+
sync: args.sync,
|
|
1108
|
+
fetch: args.fetch
|
|
1109
|
+
});
|
|
1110
|
+
const status = httpStatus === 202 ? "pending" : typeof response.message_status === "string" ? response.message_status : "unknown";
|
|
1111
|
+
return {
|
|
1112
|
+
itemHash: message.item_hash,
|
|
1113
|
+
httpStatus,
|
|
1114
|
+
status,
|
|
1115
|
+
message,
|
|
1116
|
+
response,
|
|
1117
|
+
rejectionReason: status === "rejected" ? String(response.details ?? "Aleph rejected this deployment.") : null
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// src/shared/controller.ts
|
|
1122
|
+
function defaultState(props = {}) {
|
|
1123
|
+
return {
|
|
1124
|
+
ready: false,
|
|
1125
|
+
open: Boolean(props.openByDefault),
|
|
1126
|
+
wallet: {
|
|
1127
|
+
connected: false,
|
|
1128
|
+
address: null,
|
|
1129
|
+
chainId: null,
|
|
1130
|
+
isMetaMask: false
|
|
1131
|
+
},
|
|
1132
|
+
manifestUrl: props.manifestUrl ?? DEFAULT_MANIFEST_URL,
|
|
1133
|
+
manifestJson: props.manifestJson ?? "",
|
|
1134
|
+
sshPublicKey: props.sshPublicKey ?? "",
|
|
1135
|
+
instanceName: props.instanceName ?? DEFAULT_INSTANCE_NAME,
|
|
1136
|
+
tierId: DEFAULT_TIER_ID,
|
|
1137
|
+
showInstances: props.showInstances ?? true,
|
|
1138
|
+
showPasteManifest: false,
|
|
1139
|
+
busy: {
|
|
1140
|
+
connectingWallet: false,
|
|
1141
|
+
refreshing: false,
|
|
1142
|
+
deploying: false,
|
|
1143
|
+
deletingInstanceHash: null
|
|
1144
|
+
},
|
|
1145
|
+
statusText: "Ready",
|
|
1146
|
+
errorText: null,
|
|
1147
|
+
manifestState: {
|
|
1148
|
+
manifest: null,
|
|
1149
|
+
valid: false,
|
|
1150
|
+
errors: ["Manifest not loaded yet."]
|
|
1151
|
+
},
|
|
1152
|
+
manifest: null,
|
|
1153
|
+
rootfsResolution: null,
|
|
1154
|
+
rootfsVerified: false,
|
|
1155
|
+
rootfsHealth: ROOTFS_MISSING_STATE,
|
|
1156
|
+
pricingSummary: {
|
|
1157
|
+
pricing: null,
|
|
1158
|
+
tier: null,
|
|
1159
|
+
requiredCredits: null,
|
|
1160
|
+
availableCredits: null,
|
|
1161
|
+
vcpus: null,
|
|
1162
|
+
memoryMiB: null,
|
|
1163
|
+
diskMiB: null
|
|
1164
|
+
},
|
|
1165
|
+
balance: null,
|
|
1166
|
+
crns: [],
|
|
1167
|
+
selectedCrn: null,
|
|
1168
|
+
instances: [],
|
|
1169
|
+
relayPing: RELAY_PING_IDLE_STATE,
|
|
1170
|
+
lastDeploymentHash: null
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
async function sha256Hex(payload) {
|
|
1174
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload));
|
|
1175
|
+
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1176
|
+
}
|
|
1177
|
+
function mappedPorts(execution) {
|
|
1178
|
+
return Object.entries(execution?.networking?.mapped_ports ?? {}).map(([port, mapping]) => ({
|
|
1179
|
+
label: `${port}/${mapping.udp ? "udp" : "tcp"}`,
|
|
1180
|
+
hostPort: mapping.host ?? null
|
|
1181
|
+
}));
|
|
1182
|
+
}
|
|
1183
|
+
function rootfsHealth(args) {
|
|
1184
|
+
if (!args.manifestState.valid || !args.manifestState.manifest) {
|
|
1185
|
+
return {
|
|
1186
|
+
tone: "error",
|
|
1187
|
+
label: "manifest invalid",
|
|
1188
|
+
detail: args.manifestState.errors[0] ?? "Manifest could not be parsed."
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
if (!args.rootfsVerified) {
|
|
1192
|
+
return {
|
|
1193
|
+
tone: "error",
|
|
1194
|
+
label: "not found on Aleph",
|
|
1195
|
+
detail: "The referenced rootfs STORE message is not available yet."
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
if (!args.resolution) {
|
|
1199
|
+
return {
|
|
1200
|
+
tone: "caution",
|
|
1201
|
+
label: "verifying rootfs",
|
|
1202
|
+
detail: "The rootfs reference is still being resolved."
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
if (args.resolution.messageStatus === "processed") {
|
|
1206
|
+
return {
|
|
1207
|
+
tone: "ok",
|
|
1208
|
+
label: "deployable",
|
|
1209
|
+
detail: args.resolution.gatewayUrl
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
if (args.resolution.messageStatus === "pending" && args.resolution.gatewayStatus === "reachable") {
|
|
1213
|
+
return {
|
|
1214
|
+
tone: "caution",
|
|
1215
|
+
label: "pending but reachable",
|
|
1216
|
+
detail: "Gateway probe succeeded even though Aleph still reports pending."
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
if (args.resolution.messageStatus === "pending") {
|
|
1220
|
+
return {
|
|
1221
|
+
tone: "caution",
|
|
1222
|
+
label: "pending on Aleph",
|
|
1223
|
+
detail: "Wait until the STORE message is processed."
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
return {
|
|
1227
|
+
tone: "error",
|
|
1228
|
+
label: "not deployable",
|
|
1229
|
+
detail: args.resolution.rejectionReason ?? "Aleph rejected the rootfs reference."
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
async function resolveManifest(args) {
|
|
1233
|
+
const pasted = resolveManifestSource({ manifestJson: args.manifestJson });
|
|
1234
|
+
if (pasted) return pasted;
|
|
1235
|
+
return loadRootfsManifest(args.manifestUrl);
|
|
1236
|
+
}
|
|
1237
|
+
async function inspectInstanceRuntime(args) {
|
|
1238
|
+
const details = {
|
|
1239
|
+
messageStatus: String(args.instance.status ?? (args.instance.confirmed ? "processed" : "unknown")).toLowerCase(),
|
|
1240
|
+
allocationSource: null,
|
|
1241
|
+
crnUrl: null,
|
|
1242
|
+
hostIpv4: null,
|
|
1243
|
+
ipv6: null,
|
|
1244
|
+
vmIpv4: null,
|
|
1245
|
+
webUrl: null,
|
|
1246
|
+
sshCommand: null,
|
|
1247
|
+
mappedPorts: [],
|
|
1248
|
+
execution: null,
|
|
1249
|
+
error: null
|
|
1250
|
+
};
|
|
1251
|
+
if (details.messageStatus !== "processed") {
|
|
1252
|
+
return details;
|
|
1253
|
+
}
|
|
1254
|
+
try {
|
|
1255
|
+
const allocation = await args.client.fetchSchedulerAllocation(args.instance.item_hash) ?? (() => {
|
|
1256
|
+
const nodeHash = args.instance.content?.requirements?.node?.node_hash;
|
|
1257
|
+
const crn = args.crns.find((candidate) => candidate.hash === nodeHash);
|
|
1258
|
+
return nodeHash ? {
|
|
1259
|
+
source: "manual",
|
|
1260
|
+
crnHash: nodeHash,
|
|
1261
|
+
crnUrl: crn?.address ?? null,
|
|
1262
|
+
node: crn ? { url: crn.address } : null,
|
|
1263
|
+
vmIpv6: null,
|
|
1264
|
+
period: null
|
|
1265
|
+
} : null;
|
|
1266
|
+
})();
|
|
1267
|
+
details.allocationSource = allocation?.source ?? null;
|
|
1268
|
+
details.crnUrl = allocation?.crnUrl ?? null;
|
|
1269
|
+
details.ipv6 = allocation?.vmIpv6 ?? null;
|
|
1270
|
+
details.webUrl = await args.client.fetch2n6WebAccessUrl(args.instance.item_hash);
|
|
1271
|
+
if (!allocation?.crnUrl) {
|
|
1272
|
+
return details;
|
|
1273
|
+
}
|
|
1274
|
+
const executionLookup = await args.client.fetchCrnExecutionMap(allocation.crnUrl);
|
|
1275
|
+
const executionPayload = executionLookup.payload?.[args.instance.item_hash];
|
|
1276
|
+
if (!executionPayload) {
|
|
1277
|
+
return details;
|
|
1278
|
+
}
|
|
1279
|
+
const execution = normalizeExecution(executionPayload, allocation.crnUrl);
|
|
1280
|
+
if (!execution.networking.proxy_url && details.webUrl) {
|
|
1281
|
+
execution.networking.proxy_url = details.webUrl;
|
|
1282
|
+
}
|
|
1283
|
+
details.execution = execution;
|
|
1284
|
+
details.hostIpv4 = execution.networking.host_ipv4 ?? execution.networking.ipv4 ?? null;
|
|
1285
|
+
details.ipv6 = execution.networking.ipv6_ip ?? execution.networking.ipv6 ?? details.ipv6;
|
|
1286
|
+
details.vmIpv4 = execution.networking.ipv4_ip ?? null;
|
|
1287
|
+
details.webUrl = execution.networking.proxy_url ?? details.webUrl;
|
|
1288
|
+
details.mappedPorts = mappedPorts(execution);
|
|
1289
|
+
details.sshCommand = buildSshCommand(details.hostIpv4, details.mappedPorts);
|
|
1290
|
+
return details;
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
return {
|
|
1293
|
+
...details,
|
|
1294
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
function compatibleCrnsForTier(crns, state) {
|
|
1299
|
+
if (!state.pricingSummary.pricing || !state.pricingSummary.tier) {
|
|
1300
|
+
return [];
|
|
1301
|
+
}
|
|
1302
|
+
const spec = tierSpec(state.pricingSummary.pricing, state.pricingSummary.tier);
|
|
1303
|
+
return filterDeployableCrns(crns, { spec });
|
|
1304
|
+
}
|
|
1305
|
+
async function pingPeer(libp2p) {
|
|
1306
|
+
if (!libp2p || typeof libp2p !== "object") {
|
|
1307
|
+
return RELAY_PING_IDLE_STATE;
|
|
1308
|
+
}
|
|
1309
|
+
const candidate = libp2p;
|
|
1310
|
+
const peers = candidate.getPeers?.() ?? [];
|
|
1311
|
+
const firstPeer = peers[0];
|
|
1312
|
+
if (!firstPeer) {
|
|
1313
|
+
return {
|
|
1314
|
+
...RELAY_PING_IDLE_STATE,
|
|
1315
|
+
tone: "caution",
|
|
1316
|
+
error: "No connected relay peers available."
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
const sentAt = Date.now();
|
|
1320
|
+
try {
|
|
1321
|
+
const pingFn = candidate.services?.ping?.ping?.bind(candidate.services.ping) ?? candidate.ping?.bind(candidate);
|
|
1322
|
+
if (!pingFn) {
|
|
1323
|
+
return {
|
|
1324
|
+
...RELAY_PING_IDLE_STATE,
|
|
1325
|
+
tone: "caution",
|
|
1326
|
+
sent: true,
|
|
1327
|
+
lastPeerId: String(firstPeer),
|
|
1328
|
+
lastSentAt: sentAt,
|
|
1329
|
+
error: "libp2p ping service not available."
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
const latency = await pingFn(firstPeer);
|
|
1333
|
+
return {
|
|
1334
|
+
tone: "ok",
|
|
1335
|
+
sent: true,
|
|
1336
|
+
received: true,
|
|
1337
|
+
lastPeerId: String(firstPeer),
|
|
1338
|
+
lastLatencyMs: Number(latency),
|
|
1339
|
+
lastSentAt: sentAt,
|
|
1340
|
+
lastReceivedAt: Date.now(),
|
|
1341
|
+
error: null
|
|
1342
|
+
};
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
return {
|
|
1345
|
+
tone: "error",
|
|
1346
|
+
sent: true,
|
|
1347
|
+
received: false,
|
|
1348
|
+
lastPeerId: String(firstPeer),
|
|
1349
|
+
lastLatencyMs: null,
|
|
1350
|
+
lastSentAt: sentAt,
|
|
1351
|
+
lastReceivedAt: null,
|
|
1352
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
var SponsorRelayController = class {
|
|
1357
|
+
state;
|
|
1358
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
1359
|
+
client;
|
|
1360
|
+
refreshTimer = null;
|
|
1361
|
+
pingTimer = null;
|
|
1362
|
+
stopWalletWatch = null;
|
|
1363
|
+
props;
|
|
1364
|
+
constructor(props = {}) {
|
|
1365
|
+
this.props = props;
|
|
1366
|
+
this.state = defaultState(props);
|
|
1367
|
+
this.client = createAlephBrowserClient({
|
|
1368
|
+
apiHost: props.apiHost,
|
|
1369
|
+
crnListUrl: props.crnListUrl,
|
|
1370
|
+
schedulerApiHost: props.schedulerApiHost,
|
|
1371
|
+
twoN6ApiHost: props.twoN6ApiHost
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
subscribe(subscriber) {
|
|
1375
|
+
this.subscribers.add(subscriber);
|
|
1376
|
+
return () => {
|
|
1377
|
+
this.subscribers.delete(subscriber);
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
getState() {
|
|
1381
|
+
return this.state;
|
|
1382
|
+
}
|
|
1383
|
+
emit() {
|
|
1384
|
+
const next = this.state;
|
|
1385
|
+
this.subscribers.forEach((subscriber) => subscriber(next));
|
|
1386
|
+
}
|
|
1387
|
+
patch(patch) {
|
|
1388
|
+
this.state = {
|
|
1389
|
+
...this.state,
|
|
1390
|
+
...patch,
|
|
1391
|
+
busy: patch.busy ? { ...this.state.busy, ...patch.busy } : this.state.busy,
|
|
1392
|
+
wallet: patch.wallet ? { ...this.state.wallet, ...patch.wallet } : this.state.wallet,
|
|
1393
|
+
pricingSummary: patch.pricingSummary ? { ...this.state.pricingSummary, ...patch.pricingSummary } : this.state.pricingSummary,
|
|
1394
|
+
relayPing: patch.relayPing ? { ...this.state.relayPing, ...patch.relayPing } : this.state.relayPing
|
|
1395
|
+
};
|
|
1396
|
+
this.emit();
|
|
1397
|
+
}
|
|
1398
|
+
async init() {
|
|
1399
|
+
this.stopWalletWatch = watchWallet(() => {
|
|
1400
|
+
void this.refreshWalletDerivedState();
|
|
1401
|
+
});
|
|
1402
|
+
await this.refresh();
|
|
1403
|
+
this.refreshTimer = setInterval(() => {
|
|
1404
|
+
void this.refresh();
|
|
1405
|
+
}, REFRESH_INTERVAL_MS);
|
|
1406
|
+
this.pingTimer = setInterval(() => {
|
|
1407
|
+
void this.refreshRelayPing();
|
|
1408
|
+
}, RELAY_PING_INTERVAL_MS);
|
|
1409
|
+
await this.refreshRelayPing();
|
|
1410
|
+
this.patch({ ready: true });
|
|
1411
|
+
}
|
|
1412
|
+
destroy() {
|
|
1413
|
+
if (this.refreshTimer) clearInterval(this.refreshTimer);
|
|
1414
|
+
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
1415
|
+
this.stopWalletWatch?.();
|
|
1416
|
+
}
|
|
1417
|
+
setOpen(open) {
|
|
1418
|
+
this.patch({ open });
|
|
1419
|
+
}
|
|
1420
|
+
toggleOpen() {
|
|
1421
|
+
this.patch({ open: !this.state.open });
|
|
1422
|
+
}
|
|
1423
|
+
setManifestUrl(manifestUrl) {
|
|
1424
|
+
this.patch({ manifestUrl });
|
|
1425
|
+
}
|
|
1426
|
+
setManifestJson(manifestJson) {
|
|
1427
|
+
this.patch({ manifestJson });
|
|
1428
|
+
}
|
|
1429
|
+
setShowPasteManifest(showPasteManifest) {
|
|
1430
|
+
this.patch({ showPasteManifest });
|
|
1431
|
+
}
|
|
1432
|
+
setSshPublicKey(sshPublicKey) {
|
|
1433
|
+
this.patch({ sshPublicKey });
|
|
1434
|
+
}
|
|
1435
|
+
setInstanceName(instanceName) {
|
|
1436
|
+
this.patch({ instanceName });
|
|
1437
|
+
}
|
|
1438
|
+
setTierId(tierId) {
|
|
1439
|
+
this.patch({ tierId });
|
|
1440
|
+
this.recomputePricingSummary();
|
|
1441
|
+
}
|
|
1442
|
+
recomputePricingSummary() {
|
|
1443
|
+
const pricing = this.state.pricingSummary.pricing;
|
|
1444
|
+
const tier = pricing?.tiers.find((entry) => entry.id === this.state.tierId) ?? pricing?.tiers[0] ?? null;
|
|
1445
|
+
const balance = this.state.balance;
|
|
1446
|
+
const quote = pricing && tier && balance ? buildPaymentQuote(tier, pricing, balance) : null;
|
|
1447
|
+
const spec = pricing && tier ? tierSpec(pricing, tier) : null;
|
|
1448
|
+
const selectedCrn = compatibleCrnsForTier(this.state.crns, {
|
|
1449
|
+
...this.state,
|
|
1450
|
+
pricingSummary: {
|
|
1451
|
+
...this.state.pricingSummary,
|
|
1452
|
+
pricing,
|
|
1453
|
+
tier
|
|
1454
|
+
}
|
|
1455
|
+
})[0] ?? null;
|
|
1456
|
+
this.patch({
|
|
1457
|
+
pricingSummary: {
|
|
1458
|
+
pricing,
|
|
1459
|
+
tier,
|
|
1460
|
+
requiredCredits: quote?.required ?? null,
|
|
1461
|
+
availableCredits: quote?.available ?? balance?.credit_balance ?? null,
|
|
1462
|
+
vcpus: spec?.vcpus ?? null,
|
|
1463
|
+
memoryMiB: spec?.memoryMiB ?? null,
|
|
1464
|
+
diskMiB: spec?.diskMiB ?? null
|
|
1465
|
+
},
|
|
1466
|
+
selectedCrn
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
async connectWallet() {
|
|
1470
|
+
this.patch({
|
|
1471
|
+
busy: { connectingWallet: true },
|
|
1472
|
+
errorText: null,
|
|
1473
|
+
statusText: "Connecting MetaMask"
|
|
1474
|
+
});
|
|
1475
|
+
try {
|
|
1476
|
+
const wallet = await connectWallet();
|
|
1477
|
+
this.patch({
|
|
1478
|
+
wallet,
|
|
1479
|
+
busy: { connectingWallet: false },
|
|
1480
|
+
statusText: "Wallet connected"
|
|
1481
|
+
});
|
|
1482
|
+
await this.refresh();
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
this.patch({
|
|
1485
|
+
busy: { connectingWallet: false },
|
|
1486
|
+
errorText: error instanceof Error ? error.message : String(error),
|
|
1487
|
+
statusText: "Wallet connection failed"
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
async refreshWalletDerivedState() {
|
|
1492
|
+
if (!this.state.wallet.connected) {
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
try {
|
|
1496
|
+
const wallet = await connectWallet();
|
|
1497
|
+
this.patch({ wallet });
|
|
1498
|
+
await this.refresh();
|
|
1499
|
+
} catch {
|
|
1500
|
+
this.patch({
|
|
1501
|
+
wallet: {
|
|
1502
|
+
connected: false,
|
|
1503
|
+
address: null,
|
|
1504
|
+
chainId: null,
|
|
1505
|
+
isMetaMask: false
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
async refresh() {
|
|
1511
|
+
this.patch({
|
|
1512
|
+
busy: { refreshing: true },
|
|
1513
|
+
errorText: null,
|
|
1514
|
+
statusText: "Refreshing relay sponsor data"
|
|
1515
|
+
});
|
|
1516
|
+
try {
|
|
1517
|
+
const manifestState = await resolveManifest({
|
|
1518
|
+
manifestUrl: this.state.manifestUrl,
|
|
1519
|
+
manifestJson: this.state.manifestJson
|
|
1520
|
+
});
|
|
1521
|
+
const manifest = manifestState.manifest;
|
|
1522
|
+
const [pricingSummary, crns] = await Promise.all([
|
|
1523
|
+
fetchInstancePricing(this.client.apiHost),
|
|
1524
|
+
this.client.fetchCrns()
|
|
1525
|
+
]);
|
|
1526
|
+
let balance = this.state.balance;
|
|
1527
|
+
let instances = [];
|
|
1528
|
+
if (this.state.wallet.address) {
|
|
1529
|
+
const [nextBalance, rawInstances] = await Promise.all([
|
|
1530
|
+
this.client.fetchBalance(this.state.wallet.address),
|
|
1531
|
+
this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([])
|
|
1532
|
+
]);
|
|
1533
|
+
balance = nextBalance;
|
|
1534
|
+
instances = await Promise.all(
|
|
1535
|
+
rawInstances.map(async (instance) => ({
|
|
1536
|
+
instance,
|
|
1537
|
+
details: await inspectInstanceRuntime({
|
|
1538
|
+
client: this.client,
|
|
1539
|
+
instance,
|
|
1540
|
+
crns
|
|
1541
|
+
})
|
|
1542
|
+
}))
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
let rootfsVerified = false;
|
|
1546
|
+
let rootfsResolution = null;
|
|
1547
|
+
if (manifestState.valid && manifest) {
|
|
1548
|
+
rootfsVerified = await verifyRootfsExists(manifest.rootfsItemHash, this.client.apiHost);
|
|
1549
|
+
rootfsResolution = await resolveRootfsReference(manifest.rootfsItemHash, this.client.apiHost);
|
|
1550
|
+
}
|
|
1551
|
+
this.patch({
|
|
1552
|
+
manifestState,
|
|
1553
|
+
manifest,
|
|
1554
|
+
rootfsVerified,
|
|
1555
|
+
rootfsResolution,
|
|
1556
|
+
rootfsHealth: rootfsHealth({
|
|
1557
|
+
manifestState,
|
|
1558
|
+
rootfsVerified,
|
|
1559
|
+
resolution: rootfsResolution
|
|
1560
|
+
}),
|
|
1561
|
+
pricingSummary: {
|
|
1562
|
+
...this.state.pricingSummary,
|
|
1563
|
+
pricing: pricingSummary.pricing
|
|
1564
|
+
},
|
|
1565
|
+
balance,
|
|
1566
|
+
crns,
|
|
1567
|
+
instances,
|
|
1568
|
+
busy: { refreshing: false },
|
|
1569
|
+
statusText: "Relay sponsor data ready"
|
|
1570
|
+
});
|
|
1571
|
+
this.recomputePricingSummary();
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
this.patch({
|
|
1574
|
+
busy: { refreshing: false },
|
|
1575
|
+
errorText: error instanceof Error ? error.message : String(error),
|
|
1576
|
+
statusText: "Refresh failed"
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
async refreshRelayPing() {
|
|
1581
|
+
const relayPing = await pingPeer(this.props.libp2p);
|
|
1582
|
+
this.patch({ relayPing });
|
|
1583
|
+
}
|
|
1584
|
+
async deploy() {
|
|
1585
|
+
if (!this.state.wallet.address) {
|
|
1586
|
+
this.patch({ errorText: "Connect MetaMask before deploying." });
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
if (!this.state.manifest || !this.state.rootfsVerified || !this.state.pricingSummary.pricing || !this.state.pricingSummary.tier) {
|
|
1590
|
+
this.patch({ errorText: "Manifest, rootfs, and pricing must be ready before deploying." });
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
this.patch({
|
|
1594
|
+
busy: { deploying: true },
|
|
1595
|
+
errorText: null,
|
|
1596
|
+
statusText: "Broadcasting deployment"
|
|
1597
|
+
});
|
|
1598
|
+
try {
|
|
1599
|
+
const spec = tierSpec(this.state.pricingSummary.pricing, this.state.pricingSummary.tier);
|
|
1600
|
+
const content = createInstanceContent({
|
|
1601
|
+
address: this.state.wallet.address,
|
|
1602
|
+
name: this.state.instanceName.trim(),
|
|
1603
|
+
sshPublicKey: this.state.sshPublicKey.trim(),
|
|
1604
|
+
rootfsItemHash: this.state.manifest.rootfsItemHash,
|
|
1605
|
+
rootfsSizeMiB: Math.max(this.state.manifest.rootfsSizeMiB, spec.diskMiB),
|
|
1606
|
+
vcpus: spec.vcpus,
|
|
1607
|
+
memoryMiB: spec.memoryMiB,
|
|
1608
|
+
rootfsVersion: this.state.manifest.version,
|
|
1609
|
+
crnHash: this.state.selectedCrn?.hash
|
|
1610
|
+
});
|
|
1611
|
+
const result = await deployInstance({
|
|
1612
|
+
sender: this.state.wallet.address,
|
|
1613
|
+
content,
|
|
1614
|
+
hasher: sha256Hex,
|
|
1615
|
+
signer: personalSign,
|
|
1616
|
+
fetch: (url, init) => fetch(url, init),
|
|
1617
|
+
apiHost: this.client.apiHost,
|
|
1618
|
+
sync: false
|
|
1619
|
+
});
|
|
1620
|
+
this.patch({
|
|
1621
|
+
busy: { deploying: false },
|
|
1622
|
+
statusText: `Deployment submitted: ${result.itemHash}`,
|
|
1623
|
+
lastDeploymentHash: result.itemHash
|
|
1624
|
+
});
|
|
1625
|
+
await this.refresh();
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
this.patch({
|
|
1628
|
+
busy: { deploying: false },
|
|
1629
|
+
errorText: error instanceof Error ? error.message : String(error),
|
|
1630
|
+
statusText: "Deployment failed"
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
async deleteInstance(instanceHash) {
|
|
1635
|
+
if (!this.state.wallet.address) {
|
|
1636
|
+
this.patch({ errorText: "Connect MetaMask before deleting instances." });
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
this.patch({
|
|
1640
|
+
busy: { deletingInstanceHash: instanceHash },
|
|
1641
|
+
errorText: null,
|
|
1642
|
+
statusText: `Deleting ${instanceHash}`
|
|
1643
|
+
});
|
|
1644
|
+
try {
|
|
1645
|
+
await forgetAlephMessages({
|
|
1646
|
+
sender: this.state.wallet.address,
|
|
1647
|
+
hashes: [instanceHash],
|
|
1648
|
+
reason: "Deleted from Sponsor Relay panel",
|
|
1649
|
+
signer: personalSign,
|
|
1650
|
+
hasher: sha256Hex,
|
|
1651
|
+
fetch: (url, init) => fetch(url, init).then(async (response) => ({
|
|
1652
|
+
ok: response.ok,
|
|
1653
|
+
status: response.status,
|
|
1654
|
+
json: async () => await response.json()
|
|
1655
|
+
})),
|
|
1656
|
+
apiHost: this.client.apiHost
|
|
1657
|
+
});
|
|
1658
|
+
this.patch({
|
|
1659
|
+
busy: { deletingInstanceHash: null },
|
|
1660
|
+
statusText: `Deletion submitted for ${instanceHash}`
|
|
1661
|
+
});
|
|
1662
|
+
await this.refresh();
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
this.patch({
|
|
1665
|
+
busy: { deletingInstanceHash: null },
|
|
1666
|
+
errorText: error instanceof Error ? error.message : String(error),
|
|
1667
|
+
statusText: "Delete failed"
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
function createSponsorRelayController(props = {}) {
|
|
1673
|
+
return new SponsorRelayController(props);
|
|
1674
|
+
}
|
|
1675
|
+
export {
|
|
1676
|
+
DEFAULT_INSTANCE_NAME,
|
|
1677
|
+
DEFAULT_MANIFEST_URL,
|
|
1678
|
+
DEFAULT_TIER_ID,
|
|
1679
|
+
REFRESH_INTERVAL_MS,
|
|
1680
|
+
RELAY_PING_IDLE_STATE,
|
|
1681
|
+
RELAY_PING_INTERVAL_MS,
|
|
1682
|
+
ROOTFS_MISSING_STATE,
|
|
1683
|
+
SponsorRelayController,
|
|
1684
|
+
buildSshCommand,
|
|
1685
|
+
connectWallet,
|
|
1686
|
+
createSponsorRelayController,
|
|
1687
|
+
formatDateTime,
|
|
1688
|
+
formatNumber,
|
|
1689
|
+
getEthereumProvider,
|
|
1690
|
+
joinMappedPorts,
|
|
1691
|
+
personalSign,
|
|
1692
|
+
resolveManifestSource,
|
|
1693
|
+
shortHash,
|
|
1694
|
+
watchWallet
|
|
1695
|
+
};
|