UNPKG

ox

Version:

Ethereum Standard Library

791 lines (663 loc) 20 kB
import { Siwe } from 'ox' import { describe, expect, test, vi } from 'vp/test' describe('createMessage', () => { const message = { address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', chainId: 1, domain: 'example.com', nonce: 'foobarbaz', uri: 'https://example.com/path', version: '1', } satisfies Siwe.Message test('default', () => { vi.useFakeTimers() vi.setSystemTime(new Date(Date.UTC(2023, 1, 1))) expect(Siwe.createMessage(message)).toMatchInlineSnapshot(` "example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z" `) vi.useRealTimers() }) test('parameters: domain', () => { vi.useFakeTimers() vi.setSystemTime(new Date(Date.UTC(2023, 1, 1))) expect( Siwe.createMessage({ ...message, domain: 'foo.example.com', }), ).toMatchInlineSnapshot(` "foo.example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z" `) expect( Siwe.createMessage({ ...message, domain: 'example.co.uk', }), ).toMatchInlineSnapshot(` "example.co.uk wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z" `) vi.useRealTimers() }) test('parameters: scheme', () => { vi.useFakeTimers() vi.setSystemTime(new Date(Date.UTC(2023, 1, 1))) expect( Siwe.createMessage({ ...message, scheme: 'https', }), ).toMatchInlineSnapshot(` "https://example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z" `) vi.useRealTimers() }) test('parameters: statement', () => { vi.useFakeTimers() vi.setSystemTime(new Date(Date.UTC(2023, 1, 1))) expect( Siwe.createMessage({ ...message, statement: 'I accept the ExampleOrg Terms of Service: https://example.com/tos', }), ).toMatchInlineSnapshot(` "example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e I accept the ExampleOrg Terms of Service: https://example.com/tos URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z" `) vi.useRealTimers() }) test('parameters: issuedAt', () => { const issuedAt = new Date(Date.UTC(2022, 1, 4)) expect(Siwe.createMessage({ ...message, issuedAt })).toMatchInlineSnapshot(` "example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2022-02-04T00:00:00.000Z" `) }) test('parameters: expirationTime', () => { vi.useFakeTimers() vi.setSystemTime(new Date(Date.UTC(2023, 1, 1))) expect( Siwe.createMessage({ ...message, expirationTime: new Date(Date.UTC(2022, 1, 4)), }), ).toMatchInlineSnapshot(` "example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z Expiration Time: 2022-02-04T00:00:00.000Z" `) vi.useRealTimers() }) test('parameters: notBefore', () => { vi.useFakeTimers() vi.setSystemTime(new Date(Date.UTC(2023, 1, 1))) expect( Siwe.createMessage({ ...message, notBefore: new Date(Date.UTC(2022, 1, 4)), }), ).toMatchInlineSnapshot(` "example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z Not Before: 2022-02-04T00:00:00.000Z" `) vi.useRealTimers() }) test('parameters: requestId', () => { vi.useFakeTimers() vi.setSystemTime(new Date(Date.UTC(2023, 1, 1))) expect( Siwe.createMessage({ ...message, requestId: '123e4567-e89b-12d3-a456-426614174000', }), ).toMatchInlineSnapshot(` "example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z Request ID: 123e4567-e89b-12d3-a456-426614174000" `) vi.useRealTimers() }) test('parameters: resources', () => { vi.useFakeTimers() vi.setSystemTime(new Date(Date.UTC(2023, 1, 1))) expect( Siwe.createMessage({ ...message, resources: [ 'https://example.com/foo', 'https://example.com/bar', 'https://example.com/baz', ], }), ).toMatchInlineSnapshot(` "example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z Resources: - https://example.com/foo - https://example.com/bar - https://example.com/baz" `) vi.useRealTimers() }) test('behavior: non-checksummed address', () => { vi.useFakeTimers() vi.setSystemTime(new Date(Date.UTC(2023, 1, 1))) expect( Siwe.createMessage({ ...message, address: '0xa0cf798816d4b9b9866b5330eea46a18382f251e', }), ).toMatchInlineSnapshot(` "example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z" `) vi.useRealTimers() }) test('behavior: invalid address', () => { expect(() => Siwe.createMessage({ ...message, address: '0xfoobarbaz' })) .toThrowErrorMatchingInlineSnapshot(` [Address.InvalidAddressError: Address "0xfoobarbaz" is invalid. Details: Address is not a 20 byte (40 hexadecimal character) value.] `) }) test('behavior: invalid chainId', () => { expect(() => Siwe.createMessage({ ...message, chainId: 1.1 })) .toThrowErrorMatchingInlineSnapshot(` [Siwe.InvalidMessageFieldError: Invalid Sign-In with Ethereum message field "chainId". - Chain ID must be a EIP-155 chain ID. - See https://eips.ethereum.org/EIPS/eip-155 Provided value: 1.1] `) }) test('behavior: invalid domain', () => { expect(() => Siwe.createMessage({ ...message, domain: '#foo' })) .toThrowErrorMatchingInlineSnapshot(` [Siwe.InvalidMessageFieldError: Invalid Sign-In with Ethereum message field "domain". - Domain must be an RFC 3986 authority. - See https://www.rfc-editor.org/rfc/rfc3986 Provided value: #foo] `) }) test('behavior: invalid nonce', () => { expect(() => Siwe.createMessage({ ...message, nonce: '#foo' })) .toThrowErrorMatchingInlineSnapshot(` [Siwe.InvalidMessageFieldError: Invalid Sign-In with Ethereum message field "nonce". - Nonce must be at least 8 characters. - Nonce must be alphanumeric. Provided value: #foo] `) }) test('behavior: invalid uri', () => { expect(() => Siwe.createMessage({ ...message, uri: '#foo' })) .toThrowErrorMatchingInlineSnapshot(` [Siwe.InvalidMessageFieldError: Invalid Sign-In with Ethereum message field "uri". - URI must be a RFC 3986 URI referring to the resource that is the subject of the signing. - See https://www.rfc-editor.org/rfc/rfc3986 Provided value: #foo] `) }) test('behavior: invalid version', () => { expect(() => // @ts-expect-error Siwe.createMessage({ ...message, version: '2' }), ).toThrowErrorMatchingInlineSnapshot(` [Siwe.InvalidMessageFieldError: Invalid Sign-In with Ethereum message field "version". - Version must be '1'. Provided value: 2] `) }) test('behavior: invalid scheme', () => { expect(() => Siwe.createMessage({ ...message, scheme: 'foo_bar' })) .toThrowErrorMatchingInlineSnapshot(` [Siwe.InvalidMessageFieldError: Invalid Sign-In with Ethereum message field "scheme". - Scheme must be an RFC 3986 URI scheme. - See https://www.rfc-editor.org/rfc/rfc3986#section-3.1 Provided value: foo_bar] `) }) test('behavior: invalid statement', () => { expect(() => Siwe.createMessage({ ...message, statement: 'foo\nbar' })) .toThrowErrorMatchingInlineSnapshot(` [Siwe.InvalidMessageFieldError: Invalid Sign-In with Ethereum message field "statement". - Statement must not include '\\n'. Provided value: foo bar] `) }) test('behavior: invalid resources', () => { expect(() => Siwe.createMessage({ ...message, resources: ['https://example.com', 'foo'], }), ).toThrowErrorMatchingInlineSnapshot(` [Siwe.InvalidMessageFieldError: Invalid Sign-In with Ethereum message field "resources". - Every resource must be a RFC 3986 URI. - See https://www.rfc-editor.org/rfc/rfc3986 Provided value: https://example.com] `) }) test.each([ 'example.com', 'localhost', '127.0.0.1', 'example.com:3000', 'localhost:3000', '127.0.0.1:3000', ])('valid domain `%s`', (domain) => { expect( Siwe.createMessage({ ...message, domain, }), ).toBeTypeOf('string') }) test.each([ 'http://example.com', 'http://localhost', 'http://127.0.0.1', 'http://example.com:3000', 'http://localhost:3000', 'http://127.0.0.1:3000', 'foobarbaz', '-example.com', ])('invalid domain `%s`', (domain) => { expect(() => Siwe.createMessage({ ...message, domain, }), ).toThrowError() }) }) describe('generateNonce', () => { test('default', () => { const nonce = Siwe.generateNonce() expect(nonce.length).toMatchInlineSnapshot('96') }) }) describe('isUri', () => { test('default', () => { expect(Siwe.isUri('https://example.com/foo')).toMatchInlineSnapshot( `"https://example.com/foo"`, ) }) test('behavior: check for illegal characters', () => { expect(Siwe.isUri('^')).toBeFalsy() }) test('incomplete hex escapes', () => { expect(Siwe.isUri('%$#')).toBeFalsy() expect(Siwe.isUri('%0:#')).toBeFalsy() }) test('missing scheme', () => { expect(Siwe.isUri('example.com/foo')).toBeFalsy() }) test('authority with missing path', () => { expect(Siwe.isUri('1http:////foo.html')).toBeFalsy() }) test('scheme begins with letter', () => { expect(Siwe.isUri('$https://example.com/foo')).toBeFalsy() }) test('query', () => { expect(Siwe.isUri('https://example.com/foo?bar')).toMatchInlineSnapshot( `"https://example.com/foo?bar"`, ) }) test('fragment', () => { expect(Siwe.isUri('https://example.com/foo#bar')).toMatchInlineSnapshot( `"https://example.com/foo#bar"`, ) }) }) describe('parseMessage', () => { test('default', () => { const message = `example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e I accept the ExampleOrg Terms of Service: https://example.com/tos URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z` const parsed = Siwe.parseMessage(message) expect(parsed).toMatchInlineSnapshot(` { "address": "0xA0Cf798816D4b9b9866b5330EEa46a18382f251e", "chainId": 1, "domain": "example.com", "issuedAt": 2023-02-01T00:00:00.000Z, "nonce": "foobarbaz", "statement": "I accept the ExampleOrg Terms of Service: https://example.com/tos", "uri": "https://example.com/path", "version": "1", } `) }) test('behavior: with scheme', () => { const message = `https://example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z` const parsed = Siwe.parseMessage(message) expect(parsed.scheme).toMatchInlineSnapshot(`"https"`) }) test('behavior: domain with port', () => { const message = `example.com:8080 wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z` const parsed = Siwe.parseMessage(message) expect(parsed.domain).toMatchInlineSnapshot(`"example.com:8080"`) }) test('behavior: with statement', () => { const message = `example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e I accept the ExampleOrg Terms of Service: https://example.com/tos URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z` const parsed = Siwe.parseMessage(message) expect(parsed.statement).toMatchInlineSnapshot( `"I accept the ExampleOrg Terms of Service: https://example.com/tos"`, ) }) test('behavior: with expirationTime', () => { const message = `https://example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z Expiration Time: 2022-02-04T00:00:00.000Z` const parsed = Siwe.parseMessage(message) expect(parsed.expirationTime).toMatchInlineSnapshot( '2022-02-04T00:00:00.000Z', ) }) test('behavior: with notBefore', () => { const message = `https://example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z Not Before: 2022-02-04T00:00:00.000Z` const parsed = Siwe.parseMessage(message) expect(parsed.notBefore).toMatchInlineSnapshot('2022-02-04T00:00:00.000Z') }) test('behavior: with requestId', () => { const message = `https://example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z Request ID: 123e4567-e89b-12d3-a456-426614174000` const parsed = Siwe.parseMessage(message) expect(parsed.requestId).toMatchInlineSnapshot( `"123e4567-e89b-12d3-a456-426614174000"`, ) }) test('behavior: with resources', () => { const message = `https://example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z Resources: - https://example.com/foo - https://example.com/bar - https://example.com/baz` const parsed = Siwe.parseMessage(message) expect(parsed.resources).toMatchInlineSnapshot(` [ "https://example.com/foo", "https://example.com/bar", "https://example.com/baz", ] `) }) test('behavior: no suffix', () => { const message = `https://example.com wants you to sign in with your Ethereum account: 0xA0Cf798816D4b9b9866b5330EEa46a18382f251e ` const parsed = Siwe.parseMessage(message) expect(parsed).toMatchInlineSnapshot(` { "address": "0xA0Cf798816D4b9b9866b5330EEa46a18382f251e", "domain": "example.com", "scheme": "https", } `) }) test('behavior: no prefix', () => { const message = `URI: https://example.com/path Version: 1 Chain ID: 1 Nonce: foobarbaz Issued At: 2023-02-01T00:00:00.000Z Request ID: 123e4567-e89b-12d3-a456-426614174000` const parsed = Siwe.parseMessage(message) expect(parsed).toMatchInlineSnapshot(` { "chainId": 1, "issuedAt": 2023-02-01T00:00:00.000Z, "nonce": "foobarbaz", "requestId": "123e4567-e89b-12d3-a456-426614174000", "uri": "https://example.com/path", "version": "1", } `) }) test('behavior: bogus message', () => { const message = 'foobarbaz' const parsed = Siwe.parseMessage(message) expect(parsed).toMatchInlineSnapshot('{}') }) }) describe('validateMessage', () => { const message = { address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', chainId: 1, domain: 'example.com', nonce: 'foobarbaz', uri: 'https://example.com/path', version: '1', } satisfies Siwe.Message test('default', () => { expect( Siwe.validateMessage({ message, }), ).toBeTruthy() }) test('behavior: invalid address', () => { expect( Siwe.validateMessage({ message: { ...message, address: undefined, }, }), ).toBeFalsy() }) test('behavior: address mismatch', () => { expect( Siwe.validateMessage({ address: '0xd2135CfB216b74109775236E36d4b433F1DF507B', message, }), ).toBeFalsy() }) test('behavior: invalid address', () => { expect( Siwe.validateMessage({ address: '0xfoobarbaz', message, }), ).toBeFalsy() }) test('behavior: domain mismatch', () => { expect( Siwe.validateMessage({ domain: 'viem.sh', message, }), ).toBeFalsy() }) test('behavior: nonce mismatch', () => { expect( Siwe.validateMessage({ nonce: 'f0obarbaz', message, }), ).toBeFalsy() }) test('behavior: scheme mismatch', () => { expect( Siwe.validateMessage({ scheme: 'http', message: { ...message, scheme: 'https', }, }), ).toBeFalsy() }) test('behavior: time is after expirationTime', () => { expect( Siwe.validateMessage({ message: { ...message, expirationTime: new Date(Date.UTC(2024, 1, 1)), }, time: new Date(Date.UTC(2025, 1, 1)), }), ).toBeFalsy() }) test('behavior: time is before notBefore', () => { vi.useFakeTimers() vi.setSystemTime(new Date(Date.UTC(2023, 1, 1))) expect( Siwe.validateMessage({ message: { ...message, notBefore: new Date(Date.UTC(2024, 1, 1)), }, time: new Date(Date.UTC(2023, 1, 1)), }), ).toBeFalsy() vi.useRealTimers() }) }) test('InvalidMessageFieldError', () => { expect( new Siwe.InvalidMessageFieldError({ field: 'nonce', metaMessages: [ '- Nonce must be at least 8 characters.', '- Nonce must be alphanumeric.', '', 'Provided value: foobarbaz$', ], }), ).toMatchInlineSnapshot(` [Siwe.InvalidMessageFieldError: Invalid Sign-In with Ethereum message field "nonce". - Nonce must be at least 8 characters. - Nonce must be alphanumeric. Provided value: foobarbaz$] `) }) test('exports', () => { expect(Object.keys(Siwe)).toMatchInlineSnapshot(` [ "domainRegex", "ipRegex", "localhostRegex", "nonceRegex", "schemeRegex", "prefixRegex", "suffixRegex", "createMessage", "generateNonce", "isUri", "parseMessage", "validateMessage", "InvalidMessageFieldError", ] `) })