import * as SignatureEnvelope from 'ox/tempo/SignatureEnvelope';
import { decodeFunctionData, formatUnits, hashTypedData, keccak256, parseEventLogs, } from 'viem';
import { getTransactionReceipt, sendRawTransaction, sendRawTransactionSync, signTransaction, verifyTypedData, call as viem_call, } from 'viem/actions';
import { tempo as tempo_chain } from 'viem/chains';
import { Abis, Actions, Transaction } from 'viem/tempo';
import { PaymentError, VerificationFailedError } from '../../Errors.js';
import * as Expires from '../../Expires.js';
import * as Method from '../../Method.js';
import * as Store from '../../Store.js';
import * as Client from '../../viem/Client.js';
import * as Attribution from '../Attribution.js';
import * as Account from '../internal/account.js';
import * as TempoAddress from '../internal/address.js';
import * as Charge_internal from '../internal/charge.js';
import * as defaults from '../internal/defaults.js';
import * as FeePayer from '../internal/fee-payer.js';
import * as Proof from '../internal/proof.js';
import * as Selectors from '../internal/selectors.js';
import * as Methods from '../Methods.js';
import { html as htmlContent } from './internal/html.gen.js';
/**
 * Creates a Tempo charge method intent for usage on the server.
 *
 * @example
 * ```ts
 * import { tempo } from 'mppx/server'
 *
 * const charge = tempo.charge()
 * ```
 */
export function charge(parameters = {}) {
    const { amount, currency = defaults.resolveCurrency(parameters), decimals = defaults.decimals, description, externalId, feePayerPolicy, html, memo, waitForConfirmation = true, } = parameters;
    const store = (parameters.store ?? Store.memory());
    const proofStore = parameters.store;
    const { recipient, feePayer, feePayerUrl } = Account.resolve(parameters);
    const getClient = Client.getResolver({
        chain: { ...tempo_chain, experimental_preconfirmationTime: 500 },
        feePayerUrl,
        getClient: parameters.getClient,
        rpcUrl: defaults.rpcUrl,
    });
    return Method.toServer(Methods.charge, {
        defaults: {
            amount,
            currency,
            decimals,
            description,
            externalId,
            memo,
            recipient,
        },
        html: html
            ? {
                config: {},
                content: htmlContent,
                formatAmount: async (request) => {
                    try {
                        const chainId = request.methodDetails?.chainId;
                        if (chainId === undefined)
                            throw new Error('no chainId');
                        const client = await getClient({ chainId });
                        const metadata = await Actions.token.getMetadata(client, {
                            token: request.currency,
                        });
                        const symbol = new Intl.NumberFormat('en', {
                            style: 'currency',
                            currency: metadata.currency,
                            currencyDisplay: 'narrowSymbol',
                        })
                            .formatToParts(0)
                            .find((p) => p.type === 'currency')?.value ?? metadata.currency;
                        return `${symbol}${formatUnits(BigInt(request.amount), metadata.decimals)}`;
                    }
                    catch {
                        return `$${request.amount}`;
                    }
                },
                text: typeof html === 'object' ? html.text : undefined,
                theme: typeof html === 'object' ? html.theme : undefined,
            }
            : undefined,
        // TODO: dedupe `{charge,session}.request`
        async request({ credential, request }) {
            const chainId = await (async () => {
                if (request.chainId)
                    return request.chainId;
                if (parameters.testnet)
                    return defaults.chainId.testnet;
                return (await getClient({})).chain?.id;
            })();
            const client = await (async () => {
                try {
                    return await getClient({ chainId });
                }
                catch {
                    throw new Error(`No client configured with chainId ${chainId}.`);
                }
            })();
            if (client.chain?.id !== chainId)
                throw new Error(`Client not configured with chainId ${chainId}.`);
            const resolvedFeePayer = (() => {
                if (request.feePayer === false)
                    return credential ? false : undefined;
                const account = typeof request.feePayer === 'object' ? request.feePayer : feePayer;
                const requested = account ?? feePayer ?? feePayerUrl;
                if (credential)
                    return account ?? (feePayerUrl ? true : undefined);
                if (requested)
                    return true;
                return undefined;
            })();
            return {
                ...request,
                chainId,
                feePayer: resolvedFeePayer,
                memo: request.memo || undefined,
            };
        },
        async verify({ credential, request }) {
            const { challenge } = credential;
            const resolvedRequest = (() => {
                const parsed = Methods.charge.schema.request.safeParse(request);
                if (parsed.success)
                    return parsed.data;
                // verifyCredential() passes the HMAC-bound challenge request, which is
                // already in canonical output form and should not be transformed again.
                return request;
            })();
            const chainId = resolvedRequest.methodDetails?.chainId ?? request.chainId;
            const client = await getClient({ chainId });
            const { amount, methodDetails } = resolvedRequest;
            const requestAllowsFeePayer = request.feePayer !== false &&
                (request.feePayer === undefined ||
                    request.feePayer === true ||
                    typeof request.feePayer === 'object');
            const feePayerAccount = methodDetails?.feePayer === true && requestAllowsFeePayer
                ? typeof request.feePayer === 'object'
                    ? request.feePayer
                    : feePayer
                : undefined;
            const expires = challenge.expires;
            const supportedModes = methodDetails?.supportedModes;
            const currency = resolvedRequest.currency;
            const recipient = resolvedRequest.recipient;
            Expires.assert(expires, challenge.id);
            const memo = methodDetails?.memo;
            const payload = credential.payload;
            const isZeroAmount = BigInt(amount) === 0n;
            if (isZeroAmount && payload.type !== 'proof')
                throw new MismatchError('Zero-amount challenges require a proof credential.', {});
            switch (payload.type) {
                case 'hash': {
                    if (supportedModes && !supportedModes.includes('push'))
                        throw new MismatchError('Hash credentials are not supported for this challenge.', {});
                    const hash = payload.hash;
                    // Validate client-supplied identity before reserving the hash so a
                    // malformed source cannot burn an otherwise valid payment attempt.
                    const source = parseHashCredentialSource({
                        chainId: chainId ?? client.chain?.id,
                        source: credential.source,
                    });
                    // Reserve the hash while we verify it. This blocks concurrent
                    // requests from racing to reuse the same on-chain payment.
                    if (!(await markHashUsed(store, hash))) {
                        throw new VerificationFailedError({ reason: 'Transaction hash has already been used' });
                    }
                    // If verification fails after reservation, release it so transient
                    // RPC/log-validation errors do not force the payer to pay again.
                    // Once we have proven the receipt is a successful matching payment,
                    // keep the marker to enforce single-use semantics.
                    let releaseReservation = true;
                    try {
                        const expectedTransfers = getExpectedTransfers({
                            amount,
                            memo,
                            methodDetails,
                            recipient,
                        });
                        const receipt = await getTransactionReceipt(client, { hash });
                        const sender = source?.address ?? receipt.from;
                        const matchedLogs = assertTransferLogs(receipt, {
                            currency,
                            sender,
                            transfers: expectedTransfers,
                        });
                        // Only verify challenge binding when using auto-generated attribution memos.
                        // Explicit memos (set by the server) are strictly matched by assertTransferLogs
                        // but are NOT challenge-bound — callers that set explicit memos are responsible
                        // for ensuring memo uniqueness per challenge to prevent cross-challenge hash reuse.
                        if (!memo)
                            assertChallengeBoundMemo(matchedLogs, {
                                challengeId: challenge.id,
                                realm: challenge.realm,
                            });
                        const paymentReceipt = toReceipt(receipt);
                        // `toReceipt` can throw for reverted transactions. Only keep the
                        // reservation after it confirms the referenced transaction settled.
                        releaseReservation = false;
                        return paymentReceipt;
                    }
                    catch (error) {
                        if (releaseReservation)
                            await releaseHashUse(store, hash);
                        throw error;
                    }
                }
                case 'proof': {
                    if (!isZeroAmount)
                        throw new MismatchError('Proof credentials are only valid for zero-amount challenges.', {});
                    const expectedSource = credential.source;
                    if (!expectedSource)
                        throw new MismatchError('Proof credential must include a source.', {});
                    const resolvedChainId = challenge.request.methodDetails?.chainId ?? chainId;
                    const source = Proof.parsePkhSource(expectedSource);
                    if (!source || source.chainId !== resolvedChainId) {
                        throw new MismatchError('Proof credential source is invalid.', {});
                    }
                    const valid = await verifyTypedData(client, {
                        address: source.address,
                        domain: Proof.domain(resolvedChainId),
                        types: Proof.types,
                        primaryType: 'Proof',
                        message: Proof.message(challenge.id, challenge.realm),
                        signature: payload.signature,
                    });
                    if (!valid) {
                        const proofSigner = recoverAuthorizedProofSigner({
                            chainId: resolvedChainId,
                            challengeId: challenge.id,
                            realm: challenge.realm,
                            signature: payload.signature,
                            sourceAddress: source.address,
                        });
                        const authorized = proofSigner
                            ? await isActiveAccessKey(client, {
                                accessKey: proofSigner,
                                account: source.address,
                            })
                            : false;
                        if (!authorized)
                            throw new MismatchError('Proof signature does not match source.', {});
                    }
                    if (proofStore && !(await markProofUsed(proofStore, challenge.id))) {
                        throw new VerificationFailedError({ reason: 'Proof credential has already been used' });
                    }
                    return {
                        method: 'tempo',
                        status: 'success',
                        timestamp: new Date().toISOString(),
                        reference: challenge.id,
                    };
                }
                case 'transaction': {
                    if (supportedModes && !supportedModes.includes('pull'))
                        throw new MismatchError('Transaction credentials are not supported for this challenge.', {});
                    const serializedTransaction = payload.signature;
                    // Pre-broadcast dedup: catch exact byte-for-byte replays early.
                    const hash = keccak256(serializedTransaction);
                    if (!(await markHashUsed(store, hash))) {
                        throw new VerificationFailedError({ reason: 'Transaction hash has already been used' });
                    }
                    let releaseReservation = true;
                    let sponsoredSenderReservation;
                    try {
                        if (!FeePayer.isTempoTransaction(serializedTransaction))
                            throw new MismatchError('Only Tempo (0x76/0x78) transactions are supported.', {});
                        const transaction = Transaction.deserialize(serializedTransaction);
                        if (!transaction.signature || !transaction.from)
                            throw new MismatchError('Transaction must be signed by the sender before fee payer co-signing.', {});
                        const calls = (transaction.calls ?? []);
                        const transfers = getExpectedTransfers({ amount, memo, methodDetails, recipient });
                        const isFeePayerTx = methodDetails?.feePayer === true &&
                            requestAllowsFeePayer &&
                            !!(feePayerAccount || feePayerUrl);
                        const matchedCalls = assertTransferCalls(calls, {
                            currency,
                            exactCount: isFeePayerTx,
                            transfers,
                        });
                        if (!memo)
                            assertChallengeBoundCallMemo(matchedCalls, {
                                challengeId: challenge.id,
                                realm: challenge.realm,
                            });
                        if (isFeePayerTx) {
                            const reservationChainId = chainId ?? client.chain.id;
                            if (!(await markSponsoredSenderInFlight(store, {
                                chainId: reservationChainId,
                                sender: transaction.from,
                            }))) {
                                throw new VerificationFailedError({
                                    reason: 'Sponsored transaction from this sender is already in flight',
                                });
                            }
                            sponsoredSenderReservation = {
                                chainId: reservationChainId,
                                sender: transaction.from,
                            };
                            FeePayer.validateCalls(transaction.calls, { amount, currency, recipient }, { currency, expectedTransfers: transfers });
                        }
                        const expectedFeeToken = defaults.currency[chainId];
                        const resolvedFeeToken = transaction.feeToken ?? expectedFeeToken;
                        const serializedTransaction_final = await (async () => {
                            if (feePayerAccount && methodDetails?.feePayer !== false) {
                                const sponsored = FeePayer.prepareSponsoredTransaction({
                                    account: feePayerAccount,
                                    challengeExpires: expires,
                                    chainId: chainId ?? client.chain.id,
                                    details: { amount, currency, recipient },
                                    expectedFeeToken,
                                    policy: feePayerPolicy,
                                    transaction: {
                                        ...transaction,
                                        ...(resolvedFeeToken ? { feeToken: resolvedFeeToken } : {}),
                                    },
                                });
                                return signTransaction(client, sponsored);
                            }
                            return serializedTransaction;
                        })();
                        if (waitForConfirmation) {
                            await viem_call(client, {
                                ...transaction,
                                account: transaction.from,
                                calls: transaction.calls,
                                feePayerSignature: undefined,
                            });
                            const receipt = await sendRawTransactionSync(client, {
                                serializedTransaction: serializedTransaction_final,
                            });
                            const matchedLogs = assertTransferLogs(receipt, {
                                currency,
                                sender: transaction.from,
                                transfers,
                            });
                            if (!memo)
                                assertChallengeBoundMemo(matchedLogs, {
                                    challengeId: challenge.id,
                                    realm: challenge.realm,
                                });
                            // Post-broadcast dedup: catch malleable input variants
                            // (different serialized bytes, same underlying tx) that
                            // bypass the pre-broadcast check. Skip if the broadcast
                            // hash matches the input hash (already stored above).
                            if (receipt.transactionHash.toLowerCase() !== hash.toLowerCase() &&
                                !(await markHashUsed(store, receipt.transactionHash))) {
                                throw new VerificationFailedError({
                                    reason: 'Transaction hash has already been used',
                                });
                            }
                            releaseReservation = false;
                            return toReceipt(receipt);
                        }
                        // Optimistic path: simulate to catch obvious reverts, then broadcast
                        // without waiting for on-chain confirmation. The returned receipt
                        // assumes success — callers opt into this risk via waitForConfirmation: false.
                        await viem_call(client, {
                            ...transaction,
                            account: transaction.from,
                            calls: transaction.calls,
                            feePayerSignature: undefined,
                        });
                        const reference = await sendRawTransaction(client, {
                            serializedTransaction: serializedTransaction_final,
                        });
                        // Post-broadcast dedup: same
                        if (reference.toLowerCase() !== hash.toLowerCase() &&
                            !(await markHashUsed(store, reference))) {
                            throw new VerificationFailedError({
                                reason: 'Transaction hash has already been used',
                            });
                        }
                        releaseReservation = false;
                        return {
                            method: 'tempo',
                            status: 'success',
                            timestamp: new Date().toISOString(),
                            reference,
                        };
                    }
                    catch (error) {
                        if (releaseReservation)
                            await releaseHashUse(store, hash);
                        throw error;
                    }
                    finally {
                        if (sponsoredSenderReservation)
                            await releaseSponsoredSenderInFlight(store, sponsoredSenderReservation);
                    }
                }
                default:
                    throw new Error(`Unsupported credential type "${payload.type}".`);
            }
        },
    });
}
function getExpectedTransfers(parameters) {
    return Charge_internal.getTransfers({
        amount: parameters.amount,
        methodDetails: {
            memo: parameters.memo,
            splits: parameters.methodDetails?.splits,
        },
        recipient: parameters.recipient,
    }).map((transfer) => ({
        ...transfer,
        ...(!transfer.memo ? { allowAnyMemo: true } : {}),
    }));
}
function assertTransferCalls(calls, parameters) {
    const transferCalls = getTransferCalls(calls);
    if (parameters.exactCount && transferCalls.length !== parameters.transfers.length)
        throw new MismatchError('Invalid transaction: no matching payment call found', {
            expectedCalls: String(parameters.transfers.length),
            actualCalls: String(transferCalls.length),
        });
    // Match memo-specific transfers before wildcards to avoid greedy
    // consumption of memo-bearing calls by allowAnyMemo entries.
    const sorted = [...parameters.transfers].sort((a, b) => {
        if (a.memo && !b.memo)
            return -1;
        if (!a.memo && b.memo)
            return 1;
        return 0;
    });
    const used = new Set();
    const matched = [];
    for (const expected of sorted) {
        const matchIndex = transferCalls.findIndex((call, index) => {
            if (used.has(index))
                return false;
            const decoded = decodeTransferCall(call, parameters.currency);
            if (!decoded)
                return false;
            if (!TempoAddress.isEqual(decoded.recipient, expected.recipient))
                return false;
            if (decoded.amount !== expected.amount)
                return false;
            if (expected.memo) {
                return decoded.memo?.toLowerCase() === expected.memo.toLowerCase();
            }
            if (expected.allowAnyMemo)
                return true;
            return decoded.memo === undefined;
        });
        if (matchIndex === -1) {
            throw new MismatchError('Invalid transaction: no matching payment call found', {
                amount: expected.amount,
                currency: parameters.currency,
                recipient: expected.recipient,
            });
        }
        used.add(matchIndex);
        matched.push(decodeTransferCall(transferCalls[matchIndex], parameters.currency));
    }
    return matched;
}
function getTransferCalls(calls) {
    const selectors = calls.map((call) => call.data?.slice(0, 10));
    const offset = selectors[0] === Selectors.approve && selectors[1] === Selectors.swapExactAmountOut ? 2 : 0;
    const transferCalls = calls.slice(offset);
    if (transferCalls.length === 0 ||
        selectors
            .slice(offset)
            .some((selector) => selector !== Selectors.transfer && selector !== Selectors.transferWithMemo)) {
        throw new MismatchError('Invalid transaction: no matching payment call found', {});
    }
    return transferCalls;
}
function decodeTransferCall(call, currency) {
    if (!call.to || !TempoAddress.isEqual(call.to, currency) || !call.data)
        return null;
    try {
        const selector = call.data.slice(0, 10);
        if (selector === Selectors.transfer) {
            const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data });
            const [recipient, amount] = args;
            return { amount: amount.toString(), recipient };
        }
        if (selector === Selectors.transferWithMemo) {
            const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data });
            const [recipient, amount, memo] = args;
            return { amount: amount.toString(), memo, recipient };
        }
    }
    catch {
        return null;
    }
    return null;
}
function assertTransferLogs(receipt, parameters) {
    const transferLogs = parseEventLogs({
        abi: Abis.tip20,
        eventName: 'Transfer',
        logs: receipt.logs,
    }).map((log) => ({ ...log, kind: 'transfer' }));
    const memoLogs = parseEventLogs({
        abi: Abis.tip20,
        eventName: 'TransferWithMemo',
        logs: receipt.logs,
    }).map((log) => ({ ...log, kind: 'memo' }));
    // Prefer memo logs so allowAnyMemo matches TransferWithMemo before Transfer,
    // preserving the memo for challenge binding verification.
    const logs = [...memoLogs, ...transferLogs];
    const used = new Set();
    const matched = [];
    // Match memo-specific transfers before wildcards to avoid greedy
    // consumption of memo-bearing logs by allowAnyMemo entries.
    const sorted = [...parameters.transfers].sort((a, b) => {
        if (a.memo && !b.memo)
            return -1;
        if (!a.memo && b.memo)
            return 1;
        return 0;
    });
    for (const transfer of sorted) {
        const matchIndex = logs.findIndex((log, index) => {
            if (used.has(index))
                return false;
            if (!TempoAddress.isEqual(log.address, parameters.currency))
                return false;
            if (!TempoAddress.isEqual(log.args.from, parameters.sender))
                return false;
            if (!TempoAddress.isEqual(log.args.to, transfer.recipient))
                return false;
            if (log.args.amount.toString() !== transfer.amount)
                return false;
            if (transfer.memo) {
                return log.kind === 'memo' && log.args.memo.toLowerCase() === transfer.memo.toLowerCase();
            }
            if (transfer.allowAnyMemo)
                return log.kind === 'transfer' || log.kind === 'memo';
            return log.kind === 'transfer';
        });
        if (matchIndex === -1) {
            throw new MismatchError('Payment verification failed: no matching transfer found.', {
                amount: transfer.amount,
                currency: parameters.currency,
                recipient: transfer.recipient,
            });
        }
        used.add(matchIndex);
        matched.push(logs[matchIndex]);
    }
    return matched;
}
/** @internal */
function getHashStoreKey(hash) {
    return `mppx:charge:${hash.toLowerCase()}`;
}
/** @internal */
function getProofStoreKey(challengeId) {
    return `mppx:charge:proof:${challengeId}`;
}
/** @internal */
function getSponsoredSenderStoreKey(parameters) {
    return `mppx:charge:sponsor:${parameters.chainId}:${parameters.sender.toLowerCase()}`;
}
async function markHashUsed(store, hash) {
    return store.update(getHashStoreKey(hash), (current) => {
        if (current !== null)
            return { op: 'noop', result: false };
        return { op: 'set', value: Date.now(), result: true };
    });
}
/** @internal */
async function releaseHashUse(store, hash) {
    await store.delete(getHashStoreKey(hash));
}
function parseHashCredentialSource(parameters) {
    const { chainId, source } = parameters;
    if (!source)
        return undefined;
    const parsed = Proof.parsePkhSource(source);
    if (!parsed || (chainId !== undefined && parsed.chainId !== chainId)) {
        throw new MismatchError('Hash credential source is invalid.', {});
    }
    return parsed;
}
/** @internal */
async function markSponsoredSenderInFlight(store, parameters) {
    return store.update(getSponsoredSenderStoreKey(parameters), (current) => {
        if (current !== null)
            return { op: 'noop', result: false };
        return { op: 'set', value: Date.now(), result: true };
    });
}
/** @internal */
async function releaseSponsoredSenderInFlight(store, parameters) {
    await store.delete(getSponsoredSenderStoreKey(parameters));
}
/** @internal */
async function markProofUsed(store, challengeId) {
    return store.update(getProofStoreKey(challengeId), (current) => {
        if (current !== null)
            return { op: 'noop', result: false };
        return { op: 'set', value: Date.now(), result: true };
    });
}
function recoverAuthorizedProofSigner(parameters) {
    const { chainId, challengeId, realm, signature, sourceAddress } = parameters;
    try {
        const envelope = SignatureEnvelope.from(signature);
        const proofHash = hashTypedData({
            domain: Proof.domain(chainId),
            types: Proof.types,
            primaryType: 'Proof',
            message: Proof.message(challengeId, realm),
        });
        if (envelope.type === 'keychain') {
            if (!TempoAddress.isEqual(envelope.userAddress, sourceAddress))
                return null;
            const keychainPayload = envelope.version === 'v2'
                ? keccak256(`0x04${proofHash.slice(2)}${sourceAddress.slice(2)}`)
                : proofHash;
            const signer = SignatureEnvelope.extractAddress({
                payload: keychainPayload,
                signature: envelope.inner,
            });
            const valid = SignatureEnvelope.verify(envelope.inner, {
                address: signer,
                payload: keychainPayload,
            });
            if (!valid)
                return null;
            return signer;
        }
        const signer = SignatureEnvelope.extractAddress({
            payload: proofHash,
            signature: envelope,
        });
        const valid = SignatureEnvelope.verify(envelope, {
            address: signer,
            payload: proofHash,
        });
        if (!valid)
            return null;
        return signer;
    }
    catch {
        return null;
    }
}
async function isActiveAccessKey(client, parameters) {
    try {
        const metadata = await Actions.accessKey.getMetadata(client, parameters);
        const nowSeconds = BigInt(Math.floor(Date.now() / 1000));
        return !metadata.isRevoked && metadata.expiry > nowSeconds;
    }
    catch {
        return false;
    }
}
/** @internal */
function toReceipt(receipt) {
    const { status, transactionHash } = receipt;
    if (status !== 'success') {
        throw new Error(`Transaction reverted: ${transactionHash}`);
    }
    return {
        method: 'tempo',
        status: 'success',
        timestamp: new Date().toISOString(),
        reference: transactionHash,
    };
}
/**
 * Asserts that at least one of the matched payment logs carries a
 * challenge-bound memo nonce (keccak256(challengeId)[0..6] in bytes 25–31).
 * Only checks logs that were matched by `assertTransferLogs`, not the
 * entire receipt — preventing unrelated dust transfers from satisfying
 * the binding.
 * @internal
 */
function assertChallengeBoundMemo(matchedLogs, parameters) {
    const bound = matchedLogs.some((log) => {
        if (log.kind !== 'memo')
            return false;
        if (!Attribution.verifyServer(log.args.memo, parameters.realm))
            return false;
        return Attribution.verifyChallengeBinding(log.args.memo, parameters.challengeId);
    });
    if (!bound)
        throw new MismatchError('Payment verification failed: memo is not bound to this challenge.', {});
}
function assertChallengeBoundCallMemo(matchedCalls, parameters) {
    const bound = matchedCalls.some((call) => {
        if (!call.memo)
            return false;
        const memo = call.memo;
        if (!Attribution.verifyServer(memo, parameters.realm))
            return false;
        return Attribution.verifyChallengeBinding(memo, parameters.challengeId);
    });
    if (!bound)
        throw new MismatchError('Payment verification failed: memo is not bound to this challenge.', {});
}
/** @internal */
class MismatchError extends PaymentError {
    name = 'MismatchError';
    title = 'Verification Failed';
    type = 'https://paymentauth.org/problems/verification-failed';
    constructor(reason, details) {
        super([
            reason.startsWith('Payment verification failed')
                ? reason
                : `Payment verification failed: ${reason}`,
            ...Object.entries(details).map(([k, v]) => `  - ${k}: ${v}`),
        ].join('\n'));
    }
}
//# sourceMappingURL=Charge.js.map