"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DlcAcceptContainer = exports.DlcAcceptWithoutSigs = exports.DlcAccept = void 0;
const bitcoin_1 = require("@node-dlc/bitcoin");
const bufio_1 = require("@node-dlc/bufio");
const crypto_1 = require("@node-dlc/crypto");
const bitcoinjs_lib_1 = require("bitcoinjs-lib");
const secp256k1_1 = __importDefault(require("secp256k1"));
const MessageType_1 = require("../MessageType");
const deserializeTlv_1 = require("../serialize/deserializeTlv");
const getTlv_1 = require("../serialize/getTlv");
const util_1 = require("../util");
const BatchFundingGroup_1 = require("./BatchFundingGroup");
const CetAdaptorSignatures_1 = require("./CetAdaptorSignatures");
const FundingInput_1 = require("./FundingInput");
const NegotiationFields_1 = require("./NegotiationFields");
/**
 * DlcAccept contains information about a node and indicates its
 * acceptance of the new DLC, as well as its CET and refund
 * transaction signatures. This is the second step toward creating
 * the funding transaction and closing transactions.
 * Updated to support dlcspecs PR #163 format.
 */
class DlcAccept {
    constructor() {
        /**
         * The type for accept_dlc message. accept_dlc = 42780
         */
        this.type = DlcAccept.type;
        // New fields as per dlcspecs PR #163
        this.protocolVersion = MessageType_1.PROTOCOL_VERSION; // Default to current protocol version
        this.fundingInputs = [];
        /**
         * Flag to indicate if this is a single funded DLC
         * In single funded DLCs, the acceptor provides minimal or no collateral
         */
        this.singleFunded = false;
    }
    /**
     * Creates a DlcAccept from JSON data (e.g., from test vectors)
     * Handles both our internal format and external test vector formats
     * @param json JSON object representing a DLC accept
     */
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
    static fromJSON(json) {
        const instance = new DlcAccept();
        // Helper function to parse DER-encoded signature and extract raw r,s values (64 bytes total)
        const parseDerSignature = (hexSig) => {
            const sigBuffer = Buffer.from(hexSig, 'hex');
            // If it's already 64 bytes, assume it's raw
            if (sigBuffer.length === 64) {
                return sigBuffer;
            }
            // Use secp256k1.signatureImport to parse DER signature
            try {
                const rawSig = secp256k1_1.default.signatureImport(sigBuffer);
                return Buffer.from(rawSig);
            }
            catch (ex) {
                throw new Error(`Invalid DER signature: ${ex.message}`);
            }
        };
        // Handle both internal and external field naming conventions
        instance.protocolVersion =
            json.protocolVersion || json.protocol_version || MessageType_1.PROTOCOL_VERSION;
        instance.temporaryContractId = Buffer.from(json.temporaryContractId ||
            json.tempContractId ||
            json.temporary_contract_id, 'hex');
        instance.acceptCollateral = (0, util_1.toBigInt)(json.acceptCollateral ||
            json.acceptCollateralSatoshis ||
            json.accept_collateral);
        // Handle field name variations between formats
        instance.fundingPubkey = Buffer.from(json.fundingPubkey || json.fundingPubKey || json.funding_pubkey, 'hex');
        instance.payoutSpk = Buffer.from(json.payoutSpk || json.payoutSPK || json.payout_spk, 'hex');
        instance.payoutSerialId = (0, util_1.toBigInt)(json.payoutSerialId || json.payout_serial_id);
        instance.changeSpk = Buffer.from(json.changeSpk || json.changeSPK || json.change_spk, 'hex');
        instance.changeSerialId = (0, util_1.toBigInt)(json.changeSerialId || json.change_serial_id);
        // Parse FundingInputs
        instance.fundingInputs = DlcAccept.parseFundingInputsFromJSON(json.fundingInputs || json.funding_inputs || []);
        // Parse CET adaptor signatures
        instance.cetAdaptorSignatures = DlcAccept.parseCetAdaptorSignaturesFromJSON(json.cetAdaptorSignatures || json.cet_adaptor_signatures);
        // Parse refund signature - handle DER encoding
        const refundSigHex = json.refundSignature || json.refund_signature;
        instance.refundSignature = parseDerSignature(refundSigHex);
        // Parse optional negotiation fields
        if (json.negotiationFields || json.negotiation_fields) {
            instance.negotiationFields = DlcAccept.parseNegotiationFieldsFromJSON(json.negotiationFields || json.negotiation_fields);
        }
        return instance;
    }
    /**
     * Parses FundingInputs from JSON
     * @param fundingInputsJson Array of JSON objects representing funding inputs
     */
    static parseFundingInputsFromJSON(
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    fundingInputsJson) {
        return fundingInputsJson.map((inputJson) => {
            // Use the existing FundingInput.fromJSON method which handles all the field mapping correctly
            return FundingInput_1.FundingInput.fromJSON(inputJson);
        });
    }
    /**
     * Parses CetAdaptorSignatures from JSON
     * @param cetSigsJson JSON object representing CET adaptor signatures
     */
    static parseCetAdaptorSignaturesFromJSON(
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    cetSigsJson) {
        const instance = new CetAdaptorSignatures_1.CetAdaptorSignatures();
        if (cetSigsJson.ecdsaAdaptorSignatures ||
            cetSigsJson.ecdsa_adaptor_signatures) {
            const ecdsaSigs = cetSigsJson.ecdsaAdaptorSignatures ||
                cetSigsJson.ecdsa_adaptor_signatures;
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            instance.sigs = ecdsaSigs.map((sig) => {
                // The test vectors use 'signature' field, but our internal format uses encryptedSig/dleqProof
                // Adaptor signatures have different format than regular ECDSA (65 bytes + 97 bytes)
                const sigBuffer = Buffer.from(sig.signature, 'hex');
                return {
                    encryptedSig: sigBuffer.slice(0, 65),
                    dleqProof: sigBuffer.length > 65 ? sigBuffer.slice(65, 162) : Buffer.alloc(97), // Next 97 bytes or empty
                };
            });
        }
        return instance;
    }
    /**
     * Parses NegotiationFields from JSON
     * @param negotiationJson JSON object representing negotiation fields
     */
    static parseNegotiationFieldsFromJSON(
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    negotiationJson) {
        return NegotiationFields_1.NegotiationFields.fromJSON(negotiationJson);
    }
    /**
     * Deserializes an accept_dlc message
     * @param buf
     */
    static deserialize(buf, parseCets = true) {
        const instance = new DlcAccept();
        const reader = new bufio_1.BufferReader(buf);
        reader.readUInt16BE(); // read type
        // New fields as per dlcspecs PR #163
        instance.protocolVersion = reader.readUInt32BE();
        instance.temporaryContractId = reader.readBytes(32);
        instance.acceptCollateral = reader.readUInt64BE();
        instance.fundingPubkey = reader.readBytes(33);
        const payoutSpkLen = reader.readUInt16BE();
        instance.payoutSpk = reader.readBytes(payoutSpkLen);
        instance.payoutSerialId = reader.readUInt64BE();
        // Changed from u16 to bigsize as per dlcspecs PR #163
        const fundingInputsLen = Number(reader.readBigSize());
        for (let i = 0; i < fundingInputsLen; i++) {
            // FundingInput body is serialized directly without TLV wrapper in rust-dlc format
            const fundingInput = FundingInput_1.FundingInput.deserializeBody(reader.buffer.subarray(reader.position));
            instance.fundingInputs.push(fundingInput);
            // Skip past the FundingInput we just read
            const fundingInputLength = fundingInput.serializeBody().length;
            reader.position += fundingInputLength;
        }
        const changeSpkLen = reader.readUInt16BE();
        instance.changeSpk = reader.readBytes(changeSpkLen);
        instance.changeSerialId = reader.readUInt64BE();
        if (parseCets) {
            // Read CET adaptor signatures directly to match serialize format (no TLV wrapping)
            instance.cetAdaptorSignatures = CetAdaptorSignatures_1.CetAdaptorSignatures.deserialize(reader.buffer.subarray(reader.position));
            // Skip past the CET adaptor signatures we just read
            const cetLength = instance.cetAdaptorSignatures.serialize().length;
            reader.position += cetLength;
        }
        else {
            instance.cetAdaptorSignatures = new CetAdaptorSignatures_1.CetAdaptorSignatures();
        }
        instance.refundSignature = reader.readBytes(64);
        // negotiation_fields is now optional as per dlcspecs PR #163
        // Check if there's enough data left for the new format fields (backward compatibility)
        // The old format ends exactly after refundSignature. If there's more data, it should be new format.
        const remainingBytes = reader.buffer.length - reader.position;
        if (remainingBytes > 0) {
            // Only try to parse new fields if we have a reasonable amount of extra data
            // A single stray byte is likely not valid new format data
            if (remainingBytes >= 1) {
                try {
                    const hasNegotiationFields = reader.readUInt8();
                    if (hasNegotiationFields === 0x01) {
                        instance.negotiationFields = NegotiationFields_1.NegotiationFields.deserialize((0, getTlv_1.getTlv)(reader));
                    }
                    // Parse TLV stream as per dlcspecs PR #163
                    // Only continue if there's still data left after the hasNegotiationFields flag
                    while (reader.position < reader.buffer.length) {
                        const buf = (0, getTlv_1.getTlv)(reader);
                        const tlvReader = new bufio_1.BufferReader(buf);
                        const { type } = (0, deserializeTlv_1.deserializeTlv)(tlvReader);
                        switch (Number(type)) {
                            case MessageType_1.MessageType.BatchFundingGroup:
                                if (!instance.batchFundingGroups) {
                                    instance.batchFundingGroups = [];
                                }
                                instance.batchFundingGroups.push(BatchFundingGroup_1.BatchFundingGroup.deserialize(buf));
                                break;
                            default:
                                // Store unknown TLVs for future compatibility
                                if (!instance.unknownTlvs) {
                                    instance.unknownTlvs = [];
                                }
                                instance.unknownTlvs.push({ type: Number(type), data: buf });
                                break;
                        }
                    }
                }
                catch (error) {
                    // If parsing new format fails, assume it's old format and ignore the extra bytes
                    // This provides backward compatibility for malformed or old format data
                    // Silently ignore parsing errors for backward compatibility
                }
            }
        }
        // Auto-detect single funded DLCs based on minimal acceptCollateral
        // In single funded DLCs, acceptCollateral is typically 0 or very small
        if (instance.acceptCollateral === BigInt(0)) {
            instance.singleFunded = true;
        }
        return instance;
    }
    /**
     * Marks this DLC accept as single funded
     * For single funded DLCs, acceptCollateral is typically 0 or minimal
     */
    markAsSingleFunded() {
        this.singleFunded = true;
    }
    /**
     * Checks if this DLC accept is for a single funded DLC
     * @returns True if this is a single funded DLC
     */
    isSingleFunded() {
        return this.singleFunded;
    }
    /**
     * Get funding, change and payout address from DlcAccept
     * @param network Bitcoin Network
     * @returns {IDlcAcceptAddresses}
     */
    getAddresses(network) {
        const fundingSPK = bitcoin_1.Script.p2wpkhLock((0, crypto_1.hash160)(this.fundingPubkey))
            .serialize()
            .slice(1);
        const fundingAddress = bitcoinjs_lib_1.address.fromOutputScript(fundingSPK, network);
        const changeAddress = bitcoinjs_lib_1.address.fromOutputScript(this.changeSpk, network);
        const payoutAddress = bitcoinjs_lib_1.address.fromOutputScript(this.payoutSpk, network);
        return {
            fundingAddress,
            changeAddress,
            payoutAddress,
        };
    }
    /**
     * Validates correctness of all fields
     * Updated validation rules as per dlcspecs PR #163
     * https://github.com/discreetlogcontracts/dlcspecs/blob/master/Protocol.md#the-accept_dlc-message
     * @throws Will throw an error if validation fails
     */
    validate() {
        // 1. Type is set automatically in class
        // 2. protocol_version validation
        if (this.protocolVersion !== MessageType_1.PROTOCOL_VERSION) {
            throw new Error(`Unsupported protocol version: ${this.protocolVersion}, expected: ${MessageType_1.PROTOCOL_VERSION}`);
        }
        // 3. temporary_contract_id must match the one from offer_dlc
        if (!this.temporaryContractId || this.temporaryContractId.length !== 32) {
            throw new Error('temporaryContractId must be 32 bytes');
        }
        // 4. payout_spk and change_spk must be standard script pubkeys
        try {
            bitcoinjs_lib_1.address.fromOutputScript(this.payoutSpk);
        }
        catch (e) {
            throw new Error('payoutSpk is invalid');
        }
        try {
            bitcoinjs_lib_1.address.fromOutputScript(this.changeSpk);
        }
        catch (e) {
            throw new Error('changeSpk is invalid');
        }
        // 5. funding_pubkey must be a valid secp256k1 pubkey in compressed format
        // https://github.com/bitcoin/bips/blob/master/bip-0137.mediawiki#background-on-ecdsa-signatures
        if (secp256k1_1.default.publicKeyVerify(Buffer.from(this.fundingPubkey))) {
            if (this.fundingPubkey[0] != 0x02 && this.fundingPubkey[0] != 0x03) {
                throw new Error('fundingPubkey must be in compressed format');
            }
        }
        else {
            throw new Error('fundingPubkey is not a valid secp256k1 key');
        }
        // 6. inputSerialId must be unique for each input
        const inputSerialIds = this.fundingInputs.map((input) => input.inputSerialId);
        if (new Set(inputSerialIds).size !== inputSerialIds.length) {
            throw new Error('inputSerialIds must be unique');
        }
        // 7. Ensure funding inputs are segwit
        this.fundingInputs.forEach((input) => input.validate());
        // 8. validate funding amount
        const fundingAmount = this.fundingInputs.reduce((acc, fundingInput) => {
            const input = fundingInput;
            return acc + input.prevTx.outputs[input.prevTxVout].value.sats;
        }, BigInt(0));
        if (this.isSingleFunded()) {
            // For single funded DLCs, acceptor may provide minimal or no collateral
            // fundingAmount should be >= acceptCollateral (allowing for 0 collateral case)
            if (this.acceptCollateral > 0 && fundingAmount < this.acceptCollateral) {
                throw new Error('For single funded DLCs, fundingAmount must be at least acceptCollateral');
            }
        }
        else {
            // For regular DLCs, funding amount must be greater than accept collateral
            if (this.acceptCollateral >= fundingAmount) {
                throw new Error('fundingAmount must be greater than acceptCollateral');
            }
        }
    }
    /**
     * Converts accept_dlc to JSON (canonical rust-dlc format)
     */
    toJSON() {
        // Convert raw signature back to DER format for canonical rust-dlc JSON
        const derRefundSignature = secp256k1_1.default.signatureExport(this.refundSignature);
        return {
            protocolVersion: this.protocolVersion,
            temporaryContractId: this.temporaryContractId.toString('hex'),
            acceptCollateral: (0, util_1.bigIntToNumber)(this.acceptCollateral),
            fundingPubkey: this.fundingPubkey.toString('hex'),
            payoutSpk: this.payoutSpk.toString('hex'),
            payoutSerialId: (0, util_1.bigIntToNumber)(this.payoutSerialId),
            fundingInputs: this.fundingInputs.map((input) => input.toJSON()),
            changeSpk: this.changeSpk.toString('hex'),
            changeSerialId: (0, util_1.bigIntToNumber)(this.changeSerialId),
            cetAdaptorSignatures: this.cetAdaptorSignatures.toJSON(),
            refundSignature: Buffer.from(derRefundSignature).toString('hex'),
            negotiationFields: this.negotiationFields?.toJSON(),
        };
    }
    /**
     * Serializes the accept_dlc message into a Buffer
     * Updated serialization format as per dlcspecs PR #163
     */
    serialize() {
        const writer = new bufio_1.BufferWriter();
        writer.writeUInt16BE(this.type);
        // New fields as per dlcspecs PR #163
        writer.writeUInt32BE(this.protocolVersion);
        writer.writeBytes(this.temporaryContractId);
        writer.writeUInt64BE(this.acceptCollateral);
        writer.writeBytes(this.fundingPubkey);
        writer.writeUInt16BE(this.payoutSpk.length);
        writer.writeBytes(this.payoutSpk);
        writer.writeUInt64BE(this.payoutSerialId);
        // Changed from u16 to bigsize as per dlcspecs PR #163
        writer.writeBigSize(this.fundingInputs.length);
        for (const fundingInput of this.fundingInputs) {
            // Use serializeBody() to match rust-dlc behavior - funding inputs in vec are serialized without TLV wrapper
            writer.writeBytes(fundingInput.serializeBody());
        }
        writer.writeUInt16BE(this.changeSpk.length);
        writer.writeBytes(this.changeSpk);
        writer.writeUInt64BE(this.changeSerialId);
        writer.writeBytes(this.cetAdaptorSignatures.serialize());
        writer.writeBytes(this.refundSignature);
        // negotiation_fields is now optional as per dlcspecs PR #163
        if (this.negotiationFields) {
            writer.writeUInt8(0x01); // present
            writer.writeBytes(this.negotiationFields.serialize());
        }
        else {
            writer.writeUInt8(0x00); // absent
        }
        // TLV stream as per dlcspecs PR #163
        if (this.batchFundingGroups)
            this.batchFundingGroups.forEach((fundingInfo) => writer.writeBytes(fundingInfo.serialize()));
        // Write unknown TLVs for forward compatibility
        if (this.unknownTlvs) {
            this.unknownTlvs.forEach((tlv) => {
                writer.writeBytes(tlv.data);
            });
        }
        return writer.toBuffer();
    }
    withoutSigs() {
        return new DlcAcceptWithoutSigs(this.protocolVersion, this.temporaryContractId, this.acceptCollateral, this.fundingPubkey, this.payoutSpk, this.payoutSerialId, this.fundingInputs, this.changeSpk, this.changeSerialId, this.negotiationFields, this.batchFundingGroups);
    }
}
exports.DlcAccept = DlcAccept;
DlcAccept.type = MessageType_1.MessageType.DlcAccept;
class DlcAcceptWithoutSigs {
    constructor(protocolVersion, temporaryContractId, acceptCollateral, fundingPubkey, payoutSpk, payoutSerialId, fundingInputs, changeSpk, changeSerialId, negotiationFields, batchFundingGroups) {
        this.protocolVersion = protocolVersion;
        this.temporaryContractId = temporaryContractId;
        this.acceptCollateral = acceptCollateral;
        this.fundingPubkey = fundingPubkey;
        this.payoutSpk = payoutSpk;
        this.payoutSerialId = payoutSerialId;
        this.fundingInputs = fundingInputs;
        this.changeSpk = changeSpk;
        this.changeSerialId = changeSerialId;
        this.negotiationFields = negotiationFields;
        this.batchFundingGroups = batchFundingGroups;
    }
}
exports.DlcAcceptWithoutSigs = DlcAcceptWithoutSigs;
class DlcAcceptContainer {
    constructor() {
        this.accepts = [];
    }
    /**
     * Adds a DlcAccept to the container.
     * @param accept The DlcAccept to add.
     */
    addAccept(accept) {
        this.accepts.push(accept);
    }
    /**
     * Returns all DlcAccepts in the container.
     * @returns An array of DlcAccept instances.
     */
    getAccepts() {
        return this.accepts;
    }
    /**
     * Serializes all DlcAccepts in the container to a Buffer.
     * @returns A Buffer containing the serialized DlcAccepts.
     */
    serialize() {
        const writer = new bufio_1.BufferWriter();
        // Write the number of accepts in the container first.
        writer.writeBigSize(this.accepts.length);
        // Serialize each accept and write it.
        this.accepts.forEach((accept) => {
            const serializedAccept = accept.serialize();
            // Optionally, write the length of the serialized accept for easier deserialization.
            writer.writeBigSize(serializedAccept.length);
            writer.writeBytes(serializedAccept);
        });
        return writer.toBuffer();
    }
    /**
     * Deserializes a Buffer into a DlcAcceptContainer with DlcAccepts.
     * @param buf The Buffer to deserialize.
     * @returns A DlcAcceptContainer instance.
     */
    static deserialize(buf, parseCets = true) {
        const reader = new bufio_1.BufferReader(buf);
        const container = new DlcAcceptContainer();
        const acceptsCount = reader.readBigSize();
        for (let i = 0; i < acceptsCount; i++) {
            const acceptLength = reader.readBigSize();
            const acceptBuf = reader.readBytes(Number(acceptLength));
            const accept = DlcAccept.deserialize(acceptBuf, parseCets);
            container.addAccept(accept);
        }
        return container;
    }
}
exports.DlcAcceptContainer = DlcAcceptContainer;
//# sourceMappingURL=DlcAccept.js.map