@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.
Files changed (47) hide show
  1. package/README.md +38 -4
  2. package/dist/BlueprintHostPanel-L1KKLNbr.d.ts +124 -0
  3. package/dist/chunk-37ADATBT.js +55 -0
  4. package/dist/chunk-37ADATBT.js.map +1 -0
  5. package/dist/chunk-5PCH2RJF.js +1540 -0
  6. package/dist/chunk-5PCH2RJF.js.map +1 -0
  7. package/dist/components.d.ts +179 -0
  8. package/dist/components.js +1130 -0
  9. package/dist/components.js.map +1 -0
  10. package/dist/index.d.ts +8604 -0
  11. package/dist/index.js +839 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/preset.d.ts +60 -0
  14. package/dist/preset.js +7 -0
  15. package/dist/preset.js.map +1 -0
  16. package/dist/styles.css +560 -0
  17. package/dist/wallet/index.d.ts +188 -0
  18. package/dist/wallet/index.js +466 -0
  19. package/dist/wallet/index.js.map +1 -0
  20. package/package.json +39 -9
  21. package/src/components/forms/JobExecutionDialog.tsx +10 -2
  22. package/src/components.ts +3 -0
  23. package/src/contracts/abi.ts +12 -0
  24. package/src/contracts/chains.ts +4 -3
  25. package/src/contracts/publicClient.ts +2 -1
  26. package/src/hooks/useJobPrice.test.ts +214 -0
  27. package/src/hooks/useJobPrice.ts +56 -2
  28. package/src/hooks/useProvisionProgress.ts +2 -1
  29. package/src/hooks/useQuotes.ts +112 -14
  30. package/src/hooks/useSessionAuth.ts +2 -1
  31. package/src/host/components/BlueprintHostHero.tsx +91 -0
  32. package/src/host/components/BlueprintHostPanel.tsx +24 -0
  33. package/src/host/index.ts +42 -0
  34. package/src/host/resolver.ts +204 -0
  35. package/src/host/types.ts +111 -0
  36. package/src/index.ts +41 -1
  37. package/src/stores/infra.ts +3 -2
  38. package/src/styles.css +128 -0
  39. package/src/test-setup.ts +1 -0
  40. package/src/utils/env.ts +22 -0
  41. package/src/wallet/detectParentOrigin.ts +74 -0
  42. package/src/wallet/index.ts +67 -0
  43. package/src/wallet/parentBridgeConnector.ts +156 -0
  44. package/src/wallet/parentBridgeProtocol.ts +109 -0
  45. package/src/wallet/parentBridgeProvider.test.ts +209 -0
  46. package/src/wallet/parentBridgeProvider.ts +411 -0
  47. package/tsconfig.json +1 -1
@@ -0,0 +1,111 @@
1
+ export type BlueprintAppVisibility = 'first-party' | 'third-party';
2
+
3
+ export type BlueprintPublisherVerification =
4
+ | 'first-party'
5
+ | 'verified'
6
+ | 'unverified';
7
+
8
+ export type BlueprintExperienceTier =
9
+ | 'generic'
10
+ | 'declarative'
11
+ | 'curated-module'
12
+ | 'external-app';
13
+
14
+ export type BlueprintSlugPolicy =
15
+ | 'reserved'
16
+ | 'publisher-scoped'
17
+ | 'public-requested';
18
+
19
+ export type BlueprintUiSurface =
20
+ | 'generic-overview'
21
+ | 'service-explorer'
22
+ | 'service-console'
23
+ | 'actions-panel'
24
+ | 'resources'
25
+ | 'chat'
26
+ | 'vaults'
27
+ | 'metrics'
28
+ | 'permissions';
29
+
30
+ export type BlueprintResourceRoute =
31
+ | 'bots'
32
+ | 'agents'
33
+ | 'runs'
34
+ | 'vault'
35
+ | 'chat'
36
+ | 'custom';
37
+
38
+ export type BlueprintPermissionScope =
39
+ | 'blueprint'
40
+ | 'service'
41
+ | 'resource';
42
+
43
+ export type BlueprintExternalAppMode = 'link' | 'iframe';
44
+ export type BlueprintExternalAppTrust = 'trusted' | 'restricted';
45
+
46
+ export type BlueprintPublisher = {
47
+ label: string;
48
+ namespace?: string;
49
+ visibility: BlueprintAppVisibility;
50
+ verification: BlueprintPublisherVerification;
51
+ };
52
+
53
+ export type BlueprintResourceModel = {
54
+ serviceNoun: string;
55
+ resourceNoun: string;
56
+ resourceRoute?: BlueprintResourceRoute;
57
+ };
58
+
59
+ export type BlueprintPermissionDescriptor = {
60
+ key: string;
61
+ label: string;
62
+ scope: BlueprintPermissionScope;
63
+ description?: string;
64
+ };
65
+
66
+ export type BlueprintExternalAppConfig = {
67
+ url: string;
68
+ mode: BlueprintExternalAppMode;
69
+ label?: string;
70
+ host: string;
71
+ trust: BlueprintExternalAppTrust;
72
+ reason?: string;
73
+ };
74
+
75
+ export type BlueprintUiManifest = {
76
+ displayName: string;
77
+ tagline: string;
78
+ description: string;
79
+ surfaces: BlueprintUiSurface[];
80
+ resources: BlueprintResourceModel;
81
+ permissions?: BlueprintPermissionDescriptor[];
82
+ externalApp?: BlueprintExternalAppConfig;
83
+ };
84
+
85
+ export type BlueprintAppModuleBinding = {
86
+ moduleId: string;
87
+ status: 'active' | 'planned';
88
+ };
89
+
90
+ export type BlueprintAppEntry = {
91
+ slug: string;
92
+ canonicalSlug?: string;
93
+ blueprintId?: bigint;
94
+ publisher: BlueprintPublisher;
95
+ tier: BlueprintExperienceTier;
96
+ slugPolicy: BlueprintSlugPolicy;
97
+ manifest: BlueprintUiManifest;
98
+ module?: BlueprintAppModuleBinding;
99
+ };
100
+
101
+ export type BlueprintAppResolvedView = {
102
+ slug: string;
103
+ canonicalSlug: string;
104
+ blueprintId?: bigint;
105
+ publisher: BlueprintPublisher;
106
+ tier: BlueprintExperienceTier;
107
+ slugPolicy: BlueprintSlugPolicy;
108
+ manifest: BlueprintUiManifest;
109
+ module?: BlueprintAppModuleBinding;
110
+ fallbackEnabled: boolean;
111
+ };
package/src/index.ts CHANGED
@@ -61,9 +61,49 @@ export {
61
61
  getJobById,
62
62
  } from './blueprints/registry';
63
63
 
64
+ // ── Blueprint Host ──
65
+ export type {
66
+ BlueprintAppVisibility,
67
+ BlueprintPublisherVerification,
68
+ BlueprintExperienceTier,
69
+ BlueprintSlugPolicy,
70
+ BlueprintUiSurface,
71
+ BlueprintResourceRoute,
72
+ BlueprintPermissionScope,
73
+ BlueprintExternalAppMode,
74
+ BlueprintExternalAppTrust,
75
+ BlueprintPublisher,
76
+ BlueprintResourceModel,
77
+ BlueprintPermissionDescriptor,
78
+ BlueprintExternalAppConfig,
79
+ BlueprintUiManifest,
80
+ BlueprintAppModuleBinding,
81
+ BlueprintAppEntry,
82
+ BlueprintAppResolvedView,
83
+ } from './host';
84
+ export {
85
+ buildCanonicalBlueprintSlug,
86
+ resolveBlueprintAppView,
87
+ toBlueprintAppEntry,
88
+ getBlueprintExperienceTierLabel,
89
+ getBlueprintSlugPolicyLabel,
90
+ getBlueprintSurfaceLabel,
91
+ getBlueprintPublisherVerificationLabel,
92
+ getExternalAppTrustLabel,
93
+ isVerifiedBlueprintPublisher,
94
+ canPublisherClaimSlug,
95
+ isTrustedExternalAppHost,
96
+ getBlueprintPath,
97
+ getBlueprintServicePath,
98
+ sanitizeBlueprintSlugPart,
99
+ deriveBlueprintRequestedSlug,
100
+ } from './host';
101
+ export type { BlueprintHostHeroProps, BlueprintHostPanelProps } from './host';
102
+ export { BlueprintHostHero, BlueprintHostPanel } from './host';
103
+
64
104
  // ── Hooks ──
65
105
  export type { DiscoveredOperator } from './hooks/useOperators';
66
- export { useOperators } from './hooks/useOperators';
106
+ export { discoverOperatorsWithClient, useOperators } from './hooks/useOperators';
67
107
  export type { JobFormState } from './hooks/useJobForm';
68
108
  export { useJobForm } from './hooks/useJobForm';
69
109
  export type { JobQuote, UseJobPriceResult, JobPriceEntry, UseJobPricesResult } from './hooks/useJobPrice';
@@ -1,7 +1,8 @@
1
1
  import { persistedAtom } from './persistedAtom';
2
+ import { getEnvVar } from '../utils/env';
2
3
 
3
- const defaultBlueprintId = import.meta.env.VITE_BLUEPRINT_ID ?? '0';
4
- const defaultServiceId = import.meta.env.VITE_SERVICE_ID ?? import.meta.env.VITE_SERVICE_IDS?.split(',')[0] ?? '0';
4
+ const defaultBlueprintId = getEnvVar('VITE_BLUEPRINT_ID') ?? '0';
5
+ const defaultServiceId = getEnvVar('VITE_SERVICE_ID') ?? getEnvVar('VITE_SERVICE_IDS')?.split(',')[0] ?? '0';
5
6
 
6
7
  export interface OperatorInfo {
7
8
  address: string;
package/src/styles.css ADDED
@@ -0,0 +1,128 @@
1
+ :root,
2
+ .bp-tone-cloud {
3
+ --bp-font-display: var(--font-display, 'Outfit', system-ui, sans-serif);
4
+ --bp-font-data: var(--font-data, 'IBM Plex Mono', 'JetBrains Mono', ui-monospace, monospace);
5
+
6
+ --bp-glass-bg: var(--glass-bg, transparent);
7
+ --bp-glass-bg-strong: var(--glass-bg-strong, var(--bp-glass-bg, transparent));
8
+ --bp-glass-border: var(--glass-border, transparent);
9
+ --bp-glass-border-hover: var(--glass-border-hover, var(--bp-glass-border, transparent));
10
+ --bp-glass-blur: var(--glass-blur, 0px);
11
+ --bp-glass-blur-strong: 24px;
12
+
13
+ --bp-elements-borderColor: var(--cloud-elements-borderColor, rgba(240, 240, 245, 0.06));
14
+ --bp-elements-borderColorActive: var(--cloud-elements-borderColorActive, #8B5CF6);
15
+
16
+ --bp-elements-bg-depth-1: var(--cloud-elements-bg-depth-1, #0A0A0F);
17
+ --bp-elements-bg-depth-2: var(--cloud-elements-bg-depth-2, #12121A);
18
+ --bp-elements-bg-depth-3: var(--cloud-elements-bg-depth-3, #1A1A25);
19
+ --bp-elements-bg-depth-4: var(--cloud-elements-bg-depth-4, #22222E);
20
+
21
+ --bp-elements-textPrimary: var(--cloud-elements-textPrimary, #F0F0F5);
22
+ --bp-elements-textSecondary: var(--cloud-elements-textSecondary, #8A8A9E);
23
+ --bp-elements-textTertiary: var(--cloud-elements-textTertiary, #5A5A6E);
24
+
25
+ --bp-elements-button-primary-background: var(--cloud-elements-button-primary-background, rgba(142, 89, 255, 0.14));
26
+ --bp-elements-button-primary-backgroundHover: var(--cloud-elements-button-primary-backgroundHover, rgba(142, 89, 255, 0.24));
27
+ --bp-elements-button-primary-text: var(--cloud-elements-button-primary-text, #A87BFF);
28
+
29
+ --bp-elements-button-secondary-background: var(--cloud-elements-button-secondary-background, rgba(240, 240, 245, 0.06));
30
+ --bp-elements-button-secondary-backgroundHover: var(--cloud-elements-button-secondary-backgroundHover, rgba(240, 240, 245, 0.10));
31
+ --bp-elements-button-secondary-text: var(--cloud-elements-button-secondary-text, #F0F0F5);
32
+
33
+ --bp-elements-button-danger-background: var(--cloud-elements-button-danger-background, rgba(255, 59, 92, 0.12));
34
+ --bp-elements-button-danger-backgroundHover: var(--cloud-elements-button-danger-backgroundHover, rgba(255, 59, 92, 0.20));
35
+ --bp-elements-button-danger-text: var(--cloud-elements-button-danger-text, #FF3B5C);
36
+
37
+ --bp-elements-icon-success: var(--cloud-elements-icon-success, #38B2AC);
38
+ --bp-elements-icon-error: var(--cloud-elements-icon-error, #FF4D6A);
39
+ --bp-elements-icon-warning: var(--cloud-elements-icon-warning, #FFB800);
40
+ --bp-elements-icon-primary: var(--cloud-elements-icon-primary, #F0F0F5);
41
+ --bp-elements-icon-secondary: var(--cloud-elements-icon-secondary, #5A5A6E);
42
+
43
+ --bp-elements-dividerColor: var(--cloud-elements-dividerColor, rgba(240, 240, 245, 0.06));
44
+ --bp-elements-item-backgroundHover: var(--cloud-elements-item-backgroundHover, rgba(240, 240, 245, 0.03));
45
+ --bp-elements-item-backgroundActive: var(--cloud-elements-item-backgroundActive, rgba(240, 240, 245, 0.06));
46
+ --bp-elements-focus: var(--cloud-elements-focus, #8B5CF6);
47
+ }
48
+
49
+ .bp-tone-arena {
50
+ --bp-elements-borderColor: var(--arena-elements-borderColor, var(--bp-elements-borderColor));
51
+ --bp-elements-borderColorActive: var(--arena-elements-borderColorActive, var(--bp-elements-borderColorActive));
52
+
53
+ --bp-elements-bg-depth-1: var(--arena-elements-bg-depth-1, var(--bp-elements-bg-depth-1));
54
+ --bp-elements-bg-depth-2: var(--arena-elements-bg-depth-2, var(--bp-elements-bg-depth-2));
55
+ --bp-elements-bg-depth-3: var(--arena-elements-bg-depth-3, var(--bp-elements-bg-depth-3));
56
+ --bp-elements-bg-depth-4: var(--arena-elements-bg-depth-4, var(--bp-elements-bg-depth-4));
57
+
58
+ --bp-elements-textPrimary: var(--arena-elements-textPrimary, var(--bp-elements-textPrimary));
59
+ --bp-elements-textSecondary: var(--arena-elements-textSecondary, var(--bp-elements-textSecondary));
60
+ --bp-elements-textTertiary: var(--arena-elements-textTertiary, var(--bp-elements-textTertiary));
61
+
62
+ --bp-elements-button-primary-background: var(--arena-elements-button-primary-background, var(--bp-elements-button-primary-background));
63
+ --bp-elements-button-primary-backgroundHover: var(--arena-elements-button-primary-backgroundHover, var(--bp-elements-button-primary-backgroundHover));
64
+ --bp-elements-button-primary-text: var(--arena-elements-button-primary-text, var(--bp-elements-button-primary-text));
65
+
66
+ --bp-elements-button-secondary-background: var(--arena-elements-button-secondary-background, var(--bp-elements-button-secondary-background));
67
+ --bp-elements-button-secondary-backgroundHover: var(--arena-elements-button-secondary-backgroundHover, var(--bp-elements-button-secondary-backgroundHover));
68
+ --bp-elements-button-secondary-text: var(--arena-elements-button-secondary-text, var(--bp-elements-button-secondary-text));
69
+
70
+ --bp-elements-button-danger-background: var(--arena-elements-button-danger-background, var(--bp-elements-button-danger-background));
71
+ --bp-elements-button-danger-backgroundHover: var(--arena-elements-button-danger-backgroundHover, var(--bp-elements-button-danger-backgroundHover));
72
+ --bp-elements-button-danger-text: var(--arena-elements-button-danger-text, var(--bp-elements-button-danger-text));
73
+
74
+ --bp-elements-icon-success: var(--arena-elements-icon-success, var(--bp-elements-icon-success));
75
+ --bp-elements-icon-error: var(--arena-elements-icon-error, var(--bp-elements-icon-error));
76
+ --bp-elements-icon-warning: var(--arena-elements-icon-warning, var(--bp-elements-icon-warning));
77
+ --bp-elements-icon-primary: var(--arena-elements-icon-primary, var(--bp-elements-icon-primary));
78
+ --bp-elements-icon-secondary: var(--arena-elements-icon-secondary, var(--bp-elements-icon-secondary));
79
+
80
+ --bp-elements-dividerColor: var(--arena-elements-dividerColor, var(--bp-elements-dividerColor));
81
+ --bp-elements-item-backgroundHover: var(--arena-elements-item-backgroundHover, var(--bp-elements-item-backgroundHover));
82
+ --bp-elements-item-backgroundActive: var(--arena-elements-item-backgroundActive, var(--bp-elements-item-backgroundActive));
83
+ --bp-elements-focus: var(--arena-elements-focus, var(--bp-elements-focus));
84
+ }
85
+
86
+ .font-display {
87
+ font-family: var(--bp-font-display);
88
+ }
89
+
90
+ .font-data {
91
+ font-family: var(--bp-font-data);
92
+ font-feature-settings: 'tnum' 1;
93
+ }
94
+
95
+ .glass {
96
+ background: var(--bp-glass-bg);
97
+ backdrop-filter: blur(var(--bp-glass-blur));
98
+ -webkit-backdrop-filter: blur(var(--bp-glass-blur));
99
+ border: 1px solid var(--bp-glass-border);
100
+ }
101
+
102
+ .glass-hover {
103
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
104
+ }
105
+
106
+ .glass-hover:hover {
107
+ background: var(--bp-glass-bg-strong);
108
+ border-color: var(--bp-glass-border-hover);
109
+ }
110
+
111
+ .glass-card {
112
+ background: var(--bp-glass-bg);
113
+ backdrop-filter: blur(var(--bp-glass-blur));
114
+ -webkit-backdrop-filter: blur(var(--bp-glass-blur));
115
+ border: 1px solid var(--bp-glass-border);
116
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
117
+ }
118
+
119
+ .glass-card:hover {
120
+ border-color: var(--bp-glass-border-hover);
121
+ }
122
+
123
+ .glass-card-strong {
124
+ background: var(--bp-glass-bg-strong);
125
+ backdrop-filter: blur(var(--bp-glass-blur-strong));
126
+ -webkit-backdrop-filter: blur(var(--bp-glass-blur-strong));
127
+ border: 1px solid var(--bp-glass-border);
128
+ }
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom/vitest';
@@ -0,0 +1,22 @@
1
+ type ImportMetaEnvLike = {
2
+ DEV?: boolean;
3
+ VITE_RPC_URL?: string;
4
+ VITE_CHAIN_ID?: string;
5
+ VITE_BLUEPRINT_ID?: string;
6
+ VITE_SERVICE_ID?: string;
7
+ VITE_SERVICE_IDS?: string;
8
+ VITE_OPERATOR_API_URL?: string;
9
+ };
10
+
11
+ function readImportMetaEnv(): ImportMetaEnvLike {
12
+ return ((import.meta as ImportMeta & { env?: ImportMetaEnvLike }).env ?? {});
13
+ }
14
+
15
+ export function getEnvVar(key: keyof ImportMetaEnvLike): string | undefined {
16
+ const value = readImportMetaEnv()[key];
17
+ return typeof value === 'string' ? value : undefined;
18
+ }
19
+
20
+ export function isDevEnv(): boolean {
21
+ return Boolean(readImportMetaEnv().DEV);
22
+ }
@@ -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
+ }