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
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { WebCryptoP256 } from 'ox'
|
|
1
|
+
import { Hex, WebCryptoP256 } from 'ox'
|
|
2
2
|
import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo'
|
|
3
3
|
import { encodeErrorResult } from 'viem'
|
|
4
|
-
import { Abis, Account as TempoAccount } from 'viem/tempo'
|
|
4
|
+
import { Abis, Account as TempoAccount, Actions } from 'viem/tempo'
|
|
5
5
|
import { describe, expect, test } from 'vp/test'
|
|
6
6
|
|
|
7
7
|
import { accounts, privateKeys } from '../../test/config.js'
|
|
@@ -16,7 +16,11 @@ const rootAddress = accounts[0]!.address
|
|
|
16
16
|
|
|
17
17
|
function createKeyAuthorization(
|
|
18
18
|
address: `0x${string}`,
|
|
19
|
-
options: {
|
|
19
|
+
options: {
|
|
20
|
+
expiry?: number | undefined
|
|
21
|
+
limits?: { token: `0x${string}`; limit: bigint }[] | undefined
|
|
22
|
+
scopes?: KeyAuthorization.Scope[] | undefined
|
|
23
|
+
} = {},
|
|
20
24
|
) {
|
|
21
25
|
return KeyAuthorization.from(
|
|
22
26
|
{
|
|
@@ -24,6 +28,7 @@ function createKeyAuthorization(
|
|
|
24
28
|
chainId: 1n,
|
|
25
29
|
expiry: options.expiry,
|
|
26
30
|
limits: options.limits,
|
|
31
|
+
scopes: options.scopes,
|
|
27
32
|
type: 'p256',
|
|
28
33
|
},
|
|
29
34
|
{ signature: SignatureEnvelope.from(`0x${'00'.repeat(65)}`) },
|
|
@@ -285,9 +290,9 @@ describe('generate', () => {
|
|
|
285
290
|
})
|
|
286
291
|
})
|
|
287
292
|
|
|
288
|
-
describe('
|
|
293
|
+
describe('prepareAuthorization', () => {
|
|
289
294
|
test('default: prepares generated p256 key authorization', async () => {
|
|
290
|
-
const result = await AccessKey.
|
|
295
|
+
const result = await AccessKey.prepareAuthorization({ chainId: 1, expiry: 123 })
|
|
291
296
|
|
|
292
297
|
expect(result.keyAuthorization.address).toMatch(/^0x[0-9a-f]{40}$/i)
|
|
293
298
|
expect(result.keyAuthorization.chainId).toMatchInlineSnapshot(`1n`)
|
|
@@ -297,7 +302,7 @@ describe('prepare', () => {
|
|
|
297
302
|
})
|
|
298
303
|
|
|
299
304
|
test('behavior: prepares external key authorization from address', async () => {
|
|
300
|
-
const result = await AccessKey.
|
|
305
|
+
const result = await AccessKey.prepareAuthorization({
|
|
301
306
|
address: accounts[1]!.address,
|
|
302
307
|
chainId: 123n,
|
|
303
308
|
expiry: 456,
|
|
@@ -349,7 +354,7 @@ describe('prepare', () => {
|
|
|
349
354
|
const keyPair = await WebCryptoP256.createKeyPair()
|
|
350
355
|
const account = TempoAccount.fromWebCryptoP256(keyPair)
|
|
351
356
|
|
|
352
|
-
const result = await AccessKey.
|
|
357
|
+
const result = await AccessKey.prepareAuthorization({
|
|
353
358
|
chainId: 123n,
|
|
354
359
|
expiry: 456,
|
|
355
360
|
keyType: 'p256',
|
|
@@ -370,7 +375,7 @@ describe('prepare', () => {
|
|
|
370
375
|
})
|
|
371
376
|
|
|
372
377
|
test('behavior: defaults external key type to secp256k1', async () => {
|
|
373
|
-
const result = await AccessKey.
|
|
378
|
+
const result = await AccessKey.prepareAuthorization({
|
|
374
379
|
address: accounts[1]!.address,
|
|
375
380
|
chainId: 1,
|
|
376
381
|
expiry: 123,
|
|
@@ -380,6 +385,87 @@ describe('prepare', () => {
|
|
|
380
385
|
})
|
|
381
386
|
})
|
|
382
387
|
|
|
388
|
+
describe('saveAuthorization', () => {
|
|
389
|
+
test('default: saves prepared authorization with provided signature', async () => {
|
|
390
|
+
const store = createStore()
|
|
391
|
+
const prepared = await AccessKey.prepareAuthorization({
|
|
392
|
+
address: accounts[1]!.address,
|
|
393
|
+
chainId: 1,
|
|
394
|
+
expiry: 123,
|
|
395
|
+
})
|
|
396
|
+
const signature = `0x${'11'.repeat(32)}${'22'.repeat(32)}1b` as const
|
|
397
|
+
|
|
398
|
+
const result = AccessKey.saveAuthorization({
|
|
399
|
+
address: rootAddress,
|
|
400
|
+
prepared,
|
|
401
|
+
signature,
|
|
402
|
+
store,
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
expect(result).toMatchInlineSnapshot(`
|
|
406
|
+
{
|
|
407
|
+
"chainId": "0x1",
|
|
408
|
+
"expiry": "0x7b",
|
|
409
|
+
"keyId": "0x8C8d35429F74ec245F8Ef2f4Fd1e551cFF97d650",
|
|
410
|
+
"keyType": "secp256k1",
|
|
411
|
+
"limits": undefined,
|
|
412
|
+
"signature": {
|
|
413
|
+
"r": "0x1111111111111111111111111111111111111111111111111111111111111111",
|
|
414
|
+
"s": "0x2222222222222222222222222222222222222222222222222222222222222222",
|
|
415
|
+
"type": "secp256k1",
|
|
416
|
+
"yParity": "0x0",
|
|
417
|
+
},
|
|
418
|
+
}
|
|
419
|
+
`)
|
|
420
|
+
expect(store.getState().accessKeys.map(({ keyAuthorization: _, ...accessKey }) => accessKey))
|
|
421
|
+
.toMatchInlineSnapshot(`
|
|
422
|
+
[
|
|
423
|
+
{
|
|
424
|
+
"access": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
|
|
425
|
+
"address": "0x8C8d35429F74ec245F8Ef2f4Fd1e551cFF97d650",
|
|
426
|
+
"chainId": 1,
|
|
427
|
+
"expiry": 123,
|
|
428
|
+
"keyType": "secp256k1",
|
|
429
|
+
"limits": undefined,
|
|
430
|
+
"scopes": undefined,
|
|
431
|
+
},
|
|
432
|
+
]
|
|
433
|
+
`)
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
describe('authorize', () => {
|
|
438
|
+
test('default: prepares, signs, and saves authorization', async () => {
|
|
439
|
+
const store = createStore()
|
|
440
|
+
const digests: Hex.Hex[] = []
|
|
441
|
+
const signature = `0x${'11'.repeat(32)}${'22'.repeat(32)}1b` as const
|
|
442
|
+
const account = {
|
|
443
|
+
...accounts[0]!,
|
|
444
|
+
sign: async (parameters: { hash: Hex.Hex }) => {
|
|
445
|
+
digests.push(parameters.hash)
|
|
446
|
+
return signature
|
|
447
|
+
},
|
|
448
|
+
} as TempoAccount.Account
|
|
449
|
+
|
|
450
|
+
await AccessKey.authorize({
|
|
451
|
+
account,
|
|
452
|
+
chainId: 1,
|
|
453
|
+
parameters: {
|
|
454
|
+
address: accounts[1]!.address,
|
|
455
|
+
expiry: 123,
|
|
456
|
+
},
|
|
457
|
+
store,
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
expect(digests).toMatchInlineSnapshot(`
|
|
461
|
+
[
|
|
462
|
+
"0xea47721547363fc82a5dca62b4544e4718d861b3df10bfac65d30102594b5c26",
|
|
463
|
+
]
|
|
464
|
+
`)
|
|
465
|
+
expect(store.getState().accessKeys.length).toMatchInlineSnapshot(`1`)
|
|
466
|
+
})
|
|
467
|
+
})
|
|
468
|
+
|
|
383
469
|
describe('hydrate', () => {
|
|
384
470
|
test('default: hydrates webCrypto access key to signable account', async () => {
|
|
385
471
|
const keyPair = await WebCryptoP256.createKeyPair()
|
|
@@ -660,6 +746,96 @@ describe('selectAccount', () => {
|
|
|
660
746
|
expect(result?.source).toMatchInlineSnapshot(`"accessKey"`)
|
|
661
747
|
})
|
|
662
748
|
|
|
749
|
+
test('behavior: scoped access key checks recipient allowlist', async () => {
|
|
750
|
+
const keyPair = await WebCryptoP256.createKeyPair()
|
|
751
|
+
const token = '0x0000000000000000000000000000000000000abc' as const
|
|
752
|
+
const recipient = '0x0000000000000000000000000000000000000def' as const
|
|
753
|
+
const store = setup([
|
|
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,
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
const result = AccessKey.selectAccount({
|
|
772
|
+
address: rootAddress,
|
|
773
|
+
chainId: 1,
|
|
774
|
+
store,
|
|
775
|
+
calls: [call],
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
expect(result?.source).toMatchInlineSnapshot(`"accessKey"`)
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
test('behavior: scoped access key skips non-allowlisted recipients', async () => {
|
|
782
|
+
const keyPair = await WebCryptoP256.createKeyPair()
|
|
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
|
+
})
|
|
805
|
+
|
|
806
|
+
const result = AccessKey.selectAccount({
|
|
807
|
+
address: rootAddress,
|
|
808
|
+
chainId: 1,
|
|
809
|
+
store,
|
|
810
|
+
calls: [call],
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
expect(result).toMatchInlineSnapshot(`undefined`)
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
test('behavior: malformed scopes skip the access key', async () => {
|
|
817
|
+
const keyPair = await WebCryptoP256.createKeyPair()
|
|
818
|
+
const store = setup([
|
|
819
|
+
{
|
|
820
|
+
access: rootAddress,
|
|
821
|
+
address: '0x0000000000000000000000000000000000000099',
|
|
822
|
+
chainId: 1,
|
|
823
|
+
keyPair,
|
|
824
|
+
keyType: 'webCrypto',
|
|
825
|
+
scopes: [{}],
|
|
826
|
+
} as never,
|
|
827
|
+
])
|
|
828
|
+
|
|
829
|
+
const result = AccessKey.selectAccount({
|
|
830
|
+
address: rootAddress,
|
|
831
|
+
chainId: 1,
|
|
832
|
+
store,
|
|
833
|
+
calls: [{ to: '0x0000000000000000000000000000000000000abc', data: '0xa9059cbb' }],
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
expect(result).toMatchInlineSnapshot(`undefined`)
|
|
837
|
+
})
|
|
838
|
+
|
|
663
839
|
test('behavior: scoped access key without selector allows any call to that address', async () => {
|
|
664
840
|
const keyPair = await WebCryptoP256.createKeyPair()
|
|
665
841
|
const token = '0x0000000000000000000000000000000000000abc' as const
|
|
@@ -703,6 +879,81 @@ describe('selectAccount', () => {
|
|
|
703
879
|
})
|
|
704
880
|
})
|
|
705
881
|
|
|
882
|
+
describe('getStatus', () => {
|
|
883
|
+
test('behavior: returns pending for locally stored key authorization', async () => {
|
|
884
|
+
const store = createStore()
|
|
885
|
+
const keyPair = await WebCryptoP256.createKeyPair()
|
|
886
|
+
const accessKey = TempoAccount.fromWebCryptoP256(keyPair)
|
|
887
|
+
const keyAuthorization = createKeyAuthorization(accessKey.address)
|
|
888
|
+
|
|
889
|
+
AccessKey.save({ address: rootAddress, keyAuthorization, keyPair, store })
|
|
890
|
+
|
|
891
|
+
const result = await AccessKey.getStatus({
|
|
892
|
+
address: rootAddress,
|
|
893
|
+
chainId: 1,
|
|
894
|
+
store,
|
|
895
|
+
})
|
|
896
|
+
|
|
897
|
+
expect(result).toMatchInlineSnapshot(`"pending"`)
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
test('behavior: returns published for local key without pending authorization', async () => {
|
|
901
|
+
const store = createStore()
|
|
902
|
+
const keyPair = await WebCryptoP256.createKeyPair()
|
|
903
|
+
const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress })
|
|
904
|
+
const keyAuthorization = createKeyAuthorization(accessKey.accessKeyAddress)
|
|
905
|
+
|
|
906
|
+
AccessKey.save({ address: rootAddress, keyAuthorization, keyPair, store })
|
|
907
|
+
AccessKey.removePending(accessKey, { store })
|
|
908
|
+
|
|
909
|
+
const result = await AccessKey.getStatus({
|
|
910
|
+
address: rootAddress,
|
|
911
|
+
chainId: 1,
|
|
912
|
+
store,
|
|
913
|
+
})
|
|
914
|
+
|
|
915
|
+
expect(result).toMatchInlineSnapshot(`"published"`)
|
|
916
|
+
})
|
|
917
|
+
|
|
918
|
+
test('behavior: returns expired for expired local key', async () => {
|
|
919
|
+
const store = createStore()
|
|
920
|
+
const keyPair = await WebCryptoP256.createKeyPair()
|
|
921
|
+
const accessKey = TempoAccount.fromWebCryptoP256(keyPair)
|
|
922
|
+
const keyAuthorization = createKeyAuthorization(accessKey.address, { expiry: 100 })
|
|
923
|
+
|
|
924
|
+
AccessKey.save({ address: rootAddress, keyAuthorization, keyPair, store })
|
|
925
|
+
|
|
926
|
+
const result = await AccessKey.getStatus({
|
|
927
|
+
address: rootAddress,
|
|
928
|
+
chainId: 1,
|
|
929
|
+
now: 101,
|
|
930
|
+
store,
|
|
931
|
+
})
|
|
932
|
+
|
|
933
|
+
expect(result).toMatchInlineSnapshot(`"expired"`)
|
|
934
|
+
})
|
|
935
|
+
|
|
936
|
+
test('behavior: returns missing when no local key matches the policy', async () => {
|
|
937
|
+
const store = createStore()
|
|
938
|
+
const keyPair = await WebCryptoP256.createKeyPair()
|
|
939
|
+
const accessKey = TempoAccount.fromWebCryptoP256(keyPair)
|
|
940
|
+
const keyAuthorization = createKeyAuthorization(accessKey.address, {
|
|
941
|
+
scopes: [{ address: '0x0000000000000000000000000000000000000abc' }],
|
|
942
|
+
})
|
|
943
|
+
|
|
944
|
+
AccessKey.save({ address: rootAddress, keyAuthorization, keyPair, store })
|
|
945
|
+
|
|
946
|
+
const result = await AccessKey.getStatus({
|
|
947
|
+
address: rootAddress,
|
|
948
|
+
calls: [{ to: '0x0000000000000000000000000000000000000def', data: '0xdeadbeef' }],
|
|
949
|
+
chainId: 1,
|
|
950
|
+
store,
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
expect(result).toMatchInlineSnapshot(`"missing"`)
|
|
954
|
+
})
|
|
955
|
+
})
|
|
956
|
+
|
|
706
957
|
describe('revoke', () => {
|
|
707
958
|
test('default: removes access keys by root address', async () => {
|
|
708
959
|
const store = createStore()
|
package/src/core/AccessKey.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AbiFunction, Address, Hex, Provider, PublicKey, WebCryptoP256 } from 'ox'
|
|
2
|
-
import { KeyAuthorization } from 'ox/tempo'
|
|
3
|
-
import {
|
|
2
|
+
import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo'
|
|
3
|
+
import type { Client, Transport } from 'viem'
|
|
4
|
+
import { Account as TempoAccount, Actions } from 'viem/tempo'
|
|
4
5
|
|
|
5
6
|
import type { OneOf } from '../internal/types.js'
|
|
6
7
|
import * as ExecutionError from './ExecutionError.js'
|
|
@@ -16,6 +17,21 @@ const removalErrorNames = new Set([
|
|
|
16
17
|
'SignatureTypeMismatch',
|
|
17
18
|
])
|
|
18
19
|
|
|
20
|
+
/** Access-key publication states. */
|
|
21
|
+
export const status = {
|
|
22
|
+
/** No matching usable access key was found. */
|
|
23
|
+
missing: 'missing',
|
|
24
|
+
/** A matching key exists locally and still needs its first transaction to publish the authorization. */
|
|
25
|
+
pending: 'pending',
|
|
26
|
+
/** A matching key exists on-chain and can be used. */
|
|
27
|
+
published: 'published',
|
|
28
|
+
/** A matching key exists but is past its expiry. */
|
|
29
|
+
expired: 'expired',
|
|
30
|
+
} as const
|
|
31
|
+
|
|
32
|
+
/** Publication state for an access key. */
|
|
33
|
+
export type Status = (typeof status)[keyof typeof status]
|
|
34
|
+
|
|
19
35
|
/** Access key entry stored alongside accounts. */
|
|
20
36
|
export type AccessKey = {
|
|
21
37
|
/** Access key address. */
|
|
@@ -91,7 +107,9 @@ export declare namespace generate {
|
|
|
91
107
|
}
|
|
92
108
|
|
|
93
109
|
/** Prepares an unsigned key authorization and local key material when needed. */
|
|
94
|
-
export async function
|
|
110
|
+
export async function prepareAuthorization(
|
|
111
|
+
options: prepareAuthorization.Options,
|
|
112
|
+
): Promise<prepareAuthorization.ReturnType> {
|
|
95
113
|
const { address, chainId, expiry, keyType, limits, publicKey, scopes } = options
|
|
96
114
|
|
|
97
115
|
if (address || publicKey) {
|
|
@@ -118,8 +136,8 @@ export async function prepare(options: prepare.Options): Promise<prepare.ReturnT
|
|
|
118
136
|
return { keyAuthorization, keyPair }
|
|
119
137
|
}
|
|
120
138
|
|
|
121
|
-
export declare namespace
|
|
122
|
-
/** Options for {@link
|
|
139
|
+
export declare namespace prepareAuthorization {
|
|
140
|
+
/** Options for {@link prepareAuthorization}. */
|
|
123
141
|
type Options = {
|
|
124
142
|
/** External access key address. Alternative to `publicKey`. */
|
|
125
143
|
address?: Address.Address | undefined
|
|
@@ -146,6 +164,91 @@ export declare namespace prepare {
|
|
|
146
164
|
}
|
|
147
165
|
}
|
|
148
166
|
|
|
167
|
+
/** Saves a prepared access key authorization with an existing signature. */
|
|
168
|
+
export function saveAuthorization(
|
|
169
|
+
options: saveAuthorization.Options,
|
|
170
|
+
): saveAuthorization.ReturnType {
|
|
171
|
+
const { address, prepared, signature, store } = options
|
|
172
|
+
const keyAuthorization = KeyAuthorization.from(prepared.keyAuthorization, {
|
|
173
|
+
signature: SignatureEnvelope.from(signature),
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
save({
|
|
177
|
+
address,
|
|
178
|
+
keyAuthorization,
|
|
179
|
+
...(prepared.keyPair ? { keyPair: prepared.keyPair } : {}),
|
|
180
|
+
store,
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
return KeyAuthorization.toRpc(keyAuthorization)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export declare namespace saveAuthorization {
|
|
187
|
+
/** Options for {@link saveAuthorization}. */
|
|
188
|
+
type Options = {
|
|
189
|
+
/** Root account address that owns this access key. */
|
|
190
|
+
address: Address.Address
|
|
191
|
+
/** Prepared unsigned key authorization returned by {@link prepareAuthorization}. */
|
|
192
|
+
prepared: prepareAuthorization.ReturnType
|
|
193
|
+
/** Signature over the key authorization digest. */
|
|
194
|
+
signature: Hex.Hex
|
|
195
|
+
/** Reactive state store. */
|
|
196
|
+
store: Store.Store
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Signed key authorization in RPC form. */
|
|
200
|
+
type ReturnType = KeyAuthorization.Rpc
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Prepares, signs, and saves an access key authorization. */
|
|
204
|
+
export async function authorize(options: authorize.Options): Promise<authorize.ReturnType> {
|
|
205
|
+
const { account, chainId, parameters, store } = options
|
|
206
|
+
const prepared = await prepareAuthorization({
|
|
207
|
+
...parameters,
|
|
208
|
+
chainId: parameters.chainId ?? chainId,
|
|
209
|
+
})
|
|
210
|
+
return await signAuthorization({ account, prepared, store })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export declare namespace authorize {
|
|
214
|
+
/** Options for {@link authorize}. */
|
|
215
|
+
type Options = {
|
|
216
|
+
/** Root account that owns this access key and signs its authorization. */
|
|
217
|
+
account: TempoAccount.Account
|
|
218
|
+
/** Default chain ID for the authorization when `parameters.chainId` is not set. */
|
|
219
|
+
chainId: bigint | number
|
|
220
|
+
/** Access key authorization parameters. */
|
|
221
|
+
parameters: Omit<prepareAuthorization.Options, 'chainId'> & {
|
|
222
|
+
/** Chain ID the key authorization is scoped to. */
|
|
223
|
+
chainId?: bigint | number | undefined
|
|
224
|
+
}
|
|
225
|
+
/** Reactive state store. */
|
|
226
|
+
store: Store.Store
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Signed key authorization in RPC form. */
|
|
230
|
+
type ReturnType = KeyAuthorization.Rpc
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function signAuthorization(
|
|
234
|
+
options: signAuthorization.Options,
|
|
235
|
+
): Promise<signAuthorization.ReturnType> {
|
|
236
|
+
const { account, prepared, store } = options
|
|
237
|
+
const digest = KeyAuthorization.getSignPayload(prepared.keyAuthorization)
|
|
238
|
+
const signature = await account.sign({ hash: digest })
|
|
239
|
+
return saveAuthorization({ address: account.address, prepared, signature, store })
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
declare namespace signAuthorization {
|
|
243
|
+
type Options = {
|
|
244
|
+
account: TempoAccount.Account
|
|
245
|
+
prepared: prepareAuthorization.ReturnType
|
|
246
|
+
store: Store.Store
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
type ReturnType = KeyAuthorization.Rpc
|
|
250
|
+
}
|
|
251
|
+
|
|
149
252
|
/** Hydrates an access key entry to a viem Account. Only works for locally-generated keys. */
|
|
150
253
|
export function hydrate(accessKey: AccessKey): TempoAccount.Account {
|
|
151
254
|
if ('keyPair' in accessKey && accessKey.keyPair)
|
|
@@ -249,6 +352,48 @@ export declare namespace selectAccount {
|
|
|
249
352
|
}
|
|
250
353
|
}
|
|
251
354
|
|
|
355
|
+
/** Returns publication status for a stored or on-chain access key. */
|
|
356
|
+
export async function getStatus(options: getStatus.Options): Promise<getStatus.ReturnType> {
|
|
357
|
+
const { accessKey, address, calls, chainId, client, store } = options
|
|
358
|
+
const now = options.now ?? Date.now() / 1000
|
|
359
|
+
const local = store
|
|
360
|
+
.getState()
|
|
361
|
+
.accessKeys.find((key) => matches(key, { accessKey, address, calls, chainId }))
|
|
362
|
+
|
|
363
|
+
if (local) {
|
|
364
|
+
if (isExpired(local.expiry, now)) return status.expired
|
|
365
|
+
if (local.keyAuthorization) return status.pending
|
|
366
|
+
if (client) return await getPublishedStatus(client, { accessKey: local.address, address, now })
|
|
367
|
+
return status.published
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (accessKey && client) return await getPublishedStatus(client, { accessKey, address, now })
|
|
371
|
+
return status.missing
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export declare namespace getStatus {
|
|
375
|
+
/** Options for {@link getStatus}. */
|
|
376
|
+
type Options = {
|
|
377
|
+
/** Root account address that owns the access key. */
|
|
378
|
+
address: Address.Address
|
|
379
|
+
/** Specific access key address to query. When omitted, the first locally matching key is used. */
|
|
380
|
+
accessKey?: Address.Address | undefined
|
|
381
|
+
/** Calls to match against access key scopes. */
|
|
382
|
+
calls?: readonly { to?: Address.Address | undefined; data?: Hex.Hex | undefined }[] | undefined
|
|
383
|
+
/** Chain ID the access key must be authorized on. */
|
|
384
|
+
chainId: number
|
|
385
|
+
/** Client used to verify published state on-chain. */
|
|
386
|
+
client?: Client<Transport> | undefined
|
|
387
|
+
/** Current Unix timestamp in seconds. Defaults to `Date.now() / 1000`. */
|
|
388
|
+
now?: number | undefined
|
|
389
|
+
/** Reactive state store. */
|
|
390
|
+
store: Store.Store
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Access-key publication status. */
|
|
394
|
+
type ReturnType = Status
|
|
395
|
+
}
|
|
396
|
+
|
|
252
397
|
/** Removes an access key from the store. */
|
|
253
398
|
export function revoke(options: revoke.Options): void {
|
|
254
399
|
const { address, store } = options
|
|
@@ -273,25 +418,113 @@ function scopesMatch(
|
|
|
273
418
|
calls?: readonly { to?: Address.Address | undefined; data?: Hex.Hex | undefined }[] | undefined
|
|
274
419
|
},
|
|
275
420
|
): boolean {
|
|
276
|
-
|
|
421
|
+
const scopes = key.scopes
|
|
422
|
+
if (typeof scopes === 'undefined') return true
|
|
423
|
+
if (!Array.isArray(scopes)) return false
|
|
277
424
|
if (!options.calls) return false
|
|
278
425
|
return options.calls.every((call) => {
|
|
279
426
|
if (!call.to) return false
|
|
280
427
|
const callTo = call.to.toLowerCase()
|
|
281
428
|
const callSelector = call.data?.slice(0, 10).toLowerCase()
|
|
282
|
-
return
|
|
429
|
+
return scopes.some((scope) => {
|
|
430
|
+
if (!isScope(scope)) return false
|
|
283
431
|
if (scope.address.toLowerCase() !== callTo) return false
|
|
284
|
-
if (!scope.selector) return true
|
|
285
|
-
const scopeSelector = (
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
: AbiFunction.getSelector(scope.selector)
|
|
289
|
-
).toLowerCase()
|
|
290
|
-
return callSelector === scopeSelector
|
|
432
|
+
if (!scope.selector) return scope.recipients ? scope.recipients.length === 0 : true
|
|
433
|
+
const scopeSelector = getSelector(scope.selector)
|
|
434
|
+
if (!scopeSelector || callSelector !== scopeSelector) return false
|
|
435
|
+
return recipientsMatch(scope.recipients, call.data)
|
|
291
436
|
})
|
|
292
437
|
})
|
|
293
438
|
}
|
|
294
439
|
|
|
440
|
+
function matches(
|
|
441
|
+
key: AccessKey,
|
|
442
|
+
options: {
|
|
443
|
+
accessKey?: Address.Address | undefined
|
|
444
|
+
address: Address.Address
|
|
445
|
+
calls?: readonly { to?: Address.Address | undefined; data?: Hex.Hex | undefined }[] | undefined
|
|
446
|
+
chainId: number
|
|
447
|
+
},
|
|
448
|
+
): boolean {
|
|
449
|
+
const { accessKey, address, calls, chainId } = options
|
|
450
|
+
if (key.access.toLowerCase() !== address.toLowerCase()) return false
|
|
451
|
+
if (key.chainId !== chainId) return false
|
|
452
|
+
if (accessKey && key.address.toLowerCase() !== accessKey.toLowerCase()) return false
|
|
453
|
+
return scopesMatch(key, { calls })
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function isExpired(expiry: number | undefined, now: number): boolean {
|
|
457
|
+
return typeof expiry === 'number' && expiry < now
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function getPublishedStatus(
|
|
461
|
+
client: Client<Transport>,
|
|
462
|
+
options: { accessKey: Address.Address; address: Address.Address; now: number },
|
|
463
|
+
): Promise<Status> {
|
|
464
|
+
const { accessKey, address, now } = options
|
|
465
|
+
try {
|
|
466
|
+
const metadata = await Actions.accessKey.getMetadata(client, {
|
|
467
|
+
account: address,
|
|
468
|
+
accessKey,
|
|
469
|
+
})
|
|
470
|
+
if (metadata.isRevoked) return status.missing
|
|
471
|
+
if (metadata.expiry > 0n && metadata.expiry < BigInt(Math.floor(now))) return status.expired
|
|
472
|
+
return status.published
|
|
473
|
+
} catch (error) {
|
|
474
|
+
if (!(error instanceof Error)) throw error
|
|
475
|
+
const parsed = ExecutionError.parse(error)
|
|
476
|
+
if (parsed.errorName === 'KeyNotFound' || parsed.errorName === 'KeyAlreadyRevoked')
|
|
477
|
+
return status.missing
|
|
478
|
+
throw error
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function isScope(scope: unknown): scope is NonNullable<AccessKey['scopes']>[number] {
|
|
483
|
+
if (!scope || typeof scope !== 'object') return false
|
|
484
|
+
const value = scope as {
|
|
485
|
+
address?: unknown
|
|
486
|
+
recipients?: unknown
|
|
487
|
+
selector?: unknown
|
|
488
|
+
}
|
|
489
|
+
if (typeof value.address !== 'string' || !Address.validate(value.address)) return false
|
|
490
|
+
if (typeof value.selector !== 'undefined' && typeof value.selector !== 'string') return false
|
|
491
|
+
if (typeof value.recipients !== 'undefined') {
|
|
492
|
+
if (!Array.isArray(value.recipients)) return false
|
|
493
|
+
if (value.recipients.some((recipient) => typeof recipient !== 'string')) return false
|
|
494
|
+
if (value.recipients.some((recipient) => !Address.validate(recipient))) return false
|
|
495
|
+
}
|
|
496
|
+
return true
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function getSelector(selector: string): string | undefined {
|
|
500
|
+
try {
|
|
501
|
+
return (
|
|
502
|
+
selector.startsWith('0x') && selector.length === 10
|
|
503
|
+
? selector
|
|
504
|
+
: AbiFunction.getSelector(selector)
|
|
505
|
+
).toLowerCase()
|
|
506
|
+
} catch {
|
|
507
|
+
return undefined
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function recipientsMatch(
|
|
512
|
+
recipients: readonly Address.Address[] | undefined,
|
|
513
|
+
data: Hex.Hex | undefined,
|
|
514
|
+
): boolean {
|
|
515
|
+
if (!recipients || recipients.length === 0) return true
|
|
516
|
+
const recipient = getCallRecipient(data)
|
|
517
|
+
if (!recipient) return false
|
|
518
|
+
return recipients.some((address) => address.toLowerCase() === recipient.toLowerCase())
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function getCallRecipient(data: Hex.Hex | undefined): Address.Address | undefined {
|
|
522
|
+
if (!data || data.length < 74) return undefined
|
|
523
|
+
const recipient = `0x${data.slice(34, 74)}` as Address.Address
|
|
524
|
+
if (!Address.validate(recipient)) return undefined
|
|
525
|
+
return recipient
|
|
526
|
+
}
|
|
527
|
+
|
|
295
528
|
function shouldRemoveForError(error: unknown): boolean {
|
|
296
529
|
if (!(error instanceof Error)) return false
|
|
297
530
|
const parsed = ExecutionError.parse(error)
|
package/src/core/Adapter.ts
CHANGED
|
@@ -311,8 +311,6 @@ export declare namespace authorizeAccessKey {
|
|
|
311
311
|
recipients?: readonly Address[] | undefined
|
|
312
312
|
}[]
|
|
313
313
|
| undefined
|
|
314
|
-
/** Pre-computed signature over the key authorization digest (skips a second signing ceremony). */
|
|
315
|
-
signature?: Hex | undefined
|
|
316
314
|
}
|
|
317
315
|
|
|
318
316
|
type ReturnType = {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RpcSchema } from 'ox'
|
|
2
2
|
import { describe, expectTypeOf, test } from 'vp/test'
|
|
3
3
|
|
|
4
|
+
import * as Provider from './Provider.js'
|
|
4
5
|
import type * as Schema from './Schema.js'
|
|
5
6
|
|
|
6
7
|
type Result<method extends RpcSchema.MethodNameGeneric<Schema.Ox>> = RpcSchema.ExtractReturnType<
|
|
@@ -57,3 +58,17 @@ describe('request', () => {
|
|
|
57
58
|
expectTypeOf<Result<'wallet_switchEthereumChain'>>().toEqualTypeOf<undefined>()
|
|
58
59
|
})
|
|
59
60
|
})
|
|
61
|
+
|
|
62
|
+
describe('create options', () => {
|
|
63
|
+
test('mpp accepts session deposit options', () => {
|
|
64
|
+
expectTypeOf<NonNullable<Parameters<typeof Provider.create>[0]>>().toMatchTypeOf<{
|
|
65
|
+
mpp?:
|
|
66
|
+
| boolean
|
|
67
|
+
| {
|
|
68
|
+
deposit?: string | undefined
|
|
69
|
+
maxDeposit?: string | undefined
|
|
70
|
+
}
|
|
71
|
+
| undefined
|
|
72
|
+
}>()
|
|
73
|
+
})
|
|
74
|
+
})
|