accounts
Version:
Tempo Accounts SDK
227 lines (200 loc) • 6.98 kB
text/typescript
import { Hex } from 'ox'
import { describe, expect, test } from 'vp/test'
import type { DeviceCode } from 'wata'
import { accounts, chain } from '../../../../test/config.js'
import { createDeviceCodeHost, submitVerify } from '../../../../test/deviceCode.js'
import { createServer } from '../../../../test/utils.js'
import * as Provider from '../../Provider.js'
import { deviceCode } from './deviceCode.js'
const root = accounts[0]!
const accessKey = accounts[1]!
const expiry = Math.floor(Date.now() / 1000) + 3_600
function connectRequest() {
return {
method: 'wallet_connect',
params: [
{
capabilities: {
authorizeAccessKey: {
expiry,
keyType: accessKey.keyType,
publicKey: accessKey.publicKey,
},
},
},
],
} as const
}
function createProvider(options: Partial<deviceCode.Options> & { url: string }) {
return Provider.create({
adapter: deviceCode({
name: 'Accounts Test CLI',
onPrompt: (prompt) => void submitVerify(prompt),
rdns: 'xyz.tempo.accounts.test',
...options,
}),
chains: [chain],
})
}
describe('deviceCode', () => {
test('default: bootstraps wallet_connect through the device-code flow', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
const prompts: DeviceCode.Prompt[] = []
try {
const provider = createProvider({
onPrompt: (prompt) => {
prompts.push(prompt)
void submitVerify(prompt)
},
url: `${server.url}/auth/device`,
})
const result = await provider.request(connectRequest())
const account = result.accounts[0]!
const keyAuthorization = account.capabilities.keyAuthorization
? {
...account.capabilities.keyAuthorization,
signature: { type: account.capabilities.keyAuthorization.signature.type },
}
: undefined
expect(prompts).toHaveLength(1)
expect(prompts[0]!.userCode).toMatch(/^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$/)
expect(prompts[0]!.verificationUri).toBe(`${server.url}/auth/device/verify`)
expect(prompts[0]!.verificationUriFull).toBe(
`${server.url}/auth/device/verify?user_code=${prompts[0]!.userCode}`,
)
expect({
account: {
address: account.address,
capabilities: keyAuthorization ? { keyAuthorization } : {},
},
}).toMatchInlineSnapshot(`
{
"account": {
"address": "${root.address}",
"capabilities": {
"keyAuthorization": {
"address": "${accessKey.address.toLowerCase()}",
"chainId": "${Hex.fromNumber(chain.id)}",
"expiry": "${Hex.fromNumber(expiry)}",
"keyId": "${accessKey.address.toLowerCase()}",
"keyType": "secp256k1",
"signature": {
"type": "secp256k1",
},
},
},
},
}
`)
} finally {
await server.closeAsync()
}
})
test('behavior: forwards request context to the host', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
try {
const provider = createProvider({ url: `${server.url}/auth/device` })
await provider.request(connectRequest())
expect(host.requests()).toHaveLength(1)
expect(host.requests()[0]!.method).toBe('wallet_connect')
} finally {
await server.closeAsync()
}
})
test('behavior: preserves a null username capability', async () => {
const host = createDeviceCodeHost({ username: null })
const server = await createServer(host.listener)
try {
const provider = createProvider({ url: `${server.url}/auth/device` })
const result = await provider.request(connectRequest())
expect(result.accounts[0]!.capabilities.username).toBeNull()
} finally {
await server.closeAsync()
}
})
test('behavior: denial rejects with a user-rejected error', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
try {
const provider = createProvider({
onPrompt: (prompt) => void submitVerify(prompt, { action: 'deny' }),
url: `${server.url}/auth/device`,
})
await expect(
provider.request(connectRequest()).catch((error: { code: number; message: string }) => ({
code: error.code,
message: error.message,
})),
).resolves.toMatchInlineSnapshot(`
{
"code": 4001,
"message": "User denied the device-code request.",
}
`)
} finally {
await server.closeAsync()
}
})
test('behavior: times out while waiting for authorization', async () => {
const host = createDeviceCodeHost({ pollingInterval: 10 })
const server = await createServer(host.listener)
try {
const provider = createProvider({
onPrompt: () => {},
timeout: 100,
url: `${server.url}/auth/device`,
})
const error = await provider.request(connectRequest()).then(
() => undefined,
(error: Error) => error,
)
expect(error?.message).toMatch(/^Timed out waiting for device code [A-Z-]+\. Continue at /)
} finally {
await server.closeAsync()
}
})
test('behavior: onPrompt failures surface the URL and code', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
try {
const provider = createProvider({
onPrompt: () => {
throw new Error('browser unavailable')
},
url: `${server.url}/auth/device`,
})
const error = await provider.request(connectRequest()).then(
() => undefined,
(error: Error) => error,
)
expect(error?.message).toMatch(
/^Failed to surface device code [A-Z-]+\. Open .*\/auth\/device\/verify\?user_code=[A-Z-]+ manually\.$/,
)
} finally {
await server.closeAsync()
}
})
test('behavior: rejects methods outside the allowlist without a ceremony', async () => {
const host = createDeviceCodeHost()
const server = await createServer(host.listener)
const prompts: DeviceCode.Prompt[] = []
try {
const provider = createProvider({
methods: ['wallet_authorizeAccessKey'],
onPrompt: (prompt) => {
prompts.push(prompt)
void submitVerify(prompt)
},
url: `${server.url}/auth/device`,
})
await expect(provider.request(connectRequest())).rejects.toThrowErrorMatchingInlineSnapshot(
`[Provider.UnsupportedMethodError: \`wallet_connect\` not supported by device-code adapter.]`,
)
expect(prompts).toHaveLength(0)
} finally {
await server.closeAsync()
}
})
})