Home Reference Source Test

src/main/generic/consensus/base/transaction/Transaction.js

/**
 * @abstract
 */
class Transaction {
    /**
     * @param {Transaction.Format} format
     * @param {Address} sender
     * @param {Account.Type} senderType
     * @param {Address} recipient
     * @param {Account.Type} recipientType
     * @param {number} value
     * @param {number} fee
     * @param {number} validityStartHeight
     * @param {Transaction.Flag | *} flags
     * @param {Uint8Array} data
     * @param {Uint8Array} [proof]
     * @param {number} [networkId]
     */
    constructor(format, sender, senderType, recipient, recipientType, value, fee, validityStartHeight, flags, data, proof, networkId = GenesisConfig.NETWORK_ID) {
        if (!(sender instanceof Address)) throw new Error('Malformed sender');
        if (!NumberUtils.isUint8(senderType)) throw new Error('Malformed sender type');
        if (!(recipient instanceof Address)) throw new Error('Malformed recipient');
        if (!NumberUtils.isUint8(recipientType)) throw new Error('Malformed recipient type');
        if (!NumberUtils.isUint64(value) || value === 0) throw new Error('Malformed value');
        if (!NumberUtils.isUint64(fee)) throw new Error('Malformed fee');
        if (!NumberUtils.isUint32(validityStartHeight)) throw new Error('Malformed validityStartHeight');
        if (!NumberUtils.isUint8(flags) && (flags & ~(Transaction.Flag.ALL)) > 0) throw new Error('Malformed flags');
        if (!(data instanceof Uint8Array) || !(NumberUtils.isUint16(data.byteLength))) throw new Error('Malformed data');
        if (proof && (!(proof instanceof Uint8Array) || !(NumberUtils.isUint16(proof.byteLength)))) throw new Error('Malformed proof');
        if (!NumberUtils.isUint8(networkId)) throw new Error('Malformed networkId');

        /** @type {Transaction.Format} */
        this._format = format;
        /** @type {Address} */
        this._sender = sender;
        /** @type {Account.Type} */
        this._senderType = senderType;
        /** @type {Address} */
        this._recipient = recipient;
        /** @type {Account.Type} */
        this._recipientType = recipientType;
        /** @type {number} */
        this._value = value;
        /** @type {number} */
        this._fee = fee;
        /** @type {number} */
        this._networkId = networkId;
        /** @type {number} */
        this._validityStartHeight = validityStartHeight;
        /** @type {Transaction.Flag | *} */
        this._flags = flags;
        /** @type {Uint8Array} */
        this._data = data;
        /** @type {Uint8Array} */
        this._proof = proof;

        if (this._recipient === Address.CONTRACT_CREATION) this._recipient = this.getContractCreationAddress();
    }

    /**
     * @param {SerialBuffer} buf
     * @return {Transaction}
     */
    static unserialize(buf) {
        const format = /** @type {Transaction.Format} */ buf.readUint8();
        buf.readPos--;

        if (!Transaction.FORMAT_MAP.has(format)) throw new Error('Invalid transaction type');
        return Transaction.FORMAT_MAP.get(format).unserialize(buf);
    }

    /**
     * @param {?SerialBuffer} [buf]
     * @return {SerialBuffer}
     */
    serializeContent(buf) {
        buf = buf || new SerialBuffer(this.serializedContentSize);
        buf.writeUint16(this._data.byteLength);
        buf.write(this._data);
        this._sender.serialize(buf);
        buf.writeUint8(this._senderType);
        this._recipient.serialize(buf);
        buf.writeUint8(this._recipientType);
        buf.writeUint64(this._value);
        buf.writeUint64(this._fee);
        buf.writeUint32(this._validityStartHeight);
        buf.writeUint8(this._networkId);
        buf.writeUint8(this._flags);
        return buf;
    }

    /** @type {number} */
    get serializedContentSize() {
        return /*dataSize*/ 2
            + this._data.byteLength
            + this._sender.serializedSize
            + /*senderType*/ 1
            + this._recipient.serializedSize
            + /*recipientType*/ 1
            + /*value*/ 8
            + /*fee*/ 8
            + /*validityStartHeight*/ 4
            + /*networkId*/ 1
            + /*flags*/ 1;
    }

    /**
     * @param {number} [networkId]
     * @returns {boolean}
     */
    verify(networkId) {
        if (this._valid === undefined) {
            this._valid = this._verify(networkId);
        }
        return this._valid;
    }

    /**
     * @param {number} [networkId]
     * @returns {boolean}
     * @private
     */
    _verify(networkId = GenesisConfig.NETWORK_ID) {
        if (this._networkId !== networkId) {
            Log.w(Transaction, 'Transaction is not valid in this network', this);
            return false;
        }
        // Check that sender != recipient.
        if (this._recipient.equals(this._sender)) {
            Log.w(Transaction, 'Sender and recipient must not match', this);
            return false;
        }
        if (!Account.TYPE_MAP.has(this._senderType) || !Account.TYPE_MAP.has(this._recipientType)) {
            Log.w(Transaction, 'Invalid account type', this);
            return false;
        }
        if (!Account.TYPE_MAP.get(this._senderType).verifyOutgoingTransaction(this)) {
            Log.w(Transaction, 'Invalid for sender', this);
            return false;
        }
        if (!Account.TYPE_MAP.get(this._recipientType).verifyIncomingTransaction(this)) {
            Log.w(Transaction, 'Invalid for recipient', this);
            return false;
        }
        return true;
    }

    /** @type {number} */
    get serializedSize() {
        throw new Error('Getter needs to be overwritten by subclasses');
    }

    /**
     * @param {?SerialBuffer} [buf]
     * @return {SerialBuffer}
     */
    serialize(buf) {
        throw new Error('Method needs to be overwritten by subclasses');
    }

    /**
     * @return {Hash}
     */
    hash() {
        // Exclude the signature, we don't want transactions to be malleable.
        this._hash = this._hash || Hash.light(this.serializeContent());
        return this._hash;
    }

    /**
     * @param {Transaction} o
     * @return {number}
     */
    compare(o) {
        if (this.fee / this.serializedSize > o.fee / o.serializedSize) return -1;
        if (this.fee / this.serializedSize < o.fee / o.serializedSize) return 1;
        if (this.serializedSize > o.serializedSize) return -1;
        if (this.serializedSize < o.serializedSize) return 1;
        if (this.fee > o.fee) return -1;
        if (this.fee < o.fee) return 1;
        if (this.value > o.value) return -1;
        if (this.value < o.value) return 1;
        return this.compareBlockOrder(o);
    }

    /**
     * @param {Transaction} o
     * @return {number}
     */
    compareBlockOrder(o) {
        // This function must return 0 iff this.equals(o).
        const recCompare = this._recipient.compare(o._recipient);
        if (recCompare !== 0) return recCompare;
        if (this._validityStartHeight < o._validityStartHeight) return -1;
        if (this._validityStartHeight > o._validityStartHeight) return 1;
        if (this._fee > o._fee) return -1;
        if (this._fee < o._fee) return 1;
        if (this._value > o._value) return -1;
        if (this._value < o._value) return 1;
        const senderCompare = this._sender.compare(o._sender);
        if (senderCompare !== 0) return senderCompare;
        if (this._recipientType < o._recipientType) return -1;
        if (this._recipientType > o._recipientType) return 1;
        if (this._senderType < o._senderType) return -1;
        if (this._senderType > o._senderType) return 1;
        if (this._flags < o._flags) return -1;
        if (this._flags > o._flags) return 1;
        return BufferUtils.compare(this._data, o._data);
    }

    /**
     * @param {Transaction} o
     * @return {boolean}
     */
    equals(o) {
        // This ignores format and proof to be consistent with hash():
        //   tx1.hash() == tx2.hash() iff tx1.equals(t2)
        return o instanceof Transaction
            && this._sender.equals(o._sender)
            && this._senderType === o._senderType
            && this._recipient.equals(o._recipient)
            && this._recipientType === o._recipientType
            && this._value === o._value
            && this._fee === o._fee
            && this._validityStartHeight === o._validityStartHeight
            && this._networkId === o._networkId
            && this._flags === o._flags
            && BufferUtils.equals(this._data, o._data);
    }

    /**
     * @return {string}
     */
    toString() {
        return `Transaction{`
            + `sender=${this._sender.toBase64()}, `
            + `recipient=${this._recipient.toBase64()}, `
            + `value=${this._value}, `
            + `fee=${this._fee}, `
            + `validityStartHeight=${this._validityStartHeight}, `
            + `networkId=${this._networkId}`
            + `}`;
    }

    toPlain() {
        const data = Account.TYPE_MAP.get(this.recipientType).dataToPlain(this.data);
        data.raw = BufferUtils.toHex(this.data);
        const proof = Account.TYPE_MAP.get(this.senderType).proofToPlain(this.proof);
        proof.raw = BufferUtils.toHex(this.proof);
        return {
            transactionHash: this.hash().toPlain(),
            format: Transaction.Format.toString(this._format),
            sender: this.sender.toPlain(),
            senderType: Account.Type.toString(this.senderType),
            recipient: this.recipient.toPlain(),
            recipientType: Account.Type.toString(this.recipientType),
            value: this.value,
            fee: this.fee,
            feePerByte: this.feePerByte,
            validityStartHeight: this.validityStartHeight,
            network: GenesisConfig.networkIdToNetworkName(this.networkId),
            flags: this.flags,
            data,
            proof,
            size: this.serializedSize,
            valid: this.verify()
        };
    }

    /**
     * @param {object} plain
     * @return {Transaction}
     */
    static fromPlain(plain) {
        if (!plain) throw new Error('Invalid transaction format');
        const format = Transaction.Format.fromAny(plain.format);
        if (!Transaction.FORMAT_MAP.has(format)) throw new Error('Invalid transaction type');
        return Transaction.FORMAT_MAP.get(format).fromPlain(plain);
    }

    /**
     * @param {Transaction|string|object} tx
     * @returns {Transaction}
     */
    static fromAny(tx) {
        if (tx instanceof Transaction) return tx;
        if (typeof tx === 'object') return Transaction.fromPlain(tx);
        if (typeof tx === 'string') return Transaction.unserialize(new SerialBuffer(BufferUtils.fromHex(tx)));
        throw new Error('Invalid transaction format');
    }

    /**
     * @return {Address}
     */
    getContractCreationAddress() {
        const tx = Transaction.unserialize(this.serialize());
        tx._recipient = Address.NULL;
        tx._hash = null;
        return Address.fromHash(tx.hash());
    }

    /** @type {Transaction.Format} */
    get format() {
        return this._format;
    }

    /** @type {Address} */
    get sender() {
        return this._sender;
    }

    /** @type {Account.Type} */
    get senderType() {
        return this._senderType;
    }

    /** @type {Address} */
    get recipient() {
        return this._recipient;
    }

    /** @type {Account.Type} */
    get recipientType() {
        return this._recipientType;
    }

    /** @type {number} */
    get value() {
        return this._value;
    }

    /** @type {number} */
    get fee() {
        return this._fee;
    }

    /** @type {number} */
    get feePerByte() {
        return this._fee / this.serializedSize;
    }

    /** @type {number} */
    get networkId() {
        return this._networkId;
    }

    /** @type {number} */
    get validityStartHeight() {
        return this._validityStartHeight;
    }

    /** @type {number} */
    get flags() {
        return this._flags;
    }

    /**
     * @param {Transaction.Flag} flag
     * @returns {boolean}
     */
    hasFlag(flag) {
        return (this._flags & flag) > 0;
    }

    /** @type {Uint8Array} */
    get data() {
        return this._data;
    }

    /** @type {Uint8Array} */
    get proof() {
        return this._proof;
    }

    // Sender proof is set by the Wallet after signing a transaction.
    /** @type {Uint8Array} */
    set proof(proof) {
        this._proof = proof;
    }
}

/**
 * Enum for Transaction formats.
 * @enum
 */
Transaction.Format = {
    BASIC: 0,
    EXTENDED: 1
};
/**
 * @param {Transaction.Format} format
 */
Transaction.Format.toString = function(format) {
    switch (format) {
        case Transaction.Format.BASIC: return 'basic';
        case Transaction.Format.EXTENDED: return 'extended';
    }
    throw new Error('Invalid transaction format');
};
/**
 * @param {Transaction.Format|string} format
 * @return {Transaction.Format}
 */
Transaction.Format.fromAny = function(format) {
    if (typeof format === 'number') return format;
    switch (format) {
        case 'basic': return Transaction.Format.BASIC;
        case 'extended': return Transaction.Format.EXTENDED;
    }
    throw new Error('Invalid transaction format');
};
/**
 * @enum
 */
Transaction.Flag = {
    NONE: 0,
    CONTRACT_CREATION: 0b1,
    ALL: 0b1
};
/** @type {Map.<Transaction.Format, {unserialize: function(buf: SerialBuffer):Transaction, fromPlain: function(plain:object):Transaction}>} */
Transaction.FORMAT_MAP = new Map();

Class.register(Transaction);