@meshconnect/uwc-core 1.1.0 → 1.1.1-snapshot.2e0528b
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/events.d.ts +5 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/observability/lifecycle-bridge.d.ts +31 -13
- package/dist/observability/lifecycle-bridge.d.ts.map +1 -1
- package/dist/observability/lifecycle-bridge.js +167 -92
- package/dist/observability/lifecycle-bridge.js.map +1 -1
- package/dist/services/connection-service.d.ts +43 -0
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +163 -9
- package/dist/services/connection-service.js.map +1 -1
- package/dist/services/network-switch-service.d.ts.map +1 -1
- package/dist/services/network-switch-service.js +4 -1
- package/dist/services/network-switch-service.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -5
- package/src/events.ts +5 -2
- package/src/observability/lifecycle-bridge.test.ts +197 -0
- package/src/observability/lifecycle-bridge.ts +211 -99
- package/src/services/connection-service.test.ts +272 -0
- package/src/services/connection-service.ts +190 -10
- package/src/services/network-switch-service.test.ts +33 -0
- package/src/services/network-switch-service.ts +4 -1
- package/src/version.ts +1 -1
|
@@ -26,6 +26,12 @@ export class ConnectionService {
|
|
|
26
26
|
private activeConnector: Connector | null = null
|
|
27
27
|
/** Tears down the post-connect lifecycle subscription; set while connected. */
|
|
28
28
|
private lifecycleTeardown: (() => void) | undefined
|
|
29
|
+
/**
|
|
30
|
+
* Teardowns for the accounts-sync attachments of the session's SECONDARY
|
|
31
|
+
* namespaces (see `attachSecondaryAccountSync`); set while connected,
|
|
32
|
+
* cleared together with `lifecycleTeardown`.
|
|
33
|
+
*/
|
|
34
|
+
private secondaryAccountSyncTeardowns: Array<() => void> = []
|
|
29
35
|
|
|
30
36
|
constructor(
|
|
31
37
|
private networks: Network[],
|
|
@@ -154,6 +160,11 @@ export class ConnectionService {
|
|
|
154
160
|
// disconnect, expiry) from the live provider — telemetry only, never changes
|
|
155
161
|
// connection behaviour. Torn down on disconnect.
|
|
156
162
|
this.attachLifecycle(connectionMode, wallet.id, network.namespace)
|
|
163
|
+
this.attachSecondaryAccountSync(
|
|
164
|
+
connectionMode,
|
|
165
|
+
wallet.id,
|
|
166
|
+
network.namespace
|
|
167
|
+
)
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
/** Wire the active connector's lifecycle stream onto the bus (or signal absence). */
|
|
@@ -161,7 +172,7 @@ export class ConnectionService {
|
|
|
161
172
|
connectionMode: ConnectionMode,
|
|
162
173
|
walletId: string,
|
|
163
174
|
namespace: Namespace,
|
|
164
|
-
initialSwitch?: { chainId: NetworkId; at: number }
|
|
175
|
+
initialSwitch?: { chainId: NetworkId; address?: string; at: number }
|
|
165
176
|
): void {
|
|
166
177
|
this.teardownLifecycle()
|
|
167
178
|
const connector = this.activeConnector
|
|
@@ -181,6 +192,16 @@ export class ConnectionService {
|
|
|
181
192
|
walletId
|
|
182
193
|
})
|
|
183
194
|
: undefined,
|
|
195
|
+
subscribeAccounts: connector?.onAccountsChanged
|
|
196
|
+
? listener =>
|
|
197
|
+
connector.onAccountsChanged!(listener, {
|
|
198
|
+
connectionMode,
|
|
199
|
+
namespace,
|
|
200
|
+
walletId
|
|
201
|
+
})
|
|
202
|
+
: undefined,
|
|
203
|
+
onAccountsChanged: accounts =>
|
|
204
|
+
this.handleAccountsChanged(accounts, namespace),
|
|
184
205
|
getRouting: () => {
|
|
185
206
|
const s = this.sessionManager.getSession()
|
|
186
207
|
return {
|
|
@@ -203,6 +224,63 @@ export class ConnectionService {
|
|
|
203
224
|
})
|
|
204
225
|
}
|
|
205
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Apply a REAL account-list change (from `Connector.onAccountsChanged`, via
|
|
229
|
+
* the lifecycle bridge's second stream) to the session. `namespace` is the
|
|
230
|
+
* namespace of the provider that reported the change — `activeAddress`
|
|
231
|
+
* only moves when that matches the session's currently active network; an
|
|
232
|
+
* empty list is a full, wallet-initiated disconnect.
|
|
233
|
+
*/
|
|
234
|
+
private handleAccountsChanged(
|
|
235
|
+
accounts: string[],
|
|
236
|
+
namespace: Namespace
|
|
237
|
+
): void {
|
|
238
|
+
if (accounts.length === 0) {
|
|
239
|
+
// Idempotency guard: this runs synchronously FROM INSIDE the
|
|
240
|
+
// connector's own accountsChanged dispatch — finishDisconnect() below
|
|
241
|
+
// tears down (among other things) the very subscription that is
|
|
242
|
+
// currently delivering this callback. If a wallet double-fires the
|
|
243
|
+
// empty-array disconnect signal (or one slips through before teardown
|
|
244
|
+
// takes effect), `activeConnector` is already null from the first call
|
|
245
|
+
// — skip re-clearing an already-cleared session and re-emitting
|
|
246
|
+
// disconnected/sessionChanged a second time. Scoped to THIS wallet-
|
|
247
|
+
// initiated path only; the public disconnect() below still always
|
|
248
|
+
// runs finishDisconnect() defensively, even with no active connector.
|
|
249
|
+
if (this.activeConnector === null) return
|
|
250
|
+
// The wallet ended the session on its own side, but the connector's
|
|
251
|
+
// OWN local state (e.g. EthereumWalletService's cached account/
|
|
252
|
+
// provider/uuid) is never cleared by finishDisconnect() below — it
|
|
253
|
+
// only clears core's sessionManager. Left uncleared, a later connect
|
|
254
|
+
// to a DIFFERENT wallet reads that stale cached account via
|
|
255
|
+
// checkExistingConnections() and never rebinds to the new wallet's
|
|
256
|
+
// provider/uuid, silently keeping the old wallet's address live under
|
|
257
|
+
// the new wallet's session. disconnect() is fire-and-forget and
|
|
258
|
+
// error-swallowed here: for injected connectors it's local-only
|
|
259
|
+
// cleanup (safe to call any number of times), and for protocol-based
|
|
260
|
+
// connectors that might reject on an already-closed session, there is
|
|
261
|
+
// no caller to propagate that error to anyway.
|
|
262
|
+
this.activeConnector?.disconnect?.().catch(() => {})
|
|
263
|
+
this.finishDisconnect()
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
const newAddress = accounts[0]!
|
|
267
|
+
const session = this.sessionManager.getSession()
|
|
268
|
+
const availableAddresses = session.availableAddresses.map(addr => {
|
|
269
|
+
const network = this.networks.find(n => n.id === addr.networkId)
|
|
270
|
+
return network?.namespace === namespace
|
|
271
|
+
? { ...addr, address: newAddress }
|
|
272
|
+
: addr
|
|
273
|
+
})
|
|
274
|
+
const activeAddress =
|
|
275
|
+
session.activeNetwork?.namespace === namespace
|
|
276
|
+
? newAddress
|
|
277
|
+
: session.activeAddress
|
|
278
|
+
this.sessionManager.updateSession({ activeAddress, availableAddresses })
|
|
279
|
+
this.eventManager.emit('sessionChanged', {
|
|
280
|
+
session: this.sessionManager.getSession()
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
206
284
|
/**
|
|
207
285
|
* Remove the active provider-lifecycle subscription (EIP-1193 `.on` / wallet-
|
|
208
286
|
* adapter / SignClient listeners) WITHOUT disconnecting the wallet. Idempotent.
|
|
@@ -213,6 +291,8 @@ export class ConnectionService {
|
|
|
213
291
|
teardownLifecycle(): void {
|
|
214
292
|
this.lifecycleTeardown?.()
|
|
215
293
|
this.lifecycleTeardown = undefined
|
|
294
|
+
for (const teardown of this.secondaryAccountSyncTeardowns) teardown()
|
|
295
|
+
this.secondaryAccountSyncTeardowns = []
|
|
216
296
|
}
|
|
217
297
|
|
|
218
298
|
/**
|
|
@@ -237,13 +317,95 @@ export class ConnectionService {
|
|
|
237
317
|
session.connectionMode,
|
|
238
318
|
session.activeWallet.id,
|
|
239
319
|
session.activeNetwork.namespace,
|
|
240
|
-
// The reattach follows a JUST-completed switch to the now-active chain
|
|
241
|
-
// arm echo suppression for
|
|
242
|
-
// for a wallet-initiated change (one-shot, same as
|
|
243
|
-
|
|
320
|
+
// The reattach follows a JUST-completed switch to the now-active chain
|
|
321
|
+
// (and address) — arm echo suppression for both so the new provider's
|
|
322
|
+
// echo isn't mistaken for a wallet-initiated change (one-shot, same as
|
|
323
|
+
// bus-armed switches).
|
|
324
|
+
{
|
|
325
|
+
chainId: session.activeNetwork.id,
|
|
326
|
+
...(session.activeAddress && { address: session.activeAddress }),
|
|
327
|
+
at: Date.now()
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
// Re-derive the secondary set against the NOW-active namespace — a
|
|
331
|
+
// cross-namespace switch swaps which namespace is "secondary" (the
|
|
332
|
+
// attachLifecycle above already tore the previous set down via
|
|
333
|
+
// teardownLifecycle).
|
|
334
|
+
this.attachSecondaryAccountSync(
|
|
335
|
+
session.connectionMode,
|
|
336
|
+
session.activeWallet.id,
|
|
337
|
+
session.activeNetwork.namespace
|
|
244
338
|
)
|
|
245
339
|
}
|
|
246
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Attach account-sync for every OTHER namespace the session's
|
|
343
|
+
* `availableAddresses` span. `attachLifecycle` observes only the ACTIVE
|
|
344
|
+
* network's namespace, but the cross-namespace gather seeds addresses from
|
|
345
|
+
* other namespaces' live surfaces at connect time — without their own
|
|
346
|
+
* subscription, a wallet-side account switch on such a namespace is never
|
|
347
|
+
* observed and the session serves the connect-time addresses forever (e.g.
|
|
348
|
+
* a Solana-active MetaMask session whose eip155 addresses never follow the
|
|
349
|
+
* extension's account switch).
|
|
350
|
+
*
|
|
351
|
+
* Accounts-sync ONLY, no lifecycle-telemetry stream: the active namespace
|
|
352
|
+
* stays the single source of wallet* telemetry and of the wallet-initiated
|
|
353
|
+
* disconnect signal.
|
|
354
|
+
*/
|
|
355
|
+
private attachSecondaryAccountSync(
|
|
356
|
+
connectionMode: ConnectionMode,
|
|
357
|
+
walletId: string,
|
|
358
|
+
activeNamespace: Namespace
|
|
359
|
+
): void {
|
|
360
|
+
const connector = this.activeConnector
|
|
361
|
+
if (!connector?.onAccountsChanged) return
|
|
362
|
+
|
|
363
|
+
const session = this.sessionManager.getSession()
|
|
364
|
+
const secondaryNamespaces = new Set<Namespace>()
|
|
365
|
+
for (const addr of session.availableAddresses) {
|
|
366
|
+
const namespace = this.networks.find(
|
|
367
|
+
n => n.id === addr.networkId
|
|
368
|
+
)?.namespace
|
|
369
|
+
if (namespace && namespace !== activeNamespace) {
|
|
370
|
+
secondaryNamespaces.add(namespace)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const runtime =
|
|
375
|
+
(globalThis as { UWCBridgeChildInitialized?: boolean })
|
|
376
|
+
.UWCBridgeChildInitialized === true
|
|
377
|
+
? ('iframe' as const)
|
|
378
|
+
: ('direct' as const)
|
|
379
|
+
|
|
380
|
+
for (const namespace of secondaryNamespaces) {
|
|
381
|
+
this.secondaryAccountSyncTeardowns.push(
|
|
382
|
+
attachLifecycle({
|
|
383
|
+
eventManager: this.eventManager,
|
|
384
|
+
subscribe: undefined,
|
|
385
|
+
subscribeAccounts: listener =>
|
|
386
|
+
connector.onAccountsChanged!(listener, {
|
|
387
|
+
connectionMode,
|
|
388
|
+
namespace,
|
|
389
|
+
walletId
|
|
390
|
+
}),
|
|
391
|
+
onAccountsChanged: accounts => {
|
|
392
|
+
// An empty list from a SECONDARY namespace means that namespace's
|
|
393
|
+
// own authorization ended — NOT the session. Only the active
|
|
394
|
+
// namespace's stream may signal the full wallet-initiated
|
|
395
|
+
// disconnect (handleAccountsChanged's empty-list path).
|
|
396
|
+
if (accounts.length === 0) return
|
|
397
|
+
this.handleAccountsChanged(accounts, namespace)
|
|
398
|
+
},
|
|
399
|
+
getRouting: () => ({ connectionMode, walletId, namespace }),
|
|
400
|
+
connectionMode,
|
|
401
|
+
walletId,
|
|
402
|
+
namespace,
|
|
403
|
+
runtime
|
|
404
|
+
})
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
247
409
|
private async connectWithURIPoll(
|
|
248
410
|
connector: Connector,
|
|
249
411
|
network: Network,
|
|
@@ -420,14 +582,32 @@ export class ConnectionService {
|
|
|
420
582
|
await this.activeConnector.disconnect()
|
|
421
583
|
}
|
|
422
584
|
} finally {
|
|
423
|
-
this.
|
|
424
|
-
this.sessionManager.clearSession()
|
|
425
|
-
const session = this.sessionManager.getSession()
|
|
426
|
-
this.eventManager.emit('disconnected', undefined)
|
|
427
|
-
this.eventManager.emit('sessionChanged', { session })
|
|
585
|
+
this.finishDisconnect()
|
|
428
586
|
}
|
|
429
587
|
}
|
|
430
588
|
|
|
589
|
+
/**
|
|
590
|
+
* Clears core's own local state (session, active connector reference,
|
|
591
|
+
* lifecycle subscription) and emits the disconnect events. Does NOT itself
|
|
592
|
+
* call the connector's `disconnect()` — that happens in each CALLER,
|
|
593
|
+
* because the two callers need different semantics: the public
|
|
594
|
+
* `disconnect()` above awaits it (the caller may care whether the wallet
|
|
595
|
+
* side actually tore down); the wallet-initiated path in
|
|
596
|
+
* `handleAccountsChanged` fires it and swallows the result (the wallet
|
|
597
|
+
* already ended the session on its own side, but the connector's own
|
|
598
|
+
* LOCAL state — e.g. a cached account/uuid — still needs clearing, or a
|
|
599
|
+
* later connect to a different wallet reads it as stale and never
|
|
600
|
+
* rebinds).
|
|
601
|
+
*/
|
|
602
|
+
private finishDisconnect(): void {
|
|
603
|
+
this.teardownLifecycle()
|
|
604
|
+
this.activeConnector = null
|
|
605
|
+
this.sessionManager.clearSession()
|
|
606
|
+
const session = this.sessionManager.getSession()
|
|
607
|
+
this.eventManager.emit('disconnected', undefined)
|
|
608
|
+
this.eventManager.emit('sessionChanged', { session })
|
|
609
|
+
}
|
|
610
|
+
|
|
431
611
|
getConnectionURI(): string | undefined {
|
|
432
612
|
if (
|
|
433
613
|
this.activeConnector &&
|
|
@@ -234,4 +234,37 @@ describe('NetworkSwitchService', () => {
|
|
|
234
234
|
expect(events[0].data.operation).toBe('switchNetwork')
|
|
235
235
|
expect(events[0].data.chainId).toBe('eip155:137')
|
|
236
236
|
})
|
|
237
|
+
|
|
238
|
+
it('includes the new address on the emitted networkSwitched event', async () => {
|
|
239
|
+
sessionManager.updateSession({
|
|
240
|
+
connectionMode: 'injected',
|
|
241
|
+
activeWallet: {
|
|
242
|
+
id: 'metamask',
|
|
243
|
+
name: 'MetaMask',
|
|
244
|
+
metadata: {
|
|
245
|
+
icon: 'test',
|
|
246
|
+
downloadUrl: 'test',
|
|
247
|
+
supportedChains: ['eip155:1', 'eip155:137']
|
|
248
|
+
},
|
|
249
|
+
extensionInjectedProvider: {
|
|
250
|
+
supportedNetworkIds: ['eip155:1', 'eip155:137']
|
|
251
|
+
}
|
|
252
|
+
} as any
|
|
253
|
+
})
|
|
254
|
+
mockConnector.switchNetwork = vi.fn().mockResolvedValue({
|
|
255
|
+
address: '0xabc',
|
|
256
|
+
availableAddresses: [{ address: '0xabc', networkId: 'eip155:137' }]
|
|
257
|
+
})
|
|
258
|
+
const switched: Array<{ network: unknown; address?: string }> = []
|
|
259
|
+
eventManager.on('networkSwitched', d => switched.push(d))
|
|
260
|
+
|
|
261
|
+
await networkSwitchService.switchNetwork('eip155:137')
|
|
262
|
+
|
|
263
|
+
expect(switched).toEqual([
|
|
264
|
+
{
|
|
265
|
+
network: expect.objectContaining({ id: 'eip155:137' }),
|
|
266
|
+
address: '0xabc'
|
|
267
|
+
}
|
|
268
|
+
])
|
|
269
|
+
})
|
|
237
270
|
})
|
|
@@ -188,7 +188,10 @@ export class NetworkSwitchService {
|
|
|
188
188
|
})
|
|
189
189
|
})
|
|
190
190
|
|
|
191
|
-
this.eventManager.emit('networkSwitched', {
|
|
191
|
+
this.eventManager.emit('networkSwitched', {
|
|
192
|
+
network,
|
|
193
|
+
address: result.address
|
|
194
|
+
})
|
|
192
195
|
this.eventManager.emit('sessionChanged', {
|
|
193
196
|
session: this.sessionManager.getSession()
|
|
194
197
|
})
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated from package.json by scripts/generate-version.mjs — do not edit.
|
|
2
|
-
export const UWC_CORE_VERSION = '1.1.
|
|
2
|
+
export const UWC_CORE_VERSION = '1.1.1'
|