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.
- package/CHANGELOG.md +8 -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 +7 -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 +36 -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 +3 -1
- package/dist/core/adapters/local.js.map +1 -1
- package/dist/core/zod/rpc.d.ts +78 -0
- package/dist/core/zod/rpc.d.ts.map +1 -1
- package/dist/core/zod/rpc.js +12 -2
- 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/package.json +4 -4
- 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 +9 -1
- package/src/core/Provider.test.ts +27 -19
- package/src/core/Provider.ts +3 -3
- package/src/core/adapters/local.ts +3 -1
- package/src/core/zod/rpc.ts +32 -2
- package/src/react-native/Provider.test.ts +115 -0
- package/src/react-native/adapter.ts +95 -22
- package/src/server/internal/handlers/codeAuth.ts +3 -3
- 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<
|
|
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
|
|
38
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
/**
|