@tangle-network/blueprint-ui 0.1.0 → 0.1.2
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 +47 -11
- package/dist/BlueprintHostPanel-6iVEh-f1.d.ts +39 -0
- package/dist/chunk-37ADATBT.js +55 -0
- package/dist/chunk-37ADATBT.js.map +1 -0
- package/dist/chunk-A6PJT5YQ.js +1180 -0
- package/dist/chunk-A6PJT5YQ.js.map +1 -0
- package/dist/chunk-GD3AZEJL.js +327 -0
- package/dist/chunk-GD3AZEJL.js.map +1 -0
- package/dist/components.d.ts +179 -0
- package/dist/components.js +1127 -0
- package/dist/components.js.map +1 -0
- package/dist/host.d.ts +96 -0
- package/dist/host.js +39 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +8470 -0
- package/dist/index.js +841 -0
- package/dist/index.js.map +1 -0
- package/dist/preset.d.ts +60 -0
- package/dist/preset.js +7 -0
- package/dist/preset.js.map +1 -0
- package/dist/registry-JhwB9BPD.d.ts +87 -0
- package/dist/styles.css +559 -0
- package/package.json +42 -13
- package/src/components/layout/AppDocument.tsx +1 -1
- package/src/components/layout/ChainSwitcher.tsx +27 -10
- package/src/components/layout/Web3Shell.tsx +81 -6
- package/src/components/web3/ConnectWalletCta.tsx +21 -0
- package/src/components.ts +6 -0
- package/src/contracts/abi.ts +10 -1
- package/src/contracts/chains.ts +23 -10
- package/src/contracts/publicClient.ts +52 -10
- package/src/hooks/useOperators.ts +203 -96
- package/src/hooks/useProvisionProgress.ts +2 -1
- package/src/hooks/useQuotes.ts +69 -14
- package/src/hooks/useSessionAuth.ts +2 -1
- package/src/hooks/useSidecarAuth.ts +173 -0
- package/src/hooks/useWagmiSidecarAuth.ts +11 -0
- package/src/hooks/useWalletEthBalance.ts +68 -0
- package/src/host/components/BlueprintHostHero.tsx +91 -0
- package/src/host/components/BlueprintHostPanel.tsx +24 -0
- package/src/host/index.ts +42 -0
- package/src/host/resolver.ts +204 -0
- package/src/host/types.ts +111 -0
- package/src/index.ts +48 -2
- package/src/stores/infra.ts +3 -2
- package/src/styles.css +128 -0
- package/src/utils/env.ts +22 -0
- package/src/utils/web3.ts +9 -3
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
// src/utils/resolveOperatorRpc.ts
|
|
2
|
+
function resolveOperatorRpc(raw) {
|
|
3
|
+
if (typeof window === "undefined") return raw;
|
|
4
|
+
const withProto = raw.includes("://") ? raw : `http://${raw}`;
|
|
5
|
+
try {
|
|
6
|
+
const url = new URL(withProto);
|
|
7
|
+
const pageHost = window.location.hostname;
|
|
8
|
+
const isNonRoutable = url.hostname.endsWith(".local") || !url.hostname.includes(".") || url.hostname === "127.0.0.1" || url.hostname === "localhost";
|
|
9
|
+
if (isNonRoutable && pageHost !== url.hostname) {
|
|
10
|
+
url.hostname = pageHost;
|
|
11
|
+
}
|
|
12
|
+
return url.toString().replace(/\/$/, "");
|
|
13
|
+
} catch {
|
|
14
|
+
return withProto;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/contracts/chains.ts
|
|
19
|
+
import { defineChain } from "viem";
|
|
20
|
+
import { mainnet } from "viem/chains";
|
|
21
|
+
|
|
22
|
+
// src/utils/env.ts
|
|
23
|
+
function readImportMetaEnv() {
|
|
24
|
+
return import.meta.env ?? {};
|
|
25
|
+
}
|
|
26
|
+
function getEnvVar(key) {
|
|
27
|
+
const value = readImportMetaEnv()[key];
|
|
28
|
+
return typeof value === "string" ? value : void 0;
|
|
29
|
+
}
|
|
30
|
+
function isDevEnv() {
|
|
31
|
+
return Boolean(readImportMetaEnv().DEV);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/contracts/chains.ts
|
|
35
|
+
function resolveRpcUrl(envUrl) {
|
|
36
|
+
const configured = envUrl ?? getEnvVar("VITE_RPC_URL") ?? "http://localhost:8545";
|
|
37
|
+
if (typeof window === "undefined") return configured;
|
|
38
|
+
try {
|
|
39
|
+
const rpc = new URL(configured);
|
|
40
|
+
const isLocalRpc = rpc.hostname === "127.0.0.1" || rpc.hostname === "localhost";
|
|
41
|
+
const pageHost = window.location.hostname;
|
|
42
|
+
const isLocalPage = pageHost === "127.0.0.1" || pageHost === "localhost";
|
|
43
|
+
if (isLocalRpc && !isLocalPage && isDevEnv()) {
|
|
44
|
+
return `${window.location.origin}/rpc-proxy`;
|
|
45
|
+
}
|
|
46
|
+
if (isLocalRpc && !isLocalPage) {
|
|
47
|
+
rpc.hostname = pageHost;
|
|
48
|
+
return rpc.toString().replace(/\/$/, "");
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
return configured;
|
|
53
|
+
}
|
|
54
|
+
var rpcUrl = resolveRpcUrl();
|
|
55
|
+
function createTangleLocalChain(options = {}) {
|
|
56
|
+
const chainId = options.chainId ?? Number(getEnvVar("VITE_CHAIN_ID") ?? 31337);
|
|
57
|
+
const localRpcUrl = resolveRpcUrl(options.rpcUrl);
|
|
58
|
+
return defineChain({
|
|
59
|
+
id: chainId,
|
|
60
|
+
name: "Tangle Local",
|
|
61
|
+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
62
|
+
rpcUrls: { default: { http: [localRpcUrl] } },
|
|
63
|
+
blockExplorers: { default: { name: "Explorer", url: "" } },
|
|
64
|
+
contracts: { multicall3: { address: "0xcA11bde05977b3631167028862bE2a173976CA11" } }
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
var tangleLocal = createTangleLocalChain();
|
|
68
|
+
var tangleTestnet = defineChain({
|
|
69
|
+
id: 3799,
|
|
70
|
+
name: "Tangle Testnet",
|
|
71
|
+
nativeCurrency: { name: "Tangle", symbol: "tTNT", decimals: 18 },
|
|
72
|
+
rpcUrls: {
|
|
73
|
+
default: {
|
|
74
|
+
http: ["https://testnet-rpc.tangle.tools"],
|
|
75
|
+
webSocket: ["wss://testnet-rpc.tangle.tools"]
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
blockExplorers: { default: { name: "Tangle Explorer", url: "https://testnet-explorer.tangle.tools" } },
|
|
79
|
+
contracts: { multicall3: { address: "0xcA11bde05977b3631167028862bE2a173976CA11" } }
|
|
80
|
+
});
|
|
81
|
+
var tangleMainnet = defineChain({
|
|
82
|
+
id: 5845,
|
|
83
|
+
name: "Tangle",
|
|
84
|
+
nativeCurrency: { name: "Tangle", symbol: "TNT", decimals: 18 },
|
|
85
|
+
rpcUrls: {
|
|
86
|
+
default: {
|
|
87
|
+
http: ["https://rpc.tangle.tools"],
|
|
88
|
+
webSocket: ["wss://rpc.tangle.tools"]
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
blockExplorers: { default: { name: "Tangle Explorer", url: "https://explorer.tangle.tools" } },
|
|
92
|
+
contracts: { multicall3: { address: "0xcA11bde05977b3631167028862bE2a173976CA11" } }
|
|
93
|
+
});
|
|
94
|
+
var _networks = {};
|
|
95
|
+
function configureNetworks(nets) {
|
|
96
|
+
_networks = nets;
|
|
97
|
+
}
|
|
98
|
+
function getNetworks() {
|
|
99
|
+
return _networks;
|
|
100
|
+
}
|
|
101
|
+
var allTangleChains = [tangleLocal, tangleTestnet, tangleMainnet];
|
|
102
|
+
|
|
103
|
+
// src/stores/persistedAtom.ts
|
|
104
|
+
import { atom } from "nanostores";
|
|
105
|
+
function serializeWithBigInt(value) {
|
|
106
|
+
return JSON.stringify(
|
|
107
|
+
value,
|
|
108
|
+
(_key, v) => typeof v === "bigint" ? { __bigint: v.toString() } : v
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
function deserializeWithBigInt(raw) {
|
|
112
|
+
return JSON.parse(raw, (_key, v) => {
|
|
113
|
+
if (v && typeof v === "object" && "__bigint" in v && typeof v.__bigint === "string") {
|
|
114
|
+
return BigInt(v.__bigint);
|
|
115
|
+
}
|
|
116
|
+
return v;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function persistedAtom(opts) {
|
|
120
|
+
const { key, initial, serialize = JSON.stringify, deserialize = JSON.parse } = opts;
|
|
121
|
+
let restored = initial;
|
|
122
|
+
if (typeof window !== "undefined") {
|
|
123
|
+
try {
|
|
124
|
+
const raw = localStorage.getItem(key);
|
|
125
|
+
if (raw !== null) {
|
|
126
|
+
restored = deserialize(raw);
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const store = atom(restored);
|
|
132
|
+
if (typeof window !== "undefined") {
|
|
133
|
+
store.subscribe((value) => {
|
|
134
|
+
try {
|
|
135
|
+
localStorage.setItem(key, serialize(value));
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return store;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/stores/txHistory.ts
|
|
144
|
+
import { computed } from "nanostores";
|
|
145
|
+
var MAX_TXS = 50;
|
|
146
|
+
var txListStore = persistedAtom({
|
|
147
|
+
key: "bp_tx_history",
|
|
148
|
+
initial: [],
|
|
149
|
+
serialize: serializeWithBigInt,
|
|
150
|
+
deserialize: deserializeWithBigInt
|
|
151
|
+
});
|
|
152
|
+
var pendingCount = computed(
|
|
153
|
+
txListStore,
|
|
154
|
+
(txs) => txs.filter((tx) => tx.status === "pending").length
|
|
155
|
+
);
|
|
156
|
+
function addTx(hash, label, chainId) {
|
|
157
|
+
const existing = txListStore.get();
|
|
158
|
+
if (existing.some((tx) => tx.hash === hash)) return;
|
|
159
|
+
const newTx = { hash, label, status: "pending", timestamp: Date.now(), chainId };
|
|
160
|
+
txListStore.set([newTx, ...existing].slice(0, MAX_TXS));
|
|
161
|
+
}
|
|
162
|
+
function updateTx(hash, update) {
|
|
163
|
+
txListStore.set(
|
|
164
|
+
txListStore.get().map(
|
|
165
|
+
(tx) => tx.hash === hash ? { ...tx, ...update } : tx
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
function clearTxs() {
|
|
170
|
+
txListStore.set([]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/stores/infra.ts
|
|
174
|
+
var defaultBlueprintId = getEnvVar("VITE_BLUEPRINT_ID") ?? "0";
|
|
175
|
+
var defaultServiceId = getEnvVar("VITE_SERVICE_ID") ?? getEnvVar("VITE_SERVICE_IDS")?.split(",")[0] ?? "0";
|
|
176
|
+
var infraStore = persistedAtom({
|
|
177
|
+
key: "bp_infra",
|
|
178
|
+
initial: {
|
|
179
|
+
blueprintId: defaultBlueprintId,
|
|
180
|
+
serviceId: defaultServiceId,
|
|
181
|
+
serviceValidated: false
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
function updateInfra(update) {
|
|
185
|
+
infraStore.set({ ...infraStore.get(), ...update });
|
|
186
|
+
}
|
|
187
|
+
function getInfra() {
|
|
188
|
+
return infraStore.get();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/stores/theme.ts
|
|
192
|
+
import { atom as atom2 } from "nanostores";
|
|
193
|
+
var kTheme = "bp_theme";
|
|
194
|
+
var DEFAULT_THEME = "dark";
|
|
195
|
+
var themeStore = atom2(initStore());
|
|
196
|
+
function themeIsDark() {
|
|
197
|
+
return themeStore.get() === "dark";
|
|
198
|
+
}
|
|
199
|
+
function initStore() {
|
|
200
|
+
if (typeof window !== "undefined") {
|
|
201
|
+
const persisted = localStorage.getItem(kTheme);
|
|
202
|
+
const attr = document.querySelector("html")?.getAttribute("data-theme");
|
|
203
|
+
return persisted ?? attr ?? DEFAULT_THEME;
|
|
204
|
+
}
|
|
205
|
+
return DEFAULT_THEME;
|
|
206
|
+
}
|
|
207
|
+
function toggleTheme() {
|
|
208
|
+
const next = themeStore.get() === "dark" ? "light" : "dark";
|
|
209
|
+
themeStore.set(next);
|
|
210
|
+
localStorage.setItem(kTheme, next);
|
|
211
|
+
document.querySelector("html")?.setAttribute("data-theme", next);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/contracts/abi.ts
|
|
215
|
+
var tangleJobsAbi = [
|
|
216
|
+
{
|
|
217
|
+
type: "function",
|
|
218
|
+
name: "submitJob",
|
|
219
|
+
inputs: [
|
|
220
|
+
{ name: "serviceId", type: "uint64" },
|
|
221
|
+
{ name: "job", type: "uint8" },
|
|
222
|
+
{ name: "args", type: "bytes" }
|
|
223
|
+
],
|
|
224
|
+
outputs: [{ name: "callId", type: "uint64" }],
|
|
225
|
+
stateMutability: "payable"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
type: "event",
|
|
229
|
+
name: "JobCalled",
|
|
230
|
+
inputs: [
|
|
231
|
+
{ name: "serviceId", type: "uint64", indexed: true },
|
|
232
|
+
{ name: "job", type: "uint8", indexed: true },
|
|
233
|
+
{ name: "callId", type: "uint64", indexed: true },
|
|
234
|
+
{ name: "caller", type: "address", indexed: false },
|
|
235
|
+
{ name: "args", type: "bytes", indexed: false }
|
|
236
|
+
]
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
type: "event",
|
|
240
|
+
name: "JobResultReceived",
|
|
241
|
+
inputs: [
|
|
242
|
+
{ name: "serviceId", type: "uint64", indexed: true },
|
|
243
|
+
{ name: "job", type: "uint8", indexed: true },
|
|
244
|
+
{ name: "callId", type: "uint64", indexed: true },
|
|
245
|
+
{ name: "operator", type: "address", indexed: false },
|
|
246
|
+
{ name: "outputs", type: "bytes", indexed: false }
|
|
247
|
+
]
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
type: "event",
|
|
251
|
+
name: "JobSubmitted",
|
|
252
|
+
inputs: [
|
|
253
|
+
{ name: "serviceId", type: "uint64", indexed: true },
|
|
254
|
+
{ name: "callId", type: "uint64", indexed: true },
|
|
255
|
+
{ name: "jobIndex", type: "uint8", indexed: false },
|
|
256
|
+
{ name: "caller", type: "address", indexed: false },
|
|
257
|
+
{ name: "inputs", type: "bytes", indexed: false }
|
|
258
|
+
]
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
type: "event",
|
|
262
|
+
name: "JobCompleted",
|
|
263
|
+
inputs: [
|
|
264
|
+
{ name: "serviceId", type: "uint64", indexed: true },
|
|
265
|
+
{ name: "callId", type: "uint64", indexed: true }
|
|
266
|
+
]
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
type: "event",
|
|
270
|
+
name: "JobResultSubmitted",
|
|
271
|
+
inputs: [
|
|
272
|
+
{ name: "serviceId", type: "uint64", indexed: true },
|
|
273
|
+
{ name: "callId", type: "uint64", indexed: true },
|
|
274
|
+
{ name: "operator", type: "address", indexed: true },
|
|
275
|
+
{ name: "output", type: "bytes", indexed: false }
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
];
|
|
279
|
+
var tangleServicesAbi = [
|
|
280
|
+
{
|
|
281
|
+
type: "function",
|
|
282
|
+
name: "requestService",
|
|
283
|
+
inputs: [
|
|
284
|
+
{ name: "blueprintId", type: "uint64" },
|
|
285
|
+
{ name: "operators", type: "address[]" },
|
|
286
|
+
{ name: "config", type: "bytes" },
|
|
287
|
+
{ name: "permittedCallers", type: "address[]" },
|
|
288
|
+
{ name: "ttl", type: "uint64" },
|
|
289
|
+
{ name: "paymentToken", type: "address" },
|
|
290
|
+
{ name: "paymentAmount", type: "uint256" }
|
|
291
|
+
],
|
|
292
|
+
outputs: [{ name: "requestId", type: "uint64" }],
|
|
293
|
+
stateMutability: "payable"
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
type: "function",
|
|
297
|
+
name: "createServiceFromQuotes",
|
|
298
|
+
inputs: [
|
|
299
|
+
{ name: "blueprintId", type: "uint64" },
|
|
300
|
+
{
|
|
301
|
+
name: "quotes",
|
|
302
|
+
type: "tuple[]",
|
|
303
|
+
components: [
|
|
304
|
+
{
|
|
305
|
+
name: "details",
|
|
306
|
+
type: "tuple",
|
|
307
|
+
components: [
|
|
308
|
+
{ name: "blueprintId", type: "uint64" },
|
|
309
|
+
{ name: "ttlBlocks", type: "uint64" },
|
|
310
|
+
{ name: "totalCost", type: "uint256" },
|
|
311
|
+
{ name: "timestamp", type: "uint64" },
|
|
312
|
+
{ name: "expiry", type: "uint64" },
|
|
313
|
+
{ name: "confidentiality", type: "uint8" },
|
|
314
|
+
{
|
|
315
|
+
name: "securityCommitments",
|
|
316
|
+
type: "tuple[]",
|
|
317
|
+
components: [
|
|
318
|
+
{
|
|
319
|
+
name: "asset",
|
|
320
|
+
type: "tuple",
|
|
321
|
+
components: [
|
|
322
|
+
{ name: "kind", type: "uint8" },
|
|
323
|
+
{ name: "token", type: "address" }
|
|
324
|
+
]
|
|
325
|
+
},
|
|
326
|
+
{ name: "exposureBps", type: "uint16" }
|
|
327
|
+
]
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: "resourceCommitments",
|
|
331
|
+
type: "tuple[]",
|
|
332
|
+
components: [
|
|
333
|
+
{ name: "kind", type: "uint8" },
|
|
334
|
+
{ name: "count", type: "uint64" }
|
|
335
|
+
]
|
|
336
|
+
}
|
|
337
|
+
]
|
|
338
|
+
},
|
|
339
|
+
{ name: "signature", type: "bytes" },
|
|
340
|
+
{ name: "operator", type: "address" }
|
|
341
|
+
]
|
|
342
|
+
},
|
|
343
|
+
{ name: "config", type: "bytes" },
|
|
344
|
+
{ name: "permittedCallers", type: "address[]" },
|
|
345
|
+
{ name: "ttl", type: "uint64" }
|
|
346
|
+
],
|
|
347
|
+
outputs: [{ name: "serviceId", type: "uint64" }],
|
|
348
|
+
stateMutability: "payable"
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
type: "function",
|
|
352
|
+
name: "getService",
|
|
353
|
+
inputs: [{ name: "serviceId", type: "uint64" }],
|
|
354
|
+
outputs: [
|
|
355
|
+
{
|
|
356
|
+
name: "",
|
|
357
|
+
type: "tuple",
|
|
358
|
+
components: [
|
|
359
|
+
{ name: "blueprintId", type: "uint64" },
|
|
360
|
+
{ name: "owner", type: "address" },
|
|
361
|
+
{ name: "createdAt", type: "uint64" },
|
|
362
|
+
{ name: "ttl", type: "uint64" },
|
|
363
|
+
{ name: "terminatedAt", type: "uint64" },
|
|
364
|
+
{ name: "lastPaymentAt", type: "uint64" },
|
|
365
|
+
{ name: "operatorCount", type: "uint32" },
|
|
366
|
+
{ name: "minOperators", type: "uint32" },
|
|
367
|
+
{ name: "maxOperators", type: "uint32" },
|
|
368
|
+
{ name: "membership", type: "uint8" },
|
|
369
|
+
{ name: "pricing", type: "uint8" },
|
|
370
|
+
{ name: "status", type: "uint8" }
|
|
371
|
+
]
|
|
372
|
+
}
|
|
373
|
+
],
|
|
374
|
+
stateMutability: "view"
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
type: "function",
|
|
378
|
+
name: "isServiceActive",
|
|
379
|
+
inputs: [{ name: "serviceId", type: "uint64" }],
|
|
380
|
+
outputs: [{ name: "", type: "bool" }],
|
|
381
|
+
stateMutability: "view"
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
type: "function",
|
|
385
|
+
name: "getServiceOperators",
|
|
386
|
+
inputs: [{ name: "serviceId", type: "uint64" }],
|
|
387
|
+
outputs: [{ name: "", type: "address[]" }],
|
|
388
|
+
stateMutability: "view"
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
type: "function",
|
|
392
|
+
name: "isPermittedCaller",
|
|
393
|
+
inputs: [
|
|
394
|
+
{ name: "serviceId", type: "uint64" },
|
|
395
|
+
{ name: "caller", type: "address" }
|
|
396
|
+
],
|
|
397
|
+
outputs: [{ name: "", type: "bool" }],
|
|
398
|
+
stateMutability: "view"
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
type: "event",
|
|
402
|
+
name: "ServiceRequested",
|
|
403
|
+
inputs: [
|
|
404
|
+
{ name: "requestId", type: "uint64", indexed: true },
|
|
405
|
+
{ name: "blueprintId", type: "uint64", indexed: true },
|
|
406
|
+
{ name: "requester", type: "address", indexed: true }
|
|
407
|
+
]
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
type: "event",
|
|
411
|
+
name: "ServiceActivated",
|
|
412
|
+
inputs: [
|
|
413
|
+
{ name: "serviceId", type: "uint64", indexed: true },
|
|
414
|
+
{ name: "requestId", type: "uint64", indexed: true },
|
|
415
|
+
{ name: "blueprintId", type: "uint64", indexed: true }
|
|
416
|
+
]
|
|
417
|
+
}
|
|
418
|
+
];
|
|
419
|
+
var tangleOperatorsAbi = [
|
|
420
|
+
{
|
|
421
|
+
type: "function",
|
|
422
|
+
name: "blueprintOperatorCount",
|
|
423
|
+
inputs: [{ name: "blueprintId", type: "uint64" }],
|
|
424
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
425
|
+
stateMutability: "view"
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
type: "function",
|
|
429
|
+
name: "isOperatorRegistered",
|
|
430
|
+
inputs: [
|
|
431
|
+
{ name: "blueprintId", type: "uint64" },
|
|
432
|
+
{ name: "operator", type: "address" }
|
|
433
|
+
],
|
|
434
|
+
outputs: [{ name: "", type: "bool" }],
|
|
435
|
+
stateMutability: "view"
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
type: "function",
|
|
439
|
+
name: "getOperatorPreferences",
|
|
440
|
+
inputs: [
|
|
441
|
+
{ name: "blueprintId", type: "uint64" },
|
|
442
|
+
{ name: "operator", type: "address" }
|
|
443
|
+
],
|
|
444
|
+
outputs: [
|
|
445
|
+
{
|
|
446
|
+
name: "preferences",
|
|
447
|
+
type: "tuple",
|
|
448
|
+
components: [
|
|
449
|
+
{ name: "ecdsaPublicKey", type: "bytes" },
|
|
450
|
+
{ name: "rpcAddress", type: "string" }
|
|
451
|
+
]
|
|
452
|
+
}
|
|
453
|
+
],
|
|
454
|
+
stateMutability: "view"
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
type: "event",
|
|
458
|
+
name: "OperatorRegistered",
|
|
459
|
+
inputs: [
|
|
460
|
+
{ name: "blueprintId", type: "uint64", indexed: true },
|
|
461
|
+
{ name: "operator", type: "address", indexed: true },
|
|
462
|
+
{ name: "ecdsaPublicKey", type: "bytes", indexed: false },
|
|
463
|
+
{ name: "rpcAddress", type: "string", indexed: false }
|
|
464
|
+
]
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
type: "event",
|
|
468
|
+
name: "OperatorUnregistered",
|
|
469
|
+
inputs: [
|
|
470
|
+
{ name: "blueprintId", type: "uint64", indexed: true },
|
|
471
|
+
{ name: "operator", type: "address", indexed: true }
|
|
472
|
+
]
|
|
473
|
+
}
|
|
474
|
+
];
|
|
475
|
+
|
|
476
|
+
// src/contracts/publicClient.ts
|
|
477
|
+
import { createPublicClient, http } from "viem";
|
|
478
|
+
import { atom as atom3 } from "nanostores";
|
|
479
|
+
var defaultChainId = Number(getEnvVar("VITE_CHAIN_ID") ?? tangleLocal.id);
|
|
480
|
+
var selectedChainIdStore = persistedAtom({
|
|
481
|
+
key: "bp_selected_chain",
|
|
482
|
+
initial: defaultChainId
|
|
483
|
+
});
|
|
484
|
+
var clientCache = /* @__PURE__ */ new Map();
|
|
485
|
+
function configuredDefaultChainId() {
|
|
486
|
+
const networks = getNetworks();
|
|
487
|
+
if (networks[defaultChainId]) return defaultChainId;
|
|
488
|
+
for (const [chainId, net] of Object.entries(networks)) {
|
|
489
|
+
if (net?.shortLabel === "Local" || net?.label === "Tangle Local" || net?.chain?.name === "Tangle Local") {
|
|
490
|
+
return Number(chainId);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const [firstConfigured] = Object.keys(networks);
|
|
494
|
+
return firstConfigured ? Number(firstConfigured) : defaultChainId;
|
|
495
|
+
}
|
|
496
|
+
function normalizeSelectedChainId(chainId) {
|
|
497
|
+
const networks = getNetworks();
|
|
498
|
+
if (!Object.keys(networks).length) return chainId;
|
|
499
|
+
return networks[chainId] ? chainId : configuredDefaultChainId();
|
|
500
|
+
}
|
|
501
|
+
function sanitizeSelectedChainId() {
|
|
502
|
+
const normalized = normalizeSelectedChainId(selectedChainIdStore.get());
|
|
503
|
+
if (normalized !== selectedChainIdStore.get()) {
|
|
504
|
+
selectedChainIdStore.set(normalized);
|
|
505
|
+
}
|
|
506
|
+
return normalized;
|
|
507
|
+
}
|
|
508
|
+
function getOrCreateClient(chainId) {
|
|
509
|
+
const normalizedChainId = normalizeSelectedChainId(chainId);
|
|
510
|
+
const cached = clientCache.get(normalizedChainId);
|
|
511
|
+
if (cached) return cached;
|
|
512
|
+
const networks = getNetworks();
|
|
513
|
+
const net = networks[normalizedChainId];
|
|
514
|
+
if (!net) {
|
|
515
|
+
const fallback = networks[configuredDefaultChainId()];
|
|
516
|
+
if (!fallback) {
|
|
517
|
+
return createPublicClient({ chain: tangleLocal, transport: http() });
|
|
518
|
+
}
|
|
519
|
+
return createPublicClient({ chain: fallback.chain, transport: http(fallback.rpcUrl) });
|
|
520
|
+
}
|
|
521
|
+
const client = createPublicClient({ chain: net.chain, transport: http(net.rpcUrl) });
|
|
522
|
+
clientCache.set(normalizedChainId, client);
|
|
523
|
+
return client;
|
|
524
|
+
}
|
|
525
|
+
var publicClientStore = atom3(getOrCreateClient(sanitizeSelectedChainId()));
|
|
526
|
+
selectedChainIdStore.subscribe((chainId) => {
|
|
527
|
+
const normalized = normalizeSelectedChainId(chainId);
|
|
528
|
+
if (normalized !== chainId) {
|
|
529
|
+
selectedChainIdStore.set(normalized);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
publicClientStore.set(getOrCreateClient(normalized));
|
|
533
|
+
});
|
|
534
|
+
function getPublicClient() {
|
|
535
|
+
sanitizeSelectedChainId();
|
|
536
|
+
return publicClientStore.get();
|
|
537
|
+
}
|
|
538
|
+
var publicClient = new Proxy({}, {
|
|
539
|
+
get(_target, prop) {
|
|
540
|
+
const client = getOrCreateClient(sanitizeSelectedChainId());
|
|
541
|
+
const value = client[prop];
|
|
542
|
+
return typeof value === "function" ? value.bind(client) : value;
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
function getAddresses() {
|
|
546
|
+
const networks = getNetworks();
|
|
547
|
+
const selectedChainId = sanitizeSelectedChainId();
|
|
548
|
+
const net = networks[selectedChainId];
|
|
549
|
+
return net?.addresses ?? networks[configuredDefaultChainId()]?.addresses ?? {};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/contracts/generic-encoder.ts
|
|
553
|
+
import { encodeAbiParameters } from "viem";
|
|
554
|
+
function encodeJobArgs(job, formValues, context) {
|
|
555
|
+
if (job.customEncoder) {
|
|
556
|
+
return job.customEncoder(formValues, context);
|
|
557
|
+
}
|
|
558
|
+
const abiDefs = [];
|
|
559
|
+
const values = [];
|
|
560
|
+
if (job.contextParams) {
|
|
561
|
+
for (const cp of job.contextParams) {
|
|
562
|
+
abiDefs.push({ name: cp.abiName, type: cp.abiType });
|
|
563
|
+
values.push(coerceValue(context?.[cp.abiName], cp.abiType));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
for (const field of job.fields) {
|
|
567
|
+
if (!field.abiType) continue;
|
|
568
|
+
abiDefs.push({ name: field.abiParam ?? field.name, type: field.abiType });
|
|
569
|
+
values.push(coerceValue(formValues[field.name], field.abiType));
|
|
570
|
+
}
|
|
571
|
+
return encodeAbiParameters(abiDefs, values);
|
|
572
|
+
}
|
|
573
|
+
function coerceValue(value, abiType) {
|
|
574
|
+
switch (abiType) {
|
|
575
|
+
case "bool":
|
|
576
|
+
return Boolean(value);
|
|
577
|
+
case "uint8":
|
|
578
|
+
case "uint16":
|
|
579
|
+
case "uint32":
|
|
580
|
+
return Number(value) || 0;
|
|
581
|
+
case "uint64":
|
|
582
|
+
case "uint128":
|
|
583
|
+
case "uint256":
|
|
584
|
+
return BigInt(Number(value) || 0);
|
|
585
|
+
case "string":
|
|
586
|
+
return String(value ?? "");
|
|
587
|
+
case "string[]":
|
|
588
|
+
if (Array.isArray(value)) return value.map(String);
|
|
589
|
+
return String(value ?? "").split("\n").filter(Boolean);
|
|
590
|
+
case "address[]":
|
|
591
|
+
if (Array.isArray(value)) return value;
|
|
592
|
+
return String(value ?? "").split("\n").map((s) => s.trim()).filter((s) => /^0x[a-fA-F0-9]{40}$/.test(s));
|
|
593
|
+
default:
|
|
594
|
+
return value;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/hooks/useJobForm.ts
|
|
599
|
+
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
600
|
+
function buildDefaults(job) {
|
|
601
|
+
const init = {};
|
|
602
|
+
for (const f of job.fields) {
|
|
603
|
+
if (f.internal) continue;
|
|
604
|
+
if (f.defaultValue !== void 0) {
|
|
605
|
+
init[f.name] = f.defaultValue;
|
|
606
|
+
} else if (f.type === "boolean") {
|
|
607
|
+
init[f.name] = false;
|
|
608
|
+
} else if (f.type === "number") {
|
|
609
|
+
init[f.name] = f.min ?? 0;
|
|
610
|
+
} else {
|
|
611
|
+
init[f.name] = "";
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return init;
|
|
615
|
+
}
|
|
616
|
+
function useJobForm(job) {
|
|
617
|
+
const defaults = useMemo(() => job ? buildDefaults(job) : {}, [job]);
|
|
618
|
+
const [values, setValues] = useState(defaults);
|
|
619
|
+
const [errors, setErrors] = useState({});
|
|
620
|
+
useEffect(() => {
|
|
621
|
+
setValues(defaults);
|
|
622
|
+
setErrors({});
|
|
623
|
+
}, [defaults]);
|
|
624
|
+
const onChange = useCallback((name, value) => {
|
|
625
|
+
setValues((prev) => ({ ...prev, [name]: value }));
|
|
626
|
+
setErrors((prev) => {
|
|
627
|
+
if (!prev[name]) return prev;
|
|
628
|
+
const next = { ...prev };
|
|
629
|
+
delete next[name];
|
|
630
|
+
return next;
|
|
631
|
+
});
|
|
632
|
+
}, []);
|
|
633
|
+
const validate = useCallback(() => {
|
|
634
|
+
if (!job) return false;
|
|
635
|
+
const errs = {};
|
|
636
|
+
for (const f of job.fields) {
|
|
637
|
+
if (f.internal) continue;
|
|
638
|
+
const v = values[f.name];
|
|
639
|
+
if (f.required && (v === void 0 || v === null || v === "")) {
|
|
640
|
+
errs[f.name] = `${f.label} is required`;
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
if (f.type === "number" && typeof v === "number") {
|
|
644
|
+
if (f.min != null && v < f.min) {
|
|
645
|
+
errs[f.name] = `${f.label} must be at least ${f.min}`;
|
|
646
|
+
} else if (f.max != null && v > f.max) {
|
|
647
|
+
errs[f.name] = `${f.label} must be at most ${f.max}`;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
setErrors(errs);
|
|
652
|
+
return Object.keys(errs).length === 0;
|
|
653
|
+
}, [job, values]);
|
|
654
|
+
const reset = useCallback(() => {
|
|
655
|
+
setValues(defaults);
|
|
656
|
+
setErrors({});
|
|
657
|
+
}, [defaults]);
|
|
658
|
+
return { values, errors, onChange, validate, reset };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/hooks/useQuotes.ts
|
|
662
|
+
import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
|
|
663
|
+
import { sha256 as viemSha256, toHex } from "viem";
|
|
664
|
+
var POW_DIFFICULTY = 20;
|
|
665
|
+
var WEI_PER_TNT = 1e18;
|
|
666
|
+
var RESOURCE_KIND_TO_ID = {
|
|
667
|
+
CPU: 0,
|
|
668
|
+
MemoryMB: 1,
|
|
669
|
+
StorageMB: 2,
|
|
670
|
+
NetworkEgressMB: 3,
|
|
671
|
+
NetworkIngressMB: 4,
|
|
672
|
+
GPU: 5
|
|
673
|
+
};
|
|
674
|
+
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
675
|
+
var DEFAULT_RESOURCE_REQUIREMENTS = [
|
|
676
|
+
{ kind: "CPU", count: 1 },
|
|
677
|
+
{ kind: "MemoryMB", count: 1024 },
|
|
678
|
+
{ kind: "StorageMB", count: 10240 }
|
|
679
|
+
];
|
|
680
|
+
function sha256(data) {
|
|
681
|
+
return viemSha256(data, "bytes");
|
|
682
|
+
}
|
|
683
|
+
function generateChallenge(blueprintId, timestamp) {
|
|
684
|
+
const input = new Uint8Array(16);
|
|
685
|
+
const view = new DataView(input.buffer);
|
|
686
|
+
view.setBigUint64(0, blueprintId, false);
|
|
687
|
+
view.setBigUint64(8, timestamp, false);
|
|
688
|
+
return sha256(input);
|
|
689
|
+
}
|
|
690
|
+
function checkDifficulty(hash, difficulty) {
|
|
691
|
+
const zeroBytes = Math.floor(difficulty / 8);
|
|
692
|
+
const zeroBits = difficulty % 8;
|
|
693
|
+
for (let i = 0; i < zeroBytes; i++) {
|
|
694
|
+
if (hash[i] !== 0) return false;
|
|
695
|
+
}
|
|
696
|
+
if (zeroBits > 0) {
|
|
697
|
+
const mask = 255 << 8 - zeroBits;
|
|
698
|
+
if ((hash[zeroBytes] & mask) !== 0) return false;
|
|
699
|
+
}
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
async function solvePoW(blueprintId, timestamp) {
|
|
703
|
+
const challenge = generateChallenge(blueprintId, timestamp);
|
|
704
|
+
const buf = new Uint8Array(challenge.length + 8);
|
|
705
|
+
buf.set(challenge, 0);
|
|
706
|
+
const view = new DataView(buf.buffer);
|
|
707
|
+
for (let nonce = 0; nonce < 4294967296; nonce++) {
|
|
708
|
+
view.setBigUint64(challenge.length, BigInt(nonce), false);
|
|
709
|
+
const hash = sha256(buf);
|
|
710
|
+
if (checkDifficulty(hash, POW_DIFFICULTY)) {
|
|
711
|
+
const proof = new Uint8Array(8 + 32 + 8);
|
|
712
|
+
const pv = new DataView(proof.buffer);
|
|
713
|
+
pv.setBigUint64(0, 32n, true);
|
|
714
|
+
proof.set(hash, 8);
|
|
715
|
+
pv.setBigUint64(40, BigInt(nonce), true);
|
|
716
|
+
return { hash, nonce, proof };
|
|
717
|
+
}
|
|
718
|
+
if (nonce % 5e3 === 0 && nonce > 0) {
|
|
719
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
throw new Error("PoW: exhausted nonce space");
|
|
723
|
+
}
|
|
724
|
+
function formatCost(totalCost) {
|
|
725
|
+
const tnt = Number(totalCost) / WEI_PER_TNT;
|
|
726
|
+
if (tnt === 0) return "0 TNT";
|
|
727
|
+
if (tnt < 1e-3) return `${(tnt * 1e6).toFixed(2)} \u03BCTNT`;
|
|
728
|
+
if (tnt < 0.01) return `${(tnt * 1e3).toFixed(2)} mTNT`;
|
|
729
|
+
if (tnt < 1e3) return `${tnt.toFixed(4)} TNT`;
|
|
730
|
+
return `${tnt.toLocaleString(void 0, { maximumFractionDigits: 2 })} TNT`;
|
|
731
|
+
}
|
|
732
|
+
function quoteConfidentiality(requireTee) {
|
|
733
|
+
return requireTee ? 1 : 0;
|
|
734
|
+
}
|
|
735
|
+
function resourceKindToId(kind) {
|
|
736
|
+
const mapped = RESOURCE_KIND_TO_ID[kind];
|
|
737
|
+
if (mapped === void 0) {
|
|
738
|
+
throw new Error(`Unsupported resource kind in quote: ${kind}`);
|
|
739
|
+
}
|
|
740
|
+
return mapped;
|
|
741
|
+
}
|
|
742
|
+
function mapJsonSecurityCommitment(sc) {
|
|
743
|
+
return {
|
|
744
|
+
asset: {
|
|
745
|
+
kind: sc.asset?.kind ?? 0,
|
|
746
|
+
token: sc.asset?.token ?? ZERO_ADDRESS
|
|
747
|
+
},
|
|
748
|
+
exposureBps: sc.exposure_bps ?? 0
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
function mapJsonResourceCommitment(resource) {
|
|
752
|
+
return {
|
|
753
|
+
kind: resourceKindToId(String(resource.kind ?? "CPU")),
|
|
754
|
+
count: BigInt(resource.count ?? 0)
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function useQuotes(operators, blueprintId, ttlBlocks, enabled, requireTee = false) {
|
|
758
|
+
const [quotes, setQuotes] = useState2([]);
|
|
759
|
+
const [isLoading, setIsLoading] = useState2(false);
|
|
760
|
+
const [isSolvingPow, setIsSolvingPow] = useState2(false);
|
|
761
|
+
const [errors, setErrors] = useState2(/* @__PURE__ */ new Map());
|
|
762
|
+
const [fetchKey, setFetchKey] = useState2(0);
|
|
763
|
+
const refetch = useCallback2(() => setFetchKey((k) => k + 1), []);
|
|
764
|
+
useEffect2(() => {
|
|
765
|
+
if (!enabled || operators.length === 0) {
|
|
766
|
+
setQuotes((prev) => prev.length === 0 ? prev : []);
|
|
767
|
+
setErrors((prev) => prev.size === 0 ? prev : /* @__PURE__ */ new Map());
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
let cancelled = false;
|
|
771
|
+
setIsLoading(true);
|
|
772
|
+
setIsSolvingPow(true);
|
|
773
|
+
setQuotes([]);
|
|
774
|
+
setErrors(/* @__PURE__ */ new Map());
|
|
775
|
+
async function fetchQuotes() {
|
|
776
|
+
const results = [];
|
|
777
|
+
const errs = /* @__PURE__ */ new Map();
|
|
778
|
+
const promises = operators.map(async (op) => {
|
|
779
|
+
try {
|
|
780
|
+
if (!op.rpcAddress) throw new Error("No RPC address registered");
|
|
781
|
+
const rpcUrl2 = resolveOperatorRpc(op.rpcAddress);
|
|
782
|
+
const timestamp = BigInt(Math.floor(Date.now() / 1e3));
|
|
783
|
+
if (!cancelled) setIsSolvingPow(true);
|
|
784
|
+
const { proof } = await solvePoW(blueprintId, timestamp);
|
|
785
|
+
if (!cancelled) setIsSolvingPow(false);
|
|
786
|
+
const response = await fetchPriceFromOperator(rpcUrl2, {
|
|
787
|
+
blueprintId,
|
|
788
|
+
ttlBlocks,
|
|
789
|
+
proofOfWork: proof,
|
|
790
|
+
challengeTimestamp: timestamp,
|
|
791
|
+
requireTee
|
|
792
|
+
});
|
|
793
|
+
if (!response) throw new Error("No quote returned from operator");
|
|
794
|
+
if (!cancelled) results.push(response);
|
|
795
|
+
} catch (err) {
|
|
796
|
+
if (!cancelled) {
|
|
797
|
+
errs.set(op.address, err instanceof Error ? err.message : String(err));
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
await Promise.allSettled(promises);
|
|
802
|
+
if (!cancelled) {
|
|
803
|
+
setQuotes(results);
|
|
804
|
+
setErrors(errs);
|
|
805
|
+
setIsLoading(false);
|
|
806
|
+
setIsSolvingPow(false);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
fetchQuotes();
|
|
810
|
+
return () => {
|
|
811
|
+
cancelled = true;
|
|
812
|
+
};
|
|
813
|
+
}, [operators, blueprintId, ttlBlocks, enabled, fetchKey, requireTee]);
|
|
814
|
+
const totalCost = quotes.reduce((sum, q) => sum + q.totalCost, 0n);
|
|
815
|
+
return { quotes, isLoading, isSolvingPow, errors, totalCost, refetch };
|
|
816
|
+
}
|
|
817
|
+
async function fetchPriceFromOperator(rpcUrl2, params) {
|
|
818
|
+
try {
|
|
819
|
+
const response = await fetch(`${rpcUrl2}/pricing/quote`, {
|
|
820
|
+
method: "POST",
|
|
821
|
+
headers: { "Content-Type": "application/json" },
|
|
822
|
+
body: JSON.stringify({
|
|
823
|
+
blueprint_id: String(params.blueprintId),
|
|
824
|
+
ttl_blocks: String(params.ttlBlocks),
|
|
825
|
+
proof_of_work: toHex(params.proofOfWork),
|
|
826
|
+
challenge_timestamp: String(params.challengeTimestamp),
|
|
827
|
+
require_tee: params.requireTee,
|
|
828
|
+
resource_requirements: DEFAULT_RESOURCE_REQUIREMENTS
|
|
829
|
+
}),
|
|
830
|
+
signal: AbortSignal.timeout(1e4)
|
|
831
|
+
});
|
|
832
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
833
|
+
const data = await response.json();
|
|
834
|
+
return {
|
|
835
|
+
operator: data.operator,
|
|
836
|
+
totalCost: BigInt(data.total_cost ?? "0"),
|
|
837
|
+
signature: data.signature ?? "0x",
|
|
838
|
+
costRate: Number(data.cost_rate ?? 0),
|
|
839
|
+
teeAttested: Boolean(data.tee_attested),
|
|
840
|
+
teeProvider: data.tee_provider || void 0,
|
|
841
|
+
details: {
|
|
842
|
+
blueprintId: BigInt(data.details?.blueprint_id ?? params.blueprintId),
|
|
843
|
+
ttlBlocks: BigInt(data.details?.ttl_blocks ?? params.ttlBlocks),
|
|
844
|
+
totalCost: BigInt(data.details?.total_cost ?? "0"),
|
|
845
|
+
timestamp: BigInt(data.details?.timestamp ?? params.challengeTimestamp),
|
|
846
|
+
expiry: BigInt(data.details?.expiry ?? "0"),
|
|
847
|
+
confidentiality: Number(data.details?.confidentiality ?? quoteConfidentiality(params.requireTee)),
|
|
848
|
+
securityCommitments: (data.details?.security_commitments ?? []).map(mapJsonSecurityCommitment),
|
|
849
|
+
resourceCommitments: (data.details?.resources ?? []).map(mapJsonResourceCommitment)
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
} catch {
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/hooks/useJobPrice.ts
|
|
858
|
+
import { useState as useState3, useEffect as useEffect3, useCallback as useCallback3, useRef } from "react";
|
|
859
|
+
import { toHex as toHex2 } from "viem";
|
|
860
|
+
function resolveOperatorRpc2(raw) {
|
|
861
|
+
if (typeof window === "undefined") return raw;
|
|
862
|
+
const withProto = raw.includes("://") ? raw : `http://${raw}`;
|
|
863
|
+
try {
|
|
864
|
+
const url = new URL(withProto);
|
|
865
|
+
const pageHost = window.location.hostname;
|
|
866
|
+
const isNonRoutable = url.hostname.endsWith(".local") || !url.hostname.includes(".") || url.hostname === "127.0.0.1" || url.hostname === "localhost";
|
|
867
|
+
if (isNonRoutable && pageHost !== url.hostname) {
|
|
868
|
+
url.hostname = pageHost;
|
|
869
|
+
}
|
|
870
|
+
return url.toString().replace(/\/$/, "");
|
|
871
|
+
} catch {
|
|
872
|
+
return withProto;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function useJobPrice(operatorRpcUrl, serviceId, jobIndex, blueprintId, enabled) {
|
|
876
|
+
const [quote, setQuote] = useState3(null);
|
|
877
|
+
const [isLoading, setIsLoading] = useState3(false);
|
|
878
|
+
const [isSolvingPow, setIsSolvingPow] = useState3(false);
|
|
879
|
+
const [error, setError] = useState3(null);
|
|
880
|
+
const [fetchKey, setFetchKey] = useState3(0);
|
|
881
|
+
const cancelledRef = useRef(false);
|
|
882
|
+
const refetch = useCallback3(() => setFetchKey((k) => k + 1), []);
|
|
883
|
+
useEffect3(() => {
|
|
884
|
+
if (!enabled || !operatorRpcUrl || serviceId === 0n) {
|
|
885
|
+
setQuote(null);
|
|
886
|
+
setError(null);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
cancelledRef.current = false;
|
|
890
|
+
setIsLoading(true);
|
|
891
|
+
setError(null);
|
|
892
|
+
setQuote(null);
|
|
893
|
+
async function fetchJobQuote() {
|
|
894
|
+
try {
|
|
895
|
+
const rpcUrl2 = resolveOperatorRpc2(operatorRpcUrl);
|
|
896
|
+
const timestamp = BigInt(Math.floor(Date.now() / 1e3));
|
|
897
|
+
setIsSolvingPow(true);
|
|
898
|
+
const { proof } = await solvePoW(blueprintId, timestamp);
|
|
899
|
+
if (cancelledRef.current) return;
|
|
900
|
+
setIsSolvingPow(false);
|
|
901
|
+
const response = await fetch(`${rpcUrl2}/pricing/job-quote`, {
|
|
902
|
+
method: "POST",
|
|
903
|
+
headers: { "Content-Type": "application/json" },
|
|
904
|
+
body: JSON.stringify({
|
|
905
|
+
service_id: String(serviceId),
|
|
906
|
+
job_index: jobIndex,
|
|
907
|
+
proof_of_work: toHex2(proof),
|
|
908
|
+
challenge_timestamp: String(timestamp)
|
|
909
|
+
}),
|
|
910
|
+
signal: AbortSignal.timeout(1e4)
|
|
911
|
+
});
|
|
912
|
+
if (!response.ok) {
|
|
913
|
+
throw new Error(`HTTP ${response.status}: ${await response.text().catch(() => "Unknown error")}`);
|
|
914
|
+
}
|
|
915
|
+
const data = await response.json();
|
|
916
|
+
if (cancelledRef.current) return;
|
|
917
|
+
setQuote({
|
|
918
|
+
serviceId: BigInt(data.service_id ?? serviceId),
|
|
919
|
+
jobIndex: data.job_index ?? jobIndex,
|
|
920
|
+
price: BigInt(data.price ?? "0"),
|
|
921
|
+
timestamp: BigInt(data.timestamp ?? timestamp),
|
|
922
|
+
expiry: BigInt(data.expiry ?? "0"),
|
|
923
|
+
signature: data.signature ?? "0x",
|
|
924
|
+
operatorAddress: data.operator
|
|
925
|
+
});
|
|
926
|
+
} catch (err) {
|
|
927
|
+
if (!cancelledRef.current) {
|
|
928
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
929
|
+
}
|
|
930
|
+
} finally {
|
|
931
|
+
if (!cancelledRef.current) {
|
|
932
|
+
setIsLoading(false);
|
|
933
|
+
setIsSolvingPow(false);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
fetchJobQuote();
|
|
938
|
+
return () => {
|
|
939
|
+
cancelledRef.current = true;
|
|
940
|
+
};
|
|
941
|
+
}, [operatorRpcUrl, serviceId, jobIndex, blueprintId, enabled, fetchKey]);
|
|
942
|
+
const formattedPrice = quote ? formatCost(quote.price) : "--";
|
|
943
|
+
return { quote, isLoading, isSolvingPow, error, formattedPrice, refetch };
|
|
944
|
+
}
|
|
945
|
+
function useJobPrices(operatorRpcUrl, serviceId, blueprintId, jobIndexes, enabled) {
|
|
946
|
+
const [prices, setPrices] = useState3([]);
|
|
947
|
+
const [isLoading, setIsLoading] = useState3(false);
|
|
948
|
+
const [error, setError] = useState3(null);
|
|
949
|
+
const [fetchKey, setFetchKey] = useState3(0);
|
|
950
|
+
const refetch = useCallback3(() => setFetchKey((k) => k + 1), []);
|
|
951
|
+
useEffect3(() => {
|
|
952
|
+
if (!enabled || !operatorRpcUrl || serviceId === 0n || jobIndexes.length === 0) {
|
|
953
|
+
setPrices([]);
|
|
954
|
+
setError(null);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
let cancelled = false;
|
|
958
|
+
setIsLoading(true);
|
|
959
|
+
setError(null);
|
|
960
|
+
async function fetchAllPrices() {
|
|
961
|
+
try {
|
|
962
|
+
const rpcUrl2 = resolveOperatorRpc2(operatorRpcUrl);
|
|
963
|
+
const timestamp = BigInt(Math.floor(Date.now() / 1e3));
|
|
964
|
+
const { proof } = await solvePoW(blueprintId, timestamp);
|
|
965
|
+
if (cancelled) return;
|
|
966
|
+
const results = await Promise.allSettled(
|
|
967
|
+
jobIndexes.map(async (job) => {
|
|
968
|
+
const response = await fetch(`${rpcUrl2}/pricing/job-quote`, {
|
|
969
|
+
method: "POST",
|
|
970
|
+
headers: { "Content-Type": "application/json" },
|
|
971
|
+
body: JSON.stringify({
|
|
972
|
+
service_id: String(serviceId),
|
|
973
|
+
job_index: job.index,
|
|
974
|
+
proof_of_work: toHex2(proof),
|
|
975
|
+
challenge_timestamp: String(timestamp)
|
|
976
|
+
}),
|
|
977
|
+
signal: AbortSignal.timeout(1e4)
|
|
978
|
+
});
|
|
979
|
+
if (!response.ok) return null;
|
|
980
|
+
return response.json();
|
|
981
|
+
})
|
|
982
|
+
);
|
|
983
|
+
if (cancelled) return;
|
|
984
|
+
const entries = jobIndexes.map((job, i) => {
|
|
985
|
+
const result = results[i];
|
|
986
|
+
if (result.status === "fulfilled" && result.value) {
|
|
987
|
+
const data = result.value;
|
|
988
|
+
const price = BigInt(data.price ?? "0");
|
|
989
|
+
return {
|
|
990
|
+
jobIndex: job.index,
|
|
991
|
+
jobName: job.name,
|
|
992
|
+
price,
|
|
993
|
+
formattedPrice: formatCost(price),
|
|
994
|
+
mode: data.mode ?? "flat",
|
|
995
|
+
quote: {
|
|
996
|
+
serviceId: BigInt(data.service_id ?? serviceId),
|
|
997
|
+
jobIndex: job.index,
|
|
998
|
+
price,
|
|
999
|
+
timestamp: BigInt(data.timestamp ?? timestamp),
|
|
1000
|
+
expiry: BigInt(data.expiry ?? "0"),
|
|
1001
|
+
signature: data.signature ?? "0x",
|
|
1002
|
+
operatorAddress: data.operator
|
|
1003
|
+
},
|
|
1004
|
+
error: null
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
const estimatedPrice = BigInt(job.multiplier) * 1000000000000000n;
|
|
1008
|
+
return {
|
|
1009
|
+
jobIndex: job.index,
|
|
1010
|
+
jobName: job.name,
|
|
1011
|
+
price: estimatedPrice,
|
|
1012
|
+
formattedPrice: `~${formatCost(estimatedPrice)}`,
|
|
1013
|
+
mode: "flat",
|
|
1014
|
+
quote: null,
|
|
1015
|
+
error: "No RFQ response \u2014 showing estimate"
|
|
1016
|
+
};
|
|
1017
|
+
});
|
|
1018
|
+
setPrices(entries);
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
if (!cancelled) {
|
|
1021
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1022
|
+
}
|
|
1023
|
+
} finally {
|
|
1024
|
+
if (!cancelled) {
|
|
1025
|
+
setIsLoading(false);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
fetchAllPrices();
|
|
1030
|
+
return () => {
|
|
1031
|
+
cancelled = true;
|
|
1032
|
+
};
|
|
1033
|
+
}, [operatorRpcUrl, serviceId, blueprintId, jobIndexes, enabled, fetchKey]);
|
|
1034
|
+
return { prices, isLoading, error, refetch };
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// src/hooks/useSubmitJob.ts
|
|
1038
|
+
import { useCallback as useCallback4, useState as useState4, useMemo as useMemo2 } from "react";
|
|
1039
|
+
import { useAccount, useWriteContract, useWaitForTransactionReceipt } from "wagmi";
|
|
1040
|
+
import { decodeEventLog } from "viem";
|
|
1041
|
+
import { useEffect as useEffect4 } from "react";
|
|
1042
|
+
function useSubmitJob() {
|
|
1043
|
+
const { address } = useAccount();
|
|
1044
|
+
const { writeContractAsync, data: hash, isPending: isSigning } = useWriteContract();
|
|
1045
|
+
const [status, setStatus] = useState4("idle");
|
|
1046
|
+
const [error, setError] = useState4(null);
|
|
1047
|
+
const [txHash, setTxHash] = useState4();
|
|
1048
|
+
const { data: receipt, isSuccess, isError } = useWaitForTransactionReceipt({
|
|
1049
|
+
hash: txHash
|
|
1050
|
+
});
|
|
1051
|
+
const callId = useMemo2(() => {
|
|
1052
|
+
if (!receipt?.logs) return null;
|
|
1053
|
+
for (const log of receipt.logs) {
|
|
1054
|
+
try {
|
|
1055
|
+
const decoded = decodeEventLog({
|
|
1056
|
+
abi: tangleJobsAbi,
|
|
1057
|
+
data: log.data,
|
|
1058
|
+
topics: log.topics
|
|
1059
|
+
});
|
|
1060
|
+
if (decoded.eventName === "JobCalled" && "callId" in decoded.args) {
|
|
1061
|
+
return Number(decoded.args.callId);
|
|
1062
|
+
}
|
|
1063
|
+
} catch {
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
return null;
|
|
1067
|
+
}, [receipt]);
|
|
1068
|
+
useEffect4(() => {
|
|
1069
|
+
if (isSuccess && txHash) {
|
|
1070
|
+
setStatus("confirmed");
|
|
1071
|
+
updateTx(txHash, { status: "confirmed" });
|
|
1072
|
+
}
|
|
1073
|
+
if (isError && txHash) {
|
|
1074
|
+
setStatus("failed");
|
|
1075
|
+
updateTx(txHash, { status: "failed" });
|
|
1076
|
+
}
|
|
1077
|
+
}, [isSuccess, isError, txHash]);
|
|
1078
|
+
const submitJob = useCallback4(
|
|
1079
|
+
async (opts) => {
|
|
1080
|
+
if (!address) {
|
|
1081
|
+
setError("Wallet not connected");
|
|
1082
|
+
return void 0;
|
|
1083
|
+
}
|
|
1084
|
+
const addrs = getAddresses();
|
|
1085
|
+
const label = opts.label ?? `Job #${opts.jobId}`;
|
|
1086
|
+
try {
|
|
1087
|
+
setStatus("signing");
|
|
1088
|
+
setError(null);
|
|
1089
|
+
const result = await writeContractAsync({
|
|
1090
|
+
address: addrs.jobs,
|
|
1091
|
+
abi: tangleJobsAbi,
|
|
1092
|
+
functionName: "submitJob",
|
|
1093
|
+
args: [opts.serviceId, opts.jobId, opts.args],
|
|
1094
|
+
value: opts.value
|
|
1095
|
+
});
|
|
1096
|
+
setTxHash(result);
|
|
1097
|
+
setStatus("pending");
|
|
1098
|
+
addTx(result, label, selectedChainIdStore.get());
|
|
1099
|
+
return result;
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
setStatus("failed");
|
|
1102
|
+
const msg = err?.shortMessage ?? err?.message ?? "Transaction failed";
|
|
1103
|
+
setError(msg);
|
|
1104
|
+
return void 0;
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
[address, writeContractAsync]
|
|
1108
|
+
);
|
|
1109
|
+
const reset = useCallback4(() => {
|
|
1110
|
+
setStatus("idle");
|
|
1111
|
+
setError(null);
|
|
1112
|
+
setTxHash(void 0);
|
|
1113
|
+
}, []);
|
|
1114
|
+
return {
|
|
1115
|
+
submitJob,
|
|
1116
|
+
reset,
|
|
1117
|
+
status,
|
|
1118
|
+
error,
|
|
1119
|
+
txHash,
|
|
1120
|
+
callId,
|
|
1121
|
+
isSigning,
|
|
1122
|
+
isConnected: !!address
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// src/hooks/useThemeValue.ts
|
|
1127
|
+
import { useStore } from "@nanostores/react";
|
|
1128
|
+
function useThemeValue() {
|
|
1129
|
+
return useStore(themeStore);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
export {
|
|
1133
|
+
resolveOperatorRpc,
|
|
1134
|
+
getEnvVar,
|
|
1135
|
+
mainnet,
|
|
1136
|
+
resolveRpcUrl,
|
|
1137
|
+
rpcUrl,
|
|
1138
|
+
createTangleLocalChain,
|
|
1139
|
+
tangleLocal,
|
|
1140
|
+
tangleTestnet,
|
|
1141
|
+
tangleMainnet,
|
|
1142
|
+
configureNetworks,
|
|
1143
|
+
getNetworks,
|
|
1144
|
+
allTangleChains,
|
|
1145
|
+
serializeWithBigInt,
|
|
1146
|
+
deserializeWithBigInt,
|
|
1147
|
+
persistedAtom,
|
|
1148
|
+
txListStore,
|
|
1149
|
+
pendingCount,
|
|
1150
|
+
addTx,
|
|
1151
|
+
updateTx,
|
|
1152
|
+
clearTxs,
|
|
1153
|
+
infraStore,
|
|
1154
|
+
updateInfra,
|
|
1155
|
+
getInfra,
|
|
1156
|
+
kTheme,
|
|
1157
|
+
DEFAULT_THEME,
|
|
1158
|
+
themeStore,
|
|
1159
|
+
themeIsDark,
|
|
1160
|
+
toggleTheme,
|
|
1161
|
+
tangleJobsAbi,
|
|
1162
|
+
tangleServicesAbi,
|
|
1163
|
+
tangleOperatorsAbi,
|
|
1164
|
+
selectedChainIdStore,
|
|
1165
|
+
sanitizeSelectedChainId,
|
|
1166
|
+
publicClientStore,
|
|
1167
|
+
getPublicClient,
|
|
1168
|
+
publicClient,
|
|
1169
|
+
getAddresses,
|
|
1170
|
+
encodeJobArgs,
|
|
1171
|
+
useJobForm,
|
|
1172
|
+
solvePoW,
|
|
1173
|
+
formatCost,
|
|
1174
|
+
useQuotes,
|
|
1175
|
+
useJobPrice,
|
|
1176
|
+
useJobPrices,
|
|
1177
|
+
useSubmitJob,
|
|
1178
|
+
useThemeValue
|
|
1179
|
+
};
|
|
1180
|
+
//# sourceMappingURL=chunk-A6PJT5YQ.js.map
|