UNPKG

accounts

Version:

Tempo Accounts SDK

999 lines (846 loc) 32 kB
import { Hex, WebCryptoP256 } from 'ox' import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo' import { encodeErrorResult } from 'viem' import { Abis, Account as TempoAccount, Actions } from 'viem/tempo' import { describe, expect, test } from 'vp/test' import { accounts, privateKeys } from '../../test/config.js' import * as AccessKey from './AccessKey.js' import * as Store from './Store.js' function createStore() { return Store.create({ chainId: 1 }) } const rootAddress = accounts[0]!.address function createKeyAuthorization( address: `0x${string}`, options: { expiry?: number | undefined limits?: { token: `0x${string}`; limit: bigint }[] | undefined scopes?: KeyAuthorization.Scope[] | undefined } = {}, ) { return KeyAuthorization.from( { address, chainId: 1n, expiry: options.expiry, limits: options.limits, scopes: options.scopes, type: 'p256', }, { signature: SignatureEnvelope.from(`0x${'00'.repeat(65)}`) }, ) } function createRevert(errorName: string) { return Object.assign(new Error('reverted'), { data: encodeErrorResult({ abi: Abis.abis, errorName, args: [] } as never), }) } describe('save', () => { test('default: saves access key to store', async () => { const store = createStore() const keyPair = await WebCryptoP256.createKeyPair() const accessKey = TempoAccount.fromWebCryptoP256(keyPair) const keyAuthorization = createKeyAuthorization(accessKey.address) AccessKey.save({ address: rootAddress, keyAuthorization, store }) const { accessKeys } = store.getState() expect(accessKeys.length).toMatchInlineSnapshot(`1`) expect(accessKeys[0]!.address).toBe(accessKey.address) expect(accessKeys[0]!.access).toBe(rootAddress) expect(accessKeys[0]!.chainId).toMatchInlineSnapshot(`1`) expect(accessKeys[0]!.keyType).toMatchInlineSnapshot(`"p256"`) expect(accessKeys[0]!.keyAuthorization).toBe(keyAuthorization) }) test('behavior: saves without keyPair', async () => { const store = createStore() const keyPair = await WebCryptoP256.createKeyPair() const accessKey = TempoAccount.fromWebCryptoP256(keyPair) const keyAuthorization = createKeyAuthorization(accessKey.address) AccessKey.save({ address: rootAddress, keyAuthorization, store }) expect(store.getState().accessKeys[0]!.keyPair).toBeUndefined() }) test('behavior: saves with keyPair', async () => { const store = createStore() const keyPair = await WebCryptoP256.createKeyPair() const accessKey = TempoAccount.fromWebCryptoP256(keyPair) const keyAuthorization = createKeyAuthorization(accessKey.address) AccessKey.save({ address: rootAddress, keyAuthorization, keyPair, store }) expect(store.getState().accessKeys[0]!.keyPair).toBe(keyPair) }) test('behavior: appends to existing access keys', async () => { const store = createStore() const keyPair1 = await WebCryptoP256.createKeyPair() const keyPair2 = await WebCryptoP256.createKeyPair() const ak1 = TempoAccount.fromWebCryptoP256(keyPair1) const ak2 = TempoAccount.fromWebCryptoP256(keyPair2) AccessKey.save({ address: rootAddress, keyAuthorization: createKeyAuthorization(ak1.address), store, }) AccessKey.save({ address: rootAddress, keyAuthorization: createKeyAuthorization(ak2.address), store, }) expect(store.getState().accessKeys.length).toMatchInlineSnapshot(`2`) }) test('behavior: stores expiry from key authorization', async () => { const store = createStore() const keyPair = await WebCryptoP256.createKeyPair() const accessKey = TempoAccount.fromWebCryptoP256(keyPair) const expiry = Math.floor(Date.now() / 1000) + 3600 const keyAuthorization = createKeyAuthorization(accessKey.address, { expiry }) AccessKey.save({ address: rootAddress, keyAuthorization, store }) expect(store.getState().accessKeys[0]!.expiry).toBe(expiry) }) test('behavior: stores limits from key authorization', async () => { const store = createStore() const keyPair = await WebCryptoP256.createKeyPair() const accessKey = TempoAccount.fromWebCryptoP256(keyPair) const limits = [{ token: '0x20c0000000000000000000000000000000000001' as const, limit: 1000n }] const keyAuthorization = createKeyAuthorization(accessKey.address, { limits }) AccessKey.save({ address: rootAddress, keyAuthorization, store }) expect(store.getState().accessKeys[0]!.limits).toMatchInlineSnapshot(` [ { "limit": 1000n, "token": "0x20c0000000000000000000000000000000000001", }, ] `) }) }) describe('getPending', () => { test('default: returns key authorization for access key account', async () => { const store = createStore() const keyPair = await WebCryptoP256.createKeyPair() const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress }) const keyAuthorization = createKeyAuthorization(accessKey.accessKeyAddress) AccessKey.save({ address: rootAddress, keyAuthorization, store }) const result = AccessKey.getPending(accessKey, { store }) expect(result).toBe(keyAuthorization) }) test('behavior: returns undefined for root account', () => { const store = createStore() const result = AccessKey.getPending(accounts[0]!, { store }) expect(result).toBeUndefined() }) test('behavior: returns undefined when no matching access key', async () => { const store = createStore() const keyPair = await WebCryptoP256.createKeyPair() const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress }) const result = AccessKey.getPending(accessKey, { store }) expect(result).toBeUndefined() }) }) describe('removePending', () => { test('default: clears key authorization from access key', async () => { const store = createStore() const keyPair = await WebCryptoP256.createKeyPair() const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress }) const keyAuthorization = createKeyAuthorization(accessKey.accessKeyAddress) AccessKey.save({ address: rootAddress, keyAuthorization, store }) expect(AccessKey.getPending(accessKey, { store })).toBeDefined() AccessKey.removePending(accessKey, { store }) expect(AccessKey.getPending(accessKey, { store })).toBeUndefined() }) test('behavior: no-op for root account', async () => { const store = createStore() const keyPair = await WebCryptoP256.createKeyPair() const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress }) const keyAuthorization = createKeyAuthorization(accessKey.accessKeyAddress) AccessKey.save({ address: rootAddress, keyAuthorization, store }) AccessKey.removePending(accounts[0]!, { store }) expect(AccessKey.getPending(accessKey, { store })).toBeDefined() }) test('behavior: does not affect other access keys', async () => { const store = createStore() const keyPair1 = await WebCryptoP256.createKeyPair() const keyPair2 = await WebCryptoP256.createKeyPair() const ak1 = TempoAccount.fromWebCryptoP256(keyPair1, { access: rootAddress }) const ak2 = TempoAccount.fromWebCryptoP256(keyPair2, { access: rootAddress }) const ka1 = createKeyAuthorization(ak1.accessKeyAddress) const ka2 = createKeyAuthorization(ak2.accessKeyAddress) AccessKey.save({ address: rootAddress, keyAuthorization: ka1, store }) AccessKey.save({ address: rootAddress, keyAuthorization: ka2, store }) AccessKey.removePending(ak1, { store }) expect(AccessKey.getPending(ak1, { store })).toBeUndefined() expect(AccessKey.getPending(ak2, { store })).toBe(ka2) }) }) describe('invalidate', () => { async function setup() { const store = createStore() const keyPair = await WebCryptoP256.createKeyPair() const account = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress }) AccessKey.save({ address: rootAddress, keyAuthorization: createKeyAuthorization(account.accessKeyAddress), keyPair, store, }) return { account, store } } test('default: removes matching access key for stale-key errors', async () => { const { account, store } = await setup() const keyPair = await WebCryptoP256.createKeyPair() const account_other = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress }) AccessKey.save({ address: rootAddress, keyAuthorization: createKeyAuthorization(account_other.accessKeyAddress), keyPair, store, }) const result = AccessKey.invalidate(account, createRevert('KeyNotFound'), { store }) expect(result).toMatchInlineSnapshot(`true`) expect(store.getState().accessKeys.map((key) => key.address)).toMatchInlineSnapshot(` [ "${account_other.accessKeyAddress}", ] `) }) test('behavior: preserves access key for recoverable execution errors', async () => { const { account, store } = await setup() const result = AccessKey.invalidate(account, createRevert('SpendingLimitExceeded'), { store, }) expect(result).toMatchInlineSnapshot(`false`) expect(store.getState().accessKeys.length).toMatchInlineSnapshot(`1`) }) test('behavior: preserves access key for unknown errors', async () => { const { account, store } = await setup() const result = AccessKey.invalidate(account, new Error('network failed'), { store }) expect(result).toMatchInlineSnapshot(`false`) expect(store.getState().accessKeys.length).toMatchInlineSnapshot(`1`) }) test('behavior: no-op for root accounts', () => { const store = createStore() const result = AccessKey.invalidate(accounts[0]!, createRevert('KeyNotFound'), { store }) expect(result).toMatchInlineSnapshot(`false`) }) }) describe('generate', () => { test('default: returns access key and key pair', async () => { const result = await AccessKey.generate() expect(result.accessKey.address).toMatch(/^0x[0-9a-f]{40}$/i) expect(result.keyPair).toBeDefined() }) test('behavior: with account attaches access to root', async () => { const result = await AccessKey.generate({ account: accounts[0]! }) expect(result.accessKey.source).toMatchInlineSnapshot(`"accessKey"`) expect(result.accessKey.accessKeyAddress).toMatch(/^0x[0-9a-f]{40}$/i) }) }) describe('prepareAuthorization', () => { test('default: prepares generated p256 key authorization', async () => { const result = await AccessKey.prepareAuthorization({ chainId: 1, expiry: 123 }) expect(result.keyAuthorization.address).toMatch(/^0x[0-9a-f]{40}$/i) expect(result.keyAuthorization.chainId).toMatchInlineSnapshot(`1n`) expect(result.keyAuthorization.expiry).toMatchInlineSnapshot(`123`) expect(result.keyAuthorization.type).toMatchInlineSnapshot(`"p256"`) expect(result.keyPair).toBeDefined() }) test('behavior: prepares external key authorization from address', async () => { const result = await AccessKey.prepareAuthorization({ address: accounts[1]!.address, chainId: 123n, expiry: 456, keyType: 'webAuthn', limits: [ { limit: 1000n, period: 60, token: '0x20c0000000000000000000000000000000000001', }, ], scopes: [ { address: '0x0000000000000000000000000000000000000abc', recipients: ['0x0000000000000000000000000000000000000def'], selector: 'transfer(address,uint256)', }, ], }) expect(result.keyPair).toBeUndefined() expect(result.keyAuthorization).toMatchInlineSnapshot(` { "address": "${accounts[1]!.address}", "chainId": 123n, "expiry": 456, "limits": [ { "limit": 1000n, "period": 60, "token": "0x20c0000000000000000000000000000000000001", }, ], "scopes": [ { "address": "0x0000000000000000000000000000000000000abc", "recipients": [ "0x0000000000000000000000000000000000000def", ], "selector": "0xa9059cbb", }, ], "type": "webAuthn", } `) }) test('behavior: prepares external key authorization from public key', async () => { const keyPair = await WebCryptoP256.createKeyPair() const account = TempoAccount.fromWebCryptoP256(keyPair) const result = await AccessKey.prepareAuthorization({ chainId: 123n, expiry: 456, keyType: 'p256', publicKey: account.publicKey, }) expect(result.keyPair).toBeUndefined() expect(result.keyAuthorization).toMatchInlineSnapshot(` { "address": "${account.address.toLowerCase()}", "chainId": 123n, "expiry": 456, "limits": undefined, "scopes": undefined, "type": "p256", } `) }) test('behavior: defaults external key type to secp256k1', async () => { const result = await AccessKey.prepareAuthorization({ address: accounts[1]!.address, chainId: 1, expiry: 123, }) expect(result.keyAuthorization.type).toMatchInlineSnapshot(`"secp256k1"`) }) }) describe('saveAuthorization', () => { test('default: saves prepared authorization with provided signature', async () => { const store = createStore() const prepared = await AccessKey.prepareAuthorization({ address: accounts[1]!.address, chainId: 1, expiry: 123, }) const signature = `0x${'11'.repeat(32)}${'22'.repeat(32)}1b` as const const result = AccessKey.saveAuthorization({ address: rootAddress, prepared, signature, store, }) expect(result).toMatchInlineSnapshot(` { "chainId": "0x1", "expiry": "0x7b", "keyId": "0x8C8d35429F74ec245F8Ef2f4Fd1e551cFF97d650", "keyType": "secp256k1", "limits": undefined, "signature": { "r": "0x1111111111111111111111111111111111111111111111111111111111111111", "s": "0x2222222222222222222222222222222222222222222222222222222222222222", "type": "secp256k1", "yParity": "0x0", }, } `) expect(store.getState().accessKeys.map(({ keyAuthorization: _, ...accessKey }) => accessKey)) .toMatchInlineSnapshot(` [ { "access": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "address": "0x8C8d35429F74ec245F8Ef2f4Fd1e551cFF97d650", "chainId": 1, "expiry": 123, "keyType": "secp256k1", "limits": undefined, "scopes": undefined, }, ] `) }) }) describe('authorize', () => { test('default: prepares, signs, and saves authorization', async () => { const store = createStore() const digests: Hex.Hex[] = [] const signature = `0x${'11'.repeat(32)}${'22'.repeat(32)}1b` as const const account = { ...accounts[0]!, sign: async (parameters: { hash: Hex.Hex }) => { digests.push(parameters.hash) return signature }, } as TempoAccount.Account await AccessKey.authorize({ account, chainId: 1, parameters: { address: accounts[1]!.address, expiry: 123, }, store, }) expect(digests).toMatchInlineSnapshot(` [ "0xea47721547363fc82a5dca62b4544e4718d861b3df10bfac65d30102594b5c26", ] `) expect(store.getState().accessKeys.length).toMatchInlineSnapshot(`1`) }) }) describe('hydrate', () => { test('default: hydrates webCrypto access key to signable account', async () => { const keyPair = await WebCryptoP256.createKeyPair() const result = AccessKey.hydrate({ access: rootAddress, address: '0x0000000000000000000000000000000000000099', chainId: 1, keyPair, keyType: 'webCrypto', }) expect(result.type).toMatchInlineSnapshot(`"local"`) expect(typeof result.sign).toMatchInlineSnapshot(`"function"`) expect(result.source).toMatchInlineSnapshot(`"accessKey"`) }) test('behavior: hydrates private-key access key to signable account', () => { const result = AccessKey.hydrate({ access: rootAddress, address: accounts[1]!.address, chainId: 1, keyType: 'secp256k1', privateKey: privateKeys[1], }) expect(result.type).toMatchInlineSnapshot(`"local"`) expect(typeof result.sign).toMatchInlineSnapshot(`"function"`) expect(result.source).toMatchInlineSnapshot(`"accessKey"`) }) test('error: throws for external access key without signer material', () => { expect(() => AccessKey.hydrate({ access: rootAddress, address: '0x0000000000000000000000000000000000000099', chainId: 1, keyType: 'p256', }), ).toThrowErrorMatchingInlineSnapshot( `[Provider.UnauthorizedError: External access key cannot be hydrated for signing.]`, ) }) }) describe('selectAccount', () => { function setup(accessKeys: readonly Store.AccessKey[] = []) { const store = createStore() store.setState({ accessKeys }) return store } test('default: selects locally-signable access key for root address', async () => { const keyPair = await WebCryptoP256.createKeyPair() const store = setup([ { access: rootAddress, address: '0x0000000000000000000000000000000000000099', chainId: 1, keyPair, keyType: 'webCrypto', }, ]) const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store }) expect(result?.source).toMatchInlineSnapshot(`"accessKey"`) }) test('behavior: skips access keys for another root address', async () => { const keyPair = await WebCryptoP256.createKeyPair() const store = setup([ { access: accounts[1]!.address, address: '0x0000000000000000000000000000000000000099', chainId: 1, keyPair, keyType: 'webCrypto', }, ]) const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store }) expect(result).toMatchInlineSnapshot(`undefined`) }) test('behavior: skips access keys for another chain', async () => { const keyPair = await WebCryptoP256.createKeyPair() const store = setup([ { access: rootAddress, address: '0x0000000000000000000000000000000000000099', chainId: 1, keyPair, keyType: 'webCrypto', }, ]) const result = AccessKey.selectAccount({ address: rootAddress, chainId: 42_431, store }) expect(result).toMatchInlineSnapshot(`undefined`) }) test('behavior: skips external access keys without signer material', () => { const store = setup([ { access: rootAddress, address: '0x0000000000000000000000000000000000000099', chainId: 1, keyType: 'p256', }, ]) const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store }) expect(result).toMatchInlineSnapshot(`undefined`) }) test('behavior: removes expired access key', async () => { const keyPair = await WebCryptoP256.createKeyPair() const store = setup([ { access: rootAddress, address: '0x0000000000000000000000000000000000000099', expiry: Math.floor(Date.now() / 1000) - 3600, chainId: 1, keyPair, keyType: 'webCrypto', }, ]) const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store }) expect(result).toMatchInlineSnapshot(`undefined`) expect(store.getState().accessKeys).toMatchInlineSnapshot(`[]`) }) test('behavior: keeps future-expiring access key', async () => { const keyPair = await WebCryptoP256.createKeyPair() const store = setup([ { access: rootAddress, address: '0x0000000000000000000000000000000000000099', expiry: Math.floor(Date.now() / 1000) + 3600, chainId: 1, keyPair, keyType: 'webCrypto', }, ]) const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store }) expect(result?.source).toMatchInlineSnapshot(`"accessKey"`) expect(store.getState().accessKeys.length).toMatchInlineSnapshot(`1`) }) test('behavior: preserves limits on selected access key', async () => { const keyPair = await WebCryptoP256.createKeyPair() const store = setup([ { access: rootAddress, address: '0x0000000000000000000000000000000000000099', chainId: 1, keyPair, keyType: 'webCrypto', limits: [ { token: '0x0000000000000000000000000000000000000abc', limit: 1000n, }, ], }, ]) const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store }) expect(result?.source).toMatchInlineSnapshot(`"accessKey"`) expect(store.getState().accessKeys[0]?.limits).toMatchInlineSnapshot(` [ { "limit": 1000n, "token": "0x0000000000000000000000000000000000000abc", }, ] `) }) test('behavior: unscoped access key selects with calls', async () => { const keyPair = await WebCryptoP256.createKeyPair() const store = setup([ { access: rootAddress, address: '0x0000000000000000000000000000000000000099', chainId: 1, keyPair, keyType: 'webCrypto', }, ]) const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store, calls: [{ to: '0x0000000000000000000000000000000000000abc', data: '0xa9059cbb' }], }) expect(result?.source).toMatchInlineSnapshot(`"accessKey"`) }) test('behavior: scoped access key selects when calls match', async () => { const keyPair = await WebCryptoP256.createKeyPair() const token = '0x0000000000000000000000000000000000000abc' as const const store = setup([ { access: rootAddress, address: '0x0000000000000000000000000000000000000099', chainId: 1, keyPair, keyType: 'webCrypto', scopes: [{ address: token, selector: '0xa9059cbb' }], }, ]) const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store, calls: [{ to: token, data: '0xa9059cbb0000000000000000000000000000000000000001' }], }) expect(result?.source).toMatchInlineSnapshot(`"accessKey"`) }) test('behavior: scoped access key skips calls that do not match', async () => { const keyPair = await WebCryptoP256.createKeyPair() const token = '0x0000000000000000000000000000000000000abc' as const const store = setup([ { access: rootAddress, address: '0x0000000000000000000000000000000000000099', chainId: 1, keyPair, keyType: 'webCrypto', scopes: [{ address: token, selector: '0xa9059cbb' }], }, ]) const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store, calls: [{ to: '0x0000000000000000000000000000000000000def', data: '0xdeadbeef' }], }) expect(result).toMatchInlineSnapshot(`undefined`) }) test('behavior: scoped access key supports human-readable selectors', async () => { const keyPair = await WebCryptoP256.createKeyPair() const token = '0x0000000000000000000000000000000000000abc' as const const store = setup([ { access: rootAddress, address: '0x0000000000000000000000000000000000000099', chainId: 1, keyPair, keyType: 'webCrypto', scopes: [{ address: token, selector: 'transfer(address,uint256)' }], }, ]) const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store, calls: [{ to: token, data: '0xa9059cbb0000000000000000000000000000000000000001' }], }) expect(result?.source).toMatchInlineSnapshot(`"accessKey"`) }) test('behavior: scoped access key checks recipient allowlist', async () => { const keyPair = await WebCryptoP256.createKeyPair() const token = '0x0000000000000000000000000000000000000abc' as const const recipient = '0x0000000000000000000000000000000000000def' as const const store = setup([ { access: rootAddress, address: '0x0000000000000000000000000000000000000099', chainId: 1, keyPair, keyType: 'webCrypto', scopes: [ { address: token, selector: 'transfer(address,uint256)', recipients: [recipient] }, ], }, ]) const call = Actions.token.transfer.call({ amount: 1n, to: recipient, token, }) const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store, calls: [call], }) expect(result?.source).toMatchInlineSnapshot(`"accessKey"`) }) test('behavior: scoped access key skips non-allowlisted recipients', async () => { const keyPair = await WebCryptoP256.createKeyPair() const token = '0x0000000000000000000000000000000000000abc' as const const store = setup([ { access: rootAddress, address: '0x0000000000000000000000000000000000000099', chainId: 1, keyPair, keyType: 'webCrypto', scopes: [ { address: token, selector: 'transfer(address,uint256)', recipients: ['0x0000000000000000000000000000000000000def'], }, ], }, ]) const call = Actions.token.transfer.call({ amount: 1n, to: '0x0000000000000000000000000000000000000fed', token, }) const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store, calls: [call], }) expect(result).toMatchInlineSnapshot(`undefined`) }) test('behavior: malformed scopes skip the access key', async () => { const keyPair = await WebCryptoP256.createKeyPair() const store = setup([ { access: rootAddress, address: '0x0000000000000000000000000000000000000099', chainId: 1, keyPair, keyType: 'webCrypto', scopes: [{}], } as never, ]) const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store, calls: [{ to: '0x0000000000000000000000000000000000000abc', data: '0xa9059cbb' }], }) expect(result).toMatchInlineSnapshot(`undefined`) }) test('behavior: scoped access key without selector allows any call to that address', async () => { const keyPair = await WebCryptoP256.createKeyPair() const token = '0x0000000000000000000000000000000000000abc' as const const store = setup([ { access: rootAddress, address: '0x0000000000000000000000000000000000000099', chainId: 1, keyPair, keyType: 'webCrypto', scopes: [{ address: token }], }, ]) const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store, calls: [{ to: token, data: '0xdeadbeef' }], }) expect(result?.source).toMatchInlineSnapshot(`"accessKey"`) }) test('behavior: scoped access key skips when no calls are provided', async () => { const keyPair = await WebCryptoP256.createKeyPair() const store = setup([ { access: rootAddress, address: '0x0000000000000000000000000000000000000099', chainId: 1, keyPair, keyType: 'webCrypto', scopes: [{ address: '0x0000000000000000000000000000000000000abc' }], }, ]) const result = AccessKey.selectAccount({ address: rootAddress, chainId: 1, store }) expect(result).toMatchInlineSnapshot(`undefined`) }) }) describe('getStatus', () => { test('behavior: returns pending for locally stored key authorization', async () => { const store = createStore() const keyPair = await WebCryptoP256.createKeyPair() const accessKey = TempoAccount.fromWebCryptoP256(keyPair) const keyAuthorization = createKeyAuthorization(accessKey.address) AccessKey.save({ address: rootAddress, keyAuthorization, keyPair, store }) const result = await AccessKey.getStatus({ address: rootAddress, chainId: 1, store, }) expect(result).toMatchInlineSnapshot(`"pending"`) }) test('behavior: returns published for local key without pending authorization', async () => { const store = createStore() const keyPair = await WebCryptoP256.createKeyPair() const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress }) const keyAuthorization = createKeyAuthorization(accessKey.accessKeyAddress) AccessKey.save({ address: rootAddress, keyAuthorization, keyPair, store }) AccessKey.removePending(accessKey, { store }) const result = await AccessKey.getStatus({ address: rootAddress, chainId: 1, store, }) expect(result).toMatchInlineSnapshot(`"published"`) }) test('behavior: returns expired for expired local key', async () => { const store = createStore() const keyPair = await WebCryptoP256.createKeyPair() const accessKey = TempoAccount.fromWebCryptoP256(keyPair) const keyAuthorization = createKeyAuthorization(accessKey.address, { expiry: 100 }) AccessKey.save({ address: rootAddress, keyAuthorization, keyPair, store }) const result = await AccessKey.getStatus({ address: rootAddress, chainId: 1, now: 101, store, }) expect(result).toMatchInlineSnapshot(`"expired"`) }) test('behavior: returns missing when no local key matches the policy', async () => { const store = createStore() const keyPair = await WebCryptoP256.createKeyPair() const accessKey = TempoAccount.fromWebCryptoP256(keyPair) const keyAuthorization = createKeyAuthorization(accessKey.address, { scopes: [{ address: '0x0000000000000000000000000000000000000abc' }], }) AccessKey.save({ address: rootAddress, keyAuthorization, keyPair, store }) const result = await AccessKey.getStatus({ address: rootAddress, calls: [{ to: '0x0000000000000000000000000000000000000def', data: '0xdeadbeef' }], chainId: 1, store, }) expect(result).toMatchInlineSnapshot(`"missing"`) }) }) describe('revoke', () => { test('default: removes access keys by root address', async () => { const store = createStore() const keyPair = await WebCryptoP256.createKeyPair() const accessKey = TempoAccount.fromWebCryptoP256(keyPair) const keyAuthorization = createKeyAuthorization(accessKey.address) AccessKey.save({ address: rootAddress, keyAuthorization, store }) expect(store.getState().accessKeys.length).toMatchInlineSnapshot(`1`) AccessKey.revoke({ address: rootAddress, store }) expect(store.getState().accessKeys).toMatchInlineSnapshot(`[]`) }) test('behavior: only removes keys for matching root address', async () => { const store = createStore() const otherRoot = accounts[1]!.address const keyPair1 = await WebCryptoP256.createKeyPair() const keyPair2 = await WebCryptoP256.createKeyPair() const ak1 = TempoAccount.fromWebCryptoP256(keyPair1) const ak2 = TempoAccount.fromWebCryptoP256(keyPair2) AccessKey.save({ address: rootAddress, keyAuthorization: createKeyAuthorization(ak1.address), store, }) AccessKey.save({ address: otherRoot, keyAuthorization: createKeyAuthorization(ak2.address), store, }) expect(store.getState().accessKeys.length).toMatchInlineSnapshot(`2`) AccessKey.revoke({ address: rootAddress, store }) expect(store.getState().accessKeys.length).toMatchInlineSnapshot(`1`) expect(store.getState().accessKeys[0]!.access).toBe(otherRoot) }) })