@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,505 @@
1
+ /**
2
+ * Mock Ethereum Wallet Adapter
3
+ *
4
+ * Mock implementation for testing without browser environment.
5
+ * Simulates EIP-1193 provider behavior.
6
+ */
7
+
8
+ import type {
9
+ Asset,
10
+ HexString,
11
+ ChainId,
12
+ Signature,
13
+ UnsignedTransaction,
14
+ SignedTransaction,
15
+ TransactionReceipt,
16
+ } from '@sip-protocol/types'
17
+ import { WalletErrorCode } from '@sip-protocol/types'
18
+ import { BaseWalletAdapter } from '../base-adapter'
19
+ import { WalletError } from '../errors'
20
+ import type {
21
+ EIP1193Provider,
22
+ EIP1193RequestArguments,
23
+ EthereumTransactionRequest,
24
+ EthereumTransactionReceipt,
25
+ EIP712TypedData,
26
+ } from './types'
27
+ import { toHex, EthereumChainId } from './types'
28
+
29
+ /**
30
+ * Mock Ethereum adapter configuration
31
+ */
32
+ export interface MockEthereumAdapterConfig {
33
+ /** Mock address */
34
+ address?: string
35
+ /** Mock chain ID */
36
+ chainId?: number
37
+ /** Initial ETH balance in wei */
38
+ balance?: bigint
39
+ /** Token balances by address */
40
+ tokenBalances?: Record<string, bigint>
41
+ /** Should connection fail */
42
+ shouldFailConnect?: boolean
43
+ /** Should signing fail */
44
+ shouldFailSign?: boolean
45
+ /** Should transaction fail */
46
+ shouldFailTransaction?: boolean
47
+ }
48
+
49
+ /**
50
+ * Mock Ethereum wallet adapter for testing
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const adapter = new MockEthereumAdapter({
55
+ * address: '0x1234...',
56
+ * balance: 1_000_000_000_000_000_000n, // 1 ETH
57
+ * })
58
+ *
59
+ * await adapter.connect()
60
+ * const balance = await adapter.getBalance()
61
+ * ```
62
+ */
63
+ export class MockEthereumAdapter extends BaseWalletAdapter {
64
+ readonly chain = 'ethereum' as const
65
+ readonly name = 'mock-ethereum'
66
+
67
+ private _chainId: number
68
+ private _balance: bigint
69
+ private _tokenBalances: Map<string, bigint>
70
+ private _mockAddress: string
71
+ private _shouldFailConnect: boolean
72
+ private _shouldFailSign: boolean
73
+ private _shouldFailTransaction: boolean
74
+ private _signedTransactions: UnsignedTransaction[] = []
75
+ private _sentTransactions: string[] = []
76
+ private _signatureCounter = 0
77
+ private _txCounter = 0
78
+
79
+ constructor(config: MockEthereumAdapterConfig = {}) {
80
+ super()
81
+ this._mockAddress = config.address ?? '0x742d35Cc6634C0532925a3b844Bc9e7595f8fB1b'
82
+ this._chainId = config.chainId ?? EthereumChainId.MAINNET
83
+ this._balance = config.balance ?? 1_000_000_000_000_000_000n // 1 ETH
84
+ this._tokenBalances = new Map(Object.entries(config.tokenBalances ?? {}))
85
+ this._shouldFailConnect = config.shouldFailConnect ?? false
86
+ this._shouldFailSign = config.shouldFailSign ?? false
87
+ this._shouldFailTransaction = config.shouldFailTransaction ?? false
88
+ }
89
+
90
+ /**
91
+ * Get current chain ID
92
+ */
93
+ getChainId(): number {
94
+ return this._chainId
95
+ }
96
+
97
+ /**
98
+ * Connect to mock wallet
99
+ */
100
+ async connect(): Promise<void> {
101
+ try {
102
+ this._connectionState = 'connecting'
103
+
104
+ if (this._shouldFailConnect) {
105
+ this._connectionState = 'error'
106
+ throw new WalletError(
107
+ 'Mock connection rejected',
108
+ WalletErrorCode.CONNECTION_REJECTED
109
+ )
110
+ }
111
+
112
+ // Simulate connection delay
113
+ await new Promise((resolve) => setTimeout(resolve, 10))
114
+
115
+ // For Ethereum, public key is same as address
116
+ this.setConnected(this._mockAddress, this._mockAddress as HexString)
117
+ } catch (error) {
118
+ if (error instanceof WalletError) {
119
+ throw error
120
+ }
121
+ this._connectionState = 'error'
122
+ throw new WalletError(
123
+ `Mock connection failed: ${String(error)}`,
124
+ WalletErrorCode.CONNECTION_FAILED
125
+ )
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Disconnect from mock wallet
131
+ */
132
+ async disconnect(): Promise<void> {
133
+ this.setDisconnected()
134
+ }
135
+
136
+ /**
137
+ * Sign a message
138
+ */
139
+ async signMessage(message: Uint8Array): Promise<Signature> {
140
+ this.requireConnected()
141
+
142
+ if (this._shouldFailSign) {
143
+ throw new WalletError(
144
+ 'Mock signing rejected',
145
+ WalletErrorCode.SIGNING_REJECTED
146
+ )
147
+ }
148
+
149
+ // Create deterministic mock signature
150
+ const msgHex = Buffer.from(message).toString('hex')
151
+ const mockSig = `0x${msgHex.padEnd(130, '0').slice(0, 130)}` as HexString
152
+
153
+ return {
154
+ signature: mockSig,
155
+ publicKey: this._publicKey as HexString,
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Sign typed data (EIP-712)
161
+ */
162
+ async signTypedData(typedData: EIP712TypedData): Promise<Signature> {
163
+ this.requireConnected()
164
+
165
+ if (this._shouldFailSign) {
166
+ throw new WalletError(
167
+ 'Mock signing rejected',
168
+ WalletErrorCode.SIGNING_REJECTED
169
+ )
170
+ }
171
+
172
+ // Create mock signature from typed data
173
+ const mockSig = `0x${'1'.repeat(130)}` as HexString
174
+
175
+ return {
176
+ signature: mockSig,
177
+ publicKey: this._publicKey as HexString,
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Sign a transaction
183
+ */
184
+ async signTransaction(tx: UnsignedTransaction): Promise<SignedTransaction> {
185
+ this.requireConnected()
186
+
187
+ if (this._shouldFailSign) {
188
+ throw new WalletError(
189
+ 'Mock signing rejected',
190
+ WalletErrorCode.SIGNING_REJECTED
191
+ )
192
+ }
193
+
194
+ this._signedTransactions.push(tx)
195
+ this._signatureCounter++
196
+
197
+ const mockSig = `0x${this._signatureCounter.toString(16).padStart(130, '0')}` as HexString
198
+
199
+ return {
200
+ unsigned: tx,
201
+ signatures: [
202
+ {
203
+ signature: mockSig,
204
+ publicKey: this._publicKey as HexString,
205
+ },
206
+ ],
207
+ serialized: mockSig,
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Sign and send a transaction
213
+ */
214
+ async signAndSendTransaction(tx: UnsignedTransaction): Promise<TransactionReceipt> {
215
+ this.requireConnected()
216
+
217
+ if (this._shouldFailTransaction) {
218
+ throw new WalletError(
219
+ 'Mock transaction failed',
220
+ WalletErrorCode.TRANSACTION_FAILED
221
+ )
222
+ }
223
+
224
+ this._signedTransactions.push(tx)
225
+ this._txCounter++
226
+
227
+ const txHash = `0x${this._txCounter.toString(16).padStart(64, '0')}` as HexString
228
+ this._sentTransactions.push(txHash)
229
+
230
+ return {
231
+ txHash,
232
+ status: 'confirmed',
233
+ blockNumber: 12345678n,
234
+ feeUsed: 21000n * 20_000_000_000n, // 21000 gas * 20 gwei
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Get ETH balance
240
+ */
241
+ async getBalance(): Promise<bigint> {
242
+ this.requireConnected()
243
+ return this._balance
244
+ }
245
+
246
+ /**
247
+ * Get ERC-20 token balance
248
+ */
249
+ async getTokenBalance(asset: Asset): Promise<bigint> {
250
+ this.requireConnected()
251
+
252
+ if (asset.chain !== 'ethereum') {
253
+ throw new WalletError(
254
+ `Asset chain ${asset.chain} not supported by Ethereum adapter`,
255
+ WalletErrorCode.UNSUPPORTED_CHAIN
256
+ )
257
+ }
258
+
259
+ // Native ETH
260
+ if (!asset.address) {
261
+ return this._balance
262
+ }
263
+
264
+ return this._tokenBalances.get(asset.address) ?? 0n
265
+ }
266
+
267
+ /**
268
+ * Switch chain (mock)
269
+ */
270
+ async switchChain(chainId: number): Promise<void> {
271
+ this.requireConnected()
272
+
273
+ this._chainId = chainId
274
+
275
+ this.emit({
276
+ type: 'chainChanged',
277
+ previousChain: 'ethereum' as ChainId,
278
+ newChain: 'ethereum' as ChainId,
279
+ timestamp: Date.now(),
280
+ })
281
+ }
282
+
283
+ // ============================================================================
284
+ // Mock Control Methods
285
+ // ============================================================================
286
+
287
+ /**
288
+ * Set mock ETH balance
289
+ */
290
+ setMockBalance(balance: bigint): void {
291
+ this._balance = balance
292
+ }
293
+
294
+ /**
295
+ * Set mock token balance
296
+ */
297
+ setMockTokenBalance(tokenAddress: string, balance: bigint): void {
298
+ this._tokenBalances.set(tokenAddress, balance)
299
+ }
300
+
301
+ /**
302
+ * Get signed transactions history
303
+ */
304
+ getSignedTransactions(): UnsignedTransaction[] {
305
+ return [...this._signedTransactions]
306
+ }
307
+
308
+ /**
309
+ * Get sent transaction hashes
310
+ */
311
+ getSentTransactions(): string[] {
312
+ return [...this._sentTransactions]
313
+ }
314
+
315
+ /**
316
+ * Clear transaction history
317
+ */
318
+ clearTransactionHistory(): void {
319
+ this._signedTransactions = []
320
+ this._sentTransactions = []
321
+ }
322
+
323
+ /**
324
+ * Simulate account change
325
+ */
326
+ simulateAccountChange(newAddress: string): void {
327
+ if (!this.isConnected()) return
328
+
329
+ const previousAddress = this._address
330
+ this._address = newAddress
331
+ this._publicKey = newAddress as HexString
332
+ this._mockAddress = newAddress
333
+
334
+ this.emit({
335
+ type: 'accountChanged',
336
+ previousAddress,
337
+ newAddress,
338
+ timestamp: Date.now(),
339
+ })
340
+ }
341
+
342
+ /**
343
+ * Simulate disconnect
344
+ */
345
+ simulateDisconnect(): void {
346
+ this.setDisconnected('Mock disconnect triggered')
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Factory function to create mock Ethereum adapter
352
+ */
353
+ export function createMockEthereumAdapter(
354
+ config?: MockEthereumAdapterConfig
355
+ ): MockEthereumAdapter {
356
+ return new MockEthereumAdapter(config)
357
+ }
358
+
359
+ /**
360
+ * Create a mock EIP-1193 provider for testing
361
+ */
362
+ export function createMockEthereumProvider(
363
+ config: MockEthereumAdapterConfig = {}
364
+ ): EIP1193Provider {
365
+ const address = config.address ?? '0x742d35Cc6634C0532925a3b844Bc9e7595f8fB1b'
366
+ const chainId = config.chainId ?? EthereumChainId.MAINNET
367
+ const balance = config.balance ?? 1_000_000_000_000_000_000n
368
+ const tokenBalances = new Map(Object.entries(config.tokenBalances ?? {}))
369
+ const shouldFailConnect = config.shouldFailConnect ?? false
370
+ const shouldFailSign = config.shouldFailSign ?? false
371
+ const shouldFailTransaction = config.shouldFailTransaction ?? false
372
+
373
+ let isConnected = false
374
+ let txCounter = 0
375
+ const eventHandlers: Map<string, Set<(...args: unknown[]) => void>> = new Map()
376
+
377
+ const provider: EIP1193Provider = {
378
+ isMetaMask: true,
379
+ selectedAddress: null,
380
+ chainId: toHex(chainId),
381
+
382
+ isConnected(): boolean {
383
+ return isConnected
384
+ },
385
+
386
+ async request<T = unknown>(args: EIP1193RequestArguments): Promise<T> {
387
+ const { method, params } = args
388
+
389
+ switch (method) {
390
+ case 'eth_requestAccounts':
391
+ case 'eth_accounts': {
392
+ if (shouldFailConnect && method === 'eth_requestAccounts') {
393
+ const error = new Error('User rejected') as Error & { code: number }
394
+ error.code = 4001
395
+ throw error
396
+ }
397
+ isConnected = true
398
+ provider.selectedAddress = address
399
+ return [address] as T
400
+ }
401
+
402
+ case 'eth_chainId':
403
+ return toHex(chainId) as T
404
+
405
+ case 'eth_getBalance':
406
+ return toHex(balance) as T
407
+
408
+ case 'eth_call': {
409
+ const callParams = (params as unknown[])?.[0] as { to?: string; data?: string }
410
+ if (callParams?.data?.startsWith('0x70a08231')) {
411
+ // balanceOf call
412
+ const tokenBalance = tokenBalances.get(callParams.to ?? '') ?? 0n
413
+ return toHex(tokenBalance) as T
414
+ }
415
+ return '0x0' as T
416
+ }
417
+
418
+ case 'personal_sign': {
419
+ if (shouldFailSign) {
420
+ const error = new Error('User rejected') as Error & { code: number }
421
+ error.code = 4001
422
+ throw error
423
+ }
424
+ const message = (params as string[])?.[0] ?? ''
425
+ return `0x${message.slice(2).padEnd(130, '0').slice(0, 130)}` as T
426
+ }
427
+
428
+ case 'eth_signTypedData_v4': {
429
+ if (shouldFailSign) {
430
+ const error = new Error('User rejected') as Error & { code: number }
431
+ error.code = 4001
432
+ throw error
433
+ }
434
+ return `0x${'1'.repeat(130)}` as T
435
+ }
436
+
437
+ case 'eth_signTransaction': {
438
+ if (shouldFailSign) {
439
+ const error = new Error('User rejected') as Error & { code: number }
440
+ error.code = 4001
441
+ throw error
442
+ }
443
+ return `0x${'2'.repeat(130)}` as T
444
+ }
445
+
446
+ case 'eth_sendTransaction': {
447
+ if (shouldFailTransaction) {
448
+ const error = new Error('Transaction failed') as Error & { code: number }
449
+ error.code = -32000
450
+ throw error
451
+ }
452
+ txCounter++
453
+ return `0x${txCounter.toString(16).padStart(64, '0')}` as T
454
+ }
455
+
456
+ case 'eth_getTransactionReceipt': {
457
+ const txHash = (params as string[])?.[0]
458
+ if (txHash) {
459
+ return {
460
+ transactionHash: txHash,
461
+ blockNumber: '0xbc614e',
462
+ blockHash: '0x' + '0'.repeat(64),
463
+ from: address,
464
+ to: '0x' + '0'.repeat(40),
465
+ gasUsed: '0x5208',
466
+ effectiveGasPrice: '0x4a817c800',
467
+ status: '0x1',
468
+ contractAddress: null,
469
+ } as T
470
+ }
471
+ return null as T
472
+ }
473
+
474
+ case 'eth_blockNumber':
475
+ return '0xbc614e' as T
476
+
477
+ case 'wallet_switchEthereumChain': {
478
+ const switchParams = (params as unknown[])?.[0] as { chainId: string }
479
+ const newChainId = parseInt(switchParams?.chainId ?? '0x1', 16)
480
+ provider.chainId = switchParams?.chainId
481
+ eventHandlers.get('chainChanged')?.forEach((handler) => {
482
+ handler(switchParams?.chainId)
483
+ })
484
+ return undefined as T
485
+ }
486
+
487
+ default:
488
+ throw new Error(`Method ${method} not implemented in mock`)
489
+ }
490
+ },
491
+
492
+ on(event: string, handler: (...args: unknown[]) => void): void {
493
+ if (!eventHandlers.has(event)) {
494
+ eventHandlers.set(event, new Set())
495
+ }
496
+ eventHandlers.get(event)!.add(handler)
497
+ },
498
+
499
+ removeListener(event: string, handler: (...args: unknown[]) => void): void {
500
+ eventHandlers.get(event)?.delete(handler)
501
+ },
502
+ }
503
+
504
+ return provider
505
+ }