@sip-protocol/sdk 0.1.0 → 0.1.4

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.
@@ -0,0 +1,667 @@
1
+ /**
2
+ * Mock Hardware Wallet Adapters
3
+ *
4
+ * Mock implementations of hardware wallet adapters for testing.
5
+ * These simulate Ledger and Trezor device behavior without actual hardware.
6
+ */
7
+
8
+ import type {
9
+ ChainId,
10
+ HexString,
11
+ Asset,
12
+ Signature,
13
+ UnsignedTransaction,
14
+ SignedTransaction,
15
+ TransactionReceipt,
16
+ } from '@sip-protocol/types'
17
+ import { WalletErrorCode } from '@sip-protocol/types'
18
+ import { bytesToHex, randomBytes } from '@noble/hashes/utils'
19
+ import { BaseWalletAdapter } from '../base-adapter'
20
+ import { WalletError } from '../errors'
21
+ import {
22
+ type HardwareWalletConfig,
23
+ type HardwareDeviceInfo,
24
+ type HardwareAccount,
25
+ type HardwareWalletType,
26
+ type LedgerModel,
27
+ type TrezorModel,
28
+ HardwareErrorCode,
29
+ HardwareWalletError,
30
+ getDerivationPath,
31
+ } from './types'
32
+
33
+ // ─── Mock Configuration ─────────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * Mock hardware wallet configuration
37
+ */
38
+ export interface MockHardwareConfig extends HardwareWalletConfig {
39
+ /** Device type to simulate */
40
+ deviceType: HardwareWalletType
41
+ /** Device model */
42
+ model?: LedgerModel | TrezorModel
43
+ /** Simulate device locked state */
44
+ isLocked?: boolean
45
+ /** Simulate signing delay (ms) */
46
+ signingDelay?: number
47
+ /** Simulate user rejection */
48
+ shouldReject?: boolean
49
+ /** Simulate connection failure */
50
+ shouldFailConnect?: boolean
51
+ /** Mock address to return */
52
+ mockAddress?: string
53
+ /** Mock public key to return */
54
+ mockPublicKey?: HexString
55
+ /** Number of accounts available */
56
+ accountCount?: number
57
+ }
58
+
59
+ /**
60
+ * Mock Ledger adapter for testing
61
+ */
62
+ export class MockLedgerAdapter extends BaseWalletAdapter {
63
+ readonly chain: ChainId
64
+ readonly name: string = 'mock-ledger'
65
+
66
+ private config: MockHardwareConfig
67
+ private _derivationPath: string
68
+ private _deviceInfo: HardwareDeviceInfo | null = null
69
+ private _account: HardwareAccount | null = null
70
+ private mockAccounts: HardwareAccount[] = []
71
+
72
+ constructor(config: MockHardwareConfig) {
73
+ super()
74
+ this.chain = config.chain
75
+ this.config = {
76
+ model: 'nanoX',
77
+ accountIndex: 0,
78
+ isLocked: false,
79
+ signingDelay: 100,
80
+ shouldReject: false,
81
+ shouldFailConnect: false,
82
+ accountCount: 5,
83
+ ...config,
84
+ }
85
+ this._derivationPath = config.derivationPath ??
86
+ getDerivationPath(config.chain, config.accountIndex ?? 0)
87
+
88
+ // Generate mock accounts
89
+ this.generateMockAccounts()
90
+ }
91
+
92
+ /**
93
+ * Get device information
94
+ */
95
+ get deviceInfo(): HardwareDeviceInfo | null {
96
+ return this._deviceInfo
97
+ }
98
+
99
+ /**
100
+ * Get current derivation path
101
+ */
102
+ get derivationPath(): string {
103
+ return this._derivationPath
104
+ }
105
+
106
+ /**
107
+ * Get current account
108
+ */
109
+ get account(): HardwareAccount | null {
110
+ return this._account
111
+ }
112
+
113
+ /**
114
+ * Connect to mock Ledger device
115
+ */
116
+ async connect(): Promise<void> {
117
+ this._connectionState = 'connecting'
118
+
119
+ // Simulate connection delay
120
+ await this.delay(200)
121
+
122
+ if (this.config.shouldFailConnect) {
123
+ this._connectionState = 'error'
124
+ throw new HardwareWalletError(
125
+ 'Mock connection failure',
126
+ HardwareErrorCode.DEVICE_NOT_FOUND,
127
+ 'ledger'
128
+ )
129
+ }
130
+
131
+ if (this.config.isLocked) {
132
+ this._connectionState = 'error'
133
+ throw new HardwareWalletError(
134
+ 'Device is locked. Please enter PIN.',
135
+ HardwareErrorCode.DEVICE_LOCKED,
136
+ 'ledger'
137
+ )
138
+ }
139
+
140
+ this._account = this.mockAccounts[this.config.accountIndex ?? 0]
141
+
142
+ this._deviceInfo = {
143
+ manufacturer: 'ledger',
144
+ model: this.config.model as string ?? 'Nano X',
145
+ firmwareVersion: '2.1.0',
146
+ isLocked: false,
147
+ currentApp: this.getAppName(),
148
+ label: 'Mock Ledger',
149
+ deviceId: 'mock-ledger-001',
150
+ }
151
+
152
+ this.setConnected(this._account.address, this._account.publicKey)
153
+ }
154
+
155
+ /**
156
+ * Disconnect from mock device
157
+ */
158
+ async disconnect(): Promise<void> {
159
+ this._account = null
160
+ this._deviceInfo = null
161
+ this.setDisconnected()
162
+ }
163
+
164
+ /**
165
+ * Sign a message
166
+ */
167
+ async signMessage(message: Uint8Array): Promise<Signature> {
168
+ this.requireConnected()
169
+
170
+ // Simulate signing delay
171
+ await this.delay(this.config.signingDelay ?? 100)
172
+
173
+ if (this.config.shouldReject) {
174
+ throw new HardwareWalletError(
175
+ 'User rejected on device',
176
+ HardwareErrorCode.USER_REJECTED,
177
+ 'ledger'
178
+ )
179
+ }
180
+
181
+ // Generate mock signature
182
+ const sig = this.generateMockSignature(message)
183
+
184
+ return {
185
+ signature: sig,
186
+ publicKey: this._account!.publicKey,
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Sign a transaction
192
+ */
193
+ async signTransaction(tx: UnsignedTransaction): Promise<SignedTransaction> {
194
+ this.requireConnected()
195
+
196
+ await this.delay(this.config.signingDelay ?? 100)
197
+
198
+ if (this.config.shouldReject) {
199
+ throw new HardwareWalletError(
200
+ 'Transaction rejected on device',
201
+ HardwareErrorCode.USER_REJECTED,
202
+ 'ledger'
203
+ )
204
+ }
205
+
206
+ const sig = this.generateMockSignature(
207
+ new TextEncoder().encode(JSON.stringify(tx.data))
208
+ )
209
+
210
+ return {
211
+ unsigned: tx,
212
+ signatures: [
213
+ {
214
+ signature: sig,
215
+ publicKey: this._account!.publicKey,
216
+ },
217
+ ],
218
+ serialized: sig,
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Sign and send transaction
224
+ */
225
+ async signAndSendTransaction(tx: UnsignedTransaction): Promise<TransactionReceipt> {
226
+ const signed = await this.signTransaction(tx)
227
+
228
+ return {
229
+ txHash: signed.serialized.slice(0, 66) as HexString,
230
+ status: 'pending',
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Get balance (not supported by hardware wallets)
236
+ */
237
+ async getBalance(): Promise<bigint> {
238
+ throw new WalletError(
239
+ 'Hardware wallets do not track balances',
240
+ WalletErrorCode.UNSUPPORTED_OPERATION
241
+ )
242
+ }
243
+
244
+ /**
245
+ * Get token balance (not supported by hardware wallets)
246
+ */
247
+ async getTokenBalance(_asset: Asset): Promise<bigint> {
248
+ throw new WalletError(
249
+ 'Hardware wallets do not track balances',
250
+ WalletErrorCode.UNSUPPORTED_OPERATION
251
+ )
252
+ }
253
+
254
+ /**
255
+ * Get multiple accounts
256
+ */
257
+ async getAccounts(startIndex: number = 0, count: number = 5): Promise<HardwareAccount[]> {
258
+ this.requireConnected()
259
+ return this.mockAccounts.slice(startIndex, startIndex + count)
260
+ }
261
+
262
+ /**
263
+ * Switch account
264
+ */
265
+ async switchAccount(accountIndex: number): Promise<HardwareAccount> {
266
+ this.requireConnected()
267
+
268
+ if (accountIndex >= this.mockAccounts.length) {
269
+ throw new HardwareWalletError(
270
+ 'Account index out of range',
271
+ HardwareErrorCode.INVALID_PATH,
272
+ 'ledger'
273
+ )
274
+ }
275
+
276
+ const previousAddress = this._address
277
+ this._account = this.mockAccounts[accountIndex]
278
+ this._derivationPath = this._account.derivationPath
279
+
280
+ this.setConnected(this._account.address, this._account.publicKey)
281
+
282
+ if (previousAddress !== this._account.address) {
283
+ this.emitAccountChanged(previousAddress, this._account.address)
284
+ }
285
+
286
+ return this._account
287
+ }
288
+
289
+ // ─── Test Helpers ───────────────────────────────────────────────────────────
290
+
291
+ /**
292
+ * Set whether device should reject signing
293
+ */
294
+ setShouldReject(shouldReject: boolean): void {
295
+ this.config.shouldReject = shouldReject
296
+ }
297
+
298
+ /**
299
+ * Set signing delay
300
+ */
301
+ setSigningDelay(delay: number): void {
302
+ this.config.signingDelay = delay
303
+ }
304
+
305
+ /**
306
+ * Simulate device lock
307
+ */
308
+ simulateLock(): void {
309
+ if (this._deviceInfo) {
310
+ this._deviceInfo.isLocked = true
311
+ }
312
+ this.config.isLocked = true
313
+ }
314
+
315
+ /**
316
+ * Simulate device unlock
317
+ */
318
+ simulateUnlock(): void {
319
+ if (this._deviceInfo) {
320
+ this._deviceInfo.isLocked = false
321
+ }
322
+ this.config.isLocked = false
323
+ }
324
+
325
+ // ─── Private Methods ────────────────────────────────────────────────────────
326
+
327
+ private getAppName(): string {
328
+ switch (this.chain) {
329
+ case 'ethereum':
330
+ return 'Ethereum'
331
+ case 'solana':
332
+ return 'Solana'
333
+ default:
334
+ return 'Unknown'
335
+ }
336
+ }
337
+
338
+ private generateMockAccounts(): void {
339
+ const count = this.config.accountCount ?? 5
340
+
341
+ for (let i = 0; i < count; i++) {
342
+ const path = getDerivationPath(this.chain, i)
343
+ const address = this.config.mockAddress && i === 0
344
+ ? this.config.mockAddress
345
+ : this.generateMockAddress(i)
346
+ const publicKey = this.config.mockPublicKey && i === 0
347
+ ? this.config.mockPublicKey
348
+ : this.generateMockPublicKey(i)
349
+
350
+ this.mockAccounts.push({
351
+ address,
352
+ publicKey,
353
+ derivationPath: path,
354
+ index: i,
355
+ chain: this.chain,
356
+ })
357
+ }
358
+ }
359
+
360
+ private generateMockAddress(index: number): string {
361
+ const bytes = randomBytes(20)
362
+ bytes[0] = index // Make addresses deterministic based on index
363
+ return `0x${bytesToHex(bytes)}`
364
+ }
365
+
366
+ private generateMockPublicKey(index: number): HexString {
367
+ const bytes = randomBytes(33)
368
+ bytes[0] = 0x02 // Compressed public key prefix
369
+ bytes[1] = index
370
+ return `0x${bytesToHex(bytes)}` as HexString
371
+ }
372
+
373
+ private generateMockSignature(data: Uint8Array): HexString {
374
+ const sig = new Uint8Array(65)
375
+ for (let i = 0; i < 32; i++) {
376
+ sig[i] = (data[i % data.length] ?? 0) ^ (i * 7) // r
377
+ sig[32 + i] = (data[i % data.length] ?? 0) ^ (i * 11) // s
378
+ }
379
+ sig[64] = 27 // v
380
+ return `0x${bytesToHex(sig)}` as HexString
381
+ }
382
+
383
+ private delay(ms: number): Promise<void> {
384
+ return new Promise(resolve => setTimeout(resolve, ms))
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Mock Trezor adapter for testing
390
+ */
391
+ export class MockTrezorAdapter extends BaseWalletAdapter {
392
+ readonly chain: ChainId
393
+ readonly name: string = 'mock-trezor'
394
+
395
+ private config: MockHardwareConfig
396
+ private _derivationPath: string
397
+ private _deviceInfo: HardwareDeviceInfo | null = null
398
+ private _account: HardwareAccount | null = null
399
+ private mockAccounts: HardwareAccount[] = []
400
+
401
+ constructor(config: MockHardwareConfig) {
402
+ super()
403
+ this.chain = config.chain
404
+ this.config = {
405
+ model: 'T',
406
+ accountIndex: 0,
407
+ isLocked: false,
408
+ signingDelay: 100,
409
+ shouldReject: false,
410
+ shouldFailConnect: false,
411
+ accountCount: 5,
412
+ ...config,
413
+ }
414
+ this._derivationPath = config.derivationPath ??
415
+ getDerivationPath(config.chain, config.accountIndex ?? 0)
416
+
417
+ this.generateMockAccounts()
418
+ }
419
+
420
+ get deviceInfo(): HardwareDeviceInfo | null {
421
+ return this._deviceInfo
422
+ }
423
+
424
+ get derivationPath(): string {
425
+ return this._derivationPath
426
+ }
427
+
428
+ get account(): HardwareAccount | null {
429
+ return this._account
430
+ }
431
+
432
+ async connect(): Promise<void> {
433
+ this._connectionState = 'connecting'
434
+
435
+ await this.delay(200)
436
+
437
+ if (this.config.shouldFailConnect) {
438
+ this._connectionState = 'error'
439
+ throw new HardwareWalletError(
440
+ 'Mock connection failure',
441
+ HardwareErrorCode.DEVICE_NOT_FOUND,
442
+ 'trezor'
443
+ )
444
+ }
445
+
446
+ if (this.config.isLocked) {
447
+ this._connectionState = 'error'
448
+ throw new HardwareWalletError(
449
+ 'Device requires PIN',
450
+ HardwareErrorCode.DEVICE_LOCKED,
451
+ 'trezor'
452
+ )
453
+ }
454
+
455
+ this._account = this.mockAccounts[this.config.accountIndex ?? 0]
456
+
457
+ this._deviceInfo = {
458
+ manufacturer: 'trezor',
459
+ model: this.config.model as string ?? 'Model T',
460
+ firmwareVersion: '2.5.3',
461
+ isLocked: false,
462
+ label: 'Mock Trezor',
463
+ deviceId: 'mock-trezor-001',
464
+ }
465
+
466
+ this.setConnected(this._account.address, this._account.publicKey)
467
+ }
468
+
469
+ async disconnect(): Promise<void> {
470
+ this._account = null
471
+ this._deviceInfo = null
472
+ this.setDisconnected()
473
+ }
474
+
475
+ async signMessage(message: Uint8Array): Promise<Signature> {
476
+ this.requireConnected()
477
+
478
+ await this.delay(this.config.signingDelay ?? 100)
479
+
480
+ if (this.config.shouldReject) {
481
+ throw new HardwareWalletError(
482
+ 'User rejected on device',
483
+ HardwareErrorCode.USER_REJECTED,
484
+ 'trezor'
485
+ )
486
+ }
487
+
488
+ const sig = this.generateMockSignature(message)
489
+
490
+ return {
491
+ signature: sig,
492
+ publicKey: this._account!.publicKey,
493
+ }
494
+ }
495
+
496
+ async signTransaction(tx: UnsignedTransaction): Promise<SignedTransaction> {
497
+ this.requireConnected()
498
+
499
+ await this.delay(this.config.signingDelay ?? 100)
500
+
501
+ if (this.config.shouldReject) {
502
+ throw new HardwareWalletError(
503
+ 'Transaction rejected on device',
504
+ HardwareErrorCode.USER_REJECTED,
505
+ 'trezor'
506
+ )
507
+ }
508
+
509
+ const sig = this.generateMockSignature(
510
+ new TextEncoder().encode(JSON.stringify(tx.data))
511
+ )
512
+
513
+ return {
514
+ unsigned: tx,
515
+ signatures: [
516
+ {
517
+ signature: sig,
518
+ publicKey: this._account!.publicKey,
519
+ },
520
+ ],
521
+ serialized: sig,
522
+ }
523
+ }
524
+
525
+ async signAndSendTransaction(tx: UnsignedTransaction): Promise<TransactionReceipt> {
526
+ const signed = await this.signTransaction(tx)
527
+
528
+ return {
529
+ txHash: signed.serialized.slice(0, 66) as HexString,
530
+ status: 'pending',
531
+ }
532
+ }
533
+
534
+ async getBalance(): Promise<bigint> {
535
+ throw new WalletError(
536
+ 'Hardware wallets do not track balances',
537
+ WalletErrorCode.UNSUPPORTED_OPERATION
538
+ )
539
+ }
540
+
541
+ async getTokenBalance(_asset: Asset): Promise<bigint> {
542
+ throw new WalletError(
543
+ 'Hardware wallets do not track balances',
544
+ WalletErrorCode.UNSUPPORTED_OPERATION
545
+ )
546
+ }
547
+
548
+ async getAccounts(startIndex: number = 0, count: number = 5): Promise<HardwareAccount[]> {
549
+ this.requireConnected()
550
+ return this.mockAccounts.slice(startIndex, startIndex + count)
551
+ }
552
+
553
+ async switchAccount(accountIndex: number): Promise<HardwareAccount> {
554
+ this.requireConnected()
555
+
556
+ if (accountIndex >= this.mockAccounts.length) {
557
+ throw new HardwareWalletError(
558
+ 'Account index out of range',
559
+ HardwareErrorCode.INVALID_PATH,
560
+ 'trezor'
561
+ )
562
+ }
563
+
564
+ const previousAddress = this._address
565
+ this._account = this.mockAccounts[accountIndex]
566
+ this._derivationPath = this._account.derivationPath
567
+
568
+ this.setConnected(this._account.address, this._account.publicKey)
569
+
570
+ if (previousAddress !== this._account.address) {
571
+ this.emitAccountChanged(previousAddress, this._account.address)
572
+ }
573
+
574
+ return this._account
575
+ }
576
+
577
+ setShouldReject(shouldReject: boolean): void {
578
+ this.config.shouldReject = shouldReject
579
+ }
580
+
581
+ setSigningDelay(delay: number): void {
582
+ this.config.signingDelay = delay
583
+ }
584
+
585
+ simulateLock(): void {
586
+ if (this._deviceInfo) {
587
+ this._deviceInfo.isLocked = true
588
+ }
589
+ this.config.isLocked = true
590
+ }
591
+
592
+ simulateUnlock(): void {
593
+ if (this._deviceInfo) {
594
+ this._deviceInfo.isLocked = false
595
+ }
596
+ this.config.isLocked = false
597
+ }
598
+
599
+ private generateMockAccounts(): void {
600
+ const count = this.config.accountCount ?? 5
601
+
602
+ for (let i = 0; i < count; i++) {
603
+ const path = getDerivationPath(this.chain, i)
604
+ const address = this.config.mockAddress && i === 0
605
+ ? this.config.mockAddress
606
+ : this.generateMockAddress(i)
607
+ const publicKey = this.config.mockPublicKey && i === 0
608
+ ? this.config.mockPublicKey
609
+ : this.generateMockPublicKey(i)
610
+
611
+ this.mockAccounts.push({
612
+ address,
613
+ publicKey,
614
+ derivationPath: path,
615
+ index: i,
616
+ chain: this.chain,
617
+ })
618
+ }
619
+ }
620
+
621
+ private generateMockAddress(index: number): string {
622
+ const bytes = randomBytes(20)
623
+ bytes[0] = index + 100 // Different from Ledger mocks
624
+ return `0x${bytesToHex(bytes)}`
625
+ }
626
+
627
+ private generateMockPublicKey(index: number): HexString {
628
+ const bytes = randomBytes(33)
629
+ bytes[0] = 0x03 // Different compressed prefix
630
+ bytes[1] = index + 100
631
+ return `0x${bytesToHex(bytes)}` as HexString
632
+ }
633
+
634
+ private generateMockSignature(data: Uint8Array): HexString {
635
+ const sig = new Uint8Array(65)
636
+ for (let i = 0; i < 32; i++) {
637
+ sig[i] = (data[i % data.length] ?? 0) ^ (i * 13)
638
+ sig[32 + i] = (data[i % data.length] ?? 0) ^ (i * 17)
639
+ }
640
+ sig[64] = 28
641
+ return `0x${bytesToHex(sig)}` as HexString
642
+ }
643
+
644
+ private delay(ms: number): Promise<void> {
645
+ return new Promise(resolve => setTimeout(resolve, ms))
646
+ }
647
+ }
648
+
649
+ // ─── Factory Functions ────────────────────────────────────────────────────────
650
+
651
+ /**
652
+ * Create a mock Ledger adapter
653
+ */
654
+ export function createMockLedgerAdapter(
655
+ config: Omit<MockHardwareConfig, 'deviceType'>
656
+ ): MockLedgerAdapter {
657
+ return new MockLedgerAdapter({ ...config, deviceType: 'ledger' })
658
+ }
659
+
660
+ /**
661
+ * Create a mock Trezor adapter
662
+ */
663
+ export function createMockTrezorAdapter(
664
+ config: Omit<MockHardwareConfig, 'deviceType'>
665
+ ): MockTrezorAdapter {
666
+ return new MockTrezorAdapter({ ...config, deviceType: 'trezor' })
667
+ }