viem
Version:
179 lines (167 loc) • 5.23 kB
text/typescript
import {
SiweInvalidMessageFieldError,
type SiweInvalidMessageFieldErrorType,
} from '../../errors/siwe.js'
import type { ErrorType } from '../../errors/utils.js'
import { type GetAddressErrorType, getAddress } from '../address/getAddress.js'
import type { SiweMessage } from './types.js'
import { isUri } from './utils.js'
export type CreateSiweMessageParameters = SiweMessage
export type CreateSiweMessageReturnType = string
export type CreateSiweMessageErrorType =
| GetAddressErrorType
| SiweInvalidMessageFieldErrorType
| ErrorType
/**
* @description Creates EIP-4361 formatted message.
*
* @example
* const message = createMessage({
* address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
* chainId: 1,
* domain: 'example.com',
* nonce: 'foobarbaz',
* uri: 'https://example.com/path',
* version: '1',
* })
*
* @see https://eips.ethereum.org/EIPS/eip-4361
*/
export function createSiweMessage(
parameters: CreateSiweMessageParameters,
): CreateSiweMessageReturnType {
const {
chainId,
domain,
expirationTime,
issuedAt = new Date(),
nonce,
notBefore,
requestId,
resources,
scheme,
uri,
version,
} = parameters
// Validate fields
{
// Required fields
if (chainId !== Math.floor(chainId))
throw new SiweInvalidMessageFieldError({
field: 'chainId',
metaMessages: [
'- Chain ID must be a EIP-155 chain ID.',
'- See https://eips.ethereum.org/EIPS/eip-155',
'',
`Provided value: ${chainId}`,
],
})
if (
!(
domainRegex.test(domain) ||
ipRegex.test(domain) ||
localhostRegex.test(domain)
)
)
throw new SiweInvalidMessageFieldError({
field: 'domain',
metaMessages: [
'- Domain must be an RFC 3986 authority.',
'- See https://www.rfc-editor.org/rfc/rfc3986',
'',
`Provided value: ${domain}`,
],
})
if (!nonceRegex.test(nonce))
throw new SiweInvalidMessageFieldError({
field: 'nonce',
metaMessages: [
'- Nonce must be at least 8 characters.',
'- Nonce must be alphanumeric.',
'',
`Provided value: ${nonce}`,
],
})
if (!isUri(uri))
throw new SiweInvalidMessageFieldError({
field: 'uri',
metaMessages: [
'- 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: ${uri}`,
],
})
if (version !== '1')
throw new SiweInvalidMessageFieldError({
field: 'version',
metaMessages: [
"- Version must be '1'.",
'',
`Provided value: ${version}`,
],
})
// Optional fields
if (scheme && !schemeRegex.test(scheme))
throw new SiweInvalidMessageFieldError({
field: 'scheme',
metaMessages: [
'- Scheme must be an RFC 3986 URI scheme.',
'- See https://www.rfc-editor.org/rfc/rfc3986#section-3.1',
'',
`Provided value: ${scheme}`,
],
})
const statement = parameters.statement
if (statement?.includes('\n'))
throw new SiweInvalidMessageFieldError({
field: 'statement',
metaMessages: [
"- Statement must not include '\\n'.",
'',
`Provided value: ${statement}`,
],
})
}
// Construct message
const address = getAddress(parameters.address)
const origin = (() => {
if (scheme) return `${scheme}://${domain}`
return domain
})()
const statement = (() => {
if (!parameters.statement) return ''
return `${parameters.statement}\n`
})()
const prefix = `${origin} wants you to sign in with your Ethereum account:\n${address}\n\n${statement}`
let suffix = `URI: ${uri}\nVersion: ${version}\nChain ID: ${chainId}\nNonce: ${nonce}\nIssued At: ${issuedAt.toISOString()}`
if (expirationTime)
suffix += `\nExpiration Time: ${expirationTime.toISOString()}`
if (notBefore) suffix += `\nNot Before: ${notBefore.toISOString()}`
if (requestId) suffix += `\nRequest ID: ${requestId}`
if (resources) {
let content = '\nResources:'
for (const resource of resources) {
if (!isUri(resource))
throw new SiweInvalidMessageFieldError({
field: 'resources',
metaMessages: [
'- Every resource must be a RFC 3986 URI.',
'- See https://www.rfc-editor.org/rfc/rfc3986',
'',
`Provided value: ${resource}`,
],
})
content += `\n- ${resource}`
}
suffix += content
}
return `${prefix}\n${suffix}`
}
const domainRegex =
/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(:[0-9]{1,5})?$/
const ipRegex =
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(:[0-9]{1,5})?$/
const localhostRegex = /^localhost(:[0-9]{1,5})?$/
const nonceRegex = /^[a-zA-Z0-9]{8,}$/
const schemeRegex = /^([a-zA-Z][a-zA-Z0-9+-.]*)$/