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,1566 @@
1
+ import { Hex, Provider as core_Provider, WebCryptoP256 } from 'ox'
2
+ import { KeyAuthorization } from 'ox/tempo'
3
+ import { type Address, createClient, custom, parseUnits } from 'viem'
4
+ import {
5
+ getBalance,
6
+ sendTransactionSync,
7
+ signMessage,
8
+ verifyHash,
9
+ verifyMessage,
10
+ verifyTypedData,
11
+ waitForTransactionReceipt,
12
+ } from 'viem/actions'
13
+ import { tempo, tempoModerato } from 'viem/chains'
14
+ import { Account as TempoAccount, Actions, Addresses } from 'viem/tempo'
15
+ import { afterAll, beforeAll, describe, expect, test } from 'vp/test'
16
+
17
+ import { headlessWebAuthn, secp256k1 } from '../../test/adapters.js'
18
+ import { accounts, chain, getClient, http } from '../../test/config.js'
19
+ import { createServer, type Server } from '../../test/utils.js'
20
+ import * as Handler from '../server/Handler.js'
21
+ import * as Expiry from './Expiry.js'
22
+ import * as Provider from './Provider.js'
23
+ import * as Storage from './Storage.js'
24
+
25
+ const adapters = [
26
+ { name: 'headlessWebAuthn', adapter: headlessWebAuthn },
27
+ { name: 'secp256k1', adapter: secp256k1 },
28
+ ] as const
29
+
30
+ describe.each(adapters)('$name', ({ adapter }: (typeof adapters)[number]) => {
31
+ const transferCall = Actions.token.transfer.call({
32
+ to: '0x0000000000000000000000000000000000000001',
33
+ token: Addresses.pathUsd,
34
+ amount: parseUnits('1', 6),
35
+ })
36
+
37
+ /** Connects via login (or register if login returns no accounts), returns the active account address. */
38
+ async function connect(provider: ReturnType<typeof Provider.create>) {
39
+ const login = await provider.request({ method: 'wallet_connect' })
40
+ if (login.accounts.length > 0) return login.accounts[0]!.address
41
+ const register = await provider.request({
42
+ method: 'wallet_connect',
43
+ params: [{ capabilities: { method: 'register' } }],
44
+ })
45
+ return register.accounts[0]!.address
46
+ }
47
+
48
+ /** Funds an address with PathUSD from the pre-funded test account. */
49
+ async function fund(address: Address) {
50
+ const client = getClient()
51
+ await Actions.token.transferSync(client, {
52
+ account: accounts[0]!,
53
+ feeToken: Addresses.pathUsd,
54
+ to: address,
55
+ token: Addresses.pathUsd,
56
+ amount: parseUnits('10', 6),
57
+ })
58
+ }
59
+
60
+ describe('create', () => {
61
+ test('default: returns an EIP-1193 provider', async () => {
62
+ const provider = Provider.create({ adapter: adapter() })
63
+ expect(typeof provider.request).toMatch(/function/)
64
+ })
65
+ })
66
+
67
+ describe('eth_chainId', () => {
68
+ test('default: returns configured chain ID as hex', async () => {
69
+ const provider = Provider.create({ adapter: adapter() })
70
+ const chainId = await provider.request({ method: 'eth_chainId' })
71
+ expect(chainId).toMatchInlineSnapshot(`"0x1079"`)
72
+ })
73
+ })
74
+
75
+ describe('eth_accounts', () => {
76
+ test('default: returns empty array initially', async () => {
77
+ const provider = Provider.create({ adapter: adapter() })
78
+ const accounts = await provider.request({ method: 'eth_accounts' })
79
+ expect(accounts).toMatchInlineSnapshot(`[]`)
80
+ })
81
+
82
+ test('behavior: returns accounts after connecting', async () => {
83
+ const provider = Provider.create({ adapter: adapter() })
84
+
85
+ await connect(provider)
86
+ const result = await provider.request({ method: 'eth_accounts' })
87
+ expect(result.length).toBeGreaterThanOrEqual(1)
88
+ })
89
+ })
90
+
91
+ describe('eth_requestAccounts', () => {
92
+ test('default: returns accounts after connecting', async () => {
93
+ const provider = Provider.create({ adapter: adapter() })
94
+ await connect(provider)
95
+ const result = await provider.request({ method: 'eth_requestAccounts' })
96
+ expect(result.length).toBeGreaterThanOrEqual(1)
97
+ })
98
+ })
99
+
100
+ describe('wallet_connect', () => {
101
+ test('default: without capabilities calls loadAccounts', async () => {
102
+ const provider = Provider.create({ adapter: adapter() })
103
+ const result = await provider.request({ method: 'wallet_connect' })
104
+ for (const account of result.accounts) {
105
+ expect(account.address).toMatch(/^0x[0-9a-f]{40}$/i)
106
+ expect(account.capabilities).toMatchInlineSnapshot(`{}`)
107
+ }
108
+ })
109
+
110
+ test('behavior: with register capability calls createAccount', async () => {
111
+ const provider = Provider.create({ adapter: adapter() })
112
+
113
+ const result = await provider.request({
114
+ method: 'wallet_connect',
115
+ params: [{ capabilities: { method: 'register' } }],
116
+ })
117
+ expect(result.accounts.length).toMatchInlineSnapshot(`1`)
118
+ expect(result.accounts[0]!.address).toMatch(/^0x[0-9a-f]{40}$/i)
119
+ expect(result.accounts[0]!.capabilities).toMatchInlineSnapshot(`{}`)
120
+ })
121
+
122
+ test('behavior: register passes name to createAccount', async () => {
123
+ const provider = Provider.create({ adapter: adapter() })
124
+
125
+ await provider.request({
126
+ method: 'wallet_connect',
127
+ params: [{ capabilities: { method: 'register', name: 'alice' } }],
128
+ })
129
+ expect(provider.store.getState().accounts.length).toBeGreaterThanOrEqual(1)
130
+ })
131
+
132
+ test('behavior: register defaults name to "default"', async () => {
133
+ const provider = Provider.create({ adapter: adapter() })
134
+
135
+ await provider.request({
136
+ method: 'wallet_connect',
137
+ params: [{ capabilities: { method: 'register' } }],
138
+ })
139
+ expect(provider.store.getState().accounts.length).toBeGreaterThanOrEqual(1)
140
+ })
141
+
142
+ test('behavior: login sets activeAccount to loaded account', async () => {
143
+ const provider = Provider.create({ adapter: adapter() })
144
+
145
+ await provider.request({
146
+ method: 'wallet_connect',
147
+ params: [{ capabilities: { method: 'register' } }],
148
+ })
149
+ const login = await provider.request({ method: 'wallet_connect' })
150
+ const result = await provider.request({ method: 'wallet_connect' })
151
+ expect(result.accounts[0]!.address).toBe(login.accounts[0]!.address)
152
+ })
153
+
154
+ test('behavior: login with digest returns signature in account capabilities', async () => {
155
+ const provider = Provider.create({ adapter: adapter() })
156
+
157
+ await connect(provider)
158
+ const result = await provider.request({
159
+ method: 'wallet_connect',
160
+ params: [{ capabilities: { digest: '0x1234' } }],
161
+ })
162
+ expect(result.accounts[0]!.capabilities.signature).toMatch(/^0x[0-9a-f]+$/)
163
+ })
164
+
165
+ test('behavior: digest signature is verifiable on-chain', async () => {
166
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
167
+ const client = provider.getClient()
168
+
169
+ await connect(provider)
170
+ const digest = '0x00000000000000000000000000000000000000000000000000000000deadbeef' as const
171
+ const result = await provider.request({
172
+ method: 'wallet_connect',
173
+ params: [{ capabilities: { digest } }],
174
+ })
175
+
176
+ const valid = await verifyHash(client, {
177
+ address: result.accounts[0]!.address,
178
+ hash: digest,
179
+ signature: result.accounts[0]!.capabilities.signature!,
180
+ })
181
+ expect(valid).toMatchInlineSnapshot(`true`)
182
+ })
183
+
184
+ test('behavior: login without digest returns empty capabilities', async () => {
185
+ const provider = Provider.create({ adapter: adapter() })
186
+ await connect(provider)
187
+ const result = await provider.request({ method: 'wallet_connect' })
188
+ expect(result.accounts[0]!.capabilities).toMatchInlineSnapshot(`{}`)
189
+ })
190
+
191
+ test('behavior: register without digest returns empty capabilities', async () => {
192
+ const provider = Provider.create({ adapter: adapter() })
193
+
194
+ const result = await provider.request({
195
+ method: 'wallet_connect',
196
+ params: [{ capabilities: { method: 'register' } }],
197
+ })
198
+ expect(result.accounts[0]!.capabilities).toMatchInlineSnapshot(`{}`)
199
+ })
200
+
201
+ test('behavior: register with digest returns signature in capabilities', async () => {
202
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
203
+
204
+ const result = await provider.request({
205
+ method: 'wallet_connect',
206
+ params: [{ capabilities: { method: 'register', digest: '0x1234' } }],
207
+ })
208
+ expect(result.accounts[0]!.capabilities.signature).toMatch(/^0x[0-9a-f]+$/)
209
+ })
210
+
211
+ test('behavior: register digest signature is verifiable on-chain', async () => {
212
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
213
+ const client = provider.getClient()
214
+
215
+ const digest = '0x00000000000000000000000000000000000000000000000000000000deadbeef' as const
216
+ const result = await provider.request({
217
+ method: 'wallet_connect',
218
+ params: [{ capabilities: { method: 'register', digest } }],
219
+ })
220
+
221
+ const valid = await verifyHash(client, {
222
+ address: result.accounts[0]!.address,
223
+ hash: digest,
224
+ signature: result.accounts[0]!.capabilities.signature!,
225
+ })
226
+ expect(valid).toMatchInlineSnapshot(`true`)
227
+ })
228
+ })
229
+
230
+ describe('wallet_disconnect', () => {
231
+ test('default: disconnects and clears accounts', async () => {
232
+ const provider = Provider.create({ adapter: adapter() })
233
+
234
+ await connect(provider)
235
+ await provider.request({ method: 'wallet_disconnect' })
236
+
237
+ const accounts = await provider.request({ method: 'eth_accounts' })
238
+ expect(accounts).toMatchInlineSnapshot(`[]`)
239
+ })
240
+ })
241
+
242
+ describe('wallet_switchEthereumChain', () => {
243
+ test('default: switches chain', async () => {
244
+ const provider = Provider.create({ adapter: adapter() })
245
+
246
+ await provider.request({
247
+ method: 'wallet_switchEthereumChain',
248
+ params: [{ chainId: `0x${tempoModerato.id.toString(16)}` }],
249
+ })
250
+
251
+ const chainId = await provider.request({ method: 'eth_chainId' })
252
+ expect(chainId).toMatchInlineSnapshot(`"0xa5bf"`)
253
+ })
254
+
255
+ test('error: throws for unconfigured chain', async () => {
256
+ const provider = Provider.create({ adapter: adapter() })
257
+
258
+ await expect(
259
+ provider.request({
260
+ method: 'wallet_switchEthereumChain',
261
+ params: [{ chainId: '0x1' }],
262
+ }),
263
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
264
+ `[Provider.UnsupportedChainIdError: Chain 1 not configured.]`,
265
+ )
266
+ })
267
+ })
268
+
269
+ describe('events', () => {
270
+ test('behavior: emits accountsChanged on connect', async () => {
271
+ const provider = Provider.create({ adapter: adapter() })
272
+
273
+ const events: unknown[] = []
274
+ provider.on('accountsChanged', (accounts) => events.push(accounts))
275
+
276
+ const connected = await connect(provider)
277
+
278
+ expect(events).toEqual([[connected]])
279
+ })
280
+
281
+ test('behavior: emits connect on status change', async () => {
282
+ const provider = Provider.create({ adapter: adapter() })
283
+
284
+ const events: unknown[] = []
285
+ provider.on('connect', (info) => events.push(info))
286
+
287
+ await connect(provider)
288
+
289
+ expect(events).toMatchInlineSnapshot(`
290
+ [
291
+ {
292
+ "chainId": "0x1079",
293
+ },
294
+ ]
295
+ `)
296
+ })
297
+
298
+ test('behavior: emits disconnect on disconnect', async () => {
299
+ const provider = Provider.create({ adapter: adapter() })
300
+
301
+ await connect(provider)
302
+
303
+ const events: unknown[] = []
304
+ provider.on('disconnect', (error) => events.push(error))
305
+
306
+ await provider.request({ method: 'wallet_disconnect' })
307
+
308
+ expect(events.length).toMatchInlineSnapshot(`1`)
309
+ expect(events[0]).toBeInstanceOf(core_Provider.DisconnectedError)
310
+ })
311
+
312
+ test('behavior: does not emit accountsChanged on duplicate login', async () => {
313
+ const provider = Provider.create({ adapter: adapter() })
314
+
315
+ await connect(provider)
316
+
317
+ const events: unknown[] = []
318
+ provider.on('accountsChanged', (accounts) => events.push(accounts))
319
+
320
+ await provider.request({ method: 'wallet_connect' })
321
+
322
+ expect(events).toMatchInlineSnapshot(`[]`)
323
+ })
324
+
325
+ test('behavior: emits chainChanged on switch', async () => {
326
+ const provider = Provider.create({ adapter: adapter() })
327
+
328
+ const events: unknown[] = []
329
+ provider.on('chainChanged', (chainId) => events.push(chainId))
330
+
331
+ await provider.request({
332
+ method: 'wallet_switchEthereumChain',
333
+ params: [{ chainId: `0x${tempoModerato.id.toString(16)}` }],
334
+ })
335
+
336
+ expect(events).toMatchInlineSnapshot(`
337
+ [
338
+ "0xa5bf",
339
+ ]
340
+ `)
341
+ })
342
+ })
343
+
344
+ describe('eth_sendTransaction', () => {
345
+ test('default: sends transaction and returns hash', async () => {
346
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
347
+
348
+ const connected = await connect(provider)
349
+ await fund(connected)
350
+
351
+ const hash = await provider.request({
352
+ method: 'eth_sendTransaction',
353
+ params: [{ calls: [transferCall] }],
354
+ })
355
+
356
+ expect(hash).toMatch(/^0x[0-9a-f]{64}$/)
357
+ })
358
+
359
+ test('behavior: transaction is confirmed on-chain', async () => {
360
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
361
+
362
+ const connected = await connect(provider)
363
+ await fund(connected)
364
+
365
+ const hash = await provider.request({
366
+ method: 'eth_sendTransaction',
367
+ params: [{ calls: [transferCall] }],
368
+ })
369
+
370
+ const client = provider.getClient()
371
+ const receipt = await waitForTransactionReceipt(client, { hash })
372
+
373
+ const {
374
+ blockHash,
375
+ blockNumber,
376
+ cumulativeGasUsed,
377
+ effectiveGasPrice,
378
+ feePayer,
379
+ from,
380
+ gasUsed,
381
+ logs,
382
+ logsBloom,
383
+ transactionHash,
384
+ transactionIndex,
385
+ ...rest
386
+ } = receipt
387
+ expect(blockHash).toMatch(/^0x[0-9a-f]{64}$/)
388
+ expect(typeof blockNumber).toMatch(/bigint/)
389
+ expect(typeof cumulativeGasUsed).toMatch(/bigint/)
390
+ expect(typeof effectiveGasPrice).toMatch(/bigint/)
391
+ expect(feePayer).toMatch(/^0x[0-9a-f]{40}$/i)
392
+ expect(from).toMatch(/^0x[0-9a-f]{40}$/i)
393
+ expect(typeof gasUsed).toMatch(/bigint/)
394
+ for (const log of logs) expect(log.address).toMatch(/^0x[0-9a-f]{40}$/i)
395
+ expect(logsBloom).toMatch(/^0x/)
396
+ expect(transactionHash).toMatch(/^0x[0-9a-f]{64}$/)
397
+ expect(typeof transactionIndex).toMatch(/number/)
398
+ expect(rest).toMatchInlineSnapshot(`
399
+ {
400
+ "contractAddress": null,
401
+ "feeToken": "0x20c0000000000000000000000000000000000000",
402
+ "status": "success",
403
+ "to": "0x20c0000000000000000000000000000000000000",
404
+ "type": "0x76",
405
+ }
406
+ `)
407
+ })
408
+ })
409
+
410
+ describe('eth_sendTransactionSync', () => {
411
+ test('default: sends transaction and returns receipt', async () => {
412
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
413
+
414
+ const connected = await connect(provider)
415
+ await fund(connected)
416
+
417
+ const receipt = await provider.request({
418
+ method: 'eth_sendTransactionSync',
419
+ params: [{ calls: [transferCall] }],
420
+ })
421
+
422
+ const {
423
+ blockHash,
424
+ blockNumber,
425
+ cumulativeGasUsed,
426
+ effectiveGasPrice,
427
+ feePayer,
428
+ from,
429
+ gasUsed,
430
+ logs,
431
+ logsBloom,
432
+ transactionHash,
433
+ transactionIndex,
434
+ ...rest
435
+ } = receipt
436
+ expect(blockHash).toMatch(/^0x[0-9a-f]{64}$/)
437
+ expect(blockNumber).toMatch(/^0x/)
438
+ expect(cumulativeGasUsed).toMatch(/^0x/)
439
+ expect(effectiveGasPrice).toMatch(/^0x/)
440
+ expect(feePayer).toMatch(/^0x[0-9a-f]{40}$/i)
441
+ expect(from).toMatch(/^0x[0-9a-f]{40}$/i)
442
+ expect(gasUsed).toMatch(/^0x/)
443
+ for (const log of logs) expect(log.address).toMatch(/^0x[0-9a-f]{40}$/i)
444
+ expect(logsBloom).toMatch(/^0x/)
445
+ expect(transactionHash).toMatch(/^0x[0-9a-f]{64}$/)
446
+ expect(transactionIndex).toMatch(/^0x/)
447
+ expect(rest).toMatchInlineSnapshot(`
448
+ {
449
+ "contractAddress": null,
450
+ "feeToken": "0x20c0000000000000000000000000000000000000",
451
+ "status": "0x1",
452
+ "to": "0x20c0000000000000000000000000000000000000",
453
+ "type": "0x76",
454
+ }
455
+ `)
456
+ })
457
+ })
458
+
459
+ describe('eth_signTransaction', () => {
460
+ test('default: signs transaction and returns serialized', async () => {
461
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
462
+
463
+ const connected = await connect(provider)
464
+ await fund(connected)
465
+
466
+ const signed = await provider.request({
467
+ method: 'eth_signTransaction',
468
+ params: [{ calls: [transferCall] }],
469
+ })
470
+
471
+ expect(signed).toMatch(/^0x/)
472
+ })
473
+
474
+ test('behavior: signed transaction can be sent via eth_sendRawTransactionSync', async () => {
475
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
476
+
477
+ const connected = await connect(provider)
478
+ await fund(connected)
479
+
480
+ const signed = await provider.request({
481
+ method: 'eth_signTransaction',
482
+ params: [{ calls: [transferCall] }],
483
+ })
484
+
485
+ const receipt = await provider.request({
486
+ method: 'eth_sendRawTransactionSync',
487
+ params: [signed],
488
+ })
489
+
490
+ const {
491
+ blockHash,
492
+ blockNumber,
493
+ cumulativeGasUsed,
494
+ effectiveGasPrice,
495
+ // @ts-expect-error
496
+ feePayer,
497
+ from,
498
+ gasUsed,
499
+ logs,
500
+ logsBloom,
501
+ transactionHash,
502
+ transactionIndex,
503
+ ...rest
504
+ } = receipt
505
+ expect(blockHash).toMatch(/^0x[0-9a-f]{64}$/)
506
+ expect(blockNumber).toMatch(/^0x/)
507
+ expect(cumulativeGasUsed).toMatch(/^0x/)
508
+ expect(effectiveGasPrice).toMatch(/^0x/)
509
+ expect(feePayer).toMatch(/^0x[0-9a-f]{40}$/i)
510
+ expect(from).toMatch(/^0x[0-9a-f]{40}$/i)
511
+ expect(gasUsed).toMatch(/^0x/)
512
+ for (const log of logs) expect(log.address).toMatch(/^0x[0-9a-f]{40}$/i)
513
+ expect(logsBloom).toMatch(/^0x/)
514
+ expect(transactionHash).toMatch(/^0x[0-9a-f]{64}$/)
515
+ expect(transactionIndex).toMatch(/^0x/)
516
+ expect(rest).toMatchInlineSnapshot(`
517
+ {
518
+ "contractAddress": null,
519
+ "feeToken": "0x20c0000000000000000000000000000000000000",
520
+ "status": "0x1",
521
+ "to": "0x20c0000000000000000000000000000000000000",
522
+ "type": "0x76",
523
+ }
524
+ `)
525
+ })
526
+
527
+ test('error: throws when not connected', async () => {
528
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
529
+
530
+ await expect(
531
+ provider.request({
532
+ method: 'eth_signTransaction',
533
+ params: [{ calls: [transferCall] }],
534
+ }),
535
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
536
+ `[Provider.DisconnectedError: No accounts connected.]`,
537
+ )
538
+ })
539
+ })
540
+
541
+ describe('wallet_sendCalls', () => {
542
+ test('default: sends calls and returns id', async () => {
543
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
544
+
545
+ const connected = await connect(provider)
546
+ await fund(connected)
547
+
548
+ const result = await provider.request({
549
+ method: 'wallet_sendCalls',
550
+ params: [{ calls: [transferCall] }],
551
+ })
552
+
553
+ expect(result.id).toMatch(/^0x[0-9a-f]+$/)
554
+ })
555
+
556
+ test('behavior: with sync capability returns id and receipt is available', async () => {
557
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
558
+
559
+ const connected = await connect(provider)
560
+ await fund(connected)
561
+
562
+ const result = await provider.request({
563
+ method: 'wallet_sendCalls',
564
+ params: [
565
+ {
566
+ calls: [transferCall],
567
+ capabilities: { sync: true },
568
+ },
569
+ ],
570
+ })
571
+
572
+ expect(result.id).toMatch(/^0x[0-9a-f]+$/)
573
+ expect(result.capabilities).toMatchInlineSnapshot(`
574
+ {
575
+ "sync": true,
576
+ }
577
+ `)
578
+ expect(result.atomic).toMatchInlineSnapshot(`true`)
579
+ expect(result.chainId).toMatch(/^0x[0-9a-f]+$/)
580
+ expect(result.status).toMatchInlineSnapshot(`200`)
581
+ expect(result.version).toMatchInlineSnapshot(`"2.0.0"`)
582
+ expect(result.receipts?.length).toMatchInlineSnapshot(`1`)
583
+ expect(result.receipts?.[0]?.status).toMatchInlineSnapshot(`"0x1"`)
584
+ })
585
+ })
586
+
587
+ describe('wallet_getCallsStatus', () => {
588
+ test('default: returns encoded status for a sent call batch', async () => {
589
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
590
+
591
+ const connected = await connect(provider)
592
+ await fund(connected)
593
+
594
+ const { id } = await provider.request({
595
+ method: 'wallet_sendCalls',
596
+ params: [
597
+ {
598
+ calls: [transferCall],
599
+ capabilities: { sync: true },
600
+ },
601
+ ],
602
+ })
603
+
604
+ const result = await provider.request({
605
+ method: 'wallet_getCallsStatus',
606
+ params: [id],
607
+ })
608
+
609
+ expect(result.atomic).toMatchInlineSnapshot(`true`)
610
+ expect(result.chainId).toMatch(/^0x[0-9a-f]+$/)
611
+ expect(result.status).toMatchInlineSnapshot(`200`)
612
+ expect(result.version).toMatchInlineSnapshot(`"2.0.0"`)
613
+ expect(result.receipts?.length).toMatchInlineSnapshot(`1`)
614
+ expect(result.receipts?.[0]?.status).toMatchInlineSnapshot(`"0x1"`)
615
+ })
616
+
617
+ test('error: throws for unsupported id format', async () => {
618
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
619
+
620
+ await expect(
621
+ provider.request({
622
+ method: 'wallet_getCallsStatus',
623
+ params: ['0xdeadbeef'],
624
+ }),
625
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
626
+ `[RpcResponse.InternalError: \`id\` not supported]`,
627
+ )
628
+ })
629
+ })
630
+
631
+ describe('wallet_getCapabilities', () => {
632
+ test('default: returns atomic supported for all chains', async () => {
633
+ const provider = Provider.create({ adapter: adapter() })
634
+
635
+ const result = await provider.request({ method: 'wallet_getCapabilities' })
636
+ expect(result).toMatchInlineSnapshot(`
637
+ {
638
+ "0x1079": {
639
+ "accessKeys": {
640
+ "status": "supported",
641
+ },
642
+ "atomic": {
643
+ "status": "supported",
644
+ },
645
+ },
646
+ "0xa5bf": {
647
+ "accessKeys": {
648
+ "status": "supported",
649
+ },
650
+ "atomic": {
651
+ "status": "supported",
652
+ },
653
+ },
654
+ }
655
+ `)
656
+ })
657
+
658
+ test('behavior: filters by chainIds', async () => {
659
+ const provider = Provider.create({ adapter: adapter() })
660
+
661
+ const connected = await connect(provider)
662
+
663
+ const result = await provider.request({
664
+ method: 'wallet_getCapabilities',
665
+ params: [connected, [Hex.fromNumber(tempoModerato.id)]],
666
+ })
667
+ expect(result).toMatchInlineSnapshot(`
668
+ {
669
+ "0xa5bf": {
670
+ "accessKeys": {
671
+ "status": "supported",
672
+ },
673
+ "atomic": {
674
+ "status": "supported",
675
+ },
676
+ },
677
+ }
678
+ `)
679
+ })
680
+
681
+ test('behavior: returns empty object for unknown chainIds', async () => {
682
+ const provider = Provider.create({ adapter: adapter() })
683
+
684
+ const connected = await connect(provider)
685
+
686
+ const result = await provider.request({
687
+ method: 'wallet_getCapabilities',
688
+ params: [connected, ['0x1']],
689
+ })
690
+ expect(result).toMatchInlineSnapshot(`{}`)
691
+ })
692
+
693
+ test('error: throws UnauthorizedError for unconnected address', async () => {
694
+ const provider = Provider.create({ adapter: adapter() })
695
+
696
+ await expect(
697
+ provider.request({
698
+ method: 'wallet_getCapabilities',
699
+ params: ['0x0000000000000000000000000000000000000001'],
700
+ }),
701
+ ).rejects.toThrow(core_Provider.UnauthorizedError)
702
+ })
703
+
704
+ test('behavior: succeeds with connected address', async () => {
705
+ const provider = Provider.create({ adapter: adapter() })
706
+
707
+ const connected = await connect(provider)
708
+
709
+ const result = await provider.request({
710
+ method: 'wallet_getCapabilities',
711
+ params: [connected],
712
+ })
713
+ expect(Object.keys(result).length).toMatchInlineSnapshot(`2`)
714
+ expect(result[Hex.fromNumber(tempo.id)]!.atomic.status).toMatchInlineSnapshot(`"supported"`)
715
+ })
716
+ })
717
+
718
+ describe('wallet_getBalances', () => {
719
+ test('error: throws when no tokens provided', async () => {
720
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
721
+
722
+ await connect(provider)
723
+
724
+ await expect(
725
+ provider.request({ method: 'wallet_getBalances' }),
726
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
727
+ `[RpcResponse.InvalidParamsError: \`tokens\` is required.]`,
728
+ )
729
+ })
730
+
731
+ test('default: returns token balances with metadata', async () => {
732
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
733
+
734
+ await connect(provider)
735
+
736
+ const result = await provider.request({
737
+ method: 'wallet_getBalances',
738
+ params: [{ tokens: ['0x20c0000000000000000000000000000000000001'] }],
739
+ })
740
+
741
+ expect(result.length).toMatchInlineSnapshot(`1`)
742
+ expect(result[0]!.address).toMatchInlineSnapshot(
743
+ `"0x20c0000000000000000000000000000000000001"`,
744
+ )
745
+ expect(typeof result[0]!.name).toMatch(/string/)
746
+ expect(typeof result[0]!.symbol).toMatch(/string/)
747
+ expect(typeof result[0]!.decimals).toMatchInlineSnapshot(`"number"`)
748
+ expect(result[0]!.balance).toMatch(/^0x/)
749
+ })
750
+
751
+ test('behavior: accepts explicit account param', async () => {
752
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
753
+
754
+ const connected = await connect(provider)
755
+
756
+ const result = await provider.request({
757
+ method: 'wallet_getBalances',
758
+ params: [
759
+ {
760
+ account: connected,
761
+ tokens: ['0x20c0000000000000000000000000000000000001'],
762
+ },
763
+ ],
764
+ })
765
+
766
+ expect(result.length).toMatchInlineSnapshot(`1`)
767
+ expect(result[0]!.balance).toMatch(/^0x/)
768
+ })
769
+
770
+ test('error: throws DisconnectedError when no accounts connected', async () => {
771
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
772
+
773
+ await expect(
774
+ provider.request({
775
+ method: 'wallet_getBalances',
776
+ params: [{ tokens: ['0x20c0000000000000000000000000000000000001'] }],
777
+ }),
778
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
779
+ `[Provider.DisconnectedError: No accounts connected.]`,
780
+ )
781
+ })
782
+ })
783
+
784
+ describe('eth_signTypedData_v4', () => {
785
+ const typedData = {
786
+ domain: { name: 'Test', version: '1', chainId: 1 },
787
+ types: {
788
+ Person: [
789
+ { name: 'name', type: 'string' },
790
+ { name: 'wallet', type: 'address' },
791
+ ],
792
+ },
793
+ primaryType: 'Person' as const,
794
+ message: { name: 'Bob', wallet: '0x0000000000000000000000000000000000000000' },
795
+ }
796
+
797
+ test('default: signs typed data and returns signature', async () => {
798
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
799
+
800
+ const connected = await connect(provider)
801
+
802
+ const signature = await provider.request({
803
+ method: 'eth_signTypedData_v4',
804
+ params: [connected, JSON.stringify(typedData)],
805
+ })
806
+
807
+ expect(signature).toMatch(/^0x[0-9a-f]+$/)
808
+ })
809
+
810
+ test('behavior: signature is verifiable on-chain', async () => {
811
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
812
+ const client = provider.getClient()
813
+
814
+ const connected = await connect(provider)
815
+
816
+ const signature = await provider.request({
817
+ method: 'eth_signTypedData_v4',
818
+ params: [connected, JSON.stringify(typedData)],
819
+ })
820
+
821
+ const valid = await verifyTypedData(client, {
822
+ address: connected,
823
+ signature,
824
+ ...typedData,
825
+ })
826
+ expect(valid).toMatchInlineSnapshot(`true`)
827
+ })
828
+
829
+ test('error: throws when not connected', async () => {
830
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
831
+
832
+ await expect(
833
+ provider.request({
834
+ method: 'eth_signTypedData_v4',
835
+ params: ['0x0000000000000000000000000000000000000001', JSON.stringify(typedData)],
836
+ }),
837
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
838
+ `[Provider.DisconnectedError: No accounts connected.]`,
839
+ )
840
+ })
841
+ })
842
+
843
+ describe('personal_sign', () => {
844
+ test('default: signs a message and returns signature', async () => {
845
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
846
+
847
+ const connected = await connect(provider)
848
+
849
+ const message = Hex.fromString('hello world')
850
+ const signature = await provider.request({
851
+ method: 'personal_sign',
852
+ params: [message, connected],
853
+ })
854
+
855
+ expect(signature).toMatch(/^0x[0-9a-f]+$/)
856
+ })
857
+
858
+ test('behavior: signature is verifiable on-chain', async () => {
859
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
860
+ const client = provider.getClient()
861
+
862
+ const connected = await connect(provider)
863
+
864
+ const message = Hex.fromString('hello world')
865
+ const signature = await provider.request({
866
+ method: 'personal_sign',
867
+ params: [message, connected],
868
+ })
869
+
870
+ const valid = await verifyMessage(client, {
871
+ address: connected,
872
+ message: { raw: message },
873
+ signature,
874
+ })
875
+ expect(valid).toMatchInlineSnapshot(`true`)
876
+ })
877
+
878
+ test('error: throws when not connected', async () => {
879
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
880
+
881
+ await expect(
882
+ provider.request({
883
+ method: 'personal_sign',
884
+ params: [Hex.fromString('hello'), '0x0000000000000000000000000000000000000001'],
885
+ }),
886
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
887
+ `[Provider.DisconnectedError: No accounts connected.]`,
888
+ )
889
+ })
890
+ })
891
+
892
+ describe('persistence', () => {
893
+ test('behavior: new provider hydrates accounts from shared storage', async () => {
894
+ const storage = Storage.memory({ key: 'persist-test' })
895
+
896
+ const provider1 = Provider.create({ adapter: adapter(), storage })
897
+ await connect(provider1)
898
+
899
+ const accts1 = await provider1.request({ method: 'eth_accounts' })
900
+ expect(accts1.length).toBeGreaterThanOrEqual(1)
901
+
902
+ // Create a second provider with the same storage — it should hydrate.
903
+ const provider2 = Provider.create({ adapter: adapter(), storage })
904
+
905
+ // Wait for hydration + reconnection.
906
+ await new Promise((resolve) => setTimeout(resolve, 200))
907
+
908
+ const accts2 = await provider2.request({ method: 'eth_accounts' })
909
+ expect(accts2.length).toBeGreaterThanOrEqual(1)
910
+ expect(accts2[0]).toBe(accts1[0])
911
+ })
912
+
913
+ test('behavior: concurrent providers with different storage keys are isolated', async () => {
914
+ const providerA = Provider.create({
915
+ adapter: adapter(),
916
+ storage: Storage.memory({ key: 'provider-a' }),
917
+ })
918
+ const providerB = Provider.create({
919
+ adapter: adapter(),
920
+ storage: Storage.memory({ key: 'provider-b' }),
921
+ })
922
+
923
+ await connect(providerA)
924
+
925
+ const acctsA = await providerA.request({ method: 'eth_accounts' })
926
+ const acctsB = await providerB.request({ method: 'eth_accounts' })
927
+
928
+ expect(acctsA.length).toBeGreaterThanOrEqual(1)
929
+ expect(acctsB).toMatchInlineSnapshot(`[]`)
930
+ })
931
+ })
932
+
933
+ describe('reconnection', () => {
934
+ test('behavior: hydrated provider has accounts available', async () => {
935
+ const storage = Storage.memory({ key: 'reconnect' })
936
+
937
+ const provider1 = Provider.create({ adapter: adapter(), storage })
938
+ await connect(provider1)
939
+
940
+ const provider2 = Provider.create({ adapter: adapter(), storage })
941
+
942
+ // Wait for hydration.
943
+ await new Promise((resolve) => setTimeout(resolve, 200))
944
+
945
+ const accts = await provider2.request({ method: 'eth_accounts' })
946
+ expect(accts.length).toBeGreaterThanOrEqual(1)
947
+ })
948
+ })
949
+
950
+ describe('viem compatibility', () => {
951
+ test('behavior: works with viem custom() transport', async () => {
952
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
953
+ const address = await connect(provider)
954
+ await fund(address)
955
+
956
+ const client = provider.getClient()
957
+
958
+ // Read action: getBalance
959
+ const balance = await getBalance(client, { address })
960
+ expect(balance).toBeGreaterThanOrEqual(0n)
961
+ })
962
+
963
+ test('behavior: WalletClient can sign messages', async () => {
964
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
965
+ const address = await connect(provider)
966
+
967
+ const client = createClient({
968
+ account: address,
969
+ chain,
970
+ transport: custom(provider),
971
+ })
972
+
973
+ const signature = await signMessage(client, {
974
+ account: address,
975
+ message: 'hello',
976
+ })
977
+ expect(signature).toMatch(/^0x[0-9a-f]+$/)
978
+ })
979
+
980
+ test('behavior: WalletClient can send transactions', async () => {
981
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
982
+ const address = await connect(provider)
983
+ await fund(address)
984
+
985
+ const client = createClient({
986
+ account: address,
987
+ chain,
988
+ transport: custom(provider),
989
+ })
990
+
991
+ const receipt = await sendTransactionSync(client, {
992
+ account: address,
993
+ to: '0x0000000000000000000000000000000000000001',
994
+ value: 0n,
995
+ })
996
+ expect(receipt.status).toBe('success')
997
+ })
998
+ })
999
+
1000
+ describe('wallet_authorizeAccessKey', () => {
1001
+ test('default: grants an access key and returns its address', async () => {
1002
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1003
+ await connect(provider)
1004
+
1005
+ const result = await provider.request({
1006
+ method: 'wallet_authorizeAccessKey',
1007
+ params: [{ expiry: Expiry.days(1) }],
1008
+ })
1009
+ expect(result.keyId).toMatch(/^0x[0-9a-fA-F]{40}$/)
1010
+ })
1011
+
1012
+ test('behavior: granted access key is used for sendTransactionSync', async () => {
1013
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1014
+ const address = await connect(provider)
1015
+ await fund(address)
1016
+
1017
+ await provider.request({
1018
+ method: 'wallet_authorizeAccessKey',
1019
+ params: [{ expiry: Expiry.days(1) }],
1020
+ })
1021
+
1022
+ const receipt = await provider.request({
1023
+ method: 'eth_sendTransactionSync',
1024
+ params: [{ calls: [transferCall] }],
1025
+ })
1026
+ expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
1027
+ })
1028
+
1029
+ test('behavior: with expiry option', async () => {
1030
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1031
+ await connect(provider)
1032
+
1033
+ const expiry = Math.floor(Date.now() / 1000) + 3600
1034
+ const result = await provider.request({
1035
+ method: 'wallet_authorizeAccessKey',
1036
+ params: [{ expiry }],
1037
+ })
1038
+ expect(result.keyId).toMatch(/^0x[0-9a-fA-F]{40}$/)
1039
+ expect(result.expiry).toBe(Hex.fromNumber(expiry))
1040
+ })
1041
+
1042
+ test('behavior: expired access key falls back to root account', async () => {
1043
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1044
+ const address = await connect(provider)
1045
+ await fund(address)
1046
+
1047
+ await provider.request({
1048
+ method: 'wallet_authorizeAccessKey',
1049
+ params: [{ expiry: Expiry.days(1) }],
1050
+ })
1051
+ expect(provider.store.getState().accessKeys.length).toBe(1)
1052
+
1053
+ // Expire the access key by mutating the store.
1054
+ const { accessKeys } = provider.store.getState()
1055
+ provider.store.setState({
1056
+ accessKeys: accessKeys.map((k) => ({
1057
+ ...k,
1058
+ expiry: Math.floor(Date.now() / 1000) - 1,
1059
+ })),
1060
+ })
1061
+
1062
+ // Transaction should still succeed via root account fallback.
1063
+ const receipt = await provider.request({
1064
+ method: 'eth_sendTransactionSync',
1065
+ params: [{ calls: [transferCall] }],
1066
+ })
1067
+ expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
1068
+ // Verify transaction was sent by the root account, not the access key.
1069
+ expect(receipt.from.toLowerCase()).toBe(address.toLowerCase())
1070
+
1071
+ // Expired access key should be removed from the store.
1072
+ expect(provider.store.getState().accessKeys).toMatchInlineSnapshot(`[]`)
1073
+ })
1074
+
1075
+ test('behavior: with limits option', async () => {
1076
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1077
+ const address = await connect(provider)
1078
+ await fund(address)
1079
+
1080
+ const result = await provider.request({
1081
+ method: 'wallet_authorizeAccessKey',
1082
+ params: [
1083
+ {
1084
+ expiry: Expiry.days(1),
1085
+ limits: [{ token: Addresses.pathUsd, limit: Hex.fromNumber(parseUnits('5', 6)) }],
1086
+ },
1087
+ ],
1088
+ })
1089
+ expect(result.limits).toMatchInlineSnapshot(`
1090
+ [
1091
+ {
1092
+ "limit": "0x4c4b40",
1093
+ "token": "0x20c0000000000000000000000000000000000000",
1094
+ },
1095
+ ]
1096
+ `)
1097
+
1098
+ // Transaction within limit should succeed.
1099
+ const receipt = await provider.request({
1100
+ method: 'eth_sendTransactionSync',
1101
+ params: [{ calls: [transferCall] }],
1102
+ })
1103
+ expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
1104
+ })
1105
+
1106
+ test('exceeding access key limits falls back to root account', async () => {
1107
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1108
+ const address = await connect(provider)
1109
+ await fund(address)
1110
+
1111
+ // Grant access key with a tiny limit (0.01 PUSD).
1112
+ await provider.request({
1113
+ method: 'wallet_authorizeAccessKey',
1114
+ params: [
1115
+ {
1116
+ expiry: Expiry.days(1),
1117
+ limits: [{ token: Addresses.pathUsd, limit: Hex.fromNumber(parseUnits('0.01', 6)) }],
1118
+ },
1119
+ ],
1120
+ })
1121
+
1122
+ // Transfer 1 PUSD — exceeds access key limit, falls back to root account.
1123
+ const receipt = await provider.request({
1124
+ method: 'eth_sendTransactionSync',
1125
+ params: [{ calls: [transferCall] }],
1126
+ })
1127
+ expect(receipt.status).toBe('0x1')
1128
+ expect(receipt.from.toLowerCase()).toBe(address.toLowerCase())
1129
+ })
1130
+
1131
+ test('behavior: access key is removed after key-auth error (spending limit exceeded)', async () => {
1132
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1133
+ const address = await connect(provider)
1134
+ await fund(address)
1135
+
1136
+ // Grant access key with a tiny limit.
1137
+ await provider.request({
1138
+ method: 'wallet_authorizeAccessKey',
1139
+ params: [
1140
+ {
1141
+ expiry: Expiry.days(1),
1142
+ limits: [{ token: Addresses.pathUsd, limit: Hex.fromNumber(parseUnits('0.01', 6)) }],
1143
+ },
1144
+ ],
1145
+ })
1146
+
1147
+ expect(provider.store.getState().accessKeys).toHaveLength(1)
1148
+
1149
+ // Transfer exceeds limit — key-auth error — access key should be removed.
1150
+ await provider.request({
1151
+ method: 'eth_sendTransactionSync',
1152
+ params: [{ calls: [transferCall] }],
1153
+ })
1154
+
1155
+ expect(provider.store.getState().accessKeys).toMatchInlineSnapshot(`[]`)
1156
+ })
1157
+ })
1158
+
1159
+ describe('wallet_revokeAccessKey', () => {
1160
+ test('default: revokes a granted access key', async () => {
1161
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1162
+ await connect(provider)
1163
+
1164
+ const connected = (await provider.request({ method: 'eth_accounts' }))[0]!
1165
+ const { keyId } = await provider.request({
1166
+ method: 'wallet_authorizeAccessKey',
1167
+ params: [{ expiry: Expiry.days(1) }],
1168
+ })
1169
+
1170
+ await provider.request({
1171
+ method: 'wallet_revokeAccessKey',
1172
+ params: [{ address: connected, accessKeyAddress: keyId }],
1173
+ })
1174
+
1175
+ // After revoking, sendTransactionSync should use root key (still works)
1176
+ const address = (await provider.request({ method: 'eth_accounts' }))[0]!
1177
+ await fund(address)
1178
+
1179
+ const receipt = await provider.request({
1180
+ method: 'eth_sendTransactionSync',
1181
+ params: [{ calls: [transferCall] }],
1182
+ })
1183
+ expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
1184
+ })
1185
+ })
1186
+
1187
+ describe('wallet_connect with authorizeAccessKey', () => {
1188
+ test('default: grants access key during register', async () => {
1189
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1190
+
1191
+ const result = await provider.request({
1192
+ method: 'wallet_connect',
1193
+ params: [
1194
+ {
1195
+ capabilities: {
1196
+ method: 'register',
1197
+ authorizeAccessKey: { expiry: Math.floor(Date.now() / 1000) + 3600 },
1198
+ },
1199
+ },
1200
+ ],
1201
+ })
1202
+ expect(result.accounts.length).toBeGreaterThanOrEqual(1)
1203
+ expect(result.accounts[0]!.capabilities.keyAuthorization).toBeDefined()
1204
+ expect(result.accounts[0]!.capabilities.keyAuthorization!.keyId).toMatch(/^0x[0-9a-f]{40}$/i)
1205
+
1206
+ // Access key should be provisioned — sendTransactionSync should work
1207
+ const address = result.accounts[0]!.address
1208
+ await fund(address)
1209
+
1210
+ const receipt = await provider.request({
1211
+ method: 'eth_sendTransactionSync',
1212
+ params: [{ calls: [transferCall] }],
1213
+ })
1214
+ expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
1215
+ })
1216
+
1217
+ test('behavior: authorizeAccessKey with expiry during register', async () => {
1218
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1219
+
1220
+ const expiry = Math.floor(Date.now() / 1000) + 3600
1221
+ const result = await provider.request({
1222
+ method: 'wallet_connect',
1223
+ params: [{ capabilities: { method: 'register', authorizeAccessKey: { expiry } } }],
1224
+ })
1225
+ expect(result.accounts.length).toBeGreaterThanOrEqual(1)
1226
+ })
1227
+
1228
+ test('behavior: authorizeAccessKey during login', async () => {
1229
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1230
+
1231
+ // Register first
1232
+ await provider.request({
1233
+ method: 'wallet_connect',
1234
+ params: [{ capabilities: { method: 'register' } }],
1235
+ })
1236
+
1237
+ // Login with authorizeAccessKey
1238
+ const result = await provider.request({
1239
+ method: 'wallet_connect',
1240
+ params: [
1241
+ {
1242
+ capabilities: { authorizeAccessKey: { expiry: Math.floor(Date.now() / 1000) + 3600 } },
1243
+ },
1244
+ ],
1245
+ })
1246
+ expect(result.accounts.length).toBeGreaterThanOrEqual(1)
1247
+
1248
+ const address = result.accounts[0]!.address
1249
+ await fund(address)
1250
+
1251
+ const receipt = await provider.request({
1252
+ method: 'eth_sendTransactionSync',
1253
+ params: [{ calls: [transferCall] }],
1254
+ })
1255
+ expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
1256
+ })
1257
+ })
1258
+
1259
+ describe('Provider.create authorizeAccessKey option', () => {
1260
+ test('default: wallet_connect auto-authorizes access key', async () => {
1261
+ const provider = Provider.create({
1262
+ adapter: adapter(),
1263
+ chains: [chain],
1264
+ authorizeAccessKey: () => ({ expiry: Expiry.days(1) }),
1265
+ })
1266
+
1267
+ const result = await provider.request({
1268
+ method: 'wallet_connect',
1269
+ params: [{ capabilities: { method: 'register' } }],
1270
+ })
1271
+ expect(result.accounts.length).toBeGreaterThanOrEqual(1)
1272
+ expect(result.accounts[0]!.capabilities.keyAuthorization).toBeDefined()
1273
+ expect(result.accounts[0]!.capabilities.keyAuthorization!.keyId).toMatch(/^0x[0-9a-f]{40}$/i)
1274
+ })
1275
+
1276
+ test('behavior: auto-authorized access key can send transactions', async () => {
1277
+ const provider = Provider.create({
1278
+ adapter: adapter(),
1279
+ chains: [chain],
1280
+ authorizeAccessKey: () => ({ expiry: Expiry.days(1) }),
1281
+ })
1282
+
1283
+ const result = await provider.request({
1284
+ method: 'wallet_connect',
1285
+ params: [{ capabilities: { method: 'register' } }],
1286
+ })
1287
+ const address = result.accounts[0]!.address
1288
+ await fund(address)
1289
+
1290
+ const receipt = await provider.request({
1291
+ method: 'eth_sendTransactionSync',
1292
+ params: [{ calls: [transferCall] }],
1293
+ })
1294
+ expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
1295
+ })
1296
+
1297
+ test('behavior: explicit authorizeAccessKey overrides default', async () => {
1298
+ const expiry = Math.floor(Date.now() / 1000) + 3600
1299
+ const provider = Provider.create({
1300
+ adapter: adapter(),
1301
+ chains: [chain],
1302
+ authorizeAccessKey: () => ({ expiry: Expiry.days(7) }),
1303
+ })
1304
+
1305
+ const result = await provider.request({
1306
+ method: 'wallet_connect',
1307
+ params: [{ capabilities: { method: 'register', authorizeAccessKey: { expiry } } }],
1308
+ })
1309
+ expect(result.accounts[0]!.capabilities.keyAuthorization!.expiry).toBe(Hex.fromNumber(expiry))
1310
+ })
1311
+
1312
+ test('behavior: login auto-authorizes access key', async () => {
1313
+ const provider = Provider.create({
1314
+ adapter: adapter(),
1315
+ chains: [chain],
1316
+ authorizeAccessKey: () => ({ expiry: Expiry.days(1) }),
1317
+ })
1318
+
1319
+ await connect(provider)
1320
+ const result = await provider.request({ method: 'wallet_connect' })
1321
+ expect(result.accounts[0]!.capabilities.keyAuthorization).toBeDefined()
1322
+ })
1323
+
1324
+ test('behavior: without option, wallet_connect does not auto-authorize', async () => {
1325
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1326
+
1327
+ const result = await provider.request({
1328
+ method: 'wallet_connect',
1329
+ params: [{ capabilities: { method: 'register' } }],
1330
+ })
1331
+ expect(result.accounts[0]!.capabilities.keyAuthorization).toBeUndefined()
1332
+ })
1333
+ })
1334
+
1335
+ describe('wallet_authorizeAccessKey with external key', () => {
1336
+ test('default: grants access key for an external key', async () => {
1337
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1338
+ await connect(provider)
1339
+
1340
+ const keyPair = await WebCryptoP256.createKeyPair()
1341
+ const accessKeyAccount = TempoAccount.fromWebCryptoP256(keyPair)
1342
+
1343
+ const result = await provider.request({
1344
+ method: 'wallet_authorizeAccessKey',
1345
+ params: [{ ...accessKeyAccount, expiry: Expiry.days(1) }],
1346
+ })
1347
+ expect(result.keyId).toBe(accessKeyAccount.address)
1348
+ expect(result.keyType).toBe('p256')
1349
+ })
1350
+
1351
+ test('behavior: external key authorization can be used to send a transaction', async () => {
1352
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1353
+ const rootAddress = await connect(provider)
1354
+ await fund(rootAddress)
1355
+
1356
+ const keyPair = await WebCryptoP256.createKeyPair()
1357
+ const accessKeyAccount = TempoAccount.fromWebCryptoP256(keyPair)
1358
+
1359
+ const result = await provider.request({
1360
+ method: 'wallet_authorizeAccessKey',
1361
+ params: [{ ...accessKeyAccount, expiry: Expiry.days(1) }],
1362
+ })
1363
+
1364
+ const client = provider.getClient()
1365
+ const receipt = await sendTransactionSync(client, {
1366
+ account: TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress }),
1367
+ calls: [transferCall],
1368
+ keyAuthorization: KeyAuthorization.fromRpc(result),
1369
+ })
1370
+ expect(receipt.status).toBe('success')
1371
+ })
1372
+ })
1373
+
1374
+ describe('wallet_connect with external authorizeAccessKey', () => {
1375
+ test('default: external key authorization via register', async () => {
1376
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1377
+
1378
+ const keyPair = await WebCryptoP256.createKeyPair()
1379
+ const accessKeyAccount = TempoAccount.fromWebCryptoP256(keyPair)
1380
+
1381
+ const expiry = Math.floor(Date.now() / 1000) + 3600
1382
+ const connectResult = await provider.request({
1383
+ method: 'wallet_connect',
1384
+ params: [
1385
+ {
1386
+ capabilities: {
1387
+ method: 'register',
1388
+ authorizeAccessKey: { expiry, ...accessKeyAccount },
1389
+ },
1390
+ },
1391
+ ],
1392
+ })
1393
+
1394
+ const rootAddress = connectResult.accounts[0]!.address
1395
+ const keyAuthorization = connectResult.accounts[0]!.capabilities.keyAuthorization
1396
+ expect(keyAuthorization).toBeDefined()
1397
+ expect(keyAuthorization!.keyId).toBe(accessKeyAccount.address)
1398
+
1399
+ await fund(rootAddress)
1400
+
1401
+ const client = provider.getClient()
1402
+ const receipt = await sendTransactionSync(client, {
1403
+ account: TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress }),
1404
+ calls: [transferCall],
1405
+ keyAuthorization: KeyAuthorization.fromRpc(keyAuthorization!),
1406
+ })
1407
+ expect(receipt.status).toBe('success')
1408
+ })
1409
+
1410
+ test('behavior: external key authorization via login', async () => {
1411
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1412
+
1413
+ const rootAddress = await connect(provider)
1414
+ await fund(rootAddress)
1415
+
1416
+ const keyPair = await WebCryptoP256.createKeyPair()
1417
+ const accessKeyAccount = TempoAccount.fromWebCryptoP256(keyPair)
1418
+
1419
+ const expiry = Math.floor(Date.now() / 1000) + 3600
1420
+ const loginResult = await provider.request({
1421
+ method: 'wallet_connect',
1422
+ params: [
1423
+ {
1424
+ capabilities: {
1425
+ authorizeAccessKey: { expiry, ...accessKeyAccount },
1426
+ },
1427
+ },
1428
+ ],
1429
+ })
1430
+
1431
+ const keyAuthorization = loginResult.accounts[0]!.capabilities.keyAuthorization
1432
+ expect(keyAuthorization).toBeDefined()
1433
+
1434
+ const client = provider.getClient()
1435
+ const receipt = await sendTransactionSync(client, {
1436
+ account: TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress }),
1437
+ calls: [transferCall],
1438
+ keyAuthorization: KeyAuthorization.fromRpc(keyAuthorization!),
1439
+ })
1440
+ expect(receipt.status).toBe('success')
1441
+ })
1442
+ })
1443
+
1444
+ describe('feePayer', () => {
1445
+ const feePayerAccount = accounts[0]!
1446
+ let server: Server
1447
+
1448
+ beforeAll(async () => {
1449
+ server = await createServer(
1450
+ Handler.feePayer({
1451
+ account: feePayerAccount,
1452
+ chains: [chain],
1453
+ transports: { [chain.id]: http() },
1454
+ }).listener,
1455
+ )
1456
+ })
1457
+
1458
+ afterAll(() => {
1459
+ server.close()
1460
+ })
1461
+
1462
+ test('default: feePayer URL on eth_sendTransaction', async () => {
1463
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1464
+
1465
+ const connected = await connect(provider)
1466
+ await fund(connected)
1467
+
1468
+ const hash = await provider.request({
1469
+ method: 'eth_sendTransaction',
1470
+ params: [{ calls: [transferCall], feePayer: server.url }],
1471
+ })
1472
+
1473
+ expect(hash).toMatch(/^0x[0-9a-f]{64}$/)
1474
+
1475
+ const client = provider.getClient()
1476
+ const receipt = await waitForTransactionReceipt(client, { hash })
1477
+ expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
1478
+ })
1479
+
1480
+ test('behavior: feePayer URL on eth_sendTransactionSync', async () => {
1481
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1482
+
1483
+ const connected = await connect(provider)
1484
+ await fund(connected)
1485
+
1486
+ const receipt = await provider.request({
1487
+ method: 'eth_sendTransactionSync',
1488
+ params: [{ calls: [transferCall], feePayer: server.url }],
1489
+ })
1490
+
1491
+ expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
1492
+ })
1493
+
1494
+ test('behavior: feePayer URL on eth_signTransaction', async () => {
1495
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1496
+
1497
+ const connected = await connect(provider)
1498
+ await fund(connected)
1499
+
1500
+ const signed = await provider.request({
1501
+ method: 'eth_signTransaction',
1502
+ params: [{ calls: [transferCall], feePayer: server.url }],
1503
+ })
1504
+
1505
+ expect(signed).toMatch(/^0x/)
1506
+ })
1507
+
1508
+ test('behavior: feePayer: true uses default from Provider.create', async () => {
1509
+ const provider = Provider.create({
1510
+ adapter: adapter(),
1511
+ chains: [chain],
1512
+ feePayerUrl: server.url,
1513
+ })
1514
+
1515
+ const connected = await connect(provider)
1516
+ await fund(connected)
1517
+
1518
+ const hash = await provider.request({
1519
+ method: 'eth_sendTransaction',
1520
+ params: [{ calls: [transferCall], feePayer: true }],
1521
+ })
1522
+
1523
+ expect(hash).toMatch(/^0x[0-9a-f]{64}$/)
1524
+
1525
+ const client = provider.getClient()
1526
+ const receipt = await waitForTransactionReceipt(client, { hash })
1527
+ expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
1528
+ })
1529
+
1530
+ test('behavior: feePayer: true on eth_sendTransactionSync', async () => {
1531
+ const provider = Provider.create({
1532
+ adapter: adapter(),
1533
+ chains: [chain],
1534
+ feePayerUrl: server.url,
1535
+ })
1536
+
1537
+ const connected = await connect(provider)
1538
+ await fund(connected)
1539
+
1540
+ const receipt = await provider.request({
1541
+ method: 'eth_sendTransactionSync',
1542
+ params: [{ calls: [transferCall], feePayer: true }],
1543
+ })
1544
+
1545
+ expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
1546
+ })
1547
+
1548
+ test('behavior: no feePayer does not use fee payer', async () => {
1549
+ const provider = Provider.create({ adapter: adapter(), chains: [chain] })
1550
+
1551
+ const connected = await connect(provider)
1552
+ await fund(connected)
1553
+
1554
+ const hash = await provider.request({
1555
+ method: 'eth_sendTransaction',
1556
+ params: [{ calls: [transferCall] }],
1557
+ })
1558
+
1559
+ expect(hash).toMatch(/^0x[0-9a-f]{64}$/)
1560
+
1561
+ const client = provider.getClient()
1562
+ const receipt = await waitForTransactionReceipt(client, { hash })
1563
+ expect(receipt.feePayer).not.toBe(feePayerAccount.address.toLowerCase())
1564
+ })
1565
+ })
1566
+ })