@meshconnect/uwc-bridge-parent 1.5.0 → 1.6.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.
@@ -276,8 +276,10 @@ describe('BridgeParent', () => {
276
276
  await vi.advanceTimersByTimeAsync(400)
277
277
 
278
278
  const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any
279
- // Solana readiness is true right after init — a real precondition, not a setup.
279
+ // Solana/Bitcoin readiness is true right after init — a real
280
+ // precondition, not a setup.
280
281
  expect(exposedAPI.walletStandardWalletsReady).toBe(true)
282
+ expect(exposedAPI.bip122WalletsReady).toBe(true)
281
283
  exposedAPI.tronWalletsReady = true
282
284
  exposedAPI.tonWalletsReady = true
283
285
  exposedAPI.eip6963WalletsReady = true
@@ -288,6 +290,37 @@ describe('BridgeParent', () => {
288
290
  expect(exposedAPI.tonWalletsReady).toBe(false)
289
291
  expect(exposedAPI.eip6963WalletsReady).toBe(false)
290
292
  expect(exposedAPI.walletStandardWalletsReady).toBe(false)
293
+ expect(exposedAPI.bip122WalletsReady).toBe(false)
294
+ })
295
+
296
+ it('resets bip122Wallets to an empty array on destroy', async () => {
297
+ const { getWallets } = await import('@wallet-standard/app')
298
+ const Comlink = await import('comlink')
299
+ const { BridgeParent } = await import('./BridgeParent')
300
+
301
+ vi.mocked(getWallets).mockReturnValue({
302
+ get: () =>
303
+ [
304
+ {
305
+ name: 'MetaMask',
306
+ chains: ['bitcoin:mainnet'],
307
+ features: { 'bitcoin:connect': { connect: vi.fn() } },
308
+ accounts: []
309
+ }
310
+ ] as any,
311
+ on: vi.fn(),
312
+ register: vi.fn()
313
+ } as any)
314
+
315
+ const bridge = new BridgeParent(mockIframe)
316
+ await vi.advanceTimersByTimeAsync(200)
317
+
318
+ const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any
319
+ expect(exposedAPI.bip122Wallets).toHaveLength(1)
320
+
321
+ bridge.destroy()
322
+
323
+ expect(exposedAPI.bip122Wallets).toEqual([])
291
324
  })
292
325
 
293
326
  it('is a safe no-op when the iframe never had a contentWindow', async () => {
@@ -443,6 +476,287 @@ describe('BridgeParent', () => {
443
476
  })
444
477
  })
445
478
 
479
+ describe('Bitcoin wallet discovery', () => {
480
+ it('should call getWallets and filter bitcoin:* wallets', async () => {
481
+ const { getWallets } = await import('@wallet-standard/app')
482
+ const Comlink = await import('comlink')
483
+ const { BridgeParent } = await import('./BridgeParent')
484
+
485
+ const mockBitcoinWallet = {
486
+ name: 'MetaMask',
487
+ chains: ['bitcoin:mainnet'],
488
+ features: {
489
+ 'bitcoin:connect': { connect: vi.fn() },
490
+ 'bitcoin:signMessage': { signMessage: vi.fn() },
491
+ 'bitcoin:signTransaction': { signTransaction: vi.fn() }
492
+ },
493
+ accounts: []
494
+ }
495
+ const mockNonBitcoinWallet = {
496
+ name: 'SomeWallet',
497
+ chains: ['ethereum:1'],
498
+ features: {},
499
+ accounts: []
500
+ }
501
+
502
+ vi.mocked(getWallets).mockReturnValue({
503
+ get: () => [mockBitcoinWallet, mockNonBitcoinWallet] as any,
504
+ on: vi.fn(),
505
+ register: vi.fn()
506
+ } as any)
507
+
508
+ new BridgeParent(mockIframe)
509
+ await vi.advanceTimersByTimeAsync(200)
510
+
511
+ const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any
512
+ expect(exposedAPI.bip122Wallets).toHaveLength(1)
513
+ expect(exposedAPI.bip122Wallets[0].name).toBe('MetaMask')
514
+ expect(exposedAPI.bip122Wallets[0].features).toEqual([
515
+ 'bitcoin:connect',
516
+ 'bitcoin:signMessage',
517
+ 'bitcoin:signTransaction'
518
+ ])
519
+ expect(exposedAPI.bip122Wallets[0].wallet.accounts).toEqual([])
520
+ expect(
521
+ exposedAPI.bip122Wallets[0].wallet.features['bitcoin:connect']
522
+ ).toBeDefined()
523
+ expect(exposedAPI.bip122WalletsReady).toBe(true)
524
+ })
525
+
526
+ it('keys the uuid on the bitcoin: chain, not chains[0], for a multi-chain wallet', async () => {
527
+ const { getWallets } = await import('@wallet-standard/app')
528
+ const Comlink = await import('comlink')
529
+ const { BridgeParent } = await import('./BridgeParent')
530
+
531
+ // MetaMask-shaped: eip155 chains registered first, bitcoin: chain last.
532
+ const mockMultiChainWallet = {
533
+ name: 'MetaMask',
534
+ chains: ['eip155:1', 'eip155:137', 'bitcoin:mainnet'],
535
+ features: {
536
+ 'bitcoin:connect': { connect: vi.fn() },
537
+ 'bitcoin:signMessage': { signMessage: vi.fn() },
538
+ 'bitcoin:signTransaction': { signTransaction: vi.fn() }
539
+ },
540
+ accounts: []
541
+ }
542
+
543
+ vi.mocked(getWallets).mockReturnValue({
544
+ get: () => [mockMultiChainWallet] as any,
545
+ on: vi.fn(),
546
+ register: vi.fn()
547
+ } as any)
548
+
549
+ new BridgeParent(mockIframe)
550
+ await vi.advanceTimersByTimeAsync(200)
551
+
552
+ const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any
553
+ expect(exposedAPI.bip122Wallets).toHaveLength(1)
554
+ // Must key on 'bitcoin:mainnet' (bitcoinChain), not 'eip155:1'
555
+ // (chains[0]) — otherwise this uuid disagrees with the local
556
+ // (unframed) discovery path's uuid for the exact same wallet.
557
+ expect(exposedAPI.bip122Wallets[0].uuid).toBe('metamask-bitcoin:mainnet')
558
+ })
559
+
560
+ it('should return empty array when no bitcoin wallets found', async () => {
561
+ const { getWallets } = await import('@wallet-standard/app')
562
+ const Comlink = await import('comlink')
563
+ const { BridgeParent } = await import('./BridgeParent')
564
+
565
+ vi.mocked(getWallets).mockReturnValue({
566
+ get: () => [],
567
+ on: vi.fn(),
568
+ register: vi.fn()
569
+ } as any)
570
+
571
+ new BridgeParent(mockIframe)
572
+ await vi.advanceTimersByTimeAsync(200)
573
+
574
+ const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any
575
+ expect(exposedAPI.bip122Wallets).toEqual([])
576
+ })
577
+
578
+ it('omits a feature entry the wallet does not declare', async () => {
579
+ const { getWallets } = await import('@wallet-standard/app')
580
+ const Comlink = await import('comlink')
581
+ const { BridgeParent } = await import('./BridgeParent')
582
+
583
+ const mockWallet = {
584
+ name: 'PartialWallet',
585
+ chains: ['bitcoin:mainnet'],
586
+ features: { 'bitcoin:connect': { connect: vi.fn() } },
587
+ accounts: []
588
+ }
589
+
590
+ vi.mocked(getWallets).mockReturnValue({
591
+ get: () => [mockWallet] as any,
592
+ on: vi.fn(),
593
+ register: vi.fn()
594
+ } as any)
595
+
596
+ new BridgeParent(mockIframe)
597
+ await vi.advanceTimersByTimeAsync(200)
598
+
599
+ const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any
600
+ const wallet = exposedAPI.bip122Wallets[0].wallet
601
+ expect(wallet.features['bitcoin:connect']).toBeDefined()
602
+ expect(wallet.features['bitcoin:signMessage']).toBeUndefined()
603
+ expect(wallet.features['bitcoin:signTransaction']).toBeUndefined()
604
+ })
605
+ })
606
+
607
+ describe('subscribeBip122WalletEvent', () => {
608
+ // Bitcoin has no adapter class to construct (unlike Solana's
609
+ // StandardWalletAdapter) — `getBitcoinWallets()` reshapes the raw
610
+ // registry wallet directly, so the resolver just re-scans the registry
611
+ // for a `bitcoin:*` wallet whose uuid matches. `standard:events` is the
612
+ // ONLY path (no connect/disconnect fallback), since there is no adapter
613
+ // to re-emit them.
614
+ async function armStandardEventsBitcoinWallet(
615
+ initialAccounts: { address: string }[],
616
+ chains: string[] = ['bitcoin:mainnet']
617
+ ) {
618
+ const { getWallets } = await import('@wallet-standard/app')
619
+
620
+ let changeListener: ((props: unknown) => void) | undefined
621
+ const off = vi.fn()
622
+ const wallet = {
623
+ name: 'MetaMask',
624
+ chains,
625
+ accounts: initialAccounts,
626
+ features: {
627
+ 'bitcoin:connect': { connect: vi.fn() },
628
+ 'standard:events': {
629
+ on: vi.fn((event: string, listener: (props: unknown) => void) => {
630
+ if (event === 'change') changeListener = listener
631
+ return off
632
+ })
633
+ }
634
+ }
635
+ }
636
+
637
+ vi.mocked(getWallets).mockReturnValue({
638
+ get: () => [wallet] as any,
639
+ on: vi.fn(),
640
+ register: vi.fn()
641
+ } as any)
642
+
643
+ const { BridgeParent } = await import('./BridgeParent')
644
+ const bridgeParent = new BridgeParent(mockIframe)
645
+ await vi.advanceTimersByTimeAsync(400)
646
+
647
+ const callback = vi.fn()
648
+ const walletId = `metamask-${chains.find(c => c.startsWith('bitcoin:')) ?? chains[0]}`
649
+ const teardown = (
650
+ bridgeParent as any
651
+ ).parentAPI.subscribeBip122WalletEvent(walletId, callback)
652
+
653
+ return {
654
+ wallet,
655
+ off,
656
+ callback,
657
+ teardown,
658
+ fire: (props: unknown) => changeListener?.(props)
659
+ }
660
+ }
661
+
662
+ it('throws when no wallet matches the given walletId', async () => {
663
+ const { getWallets } = await import('@wallet-standard/app')
664
+ vi.mocked(getWallets).mockReturnValue({
665
+ get: () => [],
666
+ on: vi.fn(),
667
+ register: vi.fn()
668
+ } as any)
669
+ const { BridgeParent } = await import('./BridgeParent')
670
+ const bridgeParent = new BridgeParent(mockIframe)
671
+ await vi.advanceTimersByTimeAsync(400)
672
+
673
+ expect(() =>
674
+ (bridgeParent as any).parentAPI.subscribeBip122WalletEvent(
675
+ 'unknown-wallet-id',
676
+ vi.fn()
677
+ )
678
+ ).toThrow(/no wallet found for walletId/)
679
+ })
680
+
681
+ it('returns undefined when the resolved wallet exposes no standard:events', async () => {
682
+ const { getWallets } = await import('@wallet-standard/app')
683
+ vi.mocked(getWallets).mockReturnValue({
684
+ get: () =>
685
+ [
686
+ {
687
+ name: 'MetaMask',
688
+ chains: ['bitcoin:mainnet'],
689
+ accounts: [],
690
+ features: { 'bitcoin:connect': { connect: vi.fn() } }
691
+ }
692
+ ] as any,
693
+ on: vi.fn(),
694
+ register: vi.fn()
695
+ } as any)
696
+ const { BridgeParent } = await import('./BridgeParent')
697
+ const bridgeParent = new BridgeParent(mockIframe)
698
+ await vi.advanceTimersByTimeAsync(400)
699
+
700
+ const teardown = (
701
+ bridgeParent as any
702
+ ).parentAPI.subscribeBip122WalletEvent(
703
+ 'metamask-bitcoin:mainnet',
704
+ vi.fn()
705
+ )
706
+ expect(teardown).toBeUndefined()
707
+ })
708
+
709
+ it('resolves a multi-chain wallet (eip155 first) by its bitcoin:-keyed walletId, not chains[0]', async () => {
710
+ const { teardown } = await armStandardEventsBitcoinWallet(
711
+ [{ address: 'bc1qexisting' }],
712
+ ['eip155:1', 'eip155:137', 'bitcoin:mainnet']
713
+ )
714
+ expect(typeof teardown).toBe('function')
715
+ })
716
+
717
+ it('subscribes to standard:events change and forwards the active address as a plain string', async () => {
718
+ const { wallet, teardown, off } = await armStandardEventsBitcoinWallet([
719
+ { address: 'bc1qoriginal' }
720
+ ])
721
+
722
+ expect(wallet.features['standard:events'].on).toHaveBeenCalledWith(
723
+ 'change',
724
+ expect.any(Function)
725
+ )
726
+ expect(typeof teardown).toBe('function')
727
+ expect(Comlink.proxy).toHaveBeenCalledWith(expect.any(Function))
728
+ expect(off).not.toHaveBeenCalled()
729
+ })
730
+
731
+ it('propagates a switch, then null on de-auth, and ignores non-account changes', async () => {
732
+ const { wallet, callback, off, teardown, fire } =
733
+ await armStandardEventsBitcoinWallet([{ address: 'bc1qACC1' }])
734
+
735
+ wallet.accounts = [{ address: 'bc1qACC2' }]
736
+ fire({ accounts: wallet.accounts })
737
+ expect(callback).toHaveBeenNthCalledWith(1, 'bc1qACC2')
738
+
739
+ // A pure chain/feature change (no `accounts` key) must be ignored.
740
+ fire({ chains: ['bitcoin:mainnet'] })
741
+ expect(callback).toHaveBeenCalledTimes(1)
742
+
743
+ wallet.accounts = []
744
+ fire({ accounts: [] })
745
+ expect(callback).toHaveBeenNthCalledWith(2, null)
746
+
747
+ teardown?.()
748
+ expect(off).toHaveBeenCalledTimes(1)
749
+ })
750
+
751
+ it('never lets the wallet-standard unsubscribe return value escape the teardown (unserializable-return-value guard)', async () => {
752
+ const { off, teardown } = await armStandardEventsBitcoinWallet([
753
+ { address: 'bc1qACC1' }
754
+ ])
755
+ off.mockReturnValue({ some: 'non-cloneable-emitter-return' })
756
+ expect(() => teardown?.()).not.toThrow()
757
+ })
758
+ })
759
+
446
760
  describe('Tron wallet discovery', () => {
447
761
  it('should start with empty tron wallets and not ready', async () => {
448
762
  const Comlink = await import('comlink')
@@ -100,6 +100,44 @@ export interface WalletStandardInfo {
100
100
  adapter: ExtendedStandardWalletAdapter | undefined
101
101
  }
102
102
 
103
+ interface Bip122ConnectFeature {
104
+ connect: (input: unknown) => Promise<unknown>
105
+ }
106
+
107
+ interface Bip122SignMessageFeature {
108
+ signMessage: (input: unknown) => Promise<unknown>
109
+ }
110
+
111
+ interface Bip122SignTransactionFeature {
112
+ signTransaction: (input: unknown) => Promise<unknown>
113
+ }
114
+
115
+ /**
116
+ * Wallet Standard `bitcoin:*` wallet, reshaped into plain data (`name`,
117
+ * `accounts`) plus Comlink-proxied feature methods so it survives the
118
+ * postMessage bridge — mirrors `WalletStandardBitcoinWallet`
119
+ * (`@meshconnect/uwc-bitcoin`), duck-typed here rather than imported to avoid
120
+ * a new cross-package dependency (same approach as the Tron/TON provider
121
+ * shapes below).
122
+ */
123
+ export interface Bip122WireWallet {
124
+ name: string
125
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
126
+ accounts: any[]
127
+ features: {
128
+ 'bitcoin:connect'?: Bip122ConnectFeature | undefined
129
+ 'bitcoin:signMessage'?: Bip122SignMessageFeature | undefined
130
+ 'bitcoin:signTransaction'?: Bip122SignTransactionFeature | undefined
131
+ }
132
+ }
133
+
134
+ export interface Bip122WalletInfo {
135
+ uuid: string
136
+ name: string
137
+ features: string[]
138
+ wallet: Bip122WireWallet
139
+ }
140
+
103
141
  export interface EIP6963ProviderInfo {
104
142
  uuid: string
105
143
  name: string
@@ -149,6 +187,9 @@ export interface ParentAPI {
149
187
  walletStandardWallets: WalletStandardInfo[]
150
188
  walletStandardWalletsReady: boolean
151
189
 
190
+ bip122Wallets: Bip122WalletInfo[]
191
+ bip122WalletsReady: boolean
192
+
152
193
  tronWallets: TronWalletInfo[]
153
194
  tronWalletsReady: boolean
154
195
 
@@ -210,6 +251,24 @@ export interface ParentAPI {
210
251
  walletId: string,
211
252
  callback: (address: string | null) => void
212
253
  ) => (() => void) | undefined
254
+
255
+ /**
256
+ * Bitcoin peer of `subscribeSolanaWalletEvent`. `walletId` is the SAME id
257
+ * `bip122Wallets[].uuid` publishes (see `generateBip122WalletId`), which the
258
+ * child mirrors back as `BitcoinWalletService.connectedWalletUuid`. Bitcoin
259
+ * has no adapter class re-emitting `connect`/`disconnect` on a switch (the
260
+ * way `StandardWalletAdapter` does for Solana) — `standard:events` is the
261
+ * ONLY path, not a preferred one with a fallback. Forwards a plain
262
+ * `string | null` (never the raw account object) to `callback`, a
263
+ * Comlink-proxied callback the child supplies. Throws when no wallet
264
+ * matches `walletId` (reported as a bridge error on the child side,
265
+ * mirroring `subscribeSolanaWalletEvent`'s walletId-miss); returns
266
+ * `undefined` when the resolved wallet exposes no `standard:events`.
267
+ */
268
+ subscribeBip122WalletEvent: (
269
+ walletId: string,
270
+ callback: (address: string | null) => void
271
+ ) => (() => void) | undefined
213
272
  }
214
273
 
215
274
  export class BridgeParent {
@@ -252,14 +311,18 @@ export class BridgeParent {
252
311
  // flags gate the child from reading a still-empty array.
253
312
  const eip6963Promise = this.discovereip6963Wallets()
254
313
  this.eip6963DiscoveryPromise = eip6963Promise
255
- // Solana is synchronous, so run it inline (no benefit to a microtask hop)
314
+ // Solana and Bitcoin are both synchronous Wallet Standard registry reads,
315
+ // so run them inline (no benefit to a microtask hop).
256
316
  const walletStandardWallets = this.getSolanaWallets()
317
+ const bip122Wallets = this.getBitcoinWallets()
257
318
 
258
319
  this.parentAPI = {
259
320
  eip6963Wallets: [],
260
321
  eip6963WalletsReady: false,
261
322
  walletStandardWallets: walletStandardWallets,
262
323
  walletStandardWalletsReady: true,
324
+ bip122Wallets: bip122Wallets,
325
+ bip122WalletsReady: true,
263
326
  tronWallets: [],
264
327
  tronWalletsReady: false,
265
328
 
@@ -491,6 +554,49 @@ export class BridgeParent {
491
554
  adapter.off('connect', onConnect)
492
555
  adapter.off('disconnect', onDisconnect)
493
556
  })
557
+ },
558
+ subscribeBip122WalletEvent: (
559
+ walletId: string,
560
+ callback: (address: string | null) => void
561
+ ): (() => void) | undefined => {
562
+ const wallet = this.resolveBip122WalletForEvent(walletId)
563
+ if (!wallet) {
564
+ // Same rationale as subscribeSolanaWalletEvent's walletId miss —
565
+ // surface a genuine "nothing at this walletId" miss as a reported
566
+ // bridge error instead of a silent no-op.
567
+ throw new Error(
568
+ `subscribeBip122WalletEvent: no wallet found for walletId "${walletId}"`
569
+ )
570
+ }
571
+ // ONLY path — Bitcoin has no adapter class to re-emit connect/disconnect
572
+ // on a switch the way StandardWalletAdapter does for Solana, so there is
573
+ // no fallback branch here: standard:events is the sole account-change
574
+ // signal a bitcoin:* wallet exposes.
575
+ const standardWallet = wallet as unknown as StandardEventsCapableWallet
576
+ const standardEvents = standardWallet.features?.['standard:events']
577
+ if (typeof standardEvents?.on !== 'function') return undefined
578
+
579
+ // Reduce to a plain string | null BEFORE crossing Comlink — the raw
580
+ // account object isn't structured-cloneable and the child only needs
581
+ // the address. null = the switch de-authorized the dapp → the child
582
+ // maps it to [] (disconnect), a non-null address maps to [address].
583
+ const forwardActiveAccount = () => {
584
+ callback(standardWallet.accounts?.[0]?.address ?? null)
585
+ }
586
+ const off = standardEvents.on('change', properties => {
587
+ // React only to account-set changes; ignore pure chain/feature deltas.
588
+ if (properties && 'accounts' in properties) forwardActiveAccount()
589
+ })
590
+ // Block body (not an expression arrow): the wallet-standard unsubscribe
591
+ // returns its own value, which must not become this teardown's
592
+ // (unserializable) return across Comlink. Best-effort + idempotent.
593
+ return Comlink.proxy(() => {
594
+ try {
595
+ off?.()
596
+ } catch {
597
+ // Unsubscribe must never throw across the bridge.
598
+ }
599
+ })
494
600
  }
495
601
  }
496
602
 
@@ -1018,11 +1124,113 @@ export class BridgeParent {
1018
1124
  return hasSolanaChain
1019
1125
  }
1020
1126
 
1127
+ private isBitcoinWallet(wallet: Wallet): boolean {
1128
+ const chains = wallet.chains || []
1129
+ return chains.some(chain => chain.startsWith('bitcoin:'))
1130
+ }
1131
+
1132
+ /**
1133
+ * Wallet Standard `bitcoin:*` discovery — the Bitcoin peer of
1134
+ * `getSolanaWallets`. Reshapes each matching registry wallet into plain
1135
+ * data (`accounts`) plus individually Comlink-proxied feature methods
1136
+ * (`bitcoin:connect`/`signMessage`/`signTransaction`), each left `undefined`
1137
+ * when the real wallet doesn't declare it — so `WalletStandardBitcoinAdapter`'s
1138
+ * own presence checks (`if (!feature) throw ...`) still work once marshaled
1139
+ * across the bridge.
1140
+ */
1141
+ private getBitcoinWallets(): Bip122WalletInfo[] {
1142
+ const { get } = getWallets()
1143
+ const wallets = get()
1144
+
1145
+ const bitcoinWallets: Bip122WalletInfo[] = []
1146
+
1147
+ for (const wallet of wallets) {
1148
+ if (!this.isBitcoinWallet(wallet)) continue
1149
+
1150
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1151
+ const features = wallet.features as Record<string, any>
1152
+ const connectFeature = features['bitcoin:connect']
1153
+ const signMessageFeature = features['bitcoin:signMessage']
1154
+ const signTransactionFeature = features['bitcoin:signTransaction']
1155
+
1156
+ bitcoinWallets.push({
1157
+ uuid: this.generateBip122WalletId(wallet),
1158
+ name: wallet.name,
1159
+ features: Object.keys(wallet.features),
1160
+ wallet: {
1161
+ name: wallet.name,
1162
+ accounts: wallet.accounts as unknown[],
1163
+ features: {
1164
+ 'bitcoin:connect': connectFeature
1165
+ ? Comlink.proxy({
1166
+ connect: async (input: unknown) =>
1167
+ await connectFeature.connect(input)
1168
+ })
1169
+ : undefined,
1170
+ 'bitcoin:signMessage': signMessageFeature
1171
+ ? Comlink.proxy({
1172
+ signMessage: async (input: unknown) =>
1173
+ await signMessageFeature.signMessage(input)
1174
+ })
1175
+ : undefined,
1176
+ 'bitcoin:signTransaction': signTransactionFeature
1177
+ ? Comlink.proxy({
1178
+ signTransaction: async (input: unknown) =>
1179
+ await signTransactionFeature.signTransaction(input)
1180
+ })
1181
+ : undefined
1182
+ }
1183
+ }
1184
+ })
1185
+ }
1186
+
1187
+ return bitcoinWallets
1188
+ }
1189
+
1021
1190
  private generateWalletId(wallet: Wallet): string {
1022
1191
  const chain = wallet.chains?.[0] || 'unknown'
1023
1192
  return `${wallet.name}-${chain}`.toLowerCase().replace(/\s+/g, '-')
1024
1193
  }
1025
1194
 
1195
+ /**
1196
+ * Bitcoin peer of `generateWalletId`, kept separate rather than shared: it
1197
+ * keys on the wallet's first `bitcoin:` chain, not `chains[0]` — MetaMask's
1198
+ * bitcoin:* wallet is multi-chain (eip155 chains registered first), so
1199
+ * `chains[0]` would key this id off an unrelated namespace. Mirrors the
1200
+ * same fix already applied to the local (unframed) discovery path in
1201
+ * wallet-standard-discovery.ts. Used by both `getBitcoinWallets()` and
1202
+ * `resolveBip122WalletForEvent()` so they agree on the same id.
1203
+ */
1204
+ private generateBip122WalletId(wallet: Wallet): string {
1205
+ const bitcoinChain =
1206
+ wallet.chains?.find(chain => chain.startsWith('bitcoin:')) ||
1207
+ wallet.chains?.[0] ||
1208
+ 'unknown'
1209
+ return `${wallet.name}-${bitcoinChain}`.toLowerCase().replace(/\s+/g, '-')
1210
+ }
1211
+
1212
+ /**
1213
+ * Resolve the REAL registry wallet for `walletId` (the same id
1214
+ * `getBitcoinWallets()` publishes as `Bip122WalletInfo.uuid`) so
1215
+ * `subscribeBip122WalletEvent` can attach a live listener. Unlike Solana,
1216
+ * Bitcoin has no adapter class to construct — `getBitcoinWallets()` already
1217
+ * reshapes the raw registry `Wallet` directly, so this returns that same
1218
+ * raw `Wallet`, which still carries `standard:events` (the reshaped
1219
+ * `Bip122WireWallet` sent across the bridge does not).
1220
+ */
1221
+ private resolveBip122WalletForEvent(walletId: string): Wallet | undefined {
1222
+ const { get } = getWallets()
1223
+ const wallets = get()
1224
+
1225
+ for (const wallet of wallets) {
1226
+ if (!this.isBitcoinWallet(wallet)) continue
1227
+ if (this.generateBip122WalletId(wallet) !== walletId) continue
1228
+ return wallet
1229
+ }
1230
+
1231
+ return undefined
1232
+ }
1233
+
1026
1234
  /**
1027
1235
  * Checks if a value looks like a Tron provider (has request and tronWeb)
1028
1236
  */
@@ -1351,10 +1559,12 @@ export class BridgeParent {
1351
1559
  if (this.parentAPI) {
1352
1560
  this.parentAPI.eip6963WalletsReady = false
1353
1561
  this.parentAPI.walletStandardWalletsReady = false
1562
+ this.parentAPI.bip122WalletsReady = false
1354
1563
  this.parentAPI.tronWalletsReady = false
1355
1564
  this.parentAPI.tonWalletsReady = false
1356
1565
  this.parentAPI.eip6963Wallets = []
1357
1566
  this.parentAPI.walletStandardWallets = []
1567
+ this.parentAPI.bip122Wallets = []
1358
1568
  this.parentAPI.tronWallets = []
1359
1569
  this.parentAPI.tonWallets = []
1360
1570
  this.parentAPI = null