@meshconnect/uwc-bridge-child 0.2.2 → 0.2.3-snapshot.35caafb

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/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,gFAAgF;AAChF,gDAAgD;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meshconnect/uwc-bridge-child",
3
- "version": "0.2.2",
3
+ "version": "0.2.3-snapshot.35caafb",
4
4
  "description": "Child iframe bridge for Universal Wallet Connector",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -337,6 +337,319 @@ describe('BridgeChild', () => {
337
337
  })
338
338
  })
339
339
 
340
+ describe('Tron/TON discovery race (premature empty default)', () => {
341
+ it('does not default Tron to [] while a triggered discovery is still pending, then delivers the wallet once ready', async () => {
342
+ const mockTron = [
343
+ {
344
+ uuid: 'tron-tronlink',
345
+ name: 'tronLink',
346
+ injectedId: 'tronLink',
347
+ provider: { tronWeb: {} }
348
+ }
349
+ ]
350
+
351
+ // Parent supports Tron but reports not-ready at first (e.g. TronLink's
352
+ // tronWeb hasn't finished injecting yet). EVM + Solana resolve instantly.
353
+ let tronReady = false
354
+ const api = {
355
+ eip6963WalletsReady: true,
356
+ walletStandardWalletsReady: true,
357
+ tonWalletsReady: true,
358
+ eip6963Wallets: [],
359
+ walletStandardWallets: [],
360
+ tonWallets: [],
361
+ tronWallets: mockTron,
362
+ get tronWalletsReady() {
363
+ return tronReady
364
+ },
365
+ discoverTronWallets: vi.fn(),
366
+ discoverTonWallets: vi.fn()
367
+ }
368
+ mockWrappedAPI = api as any
369
+
370
+ await createBridgeChildInIframe({ tronInjectedIds: ['tronLink'] })
371
+
372
+ // EVM + Solana resolve and several poll ticks pass while Tron is pending.
373
+ await vi.advanceTimersByTimeAsync(200)
374
+
375
+ // The placeholder must NOT default Tron to `[]` here: the injected
376
+ // connector's pollForBridgeTronWallets resolves on the first array it
377
+ // sees, so a premature `[]` makes it report "none found" and miss the
378
+ // real wallet marshalled below.
379
+ expect((window as any).tronWallets).toBeUndefined()
380
+
381
+ // Parent finishes Tron discovery (tronWeb now injected).
382
+ tronReady = true
383
+ await vi.advanceTimersByTimeAsync(100)
384
+
385
+ expect((window as any).tronWallets).toHaveLength(1)
386
+ expect((window as any).tronWallets[0].uuid).toBe('tron-tronlink')
387
+ })
388
+
389
+ it('does not default TON to [] while a triggered discovery is still pending, then delivers the wallet once ready', async () => {
390
+ const mockTon = [
391
+ {
392
+ uuid: 'ton-trustwalletto',
393
+ name: 'Trust Wallet',
394
+ icon: '',
395
+ jsBridgeKey: 'trustwalletTon',
396
+ provider: createMockTonProvider('Trust Wallet')
397
+ }
398
+ ]
399
+
400
+ let tonReady = false
401
+ const api = {
402
+ eip6963WalletsReady: true,
403
+ walletStandardWalletsReady: true,
404
+ tronWalletsReady: true,
405
+ eip6963Wallets: [],
406
+ walletStandardWallets: [],
407
+ tronWallets: [],
408
+ tonWallets: mockTon,
409
+ get tonWalletsReady() {
410
+ return tonReady
411
+ },
412
+ discoverTronWallets: vi.fn(),
413
+ discoverTonWallets: vi.fn()
414
+ }
415
+ mockWrappedAPI = api as any
416
+
417
+ await createBridgeChildInIframe({ tonJsBridgeKeys: ['trustwalletTon'] })
418
+ await vi.advanceTimersByTimeAsync(200)
419
+
420
+ // Same race as Tron: the placeholder must not pre-empt a pending TON
421
+ // discovery, or the connector's poll resolves empty and misses it.
422
+ expect((window as any).tonWallets).toBeUndefined()
423
+
424
+ tonReady = true
425
+ await vi.advanceTimersByTimeAsync(100)
426
+
427
+ expect((window as any).tonWallets).toHaveLength(1)
428
+ expect((window as any).tonWallets[0].uuid).toBe('ton-trustwalletto')
429
+ })
430
+
431
+ it('still backstops Tron to [] at MAX_POLL_MS when the parent never reports ready', async () => {
432
+ const api = {
433
+ eip6963WalletsReady: true,
434
+ walletStandardWalletsReady: true,
435
+ tonWalletsReady: true,
436
+ eip6963Wallets: [],
437
+ walletStandardWallets: [],
438
+ tonWallets: [],
439
+ tronWallets: [],
440
+ tronWalletsReady: false,
441
+ discoverTronWallets: vi.fn(),
442
+ discoverTonWallets: vi.fn()
443
+ }
444
+ mockWrappedAPI = api as any
445
+
446
+ await createBridgeChildInIframe({ tronInjectedIds: ['tronLink'] })
447
+
448
+ // Pending → not prematurely defaulted.
449
+ await vi.advanceTimersByTimeAsync(500)
450
+ expect((window as any).tronWallets).toBeUndefined()
451
+
452
+ // Past MAX_POLL_MS (5s) the fillEmptyDefaults backstop resolves it to [].
453
+ await vi.advanceTimersByTimeAsync(5000)
454
+ expect((window as any).tronWallets).toEqual([])
455
+ })
456
+
457
+ it('recovers Tron when the parent exposes late (Comlink reads hang until then)', async () => {
458
+ // Reproduces the iframe-example race: BridgeParent is created on the
459
+ // iframe `load` event, so its Comlink endpoint isn't live when the child
460
+ // starts polling. A Comlink read to a not-yet-exposed parent never settles
461
+ // (comlink.ts requestResponseMessage has no timeout) — simulate with a
462
+ // getter returning a never-settling promise until the parent goes "live".
463
+ const mockTron = [
464
+ {
465
+ uuid: 'tron-tronlink',
466
+ name: 'tronLink',
467
+ injectedId: 'tronLink',
468
+ provider: { tronWeb: {} }
469
+ }
470
+ ]
471
+ let live = false
472
+ const hang = () => new Promise(() => {})
473
+ const api = {
474
+ get eip6963WalletsReady() {
475
+ return live ? true : hang()
476
+ },
477
+ get walletStandardWalletsReady() {
478
+ return live ? true : hang()
479
+ },
480
+ get tronWalletsReady() {
481
+ return live ? true : hang()
482
+ },
483
+ get tonWalletsReady() {
484
+ return live ? true : hang()
485
+ },
486
+ eip6963Wallets: [],
487
+ walletStandardWallets: [],
488
+ tronWallets: mockTron,
489
+ tonWallets: [],
490
+ discoverTronWallets: vi.fn(),
491
+ discoverTonWallets: vi.fn()
492
+ }
493
+ mockWrappedAPI = api as any
494
+
495
+ await createBridgeChildInIframe({ tronInjectedIds: ['tronLink'] })
496
+
497
+ // Parent not live yet: reads time out and the loop retries — it must NOT
498
+ // hang (the bug). Tron stays undiscovered for now.
499
+ await vi.advanceTimersByTimeAsync(600)
500
+ expect((window as any).tronWallets).toBeUndefined()
501
+
502
+ // Parent's BridgeParent finally exposes — the child must recover.
503
+ live = true
504
+ await vi.advanceTimersByTimeAsync(600)
505
+
506
+ expect((window as any).tronWallets).toHaveLength(1)
507
+ expect((window as any).tronWallets[0].uuid).toBe('tron-tronlink')
508
+ })
509
+
510
+ it('recovers EIP-6963 when the parent exposes late (same raceParentRead path as Tron)', async () => {
511
+ // EVM/Solana use the same per-tick raceParentRead recovery as Tron, so a
512
+ // late-exposing parent must not strand them either.
513
+ const mockEip = [
514
+ {
515
+ uuid: 'mm',
516
+ name: 'MetaMask',
517
+ icon: '',
518
+ rdns: 'io.metamask',
519
+ provider: { request: vi.fn() }
520
+ }
521
+ ]
522
+ let live = false
523
+ const hang = () => new Promise(() => {})
524
+ const api = {
525
+ get eip6963WalletsReady() {
526
+ return live ? true : hang()
527
+ },
528
+ get walletStandardWalletsReady() {
529
+ return live ? true : hang()
530
+ },
531
+ get tronWalletsReady() {
532
+ return live ? true : hang()
533
+ },
534
+ get tonWalletsReady() {
535
+ return live ? true : hang()
536
+ },
537
+ eip6963Wallets: mockEip,
538
+ walletStandardWallets: [],
539
+ tronWallets: [],
540
+ tonWallets: [],
541
+ discoverTronWallets: vi.fn(),
542
+ discoverTonWallets: vi.fn()
543
+ }
544
+ mockWrappedAPI = api as any
545
+
546
+ await createBridgeChildInIframe()
547
+
548
+ // Pre-expose: reads time out, the loop retries, nothing set yet.
549
+ await vi.advanceTimersByTimeAsync(600)
550
+ expect((window as any).eip6963Wallets).toBeUndefined()
551
+
552
+ // Parent exposes — the child must recover EVM, not just Tron.
553
+ live = true
554
+ await vi.advanceTimersByTimeAsync(600)
555
+
556
+ expect((window as any).eip6963Wallets).toHaveLength(1)
557
+ expect((window as any).eip6963Wallets[0].uuid).toBe('mm')
558
+ })
559
+
560
+ it('recovers TON when the parent exposes late (same raceParentRead path as Tron)', async () => {
561
+ const mockTon = [
562
+ {
563
+ uuid: 'ton-tonkeeper',
564
+ name: 'Tonkeeper',
565
+ icon: '',
566
+ jsBridgeKey: 'tonkeeper',
567
+ provider: createMockTonProvider('Tonkeeper')
568
+ }
569
+ ]
570
+ let live = false
571
+ const hang = () => new Promise(() => {})
572
+ const api = {
573
+ get eip6963WalletsReady() {
574
+ return live ? true : hang()
575
+ },
576
+ get walletStandardWalletsReady() {
577
+ return live ? true : hang()
578
+ },
579
+ get tronWalletsReady() {
580
+ return live ? true : hang()
581
+ },
582
+ get tonWalletsReady() {
583
+ return live ? true : hang()
584
+ },
585
+ eip6963Wallets: [],
586
+ walletStandardWallets: [],
587
+ tronWallets: [],
588
+ tonWallets: mockTon,
589
+ discoverTronWallets: vi.fn(),
590
+ discoverTonWallets: vi.fn()
591
+ }
592
+ mockWrappedAPI = api as any
593
+
594
+ await createBridgeChildInIframe({ tonJsBridgeKeys: ['tonkeeper'] })
595
+
596
+ await vi.advanceTimersByTimeAsync(600)
597
+ expect((window as any).tonWallets).toBeUndefined()
598
+
599
+ live = true
600
+ await vi.advanceTimersByTimeAsync(600)
601
+
602
+ expect((window as any).tonWallets).toHaveLength(1)
603
+ expect((window as any).tonWallets[0].uuid).toBe('ton-tonkeeper')
604
+ })
605
+
606
+ it('recovers Solana when the parent exposes late (same raceParentRead path as Tron)', async () => {
607
+ const mockSolana = [
608
+ {
609
+ uuid: 'phantom-solana',
610
+ name: 'Phantom',
611
+ chains: ['solana:mainnet'],
612
+ features: ['standard:connect'],
613
+ adapter: {}
614
+ }
615
+ ]
616
+ let live = false
617
+ const hang = () => new Promise(() => {})
618
+ const api = {
619
+ get eip6963WalletsReady() {
620
+ return live ? true : hang()
621
+ },
622
+ get walletStandardWalletsReady() {
623
+ return live ? true : hang()
624
+ },
625
+ get tronWalletsReady() {
626
+ return live ? true : hang()
627
+ },
628
+ get tonWalletsReady() {
629
+ return live ? true : hang()
630
+ },
631
+ eip6963Wallets: [],
632
+ walletStandardWallets: mockSolana,
633
+ tronWallets: [],
634
+ tonWallets: [],
635
+ discoverTronWallets: vi.fn(),
636
+ discoverTonWallets: vi.fn()
637
+ }
638
+ mockWrappedAPI = api as any
639
+
640
+ await createBridgeChildInIframe()
641
+
642
+ await vi.advanceTimersByTimeAsync(600)
643
+ expect((window as any).walletStandardWallets).toBeUndefined()
644
+
645
+ live = true
646
+ await vi.advanceTimersByTimeAsync(600)
647
+
648
+ expect((window as any).walletStandardWallets).toHaveLength(1)
649
+ expect((window as any).walletStandardWallets[0].name).toBe('Phantom')
650
+ })
651
+ })
652
+
340
653
  describe('backward compatibility with older BridgeParent (no Tron)', () => {
341
654
  it('should still set EVM and Solana wallets when parent has no Tron support', async () => {
342
655
  const eip6963Wallets = [
@@ -358,7 +671,10 @@ describe('BridgeChild', () => {
358
671
  }
359
672
  ]
360
673
 
361
- // Simulate old parent that throws on any Tron property access
674
+ // Defensive path: a parent whose Tron property access THROWS (e.g. a
675
+ // non-Comlink stub). Real old Comlink parents instead resolve a missing
676
+ // prop to undefined — see the MAX_POLL_MS backstop test below. Both end at
677
+ // tronWallets = [].
362
678
  const oldParentAPI = {
363
679
  eip6963WalletsReady: true,
364
680
  walletStandardWalletsReady: true,
@@ -389,6 +705,33 @@ describe('BridgeChild', () => {
389
705
  expect((window as any).tronWallets).toEqual([])
390
706
  })
391
707
 
708
+ it('defaults Tron to [] via the MAX_POLL_MS backstop on a real old parent (missing props resolve to undefined, not throw)', async () => {
709
+ // Real Comlink resolves a GET on a missing property to `undefined` (only
710
+ // an APPLY on a missing method rejects). So an old parent without Tron
711
+ // support reports tronWalletsReady=undefined every tick; the child retries
712
+ // until MAX_POLL_MS, then fillEmptyDefaults sets [].
713
+ const oldParentAPI = {
714
+ eip6963WalletsReady: true,
715
+ walletStandardWalletsReady: true,
716
+ eip6963Wallets: [],
717
+ walletStandardWallets: [],
718
+ parentOrigin: 'https://parent.example.com'
719
+ // No tronWalletsReady / tronWallets / discoverTronWallets — a Comlink GET
720
+ // on a missing prop would resolve them to undefined.
721
+ }
722
+ mockWrappedAPI = oldParentAPI as any
723
+
724
+ await createBridgeChildInIframe({ tronInjectedIds: ['tronLink'] })
725
+
726
+ // Not prematurely defaulted while readiness stays undefined.
727
+ await vi.advanceTimersByTimeAsync(500)
728
+ expect((window as any).tronWallets).toBeUndefined()
729
+
730
+ // Past MAX_POLL_MS (5s) the backstop resolves it to [].
731
+ await vi.advanceTimersByTimeAsync(5000)
732
+ expect((window as any).tronWallets).toEqual([])
733
+ })
734
+
392
735
  it('should set tron to empty array when discoverTronWallets throws', async () => {
393
736
  const apiWithBrokenDiscover = {
394
737
  eip6963WalletsReady: true,
@@ -435,13 +778,19 @@ describe('BridgeChild', () => {
435
778
  // EVM and Solana should be set even though tron is not ready
436
779
  expect((window as any).eip6963Wallets).toHaveLength(1)
437
780
  expect((window as any).walletStandardWallets).toEqual([])
438
- // Tron should be empty (not ready, so inner block skipped)
439
- expect((window as any).tronWallets).toEqual([])
781
+ // Tron stays undefined while the parent reports not-ready we must NOT
782
+ // prematurely default it to []. The connector's poll resolves on the first
783
+ // array it sees, so an early [] makes it miss a wallet that arrives a
784
+ // moment later. tryMarshalTron writes it once the parent reports ready, or
785
+ // the MAX_POLL_MS backstop fills [] if it never does.
786
+ expect((window as any).tronWallets).toBeUndefined()
440
787
  })
441
788
 
442
- it('should only call discoverTronWallets once across multiple polls', async () => {
443
- // First poll: eip6963 not ready, so returns 'continue'
444
- // Second poll: eip6963 ready, completes
789
+ it('calls discoverTronWallets once when Tron resolves on the first tick (re-issues per tick until tronDone otherwise)', async () => {
790
+ // Not a general invariant: the design re-issues discovery every tick until
791
+ // tronDone. Here Tron is ready immediately, so it marshals and sets
792
+ // tronDone on tick 1 — hence exactly one call. A slow parent would see one
793
+ // call per tick until ready.
445
794
  let callCount = 0
446
795
  const progressiveAPI = {
447
796
  get eip6963WalletsReady() {
@@ -463,7 +812,8 @@ describe('BridgeChild', () => {
463
812
  })
464
813
  await vi.advanceTimersByTimeAsync(1000)
465
814
 
466
- // discoverTronWallets should only be called once due to tronDiscoveryTriggered flag
815
+ // Tron reports ready on the first tick, so tryMarshalTron marshals and
816
+ // sets tronDone — it stops re-issuing discoverTronWallets after that.
467
817
  expect(progressiveAPI.discoverTronWallets).toHaveBeenCalledTimes(1)
468
818
  })
469
819
  })
@@ -708,6 +1058,42 @@ describe('BridgeChild', () => {
708
1058
  manifestUrl: 'https://example.com'
709
1059
  })
710
1060
  })
1061
+
1062
+ it('recovers parentOrigin when the parent exposes late (same raceParentRead path as wallets)', async () => {
1063
+ // syncParentOrigin shares the late-expose hazard: a parentOrigin read posted
1064
+ // before the parent's Comlink.expose never settles. The retry loop must keep
1065
+ // trying until the parent comes up, then set window.uwcParentOrigin.
1066
+ let live = false
1067
+ const hang = () => new Promise(() => {})
1068
+ const api = {
1069
+ eip6963WalletsReady: true,
1070
+ walletStandardWalletsReady: true,
1071
+ tronWalletsReady: true,
1072
+ tonWalletsReady: true,
1073
+ eip6963Wallets: [],
1074
+ walletStandardWallets: [],
1075
+ tronWallets: [],
1076
+ tonWallets: [],
1077
+ get parentOrigin() {
1078
+ return live ? 'https://stake.com' : hang()
1079
+ },
1080
+ discoverTronWallets: vi.fn(),
1081
+ discoverTonWallets: vi.fn()
1082
+ }
1083
+ mockWrappedAPI = api as any
1084
+
1085
+ await createBridgeChildInIframe()
1086
+
1087
+ // Parent not live yet: the read times out and the loop retries — it must
1088
+ // NOT hang and must NOT set a value.
1089
+ await vi.advanceTimersByTimeAsync(600)
1090
+ expect((window as any).uwcParentOrigin).toBeUndefined()
1091
+
1092
+ // Parent's BridgeParent finally exposes — the retry must recover.
1093
+ live = true
1094
+ await vi.advanceTimersByTimeAsync(600)
1095
+ expect((window as any).uwcParentOrigin).toBe('https://stake.com')
1096
+ })
711
1097
  })
712
1098
 
713
1099
  describe('checkWallets with all wallet types', () => {