@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.
- package/dist/BridgeParent.d.ts +79 -0
- package/dist/BridgeParent.d.ts.map +1 -1
- package/dist/BridgeParent.js +137 -1
- package/dist/BridgeParent.js.map +1 -1
- package/dist/BridgeParent.test.js +246 -1
- package/dist/BridgeParent.test.js.map +1 -1
- package/package.json +4 -4
- package/src/BridgeParent.test.ts +315 -1
- package/src/BridgeParent.ts +211 -1
package/src/BridgeParent.test.ts
CHANGED
|
@@ -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
|
|
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')
|
package/src/BridgeParent.ts
CHANGED
|
@@ -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
|
|
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
|