UNPKG

ox

Version:

Ethereum Standard Library

227 lines (195 loc) 6.91 kB
import { PublicKey, RpcTransport, Secp256k1, Signature, WebAuthnP256 } from 'ox' import { describe, expect, expectTypeOf, test } from 'vp/test' import * as SignatureEnvelope from './SignatureEnvelope.js' import * as ZoneRpcAuthentication from './ZoneRpcAuthentication.js' const privateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' as const const publicKey = PublicKey.from({ prefix: 4, x: '0xad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f5', y: '0x11edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba0', }) const p256Signature = Signature.from({ r: '0xccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e', s: '0x77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35', yParity: 0, }) const authentication = { chainId: 4217000006, expiresAt: 1711235160, issuedAt: 1711234560, zoneId: 6, } as const describe('from', () => { test('default', () => { const token = ZoneRpcAuthentication.from(authentication) expectTypeOf(token).toExtend< ZoneRpcAuthentication.ZoneRpcAuthentication<false> >() expect(token).toMatchInlineSnapshot(` { "chainId": 4217000006, "expiresAt": 1711235160, "issuedAt": 1711234560, "version": 0, "zoneId": 6, } `) }) test('options: signature', () => { const token = ZoneRpcAuthentication.from(authentication) const signature = Secp256k1.sign({ payload: ZoneRpcAuthentication.getSignPayload(token), privateKey, }) const token_signed = ZoneRpcAuthentication.from(token, { signature }) expectTypeOf(token_signed).toExtend< ZoneRpcAuthentication.ZoneRpcAuthentication<true> >() expect(token_signed).toMatchInlineSnapshot(` { "chainId": 4217000006, "expiresAt": 1711235160, "issuedAt": 1711234560, "signature": { "signature": { "r": "0xd7352eb40bcd052ad97aa4ef761223eaa757d90cc78e4f66d4ea6aaa5dca79a6", "s": "0x388aa84fef3c9c0430756b6614248e9ae6d1a7741255dbf1173db5436469443b", "yParity": 0, }, "type": "secp256k1", }, "version": 0, "zoneId": 6, } `) }) }) describe('getFields', () => { test('default', () => { const token = ZoneRpcAuthentication.from(authentication) const fields = ZoneRpcAuthentication.getFields(token) // 29 bytes = 58 hex chars + 0x prefix expect(fields).toMatch(/^0x[0-9a-f]{58}$/i) const payload = ZoneRpcAuthentication.getSignPayload(token) // keccak256 = 32 bytes expect(payload).toMatch(/^0x[0-9a-f]{64}$/) const keychainPayload = ZoneRpcAuthentication.getSignPayload(token, { userAddress: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c', }) expect(keychainPayload).toMatch(/^0x[0-9a-f]{64}$/) expect(keychainPayload).not.toBe(payload) }) }) describe('serialize', () => { test('roundtrip', () => { const token = ZoneRpcAuthentication.from(authentication) const signature = Secp256k1.sign({ payload: ZoneRpcAuthentication.getSignPayload(token), privateKey, }) const serialized = ZoneRpcAuthentication.serialize(token, { signature, }) const deserialized = ZoneRpcAuthentication.deserialize(serialized) expect(deserialized.chainId).toBe(authentication.chainId) expect(deserialized.zoneId).toBe(authentication.zoneId) expect(deserialized.issuedAt).toBe(authentication.issuedAt) expect(deserialized.expiresAt).toBe(authentication.expiresAt) expect(deserialized.version).toBe(0) expect(deserialized.signature).toBeDefined() }) test('parses variable-length keychain signatures from the end', () => { const token = ZoneRpcAuthentication.from(authentication) const inner = SignatureEnvelope.from( Secp256k1.sign({ payload: ZoneRpcAuthentication.getSignPayload(token, { userAddress: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c', }), privateKey, }), ) const serialized = ZoneRpcAuthentication.serialize(token, { signature: SignatureEnvelope.from({ inner, type: 'keychain', userAddress: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c', version: 'v2', }), }) const deserialized = ZoneRpcAuthentication.deserialize(serialized) expect(deserialized.chainId).toBe(authentication.chainId) expect(deserialized.zoneId).toBe(authentication.zoneId) expect(deserialized.signature.type).toBe('keychain') }) test('parses variable-length webAuthn signatures from the end', () => { const token = ZoneRpcAuthentication.from(authentication) const signature = SignatureEnvelope.from({ signature: p256Signature, publicKey, metadata: { authenticatorData: WebAuthnP256.getAuthenticatorData({ rpId: 'localhost', }), clientDataJSON: WebAuthnP256.getClientDataJSON({ challenge: ZoneRpcAuthentication.getSignPayload(token), origin: 'http://localhost', }), }, }) const serialized = ZoneRpcAuthentication.serialize(token, { signature, }) const deserialized = ZoneRpcAuthentication.deserialize(serialized) expect(deserialized.chainId).toBe(authentication.chainId) expect(deserialized.zoneId).toBe(authentication.zoneId) expect(deserialized.signature.type).toBe('webAuthn') }) }) const credentials = import.meta.env.VITE_TEMPO_CREDENTIALS const rpcUrl = 'https://rpc-zone-a.testnet.tempo.xyz' describe('e2e', () => { test.skip('succeeds with auth token', async () => { const privateKey = Secp256k1.randomPrivateKey() const now = Math.floor(Date.now() / 1000) const auth = ZoneRpcAuthentication.from({ chainId: 4217000005, expiresAt: now + 600, issuedAt: now, zoneId: 5, }) const serialized = ZoneRpcAuthentication.serialize(auth, { signature: SignatureEnvelope.from( Secp256k1.sign({ payload: ZoneRpcAuthentication.getSignPayload(auth), privateKey, }), ), }) const transport = RpcTransport.fromHttp(rpcUrl, { fetchOptions: { headers: { Authorization: `Basic ${btoa(credentials!)}`, [ZoneRpcAuthentication.headerName]: serialized, }, }, }) const blockNumber = await transport.request({ method: 'eth_blockNumber', }) expect(blockNumber).toBeDefined() expect(typeof blockNumber).toBe('string') }) test.skipIf(!credentials)('fails without auth token', async () => { const transport = RpcTransport.fromHttp(rpcUrl, { fetchOptions: { headers: { Authorization: `Basic ${btoa(credentials!)}`, }, }, }) await expect( transport.request({ method: 'eth_blockNumber' }), ).rejects.toThrow() }) })