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
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" )
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
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
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)))
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')
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())
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)
def from_json(cls, obj): return cls( value=Blockstake.from_json(obj['value']), condition=ConditionTypes.from_json(obj['condition']))
def from_json(cls, obj): return cls( value=Currency.from_json(obj['value']), condition=ConditionTypes.from_json(obj['condition']))
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
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
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)