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,321 @@
1
+ /**
2
+ * Wagmi connector for WalletPair.
3
+ *
4
+ * Usage:
5
+ * import { walletPair } from 'walletpair-sdk/evm/wagmi'
6
+ * import { createConfig } from 'wagmi'
7
+ *
8
+ * const config = createConfig({
9
+ * connectors: [
10
+ * walletPair({
11
+ * relayUrl: 'wss://relay.walletpair.org/v1',
12
+ * meta: { name: 'MyDApp', description: 'A dApp', url: 'https://mydapp.com', icon: 'https://mydapp.com/icon.png' },
13
+ * onPairingUri: (uri) => { showQrCode(uri) },
14
+ * onSessionFingerprint: (fp) => { showFingerprint(fp) },
15
+ * }),
16
+ * ],
17
+ * })
18
+ */
19
+
20
+ import { DAppSession } from '../dapp-session.js'
21
+ import type { SessionPersistence, Transport } from '../types.js'
22
+ import { evmNumericChainId } from '../types.js'
23
+ import { WebSocketTransport } from '../ws-transport.js'
24
+ import { WalletPairProvider } from './eip1193.js'
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Wagmi types (minimal subset to avoid hard dependency)
28
+ // ---------------------------------------------------------------------------
29
+
30
+ interface Chain {
31
+ id: number
32
+ name: string
33
+ [key: string]: unknown
34
+ }
35
+
36
+ interface ConnectorEventMap {
37
+ change: { accounts?: readonly string[] | undefined; chainId?: number | undefined }
38
+ connect: { accounts: readonly string[]; chainId: number }
39
+ disconnect: never
40
+ error: { error: Error }
41
+ message: { type: string; data?: unknown | undefined }
42
+ }
43
+
44
+ interface WagmiEmitter {
45
+ emit<K extends keyof ConnectorEventMap>(event: K, data: ConnectorEventMap[K]): void
46
+ on<K extends keyof ConnectorEventMap>(
47
+ event: K,
48
+ handler: (data: ConnectorEventMap[K]) => void,
49
+ ): void
50
+ off<K extends keyof ConnectorEventMap>(
51
+ event: K,
52
+ handler: (data: ConnectorEventMap[K]) => void,
53
+ ): void
54
+ listenerCount(event: keyof ConnectorEventMap): number
55
+ }
56
+
57
+ interface Storage {
58
+ getItem(key: string): Promise<string | null>
59
+ setItem(key: string, value: string): Promise<void>
60
+ removeItem(key: string): Promise<void>
61
+ }
62
+
63
+ interface ConnectorConfig {
64
+ chains: readonly [Chain, ...Chain[]]
65
+ emitter: WagmiEmitter
66
+ storage?: Storage | null | undefined
67
+ }
68
+
69
+ type CreateConnectorFn = (config: ConnectorConfig) => {
70
+ id: string
71
+ name: string
72
+ type: string
73
+ icon?: string | undefined
74
+ connect(params?: {
75
+ chainId?: number | undefined
76
+ isReconnecting?: boolean | undefined
77
+ }): Promise<{ accounts: readonly string[]; chainId: number }>
78
+ disconnect(): Promise<void>
79
+ getAccounts(): Promise<readonly string[]>
80
+ getChainId(): Promise<number>
81
+ getProvider(params?: { chainId?: number | undefined }): Promise<WalletPairProvider>
82
+ isAuthorized(): Promise<boolean>
83
+ onAccountsChanged(accounts: string[]): void
84
+ onChainChanged(chainId: string): void
85
+ onConnect?(connectInfo: { chainId: string }): void
86
+ onDisconnect(error?: Error | undefined): void
87
+ setup?(): Promise<void>
88
+ switchChain?(params: { chainId: number }): Promise<Chain>
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Connector options
93
+ // ---------------------------------------------------------------------------
94
+
95
+ export interface WalletPairConnectorOptions {
96
+ /** WebSocket relay URL. Used when `transport` is not provided. */
97
+ relayUrl?: string | undefined
98
+ /** Custom transport instance. Overrides relayUrl when provided. */
99
+ transport?: Transport | undefined
100
+ /** DApp metadata (name, description, url, icon). */
101
+ meta: { name: string; description: string; url: string; icon: string }
102
+ /** Request timeout in ms. */
103
+ requestTimeout?: number | undefined
104
+ /** Called when a pairing URI is generated (display QR code). */
105
+ onPairingUri?: ((uri: string) => void) | undefined
106
+ /** Called when the session fingerprint is ready (display alongside QR). */
107
+ onSessionFingerprint?: ((fingerprint: string) => void) | undefined
108
+ /**
109
+ * Called after QR is shown but before transport connects (BLE mode).
110
+ * The returned Promise must resolve when the user is ready to scan.
111
+ * This gives the wallet time to scan the QR before the BLE device picker opens.
112
+ * Only relevant when using a custom transport (e.g. WebBleCentralTransport).
113
+ */
114
+ onBeforeTransportConnect?: (() => Promise<void>) | undefined
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Connector factory
119
+ // ---------------------------------------------------------------------------
120
+
121
+ export function walletPair(options: WalletPairConnectorOptions): CreateConnectorFn {
122
+ let session: DAppSession | null = null
123
+ let provider: WalletPairProvider | null = null
124
+ let sessionEventsBound = false
125
+
126
+ function persistenceFromStorage(
127
+ storage?: Storage | null | undefined,
128
+ ): SessionPersistence | undefined {
129
+ if (!storage) return undefined
130
+ return {
131
+ save: (snapshot) => storage.setItem('walletPair.session', snapshot),
132
+ load: () => storage.getItem('walletPair.session'),
133
+ clear: () => storage.removeItem('walletPair.session'),
134
+ }
135
+ }
136
+
137
+ function getOrCreateSession(config?: ConnectorConfig): DAppSession {
138
+ if (!session) {
139
+ const transport =
140
+ options.transport ??
141
+ (() => {
142
+ if (!options.relayUrl) {
143
+ throw new Error('WalletPair requires either relayUrl or transport')
144
+ }
145
+ return new WebSocketTransport(options.relayUrl)
146
+ })()
147
+ session = new DAppSession({
148
+ transport,
149
+ meta: options.meta,
150
+ requestTimeout: options.requestTimeout,
151
+ persistence: persistenceFromStorage(config?.storage),
152
+ })
153
+ }
154
+ return session
155
+ }
156
+
157
+ function getOrCreateProvider(chainId?: number, config?: ConnectorConfig): WalletPairProvider {
158
+ if (!provider) {
159
+ provider = new WalletPairProvider({
160
+ session: getOrCreateSession(config),
161
+ chainId,
162
+ })
163
+ }
164
+ return provider
165
+ }
166
+
167
+ function bindSessionEvents(config: ConnectorConfig, chainId: number): void {
168
+ const s = getOrCreateSession(config)
169
+ if (sessionEventsBound) return
170
+ sessionEventsBound = true
171
+
172
+ // Bridge WalletPair events → wagmi emitter
173
+ s.on('event', ({ event, data }) => {
174
+ if (event === 'accountsChanged') {
175
+ const accts = (data as { accounts?: string[] })?.accounts ?? (data as string[])
176
+ if (Array.isArray(accts)) {
177
+ config.emitter.emit('change', { accounts: accts })
178
+ }
179
+ } else if (event === 'chainChanged') {
180
+ const raw = (data as { chainId?: string | number })?.chainId ?? data
181
+ let newId: number
182
+ if (typeof raw === 'string' && raw.startsWith('eip155:')) {
183
+ newId = evmNumericChainId(raw) ?? chainId
184
+ } else if (typeof raw === 'string') {
185
+ newId = Number.parseInt(raw, raw.startsWith('0x') ? 16 : 10)
186
+ } else {
187
+ newId = raw as number
188
+ }
189
+ config.emitter.emit('change', { chainId: newId })
190
+ }
191
+ })
192
+ }
193
+
194
+ async function requestAccountsWhenConnected(
195
+ s: DAppSession,
196
+ p: WalletPairProvider,
197
+ ): Promise<readonly string[]> {
198
+ if (s.phase === 'connected') {
199
+ return (await p.request({ method: 'eth_requestAccounts' })) as string[]
200
+ }
201
+
202
+ return new Promise<readonly string[]>((resolve, reject) => {
203
+ const cleanup: (() => void)[] = []
204
+
205
+ cleanup.push(
206
+ s.on('phase', async (phase) => {
207
+ if (phase === 'connected') {
208
+ try {
209
+ const accts = (await p.request({ method: 'eth_requestAccounts' })) as string[]
210
+ for (const off of cleanup) off()
211
+ resolve(accts)
212
+ } catch (e) {
213
+ for (const off of cleanup) off()
214
+ reject(e)
215
+ }
216
+ } else if (phase === 'closed') {
217
+ for (const off of cleanup) off()
218
+ reject(new Error('Session closed'))
219
+ }
220
+ }),
221
+ )
222
+ })
223
+ }
224
+
225
+ return (config: ConnectorConfig) => {
226
+ return {
227
+ id: 'walletPair',
228
+ name: options.meta.name,
229
+ type: 'walletPair',
230
+
231
+ async connect(params) {
232
+ const chainId = params?.chainId ?? config.chains[0].id
233
+ const s = getOrCreateSession(config)
234
+ const p = getOrCreateProvider(chainId, config)
235
+
236
+ bindSessionEvents(config, chainId)
237
+
238
+ if (params?.isReconnecting) {
239
+ const restored = await s.restoreFromPersistence()
240
+ if (!restored) throw new Error('No persisted WalletPair session')
241
+ await s.reconnect()
242
+ const accounts = await requestAccountsWhenConnected(s, p)
243
+ return { accounts, chainId }
244
+ }
245
+
246
+ // Start pairing flow
247
+ // If onBeforeTransportConnect is set (BLE mode), defer transport connection
248
+ // so the wallet can scan the QR before the BLE device picker opens.
249
+ const deferTransport = !!options.onBeforeTransportConnect
250
+ const uri = await s.createPairing({ deferTransport })
251
+ options.onPairingUri?.(uri)
252
+
253
+ if (deferTransport && options.onBeforeTransportConnect) {
254
+ await options.onBeforeTransportConnect()
255
+ await s.connectTransport()
256
+ }
257
+
258
+ // Emit session fingerprint for display alongside QR
259
+ options.onSessionFingerprint?.(s.sessionFingerprint)
260
+
261
+ // Wait for wallet to join (auto-accepted after sealed_join verification)
262
+ const accounts = await requestAccountsWhenConnected(s, p)
263
+
264
+ // DAppSession persists snapshots write-ahead through config.storage.
265
+
266
+ return { accounts, chainId }
267
+ },
268
+
269
+ async disconnect() {
270
+ session?.close()
271
+ session = null
272
+ provider = null
273
+ sessionEventsBound = false
274
+ if (config.storage) {
275
+ await config.storage.removeItem('walletPair.session')
276
+ }
277
+ },
278
+
279
+ async getAccounts() {
280
+ return getOrCreateProvider(undefined, config).getAccounts()
281
+ },
282
+
283
+ async getChainId() {
284
+ return Number.parseInt(getOrCreateProvider(undefined, config).getChainId(), 16)
285
+ },
286
+
287
+ async getProvider() {
288
+ return getOrCreateProvider(undefined, config)
289
+ },
290
+
291
+ async isAuthorized() {
292
+ if (!config.storage) return false
293
+ return getOrCreateSession(config).restoreFromPersistence()
294
+ },
295
+
296
+ onAccountsChanged(accounts: string[]) {
297
+ config.emitter.emit('change', { accounts })
298
+ },
299
+
300
+ onChainChanged(chainId: string) {
301
+ config.emitter.emit('change', { chainId: Number.parseInt(chainId, 16) })
302
+ },
303
+
304
+ onDisconnect() {
305
+ config.emitter.emit('disconnect', undefined as never)
306
+ },
307
+
308
+ async switchChain(params) {
309
+ const p = getOrCreateProvider(undefined, config)
310
+ await p.request({
311
+ method: 'wallet_switchEthereumChain',
312
+ params: [{ chainId: `0x${params.chainId.toString(16)}` }],
313
+ })
314
+ config.emitter.emit('change', { chainId: params.chainId })
315
+ const chain = config.chains.find((c) => c.id === params.chainId)
316
+ if (!chain) throw new Error(`Chain ${params.chainId} not configured`)
317
+ return chain
318
+ },
319
+ }
320
+ }
321
+ }
package/src/index.ts ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * WalletPair SDK — connect dApps and wallets over the WalletPair protocol.
3
+ *
4
+ * Main entry point re-exports everything needed for both dApp and wallet sides.
5
+ *
6
+ * Subpath exports:
7
+ * - walletpair-sdk Core (this file)
8
+ * - walletpair-sdk/evm EVM: EIP-1193 provider + wagmi connector
9
+ * - walletpair-sdk/evm/eip1193 EIP-1193 provider only
10
+ * - walletpair-sdk/evm/wagmi Wagmi connector only
11
+ * - walletpair-sdk/ble BLE transport + framing utilities
12
+ */
13
+
14
+ export type { DirectionalSessionKeys, SessionCryptoContext, X25519KeyPair } from './crypto.js'
15
+ // Crypto
16
+ export {
17
+ b64urlDecode,
18
+ b64urlEncode,
19
+ buildPairingUri,
20
+ bytesToHex,
21
+ canonicalJson,
22
+ computeHandshakeTranscriptHash,
23
+ computeSessionFingerprint,
24
+ computeSharedSecret,
25
+ deriveDirectionalSessionKeys,
26
+ deriveJoinEncryptionKey,
27
+ deriveSessionKey,
28
+ generateChannelId,
29
+ generateX25519KeyPair,
30
+ getPublicKey,
31
+ hexToBytes,
32
+ parsePairingUri,
33
+ sealJoin,
34
+ sealPayload,
35
+ sha256Hex,
36
+ signSnapshot,
37
+ unsealJoin,
38
+ unsealPayload,
39
+ verifySnapshot,
40
+ } from './crypto.js'
41
+ // Sessions
42
+ export { DAppSession } from './dapp-session.js'
43
+ // Emitter
44
+ export { Emitter } from './emitter.js'
45
+ // Types
46
+ export type {
47
+ AcceptMessage,
48
+ Capabilities,
49
+ CloseMessage,
50
+ CloseReason,
51
+ CreateMessage,
52
+ DAppMeta,
53
+ DAppPhase,
54
+ DAppSessionEvents,
55
+ DAppSessionOptions,
56
+ EventMessage,
57
+ JoinMessage,
58
+ PairingParams,
59
+ PendingRequest,
60
+ PingMessage,
61
+ PongMessage,
62
+ ProtocolMessage,
63
+ ProtocolMessageBase,
64
+ ReadyMessage,
65
+ RequestMessage,
66
+ ResponseMessage,
67
+ SessionPersistence,
68
+ TerminateMessage,
69
+ Transport,
70
+ TransportState,
71
+ WalletMeta,
72
+ WalletPhase,
73
+ WalletSessionEvents,
74
+ WalletSessionOptions,
75
+ } from './types.js'
76
+ // Chain ID helpers (CAIP-2)
77
+ export {
78
+ evmChainId,
79
+ evmNumericChainId,
80
+ formatChainId,
81
+ parseChainId,
82
+ } from './types.js'
83
+ export { WalletSession } from './wallet-session.js'
84
+ export type { WebSocketTransportOptions } from './ws-transport.js'
85
+ // Transport
86
+ export { WebSocketTransport } from './ws-transport.js'