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,216 @@
1
+ /**
2
+ * Shared test helpers — mock transport for unit testing sessions.
3
+ */
4
+
5
+ import type { Capabilities, Transport, TransportState, ProtocolMessage, WalletMeta } from './types.js';
6
+ import {
7
+ b64urlDecode,
8
+ computeSharedSecret,
9
+ deriveJoinEncryptionKey,
10
+ deriveSessionKey,
11
+ sealJoin,
12
+ } from './crypto.js';
13
+ import type { X25519KeyPair } from './crypto.js';
14
+
15
+ export const DEFAULT_TEST_CAPABILITIES: Capabilities = {
16
+ methods: ['wallet_getAccounts', 'wallet_signMessage'],
17
+ events: ['accountsChanged', 'chainChanged'],
18
+ chains: ['eip155:1'],
19
+ };
20
+
21
+ /** Extract the JSON payload from a signed or unsigned snapshot string. */
22
+ export function parseSnapshot(signed: string): Record<string, unknown> {
23
+ // Signed format: "<64-hex-mac>.<json>"
24
+ if (signed.length > 65 && signed[64] === '.') {
25
+ return JSON.parse(signed.slice(65));
26
+ }
27
+ return JSON.parse(signed);
28
+ }
29
+
30
+ export const DEFAULT_TEST_WALLET_META: WalletMeta = {
31
+ name: 'Test Wallet',
32
+ description: 'Test wallet',
33
+ url: 'https://wallet.test',
34
+ icon: 'https://wallet.test/icon.png',
35
+ };
36
+
37
+ export function makeSealedJoin(
38
+ channelId: string,
39
+ dappPubKeyB64: string,
40
+ walletKp: X25519KeyPair,
41
+ capabilities: Capabilities = DEFAULT_TEST_CAPABILITIES,
42
+ meta: WalletMeta = DEFAULT_TEST_WALLET_META,
43
+ ): string {
44
+ const dappPub = b64urlDecode(dappPubKeyB64);
45
+ const shared = computeSharedSecret(walletKp.privateKey, dappPub);
46
+ const rootKey = deriveSessionKey(shared, channelId);
47
+ const joinKey = deriveJoinEncryptionKey(rootKey, channelId);
48
+ const sealed = sealJoin(joinKey, channelId, capabilities, meta);
49
+ shared.fill(0);
50
+ rootKey.fill(0);
51
+ joinKey.fill(0);
52
+ return sealed;
53
+ }
54
+
55
+ export function makeJoinBody(
56
+ channelId: string,
57
+ dappPubKeyB64: string,
58
+ walletKp: X25519KeyPair,
59
+ capabilities: Capabilities = DEFAULT_TEST_CAPABILITIES,
60
+ meta: WalletMeta = DEFAULT_TEST_WALLET_META,
61
+ ): { sealed_join: string } {
62
+ return {
63
+ sealed_join: makeSealedJoin(channelId, dappPubKeyB64, walletKp, capabilities, meta),
64
+ };
65
+ }
66
+
67
+ /**
68
+ * In-memory transport for testing. Two MockTransports can be linked
69
+ * to simulate a relay (messages sent on one arrive on the other).
70
+ */
71
+ export class MockTransport implements Transport {
72
+ state: TransportState = 'disconnected';
73
+ sent: ProtocolMessage[] = [];
74
+
75
+ private messageHandler: ((msg: ProtocolMessage) => void) | null = null;
76
+ private closeHandler: (() => void) | null = null;
77
+ private openHandler: (() => void) | null = null;
78
+
79
+ /** Link to the peer's transport. */
80
+ peer: MockTransport | null = null;
81
+
82
+ onMessage(handler: (msg: ProtocolMessage) => void): void { this.messageHandler = handler; }
83
+ onClose(handler: () => void): void { this.closeHandler = handler; }
84
+ onOpen(handler: () => void): void { this.openHandler = handler; }
85
+
86
+ async connect(): Promise<void> {
87
+ this.state = 'connected';
88
+ this.openHandler?.();
89
+ }
90
+
91
+ send(msg: ProtocolMessage): void {
92
+ this.sent.push(msg);
93
+ // Deliver to peer asynchronously (simulates relay)
94
+ if (this.peer) {
95
+ const peer = this.peer;
96
+ queueMicrotask(() => peer.receive(msg));
97
+ }
98
+ }
99
+
100
+ disconnect(): void {
101
+ this.state = 'disconnected';
102
+ }
103
+
104
+ /** Simulate receiving a message from the relay. */
105
+ receive(msg: ProtocolMessage): void {
106
+ this.messageHandler?.(msg);
107
+ }
108
+
109
+ /** Simulate transport close (disconnect from relay). */
110
+ simulateClose(): void {
111
+ this.state = 'disconnected';
112
+ this.closeHandler?.();
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Create a pair of linked mock transports.
118
+ * Messages sent on dapp arrive on wallet and vice versa.
119
+ */
120
+ export function createLinkedTransports(): { dapp: MockTransport; wallet: MockTransport } {
121
+ const dapp = new MockTransport();
122
+ const wallet = new MockTransport();
123
+ dapp.peer = wallet;
124
+ wallet.peer = dapp;
125
+ return { dapp, wallet };
126
+ }
127
+
128
+ /**
129
+ * Simulate the relay's role: when dApp sends "create", respond with "ready.waiting".
130
+ * When wallet sends "join", forward to dApp and respond with "ready.waiting".
131
+ * When dApp sends "accept", respond with "ready.connected" to both.
132
+ */
133
+ export class MockRelay {
134
+ private dappTransport: MockTransport;
135
+ private walletTransport: MockTransport;
136
+
137
+ constructor(dapp: MockTransport, wallet: MockTransport) {
138
+ this.dappTransport = dapp;
139
+ this.walletTransport = wallet;
140
+
141
+ // Intercept sends and inject relay behavior
142
+ const origDappSend = dapp.send.bind(dapp);
143
+ dapp.send = (msg: ProtocolMessage) => {
144
+ origDappSend(msg);
145
+ this.handleDappMessage(msg);
146
+ };
147
+
148
+ const origWalletSend = wallet.send.bind(wallet);
149
+ wallet.send = (msg: ProtocolMessage) => {
150
+ origWalletSend(msg);
151
+ this.handleWalletMessage(msg);
152
+ };
153
+
154
+ // Don't forward to peer directly — relay controls message flow
155
+ dapp.peer = null;
156
+ wallet.peer = null;
157
+ }
158
+
159
+ private handleDappMessage(msg: ProtocolMessage): void {
160
+ if (msg.t === 'create') {
161
+ queueMicrotask(() => {
162
+ this.dappTransport.receive({
163
+ v: 1, t: 'ready', ch: msg.ch,
164
+ ts: Date.now(), from: '_adapter',
165
+ body: { state: 'waiting', reconnect: false, remote: null },
166
+ } as ProtocolMessage);
167
+ });
168
+ } else if (msg.t === 'accept') {
169
+ queueMicrotask(() => {
170
+ const target = (msg.body as any).target;
171
+ this.dappTransport.receive({
172
+ v: 1, t: 'ready', ch: msg.ch,
173
+ ts: Date.now(), from: '_adapter',
174
+ body: { state: 'connected', reconnect: false, remote: target },
175
+ } as ProtocolMessage);
176
+ this.walletTransport.receive({
177
+ v: 1, t: 'ready', ch: msg.ch,
178
+ ts: Date.now(), from: '_adapter',
179
+ body: { state: 'connected', reconnect: false, remote: msg.from },
180
+ } as ProtocolMessage);
181
+ });
182
+ } else if (msg.t === 'req') {
183
+ // Forward to wallet
184
+ queueMicrotask(() => this.walletTransport.receive(msg));
185
+ } else if (msg.t === 'ping') {
186
+ // Forward to wallet
187
+ queueMicrotask(() => this.walletTransport.receive(msg));
188
+ } else if (msg.t === 'close') {
189
+ queueMicrotask(() => this.walletTransport.receive(msg));
190
+ }
191
+ }
192
+
193
+ private handleWalletMessage(msg: ProtocolMessage): void {
194
+ if (msg.t === 'join') {
195
+ queueMicrotask(() => {
196
+ // Relay sends ready.waiting to wallet
197
+ this.walletTransport.receive({
198
+ v: 1, t: 'ready', ch: msg.ch,
199
+ ts: Date.now(), from: '_adapter',
200
+ body: { state: 'waiting', reconnect: false, remote: null },
201
+ } as ProtocolMessage);
202
+ // Relay forwards join to dApp
203
+ this.dappTransport.receive(msg);
204
+ });
205
+ } else if (msg.t === 'res') {
206
+ // Forward to dApp
207
+ queueMicrotask(() => this.dappTransport.receive(msg));
208
+ } else if (msg.t === 'evt') {
209
+ queueMicrotask(() => this.dappTransport.receive(msg));
210
+ } else if (msg.t === 'pong') {
211
+ queueMicrotask(() => this.dappTransport.receive(msg));
212
+ } else if (msg.t === 'close') {
213
+ queueMicrotask(() => this.dappTransport.receive(msg));
214
+ }
215
+ }
216
+ }
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ parseChainId,
4
+ formatChainId,
5
+ evmChainId,
6
+ evmNumericChainId,
7
+ } from './types.js';
8
+
9
+ describe('parseChainId', () => {
10
+ it('parses eip155:1', () => {
11
+ const { namespace, reference } = parseChainId('eip155:1');
12
+ expect(namespace).toBe('eip155');
13
+ expect(reference).toBe('1');
14
+ });
15
+
16
+ it('parses solana:mainnet', () => {
17
+ const { namespace, reference } = parseChainId('solana:mainnet');
18
+ expect(namespace).toBe('solana');
19
+ expect(reference).toBe('mainnet');
20
+ });
21
+
22
+ it('parses cosmos:cosmoshub-4', () => {
23
+ const { namespace, reference } = parseChainId('cosmos:cosmoshub-4');
24
+ expect(namespace).toBe('cosmos');
25
+ expect(reference).toBe('cosmoshub-4');
26
+ });
27
+
28
+ it('throws on missing reference', () => {
29
+ expect(() => parseChainId('eip155')).toThrow('Invalid CAIP-2');
30
+ });
31
+
32
+ it('throws on empty string', () => {
33
+ expect(() => parseChainId('')).toThrow('Invalid CAIP-2');
34
+ });
35
+
36
+ it('throws on just a colon', () => {
37
+ expect(() => parseChainId(':')).toThrow('Invalid CAIP-2');
38
+ });
39
+ });
40
+
41
+ describe('formatChainId', () => {
42
+ it('formats eip155 + 1', () => {
43
+ expect(formatChainId('eip155', '1')).toBe('eip155:1');
44
+ });
45
+
46
+ it('formats solana + mainnet', () => {
47
+ expect(formatChainId('solana', 'mainnet')).toBe('solana:mainnet');
48
+ });
49
+
50
+ it('round-trips with parseChainId', () => {
51
+ const caip2 = 'eip155:137';
52
+ const { namespace, reference } = parseChainId(caip2);
53
+ expect(formatChainId(namespace, reference)).toBe(caip2);
54
+ });
55
+ });
56
+
57
+ describe('evmChainId', () => {
58
+ it('converts numeric chain ID to CAIP-2', () => {
59
+ expect(evmChainId(1)).toBe('eip155:1');
60
+ expect(evmChainId(137)).toBe('eip155:137');
61
+ expect(evmChainId(42161)).toBe('eip155:42161');
62
+ });
63
+ });
64
+
65
+ describe('evmNumericChainId', () => {
66
+ it('extracts numeric ID from eip155: prefix', () => {
67
+ expect(evmNumericChainId('eip155:1')).toBe(1);
68
+ expect(evmNumericChainId('eip155:137')).toBe(137);
69
+ expect(evmNumericChainId('eip155:42161')).toBe(42161);
70
+ });
71
+
72
+ it('returns null for non-eip155 chains', () => {
73
+ expect(evmNumericChainId('solana:mainnet')).toBeNull();
74
+ expect(evmNumericChainId('cosmos:cosmoshub-4')).toBeNull();
75
+ });
76
+
77
+ it('round-trips with evmChainId', () => {
78
+ for (const id of [1, 5, 10, 56, 137, 42161]) {
79
+ expect(evmNumericChainId(evmChainId(id))).toBe(id);
80
+ }
81
+ });
82
+ });
package/src/types.ts ADDED
@@ -0,0 +1,305 @@
1
+ /**
2
+ * WalletPair SDK — shared types and interfaces.
3
+ *
4
+ * Multi-chain ready: uses CAIP-2 chain IDs throughout (e.g. "eip155:1").
5
+ */
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Transport
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export type TransportState = 'disconnected' | 'connecting' | 'connected'
12
+
13
+ export interface Transport {
14
+ readonly state: TransportState
15
+ send(msg: ProtocolMessage): void
16
+ connect(): Promise<void>
17
+ disconnect(): void
18
+ onMessage(handler: (msg: ProtocolMessage) => void): void
19
+ onClose(handler: () => void): void
20
+ onOpen(handler: () => void): void
21
+ }
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Protocol messages (wire format)
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export interface ProtocolMessageBase {
28
+ v: 1
29
+ t: string
30
+ ch: string
31
+ ts: number
32
+ from: string
33
+ body: Record<string, unknown>
34
+ }
35
+
36
+ export interface CreateMessage extends ProtocolMessageBase {
37
+ t: 'create'
38
+ body: { meta: DAppMeta }
39
+ }
40
+
41
+ export interface JoinMessage extends ProtocolMessageBase {
42
+ t: 'join'
43
+ body: { sealed_join: string | null }
44
+ }
45
+
46
+ export interface AcceptMessage extends ProtocolMessageBase {
47
+ t: 'accept'
48
+ body: { target: string }
49
+ }
50
+
51
+ export interface ReadyMessage extends ProtocolMessageBase {
52
+ t: 'ready'
53
+ body: {
54
+ state: 'waiting' | 'connected'
55
+ role: 'dapp' | 'wallet'
56
+ self: string
57
+ remote: string | null
58
+ reconnect: boolean
59
+ }
60
+ }
61
+
62
+ export interface RequestMessage extends ProtocolMessageBase {
63
+ t: 'req'
64
+ body: { id: string; sealed: string }
65
+ }
66
+
67
+ export interface ResponseMessage extends ProtocolMessageBase {
68
+ t: 'res'
69
+ body: { id: string; sealed: string }
70
+ }
71
+
72
+ export interface EventMessage extends ProtocolMessageBase {
73
+ t: 'evt'
74
+ body: { id: string; sealed: string }
75
+ }
76
+
77
+ export interface PingMessage extends ProtocolMessageBase {
78
+ t: 'ping'
79
+ body: Record<string, never>
80
+ }
81
+
82
+ export interface PongMessage extends ProtocolMessageBase {
83
+ t: 'pong'
84
+ body: Record<string, never>
85
+ }
86
+
87
+ export interface CloseMessage extends ProtocolMessageBase {
88
+ t: 'close'
89
+ body: { reason: CloseReason }
90
+ }
91
+
92
+ export interface TerminateMessage extends ProtocolMessageBase {
93
+ t: 'terminate'
94
+ from: '_adapter'
95
+ body: { reason: CloseReason }
96
+ }
97
+
98
+ export type ProtocolMessage =
99
+ | CreateMessage
100
+ | JoinMessage
101
+ | AcceptMessage
102
+ | ReadyMessage
103
+ | RequestMessage
104
+ | ResponseMessage
105
+ | EventMessage
106
+ | PingMessage
107
+ | PongMessage
108
+ | CloseMessage
109
+ | TerminateMessage
110
+
111
+ export type CloseReason =
112
+ | 'normal'
113
+ | 'user_rejected'
114
+ | 'unsupported_capability'
115
+ | 'channel_not_found'
116
+ | 'channel_exists'
117
+ | 'already_connected'
118
+ | 'invalid_state'
119
+ | 'invalid_role'
120
+ | 'timeout'
121
+ | 'rate_limited'
122
+ | 'payload_too_large'
123
+ | 'protocol_error'
124
+ | 'unsupported_version'
125
+ | 'decryption_failed'
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Capabilities & metadata (multi-chain via CAIP-2)
129
+ // ---------------------------------------------------------------------------
130
+
131
+ export interface Capabilities {
132
+ /** Supported RPC methods (e.g. "wallet_getAccounts", "wallet_signMessage"). */
133
+ methods: string[]
134
+ /** Supported events (e.g. "accountsChanged", "chainChanged"). */
135
+ events: string[]
136
+ /** CAIP-2 chain IDs (e.g. "eip155:1", "solana:mainnet"). */
137
+ chains: string[]
138
+ /** Sub-protocol version map (e.g. { evm: 1 }). §8 */
139
+ version?: Record<string, number> | undefined
140
+ }
141
+
142
+ export interface DAppMeta {
143
+ name: string
144
+ description: string
145
+ url: string
146
+ icon: string
147
+ }
148
+
149
+ export interface WalletMeta {
150
+ name: string
151
+ description: string
152
+ url: string
153
+ icon: string
154
+ [key: string]: unknown
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Session phases
159
+ // ---------------------------------------------------------------------------
160
+
161
+ export type DAppPhase =
162
+ | 'idle'
163
+ | 'waiting'
164
+ | 'pending_accept'
165
+ | 'connected'
166
+ | 'disconnected'
167
+ | 'closed'
168
+
169
+ export type WalletPhase =
170
+ | 'idle'
171
+ | 'waiting'
172
+ | 'waiting_accept'
173
+ | 'connected'
174
+ | 'disconnected'
175
+ | 'closed'
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // Session events
179
+ // ---------------------------------------------------------------------------
180
+
181
+ export interface DAppSessionEvents {
182
+ [key: string]: unknown
183
+ phase: DAppPhase
184
+ pairingUri: string
185
+ sessionFingerprint: string
186
+ walletJoined: { capabilities?: Capabilities | undefined; meta?: WalletMeta | undefined }
187
+ response: { id: string; ok: boolean; result: unknown }
188
+ event: { event: string; data: unknown }
189
+ error: Error
190
+ }
191
+
192
+ export interface WalletSessionEvents {
193
+ [key: string]: unknown
194
+ phase: WalletPhase
195
+ sessionFingerprint: string
196
+ request: { id: string; method: string; params: unknown }
197
+ error: Error
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Pairing URI
202
+ // ---------------------------------------------------------------------------
203
+
204
+ export interface PairingParams {
205
+ ch: string
206
+ pubkey: string
207
+ /** Relay URL. Undefined when using direct transport (BLE). */
208
+ relay?: string | undefined
209
+ name: string
210
+ /** DApp website URL. */
211
+ url: string
212
+ /** DApp icon URL. */
213
+ icon: string
214
+ /** Methods the dApp intends to call (§9.1). */
215
+ methods?: string[] | undefined
216
+ /** CAIP-2 chains the dApp intends to use (§9.1). */
217
+ chains?: string[] | undefined
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // Request/response helpers
222
+ // ---------------------------------------------------------------------------
223
+
224
+ export interface PendingRequest {
225
+ id: string
226
+ method: string
227
+ resolve: (result: unknown) => void
228
+ reject: (error: Error) => void
229
+ timer?: ReturnType<typeof setTimeout>
230
+ }
231
+
232
+ export interface SessionPersistence {
233
+ /**
234
+ * Persist a complete serialized session snapshot. Implementations used for
235
+ * reconnect MUST make this durable before resolving; the SDK calls this
236
+ * before sending each encrypted message after advancing `sendSeq`.
237
+ */
238
+ save(snapshot: string): void | Promise<void>
239
+ /** Load the most recent durable snapshot. */
240
+ load?(): string | null | Promise<string | null>
241
+ /** Remove the durable snapshot when the channel closes. */
242
+ clear?(): void | Promise<void>
243
+ }
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Session options
247
+ // ---------------------------------------------------------------------------
248
+
249
+ export interface DAppSessionOptions {
250
+ transport: Transport
251
+ /** DApp metadata (name, description, url, icon). Included in pairing URI and create message. */
252
+ meta: DAppMeta
253
+ /** Methods the dApp intends to call (included in pairing URI §9.1). */
254
+ methods?: string[] | undefined
255
+ /** CAIP-2 chains the dApp intends to use (included in pairing URI §9.1). */
256
+ chains?: string[] | undefined
257
+ /** Request timeout in ms (default 120_000). */
258
+ requestTimeout?: number | undefined
259
+ /** Session lifetime in ms (default 86_400_000 = 24h). §16 rule 16. */
260
+ sessionTtl?: number | undefined
261
+ /** Auto-accept known wallets on rejoin (default true). */
262
+ autoAccept?: boolean | undefined
263
+ /** Durable snapshot persistence for reconnect and write-ahead counters. */
264
+ persistence?: SessionPersistence | undefined
265
+ }
266
+
267
+ export interface WalletSessionOptions {
268
+ transport: Transport
269
+ /** Wallet capabilities to advertise. */
270
+ capabilities: Capabilities
271
+ /** Wallet metadata (name, description, url, icon). */
272
+ meta: WalletMeta
273
+ /** Session lifetime in ms (default 86_400_000 = 24h). §16 rule 16. */
274
+ sessionTtl?: number | undefined
275
+ /** Durable snapshot persistence for reconnect and write-ahead counters. */
276
+ persistence?: SessionPersistence | undefined
277
+ }
278
+
279
+ // ---------------------------------------------------------------------------
280
+ // Chain namespace helpers (for future multi-chain)
281
+ // ---------------------------------------------------------------------------
282
+
283
+ /** Parse CAIP-2 chain ID into namespace and reference. */
284
+ export function parseChainId(caip2: string): { namespace: string; reference: string } {
285
+ const [namespace, reference] = caip2.split(':')
286
+ if (!namespace || !reference) throw new Error(`Invalid CAIP-2 chain ID: ${caip2}`)
287
+ return { namespace, reference }
288
+ }
289
+
290
+ /** Build CAIP-2 chain ID from namespace and reference. */
291
+ export function formatChainId(namespace: string, reference: string): string {
292
+ return `${namespace}:${reference}`
293
+ }
294
+
295
+ /** Convert EVM numeric chain ID to CAIP-2. */
296
+ export function evmChainId(id: number): string {
297
+ return `eip155:${id}`
298
+ }
299
+
300
+ /** Extract EVM numeric chain ID from CAIP-2. Returns null if not eip155. */
301
+ export function evmNumericChainId(caip2: string): number | null {
302
+ const { namespace, reference } = parseChainId(caip2)
303
+ if (namespace !== 'eip155') return null
304
+ return Number.parseInt(reference, 10)
305
+ }