@sip-protocol/sdk 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.
@@ -0,0 +1,407 @@
1
+ /**
2
+ * Base Wallet Adapter
3
+ *
4
+ * Abstract base class that provides common functionality for wallet adapters.
5
+ * Chain-specific implementations extend this class.
6
+ */
7
+
8
+ import type {
9
+ ChainId,
10
+ HexString,
11
+ Asset,
12
+ WalletAdapter,
13
+ WalletConnectionState,
14
+ Signature,
15
+ UnsignedTransaction,
16
+ SignedTransaction,
17
+ TransactionReceipt,
18
+ WalletEventType,
19
+ WalletEventHandler,
20
+ WalletEvent,
21
+ WalletConnectEvent,
22
+ WalletDisconnectEvent,
23
+ WalletAccountChangedEvent,
24
+ WalletChainChangedEvent,
25
+ WalletErrorEvent,
26
+ } from '@sip-protocol/types'
27
+ import { WalletError, notConnectedError } from './errors'
28
+ import { WalletErrorCode } from '@sip-protocol/types'
29
+
30
+ /**
31
+ * Event emitter for wallet events
32
+ */
33
+ type EventHandlers = {
34
+ [K in WalletEventType]: Set<WalletEventHandler<Extract<WalletEvent, { type: K }>>>
35
+ }
36
+
37
+ /**
38
+ * Abstract base class for wallet adapters
39
+ *
40
+ * Provides:
41
+ * - Event emitter infrastructure
42
+ * - Connection state management
43
+ * - Common validation logic
44
+ *
45
+ * Subclasses must implement:
46
+ * - connect() / disconnect()
47
+ * - signMessage() / signTransaction() / signAndSendTransaction()
48
+ * - getBalance() / getTokenBalance()
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * class MyWalletAdapter extends BaseWalletAdapter {
53
+ * readonly chain = 'solana'
54
+ * readonly name = 'my-wallet'
55
+ *
56
+ * async connect(): Promise<void> {
57
+ * // Implementation
58
+ * }
59
+ *
60
+ * // ... other required methods
61
+ * }
62
+ * ```
63
+ */
64
+ export abstract class BaseWalletAdapter implements WalletAdapter {
65
+ // ── Identity ──────────────────────────────────────────────────────────────
66
+
67
+ abstract readonly chain: ChainId
68
+ abstract readonly name: string
69
+
70
+ protected _address: string = ''
71
+ protected _publicKey: HexString | '' = ''
72
+ protected _connectionState: WalletConnectionState = 'disconnected'
73
+
74
+ get address(): string {
75
+ return this._address
76
+ }
77
+
78
+ get publicKey(): HexString | '' {
79
+ return this._publicKey
80
+ }
81
+
82
+ get connectionState(): WalletConnectionState {
83
+ return this._connectionState
84
+ }
85
+
86
+ // ── Event Handling ────────────────────────────────────────────────────────
87
+
88
+ private eventHandlers: EventHandlers = {
89
+ connect: new Set(),
90
+ disconnect: new Set(),
91
+ accountChanged: new Set(),
92
+ chainChanged: new Set(),
93
+ error: new Set(),
94
+ }
95
+
96
+ /**
97
+ * Subscribe to wallet events
98
+ */
99
+ on<T extends WalletEventType>(
100
+ event: T,
101
+ handler: WalletEventHandler<Extract<WalletEvent, { type: T }>>
102
+ ): void {
103
+ const handlers = this.eventHandlers[event] as Set<typeof handler>
104
+ handlers.add(handler)
105
+ }
106
+
107
+ /**
108
+ * Unsubscribe from wallet events
109
+ */
110
+ off<T extends WalletEventType>(
111
+ event: T,
112
+ handler: WalletEventHandler<Extract<WalletEvent, { type: T }>>
113
+ ): void {
114
+ const handlers = this.eventHandlers[event] as Set<typeof handler>
115
+ handlers.delete(handler)
116
+ }
117
+
118
+ /**
119
+ * Emit a wallet event
120
+ */
121
+ protected emit<T extends WalletEvent>(event: T): void {
122
+ const handlers = this.eventHandlers[event.type] as Set<WalletEventHandler<T>>
123
+ handlers.forEach((handler) => handler(event))
124
+ }
125
+
126
+ /**
127
+ * Emit a connect event
128
+ */
129
+ protected emitConnect(address: string, chain: ChainId): void {
130
+ this.emit({
131
+ type: 'connect',
132
+ address,
133
+ chain,
134
+ timestamp: Date.now(),
135
+ } satisfies WalletConnectEvent)
136
+ }
137
+
138
+ /**
139
+ * Emit a disconnect event
140
+ */
141
+ protected emitDisconnect(reason?: string): void {
142
+ this.emit({
143
+ type: 'disconnect',
144
+ reason,
145
+ timestamp: Date.now(),
146
+ } satisfies WalletDisconnectEvent)
147
+ }
148
+
149
+ /**
150
+ * Emit an account changed event
151
+ */
152
+ protected emitAccountChanged(previousAddress: string, newAddress: string): void {
153
+ this.emit({
154
+ type: 'accountChanged',
155
+ previousAddress,
156
+ newAddress,
157
+ timestamp: Date.now(),
158
+ } satisfies WalletAccountChangedEvent)
159
+ }
160
+
161
+ /**
162
+ * Emit a chain changed event
163
+ */
164
+ protected emitChainChanged(previousChain: ChainId, newChain: ChainId): void {
165
+ this.emit({
166
+ type: 'chainChanged',
167
+ previousChain,
168
+ newChain,
169
+ timestamp: Date.now(),
170
+ } satisfies WalletChainChangedEvent)
171
+ }
172
+
173
+ /**
174
+ * Emit an error event
175
+ */
176
+ protected emitError(code: string, message: string, details?: unknown): void {
177
+ this.emit({
178
+ type: 'error',
179
+ code,
180
+ message,
181
+ details,
182
+ timestamp: Date.now(),
183
+ } satisfies WalletErrorEvent)
184
+ }
185
+
186
+ // ── Connection ────────────────────────────────────────────────────────────
187
+
188
+ /**
189
+ * Check if wallet is connected
190
+ */
191
+ isConnected(): boolean {
192
+ return this._connectionState === 'connected' && this._address !== ''
193
+ }
194
+
195
+ /**
196
+ * Ensure wallet is connected, throw if not
197
+ */
198
+ protected requireConnected(): void {
199
+ if (!this.isConnected()) {
200
+ throw notConnectedError()
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Set connection state and emit events
206
+ */
207
+ protected setConnected(address: string, publicKey: HexString): void {
208
+ this._address = address
209
+ this._publicKey = publicKey
210
+ this._connectionState = 'connected'
211
+ this.emitConnect(address, this.chain)
212
+ }
213
+
214
+ /**
215
+ * Set disconnected state and emit events
216
+ */
217
+ protected setDisconnected(reason?: string): void {
218
+ this._address = ''
219
+ this._publicKey = ''
220
+ this._connectionState = 'disconnected'
221
+ this.emitDisconnect(reason)
222
+ }
223
+
224
+ /**
225
+ * Set error state and emit events
226
+ */
227
+ protected setError(code: string, message: string, details?: unknown): void {
228
+ this._connectionState = 'error'
229
+ this.emitError(code, message, details)
230
+ }
231
+
232
+ // ── Abstract Methods ──────────────────────────────────────────────────────
233
+
234
+ abstract connect(): Promise<void>
235
+ abstract disconnect(): Promise<void>
236
+ abstract signMessage(message: Uint8Array): Promise<Signature>
237
+ abstract signTransaction(tx: UnsignedTransaction): Promise<SignedTransaction>
238
+ abstract signAndSendTransaction(tx: UnsignedTransaction): Promise<TransactionReceipt>
239
+ abstract getBalance(): Promise<bigint>
240
+ abstract getTokenBalance(asset: Asset): Promise<bigint>
241
+ }
242
+
243
+ /**
244
+ * Mock wallet adapter for testing
245
+ *
246
+ * Provides a complete wallet implementation with mock data.
247
+ * Useful for testing and development without real wallet connections.
248
+ *
249
+ * @example
250
+ * ```typescript
251
+ * const mockWallet = new MockWalletAdapter({
252
+ * chain: 'solana',
253
+ * address: 'SoLaNaAddReSS...',
254
+ * balance: 1000000000n, // 1 SOL
255
+ * })
256
+ *
257
+ * await mockWallet.connect()
258
+ * const balance = await mockWallet.getBalance()
259
+ * ```
260
+ */
261
+ export class MockWalletAdapter extends BaseWalletAdapter {
262
+ readonly chain: ChainId
263
+ readonly name: string
264
+
265
+ private mockAddress: string
266
+ private mockPublicKey: HexString
267
+ private mockBalance: bigint
268
+ private mockTokenBalances: Map<string, bigint>
269
+ private shouldFailConnect: boolean
270
+ private shouldFailSign: boolean
271
+
272
+ constructor(options: {
273
+ chain: ChainId
274
+ address?: string
275
+ publicKey?: HexString
276
+ balance?: bigint
277
+ tokenBalances?: Record<string, bigint>
278
+ name?: string
279
+ shouldFailConnect?: boolean
280
+ shouldFailSign?: boolean
281
+ }) {
282
+ super()
283
+ this.chain = options.chain
284
+ this.name = options.name ?? `mock-${options.chain}`
285
+ this.mockAddress = options.address ?? `mock-address-${options.chain}`
286
+ this.mockPublicKey = options.publicKey ?? '0x0000000000000000000000000000000000000000000000000000000000000001'
287
+ this.mockBalance = options.balance ?? 0n
288
+ this.mockTokenBalances = new Map(Object.entries(options.tokenBalances ?? {}))
289
+ this.shouldFailConnect = options.shouldFailConnect ?? false
290
+ this.shouldFailSign = options.shouldFailSign ?? false
291
+ }
292
+
293
+ async connect(): Promise<void> {
294
+ this._connectionState = 'connecting'
295
+
296
+ if (this.shouldFailConnect) {
297
+ this.setError(
298
+ WalletErrorCode.CONNECTION_FAILED,
299
+ 'Mock connection failure'
300
+ )
301
+ throw new WalletError(
302
+ 'Mock connection failure',
303
+ WalletErrorCode.CONNECTION_FAILED
304
+ )
305
+ }
306
+
307
+ // Simulate async connection
308
+ await new Promise((resolve) => setTimeout(resolve, 10))
309
+
310
+ this.setConnected(this.mockAddress, this.mockPublicKey)
311
+ }
312
+
313
+ async disconnect(): Promise<void> {
314
+ this.setDisconnected('User disconnected')
315
+ }
316
+
317
+ async signMessage(message: Uint8Array): Promise<Signature> {
318
+ this.requireConnected()
319
+
320
+ if (this.shouldFailSign) {
321
+ throw new WalletError('Mock signing failure', WalletErrorCode.SIGNING_FAILED)
322
+ }
323
+
324
+ // Create mock signature (64 bytes)
325
+ const mockSig = new Uint8Array(64)
326
+ for (let i = 0; i < 64; i++) {
327
+ mockSig[i] = (message[i % message.length] ?? 0) ^ (i * 7)
328
+ }
329
+
330
+ return {
331
+ signature: ('0x' + Buffer.from(mockSig).toString('hex')) as HexString,
332
+ publicKey: this._publicKey as HexString,
333
+ recoveryId: 0,
334
+ }
335
+ }
336
+
337
+ async signTransaction(tx: UnsignedTransaction): Promise<SignedTransaction> {
338
+ this.requireConnected()
339
+
340
+ if (this.shouldFailSign) {
341
+ throw new WalletError('Mock signing failure', WalletErrorCode.SIGNING_FAILED)
342
+ }
343
+
344
+ const signature = await this.signMessage(
345
+ new TextEncoder().encode(JSON.stringify(tx.data))
346
+ )
347
+
348
+ return {
349
+ unsigned: tx,
350
+ signatures: [signature],
351
+ serialized: ('0x' + Buffer.from(JSON.stringify(tx)).toString('hex')) as HexString,
352
+ }
353
+ }
354
+
355
+ async signAndSendTransaction(tx: UnsignedTransaction): Promise<TransactionReceipt> {
356
+ const signed = await this.signTransaction(tx)
357
+
358
+ // Mock transaction hash
359
+ const txHash = ('0x' + Buffer.from(signed.serialized.slice(2, 66)).toString('hex')) as HexString
360
+
361
+ return {
362
+ txHash,
363
+ status: 'confirmed',
364
+ blockNumber: 12345n,
365
+ feeUsed: 5000n,
366
+ timestamp: Date.now(),
367
+ }
368
+ }
369
+
370
+ async getBalance(): Promise<bigint> {
371
+ this.requireConnected()
372
+ return this.mockBalance
373
+ }
374
+
375
+ async getTokenBalance(asset: Asset): Promise<bigint> {
376
+ this.requireConnected()
377
+ const key = `${asset.chain}:${asset.symbol}`
378
+ return this.mockTokenBalances.get(key) ?? 0n
379
+ }
380
+
381
+ // ── Mock Control Methods ──────────────────────────────────────────────────
382
+
383
+ /**
384
+ * Set mock balance (for testing)
385
+ */
386
+ setMockBalance(balance: bigint): void {
387
+ this.mockBalance = balance
388
+ }
389
+
390
+ /**
391
+ * Set mock token balance (for testing)
392
+ */
393
+ setMockTokenBalance(asset: Asset, balance: bigint): void {
394
+ const key = `${asset.chain}:${asset.symbol}`
395
+ this.mockTokenBalances.set(key, balance)
396
+ }
397
+
398
+ /**
399
+ * Simulate account change (for testing)
400
+ */
401
+ simulateAccountChange(newAddress: string): void {
402
+ const previousAddress = this._address
403
+ this._address = newAddress
404
+ this.mockAddress = newAddress
405
+ this.emitAccountChanged(previousAddress, newAddress)
406
+ }
407
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Wallet-specific errors for SIP SDK
3
+ */
4
+
5
+ import { SIPError, ErrorCode } from '../errors'
6
+ import { WalletErrorCode, type WalletErrorCodeType } from '@sip-protocol/types'
7
+
8
+ /**
9
+ * Error thrown by wallet adapters
10
+ */
11
+ export class WalletError extends SIPError {
12
+ readonly walletCode: WalletErrorCodeType
13
+
14
+ constructor(
15
+ message: string,
16
+ walletCode: WalletErrorCodeType = WalletErrorCode.UNKNOWN,
17
+ options?: { cause?: Error; context?: Record<string, unknown> }
18
+ ) {
19
+ super(message, ErrorCode.WALLET_ERROR, options)
20
+ this.walletCode = walletCode
21
+ this.name = 'WalletError'
22
+ }
23
+
24
+ /**
25
+ * Check if this is a connection-related error
26
+ */
27
+ isConnectionError(): boolean {
28
+ const codes: WalletErrorCodeType[] = [
29
+ WalletErrorCode.NOT_INSTALLED,
30
+ WalletErrorCode.CONNECTION_REJECTED,
31
+ WalletErrorCode.CONNECTION_FAILED,
32
+ WalletErrorCode.NOT_CONNECTED,
33
+ ]
34
+ return codes.includes(this.walletCode)
35
+ }
36
+
37
+ /**
38
+ * Check if this is a signing-related error
39
+ */
40
+ isSigningError(): boolean {
41
+ const codes: WalletErrorCodeType[] = [
42
+ WalletErrorCode.SIGNING_REJECTED,
43
+ WalletErrorCode.SIGNING_FAILED,
44
+ WalletErrorCode.INVALID_MESSAGE,
45
+ ]
46
+ return codes.includes(this.walletCode)
47
+ }
48
+
49
+ /**
50
+ * Check if this is a transaction-related error
51
+ */
52
+ isTransactionError(): boolean {
53
+ const codes: WalletErrorCodeType[] = [
54
+ WalletErrorCode.INSUFFICIENT_FUNDS,
55
+ WalletErrorCode.TRANSACTION_REJECTED,
56
+ WalletErrorCode.TRANSACTION_FAILED,
57
+ WalletErrorCode.INVALID_TRANSACTION,
58
+ ]
59
+ return codes.includes(this.walletCode)
60
+ }
61
+
62
+ /**
63
+ * Check if this is a privacy-related error
64
+ */
65
+ isPrivacyError(): boolean {
66
+ const codes: WalletErrorCodeType[] = [
67
+ WalletErrorCode.STEALTH_NOT_SUPPORTED,
68
+ WalletErrorCode.VIEWING_KEY_NOT_SUPPORTED,
69
+ WalletErrorCode.SHIELDED_NOT_SUPPORTED,
70
+ ]
71
+ return codes.includes(this.walletCode)
72
+ }
73
+
74
+ /**
75
+ * Check if this error was caused by user rejection
76
+ */
77
+ isUserRejection(): boolean {
78
+ const codes: WalletErrorCodeType[] = [
79
+ WalletErrorCode.CONNECTION_REJECTED,
80
+ WalletErrorCode.SIGNING_REJECTED,
81
+ WalletErrorCode.TRANSACTION_REJECTED,
82
+ WalletErrorCode.CHAIN_SWITCH_REJECTED,
83
+ ]
84
+ return codes.includes(this.walletCode)
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Create a WalletError for not connected state
90
+ */
91
+ export function notConnectedError(): WalletError {
92
+ return new WalletError(
93
+ 'Wallet not connected. Call connect() first.',
94
+ WalletErrorCode.NOT_CONNECTED
95
+ )
96
+ }
97
+
98
+ /**
99
+ * Create a WalletError for feature not supported
100
+ */
101
+ export function featureNotSupportedError(
102
+ feature: string,
103
+ code: WalletErrorCodeType = WalletErrorCode.UNKNOWN
104
+ ): WalletError {
105
+ return new WalletError(`${feature} is not supported by this wallet`, code)
106
+ }