@meshconnect/uwc-core 1.2.3-snapshot.f6952a6 → 1.2.3-snapshot.f7d72d8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meshconnect/uwc-core",
3
- "version": "1.2.3-snapshot.f6952a6",
3
+ "version": "1.2.3-snapshot.f7d72d8",
4
4
  "description": "Core functionality for Universal Wallet Connector",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,10 +16,10 @@
16
16
  "src"
17
17
  ],
18
18
  "dependencies": {
19
- "@meshconnect/uwc-injected-connector": "0.16.4-snapshot.f6952a6",
20
- "@meshconnect/uwc-types": "0.19.0-snapshot.f6952a6",
21
- "@meshconnect/uwc-wallet-connect-connector": "0.8.2-snapshot.f6952a6",
22
- "@meshconnect/uwc-ton-connector": "0.7.2-snapshot.f6952a6"
19
+ "@meshconnect/uwc-injected-connector": "0.16.4-snapshot.f7d72d8",
20
+ "@meshconnect/uwc-ton-connector": "0.7.2-snapshot.f7d72d8",
21
+ "@meshconnect/uwc-types": "0.19.0-snapshot.f7d72d8",
22
+ "@meshconnect/uwc-wallet-connect-connector": "0.8.2-snapshot.f7d72d8"
23
23
  },
24
24
  "devDependencies": {
25
25
  "typescript": "^5.9.3"
@@ -1418,4 +1418,199 @@ describe('ConnectionService', () => {
1418
1418
  }
1419
1419
  })
1420
1420
  })
1421
+
1422
+ describe('Solana account-change event + sibling-poll coexistence (Task 3.3)', () => {
1423
+ // 3.1/3.2 wire a REAL solana `onAccountsChanged` into core — active-
1424
+ // namespace path via `attachLifecycle`, secondary-namespace path via
1425
+ // `attachSecondaryAccountSync`. Separately, `refreshSiblingNamespaces`
1426
+ // polls every OTHER connected namespace's live account after a sibling's
1427
+ // switch. These tests confirm the two mechanisms coexist for a solana
1428
+ // session: no double `sessionChanged` emit, no re-entrant loop, no stale
1429
+ // clobber of a fresher value, and an empty solana event on a SECONDARY
1430
+ // namespace never ends the session.
1431
+ let accountSubs: Map<string, (accounts: string[]) => void>
1432
+
1433
+ beforeEach(() => {
1434
+ networks.push({
1435
+ id: 'solana:mainnet',
1436
+ internalId: 101,
1437
+ namespace: 'solana',
1438
+ name: 'Solana',
1439
+ logoUrl: '',
1440
+ nativeCurrency: { name: 'SOL', symbol: 'SOL', decimals: 9 },
1441
+ networkType: 'solana'
1442
+ } as never)
1443
+ wallets[0]!.extensionInjectedProvider = {
1444
+ supportedNetworkIds: ['solana:mainnet', 'eip155:1']
1445
+ } as never
1446
+ accountSubs = new Map()
1447
+ mockConnector.onAccountsChanged = ((
1448
+ listener: (accounts: string[]) => void,
1449
+ context: { namespace: string }
1450
+ ) => {
1451
+ accountSubs.set(context.namespace, listener)
1452
+ return () => {}
1453
+ }) as never
1454
+ })
1455
+
1456
+ it('a solana accountsChanged on a SOLANA-active session updates the address + activeAddress with exactly ONE sessionChanged (no double-emit)', async () => {
1457
+ mockConnector.connect = vi.fn().mockResolvedValue({
1458
+ networkId: 'solana:mainnet',
1459
+ address: 'SoLAddr111',
1460
+ availableAddresses: [
1461
+ { address: 'SoLAddr111', networkId: 'solana:mainnet' },
1462
+ { address: '0x1234', networkId: 'eip155:1' }
1463
+ ]
1464
+ })
1465
+ // No getLiveAccount on this connector: isolates the direct-event path
1466
+ // from the sibling-poll path for this assertion.
1467
+ expect((mockConnector as any).getLiveAccount).toBeUndefined()
1468
+
1469
+ await connectionService.connect('injected', 'metamask', 'solana:mainnet')
1470
+
1471
+ const sessions: unknown[] = []
1472
+ eventManager.on('sessionChanged', d => sessions.push(d))
1473
+
1474
+ accountSubs.get('solana')!(['SoLAddr_NEW'])
1475
+
1476
+ const session = sessionManager.getSession()
1477
+ expect(session.activeAddress).toBe('SoLAddr_NEW')
1478
+ expect(session.availableAddresses).toEqual([
1479
+ { address: 'SoLAddr_NEW', networkId: 'solana:mainnet' },
1480
+ { address: '0x1234', networkId: 'eip155:1' }
1481
+ ])
1482
+ expect(sessions.length).toBe(1)
1483
+ })
1484
+
1485
+ it('a solana accountsChanged on an EIP155-active session updates only the solana sibling entry, leaves activeAddress alone, and does not end the session', async () => {
1486
+ mockConnector.connect = vi.fn().mockResolvedValue({
1487
+ networkId: 'eip155:1',
1488
+ address: '0x1234',
1489
+ availableAddresses: [
1490
+ { address: 'SoLAddr111', networkId: 'solana:mainnet' },
1491
+ { address: '0x1234', networkId: 'eip155:1' }
1492
+ ]
1493
+ })
1494
+
1495
+ await connectionService.connect('injected', 'metamask', 'eip155:1')
1496
+
1497
+ accountSubs.get('solana')!(['SoLAddr_NEW'])
1498
+
1499
+ const session = sessionManager.getSession()
1500
+ expect(session.connectionMode).toBe('injected')
1501
+ expect(session.activeAddress).toBe('0x1234')
1502
+ expect(session.availableAddresses).toEqual([
1503
+ { address: 'SoLAddr_NEW', networkId: 'solana:mainnet' },
1504
+ { address: '0x1234', networkId: 'eip155:1' }
1505
+ ])
1506
+ })
1507
+
1508
+ it('an empty solana accountsChanged on the SECONDARY (non-active) namespace is a no-op — it does not end the session', async () => {
1509
+ const disconnectSpy = vi.fn().mockResolvedValue(undefined)
1510
+ mockConnector.disconnect = disconnectSpy
1511
+ mockConnector.connect = vi.fn().mockResolvedValue({
1512
+ networkId: 'eip155:1',
1513
+ address: '0x1234',
1514
+ availableAddresses: [
1515
+ { address: 'SoLAddr111', networkId: 'solana:mainnet' },
1516
+ { address: '0x1234', networkId: 'eip155:1' }
1517
+ ]
1518
+ })
1519
+
1520
+ await connectionService.connect('injected', 'metamask', 'eip155:1')
1521
+
1522
+ accountSubs.get('solana')!([])
1523
+
1524
+ const session = sessionManager.getSession()
1525
+ expect(session.connectionMode).toBe('injected')
1526
+ expect(session.activeAddress).toBe('0x1234')
1527
+ expect(session.availableAddresses).toEqual([
1528
+ { address: 'SoLAddr111', networkId: 'solana:mainnet' },
1529
+ { address: '0x1234', networkId: 'eip155:1' }
1530
+ ])
1531
+ expect(disconnectSpy).not.toHaveBeenCalled()
1532
+ })
1533
+
1534
+ it('a direct solana event and an eip155-triggered sibling poll converge on the same address without a stale clobber (generation guard)', async () => {
1535
+ vi.useFakeTimers()
1536
+ try {
1537
+ mockConnector.connect = vi.fn().mockResolvedValue({
1538
+ networkId: 'solana:mainnet',
1539
+ address: 'SoLAddr111',
1540
+ availableAddresses: [
1541
+ { address: 'SoLAddr111', networkId: 'solana:mainnet' },
1542
+ { address: '0x1234', networkId: 'eip155:1' }
1543
+ ]
1544
+ })
1545
+ let solanaLiveCalls = 0
1546
+ const liveAccount = vi.fn(async (ns: string) => {
1547
+ if (ns === 'solana') {
1548
+ solanaLiveCalls++
1549
+ // First read still reflects the OLD (stored) address — unchanged,
1550
+ // so the poll keeps going. A SECOND read is reached only if the
1551
+ // superseded refresh is wrongly allowed to continue; it returns a
1552
+ // value that is neither the original nor the direct event's
1553
+ // value, so any write from it is a visible clobber.
1554
+ return solanaLiveCalls === 1
1555
+ ? 'SoLAddr111'
1556
+ : 'SoLAddr_STALE_FROM_POLL'
1557
+ }
1558
+ // eip155 sibling (queried by the solana-event-triggered refresh):
1559
+ // never resolves — the direct event's own change already landed
1560
+ // through the non-polling path.
1561
+ return null
1562
+ })
1563
+ ;(mockConnector as any).getLiveAccount = liveAccount
1564
+
1565
+ const sessions: unknown[] = []
1566
+ eventManager.on('sessionChanged', d => sessions.push(d))
1567
+
1568
+ await connectionService.connect(
1569
+ 'injected',
1570
+ 'metamask',
1571
+ 'solana:mainnet'
1572
+ )
1573
+ sessions.length = 0 // drop the connect's own sessionChanged
1574
+
1575
+ // eip155 fires first: updates its own entry and kicks off a sibling
1576
+ // poll of solana (generation N).
1577
+ accountSubs.get('eip155')!(['0xSWITCHED'])
1578
+ await vi.advanceTimersByTimeAsync(0) // let attempt-0's read land (unchanged, keeps polling)
1579
+
1580
+ // A REAL solana accounts-change event lands next (the 3.1/3.2 direct
1581
+ // path) carrying the same new address the poll is chasing. It writes
1582
+ // directly and starts its OWN sibling refresh (generation N+1),
1583
+ // superseding the still-polling eip155-triggered one.
1584
+ accountSubs.get('solana')!(['SoLAddr_NEW'])
1585
+
1586
+ // Drain both: the superseded refresh must bail on the generation
1587
+ // check instead of issuing a second solana read and clobbering.
1588
+ await vi.advanceTimersByTimeAsync(
1589
+ SIBLING_REFRESH_MAX_ATTEMPTS * SIBLING_REFRESH_DELAY_MS
1590
+ )
1591
+
1592
+ const session = sessionManager.getSession()
1593
+ expect(
1594
+ session.availableAddresses.find(a => a.networkId === 'solana:mainnet')
1595
+ ?.address
1596
+ ).toBe('SoLAddr_NEW')
1597
+ expect(session.activeAddress).toBe('SoLAddr_NEW')
1598
+ expect(
1599
+ session.availableAddresses.find(a => a.networkId === 'eip155:1')
1600
+ ?.address
1601
+ ).toBe('0xSWITCHED')
1602
+ expect(session.availableAddresses.map(a => a.address)).not.toContain(
1603
+ 'SoLAddr_STALE_FROM_POLL'
1604
+ )
1605
+ // The superseded refresh never got a second read in.
1606
+ expect(solanaLiveCalls).toBe(1)
1607
+ // Exactly two emits: the eip155 event's own, and the solana event's
1608
+ // own. Neither sibling-refresh resolves a sibling, so neither adds an
1609
+ // extra emit — confirming no double-emit / no loop.
1610
+ expect(sessions.length).toBe(2)
1611
+ } finally {
1612
+ vi.useRealTimers()
1613
+ }
1614
+ })
1615
+ })
1421
1616
  })