walletpair-sdk 1.0.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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +415 -0
  3. package/dist/ble/framing.d.ts +23 -0
  4. package/dist/ble/framing.d.ts.map +1 -0
  5. package/dist/ble/framing.js +83 -0
  6. package/dist/ble/framing.js.map +1 -0
  7. package/dist/ble/index.d.ts +9 -0
  8. package/dist/ble/index.d.ts.map +1 -0
  9. package/dist/ble/index.js +9 -0
  10. package/dist/ble/index.js.map +1 -0
  11. package/dist/ble/web-ble-transport.d.ts +29 -0
  12. package/dist/ble/web-ble-transport.d.ts.map +1 -0
  13. package/dist/ble/web-ble-transport.js +93 -0
  14. package/dist/ble/web-ble-transport.js.map +1 -0
  15. package/dist/crypto.d.ts +102 -0
  16. package/dist/crypto.d.ts.map +1 -0
  17. package/dist/crypto.js +279 -0
  18. package/dist/crypto.js.map +1 -0
  19. package/dist/dapp-session.d.ts +106 -0
  20. package/dist/dapp-session.d.ts.map +1 -0
  21. package/dist/dapp-session.js +918 -0
  22. package/dist/dapp-session.js.map +1 -0
  23. package/dist/emitter.d.ts +16 -0
  24. package/dist/emitter.d.ts.map +1 -0
  25. package/dist/emitter.js +41 -0
  26. package/dist/emitter.js.map +1 -0
  27. package/dist/evm/eip1193.d.ts +83 -0
  28. package/dist/evm/eip1193.d.ts.map +1 -0
  29. package/dist/evm/eip1193.js +270 -0
  30. package/dist/evm/eip1193.js.map +1 -0
  31. package/dist/evm/index.d.ts +8 -0
  32. package/dist/evm/index.d.ts.map +1 -0
  33. package/dist/evm/index.js +8 -0
  34. package/dist/evm/index.js.map +1 -0
  35. package/dist/evm/wagmi.d.ts +118 -0
  36. package/dist/evm/wagmi.d.ts.map +1 -0
  37. package/dist/evm/wagmi.js +205 -0
  38. package/dist/evm/wagmi.js.map +1 -0
  39. package/dist/index.d.ts +22 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +24 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/types.d.ts +225 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +31 -0
  46. package/dist/types.js.map +1 -0
  47. package/dist/wallet-session.d.ts +107 -0
  48. package/dist/wallet-session.d.ts.map +1 -0
  49. package/dist/wallet-session.js +794 -0
  50. package/dist/wallet-session.js.map +1 -0
  51. package/dist/ws-transport.d.ts +29 -0
  52. package/dist/ws-transport.d.ts.map +1 -0
  53. package/dist/ws-transport.js +79 -0
  54. package/dist/ws-transport.js.map +1 -0
  55. package/package.json +55 -0
  56. package/src/__tests__/adversarial/crypto-attacks.test.ts +557 -0
  57. package/src/__tests__/adversarial/malicious-dapp.test.ts +505 -0
  58. package/src/__tests__/adversarial/malicious-relay.test.ts +528 -0
  59. package/src/__tests__/adversarial/malicious-wallet.test.ts +467 -0
  60. package/src/__tests__/spec-compliance/canonical-json.test.ts +227 -0
  61. package/src/__tests__/spec-compliance/crypto-vectors.test.ts +321 -0
  62. package/src/__tests__/spec-compliance/message-format.test.ts +356 -0
  63. package/src/__tests__/spec-compliance/sequence-numbers.test.ts +300 -0
  64. package/src/__tests__/spec-compliance/state-machine.test.ts +364 -0
  65. package/src/ble/framing.test.ts +196 -0
  66. package/src/ble/framing.ts +100 -0
  67. package/src/ble/index.ts +18 -0
  68. package/src/ble/web-ble-transport.test.ts +192 -0
  69. package/src/ble/web-ble-transport.ts +116 -0
  70. package/src/ble/web-bluetooth.d.ts +47 -0
  71. package/src/canonical-json.test.ts +612 -0
  72. package/src/crypto-directional.test.ts +263 -0
  73. package/src/crypto-hardening.test.ts +529 -0
  74. package/src/crypto.test.ts +635 -0
  75. package/src/crypto.ts +405 -0
  76. package/src/dapp-session.test.ts +647 -0
  77. package/src/dapp-session.ts +1004 -0
  78. package/src/emitter.test.ts +169 -0
  79. package/src/emitter.ts +45 -0
  80. package/src/evm/eip1193.test.ts +365 -0
  81. package/src/evm/eip1193.ts +346 -0
  82. package/src/evm/index.ts +19 -0
  83. package/src/evm/wagmi.test.ts +396 -0
  84. package/src/evm/wagmi.ts +321 -0
  85. package/src/index.ts +86 -0
  86. package/src/integration.test.ts +385 -0
  87. package/src/security.test.ts +430 -0
  88. package/src/sequence-validation.test.ts +1185 -0
  89. package/src/test-helpers.ts +216 -0
  90. package/src/types.test.ts +82 -0
  91. package/src/types.ts +305 -0
  92. package/src/wallet-session.test.ts +683 -0
  93. package/src/wallet-session.ts +922 -0
  94. package/src/ws-transport.test.ts +231 -0
  95. package/src/ws-transport.ts +92 -0
@@ -0,0 +1,346 @@
1
+ /**
2
+ * EIP-1193 Provider — wraps a DAppSession for Ethereum/EVM dApps.
3
+ *
4
+ * Maps standard Ethereum JSON-RPC methods to WalletPair protocol requests.
5
+ * Emits standard EIP-1193 events: connect, disconnect, chainChanged, accountsChanged.
6
+ *
7
+ * Usage:
8
+ * import { WalletPairProvider } from 'walletpair-sdk/evm'
9
+ * const provider = new WalletPairProvider({ session })
10
+ */
11
+
12
+ import type { DAppSession } from '../dapp-session.js';
13
+ import { evmNumericChainId } from '../types.js';
14
+ import { Emitter } from '../emitter.js';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // EIP-1193 types
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export interface EIP1193RequestArgs {
21
+ method: string;
22
+ params?: unknown[] | Record<string, unknown>;
23
+ }
24
+
25
+ export interface EIP1193ProviderEvents {
26
+ [key: string]: unknown;
27
+ connect: { chainId: string };
28
+ disconnect: { code: number; message: string };
29
+ chainChanged: string;
30
+ accountsChanged: string[];
31
+ message: { type: string; data?: unknown | undefined };
32
+ }
33
+
34
+ export interface EIP1193Provider {
35
+ request(args: EIP1193RequestArgs): Promise<unknown>;
36
+ on(event: string, handler: (...args: any[]) => void): void;
37
+ removeListener(event: string, handler: (...args: any[]) => void): void;
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Method mapping: EVM JSON-RPC → WalletPair protocol methods
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export interface MethodMapper {
45
+ mapRequest(method: string, params?: unknown): { method: string; params?: unknown | undefined } | null;
46
+ mapResponse(method: string, result: unknown): unknown;
47
+ }
48
+
49
+ /** Convert hex chainId "0x89" to CAIP-2 "eip155:137". */
50
+ function hexChainToCaip2(hex: string): string {
51
+ return `eip155:${Number.parseInt(hex, 16)}`;
52
+ }
53
+
54
+ /** Validate that a transaction object contains all required fields. */
55
+ function validateTxFields(tx: Record<string, unknown> | undefined): void {
56
+ const required = ['value', 'data', 'type', 'chainId'];
57
+ const missing = required.filter(f => tx?.[f] === undefined || tx?.[f] === null);
58
+ if (missing.length > 0) {
59
+ throw Object.assign(
60
+ new Error(`Missing required transaction fields: ${missing.join(', ')}`),
61
+ { code: -32602 },
62
+ );
63
+ }
64
+ }
65
+
66
+ const defaultMapper: MethodMapper = {
67
+ mapRequest(method, params) {
68
+ switch (method) {
69
+ case 'eth_requestAccounts':
70
+ case 'eth_accounts':
71
+ return { method: 'wallet_getAccounts' };
72
+ case 'personal_sign': {
73
+ // personal_sign params: [message, address] where message is hex-encoded bytes
74
+ const p = params as [string, string] | undefined;
75
+ const msg = p?.[0];
76
+ // EIP-1193 personal_sign: message is always hex-encoded bytes.
77
+ // Decode hex to UTF-8 text and route to wallet_signMessage.
78
+ let text = msg ?? '';
79
+ if (msg && msg.startsWith('0x')) {
80
+ try {
81
+ const hex = msg.slice(2);
82
+ const bytes = new Uint8Array(hex.length / 2);
83
+ for (let i = 0; i < bytes.length; i++) {
84
+ bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
85
+ }
86
+ text = new TextDecoder().decode(bytes);
87
+ } catch {
88
+ text = msg;
89
+ }
90
+ }
91
+ return { method: 'wallet_signMessage', params: { message: text, address: p?.[1] } };
92
+ }
93
+ case 'eth_signTypedData_v4': {
94
+ // params: [address, typedDataJSON]
95
+ const p = params as [string, string] | undefined;
96
+ let typedData: unknown;
97
+ try { typedData = typeof p?.[1] === 'string' ? JSON.parse(p[1]) : p?.[1]; }
98
+ catch { typedData = p?.[1]; }
99
+ return { method: 'wallet_signTypedData', params: { address: p?.[0], typedData } };
100
+ }
101
+ case 'eth_sendTransaction': {
102
+ // params: [txObject] — maps to wallet_sendTransaction (sign + broadcast)
103
+ const p = params as [Record<string, unknown>] | undefined;
104
+ const tx = p?.[0];
105
+ validateTxFields(tx);
106
+ return { method: 'wallet_sendTransaction', params: { address: tx?.from, tx } };
107
+ }
108
+ case 'eth_signTransaction': {
109
+ // params: [txObject] — maps to wallet_signTransaction (sign only)
110
+ const p = params as [Record<string, unknown>] | undefined;
111
+ const tx = p?.[0];
112
+ validateTxFields(tx);
113
+ return { method: 'wallet_signTransaction', params: { address: tx?.from, tx } };
114
+ }
115
+ case 'wallet_switchEthereumChain': {
116
+ // params: [{ chainId: "0x89" }] — convert hex to CAIP-2
117
+ const p = params as [{ chainId: string }] | undefined;
118
+ const hexId = p?.[0]?.chainId;
119
+ return { method: 'wallet_switchChain', params: { chain: hexId ? hexChainToCaip2(hexId) : undefined } };
120
+ }
121
+ case 'wallet_addEthereumChain':
122
+ return null; // unsupported — mapRequest returning null triggers unsupported_method error
123
+ default:
124
+ return null; // unknown method — routed to rpcProvider or rejected before reaching here
125
+ }
126
+ },
127
+ mapResponse(method, result) {
128
+ // Unwrap wallet_getAccounts result to EIP-1193 format (string[])
129
+ if (method === 'eth_requestAccounts' || method === 'eth_accounts') {
130
+ const r = result as { accounts?: { address: string }[] } | undefined;
131
+ if (r?.accounts) return r.accounts.map((a) => a.address);
132
+ }
133
+ // Unwrap wallet_sendTransaction result
134
+ if (method === 'eth_sendTransaction') {
135
+ const r = result as { txHash?: string } | undefined;
136
+ if (r?.txHash) return r.txHash;
137
+ }
138
+ // Unwrap wallet_signTransaction result
139
+ if (method === 'eth_signTransaction') {
140
+ const r = result as { signedTx?: string } | undefined;
141
+ if (r?.signedTx) return r.signedTx;
142
+ }
143
+ // Unwrap signature results
144
+ if (method === 'personal_sign' || method === 'eth_signTypedData_v4') {
145
+ const r = result as { signature?: string } | undefined;
146
+ if (r?.signature) return r.signature;
147
+ }
148
+ return result;
149
+ },
150
+ };
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // RPC routing: wallet methods vs read-only RPC
154
+ // ---------------------------------------------------------------------------
155
+
156
+ /** Methods that MUST go through WalletPair (require wallet signing/authorization). */
157
+ const WALLET_METHODS = new Set([
158
+ 'eth_requestAccounts', 'eth_accounts',
159
+ 'personal_sign',
160
+ 'eth_signTypedData_v4', 'eth_signTypedData_v3',
161
+ 'eth_sendTransaction', 'eth_signTransaction',
162
+ 'wallet_switchEthereumChain', 'wallet_addEthereumChain',
163
+ ]);
164
+
165
+ /** Methods handled locally by the provider (no RPC or WalletPair needed). */
166
+ const LOCAL_METHODS = new Set([
167
+ 'eth_chainId', 'net_version',
168
+ ]);
169
+
170
+ /**
171
+ * An RPC provider that handles read-only Ethereum JSON-RPC calls.
172
+ * Pass any EIP-1193-compatible provider, or a simple fetch-based JSON-RPC client.
173
+ */
174
+ export interface RpcProvider {
175
+ request(args: EIP1193RequestArgs): Promise<unknown>;
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // WalletPairProvider
180
+ // ---------------------------------------------------------------------------
181
+
182
+ export interface WalletPairProviderOptions {
183
+ session: DAppSession;
184
+ /** Initial EVM chain ID (numeric). Default 1 (mainnet). */
185
+ chainId?: number | undefined;
186
+ /** Custom method mapper. */
187
+ mapper?: MethodMapper | undefined;
188
+ /**
189
+ * Optional RPC provider for read-only methods (eth_call, eth_getBalance,
190
+ * eth_blockNumber, etc.). If provided, any method not handled by WalletPair
191
+ * is routed here instead of being sent to the wallet. If omitted, unknown
192
+ * methods throw unsupported_method (4200).
193
+ */
194
+ rpcProvider?: RpcProvider | undefined;
195
+ }
196
+
197
+ export class WalletPairProvider implements EIP1193Provider {
198
+ private session: DAppSession;
199
+ private mapper: MethodMapper;
200
+ private rpcProvider: RpcProvider | undefined;
201
+ private emitter = new Emitter<EIP1193ProviderEvents>();
202
+ private chainId: number;
203
+ private accounts: string[] = [];
204
+ private connected = false;
205
+ private disconnected = false;
206
+
207
+ constructor(options: WalletPairProviderOptions) {
208
+ this.session = options.session;
209
+ this.mapper = options.mapper ?? defaultMapper;
210
+ this.rpcProvider = options.rpcProvider;
211
+ this.chainId = options.chainId ?? 1;
212
+
213
+ this.session.on('phase', (phase) => {
214
+ if (phase === 'connected' && !this.connected) {
215
+ this.connected = true;
216
+ this.emitter.emit('connect', { chainId: `0x${this.chainId.toString(16)}` });
217
+ } else if ((phase === 'closed' || phase === 'disconnected') && this.connected) {
218
+ this.connected = false;
219
+ this.emitter.emit('disconnect', { code: 4900, message: 'Disconnected' });
220
+ }
221
+ });
222
+
223
+ this.session.on('event', ({ event, data }) => {
224
+ if (event === 'disconnect') {
225
+ this.connected = false;
226
+ this.disconnected = true;
227
+ const reason = (data as any)?.reason ?? 'unknown';
228
+ const msg = (data as any)?.message ?? `Disconnected by wallet (${reason})`;
229
+ this.emitter.emit('disconnect', { code: 4900, message: msg });
230
+ this.session.close('normal');
231
+ return;
232
+ }
233
+ if (event === 'accountsChanged') {
234
+ // Handle both formats:
235
+ // - Simple: { accounts: ['0x...'] } or just ['0x...']
236
+ // - Sub-protocol: { accounts: [{ address: '0x...', chains?: [...] }] }
237
+ const payload = data as { accounts?: (string | { address: string })[] } | string[];
238
+ const rawAccounts = Array.isArray(payload) ? payload : (payload as any)?.accounts;
239
+ if (Array.isArray(rawAccounts)) {
240
+ this.accounts = rawAccounts.map((a: string | { address: string }) =>
241
+ typeof a === 'string' ? a : a.address,
242
+ );
243
+ this.emitter.emit('accountsChanged', this.accounts);
244
+ }
245
+ } else if (event === 'chainChanged') {
246
+ // Handle multiple formats:
247
+ // - { chainId: 'eip155:137' } or { chainId: '0x89' } or { chainId: 137 }
248
+ // - { chain: 'eip155:137' }
249
+ // - raw string 'eip155:137' or '0x89'
250
+ const raw = typeof data === 'object' && data !== null
251
+ ? (data as any).chainId ?? (data as any).chain
252
+ : data;
253
+ let newChainId: number | null = null;
254
+ if (typeof raw === 'string') {
255
+ if (raw.startsWith('eip155:')) {
256
+ newChainId = evmNumericChainId(raw);
257
+ } else if (raw.startsWith('0x')) {
258
+ newChainId = Number.parseInt(raw, 16);
259
+ } else {
260
+ newChainId = Number.parseInt(raw, 10) || null;
261
+ }
262
+ } else if (typeof raw === 'number') {
263
+ newChainId = raw;
264
+ }
265
+ if (newChainId != null && newChainId !== this.chainId) {
266
+ this.chainId = newChainId;
267
+ this.emitter.emit('chainChanged', `0x${newChainId.toString(16)}`);
268
+ }
269
+ }
270
+ });
271
+ }
272
+
273
+ async request(args: EIP1193RequestArgs): Promise<unknown> {
274
+ if (this.disconnected) {
275
+ throw Object.assign(new Error('Provider is disconnected'), { code: 4900 });
276
+ }
277
+
278
+ const { method, params } = args;
279
+
280
+ if (method === 'eth_chainId') {
281
+ return `0x${this.chainId.toString(16)}`;
282
+ }
283
+ if (method === 'net_version') {
284
+ return String(this.chainId);
285
+ }
286
+
287
+ // Route non-wallet methods to RPC provider if available
288
+ if (!WALLET_METHODS.has(method) && !LOCAL_METHODS.has(method)) {
289
+ if (this.rpcProvider) {
290
+ return this.rpcProvider.request(args);
291
+ }
292
+ throw Object.assign(new Error(`Unsupported method: ${method}. Pass rpcProvider to handle read-only RPC calls.`), { code: 4200 });
293
+ }
294
+
295
+ const mapped = this.mapper.mapRequest(method, params);
296
+ if (!mapped) {
297
+ throw Object.assign(new Error(`Unsupported method: ${method}`), { code: 4200 });
298
+ }
299
+
300
+ // Inject chain for methods that require it per EVM sub-protocol
301
+ const chainRequiredMethods = [
302
+ 'wallet_signMessage', 'wallet_signTypedData',
303
+ 'wallet_signTransaction', 'wallet_sendTransaction',
304
+ 'wallet_getAccounts',
305
+ ];
306
+ if (mapped.params && typeof mapped.params === 'object' && chainRequiredMethods.includes(mapped.method)) {
307
+ const p = mapped.params as Record<string, unknown>;
308
+ if (!p.chain) {
309
+ p.chain = `eip155:${this.chainId}`;
310
+ }
311
+ }
312
+
313
+ const result = await this.session.request(mapped.method, mapped.params);
314
+ const mappedResult = this.mapper.mapResponse(method, result);
315
+
316
+ if (method === 'eth_requestAccounts' || method === 'eth_accounts') {
317
+ if (Array.isArray(mappedResult)) this.accounts = mappedResult;
318
+ }
319
+
320
+ return mappedResult;
321
+ }
322
+
323
+ on(event: string, handler: (...args: any[]) => void): void {
324
+ this.emitter.on(event as keyof EIP1193ProviderEvents, handler as any);
325
+ }
326
+
327
+ removeListener(event: string, handler: (...args: any[]) => void): void {
328
+ this.emitter.off(event as keyof EIP1193ProviderEvents, handler as any);
329
+ }
330
+
331
+ getChainId(): string {
332
+ return `0x${this.chainId.toString(16)}`;
333
+ }
334
+
335
+ getAccounts(): string[] {
336
+ return this.accounts;
337
+ }
338
+
339
+ isConnected(): boolean {
340
+ return this.connected;
341
+ }
342
+
343
+ getSession(): DAppSession {
344
+ return this.session;
345
+ }
346
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * EVM-specific exports for WalletPair SDK.
3
+ *
4
+ * Provides EIP-1193 provider and wagmi connector for Ethereum/EVM networks.
5
+ */
6
+
7
+ export {
8
+ WalletPairProvider,
9
+ type WalletPairProviderOptions,
10
+ type EIP1193Provider,
11
+ type EIP1193ProviderEvents,
12
+ type EIP1193RequestArgs,
13
+ type MethodMapper,
14
+ } from './eip1193.js';
15
+
16
+ export {
17
+ walletPair,
18
+ type WalletPairConnectorOptions,
19
+ } from './wagmi.js';