@thru/wallet 0.2.22
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 +67 -0
- package/android/build.gradle +37 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/org/thru/walletnative/ThruWebViewBridgeModule.kt +77 -0
- package/app.plugin.cjs +101 -0
- package/dist/BrowserSDK-CpRFiJsW.d.ts +409 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +941 -0
- package/dist/index.js.map +1 -0
- package/dist/native/react.d.ts +109 -0
- package/dist/native/react.js +2381 -0
- package/dist/native/react.js.map +1 -0
- package/dist/native.d.ts +329 -0
- package/dist/native.js +1126 -0
- package/dist/native.js.map +1 -0
- package/dist/react-ui.d.ts +5 -0
- package/dist/react-ui.js +266 -0
- package/dist/react-ui.js.map +1 -0
- package/dist/react.d.ts +66 -0
- package/dist/react.js +1151 -0
- package/dist/react.js.map +1 -0
- package/expo-module.config.json +6 -0
- package/package.json +114 -0
- package/src/BrowserSDK.ts +315 -0
- package/src/index.ts +27 -0
- package/src/interfaces/IThruChain.ts +37 -0
- package/src/interfaces/accounts.ts +61 -0
- package/src/interfaces/index.ts +9 -0
- package/src/interfaces/types.ts +95 -0
- package/src/native/NativeSDK.test.ts +819 -0
- package/src/native/NativeSDK.ts +773 -0
- package/src/native/index.ts +39 -0
- package/src/native/provider/NativeProvider.ts +363 -0
- package/src/native/provider/WebViewBridge.test.ts +339 -0
- package/src/native/provider/WebViewBridge.ts +339 -0
- package/src/native/provider/chains/ThruChain.ts +85 -0
- package/src/native/provider/shell.html +88 -0
- package/src/native/provider/shell.test.ts +56 -0
- package/src/native/provider/shell.ts +111 -0
- package/src/native/provider/shims-html.d.ts +4 -0
- package/src/native/react/ThruContext.ts +37 -0
- package/src/native/react/ThruProvider.tsx +168 -0
- package/src/native/react/ThruWalletSheet.tsx +1162 -0
- package/src/native/react/android-webauthn.ts +37 -0
- package/src/native/react/hooks/useAccounts.ts +35 -0
- package/src/native/react/hooks/useThru.ts +11 -0
- package/src/native/react/hooks/useWallet.ts +71 -0
- package/src/native/react/hooks/useWalletAvailability.ts +31 -0
- package/src/native/react/hooks/waitForWallet.ts +21 -0
- package/src/native/react/index.ts +29 -0
- package/src/protocol/index.ts +2 -0
- package/src/protocol/postMessage.ts +283 -0
- package/src/protocol/walletState.ts +12 -0
- package/src/provider/EmbeddedProvider.ts +330 -0
- package/src/provider/IframeManager.ts +438 -0
- package/src/provider/chains/ThruChain.ts +86 -0
- package/src/provider/index.ts +17 -0
- package/src/provider/types/messages.ts +37 -0
- package/src/react/ThruContext.ts +31 -0
- package/src/react/ThruProvider.tsx +169 -0
- package/src/react/hooks/useAccounts.ts +38 -0
- package/src/react/hooks/useThru.ts +11 -0
- package/src/react/hooks/useWallet.ts +81 -0
- package/src/react/index.ts +30 -0
- package/src/react-ui/ThruAccountSwitcher.tsx +187 -0
- package/src/react-ui/custom.d.ts +8 -0
- package/src/react-ui/index.ts +1 -0
- package/src/static/logo.png +0 -0
- package/src/static/logomark_red.svg +11 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InferSuccessfulPostMessageResponse,
|
|
3
|
+
PostMessageEvent,
|
|
4
|
+
PostMessageRequest,
|
|
5
|
+
PostMessageResponse,
|
|
6
|
+
} from './types/messages';
|
|
7
|
+
import {
|
|
8
|
+
IFRAME_READY_EVENT,
|
|
9
|
+
POST_MESSAGE_EVENT_TYPE,
|
|
10
|
+
POST_MESSAGE_REQUEST_TYPES,
|
|
11
|
+
createRequestId,
|
|
12
|
+
} from './types/messages';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Allowed production origins for wallet iframe URLs.
|
|
16
|
+
* Development builds additionally allow localhost, LAN, and Tailscale
|
|
17
|
+
* origins so local HTTPS RP-ID testing can use the hosted wallet path.
|
|
18
|
+
*/
|
|
19
|
+
const PRODUCTION_IFRAME_ORIGINS = ['https://wallet.thru.org'];
|
|
20
|
+
|
|
21
|
+
const SLOW_REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
|
|
22
|
+
const FAST_REQUEST_TIMEOUT_MS = 30 * 1000;
|
|
23
|
+
|
|
24
|
+
const SLOW_REQUEST_TYPES: ReadonlySet<string> = new Set([
|
|
25
|
+
POST_MESSAGE_REQUEST_TYPES.CONNECT,
|
|
26
|
+
POST_MESSAGE_REQUEST_TYPES.SIGN_MESSAGE,
|
|
27
|
+
POST_MESSAGE_REQUEST_TYPES.SIGN_TRANSACTION,
|
|
28
|
+
POST_MESSAGE_REQUEST_TYPES.MANAGE_ACCOUNTS,
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function isPrivateIpv4Host(hostname: string): boolean {
|
|
32
|
+
const parts = hostname.split('.').map((part) => Number(part));
|
|
33
|
+
if (
|
|
34
|
+
parts.length !== 4 ||
|
|
35
|
+
parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)
|
|
36
|
+
) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const [a, b] = parts;
|
|
41
|
+
return (
|
|
42
|
+
a === 10 ||
|
|
43
|
+
a === 127 ||
|
|
44
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
45
|
+
(a === 192 && b === 168) ||
|
|
46
|
+
(a === 100 && b >= 64 && b <= 127)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isDevelopmentHostname(hostname: string): boolean {
|
|
51
|
+
return (
|
|
52
|
+
hostname === 'localhost' ||
|
|
53
|
+
hostname === '::1' ||
|
|
54
|
+
!hostname.includes('.') ||
|
|
55
|
+
hostname.endsWith('.local') ||
|
|
56
|
+
hostname.endsWith('.ts.net') ||
|
|
57
|
+
isPrivateIpv4Host(hostname)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isAllowedDevelopmentOrigin(url: URL): boolean {
|
|
62
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
|
|
63
|
+
if (typeof window === 'undefined') return false;
|
|
64
|
+
|
|
65
|
+
const appHostname = window.location.hostname.toLowerCase();
|
|
66
|
+
if (!isDevelopmentHostname(appHostname)) return false;
|
|
67
|
+
|
|
68
|
+
return isDevelopmentHostname(url.hostname.toLowerCase());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validates that the iframe URL is from a trusted origin
|
|
73
|
+
* @throws Error if the origin is not allowed
|
|
74
|
+
*/
|
|
75
|
+
function validateIframeOrigin(iframeUrl: string): void {
|
|
76
|
+
let url: URL;
|
|
77
|
+
try {
|
|
78
|
+
url = new URL(iframeUrl);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Invalid iframe URL: ${iframeUrl}. URL must be a valid absolute URL.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const origin = url.origin;
|
|
86
|
+
const isAllowed =
|
|
87
|
+
PRODUCTION_IFRAME_ORIGINS.includes(origin) || isAllowedDevelopmentOrigin(url);
|
|
88
|
+
|
|
89
|
+
if (!isAllowed) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Untrusted iframe origin: ${origin}. ` +
|
|
92
|
+
`Only trusted wallet origins are allowed: ${PRODUCTION_IFRAME_ORIGINS.join(', ')}. ` +
|
|
93
|
+
`Development builds also allow localhost, LAN, and Tailscale wallet origins. ` +
|
|
94
|
+
`This security check prevents malicious websites from loading unauthorized wallet iframes.`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Manages iframe lifecycle and postMessage communication
|
|
101
|
+
* Handles creating, showing/hiding iframe, and message passing
|
|
102
|
+
*/
|
|
103
|
+
export class IframeManager {
|
|
104
|
+
private iframe: HTMLIFrameElement | null = null;
|
|
105
|
+
private iframeUrl: string;
|
|
106
|
+
private iframeOrigin: string;
|
|
107
|
+
private frameId: string;
|
|
108
|
+
private messageHandlers = new Map<string, (response: PostMessageResponse) => void>();
|
|
109
|
+
private messageListener: ((event: MessageEvent) => void) | null = null;
|
|
110
|
+
private readyPromise: Promise<void> | null = null;
|
|
111
|
+
private displayMode: 'modal' | 'inline' = 'modal';
|
|
112
|
+
private inlineContainer: HTMLElement | null = null;
|
|
113
|
+
private visible = false;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Callback for event broadcasts from iframe (no request id)
|
|
117
|
+
*/
|
|
118
|
+
public onEvent?: (eventType: string, payload: any) => void;
|
|
119
|
+
|
|
120
|
+
constructor(iframeUrl: string) {
|
|
121
|
+
// Validate origin before accepting the URL
|
|
122
|
+
validateIframeOrigin(iframeUrl);
|
|
123
|
+
|
|
124
|
+
this.iframeUrl = iframeUrl;
|
|
125
|
+
this.iframeOrigin = new URL(iframeUrl).origin;
|
|
126
|
+
/* Used to correlate postMessage traffic with the correct iframe instance.
|
|
127
|
+
Important in dev (React Strict Mode) where iframes can be created twice. */
|
|
128
|
+
this.frameId = createRequestId('frame');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private getIframeSrc(): string {
|
|
132
|
+
const url = new URL(this.iframeUrl);
|
|
133
|
+
url.searchParams.set('tn_frame_id', this.frameId);
|
|
134
|
+
return url.toString();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Create and inject iframe into DOM
|
|
139
|
+
* Returns a promise that resolves when iframe is ready
|
|
140
|
+
*/
|
|
141
|
+
async createIframe(): Promise<void> {
|
|
142
|
+
if (this.readyPromise) {
|
|
143
|
+
return this.readyPromise;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.readyPromise = (async () => {
|
|
147
|
+
if (!this.iframe) {
|
|
148
|
+
this.iframe = document.createElement('iframe');
|
|
149
|
+
this.iframe.src = this.getIframeSrc();
|
|
150
|
+
/* Allow WebAuthn in cross-origin iframe for passkey auth. */
|
|
151
|
+
this.iframe.allow = 'publickey-credentials-get; publickey-credentials-create';
|
|
152
|
+
this.applyIframeStyles();
|
|
153
|
+
/* Keep hidden (but still load) until the wallet asks to show UI. */
|
|
154
|
+
this.setVisibility(false);
|
|
155
|
+
|
|
156
|
+
if (this.displayMode === 'inline' && this.inlineContainer) {
|
|
157
|
+
this.inlineContainer.appendChild(this.iframe);
|
|
158
|
+
} else {
|
|
159
|
+
document.body.appendChild(this.iframe);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Set up message listener
|
|
163
|
+
this.messageListener = this.handleMessage.bind(this);
|
|
164
|
+
window.addEventListener('message', this.messageListener);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await this.waitForReady();
|
|
168
|
+
})().catch((error) => {
|
|
169
|
+
this.readyPromise = null;
|
|
170
|
+
throw error;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return this.readyPromise;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Wait for iframe to send 'ready' signal
|
|
178
|
+
*/
|
|
179
|
+
private waitForReady(): Promise<void> {
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
let resolved = false;
|
|
182
|
+
let readyHandler: (event: MessageEvent) => void;
|
|
183
|
+
const cleanup = () => {
|
|
184
|
+
if (resolved) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
resolved = true;
|
|
188
|
+
window.removeEventListener('message', readyHandler);
|
|
189
|
+
clearTimeout(timeout);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const timeout = setTimeout(() => {
|
|
193
|
+
cleanup();
|
|
194
|
+
reject(new Error('Iframe ready timeout - wallet failed to load'));
|
|
195
|
+
}, 10000);
|
|
196
|
+
|
|
197
|
+
readyHandler = (event: MessageEvent) => {
|
|
198
|
+
if (!this.isMessageFromIframe(event)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (event.data?.type === IFRAME_READY_EVENT) {
|
|
203
|
+
cleanup();
|
|
204
|
+
resolve();
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
window.addEventListener('message', readyHandler);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Mount iframe inline inside the provided container.
|
|
214
|
+
*/
|
|
215
|
+
async mountInline(container: HTMLElement): Promise<void> {
|
|
216
|
+
this.inlineContainer = container;
|
|
217
|
+
this.displayMode = 'inline';
|
|
218
|
+
await this.createIframe();
|
|
219
|
+
this.showInline();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Show iframe inline (embedded in container).
|
|
224
|
+
*/
|
|
225
|
+
showInline(): void {
|
|
226
|
+
if (!this.iframe) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
this.displayMode = 'inline';
|
|
230
|
+
if (this.inlineContainer && this.iframe.parentElement !== this.inlineContainer) {
|
|
231
|
+
this.inlineContainer.appendChild(this.iframe);
|
|
232
|
+
}
|
|
233
|
+
this.applyIframeStyles();
|
|
234
|
+
this.setVisibility(true);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Show iframe as a full-screen modal.
|
|
239
|
+
*/
|
|
240
|
+
showModal(): void {
|
|
241
|
+
if (!this.iframe) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
this.displayMode = 'modal';
|
|
245
|
+
if (this.iframe.parentElement !== document.body) {
|
|
246
|
+
document.body.appendChild(this.iframe);
|
|
247
|
+
}
|
|
248
|
+
this.applyIframeStyles();
|
|
249
|
+
this.setVisibility(true);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Show iframe modal
|
|
254
|
+
*/
|
|
255
|
+
show(): void {
|
|
256
|
+
this.showModal();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Hide iframe modal
|
|
261
|
+
*/
|
|
262
|
+
hide(): void {
|
|
263
|
+
this.setVisibility(false);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
isInline(): boolean {
|
|
267
|
+
return this.displayMode === 'inline';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private applyIframeStyles(): void {
|
|
271
|
+
if (!this.iframe) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (this.displayMode === 'inline') {
|
|
276
|
+
this.iframe.style.cssText = `
|
|
277
|
+
position: relative;
|
|
278
|
+
width: 100%;
|
|
279
|
+
height: 100%;
|
|
280
|
+
border: none;
|
|
281
|
+
z-index: 1;
|
|
282
|
+
display: block;
|
|
283
|
+
background: transparent;
|
|
284
|
+
`;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.iframe.style.cssText = `
|
|
289
|
+
position: fixed;
|
|
290
|
+
top: 0;
|
|
291
|
+
left: 0;
|
|
292
|
+
width: 100%;
|
|
293
|
+
height: 100%;
|
|
294
|
+
border: none;
|
|
295
|
+
z-index: 999999;
|
|
296
|
+
display: block;
|
|
297
|
+
background: rgba(0, 0, 0, 0.5);
|
|
298
|
+
`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private setVisibility(visible: boolean): void {
|
|
302
|
+
if (!this.iframe) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
this.visible = visible;
|
|
306
|
+
this.iframe.style.opacity = visible ? '1' : '0';
|
|
307
|
+
this.iframe.style.pointerEvents = visible ? 'auto' : 'none';
|
|
308
|
+
this.iframe.style.visibility = visible ? 'visible' : 'hidden';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Send message to iframe and wait for response
|
|
313
|
+
*/
|
|
314
|
+
async sendMessage<TRequest extends PostMessageRequest>(
|
|
315
|
+
request: TRequest
|
|
316
|
+
): Promise<InferSuccessfulPostMessageResponse<TRequest>> {
|
|
317
|
+
/* Ensure the iframe has navigated to the wallet origin before we try to
|
|
318
|
+
postMessage to a strict targetOrigin. Otherwise the iframe can still be
|
|
319
|
+
about:blank (same-origin with the dapp) and postMessage will throw. */
|
|
320
|
+
if (this.readyPromise) {
|
|
321
|
+
await this.readyPromise;
|
|
322
|
+
} else {
|
|
323
|
+
await this.createIframe();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!this.iframe?.contentWindow) {
|
|
327
|
+
throw new Error('Iframe not initialized - call createIframe() first');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return new Promise<InferSuccessfulPostMessageResponse<TRequest>>((resolve, reject) => {
|
|
331
|
+
/* CONNECT, signing, and account-management requests require a human click and can take minutes.
|
|
332
|
+
Keep a longer timeout to avoid breaking "inline connect button" flows. */
|
|
333
|
+
const timeoutMs = SLOW_REQUEST_TYPES.has(request.type)
|
|
334
|
+
? SLOW_REQUEST_TIMEOUT_MS
|
|
335
|
+
: FAST_REQUEST_TIMEOUT_MS;
|
|
336
|
+
|
|
337
|
+
const timeout = setTimeout(() => {
|
|
338
|
+
this.messageHandlers.delete(request.id);
|
|
339
|
+
reject(new Error('Request timeout - wallet did not respond'));
|
|
340
|
+
}, timeoutMs);
|
|
341
|
+
|
|
342
|
+
// Store handler for this request
|
|
343
|
+
this.messageHandlers.set(request.id, (response: PostMessageResponse) => {
|
|
344
|
+
clearTimeout(timeout);
|
|
345
|
+
this.messageHandlers.delete(request.id);
|
|
346
|
+
|
|
347
|
+
if (response.success) {
|
|
348
|
+
resolve(response as InferSuccessfulPostMessageResponse<TRequest>);
|
|
349
|
+
} else {
|
|
350
|
+
const error = new Error(response.error?.message || 'Unknown error');
|
|
351
|
+
(error as any).code = response.error?.code;
|
|
352
|
+
(error as any).data = response.error?.data;
|
|
353
|
+
reject(error);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Send message to iframe
|
|
358
|
+
this.iframe!.contentWindow!.postMessage(request, this.iframeOrigin);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Handle incoming messages from iframe
|
|
364
|
+
*/
|
|
365
|
+
private handleMessage(event: MessageEvent): void {
|
|
366
|
+
if (!this.isMessageFromIframe(event)) {
|
|
367
|
+
return; // Ignore messages from other origins
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const data = event.data;
|
|
371
|
+
|
|
372
|
+
// Handle response to a specific request (has id)
|
|
373
|
+
if (data.id && this.messageHandlers.has(data.id)) {
|
|
374
|
+
const handler = this.messageHandlers.get(data.id);
|
|
375
|
+
if (handler) {
|
|
376
|
+
handler(data as PostMessageResponse);
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Handle event broadcasts (type === 'event')
|
|
382
|
+
if (data.type === POST_MESSAGE_EVENT_TYPE) {
|
|
383
|
+
this.handleEvent(data as PostMessageEvent);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Handle event broadcasts from iframe
|
|
389
|
+
*/
|
|
390
|
+
private handleEvent(data: PostMessageEvent): void {
|
|
391
|
+
// Forward to EmbeddedProvider via callback
|
|
392
|
+
if (this.onEvent) {
|
|
393
|
+
this.onEvent(data.event, data.data);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private isMessageFromIframe(event: MessageEvent): boolean {
|
|
398
|
+
if (event.origin !== this.iframeOrigin) {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const data = event.data as any;
|
|
403
|
+
if (!data || data.frameId !== this.frameId) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* Some browsers (notably Safari) can provide a null `event.source` for
|
|
408
|
+
cross-origin postMessage events. Frame id + origin is sufficient. */
|
|
409
|
+
if (!event.source) {
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (this.iframe?.contentWindow && event.source !== this.iframe.contentWindow) {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Destroy iframe and cleanup
|
|
422
|
+
*/
|
|
423
|
+
destroy(): void {
|
|
424
|
+
if (this.iframe) {
|
|
425
|
+
this.iframe.remove();
|
|
426
|
+
this.iframe = null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
this.readyPromise = null;
|
|
430
|
+
|
|
431
|
+
if (this.messageListener) {
|
|
432
|
+
window.removeEventListener('message', this.messageListener);
|
|
433
|
+
this.messageListener = null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
this.messageHandlers.clear();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AddressType,
|
|
3
|
+
type IThruChain,
|
|
4
|
+
type ThruSigningContext,
|
|
5
|
+
type ThruTransactionIntent,
|
|
6
|
+
} from "../../interfaces";
|
|
7
|
+
import { POST_MESSAGE_REQUEST_TYPES, createRequestId } from "../../protocol";
|
|
8
|
+
import type { EmbeddedProvider } from "../EmbeddedProvider";
|
|
9
|
+
import type { IframeManager } from "../IframeManager";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* EmbeddedThruChain - postMessage-backed Thru chain adapter.
|
|
13
|
+
*/
|
|
14
|
+
export class EmbeddedThruChain implements IThruChain {
|
|
15
|
+
private readonly iframeManager: IframeManager;
|
|
16
|
+
private readonly provider: EmbeddedProvider;
|
|
17
|
+
|
|
18
|
+
constructor(iframeManager: IframeManager, provider: EmbeddedProvider) {
|
|
19
|
+
this.iframeManager = iframeManager;
|
|
20
|
+
this.provider = provider;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get connected(): boolean {
|
|
24
|
+
return this.provider.isConnected();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async connect(): Promise<{ publicKey: string }> {
|
|
28
|
+
const result = await this.provider.connect();
|
|
29
|
+
const selectedAccount = result.selectedAccount;
|
|
30
|
+
const thruAccount =
|
|
31
|
+
selectedAccount?.accountType === AddressType.THRU
|
|
32
|
+
? selectedAccount
|
|
33
|
+
: result.accounts.find((addr) => addr.accountType === AddressType.THRU);
|
|
34
|
+
|
|
35
|
+
if (!thruAccount) {
|
|
36
|
+
throw new Error("Thru address not found in connection result");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { publicKey: thruAccount.address };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async disconnect(): Promise<void> {
|
|
43
|
+
await this.provider.disconnect();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async getSigningContext(): Promise<ThruSigningContext> {
|
|
47
|
+
if (!this.provider.isConnected()) {
|
|
48
|
+
throw new Error("Wallet not connected");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const response = await this.iframeManager.sendMessage({
|
|
52
|
+
id: createRequestId(),
|
|
53
|
+
type: POST_MESSAGE_REQUEST_TYPES.GET_SIGNING_CONTEXT,
|
|
54
|
+
origin: window.location.origin,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return response.result.signingContext;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async signTransaction(transaction: ThruTransactionIntent): Promise<string> {
|
|
61
|
+
if (!this.provider.isConnected()) {
|
|
62
|
+
throw new Error("Wallet not connected");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.iframeManager.show();
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const response = await this.iframeManager.sendMessage({
|
|
69
|
+
id: createRequestId(),
|
|
70
|
+
type: POST_MESSAGE_REQUEST_TYPES.SIGN_TRANSACTION,
|
|
71
|
+
payload: {
|
|
72
|
+
walletAddress: transaction.walletAddress,
|
|
73
|
+
programAddress: transaction.programAddress,
|
|
74
|
+
instructionData: transaction.instructionData,
|
|
75
|
+
readWriteAddresses: transaction.readWriteAddresses,
|
|
76
|
+
readOnlyAddresses: transaction.readOnlyAddresses,
|
|
77
|
+
review: transaction.review,
|
|
78
|
+
},
|
|
79
|
+
origin: window.location.origin,
|
|
80
|
+
});
|
|
81
|
+
return response.result.signedTransaction;
|
|
82
|
+
} finally {
|
|
83
|
+
this.iframeManager.hide();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Main exports
|
|
2
|
+
export { EmbeddedThruChain } from './chains/ThruChain';
|
|
3
|
+
export { EmbeddedProvider, type ConnectOptions, type EmbeddedProviderConfig } from './EmbeddedProvider';
|
|
4
|
+
|
|
5
|
+
// Type exports
|
|
6
|
+
export type {
|
|
7
|
+
ConnectResult, EmbeddedProviderEvent, PostMessageEvent, PostMessageRequest,
|
|
8
|
+
PostMessageResponse, RequestType, SignMessagePayload,
|
|
9
|
+
SignMessageResult,
|
|
10
|
+
SignTransactionPayload,
|
|
11
|
+
SignTransactionResult
|
|
12
|
+
} from './types/messages';
|
|
13
|
+
|
|
14
|
+
export { ErrorCode } from './types/messages';
|
|
15
|
+
|
|
16
|
+
// Re-export types from chain-interfaces for convenience
|
|
17
|
+
export type { IThruChain, WalletAccount } from '../interfaces';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export {
|
|
2
|
+
POST_MESSAGE_REQUEST_TYPES,
|
|
3
|
+
EMBEDDED_PROVIDER_EVENTS,
|
|
4
|
+
POST_MESSAGE_EVENT_TYPE,
|
|
5
|
+
IFRAME_READY_EVENT,
|
|
6
|
+
DEFAULT_IFRAME_URL,
|
|
7
|
+
createRequestId,
|
|
8
|
+
ErrorCode,
|
|
9
|
+
type RequestType,
|
|
10
|
+
type EmbeddedProviderEvent,
|
|
11
|
+
type PostMessageRequest,
|
|
12
|
+
type ConnectRequestMessage,
|
|
13
|
+
type DisconnectRequestMessage,
|
|
14
|
+
type SignMessageRequestMessage,
|
|
15
|
+
type SignTransactionRequestMessage,
|
|
16
|
+
type GetAccountsRequestMessage,
|
|
17
|
+
type GetSigningContextRequestMessage,
|
|
18
|
+
type ManageAccountsRequestMessage,
|
|
19
|
+
type SelectAccountRequestMessage,
|
|
20
|
+
type DisconnectResult,
|
|
21
|
+
type GetAccountsResult,
|
|
22
|
+
type GetSigningContextResult,
|
|
23
|
+
type ManageAccountsResult,
|
|
24
|
+
type SelectAccountPayload,
|
|
25
|
+
type SelectAccountResult,
|
|
26
|
+
type PostMessageResponse,
|
|
27
|
+
type SuccessfulPostMessageResponse,
|
|
28
|
+
type InferPostMessageResponse,
|
|
29
|
+
type InferSuccessfulPostMessageResponse,
|
|
30
|
+
type PostMessageEvent,
|
|
31
|
+
type ConnectRequestPayload,
|
|
32
|
+
type ConnectResult,
|
|
33
|
+
type SignMessagePayload,
|
|
34
|
+
type SignMessageResult,
|
|
35
|
+
type SignTransactionPayload,
|
|
36
|
+
type SignTransactionResult,
|
|
37
|
+
} from "../../protocol";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { BrowserSDK } from "../BrowserSDK";
|
|
2
|
+
import type { WalletAccount } from "../interfaces";
|
|
3
|
+
import type { ManageAccountsResult } from "../protocol";
|
|
4
|
+
import { Thru } from "@thru/sdk/client";
|
|
5
|
+
import { createContext } from "react";
|
|
6
|
+
|
|
7
|
+
export interface ThruContextValue {
|
|
8
|
+
wallet: BrowserSDK | null;
|
|
9
|
+
isConnected: boolean;
|
|
10
|
+
accounts: WalletAccount[];
|
|
11
|
+
isConnecting: boolean;
|
|
12
|
+
error: Error | null;
|
|
13
|
+
thru: Thru | null;
|
|
14
|
+
selectedAccount: WalletAccount | null;
|
|
15
|
+
selectAccount: (account: WalletAccount) => Promise<void>;
|
|
16
|
+
manageAccounts: () => Promise<ManageAccountsResult>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const defaultContextValue: ThruContextValue = {
|
|
20
|
+
wallet: null,
|
|
21
|
+
isConnected: false,
|
|
22
|
+
accounts: [],
|
|
23
|
+
isConnecting: false,
|
|
24
|
+
error: null,
|
|
25
|
+
thru: null,
|
|
26
|
+
selectedAccount: null,
|
|
27
|
+
selectAccount: async () => undefined,
|
|
28
|
+
manageAccounts: async () => ({ accounts: [], selectedAccount: null }),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const ThruContext = createContext<ThruContextValue>(defaultContextValue);
|