@meshconnect/uwc-bridge-parent 1.1.1 → 1.2.1

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.
@@ -14,7 +14,13 @@ vi.mock('@wallet-standard/app', () => ({
14
14
 
15
15
  // Mock @solana/wallet-adapter-base
16
16
  vi.mock('@solana/wallet-adapter-base', () => ({
17
- isWalletAdapterCompatibleStandardWallet: vi.fn().mockReturnValue(false)
17
+ isWalletAdapterCompatibleStandardWallet: vi.fn().mockReturnValue(false),
18
+ WalletReadyState: {
19
+ Installed: 'Installed',
20
+ NotDetected: 'NotDetected',
21
+ Loadable: 'Loadable',
22
+ Unsupported: 'Unsupported'
23
+ }
18
24
  }))
19
25
 
20
26
  // Mock @solana/wallet-standard-wallet-adapter-base
@@ -29,6 +35,20 @@ vi.mock('@solana/web3.js', () => ({
29
35
  VersionedTransaction: { deserialize: vi.fn() }
30
36
  }))
31
37
 
38
+ // Hoisted mock for BaseWalletAdapter - direct reference avoids restoreAllMocks issues
39
+ const baseAdapterMock = vi.hoisted(() => ({
40
+ readyState: 'NotDetected' as string,
41
+ shouldThrow: false,
42
+ fn: null as ReturnType<typeof vi.fn> | null
43
+ }))
44
+
45
+ baseAdapterMock.fn = vi.fn()
46
+
47
+ // Mock ./BaseWalletAdapter
48
+ vi.mock('./BaseWalletAdapter', () => ({
49
+ BaseWalletAdapter: baseAdapterMock.fn
50
+ }))
51
+
32
52
  describe('BridgeParent', () => {
33
53
  let mockIframe: HTMLIFrameElement
34
54
 
@@ -394,6 +414,137 @@ describe('BridgeParent', () => {
394
414
  })
395
415
  })
396
416
 
417
+ describe('Base Wallet detection', () => {
418
+ beforeEach(async () => {
419
+ baseAdapterMock.readyState = 'NotDetected'
420
+ baseAdapterMock.shouldThrow = false
421
+
422
+ // Re-set mocks that vi.restoreAllMocks() may have cleared
423
+ const { getWallets } = await import('@wallet-standard/app')
424
+ vi.mocked(getWallets).mockReturnValue({
425
+ get: () => [],
426
+ on: vi.fn(),
427
+ register: vi.fn()
428
+ } as any)
429
+
430
+ // Re-set BaseWalletAdapter mock - must use 'function' (not arrow) for 'new' compatibility
431
+ baseAdapterMock.fn!.mockImplementation(function () {
432
+ if (baseAdapterMock.shouldThrow) throw new Error('init failed')
433
+ return {
434
+ readyState: baseAdapterMock.readyState,
435
+ name: 'Base Wallet',
436
+ icon: 'data:image/svg+xml;base64,test',
437
+ url: 'https://example.com',
438
+ publicKey: null,
439
+ connecting: false,
440
+ connected: false,
441
+ supportedTransactionVersions: new Set(['legacy', 0]),
442
+ connect: vi.fn(),
443
+ disconnect: vi.fn(),
444
+ sendTransaction: vi.fn(),
445
+ signTransaction: vi.fn(),
446
+ signAllTransactions: vi.fn(),
447
+ signMessage: vi.fn()
448
+ }
449
+ })
450
+ })
451
+
452
+ it('should include Base Wallet when detected as Installed', async () => {
453
+ const Comlink = await import('comlink')
454
+ const { BridgeParent } = await import('./BridgeParent')
455
+
456
+ baseAdapterMock.readyState = 'Installed'
457
+
458
+ new BridgeParent(mockIframe)
459
+ await vi.advanceTimersByTimeAsync(200)
460
+
461
+ const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any
462
+ const baseWallet = exposedAPI.walletStandardWallets.find(
463
+ (w: any) => w.name === 'Base Wallet'
464
+ )
465
+ expect(baseWallet).toBeDefined()
466
+ expect(baseWallet.uuid).toBe('base-wallet-traditional')
467
+ expect(baseWallet.chains).toEqual(['solana:mainnet'])
468
+ expect(baseWallet.features).toEqual(['traditional-adapter'])
469
+ expect(baseWallet.adapter).toBeDefined()
470
+ })
471
+
472
+ it('should include Base Wallet when detected as Loadable', async () => {
473
+ const Comlink = await import('comlink')
474
+ const { BridgeParent } = await import('./BridgeParent')
475
+
476
+ baseAdapterMock.readyState = 'Loadable'
477
+
478
+ new BridgeParent(mockIframe)
479
+ await vi.advanceTimersByTimeAsync(200)
480
+
481
+ const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any
482
+ const baseWallet = exposedAPI.walletStandardWallets.find(
483
+ (w: any) => w.name === 'Base Wallet'
484
+ )
485
+ expect(baseWallet).toBeDefined()
486
+ })
487
+
488
+ it('should not include Base Wallet when not detected', async () => {
489
+ const Comlink = await import('comlink')
490
+ const { BridgeParent } = await import('./BridgeParent')
491
+
492
+ new BridgeParent(mockIframe)
493
+ await vi.advanceTimersByTimeAsync(200)
494
+
495
+ const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any
496
+ const baseWallet = exposedAPI.walletStandardWallets.find(
497
+ (w: any) => w.name === 'Base Wallet'
498
+ )
499
+ expect(baseWallet).toBeUndefined()
500
+ })
501
+
502
+ it('should skip Base Wallet if name already in Wallet Standard wallets', async () => {
503
+ const { getWallets } = await import('@wallet-standard/app')
504
+ const Comlink = await import('comlink')
505
+ const { BridgeParent } = await import('./BridgeParent')
506
+
507
+ const existingWallet = {
508
+ name: 'Base Wallet',
509
+ chains: ['solana:mainnet'],
510
+ features: { 'standard:connect': {} },
511
+ accounts: []
512
+ }
513
+
514
+ vi.mocked(getWallets).mockReturnValue({
515
+ get: () => [existingWallet] as any,
516
+ on: vi.fn(),
517
+ register: vi.fn()
518
+ } as any)
519
+
520
+ baseAdapterMock.readyState = 'Installed'
521
+
522
+ new BridgeParent(mockIframe)
523
+ await vi.advanceTimersByTimeAsync(200)
524
+
525
+ const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any
526
+ const baseWallets = exposedAPI.walletStandardWallets.filter(
527
+ (w: any) => w.name === 'Base Wallet'
528
+ )
529
+ expect(baseWallets).toHaveLength(1)
530
+ expect(baseWallets[0].features).not.toContain('traditional-adapter')
531
+ })
532
+
533
+ it('should handle BaseWalletAdapter constructor errors gracefully', async () => {
534
+ const Comlink = await import('comlink')
535
+ const { BridgeParent } = await import('./BridgeParent')
536
+
537
+ baseAdapterMock.shouldThrow = true
538
+
539
+ new BridgeParent(mockIframe)
540
+ await vi.advanceTimersByTimeAsync(200)
541
+
542
+ const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any
543
+ expect(exposedAPI).toBeDefined()
544
+ expect(exposedAPI.walletStandardWallets).toEqual([])
545
+ })
546
+ })
547
+
397
548
  describe('destroy', () => {
398
549
  it('should clear all state', async () => {
399
550
  const { BridgeParent } = await import('./BridgeParent')
@@ -1,9 +1,13 @@
1
1
  import * as Comlink from 'comlink'
2
2
  import type { Wallet } from '@wallet-standard/base'
3
3
  import { getWallets } from '@wallet-standard/app'
4
- import { isWalletAdapterCompatibleStandardWallet } from '@solana/wallet-adapter-base'
4
+ import {
5
+ isWalletAdapterCompatibleStandardWallet,
6
+ WalletReadyState
7
+ } from '@solana/wallet-adapter-base'
5
8
  import { Connection, Transaction, VersionedTransaction } from '@solana/web3.js'
6
9
  import { StandardWalletAdapter } from '@solana/wallet-standard-wallet-adapter-base'
10
+ import { BaseWalletAdapter } from './BaseWalletAdapter'
7
11
 
8
12
  export interface EthereumProvider {
9
13
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -73,6 +77,15 @@ export interface TronWalletInfo {
73
77
  provider: any
74
78
  }
75
79
 
80
+ export interface TonWalletInfo {
81
+ uuid: string
82
+ name: string
83
+ icon: string
84
+ jsBridgeKey: string
85
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
+ provider: any
87
+ }
88
+
76
89
  export interface ParentAPI {
77
90
  eip6963Wallets: DetectedWallet[]
78
91
  eip6963WalletsReady: boolean
@@ -83,7 +96,11 @@ export interface ParentAPI {
83
96
  tronWallets: TronWalletInfo[]
84
97
  tronWalletsReady: boolean
85
98
 
99
+ tonWallets: TonWalletInfo[]
100
+ tonWalletsReady: boolean
101
+
86
102
  discoverTronWallets: (injectedIds: string[]) => void
103
+ discoverTonWallets: (jsBridgeKeys: string[]) => void
87
104
  }
88
105
 
89
106
  export class BridgeParent {
@@ -118,12 +135,23 @@ export class BridgeParent {
118
135
  tronWallets: [],
119
136
  tronWalletsReady: false,
120
137
 
138
+ tonWallets: [],
139
+ tonWalletsReady: false,
140
+
121
141
  // Called by BridgeChild to trigger Tron wallet discovery with injectedIds
122
142
  discoverTronWallets: (injectedIds: string[]) => {
123
143
  if (this.parentAPI) {
124
144
  this.parentAPI.tronWallets = this.getTronWallets(injectedIds)
125
145
  this.parentAPI.tronWalletsReady = true
126
146
  }
147
+ },
148
+
149
+ // Called by BridgeChild to trigger TON wallet discovery with jsBridgeKeys
150
+ discoverTonWallets: (jsBridgeKeys: string[]) => {
151
+ if (this.parentAPI) {
152
+ this.parentAPI.tonWallets = this.getTonWallets(jsBridgeKeys)
153
+ this.parentAPI.tonWalletsReady = true
154
+ }
127
155
  }
128
156
  }
129
157
 
@@ -375,6 +403,170 @@ export class BridgeParent {
375
403
  walletNames.add(wallet.name)
376
404
  }
377
405
  }
406
+
407
+ // Add Base Wallet via traditional adapter (detects window.coinbaseSolana)
408
+ try {
409
+ const baseAdapter = new BaseWalletAdapter()
410
+ const isDetected =
411
+ baseAdapter.readyState === WalletReadyState.Installed ||
412
+ baseAdapter.readyState === WalletReadyState.Loadable
413
+
414
+ if (isDetected && !walletNames.has(baseAdapter.name)) {
415
+ solanaWallets.push({
416
+ uuid: `${baseAdapter.name}-traditional`
417
+ .toLowerCase()
418
+ .replace(/\s+/g, '-'),
419
+ name: baseAdapter.name,
420
+ chains: ['solana:mainnet'],
421
+ features: ['traditional-adapter'],
422
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
423
+ // @ts-ignore
424
+ adapter: {
425
+ name: baseAdapter.name,
426
+ url: baseAdapter.url,
427
+ // @ts-expect-error icon is string but type expects template literal
428
+ icon: baseAdapter.icon,
429
+ readyState: baseAdapter.readyState,
430
+ // @ts-expect-error publicKey is a Comlink proxy, not a real PublicKey
431
+ publicKey: Comlink.proxy({
432
+ get value() {
433
+ return baseAdapter.publicKey
434
+ },
435
+ toBase58: async () => {
436
+ return baseAdapter.publicKey?.toBase58()
437
+ },
438
+ toJSON: async () => {
439
+ return baseAdapter.publicKey?.toJSON()
440
+ },
441
+ toBytes: async () => {
442
+ return baseAdapter.publicKey?.toBytes()
443
+ },
444
+ toBuffer: async () => {
445
+ return baseAdapter.publicKey?.toBuffer()
446
+ },
447
+ toString: async () => {
448
+ return baseAdapter.publicKey?.toString()
449
+ }
450
+ }),
451
+ connecting: baseAdapter.connecting,
452
+ connected: baseAdapter.connected,
453
+ supportedTransactionVersions:
454
+ baseAdapter.supportedTransactionVersions,
455
+ connect: async () => await baseAdapter.connect(),
456
+ disconnect: async () => await baseAdapter.disconnect(),
457
+ sendTransaction: async (transaction, connection, options) => {
458
+ try {
459
+ return await baseAdapter.sendTransaction(
460
+ transaction,
461
+ connection,
462
+ options
463
+ )
464
+ } catch {
465
+ throw new Error('Failed to send transaction')
466
+ }
467
+ },
468
+ signTransaction: async transaction => {
469
+ return await baseAdapter.signTransaction(transaction)
470
+ },
471
+ signAllTransactions: async transactions => {
472
+ return await baseAdapter.signAllTransactions(transactions)
473
+ },
474
+ signMessage: async message => {
475
+ return await baseAdapter.signMessage(message)
476
+ },
477
+ customFunctions: [
478
+ 'sendSerializedTransaction',
479
+ 'signAllSerializedTransactions',
480
+ 'signSerializedTransaction'
481
+ ],
482
+ sendSerializedTransaction: async (
483
+ transaction: ArrayBufferLike,
484
+ rpcUrl: string
485
+ ) => {
486
+ const connection = new Connection(rpcUrl)
487
+ const uint8Array = new Uint8Array(transaction)
488
+
489
+ let deserializedTx: Transaction | VersionedTransaction
490
+ try {
491
+ deserializedTx = VersionedTransaction.deserialize(uint8Array)
492
+ } catch {
493
+ try {
494
+ deserializedTx = Transaction.from(uint8Array)
495
+ } catch {
496
+ throw new Error(
497
+ 'Failed to deserialize transaction as either versioned or legacy format'
498
+ )
499
+ }
500
+ }
501
+ try {
502
+ const txHash = await baseAdapter.sendTransaction(
503
+ deserializedTx,
504
+ connection
505
+ )
506
+ return txHash
507
+ } catch {
508
+ throw new Error('Failed to send serialized transaction')
509
+ }
510
+ },
511
+ // @ts-expect-error return type includes VersionedTransaction
512
+ signSerializedTransaction: async (transaction: ArrayBufferLike) => {
513
+ const uint8Array = new Uint8Array(transaction)
514
+
515
+ let deserializedTx: Transaction | VersionedTransaction
516
+ try {
517
+ deserializedTx = VersionedTransaction.deserialize(uint8Array)
518
+ } catch {
519
+ try {
520
+ deserializedTx = Transaction.from(uint8Array)
521
+ } catch {
522
+ throw new Error(
523
+ 'Failed to deserialize transaction as either versioned or legacy format'
524
+ )
525
+ }
526
+ }
527
+
528
+ return await baseAdapter.signTransaction(deserializedTx)
529
+ },
530
+ // @ts-expect-error return type includes VersionedTransaction
531
+ signAllSerializedTransactions: async (
532
+ transactions: ArrayBufferLike[]
533
+ ) => {
534
+ const deserializedTransactions: (
535
+ | Transaction
536
+ | VersionedTransaction
537
+ )[] = []
538
+
539
+ for (const transaction of transactions) {
540
+ const uint8Array = new Uint8Array(transaction)
541
+
542
+ let deserializedTx: Transaction | VersionedTransaction
543
+ try {
544
+ deserializedTx = VersionedTransaction.deserialize(uint8Array)
545
+ } catch {
546
+ try {
547
+ deserializedTx = Transaction.from(uint8Array)
548
+ } catch {
549
+ throw new Error(
550
+ 'Failed to deserialize one or more transactions'
551
+ )
552
+ }
553
+ }
554
+
555
+ deserializedTransactions.push(deserializedTx)
556
+ }
557
+
558
+ return await baseAdapter.signAllTransactions(
559
+ deserializedTransactions
560
+ )
561
+ }
562
+ }
563
+ })
564
+ walletNames.add(baseAdapter.name)
565
+ }
566
+ } catch {
567
+ // Silently handle if Base Wallet adapter fails to initialize
568
+ }
569
+
378
570
  return solanaWallets
379
571
  }
380
572
 
@@ -485,11 +677,67 @@ export class BridgeParent {
485
677
  return wallets
486
678
  }
487
679
 
680
+ private isTonConnectBridge(value: unknown): boolean {
681
+ if (!value || typeof value !== 'object') return false
682
+ const bridge = value as Record<string, unknown>
683
+ return (
684
+ typeof bridge['connect'] === 'function' &&
685
+ typeof bridge['send'] === 'function' &&
686
+ typeof bridge['listen'] === 'function'
687
+ )
688
+ }
689
+
690
+ private getTonWallets(jsBridgeKeys: string[]): TonWalletInfo[] {
691
+ if (typeof window === 'undefined' || jsBridgeKeys.length === 0) {
692
+ return []
693
+ }
694
+
695
+ const wallets: TonWalletInfo[] = []
696
+ const seen = new Set<string>()
697
+
698
+ for (const key of jsBridgeKeys) {
699
+ if (seen.has(key)) continue
700
+
701
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
702
+ const root = (window as any)[key]
703
+ if (!root || typeof root !== 'object') continue
704
+
705
+ const bridge = root['tonconnect']
706
+ if (!this.isTonConnectBridge(bridge)) continue
707
+
708
+ seen.add(key)
709
+
710
+ // Explicitly whitelist bridge methods to bound cross-origin surface
711
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
712
+ const actualBridge = bridge as any
713
+ wallets.push({
714
+ uuid: `ton-${key}`.toLowerCase(),
715
+ name: actualBridge.deviceInfo?.appName || key,
716
+ icon: '',
717
+ jsBridgeKey: key,
718
+ provider: Comlink.proxy({
719
+ deviceInfo: actualBridge.deviceInfo,
720
+ connect: async (protocolVersion: number, message: string) =>
721
+ await actualBridge.connect(protocolVersion, message),
722
+ send: async (message: string) => await actualBridge.send(message),
723
+ listen: (callback: (event: string) => void) =>
724
+ actualBridge.listen(callback),
725
+ disconnect: actualBridge.disconnect
726
+ ? async () => await actualBridge.disconnect()
727
+ : undefined
728
+ })
729
+ })
730
+ }
731
+
732
+ return wallets
733
+ }
734
+
488
735
  public destroy(): void {
489
736
  if (this.parentAPI) {
490
737
  this.parentAPI.eip6963Wallets = []
491
738
  this.parentAPI.walletStandardWallets = []
492
739
  this.parentAPI.tronWallets = []
740
+ this.parentAPI.tonWallets = []
493
741
  this.parentAPI = null
494
742
  }
495
743
  this.endpoint = null