import * as AbiParameters from './AbiParameters.js';
import * as Address from './Address.js';
import * as Bytes from './Bytes.js';
import * as Errors from './Errors.js';
import * as Hash from './Hash.js';
import * as Hex from './Hex.js';
import * as Json from './Json.js';
import * as Solidity from './Solidity.js';
/**
 * Asserts that [EIP-712 Typed Data](https://eips.ethereum.org/EIPS/eip-712) is valid.
 *
 * @example
 * ```ts twoslash
 * import { TypedData } from 'ox'
 *
 * TypedData.assert({
 *   domain: {
 *     name: 'Ether!',
 *     version: '1',
 *     chainId: 1,
 *     verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
 *   },
 *   primaryType: 'Foo',
 *   types: {
 *     Foo: [
 *       { name: 'address', type: 'address' },
 *       { name: 'name', type: 'string' },
 *       { name: 'foo', type: 'string' },
 *     ],
 *   },
 *   message: {
 *     address: '0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9',
 *     name: 'jxom',
 *     foo: '0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9',
 *   },
 * })
 * ```
 *
 * @param value - The Typed Data to validate.
 */
export function assert(value) {
    const { domain, message, primaryType, types } = value;
    const validateData = (struct, data) => {
        for (const param of struct) {
            const { name, type } = param;
            const value = data[name];
            const integerMatch = type.match(Solidity.integerRegex);
            if (integerMatch &&
                (typeof value === 'number' || typeof value === 'bigint')) {
                const [, base, size_] = integerMatch;
                // If number cannot be cast to a sized hex value, it is out of range
                // and will throw.
                Hex.fromNumber(value, {
                    signed: base === 'int',
                    size: Number.parseInt(size_ ?? '', 10) / 8,
                });
            }
            if (type === 'address' &&
                typeof value === 'string' &&
                !Address.validate(value))
                throw new Address.InvalidAddressError({
                    address: value,
                    cause: new Address.InvalidInputError(),
                });
            const bytesMatch = type.match(Solidity.bytesRegex);
            if (bytesMatch) {
                const [, size] = bytesMatch;
                if (size && Hex.size(value) !== Number.parseInt(size, 10))
                    throw new BytesSizeMismatchError({
                        expectedSize: Number.parseInt(size, 10),
                        givenSize: Hex.size(value),
                    });
            }
            const struct = types[type];
            if (struct) {
                validateReference(type);
                validateData(struct, value);
            }
        }
    };
    // Validate domain types.
    if (types.EIP712Domain && domain) {
        if (typeof domain !== 'object')
            throw new InvalidDomainError({ domain });
        validateData(types.EIP712Domain, domain);
    }
    // Validate message types.
    if (primaryType !== 'EIP712Domain') {
        if (types[primaryType])
            validateData(types[primaryType], message);
        else
            throw new InvalidPrimaryTypeError({ primaryType, types });
    }
}
/**
 * Creates [EIP-712 Typed Data](https://eips.ethereum.org/EIPS/eip-712) [`domainSeparator`](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator) for the provided domain.
 *
 * @example
 * ```ts twoslash
 * import { TypedData } from 'ox'
 *
 * TypedData.domainSeparator({
 *   name: 'Ether!',
 *   version: '1',
 *   chainId: 1,
 *   verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
 * })
 * // @log: '0x9911ee4f58a7059a8f5385248040e6984d80e2c849500fe6a4d11c4fa98c2af3'
 * ```
 *
 * @param domain - The domain for which to create the domain separator.
 * @returns The domain separator.
 */
export function domainSeparator(domain) {
    return hashDomain({
        domain,
    });
}
/**
 * Encodes typed data in [EIP-712 format](https://eips.ethereum.org/EIPS/eip-712): `0x19 ‖ 0x01 ‖ domainSeparator ‖ hashStruct(message)`.
 *
 * @example
 * ```ts twoslash
 * import { TypedData, Hash } from 'ox'
 *
 * const data = TypedData.encode({ // [!code focus:33]
 *   domain: {
 *     name: 'Ether Mail',
 *     version: '1',
 *     chainId: 1,
 *     verifyingContract: '0x0000000000000000000000000000000000000000',
 *   },
 *   types: {
 *     Person: [
 *       { name: 'name', type: 'string' },
 *       { name: 'wallet', type: 'address' },
 *     ],
 *     Mail: [
 *       { name: 'from', type: 'Person' },
 *       { name: 'to', type: 'Person' },
 *       { name: 'contents', type: 'string' },
 *     ],
 *   },
 *   primaryType: 'Mail',
 *   message: {
 *     from: {
 *       name: 'Cow',
 *       wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
 *     },
 *     to: {
 *       name: 'Bob',
 *       wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
 *     },
 *     contents: 'Hello, Bob!',
 *   },
 * })
 * // @log: '0x19012fdf3441fcaf4f30c7e16292b258a5d7054a4e2e00dbd7b7d2f467f2b8fb9413c52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e'
 * // @log: (0x19 ‖ 0x01 ‖ domainSeparator ‖ hashStruct(message))
 *
 * const hash = Hash.keccak256(data)
 * ```
 *
 * @param value - The Typed Data to encode.
 * @returns The encoded Typed Data.
 */
export function encode(value) {
    const { domain = {}, message, primaryType } = value;
    const types = {
        EIP712Domain: extractEip712DomainTypes(domain),
        ...value.types,
    };
    // Need to do a runtime validation check on addresses, byte ranges, integer ranges, etc
    // as we can't statically check this with TypeScript.
    assert({
        domain,
        message,
        primaryType,
        types,
    });
    // Typed Data Format: `0x19 ‖ 0x01 ‖ domainSeparator ‖ hashStruct(message)`
    const parts = ['0x19', '0x01'];
    if (domain)
        parts.push(hashDomain({
            domain,
            types,
        }));
    if (primaryType !== 'EIP712Domain')
        parts.push(hashStruct({
            data: message,
            primaryType,
            types,
        }));
    return Hex.concat(...parts);
}
/**
 * Encodes [EIP-712 Typed Data](https://eips.ethereum.org/EIPS/eip-712) schema for the provided primaryType.
 *
 * @example
 * ```ts twoslash
 * import { TypedData } from 'ox'
 *
 * TypedData.encodeType({
 *   types: {
 *     Foo: [
 *       { name: 'address', type: 'address' },
 *       { name: 'name', type: 'string' },
 *       { name: 'foo', type: 'string' },
 *     ],
 *   },
 *   primaryType: 'Foo',
 * })
 * // @log: 'Foo(address address,string name,string foo)'
 * ```
 *
 * @param value - The Typed Data schema.
 * @returns The encoded type.
 */
export function encodeType(value) {
    const { primaryType, types } = value;
    let result = '';
    const unsortedDeps = findTypeDependencies({ primaryType, types });
    unsortedDeps.delete(primaryType);
    const deps = [primaryType, ...Array.from(unsortedDeps).sort()];
    for (const type of deps) {
        result += `${type}(${(types[type] ?? [])
            .map(({ name, type: t }) => `${t} ${name}`)
            .join(',')})`;
    }
    return result;
}
/**
 * Gets [EIP-712 Typed Data](https://eips.ethereum.org/EIPS/eip-712) schema for EIP-721 domain.
 *
 * @example
 * ```ts twoslash
 * import { TypedData } from 'ox'
 *
 * TypedData.extractEip712DomainTypes({
 *   name: 'Ether!',
 *   version: '1',
 *   chainId: 1,
 *   verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
 * })
 * // @log: [
 * // @log:   { 'name': 'name', 'type': 'string' },
 * // @log:   { 'name': 'version', 'type': 'string' },
 * // @log:   { 'name': 'chainId', 'type': 'uint256' },
 * // @log:   { 'name': 'verifyingContract', 'type': 'address' },
 * // @log: ]
 * ```
 *
 * @param domain - The EIP-712 domain.
 * @returns The EIP-712 domain schema.
 */
export function extractEip712DomainTypes(domain) {
    return [
        typeof domain?.name === 'string' && { name: 'name', type: 'string' },
        domain?.version && { name: 'version', type: 'string' },
        (typeof domain?.chainId === 'number' ||
            typeof domain?.chainId === 'bigint') && {
            name: 'chainId',
            type: 'uint256',
        },
        domain?.verifyingContract && {
            name: 'verifyingContract',
            type: 'address',
        },
        domain?.salt && { name: 'salt', type: 'bytes32' },
    ].filter(Boolean);
}
/**
 * Gets the payload to use for signing typed data in [EIP-712 format](https://eips.ethereum.org/EIPS/eip-712).
 *
 * @example
 * ```ts twoslash
 * import { Secp256k1, TypedData, Hash } from 'ox'
 *
 * const payload = TypedData.getSignPayload({ // [!code focus:99]
 *   domain: {
 *     name: 'Ether Mail',
 *     version: '1',
 *     chainId: 1,
 *     verifyingContract: '0x0000000000000000000000000000000000000000',
 *   },
 *   types: {
 *     Person: [
 *       { name: 'name', type: 'string' },
 *       { name: 'wallet', type: 'address' },
 *     ],
 *     Mail: [
 *       { name: 'from', type: 'Person' },
 *       { name: 'to', type: 'Person' },
 *       { name: 'contents', type: 'string' },
 *     ],
 *   },
 *   primaryType: 'Mail',
 *   message: {
 *     from: {
 *       name: 'Cow',
 *       wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
 *     },
 *     to: {
 *       name: 'Bob',
 *       wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
 *     },
 *     contents: 'Hello, Bob!',
 *   },
 * })
 *
 * const signature = Secp256k1.sign({ payload, privateKey: '0x...' })
 * ```
 *
 * @param value - The typed data to get the sign payload for.
 * @returns The payload to use for signing.
 */
export function getSignPayload(value) {
    return Hash.keccak256(encode(value));
}
/**
 * Hashes [EIP-712 Typed Data](https://eips.ethereum.org/EIPS/eip-712) domain.
 *
 * @example
 * ```ts twoslash
 * import { TypedData } from 'ox'
 *
 * TypedData.hashDomain({
 *   domain: {
 *     name: 'Ether Mail',
 *     version: '1',
 *     chainId: 1,
 *     verifyingContract: '0x0000000000000000000000000000000000000000',
 *   },
 * })
 * // @log: '0x6192106f129ce05c9075d319c1fa6ea9b3ae37cbd0c1ef92e2be7137bb07baa1'
 * ```
 *
 * @param value - The Typed Data domain and types.
 * @returns The hashed domain.
 */
export function hashDomain(value) {
    const { domain, types } = value;
    return hashStruct({
        data: domain,
        primaryType: 'EIP712Domain',
        types: {
            ...types,
            EIP712Domain: types?.EIP712Domain || extractEip712DomainTypes(domain),
        },
    });
}
/**
 * Hashes [EIP-712 Typed Data](https://eips.ethereum.org/EIPS/eip-712) struct.
 *
 * @example
 * ```ts twoslash
 * import { TypedData } from 'ox'
 *
 * TypedData.hashStruct({
 *   types: {
 *     Foo: [
 *       { name: 'address', type: 'address' },
 *       { name: 'name', type: 'string' },
 *       { name: 'foo', type: 'string' },
 *     ],
 *   },
 *   primaryType: 'Foo',
 *   data: {
 *     address: '0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9',
 *     name: 'jxom',
 *     foo: '0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9',
 *   },
 * })
 * // @log: '0x996fb3b6d48c50312d69abdd4c1b6fb02057c85aa86bb8d04c6f023326a168ce'
 * ```
 *
 * @param value - The Typed Data struct to hash.
 * @returns The hashed Typed Data struct.
 */
export function hashStruct(value) {
    const { data, primaryType, types } = value;
    const encoded = encodeData({
        data,
        primaryType,
        types,
    });
    return Hash.keccak256(encoded);
}
/**
 * Serializes [EIP-712 Typed Data](https://eips.ethereum.org/EIPS/eip-712) schema into string.
 *
 * @example
 * ```ts twoslash
 * import { TypedData } from 'ox'
 *
 * TypedData.serialize({
 *   domain: {
 *     name: 'Ether!',
 *     version: '1',
 *     chainId: 1,
 *     verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
 *   },
 *   primaryType: 'Foo',
 *   types: {
 *     Foo: [
 *       { name: 'address', type: 'address' },
 *       { name: 'name', type: 'string' },
 *       { name: 'foo', type: 'string' },
 *     ],
 *   },
 *   message: {
 *     address: '0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9',
 *     name: 'jxom',
 *     foo: '0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9',
 *   },
 * })
 * // @log: "{"domain":{},"message":{"address":"0xb9cab4f0e46f7f6b1024b5a7463734fa68e633f9","name":"jxom","foo":"0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9"},"primaryType":"Foo","types":{"Foo":[{"name":"address","type":"address"},{"name":"name","type":"string"},{"name":"foo","type":"string"}]}}"
 * ```
 *
 * @param value - The Typed Data schema to serialize.
 * @returns The serialized Typed Data schema. w
 */
export function serialize(value) {
    const { domain: domain_, message: message_, primaryType, types, } = value;
    const normalizeData = (struct, value) => {
        const data = { ...value };
        for (const param of struct) {
            const { name, type } = param;
            if (type === 'address')
                data[name] = data[name].toLowerCase();
        }
        return data;
    };
    const domain = (() => {
        if (!domain_)
            return {};
        const type = types.EIP712Domain ?? extractEip712DomainTypes(domain_);
        return normalizeData(type, domain_);
    })();
    const message = (() => {
        if (primaryType === 'EIP712Domain')
            return undefined;
        if (!types[primaryType])
            return {};
        return normalizeData(types[primaryType], message_);
    })();
    return Json.stringify({ domain, message, primaryType, types }, (_, value) => {
        if (typeof value === 'bigint')
            return value.toString();
        return value;
    });
}
/**
 * Checks if [EIP-712 Typed Data](https://eips.ethereum.org/EIPS/eip-712) is valid.
 *
 * @example
 * ```ts twoslash
 * import { TypedData } from 'ox'
 *
 * const valid = TypedData.validate({
 *   domain: {
 *     name: 'Ether!',
 *     version: '1',
 *     chainId: 1,
 *     verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
 *   },
 *   primaryType: 'Foo',
 *   types: {
 *     Foo: [
 *       { name: 'address', type: 'address' },
 *       { name: 'name', type: 'string' },
 *       { name: 'foo', type: 'string' },
 *     ],
 *   },
 *   message: {
 *     address: '0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9',
 *     name: 'jxom',
 *     foo: '0xb9CAB4F0E46F7F6b1024b5A7463734fa68E633f9',
 *   },
 * })
 * // @log: true
 * ```
 *
 * @param value - The Typed Data to validate.
 */
export function validate(value) {
    try {
        assert(value);
        return true;
    }
    catch {
        return false;
    }
}
/** Thrown when the bytes size of a typed data value does not match the expected size. */
export class BytesSizeMismatchError extends Errors.BaseError {
    constructor({ expectedSize, givenSize, }) {
        super(`Expected bytes${expectedSize}, got bytes${givenSize}.`);
        Object.defineProperty(this, "name", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 'TypedData.BytesSizeMismatchError'
        });
    }
}
/** Thrown when the domain is invalid. */
export class InvalidDomainError extends Errors.BaseError {
    constructor({ domain }) {
        super(`Invalid domain "${Json.stringify(domain)}".`, {
            metaMessages: ['Must be a valid EIP-712 domain.'],
        });
        Object.defineProperty(this, "name", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 'TypedData.InvalidDomainError'
        });
    }
}
/** Thrown when the primary type of a typed data value is invalid. */
export class InvalidPrimaryTypeError extends Errors.BaseError {
    constructor({ primaryType, types, }) {
        super(`Invalid primary type \`${primaryType}\` must be one of \`${JSON.stringify(Object.keys(types))}\`.`, {
            metaMessages: ['Check that the primary type is a key in `types`.'],
        });
        Object.defineProperty(this, "name", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 'TypedData.InvalidPrimaryTypeError'
        });
    }
}
/** Thrown when the struct type is not a valid type. */
export class InvalidStructTypeError extends Errors.BaseError {
    constructor({ type }) {
        super(`Struct type "${type}" is invalid.`, {
            metaMessages: ['Struct type must not be a Solidity type.'],
        });
        Object.defineProperty(this, "name", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 'TypedData.InvalidStructTypeError'
        });
    }
}
/** @internal */
export function encodeData(value) {
    const { data, primaryType, types } = value;
    const encodedTypes = [{ type: 'bytes32' }];
    const encodedValues = [hashType({ primaryType, types })];
    for (const field of types[primaryType] ?? []) {
        const [type, value] = encodeField({
            types,
            name: field.name,
            type: field.type,
            value: data[field.name],
        });
        encodedTypes.push(type);
        encodedValues.push(value);
    }
    return AbiParameters.encode(encodedTypes, encodedValues);
}
/** @internal */
export function hashType(value) {
    const { primaryType, types } = value;
    const encodedHashType = Hex.fromString(encodeType({ primaryType, types }));
    return Hash.keccak256(encodedHashType);
}
/** @internal */
export function encodeField(properties) {
    let { types, name, type, value } = properties;
    if (types[type] !== undefined)
        return [
            { type: 'bytes32' },
            Hash.keccak256(encodeData({ data: value, primaryType: type, types })),
        ];
    if (type === 'bytes') {
        const prepend = value.length % 2 ? '0' : '';
        value = `0x${prepend + value.slice(2)}`;
        return [{ type: 'bytes32' }, Hash.keccak256(value, { as: 'Hex' })];
    }
    if (type === 'string')
        return [
            { type: 'bytes32' },
            Hash.keccak256(Bytes.fromString(value), { as: 'Hex' }),
        ];
    if (type.lastIndexOf(']') === type.length - 1) {
        const parsedType = type.slice(0, type.lastIndexOf('['));
        const typeValuePairs = value.map((item) => encodeField({
            name,
            type: parsedType,
            types,
            value: item,
        }));
        return [
            { type: 'bytes32' },
            Hash.keccak256(AbiParameters.encode(typeValuePairs.map(([t]) => t), typeValuePairs.map(([, v]) => v))),
        ];
    }
    return [{ type }, value];
}
/** @internal */
export function findTypeDependencies(value, results = new Set()) {
    const { primaryType: primaryType_, types } = value;
    const match = primaryType_.match(/^\w*/u);
    const primaryType = match?.[0];
    if (results.has(primaryType) || types[primaryType] === undefined)
        return results;
    results.add(primaryType);
    for (const field of types[primaryType])
        findTypeDependencies({ primaryType: field.type, types }, results);
    return results;
}
/** @internal */
function validateReference(type) {
    // Struct type must not be a Solidity type.
    if (type === 'address' ||
        type === 'bool' ||
        type === 'string' ||
        type.startsWith('bytes') ||
        type.startsWith('uint') ||
        type.startsWith('int'))
        throw new InvalidStructTypeError({ type });
}
//# sourceMappingURL=TypedData.js.map