@meshconnect/uwc-tron-connector 0.1.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.
Files changed (80) hide show
  1. package/dist/events/state-machine.d.ts +26 -0
  2. package/dist/events/state-machine.d.ts.map +1 -0
  3. package/dist/events/state-machine.js +21 -0
  4. package/dist/events/state-machine.js.map +1 -0
  5. package/dist/index.d.ts +7 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +3 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/rest/abi.d.ts +23 -0
  10. package/dist/rest/abi.d.ts.map +1 -0
  11. package/dist/rest/abi.js +28 -0
  12. package/dist/rest/abi.js.map +1 -0
  13. package/dist/rest/address.d.ts +45 -0
  14. package/dist/rest/address.d.ts.map +1 -0
  15. package/dist/rest/address.js +124 -0
  16. package/dist/rest/address.js.map +1 -0
  17. package/dist/rest/trongrid-client.d.ts +57 -0
  18. package/dist/rest/trongrid-client.d.ts.map +1 -0
  19. package/dist/rest/trongrid-client.js +133 -0
  20. package/dist/rest/trongrid-client.js.map +1 -0
  21. package/dist/shared/error-utils.d.ts +9 -0
  22. package/dist/shared/error-utils.d.ts.map +1 -0
  23. package/dist/shared/error-utils.js +52 -0
  24. package/dist/shared/error-utils.js.map +1 -0
  25. package/dist/tron-connector.d.ts +85 -0
  26. package/dist/tron-connector.d.ts.map +1 -0
  27. package/dist/tron-connector.js +456 -0
  28. package/dist/tron-connector.js.map +1 -0
  29. package/dist/types.d.ts +8 -0
  30. package/dist/types.d.ts.map +1 -0
  31. package/dist/types.js +2 -0
  32. package/dist/types.js.map +1 -0
  33. package/dist/wallets/base.d.ts +87 -0
  34. package/dist/wallets/base.d.ts.map +1 -0
  35. package/dist/wallets/base.js +118 -0
  36. package/dist/wallets/base.js.map +1 -0
  37. package/dist/wallets/bitget.d.ts +8 -0
  38. package/dist/wallets/bitget.d.ts.map +1 -0
  39. package/dist/wallets/bitget.js +14 -0
  40. package/dist/wallets/bitget.js.map +1 -0
  41. package/dist/wallets/okx.d.ts +4 -0
  42. package/dist/wallets/okx.d.ts.map +1 -0
  43. package/dist/wallets/okx.js +10 -0
  44. package/dist/wallets/okx.js.map +1 -0
  45. package/dist/wallets/registry.d.ts +8 -0
  46. package/dist/wallets/registry.d.ts.map +1 -0
  47. package/dist/wallets/registry.js +18 -0
  48. package/dist/wallets/registry.js.map +1 -0
  49. package/dist/wallets/tokenpocket.d.ts +9 -0
  50. package/dist/wallets/tokenpocket.d.ts.map +1 -0
  51. package/dist/wallets/tokenpocket.js +15 -0
  52. package/dist/wallets/tokenpocket.js.map +1 -0
  53. package/dist/wallets/tronlink.d.ts +8 -0
  54. package/dist/wallets/tronlink.d.ts.map +1 -0
  55. package/dist/wallets/tronlink.js +15 -0
  56. package/dist/wallets/tronlink.js.map +1 -0
  57. package/dist/wallets/trust.d.ts +9 -0
  58. package/dist/wallets/trust.d.ts.map +1 -0
  59. package/dist/wallets/trust.js +15 -0
  60. package/dist/wallets/trust.js.map +1 -0
  61. package/package.json +34 -0
  62. package/src/events/state-machine.ts +44 -0
  63. package/src/index.ts +17 -0
  64. package/src/rest/abi.test.ts +25 -0
  65. package/src/rest/abi.ts +33 -0
  66. package/src/rest/address.test.ts +55 -0
  67. package/src/rest/address.ts +140 -0
  68. package/src/rest/trongrid-client.test.ts +169 -0
  69. package/src/rest/trongrid-client.ts +205 -0
  70. package/src/shared/error-utils.ts +60 -0
  71. package/src/tron-connector.test.ts +612 -0
  72. package/src/tron-connector.ts +568 -0
  73. package/src/types.ts +11 -0
  74. package/src/wallets/base.ts +184 -0
  75. package/src/wallets/bitget.ts +17 -0
  76. package/src/wallets/okx.ts +10 -0
  77. package/src/wallets/registry.ts +26 -0
  78. package/src/wallets/tokenpocket.ts +15 -0
  79. package/src/wallets/tronlink.ts +15 -0
  80. package/src/wallets/trust.ts +18 -0
@@ -0,0 +1,612 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2
+ import type { Network, TronWalletId } from '@meshconnect/uwc-types'
3
+ import { WalletConnectorError } from '@meshconnect/uwc-types'
4
+ import { TronConnector } from './tron-connector'
5
+ import { encodeTrc20TransferParams } from './rest/abi'
6
+
7
+ // Build the raw_data shape the full node returns, so the connector's
8
+ // "built tx matches request" verification (before signing) passes for the
9
+ // happy path. Mismatching any field simulates a tampered/MITM endpoint.
10
+ const nativeRawData = (from: string, to: string, amount: number) => ({
11
+ contract: [
12
+ {
13
+ type: 'TransferContract',
14
+ parameter: { value: { owner_address: from, to_address: to, amount } }
15
+ }
16
+ ]
17
+ })
18
+ const trc20RawData = (
19
+ from: string,
20
+ contractAddress: string,
21
+ to: string,
22
+ amount: number
23
+ ) => ({
24
+ contract: [
25
+ {
26
+ type: 'TriggerSmartContract',
27
+ parameter: {
28
+ value: {
29
+ owner_address: from,
30
+ contract_address: contractAddress,
31
+ data: 'a9059cbb' + encodeTrc20TransferParams(to, amount)
32
+ }
33
+ }
34
+ }
35
+ ]
36
+ })
37
+
38
+ function makeNetwork(id: Network['id'], name = 'Tron'): Network {
39
+ return {
40
+ internalId: 1,
41
+ id,
42
+ namespace: 'tron',
43
+ name,
44
+ logoUrl: '',
45
+ nativeCurrency: { name: 'Tron', symbol: 'TRX', decimals: 6 },
46
+ networkType: 'tron'
47
+ }
48
+ }
49
+ const MAINNET = makeNetwork('tron:0x2b6653dc', 'Tron Mainnet')
50
+ const SHASTA = makeNetwork('tron:0x94a9059e', 'Shasta')
51
+
52
+ // ---- injected-provider stubs -------------------------------------------------
53
+
54
+ function makeProvider(address: string | null) {
55
+ const handlers = new Map<string, Array<(...a: unknown[]) => void>>()
56
+ return {
57
+ tronWeb: {
58
+ defaultAddress: { base58: address },
59
+ trx: {
60
+ sign: vi.fn(async (tx: object) => ({ ...tx, signature: ['SIG'] })),
61
+ signMessageV2: vi.fn(async () => '0xsignature')
62
+ }
63
+ },
64
+ request: vi.fn(async () => ({ code: 200 })),
65
+ on: vi.fn((event: string, handler: (...a: unknown[]) => void) => {
66
+ const list = handlers.get(event) ?? []
67
+ list.push(handler)
68
+ handlers.set(event, list)
69
+ }),
70
+ removeListener: vi.fn(
71
+ (event: string, handler: (...a: unknown[]) => void) => {
72
+ handlers.set(
73
+ event,
74
+ (handlers.get(event) ?? []).filter(h => h !== handler)
75
+ )
76
+ }
77
+ ),
78
+ // Test-only: fire an event the way a wallet would.
79
+ emit(event: string, payload?: unknown) {
80
+ for (const h of handlers.get(event) ?? []) h(payload)
81
+ }
82
+ }
83
+ }
84
+ type Provider = ReturnType<typeof makeProvider>
85
+
86
+ const PATHS: Record<string, string[]> = {
87
+ tronlink: ['tronLink'],
88
+ okx: ['okxwallet', 'tronLink'],
89
+ trust: ['trustWallet', 'tronLink'],
90
+ bitget: ['bitkeep', 'tronLink'],
91
+ tokenpocket: ['tokenpocket', 'tron']
92
+ }
93
+
94
+ function install(walletId: string, provider: Provider): void {
95
+ const path = PATHS[walletId]!
96
+ let node = window as unknown as Record<string, unknown>
97
+ for (let i = 0; i < path.length - 1; i++) {
98
+ const key = path[i]!
99
+ if (!node[key]) node[key] = {}
100
+ node = node[key] as Record<string, unknown>
101
+ }
102
+ node[path[path.length - 1]!] = provider
103
+ }
104
+
105
+ function clearWindow(): void {
106
+ const w = window as unknown as Record<string, unknown>
107
+ for (const key of [
108
+ 'tronLink',
109
+ 'tron',
110
+ 'okxwallet',
111
+ 'trustWallet',
112
+ 'trustwallet',
113
+ 'bitkeep',
114
+ 'tokenpocket'
115
+ ]) {
116
+ delete w[key]
117
+ }
118
+ }
119
+
120
+ // ---- fetch queue -------------------------------------------------------------
121
+
122
+ let fetchQueue: unknown[] = []
123
+ function enqueue(...bodies: unknown[]): void {
124
+ fetchQueue.push(...bodies)
125
+ }
126
+
127
+ beforeEach(() => {
128
+ clearWindow()
129
+ fetchQueue = []
130
+ vi.stubGlobal(
131
+ 'fetch',
132
+ vi.fn(async () => {
133
+ const body = fetchQueue.shift()
134
+ return {
135
+ ok: true,
136
+ status: 200,
137
+ statusText: 'OK',
138
+ json: async () => body ?? {}
139
+ }
140
+ })
141
+ )
142
+ })
143
+ afterEach(() => {
144
+ clearWindow()
145
+ vi.unstubAllGlobals()
146
+ })
147
+
148
+ // Short probe timeouts keep "absent wallet" paths from waiting the full default.
149
+ const fastConfig = (enabledWallets: TronWalletId[]) => ({
150
+ enabledWallets,
151
+ readyStateTimeoutMs: 100,
152
+ discoveryTimeoutMs: 40,
153
+ readyStatePollIntervalMs: 10
154
+ })
155
+
156
+ describe('getAvailableWallets', () => {
157
+ it('returns only enabled wallets that are injected', async () => {
158
+ install('tronlink', makeProvider('TtronlinkAddr'))
159
+ install('okx', makeProvider('TokxAddr'))
160
+ const c = new TronConnector(fastConfig(['tronlink', 'okx', 'trust']))
161
+ const result = await c.getAvailableWallets('tron')
162
+ expect(result.map(r => r.uuid).sort()).toEqual(['okx', 'tronlink'])
163
+ })
164
+
165
+ it('filters by expectedWallets when supplied', async () => {
166
+ install('tronlink', makeProvider('T1'))
167
+ install('okx', makeProvider('T2'))
168
+ const c = new TronConnector(fastConfig(['tronlink', 'okx']))
169
+ const result = await c.getAvailableWallets('tron', [
170
+ { id: 'okx', name: 'OKX', metadata: {} }
171
+ ])
172
+ expect(result.map(r => r.uuid)).toEqual(['okx'])
173
+ })
174
+
175
+ it('returns [] for a non-tron namespace', async () => {
176
+ const c = new TronConnector(fastConfig(['tronlink']))
177
+ expect(await c.getAvailableWallets('eip155')).toEqual([])
178
+ })
179
+
180
+ it('preserves a stable order (matches the registry, not probe timing)', async () => {
181
+ install('tronlink', makeProvider('T1'))
182
+ install('okx', makeProvider('T2'))
183
+ install('trust', makeProvider('T3'))
184
+ const c = new TronConnector(fastConfig(['trust', 'tronlink', 'okx']))
185
+ // resolveAllowedIds walks ALL_TRON_WALLET_IDS order: tronlink, okx, trust.
186
+ const result = await c.getAvailableWallets('tron')
187
+ expect(result.map(r => r.uuid)).toEqual(['tronlink', 'okx', 'trust'])
188
+ })
189
+
190
+ it('detects Trust at both window.trustWallet and window.trustwallet', async () => {
191
+ // Capital-W (repo canonical) — install() uses PATHS.trust = trustWallet.
192
+ install('trust', makeProvider('TtrustAddr'))
193
+ let c = new TronConnector(fastConfig(['trust']))
194
+ expect((await c.getAvailableWallets('tron')).map(r => r.uuid)).toEqual([
195
+ 'trust'
196
+ ])
197
+
198
+ clearWindow()
199
+ // Lowercase fallback.
200
+ ;(window as unknown as Record<string, unknown>)['trustwallet'] = {
201
+ tronLink: makeProvider('TtrustAddr')
202
+ }
203
+ c = new TronConnector(fastConfig(['trust']))
204
+ expect((await c.getAvailableWallets('tron')).map(r => r.uuid)).toEqual([
205
+ 'trust'
206
+ ])
207
+ })
208
+ })
209
+
210
+ describe('connect', () => {
211
+ it('connects an enabled, installed wallet and returns the address', async () => {
212
+ install('tronlink', makeProvider('TtronlinkAddr'))
213
+ const c = new TronConnector(fastConfig(['tronlink']))
214
+ const result = await c.connect(MAINNET, { uuid: 'tronlink' })
215
+ expect(result.networkId).toBe('tron:0x2b6653dc')
216
+ expect(result.address).toBe('TtronlinkAddr')
217
+ expect(result.availableAddresses).toHaveLength(1)
218
+ })
219
+
220
+ it('throws when the wallet is not enabled', async () => {
221
+ install('okx', makeProvider('T'))
222
+ const c = new TronConnector(fastConfig(['tronlink']))
223
+ await expect(c.connect(MAINNET, { uuid: 'okx' })).rejects.toBeInstanceOf(
224
+ WalletConnectorError
225
+ )
226
+ })
227
+
228
+ it('throws on an unknown wallet id', async () => {
229
+ const c = new TronConnector(fastConfig(['tronlink']))
230
+ await expect(
231
+ c.connect(MAINNET, { uuid: 'phantom' })
232
+ ).rejects.toBeInstanceOf(WalletConnectorError)
233
+ })
234
+
235
+ it('throws when no wallet id is provided', async () => {
236
+ const c = new TronConnector(fastConfig(['tronlink']))
237
+ await expect(c.connect(MAINNET)).rejects.toBeInstanceOf(
238
+ WalletConnectorError
239
+ )
240
+ })
241
+
242
+ it('throws on a non-tron network', async () => {
243
+ const c = new TronConnector(fastConfig(['tronlink']))
244
+ await expect(
245
+ c.connect(
246
+ { ...MAINNET, namespace: 'eip155', id: 'eip155:1' },
247
+ { uuid: 'tronlink' }
248
+ )
249
+ ).rejects.toThrow(/expected a 'tron' network/)
250
+ })
251
+
252
+ it('throws when the wallet is not installed', async () => {
253
+ const c = new TronConnector(fastConfig(['tronlink']))
254
+ await expect(c.connect(MAINNET, { uuid: 'tronlink' })).rejects.toThrow(
255
+ /not installed/
256
+ )
257
+ })
258
+
259
+ it('translates a wallet "user rejected" into type:rejected', async () => {
260
+ const provider = makeProvider('TtronlinkAddr')
261
+ provider.request.mockRejectedValueOnce(
262
+ new Error('user rejected the request')
263
+ )
264
+ install('tronlink', provider)
265
+ const c = new TronConnector(fastConfig(['tronlink']))
266
+ await expect(
267
+ c.connect(MAINNET, { uuid: 'tronlink' })
268
+ ).rejects.toMatchObject({ type: 'rejected' })
269
+ })
270
+
271
+ it('throws when the wallet exposes no address after the request', async () => {
272
+ install('tronlink', makeProvider(null))
273
+ const c = new TronConnector(fastConfig(['tronlink']))
274
+ await expect(c.connect(MAINNET, { uuid: 'tronlink' })).rejects.toThrow(
275
+ /no address/
276
+ )
277
+ })
278
+ })
279
+
280
+ describe('sendTransaction', () => {
281
+ it('builds a native transfer via REST, signs via the wallet, and broadcasts', async () => {
282
+ const provider = makeProvider('TFrom')
283
+ install('tronlink', provider)
284
+ const c = new TronConnector(fastConfig(['tronlink']))
285
+ await c.connect(MAINNET, { uuid: 'tronlink' })
286
+
287
+ enqueue(
288
+ {
289
+ txID: 'native1',
290
+ raw_data: nativeRawData('TFrom', 'TTo', 1_000_000),
291
+ visible: true
292
+ },
293
+ { result: true, txid: 'native1' }
294
+ )
295
+ const txid = await c.sendTransaction({
296
+ from: 'TFrom',
297
+ to: 'TTo',
298
+ amount: 1_000_000
299
+ })
300
+ expect(txid).toBe('native1')
301
+ expect(provider.tronWeb.trx.sign).toHaveBeenCalledOnce()
302
+ // The signed (not unsigned) tx is what gets broadcast.
303
+ const broadcastBody = JSON.parse(
304
+ (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[1][1].body
305
+ )
306
+ expect(broadcastBody.signature).toEqual(['SIG'])
307
+ })
308
+
309
+ it('builds a TRC20 transfer via triggersmartcontract', async () => {
310
+ const provider = makeProvider('TFrom')
311
+ install('tronlink', provider)
312
+ const c = new TronConnector(fastConfig(['tronlink']))
313
+ await c.connect(MAINNET, { uuid: 'tronlink' })
314
+
315
+ enqueue(
316
+ {
317
+ result: { result: true },
318
+ transaction: {
319
+ txID: 'trc1',
320
+ raw_data: trc20RawData(
321
+ 'TFrom',
322
+ 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
323
+ 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
324
+ 5_000_000
325
+ )
326
+ }
327
+ },
328
+ { result: true, txid: 'trc1' }
329
+ )
330
+ const txid = await c.sendTransaction({
331
+ from: 'TFrom',
332
+ to: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
333
+ amount: 5_000_000,
334
+ contractAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'
335
+ })
336
+ expect(txid).toBe('trc1')
337
+ const triggerBody = JSON.parse(
338
+ (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body
339
+ )
340
+ expect(triggerBody.function_selector).toBe('transfer(address,uint256)')
341
+ })
342
+
343
+ it('rejects a built tx whose recipient does not match the request (MITM guard)', async () => {
344
+ const provider = makeProvider('TFrom')
345
+ install('tronlink', provider)
346
+ const c = new TronConnector(fastConfig(['tronlink']))
347
+ await c.connect(MAINNET, { uuid: 'tronlink' })
348
+ // Endpoint returns a tx paying a DIFFERENT recipient than requested.
349
+ enqueue({
350
+ txID: 'evil',
351
+ raw_data: nativeRawData('TFrom', 'TEvil', 1_000_000)
352
+ })
353
+ await expect(
354
+ c.sendTransaction({ from: 'TFrom', to: 'TTo', amount: 1_000_000 })
355
+ ).rejects.toBeInstanceOf(WalletConnectorError)
356
+ expect(provider.tronWeb.trx.sign).not.toHaveBeenCalled()
357
+ })
358
+
359
+ it('rejects when `from` does not match the connected address', async () => {
360
+ install('tronlink', makeProvider('TFrom'))
361
+ const c = new TronConnector(fastConfig(['tronlink']))
362
+ await c.connect(MAINNET, { uuid: 'tronlink' })
363
+ await expect(
364
+ c.sendTransaction({ from: 'TSomeoneElse', to: 'TTo', amount: 1 })
365
+ ).rejects.toBeInstanceOf(WalletConnectorError)
366
+ })
367
+
368
+ it('throws when no wallet is connected', async () => {
369
+ const c = new TronConnector(fastConfig(['tronlink']))
370
+ await expect(
371
+ c.sendTransaction({ from: 'a', to: 'b', amount: 1 })
372
+ ).rejects.toBeInstanceOf(WalletConnectorError)
373
+ })
374
+
375
+ it('translates a sign rejection into type:rejected', async () => {
376
+ const provider = makeProvider('TFrom')
377
+ provider.tronWeb.trx.sign.mockRejectedValueOnce(
378
+ new Error('Confirmation declined by user')
379
+ )
380
+ install('tronlink', provider)
381
+ const c = new TronConnector(fastConfig(['tronlink']))
382
+ await c.connect(MAINNET, { uuid: 'tronlink' })
383
+ enqueue({ txID: 'x', raw_data: nativeRawData('TFrom', 'TTo', 1) })
384
+ await expect(
385
+ c.sendTransaction({ from: 'TFrom', to: 'TTo', amount: 1 })
386
+ ).rejects.toMatchObject({ type: 'rejected' })
387
+ })
388
+ })
389
+
390
+ describe('signMessage', () => {
391
+ it('proxies to the wallet tronWeb.trx.signMessageV2', async () => {
392
+ const provider = makeProvider('TFrom')
393
+ install('tronlink', provider)
394
+ const c = new TronConnector(fastConfig(['tronlink']))
395
+ await c.connect(MAINNET, { uuid: 'tronlink' })
396
+ expect(await c.signMessage('hello')).toEqual({
397
+ type: 'standard',
398
+ signature: '0xsignature'
399
+ })
400
+ expect(provider.tronWeb.trx.signMessageV2).toHaveBeenCalledWith('hello')
401
+ })
402
+
403
+ it('throws when no wallet is connected', async () => {
404
+ const c = new TronConnector(fastConfig(['tronlink']))
405
+ await expect(c.signMessage('hi')).rejects.toBeInstanceOf(
406
+ WalletConnectorError
407
+ )
408
+ })
409
+ })
410
+
411
+ describe('disconnect + state', () => {
412
+ it('clears the active wallet and emits state transitions', async () => {
413
+ install('tronlink', makeProvider('TFrom'))
414
+ const c = new TronConnector(fastConfig(['tronlink']))
415
+ const states: string[] = []
416
+ c.subscribe(s => states.push(s.status))
417
+
418
+ await c.connect(MAINNET, { uuid: 'tronlink' })
419
+ await c.disconnect()
420
+
421
+ expect(states).toEqual(['connecting', 'connected', 'disconnected'])
422
+ await expect(c.signMessage('x')).rejects.toBeInstanceOf(
423
+ WalletConnectorError
424
+ )
425
+ })
426
+ })
427
+
428
+ describe('wallet events', () => {
429
+ it('updates the active address on accountsChanged with a string[] payload', async () => {
430
+ const provider = makeProvider('TFrom')
431
+ install('okx', provider)
432
+ const c = new TronConnector(fastConfig(['okx']))
433
+ const seen: Array<string | undefined> = []
434
+ c.subscribe(s => seen.push(s.address))
435
+ await c.connect(MAINNET, { uuid: 'okx' })
436
+
437
+ provider.emit('accountsChanged', ['TSwitched'])
438
+ expect(seen[seen.length - 1]).toBe('TSwitched')
439
+ })
440
+
441
+ it('treats accountsChanged([]) as a disconnect', async () => {
442
+ const provider = makeProvider('TFrom')
443
+ install('okx', provider)
444
+ const c = new TronConnector(fastConfig(['okx']))
445
+ const states: string[] = []
446
+ c.subscribe(s => states.push(s.status))
447
+ await c.connect(MAINNET, { uuid: 'okx' })
448
+
449
+ provider.emit('accountsChanged', [])
450
+ expect(states[states.length - 1]).toBe('disconnected')
451
+ await expect(c.signMessage('x')).rejects.toBeInstanceOf(
452
+ WalletConnectorError
453
+ )
454
+ })
455
+
456
+ it('cleans up listeners on disconnect (no events after teardown)', async () => {
457
+ const provider = makeProvider('TFrom')
458
+ install('tronlink', provider)
459
+ const c = new TronConnector(fastConfig(['tronlink']))
460
+ await c.connect(MAINNET, { uuid: 'tronlink' })
461
+ await c.disconnect()
462
+
463
+ expect(provider.removeListener).toHaveBeenCalled()
464
+ const states: string[] = []
465
+ c.subscribe(s => states.push(s.status))
466
+ // Firing after teardown must not resurrect state.
467
+ provider.emit('accountsChanged', ['TGhost'])
468
+ expect(states).toEqual([])
469
+ })
470
+ })
471
+
472
+ describe('sendTransaction — wallet dropped tronWeb post-connect', () => {
473
+ it('throws a clear WalletConnectorError instead of a raw TypeError', async () => {
474
+ const provider = makeProvider('TFrom')
475
+ install('tronlink', provider)
476
+ const c = new TronConnector(fastConfig(['tronlink']))
477
+ await c.connect(MAINNET, { uuid: 'tronlink' })
478
+ enqueue({ txID: 'x', raw_data: nativeRawData('TFrom', 'TTo', 1) })
479
+ ;(provider as { tronWeb?: unknown }).tronWeb = undefined
480
+
481
+ await expect(
482
+ c.sendTransaction({ from: 'TFrom', to: 'TTo', amount: 1 })
483
+ ).rejects.toBeInstanceOf(WalletConnectorError)
484
+ })
485
+ })
486
+
487
+ describe('sendTransaction — input validation', () => {
488
+ it('rejects a non-integer amount before building (no raw RangeError)', async () => {
489
+ install('tronlink', makeProvider('TFrom'))
490
+ const c = new TronConnector(fastConfig(['tronlink']))
491
+ await c.connect(MAINNET, { uuid: 'tronlink' })
492
+ await expect(
493
+ c.sendTransaction({ from: 'TFrom', to: 'TTo', amount: 1.5 })
494
+ ).rejects.toBeInstanceOf(WalletConnectorError)
495
+ })
496
+
497
+ it('rejects an amount beyond the safe-integer range (silent precision loss)', async () => {
498
+ install('tronlink', makeProvider('TFrom'))
499
+ const c = new TronConnector(fastConfig(['tronlink']))
500
+ await c.connect(MAINNET, { uuid: 'tronlink' })
501
+ await expect(
502
+ c.sendTransaction({
503
+ from: 'TFrom',
504
+ to: 'TTo',
505
+ amount: Number.MAX_SAFE_INTEGER + 2
506
+ })
507
+ ).rejects.toBeInstanceOf(WalletConnectorError)
508
+ })
509
+
510
+ it('rejects a TRC20 recipient whose base58 checksum is invalid', async () => {
511
+ install('tronlink', makeProvider('TFrom'))
512
+ const c = new TronConnector(fastConfig(['tronlink']))
513
+ await c.connect(MAINNET, { uuid: 'tronlink' })
514
+ await expect(
515
+ c.sendTransaction({
516
+ from: 'TFrom',
517
+ to: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6u', // corrupted checksum
518
+ amount: 1_000_000,
519
+ contractAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'
520
+ })
521
+ ).rejects.toBeInstanceOf(WalletConnectorError)
522
+ })
523
+ })
524
+
525
+ describe('connect failure does not leave a stale active connection', () => {
526
+ it('clears the prior connection when a reconnect fails', async () => {
527
+ install('tronlink', makeProvider('TFrom'))
528
+ const okx = makeProvider('TOkx')
529
+ okx.request.mockRejectedValueOnce(new Error('user rejected the request'))
530
+ install('okx', okx)
531
+ const c = new TronConnector(fastConfig(['tronlink', 'okx']))
532
+
533
+ await c.connect(MAINNET, { uuid: 'tronlink' })
534
+ await expect(c.connect(MAINNET, { uuid: 'okx' })).rejects.toBeInstanceOf(
535
+ WalletConnectorError
536
+ )
537
+
538
+ // active must be cleared (not still pointing at tronlink).
539
+ await expect(c.signMessage('x')).rejects.toBeInstanceOf(
540
+ WalletConnectorError
541
+ )
542
+ })
543
+
544
+ it('clears the prior connection when a reconnect targets a not-installed wallet', async () => {
545
+ install('tronlink', makeProvider('TFrom'))
546
+ // okx enabled but not installed → detectProvider returns undefined.
547
+ const c = new TronConnector(fastConfig(['tronlink', 'okx']))
548
+
549
+ await c.connect(MAINNET, { uuid: 'tronlink' })
550
+ await expect(c.connect(MAINNET, { uuid: 'okx' })).rejects.toThrow(
551
+ /not installed/
552
+ )
553
+
554
+ await expect(c.signMessage('x')).rejects.toBeInstanceOf(
555
+ WalletConnectorError
556
+ )
557
+ })
558
+ })
559
+
560
+ describe('wrapper prefers a ready provider over a non-ready stub', () => {
561
+ it('detects Trust when trustWallet is a non-ready stub but trustwallet is ready', async () => {
562
+ const w = window as unknown as Record<string, unknown>
563
+ w['trustWallet'] = { tronLink: {} } // present but no tronWeb → not ready
564
+ w['trustwallet'] = { tronLink: makeProvider('TtrustAddr') }
565
+ const c = new TronConnector(fastConfig(['trust']))
566
+ expect((await c.getAvailableWallets('tron')).map(r => r.uuid)).toEqual([
567
+ 'trust'
568
+ ])
569
+ })
570
+ })
571
+
572
+ describe('switchNetwork', () => {
573
+ it('repoints the node endpoint and returns the current address', async () => {
574
+ install('tronlink', makeProvider('TFrom'))
575
+ const c = new TronConnector(fastConfig(['tronlink']))
576
+ await c.connect(MAINNET, { uuid: 'tronlink' })
577
+ const result = await c.switchNetwork(SHASTA)
578
+ expect(result.networkId).toBe('tron:0x94a9059e')
579
+ expect(result.address).toBe('TFrom')
580
+
581
+ // A subsequent send should hit the Shasta endpoint.
582
+ enqueue(
583
+ { txID: 's1', raw_data: nativeRawData('TFrom', 'TTo', 1) },
584
+ { result: true, txid: 's1' }
585
+ )
586
+ await c.sendTransaction({ from: 'TFrom', to: 'TTo', amount: 1 })
587
+ const url = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0][0]
588
+ expect(url).toContain('shasta')
589
+ })
590
+
591
+ it('throws when called before connect', async () => {
592
+ const c = new TronConnector(fastConfig(['tronlink']))
593
+ await expect(c.switchNetwork(MAINNET)).rejects.toBeInstanceOf(
594
+ WalletConnectorError
595
+ )
596
+ })
597
+
598
+ it('wraps a missing-endpoint failure as WalletConnectorError', async () => {
599
+ install('tronlink', makeProvider('TFrom'))
600
+ const c = new TronConnector(fastConfig(['tronlink']))
601
+ await c.connect(MAINNET, { uuid: 'tronlink' })
602
+ // A Tron network with no default/configured endpoint → getClient throws a
603
+ // raw Error; switchNetwork must surface it as a WalletConnectorError.
604
+ const unknownTron = makeNetwork(
605
+ 'tron:0xdeadbeef' as Network['id'],
606
+ 'Unknown Tron'
607
+ )
608
+ await expect(c.switchNetwork(unknownTron)).rejects.toBeInstanceOf(
609
+ WalletConnectorError
610
+ )
611
+ })
612
+ })