import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import type { SessionTypes } from '@walletconnect/types' import { polygonNetwork } from '@meshconnect/uwc-constants' import type { WalletConnectProvider } from '@meshconnect/uwc-types' import { WalletConnectorError } from '@meshconnect/uwc-types' import { ConnectionService } from './connection-service' import type { WalletConnectClient } from './walletconnect-client' describe('ConnectionService', () => { describe('getAccountsForNamespace', () => { it('returns the parsed addresses for the given namespace from the current session', () => { const service = new ConnectionService({} as WalletConnectClient) ;(service as unknown as { currentSession: unknown }).currentSession = { namespaces: { eip155: { accounts: ['eip155:1:0xAAA', 'eip155:137:0xAAA'] }, tron: { accounts: ['tron:0x2b6653dc:TSomething'] } } } expect(service.getAccountsForNamespace('eip155')).toEqual([ '0xAAA', '0xAAA' ]) expect(service.getAccountsForNamespace('tron')).toEqual(['TSomething']) }) it('returns an empty array when there is no current session', () => { const service = new ConnectionService({} as WalletConnectClient) expect(service.getAccountsForNamespace('eip155')).toEqual([]) }) it('returns an empty array for a namespace with no accounts in the session', () => { const service = new ConnectionService({} as WalletConnectClient) ;(service as unknown as { currentSession: unknown }).currentSession = { namespaces: { eip155: { accounts: ['eip155:1:0xAAA'] } } } expect(service.getAccountsForNamespace('solana')).toEqual([]) }) it('skips an unparseable account entry rather than throwing', () => { const service = new ConnectionService({} as WalletConnectClient) ;(service as unknown as { currentSession: unknown }).currentSession = { namespaces: { eip155: { accounts: ['not-a-valid-caip-account'] } } } expect(service.getAccountsForNamespace('eip155')).toEqual([]) }) }) }) // A session whose eip155 namespace carries one Polygon account, so // `extractAddresses` resolves a primary address for `polygonNetwork`. function buildSession( address = '0xabc0000000000000000000000000000000000001' ): SessionTypes.Struct { return { topic: 't', pairingTopic: 'p', relay: { protocol: 'irn' }, expiry: Math.floor(Date.now() / 1000) + 86400, acknowledged: true, controller: 'c', requiredNamespaces: {}, optionalNamespaces: {}, sessionProperties: undefined, self: { publicKey: 's', metadata: { name: 'dApp', description: '', url: '', icons: [] } }, peer: { publicKey: 'pk', metadata: { name: 'Wallet', description: '', url: '', icons: [] } }, namespaces: { eip155: { chains: [polygonNetwork.id], accounts: [`${polygonNetwork.id}:${address}`], methods: [], events: [] } } } as unknown as SessionTypes.Struct } const provider: WalletConnectProvider = { supportedNetworkIds: [polygonNetwork.id], deeplinks: { universal: 'https://example.app.link', native: 'example://' } } describe('ConnectionService pre-pairing (beginPairing / adopt)', () => { let mockConnect: ReturnType let service: ConnectionService beforeEach(() => { vi.clearAllMocks() mockConnect = vi.fn() const client = { connect: mockConnect } const wcClient = { getClient: vi.fn().mockResolvedValue(client) } as unknown as WalletConnectClient service = new ConnectionService(wcClient) }) afterEach(() => { vi.useRealTimers() }) it('beginPairing returns the URI without surfacing it via getConnectionURI', async () => { mockConnect.mockResolvedValueOnce({ uri: 'wc:pre@2', approval: vi.fn().mockResolvedValue(buildSession()) }) const uri = await service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) expect(uri).toBe('wc:pre@2') // The URI is returned to the caller directly; connectionUri stays reserved // for an in-flight connect() so a pre-warm can never overwrite it. expect(service.getConnectionURI()).toBeNull() expect(mockConnect).toHaveBeenCalledTimes(1) }) it('beginPairing is idempotent within the TTL: a second call reuses the pending URI', async () => { mockConnect.mockResolvedValueOnce({ uri: 'wc:pre@2', approval: vi.fn().mockResolvedValue(buildSession()) }) const first = await service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) const second = await service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) // Same URI, and no second pairing minted (no leaked relay sub / proposal). expect(first).toBe('wc:pre@2') expect(second).toBe('wc:pre@2') expect(mockConnect).toHaveBeenCalledTimes(1) }) it('beginPairing mints a fresh pairing once the prior one has expired', async () => { vi.useFakeTimers() mockConnect.mockResolvedValueOnce({ uri: 'wc:pre@2', approval: vi.fn().mockResolvedValue(buildSession()) }) await service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) // Past the 4-minute TTL, so the idempotent reuse no longer applies. vi.advanceTimersByTime(4 * 60 * 1000 + 1) mockConnect.mockResolvedValueOnce({ uri: 'wc:fresh@2', approval: vi.fn().mockResolvedValue(buildSession()) }) const fresh = await service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) expect(fresh).toBe('wc:fresh@2') expect(mockConnect).toHaveBeenCalledTimes(2) }) it('beginPairing dedupes concurrent calls: parallel invocations mint only one pairing', async () => { // Only one client.connect result is queued; if the two concurrent calls each // minted, the second would get an undefined result and throw. mockConnect.mockResolvedValueOnce({ uri: 'wc:one@2', approval: vi.fn().mockResolvedValue(buildSession()) }) const [first, second] = await Promise.all([ service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }), service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) ]) expect(first).toBe('wc:one@2') expect(second).toBe('wc:one@2') expect(mockConnect).toHaveBeenCalledTimes(1) }) it('beginPairing surfaces errors as WalletConnectorError (consistent with connect())', async () => { mockConnect.mockRejectedValueOnce(new Error('relay down')) await expect( service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) ).rejects.toBeInstanceOf(WalletConnectorError) }) it.each(['disconnect', 'clearSession'] as const)( '%s during an in-flight mint prevents the resolved pairing from being adopted', async reset => { // Hold client.connect open so the warm-up mint is still awaiting when // disconnect() runs. let resolveConnect: (r: { uri: string approval: () => Promise }) => void = () => {} mockConnect.mockReturnValueOnce( new Promise(resolve => { resolveConnect = resolve }) ) const pairingPromise = service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) // Let beginPairing reach the in-flight client.connect await. await new Promise(resolve => setTimeout(resolve, 0)) // Reset while the mint is still pending. await service[reset]() // The mint now resolves after the disconnect; it must NOT repopulate // pendingPairing, and (being superseded) it rejects rather than returning an // unadoptable URI. const staleApproval = vi.fn().mockResolvedValue(buildSession()) resolveConnect({ uri: 'wc:stale@2', approval: staleApproval }) await expect(pairingPromise).rejects.toBeInstanceOf(WalletConnectorError) // A subsequent connect() must mint fresh, not adopt the resurrected pairing. const freshApproval = vi.fn().mockResolvedValue(buildSession()) mockConnect.mockResolvedValueOnce({ uri: 'wc:fresh@2', approval: freshApproval }) await service.connect(polygonNetwork, provider) expect(staleApproval).not.toHaveBeenCalled() expect(freshApproval).toHaveBeenCalledTimes(1) expect(mockConnect).toHaveBeenCalledTimes(2) } ) it('beginPairing does not clobber the connectionUri of an in-flight connect', async () => { // A fresh connect (no pending pairing) whose approval we hold open, so it is // parked inside finalizePairing with connectionUri set to its own URI. let resolveApproval: (s: SessionTypes.Struct) => void = () => {} const approval = vi.fn().mockReturnValue( new Promise(resolve => { resolveApproval = resolve }) ) mockConnect.mockResolvedValueOnce({ uri: 'wc:connect@2', approval }) const connectPromise = service.connect(polygonNetwork, provider) // Let connect() reach finalizePairing and publish its URI. await new Promise(resolve => setTimeout(resolve, 0)) expect(service.getConnectionURI()).toBe('wc:connect@2') // A late beginPairing resolves while the connect is still awaiting approval; // it is superseded (isConnecting), so it rejects and does not touch state. mockConnect.mockResolvedValueOnce({ uri: 'wc:late@2', approval: vi.fn() }) await expect( service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) ).rejects.toBeInstanceOf(WalletConnectorError) // The in-flight connect's URI must be untouched. expect(service.getConnectionURI()).toBe('wc:connect@2') resolveApproval(buildSession()) await connectPromise expect(service.getConnectionURI()).toBeNull() }) it.each(['disconnect', 'clearSession'] as const)( '%s clears a pending pairing so a later connect() does not adopt it', async reset => { const staleApproval = vi.fn().mockResolvedValue(buildSession()) mockConnect.mockResolvedValueOnce({ uri: 'wc:pre@2', approval: staleApproval }) await service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) await service[reset]() const freshApproval = vi.fn().mockResolvedValue(buildSession()) mockConnect.mockResolvedValueOnce({ uri: 'wc:fresh@2', approval: freshApproval }) await service.connect(polygonNetwork, provider) // The pre-generated pairing was dropped by disconnect(), so connect() mints // fresh and never touches the stale approval. expect(staleApproval).not.toHaveBeenCalled() expect(freshApproval).toHaveBeenCalledTimes(1) expect(mockConnect).toHaveBeenCalledTimes(2) } ) it('does not leave an orphan pending pairing when a connect() is already in flight', async () => { // A fresh connect (no pending) with a held-open approval, so connectionUri is // set and the connect is in flight. let resolveConnectApproval: (s: SessionTypes.Struct) => void = () => {} const connectApproval = vi.fn().mockReturnValue( new Promise(resolve => { resolveConnectApproval = resolve }) ) mockConnect.mockResolvedValueOnce({ uri: 'wc:connect@2', approval: connectApproval }) const connectPromise = service.connect(polygonNetwork, provider) await new Promise(resolve => setTimeout(resolve, 0)) expect(service.getConnectionURI()).toBe('wc:connect@2') // A warm-up while that connect is in flight fails fast (superseded) without // minting a second pairing. await expect( service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) ).rejects.toBeInstanceOf(WalletConnectorError) // Finish the in-flight connect. resolveConnectApproval(buildSession()) await connectPromise // No orphan pairing was left behind: a later connect() mints fresh. const freshApproval = vi.fn().mockResolvedValue(buildSession()) mockConnect.mockResolvedValueOnce({ uri: 'wc:fresh@2', approval: freshApproval }) await service.connect(polygonNetwork, provider) expect(freshApproval).toHaveBeenCalledTimes(1) // Only the initial connect + the later fresh connect: the warm-up never minted. expect(mockConnect).toHaveBeenCalledTimes(2) }) it('beginPairing throws when the client returns no URI', async () => { mockConnect.mockResolvedValueOnce({ uri: undefined, approval: vi.fn() }) await expect( service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) ).rejects.toThrow('No connection URI generated') }) it('connect() adopts a fresh pending pairing instead of minting a second one', async () => { const approval = vi.fn().mockResolvedValue(buildSession()) mockConnect.mockResolvedValueOnce({ uri: 'wc:pre@2', approval }) await service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) const result = await service.connect(polygonNetwork, provider) // Reuses the pending approval; does NOT call client.connect a second time. expect(mockConnect).toHaveBeenCalledTimes(1) expect(approval).toHaveBeenCalledTimes(1) expect(result.networkId).toBe(polygonNetwork.id) expect(result.address).toMatch(/^0x/) // URI is cleared once approval resolves. expect(service.getConnectionURI()).toBeNull() }) it('connect() mints a fresh pairing when the pending pairing has expired', async () => { vi.useFakeTimers() const staleApproval = vi.fn().mockResolvedValue(buildSession()) mockConnect.mockResolvedValueOnce({ uri: 'wc:stale@2', approval: staleApproval }) await service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) // Past the 4-minute pending-pairing TTL. vi.advanceTimersByTime(4 * 60 * 1000 + 1) const freshApproval = vi.fn().mockResolvedValue(buildSession()) mockConnect.mockResolvedValueOnce({ uri: 'wc:fresh@2', approval: freshApproval }) await service.connect(polygonNetwork, provider) expect(mockConnect).toHaveBeenCalledTimes(2) expect(staleApproval).not.toHaveBeenCalled() expect(freshApproval).toHaveBeenCalledTimes(1) }) it('connect() propagates an adopted-pairing failure instead of silently minting a fresh one', async () => { // A rejection here is a user decline / genuine error; re-prompting the user // with a second pairing would be worse than surfacing the failure. const failingApproval = vi .fn() .mockRejectedValue(new Error('user rejected')) mockConnect.mockResolvedValueOnce({ uri: 'wc:pre@2', approval: failingApproval }) await service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) await expect(service.connect(polygonNetwork, provider)).rejects.toThrow( 'user rejected' ) expect(failingApproval).toHaveBeenCalledTimes(1) // adoption attempted expect(mockConnect).toHaveBeenCalledTimes(1) // only beginPairing's call — no fresh connect }) it('a pending pairing is single-use: a later connect() with none mints fresh', async () => { mockConnect.mockResolvedValueOnce({ uri: 'wc:pre@2', approval: vi.fn().mockResolvedValue(buildSession()) }) await service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) await service.connect(polygonNetwork, provider) // adopts the pending pairing mockConnect.mockResolvedValueOnce({ uri: 'wc:second@2', approval: vi.fn().mockResolvedValue(buildSession()) }) await service.connect(polygonNetwork, provider) // no pending pairing left expect(mockConnect).toHaveBeenCalledTimes(2) }) it('connect() with no prior beginPairing mints a pairing as before', async () => { const approval = vi.fn().mockResolvedValue(buildSession()) mockConnect.mockResolvedValueOnce({ uri: 'wc:direct@2', approval }) const result = await service.connect(polygonNetwork, provider) expect(mockConnect).toHaveBeenCalledTimes(1) expect(approval).toHaveBeenCalledTimes(1) expect(result.address).toMatch(/^0x/) }) it('does not leave an orphan pending pairing when a session is already active', async () => { // Hold a warm-up mint open so it resolves later. let resolveWarm: (r: { uri: string approval: () => Promise }) => void = () => {} mockConnect.mockReturnValueOnce( new Promise(resolve => { resolveWarm = resolve }) ) const warmPromise = service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) await new Promise(resolve => setTimeout(resolve, 0)) // Complete a separate connect() so a session is active and connectionUri is // back to null (isolating the `!currentSession` guard from `!connectionUri`). mockConnect.mockResolvedValueOnce({ uri: 'wc:connect@2', approval: vi.fn().mockResolvedValue(buildSession()) }) await service.connect(polygonNetwork, provider) expect(service.getSession()).not.toBeNull() expect(service.getConnectionURI()).toBeNull() // Release the warm-up: a session is active, so it must NOT publish a pairing; // being superseded, it rejects rather than returning an unadoptable URI. const warmApproval = vi.fn().mockResolvedValue(buildSession()) resolveWarm({ uri: 'wc:warm@2', approval: warmApproval }) await expect(warmPromise).rejects.toBeInstanceOf(WalletConnectorError) // A later connect() must mint fresh, not adopt the orphan warm pairing. const freshApproval = vi.fn().mockResolvedValue(buildSession()) mockConnect.mockResolvedValueOnce({ uri: 'wc:fresh@2', approval: freshApproval }) await service.connect(polygonNetwork, provider) expect(warmApproval).not.toHaveBeenCalled() expect(freshApproval).toHaveBeenCalledTimes(1) expect(mockConnect).toHaveBeenCalledTimes(3) }) it('connect() with no pending pairing throws when the client returns no URI', async () => { mockConnect.mockResolvedValueOnce({ uri: undefined, approval: vi.fn() }) await expect( service.connect(polygonNetwork, provider) ).rejects.toBeInstanceOf(WalletConnectorError) }) it('a beginPairing after disconnect during an in-flight mint starts a fresh mint', async () => { // First warm-up mint, held open. let resolveFirst: (r: { uri: string approval: () => Promise }) => void = () => {} mockConnect.mockReturnValueOnce( new Promise(resolve => { resolveFirst = resolve }) ) const firstPromise = service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) await new Promise(resolve => setTimeout(resolve, 0)) // Reset while the first mint is still pending: clears pendingPairingMint and // bumps the generation. await service.disconnect() // The next beginPairing must start a FRESH mint, not reuse the invalidated // in-flight slot. mockConnect.mockResolvedValueOnce({ uri: 'wc:second@2', approval: vi.fn().mockResolvedValue(buildSession()) }) const second = await service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) // The first mint now resolves post-disconnect: generation-invalidated, so it // rejects (superseded) and must not clobber the second pairing. resolveFirst({ uri: 'wc:first@2', approval: vi.fn().mockResolvedValue(buildSession()) }) await expect(firstPromise).rejects.toBeInstanceOf(WalletConnectorError) expect(second).toBe('wc:second@2') expect(mockConnect).toHaveBeenCalledTimes(2) }) it('does not leave an orphan pending pairing while a connect() is in its pre-finalize phase', async () => { // Hold connect()'s own client.connect open, so it is suspended BEFORE // finalizePairing sets connectionUri: connectionUri is still null, but // isConnecting is true (this is the window the connectionUri check missed). let resolveConnect: (r: { uri: string approval: () => Promise }) => void = () => {} mockConnect.mockReturnValueOnce( new Promise(resolve => { resolveConnect = resolve }) ) const connectPromise = service.connect(polygonNetwork, provider) await new Promise(resolve => setTimeout(resolve, 0)) expect(service.getConnectionURI()).toBeNull() // A warm-up during that pre-finalize window is superseded (isConnecting is // true even though connectionUri is still null), so it fails fast without // minting — this is the window the old connectionUri check missed. await expect( service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) ).rejects.toBeInstanceOf(WalletConnectorError) // Finish the in-flight connect. resolveConnect({ uri: 'wc:connect@2', approval: vi.fn().mockResolvedValue(buildSession()) }) await connectPromise // No orphan pairing was left behind: a later connect() mints fresh. const freshApproval = vi.fn().mockResolvedValue(buildSession()) mockConnect.mockResolvedValueOnce({ uri: 'wc:fresh@2', approval: freshApproval }) await service.connect(polygonNetwork, provider) expect(freshApproval).toHaveBeenCalledTimes(1) // Only the held connect + the later fresh connect: the warm-up never minted. expect(mockConnect).toHaveBeenCalledTimes(2) }) it('beginPairing refuses (and drops a stale cached pairing) when a session is already active', async () => { // Publish a valid pending pairing while idle. mockConnect.mockResolvedValueOnce({ uri: 'wc:warm@2', approval: vi.fn().mockResolvedValue(buildSession()) }) await service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) // A session becomes active without going through connect() (restoration). service.setSession(buildSession()) // A later beginPairing must not reuse the cached URI: it is superseded, so it // fails fast and drops the stale pairing. await expect( service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) ).rejects.toBeInstanceOf(WalletConnectorError) expect(mockConnect).toHaveBeenCalledTimes(1) // no second mint // The stale pairing was dropped, so a later connect() does not adopt it. const freshApproval = vi.fn().mockResolvedValue(buildSession()) mockConnect.mockResolvedValueOnce({ uri: 'wc:fresh@2', approval: freshApproval }) await service.connect(polygonNetwork, provider) expect(freshApproval).toHaveBeenCalledTimes(1) expect(mockConnect).toHaveBeenCalledTimes(2) }) it('rejects a warm-up that started idle when a connect begins mid-mint', async () => { // The warm-up passes the top guard while idle, then parks in its mint's // client.connect await. This exercises mintPairing's isConnecting supersede // leg (distinct from the top-level fail-fast guard). let resolveWarmConnect: (r: { uri: string approval: () => Promise }) => void = () => {} mockConnect.mockReturnValueOnce( new Promise(resolve => { resolveWarmConnect = resolve }) ) const warmPromise = service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) await new Promise(resolve => setTimeout(resolve, 0)) // A connect() begins while the warm-up mint is still awaiting: it sets // isConnecting and parks on its own held approval. let resolveConnectApproval: (s: SessionTypes.Struct) => void = () => {} mockConnect.mockResolvedValueOnce({ uri: 'wc:connect@2', approval: vi.fn().mockReturnValue( new Promise(resolve => { resolveConnectApproval = resolve }) ) }) const connectPromise = service.connect(polygonNetwork, provider) await new Promise(resolve => setTimeout(resolve, 0)) // The warm-up mint now resolves; mintPairing sees isConnecting and rejects // rather than publishing an orphan pending pairing. const warmApproval = vi.fn().mockResolvedValue(buildSession()) resolveWarmConnect({ uri: 'wc:warm@2', approval: warmApproval }) await expect(warmPromise).rejects.toBeInstanceOf(WalletConnectorError) // Finish the connect, then confirm no orphan was published. resolveConnectApproval(buildSession()) await connectPromise const freshApproval = vi.fn().mockResolvedValue(buildSession()) mockConnect.mockResolvedValueOnce({ uri: 'wc:fresh@2', approval: freshApproval }) await service.connect(polygonNetwork, provider) expect(warmApproval).not.toHaveBeenCalled() expect(freshApproval).toHaveBeenCalledTimes(1) }) it('a late-rejecting mint does not wipe a newer in-flight mint slot', async () => { // Mint A, held. let resolveA: (r: { uri: string approval: () => Promise }) => void = () => {} mockConnect.mockReturnValueOnce( new Promise(resolve => { resolveA = resolve }) ) const aPromise = service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) await new Promise(resolve => setTimeout(resolve, 0)) // Reset clears the slot + bumps generation, so A will reject when it resolves. await service.disconnect() // Mint B starts in the now-empty slot, held. let resolveB: (r: { uri: string approval: () => Promise }) => void = () => {} mockConnect.mockReturnValueOnce( new Promise(resolve => { resolveB = resolve }) ) const bPromise = service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) await new Promise(resolve => setTimeout(resolve, 0)) // A resolves post-disconnect → generation-invalidated → rejects. Its finally // must NOT clear B's slot (the owner-check), or dedupe would break. resolveA({ uri: 'wc:a@2', approval: vi.fn().mockResolvedValue(buildSession()) }) await expect(aPromise).rejects.toBeInstanceOf(WalletConnectorError) // A concurrent third call must dedupe onto B's still-in-flight mint. const cPromise = service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) resolveB({ uri: 'wc:b@2', approval: vi.fn().mockResolvedValue(buildSession()) }) const [b, c] = await Promise.all([bPromise, cPromise]) expect(b).toBe('wc:b@2') expect(c).toBe('wc:b@2') // C deduped onto B rather than minting fresh expect(mockConnect).toHaveBeenCalledTimes(2) // A + B only; C added no mint }) it('does not adopt a pending pairing at exactly the TTL boundary', async () => { vi.useFakeTimers() const staleApproval = vi.fn().mockResolvedValue(buildSession()) mockConnect.mockResolvedValueOnce({ uri: 'wc:pre@2', approval: staleApproval }) await service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) // Exactly at expiry: Date.now() === expiresAt. takeValidPendingPairing uses // `>=`, so this is treated as expired (not adopted). vi.advanceTimersByTime(4 * 60 * 1000) const freshApproval = vi.fn().mockResolvedValue(buildSession()) mockConnect.mockResolvedValueOnce({ uri: 'wc:fresh@2', approval: freshApproval }) await service.connect(polygonNetwork, provider) expect(staleApproval).not.toHaveBeenCalled() expect(freshApproval).toHaveBeenCalledTimes(1) expect(mockConnect).toHaveBeenCalledTimes(2) }) it('a connect() whose session yields no address rejects without leaving a half-connected state', async () => { // A session with no account for the requested network makes extractAddresses // throw after approval resolves. const noAddressSession = { ...buildSession(), namespaces: { eip155: { chains: [], accounts: [], methods: [], events: [] } } } as unknown as SessionTypes.Struct mockConnect.mockResolvedValueOnce({ uri: 'wc:noaddr@2', approval: vi.fn().mockResolvedValue(noAddressSession) }) await expect( service.connect(polygonNetwork, provider) ).rejects.toBeInstanceOf(WalletConnectorError) // The failed connect must not leave currentSession set... expect(service.getSession()).toBeNull() // ...and must not block a later warm-up via the currentSession guard. mockConnect.mockResolvedValueOnce({ uri: 'wc:warm@2', approval: vi.fn().mockResolvedValue(buildSession()) }) const uri = await service.beginPairing({ supportedNetworkIds: [polygonNetwork.id] }) expect(uri).toBe('wc:warm@2') }) })