@tangle-network/blueprint-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +160 -0
  2. package/package.json +82 -0
  3. package/src/blueprints/registry.ts +107 -0
  4. package/src/components/forms/BlueprintJobForm.tsx +88 -0
  5. package/src/components/forms/FormField.tsx +206 -0
  6. package/src/components/forms/FormSummary.tsx +47 -0
  7. package/src/components/forms/JobExecutionDialog.tsx +203 -0
  8. package/src/components/layout/AppDocument.tsx +48 -0
  9. package/src/components/layout/AppFooter.tsx +38 -0
  10. package/src/components/layout/AppToaster.tsx +33 -0
  11. package/src/components/layout/ChainSwitcher.tsx +71 -0
  12. package/src/components/layout/ThemeToggle.tsx +18 -0
  13. package/src/components/layout/Web3Shell.tsx +28 -0
  14. package/src/components/motion/AnimatedPage.tsx +36 -0
  15. package/src/components/shared/Identicon.tsx +22 -0
  16. package/src/components/shared/TangleLogo.tsx +49 -0
  17. package/src/components/ui/badge.tsx +37 -0
  18. package/src/components/ui/button.tsx +61 -0
  19. package/src/components/ui/card.tsx +34 -0
  20. package/src/components/ui/dialog.tsx +59 -0
  21. package/src/components/ui/input.tsx +24 -0
  22. package/src/components/ui/select.tsx +80 -0
  23. package/src/components/ui/separator.tsx +25 -0
  24. package/src/components/ui/skeleton.tsx +13 -0
  25. package/src/components/ui/table.tsx +49 -0
  26. package/src/components/ui/tabs.tsx +39 -0
  27. package/src/components/ui/textarea.tsx +23 -0
  28. package/src/components/ui/toggle.tsx +35 -0
  29. package/src/components.ts +51 -0
  30. package/src/contracts/abi.ts +259 -0
  31. package/src/contracts/chains.ts +100 -0
  32. package/src/contracts/generic-encoder.ts +69 -0
  33. package/src/contracts/publicClient.ts +55 -0
  34. package/src/env.d.ts +14 -0
  35. package/src/hooks/useAuthenticatedFetch.ts +57 -0
  36. package/src/hooks/useJobForm.ts +78 -0
  37. package/src/hooks/useJobPrice.ts +283 -0
  38. package/src/hooks/useOperators.ts +141 -0
  39. package/src/hooks/useProvisionProgress.ts +125 -0
  40. package/src/hooks/useQuotes.ts +261 -0
  41. package/src/hooks/useServiceValidation.ts +113 -0
  42. package/src/hooks/useSessionAuth.ts +103 -0
  43. package/src/hooks/useSubmitJob.ts +115 -0
  44. package/src/hooks/useThemeValue.ts +6 -0
  45. package/src/index.ts +79 -0
  46. package/src/preset.ts +61 -0
  47. package/src/stores/infra.ts +43 -0
  48. package/src/stores/persistedAtom.ts +67 -0
  49. package/src/stores/session.ts +64 -0
  50. package/src/stores/theme.ts +28 -0
  51. package/src/stores/txHistory.ts +47 -0
  52. package/src/utils/resolveOperatorRpc.ts +20 -0
  53. package/src/utils/web3.ts +21 -0
  54. package/src/utils.ts +6 -0
  55. package/tsconfig.json +21 -0
@@ -0,0 +1,261 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import type { Address } from 'viem';
3
+ import { sha256 as viemSha256, toHex } from 'viem';
4
+ import type { DiscoveredOperator } from './useOperators';
5
+ import { resolveOperatorRpc } from '../utils/resolveOperatorRpc';
6
+
7
+ /**
8
+ * RFQ (Request for Quote) hook — fetches pricing quotes from operators.
9
+ *
10
+ * Implements the Tangle pricing protocol:
11
+ * 1. Solve PoW challenge (SHA256 with 20-bit difficulty)
12
+ * 2. Send gRPC GetPrice request to each operator's registered RPC
13
+ * 3. Collect signed QuoteDetails for createServiceFromQuotes()
14
+ *
15
+ * When operators don't have a pricing engine (no rpcAddress), falls back to
16
+ * multiplier-based estimation from the on-chain getDefaultJobRates().
17
+ */
18
+
19
+ // ── Types ──
20
+
21
+ export interface OperatorQuote {
22
+ operator: Address;
23
+ totalCost: bigint;
24
+ signature: `0x${string}`;
25
+ details: {
26
+ blueprintId: bigint;
27
+ ttlBlocks: bigint;
28
+ totalCost: bigint;
29
+ timestamp: bigint;
30
+ expiry: bigint;
31
+ securityCommitments: readonly {
32
+ asset: { kind: number; token: Address };
33
+ exposureBps: number;
34
+ }[];
35
+ };
36
+ costRate: number;
37
+ }
38
+
39
+ export interface UseQuotesResult {
40
+ quotes: OperatorQuote[];
41
+ isLoading: boolean;
42
+ isSolvingPow: boolean;
43
+ errors: Map<Address, string>;
44
+ totalCost: bigint;
45
+ refetch: () => void;
46
+ }
47
+
48
+ // ── PoW solver (mirrors Tangle pricing-engine/src/pow.rs) ──
49
+
50
+ const POW_DIFFICULTY = 20;
51
+ const WEI_PER_TNT = 1_000_000_000_000_000_000; // 10^18
52
+
53
+ function sha256(data: Uint8Array): Uint8Array {
54
+ return viemSha256(data, 'bytes');
55
+ }
56
+
57
+ function generateChallenge(blueprintId: bigint, timestamp: bigint): Uint8Array {
58
+ const input = new Uint8Array(16);
59
+ const view = new DataView(input.buffer);
60
+ view.setBigUint64(0, blueprintId, false);
61
+ view.setBigUint64(8, timestamp, false);
62
+ return sha256(input);
63
+ }
64
+
65
+ function checkDifficulty(hash: Uint8Array, difficulty: number): boolean {
66
+ const zeroBytes = Math.floor(difficulty / 8);
67
+ const zeroBits = difficulty % 8;
68
+ for (let i = 0; i < zeroBytes; i++) {
69
+ if (hash[i] !== 0) return false;
70
+ }
71
+ if (zeroBits > 0) {
72
+ const mask = 0xff << (8 - zeroBits);
73
+ if ((hash[zeroBytes] & mask) !== 0) return false;
74
+ }
75
+ return true;
76
+ }
77
+
78
+ export async function solvePoW(
79
+ blueprintId: bigint,
80
+ timestamp: bigint,
81
+ ): Promise<{ hash: Uint8Array; nonce: number; proof: Uint8Array }> {
82
+ const challenge = generateChallenge(blueprintId, timestamp);
83
+ const buf = new Uint8Array(challenge.length + 8);
84
+ buf.set(challenge, 0);
85
+ const view = new DataView(buf.buffer);
86
+
87
+ for (let nonce = 0; nonce < 0x1_0000_0000; nonce++) {
88
+ view.setBigUint64(challenge.length, BigInt(nonce), false);
89
+ const hash = sha256(buf);
90
+ if (checkDifficulty(hash, POW_DIFFICULTY)) {
91
+ // Bincode-serialize Proof { hash: Vec<u8>, nonce: u64 }
92
+ const proof = new Uint8Array(8 + 32 + 8);
93
+ const pv = new DataView(proof.buffer);
94
+ pv.setBigUint64(0, 32n, true);
95
+ proof.set(hash, 8);
96
+ pv.setBigUint64(40, BigInt(nonce), true);
97
+ return { hash, nonce, proof };
98
+ }
99
+ // Yield to browser every 5000 iterations
100
+ if (nonce % 5000 === 0 && nonce > 0) {
101
+ await new Promise((r) => setTimeout(r, 0));
102
+ }
103
+ }
104
+ throw new Error('PoW: exhausted nonce space');
105
+ }
106
+
107
+ export function formatCost(totalCost: bigint): string {
108
+ // totalCost is in wei (1e18 wei = 1 TNT)
109
+ const tnt = Number(totalCost) / WEI_PER_TNT;
110
+ if (tnt === 0) return '0 TNT';
111
+ if (tnt < 0.001) return `${(tnt * 1_000_000).toFixed(2)} \u03bcTNT`;
112
+ if (tnt < 0.01) return `${(tnt * 1000).toFixed(2)} mTNT`;
113
+ if (tnt < 1000) return `${tnt.toFixed(4)} TNT`;
114
+ return `${tnt.toLocaleString(undefined, { maximumFractionDigits: 2 })} TNT`;
115
+ }
116
+
117
+ // ── Hook ──
118
+
119
+ export function useQuotes(
120
+ operators: DiscoveredOperator[],
121
+ blueprintId: bigint,
122
+ ttlBlocks: bigint,
123
+ enabled: boolean,
124
+ ): UseQuotesResult {
125
+ const [quotes, setQuotes] = useState<OperatorQuote[]>([]);
126
+ const [isLoading, setIsLoading] = useState(false);
127
+ const [isSolvingPow, setIsSolvingPow] = useState(false);
128
+ const [errors, setErrors] = useState<Map<Address, string>>(new Map());
129
+ const [fetchKey, setFetchKey] = useState(0);
130
+
131
+ const refetch = useCallback(() => setFetchKey((k) => k + 1), []);
132
+
133
+ useEffect(() => {
134
+ if (!enabled || operators.length === 0) {
135
+ // Only set state if it's actually non-empty to avoid triggering re-renders
136
+ setQuotes((prev) => (prev.length === 0 ? prev : []));
137
+ setErrors((prev) => (prev.size === 0 ? prev : new Map()));
138
+ return;
139
+ }
140
+
141
+ let cancelled = false;
142
+ setIsLoading(true);
143
+ setIsSolvingPow(true);
144
+ setQuotes([]);
145
+ setErrors(new Map());
146
+
147
+ async function fetchQuotes() {
148
+ const results: OperatorQuote[] = [];
149
+ const errs = new Map<Address, string>();
150
+
151
+ const promises = operators.map(async (op) => {
152
+ try {
153
+ if (!op.rpcAddress) throw new Error('No RPC address registered');
154
+
155
+ const rpcUrl = resolveOperatorRpc(op.rpcAddress);
156
+ const timestamp = BigInt(Math.floor(Date.now() / 1000));
157
+
158
+ // Solve PoW challenge
159
+ if (!cancelled) setIsSolvingPow(true);
160
+ const { proof } = await solvePoW(blueprintId, timestamp);
161
+ if (!cancelled) setIsSolvingPow(false);
162
+
163
+ // Try gRPC GetPrice endpoint
164
+ // Since we may not have the generated protobuf types, try a JSON/REST
165
+ // fallback first, then gRPC if available
166
+ const response = await fetchPriceFromOperator(rpcUrl, {
167
+ blueprintId,
168
+ ttlBlocks,
169
+ proofOfWork: proof,
170
+ challengeTimestamp: timestamp,
171
+ });
172
+
173
+ if (!response) throw new Error('No quote returned from operator');
174
+
175
+ if (!cancelled) results.push(response);
176
+ } catch (err) {
177
+ if (!cancelled) {
178
+ errs.set(op.address, err instanceof Error ? err.message : String(err));
179
+ }
180
+ }
181
+ });
182
+
183
+ await Promise.allSettled(promises);
184
+
185
+ if (!cancelled) {
186
+ setQuotes(results);
187
+ setErrors(errs);
188
+ setIsLoading(false);
189
+ setIsSolvingPow(false);
190
+ }
191
+ }
192
+
193
+ fetchQuotes();
194
+ return () => {
195
+ cancelled = true;
196
+ };
197
+ }, [operators, blueprintId, ttlBlocks, enabled, fetchKey]);
198
+
199
+ const totalCost = quotes.reduce((sum, q) => sum + q.totalCost, 0n);
200
+
201
+ return { quotes, isLoading, isSolvingPow, errors, totalCost, refetch };
202
+ }
203
+
204
+ /**
205
+ * Attempt to fetch a price from an operator's RPC endpoint.
206
+ * Tries a JSON endpoint first (/pricing/quote), which operators may optionally expose.
207
+ * Returns null if the operator doesn't support pricing.
208
+ */
209
+ async function fetchPriceFromOperator(
210
+ rpcUrl: string,
211
+ params: {
212
+ blueprintId: bigint;
213
+ ttlBlocks: bigint;
214
+ proofOfWork: Uint8Array;
215
+ challengeTimestamp: bigint;
216
+ },
217
+ ): Promise<OperatorQuote | null> {
218
+ // Try JSON endpoint (simpler, no protobuf dependency required)
219
+ try {
220
+ const response = await fetch(`${rpcUrl}/pricing/quote`, {
221
+ method: 'POST',
222
+ headers: { 'Content-Type': 'application/json' },
223
+ body: JSON.stringify({
224
+ blueprint_id: String(params.blueprintId),
225
+ ttl_blocks: String(params.ttlBlocks),
226
+ proof_of_work: toHex(params.proofOfWork),
227
+ challenge_timestamp: String(params.challengeTimestamp),
228
+ resource_requirements: [
229
+ { kind: 'CPU', count: 1 },
230
+ { kind: 'MemoryMB', count: 1024 },
231
+ { kind: 'StorageMB', count: 10240 },
232
+ ],
233
+ }),
234
+ signal: AbortSignal.timeout(10_000),
235
+ });
236
+
237
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
238
+ const data = await response.json();
239
+
240
+ return {
241
+ operator: data.operator as Address,
242
+ totalCost: BigInt(data.total_cost ?? '0'),
243
+ signature: (data.signature ?? '0x') as `0x${string}`,
244
+ costRate: Number(data.cost_rate ?? 0),
245
+ details: {
246
+ blueprintId: BigInt(data.details?.blueprint_id ?? params.blueprintId),
247
+ ttlBlocks: BigInt(data.details?.ttl_blocks ?? params.ttlBlocks),
248
+ totalCost: BigInt(data.details?.total_cost ?? '0'),
249
+ timestamp: BigInt(data.details?.timestamp ?? params.challengeTimestamp),
250
+ expiry: BigInt(data.details?.expiry ?? '0'),
251
+ securityCommitments: (data.details?.security_commitments ?? []).map((sc: any) => ({
252
+ asset: { kind: sc.asset?.kind ?? 0, token: (sc.asset?.token ?? '0x0000000000000000000000000000000000000000') as Address },
253
+ exposureBps: sc.exposure_bps ?? 0,
254
+ })),
255
+ },
256
+ };
257
+ } catch {
258
+ // JSON endpoint not available — operator doesn't have pricing engine
259
+ return null;
260
+ }
261
+ }
@@ -0,0 +1,113 @@
1
+ import { useState, useCallback } from 'react';
2
+ import type { Address } from 'viem';
3
+ import { tangleServicesAbi } from '../contracts/abi';
4
+ import { publicClient, getAddresses } from '../contracts/publicClient';
5
+
6
+ export interface ServiceInfo {
7
+ active: boolean;
8
+ blueprintId: bigint;
9
+ owner: Address;
10
+ operatorCount: number;
11
+ operators: Address[];
12
+ permitted: boolean;
13
+ ttl: bigint;
14
+ createdAt: bigint;
15
+ }
16
+
17
+ /**
18
+ * Validate a service on-chain: check if active, fetch operators,
19
+ * verify the current user is a permitted caller.
20
+ */
21
+ export function useServiceValidation() {
22
+ const [isValidating, setIsValidating] = useState(false);
23
+ const [serviceInfo, setServiceInfo] = useState<ServiceInfo | null>(null);
24
+ const [error, setError] = useState<string | null>(null);
25
+
26
+ const validate = useCallback(async (serviceId: bigint, userAddress?: Address) => {
27
+ setIsValidating(true);
28
+ setError(null);
29
+ setServiceInfo(null);
30
+
31
+ const addrs = getAddresses();
32
+
33
+ try {
34
+ // Run reads in parallel without multicall to avoid type union issues
35
+ const [isActiveResult, serviceDataResult, operatorsResult, permittedResult] = await Promise.all([
36
+ publicClient.readContract({
37
+ address: addrs.services,
38
+ abi: tangleServicesAbi,
39
+ functionName: 'isServiceActive',
40
+ args: [serviceId],
41
+ }).catch(() => false),
42
+
43
+ publicClient.readContract({
44
+ address: addrs.services,
45
+ abi: tangleServicesAbi,
46
+ functionName: 'getService',
47
+ args: [serviceId],
48
+ }).catch(() => null),
49
+
50
+ publicClient.readContract({
51
+ address: addrs.services,
52
+ abi: tangleServicesAbi,
53
+ functionName: 'getServiceOperators',
54
+ args: [serviceId],
55
+ }).catch(() => [] as readonly Address[]),
56
+
57
+ userAddress
58
+ ? publicClient.readContract({
59
+ address: addrs.services,
60
+ abi: tangleServicesAbi,
61
+ functionName: 'isPermittedCaller',
62
+ args: [serviceId, userAddress],
63
+ }).catch(() => false)
64
+ : Promise.resolve(true),
65
+ ]);
66
+
67
+ const isActive = isActiveResult as boolean;
68
+ const serviceData = serviceDataResult as Record<string, any> | null;
69
+ const operators = (operatorsResult ?? []) as readonly Address[];
70
+ const permitted = permittedResult as boolean;
71
+
72
+ if (!serviceData) {
73
+ setError('Service not found');
74
+ setIsValidating(false);
75
+ return null;
76
+ }
77
+
78
+ const info: ServiceInfo = {
79
+ active: isActive,
80
+ blueprintId: serviceData.blueprintId ?? serviceData[0] ?? 0n,
81
+ owner: serviceData.owner ?? serviceData[1] ?? ('0x0' as Address),
82
+ operatorCount: operators.length,
83
+ operators: [...operators],
84
+ permitted,
85
+ ttl: serviceData.ttl ?? serviceData[3] ?? 0n,
86
+ createdAt: serviceData.createdAt ?? serviceData[2] ?? 0n,
87
+ };
88
+
89
+ setServiceInfo(info);
90
+
91
+ if (!isActive) {
92
+ setError('Service is not active');
93
+ } else if (!permitted && userAddress) {
94
+ setError('You are not a permitted caller for this service');
95
+ }
96
+
97
+ setIsValidating(false);
98
+ return info;
99
+ } catch (err) {
100
+ const msg = err instanceof Error ? err.message : String(err);
101
+ setError(msg);
102
+ setIsValidating(false);
103
+ return null;
104
+ }
105
+ }, []);
106
+
107
+ const reset = useCallback(() => {
108
+ setServiceInfo(null);
109
+ setError(null);
110
+ }, []);
111
+
112
+ return { validate, reset, isValidating, serviceInfo, error };
113
+ }
@@ -0,0 +1,103 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { useSignMessage } from 'wagmi';
3
+ import { getSession, setSession, removeSession, type SessionEntry } from '../stores/session';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Types
7
+ // ---------------------------------------------------------------------------
8
+
9
+ interface ChallengeResponse {
10
+ nonce: string;
11
+ message: string;
12
+ expires_at: number;
13
+ }
14
+
15
+ interface SessionResponse {
16
+ token: string;
17
+ address: string;
18
+ expires_at: number;
19
+ }
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Hook
23
+ // ---------------------------------------------------------------------------
24
+
25
+ interface UseSessionAuthOptions {
26
+ sandboxId: string;
27
+ apiUrl?: string;
28
+ }
29
+
30
+ export function useSessionAuth({ sandboxId, apiUrl }: UseSessionAuthOptions) {
31
+ const baseUrl = apiUrl ?? import.meta.env.VITE_OPERATOR_API_URL ?? 'http://localhost:9090';
32
+ const { signMessageAsync } = useSignMessage();
33
+
34
+ const [isAuthenticating, setIsAuthenticating] = useState(false);
35
+ const [error, setError] = useState<string | null>(null);
36
+
37
+ // Get cached session
38
+ const session = getSession(sandboxId);
39
+
40
+ const authenticate = useCallback(async (): Promise<SessionEntry | null> => {
41
+ setIsAuthenticating(true);
42
+ setError(null);
43
+
44
+ try {
45
+ // Step 1: Request challenge
46
+ const challengeRes = await fetch(`${baseUrl}/api/auth/challenge`, {
47
+ method: 'POST',
48
+ });
49
+ if (!challengeRes.ok) {
50
+ throw new Error(`Challenge request failed: HTTP ${challengeRes.status}`);
51
+ }
52
+ const challenge: ChallengeResponse = await challengeRes.json();
53
+
54
+ // Step 2: Sign with wallet
55
+ const signature = await signMessageAsync({ message: challenge.message });
56
+
57
+ // Step 3: Exchange signature for session token
58
+ const sessionRes = await fetch(`${baseUrl}/api/auth/session`, {
59
+ method: 'POST',
60
+ headers: { 'Content-Type': 'application/json' },
61
+ body: JSON.stringify({
62
+ nonce: challenge.nonce,
63
+ signature,
64
+ }),
65
+ });
66
+ if (!sessionRes.ok) {
67
+ const body = await sessionRes.json().catch(() => ({}));
68
+ throw new Error(body.error ?? `Session exchange failed: HTTP ${sessionRes.status}`);
69
+ }
70
+ const sessionData: SessionResponse = await sessionRes.json();
71
+
72
+ // Step 4: Store session
73
+ const entry: SessionEntry = {
74
+ token: sessionData.token,
75
+ address: sessionData.address,
76
+ expiresAt: sessionData.expires_at,
77
+ sandboxId,
78
+ };
79
+ setSession(entry);
80
+
81
+ return entry;
82
+ } catch (err) {
83
+ const msg = err instanceof Error ? err.message : 'Authentication failed';
84
+ setError(msg);
85
+ return null;
86
+ } finally {
87
+ setIsAuthenticating(false);
88
+ }
89
+ }, [baseUrl, sandboxId, signMessageAsync]);
90
+
91
+ const logout = useCallback(() => {
92
+ removeSession(sandboxId);
93
+ }, [sandboxId]);
94
+
95
+ return {
96
+ session,
97
+ isAuthenticated: session !== null,
98
+ isAuthenticating,
99
+ error,
100
+ authenticate,
101
+ logout,
102
+ };
103
+ }
@@ -0,0 +1,115 @@
1
+ import { useCallback, useState, useMemo } from 'react';
2
+ import { useAccount, useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
3
+ import { decodeEventLog } from 'viem';
4
+ import { tangleJobsAbi } from '../contracts/abi';
5
+ import { getAddresses } from '../contracts/publicClient';
6
+ import { addTx, updateTx } from '../stores/txHistory';
7
+ import { selectedChainIdStore } from '../contracts/publicClient';
8
+ import { useEffect } from 'react';
9
+
10
+ export interface SubmitJobOpts {
11
+ serviceId: bigint;
12
+ jobId: number;
13
+ args: `0x${string}`;
14
+ label?: string;
15
+ value?: bigint;
16
+ }
17
+
18
+ export type JobSubmitStatus = 'idle' | 'signing' | 'pending' | 'confirmed' | 'failed';
19
+
20
+ export function useSubmitJob() {
21
+ const { address } = useAccount();
22
+ const { writeContractAsync, data: hash, isPending: isSigning } = useWriteContract();
23
+ const [status, setStatus] = useState<JobSubmitStatus>('idle');
24
+ const [error, setError] = useState<string | null>(null);
25
+ const [txHash, setTxHash] = useState<`0x${string}` | undefined>();
26
+
27
+ const { data: receipt, isSuccess, isError } = useWaitForTransactionReceipt({
28
+ hash: txHash,
29
+ });
30
+
31
+ // Extract callId from the JobCalled event in the receipt logs
32
+ const callId = useMemo<number | null>(() => {
33
+ if (!receipt?.logs) return null;
34
+ for (const log of receipt.logs) {
35
+ try {
36
+ const decoded = decodeEventLog({
37
+ abi: tangleJobsAbi,
38
+ data: log.data,
39
+ topics: log.topics,
40
+ });
41
+ if (decoded.eventName === 'JobCalled' && 'callId' in decoded.args) {
42
+ return Number(decoded.args.callId);
43
+ }
44
+ } catch {
45
+ // Not a matching event, skip
46
+ }
47
+ }
48
+ return null;
49
+ }, [receipt]);
50
+
51
+ useEffect(() => {
52
+ if (isSuccess && txHash) {
53
+ setStatus('confirmed');
54
+ updateTx(txHash, { status: 'confirmed' });
55
+ }
56
+ if (isError && txHash) {
57
+ setStatus('failed');
58
+ updateTx(txHash, { status: 'failed' });
59
+ }
60
+ }, [isSuccess, isError, txHash]);
61
+
62
+ const submitJob = useCallback(
63
+ async (opts: SubmitJobOpts) => {
64
+ if (!address) {
65
+ setError('Wallet not connected');
66
+ return undefined;
67
+ }
68
+
69
+ const addrs = getAddresses();
70
+ const label = opts.label ?? `Job #${opts.jobId}`;
71
+
72
+ try {
73
+ setStatus('signing');
74
+ setError(null);
75
+
76
+ const result = await writeContractAsync({
77
+ address: addrs.jobs,
78
+ abi: tangleJobsAbi,
79
+ functionName: 'submitJob',
80
+ args: [opts.serviceId, opts.jobId, opts.args],
81
+ value: opts.value,
82
+ });
83
+
84
+ setTxHash(result);
85
+ setStatus('pending');
86
+ addTx(result, label, selectedChainIdStore.get());
87
+
88
+ return result;
89
+ } catch (err: any) {
90
+ setStatus('failed');
91
+ const msg = err?.shortMessage ?? err?.message ?? 'Transaction failed';
92
+ setError(msg);
93
+ return undefined;
94
+ }
95
+ },
96
+ [address, writeContractAsync],
97
+ );
98
+
99
+ const reset = useCallback(() => {
100
+ setStatus('idle');
101
+ setError(null);
102
+ setTxHash(undefined);
103
+ }, []);
104
+
105
+ return {
106
+ submitJob,
107
+ reset,
108
+ status,
109
+ error,
110
+ txHash,
111
+ callId,
112
+ isSigning,
113
+ isConnected: !!address,
114
+ };
115
+ }
@@ -0,0 +1,6 @@
1
+ import { useStore } from '@nanostores/react';
2
+ import { themeStore, type Theme } from '../stores/theme';
3
+
4
+ export function useThemeValue(): Theme {
5
+ return useStore(themeStore);
6
+ }
package/src/index.ts ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @tangle-network/blueprint-ui — shared utilities, stores, contracts, hooks, and types
3
+ * for Tangle blueprint UIs.
4
+ */
5
+
6
+ // ── Utils ──
7
+ export { cn } from './utils';
8
+ export { resolveOperatorRpc } from './utils/resolveOperatorRpc';
9
+ export { createTangleTransports, defaultConnectKitOptions, tangleWalletChains } from './utils/web3';
10
+ export { bpThemeTokens } from './preset';
11
+
12
+ // ── Stores ──
13
+ export { serializeWithBigInt, deserializeWithBigInt, persistedAtom } from './stores/persistedAtom';
14
+ export type { SessionEntry } from './stores/session';
15
+ export { sessionMapStore, getSession, setSession, removeSession, gcSessions } from './stores/session';
16
+ export type { TrackedTx } from './stores/txHistory';
17
+ export { txListStore, pendingCount, addTx, updateTx, clearTxs } from './stores/txHistory';
18
+ export type { OperatorInfo, InfraConfig } from './stores/infra';
19
+ export { infraStore, updateInfra, getInfra } from './stores/infra';
20
+ export type { Theme } from './stores/theme';
21
+ export { kTheme, DEFAULT_THEME, themeStore, themeIsDark, toggleTheme } from './stores/theme';
22
+
23
+ // ── Contracts ──
24
+ export { tangleJobsAbi, tangleServicesAbi, tangleOperatorsAbi } from './contracts/abi';
25
+ export type { CoreAddresses, NetworkConfig } from './contracts/chains';
26
+ export {
27
+ resolveRpcUrl,
28
+ rpcUrl,
29
+ tangleLocal,
30
+ tangleTestnet,
31
+ tangleMainnet,
32
+ allTangleChains,
33
+ mainnet,
34
+ configureNetworks,
35
+ getNetworks,
36
+ } from './contracts/chains';
37
+ export {
38
+ selectedChainIdStore,
39
+ publicClientStore,
40
+ getPublicClient,
41
+ publicClient,
42
+ getAddresses,
43
+ } from './contracts/publicClient';
44
+ export { encodeJobArgs } from './contracts/generic-encoder';
45
+
46
+ // ── Blueprints ──
47
+ export type {
48
+ JobCategory,
49
+ JobFieldDef,
50
+ AbiContextParam,
51
+ JobDefinition,
52
+ BlueprintDefinition,
53
+ } from './blueprints/registry';
54
+ export {
55
+ registerBlueprint,
56
+ getBlueprint,
57
+ getAllBlueprints,
58
+ getBlueprintJobs,
59
+ getJobById,
60
+ } from './blueprints/registry';
61
+
62
+ // ── Hooks ──
63
+ export type { DiscoveredOperator } from './hooks/useOperators';
64
+ export { useOperators } from './hooks/useOperators';
65
+ export type { JobFormState } from './hooks/useJobForm';
66
+ export { useJobForm } from './hooks/useJobForm';
67
+ export type { JobQuote, UseJobPriceResult, JobPriceEntry, UseJobPricesResult } from './hooks/useJobPrice';
68
+ export { useJobPrice, useJobPrices } from './hooks/useJobPrice';
69
+ export type { ServiceInfo } from './hooks/useServiceValidation';
70
+ export { useServiceValidation } from './hooks/useServiceValidation';
71
+ export { useAuthenticatedFetch } from './hooks/useAuthenticatedFetch';
72
+ export type { OperatorQuote, UseQuotesResult } from './hooks/useQuotes';
73
+ export { solvePoW, formatCost, useQuotes } from './hooks/useQuotes';
74
+ export type { SubmitJobOpts, JobSubmitStatus } from './hooks/useSubmitJob';
75
+ export { useSubmitJob } from './hooks/useSubmitJob';
76
+ export { useSessionAuth } from './hooks/useSessionAuth';
77
+ export type { ProvisionPhase, ProvisionStatus } from './hooks/useProvisionProgress';
78
+ export { getPhaseLabel, isTerminalPhase, useProvisionProgress } from './hooks/useProvisionProgress';
79
+ export { useThemeValue } from './hooks/useThemeValue';