accounts 0.12.2 → 0.13.0
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 +23 -0
- package/README.md +6 -5
- package/dist/cli/adapter.d.ts.map +1 -1
- package/dist/cli/adapter.js +53 -67
- package/dist/cli/adapter.js.map +1 -1
- package/dist/core/AccessKey.d.ts +89 -106
- package/dist/core/AccessKey.d.ts.map +1 -1
- package/dist/core/AccessKey.js +226 -210
- package/dist/core/AccessKey.js.map +1 -1
- package/dist/core/Account.d.ts +0 -9
- package/dist/core/Account.d.ts.map +1 -1
- package/dist/core/Account.js +1 -13
- package/dist/core/Account.js.map +1 -1
- package/dist/core/Adapter.d.ts +10 -2
- package/dist/core/Adapter.d.ts.map +1 -1
- package/dist/core/Provider.d.ts +4 -5
- package/dist/core/Provider.d.ts.map +1 -1
- package/dist/core/Provider.js +66 -76
- package/dist/core/Provider.js.map +1 -1
- package/dist/core/Schema.d.ts +7 -7
- package/dist/core/adapters/dialog.d.ts.map +1 -1
- package/dist/core/adapters/dialog.js +114 -124
- package/dist/core/adapters/dialog.js.map +1 -1
- package/dist/core/adapters/local.d.ts.map +1 -1
- package/dist/core/adapters/local.js +92 -105
- package/dist/core/adapters/local.js.map +1 -1
- package/dist/core/adapters/privy.d.ts +164 -0
- package/dist/core/adapters/privy.d.ts.map +1 -0
- package/dist/core/adapters/privy.js +574 -0
- package/dist/core/adapters/privy.js.map +1 -0
- package/dist/core/adapters/turnkey.d.ts.map +1 -1
- package/dist/core/adapters/turnkey.js +136 -133
- package/dist/core/adapters/turnkey.js.map +1 -1
- package/dist/core/internal/AccessKeyTransaction.d.ts +59 -0
- package/dist/core/internal/AccessKeyTransaction.d.ts.map +1 -0
- package/dist/core/internal/AccessKeyTransaction.js +155 -0
- package/dist/core/internal/AccessKeyTransaction.js.map +1 -0
- package/dist/core/zod/rpc.d.ts +9 -4
- package/dist/core/zod/rpc.d.ts.map +1 -1
- package/dist/core/zod/rpc.js +9 -4
- package/dist/core/zod/rpc.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react-native/adapter.d.ts.map +1 -1
- package/dist/react-native/adapter.js +61 -72
- package/dist/react-native/adapter.js.map +1 -1
- package/dist/server/Kv.d.ts +19 -9
- package/dist/server/Kv.d.ts.map +1 -1
- package/dist/server/Kv.js +32 -9
- package/dist/server/Kv.js.map +1 -1
- package/dist/server/internal/handlers/auth.d.ts.map +1 -1
- package/dist/server/internal/handlers/auth.js +2 -0
- package/dist/server/internal/handlers/auth.js.map +1 -1
- package/dist/server/internal/handlers/relay.d.ts +18 -12
- package/dist/server/internal/handlers/relay.d.ts.map +1 -1
- package/dist/server/internal/handlers/relay.js +115 -2
- package/dist/server/internal/handlers/relay.js.map +1 -1
- package/dist/server/internal/handlers/webAuthn.d.ts +6 -1
- package/dist/server/internal/handlers/webAuthn.d.ts.map +1 -1
- package/dist/server/internal/handlers/webAuthn.js +16 -9
- package/dist/server/internal/handlers/webAuthn.js.map +1 -1
- package/package.json +2 -2
- package/src/cli/adapter.ts +69 -74
- package/src/core/AccessKey.test.ts +525 -559
- package/src/core/AccessKey.ts +323 -327
- package/src/core/Account.test.ts +5 -50
- package/src/core/Account.ts +1 -19
- package/src/core/Adapter.ts +6 -2
- package/src/core/{Provider.test.ts → Provider.localnet.test.ts} +97 -31
- package/src/core/Provider.ts +110 -110
- package/src/core/Schema.test-d.ts +18 -0
- package/src/core/adapters/dialog.test.ts +137 -13
- package/src/core/adapters/dialog.ts +109 -121
- package/src/core/adapters/local.ts +95 -119
- package/src/core/adapters/privy.test-d.ts +128 -0
- package/src/core/adapters/privy.test.ts +634 -0
- package/src/core/adapters/privy.ts +805 -0
- package/src/core/adapters/turnkey.test.ts +117 -13
- package/src/core/adapters/turnkey.ts +132 -149
- package/src/core/internal/AccessKeyTransaction.ts +243 -0
- package/src/core/{mppx.test.ts → mppx.localnet.test.ts} +32 -2
- package/src/core/zod/request.test.ts +36 -0
- package/src/core/zod/rpc.ts +17 -10
- package/src/index.ts +1 -1
- package/src/react-native/adapter.ts +77 -84
- package/src/server/Kv.test.ts +51 -0
- package/src/server/Kv.ts +43 -9
- package/src/server/internal/handlers/auth.test.ts +10 -5
- package/src/server/internal/handlers/auth.ts +3 -0
- package/src/server/internal/handlers/{relay.test.ts → relay.localnet.test.ts} +3 -0
- package/src/server/internal/handlers/relay.ts +138 -13
- package/src/server/internal/handlers/session.test.ts +1 -1
- package/src/server/internal/handlers/webAuthn.test.ts +20 -0
- package/src/server/internal/handlers/webAuthn.ts +24 -12
- /package/src/cli/{Provider.test.ts → Provider.localnet.test.ts} +0 -0
- /package/src/react-native/{Provider.test.ts → Provider.localnet.test.ts} +0 -0
- /package/src/server/internal/handlers/{exchange.test.ts → exchange.localnet.test.ts} +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Hex, WebCryptoP256 } from 'ox'
|
|
2
2
|
import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo'
|
|
3
|
-
import { encodeErrorResult } from 'viem'
|
|
4
|
-
import { Abis, Account as TempoAccount
|
|
3
|
+
import { BaseError, encodeErrorResult, encodeFunctionResult } from 'viem'
|
|
4
|
+
import { Abis, Account as TempoAccount } from 'viem/tempo'
|
|
5
5
|
import { describe, expect, test } from 'vp/test'
|
|
6
6
|
|
|
7
|
-
import { accounts
|
|
7
|
+
import { accounts } from '../../test/config.js'
|
|
8
8
|
import * as AccessKey from './AccessKey.js'
|
|
9
|
+
import * as AccessKeyTransaction from './internal/AccessKeyTransaction.js'
|
|
9
10
|
import * as Store from './Store.js'
|
|
10
11
|
|
|
11
12
|
function createStore() {
|
|
@@ -17,6 +18,7 @@ const rootAddress = accounts[0]!.address
|
|
|
17
18
|
function createKeyAuthorization(
|
|
18
19
|
address: `0x${string}`,
|
|
19
20
|
options: {
|
|
21
|
+
chainId?: bigint | undefined
|
|
20
22
|
expiry?: number | undefined
|
|
21
23
|
limits?: { token: `0x${string}`; limit: bigint }[] | undefined
|
|
22
24
|
scopes?: KeyAuthorization.Scope[] | undefined
|
|
@@ -25,7 +27,7 @@ function createKeyAuthorization(
|
|
|
25
27
|
return KeyAuthorization.from(
|
|
26
28
|
{
|
|
27
29
|
address,
|
|
28
|
-
chainId: 1n,
|
|
30
|
+
chainId: options.chainId ?? 1n,
|
|
29
31
|
expiry: options.expiry,
|
|
30
32
|
limits: options.limits,
|
|
31
33
|
scopes: options.scopes,
|
|
@@ -41,88 +43,129 @@ function createRevert(errorName: string) {
|
|
|
41
43
|
})
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const keyPair = await WebCryptoP256.createKeyPair()
|
|
65
|
-
const accessKey = TempoAccount.fromWebCryptoP256(keyPair)
|
|
66
|
-
const keyAuthorization = createKeyAuthorization(accessKey.address)
|
|
67
|
-
|
|
68
|
-
AccessKey.save({ address: rootAddress, keyAuthorization, store })
|
|
69
|
-
|
|
70
|
-
expect(store.getState().accessKeys[0]!.keyPair).toBeUndefined()
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
test('behavior: saves with keyPair', async () => {
|
|
74
|
-
const store = createStore()
|
|
75
|
-
const keyPair = await WebCryptoP256.createKeyPair()
|
|
76
|
-
const accessKey = TempoAccount.fromWebCryptoP256(keyPair)
|
|
77
|
-
const keyAuthorization = createKeyAuthorization(accessKey.address)
|
|
46
|
+
function createMetadataClient(
|
|
47
|
+
accessKey: Hex.Hex,
|
|
48
|
+
options: { isRevoked?: boolean | undefined; keyId?: Hex.Hex | undefined } = {},
|
|
49
|
+
) {
|
|
50
|
+
return {
|
|
51
|
+
call: async () => ({
|
|
52
|
+
data: encodeFunctionResult({
|
|
53
|
+
abi: Abis.accountKeychain,
|
|
54
|
+
functionName: 'getKey',
|
|
55
|
+
result: {
|
|
56
|
+
enforceLimits: false,
|
|
57
|
+
expiry: 0n,
|
|
58
|
+
isRevoked: options.isRevoked ?? false,
|
|
59
|
+
keyId: options.keyId ?? accessKey,
|
|
60
|
+
signatureType: 1,
|
|
61
|
+
},
|
|
62
|
+
} as never),
|
|
63
|
+
}),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
78
66
|
|
|
79
|
-
|
|
67
|
+
function createMissingClient() {
|
|
68
|
+
return {
|
|
69
|
+
call: async () => {
|
|
70
|
+
throw createRevert('KeyNotFound')
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
}
|
|
80
74
|
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
function createFillClient(
|
|
76
|
+
accessKey: Hex.Hex,
|
|
77
|
+
options: { isRevoked?: boolean | undefined; keyId?: Hex.Hex | undefined } = {},
|
|
78
|
+
) {
|
|
79
|
+
const requests: unknown[] = []
|
|
80
|
+
return {
|
|
81
|
+
client: {
|
|
82
|
+
...createMetadataClient(accessKey, options),
|
|
83
|
+
request: async (request: unknown) => {
|
|
84
|
+
requests.push(request)
|
|
85
|
+
return { capabilities: { sponsored: false }, tx: {} }
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
requests,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
83
91
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const ak2 = TempoAccount.fromWebCryptoP256(keyPair2)
|
|
92
|
+
function getStored(account: TempoAccount.AccessKeyAccount, store: Store.Store) {
|
|
93
|
+
return store
|
|
94
|
+
.getState()
|
|
95
|
+
.accessKeys.find((key) => key.address.toLowerCase() === account.accessKeyAddress.toLowerCase())
|
|
96
|
+
}
|
|
90
97
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
function addAuthorization(options: {
|
|
99
|
+
address: `0x${string}`
|
|
100
|
+
keyAuthorization: KeyAuthorization.Signed
|
|
101
|
+
keyPair?: Awaited<ReturnType<typeof WebCryptoP256.createKeyPair>> | undefined
|
|
102
|
+
privateKey?: Hex.Hex | undefined
|
|
103
|
+
state: 'signed' | 'pending' | 'authorized'
|
|
104
|
+
store: Store.Store
|
|
105
|
+
}) {
|
|
106
|
+
const { address, keyAuthorization, keyPair, privateKey, state, store } = options
|
|
107
|
+
AccessKey.add({
|
|
108
|
+
account: address,
|
|
109
|
+
authorization: keyAuthorization,
|
|
110
|
+
...(keyPair ? { keyPair } : {}),
|
|
111
|
+
...(privateKey ? { privateKey } : {}),
|
|
112
|
+
store,
|
|
113
|
+
})
|
|
114
|
+
if (state === 'pending')
|
|
115
|
+
AccessKey.markPending({
|
|
116
|
+
account: address,
|
|
117
|
+
accessKey: keyAuthorization.address,
|
|
118
|
+
chainId: Number(keyAuthorization.chainId),
|
|
94
119
|
store,
|
|
95
120
|
})
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
121
|
+
if (state === 'authorized')
|
|
122
|
+
AccessKey.markPublished({
|
|
123
|
+
account: address,
|
|
124
|
+
accessKey: keyAuthorization.address,
|
|
125
|
+
chainId: Number(keyAuthorization.chainId),
|
|
99
126
|
store,
|
|
100
127
|
})
|
|
128
|
+
}
|
|
101
129
|
|
|
102
|
-
|
|
130
|
+
function markPublished(options: {
|
|
131
|
+
accessKey: `0x${string}`
|
|
132
|
+
address?: `0x${string}` | undefined
|
|
133
|
+
chainId?: number | undefined
|
|
134
|
+
store: Store.Store
|
|
135
|
+
}) {
|
|
136
|
+
const { accessKey, store } = options
|
|
137
|
+
AccessKey.markPublished({
|
|
138
|
+
account: options.address ?? rootAddress,
|
|
139
|
+
accessKey,
|
|
140
|
+
chainId: options.chainId ?? 1,
|
|
141
|
+
store,
|
|
103
142
|
})
|
|
143
|
+
}
|
|
104
144
|
|
|
105
|
-
|
|
145
|
+
describe('add', () => {
|
|
146
|
+
test('default: saves a signed authorization', async () => {
|
|
106
147
|
const store = createStore()
|
|
107
148
|
const keyPair = await WebCryptoP256.createKeyPair()
|
|
108
149
|
const accessKey = TempoAccount.fromWebCryptoP256(keyPair)
|
|
109
150
|
const expiry = Math.floor(Date.now() / 1000) + 3600
|
|
110
|
-
const keyAuthorization = createKeyAuthorization(accessKey.address, { expiry })
|
|
111
|
-
|
|
112
|
-
AccessKey.save({ address: rootAddress, keyAuthorization, store })
|
|
113
|
-
|
|
114
|
-
expect(store.getState().accessKeys[0]!.expiry).toBe(expiry)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
test('behavior: stores limits from key authorization', async () => {
|
|
118
|
-
const store = createStore()
|
|
119
|
-
const keyPair = await WebCryptoP256.createKeyPair()
|
|
120
|
-
const accessKey = TempoAccount.fromWebCryptoP256(keyPair)
|
|
121
151
|
const limits = [{ token: '0x20c0000000000000000000000000000000000001' as const, limit: 1000n }]
|
|
122
|
-
const keyAuthorization = createKeyAuthorization(accessKey.address, { limits })
|
|
152
|
+
const keyAuthorization = createKeyAuthorization(accessKey.address, { expiry, limits })
|
|
123
153
|
|
|
124
|
-
|
|
154
|
+
addAuthorization({
|
|
155
|
+
address: rootAddress,
|
|
156
|
+
keyAuthorization,
|
|
157
|
+
state: 'signed',
|
|
158
|
+
store,
|
|
159
|
+
})
|
|
125
160
|
|
|
161
|
+
const { accessKeys } = store.getState()
|
|
162
|
+
expect(accessKeys.length).toMatchInlineSnapshot(`1`)
|
|
163
|
+
expect(accessKeys[0]!.address).toBe(accessKey.address)
|
|
164
|
+
expect(accessKeys[0]!.access).toBe(rootAddress)
|
|
165
|
+
expect(accessKeys[0]!.chainId).toMatchInlineSnapshot(`1`)
|
|
166
|
+
expect(accessKeys[0]!.expiry).toBe(expiry)
|
|
167
|
+
expect(accessKeys[0]!.keyType).toMatchInlineSnapshot(`"p256"`)
|
|
168
|
+
expect(accessKeys[0]!.keyAuthorization).toBe(keyAuthorization)
|
|
126
169
|
expect(store.getState().accessKeys[0]!.limits).toMatchInlineSnapshot(`
|
|
127
170
|
[
|
|
128
171
|
{
|
|
@@ -134,110 +177,129 @@ describe('save', () => {
|
|
|
134
177
|
})
|
|
135
178
|
})
|
|
136
179
|
|
|
137
|
-
describe('
|
|
138
|
-
test('default: returns key authorization for access key account', async () => {
|
|
139
|
-
const store = createStore()
|
|
140
|
-
const keyPair = await WebCryptoP256.createKeyPair()
|
|
141
|
-
const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress })
|
|
142
|
-
const keyAuthorization = createKeyAuthorization(accessKey.accessKeyAddress)
|
|
143
|
-
|
|
144
|
-
AccessKey.save({ address: rootAddress, keyAuthorization, store })
|
|
145
|
-
|
|
146
|
-
const result = AccessKey.getPending(accessKey, { store })
|
|
147
|
-
expect(result).toBe(keyAuthorization)
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
test('behavior: returns undefined for root account', () => {
|
|
151
|
-
const store = createStore()
|
|
152
|
-
const result = AccessKey.getPending(accounts[0]!, { store })
|
|
153
|
-
expect(result).toBeUndefined()
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
test('behavior: returns undefined when no matching access key', async () => {
|
|
157
|
-
const store = createStore()
|
|
158
|
-
const keyPair = await WebCryptoP256.createKeyPair()
|
|
159
|
-
const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress })
|
|
160
|
-
|
|
161
|
-
const result = AccessKey.getPending(accessKey, { store })
|
|
162
|
-
expect(result).toBeUndefined()
|
|
163
|
-
})
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
describe('removePending', () => {
|
|
180
|
+
describe('markPublished', () => {
|
|
167
181
|
test('default: clears key authorization from access key', async () => {
|
|
168
182
|
const store = createStore()
|
|
169
183
|
const keyPair = await WebCryptoP256.createKeyPair()
|
|
170
184
|
const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress })
|
|
171
185
|
const keyAuthorization = createKeyAuthorization(accessKey.accessKeyAddress)
|
|
172
186
|
|
|
173
|
-
|
|
174
|
-
|
|
187
|
+
addAuthorization({
|
|
188
|
+
address: rootAddress,
|
|
189
|
+
keyAuthorization,
|
|
190
|
+
state: 'signed',
|
|
191
|
+
store,
|
|
192
|
+
})
|
|
193
|
+
expect(getStored(accessKey, store)?.keyAuthorization).toBeDefined()
|
|
175
194
|
|
|
176
|
-
|
|
195
|
+
markPublished({ accessKey: accessKey.accessKeyAddress, store })
|
|
177
196
|
|
|
178
|
-
expect(
|
|
197
|
+
expect(getStored(accessKey, store)?.keyAuthorization).toBeUndefined()
|
|
198
|
+
expect(getStored(accessKey, store)?.keyAuthorizationPending).toBeUndefined()
|
|
179
199
|
})
|
|
180
200
|
|
|
181
|
-
test('behavior:
|
|
201
|
+
test('behavior: only clears the matching account and chain', async () => {
|
|
182
202
|
const store = createStore()
|
|
183
203
|
const keyPair = await WebCryptoP256.createKeyPair()
|
|
184
204
|
const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress })
|
|
185
205
|
const keyAuthorization = createKeyAuthorization(accessKey.accessKeyAddress)
|
|
206
|
+
const keyAuthorization_chain = createKeyAuthorization(accessKey.accessKeyAddress, {
|
|
207
|
+
chainId: 2n,
|
|
208
|
+
})
|
|
209
|
+
const keyAuthorization_account = createKeyAuthorization(accessKey.accessKeyAddress)
|
|
186
210
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
211
|
+
addAuthorization({
|
|
212
|
+
address: rootAddress,
|
|
213
|
+
keyAuthorization,
|
|
214
|
+
state: 'signed',
|
|
215
|
+
store,
|
|
216
|
+
})
|
|
217
|
+
addAuthorization({
|
|
218
|
+
address: rootAddress,
|
|
219
|
+
keyAuthorization: keyAuthorization_chain,
|
|
220
|
+
state: 'signed',
|
|
221
|
+
store,
|
|
222
|
+
})
|
|
223
|
+
addAuthorization({
|
|
224
|
+
address: accounts[1]!.address,
|
|
225
|
+
keyAuthorization: keyAuthorization_account,
|
|
226
|
+
state: 'signed',
|
|
227
|
+
store,
|
|
228
|
+
})
|
|
205
229
|
|
|
206
|
-
|
|
230
|
+
markPublished({ accessKey: accessKey.accessKeyAddress, store })
|
|
207
231
|
|
|
208
|
-
expect(
|
|
209
|
-
|
|
232
|
+
expect(
|
|
233
|
+
store.getState().accessKeys.map((key) => ({
|
|
234
|
+
access: key.access,
|
|
235
|
+
chainId: key.chainId,
|
|
236
|
+
keyAuthorization: !!key.keyAuthorization,
|
|
237
|
+
})),
|
|
238
|
+
).toMatchInlineSnapshot(`
|
|
239
|
+
[
|
|
240
|
+
{
|
|
241
|
+
"access": "${accounts[1]!.address}",
|
|
242
|
+
"chainId": 1,
|
|
243
|
+
"keyAuthorization": true,
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"access": "${rootAddress}",
|
|
247
|
+
"chainId": 2,
|
|
248
|
+
"keyAuthorization": true,
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"access": "${rootAddress}",
|
|
252
|
+
"chainId": 1,
|
|
253
|
+
"keyAuthorization": false,
|
|
254
|
+
},
|
|
255
|
+
]
|
|
256
|
+
`)
|
|
210
257
|
})
|
|
211
258
|
})
|
|
212
259
|
|
|
213
|
-
describe('
|
|
214
|
-
async function setup() {
|
|
260
|
+
describe('create invalidation', () => {
|
|
261
|
+
async function setup(options: { other?: boolean | undefined } = {}) {
|
|
215
262
|
const store = createStore()
|
|
263
|
+
const keyPair_other = await WebCryptoP256.createKeyPair()
|
|
264
|
+
const account_other = TempoAccount.fromWebCryptoP256(keyPair_other, { access: rootAddress })
|
|
265
|
+
if (options.other)
|
|
266
|
+
addAuthorization({
|
|
267
|
+
address: rootAddress,
|
|
268
|
+
keyAuthorization: createKeyAuthorization(account_other.accessKeyAddress),
|
|
269
|
+
keyPair: keyPair_other,
|
|
270
|
+
state: 'signed',
|
|
271
|
+
store,
|
|
272
|
+
})
|
|
273
|
+
|
|
216
274
|
const keyPair = await WebCryptoP256.createKeyPair()
|
|
217
275
|
const account = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress })
|
|
218
|
-
|
|
276
|
+
addAuthorization({
|
|
219
277
|
address: rootAddress,
|
|
220
278
|
keyAuthorization: createKeyAuthorization(account.accessKeyAddress),
|
|
221
279
|
keyPair,
|
|
280
|
+
state: 'signed',
|
|
222
281
|
store,
|
|
223
282
|
})
|
|
224
|
-
return {
|
|
283
|
+
return { account_other, store }
|
|
225
284
|
}
|
|
226
285
|
|
|
227
|
-
test('
|
|
228
|
-
const {
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
AccessKey.save({
|
|
286
|
+
test('behavior: removes matching access key for stale-key errors', async () => {
|
|
287
|
+
const { account_other, store } = await setup({ other: true })
|
|
288
|
+
|
|
289
|
+
const transaction = await AccessKeyTransaction.create({
|
|
232
290
|
address: rootAddress,
|
|
233
|
-
|
|
234
|
-
|
|
291
|
+
chainId: 1,
|
|
292
|
+
client: {
|
|
293
|
+
request: async () => {
|
|
294
|
+
throw createRevert('KeyNotFound')
|
|
295
|
+
},
|
|
296
|
+
} as never,
|
|
235
297
|
store,
|
|
236
298
|
})
|
|
237
299
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
300
|
+
await expect(
|
|
301
|
+
transaction?.fill({ chainId: 1, from: rootAddress }),
|
|
302
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: reverted]`)
|
|
241
303
|
expect(store.getState().accessKeys.map((key) => key.address)).toMatchInlineSnapshot(`
|
|
242
304
|
[
|
|
243
305
|
"${account_other.accessKeyAddress}",
|
|
@@ -246,31 +308,65 @@ describe('invalidate', () => {
|
|
|
246
308
|
})
|
|
247
309
|
|
|
248
310
|
test('behavior: preserves access key for recoverable execution errors', async () => {
|
|
249
|
-
const {
|
|
311
|
+
const { store } = await setup()
|
|
250
312
|
|
|
251
|
-
const
|
|
313
|
+
const transaction = await AccessKeyTransaction.create({
|
|
314
|
+
address: rootAddress,
|
|
315
|
+
chainId: 1,
|
|
316
|
+
client: {
|
|
317
|
+
request: async () => {
|
|
318
|
+
throw createRevert('SpendingLimitExceeded')
|
|
319
|
+
},
|
|
320
|
+
} as never,
|
|
252
321
|
store,
|
|
253
322
|
})
|
|
254
323
|
|
|
255
|
-
expect(
|
|
324
|
+
await expect(
|
|
325
|
+
transaction?.fill({ chainId: 1, from: rootAddress }),
|
|
326
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: reverted]`)
|
|
256
327
|
expect(store.getState().accessKeys.length).toMatchInlineSnapshot(`1`)
|
|
257
328
|
})
|
|
258
329
|
|
|
259
330
|
test('behavior: preserves access key for unknown errors', async () => {
|
|
260
|
-
const {
|
|
331
|
+
const { store } = await setup()
|
|
261
332
|
|
|
262
|
-
const
|
|
333
|
+
const transaction = await AccessKeyTransaction.create({
|
|
334
|
+
address: rootAddress,
|
|
335
|
+
chainId: 1,
|
|
336
|
+
client: {
|
|
337
|
+
request: async () => {
|
|
338
|
+
throw new Error('network failed')
|
|
339
|
+
},
|
|
340
|
+
} as never,
|
|
341
|
+
store,
|
|
342
|
+
})
|
|
263
343
|
|
|
264
|
-
expect(
|
|
344
|
+
await expect(
|
|
345
|
+
transaction?.fill({ chainId: 1, from: rootAddress }),
|
|
346
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: network failed]`)
|
|
265
347
|
expect(store.getState().accessKeys.length).toMatchInlineSnapshot(`1`)
|
|
266
348
|
})
|
|
349
|
+
})
|
|
267
350
|
|
|
268
|
-
|
|
269
|
-
|
|
351
|
+
describe('isUnavailableError', () => {
|
|
352
|
+
test('default: recognizes unavailable key revert errors', () => {
|
|
353
|
+
expect(AccessKey.isUnavailableError(createRevert('KeyNotFound'))).toMatchInlineSnapshot(`true`)
|
|
354
|
+
expect(AccessKey.isUnavailableError(createRevert('KeyAlreadyRevoked'))).toMatchInlineSnapshot(
|
|
355
|
+
`true`,
|
|
356
|
+
)
|
|
357
|
+
expect(
|
|
358
|
+
AccessKey.isUnavailableError(createRevert('SpendingLimitExceeded')),
|
|
359
|
+
).toMatchInlineSnapshot(`false`)
|
|
360
|
+
})
|
|
270
361
|
|
|
271
|
-
|
|
362
|
+
test('behavior: recognizes nested viem error data', () => {
|
|
363
|
+
const error = new BaseError('revoke failed', {
|
|
364
|
+
cause: Object.assign(new Error('execution reverted'), {
|
|
365
|
+
data: { errorName: 'KeyAlreadyRevoked' },
|
|
366
|
+
}),
|
|
367
|
+
})
|
|
272
368
|
|
|
273
|
-
expect(
|
|
369
|
+
expect(AccessKey.isUnavailableError(error)).toMatchInlineSnapshot(`true`)
|
|
274
370
|
})
|
|
275
371
|
})
|
|
276
372
|
|
|
@@ -385,23 +481,34 @@ describe('prepareAuthorization', () => {
|
|
|
385
481
|
})
|
|
386
482
|
})
|
|
387
483
|
|
|
388
|
-
describe('
|
|
389
|
-
test('default:
|
|
484
|
+
describe('authorize', () => {
|
|
485
|
+
test('default: prepares, signs, and saves authorization', async () => {
|
|
390
486
|
const store = createStore()
|
|
391
|
-
const
|
|
392
|
-
address: accounts[1]!.address,
|
|
393
|
-
chainId: 1,
|
|
394
|
-
expiry: 123,
|
|
395
|
-
})
|
|
487
|
+
const digests: Hex.Hex[] = []
|
|
396
488
|
const signature = `0x${'11'.repeat(32)}${'22'.repeat(32)}1b` as const
|
|
489
|
+
const account = {
|
|
490
|
+
...accounts[0]!,
|
|
491
|
+
sign: async (parameters: { hash: Hex.Hex }) => {
|
|
492
|
+
digests.push(parameters.hash)
|
|
493
|
+
return signature
|
|
494
|
+
},
|
|
495
|
+
} as TempoAccount.Account
|
|
397
496
|
|
|
398
|
-
const result = AccessKey.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
497
|
+
const result = await AccessKey.authorize({
|
|
498
|
+
account,
|
|
499
|
+
chainId: 1,
|
|
500
|
+
parameters: {
|
|
501
|
+
address: accounts[1]!.address,
|
|
502
|
+
expiry: 123,
|
|
503
|
+
},
|
|
402
504
|
store,
|
|
403
505
|
})
|
|
404
506
|
|
|
507
|
+
expect(digests).toMatchInlineSnapshot(`
|
|
508
|
+
[
|
|
509
|
+
"0xea47721547363fc82a5dca62b4544e4718d861b3df10bfac65d30102594b5c26",
|
|
510
|
+
]
|
|
511
|
+
`)
|
|
405
512
|
expect(result).toMatchInlineSnapshot(`
|
|
406
513
|
{
|
|
407
514
|
"chainId": "0x1",
|
|
@@ -434,448 +541,298 @@ describe('saveAuthorization', () => {
|
|
|
434
541
|
})
|
|
435
542
|
})
|
|
436
543
|
|
|
437
|
-
describe('
|
|
438
|
-
|
|
544
|
+
describe('select', () => {
|
|
545
|
+
async function setup(options: { pending?: boolean | undefined } = {}) {
|
|
439
546
|
const store = createStore()
|
|
440
|
-
const
|
|
441
|
-
const
|
|
442
|
-
const
|
|
443
|
-
...accounts[0]!,
|
|
444
|
-
sign: async (parameters: { hash: Hex.Hex }) => {
|
|
445
|
-
digests.push(parameters.hash)
|
|
446
|
-
return signature
|
|
447
|
-
},
|
|
448
|
-
} as TempoAccount.Account
|
|
547
|
+
const keyPair = await WebCryptoP256.createKeyPair()
|
|
548
|
+
const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress })
|
|
549
|
+
const keyAuthorization = createKeyAuthorization(accessKey.accessKeyAddress)
|
|
449
550
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
expiry: 123,
|
|
456
|
-
},
|
|
551
|
+
addAuthorization({
|
|
552
|
+
address: rootAddress,
|
|
553
|
+
keyAuthorization,
|
|
554
|
+
keyPair,
|
|
555
|
+
state: options.pending ? 'pending' : 'signed',
|
|
457
556
|
store,
|
|
458
557
|
})
|
|
459
558
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
"0xea47721547363fc82a5dca62b4544e4718d861b3df10bfac65d30102594b5c26",
|
|
463
|
-
]
|
|
464
|
-
`)
|
|
465
|
-
expect(store.getState().accessKeys.length).toMatchInlineSnapshot(`1`)
|
|
466
|
-
})
|
|
467
|
-
})
|
|
559
|
+
return { accessKey, keyAuthorization, store }
|
|
560
|
+
}
|
|
468
561
|
|
|
469
|
-
|
|
470
|
-
|
|
562
|
+
test('behavior: skips access keys for another root address', async () => {
|
|
563
|
+
const store = createStore()
|
|
471
564
|
const keyPair = await WebCryptoP256.createKeyPair()
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
address:
|
|
475
|
-
|
|
565
|
+
const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: accounts[1]!.address })
|
|
566
|
+
addAuthorization({
|
|
567
|
+
address: accounts[1]!.address,
|
|
568
|
+
keyAuthorization: createKeyAuthorization(accessKey.accessKeyAddress),
|
|
476
569
|
keyPair,
|
|
477
|
-
|
|
570
|
+
state: 'signed',
|
|
571
|
+
store,
|
|
478
572
|
})
|
|
479
573
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
expect(result.source).toMatchInlineSnapshot(`"accessKey"`)
|
|
483
|
-
})
|
|
484
|
-
|
|
485
|
-
test('behavior: hydrates private-key access key to signable account', () => {
|
|
486
|
-
const result = AccessKey.hydrate({
|
|
487
|
-
access: rootAddress,
|
|
488
|
-
address: accounts[1]!.address,
|
|
574
|
+
const result = await AccessKey.select({
|
|
575
|
+
account: rootAddress,
|
|
489
576
|
chainId: 1,
|
|
490
|
-
|
|
491
|
-
|
|
577
|
+
client: createMetadataClient(accessKey.accessKeyAddress) as never,
|
|
578
|
+
store,
|
|
492
579
|
})
|
|
493
580
|
|
|
494
|
-
expect(result.type).toMatchInlineSnapshot(`"local"`)
|
|
495
|
-
expect(typeof result.sign).toMatchInlineSnapshot(`"function"`)
|
|
496
|
-
expect(result.source).toMatchInlineSnapshot(`"accessKey"`)
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
test('error: throws for external access key without signer material', () => {
|
|
500
|
-
expect(() =>
|
|
501
|
-
AccessKey.hydrate({
|
|
502
|
-
access: rootAddress,
|
|
503
|
-
address: '0x0000000000000000000000000000000000000099',
|
|
504
|
-
chainId: 1,
|
|
505
|
-
keyType: 'p256',
|
|
506
|
-
}),
|
|
507
|
-
).toThrowErrorMatchingInlineSnapshot(
|
|
508
|
-
`[Provider.UnauthorizedError: External access key cannot be hydrated for signing.]`,
|
|
509
|
-
)
|
|
510
|
-
})
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
describe('selectAccount', () => {
|
|
514
|
-
function setup(accessKeys: readonly Store.AccessKey[] = []) {
|
|
515
|
-
const store = createStore()
|
|
516
|
-
store.setState({ accessKeys })
|
|
517
|
-
return store
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
test('default: selects locally-signable access key for root address', async () => {
|
|
521
|
-
const keyPair = await WebCryptoP256.createKeyPair()
|
|
522
|
-
const store = setup([
|
|
523
|
-
{
|
|
524
|
-
access: rootAddress,
|
|
525
|
-
address: '0x0000000000000000000000000000000000000099',
|
|
526
|
-
chainId: 1,
|
|
527
|
-
keyPair,
|
|
528
|
-
keyType: 'webCrypto',
|
|
529
|
-
},
|
|
530
|
-
])
|
|
531
|
-
|
|
532
|
-
const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store })
|
|
533
|
-
|
|
534
|
-
expect(result?.source).toMatchInlineSnapshot(`"accessKey"`)
|
|
535
|
-
})
|
|
536
|
-
|
|
537
|
-
test('behavior: skips access keys for another root address', async () => {
|
|
538
|
-
const keyPair = await WebCryptoP256.createKeyPair()
|
|
539
|
-
const store = setup([
|
|
540
|
-
{
|
|
541
|
-
access: accounts[1]!.address,
|
|
542
|
-
address: '0x0000000000000000000000000000000000000099',
|
|
543
|
-
chainId: 1,
|
|
544
|
-
keyPair,
|
|
545
|
-
keyType: 'webCrypto',
|
|
546
|
-
},
|
|
547
|
-
])
|
|
548
|
-
|
|
549
|
-
const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store })
|
|
550
|
-
|
|
551
581
|
expect(result).toMatchInlineSnapshot(`undefined`)
|
|
552
582
|
})
|
|
553
583
|
|
|
554
584
|
test('behavior: skips access keys for another chain', async () => {
|
|
555
|
-
const
|
|
556
|
-
const store = setup([
|
|
557
|
-
{
|
|
558
|
-
access: rootAddress,
|
|
559
|
-
address: '0x0000000000000000000000000000000000000099',
|
|
560
|
-
chainId: 1,
|
|
561
|
-
keyPair,
|
|
562
|
-
keyType: 'webCrypto',
|
|
563
|
-
},
|
|
564
|
-
])
|
|
565
|
-
|
|
566
|
-
const result = AccessKey.selectAccount({ address: rootAddress, chainId: 42_431, store })
|
|
567
|
-
|
|
568
|
-
expect(result).toMatchInlineSnapshot(`undefined`)
|
|
569
|
-
})
|
|
570
|
-
|
|
571
|
-
test('behavior: skips external access keys without signer material', () => {
|
|
572
|
-
const store = setup([
|
|
573
|
-
{
|
|
574
|
-
access: rootAddress,
|
|
575
|
-
address: '0x0000000000000000000000000000000000000099',
|
|
576
|
-
chainId: 1,
|
|
577
|
-
keyType: 'p256',
|
|
578
|
-
},
|
|
579
|
-
])
|
|
585
|
+
const { accessKey, store } = await setup()
|
|
580
586
|
|
|
581
|
-
const result = AccessKey.
|
|
587
|
+
const result = await AccessKey.select({
|
|
588
|
+
account: rootAddress,
|
|
589
|
+
chainId: 42_431,
|
|
590
|
+
client: createMetadataClient(accessKey.accessKeyAddress) as never,
|
|
591
|
+
store,
|
|
592
|
+
})
|
|
582
593
|
|
|
583
594
|
expect(result).toMatchInlineSnapshot(`undefined`)
|
|
584
595
|
})
|
|
585
596
|
|
|
586
|
-
test('behavior:
|
|
587
|
-
const
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
keyType: 'webCrypto',
|
|
596
|
-
},
|
|
597
|
-
])
|
|
597
|
+
test('behavior: skips external access keys without signer material', async () => {
|
|
598
|
+
const store = createStore()
|
|
599
|
+
const keyAuthorization = createKeyAuthorization('0x0000000000000000000000000000000000000099')
|
|
600
|
+
addAuthorization({
|
|
601
|
+
address: rootAddress,
|
|
602
|
+
keyAuthorization,
|
|
603
|
+
state: 'signed',
|
|
604
|
+
store,
|
|
605
|
+
})
|
|
598
606
|
|
|
599
|
-
const result = AccessKey.
|
|
607
|
+
const result = await AccessKey.select({
|
|
608
|
+
account: rootAddress,
|
|
609
|
+
chainId: 1,
|
|
610
|
+
client: createMetadataClient(keyAuthorization.address) as never,
|
|
611
|
+
store,
|
|
612
|
+
})
|
|
600
613
|
|
|
601
614
|
expect(result).toMatchInlineSnapshot(`undefined`)
|
|
602
|
-
expect(store.getState().accessKeys).toMatchInlineSnapshot(`[]`)
|
|
603
615
|
})
|
|
604
616
|
|
|
605
|
-
test('behavior:
|
|
617
|
+
test('behavior: matches access key scopes against transaction calls', async () => {
|
|
618
|
+
const store = createStore()
|
|
606
619
|
const keyPair = await WebCryptoP256.createKeyPair()
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store })
|
|
620
|
+
const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress })
|
|
621
|
+
const token = '0x0000000000000000000000000000000000000abc' as const
|
|
622
|
+
addAuthorization({
|
|
623
|
+
address: rootAddress,
|
|
624
|
+
keyAuthorization: createKeyAuthorization(accessKey.accessKeyAddress, {
|
|
625
|
+
scopes: [{ address: token, selector: 'transfer(address,uint256)' }],
|
|
626
|
+
}),
|
|
627
|
+
keyPair,
|
|
628
|
+
state: 'signed',
|
|
629
|
+
store,
|
|
630
|
+
})
|
|
619
631
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
632
|
+
const match = await AccessKey.select({
|
|
633
|
+
account: rootAddress,
|
|
634
|
+
calls: [{ to: token, data: '0xa9059cbb0000000000000000000000000000000000000001' }],
|
|
635
|
+
chainId: 1,
|
|
636
|
+
client: createMetadataClient(accessKey.accessKeyAddress) as never,
|
|
637
|
+
store,
|
|
638
|
+
})
|
|
639
|
+
const miss = await AccessKey.select({
|
|
640
|
+
account: rootAddress,
|
|
641
|
+
calls: [{ to: '0x0000000000000000000000000000000000000def', data: '0xdeadbeef' }],
|
|
642
|
+
chainId: 1,
|
|
643
|
+
client: createMetadataClient(accessKey.accessKeyAddress) as never,
|
|
644
|
+
store,
|
|
645
|
+
})
|
|
623
646
|
|
|
624
|
-
|
|
625
|
-
const keyPair = await WebCryptoP256.createKeyPair()
|
|
626
|
-
const store = setup([
|
|
647
|
+
expect({ match: !!match, miss: !!miss }).toMatchInlineSnapshot(`
|
|
627
648
|
{
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
keyPair,
|
|
632
|
-
keyType: 'webCrypto',
|
|
633
|
-
limits: [
|
|
634
|
-
{
|
|
635
|
-
token: '0x0000000000000000000000000000000000000abc',
|
|
636
|
-
limit: 1000n,
|
|
637
|
-
},
|
|
638
|
-
],
|
|
639
|
-
},
|
|
640
|
-
])
|
|
641
|
-
|
|
642
|
-
const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store })
|
|
643
|
-
|
|
644
|
-
expect(result?.source).toMatchInlineSnapshot(`"accessKey"`)
|
|
645
|
-
expect(store.getState().accessKeys[0]?.limits).toMatchInlineSnapshot(`
|
|
646
|
-
[
|
|
647
|
-
{
|
|
648
|
-
"limit": 1000n,
|
|
649
|
-
"token": "0x0000000000000000000000000000000000000abc",
|
|
650
|
-
},
|
|
651
|
-
]
|
|
649
|
+
"match": true,
|
|
650
|
+
"miss": false,
|
|
651
|
+
}
|
|
652
652
|
`)
|
|
653
653
|
})
|
|
654
|
+
})
|
|
654
655
|
|
|
655
|
-
|
|
656
|
+
describe('create', () => {
|
|
657
|
+
async function setup(options: { pending?: boolean | undefined } = {}) {
|
|
658
|
+
const store = createStore()
|
|
656
659
|
const keyPair = await WebCryptoP256.createKeyPair()
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
access: rootAddress,
|
|
660
|
-
address: '0x0000000000000000000000000000000000000099',
|
|
661
|
-
chainId: 1,
|
|
662
|
-
keyPair,
|
|
663
|
-
keyType: 'webCrypto',
|
|
664
|
-
},
|
|
665
|
-
])
|
|
660
|
+
const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress })
|
|
661
|
+
const keyAuthorization = createKeyAuthorization(accessKey.accessKeyAddress)
|
|
666
662
|
|
|
667
|
-
|
|
663
|
+
addAuthorization({
|
|
668
664
|
address: rootAddress,
|
|
669
|
-
|
|
665
|
+
keyAuthorization,
|
|
666
|
+
keyPair,
|
|
667
|
+
state: options.pending ? 'pending' : 'signed',
|
|
670
668
|
store,
|
|
671
|
-
calls: [{ to: '0x0000000000000000000000000000000000000abc', data: '0xa9059cbb' }],
|
|
672
669
|
})
|
|
673
670
|
|
|
674
|
-
|
|
675
|
-
}
|
|
671
|
+
return { accessKey, keyAuthorization, store }
|
|
672
|
+
}
|
|
676
673
|
|
|
677
|
-
test('behavior:
|
|
678
|
-
const
|
|
679
|
-
const
|
|
680
|
-
const store = setup([
|
|
681
|
-
{
|
|
682
|
-
access: rootAddress,
|
|
683
|
-
address: '0x0000000000000000000000000000000000000099',
|
|
684
|
-
chainId: 1,
|
|
685
|
-
keyPair,
|
|
686
|
-
keyType: 'webCrypto',
|
|
687
|
-
scopes: [{ address: token, selector: '0xa9059cbb' }],
|
|
688
|
-
},
|
|
689
|
-
])
|
|
674
|
+
test('behavior: returns undefined when no matching access key exists', async () => {
|
|
675
|
+
const store = createStore()
|
|
676
|
+
const { client } = createFillClient(accounts[1]!.address)
|
|
690
677
|
|
|
691
|
-
const result =
|
|
678
|
+
const result = await AccessKeyTransaction.create({
|
|
692
679
|
address: rootAddress,
|
|
693
680
|
chainId: 1,
|
|
681
|
+
client: client as never,
|
|
694
682
|
store,
|
|
695
|
-
calls: [{ to: token, data: '0xa9059cbb0000000000000000000000000000000000000001' }],
|
|
696
683
|
})
|
|
697
684
|
|
|
698
|
-
expect(result
|
|
685
|
+
expect(result).toMatchInlineSnapshot(`undefined`)
|
|
699
686
|
})
|
|
700
687
|
|
|
701
|
-
test('
|
|
702
|
-
const
|
|
703
|
-
const
|
|
704
|
-
const store = setup([
|
|
705
|
-
{
|
|
706
|
-
access: rootAddress,
|
|
707
|
-
address: '0x0000000000000000000000000000000000000099',
|
|
708
|
-
chainId: 1,
|
|
709
|
-
keyPair,
|
|
710
|
-
keyType: 'webCrypto',
|
|
711
|
-
scopes: [{ address: token, selector: '0xa9059cbb' }],
|
|
712
|
-
},
|
|
713
|
-
])
|
|
688
|
+
test('default: returns selected account with pending key authorization', async () => {
|
|
689
|
+
const { keyAuthorization, store } = await setup()
|
|
690
|
+
const { client, requests } = createFillClient(keyAuthorization.address)
|
|
714
691
|
|
|
715
|
-
const result =
|
|
692
|
+
const result = await AccessKeyTransaction.create({
|
|
716
693
|
address: rootAddress,
|
|
717
694
|
chainId: 1,
|
|
695
|
+
client: client as never,
|
|
718
696
|
store,
|
|
719
|
-
calls: [{ to: '0x0000000000000000000000000000000000000def', data: '0xdeadbeef' }],
|
|
720
697
|
})
|
|
698
|
+
await result?.fill({ chainId: 1, from: rootAddress })
|
|
721
699
|
|
|
722
|
-
expect(result).toMatchInlineSnapshot(`
|
|
700
|
+
expect(!!result).toMatchInlineSnapshot(`true`)
|
|
701
|
+
const request = requests[0] as {
|
|
702
|
+
params: readonly [{ keyAuthorization?: { keyId?: string | undefined } | undefined }]
|
|
703
|
+
}
|
|
704
|
+
expect(request.params[0].keyAuthorization?.keyId).toBe(keyAuthorization.address)
|
|
723
705
|
})
|
|
724
706
|
|
|
725
|
-
test('behavior:
|
|
726
|
-
const
|
|
727
|
-
const
|
|
728
|
-
const store = setup([
|
|
729
|
-
{
|
|
730
|
-
access: rootAddress,
|
|
731
|
-
address: '0x0000000000000000000000000000000000000099',
|
|
732
|
-
chainId: 1,
|
|
733
|
-
keyPair,
|
|
734
|
-
keyType: 'webCrypto',
|
|
735
|
-
scopes: [{ address: token, selector: 'transfer(address,uint256)' }],
|
|
736
|
-
},
|
|
737
|
-
])
|
|
738
|
-
|
|
739
|
-
const result = AccessKey.selectAccount({
|
|
707
|
+
test('behavior: signing prepared transaction marks key authorization pending', async () => {
|
|
708
|
+
const { accessKey, keyAuthorization, store } = await setup()
|
|
709
|
+
const result = await AccessKeyTransaction.create({
|
|
740
710
|
address: rootAddress,
|
|
741
711
|
chainId: 1,
|
|
712
|
+
client: { chain: { id: 1 } } as never,
|
|
742
713
|
store,
|
|
743
|
-
calls: [{ to: token, data: '0xa9059cbb0000000000000000000000000000000000000001' }],
|
|
744
714
|
})
|
|
745
715
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
{
|
|
755
|
-
access: rootAddress,
|
|
756
|
-
address: '0x0000000000000000000000000000000000000099',
|
|
757
|
-
chainId: 1,
|
|
758
|
-
keyPair,
|
|
759
|
-
keyType: 'webCrypto',
|
|
760
|
-
scopes: [
|
|
761
|
-
{ address: token, selector: 'transfer(address,uint256)', recipients: [recipient] },
|
|
762
|
-
],
|
|
763
|
-
},
|
|
764
|
-
])
|
|
765
|
-
const call = Actions.token.transfer.call({
|
|
766
|
-
amount: 1n,
|
|
767
|
-
to: recipient,
|
|
768
|
-
token,
|
|
716
|
+
const prepared = await result?.prepare({
|
|
717
|
+
from: rootAddress,
|
|
718
|
+
gas: 21_000n,
|
|
719
|
+
maxFeePerGas: 1n,
|
|
720
|
+
maxPriorityFeePerGas: 1n,
|
|
721
|
+
nonce: 0,
|
|
722
|
+
to: accounts[1]!.address,
|
|
723
|
+
value: 1n,
|
|
769
724
|
})
|
|
725
|
+
await prepared?.sign()
|
|
770
726
|
|
|
771
|
-
const
|
|
727
|
+
const stored = getStored(accessKey, store)
|
|
728
|
+
expect(stored?.keyAuthorization).toBe(keyAuthorization)
|
|
729
|
+
expect(stored?.keyAuthorizationPending).toMatchInlineSnapshot(`true`)
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
test('behavior: sending asynchronously keeps key authorization pending', async () => {
|
|
733
|
+
const { accessKey, keyAuthorization, store } = await setup()
|
|
734
|
+
const requests: unknown[] = []
|
|
735
|
+
const result = await AccessKeyTransaction.create({
|
|
772
736
|
address: rootAddress,
|
|
773
737
|
chainId: 1,
|
|
738
|
+
client: {
|
|
739
|
+
chain: { id: 1 },
|
|
740
|
+
request: async (request: unknown) => {
|
|
741
|
+
requests.push(request)
|
|
742
|
+
return '0xtransaction'
|
|
743
|
+
},
|
|
744
|
+
} as never,
|
|
774
745
|
store,
|
|
775
|
-
calls: [call],
|
|
776
746
|
})
|
|
777
747
|
|
|
778
|
-
|
|
748
|
+
const prepared = await result?.prepare({
|
|
749
|
+
from: rootAddress,
|
|
750
|
+
gas: 21_000n,
|
|
751
|
+
maxFeePerGas: 1n,
|
|
752
|
+
maxPriorityFeePerGas: 1n,
|
|
753
|
+
nonce: 0,
|
|
754
|
+
to: accounts[1]!.address,
|
|
755
|
+
value: 1n,
|
|
756
|
+
})
|
|
757
|
+
await prepared?.send()
|
|
758
|
+
|
|
759
|
+
const stored = getStored(accessKey, store)
|
|
760
|
+
expect(stored?.keyAuthorization).toBe(keyAuthorization)
|
|
761
|
+
expect(stored?.keyAuthorizationPending).toMatchInlineSnapshot(`true`)
|
|
762
|
+
expect(requests.map((request) => (request as { method: string }).method))
|
|
763
|
+
.toMatchInlineSnapshot(`
|
|
764
|
+
[
|
|
765
|
+
"eth_fillTransaction",
|
|
766
|
+
"eth_sendRawTransaction",
|
|
767
|
+
]
|
|
768
|
+
`)
|
|
779
769
|
})
|
|
780
770
|
|
|
781
|
-
test('behavior:
|
|
782
|
-
const
|
|
783
|
-
const token = '0x0000000000000000000000000000000000000abc' as const
|
|
784
|
-
const store = setup([
|
|
785
|
-
{
|
|
786
|
-
access: rootAddress,
|
|
787
|
-
address: '0x0000000000000000000000000000000000000099',
|
|
788
|
-
chainId: 1,
|
|
789
|
-
keyPair,
|
|
790
|
-
keyType: 'webCrypto',
|
|
791
|
-
scopes: [
|
|
792
|
-
{
|
|
793
|
-
address: token,
|
|
794
|
-
selector: 'transfer(address,uint256)',
|
|
795
|
-
recipients: ['0x0000000000000000000000000000000000000def'],
|
|
796
|
-
},
|
|
797
|
-
],
|
|
798
|
-
},
|
|
799
|
-
])
|
|
800
|
-
const call = Actions.token.transfer.call({
|
|
801
|
-
amount: 1n,
|
|
802
|
-
to: '0x0000000000000000000000000000000000000fed',
|
|
803
|
-
token,
|
|
804
|
-
})
|
|
771
|
+
test('behavior: clears pending authorization when pending key is published on-chain', async () => {
|
|
772
|
+
const { accessKey, store } = await setup({ pending: true })
|
|
805
773
|
|
|
806
|
-
const result =
|
|
774
|
+
const result = await AccessKeyTransaction.create({
|
|
807
775
|
address: rootAddress,
|
|
808
776
|
chainId: 1,
|
|
777
|
+
client: createMetadataClient(accessKey.accessKeyAddress) as never,
|
|
809
778
|
store,
|
|
810
|
-
calls: [call],
|
|
811
779
|
})
|
|
812
780
|
|
|
813
|
-
expect(result).toMatchInlineSnapshot(`
|
|
781
|
+
expect(!!result).toMatchInlineSnapshot(`true`)
|
|
782
|
+
expect(store.getState().accessKeys[0]!.keyAuthorization).toMatchInlineSnapshot(`undefined`)
|
|
783
|
+
expect(store.getState().accessKeys[0]!.keyAuthorizationPending).toMatchInlineSnapshot(
|
|
784
|
+
`undefined`,
|
|
785
|
+
)
|
|
814
786
|
})
|
|
815
787
|
|
|
816
|
-
test('behavior:
|
|
817
|
-
const
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
address: '0x0000000000000000000000000000000000000099',
|
|
822
|
-
chainId: 1,
|
|
823
|
-
keyPair,
|
|
824
|
-
keyType: 'webCrypto',
|
|
825
|
-
scopes: [{}],
|
|
826
|
-
} as never,
|
|
827
|
-
])
|
|
788
|
+
test('behavior: reuses pending authorization when direct key check is missing', async () => {
|
|
789
|
+
const { accessKey, keyAuthorization, store } = await setup({ pending: true })
|
|
790
|
+
const { client, requests } = createFillClient(accessKey.accessKeyAddress, {
|
|
791
|
+
keyId: accounts[1]!.address,
|
|
792
|
+
})
|
|
828
793
|
|
|
829
|
-
const result =
|
|
794
|
+
const result = await AccessKeyTransaction.create({
|
|
830
795
|
address: rootAddress,
|
|
831
796
|
chainId: 1,
|
|
797
|
+
client: client as never,
|
|
832
798
|
store,
|
|
833
|
-
calls: [{ to: '0x0000000000000000000000000000000000000abc', data: '0xa9059cbb' }],
|
|
834
799
|
})
|
|
800
|
+
await result?.fill({ chainId: 1, from: rootAddress })
|
|
835
801
|
|
|
836
|
-
|
|
802
|
+
const request = requests[0] as {
|
|
803
|
+
params: readonly [{ keyAuthorization?: { keyId?: string | undefined } | undefined }]
|
|
804
|
+
}
|
|
805
|
+
expect(request.params[0].keyAuthorization?.keyId).toBe(keyAuthorization.address)
|
|
806
|
+
expect(store.getState().accessKeys[0]!.keyAuthorization).toBe(keyAuthorization)
|
|
807
|
+
expect(store.getState().accessKeys[0]!.keyAuthorizationPending).toMatchInlineSnapshot(`true`)
|
|
837
808
|
})
|
|
838
809
|
|
|
839
|
-
test('behavior:
|
|
840
|
-
const
|
|
841
|
-
const
|
|
842
|
-
const store = setup([
|
|
843
|
-
{
|
|
844
|
-
access: rootAddress,
|
|
845
|
-
address: '0x0000000000000000000000000000000000000099',
|
|
846
|
-
chainId: 1,
|
|
847
|
-
keyPair,
|
|
848
|
-
keyType: 'webCrypto',
|
|
849
|
-
scopes: [{ address: token }],
|
|
850
|
-
},
|
|
851
|
-
])
|
|
810
|
+
test('behavior: reuses pending authorization when direct key check fails', async () => {
|
|
811
|
+
const { keyAuthorization, store } = await setup({ pending: true })
|
|
812
|
+
const requests: unknown[] = []
|
|
852
813
|
|
|
853
|
-
const result =
|
|
814
|
+
const result = await AccessKeyTransaction.create({
|
|
854
815
|
address: rootAddress,
|
|
855
816
|
chainId: 1,
|
|
817
|
+
client: {
|
|
818
|
+
call: async () => {
|
|
819
|
+
throw new Error('network failed')
|
|
820
|
+
},
|
|
821
|
+
request: async (request: unknown) => {
|
|
822
|
+
requests.push(request)
|
|
823
|
+
return { capabilities: { sponsored: false }, tx: {} }
|
|
824
|
+
},
|
|
825
|
+
} as never,
|
|
856
826
|
store,
|
|
857
|
-
calls: [{ to: token, data: '0xdeadbeef' }],
|
|
858
827
|
})
|
|
828
|
+
await result?.fill({ chainId: 1, from: rootAddress })
|
|
859
829
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
{
|
|
867
|
-
access: rootAddress,
|
|
868
|
-
address: '0x0000000000000000000000000000000000000099',
|
|
869
|
-
chainId: 1,
|
|
870
|
-
keyPair,
|
|
871
|
-
keyType: 'webCrypto',
|
|
872
|
-
scopes: [{ address: '0x0000000000000000000000000000000000000abc' }],
|
|
873
|
-
},
|
|
874
|
-
])
|
|
875
|
-
|
|
876
|
-
const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store })
|
|
877
|
-
|
|
878
|
-
expect(result).toMatchInlineSnapshot(`undefined`)
|
|
830
|
+
const request = requests[0] as {
|
|
831
|
+
params: readonly [{ keyAuthorization?: { keyId?: string | undefined } | undefined }]
|
|
832
|
+
}
|
|
833
|
+
expect(request.params[0].keyAuthorization?.keyId).toBe(keyAuthorization.address)
|
|
834
|
+
expect(store.getState().accessKeys[0]!.keyAuthorization).toBe(keyAuthorization)
|
|
835
|
+
expect(store.getState().accessKeys[0]!.keyAuthorizationPending).toMatchInlineSnapshot(`true`)
|
|
879
836
|
})
|
|
880
837
|
})
|
|
881
838
|
|
|
@@ -886,11 +843,18 @@ describe('getStatus', () => {
|
|
|
886
843
|
const accessKey = TempoAccount.fromWebCryptoP256(keyPair)
|
|
887
844
|
const keyAuthorization = createKeyAuthorization(accessKey.address)
|
|
888
845
|
|
|
889
|
-
|
|
846
|
+
addAuthorization({
|
|
847
|
+
address: rootAddress,
|
|
848
|
+
keyAuthorization,
|
|
849
|
+
keyPair,
|
|
850
|
+
state: 'signed',
|
|
851
|
+
store,
|
|
852
|
+
})
|
|
890
853
|
|
|
891
854
|
const result = await AccessKey.getStatus({
|
|
892
|
-
|
|
855
|
+
account: rootAddress,
|
|
893
856
|
chainId: 1,
|
|
857
|
+
client: createMetadataClient(accessKey.address) as never,
|
|
894
858
|
store,
|
|
895
859
|
})
|
|
896
860
|
|
|
@@ -903,96 +867,98 @@ describe('getStatus', () => {
|
|
|
903
867
|
const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress })
|
|
904
868
|
const keyAuthorization = createKeyAuthorization(accessKey.accessKeyAddress)
|
|
905
869
|
|
|
906
|
-
|
|
907
|
-
|
|
870
|
+
addAuthorization({
|
|
871
|
+
address: rootAddress,
|
|
872
|
+
keyAuthorization,
|
|
873
|
+
keyPair,
|
|
874
|
+
state: 'signed',
|
|
875
|
+
store,
|
|
876
|
+
})
|
|
877
|
+
markPublished({ accessKey: accessKey.accessKeyAddress, store })
|
|
908
878
|
|
|
909
879
|
const result = await AccessKey.getStatus({
|
|
910
|
-
|
|
880
|
+
account: rootAddress,
|
|
911
881
|
chainId: 1,
|
|
882
|
+
client: createMetadataClient(accessKey.accessKeyAddress) as never,
|
|
912
883
|
store,
|
|
913
884
|
})
|
|
914
885
|
|
|
915
886
|
expect(result).toMatchInlineSnapshot(`"published"`)
|
|
916
887
|
})
|
|
917
888
|
|
|
918
|
-
test('behavior:
|
|
889
|
+
test('behavior: checks pending authorization before returning pending', async () => {
|
|
919
890
|
const store = createStore()
|
|
920
891
|
const keyPair = await WebCryptoP256.createKeyPair()
|
|
921
|
-
const accessKey = TempoAccount.fromWebCryptoP256(keyPair)
|
|
922
|
-
const keyAuthorization = createKeyAuthorization(accessKey.
|
|
892
|
+
const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress })
|
|
893
|
+
const keyAuthorization = createKeyAuthorization(accessKey.accessKeyAddress)
|
|
923
894
|
|
|
924
|
-
|
|
895
|
+
addAuthorization({
|
|
896
|
+
address: rootAddress,
|
|
897
|
+
keyAuthorization,
|
|
898
|
+
keyPair,
|
|
899
|
+
state: 'pending',
|
|
900
|
+
store,
|
|
901
|
+
})
|
|
925
902
|
|
|
926
903
|
const result = await AccessKey.getStatus({
|
|
927
|
-
|
|
904
|
+
account: rootAddress,
|
|
928
905
|
chainId: 1,
|
|
929
|
-
|
|
906
|
+
client: createMetadataClient(accessKey.accessKeyAddress) as never,
|
|
930
907
|
store,
|
|
931
908
|
})
|
|
932
909
|
|
|
933
|
-
expect(result).toMatchInlineSnapshot(`"
|
|
910
|
+
expect(result).toMatchInlineSnapshot(`"published"`)
|
|
934
911
|
})
|
|
935
912
|
|
|
936
|
-
test('behavior: returns
|
|
913
|
+
test('behavior: returns expired for expired local key', async () => {
|
|
937
914
|
const store = createStore()
|
|
938
915
|
const keyPair = await WebCryptoP256.createKeyPair()
|
|
939
916
|
const accessKey = TempoAccount.fromWebCryptoP256(keyPair)
|
|
940
|
-
const keyAuthorization = createKeyAuthorization(accessKey.address, {
|
|
941
|
-
scopes: [{ address: '0x0000000000000000000000000000000000000abc' }],
|
|
942
|
-
})
|
|
917
|
+
const keyAuthorization = createKeyAuthorization(accessKey.address, { expiry: 100 })
|
|
943
918
|
|
|
944
|
-
|
|
919
|
+
addAuthorization({
|
|
920
|
+
address: rootAddress,
|
|
921
|
+
keyAuthorization,
|
|
922
|
+
keyPair,
|
|
923
|
+
state: 'signed',
|
|
924
|
+
store,
|
|
925
|
+
})
|
|
945
926
|
|
|
946
927
|
const result = await AccessKey.getStatus({
|
|
947
|
-
|
|
948
|
-
calls: [{ to: '0x0000000000000000000000000000000000000def', data: '0xdeadbeef' }],
|
|
928
|
+
account: rootAddress,
|
|
949
929
|
chainId: 1,
|
|
930
|
+
client: createMetadataClient(accessKey.address) as never,
|
|
931
|
+
now: 101,
|
|
950
932
|
store,
|
|
951
933
|
})
|
|
952
934
|
|
|
953
|
-
expect(result).toMatchInlineSnapshot(`"
|
|
935
|
+
expect(result).toMatchInlineSnapshot(`"expired"`)
|
|
954
936
|
})
|
|
955
|
-
})
|
|
956
937
|
|
|
957
|
-
|
|
958
|
-
test('default: removes access keys by root address', async () => {
|
|
938
|
+
test('behavior: returns missing when no local key matches the policy', async () => {
|
|
959
939
|
const store = createStore()
|
|
960
940
|
const keyPair = await WebCryptoP256.createKeyPair()
|
|
961
941
|
const accessKey = TempoAccount.fromWebCryptoP256(keyPair)
|
|
962
|
-
const keyAuthorization = createKeyAuthorization(accessKey.address
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
expect(store.getState().accessKeys.length).toMatchInlineSnapshot(`1`)
|
|
966
|
-
|
|
967
|
-
AccessKey.revoke({ address: rootAddress, store })
|
|
968
|
-
|
|
969
|
-
expect(store.getState().accessKeys).toMatchInlineSnapshot(`[]`)
|
|
970
|
-
})
|
|
971
|
-
|
|
972
|
-
test('behavior: only removes keys for matching root address', async () => {
|
|
973
|
-
const store = createStore()
|
|
974
|
-
const otherRoot = accounts[1]!.address
|
|
975
|
-
const keyPair1 = await WebCryptoP256.createKeyPair()
|
|
976
|
-
const keyPair2 = await WebCryptoP256.createKeyPair()
|
|
977
|
-
const ak1 = TempoAccount.fromWebCryptoP256(keyPair1)
|
|
978
|
-
const ak2 = TempoAccount.fromWebCryptoP256(keyPair2)
|
|
942
|
+
const keyAuthorization = createKeyAuthorization(accessKey.address, {
|
|
943
|
+
scopes: [{ address: '0x0000000000000000000000000000000000000abc' }],
|
|
944
|
+
})
|
|
979
945
|
|
|
980
|
-
|
|
946
|
+
addAuthorization({
|
|
981
947
|
address: rootAddress,
|
|
982
|
-
keyAuthorization
|
|
948
|
+
keyAuthorization,
|
|
949
|
+
keyPair,
|
|
950
|
+
state: 'signed',
|
|
983
951
|
store,
|
|
984
952
|
})
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
953
|
+
|
|
954
|
+
const result = await AccessKey.getStatus({
|
|
955
|
+
account: rootAddress,
|
|
956
|
+
calls: [{ to: '0x0000000000000000000000000000000000000def', data: '0xdeadbeef' }],
|
|
957
|
+
chainId: 1,
|
|
958
|
+
client: createMissingClient() as never,
|
|
988
959
|
store,
|
|
989
960
|
})
|
|
990
961
|
|
|
991
|
-
expect(
|
|
992
|
-
|
|
993
|
-
AccessKey.revoke({ address: rootAddress, store })
|
|
994
|
-
|
|
995
|
-
expect(store.getState().accessKeys.length).toMatchInlineSnapshot(`1`)
|
|
996
|
-
expect(store.getState().accessKeys[0]!.access).toBe(otherRoot)
|
|
962
|
+
expect(result).toMatchInlineSnapshot(`"missing"`)
|
|
997
963
|
})
|
|
998
964
|
})
|