@tangle-network/blueprint-ui 0.1.1 → 0.3.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.
- package/README.md +38 -4
- package/dist/BlueprintHostPanel-L1KKLNbr.d.ts +124 -0
- package/dist/chunk-37ADATBT.js +55 -0
- package/dist/chunk-37ADATBT.js.map +1 -0
- package/dist/chunk-5PCH2RJF.js +1540 -0
- package/dist/chunk-5PCH2RJF.js.map +1 -0
- package/dist/components.d.ts +179 -0
- package/dist/components.js +1130 -0
- package/dist/components.js.map +1 -0
- package/dist/index.d.ts +8604 -0
- package/dist/index.js +839 -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/styles.css +560 -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 +39 -9
- package/src/components/forms/JobExecutionDialog.tsx +10 -2
- package/src/components.ts +3 -0
- package/src/contracts/abi.ts +12 -0
- package/src/contracts/chains.ts +4 -3
- package/src/contracts/publicClient.ts +2 -1
- package/src/hooks/useJobPrice.test.ts +214 -0
- package/src/hooks/useJobPrice.ts +56 -2
- package/src/hooks/useProvisionProgress.ts +2 -1
- package/src/hooks/useQuotes.ts +112 -14
- package/src/hooks/useSessionAuth.ts +2 -1
- 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 +41 -1
- package/src/stores/infra.ts +3 -2
- package/src/styles.css +128 -0
- package/src/test-setup.ts +1 -0
- package/src/utils/env.ts +22 -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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ParentBridgeProvider } from './parentBridgeProvider';
|
|
3
|
+
import {
|
|
4
|
+
NO_WALLET_ADDRESS,
|
|
5
|
+
TANGLE_IFRAME_PROTOCOL_VERSION,
|
|
6
|
+
} from './parentBridgeProtocol';
|
|
7
|
+
|
|
8
|
+
const PARENT_ORIGIN = 'https://cloud.tangle.tools';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Drive the provider against a fake parent: capture every message the
|
|
12
|
+
* provider posts, route a scripted response (or broadcast) back, and assert
|
|
13
|
+
* the provider's observable behavior.
|
|
14
|
+
*
|
|
15
|
+
* The fake parent runs in the same JS context as the provider (jsdom's
|
|
16
|
+
* `window.parent === window`), so we monkey-patch `window.parent.postMessage`
|
|
17
|
+
* to intercept; for inbound messages we synthesize `MessageEvent`s with the
|
|
18
|
+
* trusted origin and dispatch them onto `window`.
|
|
19
|
+
*/
|
|
20
|
+
type Captured = { message: unknown; origin: string };
|
|
21
|
+
|
|
22
|
+
function setupFakeParent() {
|
|
23
|
+
const captured: Captured[] = [];
|
|
24
|
+
const originalParent = window.parent;
|
|
25
|
+
Object.defineProperty(window, 'parent', {
|
|
26
|
+
configurable: true,
|
|
27
|
+
get() {
|
|
28
|
+
return {
|
|
29
|
+
postMessage: (message: unknown, targetOrigin: string) => {
|
|
30
|
+
captured.push({ message, origin: targetOrigin });
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const restore = () => {
|
|
36
|
+
Object.defineProperty(window, 'parent', {
|
|
37
|
+
configurable: true,
|
|
38
|
+
value: originalParent,
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
const inbound = (data: object) =>
|
|
42
|
+
window.dispatchEvent(new MessageEvent('message', { data, origin: PARENT_ORIGIN }));
|
|
43
|
+
return { captured, inbound, restore };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('ParentBridgeProvider', () => {
|
|
47
|
+
let fake: ReturnType<typeof setupFakeParent>;
|
|
48
|
+
let provider: ParentBridgeProvider;
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
fake = setupFakeParent();
|
|
52
|
+
provider = new ParentBridgeProvider({
|
|
53
|
+
parentOrigin: PARENT_ORIGIN,
|
|
54
|
+
appId: 'agent-sandbox',
|
|
55
|
+
requestTimeoutMs: 1_000,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
provider.uninstall();
|
|
61
|
+
fake.restore();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('sends a handshake on install + pins targetOrigin to the parent', () => {
|
|
65
|
+
provider.install();
|
|
66
|
+
expect(fake.captured).toHaveLength(1);
|
|
67
|
+
expect(fake.captured[0].origin).toBe(PARENT_ORIGIN);
|
|
68
|
+
expect(fake.captured[0].message).toEqual({
|
|
69
|
+
kind: 'tangle.app.handshake',
|
|
70
|
+
appId: 'agent-sandbox',
|
|
71
|
+
version: TANGLE_IFRAME_PROTOCOL_VERSION,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('rejects messages from origins that are not the configured parent', () => {
|
|
76
|
+
provider.install();
|
|
77
|
+
// Manually dispatch a message from a different origin claiming to be a
|
|
78
|
+
// handshake ack. The provider must ignore it — verified by inspecting
|
|
79
|
+
// `getCachedAccount` (which gets set on a successful readAccountResult).
|
|
80
|
+
window.dispatchEvent(
|
|
81
|
+
new MessageEvent('message', {
|
|
82
|
+
data: {
|
|
83
|
+
kind: 'tangle.app.accountChanged',
|
|
84
|
+
account: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
|
|
85
|
+
},
|
|
86
|
+
origin: 'https://evil.example.com',
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
expect(provider.getCachedAccount()).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('resolves eth_accounts to [] when parent reports zero-address', async () => {
|
|
93
|
+
provider.install();
|
|
94
|
+
fake.inbound({
|
|
95
|
+
kind: 'tangle.app.handshakeAck',
|
|
96
|
+
appId: 'agent-sandbox',
|
|
97
|
+
protocolVersion: TANGLE_IFRAME_PROTOCOL_VERSION,
|
|
98
|
+
});
|
|
99
|
+
// Provider auto-fires a readAccount after handshakeAck. Wait for it,
|
|
100
|
+
// then reply with the zero-address sentinel.
|
|
101
|
+
const readAccountMsg = await vi.waitFor(() => {
|
|
102
|
+
const m = fake.captured.find(
|
|
103
|
+
(c) =>
|
|
104
|
+
typeof c.message === 'object' &&
|
|
105
|
+
c.message !== null &&
|
|
106
|
+
(c.message as { kind?: string }).kind === 'tangle.app.readAccount',
|
|
107
|
+
);
|
|
108
|
+
if (!m) throw new Error('readAccount not posted yet');
|
|
109
|
+
return m;
|
|
110
|
+
});
|
|
111
|
+
const correlationId = (readAccountMsg.message as { correlationId: string })
|
|
112
|
+
.correlationId;
|
|
113
|
+
fake.inbound({
|
|
114
|
+
kind: 'tangle.app.readAccountResult',
|
|
115
|
+
correlationId,
|
|
116
|
+
ok: true,
|
|
117
|
+
data: { account: NO_WALLET_ADDRESS, chainId: 84532 },
|
|
118
|
+
});
|
|
119
|
+
const accounts = await provider.request({ method: 'eth_accounts' });
|
|
120
|
+
expect(accounts).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('emits accountsChanged when the parent broadcasts a new account', () => {
|
|
124
|
+
provider.install();
|
|
125
|
+
fake.inbound({
|
|
126
|
+
kind: 'tangle.app.handshakeAck',
|
|
127
|
+
appId: 'agent-sandbox',
|
|
128
|
+
protocolVersion: TANGLE_IFRAME_PROTOCOL_VERSION,
|
|
129
|
+
});
|
|
130
|
+
const seen: unknown[] = [];
|
|
131
|
+
provider.on('accountsChanged', (accounts) => seen.push(accounts));
|
|
132
|
+
fake.inbound({
|
|
133
|
+
kind: 'tangle.app.accountChanged',
|
|
134
|
+
account: '0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc',
|
|
135
|
+
});
|
|
136
|
+
expect(seen).toEqual([['0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc']]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('forwards personal_sign through the bridge and resolves on signMessageResult', async () => {
|
|
140
|
+
provider.install();
|
|
141
|
+
fake.inbound({
|
|
142
|
+
kind: 'tangle.app.handshakeAck',
|
|
143
|
+
appId: 'agent-sandbox',
|
|
144
|
+
protocolVersion: TANGLE_IFRAME_PROTOCOL_VERSION,
|
|
145
|
+
});
|
|
146
|
+
// Establish chain context — the bridge needs `cachedChainId` to sign.
|
|
147
|
+
fake.inbound({ kind: 'tangle.app.chainChanged', chainId: 84532 });
|
|
148
|
+
|
|
149
|
+
const signed = provider.request({
|
|
150
|
+
method: 'personal_sign',
|
|
151
|
+
params: ['hello', '0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc'],
|
|
152
|
+
});
|
|
153
|
+
// Find the outbound signMessage request.
|
|
154
|
+
const outbound = await vi.waitFor(() => {
|
|
155
|
+
const msg = fake.captured.find(
|
|
156
|
+
(c) =>
|
|
157
|
+
typeof c.message === 'object' &&
|
|
158
|
+
c.message !== null &&
|
|
159
|
+
(c.message as { kind?: string }).kind === 'tangle.app.signMessage',
|
|
160
|
+
);
|
|
161
|
+
if (!msg) throw new Error('signMessage not posted yet');
|
|
162
|
+
return msg;
|
|
163
|
+
});
|
|
164
|
+
const correlationId = (outbound.message as { correlationId: string })
|
|
165
|
+
.correlationId;
|
|
166
|
+
expect((outbound.message as { message: string }).message).toBe('hello');
|
|
167
|
+
fake.inbound({
|
|
168
|
+
kind: 'tangle.app.signMessageResult',
|
|
169
|
+
correlationId,
|
|
170
|
+
ok: true,
|
|
171
|
+
data: { signature: '0xdeadbeef' },
|
|
172
|
+
});
|
|
173
|
+
await expect(signed).resolves.toBe('0xdeadbeef');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('rejects on parent error response with the parent-provided error message', async () => {
|
|
177
|
+
provider.install();
|
|
178
|
+
fake.inbound({
|
|
179
|
+
kind: 'tangle.app.handshakeAck',
|
|
180
|
+
appId: 'agent-sandbox',
|
|
181
|
+
protocolVersion: TANGLE_IFRAME_PROTOCOL_VERSION,
|
|
182
|
+
});
|
|
183
|
+
fake.inbound({ kind: 'tangle.app.chainChanged', chainId: 84532 });
|
|
184
|
+
|
|
185
|
+
const signed = provider.request({
|
|
186
|
+
method: 'personal_sign',
|
|
187
|
+
params: ['hello', '0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc'],
|
|
188
|
+
});
|
|
189
|
+
const outbound = await vi.waitFor(() => {
|
|
190
|
+
const msg = fake.captured.find(
|
|
191
|
+
(c) =>
|
|
192
|
+
typeof c.message === 'object' &&
|
|
193
|
+
c.message !== null &&
|
|
194
|
+
(c.message as { kind?: string }).kind === 'tangle.app.signMessage',
|
|
195
|
+
);
|
|
196
|
+
if (!msg) throw new Error('not posted yet');
|
|
197
|
+
return msg;
|
|
198
|
+
});
|
|
199
|
+
const correlationId = (outbound.message as { correlationId: string })
|
|
200
|
+
.correlationId;
|
|
201
|
+
fake.inbound({
|
|
202
|
+
kind: 'tangle.app.signMessageResult',
|
|
203
|
+
correlationId,
|
|
204
|
+
ok: false,
|
|
205
|
+
error: 'user-rejected',
|
|
206
|
+
});
|
|
207
|
+
await expect(signed).rejects.toThrow('user-rejected');
|
|
208
|
+
});
|
|
209
|
+
});
|