accounts 0.12.0 → 0.12.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 +18 -0
- package/README.md +6 -6
- package/dist/cli/adapter.d.ts.map +1 -1
- package/dist/cli/adapter.js +7 -5
- package/dist/cli/adapter.js.map +1 -1
- package/dist/core/AccessKey.d.ts +80 -3
- package/dist/core/AccessKey.d.ts.map +1 -1
- package/dist/core/AccessKey.js +154 -10
- package/dist/core/AccessKey.js.map +1 -1
- package/dist/core/Adapter.d.ts +0 -2
- package/dist/core/Adapter.d.ts.map +1 -1
- package/dist/core/Provider.d.ts +24 -11
- package/dist/core/Provider.d.ts.map +1 -1
- package/dist/core/Provider.js +97 -49
- package/dist/core/Provider.js.map +1 -1
- package/dist/core/adapters/dialog.d.ts.map +1 -1
- package/dist/core/adapters/dialog.js +11 -8
- 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 +36 -52
- package/dist/core/adapters/local.js.map +1 -1
- package/dist/core/adapters/turnkey.d.ts.map +1 -1
- package/dist/core/adapters/turnkey.js +28 -44
- package/dist/core/adapters/turnkey.js.map +1 -1
- package/dist/core/zod/rpc.d.ts +12 -12
- package/dist/core/zod/rpc.d.ts.map +1 -1
- package/dist/core/zod/rpc.js +7 -3
- package/dist/core/zod/rpc.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/react-native/adapter.d.ts.map +1 -1
- package/dist/react-native/adapter.js +7 -5
- package/dist/react-native/adapter.js.map +1 -1
- package/dist/server/internal/handlers/relay.d.ts.map +1 -1
- package/dist/server/internal/handlers/relay.js +16 -1
- package/dist/server/internal/handlers/relay.js.map +1 -1
- package/package.json +2 -2
- package/src/cli/adapter.ts +7 -5
- package/src/core/AccessKey.test.ts +259 -8
- package/src/core/AccessKey.ts +247 -14
- package/src/core/Adapter.ts +0 -2
- package/src/core/Provider.test-d.ts +15 -0
- package/src/core/Provider.test.ts +113 -7
- package/src/core/Provider.ts +119 -60
- package/src/core/Remote.test.ts +78 -6
- package/src/core/adapters/dialog.test.ts +66 -1
- package/src/core/adapters/dialog.ts +12 -7
- package/src/core/adapters/local.test.ts +35 -0
- package/src/core/adapters/local.ts +38 -64
- package/src/core/adapters/turnkey.ts +29 -62
- package/src/core/mppx.test.ts +81 -10
- package/src/core/zod/rpc.ts +8 -6
- package/src/index.ts +1 -0
- package/src/react-native/adapter.ts +7 -5
- package/src/server/internal/handlers/relay.test.ts +24 -0
- package/src/server/internal/handlers/relay.ts +17 -1
|
@@ -69,7 +69,7 @@ export function dialog(options: dialog.Options = {}): Adapter.Adapter {
|
|
|
69
69
|
listeners.delete(listener)
|
|
70
70
|
|
|
71
71
|
if (queued.status === 'success') resolve(queued.result)
|
|
72
|
-
else reject(
|
|
72
|
+
else reject(ox_Provider.parseError(queued.error))
|
|
73
73
|
|
|
74
74
|
// Remove the resolved request from the queue.
|
|
75
75
|
store.setState((x) => ({
|
|
@@ -154,7 +154,7 @@ export function dialog(options: dialog.Options = {}): Adapter.Adapter {
|
|
|
154
154
|
account: TempoAccount.Account,
|
|
155
155
|
keyAuthorization?: KeyAuthorization.Signed,
|
|
156
156
|
) => Promise<result>,
|
|
157
|
-
): Promise<result | undefined> {
|
|
157
|
+
): Promise<{ account: TempoAccount.Account; result: result } | undefined> {
|
|
158
158
|
if (!options.from || typeof options.chainId === 'undefined') return undefined
|
|
159
159
|
const account = AccessKey.selectAccount({
|
|
160
160
|
address: options.from,
|
|
@@ -166,8 +166,7 @@ export function dialog(options: dialog.Options = {}): Adapter.Adapter {
|
|
|
166
166
|
const keyAuthorization = AccessKey.getPending(account, { store })
|
|
167
167
|
try {
|
|
168
168
|
const result = await fn(account, keyAuthorization ?? undefined)
|
|
169
|
-
|
|
170
|
-
return result
|
|
169
|
+
return { account, result }
|
|
171
170
|
} catch (err) {
|
|
172
171
|
if (AccessKey.invalidate(account, err, { store }))
|
|
173
172
|
console.warn('[accounts] access key invalidated, falling through to dialog:', err)
|
|
@@ -307,7 +306,7 @@ export function dialog(options: dialog.Options = {}): Adapter.Adapter {
|
|
|
307
306
|
})
|
|
308
307
|
return await account.signTransaction(prepared as never)
|
|
309
308
|
})
|
|
310
|
-
if (result !== undefined) return result
|
|
309
|
+
if (result !== undefined) return result.result
|
|
311
310
|
return await provider.request({
|
|
312
311
|
...request,
|
|
313
312
|
params: [z.encode(Rpc.transactionRequest, parameters)] as const,
|
|
@@ -342,7 +341,10 @@ export function dialog(options: dialog.Options = {}): Adapter.Adapter {
|
|
|
342
341
|
params: [signed],
|
|
343
342
|
})
|
|
344
343
|
})
|
|
345
|
-
if (result !== undefined)
|
|
344
|
+
if (result !== undefined) {
|
|
345
|
+
AccessKey.removePending(result.account, { store })
|
|
346
|
+
return result.result
|
|
347
|
+
}
|
|
346
348
|
return await provider.request({
|
|
347
349
|
...request,
|
|
348
350
|
params: [z.encode(Rpc.transactionRequest, parameters)] as const,
|
|
@@ -373,7 +375,10 @@ export function dialog(options: dialog.Options = {}): Adapter.Adapter {
|
|
|
373
375
|
params: [signed],
|
|
374
376
|
})
|
|
375
377
|
})
|
|
376
|
-
if (result !== undefined)
|
|
378
|
+
if (result !== undefined) {
|
|
379
|
+
AccessKey.removePending(result.account, { store })
|
|
380
|
+
return result.result
|
|
381
|
+
}
|
|
377
382
|
return await provider.request({
|
|
378
383
|
...request,
|
|
379
384
|
params: [z.encode(Rpc.transactionRequest, parameters)] as const,
|
|
@@ -31,6 +31,41 @@ describe('local', () => {
|
|
|
31
31
|
]
|
|
32
32
|
`)
|
|
33
33
|
})
|
|
34
|
+
|
|
35
|
+
test('default: authorizeAccessKey folds the key authorization digest into the ceremony', async () => {
|
|
36
|
+
const captured: { digest: Hex | undefined }[] = []
|
|
37
|
+
const { adapter } = setup({
|
|
38
|
+
loadAccounts: makeLoadAccounts(0, captured),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const result = await adapter.actions.loadAccounts(
|
|
42
|
+
{
|
|
43
|
+
authorizeAccessKey: {
|
|
44
|
+
address: core_accounts[1]!.address,
|
|
45
|
+
expiry: 0,
|
|
46
|
+
keyType: 'secp256k1',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{ method: 'wallet_connect', params: undefined },
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
expect({
|
|
53
|
+
digest: captured[0]?.digest,
|
|
54
|
+
hasSignature: typeof result.signature === 'string',
|
|
55
|
+
keyAuthorizationSignature: result.keyAuthorization?.signature,
|
|
56
|
+
}).toMatchInlineSnapshot(`
|
|
57
|
+
{
|
|
58
|
+
"digest": "0x64d5413088ae92221fde7900d29b540efc040ac134ccf50d3e916a9011f81bd0",
|
|
59
|
+
"hasSignature": true,
|
|
60
|
+
"keyAuthorizationSignature": {
|
|
61
|
+
"r": "0x876bd6f1719bdffc65382322939303ef37a804df5011b73704e7f4d9e4603cc8",
|
|
62
|
+
"s": "0x6c377e36d7a76b2dd15fdc9599ed663136a8e0faa33c3950e72c3b503bc18bab",
|
|
63
|
+
"type": "secp256k1",
|
|
64
|
+
"yParity": "0x1",
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
`)
|
|
68
|
+
})
|
|
34
69
|
})
|
|
35
70
|
|
|
36
71
|
describe('loadAccounts: personalSign', () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { KeyAuthorization
|
|
1
|
+
import { Provider as ox_Provider } from 'ox'
|
|
2
|
+
import { KeyAuthorization } from 'ox/tempo'
|
|
3
3
|
import { BaseError, hashMessage } from 'viem'
|
|
4
4
|
import { prepareTransactionRequest } from 'viem/actions'
|
|
5
5
|
import { Account as TempoAccount, Actions } from 'viem/tempo'
|
|
@@ -28,48 +28,6 @@ export function local(options: local.Options): Adapter.Adapter {
|
|
|
28
28
|
const { createAccount, icon, loadAccounts, name, rdns } = options
|
|
29
29
|
|
|
30
30
|
return Adapter.define({ icon, name, rdns }, ({ getAccount, getClient, store }) => {
|
|
31
|
-
/**
|
|
32
|
-
* Resolves access key params into an unsigned key authorization.
|
|
33
|
-
*/
|
|
34
|
-
async function prepareKeyAuthorization(options: Adapter.authorizeAccessKey.Parameters) {
|
|
35
|
-
const { address, expiry, keyType, limits, publicKey, scopes } = options
|
|
36
|
-
return await AccessKey.prepare({
|
|
37
|
-
address,
|
|
38
|
-
chainId: options.chainId ?? getClient().chain.id,
|
|
39
|
-
expiry,
|
|
40
|
-
keyType,
|
|
41
|
-
limits,
|
|
42
|
-
publicKey,
|
|
43
|
-
scopes,
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Signs (or wraps a pre-computed signature into) a key authorization
|
|
49
|
-
* and saves the result to the store.
|
|
50
|
-
*/
|
|
51
|
-
async function signKeyAuthorization(
|
|
52
|
-
account: TempoAccount.Account,
|
|
53
|
-
prepared: Awaited<ReturnType<typeof prepareKeyAuthorization>>,
|
|
54
|
-
options: {
|
|
55
|
-
signature?: Hex.Hex | undefined
|
|
56
|
-
} = {},
|
|
57
|
-
) {
|
|
58
|
-
const { keyPair } = prepared
|
|
59
|
-
|
|
60
|
-
const keyAuthorization = await (async () => {
|
|
61
|
-
const digest = KeyAuthorization.getSignPayload(prepared.keyAuthorization)
|
|
62
|
-
const signature = options.signature ?? (await account.sign({ hash: digest }))
|
|
63
|
-
return KeyAuthorization.from(prepared.keyAuthorization, {
|
|
64
|
-
signature: SignatureEnvelope.from(signature),
|
|
65
|
-
})
|
|
66
|
-
})()
|
|
67
|
-
|
|
68
|
-
AccessKey.save({ address: account.address, keyAuthorization, keyPair, store })
|
|
69
|
-
|
|
70
|
-
return KeyAuthorization.toRpc(keyAuthorization)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
31
|
async function withAccessKey<result>(
|
|
74
32
|
options: Pick<Account.find.Options, 'address' | 'calls' | 'chainId'>,
|
|
75
33
|
fn: (
|
|
@@ -80,9 +38,7 @@ export function local(options: local.Options): Adapter.Adapter {
|
|
|
80
38
|
const account = getAccount({ ...options, signable: true })
|
|
81
39
|
const keyAuthorization = AccessKey.getPending(account, { store })
|
|
82
40
|
try {
|
|
83
|
-
|
|
84
|
-
AccessKey.removePending(account, { store })
|
|
85
|
-
return result
|
|
41
|
+
return await fn(account, keyAuthorization ?? undefined)
|
|
86
42
|
} catch (error) {
|
|
87
43
|
if (account.source !== 'accessKey') throw error
|
|
88
44
|
AccessKey.invalidate(account, error, { store })
|
|
@@ -128,8 +84,12 @@ export function local(options: local.Options): Adapter.Adapter {
|
|
|
128
84
|
|
|
129
85
|
const keyAuthorization = await (async () => {
|
|
130
86
|
if (!grantOptions) return undefined
|
|
131
|
-
|
|
132
|
-
|
|
87
|
+
return await AccessKey.authorize({
|
|
88
|
+
account,
|
|
89
|
+
chainId: getClient().chain.id,
|
|
90
|
+
parameters: grantOptions,
|
|
91
|
+
store,
|
|
92
|
+
})
|
|
133
93
|
})()
|
|
134
94
|
|
|
135
95
|
return {
|
|
@@ -142,10 +102,12 @@ export function local(options: local.Options): Adapter.Adapter {
|
|
|
142
102
|
}
|
|
143
103
|
},
|
|
144
104
|
async authorizeAccessKey(parameters) {
|
|
145
|
-
const prepared = await prepareKeyAuthorization(parameters)
|
|
146
105
|
const account = getAccount({ accessKey: false, signable: true })
|
|
147
|
-
const keyAuthorization = await
|
|
148
|
-
|
|
106
|
+
const keyAuthorization = await AccessKey.authorize({
|
|
107
|
+
account,
|
|
108
|
+
chainId: getClient().chain.id,
|
|
109
|
+
parameters,
|
|
110
|
+
store,
|
|
149
111
|
})
|
|
150
112
|
return { keyAuthorization, rootAddress: account.address }
|
|
151
113
|
},
|
|
@@ -165,7 +127,14 @@ export function local(options: local.Options): Adapter.Adapter {
|
|
|
165
127
|
const peronsalSign_digest = personalSign ? hashMessage(personalSign.message) : undefined
|
|
166
128
|
|
|
167
129
|
const keyAuthorization_unsigned = authorizeAccessKey
|
|
168
|
-
? await
|
|
130
|
+
? await AccessKey.prepareAuthorization({
|
|
131
|
+
...authorizeAccessKey,
|
|
132
|
+
chainId: authorizeAccessKey.chainId ?? getClient().chain.id,
|
|
133
|
+
})
|
|
134
|
+
: undefined
|
|
135
|
+
|
|
136
|
+
const keyAuthorization_digest = keyAuthorization_unsigned
|
|
137
|
+
? KeyAuthorization.getSignPayload(keyAuthorization_unsigned.keyAuthorization)
|
|
169
138
|
: undefined
|
|
170
139
|
|
|
171
140
|
// Slot allocation:
|
|
@@ -176,11 +145,7 @@ export function local(options: local.Options): Adapter.Adapter {
|
|
|
176
145
|
// `personalSign` wins the load-accounts ceremony and the key
|
|
177
146
|
// authorization gets its own follow-up `account.sign` ceremony
|
|
178
147
|
// (2 prompts total).
|
|
179
|
-
const digest =
|
|
180
|
-
peronsalSign_digest ??
|
|
181
|
-
(keyAuthorization_unsigned
|
|
182
|
-
? KeyAuthorization.getSignPayload(keyAuthorization_unsigned.keyAuthorization)
|
|
183
|
-
: rest.digest)
|
|
148
|
+
const digest = peronsalSign_digest ?? keyAuthorization_digest ?? rest.digest
|
|
184
149
|
|
|
185
150
|
// Pass the prepared digest (or the caller's) into loadAccounts so
|
|
186
151
|
// the ceremony can sign it in a single biometric prompt.
|
|
@@ -200,10 +165,15 @@ export function local(options: local.Options): Adapter.Adapter {
|
|
|
200
165
|
// - Else (key-auth digest took the slot), reuse `signature_`.
|
|
201
166
|
const keyAuthorization = await (async () => {
|
|
202
167
|
if (!keyAuthorization_unsigned || !account) return undefined
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
168
|
+
const signature_keyAuthorization =
|
|
169
|
+
peronsalSign_digest || !signature_
|
|
170
|
+
? await account.sign({ hash: keyAuthorization_digest! })
|
|
171
|
+
: signature_
|
|
172
|
+
return AccessKey.saveAuthorization({
|
|
173
|
+
address: account.address,
|
|
174
|
+
prepared: keyAuthorization_unsigned,
|
|
175
|
+
signature: signature_keyAuthorization,
|
|
176
|
+
store,
|
|
207
177
|
})
|
|
208
178
|
})()
|
|
209
179
|
|
|
@@ -301,10 +271,12 @@ export function local(options: local.Options): Adapter.Adapter {
|
|
|
301
271
|
}),
|
|
302
272
|
)
|
|
303
273
|
const signed = await account.signTransaction(prepared as never)
|
|
304
|
-
|
|
274
|
+
const result = await client.request({
|
|
305
275
|
method: 'eth_sendRawTransaction' as never,
|
|
306
276
|
params: [signed],
|
|
307
277
|
})
|
|
278
|
+
AccessKey.removePending(account, { store })
|
|
279
|
+
return result
|
|
308
280
|
},
|
|
309
281
|
async sendTransactionSync(parameters) {
|
|
310
282
|
const { feePayer, ...rest } = parameters
|
|
@@ -330,10 +302,12 @@ export function local(options: local.Options): Adapter.Adapter {
|
|
|
330
302
|
}),
|
|
331
303
|
)
|
|
332
304
|
const signed = await account.signTransaction(prepared as never)
|
|
333
|
-
|
|
305
|
+
const result = await client.request({
|
|
334
306
|
method: 'eth_sendRawTransactionSync' as never,
|
|
335
307
|
params: [signed],
|
|
336
308
|
})
|
|
309
|
+
AccessKey.removePending(account, { store })
|
|
310
|
+
return result
|
|
337
311
|
},
|
|
338
312
|
},
|
|
339
313
|
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
RpcResponse,
|
|
8
8
|
Secp256k1,
|
|
9
9
|
} from 'ox'
|
|
10
|
-
import { KeyAuthorization
|
|
10
|
+
import { KeyAuthorization } from 'ox/tempo'
|
|
11
11
|
import { hashMessage, hashTypedData, isAddressEqual } from 'viem'
|
|
12
12
|
import type { Address } from 'viem/accounts'
|
|
13
13
|
import { prepareTransactionRequest } from 'viem/actions'
|
|
@@ -273,48 +273,6 @@ export function turnkey<const client extends turnkey.Client>(
|
|
|
273
273
|
return signatureToHex(result)
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
async function prepareKeyAuthorization(options: Adapter.authorizeAccessKey.Parameters) {
|
|
277
|
-
const { address, expiry, keyType, limits, publicKey, scopes } = options
|
|
278
|
-
return await AccessKey.prepare({
|
|
279
|
-
address,
|
|
280
|
-
chainId: options.chainId ?? getClient().chain.id,
|
|
281
|
-
expiry,
|
|
282
|
-
keyType,
|
|
283
|
-
limits,
|
|
284
|
-
publicKey,
|
|
285
|
-
scopes,
|
|
286
|
-
})
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async function signKeyAuthorization(
|
|
290
|
-
account: turnkey.WalletAccount,
|
|
291
|
-
prepared: Awaited<ReturnType<typeof prepareKeyAuthorization>>,
|
|
292
|
-
options: {
|
|
293
|
-
signature?: Hex.Hex | undefined
|
|
294
|
-
} = {},
|
|
295
|
-
) {
|
|
296
|
-
const digest = KeyAuthorization.getSignPayload(prepared.keyAuthorization)
|
|
297
|
-
const signature =
|
|
298
|
-
options.signature ??
|
|
299
|
-
(await signPayload({
|
|
300
|
-
payload: digest,
|
|
301
|
-
turnkeyClient: await getTurnkeyClient(),
|
|
302
|
-
walletAccount: account,
|
|
303
|
-
}))
|
|
304
|
-
const keyAuthorization = KeyAuthorization.from(prepared.keyAuthorization, {
|
|
305
|
-
signature: SignatureEnvelope.from(signature),
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
AccessKey.save({
|
|
309
|
-
address: core_Address.from(account.address),
|
|
310
|
-
keyAuthorization,
|
|
311
|
-
...(prepared.keyPair ? { keyPair: prepared.keyPair } : {}),
|
|
312
|
-
store,
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
return KeyAuthorization.toRpc(keyAuthorization)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
276
|
async function withAccessKey<result>(
|
|
319
277
|
options: {
|
|
320
278
|
address?: Address | undefined
|
|
@@ -325,7 +283,7 @@ export function turnkey<const client extends turnkey.Client>(
|
|
|
325
283
|
account: TempoAccount.Account,
|
|
326
284
|
keyAuthorization?: KeyAuthorization.Signed,
|
|
327
285
|
) => Promise<result>,
|
|
328
|
-
) {
|
|
286
|
+
): Promise<{ account: TempoAccount.Account; result: result } | undefined> {
|
|
329
287
|
const account = (() => {
|
|
330
288
|
try {
|
|
331
289
|
return getAccount({ ...options, signable: true })
|
|
@@ -338,8 +296,7 @@ export function turnkey<const client extends turnkey.Client>(
|
|
|
338
296
|
const keyAuthorization = AccessKey.getPending(account, { store })
|
|
339
297
|
try {
|
|
340
298
|
const result = await fn(account, keyAuthorization ?? undefined)
|
|
341
|
-
|
|
342
|
-
return result
|
|
299
|
+
return { account, result }
|
|
343
300
|
} catch (error) {
|
|
344
301
|
AccessKey.invalidate(account, error, { store })
|
|
345
302
|
return undefined
|
|
@@ -420,11 +377,12 @@ export function turnkey<const client extends turnkey.Client>(
|
|
|
420
377
|
const account = walletAccounts[0]
|
|
421
378
|
const keyAuthorization = authorizeAccessKey
|
|
422
379
|
? account
|
|
423
|
-
? await
|
|
424
|
-
account,
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
380
|
+
? await AccessKey.authorize({
|
|
381
|
+
account: toTempoAccount(account),
|
|
382
|
+
chainId: getClient().chain.id,
|
|
383
|
+
parameters: authorizeAccessKey,
|
|
384
|
+
store,
|
|
385
|
+
})
|
|
428
386
|
: undefined
|
|
429
387
|
: undefined
|
|
430
388
|
|
|
@@ -463,11 +421,12 @@ export function turnkey<const client extends turnkey.Client>(
|
|
|
463
421
|
const account = walletAccounts[0]
|
|
464
422
|
const keyAuthorization =
|
|
465
423
|
authorizeAccessKey && account
|
|
466
|
-
? await
|
|
467
|
-
account,
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
424
|
+
? await AccessKey.authorize({
|
|
425
|
+
account: toTempoAccount(account),
|
|
426
|
+
chainId: getClient().chain.id,
|
|
427
|
+
parameters: authorizeAccessKey,
|
|
428
|
+
store,
|
|
429
|
+
})
|
|
471
430
|
: undefined
|
|
472
431
|
|
|
473
432
|
return {
|
|
@@ -486,9 +445,11 @@ export function turnkey<const client extends turnkey.Client>(
|
|
|
486
445
|
},
|
|
487
446
|
async authorizeAccessKey(parameters) {
|
|
488
447
|
const account = await accountForSigning(undefined)
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
448
|
+
const keyAuthorization = await AccessKey.authorize({
|
|
449
|
+
account: toTempoAccount(account),
|
|
450
|
+
chainId: getClient().chain.id,
|
|
451
|
+
parameters,
|
|
452
|
+
store,
|
|
492
453
|
})
|
|
493
454
|
return { keyAuthorization, rootAddress: core_Address.from(account.address) }
|
|
494
455
|
},
|
|
@@ -520,7 +481,7 @@ export function turnkey<const client extends turnkey.Client>(
|
|
|
520
481
|
return await account.signTransaction(prepared as never)
|
|
521
482
|
},
|
|
522
483
|
)
|
|
523
|
-
if (result !== undefined) return result
|
|
484
|
+
if (result !== undefined) return result.result
|
|
524
485
|
return await signTransaction(parameters)
|
|
525
486
|
},
|
|
526
487
|
async signTypedData(parameters) {
|
|
@@ -561,7 +522,10 @@ export function turnkey<const client extends turnkey.Client>(
|
|
|
561
522
|
})
|
|
562
523
|
},
|
|
563
524
|
)
|
|
564
|
-
if (result !== undefined)
|
|
525
|
+
if (result !== undefined) {
|
|
526
|
+
AccessKey.removePending(result.account, { store })
|
|
527
|
+
return result.result
|
|
528
|
+
}
|
|
565
529
|
const signed = await signTransaction(parameters)
|
|
566
530
|
const viemClient = getClient({
|
|
567
531
|
chainId: parameters.chainId,
|
|
@@ -595,7 +559,10 @@ export function turnkey<const client extends turnkey.Client>(
|
|
|
595
559
|
})
|
|
596
560
|
},
|
|
597
561
|
)
|
|
598
|
-
if (result !== undefined)
|
|
562
|
+
if (result !== undefined) {
|
|
563
|
+
AccessKey.removePending(result.account, { store })
|
|
564
|
+
return result.result
|
|
565
|
+
}
|
|
599
566
|
const signed = await signTransaction(parameters)
|
|
600
567
|
const viemClient = getClient({
|
|
601
568
|
chainId: parameters.chainId,
|
package/src/core/mppx.test.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { Fetch } from 'mppx/client'
|
|
1
2
|
import { Mppx as ServerMppx, tempo } from 'mppx/server'
|
|
2
3
|
import { parseUnits } from 'viem'
|
|
3
4
|
import { Addresses } from 'viem/tempo'
|
|
4
5
|
import { Actions } from 'viem/tempo'
|
|
5
|
-
import { afterAll, beforeAll, describe, expect, test } from 'vp/test'
|
|
6
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vp/test'
|
|
6
7
|
|
|
7
8
|
import { headlessWebAuthn } from '../../test/adapters.js'
|
|
8
9
|
import { accounts, chain, getClient } from '../../test/config.js'
|
|
9
10
|
import { type Server, createServer } from '../../test/utils.js'
|
|
11
|
+
import * as Expiry from './Expiry.js'
|
|
10
12
|
import * as Provider from './Provider.js'
|
|
11
13
|
|
|
12
14
|
const client = getClient()
|
|
@@ -40,6 +42,8 @@ beforeAll(async () => {
|
|
|
40
42
|
|
|
41
43
|
afterAll(() => server?.closeAsync())
|
|
42
44
|
|
|
45
|
+
afterEach(() => Fetch.restore())
|
|
46
|
+
|
|
43
47
|
describe('mppx integration', () => {
|
|
44
48
|
test('polyfilled fetch handles 402 charge automatically', async () => {
|
|
45
49
|
const provider = Provider.create({
|
|
@@ -49,15 +53,7 @@ describe('mppx integration', () => {
|
|
|
49
53
|
})
|
|
50
54
|
|
|
51
55
|
const address = await connect(provider)
|
|
52
|
-
|
|
53
|
-
const client = getClient()
|
|
54
|
-
await Actions.token.transferSync(client, {
|
|
55
|
-
account: accounts[0]!,
|
|
56
|
-
feeToken: Addresses.pathUsd,
|
|
57
|
-
to: address,
|
|
58
|
-
token: Addresses.pathUsd,
|
|
59
|
-
amount: parseUnits('10', 6),
|
|
60
|
-
})
|
|
56
|
+
await fund(address)
|
|
61
57
|
|
|
62
58
|
const res = await fetch(`${server.url}/fortune`)
|
|
63
59
|
expect(res.status).toBe(200)
|
|
@@ -69,6 +65,71 @@ describe('mppx integration', () => {
|
|
|
69
65
|
}
|
|
70
66
|
`)
|
|
71
67
|
})
|
|
68
|
+
|
|
69
|
+
test('pull mode publishes a pending access key on first charge', async () => {
|
|
70
|
+
const provider = Provider.create({
|
|
71
|
+
adapter: headlessWebAuthn(),
|
|
72
|
+
chains: [chain],
|
|
73
|
+
mpp: { mode: 'pull' },
|
|
74
|
+
})
|
|
75
|
+
const address = await connect(provider)
|
|
76
|
+
await fund(address)
|
|
77
|
+
|
|
78
|
+
await provider.request({
|
|
79
|
+
method: 'wallet_authorizeAccessKey',
|
|
80
|
+
params: [{ expiry: Expiry.days(1) }],
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const key = provider.store.getState().accessKeys[0]!
|
|
84
|
+
expect(key.keyAuthorization).toBeDefined()
|
|
85
|
+
|
|
86
|
+
const res = await fetch(`${server.url}/fortune`)
|
|
87
|
+
expect(res.status).toBe(200)
|
|
88
|
+
expect(provider.store.getState().accessKeys[0]!.keyAuthorization).toBeUndefined()
|
|
89
|
+
|
|
90
|
+
const metadata = await Actions.accessKey.getMetadata(client, {
|
|
91
|
+
account: address,
|
|
92
|
+
accessKey: key.address,
|
|
93
|
+
})
|
|
94
|
+
expect(metadata.isRevoked).toMatchInlineSnapshot(`false`)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('pull mode keeps pending access key after failed verification', async () => {
|
|
98
|
+
const failingServer = await createServer(async (req, res) => {
|
|
99
|
+
if (req.headers.authorization) {
|
|
100
|
+
res.writeHead(402, { 'Content-Type': 'application/json' })
|
|
101
|
+
res.end(JSON.stringify({ title: 'Verification Failed' }))
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await ServerMppx.toNodeListener(
|
|
106
|
+
payment.charge({
|
|
107
|
+
amount: '1',
|
|
108
|
+
}),
|
|
109
|
+
)(req, res)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const provider = Provider.create({
|
|
114
|
+
adapter: headlessWebAuthn(),
|
|
115
|
+
chains: [chain],
|
|
116
|
+
mpp: { mode: 'pull' },
|
|
117
|
+
})
|
|
118
|
+
const address = await connect(provider)
|
|
119
|
+
await fund(address)
|
|
120
|
+
|
|
121
|
+
await provider.request({
|
|
122
|
+
method: 'wallet_authorizeAccessKey',
|
|
123
|
+
params: [{ expiry: Expiry.days(1) }],
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const res = await fetch(`${failingServer.url}/fortune`)
|
|
127
|
+
expect(res.status).toMatchInlineSnapshot(`402`)
|
|
128
|
+
expect(provider.store.getState().accessKeys[0]!.keyAuthorization).toBeDefined()
|
|
129
|
+
} finally {
|
|
130
|
+
await failingServer.closeAsync()
|
|
131
|
+
}
|
|
132
|
+
})
|
|
72
133
|
})
|
|
73
134
|
|
|
74
135
|
async function connect(provider: ReturnType<typeof Provider.create>) {
|
|
@@ -80,3 +141,13 @@ async function connect(provider: ReturnType<typeof Provider.create>) {
|
|
|
80
141
|
})
|
|
81
142
|
return register.accounts[0]!.address
|
|
82
143
|
}
|
|
144
|
+
|
|
145
|
+
async function fund(address: `0x${string}`) {
|
|
146
|
+
await Actions.token.transferSync(client, {
|
|
147
|
+
account: accounts[0]!,
|
|
148
|
+
feeToken: Addresses.pathUsd,
|
|
149
|
+
to: address,
|
|
150
|
+
token: Addresses.pathUsd,
|
|
151
|
+
amount: parseUnits('10', 6),
|
|
152
|
+
})
|
|
153
|
+
}
|
package/src/core/zod/rpc.ts
CHANGED
|
@@ -393,19 +393,21 @@ export namespace wallet_authorizeAccessKey_strict {
|
|
|
393
393
|
expiry: z.number(),
|
|
394
394
|
keyType: z.optional(keyType),
|
|
395
395
|
limits: z.readonly(
|
|
396
|
-
z
|
|
396
|
+
z
|
|
397
|
+
.array(z.object({ token: u.address(), limit: u.bigint(), period: z.optional(z.number()) }))
|
|
398
|
+
.check(z.minLength(1)),
|
|
397
399
|
),
|
|
398
400
|
publicKey: z.optional(u.hex()),
|
|
399
|
-
scopes: z.
|
|
400
|
-
z
|
|
401
|
-
|
|
401
|
+
scopes: z.readonly(
|
|
402
|
+
z
|
|
403
|
+
.array(
|
|
402
404
|
z.object({
|
|
403
405
|
address: u.address(),
|
|
404
406
|
selector: z.optional(z.union([u.hex(), z.string()])),
|
|
405
407
|
recipients: z.optional(z.readonly(z.array(u.address()))),
|
|
406
408
|
}),
|
|
407
|
-
)
|
|
408
|
-
|
|
409
|
+
)
|
|
410
|
+
.check(z.minLength(1)),
|
|
409
411
|
),
|
|
410
412
|
})
|
|
411
413
|
}
|
package/src/index.ts
CHANGED
|
@@ -205,9 +205,7 @@ export function reactNative(options: reactNative.Options): Adapter.Adapter {
|
|
|
205
205
|
keyAuthorization = await reauthorizeManagedKey(rootAddress, managedKey)
|
|
206
206
|
|
|
207
207
|
try {
|
|
208
|
-
|
|
209
|
-
AccessKey.removePending(account, { store })
|
|
210
|
-
return result
|
|
208
|
+
return await fn(account, keyAuthorization ?? undefined)
|
|
211
209
|
} catch (error) {
|
|
212
210
|
AccessKey.remove(account, { store })
|
|
213
211
|
throw error
|
|
@@ -348,10 +346,12 @@ export function reactNative(options: reactNative.Options): Adapter.Adapter {
|
|
|
348
346
|
}),
|
|
349
347
|
)
|
|
350
348
|
const signed = await account.signTransaction(prepared as never)
|
|
351
|
-
|
|
349
|
+
const result = await client.request({
|
|
352
350
|
method: 'eth_sendRawTransaction' as never,
|
|
353
351
|
params: [signed],
|
|
354
352
|
})
|
|
353
|
+
AccessKey.removePending(account, { store })
|
|
354
|
+
return result
|
|
355
355
|
},
|
|
356
356
|
async sendTransactionSync(parameters) {
|
|
357
357
|
const { feePayer, ...rest } = parameters
|
|
@@ -369,10 +369,12 @@ export function reactNative(options: reactNative.Options): Adapter.Adapter {
|
|
|
369
369
|
}),
|
|
370
370
|
)
|
|
371
371
|
const signed = await account.signTransaction(prepared as never)
|
|
372
|
-
|
|
372
|
+
const result = await client.request({
|
|
373
373
|
method: 'eth_sendRawTransactionSync' as never,
|
|
374
374
|
params: [signed],
|
|
375
375
|
})
|
|
376
|
+
AccessKey.removePending(account, { store })
|
|
377
|
+
return result
|
|
376
378
|
},
|
|
377
379
|
async signPersonalMessage({ address, data }) {
|
|
378
380
|
await loadManagedKey(address)
|
|
@@ -1426,6 +1426,30 @@ describe('behavior: path A — guaranteed sponsorship (no validate)', () => {
|
|
|
1426
1426
|
expect(result.transaction.feePayerSignature).toBeDefined()
|
|
1427
1427
|
expect(result.capabilities?.sponsored).toBe(true)
|
|
1428
1428
|
})
|
|
1429
|
+
|
|
1430
|
+
test('behavior: defaults feeToken to chain default when caller omits it', async () => {
|
|
1431
|
+
// Without the default, the broadcast envelope has no feeToken and
|
|
1432
|
+
// the chain falls back to the sender's account token, which often
|
|
1433
|
+
// lacks FeeAMM liquidity.
|
|
1434
|
+
const result = await fillTransaction(client, {
|
|
1435
|
+
account: userAccount.address,
|
|
1436
|
+
calls: [transferCall()],
|
|
1437
|
+
})
|
|
1438
|
+
|
|
1439
|
+
expect(result.transaction.feeToken?.toLowerCase()).toBe(
|
|
1440
|
+
localnetTokens[0]!.address.toLowerCase(),
|
|
1441
|
+
)
|
|
1442
|
+
})
|
|
1443
|
+
|
|
1444
|
+
test('behavior: preserves caller-supplied feeToken', async () => {
|
|
1445
|
+
const result = await fillTransaction(client, {
|
|
1446
|
+
account: userAccount.address,
|
|
1447
|
+
calls: [transferCall()],
|
|
1448
|
+
feeToken: addresses.alphaUsd as Address,
|
|
1449
|
+
})
|
|
1450
|
+
|
|
1451
|
+
expect(result.transaction.feeToken?.toLowerCase()).toBe(addresses.alphaUsd.toLowerCase())
|
|
1452
|
+
})
|
|
1429
1453
|
})
|
|
1430
1454
|
|
|
1431
1455
|
describe('behavior: path B — conditional sponsorship (validate)', () => {
|