예제 #1
0
 def public_key(self, value):
     if value is None:
         self._public_key = None
         return
     if not isinstance(value, PublicKey):
         raise TypeError("cannot assign value of type {} as BotRegistration's public key (expected type: PublicKey)".format(type(value)))
     self._public_key = PublicKey(specifier=value.specifier, hash=value.hash)
예제 #2
0
    def record_get(self, identifier):
        """
        Get a 3Bot record registered on a TFchain network
        @param identifier: unique 3Bot id, public key or (bot) name to search a 3Bot record for
        """
        endpoint = "/explorer/3bot"
        if isinstance(identifier, int):
            identifier = str(identifier)
        elif isinstance(identifier, BotName):
            endpoint = "/explorer/whois/3bot"
            identifier = str(identifier)
        elif isinstance(identifier, PublicKey):
            identifier = str(identifier)
        elif isinstance(identifier, str):
            if BotName.REGEXP.match(identifier) is not None:
                endpoint = "/explorer/whois/3bot"
            else:
                try:
                    PublicKey.from_json(identifier)
                except ValueError as exc:
                    raise ValueError(
                        "a 3Bot identifier in string format has to be either a valid BotName or PublicKey, '{}' is neither"
                        .format(identifier)) from exc
        else:
            raise TypeError("identifier of type {} is invalid".format(
                type(identifier)))
        # identifier is a str at this point
        # and endpoint is configured

        # fetch the data
        endpoint += "/{}".format(identifier)
        try:
            resp = self._client.explorer_get(endpoint=endpoint)
        except tfchain.errors.ExplorerNoContent as exc:
            raise tfchain.errors.ThreeBotNotFound(identifier) from exc
        resp = json_loads(resp)
        try:
            # return the fetched record as a named tuple, for easy semi-typed access
            record = resp['record']
            return ThreeBotRecord(
                identifier=int(record['id']),
                names=[
                    BotName.from_json(name)
                    for name in record.get('names', []) or []
                ],
                addresses=[
                    NetworkAddress.from_json(address)
                    for address in record.get('addresses', []) or []
                ],
                public_key=PublicKey.from_json(record['publickey']),
                expiration=int(record['expiration']),
            )
        except KeyError as exc:
            # return a KeyError as an invalid Explorer Response
            raise tfchain.errors.ExplorerInvalidResponse(
                str(exc), endpoint, resp) from exc
예제 #3
0
 def parent_public_key(self, value):
     if value is None:
         self._parent_public_key = None
         return
     if not isinstance(value, PublicKey):
         raise TypeError("cannot assign value of type {} as BotRecordUpdateTransactions's parent public key (expected type: PublicKey)".format(type(value)))
     self._parent_public_key = PublicKey(specifier=value.specifier, hash=value.hash)
예제 #4
0
 def receiver_parent_public_key(self, value):
     if value is None:
         self._receiver_parent_public_key = None
         return
     if not isinstance(value, PublicKey):
         raise TypeError("cannot assign value of type {} as BotNameTransferTransaction's receiver parent public key (expected type: PublicKey)".format(type(value)))
     self._receiver_parent_public_key = PublicKey(specifier=value.specifier, hash=value.hash)
예제 #5
0
 def threebot_record_get(self, identifier, endpoint):
     """
     Add a block response to the stub explorer at the given height.
     """
     assert isinstance(identifier, str)
     if identifier.isdigit():
         try:
             return self._threebot_records[int(identifier)]
         except KeyError as exc:
             raise tfchain.errors.ExplorerNoContent(
                 "no 3Bot record could be found for identifier {}".format(identifier), endpoint=endpoint) from exc
     if BotName.REGEXP.match(identifier) is not None:
         for record in self._threebot_records.values():
             for name in record.names:
                 if name.value == identifier:
                     return record
         raise tfchain.errors.ExplorerNoContent(
             "no content found for 3Bot identifier {}".format(identifier), endpoint=endpoint)
     # must be a public key
     pk = PublicKey.from_json(identifier)
     for record in self._threebot_records.values():
         if record.public_key.unlockhash == pk.unlockhash:
             return record
     raise tfchain.errors.ExplorerNoContent(
         "no content found for 3Bot identifier {}".format(identifier), endpoint=endpoint)
예제 #6
0
 def _from_json_data_object(self, data):
     # decode public key
     if 'pubkey' in data:
         self._public_key = PublicKey.from_json(data['pubkey'])
     else:
         self._public_key = None
     # decode signature
     if 'signature' in data:
         self._signature = ED25519Signature.from_json(data['signature'])
     else:
         self._signature = None
     # decode registration fee
     if 'regfee' in data:
         self.registration_fee = Currency.from_json(data['regfee'])
     else:
         self.registration_fee = None
     # decode transaction fee
     if 'txfee' in data:
         self._transaction_fee = Currency.from_json(data['txfee'])
     else:
         self._transaction_fee = None
     # decode coin inputs
     self._coin_inputs = [
         CoinInput.from_json(ci) for ci in data.get('coininputs', []) or []
     ]
     # decode refund coin output (if it exists)
     if 'refundcoinoutput' in data:
         self._refund_coin_output = CoinOutput.from_json(
             data['refundcoinoutput'])
     else:
         self._refund_coin_output = None
예제 #7
0
 def _from_json_data_object(self, data):
     self._addresses = [
         NetworkAddress.from_json(address)
         for address in data.get('addresses', []) or []
     ]
     self._names = [
         BotName.from_json(name) for name in data.get('names', []) or []
     ]
     self._number_of_months = int(data.get('nrofmonths', 0) or 0)
     if 'txfee' in data:
         self._transaction_fee = Currency.from_json(data['txfee'])
     else:
         self._transaction_fee = None
     self._coin_inputs = [
         CoinInput.from_json(ci) for ci in data.get('coininputs', []) or []
     ]
     if 'refundcoinoutput' in data:
         self._refund_coin_output = CoinOutput.from_json(
             data['refundcoinoutput'])
     else:
         self._refund_coin_output = None
     if 'identification' not in data or data['identification'] in (None,
                                                                   {}):
         self._public_key = None
         self._signature = None
     else:
         identification = data['identification']
         self._public_key = PublicKey.from_json(identification['publickey'])
         self._signature = ED25519Signature.from_json(
             identification['signature'], as_array=True)
예제 #8
0
def test():
    # create a tfchain client for devnet
    c = tfchain.TFChainClient.TFChainClient(network_type="devnet")

    # (we replace internal client logic with custom logic as to ensure we can test without requiring an active network)
    explorer_client = TFChainExplorerGetClientStub()
    # add threebot record:
    explorer_client.threebot_record_add(
        ThreeBotRecord(
            identifier=3,
            names=[BotName(value="chatbot.example")],
            addresses=[NetworkAddress(address="example.org")],
            public_key=PublicKey.from_json(
                "ed25519:e4f55bc46b5feb37c03a0faa2d624a9ee1d0deb5059aaa9625d8b4f60f29bcab"
            ),
            expiration=1552581420,
        ))
    # overwrite the external get logic
    c.explorer_get = explorer_client.explorer_get

    # all the following will allow you to get the same 3Bot record
    for identifier in [
            3,
            BotName(value="chatbot.example"), "chatbot.example",
            "ed25519:e4f55bc46b5feb37c03a0faa2d624a9ee1d0deb5059aaa9625d8b4f60f29bcab",
            PublicKey.from_json(
                "ed25519:e4f55bc46b5feb37c03a0faa2d624a9ee1d0deb5059aaa9625d8b4f60f29bcab"
            )
    ]:
        record = c.threebot.record_get(identifier)
        # the unique 3Bot identifier can be accessed
        assert record.identifier == 3
        # all names (aliases of the 3Bot) can be accessed
        assert len(record.names) == 1
        assert record.names[0].value == "chatbot.example"
        # all network addresses that point can be accessed
        assert len(record.addresses) == 1
        assert record.addresses[0].value == "example.org"
        # public key can be accessed
        assert record.public_key.unlockhash == "01b73c4e869b6167abe6180ebe7a907f56e0357b4a2f65eb53d22baad84650eb62fce66ba036d0"
        # timestamp on which the 3Bot expires, unless more months are paid (see Record Update)
        assert record.expiration == 1552581420

    # if the 3Bot cannot be found, the tfchain.errors.ThreeBotNotFound exception will be raised
    with pytest.raises(tfchain.errors.ThreeBotNotFound):
        c.threebot.record_get(1)
예제 #9
0
 def public_key(self, value):
     if value == None:
         self._pub_key = None
         return
     if not isinstance(value, PublicKey):
         raise TypeError(
             "cannot assign value of type {} as FulfillmentAtomicSwap's public key (expected type: PublicKey)"
             .format(type(value)))
     self._pub_key = PublicKey(specifier=value.specifier, hash=value.hash)
예제 #10
0
 def public_key(self, pk):
     if pk == None:
         self._public_key = None
         return
     if not isinstance(pk, PublicKey):
         raise TypeError(
             "cannot assign value of type {} as PublicKeySignaturePair's public key (expected type: PublicKey)"
             .format(type(pk)))
     self._public_key = PublicKey(specifier=pk.specifier, hash=pk.hash)
예제 #11
0
 def parent_public_key(self):
     if self._parent_public_key is None:
         return PublicKey()
     return self._parent_public_key
예제 #12
0
 def receiver_parent_public_key(self):
     if self._receiver_parent_public_key is None:
         return PublicKey()
     return self._receiver_parent_public_key
예제 #13
0
class TransactionV144(BotTransactionBaseClass):
    _SPECIFIER = b'bot register tx\0'

    def __init__(self):
        self._addresses = []
        self._names = []
        self._number_of_months = 0
        self._transaction_fee = None
        self._coin_inputs = []
        self._refund_coin_output = None
        self._public_key = None
        self._signature = None

        super().__init__()

    @property
    def version(self):
        return TransactionVersion.THREEBOT_REGISTRATION

    @property
    def required_bot_fees(self):
        """
        The fees required to pay for this 3Bot Registration Transaction.
        """
        # a static registration fee has to be paid
        fees = 0 + BotTransactionBaseClass.BOT_FEE_REGISTRATION
        # the amount of desired months also has to be paid
        fees += BotTransactionBaseClass.compute_monthly_bot_fees(self._number_of_months)
        # if more than one name is defined it also has to be paid
        lnames = len(self._names)
        if lnames > 1:
            fees += BotTransactionBaseClass.BOT_FEE_ADDITIONAL_NAME * (lnames-1)
        # no fee has to be paid for the used network addresses during registration
        # return the total fees
        return fees

    @property
    def coin_inputs(self):
        """
        Coin inputs of this Transaction,
        used as funding for coin outputs, fees and any other kind of coin output.
        """
        return self._coin_inputs
    @coin_inputs.setter
    def coin_inputs(self, value):
        self._coin_inputs = []
        if not value:
            return
        for ci in value:
            self.coin_input_add(ci.parentid, ci.fulfillment, parent_output=ci.parent_output)

    @property
    def refund_coin_output(self):
        if self._refund_coin_output is None:
            return CoinOutput()
        return self._refund_coin_output

    @property
    def coin_outputs(self):
        """
        Empty list, or a singleton with the refund coin output if that one exists.
        """
        if self._refund_coin_output is None:
            return []
        return [self._refund_coin_output]
    @coin_outputs.setter
    def coin_outputs(self, value):
        if isinstance(value, list):
            lvalue = len(value)
            if lvalue == 0:
                value = None
            elif lvalue == 1:
                value = value[0]
            else:
                raise ValueError("ThreeBot only can have one coin output, a refund coin output")
        if value is None:
            self._refund_coin_output = None
        elif isinstance(value, CoinOutput):
            self._refund_coin_output = CoinOutput(value=value.value, condition=value.condition)
            self._refund_coin_output.id = value.id
        else:
            raise TypeError("cannot assign a value of type {} to coin outputs".format(type(value)))

    def coin_input_add(self, parentid, fulfillment, parent_output=None):
        ci = CoinInput(parentid=parentid, fulfillment=fulfillment)
        ci.parent_output = parent_output
        self._coin_inputs.append(ci)

    def refund_coin_output_set(self, value, condition, id=None):
        co = CoinOutput(value=value, condition=condition)
        co.id = id
        self._refund_coin_output = co

    @property
    def addresses(self):
        """
        Network addresses that will be part of the 3Bot Registration.
        """
        return self._addresses
    @addresses.setter
    def addresses(self, value):
        self._addresses = []
        if not value:
            return
        for address in value:
            self.address_add(address)

    def address_add(self, address):
        """
        Add a NetworkAddress that will be added as part of the 3Bot Registration.
        """
        if len(self._addresses) == BotTransactionBaseClass.MAX_ADDRESSES_PER_BOT:
            raise Exception("a 3Bot can have a maximum of {} addresses, there is no more space for {} ({})".format(
                BotTransactionBaseClass.MAX_ADDRESSES_PER_BOT, address, type(address)))
        self._addresses.append(NetworkAddress(address=address))
    
    @property
    def names(self):
        """
        Bot names that will be part of the 3Bot Registration.
        """
        return self._names
    @names.setter
    def names(self, value):
        self._names = []
        if not value:
            return
        for name in value:
            self.name_add(name)

    def name_add(self, name):
        """
        Add a BotName that will be added as part of the 3Bot Registration.
        """
        if len(self._names) == BotTransactionBaseClass.MAX_NAMES_PER_BOT:
            raise Exception("a 3Bot can have a maximum of {} names, there is no more space for {} ({})".format(
                BotTransactionBaseClass.MAX_NAMES_PER_BOT, name, type(name)))
        self._names.append(BotName(value=name))

    @property
    def transaction_fee(self):
        if self._transaction_fee is None:
            return Currency()
        return self._transaction_fee
    @transaction_fee.setter
    def transaction_fee(self, txfee):
        if txfee is None:
            self._transaction_fee = None
        else:
            self._transaction_fee = Currency(value=txfee)

    @property
    def miner_fees(self):
        if self._transaction_fee is None:
            return []
        return [self._transaction_fee]

    @property
    def number_of_months(self):
        return self._number_of_months

    @number_of_months.setter
    def number_of_months(self, n):
        if n < 1 or n > 24:
            raise ValueError("number of months for a 3Bot Registration Transaction has to be in the inclusive range [1,24]")
        self._number_of_months = n
    
    @property
    def signature(self):
        if self._signature is None:
            return ED25519Signature(as_array=True)
        return self._signature
    @signature.setter
    def signature(self, value):
        if value is None:
            self._signature = None
            return
        self._signature = ED25519Signature(value=value, as_array=True)

    @property
    def public_key(self):
        if self._public_key is None:
            return PublicKey()
        return self._public_key
    @public_key.setter
    def public_key(self, value):
        if value is None:
            self._public_key = None
            return
        if not isinstance(value, PublicKey):
            raise TypeError("cannot assign value of type {} as BotRegistration's public key (expected type: PublicKey)".format(type(value)))
        self._public_key = PublicKey(specifier=value.specifier, hash=value.hash)

    def signature_add(self, public_key, signature):
        """
        Implements SignatureCallbackBase.
        """
        if self._public_key.unlockhash != public_key.unlockhash:
            raise ValueError("given public key ({}) does not equal public key ({})".format(
                str(self._public_key.unlockhash), str(public_key.unlockhash)))
        self.signature = signature

    def _signature_hash_input_get(self, *extra_objects):
        e = encoder_rivine_get()

        # encode the transaction version
        e.add_int8(self.version)

        # encode the specifier
        e.add_array(TransactionV144._SPECIFIER)

        # extra objects if any
        if extra_objects:
            e.add_all(*extra_objects)

        # encode addresses, names and number of months
        e.add_all(self.addresses, self.names)
        e.add_int8(self.number_of_months)

        # encode coin inputs
        e.add(len(self.coin_inputs))
        for ci in self.coin_inputs:
            e.add(ci.parentid)

        # encode transaction fee
        e.add_all(self.transaction_fee)

        # encode refund coin output
        if self._refund_coin_output is None:
            e.add_int8(0)
        else:
            e.add_int8(1)
            e.add(self._refund_coin_output)
        
        # encode public key
        e.add(self.public_key)

        # return data
        return e.data

    def _id_input_compute(self):
        return bytearray(TransactionV144._SPECIFIER) + self._binary_encode_data()

    def _binary_encode_data(self):
        e = encoder_rivine_get()

        # get addresses and names
        addresses = self.addresses
        names = self.names
        addresses_length = len(addresses)
        names_length = len(names)

        # encode bot binary encoding prefix (containing length and refund info)
        maf = BotMonthsAndFlagsData(
            number_of_months=self.number_of_months,
            has_addresses=(addresses_length>0),
            has_names=(names_length>0),
            has_refund=(self._refund_coin_output is not None),
        )
        e.add(maf)
        # encode the address and name length
        e.add_int8(addresses_length | (names_length<< 4))
        
        # encode all addresses and names
        e.add_array(addresses)
        e.add_array(names)
        
        # encode transaction fee and coin inputs
        e.add_all(self.transaction_fee, self.coin_inputs)

        # encode refund coin output, if defined
        if maf.has_refund:
            e.add(self._refund_coin_output)
    
        # encode the identification at the end
        e.add_all(self.public_key, self.signature)

        # return encoded data
        return e.data

    def _from_json_data_object(self, data):
        self._addresses = [NetworkAddress.from_json(address) for address in data.get('addresses', []) or []]
        self._names = [BotName.from_json(name) for name in data.get('names', []) or []]
        self._number_of_months = int(data.get('nrofmonths', 0) or 0)
        if 'txfee' in data:
            self._transaction_fee = Currency.from_json(data['txfee'])
        else:
            self._transaction_fee = None
        self._coin_inputs = [CoinInput.from_json(ci) for ci in data.get('coininputs', []) or []]
        if 'refundcoinoutput' in data:
            self._refund_coin_output = CoinOutput.from_json(data['refundcoinoutput'])
        else:
            self._refund_coin_output = None
        if 'identification' not in data or data['identification'] in (None, {}):
            self._public_key = None
            self._signature = None
        else:
            identification = data['identification']
            self._public_key = PublicKey.from_json(identification['publickey'])
            self._signature = ED25519Signature.from_json(identification['signature'], as_array=True)

    def _json_data_object(self):
        output = {
            'nrofmonths': self.number_of_months,
            'txfee': self.transaction_fee.json(),
            'coininputs': [ci.json() for ci in self.coin_inputs],
            'identification': {
                'publickey': self._public_key.json(),
                'signature': self._signature.json(),
            },
        }
        addresses = self.addresses
        if len(addresses) > 0:
            output['addresses'] = [address.json() for address in addresses]
        names = self.names
        if len(names) > 0:
            output['names'] = [name.json() for name in names]
        if self._refund_coin_output is not None:
            output['refundcoinoutput'] = self._refund_coin_output.json()
        return output

    def _extra_signature_requests_new(self):
        if self._public_key is None:
            # if no parent public key is defined, cannot do anything
            return []
        if self._signature is not None:
            return [] # nothing to do
        # generate the input hash func
        input_hash_func = InputSignatureHashFactory(self, BotTransactionBaseClass.SPECIFIER_SENDER).signature_hash_new
        # define the input_hash_new generator function,
        # used to create the input hash for creating the signature
        unlockhash = self._public_key.unlockhash
        def input_hash_gen(public_key):
            return input_hash_func()
        # create the only signature request
        return [SignatureRequest(unlockhash=unlockhash, input_hash_gen=input_hash_gen, callback=self)]

    def _extra_is_fulfilled(self):
        return self._signature is not None
def test():
    # create a tfchain client for devnet
    c = tfchain.TFChainClient.TFChainClient(network_type="devnet")

    # (we replace internal client logic with custom logic as to ensure we can test without requiring an active network)
    explorer_client = TFChainExplorerGetClientStub()
    explorer_client.hash_add(
        '014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a',
        '{"hashtype":"unlockhash","block":{"minerpayoutids":null,"transactions":null,"rawblock":{"parentid":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":0,"pobsindexes":{"BlockHeight":0,"TransactionIndex":0,"OutputIndex":0},"minerpayouts":null,"transactions":null},"blockid":"0000000000000000000000000000000000000000000000000000000000000000","difficulty":"0","estimatedactivebs":"0","height":0,"maturitytimestamp":0,"target":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"totalcoins":"0","arbitrarydatatotalsize":0,"minerpayoutcount":0,"transactioncount":0,"coininputcount":0,"coinoutputcount":0,"blockstakeinputcount":0,"blockstakeoutputcount":0,"minerfeecount":0,"arbitrarydatacount":0},"blocks":null,"transaction":{"id":"0000000000000000000000000000000000000000000000000000000000000000","height":0,"parent":"0000000000000000000000000000000000000000000000000000000000000000","rawtransaction":{"version":0,"data":{"coininputs":[],"minerfees":null}},"coininputoutputs":null,"coinoutputids":null,"coinoutputunlockhashes":null,"blockstakeinputoutputs":null,"blockstakeoutputids":null,"blockstakeunlockhashes":null,"unconfirmed":false},"transactions":[{"id":"066c03b5cf6db18edd6013591ae6292e8a04f1cc18fc6f0a0f3946d40bb087b3","height":3628,"parent":"761af2728b6f07bbccf4ddaf330c0bb1465246f6fe25fbfb9989129b69fbd899","rawtransaction":{"version":1,"data":{"coininputs":[{"parentid":"670045cf43421b21577c27613569b65ba0cec613ae5437ee15dc0eea95241cbc","fulfillment":{"type":1,"data":{"publickey":"ed25519:bdea9aff09bcdc66529f6e2ef1bb763a3bab83ce542e8673d97aeaed0581ad97","signature":"e5a47b166eb6724ca7a72b5ddaeff287ebdde09751ed10e8850d320744afc5f45e59aa66e2cc4dc45ff593a0eea696c4e780b81636a1ce748c7149a9ee0ba807"}}},{"parentid":"1d686b8dfe44dfbfea55f327a52d9701a52938cb2c829f709dfb8059bc0e5f87","fulfillment":{"type":1,"data":{"publickey":"ed25519:64ae81a176302ea9ea47ec673f105da7a25e52bdf0cbb5b63d49fc2c69ed2eaa","signature":"5ce7a10373214d269837e9c176ec806469d2b80f200f9ee961c63553b6ee200f88958293a571889f26b2b52664f3c9ff2441eaf8ed61830b51d640003b06fb00"}}}],"coinoutputs":[{"value":"501000000000","condition":{"type":1,"data":{"unlockhash":"0186cea43fa0d303a6379ae76dd79f014698956fb982751549e3ff3844b23fa9551c1725470f55"}}},{"value":"198000000000","condition":{"type":1,"data":{"unlockhash":"014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a"}}}],"minerfees":["1000000000"]}},"coininputoutputs":[{"value":"200000000000","condition":{"type":1,"data":{"unlockhash":"018f5a43327fb865843808ddf549f1b1c06376e07195423778751056be626841f42dcf25a593fd"}},"unlockhash":"018f5a43327fb865843808ddf549f1b1c06376e07195423778751056be626841f42dcf25a593fd"},{"value":"500000000000","condition":{"type":1,"data":{"unlockhash":"014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a"}},"unlockhash":"014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a"}],"coinoutputids":["4b43906584620e8a13d98a917d424fb154dd1222a72b886a887c416b2a9120f5","19d4e81d057b4c93a7763f3dfe878f6a37d6111a3808b93afff4b369de0f5376"],"coinoutputunlockhashes":["0186cea43fa0d303a6379ae76dd79f014698956fb982751549e3ff3844b23fa9551c1725470f55","014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a"],"blockstakeinputoutputs":null,"blockstakeoutputids":null,"blockstakeunlockhashes":null,"unconfirmed":false},{"id":"0b5d7509e467b943af12db2d73ddb78a5ec8cfdf64c3c6ecacb2de4af68bdc4d","height":33,"parent":"40e281daee6f113b788d64b8247381c36fe4885233a7475c9be2cce852e1696e","rawtransaction":{"version":1,"data":{"coininputs":[{"parentid":"a3c8f44d64c0636018a929d2caeec09fb9698bfdcbfa3a8225585a51e09ee563","fulfillment":{"type":1,"data":{"publickey":"ed25519:d285f92d6d449d9abb27f4c6cf82713cec0696d62b8c123f1627e054dc6d7780","signature":"e5b69be3b385f0a33f7978e0d6af30cf37c975bbb99d0f3d06f199cd579d3e6854f6cb398c88082e438c89d11d04de0e7d3b2876e250d4ba8ae457f88547f605"}}}],"coinoutputs":[{"value":"1000000000000","condition":{"type":1,"data":{"unlockhash":"014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a"}}},{"value":"99998999000000000","condition":{"type":1,"data":{"unlockhash":"01f68299b26a89efdb4351a61c3a062321d23edbc1399c8499947c1313375609adbbcd3977363c"}}}],"minerfees":["1000000000"]}},"coininputoutputs":[{"value":"100000000000000000","condition":{"type":1,"data":{"unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"}},"unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"}],"coinoutputids":["75f297550acfa48490c21490f82c3c308326c16f950e17ef3a286486065a51b8","6d157a1eb23cadde122192869bc51e2a0fe44a2d8aba898cbdda8b7218a0f8cb"],"coinoutputunlockhashes":["014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a","01f68299b26a89efdb4351a61c3a062321d23edbc1399c8499947c1313375609adbbcd3977363c"],"blockstakeinputoutputs":null,"blockstakeoutputids":null,"blockstakeunlockhashes":null,"unconfirmed":false},{"id":"1b144b89c4b65cc67416e27286e802c50f78bc7326b4b0e5f6f967f99cc39419","height":200,"parent":"68e3886c9a4c59dea8c7a0eb138ede0e83716b995fd8ebfa5dfc766bfb349565","rawtransaction":{"version":1,"data":{"coininputs":[{"parentid":"6d157a1eb23cadde122192869bc51e2a0fe44a2d8aba898cbdda8b7218a0f8cb","fulfillment":{"type":1,"data":{"publickey":"ed25519:00bde9571b30e1742c41fcca8c730183402d967df5b17b5f4ced22c677806614","signature":"05c72055d4cca6d8620fd7453c89ac8deaf92c38e366604dc6405cb097321b74e2e5f7ece15b440f4af5721b8aa6ea1f57f4fce44d2a2c6c0fb7a1e068a0480d"}}}],"coinoutputs":[{"value":"500000000000","condition":{"type":1,"data":{"unlockhash":"014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a"}}},{"value":"99998498000000000","condition":{"type":1,"data":{"unlockhash":"0161fbcf58efaeba8813150e88fc33405b3a77d51277a2cdf3f4d2ab770de287c7af9d456c4e68"}}}],"minerfees":["1000000000"]}},"coininputoutputs":[{"value":"99998999000000000","condition":{"type":1,"data":{"unlockhash":"01f68299b26a89efdb4351a61c3a062321d23edbc1399c8499947c1313375609adbbcd3977363c"}},"unlockhash":"01f68299b26a89efdb4351a61c3a062321d23edbc1399c8499947c1313375609adbbcd3977363c"}],"coinoutputids":["1d686b8dfe44dfbfea55f327a52d9701a52938cb2c829f709dfb8059bc0e5f87","e27ed8bbe45177fafdfd7d21394ff483b8bde24f551dd927a046a0b7c37ec228"],"coinoutputunlockhashes":["014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a","0161fbcf58efaeba8813150e88fc33405b3a77d51277a2cdf3f4d2ab770de287c7af9d456c4e68"],"blockstakeinputoutputs":null,"blockstakeoutputids":null,"blockstakeunlockhashes":null,"unconfirmed":false},{"id":"5ab2604c7d54dd9ea39d829724e46deb8bbf2889d8627a3bb3b57907c6f4dff4","height":1433,"parent":"6b9fdad35096c5a84dcdb6e3e9ec2a8c7562f567e05b1207a7f9330ed51582bb","rawtransaction":{"version":1,"data":{"coininputs":[{"parentid":"e27ed8bbe45177fafdfd7d21394ff483b8bde24f551dd927a046a0b7c37ec228","fulfillment":{"type":1,"data":{"publickey":"ed25519:41e84f3b0f6a06dd7e45ded4d0e227869725355b73906b82d9e3ffc0b6b01416","signature":"47a4d1a6aeb7403ea5c2cbcc26f84bb1964aadcb8765696db4a623694991f794663b860898db53401dd4324477c43f9fff177b74909201a03e131c2cd885cc0d"}}}],"coinoutputs":[{"value":"500000000000","condition":{"type":1,"data":{"unlockhash":"014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a"}}},{"value":"99997997000000000","condition":{"type":1,"data":{"unlockhash":"0137f6f647a3c8019f6ae215ed09902c308efbf555b3520d2112d2b18cb577e40804d6fc5fafd2"}}}],"minerfees":["1000000000"]}},"coininputoutputs":[{"value":"99998498000000000","condition":{"type":1,"data":{"unlockhash":"0161fbcf58efaeba8813150e88fc33405b3a77d51277a2cdf3f4d2ab770de287c7af9d456c4e68"}},"unlockhash":"0161fbcf58efaeba8813150e88fc33405b3a77d51277a2cdf3f4d2ab770de287c7af9d456c4e68"}],"coinoutputids":["b90422bad2dffde79f0a46bd0a41055cf7974b080e115d76f69891ca31d31f11","f386312779f382f16fa836038b3d25536b928ba88ae1afa1f8c0e8dc25c0ba16"],"coinoutputunlockhashes":["014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a","0137f6f647a3c8019f6ae215ed09902c308efbf555b3520d2112d2b18cb577e40804d6fc5fafd2"],"blockstakeinputoutputs":null,"blockstakeoutputids":null,"blockstakeunlockhashes":null,"unconfirmed":false},{"id":"81d81b413fc8af3e528478e639d5a6e999e5e96611593cd2458b4d2d7abc3880","height":1433,"parent":"6b9fdad35096c5a84dcdb6e3e9ec2a8c7562f567e05b1207a7f9330ed51582bb","rawtransaction":{"version":1,"data":{"coininputs":[{"parentid":"f386312779f382f16fa836038b3d25536b928ba88ae1afa1f8c0e8dc25c0ba16","fulfillment":{"type":1,"data":{"publickey":"ed25519:4e42a2fcfc0963d6fa7bb718fd088d9b6544331e8562d2743e730cdfbedeb55a","signature":"4abae694cf6a6408eca6f034ea2ec6930b9a73303c3e70d3444cf36a9966b0c4cc1b65168c23640affb5bdeae539ea583d42da52c58d21f59b71c288b3faef0e"}}}],"coinoutputs":[{"value":"2000000000000","condition":{"type":1,"data":{"unlockhash":"014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a"}}},{"value":"99995996000000000","condition":{"type":1,"data":{"unlockhash":"0173f82c3ee74286c33fee8d883a7e9e759c6230b9e4e956ef233d7202bde69da45054270eef99"}}}],"minerfees":["1000000000"],"arbitrarydata":"Zm9vYmFy"}},"coininputoutputs":[{"value":"99997997000000000","condition":{"type":1,"data":{"unlockhash":"0137f6f647a3c8019f6ae215ed09902c308efbf555b3520d2112d2b18cb577e40804d6fc5fafd2"}},"unlockhash":"0137f6f647a3c8019f6ae215ed09902c308efbf555b3520d2112d2b18cb577e40804d6fc5fafd2"}],"coinoutputids":["d1f74e90eba8095e78f08a6284c7b76d4cda86b06ac742062d6e0e02dc4607eb","1edccdb04100ffd05d97431b6798533fa57dc7ed1ea58c75f0f445fb64442d6e"],"coinoutputunlockhashes":["014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a","0173f82c3ee74286c33fee8d883a7e9e759c6230b9e4e956ef233d7202bde69da45054270eef99"],"blockstakeinputoutputs":null,"blockstakeoutputids":null,"blockstakeunlockhashes":null,"unconfirmed":false}],"multisigaddresses":["039e16ed27b2dfa3a5bbb1fa2b5f240ba7ff694b34a52bfc5bed6d4c3b14b763c011d7503ccb3a"],"unconfirmed":false}'
    )
    explorer_client.hash_add(
        '018f5a43327fb865843808ddf549f1b1c06376e07195423778751056be626841f42dcf25a593fd',
        '{"hashtype":"unlockhash","block":{"minerpayoutids":null,"transactions":null,"rawblock":{"parentid":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":0,"pobsindexes":{"BlockHeight":0,"TransactionIndex":0,"OutputIndex":0},"minerpayouts":null,"transactions":null},"blockid":"0000000000000000000000000000000000000000000000000000000000000000","difficulty":"0","estimatedactivebs":"0","height":0,"maturitytimestamp":0,"target":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"totalcoins":"0","arbitrarydatatotalsize":0,"minerpayoutcount":0,"transactioncount":0,"coininputcount":0,"coinoutputcount":0,"blockstakeinputcount":0,"blockstakeoutputcount":0,"minerfeecount":0,"arbitrarydatacount":0},"blocks":null,"transaction":{"id":"0000000000000000000000000000000000000000000000000000000000000000","height":0,"parent":"0000000000000000000000000000000000000000000000000000000000000000","rawtransaction":{"version":0,"data":{"coininputs":[],"minerfees":null}},"coininputoutputs":null,"coinoutputids":null,"coinoutputunlockhashes":null,"blockstakeinputoutputs":null,"blockstakeoutputids":null,"blockstakeunlockhashes":null,"unconfirmed":false},"transactions":[{"id":"066c03b5cf6db18edd6013591ae6292e8a04f1cc18fc6f0a0f3946d40bb087b3","height":3628,"parent":"761af2728b6f07bbccf4ddaf330c0bb1465246f6fe25fbfb9989129b69fbd899","rawtransaction":{"version":1,"data":{"coininputs":[{"parentid":"670045cf43421b21577c27613569b65ba0cec613ae5437ee15dc0eea95241cbc","fulfillment":{"type":1,"data":{"publickey":"ed25519:bdea9aff09bcdc66529f6e2ef1bb763a3bab83ce542e8673d97aeaed0581ad97","signature":"e5a47b166eb6724ca7a72b5ddaeff287ebdde09751ed10e8850d320744afc5f45e59aa66e2cc4dc45ff593a0eea696c4e780b81636a1ce748c7149a9ee0ba807"}}},{"parentid":"1d686b8dfe44dfbfea55f327a52d9701a52938cb2c829f709dfb8059bc0e5f87","fulfillment":{"type":1,"data":{"publickey":"ed25519:64ae81a176302ea9ea47ec673f105da7a25e52bdf0cbb5b63d49fc2c69ed2eaa","signature":"5ce7a10373214d269837e9c176ec806469d2b80f200f9ee961c63553b6ee200f88958293a571889f26b2b52664f3c9ff2441eaf8ed61830b51d640003b06fb00"}}}],"coinoutputs":[{"value":"501000000000","condition":{"type":1,"data":{"unlockhash":"0186cea43fa0d303a6379ae76dd79f014698956fb982751549e3ff3844b23fa9551c1725470f55"}}},{"value":"198000000000","condition":{"type":1,"data":{"unlockhash":"014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a"}}}],"minerfees":["1000000000"]}},"coininputoutputs":[{"value":"200000000000","condition":{"type":1,"data":{"unlockhash":"018f5a43327fb865843808ddf549f1b1c06376e07195423778751056be626841f42dcf25a593fd"}},"unlockhash":"018f5a43327fb865843808ddf549f1b1c06376e07195423778751056be626841f42dcf25a593fd"},{"value":"500000000000","condition":{"type":1,"data":{"unlockhash":"014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a"}},"unlockhash":"014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a"}],"coinoutputids":["4b43906584620e8a13d98a917d424fb154dd1222a72b886a887c416b2a9120f5","19d4e81d057b4c93a7763f3dfe878f6a37d6111a3808b93afff4b369de0f5376"],"coinoutputunlockhashes":["0186cea43fa0d303a6379ae76dd79f014698956fb982751549e3ff3844b23fa9551c1725470f55","014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a"],"blockstakeinputoutputs":null,"blockstakeoutputids":null,"blockstakeunlockhashes":null,"unconfirmed":false},{"id":"d0d290dccddaf086708e4163eacd420bfbaa7acb77deea97c6a8792a139f6e7e","height":3583,"parent":"ca746d80740ff45f6e7f0d6c8b6278b35b1040b4e21baaf41bd03cd3a7a176be","rawtransaction":{"version":1,"data":{"coininputs":[{"parentid":"1edccdb04100ffd05d97431b6798533fa57dc7ed1ea58c75f0f445fb64442d6e","fulfillment":{"type":1,"data":{"publickey":"ed25519:7469d51063cdb690cc8025db7d28faadc71ff69f7c372779bf3a1e801a923e02","signature":"6f38467bb4ca23900e424d5ac2066a09b61fe9d9ae84a4231755063f591d04711f0a5ab11b60b42c2c32a8e7f73fc1cc1cb0073b036bc417ed504a8ef5f25402"}}}],"coinoutputs":[{"value":"200000000000","condition":{"type":1,"data":{"unlockhash":"018f5a43327fb865843808ddf549f1b1c06376e07195423778751056be626841f42dcf25a593fd"}}},{"value":"99995795000000000","condition":{"type":1,"data":{"unlockhash":"01f706dcbb9f0cb1e97d891ada3a133f68612ebff948c5bbae7851108a65dab7782907eecd86be"}}}],"minerfees":["1000000000"]}},"coininputoutputs":[{"value":"99995996000000000","condition":{"type":1,"data":{"unlockhash":"0173f82c3ee74286c33fee8d883a7e9e759c6230b9e4e956ef233d7202bde69da45054270eef99"}},"unlockhash":"0173f82c3ee74286c33fee8d883a7e9e759c6230b9e4e956ef233d7202bde69da45054270eef99"}],"coinoutputids":["670045cf43421b21577c27613569b65ba0cec613ae5437ee15dc0eea95241cbc","984555e190d58dc752aad81b0a6cf6194fa5bf89b8614efcd9604d138a8953bc"],"coinoutputunlockhashes":["018f5a43327fb865843808ddf549f1b1c06376e07195423778751056be626841f42dcf25a593fd","01f706dcbb9f0cb1e97d891ada3a133f68612ebff948c5bbae7851108a65dab7782907eecd86be"],"blockstakeinputoutputs":null,"blockstakeoutputids":null,"blockstakeunlockhashes":null,"unconfirmed":false}],"multisigaddresses":null,"unconfirmed":false}'
    )
    explorer_client.chain_info = '{"blockid":"552e410481cce1358ffcd4687f4199dd2181c799d55da26178e55643355bbd2e","difficulty":"27801","estimatedactivebs":"59","height":3644,"maturitytimestamp":1549012510,"target":[0,2,91,116,78,165,130,72,116,162,127,4,125,67,108,16,140,247,132,198,107,159,114,177,44,25,18,162,38,157,169,245],"totalcoins":"0","arbitrarydatatotalsize":6,"minerpayoutcount":3650,"transactioncount":3652,"coininputcount":12,"coinoutputcount":15,"blockstakeinputcount":3644,"blockstakeoutputcount":3645,"minerfeecount":7,"arbitrarydatacount":1}'
    explorer_client.hash_add(
        '552e410481cce1358ffcd4687f4199dd2181c799d55da26178e55643355bbd2e',
        '{"hashtype":"blockid","block":{"minerpayoutids":["468db689f752414702ef3a5aa06238f03a4539434a61624b3b8a0fb5dc38a211"],"transactions":[{"id":"2396f8e57bbb9b22bd1d749d5de3fd532ea6886e9660a556a13571d701d83e27","height":3644,"parent":"552e410481cce1358ffcd4687f4199dd2181c799d55da26178e55643355bbd2e","rawtransaction":{"version":1,"data":{"coininputs":null,"blockstakeinputs":[{"parentid":"ff5a002ec356b7cb24fbee9f076f239fb8c72d5a8a448cee92ee6d29a87aef52","fulfillment":{"type":1,"data":{"publickey":"ed25519:d285f92d6d449d9abb27f4c6cf82713cec0696d62b8c123f1627e054dc6d7780","signature":"7bec94dfb87640726c6a14de2110599db0f81cf9fa456249e7bf79b0c74b79517edde25c4ee87f181880af44fe6ee054ff20b74eda2144fe07fa5bfb9d884208"}}}],"blockstakeoutputs":[{"value":"3000","condition":{"type":1,"data":{"unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"}}}],"minerfees":null}},"coininputoutputs":null,"coinoutputids":null,"coinoutputunlockhashes":null,"blockstakeinputoutputs":[{"value":"3000","condition":{"type":1,"data":{"unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"}},"unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"}],"blockstakeoutputids":["f683e7319659c61f54e93546bc41b57c5bffe79de26c06ec7371034465804c81"],"blockstakeunlockhashes":["015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"],"unconfirmed":false}],"rawblock":{"parentid":"47db4274551b0372564f8d1ab89c596428f00e460c0b416327e53983c8765198","timestamp":1549012665,"pobsindexes":{"BlockHeight":3643,"TransactionIndex":0,"OutputIndex":0},"minerpayouts":[{"value":"10000000000","unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"}],"transactions":[{"version":1,"data":{"coininputs":null,"blockstakeinputs":[{"parentid":"ff5a002ec356b7cb24fbee9f076f239fb8c72d5a8a448cee92ee6d29a87aef52","fulfillment":{"type":1,"data":{"publickey":"ed25519:d285f92d6d449d9abb27f4c6cf82713cec0696d62b8c123f1627e054dc6d7780","signature":"7bec94dfb87640726c6a14de2110599db0f81cf9fa456249e7bf79b0c74b79517edde25c4ee87f181880af44fe6ee054ff20b74eda2144fe07fa5bfb9d884208"}}}],"blockstakeoutputs":[{"value":"3000","condition":{"type":1,"data":{"unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"}}}],"minerfees":null}}]},"blockid":"552e410481cce1358ffcd4687f4199dd2181c799d55da26178e55643355bbd2e","difficulty":"27801","estimatedactivebs":"59","height":3644,"maturitytimestamp":1549012510,"target":[0,2,91,116,78,165,130,72,116,162,127,4,125,67,108,16,140,247,132,198,107,159,114,177,44,25,18,162,38,157,169,245],"totalcoins":"0","arbitrarydatatotalsize":6,"minerpayoutcount":3650,"transactioncount":3652,"coininputcount":12,"coinoutputcount":15,"blockstakeinputcount":3644,"blockstakeoutputcount":3645,"minerfeecount":7,"arbitrarydatacount":1},"blocks":null,"transaction":{"id":"0000000000000000000000000000000000000000000000000000000000000000","height":0,"parent":"0000000000000000000000000000000000000000000000000000000000000000","rawtransaction":{"version":0,"data":{"coininputs":[],"minerfees":null}},"coininputoutputs":null,"coinoutputids":null,"coinoutputunlockhashes":null,"blockstakeinputoutputs":null,"blockstakeoutputids":null,"blockstakeunlockhashes":null,"unconfirmed":false},"transactions":null,"multisigaddresses":null,"unconfirmed":false}'
    )
    explorer_client.hash_add(
        '039e16ed27b2dfa3a5bbb1fa2b5f240ba7ff694b34a52bfc5bed6d4c3b14b763c011d7503ccb3a',
        '{"hashtype":"unlockhash","block":{"minerpayoutids":null,"transactions":null,"rawblock":{"parentid":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":0,"pobsindexes":{"BlockHeight":0,"TransactionIndex":0,"OutputIndex":0},"minerpayouts":null,"transactions":null},"blockid":"0000000000000000000000000000000000000000000000000000000000000000","difficulty":"0","estimatedactivebs":"0","height":0,"maturitytimestamp":0,"target":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"totalcoins":"0","arbitrarydatatotalsize":0,"minerpayoutcount":0,"transactioncount":0,"coininputcount":0,"coinoutputcount":0,"blockstakeinputcount":0,"blockstakeoutputcount":0,"minerfeecount":0,"arbitrarydatacount":0},"blocks":null,"transaction":{"id":"0000000000000000000000000000000000000000000000000000000000000000","height":0,"parent":"0000000000000000000000000000000000000000000000000000000000000000","rawtransaction":{"version":0,"data":{"coininputs":[],"minerfees":null}},"coininputoutputs":null,"coinoutputids":null,"coinoutputunlockhashes":null,"blockstakeinputoutputs":null,"blockstakeoutputids":null,"blockstakeunlockhashes":null,"unconfirmed":false},"transactions":[{"id":"4c70a0406f36cf354edf87642df3f34568fd0a89c052a81d11cc6e4f8fbf685e","height":45,"parent":"f7b78b17d581ff9e58ffbcce1701d4dcadb0781590ca68e839def0dc98b0360a","rawtransaction":{"version":1,"data":{"coininputs":[{"parentid":"7d4a100fc3bc08b2bdd1284c17260dd2bd6b55fd6c1429dbbd683bf362d92b50","fulfillment":{"type":1,"data":{"publickey":"ed25519:d285f92d6d449d9abb27f4c6cf82713cec0696d62b8c123f1627e054dc6d7780","signature":"c34b8ca1ab08930bc68d61026af504d62d8a8bbda9b79ae01a387560fba22d39b12021e16566732b742ea686f997b3c19c807523797cdc0d74a4d25123691004"}}},{"parentid":"83503f9cea00d562e0460eace93159a4c4dd00df4703c96947e81885b46da04c","fulfillment":{"type":1,"data":{"publickey":"ed25519:d285f92d6d449d9abb27f4c6cf82713cec0696d62b8c123f1627e054dc6d7780","signature":"f6eea681a259baf14433ac55b4293b22ca2056810ee8fed2129039224d14558f54ca58c6d96e9885cb20ecdf7e64ba81d1a83c6e9a42bf9464287fa6359d360c"}}},{"parentid":"578aa43de72b42b4f4547c5ddc7f61736b1cac206e1789bc89fcd9333cf3d1f3","fulfillment":{"type":1,"data":{"publickey":"ed25519:d285f92d6d449d9abb27f4c6cf82713cec0696d62b8c123f1627e054dc6d7780","signature":"a0521d14dfe4a0c9b8b57ed361d738b48b6a8346097246effe0b4ee67b6fecbc3a90e4671ddc0b164f6c2839df249bb5998f10216a4a674ba8d24b8ad6bdf808"}}},{"parentid":"5a1454762e6895431e1b9e4e435e4d0ad60a3881843ac46b88e220771055ca87","fulfillment":{"type":1,"data":{"publickey":"ed25519:d285f92d6d449d9abb27f4c6cf82713cec0696d62b8c123f1627e054dc6d7780","signature":"900e7868780e67bcb68af3ec6976e84289850d0db59210d4689b1c0e2deb3164b9e93eb9ee5a38850f2319463b0845163e1eee443d7b645c59485c2aa0837707"}}},{"parentid":"c04ebebe17a1759457eecaf4d5d33f5ddbe8d154b0be1606f05bc8fd02ab9cd4","fulfillment":{"type":1,"data":{"publickey":"ed25519:d285f92d6d449d9abb27f4c6cf82713cec0696d62b8c123f1627e054dc6d7780","signature":"e992b1cd3347b5362e820166d5929de7c682130c7143fc4c9ff3156f5d44110753687697a0154a2043290b3f022e2537f3e3a6807caf9150f8c255d74e386d0a"}}}],"coinoutputs":[{"value":"42000000000","condition":{"type":4,"data":{"unlockhashes":["01ffd7c884aa869056bfb832d957bb71a0005fee13c19046cebec84b3a5047ee8829eab070374b","014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a"],"minimumsignaturecount":1}}},{"value":"7000000000","condition":{"type":1,"data":{"unlockhash":"01972837ee396f22f96846a0c700f9cf7c8fa83ab4110da91a1c7d02f94f28ff03e45f1470df82"}}}],"minerfees":["1000000000"]}},"coininputoutputs":[{"value":"10000000000","condition":{"type":1,"data":{"unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"}},"unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"},{"value":"10000000000","condition":{"type":1,"data":{"unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"}},"unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"},{"value":"10000000000","condition":{"type":1,"data":{"unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"}},"unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"},{"value":"10000000000","condition":{"type":1,"data":{"unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"}},"unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"},{"value":"10000000000","condition":{"type":1,"data":{"unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"}},"unlockhash":"015a080a9259b9d4aaa550e2156f49b1a79a64c7ea463d810d4493e8242e6791584fbdac553e6f"}],"coinoutputids":["29152fe03a2c8782fcbd670579686088c52be83fa3870f5f0788073d97fb5fb2","0fc9b16bb180cd8f8a7144d65e6c8fca66994a4ccaee42e324289d4039ab2841"],"coinoutputunlockhashes":["039e16ed27b2dfa3a5bbb1fa2b5f240ba7ff694b34a52bfc5bed6d4c3b14b763c011d7503ccb3a","01972837ee396f22f96846a0c700f9cf7c8fa83ab4110da91a1c7d02f94f28ff03e45f1470df82"],"blockstakeinputoutputs":null,"blockstakeoutputids":null,"blockstakeunlockhashes":null,"unconfirmed":false}],"multisigaddresses":null,"unconfirmed":false}'
    )
    # add threebot records:
    explorer_client.threebot_record_add(
        ThreeBotRecord(
            identifier=3,
            names=[BotName(value=s) for s in ["foorbar", "chatbot.example"]],
            addresses=[
                NetworkAddress(address=s)
                for s in ["example.org", "127.0.0.1"]
            ],
            public_key=PublicKey.from_json(
                "ed25519:64ae81a176302ea9ea47ec673f105da7a25e52bdf0cbb5b63d49fc2c69ed2eaa"
            ),
            expiration=1552581420,
        ))
    explorer_client.threebot_record_add(
        ThreeBotRecord(
            identifier=5,
            names=[],
            addresses=[NetworkAddress(address=s) for s in ["bot.example.org"]],
            public_key=PublicKey.from_json(
                "ed25519:bdea9aff09bcdc66529f6e2ef1bb763a3bab83ce542e8673d97aeaed0581ad97"
            ),
            expiration=1552585420,
        ))
    c.explorer_get = explorer_client.explorer_get
    c.explorer_post = explorer_client.explorer_post

    # the devnet genesis seed is the seed of the wallet,
    # which receives all block stakes and coins in the genesis block of the tfchain devnet
    DEVNET_GENESIS_SEED = "image orchard airport business cost work mountain obscure flee alpha alert salmon damage engage trumpet route marble subway immune short tide young cycle attract"

    # create a new devnet wallet
    w = tfchain.TFChainWallet.TFChainWallet(client=c, seed=DEVNET_GENESIS_SEED)
    # we create a new wallet using an existing seed,
    # such that our seed is used and not a new randomly generated seed

    # a tfchain (JS) wallet uses the underlying tfchain client for all its
    # interaction with the tfchain network
    assert w.network_type == tfchain.TFChainClient.NetworkType.DEVNET

    # getting the balance of a wallet is as easy as getting the 'balance' property
    balance = w.balance

    # the available and locked tokens can be easily checked
    assert str(balance.available) == '3698'
    assert str(balance.locked) == '0'

    # assert the addresses are correct
    assert w.addresses == [
        "014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a",
        "018f5a43327fb865843808ddf549f1b1c06376e07195423778751056be626841f42dcf25a593fd"
    ]

    # transfering a name is easy as well:
    result = w.threebot.name_transfer(
        sender=3,  # identifier of sender 3Bot
        receiver=5,  # identifier of receiver 3Bot
        names=["foobar", "chatbot.example"
               ],  # names to be transfered from sender to receiver 3Bot
    )
    assert result.submitted  # we expect the transaction to be submitted

    expected_transaction = {
        'version': 146,
        'data': {
            'sender': {
                'id':
                3,
                'signature':
                'e1b1bb0f6f9493b17f8959d2ff67741eee0642737c277e46644e2902e805866036af2ebf8ece2c4eb02539f2f0de49679231959d677f86ff05c19bc1f6c2eb00'
            },
            'receiver': {
                'id':
                5,
                'signature':
                'be7f8a47a1de99a5aaff1684c5973d50d3e1777c271309b08175b993802d4b19980a38c7d6d59362bff542cced12818d72745405f15ed9170319020a27aae30a'
            },
            'names': ['foobar', 'chatbot.example'],
            'txfee':
            '1000000000',
            'coininputs': [{
                'parentid':
                '19d4e81d057b4c93a7763f3dfe878f6a37d6111a3808b93afff4b369de0f5376',
                'fulfillment': {
                    'type': 1,
                    'data': {
                        'publickey':
                        'ed25519:64ae81a176302ea9ea47ec673f105da7a25e52bdf0cbb5b63d49fc2c69ed2eaa',
                        'signature':
                        '47d76c8cc278c1077574d0de6aaca6c13414bbf1c13f4f97cecb7ac1a19763eb9e0cf7c2584a8ade4bc7a8f4e220aa3b5d021ffd092af6dc8a07760a21e2e101'
                    }
                }
            }],
            'refundcoinoutput': {
                'value': '97000000000',
                'condition': {
                    'type': 1,
                    'data': {
                        'unlockhash':
                        '014ad318772a09de75fb62f084a33188a7f6fb5e7b68c0ed85a5f90fe11246386b7e6fe97a5a6a'
                    }
                }
            }
        }
    }
    # ensure our transaction is as expected
    assert result.transaction.json() == expected_transaction

    # ensure the transaction is posted and as expected there as well
    txn = explorer_client.posted_transaction_get(result.transaction.id)
    assert txn.json() == expected_transaction

    # if no names to be transfered are defined, a ValueError is raised
    with pytest.raises(ValueError):
        w.threebot.name_transfer(sender=3, receiver=5, names=[])

    # if names are defined to be transfered but one of the two 3Bots do no exist,
    # an tfchain.errors.ThreeBotNotFound will be raised
    with pytest.raises(tfchain.errors.ThreeBotNotFound):
        w.threebot.name_transfer(sender=2, receiver=5, names=["foobar"])
    with pytest.raises(tfchain.errors.ThreeBotNotFound):
        w.threebot.name_transfer(sender=3, receiver=4, names=["foobar"])

    # make our receiver 3Bot expired
    explorer_client.threebot_record_add(ThreeBotRecord(
        identifier=5,
        names=[],
        addresses=[NetworkAddress(address=s) for s in ["bot.example.org"]],
        public_key=PublicKey.from_json(
            "ed25519:bdea9aff09bcdc66529f6e2ef1bb763a3bab83ce542e8673d97aeaed0581ad97"
        ),
        expiration=1549012664,
    ),
                                        force=True)

    # if the receiver 3Bot is inactive, an error will be raised
    with pytest.raises(tfchain.errors.ThreeBotInactive):
        w.threebot.name_transfer(sender=3, receiver=5, names=["foobar"])

    # make our sender 3Bot expired
    explorer_client.threebot_record_add(ThreeBotRecord(
        identifier=3,
        names=[BotName(value=s) for s in ["foorbar", "chatbot.example"]],
        addresses=[
            NetworkAddress(address=s) for s in ["example.org", "127.0.0.1"]
        ],
        public_key=PublicKey.from_json(
            "ed25519:64ae81a176302ea9ea47ec673f105da7a25e52bdf0cbb5b63d49fc2c69ed2eaa"
        ),
        expiration=1549012664,
    ),
                                        force=True)

    # if the sender and receiver 3Bots are inactive, an error will be raised
    with pytest.raises(tfchain.errors.ThreeBotInactive):
        w.threebot.name_transfer(sender=3, receiver=5, names=["foobar"])

    # make our receiver 3Bot active again
    explorer_client.threebot_record_add(ThreeBotRecord(
        identifier=5,
        names=[],
        addresses=[NetworkAddress(address=s) for s in ["bot.example.org"]],
        public_key=PublicKey.from_json(
            "ed25519:bdea9aff09bcdc66529f6e2ef1bb763a3bab83ce542e8673d97aeaed0581ad97"
        ),
        expiration=1552585420,
    ),
                                        force=True)

    # if the sender 3Bot is inactive, an error will be raised
    with pytest.raises(tfchain.errors.ThreeBotInactive):
        w.threebot.name_transfer(sender=3, receiver=5, names=["foobar"])
예제 #15
0
 def from_json_data_object(self, data):
     self._pub_key = PublicKey.from_json(data['publickey'])
     self._signature = ED25519Signature.from_json(data['signature'])
     self._secret = None
     if 'secret' in data:
         self._secret = AtomicSwapSecret.from_json(data['secret'])
예제 #16
0
 def public_key(self):
     if self._pub_key == None:
         return PublicKey()
     return self._pub_key
예제 #17
0
 def from_json(cls, obj):
     return cls(public_key=PublicKey.from_json(obj['publickey']),
                signature=ED25519Signature.from_json(obj['signature']))
예제 #18
0
 def from_json_data_object(self, data):
     self._pub_key = PublicKey.from_json(data['publickey'])
     self._signature = ED25519Signature.from_json(data['signature'])