ox
Version:
Ethereum Standard Library
267 lines (241 loc) • 8.3 kB
text/typescript
import * as AbiParameters from '../core/AbiParameters.js'
import type * as Address from '../core/Address.js'
import * as Authorization from '../core/Authorization.js'
import * as Errors from '../core/Errors.js'
import * as Hex from '../core/Hex.js'
import * as Secp256k1 from '../core/Secp256k1.js'
import * as Signature from '../core/Signature.js'
/** Unwrapped ERC-8010 signature. */
export type Unwrapped = {
/** Authorization signed by the delegatee. */
authorization: Authorization.Authorization<true>
/** Data to initialize the delegation. */
data?: Hex.Hex | undefined
/** The original signature. */
signature: Hex.Hex
/** Address of the initializer. */
to?: Address.Address | undefined
}
/** Wrapped ERC-8010 signature. */
export type Wrapped = Hex.Hex
/**
* Magic bytes used to identify ERC-8010 wrapped signatures.
*/
export const magicBytes =
'0x8010801080108010801080108010801080108010801080108010801080108010' as const
/** Suffix ABI parameters for the ERC-8010 wrapped signature. */
export const suffixParameters = AbiParameters.from(
'(uint256 chainId, address delegation, uint256 nonce, uint8 yParity, uint256 r, uint256 s), address to, bytes data',
)
/**
* Asserts that the wrapped signature is valid.
*
* @example
* ```ts twoslash
* import { SignatureErc8010 } from 'ox/erc8010'
*
* SignatureErc8010.assert('0xdeadbeef')
* // @error: InvalidWrappedSignatureError: Value `0xdeadbeef` is an invalid ERC-8010 wrapped signature.
* ```
*
* @param value - The value to assert.
*/
export function assert(value: Unwrapped | Wrapped) {
if (typeof value === 'string') {
if (Hex.slice(value, -32) !== magicBytes)
throw new InvalidWrappedSignatureError(value)
return
}
if (value === null || typeof value !== 'object')
throw new InvalidUnwrappedSignatureError(value as never)
const { authorization, data, signature, to } = value as Unwrapped
if (typeof signature !== 'string')
throw new InvalidUnwrappedSignatureError(value as Unwrapped)
if (data !== undefined && typeof data !== 'string')
throw new InvalidUnwrappedSignatureError(value as Unwrapped)
if (to !== undefined && typeof to !== 'string')
throw new InvalidUnwrappedSignatureError(value as Unwrapped)
if (authorization === null || typeof authorization !== 'object')
throw new InvalidUnwrappedSignatureError(value as Unwrapped)
Signature.assert(authorization)
}
export declare namespace assert {
type ErrorType =
| InvalidWrappedSignatureError
| InvalidUnwrappedSignatureError
| Hex.slice.ErrorType
| Errors.GlobalErrorType
}
/**
* Parses an [ERC-8010 wrapped signature](https://github.com/jxom/ERCs/blob/16f7e3891fff2e1e9c25dea0485497739db8a816/ERCS/erc-8010.md) into its constituent parts.
*
* @example
* ```ts twoslash
* // @noErrors
* import { Secp256k1 } from 'ox'
* import { SignatureErc8010 } from 'ox/erc8010' // [!code focus]
*
* const signature = Secp256k1.sign({
* payload: '0x...',
* privateKey: '0x...',
* })
*
* // Instantiate from serialized format. // [!code focus]
* const wrapped = SignatureErc8010.from('0x...') // [!code focus]
* // @log: { authorization: { ... }, data: '0x...', signature: { ... } } // [!code focus]
*
* // Instantiate from constituent parts. // [!code focus]
* const wrapped = SignatureErc8010.from({ // [!code focus]
* authorization: { ... }, // [!code focus]
* data: '0x...', // [!code focus]
* signature, // [!code focus]
* })
* // @log: { authorization: { ... }, data: '0x...', signature: { ... } }
* ```
*
* @param value - Value to parse.
* @returns Parsed value.
*/
export function from(value: Unwrapped | Wrapped): Unwrapped {
if (typeof value === 'string') return unwrap(value)
assert(value)
return value
}
export declare namespace from {
type ErrorType = unwrap.ErrorType | Errors.GlobalErrorType
}
/**
* Unwraps an [ERC-8010 wrapped signature](https://github.com/jxom/ERCs/blob/16f7e3891fff2e1e9c25dea0485497739db8a816/ERCS/erc-8010.md) into its constituent parts.
*
* @example
* ```ts twoslash
* import { SignatureErc8010 } from 'ox/erc8010'
*
* const { authorization, data, signature } =
* SignatureErc8010.unwrap('0x...')
* ```
*
* @param wrapped - Wrapped signature to unwrap.
* @returns Unwrapped signature.
*/
export function unwrap(wrapped: Wrapped): Unwrapped {
assert(wrapped)
const suffixLengthBig = Hex.toBigInt(Hex.slice(wrapped, -64, -32))
const wrappedSize = Hex.size(wrapped)
// Suffix + length word + magic bytes must fit inside the wrapped value.
if (suffixLengthBig < 0n || suffixLengthBig > BigInt(wrappedSize - 64))
throw new InvalidWrappedSignatureError(wrapped)
const suffixLength = Number(suffixLengthBig)
const suffix = Hex.slice(wrapped, -suffixLength - 64, -64)
const signature = Hex.slice(wrapped, 0, -suffixLength - 64)
const [auth, to, data] = AbiParameters.decode(suffixParameters, suffix)
const authorization = Authorization.from({
address: auth.delegation,
chainId: Number(auth.chainId),
nonce: auth.nonce,
yParity: auth.yParity,
r: Hex.fromNumber(auth.r, { size: 32 }),
s: Hex.fromNumber(auth.s, { size: 32 }),
})
return {
authorization,
signature,
...(data && data !== '0x' ? { data, to } : {}),
}
}
export declare namespace unwrap {
type ErrorType = assert.ErrorType | Errors.GlobalErrorType
}
/**
* Wraps a signature into [ERC-8010 format](https://github.com/jxom/ERCs/blob/16f7e3891fff2e1e9c25dea0485497739db8a816/ERCS/erc-8010.md).
*
* @example
* ```ts twoslash
* // @noErrors
* import { Secp256k1, Signature } from 'ox'
* import { SignatureErc8010 } from 'ox/erc8010' // [!code focus]
*
* const signature = Secp256k1.sign({
* payload: '0x...',
* privateKey: '0x...',
* })
*
* const wrapped = SignatureErc8010.wrap({ // [!code focus]
* authorization: { ... }, // [!code focus]
* data: '0xdeadbeef', // [!code focus]
* signature: Signature.toHex(signature), // [!code focus]
* }) // [!code focus]
* ```
*
* @param value - Values to wrap.
* @returns Wrapped signature.
*/
export function wrap(value: Unwrapped): Wrapped {
const { data, signature } = value
assert(value)
const to =
value.to ??
Secp256k1.recoverAddress({
payload: Authorization.getSignPayload(value.authorization),
signature: Signature.from(value.authorization),
})
const suffix = AbiParameters.encode(suffixParameters, [
{
...value.authorization,
delegation: value.authorization.address,
chainId: BigInt(value.authorization.chainId),
r: BigInt(value.authorization.r ?? 0n),
s: BigInt(value.authorization.s ?? 0n),
},
to,
data ?? '0x',
])
const suffixLength = Hex.fromNumber(Hex.size(suffix), { size: 32 })
return Hex.concat(signature, suffix, suffixLength, magicBytes)
}
export declare namespace wrap {
type ErrorType = Errors.GlobalErrorType
}
/**
* Validates a wrapped signature. Returns `true` if the wrapped signature is valid, `false` otherwise.
*
* @example
* ```ts twoslash
* import { SignatureErc8010 } from 'ox/erc8010'
*
* const valid = SignatureErc8010.validate('0xdeadbeef')
* // @log: false
* ```
*
* @param value - The value to validate.
* @returns `true` if the value is valid, `false` otherwise.
*/
export function validate(value: Unwrapped | Wrapped): boolean {
try {
assert(value)
return true
} catch {
return false
}
}
export declare namespace validate {
type ErrorType = Errors.GlobalErrorType
}
/** Thrown when the ERC-8010 wrapped signature is invalid. */
export class InvalidWrappedSignatureError extends Errors.BaseError {
override readonly name = 'SignatureErc8010.InvalidWrappedSignatureError'
constructor(wrapped: Wrapped) {
super(`Value \`${wrapped}\` is an invalid ERC-8010 wrapped signature.`)
}
}
/** Thrown when an ERC-8010 unwrapped signature object is malformed. */
export class InvalidUnwrappedSignatureError extends Errors.BaseError {
override readonly name = 'SignatureErc8010.InvalidUnwrappedSignatureError'
constructor(value: Unwrapped) {
super(
`Value \`${JSON.stringify(value, (_, v) =>
typeof v === 'bigint' ? v.toString() : v,
)}\` is an invalid ERC-8010 unwrapped signature.`,
)
}
}