UNPKG

accounts

Version:

Tempo Accounts SDK

437 lines (381 loc) 12.5 kB
import { Hex, WebCryptoP256 } from 'ox' import { type Address, createClient, defineChain, parseUnits } from 'viem' import { tempoLocalnet, tempoModerato } from 'viem/chains' import { Account as TempoAccount, Actions, Addresses } from 'viem/tempo' import { afterEach, beforeAll, describe, expect, test } from 'vp/test' import { accounts, http } from '../../test/config.js' import { interact } from '../../test/utils.browser.js' import { dialog } from './adapters/dialog.js' import * as Expiry from './Expiry.js' import * as Provider from './Provider.js' import * as Storage from './Storage.js' const host = 'https://localhost:5175' const rpcPort = import.meta.env.VITE_RPC_PORT ?? '8546' const rpcUrl = `http://localhost:${rpcPort}/99999` const chain = defineChain({ ...tempoLocalnet, rpcUrls: { default: { http: [rpcUrl] } }, }) const client = createClient({ chain, transport: http(rpcUrl), pollingInterval: 100 }) beforeAll(async () => { await Promise.all( [1n, 2n, 3n].map((tokenId) => Actions.amm.mintSync(client, { account: accounts[0]!, feeToken: Addresses.pathUsd, nonceKey: 'expiring', userTokenAddress: tokenId, validatorTokenAddress: Addresses.pathUsd, validatorTokenAmount: parseUnits('1000', 6), to: accounts[0]!.address, }), ), ) }) const transferCall = Actions.token.transfer.call({ to: '0x0000000000000000000000000000000000000001', token: Addresses.pathUsd, amount: parseUnits('1', 6), }) function getProvider(options: Partial<Provider.create.Options> = {}) { return Provider.create({ adapter: dialog({ host }), chains: [chain], storage: Storage.idb({ key: crypto.randomUUID() }), ...options, }) } let provider: Provider.Provider | undefined afterEach(() => { if (provider) { provider.store.setState(provider.store.getInitialState()) window.localStorage.clear() window.sessionStorage.clear() } document.querySelectorAll('dialog[data-tempo-wallet]').forEach((el) => el.remove()) provider = undefined }) /** Register via iframe and return the account address. */ async function connectViaIframe(p: Provider.Provider) { const result = await interact( p.request({ method: 'wallet_connect', params: [{ capabilities: { method: 'register' } }], }), async (iframe) => { await iframe.getByTestId('confirm').click() }, ) return result.accounts[0]!.address } /** Fund an address with PathUSD from the pre-funded test account. */ async function fund(address: Address) { await Actions.token.transferSync(client, { account: accounts[0]!, feeToken: Addresses.pathUsd, to: address, token: Addresses.pathUsd, amount: parseUnits('10', 6), }) } describe('wallet_connect', () => { test('default: register via iframe confirm', async () => { provider = getProvider() const result = await interact( provider.request({ method: 'wallet_connect', params: [{ capabilities: { method: 'register' } }], }), async (iframe) => { await iframe.getByTestId('confirm').click() }, ) expect(result.accounts.length).toBeGreaterThanOrEqual(1) expect(result.accounts[0]!.address).toMatch(/^0x[0-9a-fA-F]{40}$/) }) test('behavior: reject via iframe', async () => { provider = getProvider() await expect( interact( provider.request({ method: 'wallet_connect', params: [{ capabilities: { method: 'register' } }], }), async (iframe) => { await iframe.getByTestId('reject').click() }, ), ).rejects.toThrow() }) }) describe('wallet_disconnect', () => { test('default: clears state after connect', async () => { provider = getProvider() await connectViaIframe(provider) await provider.request({ method: 'wallet_disconnect' }) expect(provider.store.getState().accounts).toHaveLength(0) }) }) describe('eth_accounts', () => { test('default: returns accounts after connect', async () => { provider = getProvider() await connectViaIframe(provider) const result = await provider.request({ method: 'eth_accounts' }) expect(result.length).toBeGreaterThanOrEqual(1) }) }) describe('eth_chainId', () => { test('default: returns chain ID', async () => { provider = getProvider() const chainId = await provider.request({ method: 'eth_chainId' }) expect(chainId).toBeDefined() }) }) describe('eth_sendTransaction', () => { test('default: sends transaction via iframe confirm and returns hash', async () => { provider = getProvider() const address = await connectViaIframe(provider) await fund(address) const hash = await interact( provider.request({ method: 'eth_sendTransaction', params: [{ calls: [transferCall] }], }), async (iframe) => { await iframe.getByTestId('confirm').click() }, ) expect(hash).toMatch(/^0x[0-9a-f]{64}$/) }) test('behavior: reject via iframe', async () => { provider = getProvider() await connectViaIframe(provider) await expect( interact( provider.request({ method: 'eth_sendTransaction', params: [{ calls: [transferCall] }], }), async (iframe) => { await iframe.getByTestId('reject').click() }, ), ).rejects.toThrow() }) }) describe('eth_sendTransactionSync', () => { test('default: sends transaction via iframe confirm and returns receipt', async () => { provider = getProvider() const address = await connectViaIframe(provider) await fund(address) const receipt = await interact( provider.request({ method: 'eth_sendTransactionSync', params: [{ calls: [transferCall] }], }), async (iframe) => { await iframe.getByTestId('confirm').click() }, ) expect(receipt.status).toMatchInlineSnapshot(`"0x1"`) expect(receipt.transactionHash).toMatch(/^0x[0-9a-f]{64}$/) }) }) describe('eth_signTransaction', () => { test('default: signs transaction via iframe confirm and returns serialized', async () => { provider = getProvider() const address = await connectViaIframe(provider) await fund(address) const signed = await interact( provider.request({ method: 'eth_signTransaction', params: [{ calls: [transferCall] }], }), async (iframe) => { await iframe.getByTestId('confirm').click() }, ) expect(signed).toMatch(/^0x/) }) }) describe('personal_sign', () => { test('default: signs message via iframe confirm', async () => { provider = getProvider() const address = await connectViaIframe(provider) const message = Hex.fromString('hello world') const signature = await interact( provider.request({ method: 'personal_sign', params: [message, address], }), async (iframe) => { await iframe.getByTestId('confirm').click() }, ) expect(signature).toMatch(/^0x[0-9a-f]+$/) }) test('behavior: reject via iframe', async () => { provider = getProvider() const address = await connectViaIframe(provider) const message = Hex.fromString('hello world') await expect( interact( provider.request({ method: 'personal_sign', params: [message, address], }), async (iframe) => { await iframe.getByTestId('reject').click() }, ), ).rejects.toThrow() }) }) describe('eth_signTypedData_v4', () => { const typedData = { domain: { name: 'Test', version: '1', chainId: 1 }, types: { Person: [ { name: 'name', type: 'string' }, { name: 'wallet', type: 'address' }, ], }, primaryType: 'Person' as const, message: { name: 'Bob', wallet: '0x0000000000000000000000000000000000000000' }, } test('default: signs typed data via iframe confirm', async () => { provider = getProvider() const address = await connectViaIframe(provider) const signature = await interact( provider.request({ method: 'eth_signTypedData_v4', params: [address, JSON.stringify(typedData)], }), async (iframe) => { await iframe.getByTestId('confirm').click() }, ) expect(signature).toMatch(/^0x[0-9a-f]+$/) }) }) describe('wallet_authorizeAccessKey', () => { test('default: authorizes access key via iframe confirm', async () => { provider = getProvider() await connectViaIframe(provider) const result = await interact( provider.request({ method: 'wallet_authorizeAccessKey', params: [{ expiry: Expiry.days(1) }], }), async (iframe) => { await iframe.getByTestId('confirm').click() }, ) expect(result.keyAuthorization.address).toMatch(/^0x[0-9a-fA-F]{40}$/) }) test('behavior: authorizes an external p256 access key via iframe confirm', async () => { provider = getProvider() await connectViaIframe(provider) const keyPair = await WebCryptoP256.createKeyPair() const accessKeyAccount = TempoAccount.fromWebCryptoP256(keyPair) const result = await interact( provider.request({ method: 'wallet_authorizeAccessKey', params: [{ ...accessKeyAccount, expiry: Expiry.days(1) }], }), async (iframe) => { await iframe.getByTestId('confirm').click() }, ) expect(result.keyAuthorization.keyId).toBe(accessKeyAccount.address) expect(result.keyAuthorization.keyType).toMatchInlineSnapshot(`"p256"`) }) }) describe('wallet_revokeAccessKey', () => { test('default: revokes access key via iframe confirm', async () => { provider = getProvider() const address = await connectViaIframe(provider) const { keyAuthorization } = await interact( provider.request({ method: 'wallet_authorizeAccessKey', params: [{ expiry: Expiry.days(1) }], }), async (iframe) => { await iframe.getByTestId('confirm').click() }, ) await interact( provider.request({ method: 'wallet_revokeAccessKey', params: [{ address, accessKeyAddress: keyAuthorization.keyId }], }), async (iframe) => { await iframe.getByTestId('confirm').click() }, ) }) }) describe('wallet_switchEthereumChain', () => { test('default: switches chain', async () => { provider = getProvider({ chains: [chain, tempoModerato] }) await provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: `0x${tempoModerato.id.toString(16)}` }], }) const chainId = await provider.request({ method: 'eth_chainId' }) expect(chainId).toMatchInlineSnapshot(`"0xa5bf"`) }) test('error: throws for unconfigured chain', async () => { provider = getProvider() await expect( provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: '0x1' }], }), ).rejects.toThrow() }) }) describe('edge cases', () => { test('behavior: dialog backdrop click rejects pending request', async () => { provider = getProvider() await connectViaIframe(provider) await expect( interact( provider.request({ method: 'personal_sign', params: [Hex.fromString('hello'), provider.store.getState().accounts[0]!.address], }), async () => { const dialog = document.querySelector('dialog[data-tempo-wallet]') as HTMLDialogElement dialog.dispatchEvent(new Event('cancel')) }, ), ).rejects.toThrow() }) test('behavior: sequential requests each get confirmed', async () => { provider = getProvider() const address = await connectViaIframe(provider) await fund(address) const hash1 = await interact( provider.request({ method: 'eth_sendTransaction', params: [{ calls: [transferCall] }], }), async (iframe) => { await iframe.getByTestId('confirm').click() }, ) expect(hash1).toMatch(/^0x[0-9a-f]{64}$/) const hash2 = await interact( provider.request({ method: 'eth_sendTransaction', params: [{ calls: [transferCall] }], }), async (iframe) => { await iframe.getByTestId('confirm').click() }, ) expect(hash2).toMatch(/^0x[0-9a-f]{64}$/) expect(hash1).not.toBe(hash2) }) })