accounts 0.7.0 → 0.7.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 (46) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/core/AccessKey.d.ts.map +1 -1
  3. package/dist/core/AccessKey.js +12 -26
  4. package/dist/core/AccessKey.js.map +1 -1
  5. package/dist/core/Account.d.ts +12 -0
  6. package/dist/core/Account.d.ts.map +1 -1
  7. package/dist/core/Account.js +24 -2
  8. package/dist/core/Account.js.map +1 -1
  9. package/dist/core/Adapter.d.ts +7 -0
  10. package/dist/core/Adapter.d.ts.map +1 -1
  11. package/dist/core/Provider.d.ts +1 -1
  12. package/dist/core/Provider.d.ts.map +1 -1
  13. package/dist/core/Provider.js +2 -2
  14. package/dist/core/Provider.js.map +1 -1
  15. package/dist/core/Schema.d.ts +36 -0
  16. package/dist/core/Schema.d.ts.map +1 -1
  17. package/dist/core/adapters/local.d.ts.map +1 -1
  18. package/dist/core/adapters/local.js +3 -1
  19. package/dist/core/adapters/local.js.map +1 -1
  20. package/dist/core/zod/rpc.d.ts +78 -0
  21. package/dist/core/zod/rpc.d.ts.map +1 -1
  22. package/dist/core/zod/rpc.js +12 -2
  23. package/dist/core/zod/rpc.js.map +1 -1
  24. package/dist/react-native/adapter.d.ts.map +1 -1
  25. package/dist/react-native/adapter.js +76 -21
  26. package/dist/react-native/adapter.js.map +1 -1
  27. package/dist/server/internal/handlers/codeAuth.d.ts +1 -1
  28. package/dist/server/internal/handlers/codeAuth.js +2 -2
  29. package/dist/server/internal/handlers/codeAuth.js.map +1 -1
  30. package/dist/server/internal/handlers/relay.d.ts +2 -1
  31. package/dist/server/internal/handlers/relay.d.ts.map +1 -1
  32. package/dist/server/internal/handlers/relay.js +8 -2
  33. package/dist/server/internal/handlers/relay.js.map +1 -1
  34. package/package.json +4 -4
  35. package/src/core/AccessKey.ts +13 -30
  36. package/src/core/Account.test.ts +145 -0
  37. package/src/core/Account.ts +34 -3
  38. package/src/core/Adapter.ts +9 -1
  39. package/src/core/Provider.test.ts +27 -19
  40. package/src/core/Provider.ts +3 -3
  41. package/src/core/adapters/local.ts +3 -1
  42. package/src/core/zod/rpc.ts +32 -2
  43. package/src/react-native/Provider.test.ts +115 -0
  44. package/src/react-native/adapter.ts +95 -22
  45. package/src/server/internal/handlers/codeAuth.ts +3 -3
  46. package/src/server/internal/handlers/relay.ts +9 -3
@@ -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
+ })
@@ -9,7 +9,7 @@ import {
9
9
  } from 'ox'
10
10
  import { KeyAuthorization } from 'ox/tempo'
11
11
  import { prepareTransactionRequest } from 'viem/actions'
12
- import { Account as TempoAccount, Secp256k1 } from 'viem/tempo'
12
+ import { Actions, Account as TempoAccount, Secp256k1 } from 'viem/tempo'
13
13
 
14
14
  import * as AccessKey from '../core/AccessKey.js'
15
15
  import * as Adapter from '../core/Adapter.js'
@@ -28,27 +28,58 @@ export function reactNative(options: reactNative.Options): Adapter.Adapter {
28
28
  async function loadManagedKey(
29
29
  address: Adapter.authorizeAccessKey.ReturnType['rootAddress'],
30
30
  parameters: loadManagedKey.Options = {},
31
- ): Promise<ManagedKeyEntry | undefined> {
31
+ ): Promise<loadManagedKey.ReturnType | undefined> {
32
32
  const { keyType } = parameters
33
33
  const { secureStorage } = options
34
34
  if (!secureStorage) return undefined
35
35
 
36
36
  const { chainId } = store.getState()
37
- const storageKey = managedKeyStorageKey(address, chainId, keyType)
38
- const entry = await secureStorage.getItem<ManagedKeyEntry>(storageKey)
37
+ const storageKeys = keyType
38
+ ? [managedKeyStorageKey(address, chainId, keyType)]
39
+ : [
40
+ managedKeyStorageKey(address, chainId, 'secp256k1'),
41
+ managedKeyStorageKey(address, chainId, 'p256'),
42
+ managedKeyStorageKey(address, chainId),
43
+ ]
44
+ let entry: ManagedKeyEntry | null = null
45
+ for (const storageKey of storageKeys) {
46
+ entry = await secureStorage.getItem<ManagedKeyEntry>(storageKey)
47
+ if (entry) break
48
+ }
39
49
  if (!entry) return undefined
40
50
 
51
+ const account =
52
+ entry.keyType === 'p256'
53
+ ? TempoAccount.fromP256(entry.key, { access: address })
54
+ : TempoAccount.fromSecp256k1(entry.key, { access: address })
55
+ const keyAddress = core_Address.fromPublicKey(PublicKey.from(account.publicKey))
41
56
  const deserialized = KeyAuthorization.deserialize(entry.keyAuthorization)
42
57
  if (!deserialized.signature) throw new Error('Managed access key is missing a signature.')
43
58
  const keyAuthorization = deserialized as KeyAuthorization.Signed
44
- AccessKey.save({
45
- address,
46
- keyAuthorization,
47
- privateKey: entry.key,
48
- store,
49
- })
50
59
 
51
- return entry
60
+ if (keyAuthorization.address.toLowerCase() === keyAddress.toLowerCase())
61
+ AccessKey.save({
62
+ address,
63
+ keyAuthorization,
64
+ privateKey: entry.key,
65
+ store,
66
+ })
67
+ else
68
+ store.setState((state) => ({
69
+ accessKeys: state.accessKeys.filter(
70
+ (accessKey) => accessKey.address.toLowerCase() !== keyAuthorization.address.toLowerCase(),
71
+ ),
72
+ }))
73
+
74
+ return {
75
+ account,
76
+ expiry: entry.expiry,
77
+ key: entry.key,
78
+ keyAddress,
79
+ keyType: entry.keyType,
80
+ publicKey: account.publicKey,
81
+ storedAuthorization: keyAuthorization,
82
+ }
52
83
  }
53
84
 
54
85
  async function resolveManagedKey(
@@ -63,19 +94,14 @@ export function reactNative(options: reactNative.Options): Adapter.Adapter {
63
94
  const entry = address
64
95
  ? await loadManagedKey(address, requestedKeyType ? { keyType: requestedKeyType } : {})
65
96
  : undefined
66
- if (entry) {
67
- const account =
68
- entry.keyType === 'p256'
69
- ? TempoAccount.fromP256(entry.key, { access: address })
70
- : TempoAccount.fromSecp256k1(entry.key, { access: address })
97
+ if (entry)
71
98
  return {
72
- account,
99
+ account: entry.account,
73
100
  key: entry.key,
74
101
  keyAddress: entry.keyAddress,
75
102
  keyType: entry.keyType,
76
- publicKey: account.publicKey,
103
+ publicKey: entry.publicKey,
77
104
  }
78
- }
79
105
 
80
106
  const nextKeyType = requestedKeyType === 'p256' ? 'p256' : 'secp256k1'
81
107
  const key = nextKeyType === 'p256' ? P256.randomPrivateKey() : Secp256k1.randomPrivateKey()
@@ -124,6 +150,44 @@ export function reactNative(options: reactNative.Options): Adapter.Adapter {
124
150
  await secureStorage.setItem(storageKey, entry)
125
151
  }
126
152
 
153
+ async function isManagedKeyAuthorized(
154
+ address: Adapter.authorizeAccessKey.ReturnType['rootAddress'],
155
+ managedKey: loadManagedKey.ReturnType,
156
+ ) {
157
+ try {
158
+ const metadata = await Actions.accessKey.getMetadata(getClient(), {
159
+ account: address,
160
+ accessKey: managedKey.keyAddress,
161
+ })
162
+ return (
163
+ metadata.address.toLowerCase() === managedKey.keyAddress.toLowerCase() &&
164
+ !metadata.isRevoked
165
+ )
166
+ } catch {
167
+ return false
168
+ }
169
+ }
170
+
171
+ async function reauthorizeManagedKey(
172
+ address: Adapter.authorizeAccessKey.ReturnType['rootAddress'],
173
+ managedKey: loadManagedKey.ReturnType,
174
+ ) {
175
+ const result = await authorize({
176
+ account: address,
177
+ authorizeAccessKey: {
178
+ expiry: managedKey.expiry,
179
+ keyType: managedKey.keyType,
180
+ ...(managedKey.storedAuthorization.limits
181
+ ? { limits: managedKey.storedAuthorization.limits.map((limit) => ({ ...limit })) }
182
+ : {}),
183
+ publicKey: managedKey.publicKey,
184
+ },
185
+ method: 'wallet_authorizeAccessKey',
186
+ })
187
+ await saveManagedKey(address, managedKey, result.keyAuthorization)
188
+ return result.keyAuthorization
189
+ }
190
+
127
191
  async function withManagedAccessKey<result>(
128
192
  fn: (
129
193
  account: TempoAccount.Account,
@@ -131,10 +195,14 @@ export function reactNative(options: reactNative.Options): Adapter.Adapter {
131
195
  ) => Promise<result>,
132
196
  ) {
133
197
  const rootAddress = store.getState().accounts[store.getState().activeAccount]?.address
134
- if (rootAddress) await loadManagedKey(rootAddress)
198
+ const managedKey = rootAddress ? await loadManagedKey(rootAddress) : undefined
199
+
200
+ const account = managedKey?.account ?? getAccount({ signable: true })
201
+ let keyAuthorization = AccessKey.getPending(account, { store })
202
+ if (rootAddress && managedKey && !keyAuthorization)
203
+ if (!(await isManagedKeyAuthorized(rootAddress, managedKey)))
204
+ keyAuthorization = await reauthorizeManagedKey(rootAddress, managedKey)
135
205
 
136
- const account = getAccount({ signable: true })
137
- const keyAuthorization = AccessKey.getPending(account, { store })
138
206
  try {
139
207
  const result = await fn(account, keyAuthorization ?? undefined)
140
208
  AccessKey.removePending(account, { store })
@@ -371,6 +439,11 @@ declare namespace loadManagedKey {
371
439
  type Options = {
372
440
  keyType?: 'secp256k1' | 'p256' | undefined
373
441
  }
442
+
443
+ type ReturnType = resolveManagedKey.ReturnType & {
444
+ expiry: number
445
+ storedAuthorization: KeyAuthorization.Signed
446
+ }
374
447
  }
375
448
 
376
449
  /** Entry shape persisted to secure storage for managed access keys. */
@@ -1,6 +1,6 @@
1
1
  import type { Chain, Client, Transport } from 'viem'
2
2
  import { createClient, http } from 'viem'
3
- import { tempo, tempoModerato } from 'viem/chains'
3
+ import { tempo, tempoDevnet, tempoModerato } from 'viem/chains'
4
4
  import * as z from 'zod/mini'
5
5
 
6
6
  import * as CliAuth from '../../CliAuth.js'
@@ -20,7 +20,7 @@ import { type Handler, from } from '../../Handler.js'
20
20
  */
21
21
  export function codeAuth(options: codeAuth.Options = {}): Handler {
22
22
  const {
23
- chains = [tempo, tempoModerato],
23
+ chains = [tempo, tempoModerato, tempoDevnet],
24
24
  now,
25
25
  path = '/auth/pkce',
26
26
  policy,
@@ -127,7 +127,7 @@ export declare namespace codeAuth {
127
127
  /**
128
128
  * Supported chains. The handler resolves the client based on chain IDs carried
129
129
  * by device-code requests and key authorizations.
130
- * @default [tempo, tempoModerato]
130
+ * @default [tempo, tempoModerato, tempoDevnet]
131
131
  */
132
132
  chains?: readonly [Chain, ...Chain[]] | undefined
133
133
  /** Time source used for TTL evaluation. */
@@ -15,7 +15,7 @@ import {
15
15
  } from 'viem'
16
16
  import type { LocalAccount } from 'viem/accounts'
17
17
  import { simulateCalls } from 'viem/actions'
18
- import { tempo, tempoLocalnet, tempoMainnet, tempoModerato } from 'viem/chains'
18
+ import { tempo, tempoDevnet, tempoLocalnet, tempoMainnet, tempoModerato } from 'viem/chains'
19
19
  import { Abis, Actions, Addresses, Capabilities, Transaction } from 'viem/tempo'
20
20
 
21
21
  import * as ExecutionError from '../../../core/ExecutionError.js'
@@ -64,7 +64,7 @@ import * as Utils from './utils.js'
64
64
  */
65
65
  export function relay(options: relay.Options = {}): Handler {
66
66
  const {
67
- chains = [tempo, tempoModerato],
67
+ chains = [tempo, tempoModerato, tempoDevnet],
68
68
  onRequest,
69
69
  path = '/',
70
70
  resolveTokens = (chainId) =>
@@ -420,6 +420,12 @@ export namespace relay {
420
420
  '0x20c0000000000000000000009e8d7eb59b783726', // USDC.e
421
421
  '0x20c000000000000000000000d72572838bbee59c', // EURC.e
422
422
  ],
423
+ [tempoDevnet.id]: [
424
+ '0x20c0000000000000000000000000000000000000', // pathUSD
425
+ '0x20c0000000000000000000000000000000000001', // alphaUSD
426
+ '0x20c0000000000000000000000000000000000002', // betaUSD
427
+ '0x20c0000000000000000000000000000000000003', // thetaUSD
428
+ ],
423
429
  [tempoLocalnet.id]: [
424
430
  '0x20c0000000000000000000000000000000000000', // pathUSD
425
431
  '0x20c0000000000000000000000000000000000001', // alphaUSD
@@ -442,7 +448,7 @@ export namespace relay {
442
448
  /**
443
449
  * Supported chains. The handler resolves the client based on the
444
450
  * `chainId` in the incoming transaction.
445
- * @default [tempo, tempoModerato]
451
+ * @default [tempo, tempoModerato, tempoDevnet]
446
452
  */
447
453
  chains?: readonly [Chain, ...Chain[]] | undefined
448
454
  /**