@meshconnect/uwc-core 0.10.1 → 1.0.0
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/README.md +928 -0
- package/dist/events.d.ts +71 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/managers/event-manager.d.ts +22 -3
- package/dist/managers/event-manager.d.ts.map +1 -1
- package/dist/managers/event-manager.js +63 -7
- package/dist/managers/event-manager.js.map +1 -1
- package/dist/services/connection-service.d.ts +5 -2
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +36 -9
- package/dist/services/connection-service.js.map +1 -1
- package/dist/services/network-switch-service.d.ts +2 -1
- package/dist/services/network-switch-service.d.ts.map +1 -1
- package/dist/services/network-switch-service.js +15 -3
- package/dist/services/network-switch-service.js.map +1 -1
- package/dist/services/signature-service.d.ts +3 -1
- package/dist/services/signature-service.d.ts.map +1 -1
- package/dist/services/signature-service.js +10 -5
- package/dist/services/signature-service.js.map +1 -1
- package/dist/services/transaction-service.d.ts +3 -1
- package/dist/services/transaction-service.d.ts.map +1 -1
- package/dist/services/transaction-service.js +10 -5
- package/dist/services/transaction-service.js.map +1 -1
- package/dist/services/wallet-capabilities-service.d.ts +2 -1
- package/dist/services/wallet-capabilities-service.d.ts.map +1 -1
- package/dist/services/wallet-capabilities-service.js +9 -2
- package/dist/services/wallet-capabilities-service.js.map +1 -1
- package/dist/universal-wallet-connector.d.ts +56 -8
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +299 -194
- package/dist/universal-wallet-connector.js.map +1 -1
- package/dist/utils/abort.d.ts +14 -0
- package/dist/utils/abort.d.ts.map +1 -0
- package/dist/utils/abort.js +31 -0
- package/dist/utils/abort.js.map +1 -0
- package/package.json +2 -2
- package/src/events.ts +73 -0
- package/src/index.ts +11 -8
- package/src/managers/event-manager.test.ts +70 -0
- package/src/managers/event-manager.ts +80 -9
- package/src/services/connection-service.test.ts +148 -19
- package/src/services/connection-service.ts +63 -12
- package/src/services/network-switch-service.ts +22 -3
- package/src/services/signature-service.ts +13 -5
- package/src/services/transaction-service.ts +13 -5
- package/src/services/wallet-capabilities-service.ts +14 -2
- package/src/universal-wallet-connector.test.ts +196 -3
- package/src/universal-wallet-connector.ts +430 -222
- package/src/utils/abort.test.ts +68 -0
- package/src/utils/abort.ts +38 -0
|
@@ -12,6 +12,11 @@ import type {
|
|
|
12
12
|
} from '@meshconnect/uwc-types'
|
|
13
13
|
import type { SessionManager } from '../managers/session-manager'
|
|
14
14
|
import type { EventManager } from '../managers/event-manager'
|
|
15
|
+
import { raceAbort } from '../utils/abort'
|
|
16
|
+
|
|
17
|
+
export interface ServiceOptions {
|
|
18
|
+
signal?: AbortSignal
|
|
19
|
+
}
|
|
15
20
|
|
|
16
21
|
export class ConnectionService {
|
|
17
22
|
private activeConnector: Connector | null = null
|
|
@@ -28,8 +33,11 @@ export class ConnectionService {
|
|
|
28
33
|
async connect(
|
|
29
34
|
connectionMode: ConnectionMode,
|
|
30
35
|
walletId: string,
|
|
31
|
-
networkId?: NetworkId
|
|
36
|
+
networkId?: NetworkId,
|
|
37
|
+
options?: ServiceOptions
|
|
32
38
|
): Promise<void> {
|
|
39
|
+
options?.signal?.throwIfAborted()
|
|
40
|
+
|
|
33
41
|
// Find the wallet
|
|
34
42
|
const wallet = this.findWallet(walletId)
|
|
35
43
|
|
|
@@ -42,24 +50,44 @@ export class ConnectionService {
|
|
|
42
50
|
// Get the appropriate connector
|
|
43
51
|
const connector = this.getConnector(connectionMode)
|
|
44
52
|
|
|
53
|
+
// Emit AFTER validation so a thrown lookup/validation error doesn't leave
|
|
54
|
+
// listeners seeing a "connecting" state for an attempt that never started.
|
|
55
|
+
this.eventManager.emit('connecting', { connectionMode, walletId })
|
|
56
|
+
|
|
45
57
|
// Store the active connector BEFORE starting connection
|
|
46
58
|
// This allows getConnectionURI to work while connection is in progress
|
|
47
59
|
this.activeConnector = connector
|
|
48
60
|
|
|
49
61
|
let result
|
|
50
62
|
try {
|
|
63
|
+
// Race the connector call against the AbortSignal so a mid-flight abort
|
|
64
|
+
// (e.g. user cancels the WalletConnect QR while the prompt is open)
|
|
65
|
+
// rejects the caller's await promptly. The underlying wallet prompt
|
|
66
|
+
// itself can't be cancelled, but the dapp UI is no longer blocked on it.
|
|
51
67
|
if (connectionMode === 'injected') {
|
|
52
|
-
result = await
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
result = await raceAbort(
|
|
69
|
+
connector.connect(
|
|
70
|
+
network,
|
|
71
|
+
provider as
|
|
72
|
+
| ExtensionInjectedProvider
|
|
73
|
+
| IntegratedBrowserInjectedProvider
|
|
74
|
+
),
|
|
75
|
+
options?.signal
|
|
57
76
|
)
|
|
58
77
|
} else if (
|
|
59
78
|
connectionMode === 'tonConnect' ||
|
|
60
79
|
connectionMode === 'walletConnect'
|
|
61
80
|
) {
|
|
62
|
-
result = await
|
|
81
|
+
result = await raceAbort(
|
|
82
|
+
this.connectWithURIPoll(
|
|
83
|
+
connector,
|
|
84
|
+
network,
|
|
85
|
+
provider,
|
|
86
|
+
connectionMode,
|
|
87
|
+
options?.signal
|
|
88
|
+
),
|
|
89
|
+
options?.signal
|
|
90
|
+
)
|
|
63
91
|
} else {
|
|
64
92
|
throw new Error(`Unsupported connection mode: ${connectionMode}`)
|
|
65
93
|
}
|
|
@@ -68,6 +96,10 @@ export class ConnectionService {
|
|
|
68
96
|
throw error
|
|
69
97
|
}
|
|
70
98
|
|
|
99
|
+
// Belt-and-suspenders: if the signal fired between raceAbort resolving
|
|
100
|
+
// and this point, still bail before mutating session.
|
|
101
|
+
options?.signal?.throwIfAborted()
|
|
102
|
+
|
|
71
103
|
// Filter available addresses to only include those for configured networks
|
|
72
104
|
const filteredAvailableAddresses = result.availableAddresses.filter(addr =>
|
|
73
105
|
this.networks.some(n => n.id === addr.networkId)
|
|
@@ -84,22 +116,29 @@ export class ConnectionService {
|
|
|
84
116
|
),
|
|
85
117
|
availableAddresses: filteredAvailableAddresses
|
|
86
118
|
})
|
|
119
|
+
|
|
120
|
+
const session = this.sessionManager.getSession()
|
|
121
|
+
this.eventManager.emit('connected', { session })
|
|
122
|
+
this.eventManager.emit('sessionChanged', { session })
|
|
87
123
|
}
|
|
88
124
|
|
|
89
125
|
private async connectWithURIPoll(
|
|
90
126
|
connector: Connector,
|
|
91
127
|
network: Network,
|
|
92
|
-
provider: SupportedProvider
|
|
128
|
+
provider: SupportedProvider,
|
|
129
|
+
connectionMode: ConnectionMode,
|
|
130
|
+
signal?: AbortSignal
|
|
93
131
|
): Promise<ConnectorResult> {
|
|
94
132
|
const connectPromise = connector.connect(network, provider)
|
|
95
133
|
|
|
96
134
|
let pollingStopped = false
|
|
135
|
+
let emittedUri: string | undefined
|
|
97
136
|
|
|
98
137
|
const pollForURI = setInterval(() => {
|
|
99
138
|
const uri = this.getConnectionURI()
|
|
100
|
-
if (uri) {
|
|
101
|
-
|
|
102
|
-
this.eventManager.
|
|
139
|
+
if (uri && uri !== emittedUri) {
|
|
140
|
+
emittedUri = uri
|
|
141
|
+
this.eventManager.emit('connectionUri', { uri, connectionMode })
|
|
103
142
|
}
|
|
104
143
|
}, 100)
|
|
105
144
|
|
|
@@ -119,8 +158,16 @@ export class ConnectionService {
|
|
|
119
158
|
pollingStopped = true
|
|
120
159
|
clearInterval(pollForURI)
|
|
121
160
|
clearTimeout(watchdogTimeoutId)
|
|
161
|
+
signal?.removeEventListener('abort', stopPolling)
|
|
122
162
|
}
|
|
123
163
|
|
|
164
|
+
// raceAbort unblocks the caller on abort, but the underlying connectPromise
|
|
165
|
+
// can stay pending (the wallet popup isn't cancellable). Without this the
|
|
166
|
+
// interval lives until the watchdog and — once a later connect re-sets
|
|
167
|
+
// activeConnector — emits the new session's URI tagged with this session's
|
|
168
|
+
// connectionMode.
|
|
169
|
+
signal?.addEventListener('abort', stopPolling, { once: true })
|
|
170
|
+
|
|
124
171
|
try {
|
|
125
172
|
return await connectPromise
|
|
126
173
|
} finally {
|
|
@@ -234,7 +281,8 @@ export class ConnectionService {
|
|
|
234
281
|
return network
|
|
235
282
|
}
|
|
236
283
|
|
|
237
|
-
async disconnect(): Promise<void> {
|
|
284
|
+
async disconnect(options?: ServiceOptions): Promise<void> {
|
|
285
|
+
options?.signal?.throwIfAborted()
|
|
238
286
|
try {
|
|
239
287
|
if (
|
|
240
288
|
this.activeConnector &&
|
|
@@ -245,6 +293,9 @@ export class ConnectionService {
|
|
|
245
293
|
} finally {
|
|
246
294
|
this.activeConnector = null
|
|
247
295
|
this.sessionManager.clearSession()
|
|
296
|
+
const session = this.sessionManager.getSession()
|
|
297
|
+
this.eventManager.emit('disconnected', undefined)
|
|
298
|
+
this.eventManager.emit('sessionChanged', { session })
|
|
248
299
|
}
|
|
249
300
|
}
|
|
250
301
|
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
} from '@meshconnect/uwc-types'
|
|
12
12
|
import type { SessionManager } from '../managers/session-manager'
|
|
13
13
|
import type { EventManager } from '../managers/event-manager'
|
|
14
|
+
import type { ServiceOptions } from './connection-service'
|
|
14
15
|
|
|
15
16
|
export class NetworkSwitchService {
|
|
16
17
|
private isLoading = false
|
|
@@ -31,7 +32,12 @@ export class NetworkSwitchService {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
async switchNetwork(
|
|
35
|
+
async switchNetwork(
|
|
36
|
+
networkId: NetworkId,
|
|
37
|
+
options?: ServiceOptions
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
options?.signal?.throwIfAborted()
|
|
40
|
+
|
|
35
41
|
// Check if there's an active connection
|
|
36
42
|
const session = this.sessionManager.getSession()
|
|
37
43
|
if (!session) {
|
|
@@ -104,7 +110,10 @@ export class NetworkSwitchService {
|
|
|
104
110
|
// Set loading states
|
|
105
111
|
this.isLoading = true
|
|
106
112
|
this.isWaitingForUserApproval = requiresApproval
|
|
107
|
-
this.eventManager.
|
|
113
|
+
this.eventManager.emit('networkSwitching', {
|
|
114
|
+
isLoading: true,
|
|
115
|
+
isWaitingForUserApproval: requiresApproval
|
|
116
|
+
})
|
|
108
117
|
|
|
109
118
|
// Perform the network switch and get the new address
|
|
110
119
|
// Pass the appropriate provider based on connection mode
|
|
@@ -129,6 +138,8 @@ export class NetworkSwitchService {
|
|
|
129
138
|
result = await activeConnector.switchNetwork(network)
|
|
130
139
|
}
|
|
131
140
|
|
|
141
|
+
options?.signal?.throwIfAborted()
|
|
142
|
+
|
|
132
143
|
// Filter available addresses to only include those for configured networks
|
|
133
144
|
const filteredAvailableAddresses = result.availableAddresses
|
|
134
145
|
? result.availableAddresses.filter(addr =>
|
|
@@ -144,11 +155,19 @@ export class NetworkSwitchService {
|
|
|
144
155
|
availableAddresses: filteredAvailableAddresses
|
|
145
156
|
})
|
|
146
157
|
})
|
|
158
|
+
|
|
159
|
+
this.eventManager.emit('networkSwitched', { network })
|
|
160
|
+
this.eventManager.emit('sessionChanged', {
|
|
161
|
+
session: this.sessionManager.getSession()
|
|
162
|
+
})
|
|
147
163
|
} finally {
|
|
148
164
|
// Clear loading states
|
|
149
165
|
this.isLoading = false
|
|
150
166
|
this.isWaitingForUserApproval = false
|
|
151
|
-
this.eventManager.
|
|
167
|
+
this.eventManager.emit('networkSwitching', {
|
|
168
|
+
isLoading: false,
|
|
169
|
+
isWaitingForUserApproval: false
|
|
170
|
+
})
|
|
152
171
|
}
|
|
153
172
|
}
|
|
154
173
|
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
EIP712TypedData
|
|
10
10
|
} from '@meshconnect/uwc-types'
|
|
11
11
|
import type { SessionManager } from '../managers/session-manager'
|
|
12
|
+
import type { ServiceOptions } from './connection-service'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Service for handling signature operations across different connector types
|
|
@@ -23,12 +24,15 @@ export class SignatureService {
|
|
|
23
24
|
* Sign a message with the currently connected wallet
|
|
24
25
|
* @param message The message to sign
|
|
25
26
|
* @param provider The wallet provider to use for signing (required for injected connector)
|
|
27
|
+
* @param options Optional cancellation signal
|
|
26
28
|
* @returns A promise that resolves to the signature
|
|
27
29
|
*/
|
|
28
30
|
async signMessage(
|
|
29
31
|
message: string,
|
|
30
|
-
provider?: SupportedProvider
|
|
32
|
+
provider?: SupportedProvider,
|
|
33
|
+
options?: ServiceOptions
|
|
31
34
|
): Promise<SignatureType> {
|
|
35
|
+
options?.signal?.throwIfAborted()
|
|
32
36
|
const session = this.sessionManager.getSession()
|
|
33
37
|
|
|
34
38
|
if (!session.connectionMode) {
|
|
@@ -53,30 +57,34 @@ export class SignatureService {
|
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
// Call the connector's signMessage method with the appropriate provider
|
|
60
|
+
let signature: SignatureType
|
|
56
61
|
if (session.connectionMode === 'injected') {
|
|
57
62
|
if (!provider) {
|
|
58
63
|
throw new Error('Provider is required for injected connector')
|
|
59
64
|
}
|
|
60
|
-
|
|
65
|
+
signature = await connector.signMessage(
|
|
61
66
|
message,
|
|
62
67
|
provider as
|
|
63
68
|
| ExtensionInjectedProvider
|
|
64
69
|
| IntegratedBrowserInjectedProvider
|
|
65
70
|
)
|
|
66
71
|
} else if (session.connectionMode === 'tonConnect') {
|
|
67
|
-
|
|
72
|
+
signature = await connector.signMessage(message)
|
|
68
73
|
} else if (session.connectionMode === 'walletConnect') {
|
|
69
74
|
if (!provider) {
|
|
70
75
|
throw new Error('Provider is required for WalletConnect connector')
|
|
71
76
|
}
|
|
72
|
-
|
|
77
|
+
signature = await connector.signMessage(
|
|
73
78
|
message,
|
|
74
79
|
provider as WalletConnectProvider
|
|
75
80
|
)
|
|
76
81
|
} else {
|
|
77
82
|
// For other connectors that might not need the provider
|
|
78
|
-
|
|
83
|
+
signature = await connector.signMessage(message)
|
|
79
84
|
}
|
|
85
|
+
|
|
86
|
+
options?.signal?.throwIfAborted()
|
|
87
|
+
return signature
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
/**
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
TransactionResult
|
|
10
10
|
} from '@meshconnect/uwc-types'
|
|
11
11
|
import type { SessionManager } from '../managers/session-manager'
|
|
12
|
+
import type { ServiceOptions } from './connection-service'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Service for handling transaction operations across different connector types
|
|
@@ -23,12 +24,15 @@ export class TransactionService {
|
|
|
23
24
|
* Send a transaction with the currently connected wallet
|
|
24
25
|
* @param request The transaction request parameters
|
|
25
26
|
* @param provider The wallet provider to use for sending (required for injected connector)
|
|
27
|
+
* @param options Optional cancellation signal
|
|
26
28
|
* @returns A promise that resolves to the transaction result
|
|
27
29
|
*/
|
|
28
30
|
async sendTransaction(
|
|
29
31
|
request: TransactionRequest,
|
|
30
|
-
provider?: SupportedProvider
|
|
32
|
+
provider?: SupportedProvider,
|
|
33
|
+
options?: ServiceOptions
|
|
31
34
|
): Promise<TransactionResult> {
|
|
35
|
+
options?.signal?.throwIfAborted()
|
|
32
36
|
const session = this.sessionManager.getSession()
|
|
33
37
|
|
|
34
38
|
if (!session.connectionMode) {
|
|
@@ -53,29 +57,33 @@ export class TransactionService {
|
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
// Call the connector's sendTransaction method with the appropriate provider
|
|
60
|
+
let result: TransactionResult
|
|
56
61
|
if (session.connectionMode === 'injected') {
|
|
57
62
|
if (!provider) {
|
|
58
63
|
throw new Error('Provider is required for injected connector')
|
|
59
64
|
}
|
|
60
|
-
|
|
65
|
+
result = await connector.sendTransaction(
|
|
61
66
|
request,
|
|
62
67
|
provider as
|
|
63
68
|
| ExtensionInjectedProvider
|
|
64
69
|
| IntegratedBrowserInjectedProvider
|
|
65
70
|
)
|
|
66
71
|
} else if (session.connectionMode === 'tonConnect') {
|
|
67
|
-
|
|
72
|
+
result = await connector.sendTransaction(request)
|
|
68
73
|
} else if (session.connectionMode === 'walletConnect') {
|
|
69
74
|
if (!provider) {
|
|
70
75
|
throw new Error('Provider is required for WalletConnect connector')
|
|
71
76
|
}
|
|
72
|
-
|
|
77
|
+
result = await connector.sendTransaction(
|
|
73
78
|
request,
|
|
74
79
|
provider as WalletConnectProvider
|
|
75
80
|
)
|
|
76
81
|
} else {
|
|
77
82
|
// For other connectors that might not need the provider
|
|
78
|
-
|
|
83
|
+
result = await connector.sendTransaction(request)
|
|
79
84
|
}
|
|
85
|
+
|
|
86
|
+
options?.signal?.throwIfAborted()
|
|
87
|
+
return result
|
|
80
88
|
}
|
|
81
89
|
}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
} from '@meshconnect/uwc-types'
|
|
7
7
|
import type { SessionManager } from '../managers/session-manager'
|
|
8
8
|
import type { EventManager } from '../managers/event-manager'
|
|
9
|
+
import type { ServiceOptions } from './connection-service'
|
|
9
10
|
|
|
10
11
|
export class WalletCapabilitiesService {
|
|
11
12
|
constructor(
|
|
@@ -17,8 +18,11 @@ export class WalletCapabilitiesService {
|
|
|
17
18
|
|
|
18
19
|
async getWalletCapabilities(
|
|
19
20
|
address: string,
|
|
20
|
-
activeNetwork?: Network
|
|
21
|
+
activeNetwork?: Network,
|
|
22
|
+
options?: ServiceOptions
|
|
21
23
|
): Promise<void> {
|
|
24
|
+
options?.signal?.throwIfAborted()
|
|
25
|
+
|
|
22
26
|
let activeWalletCapabilities: Record<string, EVMCapabilities> = {}
|
|
23
27
|
let activeNetworkWalletCapabilities: EVMCapabilities = {}
|
|
24
28
|
|
|
@@ -49,6 +53,8 @@ export class WalletCapabilitiesService {
|
|
|
49
53
|
capabilities = {}
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
options?.signal?.throwIfAborted()
|
|
57
|
+
|
|
52
58
|
// keep only supported networks
|
|
53
59
|
activeWalletCapabilities = Object.fromEntries(
|
|
54
60
|
Object.entries(capabilities).filter(([networkId, _]) =>
|
|
@@ -71,7 +77,13 @@ export class WalletCapabilitiesService {
|
|
|
71
77
|
activeNetworkWalletCapabilities: null
|
|
72
78
|
})
|
|
73
79
|
}
|
|
74
|
-
|
|
80
|
+
|
|
81
|
+
this.eventManager.emit('capabilitiesUpdated', {
|
|
82
|
+
capabilities: activeWalletCapabilities
|
|
83
|
+
})
|
|
84
|
+
this.eventManager.emit('sessionChanged', {
|
|
85
|
+
session: this.sessionManager.getSession()
|
|
86
|
+
})
|
|
75
87
|
}
|
|
76
88
|
}
|
|
77
89
|
}
|
|
@@ -39,7 +39,11 @@ const mockSessionManager = {
|
|
|
39
39
|
|
|
40
40
|
const mockEventManager = {
|
|
41
41
|
subscribe: vi.fn().mockReturnValue(() => {}),
|
|
42
|
-
notify: vi.fn()
|
|
42
|
+
notify: vi.fn(),
|
|
43
|
+
emit: vi.fn(),
|
|
44
|
+
on: vi.fn().mockReturnValue(() => {}),
|
|
45
|
+
off: vi.fn(),
|
|
46
|
+
once: vi.fn().mockReturnValue(() => {})
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
const mockConnectionService = {
|
|
@@ -139,8 +143,7 @@ describe('UniversalWalletConnector', () => {
|
|
|
139
143
|
let mockWalletConnectConfig: WalletConnectConfig
|
|
140
144
|
|
|
141
145
|
beforeEach(() => {
|
|
142
|
-
|
|
143
|
-
;(UniversalWalletConnector as any).hasWarnedAboutMultipleInstances = false
|
|
146
|
+
UniversalWalletConnector.resetInstance()
|
|
144
147
|
|
|
145
148
|
mockNetworks = [
|
|
146
149
|
{
|
|
@@ -424,6 +427,87 @@ describe('UniversalWalletConnector', () => {
|
|
|
424
427
|
expect(typeof connector.sendTransaction).toBe('function')
|
|
425
428
|
expect(typeof connector.isConnectionModeAvailable).toBe('function')
|
|
426
429
|
expect(typeof connector.getActiveWalletCapabilities).toBe('function')
|
|
430
|
+
expect(typeof connector.on).toBe('function')
|
|
431
|
+
expect(typeof connector.once).toBe('function')
|
|
432
|
+
expect(typeof connector.off).toBe('function')
|
|
433
|
+
})
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
describe('getInstance singleton factory', () => {
|
|
437
|
+
it('creates an instance on first call and reuses it afterwards', () => {
|
|
438
|
+
const a = UniversalWalletConnector.getInstance({
|
|
439
|
+
networks: mockNetworks,
|
|
440
|
+
wallets: mockWallets
|
|
441
|
+
})
|
|
442
|
+
const b = UniversalWalletConnector.getInstance({
|
|
443
|
+
networks: mockNetworks,
|
|
444
|
+
wallets: mockWallets
|
|
445
|
+
})
|
|
446
|
+
expect(a).toBe(b)
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it('throws when called before any instance exists and no config is given', () => {
|
|
450
|
+
expect(() => UniversalWalletConnector.getInstance()).toThrow(
|
|
451
|
+
/getInstance.*before an instance existed/
|
|
452
|
+
)
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
it('resetInstance() clears the shared reference', () => {
|
|
456
|
+
const first = UniversalWalletConnector.getInstance({
|
|
457
|
+
networks: mockNetworks,
|
|
458
|
+
wallets: mockWallets
|
|
459
|
+
})
|
|
460
|
+
UniversalWalletConnector.resetInstance()
|
|
461
|
+
const second = UniversalWalletConnector.getInstance({
|
|
462
|
+
networks: mockNetworks,
|
|
463
|
+
wallets: mockWallets
|
|
464
|
+
})
|
|
465
|
+
expect(second).not.toBe(first)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('adopts a `new`-constructed instance as the singleton', () => {
|
|
469
|
+
const created = new UniversalWalletConnector(mockNetworks, mockWallets)
|
|
470
|
+
expect(UniversalWalletConnector.getInstance()).toBe(created)
|
|
471
|
+
})
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
describe('config-object constructor', () => {
|
|
475
|
+
it('accepts the { networks, wallets, ... } form', () => {
|
|
476
|
+
const connector = new UniversalWalletConnector({
|
|
477
|
+
networks: mockNetworks,
|
|
478
|
+
wallets: mockWallets,
|
|
479
|
+
walletConnectConfig: mockWalletConnectConfig
|
|
480
|
+
})
|
|
481
|
+
expect(connector).toBeInstanceOf(UniversalWalletConnector)
|
|
482
|
+
})
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
describe('AbortSignal support', () => {
|
|
486
|
+
it('connect rejects immediately when signal is already aborted', async () => {
|
|
487
|
+
const connector = new UniversalWalletConnector(mockNetworks, mockWallets)
|
|
488
|
+
const controller = new AbortController()
|
|
489
|
+
controller.abort()
|
|
490
|
+
|
|
491
|
+
// ConnectionService.connect is mocked to return undefined by default,
|
|
492
|
+
// so the abort check is what surfaces the error.
|
|
493
|
+
mockConnectionService.connect = vi.fn().mockImplementation(
|
|
494
|
+
async (
|
|
495
|
+
_m: unknown,
|
|
496
|
+
_w: unknown,
|
|
497
|
+
_n: unknown,
|
|
498
|
+
options?: {
|
|
499
|
+
signal?: AbortSignal
|
|
500
|
+
}
|
|
501
|
+
) => {
|
|
502
|
+
options?.signal?.throwIfAborted()
|
|
503
|
+
}
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
await expect(
|
|
507
|
+
connector.connect('injected', 'metamask', undefined, {
|
|
508
|
+
signal: controller.signal
|
|
509
|
+
})
|
|
510
|
+
).rejects.toThrow()
|
|
427
511
|
})
|
|
428
512
|
})
|
|
429
513
|
|
|
@@ -460,6 +544,115 @@ describe('UniversalWalletConnector', () => {
|
|
|
460
544
|
// Should complete without throwing
|
|
461
545
|
expect(connector.isReady()).toBe(true)
|
|
462
546
|
})
|
|
547
|
+
|
|
548
|
+
it('marks a Tron wallet installed when detected.uuid matches WalletMetadata.id (TronConnector path)', async () => {
|
|
549
|
+
// Mesh-owned TronConnector returns { uuid: TronWalletId } (e.g. "tronlink").
|
|
550
|
+
// The merge must match this directly against WalletMetadata.id, not just
|
|
551
|
+
// against the legacy `tron-${injectedId}` form. Regression guard for the
|
|
552
|
+
// post-rebase break in `applyDetectedWallet`.
|
|
553
|
+
const tronWallet: WalletMetadata = {
|
|
554
|
+
id: 'tronlink',
|
|
555
|
+
name: 'TronLink',
|
|
556
|
+
metadata: {},
|
|
557
|
+
extensionInjectedProvider: {
|
|
558
|
+
supportedNetworkIds: ['tron:728126428'] as const,
|
|
559
|
+
namespaceMetaData: {
|
|
560
|
+
tron: {
|
|
561
|
+
injectedId: 'tronWeb',
|
|
562
|
+
installed: false
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
requiresUserApprovalOnNamespaceSwitch: false
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
mockInjectedConnector.getAvailableWallets.mockImplementation(
|
|
570
|
+
async (namespace: string) =>
|
|
571
|
+
namespace === 'tron' ? [{ uuid: 'tronlink', name: 'TronLink' }] : []
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
new UniversalWalletConnector(mockNetworks, [tronWallet])
|
|
575
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
576
|
+
|
|
577
|
+
const tronMeta = tronWallet.extensionInjectedProvider!.namespaceMetaData
|
|
578
|
+
.tron as { installed?: boolean; detectedWallet?: { uuid: string } }
|
|
579
|
+
expect(tronMeta.installed).toBe(true)
|
|
580
|
+
expect(tronMeta.detectedWallet?.uuid).toBe('tronlink')
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
it('matches a catalog wallet (backend GUID id) via the canonical detected.injectedId (#174 path)', async () => {
|
|
584
|
+
// Mesh-owned TronConnector now reports the canonical `injectedId`
|
|
585
|
+
// directly on DetectedTronWalletInfo. Catalog-driven consumers (Link
|
|
586
|
+
// v1/v2) use a backend GUID as `WalletMetadata.id` — neither the id nor
|
|
587
|
+
// the legacy `tron-${injectedId}` uuid match. The canonical injectedId
|
|
588
|
+
// join key is the only thing that works for them.
|
|
589
|
+
const tronWallet: WalletMetadata = {
|
|
590
|
+
id: '7b3c2a6e-4f5a-4d6b-8e9f-1a2b3c4d5e6f',
|
|
591
|
+
name: 'TronLink',
|
|
592
|
+
metadata: {},
|
|
593
|
+
extensionInjectedProvider: {
|
|
594
|
+
supportedNetworkIds: ['tron:728126428'] as const,
|
|
595
|
+
namespaceMetaData: {
|
|
596
|
+
tron: {
|
|
597
|
+
injectedId: 'tronLink',
|
|
598
|
+
installed: false
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
requiresUserApprovalOnNamespaceSwitch: false
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
mockInjectedConnector.getAvailableWallets.mockImplementation(
|
|
606
|
+
async (namespace: string) =>
|
|
607
|
+
namespace === 'tron'
|
|
608
|
+
? [{ uuid: 'tronlink', name: 'TronLink', injectedId: 'tronLink' }]
|
|
609
|
+
: []
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
new UniversalWalletConnector(mockNetworks, [tronWallet])
|
|
613
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
614
|
+
|
|
615
|
+
const tronMeta = tronWallet.extensionInjectedProvider!.namespaceMetaData
|
|
616
|
+
.tron as { installed?: boolean; detectedWallet?: { uuid: string } }
|
|
617
|
+
expect(tronMeta.installed).toBe(true)
|
|
618
|
+
expect(tronMeta.detectedWallet?.uuid).toBe('tronlink')
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
it('still matches the legacy `tron-${injectedId}` uuid form (window-global discovery)', async () => {
|
|
622
|
+
// Belt-and-suspenders: when the legacy bridge/window-global path
|
|
623
|
+
// produces `tron-tronlink`, the same code path must still match via
|
|
624
|
+
// the injectedId-derived key — both schemes coexist in production.
|
|
625
|
+
const tronWallet: WalletMetadata = {
|
|
626
|
+
id: 'tronlink-legacy',
|
|
627
|
+
name: 'TronLink',
|
|
628
|
+
metadata: {},
|
|
629
|
+
extensionInjectedProvider: {
|
|
630
|
+
supportedNetworkIds: ['tron:728126428'] as const,
|
|
631
|
+
namespaceMetaData: {
|
|
632
|
+
tron: {
|
|
633
|
+
injectedId: 'tronLink',
|
|
634
|
+
installed: false
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
requiresUserApprovalOnNamespaceSwitch: false
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
mockInjectedConnector.getAvailableWallets.mockImplementation(
|
|
642
|
+
async (namespace: string) =>
|
|
643
|
+
namespace === 'tron'
|
|
644
|
+
? [{ uuid: 'tron-tronlink', name: 'TronLink' }]
|
|
645
|
+
: []
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
new UniversalWalletConnector(mockNetworks, [tronWallet])
|
|
649
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
650
|
+
|
|
651
|
+
const tronMeta = tronWallet.extensionInjectedProvider!.namespaceMetaData
|
|
652
|
+
.tron as { installed?: boolean; detectedWallet?: { uuid: string } }
|
|
653
|
+
expect(tronMeta.installed).toBe(true)
|
|
654
|
+
expect(tronMeta.detectedWallet?.uuid).toBe('tron-tronlink')
|
|
655
|
+
})
|
|
463
656
|
})
|
|
464
657
|
|
|
465
658
|
describe('signSolanaTransaction', () => {
|