@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,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';
|
package/src/stores/infra.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { persistedAtom } from './persistedAtom';
|
|
2
|
+
import { getEnvVar } from '../utils/env';
|
|
2
3
|
|
|
3
|
-
const defaultBlueprintId =
|
|
4
|
-
const defaultServiceId =
|
|
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';
|
package/src/utils/env.ts
ADDED
|
@@ -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
|
+
}
|