@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.
Files changed (69) hide show
  1. package/README.md +67 -0
  2. package/android/build.gradle +37 -0
  3. package/android/src/main/AndroidManifest.xml +1 -0
  4. package/android/src/main/java/org/thru/walletnative/ThruWebViewBridgeModule.kt +77 -0
  5. package/app.plugin.cjs +101 -0
  6. package/dist/BrowserSDK-CpRFiJsW.d.ts +409 -0
  7. package/dist/index.d.ts +23 -0
  8. package/dist/index.js +941 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/native/react.d.ts +109 -0
  11. package/dist/native/react.js +2381 -0
  12. package/dist/native/react.js.map +1 -0
  13. package/dist/native.d.ts +329 -0
  14. package/dist/native.js +1126 -0
  15. package/dist/native.js.map +1 -0
  16. package/dist/react-ui.d.ts +5 -0
  17. package/dist/react-ui.js +266 -0
  18. package/dist/react-ui.js.map +1 -0
  19. package/dist/react.d.ts +66 -0
  20. package/dist/react.js +1151 -0
  21. package/dist/react.js.map +1 -0
  22. package/expo-module.config.json +6 -0
  23. package/package.json +114 -0
  24. package/src/BrowserSDK.ts +315 -0
  25. package/src/index.ts +27 -0
  26. package/src/interfaces/IThruChain.ts +37 -0
  27. package/src/interfaces/accounts.ts +61 -0
  28. package/src/interfaces/index.ts +9 -0
  29. package/src/interfaces/types.ts +95 -0
  30. package/src/native/NativeSDK.test.ts +819 -0
  31. package/src/native/NativeSDK.ts +773 -0
  32. package/src/native/index.ts +39 -0
  33. package/src/native/provider/NativeProvider.ts +363 -0
  34. package/src/native/provider/WebViewBridge.test.ts +339 -0
  35. package/src/native/provider/WebViewBridge.ts +339 -0
  36. package/src/native/provider/chains/ThruChain.ts +85 -0
  37. package/src/native/provider/shell.html +88 -0
  38. package/src/native/provider/shell.test.ts +56 -0
  39. package/src/native/provider/shell.ts +111 -0
  40. package/src/native/provider/shims-html.d.ts +4 -0
  41. package/src/native/react/ThruContext.ts +37 -0
  42. package/src/native/react/ThruProvider.tsx +168 -0
  43. package/src/native/react/ThruWalletSheet.tsx +1162 -0
  44. package/src/native/react/android-webauthn.ts +37 -0
  45. package/src/native/react/hooks/useAccounts.ts +35 -0
  46. package/src/native/react/hooks/useThru.ts +11 -0
  47. package/src/native/react/hooks/useWallet.ts +71 -0
  48. package/src/native/react/hooks/useWalletAvailability.ts +31 -0
  49. package/src/native/react/hooks/waitForWallet.ts +21 -0
  50. package/src/native/react/index.ts +29 -0
  51. package/src/protocol/index.ts +2 -0
  52. package/src/protocol/postMessage.ts +283 -0
  53. package/src/protocol/walletState.ts +12 -0
  54. package/src/provider/EmbeddedProvider.ts +330 -0
  55. package/src/provider/IframeManager.ts +438 -0
  56. package/src/provider/chains/ThruChain.ts +86 -0
  57. package/src/provider/index.ts +17 -0
  58. package/src/provider/types/messages.ts +37 -0
  59. package/src/react/ThruContext.ts +31 -0
  60. package/src/react/ThruProvider.tsx +169 -0
  61. package/src/react/hooks/useAccounts.ts +38 -0
  62. package/src/react/hooks/useThru.ts +11 -0
  63. package/src/react/hooks/useWallet.ts +81 -0
  64. package/src/react/index.ts +30 -0
  65. package/src/react-ui/ThruAccountSwitcher.tsx +187 -0
  66. package/src/react-ui/custom.d.ts +8 -0
  67. package/src/react-ui/index.ts +1 -0
  68. package/src/static/logo.png +0 -0
  69. package/src/static/logomark_red.svg +11 -0
package/dist/react.js ADDED
@@ -0,0 +1,1151 @@
1
+ "use client";
2
+ import { createThruClient } from '@thru/sdk/client';
3
+ import { createContext, useState, useEffect, useCallback, useContext, useMemo, useRef } from 'react';
4
+ import { jsx } from 'react/jsx-runtime';
5
+
6
+ // src/interfaces/accounts.ts
7
+ function resolveSelectedWalletAccount(accounts, selectedAccount) {
8
+ if (selectedAccount) {
9
+ return accounts.find((account) => account.address === selectedAccount.address) ?? selectedAccount;
10
+ }
11
+ return accounts[0] ?? null;
12
+ }
13
+ function normalizeActiveWalletAccounts(accounts, selectedAccount) {
14
+ const activeAccount = resolveSelectedWalletAccount(accounts, selectedAccount);
15
+ return {
16
+ accounts: activeAccount ? [activeAccount] : [],
17
+ selectedAccount: activeAccount
18
+ };
19
+ }
20
+ function normalizeWalletAccountResult(result, selectedAccount) {
21
+ const active = normalizeActiveWalletAccounts(
22
+ result.accounts,
23
+ result.selectedAccount ?? null
24
+ );
25
+ return {
26
+ ...result,
27
+ accounts: active.accounts,
28
+ selectedAccount: active.selectedAccount
29
+ };
30
+ }
31
+
32
+ // src/interfaces/types.ts
33
+ var AddressType = {
34
+ THRU: "thru"
35
+ };
36
+
37
+ // src/protocol/postMessage.ts
38
+ var POST_MESSAGE_REQUEST_TYPES = {
39
+ CONNECT: "connect",
40
+ DISCONNECT: "disconnect",
41
+ SIGN_MESSAGE: "signMessage",
42
+ SIGN_TRANSACTION: "signTransaction",
43
+ GET_ACCOUNTS: "getAccounts",
44
+ GET_CONNECTION_STATE: "getConnectionState",
45
+ GET_SIGNING_CONTEXT: "getSigningContext",
46
+ SELECT_ACCOUNT: "selectAccount",
47
+ MANAGE_ACCOUNTS: "manageAccounts"
48
+ };
49
+ var EMBEDDED_PROVIDER_EVENTS = {
50
+ CONNECT_START: "connect_start",
51
+ CONNECT: "connect",
52
+ DISCONNECT: "disconnect",
53
+ CONNECT_ERROR: "connect_error",
54
+ ERROR: "error",
55
+ LOCK: "lock",
56
+ UI_SHOW: "ui_show",
57
+ ACCOUNT_CHANGED: "account_changed"
58
+ };
59
+ var POST_MESSAGE_EVENT_TYPE = "event";
60
+ var IFRAME_READY_EVENT = "iframe:ready";
61
+ var DEFAULT_IFRAME_URL = "http://localhost:3000/embedded";
62
+ var REQUEST_ID_PREFIX = "req";
63
+ var createRequestId = (prefix = REQUEST_ID_PREFIX) => {
64
+ const random = Math.random().toString(36).slice(2, 11);
65
+ return `${prefix}_${Date.now()}_${random}`;
66
+ };
67
+ var ErrorCode = {
68
+ USER_REJECTED: "USER_REJECTED",
69
+ WALLET_LOCKED: "WALLET_LOCKED",
70
+ INVALID_PASSWORD: "INVALID_PASSWORD",
71
+ ALREADY_CONNECTED: "ALREADY_CONNECTED",
72
+ ACCOUNT_NOT_FOUND: "ACCOUNT_NOT_FOUND",
73
+ ACCOUNT_CHANGED: "ACCOUNT_CHANGED",
74
+ INVALID_TRANSACTION: "INVALID_TRANSACTION",
75
+ TRANSACTION_FAILED: "TRANSACTION_FAILED",
76
+ INSUFFICIENT_FUNDS: "INSUFFICIENT_FUNDS",
77
+ NETWORK_ERROR: "NETWORK_ERROR",
78
+ TIMEOUT: "TIMEOUT",
79
+ UNKNOWN_ERROR: "UNKNOWN_ERROR"
80
+ };
81
+
82
+ // src/provider/IframeManager.ts
83
+ var PRODUCTION_IFRAME_ORIGINS = ["https://wallet.thru.org"];
84
+ var SLOW_REQUEST_TIMEOUT_MS = 5 * 60 * 1e3;
85
+ var FAST_REQUEST_TIMEOUT_MS = 30 * 1e3;
86
+ var SLOW_REQUEST_TYPES = /* @__PURE__ */ new Set([
87
+ POST_MESSAGE_REQUEST_TYPES.CONNECT,
88
+ POST_MESSAGE_REQUEST_TYPES.SIGN_MESSAGE,
89
+ POST_MESSAGE_REQUEST_TYPES.SIGN_TRANSACTION,
90
+ POST_MESSAGE_REQUEST_TYPES.MANAGE_ACCOUNTS
91
+ ]);
92
+ function isPrivateIpv4Host(hostname) {
93
+ const parts = hostname.split(".").map((part) => Number(part));
94
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
95
+ return false;
96
+ }
97
+ const [a, b] = parts;
98
+ return a === 10 || a === 127 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 100 && b >= 64 && b <= 127;
99
+ }
100
+ function isDevelopmentHostname(hostname) {
101
+ return hostname === "localhost" || hostname === "::1" || !hostname.includes(".") || hostname.endsWith(".local") || hostname.endsWith(".ts.net") || isPrivateIpv4Host(hostname);
102
+ }
103
+ function isAllowedDevelopmentOrigin(url) {
104
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
105
+ if (typeof window === "undefined") return false;
106
+ const appHostname = window.location.hostname.toLowerCase();
107
+ if (!isDevelopmentHostname(appHostname)) return false;
108
+ return isDevelopmentHostname(url.hostname.toLowerCase());
109
+ }
110
+ function validateIframeOrigin(iframeUrl) {
111
+ let url;
112
+ try {
113
+ url = new URL(iframeUrl);
114
+ } catch (error) {
115
+ throw new Error(
116
+ `Invalid iframe URL: ${iframeUrl}. URL must be a valid absolute URL.`
117
+ );
118
+ }
119
+ const origin = url.origin;
120
+ const isAllowed = PRODUCTION_IFRAME_ORIGINS.includes(origin) || isAllowedDevelopmentOrigin(url);
121
+ if (!isAllowed) {
122
+ throw new Error(
123
+ `Untrusted iframe origin: ${origin}. Only trusted wallet origins are allowed: ${PRODUCTION_IFRAME_ORIGINS.join(", ")}. Development builds also allow localhost, LAN, and Tailscale wallet origins. This security check prevents malicious websites from loading unauthorized wallet iframes.`
124
+ );
125
+ }
126
+ }
127
+ var IframeManager = class {
128
+ constructor(iframeUrl) {
129
+ this.iframe = null;
130
+ this.messageHandlers = /* @__PURE__ */ new Map();
131
+ this.messageListener = null;
132
+ this.readyPromise = null;
133
+ this.displayMode = "modal";
134
+ this.inlineContainer = null;
135
+ this.visible = false;
136
+ validateIframeOrigin(iframeUrl);
137
+ this.iframeUrl = iframeUrl;
138
+ this.iframeOrigin = new URL(iframeUrl).origin;
139
+ this.frameId = createRequestId("frame");
140
+ }
141
+ getIframeSrc() {
142
+ const url = new URL(this.iframeUrl);
143
+ url.searchParams.set("tn_frame_id", this.frameId);
144
+ return url.toString();
145
+ }
146
+ /**
147
+ * Create and inject iframe into DOM
148
+ * Returns a promise that resolves when iframe is ready
149
+ */
150
+ async createIframe() {
151
+ if (this.readyPromise) {
152
+ return this.readyPromise;
153
+ }
154
+ this.readyPromise = (async () => {
155
+ if (!this.iframe) {
156
+ this.iframe = document.createElement("iframe");
157
+ this.iframe.src = this.getIframeSrc();
158
+ this.iframe.allow = "publickey-credentials-get; publickey-credentials-create";
159
+ this.applyIframeStyles();
160
+ this.setVisibility(false);
161
+ if (this.displayMode === "inline" && this.inlineContainer) {
162
+ this.inlineContainer.appendChild(this.iframe);
163
+ } else {
164
+ document.body.appendChild(this.iframe);
165
+ }
166
+ this.messageListener = this.handleMessage.bind(this);
167
+ window.addEventListener("message", this.messageListener);
168
+ }
169
+ await this.waitForReady();
170
+ })().catch((error) => {
171
+ this.readyPromise = null;
172
+ throw error;
173
+ });
174
+ return this.readyPromise;
175
+ }
176
+ /**
177
+ * Wait for iframe to send 'ready' signal
178
+ */
179
+ waitForReady() {
180
+ return new Promise((resolve, reject) => {
181
+ let resolved = false;
182
+ let readyHandler;
183
+ const cleanup = () => {
184
+ if (resolved) {
185
+ return;
186
+ }
187
+ resolved = true;
188
+ window.removeEventListener("message", readyHandler);
189
+ clearTimeout(timeout);
190
+ };
191
+ const timeout = setTimeout(() => {
192
+ cleanup();
193
+ reject(new Error("Iframe ready timeout - wallet failed to load"));
194
+ }, 1e4);
195
+ readyHandler = (event) => {
196
+ if (!this.isMessageFromIframe(event)) {
197
+ return;
198
+ }
199
+ if (event.data?.type === IFRAME_READY_EVENT) {
200
+ cleanup();
201
+ resolve();
202
+ }
203
+ };
204
+ window.addEventListener("message", readyHandler);
205
+ });
206
+ }
207
+ /**
208
+ * Mount iframe inline inside the provided container.
209
+ */
210
+ async mountInline(container) {
211
+ this.inlineContainer = container;
212
+ this.displayMode = "inline";
213
+ await this.createIframe();
214
+ this.showInline();
215
+ }
216
+ /**
217
+ * Show iframe inline (embedded in container).
218
+ */
219
+ showInline() {
220
+ if (!this.iframe) {
221
+ return;
222
+ }
223
+ this.displayMode = "inline";
224
+ if (this.inlineContainer && this.iframe.parentElement !== this.inlineContainer) {
225
+ this.inlineContainer.appendChild(this.iframe);
226
+ }
227
+ this.applyIframeStyles();
228
+ this.setVisibility(true);
229
+ }
230
+ /**
231
+ * Show iframe as a full-screen modal.
232
+ */
233
+ showModal() {
234
+ if (!this.iframe) {
235
+ return;
236
+ }
237
+ this.displayMode = "modal";
238
+ if (this.iframe.parentElement !== document.body) {
239
+ document.body.appendChild(this.iframe);
240
+ }
241
+ this.applyIframeStyles();
242
+ this.setVisibility(true);
243
+ }
244
+ /**
245
+ * Show iframe modal
246
+ */
247
+ show() {
248
+ this.showModal();
249
+ }
250
+ /**
251
+ * Hide iframe modal
252
+ */
253
+ hide() {
254
+ this.setVisibility(false);
255
+ }
256
+ isInline() {
257
+ return this.displayMode === "inline";
258
+ }
259
+ applyIframeStyles() {
260
+ if (!this.iframe) {
261
+ return;
262
+ }
263
+ if (this.displayMode === "inline") {
264
+ this.iframe.style.cssText = `
265
+ position: relative;
266
+ width: 100%;
267
+ height: 100%;
268
+ border: none;
269
+ z-index: 1;
270
+ display: block;
271
+ background: transparent;
272
+ `;
273
+ return;
274
+ }
275
+ this.iframe.style.cssText = `
276
+ position: fixed;
277
+ top: 0;
278
+ left: 0;
279
+ width: 100%;
280
+ height: 100%;
281
+ border: none;
282
+ z-index: 999999;
283
+ display: block;
284
+ background: rgba(0, 0, 0, 0.5);
285
+ `;
286
+ }
287
+ setVisibility(visible) {
288
+ if (!this.iframe) {
289
+ return;
290
+ }
291
+ this.visible = visible;
292
+ this.iframe.style.opacity = visible ? "1" : "0";
293
+ this.iframe.style.pointerEvents = visible ? "auto" : "none";
294
+ this.iframe.style.visibility = visible ? "visible" : "hidden";
295
+ }
296
+ /**
297
+ * Send message to iframe and wait for response
298
+ */
299
+ async sendMessage(request) {
300
+ if (this.readyPromise) {
301
+ await this.readyPromise;
302
+ } else {
303
+ await this.createIframe();
304
+ }
305
+ if (!this.iframe?.contentWindow) {
306
+ throw new Error("Iframe not initialized - call createIframe() first");
307
+ }
308
+ return new Promise((resolve, reject) => {
309
+ const timeoutMs = SLOW_REQUEST_TYPES.has(request.type) ? SLOW_REQUEST_TIMEOUT_MS : FAST_REQUEST_TIMEOUT_MS;
310
+ const timeout = setTimeout(() => {
311
+ this.messageHandlers.delete(request.id);
312
+ reject(new Error("Request timeout - wallet did not respond"));
313
+ }, timeoutMs);
314
+ this.messageHandlers.set(request.id, (response) => {
315
+ clearTimeout(timeout);
316
+ this.messageHandlers.delete(request.id);
317
+ if (response.success) {
318
+ resolve(response);
319
+ } else {
320
+ const error = new Error(response.error?.message || "Unknown error");
321
+ error.code = response.error?.code;
322
+ error.data = response.error?.data;
323
+ reject(error);
324
+ }
325
+ });
326
+ this.iframe.contentWindow.postMessage(request, this.iframeOrigin);
327
+ });
328
+ }
329
+ /**
330
+ * Handle incoming messages from iframe
331
+ */
332
+ handleMessage(event) {
333
+ if (!this.isMessageFromIframe(event)) {
334
+ return;
335
+ }
336
+ const data = event.data;
337
+ if (data.id && this.messageHandlers.has(data.id)) {
338
+ const handler = this.messageHandlers.get(data.id);
339
+ if (handler) {
340
+ handler(data);
341
+ }
342
+ return;
343
+ }
344
+ if (data.type === POST_MESSAGE_EVENT_TYPE) {
345
+ this.handleEvent(data);
346
+ }
347
+ }
348
+ /**
349
+ * Handle event broadcasts from iframe
350
+ */
351
+ handleEvent(data) {
352
+ if (this.onEvent) {
353
+ this.onEvent(data.event, data.data);
354
+ }
355
+ }
356
+ isMessageFromIframe(event) {
357
+ if (event.origin !== this.iframeOrigin) {
358
+ return false;
359
+ }
360
+ const data = event.data;
361
+ if (!data || data.frameId !== this.frameId) {
362
+ return false;
363
+ }
364
+ if (!event.source) {
365
+ return true;
366
+ }
367
+ if (this.iframe?.contentWindow && event.source !== this.iframe.contentWindow) {
368
+ return false;
369
+ }
370
+ return true;
371
+ }
372
+ /**
373
+ * Destroy iframe and cleanup
374
+ */
375
+ destroy() {
376
+ if (this.iframe) {
377
+ this.iframe.remove();
378
+ this.iframe = null;
379
+ }
380
+ this.readyPromise = null;
381
+ if (this.messageListener) {
382
+ window.removeEventListener("message", this.messageListener);
383
+ this.messageListener = null;
384
+ }
385
+ this.messageHandlers.clear();
386
+ }
387
+ };
388
+
389
+ // src/provider/chains/ThruChain.ts
390
+ var EmbeddedThruChain = class {
391
+ constructor(iframeManager, provider) {
392
+ this.iframeManager = iframeManager;
393
+ this.provider = provider;
394
+ }
395
+ get connected() {
396
+ return this.provider.isConnected();
397
+ }
398
+ async connect() {
399
+ const result = await this.provider.connect();
400
+ const selectedAccount = result.selectedAccount;
401
+ const thruAccount = selectedAccount?.accountType === AddressType.THRU ? selectedAccount : result.accounts.find((addr) => addr.accountType === AddressType.THRU);
402
+ if (!thruAccount) {
403
+ throw new Error("Thru address not found in connection result");
404
+ }
405
+ return { publicKey: thruAccount.address };
406
+ }
407
+ async disconnect() {
408
+ await this.provider.disconnect();
409
+ }
410
+ async getSigningContext() {
411
+ if (!this.provider.isConnected()) {
412
+ throw new Error("Wallet not connected");
413
+ }
414
+ const response = await this.iframeManager.sendMessage({
415
+ id: createRequestId(),
416
+ type: POST_MESSAGE_REQUEST_TYPES.GET_SIGNING_CONTEXT,
417
+ origin: window.location.origin
418
+ });
419
+ return response.result.signingContext;
420
+ }
421
+ async signTransaction(transaction) {
422
+ if (!this.provider.isConnected()) {
423
+ throw new Error("Wallet not connected");
424
+ }
425
+ this.iframeManager.show();
426
+ try {
427
+ const response = await this.iframeManager.sendMessage({
428
+ id: createRequestId(),
429
+ type: POST_MESSAGE_REQUEST_TYPES.SIGN_TRANSACTION,
430
+ payload: {
431
+ walletAddress: transaction.walletAddress,
432
+ programAddress: transaction.programAddress,
433
+ instructionData: transaction.instructionData,
434
+ readWriteAddresses: transaction.readWriteAddresses,
435
+ readOnlyAddresses: transaction.readOnlyAddresses,
436
+ review: transaction.review
437
+ },
438
+ origin: window.location.origin
439
+ });
440
+ return response.result.signedTransaction;
441
+ } finally {
442
+ this.iframeManager.hide();
443
+ }
444
+ }
445
+ };
446
+
447
+ // src/provider/EmbeddedProvider.ts
448
+ var EmbeddedProvider = class {
449
+ constructor(config) {
450
+ this.connected = false;
451
+ this.accounts = [];
452
+ this.selectedAccount = null;
453
+ this.eventListeners = /* @__PURE__ */ new Map();
454
+ this.inlineMode = false;
455
+ const iframeUrl = config.iframeUrl || DEFAULT_IFRAME_URL;
456
+ this.iframeManager = new IframeManager(iframeUrl);
457
+ this.iframeManager.onEvent = (eventType, payload) => {
458
+ this.emit(eventType, payload);
459
+ if (eventType === EMBEDDED_PROVIDER_EVENTS.UI_SHOW) {
460
+ if (this.inlineMode) {
461
+ this.iframeManager.showInline();
462
+ } else {
463
+ this.iframeManager.showModal();
464
+ }
465
+ return;
466
+ }
467
+ if (eventType === EMBEDDED_PROVIDER_EVENTS.DISCONNECT || eventType === EMBEDDED_PROVIDER_EVENTS.LOCK) {
468
+ this.clearConnection();
469
+ return;
470
+ }
471
+ if (eventType === EMBEDDED_PROVIDER_EVENTS.ACCOUNT_CHANGED) {
472
+ const account = payload && payload.account || null;
473
+ this.refreshAccountCache(account ?? null);
474
+ }
475
+ };
476
+ const addressTypes = config.addressTypes || [AddressType.THRU];
477
+ if (addressTypes.includes(AddressType.THRU)) {
478
+ this._thruChain = new EmbeddedThruChain(this.iframeManager, this);
479
+ }
480
+ }
481
+ /**
482
+ * Initialize the provider (must be called before use)
483
+ * Creates iframe and waits for it to be ready
484
+ */
485
+ async initialize() {
486
+ await this.iframeManager.createIframe();
487
+ }
488
+ /**
489
+ * Mount the wallet iframe inline in a container (for inline connect button).
490
+ */
491
+ async mountInline(container) {
492
+ this.inlineMode = true;
493
+ await this.iframeManager.mountInline(container);
494
+ }
495
+ /**
496
+ * Connect to wallet
497
+ * Shows iframe modal and requests connection
498
+ */
499
+ async connect(options) {
500
+ this.emit(EMBEDDED_PROVIDER_EVENTS.CONNECT_START, {});
501
+ try {
502
+ if (this.inlineMode) {
503
+ this.iframeManager.showInline();
504
+ } else {
505
+ this.iframeManager.showModal();
506
+ }
507
+ const payload = {};
508
+ if (options?.metadata) {
509
+ payload.metadata = options.metadata;
510
+ }
511
+ const response = await this.iframeManager.sendMessage({
512
+ id: createRequestId(),
513
+ type: POST_MESSAGE_REQUEST_TYPES.CONNECT,
514
+ payload,
515
+ origin: window.location.origin
516
+ });
517
+ const result = normalizeWalletAccountResult(response.result);
518
+ this.connected = true;
519
+ this.accounts = result.accounts;
520
+ this.selectedAccount = result.selectedAccount;
521
+ this.emit(EMBEDDED_PROVIDER_EVENTS.CONNECT, result);
522
+ if (!this.inlineMode) {
523
+ this.iframeManager.hide();
524
+ }
525
+ return result;
526
+ } catch (error) {
527
+ if (!this.inlineMode) {
528
+ this.iframeManager.hide();
529
+ }
530
+ this.emit(EMBEDDED_PROVIDER_EVENTS.CONNECT_ERROR, { error });
531
+ throw error;
532
+ }
533
+ }
534
+ /**
535
+ * Disconnect from wallet
536
+ */
537
+ async disconnect() {
538
+ try {
539
+ await this.iframeManager.sendMessage({
540
+ id: createRequestId(),
541
+ type: POST_MESSAGE_REQUEST_TYPES.DISCONNECT,
542
+ origin: window.location.origin
543
+ });
544
+ this.clearConnection();
545
+ this.emit(EMBEDDED_PROVIDER_EVENTS.DISCONNECT, {});
546
+ } catch (error) {
547
+ this.clearConnection();
548
+ this.emit(EMBEDDED_PROVIDER_EVENTS.ERROR, { error });
549
+ throw error;
550
+ } finally {
551
+ if (!this.inlineMode) {
552
+ this.iframeManager.hide();
553
+ }
554
+ }
555
+ }
556
+ /**
557
+ * Check if connected
558
+ */
559
+ isConnected() {
560
+ return this.connected;
561
+ }
562
+ /**
563
+ * Get accounts
564
+ */
565
+ getAccounts() {
566
+ return this.accounts;
567
+ }
568
+ getSelectedAccount() {
569
+ return this.selectedAccount;
570
+ }
571
+ async selectAccount(publicKey) {
572
+ if (!this.connected) {
573
+ throw new Error("Wallet not connected");
574
+ }
575
+ const knownAccount = this.accounts.find((acc) => acc.address === publicKey) ?? null;
576
+ if (!knownAccount) {
577
+ console.warn(
578
+ "[EmbeddedProvider] Selecting account not present in local cache"
579
+ );
580
+ }
581
+ const payload = { publicKey };
582
+ const response = await this.iframeManager.sendMessage({
583
+ id: createRequestId(),
584
+ type: POST_MESSAGE_REQUEST_TYPES.SELECT_ACCOUNT,
585
+ payload,
586
+ origin: window.location.origin
587
+ });
588
+ const account = response.result.account;
589
+ this.refreshAccountCache(account);
590
+ return account;
591
+ }
592
+ async manageAccounts() {
593
+ if (!this.connected) {
594
+ throw new Error("Wallet not connected");
595
+ }
596
+ if (this.inlineMode) {
597
+ this.iframeManager.showInline();
598
+ } else {
599
+ this.iframeManager.showModal();
600
+ }
601
+ try {
602
+ const response = await this.iframeManager.sendMessage({
603
+ id: createRequestId(),
604
+ type: POST_MESSAGE_REQUEST_TYPES.MANAGE_ACCOUNTS,
605
+ origin: window.location.origin
606
+ });
607
+ const result = normalizeWalletAccountResult({
608
+ accounts: response.result.accounts,
609
+ selectedAccount: response.result.selectedAccount
610
+ });
611
+ this.accounts = result.accounts;
612
+ this.selectedAccount = result.selectedAccount;
613
+ if (this.selectedAccount) {
614
+ this.emit(EMBEDDED_PROVIDER_EVENTS.ACCOUNT_CHANGED, {
615
+ account: this.selectedAccount
616
+ });
617
+ }
618
+ return result;
619
+ } finally {
620
+ if (!this.inlineMode) {
621
+ this.iframeManager.hide();
622
+ }
623
+ }
624
+ }
625
+ /**
626
+ * Get Thru chain API
627
+ */
628
+ get thru() {
629
+ if (!this._thruChain) {
630
+ throw new Error("Thru chain not enabled in provider config");
631
+ }
632
+ return this._thruChain;
633
+ }
634
+ /**
635
+ * Event emitter: on
636
+ */
637
+ on(event, callback) {
638
+ if (!this.eventListeners.has(event)) {
639
+ this.eventListeners.set(event, /* @__PURE__ */ new Set());
640
+ }
641
+ this.eventListeners.get(event).add(callback);
642
+ }
643
+ /**
644
+ * Event emitter: off
645
+ */
646
+ off(event, callback) {
647
+ this.eventListeners.get(event)?.delete(callback);
648
+ }
649
+ /**
650
+ * Emit event to all listeners
651
+ */
652
+ emit(event, data) {
653
+ this.eventListeners.get(event)?.forEach((callback) => {
654
+ try {
655
+ callback(data);
656
+ } catch (error) {
657
+ console.error(`Error in event listener for ${event}:`, error);
658
+ }
659
+ });
660
+ }
661
+ /**
662
+ * Get iframe manager (for chain implementations)
663
+ * @internal
664
+ */
665
+ getIframeManager() {
666
+ return this.iframeManager;
667
+ }
668
+ /**
669
+ * Destroy provider and cleanup
670
+ */
671
+ destroy() {
672
+ this.iframeManager.destroy();
673
+ this.eventListeners.clear();
674
+ this.clearConnection();
675
+ }
676
+ refreshAccountCache(account) {
677
+ if (!account) {
678
+ this.accounts = [];
679
+ this.selectedAccount = null;
680
+ return;
681
+ }
682
+ this.accounts = [account];
683
+ this.selectedAccount = account;
684
+ }
685
+ clearConnection() {
686
+ this.connected = false;
687
+ this.accounts = [];
688
+ this.selectedAccount = null;
689
+ }
690
+ };
691
+ var BrowserSDK = class {
692
+ constructor(config = {}) {
693
+ this.eventListeners = /* @__PURE__ */ new Map();
694
+ this.initialized = false;
695
+ this.connectInFlight = null;
696
+ this.lastConnectResult = null;
697
+ this.provider = new EmbeddedProvider({
698
+ iframeUrl: config.iframeUrl,
699
+ addressTypes: config.addressTypes || [AddressType.THRU]
700
+ });
701
+ this.thruClient = createThruClient({
702
+ baseUrl: config.rpcUrl
703
+ });
704
+ this.setupEventForwarding();
705
+ }
706
+ /**
707
+ * Initialize the SDK (creates iframe)
708
+ * Must be called before using the SDK
709
+ */
710
+ async initialize() {
711
+ if (this.initialized) {
712
+ return;
713
+ }
714
+ await this.provider.initialize();
715
+ this.initialized = true;
716
+ }
717
+ /**
718
+ * Connect to wallet
719
+ * Shows wallet modal and requests connection
720
+ */
721
+ async connect(options) {
722
+ if (!this.initialized) {
723
+ await this.initialize();
724
+ }
725
+ if (this.connectInFlight) {
726
+ return this.connectInFlight;
727
+ }
728
+ if (this.lastConnectResult && this.provider.isConnected()) {
729
+ return this.lastConnectResult;
730
+ }
731
+ this.emit("connect", { status: "connecting" });
732
+ const inFlight = (async () => {
733
+ try {
734
+ const metadata = this.resolveMetadata(options?.metadata);
735
+ const providerOptions = metadata ? { metadata } : void 0;
736
+ const result = await this.provider.connect(providerOptions);
737
+ this.lastConnectResult = result;
738
+ this.emit("connect", result);
739
+ return result;
740
+ } catch (error) {
741
+ this.emit("error", error);
742
+ throw error;
743
+ } finally {
744
+ this.connectInFlight = null;
745
+ }
746
+ })();
747
+ this.connectInFlight = inFlight;
748
+ return inFlight;
749
+ }
750
+ /**
751
+ * Mount the wallet iframe inline in a container.
752
+ */
753
+ async mountInline(container) {
754
+ await this.provider.mountInline(container);
755
+ }
756
+ /**
757
+ * Disconnect from wallet
758
+ */
759
+ async disconnect() {
760
+ try {
761
+ await this.provider.disconnect();
762
+ this.emit("disconnect", {});
763
+ this.lastConnectResult = null;
764
+ } catch (error) {
765
+ this.emit("error", error);
766
+ throw error;
767
+ }
768
+ }
769
+ /**
770
+ * Check if connected
771
+ */
772
+ isConnected() {
773
+ return this.provider.isConnected();
774
+ }
775
+ /**
776
+ * Get all accounts
777
+ */
778
+ getAccounts() {
779
+ const accounts = this.provider.getAccounts();
780
+ this.refreshCachedAccounts(accounts);
781
+ return accounts;
782
+ }
783
+ getSelectedAccount() {
784
+ return this.provider.getSelectedAccount();
785
+ }
786
+ async selectAccount(publicKey) {
787
+ const account = await this.provider.selectAccount(publicKey);
788
+ this.refreshCachedAccounts(this.provider.getAccounts(), account);
789
+ return account;
790
+ }
791
+ async manageAccounts() {
792
+ const result = await this.provider.manageAccounts();
793
+ this.refreshCachedAccounts(result.accounts, result.selectedAccount);
794
+ this.emit("accountChanged", result.selectedAccount);
795
+ return result;
796
+ }
797
+ /**
798
+ * Get Thru chain API (iframe-backed signer)
799
+ */
800
+ get thru() {
801
+ return this.provider.thru;
802
+ }
803
+ /**
804
+ * Event emitter: on
805
+ */
806
+ on(event, callback) {
807
+ if (!this.eventListeners.has(event)) {
808
+ this.eventListeners.set(event, /* @__PURE__ */ new Set());
809
+ }
810
+ this.eventListeners.get(event).add(callback);
811
+ }
812
+ /**
813
+ * Event emitter: off
814
+ */
815
+ off(event, callback) {
816
+ this.eventListeners.get(event)?.delete(callback);
817
+ }
818
+ /**
819
+ * Event emitter: once (listen once and auto-remove)
820
+ */
821
+ once(event, callback) {
822
+ const wrappedCallback = (...args) => {
823
+ callback(...args);
824
+ this.off(event, wrappedCallback);
825
+ };
826
+ this.on(event, wrappedCallback);
827
+ }
828
+ /**
829
+ * Emit event to all listeners
830
+ */
831
+ emit(event, data) {
832
+ this.eventListeners.get(event)?.forEach((callback) => {
833
+ try {
834
+ callback(data);
835
+ } catch (error) {
836
+ console.error(`Error in SDK event listener for ${event}:`, error);
837
+ }
838
+ });
839
+ }
840
+ /**
841
+ * Set up event forwarding from provider to SDK
842
+ */
843
+ setupEventForwarding() {
844
+ this.provider.on(EMBEDDED_PROVIDER_EVENTS.CONNECT, (data) => {
845
+ });
846
+ this.provider.on(EMBEDDED_PROVIDER_EVENTS.DISCONNECT, (data) => {
847
+ this.emit("disconnect", data);
848
+ });
849
+ this.provider.on(EMBEDDED_PROVIDER_EVENTS.ERROR, (data) => {
850
+ this.emit("error", data);
851
+ });
852
+ this.provider.on(EMBEDDED_PROVIDER_EVENTS.LOCK, (data) => {
853
+ this.emit("lock", data);
854
+ this.emit("disconnect", { reason: "locked" });
855
+ });
856
+ this.provider.on(EMBEDDED_PROVIDER_EVENTS.ACCOUNT_CHANGED, (data) => {
857
+ const account = data?.account ?? data;
858
+ this.refreshCachedAccounts(this.provider.getAccounts(), account ?? null);
859
+ this.emit("accountChanged", account);
860
+ });
861
+ }
862
+ /**
863
+ * Destroy SDK and cleanup
864
+ */
865
+ destroy() {
866
+ this.provider.destroy();
867
+ this.eventListeners.clear();
868
+ this.initialized = false;
869
+ this.connectInFlight = null;
870
+ this.lastConnectResult = null;
871
+ }
872
+ resolveMetadata(input) {
873
+ const defaultOrigin = typeof window !== "undefined" ? window.location.origin : void 0;
874
+ if (!defaultOrigin && !input) {
875
+ return void 0;
876
+ }
877
+ const appId = input?.appId || defaultOrigin;
878
+ const appUrl = this.resolveAppUrl(defaultOrigin, input?.appUrl);
879
+ const appName = input?.appName || this.deriveAppName(appUrl ?? appId);
880
+ const metadata = {};
881
+ if (appId) metadata.appId = appId;
882
+ if (appUrl) metadata.appUrl = appUrl;
883
+ if (appName) metadata.appName = appName;
884
+ if (input?.imageUrl) metadata.imageUrl = input.imageUrl;
885
+ return metadata;
886
+ }
887
+ resolveAppUrl(defaultOrigin, providedUrl) {
888
+ const candidate = providedUrl || defaultOrigin;
889
+ if (!candidate) {
890
+ return void 0;
891
+ }
892
+ try {
893
+ const url = new URL(candidate, defaultOrigin);
894
+ return url.toString();
895
+ } catch {
896
+ return defaultOrigin;
897
+ }
898
+ }
899
+ deriveAppName(source) {
900
+ if (!source) {
901
+ return void 0;
902
+ }
903
+ try {
904
+ const hostname = new URL(source).hostname;
905
+ return hostname || source;
906
+ } catch {
907
+ return source;
908
+ }
909
+ }
910
+ getThru() {
911
+ return this.thruClient;
912
+ }
913
+ refreshCachedAccounts(accounts, selectedAccount) {
914
+ const active = normalizeActiveWalletAccounts(accounts, selectedAccount);
915
+ if (this.lastConnectResult) {
916
+ this.lastConnectResult = {
917
+ ...this.lastConnectResult,
918
+ accounts: active.accounts,
919
+ selectedAccount: active.selectedAccount
920
+ };
921
+ }
922
+ }
923
+ };
924
+ var defaultContextValue = {
925
+ wallet: null,
926
+ isConnected: false,
927
+ accounts: [],
928
+ isConnecting: false,
929
+ error: null,
930
+ thru: null,
931
+ selectedAccount: null,
932
+ selectAccount: async () => void 0,
933
+ manageAccounts: async () => ({ accounts: [], selectedAccount: null })
934
+ };
935
+ var ThruContext = createContext(defaultContextValue);
936
+ function ThruProvider({ children, config }) {
937
+ const [sdk, setSdk] = useState(null);
938
+ const [thru, setThru] = useState(null);
939
+ const [isConnected, setIsConnected] = useState(false);
940
+ const [accounts, setAccounts] = useState([]);
941
+ const [isConnecting, setIsConnecting] = useState(false);
942
+ const [error, setError] = useState(null);
943
+ const [selectedAccount, setSelectedAccount] = useState(null);
944
+ useEffect(() => {
945
+ const sdkInstance = new BrowserSDK(config);
946
+ setSdk(sdkInstance);
947
+ setThru(sdkInstance.getThru());
948
+ const updateAccountsFromSdk = () => {
949
+ const active = normalizeActiveWalletAccounts(
950
+ sdkInstance.getAccounts(),
951
+ sdkInstance.getSelectedAccount()
952
+ );
953
+ setAccounts(active.accounts);
954
+ setSelectedAccount(active.selectedAccount);
955
+ };
956
+ const updateSelectedAccount = (account) => {
957
+ const active = normalizeActiveWalletAccounts(
958
+ sdkInstance.getAccounts(),
959
+ account ?? sdkInstance.getSelectedAccount()
960
+ );
961
+ setAccounts(active.accounts);
962
+ setSelectedAccount(active.selectedAccount);
963
+ };
964
+ sdkInstance.initialize().catch((err) => {
965
+ console.error("Failed to initialize SDK:", err);
966
+ setError(err);
967
+ });
968
+ const handleConnect = (result) => {
969
+ if (result.status === "connecting") {
970
+ setIsConnecting(true);
971
+ setError(null);
972
+ } else {
973
+ setIsConnected(true);
974
+ updateAccountsFromSdk();
975
+ setIsConnecting(false);
976
+ setError(null);
977
+ updateSelectedAccount(result?.selectedAccount ?? null);
978
+ }
979
+ };
980
+ const resetData = () => {
981
+ setIsConnected(false);
982
+ setAccounts([]);
983
+ setIsConnecting(false);
984
+ setSelectedAccount(null);
985
+ };
986
+ const handleDisconnect = () => {
987
+ resetData();
988
+ };
989
+ const handleError = (err) => {
990
+ setError(err.error || new Error("Unknown error"));
991
+ setIsConnecting(false);
992
+ };
993
+ const handleLock = () => {
994
+ resetData();
995
+ };
996
+ const handleAccountChanged = (account) => {
997
+ updateAccountsFromSdk();
998
+ updateSelectedAccount(account ?? void 0);
999
+ };
1000
+ sdkInstance.on("connect", handleConnect);
1001
+ sdkInstance.on("disconnect", handleDisconnect);
1002
+ sdkInstance.on("error", handleError);
1003
+ sdkInstance.on("lock", handleLock);
1004
+ sdkInstance.on("accountChanged", handleAccountChanged);
1005
+ return () => {
1006
+ sdkInstance.off("connect", handleConnect);
1007
+ sdkInstance.off("disconnect", handleDisconnect);
1008
+ sdkInstance.off("error", handleError);
1009
+ sdkInstance.off("lock", handleLock);
1010
+ sdkInstance.off("accountChanged", handleAccountChanged);
1011
+ sdkInstance.destroy();
1012
+ };
1013
+ }, []);
1014
+ const selectAccount = useCallback(async (account) => {
1015
+ if (!sdk) {
1016
+ throw new Error("BrowserSDK not initialized");
1017
+ }
1018
+ try {
1019
+ const updated = await sdk.selectAccount(account.address);
1020
+ const active = normalizeActiveWalletAccounts(sdk.getAccounts(), updated);
1021
+ setSelectedAccount(active.selectedAccount);
1022
+ setAccounts(active.accounts);
1023
+ } catch (err) {
1024
+ setError(err instanceof Error ? err : new Error("Failed to select account"));
1025
+ throw err;
1026
+ }
1027
+ }, [sdk]);
1028
+ const manageAccounts = useCallback(async () => {
1029
+ if (!sdk) {
1030
+ throw new Error("BrowserSDK not initialized");
1031
+ }
1032
+ try {
1033
+ const result = await sdk.manageAccounts();
1034
+ const activeResult = normalizeWalletAccountResult(result);
1035
+ setSelectedAccount(activeResult.selectedAccount);
1036
+ setAccounts(activeResult.accounts);
1037
+ setIsConnected(Boolean(activeResult.selectedAccount));
1038
+ return activeResult;
1039
+ } catch (err) {
1040
+ setError(err instanceof Error ? err : new Error("Failed to manage accounts"));
1041
+ throw err;
1042
+ }
1043
+ }, [sdk]);
1044
+ return /* @__PURE__ */ jsx(
1045
+ ThruContext.Provider,
1046
+ {
1047
+ value: {
1048
+ thru,
1049
+ wallet: sdk,
1050
+ isConnected,
1051
+ accounts,
1052
+ isConnecting,
1053
+ error,
1054
+ selectedAccount,
1055
+ selectAccount,
1056
+ manageAccounts
1057
+ },
1058
+ children
1059
+ }
1060
+ );
1061
+ }
1062
+ function useThru() {
1063
+ const context = useContext(ThruContext);
1064
+ return context;
1065
+ }
1066
+
1067
+ // src/react/hooks/useAccounts.ts
1068
+ function useAccounts(options) {
1069
+ const { accounts, selectedAccount, isConnected, isConnecting } = useThru();
1070
+ const externalOnSelect = options?.onAccountSelect;
1071
+ useEffect(() => {
1072
+ if (selectedAccount) {
1073
+ externalOnSelect?.(selectedAccount);
1074
+ }
1075
+ }, [externalOnSelect, selectedAccount]);
1076
+ return useMemo(
1077
+ () => ({
1078
+ accounts,
1079
+ selectedAccount,
1080
+ isConnected,
1081
+ isConnecting
1082
+ }),
1083
+ [accounts, selectedAccount, isConnected, isConnecting]
1084
+ );
1085
+ }
1086
+ function waitForWallet(getWallet, timeout = 5e3, interval = 100) {
1087
+ return new Promise((resolve, reject) => {
1088
+ const start = Date.now();
1089
+ const check = () => {
1090
+ const sdk = getWallet();
1091
+ if (sdk) return resolve(sdk);
1092
+ if (Date.now() - start > timeout) return reject(new Error("SDK not initialized in time"));
1093
+ setTimeout(check, interval);
1094
+ };
1095
+ check();
1096
+ });
1097
+ }
1098
+ function useWallet() {
1099
+ const {
1100
+ wallet,
1101
+ isConnected,
1102
+ accounts,
1103
+ selectedAccount,
1104
+ selectAccount,
1105
+ manageAccounts,
1106
+ isConnecting
1107
+ } = useThru();
1108
+ const walletRef = useRef(wallet);
1109
+ useEffect(() => {
1110
+ walletRef.current = wallet;
1111
+ }, [wallet]);
1112
+ const disconnect = async () => {
1113
+ if (!wallet) {
1114
+ throw new Error("SDK not initialized");
1115
+ }
1116
+ await wallet.disconnect();
1117
+ };
1118
+ const connect = async (options) => {
1119
+ try {
1120
+ const readySdk = walletRef.current ?? await waitForWallet(() => walletRef.current);
1121
+ const result = await readySdk.connect(options);
1122
+ return result;
1123
+ } catch (err) {
1124
+ const error = err;
1125
+ throw error;
1126
+ }
1127
+ };
1128
+ const mountInline = async (container) => {
1129
+ const readySdk = walletRef.current ?? await waitForWallet(() => walletRef.current);
1130
+ await readySdk.mountInline(container);
1131
+ };
1132
+ const openAccountSettings = async () => {
1133
+ return manageAccounts();
1134
+ };
1135
+ return {
1136
+ wallet: wallet?.thru,
1137
+ accounts,
1138
+ connect,
1139
+ disconnect,
1140
+ mountInline,
1141
+ manageAccounts: openAccountSettings,
1142
+ isConnected: isConnected && !!wallet,
1143
+ isConnecting,
1144
+ selectedAccount,
1145
+ selectAccount
1146
+ };
1147
+ }
1148
+
1149
+ export { BrowserSDK, ErrorCode, ThruProvider, useAccounts, useThru, useWallet };
1150
+ //# sourceMappingURL=react.js.map
1151
+ //# sourceMappingURL=react.js.map