def _transaction_from_explorer_transaction( self, etxn, endpoint="/?", resp=None): # keyword parameters for error handling purposes only if resp == None: resp = jsobj.new_dict() # parse the transactions transaction = transactions.from_json(obj=etxn['rawtransaction'], id=etxn['id']) # add the parent (coin) outputs coininputoutputs = etxn.get_or('coininputoutputs', None) or [] if len(transaction.coin_inputs) != len(coininputoutputs): raise tferrors.ExplorerInvalidResponse( "amount of coin inputs and parent outputs are not matching: {} != {}" .format(len(transaction.coin_inputs), len(coininputoutputs)), endpoint, resp) for (idx, co) in enumerate(coininputoutputs): co = CoinOutput.from_json(obj=co) co.id = transaction.coin_inputs[idx].parentid transaction.coin_inputs[idx].parent_output = co # add the coin output ids coinoutputids = etxn.get_or('coinoutputids', None) or [] if len(transaction.coin_outputs) != len(coinoutputids): raise tferrors.ExplorerInvalidResponse( "amount of coin outputs and output identifiers are not matching: {} != {}" .format(len(transaction.coin_outputs), len(coinoutputids)), endpoint, resp) for (idx, id) in enumerate(coinoutputids): transaction.coin_outputs[idx].id = Hash.from_json(obj=id) # add the parent (blockstake) outputs blockstakeinputoutputs = etxn.get_or('blockstakeinputoutputs', None) or [] if len(transaction.blockstake_inputs) != len(blockstakeinputoutputs): raise tferrors.ExplorerInvalidResponse( "amount of blockstake inputs and parent outputs are not matching: {} != {}" .format(len(transaction.blockstake_inputs), len(blockstakeinputoutputs)), endpoint, resp) for (idx, bso) in enumerate(blockstakeinputoutputs): bso = BlockstakeOutput.from_json(obj=bso) bso.id = transaction.blockstake_inputs[idx].parentid transaction.blockstake_inputs[idx].parent_output = bso # add the blockstake output ids blockstakeoutputids = etxn.get_or('blockstakeoutputids', None) or [] if len(transaction.blockstake_outputs) != len(blockstakeoutputids): raise tferrors.ExplorerInvalidResponse( "amount of blokstake outputs and output identifiers are not matching: {} != {}" .format(len(transaction.blockstake_inputs), len(blockstakeoutputids)), endpoint, resp) for (idx, id) in enumerate(blockstakeoutputids): transaction.blockstake_outputs[idx].id = Hash.from_json(obj=id) # set the unconfirmed state transaction.unconfirmed = etxn.get_or('unconfirmed', False) # set the blockid and height of the transaction only if confirmed if not transaction.unconfirmed: transaction.height = int(etxn.get_or('height', -1)) transaction.blockid = etxn.get_or('parent', None) # return the transaction return transaction
def _block_get_parse_cb(self, result): endpoint, block = result try: # parse the transactions transactions = [] for etxn in block['transactions']: # parse the explorer transaction transaction = self._transaction_from_explorer_transaction( etxn, endpoint=endpoint, resp=block) # append the transaction to the list of transactions transactions.append(transaction) rawblock = block['rawblock'] # parse the parent id parentid = Hash.from_json(obj=rawblock['parentid']) # parse the miner payouts miner_payouts = [] minerpayoutids = block.get_or('minerpayoutids', None) or [] eminerpayouts = rawblock.get_or('minerpayouts', None) or [] if len(eminerpayouts) != len(minerpayoutids): raise tferrors.ExplorerInvalidResponse( "amount of miner payouts and payout ids are not matching: {} != {}" .format(len(eminerpayouts), len(minerpayoutids)), endpoint, block) for idx, mp in enumerate(eminerpayouts): id = Hash.from_json(minerpayoutids[idx]) value = Currency.from_json(mp['value']) unlockhash = UnlockHash.from_json(mp['unlockhash']) miner_payouts.append( ExplorerMinerPayout(id=id, value=value, unlockhash=unlockhash)) # get the timestamp and height height = int(block['height']) timestamp = int(rawblock['timestamp']) # get the block's identifier blockid = Hash.from_json(block['blockid']) # for all transactions assign these properties for transaction in transactions: _assign_block_properties_to_transacton(transaction, block) transaction.height = height transaction.blockid = blockid # return the block, as reported by the explorer return ExplorerBlock(id=blockid, parentid=parentid, height=height, timestamp=timestamp, transactions=transactions, miner_payouts=miner_payouts) except KeyError as exc: raise tferrors.ExplorerInvalidResponse(str(exc), endpoint, block) from exc
def hash(self, value): if value == None: self._hash = None elif isinstance(value, Hash): self._hash = value else: self._hash = Hash(value=value)
def _outputid_new(self, specifier, index): encoder = encoder_sia_get() encoder.add_array(specifier) encoder.add_array(self._id_input_compute()) encoder.add_int(index) hash = bytearray.fromhex(blake2_string(encoder.data)) return Hash(value=hash)
def __init__(self, id=None, parentid=None, height=None, timestamp=None, transactions=None, miner_payouts=None): """ A Block, registered on a TF blockchain, as reported by an explorer. """ self._id = id or Hash() self._parentid = parentid or Hash() self._height = height or 0 self._timestamp = timestamp or 0 self._transactions = transactions or [] self._miner_payouts = miner_payouts or []
def from_str(cls, obj): if not isinstance(obj, str): raise TypeError( "UnlockHash is expected to be a str, not {}".format(type(obj))) obj = jsstr.strip(obj) if len(obj) != UnlockHash._TOTAL_SIZE_HEX: raise ValueError( "UnlockHash is expexcted to be of length {} when stringified, not of length {}, invalid: {} ({})" .format(UnlockHash._TOTAL_SIZE_HEX, len(obj), obj, type(obj))) t = UnlockHashType( int(jsarr.slice_array(obj, 0, UnlockHash._TYPE_SIZE_HEX))) h = Hash( value=obj[UnlockHash._TYPE_SIZE_HEX:UnlockHash._TYPE_SIZE_HEX + UnlockHash._HASH_SIZE_HEX]) uh = cls(uhtype=t, uhhash=h) if t.__eq__(UnlockHashType.NIL): expectedNH = jshex.bytes_to_hex( bytes(jsarr.new_array(UnlockHash._HASH_SIZE))) nh = jshex.bytes_to_hex(h.value) if nh != expectedNH: raise ValueError("unexpected nil hash {}".format(nh)) else: expected_checksum = jshex.bytes_to_hex( jsarr.slice_array(uh._checksum(), 0, UnlockHash._CHECKSUM_SIZE)) checksum = jsarr.slice_array( obj, UnlockHash._TOTAL_SIZE_HEX - UnlockHash._CHECKSUM_SIZE_HEX) if expected_checksum != checksum: raise ValueError("unexpected checksum {}, expected {}".format( checksum, expected_checksum)) return uh
def from_json(cls, obj): if not isinstance(obj, str): raise TypeError( "UnlockHash is expected to be JSON-encoded as an str, not {}". format(type(obj))) if len(obj) != UnlockHash._TOTAL_SIZE_HEX: raise ValueError( "UnlockHash is expexcted to be of length {} when JSON-encoded, not of length {}" .format(UnlockHash._TOTAL_SIZE_HEX, len(obj))) t = UnlockHashType(int(obj[:UnlockHash._TYPE_SIZE_HEX])) h = Hash( value=obj[UnlockHash._TYPE_SIZE_HEX:UnlockHash._TYPE_SIZE_HEX + UnlockHash._HASH_SIZE_HEX]) uh = cls(type=t, hash=h) if t == UnlockHashType.NIL: expectedNH = b'\x00' * UnlockHash._HASH_SIZE if h.value != expectedNH: raise ValueError("unexpected nil hash {}".format( h.value.hex())) else: expected_checksum = uh._checksum()[:UnlockHash._CHECKSUM_SIZE].hex( ) checksum = obj[-UnlockHash._CHECKSUM_SIZE_HEX:] if expected_checksum != checksum: raise ValueError("unexpected checksum {}, expected {}".format( checksum, expected_checksum)) return uh
def cb(result): _, transaction = result try: return Hash(value=transaction['transactionid']).__str__() except (KeyError, ValueError, TypeError) as exc: # return a KeyError as an invalid Explorer Response raise tferrors.ExplorerInvalidResponse(str(exc), endpoint, transaction) from exc
def blockchain_info_get(self): """ Get the current blockchain info, using the last known block, as reported by an explorer. """ resp = self.explorer_get(endpoint="/explorer") resp = json_loads(resp) blockid = Hash.from_json(obj=resp['blockid']) last_block = self.block_get(blockid) return ExplorerBlockchainInfo(last_block=last_block)
def _id_new(self, specifier=None, index=None): encoder = SiaBinaryEncoder() if specifier != None: encoder.add_array(specifier) encoder.add_array(self._id_input_compute()) if index != None: encoder.add_int(index) hash = blake2b(encoder.data) return Hash(value=hash)
def explorer_post(self, endpoint, data): """ Put explorer data onto the stub client for the specified endpoint. """ hash_template = re.compile(r'^.*/transactionpool/transactions$') match = hash_template.match(endpoint) if match: transactionid = Hash(value=jscrypto.random(Hash.SIZE)).__str__() transaction = tftransactions.from_json(data) # ensure all coin outputs and block stake outputs have identifiers set for idx, co in enumerate(transaction.coin_outputs): co.id = transaction.coin_outputid_new(idx) for idx, bso in enumerate(transaction.blockstake_outputs): bso.id = transaction.blockstake_outputid_new(idx) self._posted_transactions[transactionid] = transaction return jsstr.sprintf('{"transactionid":"%s"}', transactionid.__str__()) raise Exception("invalid endpoint {}".format(endpoint))
def get_block(result): used_addr, raw_block = result address = used_addr def get_block_with_tag(result): return ('b', (address, result)) blockid = Hash.from_json(obj=raw_block['blockid']) return jsasync.chain(self.block_get(blockid), get_block_with_tag)
def test_types(): # currency values can be created from both # int and str values, but are never allowed to be negative jsass.equals(Currency().str(), '0') jsass.equals(Currency(value=123).str(), '123') jsass.equals(Currency(value='1').str(), '1') jsass.equals(Currency(value='0.1').json(), '100000000') # in the string versions you can also add the TFT currency notation, # or use decimal notation to express the currency in the TFT Currency Unit, # rather than the primitive unit jsass.equals(Currency(value='1 TFT').str(), '1') jsass.equals(Currency(value='0.123456789').str(), '0.123456789') jsass.equals(Currency(value='0.123456789').json(), '123456789') jsass.equals(Currency(value='9.123456789').str(), '9.123456789') jsass.equals(Currency(value='1234.34').str(), '1234.34') jsass.equals(Currency(value='1.00000').str(), '1') jsass.equals(Currency(value='1.0 tft').str(), '1') jsass.equals(Currency(value=1).str(), '1') jsass.equals(Currency(value=12344).str(), '12344') # hash values can be created directly from binary data, # or from a hex-encoded string, by default the nil hash will be created jsass.equals(Hash().str(), '0' * 64) jsass.equals( Hash(b'12345678901234567890123456789001').value, b'12345678901234567890123456789001') # binary data is very similar to a hash, # except that it doesn't have a fixed length and it is binary serialized # as a slice, not an array jsass.equals(BinaryData().str(), '') jsass.equals(BinaryData(b'1').str(), '31') jsass.equals(BinaryData(b'1', fixed_size=0).str(), '31') jsass.equals(BinaryData(b'1', fixed_size=1).str(), '31') # raw data is pretty much binary data, except that it is # base64 encoded/decoded for str/json purposes jsass.equals(BinaryData(b'data', strencoding='base64').str(), 'ZGF0YQ==') # block stake values can be created from both # int and str values, but are never allowed to be negative jsass.equals(Blockstake().str(), '0') jsass.equals(Blockstake(value=123).str(), '123') jsass.equals(Blockstake(value='1').str(), '1')
def input_hash_new(self, public_key): """ Create an input hash for the public key of the fulfiller. """ input_hash = self._input_hash_gen(public_key) if isinstance(input_hash, (str, bytearray, bytes)): input_hash = Hash(value=input_hash) elif not isinstance(input_hash, Hash): raise TypeError( "signature request requires an input hash of Type Hash, not: {}" .format(type(input_hash))) return input_hash
def chain_blockid(self, value): """ Set the blockchain block ID, such that applications that which to cache this balance object could ensure that the last block is still the same as the last known block known by this balance instance. """ if not value: self._chain_blockid = Hash() return if isinstance(value, Hash): self._chain_blockid.value = value.value else: self._chain_blockid.value = value
def from_json(cls, obj): if not obj: return cls() if not isinstance(obj, str): raise TypeError( "expected JSON-encoded PublicKey to be a string, not {}". format(type(obj))) parts = jsstr.split(obj, ':', 2) if len(parts) != 2: raise ValueError("invalid JSON-encoded PublicKey: {}".format(obj)) pk = cls() pk._specifier = PublicKeySpecifier.from_json(parts[0]) pk._hash = Hash.from_json(parts[1]) return pk
def __init__(self): # personal wallet outputs self._outputs = {} self._outputs_spent = {} self._outputs_unconfirmed = {} self._outputs_unconfirmed_spent = {} # transactions used by outputs self._transactions = {} # balance chain context self._chain_time = 0 self._chain_height = 0 self._chain_blockid = Hash() # all wallet addresses tracked in this wallet self._addresses = set()
def explorer_post(self, endpoint, data): """ Put explorer data onto the stub client for the specified endpoint. """ hash_template = re.compile(r'^.*/transactionpool/transactions$') match = hash_template.match(endpoint) if match: transactionid = str(Hash(value=generateXByteID(Hash.SIZE))) transaction = j.clients.tfchain.types.transactions.from_json(data) # ensure all coin outputs and block stake outputs have identifiers set for idx, co in enumerate(transaction.coin_outputs): co.id = transaction.coin_outputid_new(idx) for idx, bso in enumerate(transaction.blockstake_outputs): bso.id = transaction.blockstake_outputid_new(idx) self._posted_transactions[transactionid] = transaction return '{"transactionid":"%s"}' % (str(transactionid)) raise Exception("invalid endpoint {}".format(endpoint))
def transaction_put(self, transaction): """ Submit a transaction to an available explorer Node. @param transaction: the transaction to push to the client transaction pool """ if isinstance(transaction, TransactionBaseClass): transaction = transaction.json() endpoint = "/transactionpool/transactions" resp = self.explorer_post(endpoint=endpoint, data=transaction) resp = json_loads(resp) try: return str(Hash(value=resp['transactionid'])) except KeyError as exc: # return a KeyError as an invalid Explorer Response raise tfchain.errors.ExplorerInvalidResponse( str(exc), endpoint, resp) from exc
def from_json(cls, obj): return cls( parentid=Hash.from_json(obj['parentid']), fulfillment=FulfillmentTypes.from_json(obj['fulfillment']))
def _normalize_id(self, id): return Hash(value=id).str()
class UnlockHash(BaseDataTypeClass): """ An UnlockHash is a specially constructed hash of the UnlockConditions type, with a fixed binary length of 33 and a fixed string length of 78 (string version includes a checksum). """ def __init__(self, uhtype=None, uhhash=None): self._type = UnlockHashType.NIL self.uhtype = uhtype self._hash = Hash() self.hash = uhhash @classmethod def from_str(cls, obj): if not isinstance(obj, str): raise TypeError( "UnlockHash is expected to be a str, not {}".format(type(obj))) obj = jsstr.strip(obj) if len(obj) != UnlockHash._TOTAL_SIZE_HEX: raise ValueError( "UnlockHash is expexcted to be of length {} when stringified, not of length {}, invalid: {} ({})" .format(UnlockHash._TOTAL_SIZE_HEX, len(obj), obj, type(obj))) t = UnlockHashType( int(jsarr.slice_array(obj, 0, UnlockHash._TYPE_SIZE_HEX))) h = Hash( value=obj[UnlockHash._TYPE_SIZE_HEX:UnlockHash._TYPE_SIZE_HEX + UnlockHash._HASH_SIZE_HEX]) uh = cls(uhtype=t, uhhash=h) if t.__eq__(UnlockHashType.NIL): expectedNH = jshex.bytes_to_hex( bytes(jsarr.new_array(UnlockHash._HASH_SIZE))) nh = jshex.bytes_to_hex(h.value) if nh != expectedNH: raise ValueError("unexpected nil hash {}".format(nh)) else: expected_checksum = jshex.bytes_to_hex( jsarr.slice_array(uh._checksum(), 0, UnlockHash._CHECKSUM_SIZE)) checksum = jsarr.slice_array( obj, UnlockHash._TOTAL_SIZE_HEX - UnlockHash._CHECKSUM_SIZE_HEX) if expected_checksum != checksum: raise ValueError("unexpected checksum {}, expected {}".format( checksum, expected_checksum)) return uh @classmethod def from_json(cls, obj): return UnlockHash.from_str(obj) @property def uhtype(self): return self._type @uhtype.setter def uhtype(self, value): if value == None: value = UnlockHashType.NIL elif not isinstance(value, UnlockHashType): raise TypeError( "UnlockHash's type has to be of type UnlockHashType, not {}". format(type(value))) self._type = value @property def hash(self): return self._hash @hash.setter def hash(self, value): self._hash.value = value def __str__(self): checksum = jshex.bytes_to_hex( jsarr.slice_array(self._checksum(), 0, UnlockHash._CHECKSUM_SIZE)) return "{}{}{}".format( jshex.bytes_to_hex(bytes([self._type.__int__()])), self._hash.__str__(), checksum) def _checksum(self): if self._type.__eq__(UnlockHashType.NIL): return bytes(jsarr.new_array(UnlockHash._CHECKSUM_SIZE)) e = RivineBinaryEncoder() e.add_int8(self._type.value) e.add(self._hash) return jscrypto.blake2b(e.data) def __repr__(self): return self.__str__() def json(self): return self.__str__() def __eq__(self, other): other = UnlockHash._op_other_as_unlockhash(other) return self.uhtype.__eq__(other.uhtype) and self.hash.__eq__( other.hash) def __ne__(self, other): other = UnlockHash._op_other_as_unlockhash(other) return self.uhtype.__ne__(other.uhtype) or self.hash.__ne__(other.hash) def __hash__(self): return hash(self.__str__()) @staticmethod def _op_other_as_unlockhash(other): if isinstance(other, str): other = UnlockHash.from_json(other) elif not isinstance(other, UnlockHash): raise TypeError("UnlockHash of type {} is not supported".format( type(other))) return other def sia_binary_encode(self, encoder): """ Encode this unlock hash according to the Sia Binary Encoding format. """ encoder.add_byte(self._type.__int__()) encoder.add(self._hash) def rivine_binary_encode(self, encoder): """ Encode this unlock hash according to the Rivine Binary Encoding format. """ encoder.add_int8(self._type.__int__()) encoder.add(self._hash)
def id(self, id): if isinstance(id, Hash): self._id = Hash(value=id.value) self._id = Hash(value=id)
class CoinInput(BaseDataTypeClass): """ CoinIput class """ def __init__(self, parentid=None, fulfillment=None, parent_output=None): self._parent_id = None self.parentid = parentid self._fulfillment = None self.fulfillment = fulfillment # property that can be set if known, but which is not part of the actual CoinInput self._parent_output = None self.parent_output = parent_output @classmethod def from_json(cls, obj): return cls( parentid=Hash.from_json(obj['parentid']), fulfillment=FulfillmentTypes.from_json(obj['fulfillment'])) @classmethod def from_coin_output(cls, co): if not isinstance(co, CoinOutput): raise TypeError( "invalid co parameter, expected value of type CoinOutput, not {}".format(type(co))) ci = cls( parentid=co.id, fulfillment=FulfillmentTypes.from_condition(co.condition)) ci.parent_output = co return ci @property def parentid(self): return self._parent_id @parentid.setter def parentid(self, value): if isinstance(value, Hash): self._parent_id = Hash(value=value.value) return self._parent_id = Hash(value=value) @property def fulfillment(self): return self._fulfillment @fulfillment.setter def fulfillment(self, value): if value == None: self._fulfillment = FulfillmentSingleSignature() return if not isinstance(value, FulfillmentBaseClass): raise TypeError( "cannot assign value of type {} as a CoinInput's fulfillment (expected: FulfillmentBaseClass)".format(type(value))) self._fulfillment = value @property def has_parent_output(self): return self._parent_output != None @property def parent_output(self): return self._parent_output or CoinOutput() @parent_output.setter def parent_output(self, value): if value == None: self._parent_output = None return if not isinstance(value, CoinOutput): raise TypeError( "cannot assign value of type {} as a CoinInput's parent output (expected: CoinOutput)".format(type(value))) self._parent_output = value def json(self): return { 'parentid': self._parent_id.json(), 'fulfillment': self._fulfillment.json() } def sia_binary_encode(self, encoder): """ Encode this CoinInput according to the Sia Binary Encoding format. """ encoder.add_all(self._parent_id, self._fulfillment) def rivine_binary_encode(self, encoder): """ Encode this CoinInput according to the Rivine Binary Encoding format. """ encoder.add_all(self._parent_id, self._fulfillment) def signature_requests_new(self, input_hash_func): """ Returns all signature requests that can be generated for this Coin Inputs, only possible if the parent (coin) output is defined and when there are still signatures required. """ if self._parent_output == None: # no requestsd get created if the parent output is not set, # this allows for partial Tx signings return [] return self._fulfillment.signature_requests_new( input_hash_func=input_hash_func, parent_condition=self._parent_output.condition, ) def is_fulfilled(self): """ Returns true if this CoinInput is fulfilled. """ if self._parent_output == None: return False return self._fulfillment.is_fulfilled(self._parent_output.condition)
def parentid(self, value): if isinstance(value, Hash): self._parent_id = Hash(value=value.value) return self._parent_id = Hash(value=value)
def blockid(self, value): self._blockid = Hash(value=value)
def block_get(self, value): """ Get a block from an available explorer Node. @param value: the identifier or height that points to the desired block """ endpoint = "/explorer/?" resp = {} try: # get the explorer block if isinstance(value, int): endpoint = "/explorer/blocks/{}".format(int(value)) resp = self.explorer_get(endpoint=endpoint) resp = json_loads(resp) resp = resp['block'] else: blockid = self._normalize_id(value) endpoint = "/explorer/hashes/" + blockid resp = self.explorer_get(endpoint=endpoint) resp = json_loads(resp) if resp['hashtype'] != 'blockid': raise tfchain.errors.ExplorerInvalidResponse( "expected hash type 'blockid' not '{}'".format( resp['hashtype']), endpoint, resp) resp = resp['block'] if resp['blockid'] != blockid: raise tfchain.errors.ExplorerInvalidResponse( "expected block ID '{}' not '{}'".format( blockid, resp['blockid']), 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) rawblock = resp['rawblock'] # parse the parent id parentid = Hash.from_json(obj=rawblock['parentid']) # parse the miner payouts miner_payouts = [] minerpayoutids = resp.get('minerpayoutids', None) or [] eminerpayouts = rawblock.get('minerpayouts', None) or [] if len(eminerpayouts) != len(minerpayoutids): raise tfchain.errors.ExplorerInvalidResponse( "amount of miner payouts and payout ids are not matching: {} != {}" .format(len(eminerpayouts), len(minerpayoutids)), endpoint, resp) for idx, mp in enumerate(eminerpayouts): id = Hash.from_json(minerpayoutids[idx]) value = Currency.from_json(mp['value']) unlockhash = UnlockHash.from_json(mp['unlockhash']) miner_payouts.append( ExplorerMinerPayout(id=id, value=value, unlockhash=unlockhash)) # get the timestamp and height height = int(resp['height']) timestamp = int(rawblock['timestamp']) # get the block's identifier blockid = Hash.from_json(resp['blockid']) # return the block, as reported by the explorer return ExplorerBlock(id=blockid, parentid=parentid, height=height, timestamp=timestamp, transactions=transactions, miner_payouts=miner_payouts) except KeyError as exc: # return a KeyError as an invalid Explorer Response raise tfchain.errors.ExplorerInvalidResponse( str(exc), endpoint, resp) from exc
def hash(self): if self._hash == None: return Hash() return self._hash
def __init__(self, uhtype=None, uhhash=None): self._type = UnlockHashType.NIL self.uhtype = uhtype self._hash = Hash() self.hash = uhhash
def _normalize_id(self, id): return str(Hash(value=id))