UNPKG

accounts

Version:

Tempo Accounts SDK

330 lines (283 loc) 9.77 kB
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), }) }