accounts 0.3.0 → 0.4.1

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 (168) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +97 -0
  4. package/dist/core/AccessKey.d.ts +55 -0
  5. package/dist/core/AccessKey.d.ts.map +1 -0
  6. package/dist/core/AccessKey.js +69 -0
  7. package/dist/core/AccessKey.js.map +1 -0
  8. package/dist/core/Account.d.ts +91 -0
  9. package/dist/core/Account.d.ts.map +1 -0
  10. package/dist/core/Account.js +64 -0
  11. package/dist/core/Account.js.map +1 -0
  12. package/dist/core/Adapter.d.ts +187 -0
  13. package/dist/core/Adapter.d.ts.map +1 -0
  14. package/dist/core/Adapter.js +7 -0
  15. package/dist/core/Adapter.js.map +1 -0
  16. package/dist/core/Ceremony.d.ts +109 -0
  17. package/dist/core/Ceremony.d.ts.map +1 -0
  18. package/dist/core/Ceremony.js +104 -0
  19. package/dist/core/Ceremony.js.map +1 -0
  20. package/dist/core/Client.d.ts +16 -0
  21. package/dist/core/Client.d.ts.map +1 -0
  22. package/dist/core/Client.js +18 -0
  23. package/dist/core/Client.js.map +1 -0
  24. package/dist/core/Dialog.d.ts +52 -0
  25. package/dist/core/Dialog.d.ts.map +1 -0
  26. package/dist/core/Dialog.js +342 -0
  27. package/dist/core/Dialog.js.map +1 -0
  28. package/dist/core/Expiry.d.ts +15 -0
  29. package/dist/core/Expiry.d.ts.map +1 -0
  30. package/dist/core/Expiry.js +29 -0
  31. package/dist/core/Expiry.js.map +1 -0
  32. package/dist/core/Messenger.d.ts +86 -0
  33. package/dist/core/Messenger.d.ts.map +1 -0
  34. package/dist/core/Messenger.js +127 -0
  35. package/dist/core/Messenger.js.map +1 -0
  36. package/dist/core/Provider.d.ts +69 -0
  37. package/dist/core/Provider.d.ts.map +1 -0
  38. package/dist/core/Provider.js +401 -0
  39. package/dist/core/Provider.js.map +1 -0
  40. package/dist/core/Remote.d.ts +114 -0
  41. package/dist/core/Remote.d.ts.map +1 -0
  42. package/dist/core/Remote.js +116 -0
  43. package/dist/core/Remote.js.map +1 -0
  44. package/dist/core/Schema.d.ts +805 -0
  45. package/dist/core/Schema.d.ts.map +1 -0
  46. package/dist/core/Schema.js +43 -0
  47. package/dist/core/Schema.js.map +1 -0
  48. package/dist/core/Storage.d.ts +42 -0
  49. package/dist/core/Storage.d.ts.map +1 -0
  50. package/dist/core/Storage.js +173 -0
  51. package/dist/core/Storage.js.map +1 -0
  52. package/dist/core/Store.d.ts +58 -0
  53. package/dist/core/Store.d.ts.map +1 -0
  54. package/dist/core/Store.js +58 -0
  55. package/dist/core/Store.js.map +1 -0
  56. package/dist/core/adapters/dangerous_secp256k1.d.ts +30 -0
  57. package/dist/core/adapters/dangerous_secp256k1.d.ts.map +1 -0
  58. package/dist/core/adapters/dangerous_secp256k1.js +39 -0
  59. package/dist/core/adapters/dangerous_secp256k1.js.map +1 -0
  60. package/dist/core/adapters/dialog.d.ts +31 -0
  61. package/dist/core/adapters/dialog.d.ts.map +1 -0
  62. package/dist/core/adapters/dialog.js +306 -0
  63. package/dist/core/adapters/dialog.js.map +1 -0
  64. package/dist/core/adapters/local.d.ts +33 -0
  65. package/dist/core/adapters/local.d.ts.map +1 -0
  66. package/dist/core/adapters/local.js +227 -0
  67. package/dist/core/adapters/local.js.map +1 -0
  68. package/dist/core/adapters/webAuthn.d.ts +36 -0
  69. package/dist/core/adapters/webAuthn.d.ts.map +1 -0
  70. package/dist/core/adapters/webAuthn.js +93 -0
  71. package/dist/core/adapters/webAuthn.js.map +1 -0
  72. package/dist/core/internal/withDedupe.d.ts +12 -0
  73. package/dist/core/internal/withDedupe.d.ts.map +1 -0
  74. package/dist/core/internal/withDedupe.js +12 -0
  75. package/dist/core/internal/withDedupe.js.map +1 -0
  76. package/dist/core/zod/request.d.ts +31 -0
  77. package/dist/core/zod/request.d.ts.map +1 -0
  78. package/dist/core/zod/request.js +41 -0
  79. package/dist/core/zod/request.js.map +1 -0
  80. package/dist/core/zod/rpc.d.ts +603 -0
  81. package/dist/core/zod/rpc.d.ts.map +1 -0
  82. package/dist/core/zod/rpc.js +293 -0
  83. package/dist/core/zod/rpc.js.map +1 -0
  84. package/dist/core/zod/utils.d.ts +18 -0
  85. package/dist/core/zod/utils.d.ts.map +1 -0
  86. package/dist/core/zod/utils.js +21 -0
  87. package/dist/core/zod/utils.js.map +1 -0
  88. package/dist/index.d.ts +15 -0
  89. package/dist/index.d.ts.map +1 -0
  90. package/dist/index.js +15 -0
  91. package/dist/index.js.map +1 -0
  92. package/dist/internal/types.d.ts +284 -0
  93. package/dist/internal/types.d.ts.map +1 -0
  94. package/dist/internal/types.js +2 -0
  95. package/dist/internal/types.js.map +1 -0
  96. package/dist/server/Handler.d.ts +257 -0
  97. package/dist/server/Handler.d.ts.map +1 -0
  98. package/dist/server/Handler.js +433 -0
  99. package/dist/server/Handler.js.map +1 -0
  100. package/dist/server/Kv.d.ts +16 -0
  101. package/dist/server/Kv.d.ts.map +1 -0
  102. package/dist/server/Kv.js +30 -0
  103. package/dist/server/Kv.js.map +1 -0
  104. package/dist/server/index.d.ts +3 -0
  105. package/dist/server/index.d.ts.map +1 -0
  106. package/dist/server/index.js +3 -0
  107. package/dist/server/index.js.map +1 -0
  108. package/dist/server/internal/requestListener.d.ts +124 -0
  109. package/dist/server/internal/requestListener.d.ts.map +1 -0
  110. package/dist/server/internal/requestListener.js +173 -0
  111. package/dist/server/internal/requestListener.js.map +1 -0
  112. package/dist/wagmi/Connector.d.ts +93 -0
  113. package/dist/wagmi/Connector.d.ts.map +1 -0
  114. package/dist/wagmi/Connector.js +238 -0
  115. package/dist/wagmi/Connector.js.map +1 -0
  116. package/dist/wagmi/index.d.ts +3 -0
  117. package/dist/wagmi/index.d.ts.map +1 -0
  118. package/dist/wagmi/index.js +3 -0
  119. package/dist/wagmi/index.js.map +1 -0
  120. package/package.json +56 -2
  121. package/src/core/AccessKey.test.ts +257 -0
  122. package/src/core/AccessKey.ts +123 -0
  123. package/src/core/Account.test.ts +309 -0
  124. package/src/core/Account.ts +152 -0
  125. package/src/core/Adapter.ts +238 -0
  126. package/src/core/Ceremony.browser.test.ts +239 -0
  127. package/src/core/Ceremony.test.ts +151 -0
  128. package/src/core/Ceremony.ts +203 -0
  129. package/src/core/Client.ts +36 -0
  130. package/src/core/Dialog.browser.test.ts +309 -0
  131. package/src/core/Dialog.test-d.ts +19 -0
  132. package/src/core/Dialog.ts +442 -0
  133. package/src/core/Expiry.ts +34 -0
  134. package/src/core/Messenger.ts +206 -0
  135. package/src/core/Provider.browser.test.ts +774 -0
  136. package/src/core/Provider.connect.browser.test.ts +415 -0
  137. package/src/core/Provider.test-d.ts +53 -0
  138. package/src/core/Provider.test.ts +1566 -0
  139. package/src/core/Provider.ts +559 -0
  140. package/src/core/Remote.ts +262 -0
  141. package/src/core/Schema.test-d.ts +211 -0
  142. package/src/core/Schema.ts +143 -0
  143. package/src/core/Storage.ts +213 -0
  144. package/src/core/Store.test.ts +287 -0
  145. package/src/core/Store.ts +129 -0
  146. package/src/core/adapters/dangerous_secp256k1.ts +53 -0
  147. package/src/core/adapters/dialog.ts +379 -0
  148. package/src/core/adapters/local.test.ts +97 -0
  149. package/src/core/adapters/local.ts +277 -0
  150. package/src/core/adapters/webAuthn.ts +129 -0
  151. package/src/core/internal/withDedupe.test.ts +116 -0
  152. package/src/core/internal/withDedupe.ts +20 -0
  153. package/src/core/mppx.test.ts +83 -0
  154. package/src/core/zod/request.test.ts +121 -0
  155. package/src/core/zod/request.ts +70 -0
  156. package/src/core/zod/rpc.ts +374 -0
  157. package/src/core/zod/utils.test.ts +69 -0
  158. package/src/core/zod/utils.ts +40 -0
  159. package/src/index.ts +14 -0
  160. package/src/internal/types.ts +378 -0
  161. package/src/server/Handler.test.ts +1014 -0
  162. package/src/server/Handler.ts +605 -0
  163. package/src/server/Kv.ts +46 -0
  164. package/src/server/index.ts +2 -0
  165. package/src/server/internal/requestListener.ts +273 -0
  166. package/src/tsconfig.json +9 -0
  167. package/src/wagmi/Connector.ts +287 -0
  168. package/src/wagmi/index.ts +2 -0
@@ -0,0 +1,774 @@
1
+ import { requestProviders } from 'mipd'
2
+ import { Hex } from 'ox'
3
+ import { type Address, parseUnits } from 'viem'
4
+ import { verifyHash, verifyMessage, verifyTypedData } from 'viem/actions'
5
+ import { Actions, Addresses } from 'viem/tempo'
6
+ import { describe, expect, test } from 'vp/test'
7
+
8
+ import { accounts, chain, getClient } from '../../test/config.js'
9
+ import { url as webauthnUrl } from '../../test/webauthn.constants.js'
10
+ import { webAuthn } from './adapters/webAuthn.js'
11
+ import * as Ceremony from './Ceremony.js'
12
+ import * as Expiry from './Expiry.js'
13
+ import * as Provider from './Provider.js'
14
+ import * as Storage from './Storage.js'
15
+
16
+ const ceremony = Ceremony.server({ url: webauthnUrl })
17
+
18
+ function getProvider(options: Partial<Provider.create.Options> = {}) {
19
+ return Provider.create({
20
+ storage: Storage.idb({ key: crypto.randomUUID() }),
21
+ adapter: webAuthn({ ceremony }),
22
+ ...options,
23
+ })
24
+ }
25
+
26
+ const transferCall = Actions.token.transfer.call({
27
+ to: '0x0000000000000000000000000000000000000001',
28
+ token: Addresses.pathUsd,
29
+ amount: parseUnits('1', 6),
30
+ })
31
+
32
+ describe('create', () => {
33
+ test('default: returns an EIP-1193 provider', async () => {
34
+ const provider = getProvider()
35
+ expect(provider).toBeDefined()
36
+ expect(typeof provider.request).toMatchInlineSnapshot(`"function"`)
37
+ })
38
+ })
39
+
40
+ describe('eth_chainId', () => {
41
+ test('default: returns configured chain ID as hex', async () => {
42
+ const provider = getProvider()
43
+
44
+ const chainId = await provider.request({ method: 'eth_chainId' })
45
+ expect(chainId).toMatchInlineSnapshot(`"0x1079"`)
46
+ })
47
+ })
48
+
49
+ describe('eth_requestAccounts', () => {
50
+ test('default: returns accounts after registering', async () => {
51
+ const provider = getProvider()
52
+
53
+ await provider.request({
54
+ method: 'wallet_connect',
55
+ params: [{ capabilities: { method: 'register' } }],
56
+ })
57
+
58
+ const accts = await provider.request({ method: 'eth_requestAccounts' })
59
+ expect(accts).toHaveLength(1)
60
+ expect(accts[0]).toMatch(/^0x[0-9a-fA-F]{40}$/)
61
+ })
62
+ })
63
+
64
+ describe('wallet_connect', () => {
65
+ test('default: register returns ERC-7846 response', async () => {
66
+ const provider = getProvider()
67
+
68
+ const result = await provider.request({
69
+ method: 'wallet_connect',
70
+ params: [{ capabilities: { method: 'register' } }],
71
+ })
72
+ expect(result.accounts).toHaveLength(1)
73
+ expect(result.accounts[0]!.address).toMatch(/^0x[0-9a-fA-F]{40}$/)
74
+ expect(result.accounts[0]!.capabilities).toMatchInlineSnapshot(`{}`)
75
+ })
76
+
77
+ test('behavior: register preserves existing accounts', async () => {
78
+ const provider = getProvider()
79
+
80
+ await provider.request({
81
+ method: 'wallet_connect',
82
+ params: [{ capabilities: { method: 'register' } }],
83
+ })
84
+ const result = await provider.request({
85
+ method: 'wallet_connect',
86
+ params: [{ capabilities: { method: 'register' } }],
87
+ })
88
+
89
+ expect(result.accounts.length).toMatchInlineSnapshot(`2`)
90
+ // New account is active (first) and differs from the loaded one
91
+ expect(result.accounts[0]!.address).not.toBe(result.accounts[1]!.address)
92
+ })
93
+
94
+ test('behavior: login with digest returns signature in capabilities', async () => {
95
+ const provider = getProvider({ chains: [chain] })
96
+
97
+ // Register first to create a credential
98
+ await provider.request({
99
+ method: 'wallet_connect',
100
+ params: [{ capabilities: { method: 'register' } }],
101
+ })
102
+ await provider.request({ method: 'wallet_disconnect' })
103
+
104
+ const result = await provider.request({
105
+ method: 'wallet_connect',
106
+ params: [{ capabilities: { digest: '0xdeadbeef' } }],
107
+ })
108
+ expect(result.accounts[0]!.capabilities.signature).toMatch(/^0x[0-9a-f]+$/)
109
+ })
110
+
111
+ test('behavior: digest signature is verifiable on-chain', async () => {
112
+ const provider = getProvider({ chains: [chain] })
113
+
114
+ // Register and fund
115
+ const regResult = await provider.request({
116
+ method: 'wallet_connect',
117
+ params: [{ capabilities: { method: 'register' } }],
118
+ })
119
+ const address = regResult.accounts[0]!.address
120
+ await fundAccount(address)
121
+
122
+ // Disconnect and login with digest
123
+ await provider.request({ method: 'wallet_disconnect' })
124
+ const digest = '0x00000000000000000000000000000000000000000000000000000000deadbeef' as const
125
+ const result = await provider.request({
126
+ method: 'wallet_connect',
127
+ params: [{ capabilities: { digest } }],
128
+ })
129
+
130
+ const client = provider.getClient()
131
+ const valid = await verifyHash(client, {
132
+ address,
133
+ hash: digest,
134
+ signature: result.accounts[0]!.capabilities.signature!,
135
+ })
136
+ expect(valid).toMatchInlineSnapshot(`true`)
137
+ })
138
+
139
+ test('behavior: login without digest returns empty capabilities', async () => {
140
+ const provider = getProvider()
141
+
142
+ // Register first so a credential exists for login
143
+ await provider.request({
144
+ method: 'wallet_connect',
145
+ params: [{ capabilities: { method: 'register' } }],
146
+ })
147
+ await provider.request({ method: 'wallet_disconnect' })
148
+
149
+ const result = await provider.request({ method: 'wallet_connect' })
150
+ expect(result.accounts[0]!.capabilities).toMatchInlineSnapshot(`{}`)
151
+ })
152
+
153
+ test('behavior: register without digest returns empty capabilities', async () => {
154
+ const provider = getProvider()
155
+
156
+ const result = await provider.request({
157
+ method: 'wallet_connect',
158
+ params: [{ capabilities: { method: 'register' } }],
159
+ })
160
+ expect(result.accounts[0]!.capabilities).toMatchInlineSnapshot(`{}`)
161
+ })
162
+
163
+ test('behavior: register with digest returns signature in capabilities', async () => {
164
+ const provider = getProvider({ chains: [chain] })
165
+
166
+ const result = await provider.request({
167
+ method: 'wallet_connect',
168
+ params: [{ capabilities: { method: 'register', digest: '0xdeadbeef' } }],
169
+ })
170
+ expect(result.accounts[0]!.capabilities.signature).toMatch(/^0x[0-9a-f]+$/)
171
+ })
172
+
173
+ test('behavior: register digest signature is verifiable on-chain', async () => {
174
+ const provider = getProvider({ chains: [chain] })
175
+
176
+ const digest = '0x00000000000000000000000000000000000000000000000000000000deadbeef' as const
177
+ const result = await provider.request({
178
+ method: 'wallet_connect',
179
+ params: [{ capabilities: { method: 'register', digest } }],
180
+ })
181
+
182
+ const client = provider.getClient()
183
+ const valid = await verifyHash(client, {
184
+ address: result.accounts[0]!.address,
185
+ hash: digest,
186
+ signature: result.accounts[0]!.capabilities.signature!,
187
+ })
188
+ expect(valid).toMatchInlineSnapshot(`true`)
189
+ })
190
+
191
+ test('behavior: signature only on signer account, not others', async () => {
192
+ const provider = getProvider({ chains: [chain] })
193
+
194
+ // Register first, then login with digest
195
+ await provider.request({
196
+ method: 'wallet_connect',
197
+ params: [{ capabilities: { method: 'register' } }],
198
+ })
199
+ await provider.request({ method: 'wallet_disconnect' })
200
+
201
+ // Login creates a new credential, register another
202
+ await provider.request({ method: 'wallet_connect' })
203
+ await provider.request({
204
+ method: 'wallet_connect',
205
+ params: [{ capabilities: { method: 'register' } }],
206
+ })
207
+
208
+ // Now login with digest — signature only on the loaded account
209
+ const result = await provider.request({
210
+ method: 'wallet_connect',
211
+ params: [{ capabilities: { digest: '0xabcd' } }],
212
+ })
213
+
214
+ const withSig = result.accounts.filter((a) => a.capabilities.signature)
215
+ const withoutSig = result.accounts.filter((a) => !a.capabilities.signature)
216
+ expect(withSig).toHaveLength(1)
217
+ expect(withSig[0]!.capabilities.signature).toMatch(/^0x[0-9a-f]+$/)
218
+ expect(withoutSig.length).toBeGreaterThanOrEqual(1)
219
+ })
220
+
221
+ test('behavior: login deduplicates and sets active account', async () => {
222
+ const provider = getProvider()
223
+
224
+ // Register then login with same loadAccounts
225
+ await provider.request({
226
+ method: 'wallet_connect',
227
+ params: [{ capabilities: { method: 'register' } }],
228
+ })
229
+ await provider.request({ method: 'wallet_connect' })
230
+
231
+ // Deduplicated (same credential returned on login)
232
+ expect(provider.store.getState().accounts.length).toMatchInlineSnapshot(`1`)
233
+ // Active is the loaded account (returned first by eth_accounts)
234
+ const { activeAccount } = provider.store.getState()
235
+ const loadedAddress = provider.store.getState().accounts[activeAccount]!.address
236
+ const result = await provider.request({ method: 'eth_accounts' })
237
+ expect(result[0]).toBe(loadedAddress)
238
+ })
239
+ })
240
+
241
+ describe('wallet_disconnect', () => {
242
+ test('default: clears state', async () => {
243
+ const provider = getProvider()
244
+
245
+ await connect(provider)
246
+ await provider.request({ method: 'wallet_disconnect' })
247
+
248
+ expect(provider.store.getState().accounts).toMatchInlineSnapshot(`[]`)
249
+ })
250
+ })
251
+
252
+ describe('events', () => {
253
+ test('behavior: does not emit accountsChanged on duplicate login', async () => {
254
+ const provider = getProvider()
255
+
256
+ await connect(provider)
257
+
258
+ const events: unknown[] = []
259
+ provider.on('accountsChanged', (accts) => events.push(accts))
260
+
261
+ await provider.request({ method: 'wallet_connect' })
262
+
263
+ expect(events).toMatchInlineSnapshot(`[]`)
264
+ })
265
+ })
266
+
267
+ describe('eth_sendTransaction', () => {
268
+ test('default: sends transaction and returns hash', async () => {
269
+ const provider = getProvider({ chains: [chain] })
270
+
271
+ const address = await connect(provider)
272
+ await fundAccount(address)
273
+
274
+ const hash = await provider.request({
275
+ method: 'eth_sendTransaction',
276
+ params: [{ calls: [transferCall] }],
277
+ })
278
+
279
+ expect(hash).toMatch(/^0x[0-9a-f]{64}$/)
280
+ })
281
+ })
282
+
283
+ describe('eth_sendTransactionSync', () => {
284
+ test('default: sends transaction and returns receipt', async () => {
285
+ const provider = getProvider({ chains: [chain] })
286
+
287
+ const address = await connect(provider)
288
+ await fundAccount(address)
289
+
290
+ const receipt = await provider.request({
291
+ method: 'eth_sendTransactionSync',
292
+ params: [{ calls: [transferCall] }],
293
+ })
294
+
295
+ const {
296
+ blockHash,
297
+ blockNumber,
298
+ cumulativeGasUsed,
299
+ effectiveGasPrice,
300
+ gasUsed,
301
+ logs,
302
+ logsBloom,
303
+ transactionHash,
304
+ transactionIndex,
305
+ from,
306
+ to,
307
+ feePayer,
308
+ ...rest
309
+ } = receipt
310
+ expect(blockHash).toBeDefined()
311
+ expect(blockNumber).toBeDefined()
312
+ expect(cumulativeGasUsed).toBeDefined()
313
+ expect(effectiveGasPrice).toBeDefined()
314
+ expect(gasUsed).toBeDefined()
315
+ expect(logs).toBeInstanceOf(Array)
316
+ expect(logsBloom).toBeDefined()
317
+ expect(transactionHash).toBeDefined()
318
+ expect(transactionIndex).toBeDefined()
319
+ expect(from).toMatch(/^0x[0-9a-f]{40}$/)
320
+ expect(to).toMatch(/^0x[0-9a-f]{40}$/)
321
+ expect(feePayer).toMatch(/^0x[0-9a-f]{40}$/)
322
+ expect(rest).toMatchInlineSnapshot(`
323
+ {
324
+ "contractAddress": null,
325
+ "feeToken": "0x20c0000000000000000000000000000000000000",
326
+ "status": "0x1",
327
+ "type": "0x76",
328
+ }
329
+ `)
330
+ })
331
+ })
332
+
333
+ describe('eth_signTransaction', () => {
334
+ test('default: signs transaction and returns serialized', async () => {
335
+ const provider = getProvider({ chains: [chain] })
336
+
337
+ const address = await connect(provider)
338
+ await fundAccount(address)
339
+
340
+ const signed = await provider.request({
341
+ method: 'eth_signTransaction',
342
+ params: [{ calls: [transferCall] }],
343
+ })
344
+
345
+ expect(signed).toMatch(/^0x/)
346
+ })
347
+
348
+ test('behavior: signed transaction can be sent via eth_sendRawTransactionSync', async () => {
349
+ const provider = getProvider({ chains: [chain] })
350
+
351
+ const address = await connect(provider)
352
+ await fundAccount(address)
353
+
354
+ const signed = await provider.request({
355
+ method: 'eth_signTransaction',
356
+ params: [{ calls: [transferCall] }],
357
+ })
358
+
359
+ const receipt = await provider.request({
360
+ method: 'eth_sendRawTransactionSync',
361
+ params: [signed],
362
+ })
363
+
364
+ expect(receipt.transactionHash).toMatch(/^0x[0-9a-f]{64}$/)
365
+ expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
366
+ })
367
+ })
368
+
369
+ describe('wallet_sendCalls', () => {
370
+ test('default: sends calls and returns id', async () => {
371
+ const provider = getProvider({ chains: [chain] })
372
+
373
+ const address = await connect(provider)
374
+ await fundAccount(address)
375
+
376
+ const result = await provider.request({
377
+ method: 'wallet_sendCalls',
378
+ params: [
379
+ {
380
+ calls: [transferCall],
381
+ },
382
+ ],
383
+ })
384
+
385
+ expect(result.id).toMatch(/^0x[0-9a-f]+$/)
386
+ })
387
+
388
+ test('behavior: with sync capability returns id and sync capability', async () => {
389
+ const provider = getProvider({ chains: [chain] })
390
+
391
+ const address = await connect(provider)
392
+ await fundAccount(address)
393
+
394
+ const result = await provider.request({
395
+ method: 'wallet_sendCalls',
396
+ params: [
397
+ {
398
+ calls: [transferCall],
399
+ capabilities: { sync: true },
400
+ },
401
+ ],
402
+ })
403
+
404
+ expect(result.id).toMatch(/^0x[0-9a-f]+$/)
405
+ expect(result.capabilities).toMatchInlineSnapshot(`
406
+ {
407
+ "sync": true,
408
+ }
409
+ `)
410
+ })
411
+
412
+ test('behavior: sync false uses async path', async () => {
413
+ const provider = getProvider({ chains: [chain] })
414
+
415
+ const address = await connect(provider)
416
+ await fundAccount(address)
417
+
418
+ const result = await provider.request({
419
+ method: 'wallet_sendCalls',
420
+ params: [
421
+ {
422
+ calls: [transferCall],
423
+ capabilities: { sync: false },
424
+ },
425
+ ],
426
+ })
427
+
428
+ expect(result.id).toMatch(/^0x[0-9a-f]+$/)
429
+ expect(result.capabilities).toMatchInlineSnapshot(`
430
+ {
431
+ "sync": false,
432
+ }
433
+ `)
434
+ })
435
+ })
436
+
437
+ describe('personal_sign', () => {
438
+ test('default: signs a message and returns signature', async () => {
439
+ const provider = getProvider({ chains: [chain] })
440
+
441
+ const address = await connect(provider)
442
+
443
+ const message = Hex.fromString('hello world')
444
+ const signature = await provider.request({
445
+ method: 'personal_sign',
446
+ params: [message, address],
447
+ })
448
+
449
+ expect(signature).toMatch(/^0x[0-9a-f]+$/)
450
+ })
451
+
452
+ test('behavior: signature is verifiable on-chain', async () => {
453
+ const provider = getProvider({ chains: [chain] })
454
+
455
+ const address = await connect(provider)
456
+ await fundAccount(address)
457
+
458
+ const message = Hex.fromString('hello world')
459
+ const signature = await provider.request({
460
+ method: 'personal_sign',
461
+ params: [message, address],
462
+ })
463
+
464
+ const client = provider.getClient()
465
+ const valid = await verifyMessage(client, {
466
+ address,
467
+ message: { raw: message },
468
+ signature,
469
+ })
470
+ expect(valid).toMatchInlineSnapshot(`true`)
471
+ })
472
+ })
473
+
474
+ describe('eth_signTypedData_v4', () => {
475
+ const typedData = {
476
+ domain: { name: 'Test', version: '1', chainId: 1 },
477
+ types: {
478
+ Person: [
479
+ { name: 'name', type: 'string' },
480
+ { name: 'wallet', type: 'address' },
481
+ ],
482
+ },
483
+ primaryType: 'Person' as const,
484
+ message: { name: 'Bob', wallet: '0x0000000000000000000000000000000000000000' },
485
+ }
486
+
487
+ test('default: signs typed data and returns signature', async () => {
488
+ const provider = getProvider({ chains: [chain] })
489
+
490
+ const address = await connect(provider)
491
+
492
+ const signature = await provider.request({
493
+ method: 'eth_signTypedData_v4',
494
+ params: [address, JSON.stringify(typedData)],
495
+ })
496
+
497
+ expect(signature).toMatch(/^0x[0-9a-f]+$/)
498
+ })
499
+
500
+ test('behavior: signature is verifiable on-chain', async () => {
501
+ const provider = getProvider({ chains: [chain] })
502
+
503
+ const address = await connect(provider)
504
+ await fundAccount(address)
505
+
506
+ const signature = await provider.request({
507
+ method: 'eth_signTypedData_v4',
508
+ params: [address, JSON.stringify(typedData)],
509
+ })
510
+
511
+ const client = provider.getClient()
512
+ const valid = await verifyTypedData(client, {
513
+ address,
514
+ signature,
515
+ ...typedData,
516
+ })
517
+ expect(valid).toMatchInlineSnapshot(`true`)
518
+ })
519
+ })
520
+
521
+ describe('wallet_authorizeAccessKey', () => {
522
+ test('default: grants an access key and returns its address', async () => {
523
+ const provider = getProvider({ chains: [chain] })
524
+ await connect(provider)
525
+
526
+ const result = await provider.request({
527
+ method: 'wallet_authorizeAccessKey',
528
+ params: [{ expiry: Expiry.days(1) }],
529
+ })
530
+ expect(result.address).toMatch(/^0x[0-9a-fA-F]{40}$/)
531
+ })
532
+
533
+ test('behavior: granted access key is used for sendTransactionSync', async () => {
534
+ const provider = getProvider({ chains: [chain] })
535
+ const address = await connect(provider)
536
+ await fundAccount(address)
537
+
538
+ await provider.request({
539
+ method: 'wallet_authorizeAccessKey',
540
+ params: [{ expiry: Expiry.days(1) }],
541
+ })
542
+
543
+ const receipt = await provider.request({
544
+ method: 'eth_sendTransactionSync',
545
+ params: [{ calls: [transferCall] }],
546
+ })
547
+ expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
548
+ })
549
+
550
+ test('behavior: with expiry option', async () => {
551
+ const provider = getProvider({ chains: [chain] })
552
+ await connect(provider)
553
+
554
+ const expiry = Math.floor(Date.now() / 1000) + 3600
555
+ const result = await provider.request({
556
+ method: 'wallet_authorizeAccessKey',
557
+ params: [{ expiry }],
558
+ })
559
+ expect(result.address).toMatch(/^0x[0-9a-fA-F]{40}$/)
560
+ expect(result.expiry).toBe(Hex.fromNumber(expiry))
561
+ })
562
+
563
+ test('behavior: with limits option', async () => {
564
+ const provider = getProvider({ chains: [chain] })
565
+ const address = await connect(provider)
566
+ await fundAccount(address)
567
+
568
+ const result = await provider.request({
569
+ method: 'wallet_authorizeAccessKey',
570
+ params: [
571
+ {
572
+ expiry: Expiry.days(1),
573
+ limits: [{ token: Addresses.pathUsd, limit: Hex.fromNumber(parseUnits('5', 6)) }],
574
+ },
575
+ ],
576
+ })
577
+ expect(result.limits).toMatchInlineSnapshot(`
578
+ [
579
+ {
580
+ "limit": "0x4c4b40",
581
+ "token": "0x20c0000000000000000000000000000000000000",
582
+ },
583
+ ]
584
+ `)
585
+
586
+ const receipt = await provider.request({
587
+ method: 'eth_sendTransactionSync',
588
+ params: [{ calls: [transferCall] }],
589
+ })
590
+ expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
591
+ })
592
+ })
593
+
594
+ describe('wallet_revokeAccessKey', () => {
595
+ test('default: revokes a granted access key', async () => {
596
+ const provider = getProvider({ chains: [chain] })
597
+ await connect(provider)
598
+
599
+ const connected = (await provider.request({ method: 'eth_accounts' }))[0]!
600
+ const { address: keyAddress } = await provider.request({
601
+ method: 'wallet_authorizeAccessKey',
602
+ params: [{ expiry: Expiry.days(1) }],
603
+ })
604
+
605
+ await provider.request({
606
+ method: 'wallet_revokeAccessKey',
607
+ params: [{ address: connected, accessKeyAddress: keyAddress }],
608
+ })
609
+
610
+ await fundAccount(connected)
611
+
612
+ const receipt = await provider.request({
613
+ method: 'eth_sendTransactionSync',
614
+ params: [{ calls: [transferCall] }],
615
+ })
616
+ expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
617
+ })
618
+ })
619
+
620
+ describe('wallet_connect with authorizeAccessKey', () => {
621
+ test('default: grants access key during register', async () => {
622
+ const provider = getProvider({ chains: [chain] })
623
+
624
+ const result = await provider.request({
625
+ method: 'wallet_connect',
626
+ params: [
627
+ {
628
+ capabilities: {
629
+ method: 'register',
630
+ authorizeAccessKey: { expiry: Math.floor(Date.now() / 1000) + 3600 },
631
+ },
632
+ },
633
+ ],
634
+ })
635
+ expect(result.accounts.length).toBeGreaterThanOrEqual(1)
636
+ expect(result.accounts[0]!.capabilities.keyAuthorization).toBeDefined()
637
+ expect(result.accounts[0]!.capabilities.keyAuthorization!.address).toMatch(/^0x[0-9a-f]{40}$/i)
638
+
639
+ const address = result.accounts[0]!.address
640
+ await fundAccount(address)
641
+
642
+ const receipt = await provider.request({
643
+ method: 'eth_sendTransactionSync',
644
+ params: [{ calls: [transferCall] }],
645
+ })
646
+ expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
647
+ })
648
+
649
+ test('behavior: authorizeAccessKey with expiry during register', async () => {
650
+ const provider = getProvider({ chains: [chain] })
651
+
652
+ const expiry = Math.floor(Date.now() / 1000) + 3600
653
+ const result = await provider.request({
654
+ method: 'wallet_connect',
655
+ params: [{ capabilities: { method: 'register', authorizeAccessKey: { expiry } } }],
656
+ })
657
+ expect(result.accounts.length).toBeGreaterThanOrEqual(1)
658
+ })
659
+
660
+ test('behavior: authorizeAccessKey during login', async () => {
661
+ const provider = getProvider({ chains: [chain] })
662
+
663
+ await provider.request({
664
+ method: 'wallet_connect',
665
+ params: [{ capabilities: { method: 'register' } }],
666
+ })
667
+
668
+ const result = await provider.request({
669
+ method: 'wallet_connect',
670
+ params: [
671
+ {
672
+ capabilities: { authorizeAccessKey: { expiry: Math.floor(Date.now() / 1000) + 3600 } },
673
+ },
674
+ ],
675
+ })
676
+ expect(result.accounts.length).toBeGreaterThanOrEqual(1)
677
+
678
+ const address = result.accounts[0]!.address
679
+ await fundAccount(address)
680
+
681
+ const receipt = await provider.request({
682
+ method: 'eth_sendTransactionSync',
683
+ params: [{ calls: [transferCall] }],
684
+ })
685
+ expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
686
+ })
687
+
688
+ test('behavior: authorizeAccessKey with digest during register', async () => {
689
+ const provider = getProvider({ chains: [chain] })
690
+
691
+ const result = await provider.request({
692
+ method: 'wallet_connect',
693
+ params: [
694
+ {
695
+ capabilities: {
696
+ method: 'register',
697
+ digest: '0xdeadbeef',
698
+ authorizeAccessKey: { expiry: Math.floor(Date.now() / 1000) + 3600 },
699
+ },
700
+ },
701
+ ],
702
+ })
703
+ expect(result.accounts[0]!.capabilities.signature).toMatch(/^0x[0-9a-f]+$/)
704
+ expect(result.accounts[0]!.capabilities.keyAuthorization).toBeDefined()
705
+ })
706
+ })
707
+
708
+ describe('eip-6963', () => {
709
+ test('default: announces provider when adapter has icon, name, and rdns', async () => {
710
+ const discovered: { info: { icon: string; name: string; rdns: string; uuid: string } }[] = []
711
+ const unsubscribe = requestProviders((detail) => discovered.push(detail as never))
712
+
713
+ getProvider({
714
+ adapter: webAuthn({
715
+ ceremony,
716
+ icon: 'data:image/svg+xml,<svg></svg>',
717
+ name: 'Test Wallet',
718
+ rdns: 'com.example.test',
719
+ }),
720
+ })
721
+
722
+ // Wait a tick for the event to fire
723
+ await new Promise((resolve) => setTimeout(resolve, 0))
724
+
725
+ const match = discovered.find((d) => d.info.rdns === 'com.example.test')!
726
+ expect(match.info.name).toMatchInlineSnapshot(`"Test Wallet"`)
727
+ expect(match.info.rdns).toMatchInlineSnapshot(`"com.example.test"`)
728
+ expect(match.info.icon).toMatchInlineSnapshot(`"data:image/svg+xml,<svg></svg>"`)
729
+ expect(match.info.uuid).toMatch(
730
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
731
+ )
732
+
733
+ unsubscribe?.()
734
+ })
735
+
736
+ test('behavior: defaults icon and rdns when not provided', async () => {
737
+ const discovered: { info: { icon: string; name: string; rdns: string } }[] = []
738
+ const unsubscribe = requestProviders((detail) => discovered.push(detail as never))
739
+
740
+ getProvider({
741
+ adapter: webAuthn({ ceremony, name: 'My Wallet' }),
742
+ })
743
+
744
+ await new Promise((resolve) => setTimeout(resolve, 0))
745
+
746
+ const info = discovered[discovered.length - 1]!.info
747
+ expect(info.name).toMatchInlineSnapshot(`"My Wallet"`)
748
+ expect(info.rdns).toMatchInlineSnapshot(`"com.mywallet"`)
749
+ expect(info.icon).toMatch(/^data:image\/svg\+xml,/)
750
+
751
+ unsubscribe?.()
752
+ })
753
+ })
754
+
755
+ /** Funds an account with fee tokens so it can send transactions. */
756
+ async function fundAccount(address: Address) {
757
+ const client = getClient()
758
+ await Actions.token.transferSync(client, {
759
+ account: accounts[0]!,
760
+ feeToken: Addresses.pathUsd,
761
+ to: address,
762
+ token: Addresses.pathUsd,
763
+ amount: parseUnits('10', 6),
764
+ })
765
+ }
766
+
767
+ /** Registers a new account and returns its address. */
768
+ async function connect(provider: ReturnType<typeof getProvider>) {
769
+ const result = await provider.request({
770
+ method: 'wallet_connect',
771
+ params: [{ capabilities: { method: 'register' } }],
772
+ })
773
+ return result.accounts[0]!.address
774
+ }