"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Script = void 0;
const bufio_1 = require("@node-dlc/bufio");
const bufio_2 = require("@node-dlc/bufio");
const bufio_3 = require("@node-dlc/bufio");
const crypto_1 = require("@node-dlc/crypto");
const BitcoinError_1 = require("./BitcoinError");
const BitcoinErrorCode_1 = require("./BitcoinErrorCode");
const OpCodes_1 = require("./OpCodes");
const SigHashType_1 = require("./SigHashType");
const Stack_1 = require("./Stack");
function asssertValidSig(sig) {
    const der = sig.slice(0, sig.length - 1);
    const hashtype = sig[sig.length - 1];
    if (!(0, crypto_1.isDERSig)(der)) {
        throw new BitcoinError_1.BitcoinError(BitcoinErrorCode_1.BitcoinErrorCode.SigEncodingInvalid);
    }
    if (!(0, SigHashType_1.isSigHashTypeValid)(hashtype)) {
        throw new BitcoinError_1.BitcoinError(BitcoinErrorCode_1.BitcoinErrorCode.SigHashTypeInvalid, hashtype);
    }
}
function assertValidPubKey(pubkey) {
    if (!(0, crypto_1.validPublicKey)(pubkey)) {
        throw new BitcoinError_1.BitcoinError(BitcoinErrorCode_1.BitcoinErrorCode.PubKeyInvalid);
    }
}
/**
 * Bitcoin Script
 */
class Script {
    /**
     *
     * @param val
     */
    static number(val) {
        // Zero is encoded as OP_0
        if (val === 0 || val === BigInt(0)) {
            return OpCodes_1.OpCode.OP_0;
        }
        // 1-16 is encoded as OP_1 to OP_16
        if (val >= 1 && val <= 16) {
            return 0x50 + Number(val);
        }
        // anything else is encoded as signed-magnitude value
        // and added to the script as raw push bytes
        return Stack_1.Stack.encodeNum(val);
    }
    /**
     * Creates a standard (though no longer used) pay-to-pubkey
     * scriptPubKey using the provided pubkey.
     *
     * P2PK format:
     *      <pubkey> OP_CHECKSIG
     *
     * @param pubkey 33-byte compressed or 65-byte uncompressed SEC
     * encoded pubkey
     */
    static p2pkLock(pubkey) {
        assertValidPubKey(pubkey);
        return new Script(pubkey, OpCodes_1.OpCode.OP_CHECKSIG);
    }
    /**
     * Creates a standard (though no longer used) pay-to-pubkey
     * scriptSig using the provided signature.
     *
     * P2PK format:
     *      <sig>
     *
     * @param sig DER encoded signature + 1-byte sighash type
     */
    static p2pkUnlock(sig) {
        asssertValidSig(sig);
        return new Script(sig);
    }
    /**
     * Creates a standard Pay-to-Public-Key-Hash scriptPubKey by accepting a
     * hash of a public key as input and generating the script in the standard
     * P2PKH script format:
     *   OP_DUP OP_HASH160 <hash160pubkey> OP_EQUALVERIFY OP_CHECKSIG
     *
     * @param value either the 20-byte hash160 of a pubkey or an SEC
     * encoded compressed or uncompressed pubkey
     */
    static p2pkhLock(value) {
        // if not a hash160, then it must be a valid pubkey
        if (value.length !== 20) {
            assertValidPubKey(value);
        }
        // either the hash value or a valid pubkey that needs to be hashed
        const hash160PubKey = value.length === 20 ? value : (0, crypto_1.hash160)(value);
        return new Script(OpCodes_1.OpCode.OP_DUP, OpCodes_1.OpCode.OP_HASH160, hash160PubKey, OpCodes_1.OpCode.OP_EQUALVERIFY, OpCodes_1.OpCode.OP_CHECKSIG);
    }
    /**
     * Creates a standard Pay-to-Public-Key-Hash scriptSig
     * @param sig BIP66 compliant DER encoded signuture + hash byte
     * @param pubkey SEC encoded public key
     */
    static p2pkhUnlock(sig, pubkey) {
        asssertValidSig(sig);
        assertValidPubKey(pubkey);
        return new Script(sig, pubkey);
    }
    /**
     * Creates a standard Pay-to-MultiSig scriptPubKey by accepting m of n
     * public keys as inputs in the format:
     *   OP_<m> <pubkey1> <pubkey2> <pubkey..m> OP_<n> OP_CHECKMULTISIG
     */
    static p2msLock(m, ...pubkeys) {
        // assert all public keys are valid
        for (const pubkey of pubkeys) {
            assertValidPubKey(pubkey);
        }
        // ensure proper number of keys
        if (m < 1 ||
            m > pubkeys.length ||
            pubkeys.length === 0 ||
            pubkeys.length > 20) {
            throw new BitcoinError_1.BitcoinError(BitcoinErrorCode_1.BitcoinErrorCode.MultiSigSetupInvalid);
        }
        return new Script(Script.number(m), ...pubkeys, Script.number(pubkeys.length), OpCodes_1.OpCode.OP_CHECKMULTISIG); // prettier-ignore
    }
    /**
     * Creates a standard Pay-to-MultiSig scripSig using the provided
     * signatures. The signatures must be in the order of the pub keys
     * used in the lock script. This function also correctly adds OP_0
     * as the first element on the stack to ensure the p2ms off-by-one
     * error is correctly accounted for.
     *
     * Each signature must be DER encoded using BIP66 and
     * include a 1-byte sighash type at the end. The builder validates
     * the signatures. As such they will be 10 to 74 bytes.
     *
     * @param pubkeys
     */
    static p2msUnlock(...sigs) {
        // assert all signatures
        for (const sig of sigs) {
            asssertValidSig(sig);
        }
        return new Script(OpCodes_1.OpCode.OP_0, ...sigs); // prettier-ignore
    }
    /**
     * Creates a standard Pay-to-Script-Hash scriptPubKey by accepting a hash of
     * the redeem script as input and generating the P2SH script:
     *   OP_HASH160 <hashScript> OP_EQUAL
     *
     * Accepts the redeem script either as a Script object or as the
     * hash160 of the redeem script. When the hash160 Buffer is provided
     * it will throw if the Buffer is not 20-bytes.
     *
     * @param value can be either the redeem script as a Script type or
     * the hash160 as a 20-byte buffer
     */
    static p2shLock(value) {
        const scriptHash160 = value instanceof Script ? value.hash160() : value;
        if (scriptHash160.length !== 20) {
            throw new BitcoinError_1.BitcoinError(BitcoinErrorCode_1.BitcoinErrorCode.Hash160Invalid, {
                got: scriptHash160.length,
                expected: 20,
            });
        }
        return new Script(OpCodes_1.OpCode.OP_HASH160, scriptHash160, OpCodes_1.OpCode.OP_EQUAL); // prettier-ignore
    }
    static p2shUnlock(redeemScript, ...data) {
        if (data[0] instanceof Script) {
            return new Script(...data[0].cmds, redeemScript.serializeCmds());
        }
        else {
            return new Script(...data, redeemScript.serializeCmds());
        }
    }
    /**
     * Create a standard Pay-to-Witness-PubKey-Hash scriptPubKey by accepting
     * the hash160 of a compressed public key point as input. It is of the
     * format:
     *   OP_0 <hash160_pubkey>
     *
     * @param value either a 20-byte pubkeyhash or a valid pubkey
     */
    static p2wpkhLock(value) {
        // if not a hash160, then it must be a valid pubkey
        if (value.length !== 20) {
            assertValidPubKey(value);
        }
        // either the hash value or a valid pubkey that needs to be hashed
        const hash160PubKey = value.length === 20 ? value : (0, crypto_1.hash160)(value);
        return new Script(OpCodes_1.OpCode.OP_0, hash160PubKey); // prettier-ignore
    }
    /**
     * Create a standard Pay-to-Witness-Script-Hash scriptPubKey by accepting
     * the sha256 of the witness script as input. It is of the format:
     *   OP_0 <sha256_redeem_script>
     */
    static p2wshLock(value) {
        const sha256Script = value instanceof Buffer ? value : (0, crypto_1.sha256)(value.serializeCmds());
        if (sha256Script.length !== 32) {
            throw new BitcoinError_1.BitcoinError(BitcoinErrorCode_1.BitcoinErrorCode.Hash256Invalid);
        }
        return new Script(OpCodes_1.OpCode.OP_0, sha256Script); // prettier-ignore
    }
    /**
     * Parses a stream of bytes representing a Script. The stream must start
     * with a Varint length of Script data. The Script data is then parsed into
     * data blocks or op_codes depending on the meaning of the bytes
     * @param stream
     */
    static parse(reader) {
        // read the length
        const len = reader.readVarInt();
        // read the length of bytes occupied by the script and then pass it
        // through the command parser.
        const buf = reader.readBytes(Number(len));
        const cmds = Script.parseCmds(buf);
        // return the script object with the commands
        return new Script(...cmds);
    }
    /**
     * When supplied with a Buffer of cmds this method will parse the commands
     * into data blocks or op_codes depending on the meaning of the bytes
     * @param buf
     */
    static parseCmds(buf) {
        const br = new bufio_3.BufferReader(buf);
        // commands that were read off the stack
        const cmds = [];
        // loop until all bytes have been read
        while (!br.eof) {
            // read the current command from the stream
            const op = br.readUInt8();
            // data range between 1-75 bytes is OP_PUSHBYTES_xx and we simple
            // read the xx number of bytes off the script
            if (op >= 0x01 && op <= 0x4b) {
                const n = op;
                const bytes = br.readBytes(n);
                cmds.push(bytes);
            }
            // data range between 76 and 255 bytes uses OP_PUSHDATA1 and uses
            // the format with a single byte length and then the n bytes are
            // read from the script
            else if (op === OpCodes_1.OpCode.OP_PUSHDATA1) {
                const n = br.readUInt8();
                cmds.push(br.readBytes(n));
            }
            // data range between 256 and 520 uses OP_PUSHDATA2 and uses the
            // format with two bytes little-endian to determine the n bytes of
            // data of data that need to be read.
            else if (op === OpCodes_1.OpCode.OP_PUSHDATA2) {
                const n = br.readUInt16LE();
                cmds.push(br.readBytes(n));
            }
            // otherwise the value is an opcode that should be added to the cmds
            else {
                cmds.push(op);
            }
        }
        return cmds;
    }
    /**
     * Constructs a Script with the supplied ScriptCmd values
     * @param cmds
     */
    constructor(...cmds) {
        this.cmds = cmds;
    }
    /**
     * Returns true if other script is an exact match of the current script.
     * This requires all data element sto be exact matches and all operations
     * to be exact matches.
     * @param other
     */
    equals(other) {
        if (this.cmds.length !== other.cmds.length)
            return false;
        for (let i = 0; i < this.cmds.length; i++) {
            const l = this.cmds[i];
            const r = other.cmds[i];
            if (Buffer.isBuffer(l) && Buffer.isBuffer(r)) {
                if (!l.equals(r))
                    return false;
            }
            else {
                if (l !== r)
                    return false;
            }
        }
        return true;
    }
    /**
     * Returns a string with the friendly name of the opcode. For data,
     * it returns the value in hexadecimal format.
     */
    toString() {
        return this.cmds
            .map((cmd) => {
            if (Buffer.isBuffer(cmd)) {
                return cmd.toString('hex');
            }
            else {
                return OpCodes_1.OpCode[cmd];
            }
        })
            .join(' ');
    }
    /**
     * Returns a JSON serialization of the Script.
     */
    toJSON() {
        return this.toString();
    }
    /**
     * Serializes the Script to a Buffer by serializing the cmds prefixed with
     * the overall length as a varint. Therefore the format of this method is
     * the format used when encoding a Script and is:
     *
     * [varint]: length
     * [length]: script_cmds
     */
    serialize() {
        // first obtain the length of all commands in the script
        const cmdBuf = this.serializeCmds();
        // capture the length of cmd buffer
        const len = (0, bufio_2.encodeVarInt)(cmdBuf.length);
        // return combined buffer
        return Buffer.concat([len, cmdBuf]);
    }
    /**
     * Serializes the commands to a buffer. This information is the raw
     * serialization and can be directly parsed with the `parseCmds` method.
     */
    serializeCmds() {
        const results = [];
        for (const op of this.cmds) {
            // OP_CODES are just an integers and can just be pushed directly onto
            // the byte array after being converted into a single byte buffer
            if (typeof op === 'number') {
                const opBuf = Buffer.from([op]);
                results.push(opBuf);
            }
            // elements will be represented as a buffer of information and we will
            // use the length to determine how to encode it
            else if (op instanceof Buffer) {
                // between 1 and 75 bytes are OP_PUSHBYTES_XX
                // there is no op_code for these. We first need to push
                // the length of the buffer array though as the operation
                if (op.length >= 1 && op.length <= 75) {
                    results.push(Buffer.from([op.length]));
                    results.push(op);
                }
                // between 76 and 255 uses OP_PUSHDATA1
                // this requires us to push the op_code, a single byte length
                // and finally push the 76-255 bytes of data
                else if (op.length >= 76 && op.length <= 255) {
                    results.push(Buffer.from([OpCodes_1.OpCode.OP_PUSHDATA1])); // op_pushdata1
                    results.push(Buffer.from([op.length])); // single-byte
                    results.push(op);
                }
                // between 256 and 520 uses OP_PUSHDATA2
                // this requires us to push the op_code, a two-byte little-endian number
                // and finally push the 256-520 bytes of data
                else if (op.length >= 256 && op.length <= 520) {
                    results.push(Buffer.from([OpCodes_1.OpCode.OP_PUSHDATA2])); // op_pushdata2
                    results.push((0, bufio_1.bigToBufLE)(BigInt(op.length), 2)); // two-bytes little-endian
                    results.push(op);
                }
                // data longer than 520 is not supported
                else {
                    throw new Error('Data too long');
                }
            }
        }
        // combine all parts of data
        return Buffer.concat(results);
    }
    /**
     * Clone via deep copy
     */
    clone() {
        return new Script(...this.cmds.map((cmd) => {
            if (Buffer.isBuffer(cmd)) {
                return Buffer.from(cmd);
            }
            else {
                return cmd;
            }
        }));
    }
    /**
     * Performs a hash160 on the serialized commands. This is useful for
     * turning a script into a P2SH redeem script.
     */
    hash160() {
        return (0, crypto_1.hash160)(this.serializeCmds());
    }
    /**
     * Performs a sha256 hash on the serialized commands. This is useful
     * fro turning a script into a P2WSH lock script.
     */
    sha256() {
        return (0, crypto_1.sha256)(this.serializeCmds());
    }
}
exports.Script = Script;
//# sourceMappingURL=Script.js.map