@tangle-network/blueprint-ui 0.1.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{registry-JhwB9BPD.d.ts → BlueprintHostPanel-L1KKLNbr.d.ts} +38 -1
- package/dist/{chunk-A6PJT5YQ.js → chunk-5PCH2RJF.js} +370 -10
- package/dist/chunk-5PCH2RJF.js.map +1 -0
- package/dist/components.d.ts +2 -2
- package/dist/components.js +18 -15
- package/dist/components.js.map +1 -1
- package/dist/index.d.ts +142 -8
- package/dist/index.js +19 -21
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -0
- package/dist/wallet/index.d.ts +188 -0
- package/dist/wallet/index.js +466 -0
- package/dist/wallet/index.js.map +1 -0
- package/package.json +21 -15
- package/src/components/forms/JobExecutionDialog.tsx +10 -2
- package/src/contracts/abi.ts +3 -0
- package/src/hooks/useJobPrice.test.ts +214 -0
- package/src/hooks/useJobPrice.ts +56 -2
- package/src/hooks/useQuotes.ts +44 -1
- package/src/test-setup.ts +1 -0
- package/src/wallet/detectParentOrigin.ts +74 -0
- package/src/wallet/index.ts +67 -0
- package/src/wallet/parentBridgeConnector.ts +156 -0
- package/src/wallet/parentBridgeProtocol.ts +109 -0
- package/src/wallet/parentBridgeProvider.test.ts +209 -0
- package/src/wallet/parentBridgeProvider.ts +411 -0
- package/tsconfig.json +1 -1
- package/dist/BlueprintHostPanel-6iVEh-f1.d.ts +0 -39
- package/dist/chunk-A6PJT5YQ.js.map +0 -1
- package/dist/chunk-GD3AZEJL.js +0 -327
- package/dist/chunk-GD3AZEJL.js.map +0 -1
- package/dist/host.d.ts +0 -96
- package/dist/host.js +0 -39
- package/dist/host.js.map +0 -1
package/src/hooks/useQuotes.ts
CHANGED
|
@@ -33,6 +33,12 @@ export interface OperatorQuote {
|
|
|
33
33
|
totalCost: bigint;
|
|
34
34
|
signature: `0x${string}`;
|
|
35
35
|
details: {
|
|
36
|
+
/**
|
|
37
|
+
* Address of the account that will submit `createServiceFromQuotes`.
|
|
38
|
+
* Required since tnt-core v0.13.0 — the contract enforces
|
|
39
|
+
* `requester == msg.sender` and rejects `address(0)` (no wildcard quotes).
|
|
40
|
+
*/
|
|
41
|
+
requester: Address;
|
|
36
42
|
blueprintId: bigint;
|
|
37
43
|
ttlBlocks: bigint;
|
|
38
44
|
totalCost: bigint;
|
|
@@ -170,13 +176,40 @@ function mapJsonResourceCommitment(resource: any): ResourceCommitment {
|
|
|
170
176
|
|
|
171
177
|
// ── Hook ──
|
|
172
178
|
|
|
179
|
+
const ZERO_ADDRESS_LOWER = ZERO_ADDRESS.toLowerCase();
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Fetches signed `OperatorQuote` payloads from a set of operators for
|
|
183
|
+
* `createServiceFromQuotes`.
|
|
184
|
+
*
|
|
185
|
+
* @param operators Operators discovered from the on-chain registry.
|
|
186
|
+
* @param blueprintId Target blueprint id.
|
|
187
|
+
* @param ttlBlocks Requested service TTL in blocks.
|
|
188
|
+
* @param enabled Gate to disable fetching (e.g. while inputs settle).
|
|
189
|
+
* @param requester Address that will submit `createServiceFromQuotes` —
|
|
190
|
+
* must equal `msg.sender` at submission time. Required
|
|
191
|
+
* since tnt-core v0.13.0; the contract rejects
|
|
192
|
+
* `address(0)` and any mismatch.
|
|
193
|
+
* @param requireTee If true, asks operators for TEE-attested quotes.
|
|
194
|
+
*/
|
|
173
195
|
export function useQuotes(
|
|
174
196
|
operators: DiscoveredOperator[],
|
|
175
197
|
blueprintId: bigint,
|
|
176
198
|
ttlBlocks: bigint,
|
|
177
199
|
enabled: boolean,
|
|
200
|
+
requester: Address,
|
|
178
201
|
requireTee = false,
|
|
179
202
|
): UseQuotesResult {
|
|
203
|
+
// Defensive guard: only assert when the caller has actually opted in via
|
|
204
|
+
// `enabled`. This lets components compute `requester` from
|
|
205
|
+
// `useAccount().address` and pass `enabled=false` until the wallet connects.
|
|
206
|
+
if (enabled && (!requester || requester.toLowerCase() === ZERO_ADDRESS_LOWER)) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
'useQuotes: `requester` is required and must be a non-zero address when `enabled=true`. ' +
|
|
209
|
+
'Pass `useAccount().address` from wagmi. tnt-core v0.13.0 contracts ' +
|
|
210
|
+
'reject quotes whose requester is address(0) or != msg.sender.',
|
|
211
|
+
);
|
|
212
|
+
}
|
|
180
213
|
const [quotes, setQuotes] = useState<OperatorQuote[]>([]);
|
|
181
214
|
const [isLoading, setIsLoading] = useState(false);
|
|
182
215
|
const [isSolvingPow, setIsSolvingPow] = useState(false);
|
|
@@ -224,6 +257,7 @@ export function useQuotes(
|
|
|
224
257
|
proofOfWork: proof,
|
|
225
258
|
challengeTimestamp: timestamp,
|
|
226
259
|
requireTee,
|
|
260
|
+
requester,
|
|
227
261
|
});
|
|
228
262
|
|
|
229
263
|
if (!response) throw new Error('No quote returned from operator');
|
|
@@ -250,7 +284,7 @@ export function useQuotes(
|
|
|
250
284
|
return () => {
|
|
251
285
|
cancelled = true;
|
|
252
286
|
};
|
|
253
|
-
}, [operators, blueprintId, ttlBlocks, enabled, fetchKey, requireTee]);
|
|
287
|
+
}, [operators, blueprintId, ttlBlocks, enabled, fetchKey, requireTee, requester]);
|
|
254
288
|
|
|
255
289
|
const totalCost = quotes.reduce((sum, q) => sum + q.totalCost, 0n);
|
|
256
290
|
|
|
@@ -270,6 +304,7 @@ async function fetchPriceFromOperator(
|
|
|
270
304
|
proofOfWork: Uint8Array;
|
|
271
305
|
challengeTimestamp: bigint;
|
|
272
306
|
requireTee: boolean;
|
|
307
|
+
requester: Address;
|
|
273
308
|
},
|
|
274
309
|
): Promise<OperatorQuote | null> {
|
|
275
310
|
// Try JSON endpoint (simpler, no protobuf dependency required)
|
|
@@ -283,6 +318,10 @@ async function fetchPriceFromOperator(
|
|
|
283
318
|
proof_of_work: toHex(params.proofOfWork),
|
|
284
319
|
challenge_timestamp: String(params.challengeTimestamp),
|
|
285
320
|
require_tee: params.requireTee,
|
|
321
|
+
// tnt-core v0.13.0: bind the quote to the future caller. Operators
|
|
322
|
+
// sign this address into QuoteDetails; the contract enforces
|
|
323
|
+
// `requester == msg.sender`.
|
|
324
|
+
requester: params.requester,
|
|
286
325
|
resource_requirements: DEFAULT_RESOURCE_REQUIREMENTS,
|
|
287
326
|
}),
|
|
288
327
|
signal: AbortSignal.timeout(10_000),
|
|
@@ -299,6 +338,10 @@ async function fetchPriceFromOperator(
|
|
|
299
338
|
teeAttested: Boolean(data.tee_attested),
|
|
300
339
|
teeProvider: data.tee_provider || undefined,
|
|
301
340
|
details: {
|
|
341
|
+
// Prefer the operator-signed value; fall back to the hook's input.
|
|
342
|
+
// If the operator returns a mismatched requester the contract will
|
|
343
|
+
// revert at submission, so callers should still verify equality.
|
|
344
|
+
requester: ((data.details?.requester as Address | undefined) ?? params.requester),
|
|
302
345
|
blueprintId: BigInt(data.details?.blueprint_id ?? params.blueprintId),
|
|
303
346
|
ttlBlocks: BigInt(data.details?.ttl_blocks ?? params.ttlBlocks),
|
|
304
347
|
totalCost: BigInt(data.details?.total_cost ?? '0'),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/vitest';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Determine which origin to trust as the parent dapp.
|
|
2
|
+
//
|
|
3
|
+
// `document.referrer` is the *initial* embedder — it's set when the iframe is
|
|
4
|
+
// first loaded and survives reloads (though it can be cleared by `referrerpolicy`
|
|
5
|
+
// or by the embedder). The Tangle Cloud iframe wrapper deliberately omits
|
|
6
|
+
// `referrerpolicy="no-referrer"` so we get the embedder's origin here.
|
|
7
|
+
//
|
|
8
|
+
// We compare it against an allowlist of known Tangle Cloud origins. If it
|
|
9
|
+
// matches, that's the parent. Otherwise the iframe is being loaded directly
|
|
10
|
+
// (standalone domain visit, dev server, untrusted embedder) and the bridge
|
|
11
|
+
// stays disabled — the app falls back to its normal injected/WC wallet path.
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default Tangle Cloud origins. Consumers (agent-sandbox UI,
|
|
15
|
+
* trading-arena, future iframe blueprints) pass app-specific additions
|
|
16
|
+
* via `extraOrigins` rather than mutating this list.
|
|
17
|
+
*/
|
|
18
|
+
export const TANGLE_CLOUD_ORIGINS_DEFAULT = Object.freeze([
|
|
19
|
+
'https://cloud.tangle.tools',
|
|
20
|
+
'https://develop.cloud.tangle.tools',
|
|
21
|
+
// Local dev (Vite default port for tangle-cloud + Netlify dev preview).
|
|
22
|
+
'http://localhost:4300',
|
|
23
|
+
'http://localhost:8888',
|
|
24
|
+
] as const);
|
|
25
|
+
|
|
26
|
+
function originFromReferrer(): string | null {
|
|
27
|
+
if (typeof document === 'undefined') return null;
|
|
28
|
+
const ref = document.referrer;
|
|
29
|
+
if (!ref) return null;
|
|
30
|
+
try {
|
|
31
|
+
return new URL(ref).origin;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns the parent origin to bridge to, or null when no trusted parent is
|
|
39
|
+
* detected. Caller should skip installing the bridge connector when this
|
|
40
|
+
* returns null.
|
|
41
|
+
*
|
|
42
|
+
* `extraOrigins` is the application's escape hatch for staging or dev
|
|
43
|
+
* deploys not covered by the default list. The library deliberately does
|
|
44
|
+
* not read environment variables itself (consumers may bundle for non-Vite
|
|
45
|
+
* runtimes); the consuming app threads `import.meta.env.VITE_*` or
|
|
46
|
+
* `process.env.*` in itself.
|
|
47
|
+
*
|
|
48
|
+
* Falls back to a `?parent=<origin>` query parameter when no referrer is
|
|
49
|
+
* present (some browsers strip referrer from cross-origin loads). Useful
|
|
50
|
+
* for dev embedding flows.
|
|
51
|
+
*/
|
|
52
|
+
export function detectTangleCloudParentOrigin(
|
|
53
|
+
options: { extraOrigins?: readonly string[] } = {},
|
|
54
|
+
): string | null {
|
|
55
|
+
if (typeof window === 'undefined' || window.parent === window) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const allowlist = new Set<string>([
|
|
59
|
+
...TANGLE_CLOUD_ORIGINS_DEFAULT,
|
|
60
|
+
...(options.extraOrigins ?? []),
|
|
61
|
+
]);
|
|
62
|
+
const referrerOrigin = originFromReferrer();
|
|
63
|
+
if (referrerOrigin && allowlist.has(referrerOrigin)) {
|
|
64
|
+
return referrerOrigin;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const url = new URL(window.location.href);
|
|
68
|
+
const explicit = url.searchParams.get('parent');
|
|
69
|
+
if (explicit && allowlist.has(explicit)) return explicit;
|
|
70
|
+
} catch {
|
|
71
|
+
// ignore
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tangle Cloud parent-bridge wallet adapter.
|
|
3
|
+
*
|
|
4
|
+
* iframe blueprints embedded by the Tangle Cloud dapp can't use the usual
|
|
5
|
+
* `window.ethereum` connector — browser wallet extensions don't inject into
|
|
6
|
+
* sandboxed iframes. This module ships a wagmi connector that proxies wallet
|
|
7
|
+
* operations to the parent dapp through the existing `tangle.app.*`
|
|
8
|
+
* postMessage protocol, so the iframe inherits the parent's wallet without
|
|
9
|
+
* its own picker.
|
|
10
|
+
*
|
|
11
|
+
* Usage in an iframe app's wagmi config:
|
|
12
|
+
*
|
|
13
|
+
* import {
|
|
14
|
+
* detectTangleCloudParentOrigin,
|
|
15
|
+
* parentBridgeConnector,
|
|
16
|
+
* } from '@tangle-network/blueprint-ui/wallet';
|
|
17
|
+
*
|
|
18
|
+
* const parent = detectTangleCloudParentOrigin();
|
|
19
|
+
* const config = createConfig(
|
|
20
|
+
* parent !== null
|
|
21
|
+
* ? { ...getDefaultConfig({...}), connectors: [
|
|
22
|
+
* parentBridgeConnector({ parentOrigin: parent, appId: 'my-app' }),
|
|
23
|
+
* ] }
|
|
24
|
+
* : getDefaultConfig({...}),
|
|
25
|
+
* );
|
|
26
|
+
*
|
|
27
|
+
* The bridge is intentionally the ONLY connector when running inside the
|
|
28
|
+
* dapp — surfacing injected / WalletConnect / Coinbase inside a sandboxed
|
|
29
|
+
* iframe doesn't work (no popup, no extension injection) and would just
|
|
30
|
+
* confuse operators.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
detectTangleCloudParentOrigin,
|
|
35
|
+
TANGLE_CLOUD_ORIGINS_DEFAULT,
|
|
36
|
+
} from './detectParentOrigin';
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
parentBridgeConnector,
|
|
40
|
+
type ParentBridgeConnectorOptions,
|
|
41
|
+
} from './parentBridgeConnector';
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
ParentBridgeProvider,
|
|
45
|
+
isRunningInIframe,
|
|
46
|
+
type ParentBridgeOptions,
|
|
47
|
+
} from './parentBridgeProvider';
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
TANGLE_IFRAME_PROTOCOL_PREFIX,
|
|
51
|
+
TANGLE_IFRAME_PROTOCOL_VERSION,
|
|
52
|
+
NO_WALLET_ADDRESS,
|
|
53
|
+
makeCorrelationId,
|
|
54
|
+
type AccountChanged,
|
|
55
|
+
type ChainChanged,
|
|
56
|
+
type HandshakeAck,
|
|
57
|
+
type HandshakeRequest,
|
|
58
|
+
type ParentMessage,
|
|
59
|
+
type ReadAccountRequest,
|
|
60
|
+
type ReadAccountResult,
|
|
61
|
+
type SignMessageRequest,
|
|
62
|
+
type SignMessageResult,
|
|
63
|
+
type SignTransactionRequest,
|
|
64
|
+
type SignTransactionResult,
|
|
65
|
+
type SwitchChainRequest,
|
|
66
|
+
type SwitchChainResult,
|
|
67
|
+
} from './parentBridgeProtocol';
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Wagmi connector that proxies wallet operations to the Tangle Cloud parent
|
|
2
|
+
// dapp via the iframe postMessage bridge. Becomes the autoConnect target
|
|
3
|
+
// when this app is loaded inside an iframe sandbox without a window.ethereum
|
|
4
|
+
// — i.e. always, when embedded by cloud.tangle.tools.
|
|
5
|
+
//
|
|
6
|
+
// Architecture: the connector owns one `ParentBridgeProvider` (singleton),
|
|
7
|
+
// forwards every wagmi method to it, and reflects the provider's EIP-1193
|
|
8
|
+
// events back to wagmi's emitter so the rest of the dapp (ConnectKit's
|
|
9
|
+
// account chip, hooks like useAccount/useChainId) reacts to parent-state
|
|
10
|
+
// changes without polling.
|
|
11
|
+
|
|
12
|
+
import type { Address, Chain } from 'viem';
|
|
13
|
+
import { createConnector } from 'wagmi';
|
|
14
|
+
|
|
15
|
+
import { ParentBridgeProvider, type ParentBridgeOptions } from './parentBridgeProvider';
|
|
16
|
+
|
|
17
|
+
export type ParentBridgeConnectorOptions = ParentBridgeOptions;
|
|
18
|
+
|
|
19
|
+
export function parentBridgeConnector(options: ParentBridgeConnectorOptions) {
|
|
20
|
+
let provider: ParentBridgeProvider | undefined;
|
|
21
|
+
let installed = false;
|
|
22
|
+
|
|
23
|
+
return createConnector<ParentBridgeProvider>((config) => {
|
|
24
|
+
const ensureProvider = (): ParentBridgeProvider => {
|
|
25
|
+
if (!provider) provider = new ParentBridgeProvider(options);
|
|
26
|
+
if (!installed) {
|
|
27
|
+
provider.install();
|
|
28
|
+
installed = true;
|
|
29
|
+
// Wire the provider's EIP-1193 events to wagmi's emitter so
|
|
30
|
+
// ConnectKit and useAccount/useChainId reflect parent-state changes
|
|
31
|
+
// without polling.
|
|
32
|
+
provider.on('accountsChanged', (accounts) => {
|
|
33
|
+
config.emitter.emit('change', {
|
|
34
|
+
accounts: Array.isArray(accounts)
|
|
35
|
+
? (accounts as readonly Address[])
|
|
36
|
+
: ([] as readonly Address[]),
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
provider.on('chainChanged', (chainIdHex) => {
|
|
40
|
+
const chainId =
|
|
41
|
+
typeof chainIdHex === 'string'
|
|
42
|
+
? Number.parseInt(chainIdHex, 16)
|
|
43
|
+
: Number(chainIdHex);
|
|
44
|
+
if (Number.isFinite(chainId)) {
|
|
45
|
+
config.emitter.emit('change', { chainId });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
provider.on('disconnect', () => {
|
|
49
|
+
config.emitter.emit('disconnect');
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return provider;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
id: 'tangleParentBridge',
|
|
57
|
+
name: 'Tangle Cloud',
|
|
58
|
+
type: 'parentBridge',
|
|
59
|
+
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
async connect(): Promise<any> {
|
|
62
|
+
// wagmi v3's connect() return type is a conditional based on
|
|
63
|
+
// `withCapabilities`. We always return plain addresses; cast through
|
|
64
|
+
// `any` rather than re-implementing the type predicate.
|
|
65
|
+
const p = ensureProvider();
|
|
66
|
+
const accountsResult = (await p.request({
|
|
67
|
+
method: 'eth_requestAccounts',
|
|
68
|
+
})) as readonly Address[];
|
|
69
|
+
const chainIdHex = (await p.request({ method: 'eth_chainId' })) as string;
|
|
70
|
+
const chainId = Number.parseInt(chainIdHex, 16);
|
|
71
|
+
return {
|
|
72
|
+
accounts: accountsResult,
|
|
73
|
+
chainId: Number.isFinite(chainId) ? chainId : 0,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
async disconnect() {
|
|
78
|
+
// Disconnect from the iframe's perspective is a local-only state
|
|
79
|
+
// reset — we can't ask the parent dapp to disconnect its wallet on
|
|
80
|
+
// our behalf, and a real disconnect should be initiated from the
|
|
81
|
+
// parent's UI. Tear down listeners + the message bridge so a future
|
|
82
|
+
// reconnect re-handshakes cleanly.
|
|
83
|
+
if (provider) provider.uninstall();
|
|
84
|
+
installed = false;
|
|
85
|
+
provider = undefined;
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async getAccounts() {
|
|
89
|
+
const p = ensureProvider();
|
|
90
|
+
const cached = p.getCachedAccount();
|
|
91
|
+
if (cached) return [cached];
|
|
92
|
+
const accounts = (await p.request({
|
|
93
|
+
method: 'eth_accounts',
|
|
94
|
+
})) as readonly Address[];
|
|
95
|
+
return accounts;
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
async getChainId() {
|
|
99
|
+
const p = ensureProvider();
|
|
100
|
+
const cached = p.getCachedChainId();
|
|
101
|
+
if (cached !== null) return cached;
|
|
102
|
+
const chainIdHex = (await p.request({ method: 'eth_chainId' })) as string;
|
|
103
|
+
const chainId = Number.parseInt(chainIdHex, 16);
|
|
104
|
+
return Number.isFinite(chainId) ? chainId : 0;
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async getProvider() {
|
|
108
|
+
return ensureProvider();
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
async isAuthorized() {
|
|
112
|
+
// Always authorized when in iframe mode — the parent dapp has
|
|
113
|
+
// already gated access by being the embedder. Returning `true`
|
|
114
|
+
// makes wagmi auto-reconnect on every page load, which is the
|
|
115
|
+
// right UX (iframe → parent wallet is always-on).
|
|
116
|
+
try {
|
|
117
|
+
const p = ensureProvider();
|
|
118
|
+
const accounts = (await p.request({
|
|
119
|
+
method: 'eth_accounts',
|
|
120
|
+
})) as readonly Address[];
|
|
121
|
+
return accounts.length > 0;
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
async switchChain({ chainId }): Promise<Chain> {
|
|
128
|
+
const p = ensureProvider();
|
|
129
|
+
await p.request({
|
|
130
|
+
method: 'wallet_switchEthereumChain',
|
|
131
|
+
params: [{ chainId: `0x${chainId.toString(16)}` }],
|
|
132
|
+
});
|
|
133
|
+
const chain = config.chains.find((c) => c.id === chainId);
|
|
134
|
+
if (!chain) {
|
|
135
|
+
throw new Error(`Chain ${chainId} not configured for this app`);
|
|
136
|
+
}
|
|
137
|
+
return chain;
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
onAccountsChanged(accounts) {
|
|
141
|
+
config.emitter.emit('change', {
|
|
142
|
+
accounts: accounts as readonly Address[],
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
onChainChanged(chainIdHex) {
|
|
146
|
+
const chainId = Number.parseInt(chainIdHex, 16);
|
|
147
|
+
if (Number.isFinite(chainId)) {
|
|
148
|
+
config.emitter.emit('change', { chainId });
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
onDisconnect() {
|
|
152
|
+
config.emitter.emit('disconnect');
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Tangle Cloud iframe ↔ parent dapp protocol — must mirror the parent's
|
|
2
|
+
// spec at `apps/tangle-cloud/src/blueprintApps/iframe/protocol.ts`. Bump the
|
|
3
|
+
// version constant in lockstep when either side adds a request kind.
|
|
4
|
+
|
|
5
|
+
import type { Address, Hex } from 'viem';
|
|
6
|
+
|
|
7
|
+
export const TANGLE_IFRAME_PROTOCOL_VERSION = '1' as const;
|
|
8
|
+
export const TANGLE_IFRAME_PROTOCOL_PREFIX = 'tangle.app.';
|
|
9
|
+
|
|
10
|
+
// ─── Iframe → Parent requests ────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export type HandshakeRequest = {
|
|
13
|
+
kind: 'tangle.app.handshake';
|
|
14
|
+
appId: string;
|
|
15
|
+
version: typeof TANGLE_IFRAME_PROTOCOL_VERSION;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ReadAccountRequest = {
|
|
19
|
+
kind: 'tangle.app.readAccount';
|
|
20
|
+
correlationId: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type SwitchChainRequest = {
|
|
24
|
+
kind: 'tangle.app.switchChain';
|
|
25
|
+
correlationId: string;
|
|
26
|
+
chainId: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type SignMessageRequest = {
|
|
30
|
+
kind: 'tangle.app.signMessage';
|
|
31
|
+
correlationId: string;
|
|
32
|
+
chainId: number;
|
|
33
|
+
message: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type SignTransactionRequest = {
|
|
37
|
+
kind: 'tangle.app.signTransaction';
|
|
38
|
+
correlationId: string;
|
|
39
|
+
chainId: number;
|
|
40
|
+
to: Address;
|
|
41
|
+
data: Hex;
|
|
42
|
+
value?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ─── Parent → Iframe messages ────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export type HandshakeAck = {
|
|
48
|
+
kind: 'tangle.app.handshakeAck';
|
|
49
|
+
appId: string;
|
|
50
|
+
protocolVersion: typeof TANGLE_IFRAME_PROTOCOL_VERSION;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type ResultEnvelope<T> = { correlationId: string } & (
|
|
54
|
+
| { ok: true; data: T }
|
|
55
|
+
| { ok: false; error: string }
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
export type ReadAccountResult = {
|
|
59
|
+
kind: 'tangle.app.readAccountResult';
|
|
60
|
+
} & ResultEnvelope<{ account: Address; chainId: number }>;
|
|
61
|
+
|
|
62
|
+
export type SwitchChainResult = {
|
|
63
|
+
kind: 'tangle.app.switchChainResult';
|
|
64
|
+
} & ResultEnvelope<{ chainId: number }>;
|
|
65
|
+
|
|
66
|
+
export type SignMessageResult = {
|
|
67
|
+
kind: 'tangle.app.signMessageResult';
|
|
68
|
+
} & ResultEnvelope<{ signature: Hex }>;
|
|
69
|
+
|
|
70
|
+
export type SignTransactionResult = {
|
|
71
|
+
kind: 'tangle.app.signTransactionResult';
|
|
72
|
+
} & ResultEnvelope<{ txHash: Hex }>;
|
|
73
|
+
|
|
74
|
+
export type AccountChanged = {
|
|
75
|
+
kind: 'tangle.app.accountChanged';
|
|
76
|
+
account: Address | null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type ChainChanged = {
|
|
80
|
+
kind: 'tangle.app.chainChanged';
|
|
81
|
+
chainId: number;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type ParentMessage =
|
|
85
|
+
| HandshakeAck
|
|
86
|
+
| ReadAccountResult
|
|
87
|
+
| SwitchChainResult
|
|
88
|
+
| SignMessageResult
|
|
89
|
+
| SignTransactionResult
|
|
90
|
+
| AccountChanged
|
|
91
|
+
| ChainChanged;
|
|
92
|
+
|
|
93
|
+
// The zero address used by the parent when no wallet is connected. The parent
|
|
94
|
+
// always responds to readAccount with an address; this sentinel means "no
|
|
95
|
+
// wallet" without making the response type a union of result shapes.
|
|
96
|
+
export const NO_WALLET_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Cryptographically-random ASCII correlation id matching the parent's
|
|
100
|
+
* validator regex (`/^[\w.\-:]+$/`, max length 128). The connector keeps a
|
|
101
|
+
* Map<correlationId, Resolver> so each request resolves independently.
|
|
102
|
+
*/
|
|
103
|
+
export function makeCorrelationId(prefix: string): string {
|
|
104
|
+
const random =
|
|
105
|
+
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
106
|
+
? crypto.randomUUID()
|
|
107
|
+
: Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
108
|
+
return `${prefix}.${random}`;
|
|
109
|
+
}
|