@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
|
@@ -9,9 +9,208 @@ export interface DiscoveredOperator {
|
|
|
9
9
|
rpcAddress: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
interface OperatorDiscoveryClient {
|
|
13
|
+
readContract(args: Record<string, unknown>): Promise<unknown>;
|
|
14
|
+
getLogs(args: Record<string, unknown>): Promise<Array<Record<string, unknown>>>;
|
|
15
|
+
multicall(args: Record<string, unknown>): Promise<Array<Record<string, unknown>>>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface OperatorDiscoveryResult {
|
|
19
|
+
operators: DiscoveredOperator[];
|
|
20
|
+
operatorCount: bigint;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readPreferenceValue(
|
|
24
|
+
result: unknown,
|
|
25
|
+
field: 'ecdsaPublicKey' | 'rpcAddress',
|
|
26
|
+
): string {
|
|
27
|
+
if (Array.isArray(result)) {
|
|
28
|
+
return String(result[field === 'ecdsaPublicKey' ? 0 : 1] ?? '');
|
|
29
|
+
}
|
|
30
|
+
if (result && typeof result === 'object') {
|
|
31
|
+
return String((result as Record<string, unknown>)[field] ?? '');
|
|
32
|
+
}
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function verifyCandidatesWithMulticall(
|
|
37
|
+
client: OperatorDiscoveryClient,
|
|
38
|
+
servicesAddress: Address,
|
|
39
|
+
blueprintId: bigint,
|
|
40
|
+
candidates: DiscoveredOperator[],
|
|
41
|
+
): Promise<DiscoveredOperator[] | null> {
|
|
42
|
+
const [registrationResults, preferencesResults] = await Promise.all([
|
|
43
|
+
client.multicall({
|
|
44
|
+
contracts: candidates.map((op) => ({
|
|
45
|
+
address: servicesAddress,
|
|
46
|
+
abi: tangleOperatorsAbi,
|
|
47
|
+
functionName: 'isOperatorRegistered' as const,
|
|
48
|
+
args: [blueprintId, op.address] as const,
|
|
49
|
+
})),
|
|
50
|
+
}),
|
|
51
|
+
client.multicall({
|
|
52
|
+
contracts: candidates.map((op) => ({
|
|
53
|
+
address: servicesAddress,
|
|
54
|
+
abi: tangleOperatorsAbi,
|
|
55
|
+
functionName: 'getOperatorPreferences' as const,
|
|
56
|
+
args: [blueprintId, op.address] as const,
|
|
57
|
+
})),
|
|
58
|
+
}),
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const hadRegistrationFailure = registrationResults.some((result) => result?.status === 'failure');
|
|
62
|
+
const hadPreferenceFailure = preferencesResults.some((result) => result?.status === 'failure');
|
|
63
|
+
|
|
64
|
+
const active: DiscoveredOperator[] = [];
|
|
65
|
+
candidates.forEach((op, i) => {
|
|
66
|
+
if (registrationResults[i]?.result !== true) return;
|
|
67
|
+
const prefs = preferencesResults[i];
|
|
68
|
+
if (prefs?.status === 'success' && prefs.result != null) {
|
|
69
|
+
active.push({
|
|
70
|
+
...op,
|
|
71
|
+
ecdsaPublicKey: readPreferenceValue(prefs.result, 'ecdsaPublicKey') || op.ecdsaPublicKey,
|
|
72
|
+
rpcAddress: readPreferenceValue(prefs.result, 'rpcAddress') || op.rpcAddress,
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
active.push(op);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (active.length === 0 && (hadRegistrationFailure || hadPreferenceFailure)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return active;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function verifyCandidatesDirectly(
|
|
87
|
+
client: OperatorDiscoveryClient,
|
|
88
|
+
servicesAddress: Address,
|
|
89
|
+
blueprintId: bigint,
|
|
90
|
+
candidates: DiscoveredOperator[],
|
|
91
|
+
): Promise<DiscoveredOperator[]> {
|
|
92
|
+
const results = await Promise.allSettled(
|
|
93
|
+
candidates.map(async (op) => {
|
|
94
|
+
const registration = await client.readContract({
|
|
95
|
+
address: servicesAddress,
|
|
96
|
+
abi: tangleOperatorsAbi,
|
|
97
|
+
functionName: 'isOperatorRegistered',
|
|
98
|
+
args: [blueprintId, op.address],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (registration !== true) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const preferences = await client.readContract({
|
|
107
|
+
address: servicesAddress,
|
|
108
|
+
abi: tangleOperatorsAbi,
|
|
109
|
+
functionName: 'getOperatorPreferences',
|
|
110
|
+
args: [blueprintId, op.address],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
...op,
|
|
115
|
+
ecdsaPublicKey: readPreferenceValue(preferences, 'ecdsaPublicKey') || op.ecdsaPublicKey,
|
|
116
|
+
rpcAddress: readPreferenceValue(preferences, 'rpcAddress') || op.rpcAddress,
|
|
117
|
+
};
|
|
118
|
+
} catch {
|
|
119
|
+
return op;
|
|
120
|
+
}
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const active = results
|
|
125
|
+
.filter((result): result is PromiseFulfilledResult<DiscoveredOperator | null> => result.status === 'fulfilled')
|
|
126
|
+
.map((result) => result.value)
|
|
127
|
+
.filter((op): op is DiscoveredOperator => op != null);
|
|
128
|
+
|
|
129
|
+
if (active.length > 0) {
|
|
130
|
+
return active;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const failure = results.find((result): result is PromiseRejectedResult => result.status === 'rejected');
|
|
134
|
+
if (failure) {
|
|
135
|
+
throw failure.reason instanceof Error ? failure.reason : new Error(String(failure.reason));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Shared discovery logic for operator lookup.
|
|
143
|
+
* Tries multicall first for efficiency, then falls back to direct reads when
|
|
144
|
+
* multicall is unavailable in local/dev environments.
|
|
145
|
+
*/
|
|
146
|
+
export async function discoverOperatorsWithClient(
|
|
147
|
+
client: OperatorDiscoveryClient,
|
|
148
|
+
servicesAddress: Address,
|
|
149
|
+
blueprintId: bigint,
|
|
150
|
+
): Promise<OperatorDiscoveryResult> {
|
|
151
|
+
const count = await client.readContract({
|
|
152
|
+
address: servicesAddress,
|
|
153
|
+
abi: tangleOperatorsAbi,
|
|
154
|
+
functionName: 'blueprintOperatorCount',
|
|
155
|
+
args: [blueprintId],
|
|
156
|
+
});
|
|
157
|
+
const operatorCount = count as bigint;
|
|
158
|
+
|
|
159
|
+
if (operatorCount === 0n) {
|
|
160
|
+
return { operators: [], operatorCount };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const registeredLogs = await client.getLogs({
|
|
164
|
+
address: servicesAddress,
|
|
165
|
+
event: {
|
|
166
|
+
type: 'event' as const,
|
|
167
|
+
name: 'OperatorRegistered',
|
|
168
|
+
inputs: [
|
|
169
|
+
{ name: 'blueprintId', type: 'uint64', indexed: true },
|
|
170
|
+
{ name: 'operator', type: 'address', indexed: true },
|
|
171
|
+
{ name: 'ecdsaPublicKey', type: 'bytes', indexed: false },
|
|
172
|
+
{ name: 'rpcAddress', type: 'string', indexed: false },
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
args: { blueprintId },
|
|
176
|
+
fromBlock: 0n,
|
|
177
|
+
toBlock: 'latest',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const byAddress = new Map<Address, DiscoveredOperator>();
|
|
181
|
+
for (const log of registeredLogs) {
|
|
182
|
+
const args = (log.args ?? {}) as Record<string, unknown>;
|
|
183
|
+
const addr = args.operator as Address | undefined;
|
|
184
|
+
if (!addr) continue;
|
|
185
|
+
byAddress.set(addr, {
|
|
186
|
+
address: addr,
|
|
187
|
+
ecdsaPublicKey: String(args.ecdsaPublicKey ?? '0x'),
|
|
188
|
+
rpcAddress: String(args.rpcAddress ?? ''),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const candidates = Array.from(byAddress.values());
|
|
193
|
+
if (candidates.length === 0) {
|
|
194
|
+
return { operators: [], operatorCount };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const active = await verifyCandidatesWithMulticall(client, servicesAddress, blueprintId, candidates);
|
|
199
|
+
if (active != null) {
|
|
200
|
+
return { operators: active, operatorCount };
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// Fall through to direct reads below.
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const active = await verifyCandidatesDirectly(client, servicesAddress, blueprintId, candidates);
|
|
207
|
+
return { operators: active, operatorCount };
|
|
208
|
+
}
|
|
209
|
+
|
|
12
210
|
/**
|
|
13
211
|
* Discover operators registered for a blueprint by scanning OperatorRegistered
|
|
14
|
-
* events, then verifying each is still active
|
|
212
|
+
* events, then verifying each is still active. Falls back to direct reads when
|
|
213
|
+
* multicall is unavailable.
|
|
15
214
|
*/
|
|
16
215
|
export function useOperators(blueprintId: bigint) {
|
|
17
216
|
const [operators, setOperators] = useState<DiscoveredOperator[]>([]);
|
|
@@ -28,102 +227,10 @@ export function useOperators(blueprintId: bigint) {
|
|
|
28
227
|
setError(null);
|
|
29
228
|
|
|
30
229
|
try {
|
|
31
|
-
|
|
32
|
-
const count = await publicClient.readContract({
|
|
33
|
-
address: addrs.services,
|
|
34
|
-
abi: tangleOperatorsAbi,
|
|
35
|
-
functionName: 'blueprintOperatorCount',
|
|
36
|
-
args: [blueprintId],
|
|
37
|
-
});
|
|
38
|
-
if (cancelled) return;
|
|
39
|
-
setOperatorCount(count as bigint);
|
|
40
|
-
|
|
41
|
-
if ((count as bigint) === 0n) {
|
|
42
|
-
setOperators([]);
|
|
43
|
-
setIsLoading(false);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Step 2: Fetch OperatorRegistered logs
|
|
48
|
-
const registeredLogs = await publicClient.getLogs({
|
|
49
|
-
address: addrs.services,
|
|
50
|
-
event: {
|
|
51
|
-
type: 'event' as const,
|
|
52
|
-
name: 'OperatorRegistered',
|
|
53
|
-
inputs: [
|
|
54
|
-
{ name: 'blueprintId', type: 'uint64', indexed: true },
|
|
55
|
-
{ name: 'operator', type: 'address', indexed: true },
|
|
56
|
-
{ name: 'ecdsaPublicKey', type: 'bytes', indexed: false },
|
|
57
|
-
{ name: 'rpcAddress', type: 'string', indexed: false },
|
|
58
|
-
],
|
|
59
|
-
},
|
|
60
|
-
args: { blueprintId },
|
|
61
|
-
fromBlock: 0n,
|
|
62
|
-
toBlock: 'latest',
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
if (cancelled) return;
|
|
66
|
-
|
|
67
|
-
// Deduplicate by address (keep latest)
|
|
68
|
-
const byAddress = new Map<Address, DiscoveredOperator>();
|
|
69
|
-
for (const log of registeredLogs) {
|
|
70
|
-
const addr = log.args.operator as Address | undefined;
|
|
71
|
-
if (!addr) continue;
|
|
72
|
-
byAddress.set(addr, {
|
|
73
|
-
address: addr,
|
|
74
|
-
ecdsaPublicKey: (log.args.ecdsaPublicKey as string) ?? '0x',
|
|
75
|
-
rpcAddress: (log.args.rpcAddress as string) ?? '',
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const candidates = Array.from(byAddress.values());
|
|
80
|
-
if (candidates.length === 0) {
|
|
81
|
-
setOperators([]);
|
|
82
|
-
setIsLoading(false);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Step 3: Verify each is still registered + fetch current preferences
|
|
87
|
-
const [registrationResults, preferencesResults] = await Promise.all([
|
|
88
|
-
publicClient.multicall({
|
|
89
|
-
contracts: candidates.map((op) => ({
|
|
90
|
-
address: addrs.services,
|
|
91
|
-
abi: tangleOperatorsAbi,
|
|
92
|
-
functionName: 'isOperatorRegistered' as const,
|
|
93
|
-
args: [blueprintId, op.address] as const,
|
|
94
|
-
})),
|
|
95
|
-
}),
|
|
96
|
-
publicClient.multicall({
|
|
97
|
-
contracts: candidates.map((op) => ({
|
|
98
|
-
address: addrs.services,
|
|
99
|
-
abi: tangleOperatorsAbi,
|
|
100
|
-
functionName: 'getOperatorPreferences' as const,
|
|
101
|
-
args: [blueprintId, op.address] as const,
|
|
102
|
-
})),
|
|
103
|
-
}),
|
|
104
|
-
]);
|
|
105
|
-
|
|
230
|
+
const result = await discoverOperatorsWithClient(publicClient, addrs.services, blueprintId);
|
|
106
231
|
if (cancelled) return;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
candidates.forEach((op, i) => {
|
|
110
|
-
if (registrationResults[i]?.result !== true) return;
|
|
111
|
-
const prefs = preferencesResults[i];
|
|
112
|
-
if (prefs?.status === 'success' && prefs.result != null) {
|
|
113
|
-
const result = prefs.result as Record<string, unknown>;
|
|
114
|
-
const ecdsaKey = Array.isArray(result) ? String(result[0] ?? '') : String((result as any).ecdsaPublicKey ?? '');
|
|
115
|
-
const rpcAddr = Array.isArray(result) ? String(result[1] ?? '') : String((result as any).rpcAddress ?? '');
|
|
116
|
-
active.push({
|
|
117
|
-
...op,
|
|
118
|
-
ecdsaPublicKey: ecdsaKey || op.ecdsaPublicKey,
|
|
119
|
-
rpcAddress: rpcAddr || op.rpcAddress,
|
|
120
|
-
});
|
|
121
|
-
} else {
|
|
122
|
-
active.push(op);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
setOperators(active);
|
|
232
|
+
setOperatorCount(result.operatorCount);
|
|
233
|
+
setOperators(result.operators);
|
|
127
234
|
} catch (err) {
|
|
128
235
|
if (!cancelled) {
|
|
129
236
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { getEnvVar } from '../utils/env';
|
|
2
3
|
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
4
5
|
// Types matching sandbox-runtime/src/provision_progress.rs
|
|
@@ -64,7 +65,7 @@ export function useProvisionProgress({
|
|
|
64
65
|
const [isPolling, setIsPolling] = useState(false);
|
|
65
66
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
66
67
|
|
|
67
|
-
const baseUrl = apiUrl ??
|
|
68
|
+
const baseUrl = apiUrl ?? getEnvVar('VITE_OPERATOR_API_URL') ?? 'http://localhost:9090';
|
|
68
69
|
|
|
69
70
|
const fetchProgress = useCallback(async () => {
|
|
70
71
|
if (callId == null) return;
|
package/src/hooks/useQuotes.ts
CHANGED
|
@@ -18,6 +18,16 @@ import { resolveOperatorRpc } from '../utils/resolveOperatorRpc';
|
|
|
18
18
|
|
|
19
19
|
// ── Types ──
|
|
20
20
|
|
|
21
|
+
type SecurityCommitment = {
|
|
22
|
+
asset: { kind: number; token: Address };
|
|
23
|
+
exposureBps: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ResourceCommitment = {
|
|
27
|
+
kind: number;
|
|
28
|
+
count: bigint;
|
|
29
|
+
};
|
|
30
|
+
|
|
21
31
|
export interface OperatorQuote {
|
|
22
32
|
operator: Address;
|
|
23
33
|
totalCost: bigint;
|
|
@@ -28,12 +38,13 @@ export interface OperatorQuote {
|
|
|
28
38
|
totalCost: bigint;
|
|
29
39
|
timestamp: bigint;
|
|
30
40
|
expiry: bigint;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}[];
|
|
41
|
+
confidentiality: number;
|
|
42
|
+
securityCommitments: readonly SecurityCommitment[];
|
|
43
|
+
resourceCommitments: readonly ResourceCommitment[];
|
|
35
44
|
};
|
|
36
45
|
costRate: number;
|
|
46
|
+
teeAttested?: boolean;
|
|
47
|
+
teeProvider?: string;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
export interface UseQuotesResult {
|
|
@@ -49,6 +60,20 @@ export interface UseQuotesResult {
|
|
|
49
60
|
|
|
50
61
|
const POW_DIFFICULTY = 20;
|
|
51
62
|
const WEI_PER_TNT = 1_000_000_000_000_000_000; // 10^18
|
|
63
|
+
const RESOURCE_KIND_TO_ID = {
|
|
64
|
+
CPU: 0,
|
|
65
|
+
MemoryMB: 1,
|
|
66
|
+
StorageMB: 2,
|
|
67
|
+
NetworkEgressMB: 3,
|
|
68
|
+
NetworkIngressMB: 4,
|
|
69
|
+
GPU: 5,
|
|
70
|
+
} as const;
|
|
71
|
+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as Address;
|
|
72
|
+
const DEFAULT_RESOURCE_REQUIREMENTS = [
|
|
73
|
+
{ kind: 'CPU', count: 1 },
|
|
74
|
+
{ kind: 'MemoryMB', count: 1024 },
|
|
75
|
+
{ kind: 'StorageMB', count: 10240 },
|
|
76
|
+
] as const;
|
|
52
77
|
|
|
53
78
|
function sha256(data: Uint8Array): Uint8Array {
|
|
54
79
|
return viemSha256(data, 'bytes');
|
|
@@ -114,6 +139,35 @@ export function formatCost(totalCost: bigint): string {
|
|
|
114
139
|
return `${tnt.toLocaleString(undefined, { maximumFractionDigits: 2 })} TNT`;
|
|
115
140
|
}
|
|
116
141
|
|
|
142
|
+
function quoteConfidentiality(requireTee: boolean): number {
|
|
143
|
+
return requireTee ? 1 : 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resourceKindToId(kind: string): number {
|
|
147
|
+
const mapped = RESOURCE_KIND_TO_ID[kind as keyof typeof RESOURCE_KIND_TO_ID];
|
|
148
|
+
if (mapped === undefined) {
|
|
149
|
+
throw new Error(`Unsupported resource kind in quote: ${kind}`);
|
|
150
|
+
}
|
|
151
|
+
return mapped;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function mapJsonSecurityCommitment(sc: any): SecurityCommitment {
|
|
155
|
+
return {
|
|
156
|
+
asset: {
|
|
157
|
+
kind: sc.asset?.kind ?? 0,
|
|
158
|
+
token: (sc.asset?.token ?? ZERO_ADDRESS) as Address,
|
|
159
|
+
},
|
|
160
|
+
exposureBps: sc.exposure_bps ?? 0,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function mapJsonResourceCommitment(resource: any): ResourceCommitment {
|
|
165
|
+
return {
|
|
166
|
+
kind: resourceKindToId(String(resource.kind ?? 'CPU')),
|
|
167
|
+
count: BigInt(resource.count ?? 0),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
117
171
|
// ── Hook ──
|
|
118
172
|
|
|
119
173
|
export function useQuotes(
|
|
@@ -121,6 +175,7 @@ export function useQuotes(
|
|
|
121
175
|
blueprintId: bigint,
|
|
122
176
|
ttlBlocks: bigint,
|
|
123
177
|
enabled: boolean,
|
|
178
|
+
requireTee = false,
|
|
124
179
|
): UseQuotesResult {
|
|
125
180
|
const [quotes, setQuotes] = useState<OperatorQuote[]>([]);
|
|
126
181
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -168,6 +223,7 @@ export function useQuotes(
|
|
|
168
223
|
ttlBlocks,
|
|
169
224
|
proofOfWork: proof,
|
|
170
225
|
challengeTimestamp: timestamp,
|
|
226
|
+
requireTee,
|
|
171
227
|
});
|
|
172
228
|
|
|
173
229
|
if (!response) throw new Error('No quote returned from operator');
|
|
@@ -194,7 +250,7 @@ export function useQuotes(
|
|
|
194
250
|
return () => {
|
|
195
251
|
cancelled = true;
|
|
196
252
|
};
|
|
197
|
-
}, [operators, blueprintId, ttlBlocks, enabled, fetchKey]);
|
|
253
|
+
}, [operators, blueprintId, ttlBlocks, enabled, fetchKey, requireTee]);
|
|
198
254
|
|
|
199
255
|
const totalCost = quotes.reduce((sum, q) => sum + q.totalCost, 0n);
|
|
200
256
|
|
|
@@ -213,6 +269,7 @@ async function fetchPriceFromOperator(
|
|
|
213
269
|
ttlBlocks: bigint;
|
|
214
270
|
proofOfWork: Uint8Array;
|
|
215
271
|
challengeTimestamp: bigint;
|
|
272
|
+
requireTee: boolean;
|
|
216
273
|
},
|
|
217
274
|
): Promise<OperatorQuote | null> {
|
|
218
275
|
// Try JSON endpoint (simpler, no protobuf dependency required)
|
|
@@ -225,11 +282,8 @@ async function fetchPriceFromOperator(
|
|
|
225
282
|
ttl_blocks: String(params.ttlBlocks),
|
|
226
283
|
proof_of_work: toHex(params.proofOfWork),
|
|
227
284
|
challenge_timestamp: String(params.challengeTimestamp),
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
{ kind: 'MemoryMB', count: 1024 },
|
|
231
|
-
{ kind: 'StorageMB', count: 10240 },
|
|
232
|
-
],
|
|
285
|
+
require_tee: params.requireTee,
|
|
286
|
+
resource_requirements: DEFAULT_RESOURCE_REQUIREMENTS,
|
|
233
287
|
}),
|
|
234
288
|
signal: AbortSignal.timeout(10_000),
|
|
235
289
|
});
|
|
@@ -242,16 +296,17 @@ async function fetchPriceFromOperator(
|
|
|
242
296
|
totalCost: BigInt(data.total_cost ?? '0'),
|
|
243
297
|
signature: (data.signature ?? '0x') as `0x${string}`,
|
|
244
298
|
costRate: Number(data.cost_rate ?? 0),
|
|
299
|
+
teeAttested: Boolean(data.tee_attested),
|
|
300
|
+
teeProvider: data.tee_provider || undefined,
|
|
245
301
|
details: {
|
|
246
302
|
blueprintId: BigInt(data.details?.blueprint_id ?? params.blueprintId),
|
|
247
303
|
ttlBlocks: BigInt(data.details?.ttl_blocks ?? params.ttlBlocks),
|
|
248
304
|
totalCost: BigInt(data.details?.total_cost ?? '0'),
|
|
249
305
|
timestamp: BigInt(data.details?.timestamp ?? params.challengeTimestamp),
|
|
250
306
|
expiry: BigInt(data.details?.expiry ?? '0'),
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
})),
|
|
307
|
+
confidentiality: Number(data.details?.confidentiality ?? quoteConfidentiality(params.requireTee)),
|
|
308
|
+
securityCommitments: (data.details?.security_commitments ?? []).map(mapJsonSecurityCommitment),
|
|
309
|
+
resourceCommitments: (data.details?.resources ?? []).map(mapJsonResourceCommitment),
|
|
255
310
|
},
|
|
256
311
|
};
|
|
257
312
|
} catch {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useState } from 'react';
|
|
2
2
|
import { useSignMessage } from 'wagmi';
|
|
3
3
|
import { getSession, setSession, removeSession, type SessionEntry } from '../stores/session';
|
|
4
|
+
import { getEnvVar } from '../utils/env';
|
|
4
5
|
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Types
|
|
@@ -28,7 +29,7 @@ interface UseSessionAuthOptions {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export function useSessionAuth({ sandboxId, apiUrl }: UseSessionAuthOptions) {
|
|
31
|
-
const baseUrl = apiUrl ??
|
|
32
|
+
const baseUrl = apiUrl ?? getEnvVar('VITE_OPERATOR_API_URL') ?? 'http://localhost:9090';
|
|
32
33
|
const { signMessageAsync } = useSignMessage();
|
|
33
34
|
|
|
34
35
|
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Types
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
export interface UseSidecarAuthOptions {
|
|
8
|
+
/** Scoping key for token storage (e.g. botId, sandboxId). */
|
|
9
|
+
resourceId: string;
|
|
10
|
+
/** Base URL of the sidecar or operator API. */
|
|
11
|
+
apiUrl: string;
|
|
12
|
+
/**
|
|
13
|
+
* Sign a plaintext message and return the hex signature.
|
|
14
|
+
* Consuming apps wire this to their wallet library (e.g. wagmi's signMessageAsync).
|
|
15
|
+
*/
|
|
16
|
+
signMessage: (message: string) => Promise<string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SidecarAuth {
|
|
20
|
+
token: string | null;
|
|
21
|
+
isAuthenticated: boolean;
|
|
22
|
+
isAuthenticating: boolean;
|
|
23
|
+
authenticate: () => Promise<string | null>;
|
|
24
|
+
clearCachedToken: () => void;
|
|
25
|
+
error: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// localStorage helpers
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
function storageKey(resourceId: string, apiUrl: string): string {
|
|
33
|
+
return `sidecar_session_${resourceId}__${apiUrl}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function loadSession(resourceId: string, apiUrl: string): { token: string; expiresAt: number } | null {
|
|
37
|
+
if (typeof window === 'undefined') return null;
|
|
38
|
+
try {
|
|
39
|
+
const raw = localStorage.getItem(storageKey(resourceId, apiUrl));
|
|
40
|
+
if (!raw) return null;
|
|
41
|
+
const data = JSON.parse(raw) as { token: string; expiresAt: number };
|
|
42
|
+
// Discard if within 60s of expiry
|
|
43
|
+
if (data.expiresAt * 1000 - Date.now() < 60_000) {
|
|
44
|
+
localStorage.removeItem(storageKey(resourceId, apiUrl));
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return data;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function saveSession(resourceId: string, apiUrl: string, token: string, expiresAt: number) {
|
|
54
|
+
if (typeof window === 'undefined') return;
|
|
55
|
+
try {
|
|
56
|
+
localStorage.setItem(storageKey(resourceId, apiUrl), JSON.stringify({ token, expiresAt }));
|
|
57
|
+
} catch {
|
|
58
|
+
// storage full — ignore
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function clearSession(resourceId: string, apiUrl: string) {
|
|
63
|
+
if (typeof window === 'undefined') return;
|
|
64
|
+
localStorage.removeItem(storageKey(resourceId, apiUrl));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Hook
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generic sidecar PASETO challenge/response auth.
|
|
73
|
+
*
|
|
74
|
+
* Flow:
|
|
75
|
+
* 1. POST /api/auth/challenge -> { nonce, message, expires_at }
|
|
76
|
+
* 2. signMessage(message) -> signature (provided by consuming app)
|
|
77
|
+
* 3. POST /api/auth/session -> { token, address, expires_at }
|
|
78
|
+
*
|
|
79
|
+
* Tokens are cached in localStorage and auto-refreshed 5 minutes before expiry.
|
|
80
|
+
*/
|
|
81
|
+
export function useSidecarAuth({ resourceId, apiUrl, signMessage }: UseSidecarAuthOptions): SidecarAuth {
|
|
82
|
+
const cached = loadSession(resourceId, apiUrl);
|
|
83
|
+
const [token, setToken] = useState<string | null>(cached?.token ?? null);
|
|
84
|
+
const [expiresAt, setExpiresAt] = useState<number>(cached?.expiresAt ?? 0);
|
|
85
|
+
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
|
86
|
+
const [error, setError] = useState<string | null>(null);
|
|
87
|
+
const refreshTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
|
88
|
+
|
|
89
|
+
const clearCachedToken = useCallback(() => {
|
|
90
|
+
setToken(null);
|
|
91
|
+
setExpiresAt(0);
|
|
92
|
+
clearSession(resourceId, apiUrl);
|
|
93
|
+
}, [resourceId, apiUrl]);
|
|
94
|
+
|
|
95
|
+
const authenticate = useCallback(async (): Promise<string | null> => {
|
|
96
|
+
if (!apiUrl) return null;
|
|
97
|
+
setIsAuthenticating(true);
|
|
98
|
+
setError(null);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
// Step 1: Get challenge
|
|
102
|
+
const challengeRes = await fetch(`${apiUrl}/api/auth/challenge`, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
});
|
|
105
|
+
if (!challengeRes.ok) {
|
|
106
|
+
throw new Error(`Challenge failed: ${challengeRes.status}`);
|
|
107
|
+
}
|
|
108
|
+
const { nonce, message } = await challengeRes.json();
|
|
109
|
+
|
|
110
|
+
// Step 2: Sign with wallet (injected)
|
|
111
|
+
const signature = await signMessage(message);
|
|
112
|
+
|
|
113
|
+
// Step 3: Exchange for session token
|
|
114
|
+
const sessionRes = await fetch(`${apiUrl}/api/auth/session`, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: { 'Content-Type': 'application/json' },
|
|
117
|
+
body: JSON.stringify({ nonce, signature }),
|
|
118
|
+
});
|
|
119
|
+
if (!sessionRes.ok) {
|
|
120
|
+
const text = await sessionRes.text();
|
|
121
|
+
throw new Error(text || `Session exchange failed: ${sessionRes.status}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { token: newToken, expires_at } = await sessionRes.json();
|
|
125
|
+
setToken(newToken);
|
|
126
|
+
setExpiresAt(expires_at);
|
|
127
|
+
saveSession(resourceId, apiUrl, newToken, expires_at);
|
|
128
|
+
return newToken;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
setError(err instanceof Error ? err.message : 'Authentication failed');
|
|
131
|
+
clearCachedToken();
|
|
132
|
+
return null;
|
|
133
|
+
} finally {
|
|
134
|
+
setIsAuthenticating(false);
|
|
135
|
+
}
|
|
136
|
+
}, [resourceId, apiUrl, signMessage, clearCachedToken]);
|
|
137
|
+
|
|
138
|
+
// Auto-refresh token 5 minutes before expiry
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (refreshTimerRef.current) {
|
|
141
|
+
clearTimeout(refreshTimerRef.current);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!token || !expiresAt) return;
|
|
145
|
+
|
|
146
|
+
const msUntilRefresh = (expiresAt - 300) * 1000 - Date.now();
|
|
147
|
+
if (msUntilRefresh <= 0) {
|
|
148
|
+
clearCachedToken();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
refreshTimerRef.current = setTimeout(() => {
|
|
153
|
+
authenticate().catch(() => {
|
|
154
|
+
clearCachedToken();
|
|
155
|
+
});
|
|
156
|
+
}, msUntilRefresh);
|
|
157
|
+
|
|
158
|
+
return () => {
|
|
159
|
+
if (refreshTimerRef.current) {
|
|
160
|
+
clearTimeout(refreshTimerRef.current);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}, [token, expiresAt, authenticate, clearCachedToken]);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
token,
|
|
167
|
+
isAuthenticated: token !== null,
|
|
168
|
+
isAuthenticating,
|
|
169
|
+
authenticate,
|
|
170
|
+
clearCachedToken,
|
|
171
|
+
error,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useSignMessage } from 'wagmi';
|
|
2
|
+
import { useSidecarAuth } from './useSidecarAuth';
|
|
3
|
+
|
|
4
|
+
export function useWagmiSidecarAuth(resourceId: string, apiUrl: string) {
|
|
5
|
+
const { signMessageAsync } = useSignMessage();
|
|
6
|
+
return useSidecarAuth({
|
|
7
|
+
resourceId,
|
|
8
|
+
apiUrl,
|
|
9
|
+
signMessage: (msg: string) => signMessageAsync({ message: msg }),
|
|
10
|
+
});
|
|
11
|
+
}
|