accounts
Version:
Tempo Accounts SDK
999 lines (846 loc) • 32 kB
text/typescript
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)
})
})