@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.
|
|
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.
|
|
20
|
-
"@meshconnect/uwc-
|
|
21
|
-
"@meshconnect/uwc-
|
|
22
|
-
"@meshconnect/uwc-
|
|
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
|
})
|