@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,87 @@
1
+ /**
2
+ * Hardware Wallet Module
3
+ *
4
+ * Support for hardware wallets (Ledger, Trezor) in SIP Protocol.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import {
9
+ * LedgerWalletAdapter,
10
+ * TrezorWalletAdapter,
11
+ * MockLedgerAdapter,
12
+ * } from '@sip-protocol/sdk'
13
+ *
14
+ * // Connect to Ledger
15
+ * const ledger = new LedgerWalletAdapter({
16
+ * chain: 'ethereum',
17
+ * accountIndex: 0,
18
+ * })
19
+ * await ledger.connect()
20
+ *
21
+ * // Connect to Trezor
22
+ * const trezor = new TrezorWalletAdapter({
23
+ * chain: 'ethereum',
24
+ * manifestEmail: 'dev@myapp.com',
25
+ * manifestAppName: 'My DApp',
26
+ * manifestUrl: 'https://myapp.com',
27
+ * })
28
+ * await trezor.connect()
29
+ *
30
+ * // Use mock adapter for testing
31
+ * const mock = new MockLedgerAdapter({
32
+ * chain: 'ethereum',
33
+ * })
34
+ * await mock.connect()
35
+ * ```
36
+ *
37
+ * @remarks
38
+ * Hardware wallet adapters require external dependencies:
39
+ * - Ledger: `@ledgerhq/hw-transport-webusb`, `@ledgerhq/hw-app-eth`
40
+ * - Trezor: `@trezor/connect-web`
41
+ *
42
+ * Mock adapters are included for testing without hardware.
43
+ *
44
+ * @module hardware
45
+ */
46
+
47
+ // Types
48
+ export {
49
+ type HardwareWalletType,
50
+ type LedgerModel,
51
+ type TrezorModel,
52
+ type HardwareConnectionStatus,
53
+ type TransportType,
54
+ type HardwareDeviceInfo,
55
+ type HardwareWalletConfig,
56
+ type LedgerConfig,
57
+ type TrezorConfig,
58
+ type HardwareSignRequest,
59
+ type HardwareEthereumTx,
60
+ type HardwareSignature,
61
+ type HardwareAccount,
62
+ type HardwareTransport,
63
+ type HardwareErrorCodeType,
64
+ HardwareErrorCode,
65
+ HardwareWalletError,
66
+ DerivationPath,
67
+ getDerivationPath,
68
+ supportsWebUSB,
69
+ supportsWebHID,
70
+ supportsWebBluetooth,
71
+ getAvailableTransports,
72
+ } from './types'
73
+
74
+ // Ledger adapter
75
+ export { LedgerWalletAdapter, createLedgerAdapter } from './ledger'
76
+
77
+ // Trezor adapter
78
+ export { TrezorWalletAdapter, createTrezorAdapter } from './trezor'
79
+
80
+ // Mock adapters for testing
81
+ export {
82
+ type MockHardwareConfig,
83
+ MockLedgerAdapter,
84
+ MockTrezorAdapter,
85
+ createMockLedgerAdapter,
86
+ createMockTrezorAdapter,
87
+ } from './mock'
@@ -0,0 +1,628 @@
1
+ /**
2
+ * Ledger Hardware Wallet Adapter
3
+ *
4
+ * Provides integration with Ledger hardware wallets (Nano S, Nano X, etc.)
5
+ * for secure transaction signing.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { LedgerWalletAdapter } from '@sip-protocol/sdk'
10
+ *
11
+ * const ledger = new LedgerWalletAdapter({
12
+ * chain: 'ethereum',
13
+ * accountIndex: 0,
14
+ * })
15
+ *
16
+ * await ledger.connect()
17
+ * const signature = await ledger.signMessage(message)
18
+ * ```
19
+ *
20
+ * @remarks
21
+ * Requires Ledger device with appropriate app installed:
22
+ * - Ethereum: Ethereum app
23
+ * - Solana: Solana app
24
+ *
25
+ * External dependencies (install separately):
26
+ * - @ledgerhq/hw-transport-webusb
27
+ * - @ledgerhq/hw-app-eth (for Ethereum)
28
+ * - @ledgerhq/hw-app-solana (for Solana)
29
+ */
30
+
31
+ import type {
32
+ ChainId,
33
+ HexString,
34
+ Asset,
35
+ Signature,
36
+ UnsignedTransaction,
37
+ SignedTransaction,
38
+ TransactionReceipt,
39
+ } from '@sip-protocol/types'
40
+ import { WalletErrorCode } from '@sip-protocol/types'
41
+ import { BaseWalletAdapter } from '../base-adapter'
42
+ import { WalletError } from '../errors'
43
+ import {
44
+ type LedgerConfig,
45
+ type HardwareDeviceInfo,
46
+ type HardwareAccount,
47
+ type HardwareSignature,
48
+ type HardwareEthereumTx,
49
+ type HardwareTransport,
50
+ HardwareErrorCode,
51
+ HardwareWalletError,
52
+ getDerivationPath,
53
+ supportsWebUSB,
54
+ } from './types'
55
+
56
+ /**
57
+ * Ledger wallet adapter
58
+ *
59
+ * Supports Ethereum and Solana chains via Ledger device apps.
60
+ */
61
+ export class LedgerWalletAdapter extends BaseWalletAdapter {
62
+ readonly chain: ChainId
63
+ readonly name: string = 'ledger'
64
+
65
+ private config: LedgerConfig
66
+ private transport: HardwareTransport | null = null
67
+ private app: LedgerApp | null = null
68
+ private _derivationPath: string
69
+ private _deviceInfo: HardwareDeviceInfo | null = null
70
+ private _account: HardwareAccount | null = null
71
+
72
+ constructor(config: LedgerConfig) {
73
+ super()
74
+ this.chain = config.chain
75
+ this.config = {
76
+ accountIndex: 0,
77
+ timeout: 30000,
78
+ ...config,
79
+ }
80
+ this._derivationPath = config.derivationPath ??
81
+ getDerivationPath(config.chain, config.accountIndex ?? 0)
82
+ }
83
+
84
+ /**
85
+ * Get device information
86
+ */
87
+ get deviceInfo(): HardwareDeviceInfo | null {
88
+ return this._deviceInfo
89
+ }
90
+
91
+ /**
92
+ * Get current derivation path
93
+ */
94
+ get derivationPath(): string {
95
+ return this._derivationPath
96
+ }
97
+
98
+ /**
99
+ * Get current account
100
+ */
101
+ get account(): HardwareAccount | null {
102
+ return this._account
103
+ }
104
+
105
+ /**
106
+ * Connect to Ledger device
107
+ */
108
+ async connect(): Promise<void> {
109
+ if (!supportsWebUSB()) {
110
+ throw new HardwareWalletError(
111
+ 'WebUSB not supported in this browser',
112
+ HardwareErrorCode.TRANSPORT_ERROR,
113
+ 'ledger'
114
+ )
115
+ }
116
+
117
+ this._connectionState = 'connecting'
118
+
119
+ try {
120
+ // Dynamic import of Ledger transport
121
+ // Users must install @ledgerhq/hw-transport-webusb
122
+ const TransportWebUSB = await this.loadTransport()
123
+
124
+ // Open transport
125
+ this.transport = await TransportWebUSB.create() as unknown as HardwareTransport
126
+
127
+ // Load appropriate app
128
+ this.app = await this.loadApp()
129
+
130
+ // Get account from device
131
+ this._account = await this.getAccountFromDevice()
132
+
133
+ this._deviceInfo = {
134
+ manufacturer: 'ledger',
135
+ model: 'unknown', // Would be detected from transport
136
+ isLocked: false,
137
+ currentApp: this.getAppName(),
138
+ }
139
+
140
+ this.setConnected(this._account.address, this._account.publicKey)
141
+ } catch (error) {
142
+ this._connectionState = 'error'
143
+ throw this.handleError(error)
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Disconnect from Ledger device
149
+ */
150
+ async disconnect(): Promise<void> {
151
+ if (this.transport) {
152
+ await this.transport.close()
153
+ this.transport = null
154
+ }
155
+ this.app = null
156
+ this._account = null
157
+ this._deviceInfo = null
158
+ this.setDisconnected()
159
+ }
160
+
161
+ /**
162
+ * Sign a message
163
+ */
164
+ async signMessage(message: Uint8Array): Promise<Signature> {
165
+ this.requireConnected()
166
+
167
+ if (!this.app) {
168
+ throw new HardwareWalletError(
169
+ 'Ledger app not loaded',
170
+ HardwareErrorCode.APP_NOT_OPEN,
171
+ 'ledger'
172
+ )
173
+ }
174
+
175
+ try {
176
+ const sig = await this.signMessageOnDevice(message)
177
+
178
+ return {
179
+ signature: sig.signature,
180
+ publicKey: this._account!.publicKey,
181
+ }
182
+ } catch (error) {
183
+ throw this.handleError(error)
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Sign a transaction
189
+ */
190
+ async signTransaction(tx: UnsignedTransaction): Promise<SignedTransaction> {
191
+ this.requireConnected()
192
+
193
+ if (!this.app) {
194
+ throw new HardwareWalletError(
195
+ 'Ledger app not loaded',
196
+ HardwareErrorCode.APP_NOT_OPEN,
197
+ 'ledger'
198
+ )
199
+ }
200
+
201
+ try {
202
+ const sig = await this.signTransactionOnDevice(tx)
203
+
204
+ return {
205
+ unsigned: tx,
206
+ signatures: [
207
+ {
208
+ signature: sig.signature,
209
+ publicKey: this._account!.publicKey,
210
+ },
211
+ ],
212
+ serialized: sig.signature, // Actual implementation would serialize properly
213
+ }
214
+ } catch (error) {
215
+ throw this.handleError(error)
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Sign and send transaction
221
+ *
222
+ * Note: Hardware wallets can only sign, not send. This returns a signed
223
+ * transaction that must be broadcast separately.
224
+ */
225
+ async signAndSendTransaction(tx: UnsignedTransaction): Promise<TransactionReceipt> {
226
+ // Hardware wallets can't send transactions directly
227
+ // Sign and return as if pending broadcast
228
+ const signed = await this.signTransaction(tx)
229
+
230
+ // Return mock receipt - actual sending requires RPC/network
231
+ return {
232
+ txHash: signed.serialized.slice(0, 66) as HexString,
233
+ status: 'pending',
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Get native token balance
239
+ *
240
+ * Note: Hardware wallets don't track balances - this requires RPC.
241
+ */
242
+ async getBalance(): Promise<bigint> {
243
+ throw new WalletError(
244
+ 'Hardware wallets do not track balances. Use an RPC provider.',
245
+ WalletErrorCode.UNSUPPORTED_OPERATION
246
+ )
247
+ }
248
+
249
+ /**
250
+ * Get token balance
251
+ *
252
+ * Note: Hardware wallets don't track balances - this requires RPC.
253
+ */
254
+ async getTokenBalance(_asset: Asset): Promise<bigint> {
255
+ throw new WalletError(
256
+ 'Hardware wallets do not track balances. Use an RPC provider.',
257
+ WalletErrorCode.UNSUPPORTED_OPERATION
258
+ )
259
+ }
260
+
261
+ // ─── Account Management ─────────────────────────────────────────────────────
262
+
263
+ /**
264
+ * Get multiple accounts from device
265
+ */
266
+ async getAccounts(startIndex: number = 0, count: number = 5): Promise<HardwareAccount[]> {
267
+ this.requireConnected()
268
+
269
+ const accounts: HardwareAccount[] = []
270
+
271
+ for (let i = startIndex; i < startIndex + count; i++) {
272
+ const path = getDerivationPath(this.chain, i)
273
+ const account = await this.getAccountAtPath(path, i)
274
+ accounts.push(account)
275
+ }
276
+
277
+ return accounts
278
+ }
279
+
280
+ /**
281
+ * Switch to different account index
282
+ */
283
+ async switchAccount(accountIndex: number): Promise<HardwareAccount> {
284
+ this.requireConnected()
285
+
286
+ this._derivationPath = getDerivationPath(this.chain, accountIndex)
287
+ this._account = await this.getAccountFromDevice()
288
+
289
+ const previousAddress = this._address
290
+ this.setConnected(this._account.address, this._account.publicKey)
291
+
292
+ if (previousAddress !== this._account.address) {
293
+ this.emitAccountChanged(previousAddress, this._account.address)
294
+ }
295
+
296
+ return this._account
297
+ }
298
+
299
+ // ─── Private Methods ────────────────────────────────────────────────────────
300
+
301
+ /**
302
+ * Load WebUSB transport dynamically
303
+ */
304
+ private async loadTransport(): Promise<TransportWebUSBType> {
305
+ try {
306
+ // @ts-expect-error - Dynamic import
307
+ const module = await import('@ledgerhq/hw-transport-webusb')
308
+ return module.default
309
+ } catch {
310
+ throw new HardwareWalletError(
311
+ 'Failed to load Ledger transport. Install @ledgerhq/hw-transport-webusb',
312
+ HardwareErrorCode.TRANSPORT_ERROR,
313
+ 'ledger'
314
+ )
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Load chain-specific Ledger app
320
+ */
321
+ private async loadApp(): Promise<LedgerApp> {
322
+ if (!this.transport) {
323
+ throw new HardwareWalletError(
324
+ 'Transport not connected',
325
+ HardwareErrorCode.TRANSPORT_ERROR,
326
+ 'ledger'
327
+ )
328
+ }
329
+
330
+ try {
331
+ if (this.chain === 'ethereum') {
332
+ // @ts-expect-error - Dynamic import
333
+ const module = await import('@ledgerhq/hw-app-eth')
334
+ return new module.default(this.transport)
335
+ } else if (this.chain === 'solana') {
336
+ // @ts-expect-error - Dynamic import
337
+ const module = await import('@ledgerhq/hw-app-solana')
338
+ return new module.default(this.transport)
339
+ } else {
340
+ throw new HardwareWalletError(
341
+ `Chain ${this.chain} not supported by Ledger adapter`,
342
+ HardwareErrorCode.UNSUPPORTED,
343
+ 'ledger'
344
+ )
345
+ }
346
+ } catch (error) {
347
+ if (error instanceof HardwareWalletError) throw error
348
+
349
+ throw new HardwareWalletError(
350
+ `Failed to load Ledger app. Install @ledgerhq/hw-app-${this.chain}`,
351
+ HardwareErrorCode.APP_NOT_OPEN,
352
+ 'ledger',
353
+ error
354
+ )
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Get app name for current chain
360
+ */
361
+ private getAppName(): string {
362
+ switch (this.chain) {
363
+ case 'ethereum':
364
+ return 'Ethereum'
365
+ case 'solana':
366
+ return 'Solana'
367
+ default:
368
+ return 'Unknown'
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Get account from device at current derivation path
374
+ */
375
+ private async getAccountFromDevice(): Promise<HardwareAccount> {
376
+ return this.getAccountAtPath(this._derivationPath, this.config.accountIndex ?? 0)
377
+ }
378
+
379
+ /**
380
+ * Get account at specific derivation path
381
+ */
382
+ private async getAccountAtPath(path: string, index: number): Promise<HardwareAccount> {
383
+ if (!this.app) {
384
+ throw new HardwareWalletError(
385
+ 'App not loaded',
386
+ HardwareErrorCode.APP_NOT_OPEN,
387
+ 'ledger'
388
+ )
389
+ }
390
+
391
+ try {
392
+ if (this.chain === 'ethereum') {
393
+ const ethApp = this.app as EthApp
394
+ const { address, publicKey } = await ethApp.getAddress(path)
395
+ return {
396
+ address,
397
+ publicKey: `0x${publicKey}` as HexString,
398
+ derivationPath: path,
399
+ index,
400
+ chain: this.chain,
401
+ }
402
+ } else if (this.chain === 'solana') {
403
+ const solApp = this.app as unknown as SolanaApp
404
+ const { address } = await solApp.getAddress(path)
405
+ return {
406
+ address: address.toString(),
407
+ publicKey: address.toString() as HexString, // Base58 for Solana
408
+ derivationPath: path,
409
+ index,
410
+ chain: this.chain,
411
+ }
412
+ }
413
+
414
+ throw new HardwareWalletError(
415
+ `Unsupported chain: ${this.chain}`,
416
+ HardwareErrorCode.UNSUPPORTED,
417
+ 'ledger'
418
+ )
419
+ } catch (error) {
420
+ throw this.handleError(error)
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Sign message on device
426
+ */
427
+ private async signMessageOnDevice(message: Uint8Array): Promise<HardwareSignature> {
428
+ if (this.chain === 'ethereum') {
429
+ const ethApp = this.app as EthApp
430
+ const messageHex = Buffer.from(message).toString('hex')
431
+ const result = await ethApp.signPersonalMessage(this._derivationPath, messageHex)
432
+
433
+ return {
434
+ r: `0x${result.r}` as HexString,
435
+ s: `0x${result.s}` as HexString,
436
+ v: result.v,
437
+ signature: `0x${result.r}${result.s}${result.v.toString(16).padStart(2, '0')}` as HexString,
438
+ }
439
+ }
440
+
441
+ if (this.chain === 'solana') {
442
+ const solApp = this.app as unknown as SolanaApp
443
+ const result = await solApp.signOffchainMessage(this._derivationPath, message)
444
+
445
+ return {
446
+ r: '0x' as HexString,
447
+ s: '0x' as HexString,
448
+ v: 0,
449
+ signature: `0x${Buffer.from(result.signature).toString('hex')}` as HexString,
450
+ }
451
+ }
452
+
453
+ throw new HardwareWalletError(
454
+ `Message signing not supported for ${this.chain}`,
455
+ HardwareErrorCode.UNSUPPORTED,
456
+ 'ledger'
457
+ )
458
+ }
459
+
460
+ /**
461
+ * Sign transaction on device
462
+ */
463
+ private async signTransactionOnDevice(tx: UnsignedTransaction): Promise<HardwareSignature> {
464
+ if (this.chain === 'ethereum') {
465
+ const ethApp = this.app as EthApp
466
+ const ethTx = tx.data as HardwareEthereumTx
467
+
468
+ // Build raw transaction for signing
469
+ const rawTx = this.buildRawEthereumTx(ethTx)
470
+ const result = await ethApp.signTransaction(this._derivationPath, rawTx)
471
+
472
+ return {
473
+ r: `0x${result.r}` as HexString,
474
+ s: `0x${result.s}` as HexString,
475
+ v: parseInt(result.v, 16),
476
+ signature: `0x${result.r}${result.s}${result.v}` as HexString,
477
+ }
478
+ }
479
+
480
+ if (this.chain === 'solana') {
481
+ const solApp = this.app as unknown as SolanaApp
482
+ const txData = tx.data as Uint8Array
483
+ const result = await solApp.signTransaction(this._derivationPath, txData)
484
+
485
+ return {
486
+ r: '0x' as HexString,
487
+ s: '0x' as HexString,
488
+ v: 0,
489
+ signature: `0x${Buffer.from(result.signature).toString('hex')}` as HexString,
490
+ }
491
+ }
492
+
493
+ throw new HardwareWalletError(
494
+ `Transaction signing not supported for ${this.chain}`,
495
+ HardwareErrorCode.UNSUPPORTED,
496
+ 'ledger'
497
+ )
498
+ }
499
+
500
+ /**
501
+ * Build raw Ethereum transaction for Ledger signing
502
+ */
503
+ private buildRawEthereumTx(tx: HardwareEthereumTx): string {
504
+ // Simplified - actual implementation would use RLP encoding
505
+ // This is a placeholder for the structure
506
+ const fields = [
507
+ tx.nonce,
508
+ tx.gasPrice ?? tx.maxFeePerGas ?? '0x0',
509
+ tx.gasLimit,
510
+ tx.to,
511
+ tx.value,
512
+ tx.data ?? '0x',
513
+ ]
514
+
515
+ return fields.join('').replace(/0x/g, '')
516
+ }
517
+
518
+ /**
519
+ * Handle and transform errors
520
+ */
521
+ private handleError(error: unknown): Error {
522
+ if (error instanceof HardwareWalletError) {
523
+ return error
524
+ }
525
+
526
+ if (error instanceof WalletError) {
527
+ return error
528
+ }
529
+
530
+ const err = error as { statusCode?: number; message?: string; name?: string }
531
+
532
+ // Ledger-specific error codes
533
+ if (err.statusCode) {
534
+ switch (err.statusCode) {
535
+ case 0x6985: // User rejected
536
+ return new HardwareWalletError(
537
+ 'Transaction rejected on device',
538
+ HardwareErrorCode.USER_REJECTED,
539
+ 'ledger'
540
+ )
541
+ case 0x6a80: // Invalid data
542
+ return new HardwareWalletError(
543
+ 'Invalid transaction data',
544
+ HardwareErrorCode.TRANSPORT_ERROR,
545
+ 'ledger'
546
+ )
547
+ case 0x6b00: // Wrong app
548
+ return new HardwareWalletError(
549
+ `Please open the ${this.getAppName()} app on your Ledger`,
550
+ HardwareErrorCode.APP_NOT_OPEN,
551
+ 'ledger'
552
+ )
553
+ case 0x6f00: // Technical error
554
+ return new HardwareWalletError(
555
+ 'Device error. Try reconnecting.',
556
+ HardwareErrorCode.TRANSPORT_ERROR,
557
+ 'ledger'
558
+ )
559
+ }
560
+ }
561
+
562
+ // Generic errors
563
+ if (err.name === 'TransportOpenUserCancelled') {
564
+ return new HardwareWalletError(
565
+ 'Device selection cancelled',
566
+ HardwareErrorCode.USER_REJECTED,
567
+ 'ledger'
568
+ )
569
+ }
570
+
571
+ if (err.message?.includes('No device selected')) {
572
+ return new HardwareWalletError(
573
+ 'No Ledger device selected',
574
+ HardwareErrorCode.DEVICE_NOT_FOUND,
575
+ 'ledger'
576
+ )
577
+ }
578
+
579
+ return new HardwareWalletError(
580
+ err.message ?? 'Unknown Ledger error',
581
+ HardwareErrorCode.TRANSPORT_ERROR,
582
+ 'ledger',
583
+ error
584
+ )
585
+ }
586
+ }
587
+
588
+ // ─── Type Stubs for Dynamic Imports ───────────────────────────────────────────
589
+
590
+ /**
591
+ * Stub type for @ledgerhq/hw-transport-webusb
592
+ */
593
+ interface TransportWebUSBType {
594
+ create(): Promise<HardwareTransport>
595
+ }
596
+
597
+ /**
598
+ * Generic Ledger app interface
599
+ */
600
+ interface LedgerApp {
601
+ getAddress(path: string): Promise<{ address: string; publicKey: string }>
602
+ }
603
+
604
+ /**
605
+ * Ethereum Ledger app interface
606
+ */
607
+ interface EthApp extends LedgerApp {
608
+ signPersonalMessage(path: string, messageHex: string): Promise<{ r: string; s: string; v: number }>
609
+ signTransaction(path: string, rawTx: string): Promise<{ r: string; s: string; v: string }>
610
+ }
611
+
612
+ /**
613
+ * Solana Ledger app interface
614
+ */
615
+ interface SolanaApp {
616
+ getAddress(path: string): Promise<{ address: Uint8Array }>
617
+ signTransaction(path: string, transaction: Uint8Array): Promise<{ signature: Uint8Array }>
618
+ signOffchainMessage(path: string, message: Uint8Array): Promise<{ signature: Uint8Array }>
619
+ }
620
+
621
+ // ─── Factory Function ─────────────────────────────────────────────────────────
622
+
623
+ /**
624
+ * Create a Ledger wallet adapter
625
+ */
626
+ export function createLedgerAdapter(config: LedgerConfig): LedgerWalletAdapter {
627
+ return new LedgerWalletAdapter(config)
628
+ }