@meshconnect/uwc-bridge-parent 1.1.1 → 1.2.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.
@@ -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
@@ -375,6 +379,170 @@ export class BridgeParent {
375
379
  walletNames.add(wallet.name)
376
380
  }
377
381
  }
382
+
383
+ // Add Base Wallet via traditional adapter (detects window.coinbaseSolana)
384
+ try {
385
+ const baseAdapter = new BaseWalletAdapter()
386
+ const isDetected =
387
+ baseAdapter.readyState === WalletReadyState.Installed ||
388
+ baseAdapter.readyState === WalletReadyState.Loadable
389
+
390
+ if (isDetected && !walletNames.has(baseAdapter.name)) {
391
+ solanaWallets.push({
392
+ uuid: `${baseAdapter.name}-traditional`
393
+ .toLowerCase()
394
+ .replace(/\s+/g, '-'),
395
+ name: baseAdapter.name,
396
+ chains: ['solana:mainnet'],
397
+ features: ['traditional-adapter'],
398
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
399
+ // @ts-ignore
400
+ adapter: {
401
+ name: baseAdapter.name,
402
+ url: baseAdapter.url,
403
+ // @ts-expect-error icon is string but type expects template literal
404
+ icon: baseAdapter.icon,
405
+ readyState: baseAdapter.readyState,
406
+ // @ts-expect-error publicKey is a Comlink proxy, not a real PublicKey
407
+ publicKey: Comlink.proxy({
408
+ get value() {
409
+ return baseAdapter.publicKey
410
+ },
411
+ toBase58: async () => {
412
+ return baseAdapter.publicKey?.toBase58()
413
+ },
414
+ toJSON: async () => {
415
+ return baseAdapter.publicKey?.toJSON()
416
+ },
417
+ toBytes: async () => {
418
+ return baseAdapter.publicKey?.toBytes()
419
+ },
420
+ toBuffer: async () => {
421
+ return baseAdapter.publicKey?.toBuffer()
422
+ },
423
+ toString: async () => {
424
+ return baseAdapter.publicKey?.toString()
425
+ }
426
+ }),
427
+ connecting: baseAdapter.connecting,
428
+ connected: baseAdapter.connected,
429
+ supportedTransactionVersions:
430
+ baseAdapter.supportedTransactionVersions,
431
+ connect: async () => await baseAdapter.connect(),
432
+ disconnect: async () => await baseAdapter.disconnect(),
433
+ sendTransaction: async (transaction, connection, options) => {
434
+ try {
435
+ return await baseAdapter.sendTransaction(
436
+ transaction,
437
+ connection,
438
+ options
439
+ )
440
+ } catch {
441
+ throw new Error('Failed to send transaction')
442
+ }
443
+ },
444
+ signTransaction: async transaction => {
445
+ return await baseAdapter.signTransaction(transaction)
446
+ },
447
+ signAllTransactions: async transactions => {
448
+ return await baseAdapter.signAllTransactions(transactions)
449
+ },
450
+ signMessage: async message => {
451
+ return await baseAdapter.signMessage(message)
452
+ },
453
+ customFunctions: [
454
+ 'sendSerializedTransaction',
455
+ 'signAllSerializedTransactions',
456
+ 'signSerializedTransaction'
457
+ ],
458
+ sendSerializedTransaction: async (
459
+ transaction: ArrayBufferLike,
460
+ rpcUrl: string
461
+ ) => {
462
+ const connection = new Connection(rpcUrl)
463
+ const uint8Array = new Uint8Array(transaction)
464
+
465
+ let deserializedTx: Transaction | VersionedTransaction
466
+ try {
467
+ deserializedTx = VersionedTransaction.deserialize(uint8Array)
468
+ } catch {
469
+ try {
470
+ deserializedTx = Transaction.from(uint8Array)
471
+ } catch {
472
+ throw new Error(
473
+ 'Failed to deserialize transaction as either versioned or legacy format'
474
+ )
475
+ }
476
+ }
477
+ try {
478
+ const txHash = await baseAdapter.sendTransaction(
479
+ deserializedTx,
480
+ connection
481
+ )
482
+ return txHash
483
+ } catch {
484
+ throw new Error('Failed to send serialized transaction')
485
+ }
486
+ },
487
+ // @ts-expect-error return type includes VersionedTransaction
488
+ signSerializedTransaction: async (transaction: ArrayBufferLike) => {
489
+ const uint8Array = new Uint8Array(transaction)
490
+
491
+ let deserializedTx: Transaction | VersionedTransaction
492
+ try {
493
+ deserializedTx = VersionedTransaction.deserialize(uint8Array)
494
+ } catch {
495
+ try {
496
+ deserializedTx = Transaction.from(uint8Array)
497
+ } catch {
498
+ throw new Error(
499
+ 'Failed to deserialize transaction as either versioned or legacy format'
500
+ )
501
+ }
502
+ }
503
+
504
+ return await baseAdapter.signTransaction(deserializedTx)
505
+ },
506
+ // @ts-expect-error return type includes VersionedTransaction
507
+ signAllSerializedTransactions: async (
508
+ transactions: ArrayBufferLike[]
509
+ ) => {
510
+ const deserializedTransactions: (
511
+ | Transaction
512
+ | VersionedTransaction
513
+ )[] = []
514
+
515
+ for (const transaction of transactions) {
516
+ const uint8Array = new Uint8Array(transaction)
517
+
518
+ let deserializedTx: Transaction | VersionedTransaction
519
+ try {
520
+ deserializedTx = VersionedTransaction.deserialize(uint8Array)
521
+ } catch {
522
+ try {
523
+ deserializedTx = Transaction.from(uint8Array)
524
+ } catch {
525
+ throw new Error(
526
+ 'Failed to deserialize one or more transactions'
527
+ )
528
+ }
529
+ }
530
+
531
+ deserializedTransactions.push(deserializedTx)
532
+ }
533
+
534
+ return await baseAdapter.signAllTransactions(
535
+ deserializedTransactions
536
+ )
537
+ }
538
+ }
539
+ })
540
+ walletNames.add(baseAdapter.name)
541
+ }
542
+ } catch {
543
+ // Silently handle if Base Wallet adapter fails to initialize
544
+ }
545
+
378
546
  return solanaWallets
379
547
  }
380
548