@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,522 @@
1
+ /**
2
+ * Mock Solana Wallet Adapter
3
+ *
4
+ * Testing implementation of Solana wallet adapter.
5
+ * Provides full mock functionality without requiring browser environment.
6
+ */
7
+
8
+ import type {
9
+ HexString,
10
+ Asset,
11
+ Signature,
12
+ UnsignedTransaction,
13
+ SignedTransaction,
14
+ TransactionReceipt,
15
+ } from '@sip-protocol/types'
16
+ import { WalletErrorCode } from '@sip-protocol/types'
17
+ import { BaseWalletAdapter } from '../base-adapter'
18
+ import { WalletError } from '../errors'
19
+ import type {
20
+ SolanaWalletProvider,
21
+ SolanaPublicKey,
22
+ SolanaTransaction,
23
+ SolanaVersionedTransaction,
24
+ SolanaCluster,
25
+ SolanaConnection,
26
+ } from './types'
27
+
28
+ /**
29
+ * Mock Solana public key
30
+ */
31
+ class MockPublicKey implements SolanaPublicKey {
32
+ private base58: string
33
+ private bytes: Uint8Array
34
+
35
+ constructor(base58OrBytes: string | Uint8Array) {
36
+ if (typeof base58OrBytes === 'string') {
37
+ this.base58 = base58OrBytes
38
+ // Create deterministic bytes from base58
39
+ this.bytes = new Uint8Array(32)
40
+ for (let i = 0; i < 32; i++) {
41
+ this.bytes[i] = base58OrBytes.charCodeAt(i % base58OrBytes.length) ^ i
42
+ }
43
+ } else {
44
+ this.bytes = base58OrBytes
45
+ // Create deterministic base58 from bytes
46
+ this.base58 = 'Mock' + Buffer.from(base58OrBytes.slice(0, 8)).toString('hex')
47
+ }
48
+ }
49
+
50
+ toBase58(): string {
51
+ return this.base58
52
+ }
53
+
54
+ toBytes(): Uint8Array {
55
+ return this.bytes
56
+ }
57
+
58
+ toString(): string {
59
+ return this.base58
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Mock Solana transaction
65
+ */
66
+ class MockSolanaTransaction implements SolanaTransaction {
67
+ signature?: Uint8Array
68
+ private data: Uint8Array
69
+
70
+ constructor(data?: unknown) {
71
+ this.data = new TextEncoder().encode(JSON.stringify(data ?? { mock: true }))
72
+ }
73
+
74
+ serialize(): Uint8Array {
75
+ return this.data
76
+ }
77
+
78
+ addSignature(_pubkey: SolanaPublicKey, signature: Uint8Array): void {
79
+ this.signature = signature
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Configuration for mock Solana adapter
85
+ */
86
+ export interface MockSolanaAdapterConfig {
87
+ /** Mock address (base58) */
88
+ address?: string
89
+ /** Mock balance in lamports */
90
+ balance?: bigint
91
+ /** Token balances by mint address */
92
+ tokenBalances?: Record<string, bigint>
93
+ /** Whether to simulate connection failure */
94
+ shouldFailConnect?: boolean
95
+ /** Whether to simulate signing failure */
96
+ shouldFailSign?: boolean
97
+ /** Whether to simulate transaction failure */
98
+ shouldFailTransaction?: boolean
99
+ /** Simulated cluster */
100
+ cluster?: SolanaCluster
101
+ /** Simulated latency in ms */
102
+ latency?: number
103
+ }
104
+
105
+ /**
106
+ * Mock Solana wallet adapter for testing
107
+ *
108
+ * Provides full Solana wallet functionality with mock data.
109
+ * No browser environment or actual wallet required.
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * const wallet = new MockSolanaAdapter({
114
+ * address: 'TestWalletAddress123',
115
+ * balance: 5_000_000_000n, // 5 SOL
116
+ * })
117
+ *
118
+ * await wallet.connect()
119
+ * const balance = await wallet.getBalance() // 5_000_000_000n
120
+ *
121
+ * // Simulate failures
122
+ * const failingWallet = new MockSolanaAdapter({
123
+ * shouldFailSign: true,
124
+ * })
125
+ * ```
126
+ */
127
+ export class MockSolanaAdapter extends BaseWalletAdapter {
128
+ readonly chain = 'solana' as const
129
+ readonly name = 'mock-solana'
130
+
131
+ private mockAddress: string
132
+ private mockPublicKey: MockPublicKey
133
+ private mockBalance: bigint
134
+ private mockTokenBalances: Map<string, bigint>
135
+ private shouldFailConnect: boolean
136
+ private shouldFailSign: boolean
137
+ private shouldFailTransaction: boolean
138
+ private cluster: SolanaCluster
139
+ private latency: number
140
+
141
+ // Track signed transactions for verification
142
+ private signedTransactions: Array<SolanaTransaction | SolanaVersionedTransaction> = []
143
+ private sentTransactions: string[] = []
144
+
145
+ constructor(config: MockSolanaAdapterConfig = {}) {
146
+ super()
147
+ this.mockAddress = config.address ?? 'MockSo1anaWa11etAddress123456789'
148
+ this.mockPublicKey = new MockPublicKey(this.mockAddress)
149
+ this.mockBalance = config.balance ?? 1_000_000_000n // 1 SOL default
150
+ this.mockTokenBalances = new Map(Object.entries(config.tokenBalances ?? {}))
151
+ this.shouldFailConnect = config.shouldFailConnect ?? false
152
+ this.shouldFailSign = config.shouldFailSign ?? false
153
+ this.shouldFailTransaction = config.shouldFailTransaction ?? false
154
+ this.cluster = config.cluster ?? 'devnet'
155
+ this.latency = config.latency ?? 10
156
+ }
157
+
158
+ /**
159
+ * Get the current Solana cluster
160
+ */
161
+ getCluster(): SolanaCluster {
162
+ return this.cluster
163
+ }
164
+
165
+ /**
166
+ * Connect to the mock wallet
167
+ */
168
+ async connect(): Promise<void> {
169
+ this._connectionState = 'connecting'
170
+
171
+ // Simulate network latency
172
+ await this.simulateLatency()
173
+
174
+ if (this.shouldFailConnect) {
175
+ this.setError(WalletErrorCode.CONNECTION_FAILED, 'Mock connection failure')
176
+ throw new WalletError('Mock connection failure', WalletErrorCode.CONNECTION_FAILED)
177
+ }
178
+
179
+ const hexPubKey = ('0x' + Buffer.from(this.mockPublicKey.toBytes()).toString('hex')) as HexString
180
+ this.setConnected(this.mockAddress, hexPubKey)
181
+ }
182
+
183
+ /**
184
+ * Disconnect from the mock wallet
185
+ */
186
+ async disconnect(): Promise<void> {
187
+ await this.simulateLatency()
188
+ this.setDisconnected('User disconnected')
189
+ }
190
+
191
+ /**
192
+ * Sign a message
193
+ */
194
+ async signMessage(message: Uint8Array): Promise<Signature> {
195
+ this.requireConnected()
196
+ await this.simulateLatency()
197
+
198
+ if (this.shouldFailSign) {
199
+ throw new WalletError('Mock signing failure', WalletErrorCode.SIGNING_REJECTED)
200
+ }
201
+
202
+ // Create deterministic mock signature
203
+ const mockSig = new Uint8Array(64)
204
+ for (let i = 0; i < 64; i++) {
205
+ mockSig[i] = (message[i % message.length] ?? 0) ^ (i * 7) ^ this.mockAddress.charCodeAt(i % this.mockAddress.length)
206
+ }
207
+
208
+ return {
209
+ signature: ('0x' + Buffer.from(mockSig).toString('hex')) as HexString,
210
+ publicKey: this._publicKey as HexString,
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Sign a transaction
216
+ */
217
+ async signTransaction(tx: UnsignedTransaction): Promise<SignedTransaction> {
218
+ this.requireConnected()
219
+ await this.simulateLatency()
220
+
221
+ if (this.shouldFailSign) {
222
+ throw new WalletError('Mock signing failure', WalletErrorCode.SIGNING_REJECTED)
223
+ }
224
+
225
+ const solTx = tx.data as SolanaTransaction | SolanaVersionedTransaction
226
+ this.signedTransactions.push(solTx)
227
+
228
+ const signature = await this.signMessage(
229
+ new TextEncoder().encode(JSON.stringify(tx.data))
230
+ )
231
+
232
+ return {
233
+ unsigned: tx,
234
+ signatures: [signature],
235
+ serialized: ('0x' + Buffer.from(JSON.stringify(tx.data)).toString('hex')) as HexString,
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Sign and send a transaction
241
+ */
242
+ async signAndSendTransaction(tx: UnsignedTransaction): Promise<TransactionReceipt> {
243
+ this.requireConnected()
244
+ await this.simulateLatency()
245
+
246
+ if (this.shouldFailSign) {
247
+ throw new WalletError('Mock signing failure', WalletErrorCode.SIGNING_REJECTED)
248
+ }
249
+
250
+ if (this.shouldFailTransaction) {
251
+ throw new WalletError('Mock transaction failure', WalletErrorCode.TRANSACTION_FAILED)
252
+ }
253
+
254
+ // Generate mock transaction signature
255
+ const txSig = `mock_tx_${Date.now()}_${Math.random().toString(36).slice(2)}`
256
+ this.sentTransactions.push(txSig)
257
+
258
+ return {
259
+ txHash: ('0x' + Buffer.from(txSig).toString('hex')) as HexString,
260
+ status: 'confirmed',
261
+ blockNumber: BigInt(Math.floor(Math.random() * 1000000)),
262
+ feeUsed: 5000n, // 0.000005 SOL
263
+ timestamp: Date.now(),
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Sign multiple transactions
269
+ */
270
+ async signAllTransactions<T extends SolanaTransaction | SolanaVersionedTransaction>(
271
+ transactions: T[]
272
+ ): Promise<T[]> {
273
+ this.requireConnected()
274
+ await this.simulateLatency()
275
+
276
+ if (this.shouldFailSign) {
277
+ throw new WalletError('Mock signing failure', WalletErrorCode.SIGNING_REJECTED)
278
+ }
279
+
280
+ // Record all signed transactions
281
+ this.signedTransactions.push(...transactions)
282
+
283
+ // Return transactions with mock signatures
284
+ return transactions.map((tx) => {
285
+ if ('signature' in tx) {
286
+ (tx as SolanaTransaction).signature = new Uint8Array(64).fill(1)
287
+ }
288
+ return tx
289
+ })
290
+ }
291
+
292
+ /**
293
+ * Get SOL balance
294
+ */
295
+ async getBalance(): Promise<bigint> {
296
+ this.requireConnected()
297
+ await this.simulateLatency()
298
+ return this.mockBalance
299
+ }
300
+
301
+ /**
302
+ * Get token balance
303
+ */
304
+ async getTokenBalance(asset: Asset): Promise<bigint> {
305
+ this.requireConnected()
306
+ await this.simulateLatency()
307
+
308
+ if (asset.chain !== 'solana') {
309
+ throw new WalletError(
310
+ `Asset chain ${asset.chain} not supported by Solana adapter`,
311
+ WalletErrorCode.UNSUPPORTED_CHAIN
312
+ )
313
+ }
314
+
315
+ // Native SOL
316
+ if (!asset.address) {
317
+ return this.mockBalance
318
+ }
319
+
320
+ return this.mockTokenBalances.get(asset.address) ?? 0n
321
+ }
322
+
323
+ // ── Mock Control Methods ──────────────────────────────────────────────────
324
+
325
+ /**
326
+ * Set mock balance
327
+ */
328
+ setMockBalance(balance: bigint): void {
329
+ this.mockBalance = balance
330
+ }
331
+
332
+ /**
333
+ * Set mock token balance
334
+ */
335
+ setMockTokenBalance(mintAddress: string, balance: bigint): void {
336
+ this.mockTokenBalances.set(mintAddress, balance)
337
+ }
338
+
339
+ /**
340
+ * Get all signed transactions (for verification)
341
+ */
342
+ getSignedTransactions(): Array<SolanaTransaction | SolanaVersionedTransaction> {
343
+ return [...this.signedTransactions]
344
+ }
345
+
346
+ /**
347
+ * Get all sent transaction signatures (for verification)
348
+ */
349
+ getSentTransactions(): string[] {
350
+ return [...this.sentTransactions]
351
+ }
352
+
353
+ /**
354
+ * Clear transaction history
355
+ */
356
+ clearTransactionHistory(): void {
357
+ this.signedTransactions = []
358
+ this.sentTransactions = []
359
+ }
360
+
361
+ /**
362
+ * Simulate an account change event
363
+ */
364
+ simulateAccountChange(newAddress: string): void {
365
+ const previousAddress = this._address
366
+ this.mockAddress = newAddress
367
+ this.mockPublicKey = new MockPublicKey(newAddress)
368
+ this._address = newAddress
369
+ this._publicKey = ('0x' + Buffer.from(this.mockPublicKey.toBytes()).toString('hex')) as HexString
370
+ this.emitAccountChanged(previousAddress, newAddress)
371
+ }
372
+
373
+ /**
374
+ * Simulate a disconnect event
375
+ */
376
+ simulateDisconnect(): void {
377
+ this.setDisconnected('Simulated disconnect')
378
+ }
379
+
380
+ private async simulateLatency(): Promise<void> {
381
+ if (this.latency > 0) {
382
+ await new Promise((resolve) => setTimeout(resolve, this.latency))
383
+ }
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Create a mock Solana wallet provider for testing real adapter
389
+ */
390
+ export function createMockSolanaProvider(
391
+ config: MockSolanaAdapterConfig = {}
392
+ ): SolanaWalletProvider {
393
+ const address = config.address ?? 'MockSo1anaWa11etAddress123456789'
394
+ const pubkey = new MockPublicKey(address)
395
+ let isConnected = false
396
+
397
+ const eventHandlers: Record<string, Set<(...args: unknown[]) => void>> = {
398
+ connect: new Set(),
399
+ disconnect: new Set(),
400
+ accountChanged: new Set(),
401
+ }
402
+
403
+ return {
404
+ isPhantom: true,
405
+ publicKey: null,
406
+ isConnected: false,
407
+
408
+ async connect() {
409
+ if (config.shouldFailConnect) {
410
+ throw new Error('User rejected the request')
411
+ }
412
+ isConnected = true
413
+ ;(this as SolanaWalletProvider).isConnected = true
414
+ ;(this as SolanaWalletProvider).publicKey = pubkey
415
+ eventHandlers.connect.forEach((h) => h(pubkey))
416
+ return { publicKey: pubkey }
417
+ },
418
+
419
+ async disconnect() {
420
+ isConnected = false
421
+ ;(this as SolanaWalletProvider).isConnected = false
422
+ ;(this as SolanaWalletProvider).publicKey = null
423
+ eventHandlers.disconnect.forEach((h) => h())
424
+ },
425
+
426
+ async signMessage(message: Uint8Array) {
427
+ if (config.shouldFailSign) {
428
+ throw new Error('User rejected the request')
429
+ }
430
+ const signature = new Uint8Array(64)
431
+ for (let i = 0; i < 64; i++) {
432
+ signature[i] = message[i % message.length] ^ i
433
+ }
434
+ return { signature }
435
+ },
436
+
437
+ async signTransaction<T extends SolanaTransaction | SolanaVersionedTransaction>(tx: T): Promise<T> {
438
+ if (config.shouldFailSign) {
439
+ throw new Error('User rejected the request')
440
+ }
441
+ return tx
442
+ },
443
+
444
+ async signAllTransactions<T extends SolanaTransaction | SolanaVersionedTransaction>(txs: T[]): Promise<T[]> {
445
+ if (config.shouldFailSign) {
446
+ throw new Error('User rejected the request')
447
+ }
448
+ return txs
449
+ },
450
+
451
+ async signAndSendTransaction<T extends SolanaTransaction | SolanaVersionedTransaction>(_tx: T) {
452
+ if (config.shouldFailSign) {
453
+ throw new Error('User rejected the request')
454
+ }
455
+ if (config.shouldFailTransaction) {
456
+ throw new Error('Transaction failed')
457
+ }
458
+ return { signature: 'mock_signature_' + Date.now() }
459
+ },
460
+
461
+ on(event, handler) {
462
+ eventHandlers[event]?.add(handler)
463
+ },
464
+
465
+ off(event, handler) {
466
+ eventHandlers[event]?.delete(handler)
467
+ },
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Create a mock Solana connection for testing
473
+ */
474
+ export function createMockSolanaConnection(
475
+ config: MockSolanaAdapterConfig = {}
476
+ ): SolanaConnection {
477
+ const balance = config.balance ?? 1_000_000_000n
478
+
479
+ return {
480
+ async getBalance() {
481
+ return Number(balance)
482
+ },
483
+
484
+ async getTokenAccountBalance(publicKey) {
485
+ const mint = publicKey.toBase58()
486
+ const tokenBalance = config.tokenBalances?.[mint] ?? 0n
487
+ return {
488
+ value: {
489
+ amount: tokenBalance.toString(),
490
+ decimals: 9,
491
+ },
492
+ }
493
+ },
494
+
495
+ async getLatestBlockhash() {
496
+ return {
497
+ blockhash: 'mock_blockhash_' + Date.now(),
498
+ lastValidBlockHeight: 12345678,
499
+ }
500
+ },
501
+
502
+ async sendRawTransaction() {
503
+ if (config.shouldFailTransaction) {
504
+ throw new Error('Transaction failed')
505
+ }
506
+ return 'mock_signature_' + Date.now()
507
+ },
508
+
509
+ async confirmTransaction() {
510
+ return { value: { err: null } }
511
+ },
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Create a mock Solana adapter
517
+ */
518
+ export function createMockSolanaAdapter(
519
+ config: MockSolanaAdapterConfig = {}
520
+ ): MockSolanaAdapter {
521
+ return new MockSolanaAdapter(config)
522
+ }