accounts
Version:
Tempo Accounts SDK
330 lines (283 loc) • 9.77 kB
text/typescript
import { Fetch, Mppx as ClientMppx, tempo as clientTempo } from 'mppx/client'
import { Mppx as ServerMppx, tempo } from 'mppx/server'
import { parseUnits } from 'viem'
import { Addresses } from 'viem/tempo'
import { Actions } from 'viem/tempo'
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vp/test'
import { headlessWebAuthn } from '../../test/adapters.js'
import { accounts, chain, getClient } from '../../test/config.js'
import { type Server, createServer } from '../../test/utils.js'
import * as Expiry from './Expiry.js'
import * as Provider from './Provider.js'
const client = getClient()
const payment = ServerMppx.create({
methods: [
tempo({
account: accounts[1]!,
currency: Addresses.pathUsd,
getClient: () => client,
}),
],
realm: 'mppx-test',
secretKey: 'test-secret-key-test-secret-key-32',
})
const sponsoredPayment = ServerMppx.create({
methods: [
tempo({
account: accounts[1]!,
currency: Addresses.pathUsd,
feePayer: accounts[0]!,
getClient: () => client,
}),
],
realm: 'mppx-sponsored-test',
secretKey: 'sponsored-test-secret-key-32-bytes',
})
let server: Server
beforeAll(async () => {
server = await createServer(async (req, res) => {
const result = await ServerMppx.toNodeListener(
payment.charge({
amount: '1',
}),
)(req, res)
if (result.status === 402) return
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ fortune: 'Your code will compile on the first try.' }))
})
})
afterAll(() => server?.closeAsync())
afterEach(() => Fetch.restore())
describe('mppx integration', () => {
test('polyfilled fetch handles 402 charge automatically', async () => {
const provider = Provider.create({
adapter: headlessWebAuthn(),
chains: [chain],
mpp: true,
})
const address = await connect(provider)
await fund(address)
const res = await fetch(`${server.url}/fortune`)
expect(res.status).toBe(200)
const body = await res.json()
expect(body).toMatchInlineSnapshot(`
{
"fortune": "Your code will compile on the first try.",
}
`)
})
test('mppx front door works without global polyfill', async () => {
const provider = Provider.create({
adapter: headlessWebAuthn(),
chains: [chain],
mpp: false,
})
const address = await connect(provider)
await fund(address)
const mppx = ClientMppx.create({
methods: [
clientTempo({
account: provider.getAccount(),
...provider.getMppxParameters(),
}),
],
polyfill: false,
})
const res = await mppx.fetch(`${server.url}/fortune`)
expect(res.status).toMatchInlineSnapshot(`200`)
})
test('pull mode preserves server-sponsored transactions', async () => {
const sponsoredServer = await createServer(async (req, res) => {
const result = await ServerMppx.toNodeListener(sponsoredPayment.charge({ amount: '1' }))(
req,
res,
)
if (result.status === 402) return
res.writeHead(200)
res.end()
})
try {
const provider = Provider.create({
adapter: headlessWebAuthn(),
chains: [chain],
mpp: false,
})
const address = await connect(provider)
await fund(address, '1')
const mppx = ClientMppx.create({
methods: [
clientTempo({
account: provider.getAccount(),
...provider.getMppxParameters(),
mode: 'pull',
}),
],
polyfill: false,
})
const res = await mppx.fetch(`${sponsoredServer.url}/fortune`)
expect(res.status).toMatchInlineSnapshot(`200`)
} finally {
await sponsoredServer.closeAsync()
}
})
test('mppx global front door uses provider access keys', async () => {
const provider = Provider.create({
adapter: headlessWebAuthn(),
chains: [chain],
mpp: false,
})
const address = await connect(provider)
await fund(address)
await provider.request({
method: 'wallet_authorizeAccessKey',
params: [{ expiry: Expiry.days(1) }],
})
ClientMppx.create({
methods: [
clientTempo({
account: provider.getAccount(),
...provider.getMppxParameters(),
mode: 'pull',
}),
],
})
const res = await fetch(`${server.url}/fortune`)
expect(res.status).toMatchInlineSnapshot(`200`)
const key = provider.store.getState().accessKeys[0]!
expect(await provider.getAccessKeyStatus({ accessKey: key.address })).toMatchInlineSnapshot(
`"published"`,
)
})
test('mppx access-key authorization targets the payer account', async () => {
const provider = Provider.create({
adapter: headlessWebAuthn(),
chains: [chain],
mpp: false,
})
const payer = await connect(provider)
const payerAccount = provider.store.getState().accounts[0]!
await fund(payer)
await provider.request({
method: 'wallet_authorizeAccessKey',
params: [{ expiry: Expiry.days(1) }],
})
await provider.request({
method: 'wallet_connect',
params: [{ capabilities: { method: 'register' } }],
})
const activeAccount = provider.store.getState().accounts[0]!
provider.store.setState({ accounts: [payerAccount, activeAccount], activeAccount: 1 })
const active = activeAccount.address
expect(active.toLowerCase()).not.toBe(payer.toLowerCase())
ClientMppx.create({
methods: [
clientTempo({
account: provider.getAccount({ address: payer }),
...provider.getMppxParameters(),
mode: 'pull',
}),
],
})
const res = await fetch(`${server.url}/fortune`)
expect(res.status).toMatchInlineSnapshot(`200`)
const [key] = provider.store.getState().accessKeys
expect(key?.access.toLowerCase()).toBe(payer.toLowerCase())
})
test('pull mode publishes a pending access key authorization', async () => {
const provider = Provider.create({
adapter: headlessWebAuthn(),
chains: [chain],
mpp: { mode: 'pull' },
})
const address = await connect(provider)
await fund(address)
await provider.request({
method: 'wallet_authorizeAccessKey',
params: [{ expiry: Expiry.days(1) }],
})
const key = provider.store.getState().accessKeys[0]!
expect(key.keyAuthorization).toBeDefined()
const res = await fetch(`${server.url}/fortune`)
expect(res.status).toBe(200)
const status = await provider.getAccessKeyStatus({ accessKey: key.address })
expect(status).toMatchInlineSnapshot(`"published"`)
expect(provider.store.getState().accessKeys[0]!.keyAuthorization).toBeUndefined()
const metadata = await Actions.accessKey.getMetadata(client, {
account: address,
accessKey: key.address,
})
expect(metadata.isRevoked).toMatchInlineSnapshot(`false`)
})
test('pull mode keeps access key authorization pending after failed verification', async () => {
const failingServer = await createServer(async (req, res) => {
if (req.headers.authorization) {
res.writeHead(402, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ title: 'Verification Failed' }))
return
}
await ServerMppx.toNodeListener(
payment.charge({
amount: '1',
}),
)(req, res)
})
try {
const provider = Provider.create({
adapter: headlessWebAuthn(),
chains: [chain],
mpp: { mode: 'pull' },
})
const address = await connect(provider)
await fund(address)
await provider.request({
method: 'wallet_authorizeAccessKey',
params: [{ expiry: Expiry.days(1) }],
})
const key = provider.store.getState().accessKeys[0]!
const res = await fetch(`${failingServer.url}/fortune`)
expect(res.status).toMatchInlineSnapshot(`402`)
expect(provider.store.getState().accessKeys[0]!.keyAuthorization).toBeDefined()
const status = await provider.getAccessKeyStatus({ accessKey: key.address })
expect(status).toMatchInlineSnapshot(`"pending"`)
} finally {
await failingServer.closeAsync()
}
})
test('push mode publishes pending access key authorization', async () => {
const provider = Provider.create({
adapter: headlessWebAuthn(),
chains: [chain],
mpp: { mode: 'push' },
})
const address = await connect(provider)
await fund(address)
await provider.request({
method: 'wallet_authorizeAccessKey',
params: [{ expiry: Expiry.days(1) }],
})
const key = provider.store.getState().accessKeys[0]!
const res = await fetch(`${server.url}/fortune`)
expect(res.status).toMatchInlineSnapshot(`200`)
const status = await provider.getAccessKeyStatus({ accessKey: key.address })
expect(status).toMatchInlineSnapshot(`"published"`)
expect(provider.store.getState().accessKeys[0]!.keyAuthorization).toBeUndefined()
})
})
async function connect(provider: ReturnType<typeof Provider.create>) {
const login = await provider.request({ method: 'wallet_connect' })
if (login.accounts.length > 0) return login.accounts[0]!.address
const register = await provider.request({
method: 'wallet_connect',
params: [{ capabilities: { method: 'register' } }],
})
return register.accounts[0]!.address
}
async function fund(address: `0x${string}`, amount = '10') {
await Actions.token.transferSync(client, {
account: accounts[0]!,
feeToken: Addresses.pathUsd,
to: address,
token: Addresses.pathUsd,
amount: parseUnits(amount, 6),
})
}