|
| 1 | +import { EventEmitter } from 'node:events' |
1 | 2 | import { describe, expect, it } from 'vitest' |
2 | | -import type { SessionTypes } from '@walletconnect/types' |
| 3 | +import type { SessionTypes, SignClientTypes } from '@walletconnect/types' |
3 | 4 | import { |
| 5 | + awaitWalletAbiApprovedSession, |
4 | 6 | createWalletAbiCaipNetwork, |
| 7 | + createWalletAbiMetadata, |
5 | 8 | resolveWalletAbiNetwork, |
6 | 9 | selectWalletAbiSessions, |
7 | 10 | } from './walletConnectSession' |
@@ -69,6 +72,40 @@ function sessionWith({ |
69 | 72 | } |
70 | 73 | } |
71 | 74 |
|
| 75 | +function createMockApprovalSignClient(initialSessions: SessionTypes.Struct[] = []) { |
| 76 | + const emitter = new EventEmitter() |
| 77 | + const sessions = [...initialSessions] |
| 78 | + |
| 79 | + return { |
| 80 | + signClient: { |
| 81 | + session: { |
| 82 | + getAll() { |
| 83 | + return [...sessions] |
| 84 | + }, |
| 85 | + }, |
| 86 | + on( |
| 87 | + event: 'session_connect', |
| 88 | + listener: (event: SignClientTypes.EventArguments['session_connect']) => void |
| 89 | + ) { |
| 90 | + emitter.on(event, listener) |
| 91 | + }, |
| 92 | + off( |
| 93 | + event: 'session_connect', |
| 94 | + listener: (event: SignClientTypes.EventArguments['session_connect']) => void |
| 95 | + ) { |
| 96 | + emitter.off(event, listener) |
| 97 | + }, |
| 98 | + }, |
| 99 | + addSession(session: SessionTypes.Struct) { |
| 100 | + sessions.unshift(session) |
| 101 | + }, |
| 102 | + emitSessionConnect(session: SessionTypes.Struct) { |
| 103 | + sessions.unshift(session) |
| 104 | + emitter.emit('session_connect', { session }) |
| 105 | + }, |
| 106 | + } |
| 107 | +} |
| 108 | + |
72 | 109 | describe('walletConnectSession', () => { |
73 | 110 | it('keeps the newest wallet_abi session and marks older ones as stale', () => { |
74 | 111 | const chainId = 'walabi:testnet-liquid' |
@@ -105,6 +142,96 @@ describe('walletConnectSession', () => { |
105 | 142 | expect(resolveWalletAbiNetwork('unsupported-network')).toBe('testnet-liquid') |
106 | 143 | }) |
107 | 144 |
|
| 145 | + it('includes a redirect target in WalletConnect metadata for mobile handoff', () => { |
| 146 | + expect(createWalletAbiMetadata('https://app.example/connect?wallet=green')).toEqual({ |
| 147 | + name: 'Simplicity Lending', |
| 148 | + description: 'Wallet ABI WalletConnect session for the Simplicity Lending web app.', |
| 149 | + url: 'https://app.example/connect?wallet=green', |
| 150 | + icons: ['https://app.example/vite.svg'], |
| 151 | + redirect: { |
| 152 | + universal: 'https://app.example/connect?wallet=green', |
| 153 | + }, |
| 154 | + }) |
| 155 | + }) |
| 156 | + |
| 157 | + it('accepts session_connect as a fallback when approval does not settle', async () => { |
| 158 | + const chainId = 'walabi:testnet-liquid' |
| 159 | + const { signClient, emitSessionConnect } = createMockApprovalSignClient() |
| 160 | + const connectedSession = sessionWith({ |
| 161 | + topic: 'connected', |
| 162 | + expiry: 50, |
| 163 | + chainId, |
| 164 | + }) |
| 165 | + |
| 166 | + const approval = awaitWalletAbiApprovedSession({ |
| 167 | + approval: () => new Promise(() => undefined), |
| 168 | + signClient, |
| 169 | + chainId, |
| 170 | + connectTimeoutMs: 500, |
| 171 | + sessionPollMs: 5, |
| 172 | + }) |
| 173 | + |
| 174 | + setTimeout(() => { |
| 175 | + emitSessionConnect(connectedSession) |
| 176 | + }, 10) |
| 177 | + |
| 178 | + await expect(approval).resolves.toMatchObject({ |
| 179 | + topic: 'connected', |
| 180 | + }) |
| 181 | + }) |
| 182 | + |
| 183 | + it('falls back to the stored session when approval rejects after the wallet settles', async () => { |
| 184 | + const chainId = 'walabi:testnet-liquid' |
| 185 | + const { signClient, addSession } = createMockApprovalSignClient() |
| 186 | + const settledSession = sessionWith({ |
| 187 | + topic: 'settled', |
| 188 | + expiry: 60, |
| 189 | + chainId, |
| 190 | + }) |
| 191 | + |
| 192 | + const approval = awaitWalletAbiApprovedSession({ |
| 193 | + approval: async () => { |
| 194 | + setTimeout(() => { |
| 195 | + addSession(settledSession) |
| 196 | + }, 10) |
| 197 | + throw new Error('approval promise failed') |
| 198 | + }, |
| 199 | + signClient, |
| 200 | + chainId, |
| 201 | + connectTimeoutMs: 500, |
| 202 | + approvalRejectionGraceMs: 50, |
| 203 | + sessionPollMs: 5, |
| 204 | + }) |
| 205 | + |
| 206 | + await expect(approval).resolves.toMatchObject({ |
| 207 | + topic: 'settled', |
| 208 | + }) |
| 209 | + }) |
| 210 | + |
| 211 | + it('does not treat a stored session as approved before approval settles', async () => { |
| 212 | + const chainId = 'walabi:testnet-liquid' |
| 213 | + const { signClient, addSession } = createMockApprovalSignClient() |
| 214 | + const storedSession = sessionWith({ |
| 215 | + topic: 'stored-only', |
| 216 | + expiry: 70, |
| 217 | + chainId, |
| 218 | + }) |
| 219 | + |
| 220 | + const approval = awaitWalletAbiApprovedSession({ |
| 221 | + approval: () => new Promise(() => undefined), |
| 222 | + signClient, |
| 223 | + chainId, |
| 224 | + connectTimeoutMs: 50, |
| 225 | + sessionPollMs: 5, |
| 226 | + }) |
| 227 | + |
| 228 | + setTimeout(() => { |
| 229 | + addSession(storedSession) |
| 230 | + }, 10) |
| 231 | + |
| 232 | + await expect(approval).rejects.toThrow('WalletConnect session approval timed out') |
| 233 | + }) |
| 234 | + |
108 | 235 | it('infers wallet abi networks from Liquid address prefixes', () => { |
109 | 236 | expect( |
110 | 237 | inferWalletAbiNetworkFromAddress( |
|
0 commit comments