ox
Version:
Ethereum Standard Library
568 lines (516 loc) • 15.1 kB
text/typescript
import * as Bytes from './Bytes.js'
import * as Errors from './Errors.js'
import * as Hex from './Hex.js'
import type { Compute, ExactPartial } from './internal/types.js'
import * as Json from './Json.js'
/** Root type for an ECDSA Public Key. */
export type PublicKey<
compressed extends boolean = false,
numberType = number,
> = Compute<
compressed extends true
? {
prefix: numberType
x: Hex.Hex
y?: undefined
}
: {
prefix: numberType
x: Hex.Hex
y: Hex.Hex
}
>
/**
* Asserts that a {@link ox#PublicKey.PublicKey} is valid.
*
* @example
* ```ts twoslash
* import { PublicKey } from 'ox'
*
* PublicKey.assert({
* prefix: 4,
* y: '0x6e1c1f59ee1cf25b75a8d57b3c89e7e6b3b1da823df8b3b89497f30c1f000000'
* })
* // @error: PublicKey.InvalidError: Value \`{"y":"0x..."}\` is not a valid public key.
* // @error: Public key must contain:
* // @error: - an `x` and `prefix` value (compressed)
* // @error: - an `x`, `y`, and `prefix` value (uncompressed)
* ```
*
* @param publicKey - The public key object to assert.
*/
export function assert(
publicKey: ExactPartial<PublicKey>,
options: assert.Options = {},
): asserts publicKey is PublicKey {
const { compressed } = options
const { prefix, x, y } = publicKey
// Explicit `compressed: false` -- require uncompressed shape.
if (compressed === false) {
if (typeof x !== 'string' || typeof y !== 'string')
throw new InvalidError({ publicKey })
if (prefix !== 4)
throw new InvalidPrefixError({
prefix,
cause: new InvalidUncompressedPrefixError(),
})
return
}
// Explicit `compressed: true` -- require compressed shape.
if (compressed === true) {
if (typeof x !== 'string' || typeof y !== 'undefined')
throw new InvalidError({ publicKey })
if (prefix !== 3 && prefix !== 2)
throw new InvalidPrefixError({
prefix,
cause: new InvalidCompressedPrefixError(),
})
return
}
// No explicit `compressed` option -- infer from shape.
// Uncompressed
if (typeof x === 'string' && typeof y === 'string') {
if (prefix !== 4)
throw new InvalidPrefixError({
prefix,
cause: new InvalidUncompressedPrefixError(),
})
return
}
// Compressed
if (typeof x === 'string' && typeof y === 'undefined') {
if (prefix !== 3 && prefix !== 2)
throw new InvalidPrefixError({
prefix,
cause: new InvalidCompressedPrefixError(),
})
return
}
// Unknown/invalid
throw new InvalidError({ publicKey })
}
export declare namespace assert {
type Options = {
/** Whether or not the public key should be compressed. */
compressed?: boolean
}
type ErrorType = InvalidError | InvalidPrefixError | Errors.GlobalErrorType
}
/**
* Compresses a {@link ox#PublicKey.PublicKey}.
*
* @example
* ```ts twoslash
* import { PublicKey } from 'ox'
*
* const publicKey = PublicKey.from({
* prefix: 4,
* x: '0x83185...',
* y: '0x35477...'
* })
*
* const compressed = PublicKey.compress(publicKey) // [!code focus]
* // @log: {
* // @log: prefix: 3,
* // @log: x: '0x83185...',
* // @log: }
* ```
*
* @param publicKey - The public key to compress.
* @returns The compressed public key.
*/
export function compress(publicKey: PublicKey<false>): PublicKey<true> {
const { x, y } = publicKey
return {
prefix: Hex.toBigInt(y) % 2n === 0n ? 2 : 3,
x,
}
}
export declare namespace compress {
type ErrorType = Errors.GlobalErrorType
}
/**
* Instantiates a typed {@link ox#PublicKey.PublicKey} object from a
* {@link ox#PublicKey.PublicKey}, {@link ox#Bytes.Bytes}, or {@link ox#Hex.Hex}.
*
* @example
* ```ts twoslash
* import { PublicKey } from 'ox'
*
* const publicKey = PublicKey.from({
* prefix: 4,
* x: '0x8318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed75',
* y: '0x3547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5'
* })
* // @log: {
* // @log: prefix: 4,
* // @log: x: '0x8318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed75',
* // @log: y: '0x3547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5',
* // @log: }
* ```
*
* @example
* ### From Serialized
*
* ```ts twoslash
* import { PublicKey } from 'ox'
*
* const publicKey = PublicKey.from(
* '0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5'
* )
* // @log: {
* // @log: prefix: 4,
* // @log: x: '0x8318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed75',
* // @log: y: '0x3547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5',
* // @log: }
* ```
*
* @param value - The public key value to instantiate.
* @returns The instantiated {@link ox#PublicKey.PublicKey}.
*/
export function from<
const publicKey extends
| CompressedPublicKey
| UncompressedPublicKey
| Hex.Hex
| Bytes.Bytes,
>(value: from.Value<publicKey>): from.ReturnType<publicKey> {
const publicKey = (() => {
if (Hex.validate(value)) return fromHex(value)
if (Bytes.validate(value)) return fromBytes(value)
const { prefix, x, y } = value
if (typeof x === 'string' && typeof y === 'string')
return {
prefix: prefix ?? 0x04,
x: Hex.padLeft(x, 32),
y: Hex.padLeft(y, 32),
}
return { prefix, x: Hex.padLeft(x as Hex.Hex, 32) }
})()
assert(publicKey)
return publicKey as never
}
/** @internal */
type CompressedPublicKey = PublicKey<true>
/** @internal */
type UncompressedPublicKey = Omit<PublicKey<false>, 'prefix'> & {
prefix?: PublicKey['prefix'] | undefined
}
export declare namespace from {
type Value<
publicKey extends
| CompressedPublicKey
| UncompressedPublicKey
| Hex.Hex
| Bytes.Bytes = PublicKey,
> = publicKey | CompressedPublicKey | UncompressedPublicKey
type ReturnType<
publicKey extends
| CompressedPublicKey
| UncompressedPublicKey
| Hex.Hex
| Bytes.Bytes = PublicKey,
> = publicKey extends CompressedPublicKey | UncompressedPublicKey
? publicKey extends UncompressedPublicKey
? Compute<publicKey & { readonly prefix: 0x04 }>
: publicKey
: PublicKey
type ErrorType = assert.ErrorType | Errors.GlobalErrorType
}
/**
* Deserializes a {@link ox#PublicKey.PublicKey} from a {@link ox#Bytes.Bytes}
* value.
*
* @example
* ```ts twoslash
* // @noErrors
* import { PublicKey } from 'ox'
*
* const publicKey = PublicKey.fromBytes(new Uint8Array([128, 3, 131, ...]))
* // @log: {
* // @log: prefix: 4,
* // @log: x: '0x8318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed75',
* // @log: y: '0x3547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5',
* // @log: }
* ```
*
* @param publicKey - The serialized public key.
* @returns The deserialized public key.
*/
export function fromBytes(publicKey: Bytes.Bytes): PublicKey {
return fromHex(Hex.fromBytes(publicKey))
}
export declare namespace fromBytes {
type ErrorType =
| fromHex.ErrorType
| Hex.fromBytes.ErrorType
| Errors.GlobalErrorType
}
/**
* Deserializes a {@link ox#PublicKey.PublicKey} from a {@link ox#Hex.Hex} value.
*
* @example
* ```ts twoslash
* import { PublicKey } from 'ox'
*
* const publicKey = PublicKey.fromHex(
* '0x8318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5'
* )
* // @log: {
* // @log: prefix: 4,
* // @log: x: '0x8318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed75',
* // @log: y: '0x3547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5',
* // @log: }
* ```
*
* @example
* ### Deserializing a Compressed Public Key
*
* ```ts twoslash
* import { PublicKey } from 'ox'
*
* const publicKey = PublicKey.fromHex(
* '0x038318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed75'
* )
* // @log: {
* // @log: prefix: 3,
* // @log: x: '0x8318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed75',
* // @log: }
* ```
*
* @param publicKey - The serialized public key.
* @returns The deserialized public key.
*/
export function fromHex(publicKey: Hex.Hex): PublicKey {
if (
publicKey.length !== 132 &&
publicKey.length !== 130 &&
publicKey.length !== 68
)
throw new InvalidSerializedSizeError({ publicKey })
const result = (() => {
if (publicKey.length === 130) {
const x = Hex.slice(publicKey, 0, 32)
const y = Hex.slice(publicKey, 32, 64)
return {
prefix: 4,
x,
y,
}
}
if (publicKey.length === 132) {
const prefix = Number(Hex.slice(publicKey, 0, 1))
const x = Hex.slice(publicKey, 1, 33)
const y = Hex.slice(publicKey, 33, 65)
return {
prefix,
x,
y,
}
}
const prefix = Number(Hex.slice(publicKey, 0, 1))
const x = Hex.slice(publicKey, 1, 33)
return {
prefix,
x,
}
})()
assert(result)
return result as never
}
export declare namespace fromHex {
type ErrorType =
| assert.ErrorType
| Hex.slice.ErrorType
| InvalidSerializedSizeError
| Errors.GlobalErrorType
}
/**
* Serializes a {@link ox#PublicKey.PublicKey} to {@link ox#Bytes.Bytes}.
*
* @example
* ```ts twoslash
* import { PublicKey } from 'ox'
*
* const publicKey = PublicKey.from({
* prefix: 4,
* x: '0x8318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed75',
* y: '0x3547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5'
* })
*
* const bytes = PublicKey.toBytes(publicKey) // [!code focus]
* // @log: Uint8Array [128, 3, 131, ...]
* ```
*
* @param publicKey - The public key to serialize.
* @returns The serialized public key.
*/
export function toBytes(
publicKey: PublicKey<boolean>,
options: toBytes.Options = {},
): Bytes.Bytes {
return Bytes.fromHex(toHex(publicKey, options))
}
export declare namespace toBytes {
type Options = {
/**
* Whether to include the prefix in the serialized public key.
*
* @default true
*/
includePrefix?: boolean | undefined
}
type ErrorType =
| Hex.fromNumber.ErrorType
| Bytes.fromHex.ErrorType
| Errors.GlobalErrorType
}
/**
* Serializes a {@link ox#PublicKey.PublicKey} to {@link ox#Hex.Hex}.
*
* @example
* ```ts twoslash
* import { PublicKey } from 'ox'
*
* const publicKey = PublicKey.from({
* prefix: 4,
* x: '0x8318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed75',
* y: '0x3547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5'
* })
*
* const hex = PublicKey.toHex(publicKey) // [!code focus]
* // @log: '0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5'
* ```
*
* @param publicKey - The public key to serialize.
* @returns The serialized public key.
*/
export function toHex(
publicKey: PublicKey<boolean>,
options: toHex.Options = {},
): Hex.Hex {
assert(publicKey)
const { prefix, x, y } = publicKey
const { includePrefix = true } = options
const publicKey_ = Hex.concat(
includePrefix ? Hex.fromNumber(prefix, { size: 1 }) : '0x',
x,
// If the public key is not compressed, add the y coordinate.
typeof y === 'string' ? y : '0x',
)
return publicKey_
}
export declare namespace toHex {
type Options = {
/**
* Whether to include the prefix in the serialized public key.
*
* @default true
*/
includePrefix?: boolean | undefined
}
type ErrorType = Hex.fromNumber.ErrorType | Errors.GlobalErrorType
}
/**
* Validates a {@link ox#PublicKey.PublicKey}. Returns `true` if valid, `false`
* otherwise.
*
* @example
* ```ts twoslash
* import { PublicKey } from 'ox'
*
* const valid = PublicKey.validate({
* prefix: 4,
* y: '0x6e1c1f59ee1cf25b75a8d57b3c89e7e6b3b1da823df8b3b89497f30c1f000000'
* })
* // @log: false
* ```
*
* @param publicKey - The public key object to assert.
*/
export function validate(
publicKey: ExactPartial<PublicKey>,
options: validate.Options = {},
): boolean {
try {
assert(publicKey, options)
return true
} catch {
return false
}
}
export declare namespace validate {
type Options = {
/** Whether or not the public key should be compressed. */
compressed?: boolean
}
type ErrorType = Errors.GlobalErrorType
}
/**
* Thrown when a public key is invalid.
*
* @example
* ```ts twoslash
* import { PublicKey } from 'ox'
*
* PublicKey.assert({ y: '0x01' })
* // @error: PublicKey.InvalidError: Value `{"y":"0x01"}` is not a valid public key.
* // @error: Public key must contain:
* // @error: - an `x` and `prefix` value (compressed)
* // @error: - an `x`, `y`, and `prefix` value (uncompressed)
* ```
*/
export class InvalidError extends Errors.BaseError {
override readonly name = 'PublicKey.InvalidError'
constructor({ publicKey }: { publicKey: unknown }) {
super(`Value \`${Json.stringify(publicKey)}\` is not a valid public key.`, {
metaMessages: [
'Public key must contain:',
'- an `x` and `prefix` value (compressed)',
'- an `x`, `y`, and `prefix` value (uncompressed)',
],
})
}
}
/** Thrown when a public key has an invalid prefix. */
export class InvalidPrefixError<
cause extends InvalidCompressedPrefixError | InvalidUncompressedPrefixError =
| InvalidCompressedPrefixError
| InvalidUncompressedPrefixError,
> extends Errors.BaseError<cause> {
override readonly name = 'PublicKey.InvalidPrefixError'
constructor({ prefix, cause }: { prefix: number | undefined; cause: cause }) {
super(`Prefix "${prefix}" is invalid.`, {
cause,
})
}
}
/** Thrown when the public key has an invalid prefix for a compressed public key. */
export class InvalidCompressedPrefixError extends Errors.BaseError {
override readonly name = 'PublicKey.InvalidCompressedPrefixError'
constructor() {
super('Prefix must be 2 or 3 for compressed public keys.')
}
}
/**
* Thrown when the public key has an invalid prefix for an uncompressed public
* key.
*/
export class InvalidUncompressedPrefixError extends Errors.BaseError {
override readonly name = 'PublicKey.InvalidUncompressedPrefixError'
constructor() {
super('Prefix must be 4 for uncompressed public keys.')
}
}
/** Thrown when the public key has an invalid serialized size. */
export class InvalidSerializedSizeError extends Errors.BaseError {
override readonly name = 'PublicKey.InvalidSerializedSizeError'
constructor({ publicKey }: { publicKey: Hex.Hex | Bytes.Bytes }) {
super(`Value \`${publicKey}\` is an invalid public key size.`, {
metaMessages: [
'Expected: 33 bytes (compressed + prefix), 64 bytes (uncompressed) or 65 bytes (uncompressed + prefix).',
`Received ${Hex.size(Hex.from(publicKey))} bytes.`,
],
})
}
}