class Deck:

    def __init__(self, name: str,
                 number_of_decimals: int,
                 issue_mode: int,
                 network: str,
                 production: bool,
                 version: int,
                 asset_specific_data: bytes=None,
                 issuer: str="",
                 issue_time: int=None,
                 id: str=None,
                 tx_confirmations: int=None) -> None:
        '''
        Initialize deck object, load from dictionary Deck(**dict) or initilize
        with kwargs Deck("deck", 3, "ONCE")
        '''

        self.version = version  # protocol version
        self.name = name  # deck name
        self.issue_mode = issue_mode  # deck issue mode
        self.number_of_decimals = number_of_decimals
        self.asset_specific_data = asset_specific_data  # optional metadata for the deck
        self.id = id
        self.issuer = issuer
        self.issue_time = issue_time
        self.tx_confirmations = tx_confirmations
        self.network = network
        self.production = production

    @property
    def p2th_address(self) -> Optional[str]:
        '''P2TH address of this deck'''

        if self.id:
            return Kutil(network=self.network,
                         privkey=bytearray.fromhex(self.id)).address
        else:
            return None

    @property
    def p2th_wif(self) -> Optional[str]:
        '''P2TH privkey in WIF format'''

        if self.id:
            return Kutil(network=self.network,
                         privkey=bytearray.fromhex(self.id)).wif
        else:
            return None

    @property
    def metainfo_to_protobuf(self) -> bytes:
        '''encode deck into protobuf'''

        deck = deckspawnproto()
        deck.version = self.version
        deck.name = self.name
        deck.number_of_decimals = self.number_of_decimals
        deck.issue_mode = self.issue_mode
        if self.asset_specific_data:
            if not isinstance(self.asset_specific_data, bytes):
                deck.asset_specific_data = self.asset_specific_data.encode()
            else:
                deck.asset_specific_data = self.asset_specific_data

        if deck.ByteSize() > net_query(self.network).op_return_max_bytes:
            raise OverSizeOPReturn('''
                        Metainfo size exceeds maximum of {max} bytes supported by this network.'''
                                   .format(max=net_query(self.network)
                                           .op_return_max_bytes))

        return deck.SerializeToString()

    @property
    def metainfo_to_dict(self) -> dict:
        '''encode deck into dictionary'''

        r = {
            "version": self.version,
            "name": self.name,
            "number_of_decimals": self.number_of_decimals,
            "issue_mode": self.issue_mode
        }

        if self.asset_specific_data:
            r.update({'asset_specific_data': self.asset_specific_data})

        return r

    def to_json(self) -> dict:
        '''export the Deck object to json-ready format'''

        d = self.__dict__
        d['p2th_wif'] = self.p2th_wif
        return d

    @classmethod
    def from_json(cls, json: dict):
        '''load the Deck object from json'''

        try:
            del json['p2th_wif']
        except KeyError:
            pass

        return cls(**json)

    def __str__(self) -> str:

        r = []
        for key in self.__dict__:
            r.append("{key}='{value}'".format(key=key, value=self.__dict__[key]))

        return ', '.join(r) class CardBundle:

    '''On the low level, cards come in bundles.
    A single transaction can contain dozens of cards.
    CardBundle is a object which is pre-coursor to CardTransfer'''

    def __init__(self,
                 deck: Deck,
                 sender: str,
                 txid: str,
                 blockhash: str,
                 blocknum: int,
                 blockseq: int,
                 timestamp: int,
                 tx_confirmations: int,
                 vouts: list=[],
                 ) -> None:

        self.deck = deck
        self.txid = txid
        self.sender = sender
        self.vouts = vouts

        if blockhash:
            self.blockhash = blockhash
            self.blockseq = blockseq
            self.timestamp = timestamp
            self.blocknum = blocknum
            self.tx_confirmations = tx_confirmations
        else:
            self.blockhash = ""
            self.blockseq = 0
            self.blocknum = 0
            self.timestamp = 0
            self.tx_confirmations = 0

    def to_json(self) -> dict:
        '''export the CardBundle object to json-ready format'''

        return self.__dict__ class CardTransfer:

    def __init__(self, deck: Deck, 
                 receiver: list=[],
                 amount: List[int]=[],
                 version: int=1,
                 blockhash: str=None,
                 txid: str=None,
                 sender: str=None,
                 asset_specific_data: bytes=None,
                 number_of_decimals: int=None,
                 blockseq: int=None,
                 cardseq: int=None,
                 blocknum: int=None,
                 timestamp: int=None,
                 tx_confirmations: int=None,
                 type: str=None) -> None:

        '''CardTransfer object, used when parsing card_transfers from the blockchain
        or when sending out new card_transfer.
        It can be initialized by passing the **kwargs and it will do the parsing,
        or it can be initialized with passed arguments.
        * deck - instance of Deck object
        * receiver - list of receivers
        * amount - list of amounts to be sent, must be integer
        * version - protocol version, default 1
        * txid - transaction ID of CardTransfer
        * sender - transaction sender
        * blockhash - block ID where the tx was first included
        * blockseq - order in which tx was serialized into block
        * timestamp - unix timestamp of the block where it was first included
        * tx_confirmations - number of confirmations of the transaction
        * asset_specific_data - extra metadata
        * number_of_decimals - number of decimals for amount, inherited from Deck object
        : type: card type [CardIssue, CardTransfer, CardBurn]'''

        if not len(receiver) == len(amount):
            raise RecieverAmountMismatch

        self.version = version
        self.network = deck.network
        self.deck_id = deck.id
        self.deck_p2th = deck.p2th_address
        self.txid = txid
        self.sender = sender
        self.asset_specific_data = asset_specific_data
        if not number_of_decimals:
            self.number_of_decimals = deck.number_of_decimals
        else:
            self.number_of_decimals = number_of_decimals

        self.receiver = receiver
        self.amount = amount

        if blockhash:
            self.blockhash = blockhash
            self.blockseq = blockseq
            self.timestamp = timestamp
            self.blocknum = blocknum
            self.cardseq = cardseq
            self.tx_confirmations = tx_confirmations
        else:
            self.blockhash = ""
            self.blockseq = 0
            self.blocknum = 0
            self.timestamp = 0
            self.cardseq = 0
            self.tx_confirmations = 0

        if self.sender == deck.issuer:
            # if deck issuer is issuing cards to the deck issuing address,
            # card is burn and issue at the same time - which is invalid!
            if deck.issuer in self.receiver:
                raise InvalidCardIssue
            else:
                # card was sent from deck issuer to any random address,
                # card type is CardIssue
                self.type = "CardIssue"

        # card was sent back to issuing address
        # card type is CardBurn
        elif self.receiver[0] == deck.issuer and not self.sender == deck.issuer:
            self.type = "CardBurn"

        # issuer is anyone else,
        # card type is CardTransfer
        else:
            self.type = "CardTransfer"

        if type:
            self.type = type

    @property
    def metainfo_to_protobuf(self) -> bytes:
        '''encode card_transfer info to protobuf'''

        card = cardtransferproto()
        card.version = self.version
        card.amount.extend(self.amount)
        card.number_of_decimals = self.number_of_decimals
        if self.asset_specific_data:
            if not isinstance(self.asset_specific_data, bytes):
                card.asset_specific_data = self.asset_specific_data.encode()
            else:
                card.asset_specific_data = self.asset_specific_data

        if card.ByteSize() > net_query(self.network).op_return_max_bytes:
            raise OverSizeOPReturn('''
                        Metainfo size exceeds maximum of {max} bytes supported by this network.'''
                                   .format(max=net_query(self.network)
                                           .op_return_max_bytes))

        return card.SerializeToString()

    @property
    def metainfo_to_dict(self) -> dict:
        '''encode card into dictionary'''

        r = {
            "version": self.version,
            "amount": self.amount,
            "number_of_decimals": self.number_of_decimals
        }

        if self.asset_specific_data:
            r.update({'asset_specific_data': self.asset_specific_data})

        return r

    def to_json(self) -> dict:
        '''export the CardTransfer object to json-ready format'''

        return self.__dict__

    @classmethod
    def from_json(cls, json: dict):
        '''load the Deck object from json'''

        return cls(**json)

    def __str__(self) -> str:

        r = []
        for key in self.__dict__:
            r.append("{key}='{value}'".format(key=key, value=self.to_json()[key]))

        return ', '.join(r) def validate_card_issue_modes(issue_mode: int, cards: list) -> list:
    """validate cards against deck_issue modes"""

    supported_mask = 63  # sum of all issue_mode values

    if not bool(issue_mode & supported_mask):
        return []  # return empty list

    for i in [1 << x for x in range(len(IssueMode))]:
        if bool(i & issue_mode):

            try:
                parser_fn = cast(
                    Callable[[list], Optional[list]],
                    parsers[IssueMode(i).name]
                )
            except ValueError:
                continue

            parsed_cards = parser_fn(cards)
            if not parsed_cards:
                return []
            cards = parsed_cards

    return cards class RpcNode(Client, Provider):
    '''JSON-RPC connection to local Peercoin node'''

    def select_inputs(self, address: str, amount: int) -> dict:
        '''finds apropriate utxo's to include in rawtx, while being careful
        to never spend old transactions with a lot of coin age.
        Argument is intiger, returns list of apropriate UTXO's'''

        utxos = []
        utxo_sum = Decimal(0)
        for tx in sorted(self.listunspent(address=address), key=itemgetter('confirmations')):

            if tx["address"] not in (self.pa_parameters.P2TH_addr,
                                     self.pa_parameters.test_P2TH_addr):

                utxos.append(
                        MutableTxIn(txid=tx['txid'],
                                    txout=tx['vout'],
                                    sequence=Sequence.max(),
                                    script_sig=ScriptSig.empty())
                         )

                utxo_sum += Decimal(tx["amount"])
                if utxo_sum >= amount:
                    return {'utxos': utxos, 'total': utxo_sum}

        if utxo_sum < amount:
            raise InsufficientFunds("Insufficient funds.")

        raise Exception("undefined behavior :.(")

    @property
    def is_testnet(self) -> bool:
        '''check if node is configured to use testnet or mainnet'''

        if self.getinfo()["testnet"] is True:
            return True
        else:
            return False

    @property
    def network(self) -> str:
        '''return which network is the node operating on.'''

        if self.is_testnet:
            return "tppc"
        else:
            return "ppc"

    def listunspent(
        self,
        address: str="",
        minconf: int=1,
        maxconf: int=999999,
    ) -> list:
        '''list UTXOs
        modified version to allow filtering by address.
        '''
        if address:
            return self.req("listunspent", [minconf, maxconf, [address]])

        return self.req("listunspent", [minconf, maxconf]) class Provider(ABC):

    net = ""

    headers = {"User-Agent": "pypeerassets"}

    @staticmethod
    def _netname(name: str) -> dict:
        '''resolute network name,
        required because some providers use shortnames and other use longnames.'''

        try:
            long = net_query(name).name
            short = net_query(name).shortname
        except AttributeError:
            raise UnsupportedNetwork('''This blockchain network is not supported by the pypeerassets, check networks.py for list of supported networks.''')

        return {'long': long,
                'short': short}

    @property
    def network(self) -> str:
        '''return network full name'''

        return self._netname(self.net)['long']

    @property
    def pa_parameters(self) -> PAParams:
        '''load network PeerAssets parameters.'''

        return param_query(self.network)

    @property
    def network_properties(self) -> Constants:
        '''network parameters [min_fee, denomination, ...]'''

        return net_query(self.network)

    @property
    def is_testnet(self) -> bool:
        """testnet or not?"""

        if "testnet" in self.network:
            return True
        else:
            return False

    @classmethod
    def sendrawtransaction(cls, rawtxn: str) -> str:
        '''sendrawtransaction remote API'''

        if cls.is_testnet:
            url = 'https://testnet-explorer.peercoin.net/api/sendrawtransaction?hex={0}'.format(rawtxn)
        else:
            url = 'https://explorer.peercoin.net/api/sendrawtransaction?hex={0}'.format(rawtxn)

        resp = urllib.request.urlopen(url)
        return resp.read().decode('utf-8')

    @abstractmethod
    def getblockhash(self, blocknum: int) -> str:
        '''get blockhash using blocknum query'''
        raise NotImplementedError

    @abstractmethod
    def getblockcount(self) -> int:
        '''get block count'''
        raise NotImplementedError

    @abstractmethod
    def getblock(self, hash: str) -> dict:
        '''query block using <blockhash> as key.'''
        raise NotImplementedError

    @abstractmethod
    def getdifficulty(self) -> dict:
        raise NotImplementedError

    @abstractmethod
    def getbalance(self, address: str) -> Decimal:
        raise NotImplementedError

    @abstractmethod
    def getreceivedbyaddress(self, address: str) -> Decimal:
        raise NotImplementedError

    @abstractmethod
    def listunspent(self, address: str) -> list:
        raise NotImplementedError

    @abstractmethod
    def select_inputs(self, address: str, amount: int) -> dict:
        raise NotImplementedError

    @abstractmethod
    def getrawtransaction(self, txid: str, decrypt: int=1) -> dict:
        raise NotImplementedError

    @abstractmethod
    def listtransactions(self, address: str) -> list:
        raise NotImplementedError

    def validateaddress(self, address: str) -> bool:
        "Returns True if the passed address is valid, False otherwise."

        try:
            Address.from_string(address, self.network_properties)
        except InvalidAddress:
            return False

        return True def deck_parser(args: Tuple[Provider, dict, int, str],
                prod: bool=True) -> Optional[Deck]:
    '''deck parser function'''

    provider = args[0]
    raw_tx = args[1]
    deck_version = args[2]
    p2th = args[3]

    try:
        validate_deckspawn_p2th(provider, raw_tx, p2th)

        d = parse_deckspawn_metainfo(read_tx_opreturn(raw_tx['vout'][1]),
                                     deck_version)

        if d:

            d["id"] = raw_tx["txid"]
            try:
                d["issue_time"] = raw_tx["blocktime"]
            except KeyError:
                d["issue_time"] = 0
            d["issuer"] = find_tx_sender(provider, raw_tx)
            d["network"] = provider.network
            d["production"] = prod
            try:
                d["tx_confirmations"] = raw_tx["confirmations"]
            except KeyError:
                d["tx_confirmations"] = 0
            return Deck(**d)

    except (InvalidDeckSpawn, InvalidDeckMetainfo, InvalidDeckVersion,
            InvalidNulldataOutput) as err:
        pass

    return None def find_deck_spawns(provider: Provider, prod: bool=True) -> Iterable[str]:
    '''find deck spawn transactions via Provider,
    it requires that Deck spawn P2TH were imported in local node or
    that remote API knows about P2TH address.'''

    pa_params = param_query(provider.network)

    if isinstance(provider, RpcNode):

        if prod:
            decks = (i["txid"] for i in provider.listtransactions("PAPROD"))
        else:
            decks = (i["txid"] for i in provider.listtransactions("PATEST"))

    if isinstance(provider, Cryptoid) or isinstance(provider, Blockbook):

        if prod:
            decks = (i for i in provider.listtransactions(pa_params.P2TH_addr))
        else:
            decks = (i for i in provider.listtransactions(pa_params.test_P2TH_addr))

    return decks def card_bundle_parser(bundle: CardBundle, debug=False) -> Iterator:
    '''this function wraps all the card transfer parsing'''

    try:
        # first vout of the bundle must pay to deck.p2th
        validate_card_transfer_p2th(bundle.deck, bundle.vouts[0])

        # second vout must be OP_RETURN with card_metainfo
        card_metainfo = parse_card_transfer_metainfo(
                            read_tx_opreturn(bundle.vouts[1]),
                            bundle.deck.version
                            )

    # if any of this exceptions is raised, return None
    except (InvalidCardTransferP2TH,
            CardVersionMismatch,
            CardNumberOfDecimalsMismatch,
            RecieverAmountMismatch,
            DecodeError,
            TypeError,
            InvalidNulldataOutput) as e:

        if debug:
            print(e)  # re-do as logging later on
        return
        yield

    # check for decimals
    if not card_metainfo["number_of_decimals"] == bundle.deck.number_of_decimals:
        raise CardNumberOfDecimalsMismatch(
                {"error": "Number of decimals does not match."}
                )

    # deduce the individual cards in the bundle
    cards = card_postprocess(card_metainfo, bundle.vouts)

    #  drop the vouts property
    del bundle.__dict__['vouts']

    for c in cards:
        d = {**c, **bundle.__dict__}

        try:
            yield CardTransfer(**d)

        # issuing cards to issuing address is forbidden,
        # this will except the error
        except InvalidCardIssue as e:

            if debug:
                print(e) def tx_serialization_order(provider: Provider, blockhash: str, txid: str) -> int:
    '''find index of this tx in the blockid'''

    return provider.getblock(blockhash)["tx"].index(txid) def find_tx_sender(provider: Provider, raw_tx: dict) -> str:
    '''find transaction sender, vin[0] is used in this case.'''

    vin = raw_tx["vin"][0]
    txid = vin["txid"]
    index = vin["vout"]
    return provider.getrawtransaction(txid, 1)["vout"][index]["scriptPubKey"]["addresses"][0] class EmptyP2THDirectory(Exception):
    '''no transactions on this P2TH directory''' class Transaction(BtcPyTx):
class MutableTransaction(Transaction, BtcPyMutableTx):
class TransactionParser(BtcPyTxParser):
    def __init__(
        self,
        version: int,
        ins: list,
        outs: list,
        locktime: Locktime,
        txid: str=None,
        network=PeercoinMainnet,
        timestamp: int=0,
    ) -> None:
    def unhexlify(cls, string, network=PeercoinMainnet):
    def deserialize(cls, string, network=PeercoinMainnet):
    def from_json(cls, tx_json, network=PeercoinMainnet):
    def to_json(self):
    def serialize(self):
    def to_mutable(self):
    def __str__(self):
    def __init__(
        self,
        version: int,
        ins: list,
        outs: list,
        locktime: Locktime,
        network=PeercoinMainnet,
        timestamp: int=0,
    ) -> None:
    def to_immutable(self):
    def _timestamp(self):
    def _txout(self, n):
    def get_next_tx(self, mutable=False):
def calculate_tx_fee(tx_size: int) -> Decimal:
def nulldata_script(data: bytes) -> NulldataScript:
def p2pkh_script(network: str, address: str) -> P2pkhScript:
def p2sh_p2pkh_script(network: str, address: str) -> P2shScript:
def tx_output(network: str, value: Decimal, n: int,
              script: ScriptSig) -> TxOut:
def make_raw_transaction(
    network: str,
    inputs: list,
    outputs: list,
    locktime: Locktime,
    timestamp: int=int(time()),
    version: int=1,
) -> MutableTransaction:
def find_parent_outputs(provider: Provider, utxo: TxIn) -> TxOut:
def sign_transaction(provider: Provider, unsigned: MutableTransaction,
                     key: Kutil) -> Transaction: def param_query(name: str) -> PAParams:
    '''Find the PAParams for a network by its long or short name. Raises
    UnsupportedNetwork if no PAParams is found.
    '''

    for pa_params in params:
        if name in (pa_params.network_name, pa_params.network_shortname,):
            return pa_params

    raise UnsupportedNetwork def net_query(name: str) -> Constants:
    '''Find the NetworkParams for a network by its long or short name. Raises
    UnsupportedNetwork if no NetworkParams is found.
    '''

    for net_params in networks:
        if name in (net_params.name, net_params.shortname,):
            return net_params

    raise UnsupportedNetwork
import concurrent.futures
from typing import Iterator, Generator, Optional
from pypeerassets.protocol import (Deck,
                                   CardBundle,
                                   CardTransfer,
                                   validate_card_issue_modes
                                   )
from pypeerassets.provider import Provider, RpcNode
from pypeerassets.pautils import (deck_parser,
                                  find_deck_spawns,
                                  card_bundle_parser,
                                  tx_serialization_order,
                                  find_tx_sender
                                  )
from pypeerassets.exceptions import EmptyP2THDirectory
from pypeerassets.transactions import (nulldata_script, tx_output,
                                       p2pkh_script,
                                       make_raw_transaction,
                                       Transaction,
                                       Locktime)
from pypeerassets.pa_constants import param_query
from pypeerassets.networks import net_query
from decimal import Decimal

'''contains main protocol logic like assembly of proof-of-timeline and parsing deck info'''









def find_all_valid_decks(provider: Provider, deck_version: int,
                         prod: bool=True) -> Generator:
    '''
    Scan the blockchain for PeerAssets decks, returns list of deck objects.
    : provider - provider instance
    : version - deck protocol version (0, 1, 2, ...)
    : test True/False - test or production P2TH
    '''

    pa_params = param_query(provider.network)

    if prod:
        p2th = pa_params.P2TH_addr
    else:
        p2th = pa_params.test_P2TH_addr

    if isinstance(provider, RpcNode):
        deck_spawns = (provider.getrawtransaction(i, 1)
                       for i in find_deck_spawns(provider))

    else:
        try:
            deck_spawns = (provider.getrawtransaction(i, 1) for i in
                           provider.listtransactions(p2th))
        except TypeError as err:  # it will except if no transactions are found on this P2TH

            raise EmptyP2THDirectory(err)