Home Reference Source Test

src/main/generic/consensus/base/account/VestingContract.js

class VestingContract extends Contract {
    /**
     * @param {number} [balance]
     * @param {Address} [owner]
     * @param {number} [vestingStart]
     * @param {number} [vestingStepBlocks]
     * @param {number} [vestingStepAmount]
     * @param {number} [vestingTotalAmount]
     */
    constructor(balance = 0, owner = Address.NULL, vestingStart = 0, vestingStepBlocks = 0, vestingStepAmount = balance, vestingTotalAmount = balance) {
        super(Account.Type.VESTING, balance);
        if (!(owner instanceof Address)) throw new Error('Malformed address');
        if (!NumberUtils.isUint32(vestingStart)) throw new Error('Malformed vestingStart');
        if (!NumberUtils.isUint32(vestingStepBlocks)) throw new Error('Malformed vestingStepBlocks');
        if (!NumberUtils.isUint64(vestingStepAmount)) throw new Error('Malformed vestingStepAmount');
        if (!NumberUtils.isUint64(vestingTotalAmount)) throw new Error('Malformed lowerCap');

        /** @type {Address} */
        this._owner = owner;
        /** @type {number} */
        this._vestingStart = vestingStart;
        /** @type {number} */
        this._vestingStepBlocks = vestingStepBlocks;
        /** @type {number} */
        this._vestingStepAmount = vestingStepAmount;
        /** @type {number} */
        this._vestingTotalAmount = vestingTotalAmount;
    }

    /**
     * @param {number} balance
     * @param {number} blockHeight
     * @param {Transaction} transaction
     */
    static create(balance, blockHeight, transaction) {
        /** @type {number} */
        let vestingStart, vestingStepBlocks, vestingStepAmount, vestingTotalAmount;
        const buf = new SerialBuffer(transaction.data);
        const owner = Address.unserialize(buf);
        vestingTotalAmount = transaction.value;
        switch (transaction.data.length) {
            case Address.SERIALIZED_SIZE + 4:
                // Only block number: vest full amount at that block
                vestingStart = 0;
                vestingStepBlocks = buf.readUint32();
                vestingStepAmount = vestingTotalAmount;
                break;
            case Address.SERIALIZED_SIZE + 16:
                vestingStart = buf.readUint32();
                vestingStepBlocks = buf.readUint32();
                vestingStepAmount = buf.readUint64();
                break;
            case Address.SERIALIZED_SIZE + 24:
                // Create a vesting account with some instantly vested funds or additional funds considered.
                vestingStart = buf.readUint32();
                vestingStepBlocks = buf.readUint32();
                vestingStepAmount = buf.readUint64();
                vestingTotalAmount = buf.readUint64();
                break;
            default:
                throw new Error('Invalid transaction data');
        }
        return new VestingContract(balance, owner, vestingStart, vestingStepBlocks, vestingStepAmount, vestingTotalAmount);
    }

    /**
     * @param {SerialBuffer} buf
     * @return {VestingContract}
     */
    static unserialize(buf) {
        const type = buf.readUint8();
        if (type !== Account.Type.VESTING) throw new Error('Invalid account type');

        const balance = buf.readUint64();
        const owner = Address.unserialize(buf);
        const vestingStart = buf.readUint32();
        const vestingStepBlocks = buf.readUint32();
        const vestingStepAmount = buf.readUint64();
        const vestingTotalAmount = buf.readUint64();
        return new VestingContract(balance, owner, vestingStart, vestingStepBlocks, vestingStepAmount, vestingTotalAmount);
    }

    /**
     * @param {object} plain
     */
    static fromPlain(plain) {
        if (!plain) throw new Error('Invalid account');
        return new VestingContract(plain.balance, Address.fromAny(plain.owner), plain.vestingStart, plain.vestingStepBlocks, plain.vestingStepAmount, plain.vestingTotalAmount);
    }

    /**
     * Serialize this VestingContract object into binary form.
     * @param {?SerialBuffer} [buf] Buffer to write to.
     * @return {SerialBuffer} Buffer from `buf` or newly generated one.
     */
    serialize(buf) {
        buf = buf || new SerialBuffer(this.serializedSize);
        super.serialize(buf);
        this._owner.serialize(buf);
        buf.writeUint32(this._vestingStart);
        buf.writeUint32(this._vestingStepBlocks);
        buf.writeUint64(this._vestingStepAmount);
        buf.writeUint64(this._vestingTotalAmount);
        return buf;
    }

    /**
     * @return {number}
     */
    get serializedSize() {
        return super.serializedSize
            + this._owner.serializedSize
            + /*vestingStart*/ 4
            + /*vestingStepBlocks*/ 4
            + /*vestingStepAmount*/ 8
            + /*vestingTotalAmount*/ 8;
    }

    /** @type {Address} */
    get owner() {
        return this._owner;
    }

    /** @type {number} */
    get vestingStart() {
        return this._vestingStart;
    }

    /** @type {number} */
    get vestingStepBlocks() {
        return this._vestingStepBlocks;
    }

    /** @type {number} */
    get vestingStepAmount() {
        return this._vestingStepAmount;
    }

    /** @type {number} */
    get vestingTotalAmount() {
        return this._vestingTotalAmount;
    }

    toString() {
        return `VestingAccount{balance=${this._balance}, owner=${this._owner.toUserFriendlyAddress()}`;
    }

    /**
     * @returns {object}
     */
    toPlain() {
        const plain = super.toPlain();
        plain.owner = this.owner.toPlain();
        plain.vestingStart = this.vestingStart;
        plain.vestingStepBlocks = this.vestingStepBlocks;
        plain.vestingStepAmount = this.vestingStepAmount;
        plain.vestingTotalAmount = this.vestingTotalAmount;
        return plain;
    }

    /**
     * Check if two Accounts are the same.
     * @param {Account} o Object to compare with.
     * @return {boolean} Set if both objects describe the same data.
     */
    equals(o) {
        return o instanceof VestingContract
            && this._type === o._type
            && this._balance === o._balance
            && this._owner.equals(o._owner)
            && this._vestingStart === o._vestingStart
            && this._vestingStepBlocks === o._vestingStepBlocks
            && this._vestingStepAmount === o._vestingStepAmount
            && this._vestingTotalAmount === o._vestingTotalAmount;
    }

    /**
     * @param {Transaction} transaction
     * @return {boolean}
     */
    static verifyOutgoingTransaction(transaction) {
        const buf = new SerialBuffer(transaction.proof);

        if (!SignatureProof.unserialize(buf).verify(null, transaction.serializeContent())) {
            return false;
        }

        if (buf.readPos !== buf.byteLength) {
            return false;
        }

        return true;
    }

    /**
     * @param {Transaction} transaction
     * @return {boolean}
     */
    static verifyIncomingTransaction(transaction) {
        switch (transaction.data.length) {
            case Address.SERIALIZED_SIZE + 4:
            case Address.SERIALIZED_SIZE + 16:
            case Address.SERIALIZED_SIZE + 24:
                return Contract.verifyIncomingTransaction(transaction);
            default:
                return false;
        }
    }

    /**
     * @param {number} balance
     * @return {Account|*}
     */
    withBalance(balance) {
        return new VestingContract(balance, this._owner, this._vestingStart, this._vestingStepBlocks, this._vestingStepAmount, this._vestingTotalAmount);
    }

    /**
     * @param {Transaction} transaction
     * @param {number} blockHeight
     * @param {TransactionCache} transactionsCache
     * @param {boolean} [revert]
     * @return {Account|*}
     */
    withOutgoingTransaction(transaction, blockHeight, transactionsCache, revert = false) {
        if (!revert) {
            const minCap = this.getMinCap(blockHeight);
            const newBalance = this._balance - transaction.value - transaction.fee;
            if (newBalance < minCap) {
                throw new Account.BalanceError();
            }

            const buf = new SerialBuffer(transaction.proof);
            if (!SignatureProof.unserialize(buf).isSignedBy(this._owner)) {
                throw new Account.ProofError();
            }
        }
        return super.withOutgoingTransaction(transaction, blockHeight, transactionsCache, revert);
    }

    /**
     * @param {Transaction} transaction
     * @param {number} blockHeight
     * @param {boolean} [revert]
     * @return {Account}
     */
    withIncomingTransaction(transaction, blockHeight, revert = false) {
        throw new Error('Illegal incoming transaction');
    }

    /**
     * @param {number} blockHeight
     * @returns {number}
     */
    getMinCap(blockHeight) {
        return this._vestingStepBlocks && this._vestingStepAmount > 0
            ? Math.max(0, this._vestingTotalAmount - Math.floor((blockHeight - this._vestingStart) / this._vestingStepBlocks) * this._vestingStepAmount)
            : 0;
    }


    /**
     * @param {Uint8Array} data
     * @return {object}
     */
    static dataToPlain(data) {
        try {
            let vestingStart, vestingStepBlocks, vestingStepAmount, vestingTotalAmount;
            const buf = new SerialBuffer(data);
            const owner = Address.unserialize(buf);
            switch (transaction.data.length) {
                case Address.SERIALIZED_SIZE + 4:
                    vestingStart = 0;
                    vestingStepBlocks = buf.readUint32();
                    break;
                case Address.SERIALIZED_SIZE + 16:
                    vestingStart = buf.readUint32();
                    vestingStepBlocks = buf.readUint32();
                    vestingStepAmount = buf.readUint64();
                    break;
                case Address.SERIALIZED_SIZE + 24:
                    vestingStart = buf.readUint32();
                    vestingStepBlocks = buf.readUint32();
                    vestingStepAmount = buf.readUint64();
                    vestingTotalAmount = buf.readUint64();
                    break;
                default:
                    throw new Error('Invalid transaction data');
            }
            return {
                owner: owner.toPlain(),
                vestingStart,
                vestingStepBlocks,
                vestingStepAmount,
                vestingTotalAmount
            };
        } catch (e) {
            return Account.dataToPlain(data);
        }
    }

    /**
     * @param {Uint8Array} proof
     * @return {object}
     */
    static proofToPlain(proof) {
        try {
            const signatureProof = SignatureProof.unserialize(new SerialBuffer(proof));
            return {
                signature: signatureProof.signature.toHex(),
                publicKey: signatureProof.publicKey.toHex(),
                signer: signatureProof.publicKey.toAddress().toPlain(),
                pathLength: signatureProof.merklePath.nodes.length
            };
        } catch (e) {
            return Account.proofToPlain(proof);
        }
    }
}

Account.TYPE_MAP.set(Account.Type.VESTING, VestingContract);
Class.register(VestingContract);