accounts 0.7.0 → 0.7.2
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.
- package/CHANGELOG.md +16 -0
- package/dist/core/AccessKey.d.ts.map +1 -1
- package/dist/core/AccessKey.js +12 -26
- package/dist/core/AccessKey.js.map +1 -1
- package/dist/core/Account.d.ts +12 -0
- package/dist/core/Account.d.ts.map +1 -1
- package/dist/core/Account.js +24 -2
- package/dist/core/Account.js.map +1 -1
- package/dist/core/Adapter.d.ts +9 -0
- package/dist/core/Adapter.d.ts.map +1 -1
- package/dist/core/Provider.d.ts +1 -1
- package/dist/core/Provider.d.ts.map +1 -1
- package/dist/core/Provider.js +2 -2
- package/dist/core/Provider.js.map +1 -1
- package/dist/core/Schema.d.ts +54 -0
- package/dist/core/Schema.d.ts.map +1 -1
- package/dist/core/adapters/local.d.ts.map +1 -1
- package/dist/core/adapters/local.js +4 -2
- package/dist/core/adapters/local.js.map +1 -1
- package/dist/core/zod/rpc.d.ts +110 -0
- package/dist/core/zod/rpc.d.ts.map +1 -1
- package/dist/core/zod/rpc.js +16 -5
- package/dist/core/zod/rpc.js.map +1 -1
- package/dist/react-native/adapter.d.ts.map +1 -1
- package/dist/react-native/adapter.js +76 -21
- package/dist/react-native/adapter.js.map +1 -1
- package/dist/server/internal/handlers/codeAuth.d.ts +1 -1
- package/dist/server/internal/handlers/codeAuth.js +2 -2
- package/dist/server/internal/handlers/codeAuth.js.map +1 -1
- package/dist/server/internal/handlers/relay.d.ts +2 -1
- package/dist/server/internal/handlers/relay.d.ts.map +1 -1
- package/dist/server/internal/handlers/relay.js +8 -2
- package/dist/server/internal/handlers/relay.js.map +1 -1
- package/dist/wagmi/index.d.ts +38 -2
- package/dist/wagmi/index.d.ts.map +1 -1
- package/dist/wagmi/index.js +35 -2
- package/dist/wagmi/index.js.map +1 -1
- package/package.json +11 -7
- package/src/core/AccessKey.ts +13 -30
- package/src/core/Account.test.ts +145 -0
- package/src/core/Account.ts +34 -3
- package/src/core/Adapter.ts +11 -1
- package/src/core/Provider.test.ts +27 -19
- package/src/core/Provider.ts +3 -3
- package/src/core/adapters/local.ts +4 -2
- package/src/core/zod/rpc.ts +39 -4
- package/src/react-native/Provider.test.ts +115 -0
- package/src/react-native/adapter.ts +96 -22
- package/src/server/internal/handlers/codeAuth.ts +3 -3
- package/src/server/internal/handlers/relay.ts +9 -3
- package/src/wagmi/index.ts +38 -2
- package/dist/wagmi/Connector.d.ts +0 -130
- package/dist/wagmi/Connector.d.ts.map +0 -1
- package/dist/wagmi/Connector.js +0 -272
- package/dist/wagmi/Connector.js.map +0 -1
- package/src/wagmi/Connector.ts +0 -330
package/src/core/Account.test.ts
CHANGED
|
@@ -306,4 +306,149 @@ describe('find', () => {
|
|
|
306
306
|
`[Provider.DisconnectedError: No active account.]`,
|
|
307
307
|
)
|
|
308
308
|
})
|
|
309
|
+
|
|
310
|
+
test('behavior: unscoped access key is used regardless of calls', async () => {
|
|
311
|
+
const keyPair = await WebCryptoP256.createKeyPair()
|
|
312
|
+
const store = setup(
|
|
313
|
+
[{ address: accounts[0].address, keyType: 'secp256k1', privateKey: privateKeys[0] }],
|
|
314
|
+
[
|
|
315
|
+
{
|
|
316
|
+
address: '0x0000000000000000000000000000000000000099',
|
|
317
|
+
access: accounts[0].address,
|
|
318
|
+
keyType: 'webCrypto',
|
|
319
|
+
keyPair,
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
const result = Account.find({
|
|
325
|
+
signable: true,
|
|
326
|
+
store,
|
|
327
|
+
calls: [{ to: '0x0000000000000000000000000000000000000abc', data: '0xa9059cbb' }],
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
expect(result.source).toMatchInlineSnapshot(`"accessKey"`)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
test('behavior: scoped access key is used when calls match', async () => {
|
|
334
|
+
const keyPair = await WebCryptoP256.createKeyPair()
|
|
335
|
+
const token = '0x0000000000000000000000000000000000000abc' as const
|
|
336
|
+
const store = setup(
|
|
337
|
+
[{ address: accounts[0].address, keyType: 'secp256k1', privateKey: privateKeys[0] }],
|
|
338
|
+
[
|
|
339
|
+
{
|
|
340
|
+
address: '0x0000000000000000000000000000000000000099',
|
|
341
|
+
access: accounts[0].address,
|
|
342
|
+
keyType: 'webCrypto',
|
|
343
|
+
keyPair,
|
|
344
|
+
scopes: [{ address: token, selector: '0xa9059cbb' }],
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
const result = Account.find({
|
|
350
|
+
signable: true,
|
|
351
|
+
store,
|
|
352
|
+
calls: [{ to: token, data: '0xa9059cbb0000000000000000000000000000000000000001' }],
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
expect(result.source).toMatchInlineSnapshot(`"accessKey"`)
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
test('behavior: scoped access key falls back to root when calls do not match', async () => {
|
|
359
|
+
const keyPair = await WebCryptoP256.createKeyPair()
|
|
360
|
+
const token = '0x0000000000000000000000000000000000000abc' as const
|
|
361
|
+
const store = setup(
|
|
362
|
+
[{ address: accounts[0].address, keyType: 'secp256k1', privateKey: privateKeys[0] }],
|
|
363
|
+
[
|
|
364
|
+
{
|
|
365
|
+
address: '0x0000000000000000000000000000000000000099',
|
|
366
|
+
access: accounts[0].address,
|
|
367
|
+
keyType: 'webCrypto',
|
|
368
|
+
keyPair,
|
|
369
|
+
scopes: [{ address: token, selector: '0xa9059cbb' }],
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
const result = Account.find({
|
|
375
|
+
signable: true,
|
|
376
|
+
store,
|
|
377
|
+
calls: [{ to: '0x0000000000000000000000000000000000000def', data: '0xdeadbeef' }],
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
expect(result.address).toMatchInlineSnapshot(`"${accounts[0].address}"`)
|
|
381
|
+
expect(result.source).not.toBe('accessKey')
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
test('behavior: scoped access key with human-readable selector matches', async () => {
|
|
385
|
+
const keyPair = await WebCryptoP256.createKeyPair()
|
|
386
|
+
const token = '0x0000000000000000000000000000000000000abc' as const
|
|
387
|
+
const store = setup(
|
|
388
|
+
[{ address: accounts[0].address, keyType: 'secp256k1', privateKey: privateKeys[0] }],
|
|
389
|
+
[
|
|
390
|
+
{
|
|
391
|
+
address: '0x0000000000000000000000000000000000000099',
|
|
392
|
+
access: accounts[0].address,
|
|
393
|
+
keyType: 'webCrypto',
|
|
394
|
+
keyPair,
|
|
395
|
+
scopes: [{ address: token, selector: 'transfer(address,uint256)' }],
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
// 0xa9059cbb is the selector for transfer(address,uint256)
|
|
401
|
+
const result = Account.find({
|
|
402
|
+
signable: true,
|
|
403
|
+
store,
|
|
404
|
+
calls: [{ to: token, data: '0xa9059cbb0000000000000000000000000000000000000001' }],
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
expect(result.source).toMatchInlineSnapshot(`"accessKey"`)
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
test('behavior: scoped access key without selector allows any call to that address', async () => {
|
|
411
|
+
const keyPair = await WebCryptoP256.createKeyPair()
|
|
412
|
+
const token = '0x0000000000000000000000000000000000000abc' as const
|
|
413
|
+
const store = setup(
|
|
414
|
+
[{ address: accounts[0].address, keyType: 'secp256k1', privateKey: privateKeys[0] }],
|
|
415
|
+
[
|
|
416
|
+
{
|
|
417
|
+
address: '0x0000000000000000000000000000000000000099',
|
|
418
|
+
access: accounts[0].address,
|
|
419
|
+
keyType: 'webCrypto',
|
|
420
|
+
keyPair,
|
|
421
|
+
scopes: [{ address: token }],
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
const result = Account.find({
|
|
427
|
+
signable: true,
|
|
428
|
+
store,
|
|
429
|
+
calls: [{ to: token, data: '0xdeadbeef' }],
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
expect(result.source).toMatchInlineSnapshot(`"accessKey"`)
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
test('behavior: scoped access key used when no calls provided', async () => {
|
|
436
|
+
const keyPair = await WebCryptoP256.createKeyPair()
|
|
437
|
+
const store = setup(
|
|
438
|
+
[{ address: accounts[0].address, keyType: 'secp256k1', privateKey: privateKeys[0] }],
|
|
439
|
+
[
|
|
440
|
+
{
|
|
441
|
+
address: '0x0000000000000000000000000000000000000099',
|
|
442
|
+
access: accounts[0].address,
|
|
443
|
+
keyType: 'webCrypto',
|
|
444
|
+
keyPair,
|
|
445
|
+
scopes: [{ address: '0x0000000000000000000000000000000000000abc' }],
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
const result = Account.find({ signable: true, store })
|
|
451
|
+
|
|
452
|
+
expect(result.source).toMatchInlineSnapshot(`"accessKey"`)
|
|
453
|
+
})
|
|
309
454
|
})
|
package/src/core/Account.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Provider, type WebCryptoP256 } from 'ox'
|
|
1
|
+
import { AbiFunction, Provider, type WebCryptoP256 } from 'ox'
|
|
2
2
|
import { type KeyAuthorization } from 'ox/tempo'
|
|
3
3
|
import type { Hex } from 'viem'
|
|
4
4
|
import type { Address, JsonRpcAccount } from 'viem/accounts'
|
|
@@ -42,7 +42,15 @@ export type AccessKey = {
|
|
|
42
42
|
/** Key type. */
|
|
43
43
|
keyType: 'secp256k1' | 'p256' | 'webAuthn' | 'webCrypto'
|
|
44
44
|
/** TIP-20 spending limits for the access key. */
|
|
45
|
-
limits?: { token: Address; limit: bigint }[] | undefined
|
|
45
|
+
limits?: { token: Address; limit: bigint; period?: number | undefined }[] | undefined
|
|
46
|
+
/** Call scopes restricting which contracts/selectors this key can call. */
|
|
47
|
+
scopes?:
|
|
48
|
+
| {
|
|
49
|
+
address: Address
|
|
50
|
+
selector?: Hex | string | undefined
|
|
51
|
+
recipients?: readonly Address[] | undefined
|
|
52
|
+
}[]
|
|
53
|
+
| undefined
|
|
46
54
|
} & OneOf<
|
|
47
55
|
| {}
|
|
48
56
|
| {
|
|
@@ -82,7 +90,8 @@ export function find(options: find.Options): TempoAccount.Account | JsonRpcAccou
|
|
|
82
90
|
// Remove expired access keys.
|
|
83
91
|
if (key.expiry && key.expiry < Date.now() / 1000)
|
|
84
92
|
store.setState({ accessKeys: accessKeys.filter((a) => a !== key) })
|
|
85
|
-
|
|
93
|
+
// Use access key if unscoped or scopes cover the requested calls; otherwise fall through to root.
|
|
94
|
+
else if (scopesMatch(key, options)) return hydrateAccessKey(key) as never
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
97
|
|
|
@@ -95,6 +104,8 @@ export declare namespace find {
|
|
|
95
104
|
accessKey?: boolean | undefined
|
|
96
105
|
/** Address to resolve. Defaults to the active account. */
|
|
97
106
|
address?: Address | undefined
|
|
107
|
+
/** Calls to match against access key scopes. When provided, access keys whose scopes don't cover these calls are skipped. */
|
|
108
|
+
calls?: readonly { to?: Address | undefined; data?: Hex | undefined }[] | undefined
|
|
98
109
|
/** Whether to hydrate signing capability. @default false */
|
|
99
110
|
signable?: boolean | undefined
|
|
100
111
|
/** Reactive state store. */
|
|
@@ -168,3 +179,23 @@ export declare namespace hydrate {
|
|
|
168
179
|
signable?: boolean | undefined
|
|
169
180
|
}
|
|
170
181
|
}
|
|
182
|
+
|
|
183
|
+
/** Returns true if the access key's scopes cover the requested calls (or key is unscoped). */
|
|
184
|
+
function scopesMatch(key: AccessKey, options: find.Options): boolean {
|
|
185
|
+
if (!options.calls || !key.scopes) return true
|
|
186
|
+
return options.calls!.every((call) => {
|
|
187
|
+
if (!call.to) return false
|
|
188
|
+
const callTo = call.to.toLowerCase()
|
|
189
|
+
const callSelector = call.data?.slice(0, 10).toLowerCase()
|
|
190
|
+
return key.scopes!.some((scope) => {
|
|
191
|
+
if (scope.address.toLowerCase() !== callTo) return false
|
|
192
|
+
if (!scope.selector) return true
|
|
193
|
+
const scopeSelector = (
|
|
194
|
+
scope.selector.startsWith('0x') && scope.selector.length === 10
|
|
195
|
+
? scope.selector
|
|
196
|
+
: AbiFunction.getSelector(scope.selector)
|
|
197
|
+
).toLowerCase()
|
|
198
|
+
return callSelector === scopeSelector
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
}
|
package/src/core/Adapter.ts
CHANGED
|
@@ -206,14 +206,24 @@ export declare namespace authorizeAccessKey {
|
|
|
206
206
|
type Parameters = {
|
|
207
207
|
/** Access key address. Alternative to `publicKey` when the caller already knows the derived address. */
|
|
208
208
|
address?: Address | undefined
|
|
209
|
+
/** Chain ID the key authorization is scoped to. Defaults to the active chain. */
|
|
210
|
+
chainId?: number | undefined
|
|
209
211
|
/** Unix timestamp (seconds) when the key expires. */
|
|
210
212
|
expiry: number
|
|
211
213
|
/** Key type of the external public key. Required when `publicKey` or `address` is provided. */
|
|
212
214
|
keyType?: 'secp256k1' | 'p256' | 'webAuthn' | undefined
|
|
213
215
|
/** TIP-20 spending limits for this key. */
|
|
214
|
-
limits?: readonly { token: Address; limit: bigint }[] | undefined
|
|
216
|
+
limits?: readonly { token: Address; limit: bigint; period?: number | undefined }[] | undefined
|
|
215
217
|
/** External public key to authorize. When provided, no key pair is generated — the caller holds the signing material. */
|
|
216
218
|
publicKey?: Hex | undefined
|
|
219
|
+
/** Call scopes restricting which contracts/selectors this key can call. */
|
|
220
|
+
scopes?:
|
|
221
|
+
| readonly {
|
|
222
|
+
address: Address
|
|
223
|
+
selector?: Hex | string | undefined
|
|
224
|
+
recipients?: readonly Address[] | undefined
|
|
225
|
+
}[]
|
|
226
|
+
| undefined
|
|
217
227
|
/** Pre-computed signature over the key authorization digest (skips a second signing ceremony). */
|
|
218
228
|
signature?: Hex | undefined
|
|
219
229
|
}
|
|
@@ -648,24 +648,32 @@ describe.each(adapters)('$name', ({ adapter }: (typeof adapters)[number]) => {
|
|
|
648
648
|
|
|
649
649
|
const result = await provider.request({ method: 'wallet_getCapabilities' })
|
|
650
650
|
expect(result).toMatchInlineSnapshot(`
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
651
|
+
{
|
|
652
|
+
"0x1079": {
|
|
653
|
+
"accessKeys": {
|
|
654
|
+
"status": "supported",
|
|
655
|
+
},
|
|
656
|
+
"atomic": {
|
|
657
|
+
"status": "supported",
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
"0x7a56": {
|
|
661
|
+
"accessKeys": {
|
|
662
|
+
"status": "supported",
|
|
663
|
+
},
|
|
664
|
+
"atomic": {
|
|
665
|
+
"status": "supported",
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
"0xa5bf": {
|
|
669
|
+
"accessKeys": {
|
|
670
|
+
"status": "supported",
|
|
671
|
+
},
|
|
672
|
+
"atomic": {
|
|
673
|
+
"status": "supported",
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
}
|
|
669
677
|
`)
|
|
670
678
|
})
|
|
671
679
|
|
|
@@ -724,7 +732,7 @@ describe.each(adapters)('$name', ({ adapter }: (typeof adapters)[number]) => {
|
|
|
724
732
|
method: 'wallet_getCapabilities',
|
|
725
733
|
params: [connected],
|
|
726
734
|
})
|
|
727
|
-
expect(Object.keys(result).length).toMatchInlineSnapshot(`
|
|
735
|
+
expect(Object.keys(result).length).toMatchInlineSnapshot(`3`)
|
|
728
736
|
expect(result[Hex.fromNumber(tempo.id)]!.atomic.status).toMatchInlineSnapshot(`"supported"`)
|
|
729
737
|
})
|
|
730
738
|
|
package/src/core/Provider.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Mppx, tempo as mppx_tempo } from 'mppx/client'
|
|
|
3
3
|
import { Hash, Hex, Json, Provider as ox_Provider, RpcResponse } from 'ox'
|
|
4
4
|
import { KeyAuthorization } from 'ox/tempo'
|
|
5
5
|
import type { Chain, Client as ViemClient, Transport } from 'viem'
|
|
6
|
-
import { tempo, tempoModerato } from 'viem/chains'
|
|
6
|
+
import { tempo, tempoDevnet, tempoModerato } from 'viem/chains'
|
|
7
7
|
import { Actions } from 'viem/tempo'
|
|
8
8
|
import * as z from 'zod/mini'
|
|
9
9
|
|
|
@@ -49,7 +49,7 @@ const announced = new Set<string>()
|
|
|
49
49
|
export function create(options: create.Options = {}): create.ReturnType {
|
|
50
50
|
const {
|
|
51
51
|
adapter = dialog(),
|
|
52
|
-
chains = [tempo, tempoModerato],
|
|
52
|
+
chains = [tempo, tempoModerato, tempoDevnet],
|
|
53
53
|
persistCredentials,
|
|
54
54
|
testnet,
|
|
55
55
|
storage = typeof window !== 'undefined' ? Storage.idb() : Storage.memory(),
|
|
@@ -681,7 +681,7 @@ export declare namespace create {
|
|
|
681
681
|
authorizeAccessKey?: (() => Adapter.authorizeAccessKey.Parameters) | undefined
|
|
682
682
|
/**
|
|
683
683
|
* Supported chains. First chain is the default.
|
|
684
|
-
* @default [tempo, tempoModerato]
|
|
684
|
+
* @default [tempo, tempoModerato, tempoDevnet]
|
|
685
685
|
*/
|
|
686
686
|
chains?: readonly [Chain, ...Chain[]] | undefined
|
|
687
687
|
/** Fee payer configuration. @see {@link Client.fromChainId.Options.feePayer} */
|
|
@@ -34,8 +34,8 @@ export function local(options: local.Options): Adapter.Adapter {
|
|
|
34
34
|
* For local keys: generates a P256 key pair via `AccessKey.generate`.
|
|
35
35
|
*/
|
|
36
36
|
async function prepareKeyAuthorization(options: Adapter.authorizeAccessKey.Parameters) {
|
|
37
|
-
const { expiry, limits } = options
|
|
38
|
-
const chainId = getClient().chain.id
|
|
37
|
+
const { expiry, limits, scopes } = options
|
|
38
|
+
const chainId = options.chainId ?? getClient().chain.id
|
|
39
39
|
|
|
40
40
|
if (options.publicKey || options.address) {
|
|
41
41
|
const accessKeyAddress =
|
|
@@ -46,6 +46,7 @@ export function local(options: local.Options): Adapter.Adapter {
|
|
|
46
46
|
chainId: BigInt(chainId),
|
|
47
47
|
expiry,
|
|
48
48
|
limits,
|
|
49
|
+
scopes,
|
|
49
50
|
type: keyType,
|
|
50
51
|
})
|
|
51
52
|
return { keyAuthorization }
|
|
@@ -58,6 +59,7 @@ export function local(options: local.Options): Adapter.Adapter {
|
|
|
58
59
|
chainId: BigInt(chainId),
|
|
59
60
|
expiry,
|
|
60
61
|
limits,
|
|
62
|
+
scopes,
|
|
61
63
|
type: 'p256',
|
|
62
64
|
})
|
|
63
65
|
return { keyAuthorization, keyPair }
|
package/src/core/zod/rpc.ts
CHANGED
|
@@ -51,7 +51,11 @@ export const keyAuthorization = z.object({
|
|
|
51
51
|
expiry: z.union([u.number(), z.null(), z.undefined()]),
|
|
52
52
|
keyId: u.address(),
|
|
53
53
|
keyType,
|
|
54
|
-
limits: z.optional(
|
|
54
|
+
limits: z.optional(
|
|
55
|
+
z.readonly(
|
|
56
|
+
z.array(z.object({ token: u.address(), limit: u.bigint(), period: z.optional(u.number()) })),
|
|
57
|
+
),
|
|
58
|
+
),
|
|
55
59
|
signature: signatureEnvelope,
|
|
56
60
|
})
|
|
57
61
|
|
|
@@ -341,13 +345,31 @@ export namespace wallet_getCapabilities {
|
|
|
341
345
|
export namespace wallet_authorizeAccessKey {
|
|
342
346
|
export const parameters = z.object({
|
|
343
347
|
address: z.optional(u.address()),
|
|
348
|
+
chainId: z.optional(u.number()),
|
|
344
349
|
expiry: z.number(),
|
|
345
350
|
keyType: z.optional(keyType),
|
|
346
|
-
limits: z.optional(
|
|
351
|
+
limits: z.optional(
|
|
352
|
+
z.readonly(
|
|
353
|
+
z.array(
|
|
354
|
+
z.object({ token: u.address(), limit: u.bigint(), period: z.optional(z.number()) }),
|
|
355
|
+
),
|
|
356
|
+
),
|
|
357
|
+
),
|
|
347
358
|
publicKey: z.optional(u.hex()),
|
|
359
|
+
scopes: z.optional(
|
|
360
|
+
z.readonly(
|
|
361
|
+
z.array(
|
|
362
|
+
z.object({
|
|
363
|
+
address: u.address(),
|
|
364
|
+
selector: z.optional(z.union([u.hex(), z.string()])),
|
|
365
|
+
recipients: z.optional(z.readonly(z.array(u.address()))),
|
|
366
|
+
}),
|
|
367
|
+
),
|
|
368
|
+
),
|
|
369
|
+
),
|
|
348
370
|
})
|
|
349
371
|
|
|
350
|
-
const returns = z.object({
|
|
372
|
+
export const returns = z.object({
|
|
351
373
|
keyAuthorization,
|
|
352
374
|
rootAddress: u.address(),
|
|
353
375
|
})
|
|
@@ -366,8 +388,21 @@ export namespace wallet_authorizeAccessKey_strict {
|
|
|
366
388
|
address: z.optional(u.address()),
|
|
367
389
|
expiry: z.number(),
|
|
368
390
|
keyType: z.optional(keyType),
|
|
369
|
-
limits: z.readonly(
|
|
391
|
+
limits: z.readonly(
|
|
392
|
+
z.array(z.object({ token: u.address(), limit: u.bigint(), period: z.optional(z.number()) })),
|
|
393
|
+
),
|
|
370
394
|
publicKey: z.optional(u.hex()),
|
|
395
|
+
scopes: z.optional(
|
|
396
|
+
z.readonly(
|
|
397
|
+
z.array(
|
|
398
|
+
z.object({
|
|
399
|
+
address: u.address(),
|
|
400
|
+
selector: z.optional(z.union([u.hex(), z.string()])),
|
|
401
|
+
recipients: z.optional(z.readonly(z.array(u.address()))),
|
|
402
|
+
}),
|
|
403
|
+
),
|
|
404
|
+
),
|
|
405
|
+
),
|
|
371
406
|
})
|
|
372
407
|
}
|
|
373
408
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Address, Hex, PublicKey } from 'ox'
|
|
2
|
+
import { KeyAuthorization } from 'ox/tempo'
|
|
3
|
+
import { parseUnits, type Address as viem_Address } from 'viem'
|
|
4
|
+
import { Actions, Addresses } from 'viem/tempo'
|
|
5
|
+
import { describe, expect, test } from 'vp/test'
|
|
6
|
+
|
|
7
|
+
import { accounts, chain, getClient } from '../../test/config.js'
|
|
8
|
+
import * as Storage from '../core/Storage.js'
|
|
9
|
+
import * as Provider from './Provider.js'
|
|
10
|
+
|
|
11
|
+
const root = accounts[0]!
|
|
12
|
+
const transferCall = Actions.token.transfer.call({
|
|
13
|
+
to: '0x0000000000000000000000000000000000000001',
|
|
14
|
+
token: Addresses.pathUsd,
|
|
15
|
+
amount: parseUnits('1', 6),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
async function fund(address: viem_Address) {
|
|
19
|
+
await Actions.token.transferSync(getClient(), {
|
|
20
|
+
account: root,
|
|
21
|
+
feeToken: Addresses.pathUsd,
|
|
22
|
+
to: address,
|
|
23
|
+
token: Addresses.pathUsd,
|
|
24
|
+
amount: parseUnits('10', 6),
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createOpen(options: { mismatchFirstCall?: boolean | undefined } = {}) {
|
|
29
|
+
let calls = 0
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
calls: () => calls,
|
|
33
|
+
open: async (url: string) => {
|
|
34
|
+
calls += 1
|
|
35
|
+
|
|
36
|
+
const authUrl = new URL(url)
|
|
37
|
+
const callback = authUrl.searchParams.get('callback')
|
|
38
|
+
const chainId = authUrl.searchParams.get('chainId')
|
|
39
|
+
const pubKey = authUrl.searchParams.get('pubKey')
|
|
40
|
+
const state = authUrl.searchParams.get('state')
|
|
41
|
+
|
|
42
|
+
if (!callback || !chainId || !pubKey || !state)
|
|
43
|
+
throw new Error('Expected callback, chainId, pubKey, and state in auth URL.')
|
|
44
|
+
|
|
45
|
+
const limits = authUrl.searchParams.get('limits')
|
|
46
|
+
const keyType = authUrl.searchParams.get('keyType')
|
|
47
|
+
if (keyType !== 'p256' && keyType !== 'secp256k1')
|
|
48
|
+
throw new Error('Expected a managed key type in auth URL.')
|
|
49
|
+
|
|
50
|
+
const keyAuthorization = await root.signKeyAuthorization(
|
|
51
|
+
{
|
|
52
|
+
accessKeyAddress:
|
|
53
|
+
options.mismatchFirstCall && calls === 1
|
|
54
|
+
? accounts[1]!.address
|
|
55
|
+
: Address.fromPublicKey(PublicKey.fromHex(pubKey as Hex.Hex)),
|
|
56
|
+
keyType,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
chainId: BigInt(chainId),
|
|
60
|
+
...(authUrl.searchParams.get('expiry')
|
|
61
|
+
? { expiry: Number(authUrl.searchParams.get('expiry')) }
|
|
62
|
+
: {}),
|
|
63
|
+
...(limits
|
|
64
|
+
? {
|
|
65
|
+
limits: (JSON.parse(limits) as { token: `0x${string}`; limit: string }[]).map(
|
|
66
|
+
(x) => ({
|
|
67
|
+
limit: BigInt(x.limit),
|
|
68
|
+
token: x.token,
|
|
69
|
+
}),
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
: {}),
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const callbackUrl = new URL(callback)
|
|
77
|
+
callbackUrl.searchParams.set('accountAddress', root.address)
|
|
78
|
+
callbackUrl.searchParams.set('keyAuthorization', KeyAuthorization.serialize(keyAuthorization))
|
|
79
|
+
callbackUrl.searchParams.set('state', state)
|
|
80
|
+
return callbackUrl.toString()
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe('create', () => {
|
|
86
|
+
test('behavior: reauthorizes the managed key when the saved authorization targets the wrong key', async () => {
|
|
87
|
+
const secureStorage = Storage.memory()
|
|
88
|
+
const browser = createOpen({ mismatchFirstCall: true })
|
|
89
|
+
const provider = Provider.create({
|
|
90
|
+
authorizeAccessKey: () => ({
|
|
91
|
+
expiry: Math.floor(Date.now() / 1000) + 3600,
|
|
92
|
+
}),
|
|
93
|
+
chains: [chain],
|
|
94
|
+
host: 'https://wallet.tempo.xyz',
|
|
95
|
+
open: browser.open,
|
|
96
|
+
redirectUri: 'accounts-playground://auth',
|
|
97
|
+
secureStorage,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const result = await provider.request({
|
|
101
|
+
method: 'wallet_connect',
|
|
102
|
+
params: [{ capabilities: { method: 'register', name: 'Accounts RN Test' } }],
|
|
103
|
+
})
|
|
104
|
+
expect(result.accounts[0]!.address).toBe(root.address)
|
|
105
|
+
|
|
106
|
+
await fund(root.address)
|
|
107
|
+
|
|
108
|
+
const receipt = await provider.request({
|
|
109
|
+
method: 'eth_sendTransactionSync',
|
|
110
|
+
params: [{ calls: [transferCall], feeToken: Addresses.pathUsd }],
|
|
111
|
+
})
|
|
112
|
+
expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
|
|
113
|
+
expect(browser.calls()).toMatchInlineSnapshot(`2`)
|
|
114
|
+
})
|
|
115
|
+
})
|