@meshconnect/uwc-bridge-parent 1.4.2 → 1.5.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/dist/BridgeParent.d.ts +76 -0
- package/dist/BridgeParent.d.ts.map +1 -1
- package/dist/BridgeParent.js +331 -6
- package/dist/BridgeParent.js.map +1 -1
- package/dist/BridgeParent.test.js +621 -28
- package/dist/BridgeParent.test.js.map +1 -1
- package/package.json +1 -1
- package/src/BridgeParent.test.ts +923 -28
- package/src/BridgeParent.ts +455 -8
package/src/BridgeParent.ts
CHANGED
|
@@ -72,6 +72,26 @@ interface ExtendedStandardWalletAdapter extends StandardWalletAdapter {
|
|
|
72
72
|
) => Promise<Uint8Array>
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Minimal structural shape of a wallet-standard wallet's `standard:events`
|
|
77
|
+
* change stream — just enough for `subscribeSolanaWalletEvent` to observe live
|
|
78
|
+
* account switches without pulling in the full `@wallet-standard/features`
|
|
79
|
+
* types. `on('change', …)` returns an unsubscribe function (wallet-standard
|
|
80
|
+
* contract); `accounts` is the wallet's currently-authorized account set, whose
|
|
81
|
+
* first entry is the active account.
|
|
82
|
+
*/
|
|
83
|
+
interface StandardEventsCapableWallet {
|
|
84
|
+
accounts?: readonly { address: string }[]
|
|
85
|
+
features?: {
|
|
86
|
+
'standard:events'?: {
|
|
87
|
+
on?: (
|
|
88
|
+
event: 'change',
|
|
89
|
+
listener: (properties: { accounts?: readonly unknown[] }) => void
|
|
90
|
+
) => () => void
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
75
95
|
export interface WalletStandardInfo {
|
|
76
96
|
uuid: string
|
|
77
97
|
name: string
|
|
@@ -139,6 +159,57 @@ export interface ParentAPI {
|
|
|
139
159
|
|
|
140
160
|
discoverTronWallets: (injectedIds: string[]) => void
|
|
141
161
|
discoverTonWallets: (jsBridgeKeys: string[]) => void
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Attach a real listener for `eventName` on the EIP-6963 provider. Awaits the
|
|
165
|
+
* one-shot EIP-6963 discovery window before looking up the wallet by `uuid` in
|
|
166
|
+
* `eip6963Wallets`; if that misses and an optional `rdns` was supplied, falls
|
|
167
|
+
* back to matching by `rdns` (a wallet-defined identifier stable across
|
|
168
|
+
* injection contexts). Forwards every event firing to `callback` (a
|
|
169
|
+
* Comlink-proxied callback the child supplies). Resolves to a teardown that
|
|
170
|
+
* removes the listener. THROWS when neither `uuid` nor the `rdns` fallback
|
|
171
|
+
* resolves a wallet (surfaced to the child as a bridge error rather than a
|
|
172
|
+
* silent no-op); resolves to `undefined` only when the wallet IS found but
|
|
173
|
+
* exposes no `.on` to observe.
|
|
174
|
+
*/
|
|
175
|
+
subscribeEip6963ProviderEvent: (
|
|
176
|
+
uuid: string,
|
|
177
|
+
eventName: 'accountsChanged' | 'chainChanged' | 'disconnect',
|
|
178
|
+
callback: (payload?: unknown) => void,
|
|
179
|
+
rdns?: string
|
|
180
|
+
) => Promise<(() => void) | undefined>
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Same as `subscribeEip6963ProviderEvent`, but for an injected Tron
|
|
184
|
+
* provider resolved fresh by its window path (`injectedId`) rather than a
|
|
185
|
+
* cached discovery list, since Tron providers aren't cached the same way.
|
|
186
|
+
*/
|
|
187
|
+
subscribeTronProviderEvent: (
|
|
188
|
+
injectedId: string,
|
|
189
|
+
eventName: 'accountsChanged' | 'disconnect',
|
|
190
|
+
callback: (payload?: unknown) => void
|
|
191
|
+
) => (() => void) | undefined
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Relay a connected Solana wallet's account-change events across the
|
|
195
|
+
* bridge. `walletId` is the SAME id `walletStandardWallets[].uuid`
|
|
196
|
+
* publishes (see `generateWalletId` — name+chain based, not a real UUID),
|
|
197
|
+
* which the child mirrors back as
|
|
198
|
+
* `SolanaWalletService.connectedWalletUuid`. Deliberately re-resolves the
|
|
199
|
+
* REAL, in-frame adapter (via `getWallets()`) rather than reusing the
|
|
200
|
+
* serialized `walletStandardWallets[].adapter` object — that object only
|
|
201
|
+
* exposes the whitelisted sign/transaction methods for Comlink, not
|
|
202
|
+
* `.on`/`.off`, so it can't be used to attach a live listener. Forwards a
|
|
203
|
+
* plain `string | null` (never a `PublicKey`) to `callback`, a
|
|
204
|
+
* Comlink-proxied callback the child supplies. Throws when no wallet
|
|
205
|
+
* matches `walletId` (reported as a bridge error on the child side,
|
|
206
|
+
* mirroring `subscribeEip6963ProviderEvent`'s uuid-miss); returns
|
|
207
|
+
* `undefined` when the resolved adapter can't be observed.
|
|
208
|
+
*/
|
|
209
|
+
subscribeSolanaWalletEvent: (
|
|
210
|
+
walletId: string,
|
|
211
|
+
callback: (address: string | null) => void
|
|
212
|
+
) => (() => void) | undefined
|
|
142
213
|
}
|
|
143
214
|
|
|
144
215
|
export class BridgeParent {
|
|
@@ -154,6 +225,10 @@ export class BridgeParent {
|
|
|
154
225
|
options: EndpointAddEventListenerOptions
|
|
155
226
|
}> = []
|
|
156
227
|
private destroyed = false
|
|
228
|
+
// Captured so subscribeEip6963ProviderEvent can await the one-shot discovery
|
|
229
|
+
// window before its uuid/rdns lookup, instead of racing an empty
|
|
230
|
+
// eip6963Wallets when a subscribe call arrives before discovery resolves.
|
|
231
|
+
private eip6963DiscoveryPromise: Promise<DetectedWallet[]> | null = null
|
|
157
232
|
|
|
158
233
|
constructor(iframe: HTMLIFrameElement) {
|
|
159
234
|
if (!iframe) {
|
|
@@ -176,6 +251,7 @@ export class BridgeParent {
|
|
|
176
251
|
// while that's in flight — those paths are independent. The readiness
|
|
177
252
|
// flags gate the child from reading a still-empty array.
|
|
178
253
|
const eip6963Promise = this.discovereip6963Wallets()
|
|
254
|
+
this.eip6963DiscoveryPromise = eip6963Promise
|
|
179
255
|
// Solana is synchronous, so run it inline (no benefit to a microtask hop)
|
|
180
256
|
const walletStandardWallets = this.getSolanaWallets()
|
|
181
257
|
|
|
@@ -206,6 +282,215 @@ export class BridgeParent {
|
|
|
206
282
|
this.parentAPI.tonWallets = this.getTonWallets(jsBridgeKeys)
|
|
207
283
|
this.parentAPI.tonWalletsReady = true
|
|
208
284
|
}
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
subscribeEip6963ProviderEvent: async (
|
|
288
|
+
uuid: string,
|
|
289
|
+
eventName: 'accountsChanged' | 'chainChanged' | 'disconnect',
|
|
290
|
+
callback: (payload?: unknown) => void,
|
|
291
|
+
rdns?: string
|
|
292
|
+
): Promise<(() => void) | undefined> => {
|
|
293
|
+
// Discovery is one-shot with a fixed window (see
|
|
294
|
+
// discovereip6963Wallets) — a subscribe call can legitimately arrive
|
|
295
|
+
// before it resolves (e.g. immediately on page load during a session
|
|
296
|
+
// restore). Awaiting here means the lookup below always runs against
|
|
297
|
+
// the FINAL discovered list instead of racing a still-empty one. This
|
|
298
|
+
// is safe to await unconditionally: it resolves immediately once
|
|
299
|
+
// discovery has already completed. Comlink calls are already async
|
|
300
|
+
// over postMessage (the child already wraps this in
|
|
301
|
+
// `Promise.resolve()` — see BridgeChild.ts), so returning a Promise
|
|
302
|
+
// here changes nothing from the child's perspective.
|
|
303
|
+
await this.eip6963DiscoveryPromise
|
|
304
|
+
const wallets = this.parentAPI?.eip6963Wallets ?? []
|
|
305
|
+
// uuid is the primary match. It can genuinely diverge from what this
|
|
306
|
+
// parent discovered: each independent EIP-6963 injection context
|
|
307
|
+
// (top frame vs. this iframe) mints its own random uuid per spec, so
|
|
308
|
+
// the SAME physical wallet can be known under two different uuids —
|
|
309
|
+
// widening the discovery window (see the sibling plan this one
|
|
310
|
+
// follows) only narrows that race, it can't close it. rdns
|
|
311
|
+
// ("io.metamask"-style) is wallet-defined and stable across
|
|
312
|
+
// injection contexts, so it's a reliable fallback identity when the
|
|
313
|
+
// uuid the child captured doesn't match anything this parent saw.
|
|
314
|
+
const wallet =
|
|
315
|
+
wallets.find(w => w.uuid === uuid) ??
|
|
316
|
+
(rdns ? wallets.find(w => w.rdns === rdns) : undefined)
|
|
317
|
+
if (!wallet) {
|
|
318
|
+
// Neither the uuid nor (if supplied) the rdns matched anything —
|
|
319
|
+
// genuinely not discovered by this parent. Throwing — instead of
|
|
320
|
+
// returning undefined like the "found but not observable" branch
|
|
321
|
+
// below does — turns this into a reported bridge error on the
|
|
322
|
+
// child side (see BridgeChild.ts's
|
|
323
|
+
// window.__uwcSubscribeEip6963ProviderEvent) instead of a silent,
|
|
324
|
+
// undetectable no-op.
|
|
325
|
+
throw new Error(
|
|
326
|
+
`subscribeEip6963ProviderEvent: no wallet found for uuid "${uuid}"`
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
330
|
+
const provider = wallet.provider as any
|
|
331
|
+
if (!provider || typeof provider.on !== 'function') return undefined
|
|
332
|
+
// `callback` here IS the Comlink proxy the child passed — registering
|
|
333
|
+
// it DIRECTLY as the provider's own event listener would make Comlink
|
|
334
|
+
// serialize EVERY argument the wallet's own dispatch calls it with.
|
|
335
|
+
// Some wallets' internal EventEmitter passes extra, non-EIP-1193
|
|
336
|
+
// arguments alongside the real payload (observed in production: a
|
|
337
|
+
// wallet's own internal accountsChanged handler function), and
|
|
338
|
+
// Comlink can't clone a function — throwing "DataCloneError" the
|
|
339
|
+
// moment the wallet actually fires the event, silently breaking
|
|
340
|
+
// delivery. A local (non-proxied, ordinary JS) wrapper that only
|
|
341
|
+
// forwards the first argument sidesteps this entirely — extra
|
|
342
|
+
// arguments are simply never looked at, the same way the direct
|
|
343
|
+
// (non-bridge) `EthereumWalletService.onLifecycleEvent` path already
|
|
344
|
+
// does by using a plain single-parameter handler.
|
|
345
|
+
const localListener = (payload?: unknown) => callback(payload)
|
|
346
|
+
provider.on(eventName, localListener)
|
|
347
|
+
// The teardown crosses back to the child as a Comlink return value —
|
|
348
|
+
// like TON's `listen()` above, an un-proxied function can't be
|
|
349
|
+
// structured-cloned over postMessage (Comlink throws
|
|
350
|
+
// "Unserializable return value" the moment the child tries to
|
|
351
|
+
// receive it), silently losing the child's ability to unsubscribe.
|
|
352
|
+
// The block body below is deliberate, not stylistic: an
|
|
353
|
+
// expression-bodied `() => provider.removeListener?.(...)` would
|
|
354
|
+
// implicitly return removeListener's OWN return value — by
|
|
355
|
+
// EventEmitter convention that's `this` (the whole provider), which
|
|
356
|
+
// is itself unserializable and reproduces the exact same failure one
|
|
357
|
+
// level down, the moment this teardown is actually invoked.
|
|
358
|
+
return Comlink.proxy(() => {
|
|
359
|
+
provider.removeListener?.(eventName, localListener)
|
|
360
|
+
})
|
|
361
|
+
},
|
|
362
|
+
subscribeTronProviderEvent: (
|
|
363
|
+
injectedId: string,
|
|
364
|
+
eventName: 'accountsChanged' | 'disconnect',
|
|
365
|
+
callback: (payload?: unknown) => void
|
|
366
|
+
): (() => void) | undefined => {
|
|
367
|
+
const provider = this.resolveTronProviderByPath(injectedId)
|
|
368
|
+
if (!provider) {
|
|
369
|
+
// Same rationale as subscribeEip6963ProviderEvent above — surface
|
|
370
|
+
// a genuine "nothing at this path" miss as a reported bridge
|
|
371
|
+
// error instead of a silent no-op.
|
|
372
|
+
throw new Error(
|
|
373
|
+
`subscribeTronProviderEvent: no provider found at injectedId "${injectedId}"`
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
// Choose an EVENT-CAPABLE emitter. The provider resolved by
|
|
377
|
+
// `injectedId` (e.g. `window.tronLink`) carries `request`+`tronWeb` —
|
|
378
|
+
// enough for discovery + signing — but is NOT necessarily an event
|
|
379
|
+
// emitter. TronLink specifically injects a SEPARATE TIP-1193 provider
|
|
380
|
+
// at `window.tron` that is the one emitting `accountsChanged` /
|
|
381
|
+
// `disconnect`; `window.tronLink` has no `.on`, so binding there
|
|
382
|
+
// silently dropped every in-wallet account switch (MFS-778). Order:
|
|
383
|
+
// 1) the resolved provider itself (Trust / Bitget expose their own `.on`)
|
|
384
|
+
// 2) TronLink's TIP-1193 `window.tron` — but ONLY for the TronLink
|
|
385
|
+
// subscription (see `resolveTronEventEmitter`). A wallet whose own
|
|
386
|
+
// Tron provider lacks `.on` (e.g. OKX's `okxwallet.tronLink`) must
|
|
387
|
+
// NOT inherit TronLink's `window.tron`, or its switches bind to the
|
|
388
|
+
// wrong wallet and are dropped — it falls through to (3) instead.
|
|
389
|
+
// 3) the window `message` channel (below), which OKX actually uses.
|
|
390
|
+
const emitter =
|
|
391
|
+
typeof provider.on === 'function'
|
|
392
|
+
? provider
|
|
393
|
+
: this.resolveTronEventEmitter(injectedId, provider)
|
|
394
|
+
if (emitter) {
|
|
395
|
+
// Same "don't register the raw Comlink proxy directly" requirement
|
|
396
|
+
// as subscribeEip6963ProviderEvent above.
|
|
397
|
+
const localListener = (payload?: unknown) => callback(payload)
|
|
398
|
+
emitter.on(eventName, localListener)
|
|
399
|
+
// Same block-body requirement as subscribeEip6963ProviderEvent above
|
|
400
|
+
// — discard removeListener's own (EventEmitter-convention `this`)
|
|
401
|
+
// return value rather than implicitly returning it.
|
|
402
|
+
return Comlink.proxy(() => {
|
|
403
|
+
emitter.removeListener?.(eventName, localListener)
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
// 3) Legacy TronLink builds expose neither `.on` surface: the wallet
|
|
407
|
+
// broadcasts account/disconnect changes as `window` `message`
|
|
408
|
+
// events instead. Last-resort, and window-global (not
|
|
409
|
+
// wallet-scoped), so only reached when no emitter exists at all.
|
|
410
|
+
return Comlink.proxy(
|
|
411
|
+
this.subscribeTronWindowMessage(eventName, callback)
|
|
412
|
+
)
|
|
413
|
+
},
|
|
414
|
+
subscribeSolanaWalletEvent: (
|
|
415
|
+
walletId: string,
|
|
416
|
+
callback: (address: string | null) => void
|
|
417
|
+
): (() => void) | undefined => {
|
|
418
|
+
const adapter = this.resolveSolanaAdapterForWalletEvent(walletId)
|
|
419
|
+
if (!adapter) {
|
|
420
|
+
// Same rationale as subscribeEip6963ProviderEvent's uuid miss —
|
|
421
|
+
// surface a genuine "nothing at this walletId" miss as a reported
|
|
422
|
+
// bridge error instead of a silent no-op.
|
|
423
|
+
throw new Error(
|
|
424
|
+
`subscribeSolanaWalletEvent: no wallet found for walletId "${walletId}"`
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
// PREFERRED — subscribe to the underlying wallet's wallet-standard
|
|
428
|
+
// `standard:events` `change` stream, which fires on EVERY account switch
|
|
429
|
+
// and carries the wallet's updated `accounts`. We must NOT derive switches
|
|
430
|
+
// from the adapter's own `connect`/`disconnect` here:
|
|
431
|
+
// `resolveSolanaAdapterForWalletEvent` returns a FRESH, never-connected
|
|
432
|
+
// `StandardWalletAdapter`, and that adapter only translates a `change` into
|
|
433
|
+
// `connect`/`disconnect` when it is already connected — so on the bridge an
|
|
434
|
+
// account-to-account switch emits NEITHER event and is silently dropped
|
|
435
|
+
// (only an eventual full de-auth trips `disconnect`). That is the Phantom
|
|
436
|
+
// multi-switch regression: several switches produce no telemetry and no
|
|
437
|
+
// session update, and only the final de-auth disconnects (MFS-778).
|
|
438
|
+
// Reading the wallet's `standard:events` directly captures every switch.
|
|
439
|
+
const standardWallet = (
|
|
440
|
+
adapter as unknown as { wallet?: StandardEventsCapableWallet }
|
|
441
|
+
).wallet
|
|
442
|
+
const standardEvents = standardWallet?.features?.['standard:events']
|
|
443
|
+
if (standardWallet && typeof standardEvents?.on === 'function') {
|
|
444
|
+
// Forward the authoritative current active account as a plain base58
|
|
445
|
+
// string | null BEFORE crossing Comlink (the raw account object isn't
|
|
446
|
+
// structured-cloneable, and the child only needs the address). null =
|
|
447
|
+
// the switch de-authorized the dapp → the child maps it to [] (disconnect),
|
|
448
|
+
// a non-null address maps to [address] (switch), exactly as the legacy
|
|
449
|
+
// connect/disconnect path did.
|
|
450
|
+
const forwardActiveAccount = () => {
|
|
451
|
+
callback(standardWallet.accounts?.[0]?.address ?? null)
|
|
452
|
+
}
|
|
453
|
+
const off = standardEvents.on('change', properties => {
|
|
454
|
+
// React only to account-set changes; ignore pure chain/feature deltas.
|
|
455
|
+
if (properties && 'accounts' in properties) forwardActiveAccount()
|
|
456
|
+
})
|
|
457
|
+
// Block body (not an expression arrow): the wallet-standard unsubscribe
|
|
458
|
+
// returns its own value, which must not become this teardown's
|
|
459
|
+
// (unserializable) return across Comlink. Best-effort + idempotent.
|
|
460
|
+
return Comlink.proxy(() => {
|
|
461
|
+
try {
|
|
462
|
+
off?.()
|
|
463
|
+
} catch {
|
|
464
|
+
// Unsubscribe must never throw across the bridge.
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// FALLBACK — a traditional (non-wallet-standard) adapter, e.g. the legacy
|
|
470
|
+
// Base Wallet, exposes only EventEmitter `connect`/`disconnect` and no
|
|
471
|
+
// `standard:events` stream. Its connected instance re-emits `connect` on a
|
|
472
|
+
// switch, so this path stays correct for it.
|
|
473
|
+
if (typeof adapter.on !== 'function') return undefined
|
|
474
|
+
|
|
475
|
+
// Reduce to a plain string | null BEFORE crossing Comlink — the raw
|
|
476
|
+
// PublicKey (or the adapter/emitter itself) must never be forwarded:
|
|
477
|
+
// it isn't structured-cloneable and the child only ever needs the
|
|
478
|
+
// base58 address.
|
|
479
|
+
const onConnect = (publicKey?: { toBase58?: () => string } | null) => {
|
|
480
|
+
callback(publicKey?.toBase58?.() ?? null)
|
|
481
|
+
}
|
|
482
|
+
const onDisconnect = () => callback(null)
|
|
483
|
+
adapter.on('connect', onConnect)
|
|
484
|
+
adapter.on('disconnect', onDisconnect)
|
|
485
|
+
// Block body — same requirement as subscribeEip6963ProviderEvent's
|
|
486
|
+
// teardown above: an expression-bodied `() => adapter.off(...)`
|
|
487
|
+
// would implicitly return `.off`'s own (EventEmitter-convention
|
|
488
|
+
// `this`) value, reproducing the unserializable-return-value failure
|
|
489
|
+
// one level down, the moment this teardown is actually invoked.
|
|
490
|
+
return Comlink.proxy(() => {
|
|
491
|
+
adapter.off('connect', onConnect)
|
|
492
|
+
adapter.off('disconnect', onDisconnect)
|
|
493
|
+
})
|
|
209
494
|
}
|
|
210
495
|
}
|
|
211
496
|
|
|
@@ -288,7 +573,16 @@ export class BridgeParent {
|
|
|
288
573
|
private async discovereip6963Wallets(): Promise<DetectedWallet[]> {
|
|
289
574
|
return new Promise(resolve => {
|
|
290
575
|
const detectedWallets: DetectedWallet[] = []
|
|
291
|
-
|
|
576
|
+
// 300ms (not 100ms): a wallet extension racing other extensions to
|
|
577
|
+
// inject, or a slow page load, can announce after a 100ms window
|
|
578
|
+
// closes — this parent's discovery is one-shot with no retry, so a
|
|
579
|
+
// missed announcement is missed forever for this session. Widening
|
|
580
|
+
// the window doesn't make discovery bulletproof (no fixed window
|
|
581
|
+
// can be), but meaningfully reduces how often a real wallet gets
|
|
582
|
+
// missed here and pushed onto the CHILD's local-discovery fallback,
|
|
583
|
+
// which is where the uuid-mismatch bug this constant guards against
|
|
584
|
+
// originates.
|
|
585
|
+
const timeout = 300 // Wait 300ms for wallets to announce
|
|
292
586
|
|
|
293
587
|
// Set up listener for wallet announcements
|
|
294
588
|
const handleAnnouncement = (event: Event) => {
|
|
@@ -738,6 +1032,165 @@ export class BridgeParent {
|
|
|
738
1032
|
return provider['tronWeb'] != null && 'request' in provider
|
|
739
1033
|
}
|
|
740
1034
|
|
|
1035
|
+
/**
|
|
1036
|
+
* Resolve the REAL (non-serialized) Solana adapter for `walletId` — the
|
|
1037
|
+
* same id `getSolanaWallets()` publishes as `WalletStandardInfo.uuid` (see
|
|
1038
|
+
* `generateWalletId`). Mirrors `getSolanaWallets()`'s wallet-standard +
|
|
1039
|
+
* traditional-Base-Wallet resolution, but returns the adapter instance
|
|
1040
|
+
* itself instead of the Comlink-serialized object literal, since only the
|
|
1041
|
+
* real instance has `.on`/`.off` to subscribe to.
|
|
1042
|
+
*/
|
|
1043
|
+
private resolveSolanaAdapterForWalletEvent(
|
|
1044
|
+
walletId: string
|
|
1045
|
+
): StandardWalletAdapter | BaseWalletAdapter | undefined {
|
|
1046
|
+
const { get } = getWallets()
|
|
1047
|
+
const wallets = get()
|
|
1048
|
+
|
|
1049
|
+
for (const wallet of wallets) {
|
|
1050
|
+
if (!this.isSolanaWallet(wallet)) continue
|
|
1051
|
+
if (this.generateWalletId(wallet) !== walletId) continue
|
|
1052
|
+
if (!isWalletAdapterCompatibleStandardWallet(wallet)) return undefined
|
|
1053
|
+
return new StandardWalletAdapter({ wallet })
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
try {
|
|
1057
|
+
const baseAdapter = new BaseWalletAdapter()
|
|
1058
|
+
const baseWalletId = `${baseAdapter.name}-traditional`
|
|
1059
|
+
.toLowerCase()
|
|
1060
|
+
.replace(/\s+/g, '-')
|
|
1061
|
+
const isDetected =
|
|
1062
|
+
baseAdapter.readyState === WalletReadyState.Installed ||
|
|
1063
|
+
baseAdapter.readyState === WalletReadyState.Loadable
|
|
1064
|
+
if (baseWalletId === walletId && isDetected) {
|
|
1065
|
+
return baseAdapter
|
|
1066
|
+
}
|
|
1067
|
+
} catch {
|
|
1068
|
+
// Base Wallet adapter failed to initialize — no traditional fallback.
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
return undefined
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/** Resolve a nested window path (e.g. 'tokenpocket.tron' -> window.tokenpocket.tron). */
|
|
1075
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1076
|
+
private resolveTronProviderByPath(injectedId: string): any {
|
|
1077
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1078
|
+
return injectedId.split('.').reduce<any>(
|
|
1079
|
+
(acc, part) => acc?.[part],
|
|
1080
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1081
|
+
window as any
|
|
1082
|
+
)
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* TronLink's account/disconnect events fire on its TIP-1193 provider
|
|
1087
|
+
* (`window.tron`), NOT on the `window.tronLink` object the catalog
|
|
1088
|
+
* `injectedId` resolves to (that one carries `request`+`tronWeb` but is not
|
|
1089
|
+
* an event emitter). Returns TronLink's `window.tron` emitter, but ONLY when
|
|
1090
|
+
* the subscription is for TronLink itself.
|
|
1091
|
+
*
|
|
1092
|
+
* `window.tron` belongs exclusively to TronLink. Other Tron wallets whose own
|
|
1093
|
+
* provider happens to lack `.on` (confirmed for OKX's `okxwallet.tronLink`,
|
|
1094
|
+
* which is not an event emitter and broadcasts account switches on the window
|
|
1095
|
+
* `message` channel instead) MUST NOT inherit it — binding OKX's
|
|
1096
|
+
* `accountsChanged` on TronLink's `window.tron` means OKX's switches fire on
|
|
1097
|
+
* the wrong wallet's provider and are silently dropped. Gated on the
|
|
1098
|
+
* subscription's identity so only the TronLink path uses this fallback; every
|
|
1099
|
+
* other wallet falls through to `subscribeTronWindowMessage`.
|
|
1100
|
+
*/
|
|
1101
|
+
private resolveTronEventEmitter(
|
|
1102
|
+
injectedId: string,
|
|
1103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1104
|
+
provider: any
|
|
1105
|
+
):
|
|
1106
|
+
| {
|
|
1107
|
+
on: (event: string, handler: (...args: unknown[]) => void) => void
|
|
1108
|
+
removeListener?: (
|
|
1109
|
+
event: string,
|
|
1110
|
+
handler: (...args: unknown[]) => void
|
|
1111
|
+
) => void
|
|
1112
|
+
}
|
|
1113
|
+
| undefined {
|
|
1114
|
+
if (typeof window === 'undefined') return undefined
|
|
1115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1116
|
+
const win = window as any
|
|
1117
|
+
const tip1193 = win.tron
|
|
1118
|
+
if (
|
|
1119
|
+
!tip1193 ||
|
|
1120
|
+
typeof tip1193.on !== 'function' ||
|
|
1121
|
+
!this.isTronProvider(tip1193)
|
|
1122
|
+
) {
|
|
1123
|
+
return undefined
|
|
1124
|
+
}
|
|
1125
|
+
// Only TronLink may use its own `window.tron`. Identify the TronLink
|
|
1126
|
+
// subscription by provider identity (the resolved provider IS `window.tron`
|
|
1127
|
+
// / `window.tronLink`) or the canonical TronLink injectedId paths.
|
|
1128
|
+
const isTronLinkSubscription =
|
|
1129
|
+
provider === tip1193 ||
|
|
1130
|
+
provider === win.tronLink ||
|
|
1131
|
+
injectedId === 'tron' ||
|
|
1132
|
+
injectedId === 'tronLink'
|
|
1133
|
+
if (isTronLinkSubscription) {
|
|
1134
|
+
return tip1193
|
|
1135
|
+
}
|
|
1136
|
+
return undefined
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Last-resort account/disconnect subscription for TronLink builds that expose
|
|
1141
|
+
* neither an EIP-1193 nor a TIP-1193 `.on`: the wallet broadcasts changes as
|
|
1142
|
+
* `window` `message` events (`{ message: { action, data } }`). Maps the
|
|
1143
|
+
* TronLink `action` to the requested UWC event and forwards a
|
|
1144
|
+
* Comlink-cloneable payload (never the raw event). Returns a teardown.
|
|
1145
|
+
*
|
|
1146
|
+
* `message` is window-global (not wallet-scoped), so this is only reached
|
|
1147
|
+
* when no `.on` emitter exists anywhere for this session.
|
|
1148
|
+
*/
|
|
1149
|
+
private subscribeTronWindowMessage(
|
|
1150
|
+
eventName: 'accountsChanged' | 'disconnect',
|
|
1151
|
+
callback: (payload?: unknown) => void
|
|
1152
|
+
): () => void {
|
|
1153
|
+
const handler = (event: MessageEvent) => {
|
|
1154
|
+
// Runs in the client's TOP frame and the payload flows straight into the
|
|
1155
|
+
// live session address, so only trust broadcasts from THIS window — the
|
|
1156
|
+
// page context TronLink/OKX inject into. Without this, any same-origin
|
|
1157
|
+
// sibling frame or `window.opener` could post `{ message: { action:
|
|
1158
|
+
// 'setAccount' } }` and move the user's account. Mirrors the posture of
|
|
1159
|
+
// `wrapWithSourceGuard` and `Comlink.expose(..., [iframeOrigin])`.
|
|
1160
|
+
if (event.source !== window || event.origin !== window.location.origin) {
|
|
1161
|
+
return
|
|
1162
|
+
}
|
|
1163
|
+
const message = (
|
|
1164
|
+
event?.data as { message?: { action?: unknown; data?: unknown } }
|
|
1165
|
+
)?.message
|
|
1166
|
+
const action = message?.action
|
|
1167
|
+
if (typeof action !== 'string') return
|
|
1168
|
+
if (
|
|
1169
|
+
eventName === 'accountsChanged' &&
|
|
1170
|
+
(action === 'setAccount' || action === 'accountsChanged')
|
|
1171
|
+
) {
|
|
1172
|
+
const data = message?.data as
|
|
1173
|
+
| { address?: string }
|
|
1174
|
+
| string
|
|
1175
|
+
| string[]
|
|
1176
|
+
| undefined
|
|
1177
|
+
const address =
|
|
1178
|
+
typeof data === 'string'
|
|
1179
|
+
? data
|
|
1180
|
+
: Array.isArray(data)
|
|
1181
|
+
? data[0]
|
|
1182
|
+
: data?.address
|
|
1183
|
+
// Match the TIP-1193 `accountsChanged` shape (`string[]`) so the
|
|
1184
|
+
// connector's normalization treats an absent address as a logout.
|
|
1185
|
+
callback(address ? [address] : [])
|
|
1186
|
+
} else if (eventName === 'disconnect' && action === 'disconnect') {
|
|
1187
|
+
callback()
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
window.addEventListener('message', handler)
|
|
1191
|
+
return () => window.removeEventListener('message', handler)
|
|
1192
|
+
}
|
|
1193
|
+
|
|
741
1194
|
private getTronWallets(injectedIds: string[]): TronWalletInfo[] {
|
|
742
1195
|
if (typeof window === 'undefined' || injectedIds.length === 0) {
|
|
743
1196
|
return []
|
|
@@ -747,13 +1200,7 @@ export class BridgeParent {
|
|
|
747
1200
|
const seen = new Set<string>()
|
|
748
1201
|
|
|
749
1202
|
for (const injectedId of injectedIds) {
|
|
750
|
-
|
|
751
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
752
|
-
const provider = injectedId.split('.').reduce<any>(
|
|
753
|
-
(acc, part) => acc?.[part],
|
|
754
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
755
|
-
window as any
|
|
756
|
-
)
|
|
1203
|
+
const provider = this.resolveTronProviderByPath(injectedId)
|
|
757
1204
|
|
|
758
1205
|
// Check if it looks like a TronProvider (has tronWeb.trx)
|
|
759
1206
|
if (this.isTronProvider(provider) && !seen.has(injectedId)) {
|