예제 #1
0
    def condition_get(self, height=None):
        """
        Get the latest (coin) mint condition or the (coin) mint condition at the specified block height.

        @param height: if defined the block height at which to look up the (coin) mint condition (if none latest block will be used)
        """
        # define the endpoint
        endpoint = "/explorer/mintcondition"
        if height is not None:
            if not isinstance(height, (int, str)):
                raise TypeError("invalid block height given")
            height = int(height)
            endpoint += "/%d" % (height)

        # get the mint condition
        resp = self._client.explorer_get(endpoint=endpoint)
        resp = json_loads(resp)

        try:
            # return the decoded mint condition
            return ConditionTypes.from_json(obj=resp['mintcondition'])
        except KeyError as exc:
            # return a KeyError as an invalid Explorer Response
            raise tfchain.errors.ExplorerInvalidResponse(
                str(exc), endpoint, resp) from exc
예제 #2
0
    def fund(self, amount, source=None):
        """
        Fund the specified amount with the available outputs of this wallet's balance.
        """
        # collect addresses and multisig addresses
        addresses = set()
        refund = None
        if source == None:
            for co in self.outputs_available:
                addresses.add(co.condition.unlockhash.__str__())
            for co in self.outputs_unconfirmed_available:
                addresses.add(co.condition.unlockhash.__str__())
        else:
            # if only one address is given, transform it into an acceptable list
            if not isinstance(source, list) and not jsobj.is_js_arr(source):
                if isinstance(source, str):
                    source = UnlockHash.from_json(source)
                elif not isinstance(source, UnlockHash):
                    raise TypeError(
                        "cannot add source address from type {}".format(
                            type(source)))
                source = [source]
            # add one or multiple personal/multisig addresses
            for value in source:
                if isinstance(value, str):
                    value = UnlockHash.from_json(value)
                elif not isinstance(value, UnlockHash):
                    raise TypeError(
                        "cannot add source address from type {}".format(
                            type(value)))
                elif value.uhtype.__eq__(UnlockHashType.PUBLIC_KEY):
                    addresses.add(value)
                else:
                    raise TypeError(
                        "cannot add source address with unsupported UnlockHashType {}"
                        .format(value.uhtype))
            if len(source) == 1:
                if source[0].uhtype.__eq__(UnlockHashType.PUBLIC_KEY):
                    refund = ConditionTypes.unlockhash_new(
                        unlockhash=source[0])

        # ensure at least one address is defined
        if len(addresses) == 0:
            raise tferrors.InsufficientFunds(
                "insufficient funds in this wallet")

        # if personal addresses are given, try to use these first
        # as these are the easiest kind to deal with
        if len(addresses) == 0:
            outputs, collected = ([], Currency())  # start with nothing
        else:
            outputs, collected = self._fund_individual(amount, addresses)

        if collected.greater_than_or_equal_to(amount):
            # if we already have sufficient, we stop now
            return ([CoinInput.from_coin_output(co)
                     for co in outputs], collected.minus(amount), refund)
        raise tferrors.InsufficientFunds(
            "not enough funds available in the wallet to fund the requested amount"
        )
예제 #3
0
    def drain(self,
              recipient,
              miner_fee,
              unconfirmed=False,
              data=None,
              lock=None):
        """
        add all available outputs into as many transactions as required,
        by default only confirmed outputs are used, if unconfirmed=True
        it will use unconfirmed available outputs as well.

        Result can be an empty list if no outputs were available.

        @param recipient: required recipient towards who the drained coins will be sent
        @param the miner fee to be added to all sent transactions
        @param unconfirmed: optionally drain unconfirmed (available) outputs as well
        @param data: optional data that can be attached ot the created transactions (str or bytes), with a max length of 83
        @param lock: optional lock that can be attached to the sent coin outputs
        """
        # define recipient
        recipient = ConditionTypes.from_recipient(recipient, lock=lock)

        # validate miner fee
        if not isinstance(miner_fee, Currency):
            raise TypeError("miner fee has to be a currency")
        if miner_fee.__eq__(0):
            raise ValueError("a non-zero miner fee has to be defined")

        # collect all transactions in one list
        txns = []

        # collect all confirmed (available) outputs
        outputs = self.outputs_available
        if unconfirmed:
            # if also the unconfirmed_avaialble) outputs are desired, let's add them as well
            outputs += self.outputs_unconfirmed_available
        # drain all outputs
        while len(outputs) > 0:
            txn = transactions.new()
            txn.data = data
            txn.miner_fee_add(miner_fee)
            # select maximum _MAX_RIVINE_TRANSACTION_INPUTS outputs
            n = min(len(outputs), _MAX_RIVINE_TRANSACTION_INPUTS)
            used_outputs = outputs[:n]
            outputs = outputs[
                n:]  # and update our output list, so we do not double spend
            # compute amount, minus minimum fee and add our only output
            amount = sum([co.value for co in used_outputs]) - miner_fee
            txn.coin_output_add(condition=recipient, value=amount)
            # add the coin inputs
            txn.coin_inputs = [
                CoinInput.from_coin_output(co) for co in used_outputs
            ]
            # append the transaction
            txns.append(txn)

        # return all created transactions, if any
        return txns
예제 #4
0
 def cb(result):
     _, resp = result
     try:
         # return the decoded mint condition
         return ConditionTypes.from_json(obj=resp['mintcondition'])
     except KeyError as exc:
         # return a KeyError as an invalid Explorer Response
         raise tferrors.ExplorerInvalidResponse(str(exc), endpoint,
                                                resp) from exc
예제 #5
0
 def fee_payout_address(self, value):
     if isinstance(value, str):
         self._fee_payout_address = ConditionTypes.UnlockHash.from_json(
             value)
     elif isinstance(value, ConditionTypes.UnlockHash):
         self._fee_payout_address = ConditionTypes.UnlockHash(
             uhtype=value.uhtype, uhhash=value.hash)
     else:
         raise TypeError(
             "invalid type of fee_payout_address value: {} ({})".format(
                 value, type(value)))
예제 #6
0
 def _from_json_data_object(self, data):
     self._nonce = BinaryData.from_json(data.get('nonce', ''),
                                        strencoding='base64')
     self._mint_condition = ConditionTypes.from_json(
         data.get('mintcondition', {}))
     self._mint_fulfillment = FulfillmentTypes.from_json(
         data.get('mintfulfillment', {}))
     self._miner_fees = [
         Currency.from_json(fee) for fee in data.get('minerfees', []) or []
     ]
     self._data = BinaryData.from_json(data.get('arbitrarydata', None)
                                       or '',
                                       strencoding='base64')
예제 #7
0
 def coin_outputs(self):
     """
     Coin outputs of this Transaction,
     funded by the Transaction's coin inputs.
     """
     outputs = []
     if self.fee_payout_address != None and len(self.miner_fees) > 0:
         amount = Currency.sum(*self.miner_fees)
         condition = ConditionTypes.from_recipient(self.fee_payout_address)
         outputs.append(
             CoinOutput(value=amount,
                        condition=condition,
                        id=self._fee_payout_id,
                        is_fee=True))
     return jsarr.concat(outputs, self._custom_coin_outputs_getter())
예제 #8
0
    def unlockhash_get(self, target):
        """
        Get all transactions linked to the given unlockhash (target),
        as well as other information such as the multisig addresses linked to the given unlockhash (target).

        target can be any of:
            - None: unlockhash of the Free-For-All wallet will be used
            - str (or unlockhash/bytes/bytearray): target is assumed to be the unlockhash of a personal wallet
            - list: target is assumed to be the addresses of a MultiSig wallet where all owners (specified as a list of addresses) have to sign
            - tuple (addresses, sigcount): target is a sigcount-of-addresscount MultiSig wallet

        @param target: the target wallet to look up transactions for in the explorer, see above for more info
        """
        unlockhash = ConditionTypes.from_recipient(target).unlockhash.__str__()
        endpoint = "/explorer/hashes/" + unlockhash

        def catch_no_content(reason):
            if isinstance(reason, tferrors.ExplorerNoContent):
                return ExplorerUnlockhashResult(
                    unlockhash=UnlockHash.from_json(unlockhash),
                    transactions=[],
                    multisig_addresses=None,
                    erc20_info=None,
                )
            # pass on any other reason
            raise reason

        def cb(result):
            _, resp = result
            try:
                if resp['hashtype'] != 'unlockhash':
                    raise tferrors.ExplorerInvalidResponse(
                        "expected hash type 'unlockhash' not '{}'".format(
                            resp['hashtype']), endpoint, resp)
                # parse the transactions
                transactions = []
                resp_transactions = resp['transactions']
                if resp_transactions != None and jsobj.is_js_arr(
                        resp_transactions):
                    for etxn in resp_transactions:
                        # parse the explorer transaction
                        transaction = self._transaction_from_explorer_transaction(
                            etxn, endpoint=endpoint, resp=resp)
                        # append the transaction to the list of transactions
                        transactions.append(transaction)
                # collect all multisig addresses
                multisig_addresses = [
                    UnlockHash.from_json(obj=uh)
                    for uh in resp.get_or('multisigaddresses', None) or []
                ]
                for addr in multisig_addresses:
                    if addr.uhtype.__ne__(UnlockHashType.MULTI_SIG):
                        raise tferrors.ExplorerInvalidResponse(
                            "invalid unlock hash type {} for MultiSignature Address (expected: 3)"
                            .format(addr.uhtype.value), endpoint, resp)
                erc20_info = None
                if 'erc20info' in resp:
                    info = resp['erc20info']
                    erc20_info = ERC20AddressInfo(
                        address_tft=UnlockHash.from_json(info['tftaddress']),
                        address_erc20=ERC20Address.from_json(
                            info['erc20address']),
                        confirmations=int(info['confirmations']),
                    )

                # sort the transactions by height
                def txn_arr_sort(a, b):
                    height_a = pow(2, 64) if a.height < 0 else a.height
                    height_b = pow(2, 64) if b.height < 0 else b.height
                    if height_a < height_b:
                        return -1
                    if height_a > height_b:
                        return 1
                    tx_order_a = pow(
                        2,
                        64) if a.transaction_order < 0 else a.transaction_order
                    tx_order_b = pow(
                        2,
                        64) if b.transaction_order < 0 else b.transaction_order
                    if tx_order_a < tx_order_b:
                        return -1
                    if tx_order_a > tx_order_b:
                        return 1
                    return 0

                transactions = jsarr.sort(transactions,
                                          txn_arr_sort,
                                          reverse=True)

                # return explorer data for the unlockhash
                return ExplorerUnlockhashResult(
                    unlockhash=UnlockHash.from_json(unlockhash),
                    transactions=transactions,
                    multisig_addresses=multisig_addresses,
                    erc20_info=erc20_info,
                )
            except KeyError as exc:
                # return a KeyError as an invalid Explorer Response
                raise tferrors.ExplorerInvalidResponse(str(exc), endpoint,
                                                       resp) from exc

        # fetch timestamps seperately
        # TODO: make a pull request in Rivine to return timestamps together with regular results,
        #       as it is rediculous to have to do this
        def fetch_transacton_block(result):
            transactions = {}
            for transaction in result.transactions:
                if not transaction.unconfirmed:
                    bid = transaction.blockid.__str__()
                    if bid not in transactions:
                        transactions[bid] = []
                    transactions[bid].append(transaction)
            if len(transactions) == 0:
                return result  # return as is, nothing to do

            def generator():
                for blockid in jsobj.get_keys(transactions):
                    yield self._block_get_by_hash(blockid)

            def result_cb(block_result):
                _, block = block_result
                for transaction in transactions[block.get_or('blockid', '')]:
                    _assign_block_properties_to_transacton(transaction, block)

            def aggregate():
                return result

            return jsasync.chain(
                jsasync.promise_pool_new(generator, cb=result_cb), aggregate)

        return jsasync.catch_promise(
            jsasync.chain(self.explorer_get(endpoint=endpoint), cb,
                          fetch_transacton_block), catch_no_content)
예제 #9
0
 def from_json(cls, obj):
     return cls(
         value=Blockstake.from_json(obj['value']),
         condition=ConditionTypes.from_json(obj['condition']))
예제 #10
0
 def from_json(cls, obj):
     return cls(
         value=Currency.from_json(obj['value']),
         condition=ConditionTypes.from_json(obj['condition']))
예제 #11
0
def test_conditiontypes():
    def test_encoded(encoder, obj, expected):
        encoder.add(obj)
        output = encoder.data.hex()
        if expected != output:
            msg = "{} != {}".format(expected, output)
            raise Exception("unexpected encoding result: " + msg)

    def test_sia_encoded(obj, expected):
        test_encoded(encoder_sia_get(), obj, expected)

    def test_rivine_encoded(obj, expected):
        test_encoded(encoder_rivine_get(), obj, expected)

    # Nil conditions are supported
    for n_json in [{}, {"type": 0}, {"type": 0, "data": None}, {"type": 0, "data": {}}]:
        cn = ConditionTypes.from_json(n_json)
        assert cn.json() == {"type": 0}
        test_sia_encoded(cn, '000000000000000000')
        test_rivine_encoded(cn, '0000')
        assert str(
            cn.unlockhash) == '000000000000000000000000000000000000000000000000000000000000000000000000000000'

    # UnlockHash conditions are supported
    uh_json = {"type": 1, "data": {
        "unlockhash": "000000000000000000000000000000000000000000000000000000000000000000000000000000"}}
    cuh = ConditionTypes.from_json(uh_json)
    assert cuh.json() == uh_json
    test_sia_encoded(
        cuh, '012100000000000000000000000000000000000000000000000000000000000000000000000000000000')
    test_rivine_encoded(
        cuh, '0142000000000000000000000000000000000000000000000000000000000000000000')
    assert str(
        cuh.unlockhash) == '000000000000000000000000000000000000000000000000000000000000000000000000000000'

    # AtomicSwap conditions are supported
    as_json = {"type": 2, "data": {"sender": "01e89843e4b8231a01ba18b254d530110364432aafab8206bea72e5a20eaa55f70b1ccc65e2105",
                                   "receiver": "01a6a6c5584b2bfbd08738996cd7930831f958b9a5ed1595525236e861c1a0dc353bdcf54be7d8", "hashedsecret": "abc543defabc543defabc543defabc543defabc543defabc543defabc543defa", "timelock": 1522068743}}
    cas = ConditionTypes.from_json(as_json)
    assert cas.json() == as_json
    test_sia_encoded(cas, '026a0000000000000001e89843e4b8231a01ba18b254d530110364432aafab8206bea72e5a20eaa55f7001a6a6c5584b2bfbd08738996cd7930831f958b9a5ed1595525236e861c1a0dc35abc543defabc543defabc543defabc543defabc543defabc543defabc543defa07edb85a00000000')
    test_rivine_encoded(cas, '02d401e89843e4b8231a01ba18b254d530110364432aafab8206bea72e5a20eaa55f7001a6a6c5584b2bfbd08738996cd7930831f958b9a5ed1595525236e861c1a0dc35abc543defabc543defabc543defabc543defabc543defabc543defabc543defa07edb85a00000000')
    assert str(
        cas.unlockhash) == '026e18a53ec6e571985ea7ed404a5d51cf03a72240065952034383100738627dbf949046789e30'

    # MultiSig conditions are supported
    ms_json = {"type": 4, "data": {"unlockhashes": ["01e89843e4b8231a01ba18b254d530110364432aafab8206bea72e5a20eaa55f70b1ccc65e2105",
                                                    "01a6a6c5584b2bfbd08738996cd7930831f958b9a5ed1595525236e861c1a0dc353bdcf54be7d8"], "minimumsignaturecount": 2}}
    cms = ConditionTypes.from_json(ms_json)
    assert cms.json() == ms_json
    test_sia_encoded(cms, '0452000000000000000200000000000000020000000000000001e89843e4b8231a01ba18b254d530110364432aafab8206bea72e5a20eaa55f7001a6a6c5584b2bfbd08738996cd7930831f958b9a5ed1595525236e861c1a0dc35')
    test_rivine_encoded(
        cms, '049602000000000000000401e89843e4b8231a01ba18b254d530110364432aafab8206bea72e5a20eaa55f7001a6a6c5584b2bfbd08738996cd7930831f958b9a5ed1595525236e861c1a0dc35')
    assert str(
        cms.unlockhash) == '0313a5abd192d1bacdd1eb518fc86987d3c3d1cfe3c5bed68ec4a86b93b2f05a89f67b89b07d71'

    # LockTime conditions are supported:
    # - wrapping a nil condition
    lt_n_json = {"type": 3, "data": {
        "locktime": 500000000, "condition": {"type": 0}}}
    clt_n = ConditionTypes.from_json(lt_n_json)
    assert clt_n.json() == lt_n_json
    test_sia_encoded(clt_n, '0309000000000000000065cd1d0000000000')
    test_rivine_encoded(clt_n, '03120065cd1d0000000000')
    assert str(
        clt_n.unlockhash) == '000000000000000000000000000000000000000000000000000000000000000000000000000000'
    # - wrapping an unlock hash condition
    lt_uh_json = {"type": 3, "data": {
        "locktime": 500000000, "condition": uh_json}}
    clt_uh = ConditionTypes.from_json(lt_uh_json)
    assert clt_uh.json() == lt_uh_json
    test_sia_encoded(
        clt_uh, '032a000000000000000065cd1d0000000001000000000000000000000000000000000000000000000000000000000000000000')
    test_rivine_encoded(
        clt_uh, '03540065cd1d0000000001000000000000000000000000000000000000000000000000000000000000000000')
    assert str(
        clt_uh.unlockhash) == '000000000000000000000000000000000000000000000000000000000000000000000000000000'
    # - wrapping a multi-sig condition
    lt_ms_json = {"type": 3, "data": {
        "locktime": 500000000, "condition": ms_json}}
    clt_ms = ConditionTypes.from_json(lt_ms_json)
    assert clt_ms.json() == lt_ms_json
    test_sia_encoded(clt_ms, '035b000000000000000065cd1d00000000040200000000000000020000000000000001e89843e4b8231a01ba18b254d530110364432aafab8206bea72e5a20eaa55f7001a6a6c5584b2bfbd08738996cd7930831f958b9a5ed1595525236e861c1a0dc35')
    test_rivine_encoded(
        clt_ms, '03a80065cd1d000000000402000000000000000401e89843e4b8231a01ba18b254d530110364432aafab8206bea72e5a20eaa55f7001a6a6c5584b2bfbd08738996cd7930831f958b9a5ed1595525236e861c1a0dc35')
    assert str(
        clt_ms.unlockhash) == '0313a5abd192d1bacdd1eb518fc86987d3c3d1cfe3c5bed68ec4a86b93b2f05a89f67b89b07d71'

    # FYI, Where lock times are used, it should be known that these are pretty flexible in definition
    assert OutputLock().value == 0
    assert OutputLock(value=0).value == 0
    assert OutputLock(value=1).value == 1
    assert OutputLock(value=1549483822).value == 1549483822
    # if current_timestamp is not defined, the current time is used: int(datetime.now().timestamp)
    assert OutputLock(value='+7d', current_timestamp=1).value == 604801
    assert OutputLock(value='+7d12h5s',
                      current_timestamp=1).value == 648006
예제 #12
0
    def unlockhash_get(self, target):
        """
        Get all transactions linked to the given unlockhash (target),
        as well as other information such as the multisig addresses linked to the given unlockhash (target).

        target can be any of:
            - None: unlockhash of the Free-For-All wallet will be used
            - str (or unlockhash/bytes/bytearray): target is assumed to be the unlockhash of a personal wallet
            - list: target is assumed to be the addresses of a MultiSig wallet where all owners (specified as a list of addresses) have to sign
            - tuple (addresses, sigcount): target is a sigcount-of-addresscount MultiSig wallet

        @param target: the target wallet to look up transactions for in the explorer, see above for more info
        """
        unlockhash = str(ConditionTypes.from_recipient(target).unlockhash)
        endpoint = "/explorer/hashes/" + unlockhash
        resp = self.explorer_get(endpoint=endpoint)
        resp = json_loads(resp)
        try:
            if resp['hashtype'] != 'unlockhash':
                raise tfchain.errors.ExplorerInvalidResponse(
                    "expected hash type 'unlockhash' not '{}'".format(
                        resp['hashtype']), endpoint, resp)
            # parse the transactions
            transactions = []
            for etxn in resp['transactions']:
                # parse the explorer transaction
                transaction = self._transaction_from_explorer_transaction(
                    etxn, endpoint=endpoint, resp=resp)
                # append the transaction to the list of transactions
                transactions.append(transaction)
            # collect all multisig addresses
            multisig_addresses = [
                UnlockHash.from_json(obj=uh)
                for uh in resp.get('multisigaddresses', None) or []
            ]
            for addr in multisig_addresses:
                if addr.type != UnlockHashType.MULTI_SIG:
                    raise tfchain.errors.ExplorerInvalidResponse(
                        "invalid unlock hash type {} for MultiSignature Address (expected: 3)"
                        .format(addr.type), endpoint, resp)
            erc20_info = None
            if 'erc20info' in resp:
                info = resp['erc20info']
                erc20_info = ERC20AddressInfo(
                    address_tft=UnlockHash.from_json(info['tftaddress']),
                    address_erc20=ERC20Address.from_json(info['erc20address']),
                    confirmations=int(info['confirmations']),
                )

            # sort the transactions by height
            transactions.sort(key=(lambda txn: sys.maxsize
                                   if txn.height < 0 else txn.height),
                              reverse=True)

            # return explorer data for the unlockhash
            return ExplorerUnlockhashResult(
                unlockhash=UnlockHash.from_json(unlockhash),
                transactions=transactions,
                multisig_addresses=multisig_addresses,
                erc20_info=erc20_info,
                client=self)
        except KeyError as exc:
            # return a KeyError as an invalid Explorer Response
            raise tfchain.errors.ExplorerInvalidResponse(
                str(exc), endpoint, resp) from exc
예제 #13
0
def test_conditiontypes():
    def test_encoded(encoder, obj, expected):
        encoder.add(obj)
        jsass.equals(encoder.data, expected)

    def test_sia_encoded(obj, expected):
        test_encoded(SiaBinaryEncoder(), obj, expected)

    def test_rivine_encoded(obj, expected):
        test_encoded(RivineBinaryEncoder(), obj, expected)

    # Nil conditions are supported
    for n_json in [
            '{}', '{"type": 0}', '{"type": 0, "data": null}',
            '{"type": 0, "data": {}}'
    ]:
        cn = ConditionTypes.from_json(json_loads(n_json))
        jsass.equals(cn.json(), {"type": 0})
        test_sia_encoded(cn, '000000000000000000')
        test_rivine_encoded(cn, '0000')
        jsass.equals(
            str(cn.unlockhash),
            '000000000000000000000000000000000000000000000000000000000000000000000000000000'
        )

    # UnlockHash conditions are supported
    uh_json_raw = '{"type":1,"data":{"unlockhash":"000000000000000000000000000000000000000000000000000000000000000000000000000000"}}'
    uh_json = json_loads(uh_json_raw)
    cuh = ConditionTypes.from_json(uh_json)
    jsass.equals(cuh.json(), uh_json)
    test_sia_encoded(
        cuh,
        '012100000000000000000000000000000000000000000000000000000000000000000000000000000000'
    )
    test_rivine_encoded(
        cuh,
        '0142000000000000000000000000000000000000000000000000000000000000000000'
    )
    jsass.equals(
        cuh.unlockhash,
        '000000000000000000000000000000000000000000000000000000000000000000000000000000'
    )

    # AtomicSwap conditions are supported
    as_json = json_loads(
        '{"type":2,"data":{"sender":"01e89843e4b8231a01ba18b254d530110364432aafab8206bea72e5a20eaa55f70b1ccc65e2105","receiver":"01a6a6c5584b2bfbd08738996cd7930831f958b9a5ed1595525236e861c1a0dc353bdcf54be7d8","hashedsecret":"abc543defabc543defabc543defabc543defabc543defabc543defabc543defa","timelock":1522068743}}'
    )
    cas = ConditionTypes.from_json(as_json)
    jsass.equals(cas.json(), as_json)
    test_sia_encoded(
        cas,
        '026a0000000000000001e89843e4b8231a01ba18b254d530110364432aafab8206bea72e5a20eaa55f7001a6a6c5584b2bfbd08738996cd7930831f958b9a5ed1595525236e861c1a0dc35abc543defabc543defabc543defabc543defabc543defabc543defabc543defa07edb85a00000000'
    )
    test_rivine_encoded(
        cas,
        '02d401e89843e4b8231a01ba18b254d530110364432aafab8206bea72e5a20eaa55f7001a6a6c5584b2bfbd08738996cd7930831f958b9a5ed1595525236e861c1a0dc35abc543defabc543defabc543defabc543defabc543defabc543defabc543defa07edb85a00000000'
    )
    jsass.equals(
        str(cas.unlockhash),
        '026e18a53ec6e571985ea7ed404a5d51cf03a72240065952034383100738627dbf949046789e30'
    )

    # MultiSig conditions are supported
    ms_json_raw = '{"type":4,"data":{"unlockhashes":["01e89843e4b8231a01ba18b254d530110364432aafab8206bea72e5a20eaa55f70b1ccc65e2105","01a6a6c5584b2bfbd08738996cd7930831f958b9a5ed1595525236e861c1a0dc353bdcf54be7d8"],"minimumsignaturecount":2}}'
    ms_json = json_loads(ms_json_raw)
    cms = ConditionTypes.from_json(ms_json)
    jsass.equals(cms.json(), ms_json)
    test_sia_encoded(
        cms,
        '0452000000000000000200000000000000020000000000000001e89843e4b8231a01ba18b254d530110364432aafab8206bea72e5a20eaa55f7001a6a6c5584b2bfbd08738996cd7930831f958b9a5ed1595525236e861c1a0dc35'
    )
    test_rivine_encoded(
        cms,
        '049602000000000000000401e89843e4b8231a01ba18b254d530110364432aafab8206bea72e5a20eaa55f7001a6a6c5584b2bfbd08738996cd7930831f958b9a5ed1595525236e861c1a0dc35'
    )
    jsass.equals(
        cms.unlockhash,
        '0313a5abd192d1bacdd1eb518fc86987d3c3d1cfe3c5bed68ec4a86b93b2f05a89f67b89b07d71'
    )

    # LockTime conditions are supported:
    # - wrapping a nil condition
    lt_n_json = json_loads(
        '{"type":3,"data":{"locktime":500000000,"condition":{"type":0}}}')
    clt_n = ConditionTypes.from_json(lt_n_json)
    jsass.equals(clt_n.json(), lt_n_json)
    test_sia_encoded(clt_n, '0309000000000000000065cd1d0000000000')
    test_rivine_encoded(clt_n, '03120065cd1d0000000000')
    jsass.equals(
        str(clt_n.unlockhash),
        '000000000000000000000000000000000000000000000000000000000000000000000000000000'
    )
    # - wrapping an unlock hash condition
    lt_uh_json = json_loads(
        '{"type":3,"data":{"locktime":500000000,"condition":' + uh_json_raw +
        '}}')
    clt_uh = ConditionTypes.from_json(lt_uh_json)
    jsass.equals(clt_uh.json(), lt_uh_json)
    test_sia_encoded(
        clt_uh,
        '032a000000000000000065cd1d0000000001000000000000000000000000000000000000000000000000000000000000000000'
    )
    test_rivine_encoded(
        clt_uh,
        '03540065cd1d0000000001000000000000000000000000000000000000000000000000000000000000000000'
    )
    jsass.equals(
        str(clt_uh.unlockhash),
        '000000000000000000000000000000000000000000000000000000000000000000000000000000'
    )
    # - wrapping a multi-sig condition
    lt_ms_json = json_loads(
        '{"type":3,"data":{"locktime":500000000,"condition":' + ms_json_raw +
        '}}')
    clt_ms = ConditionTypes.from_json(lt_ms_json)
    jsass.equals(clt_ms.json(), lt_ms_json)
    test_sia_encoded(
        clt_ms,
        '035b000000000000000065cd1d00000000040200000000000000020000000000000001e89843e4b8231a01ba18b254d530110364432aafab8206bea72e5a20eaa55f7001a6a6c5584b2bfbd08738996cd7930831f958b9a5ed1595525236e861c1a0dc35'
    )
    test_rivine_encoded(
        clt_ms,
        '03a80065cd1d000000000402000000000000000401e89843e4b8231a01ba18b254d530110364432aafab8206bea72e5a20eaa55f7001a6a6c5584b2bfbd08738996cd7930831f958b9a5ed1595525236e861c1a0dc35'
    )
    jsass.equals(
        str(clt_ms.unlockhash),
        '0313a5abd192d1bacdd1eb518fc86987d3c3d1cfe3c5bed68ec4a86b93b2f05a89f67b89b07d71'
    )

    # FYI, Where lock times are used, it should be known that these are pretty flexible in definition
    jsass.equals(OutputLock().value, 0)
    jsass.equals(OutputLock(value=0).value, 0)
    jsass.equals(OutputLock(value=1).value, 1)
    jsass.equals(OutputLock(value=1549483822).value, 1549483822)
    # if current_timestamp is not defined, the current time is used: int(datetime.now().timestamp)
    jsass.equals(OutputLock(value='+7d', current_timestamp=1).value, 604801)
    jsass.equals(
        OutputLock(value='+7d12h5s', current_timestamp=1).value, 648006)