• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

1200wd / bitcoinlib / 19944567212

04 Dec 2025 09:29PM UTC coverage: 90.921% (-0.02%) from 90.944%
19944567212

push

github

Cryp Toon
RELEASE 0.7.6 - MESSAGE SIGNING AND DETERMINISTIC SIGNATURES

8042 of 8845 relevant lines covered (90.92%)

1.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.78
/bitcoinlib/services/services.py
1
# -*- coding: utf-8 -*-
2
#
3
#    BitcoinLib - Python Cryptocurrency Library
4
#    SERVICES - Main Service connector
5
#    © 2017 - 2024 June - 1200 Web Development <http://1200wd.com/>
6
#
7
#    This program is free software: you can redistribute it and/or modify
8
#    it under the terms of the GNU Affero General Public License as
9
#    published by the Free Software Foundation, either version 3 of the
10
#    License, or (at your option) any later version.
11
#
12
#    This program is distributed in the hope that it will be useful,
13
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
14
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
#    GNU Affero General Public License for more details.
16
#
17
#    You should have received a copy of the GNU Affero General Public License
18
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
#
20

21
import json
2✔
22
import random
2✔
23
import time
2✔
24
from datetime import timedelta
2✔
25
from sqlalchemy import func
2✔
26
from bitcoinlib import services
2✔
27
from bitcoinlib.networks import Network
2✔
28
from bitcoinlib.encoding import to_bytes, int_to_varbyteint, varstr
2✔
29
from bitcoinlib.db_cache import *
2✔
30
from bitcoinlib.transactions import Transaction, transaction_update_spents
2✔
31
from bitcoinlib.blocks import Block
2✔
32

33

34
_logger = logging.getLogger(__name__)
2✔
35

36

37
class ServiceError(Exception):
2✔
38
    def __init__(self, msg=''):
2✔
39
        self.msg = msg
2✔
40
        _logger.error(msg)
2✔
41

42
    def __str__(self):
2✔
43
        return self.msg
2✔
44

45

46
class Service(object):
2✔
47
    """
48
    Class to connect to various cryptocurrency service providers. Use to receive network and blockchain information,
49
    get specific transaction information, current network fees or push a raw transaction.
50

51
    The Service class connects to 1 or more service providers at random to retrieve or send information. If a service
52
    providers fails to correctly respond the Service class will try another available provider.
53

54
    """
55

56
    def __init__(self, network=DEFAULT_NETWORK, min_providers=1, max_providers=1, providers=None,
2✔
57
                 timeout=TIMEOUT_REQUESTS, cache_uri=None, ignore_priority=False, exclude_providers=None,
58
                 max_errors=SERVICE_MAX_ERRORS, strict=True, wallet_name=None, provider_name=None):
59
        """
60
        Create a service object for the specified network. By default, the object connect to 1 service provider, but you
61
        can specify a list of providers or a minimum or maximum number of providers.
62

63
        :param network: Specify network used
64
        :type network: str, Network
65
        :param min_providers: Minimum number of providers to connect to. Default is 1. Use for instance to receive fee information from a number of providers and calculate the average fee.
66
        :type min_providers: int
67
        :param max_providers: Maximum number of providers to connect to. Default is 1.
68
        :type max_providers: int
69
        :param providers: List of providers to connect to. Default is all providers and select a provider at random.
70
        :type providers: list of str
71
        :param timeout: Timeout for web requests. Leave empty to use default from config settings
72
        :type timeout: int
73
        :param cache_uri: Database to use for caching
74
        :type cache_uri: str
75
        :param ignore_priority: Ignores provider priority if set to True. Could be used for unit testing, so no providers are missed when testing. Default is False
76
        :type ignore_priority: bool
77
        :param exclude_providers: Exclude providers in this list, can be used when problems with certain providers arise.
78
        :type exclude_providers: list of str
79
        :param strict: Strict checks of valid signatures, scripts and transactions. Normally use strict=True for wallets, transaction validations etcetera. For blockchain parsing strict=False should be used, but be sure to check warnings in the log file. Default is True.
80
        :type strict: bool
81
        :param wallet_name: Name of wallet if connecting to bitcoin node
82
        :type wallet_name: str
83
        :param provider_name: Name of specific provider to connect to. Note this is different from the providers list argument: the lists mention a type of provider such as 'blockbook' or 'bcoin', the provider name is a key in providers.json list such as 'bcoin.testnet.localhost'.
84
        :type provider_name: str
85

86
        """
87

88
        self.network = network
2✔
89
        if not isinstance(network, Network):
2✔
90
            self.network = Network(network)
2✔
91
        if min_providers > max_providers:
2✔
92
            max_providers = min_providers
2✔
93
        fn = Path(BCL_DATA_DIR, 'providers.json')
2✔
94
        f = fn.open("r")
2✔
95

96
        try:
2✔
97
            self.providers_defined = json.loads(f.read())
2✔
98
        except json.decoder.JSONDecodeError as e:  # pragma: no cover
99
            errstr = "Error reading provider definitions from %s: %s" % (fn, e)
100
            _logger.warning(errstr)
101
            raise ServiceError(errstr)
102
        f.close()
2✔
103

104
        provider_set = {self.providers_defined[x]['provider'] for x in self.providers_defined}
2✔
105
        if providers is None:
2✔
106
            providers = []
2✔
107
        if exclude_providers is None:
2✔
108
            exclude_providers = []
2✔
109
        if not isinstance(providers, list):
2✔
110
            providers = [providers]
2✔
111
        for p in providers:
2✔
112
            if p not in provider_set:
2✔
113
                raise ServiceError("Provider '%s' not found in provider definitions" % p)
2✔
114

115
        self.providers = {}
2✔
116
        if provider_name:
2✔
117
            if provider_name not in self.providers_defined:
×
118
                raise ServiceError("Provider with name '%s' not found in provider definitions" % provider_name)
×
119
            if self.providers_defined[provider_name]['network'] != self.network:
×
120
                raise ServiceError("Network from provider '%s' is different than Service network" % provider_name)
×
121
            self.providers.update({provider_name: self.providers_defined[provider_name]})
×
122
        else:
123
            for p in self.providers_defined:
2✔
124
                if (self.providers_defined[p]['network'] == network or self.providers_defined[p]['network'] == '') and \
2✔
125
                        (not providers or self.providers_defined[p]['provider'] in providers):
126
                    self.providers.update({p: self.providers_defined[p]})
2✔
127
        exclude_providers_keys = {pi: self.providers[pi]['provider'] for
2✔
128
                                  pi in self.providers if self.providers[pi]['provider'] in exclude_providers}.keys()
129
        for provider_key in exclude_providers_keys:
2✔
130
            del(self.providers[provider_key])
2✔
131

132
        if not self.providers:
2✔
133
            raise ServiceError("No providers found for network %s" % network)
×
134
        self.min_providers = min_providers
2✔
135
        self.max_providers = max_providers
2✔
136
        self.results = {}
2✔
137
        self.errors = {}
2✔
138
        self.resultcount = 0
2✔
139
        self.max_errors = max_errors
2✔
140
        self.complete = None
2✔
141
        self.timeout = timeout
2✔
142
        self._blockcount_update = 0
2✔
143
        self._blockcount = None
2✔
144
        self.cache = None
2✔
145
        self.cache_uri = cache_uri
2✔
146
        self.wallet_name = wallet_name
2✔
147
        try:
2✔
148
            self.cache = Cache(self.network, db_uri=cache_uri)
2✔
149
        except Exception as e:
×
150
            self.cache = Cache(self.network, db_uri='')
×
151
            _logger.warning("Could not connect to cache database. Error: %s" % e)
×
152
        self.results_cache_n = 0
2✔
153
        self.ignore_priority = ignore_priority
2✔
154
        self.strict = strict
2✔
155
        self.execution_time = None
2✔
156
        if self.min_providers > 1:
2✔
157
            self._blockcount = Service(network=network, cache_uri=cache_uri, providers=providers,
2✔
158
                                       exclude_providers=exclude_providers, timeout=timeout).blockcount()
159
        else:
160
            self._blockcount = self.blockcount()
2✔
161

162
    def _reset_results(self):
2✔
163
        self.results = {}
2✔
164
        self.errors = {}
2✔
165
        self.complete = None
2✔
166
        self.resultcount = 0
2✔
167
        self.execution_time = None
2✔
168

169
    def _provider_execute(self, method, *arguments):
2✔
170
        self._reset_results()
2✔
171
        provider_lst = [p[0] for p in sorted([(x, self.providers[x]['priority']) for x in self.providers],
2✔
172
                        key=lambda x: (x[1], random.random()), reverse=True)]
173
        if self.ignore_priority:
2✔
174
            random.shuffle(provider_lst)
2✔
175

176
        start_time = datetime.now()
2✔
177
        for sp in provider_lst:
2✔
178
            if self.resultcount >= self.max_providers:
2✔
179
                break
×
180
            try:
2✔
181
                if sp not in ['bitcoind', 'litecoind', 'dogecoind', 'caching'] and not self.providers[sp]['url'] and \
2✔
182
                        self.network.name != 'bitcoinlib_test':
183
                    continue
×
184
                client = getattr(services, self.providers[sp]['provider'])
2✔
185
                providerclient = getattr(client, self.providers[sp]['client_class'])
2✔
186

187
                pc_instance = providerclient(
2✔
188
                    self.network, self.providers[sp]['url'], self.providers[sp]['denominator'],
189
                    self.providers[sp]['api_key'], self.providers[sp]['provider_coin_id'],
190
                    self.providers[sp]['network_overrides'], self.timeout, self._blockcount, self.strict,
191
                    self.wallet_name)
192
                if not hasattr(pc_instance, method):
2✔
193
                    _logger.debug("Method %s not found for provider %s" % (method, sp))
2✔
194
                    continue
2✔
195
                if self.providers[sp]['api_key'] == 'api-key-needed':
2✔
196
                    _logger.debug("API key needed for provider %s" % sp)
2✔
197
                    continue
2✔
198
                providermethod = getattr(pc_instance, method)
2✔
199
                res = providermethod(*arguments)
2✔
200
                if res is False:  # pragma: no cover
201
                    self.errors.update(
202
                        {sp: 'Received empty response'}
203
                    )
204
                    _logger.info("Empty response from %s when calling %s" % (sp, method))
205
                    continue
206
                self.results.update(
2✔
207
                    {sp: res}
208
                )
209
                _logger.debug("Executed method %s from provider %s" % (method, sp))
2✔
210
                self.resultcount += 1
2✔
211
            except Exception as e:
2✔
212
                if not isinstance(e, AttributeError):
2✔
213
                    try:
2✔
214
                        err = e.msg
2✔
215
                    except AttributeError:
2✔
216
                        err = e
2✔
217
                    self.errors.update(
2✔
218
                        {sp: err}
219
                    )
220
                    _logger.debug("Error %s on provider %s" % (e, sp))
2✔
221
                    # -- Use this to debug specific Services errors --
222
                    # from pprint import pprint
223
                    # pprint(self.errors)
224

225
                if len(self.errors) >= self.max_errors:
2✔
226
                    _logger.warning("Aborting, max errors exceeded: %s" %
2✔
227
                                    list(self.errors.keys()))
228
                    if len(self.results):
2✔
229
                        return list(self.results.values())[0]
×
230
                    else:
231
                        return False
2✔
232

233
            if self.resultcount >= self.max_providers:
2✔
234
                break
2✔
235

236
        self.execution_time = (datetime.now() - start_time).total_seconds() * 1000
2✔
237
        if not self.resultcount:
2✔
238
            raise ServiceError("No successful response from any serviceprovider: %s" % list(self.providers.keys()))
2✔
239
        return list(self.results.values())[0]
2✔
240

241
    def getbalance(self, addresslist, addresses_per_request=5):
2✔
242
        """
243
        Get total balance for address or list of addresses
244

245
        :param addresslist: Address or list of addresses
246
        :type addresslist: list, str
247
        :param addresses_per_request: Maximum number of addresses per request. Default is 5. Use lower setting when you experience timeouts or service request errors, or higher when possible.
248
        :type addresses_per_request: int
249

250
        :return dict: Balance per address
251
        """
252
        if isinstance(addresslist, TYPE_TEXT):
2✔
253
            addresslist = [addresslist]
2✔
254

255
        tot_balance = 0
2✔
256
        while addresslist:
2✔
257
            for address in addresslist:
2✔
258
                db_addr = self.cache.getaddress(address)
2✔
259
                if db_addr and db_addr.last_block and db_addr.last_block >= self.blockcount() and db_addr.balance:
2✔
260
                    tot_balance += db_addr.balance
×
261
                    addresslist.remove(address)
×
262

263
            balance = self._provider_execute('getbalance', addresslist[:addresses_per_request])
2✔
264
            if balance:
2✔
265
                tot_balance += balance
2✔
266
            if len(addresslist) == 1:
2✔
267
                self.cache.store_address(addresslist[0], balance=balance)
2✔
268
            addresslist = addresslist[addresses_per_request:]
2✔
269
        return tot_balance
2✔
270

271
    def getutxos(self, address, after_txid='', limit=MAX_TRANSACTIONS):
2✔
272
        """
273
        Get list of unspent outputs (UTXO's) for specified address.
274

275
        Sorted from old to new, so the highest number of confirmations first.
276

277
        :param address: Address string
278
        :type address: str
279
        :param after_txid: Transaction ID of last known transaction. Only check for utxos after given tx id. Default: Leave empty to return all utxos.
280
        :type after_txid: str
281
        :param limit: Maximum number of utxo's to return. Sometimes ignored by service providers if more results are returned by default.
282
        :type limit: int
283

284
        :return dict: UTXO's per address
285
        """
286
        if not isinstance(address, TYPE_TEXT):
2✔
287
            raise ServiceError("Address parameter must be of type text")
×
288
        self.results_cache_n = 0
2✔
289
        self.complete = True
2✔
290

291
        utxos_cache = []
2✔
292
        if self.min_providers <= 1:
2✔
293
            utxos_cache = self.cache.getutxos(address, bytes.fromhex(after_txid)) or []
2✔
294
        if utxos_cache:
2✔
295
            self.results_cache_n = len(utxos_cache)
2✔
296

297
            # Last updated block does not always include spent info...
298
            # if db_addr and db_addr.last_block and db_addr.last_block >= self.blockcount():
299
            #     return utxos_cache
300
            after_txid = utxos_cache[-1:][0]['txid']
2✔
301

302
        utxos = self._provider_execute('getutxos', address, after_txid, limit)
2✔
303
        if utxos is False:
2✔
304
            raise ServiceError("Error when retrieving UTXO's")
×
305
        else:
306
            # Update cache_transactions_node
307
            for utxo in utxos:
2✔
308
                self.cache.store_utxo(utxo['txid'], utxo['output_n'], commit=False)
2✔
309
            self.cache.commit()
2✔
310
            if utxos and len(utxos) >= limit:
2✔
311
                self.complete = False
2✔
312
            elif not after_txid:
2✔
313
                balance = sum(u['value'] for u in utxos)
2✔
314
                self.cache.store_address(address, balance=balance, n_utxos=len(utxos))
2✔
315

316
        return utxos_cache + utxos
2✔
317

318
    def gettransaction(self, txid):
2✔
319
        """
320
        Get a transaction by its transaction hash. Convert to Bitcoinlib Transaction object.
321

322
        :param txid: Transaction identification hash
323
        :type txid: str
324

325
        :return Transaction: A single transaction object
326
        """
327
        tx = None
2✔
328
        self.results_cache_n = 0
2✔
329

330
        if self.min_providers <= 1:
2✔
331
            tx = self.cache.gettransaction(bytes.fromhex(txid))
2✔
332
            if tx:
2✔
333
                self.results_cache_n = 1
2✔
334
        if not tx:
2✔
335
            tx = self._provider_execute('gettransaction', txid)
2✔
336
            if tx and tx.txid != txid:
2✔
337
                _logger.warning("Incorrect txid after parsing")
×
338
                tx.txid = txid
×
339
            if len(self.results) and self.min_providers <= 1:
2✔
340
                self.cache.store_transaction(tx)
2✔
341
        return tx
2✔
342

343
    def gettransactions(self, address, after_txid='', limit=MAX_TRANSACTIONS):
2✔
344
        """
345
        Get all transactions for specified address.
346

347
        Sorted from old to new, so transactions with the highest number of confirmations first.
348

349
        :param address: Address string
350
        :type address: str
351
        :param after_txid: Transaction ID of last known transaction. Only check for transactions after given tx id. Default: Leave empty to return all transaction. If used only provide a single address
352
        :type after_txid: str
353
        :param limit: Maximum number of transactions to return
354
        :type limit: int
355

356
        :return list: List of Transaction objects
357
        """
358
        self._reset_results()
2✔
359
        self.results_cache_n = 0
2✔
360
        if not address:
2✔
361
            return []
×
362
        if not isinstance(address, TYPE_TEXT):
2✔
363
            raise ServiceError("Address parameter must be of type text")
2✔
364
        if after_txid is None:
2✔
365
            after_txid = ''
×
366
        db_addr = self.cache.getaddress(address)
2✔
367
        txs_cache = []
2✔
368
        qry_after_txid = bytes.fromhex(after_txid)
2✔
369

370
        # Retrieve transactions from cache
371
        caching_enabled = True
2✔
372
        if self.min_providers > 1:  # Disable cache if comparing providers
2✔
373
            caching_enabled = False
2✔
374

375
        if caching_enabled:
2✔
376
            txs_cache = self.cache.gettransactions(address, qry_after_txid, limit) or []
2✔
377
            if txs_cache:
2✔
378
                self.results_cache_n = len(txs_cache)
2✔
379
                if len(txs_cache) == limit:
2✔
380
                    return txs_cache
2✔
381
                limit = limit - len(txs_cache)
2✔
382
                qry_after_txid = bytes.fromhex(txs_cache[-1:][0].txid)
2✔
383

384
        # Get (extra) transactions from service providers
385
        txs = []
2✔
386
        if not (db_addr and db_addr.last_block and db_addr.last_block >= self.blockcount()) or not caching_enabled:
2✔
387
            txs = self._provider_execute('gettransactions', address, qry_after_txid.hex(),  limit)
2✔
388
            if txs is False:
2✔
389
                raise ServiceError("Error when retrieving transactions from service provider")
×
390
            for tx in txs:
2✔
391
                if tx.date and not tx.date.tzinfo:
2✔
392
                    tx.date = tx.date.replace(tzinfo=timezone.utc)
×
393

394
        # Store transactions and address in cache
395
        # - disable cache if comparing providers or if after_txid is used and no cache is available
396
        last_block = None
2✔
397
        last_txid = None
2✔
398
        if self.min_providers <= 1 and not (after_txid and not db_addr) and caching_enabled:
2✔
399
            last_block = self.blockcount()
2✔
400
            last_txid = qry_after_txid
2✔
401
            self.complete = True
2✔
402
            if len(txs) == limit:
2✔
403
                self.complete = False
2✔
404
                last_block = txs[-1:][0].block_height
2✔
405
            if len(txs):
2✔
406
                last_txid = bytes.fromhex(txs[-1:][0].txid)
2✔
407
            if len(self.results):
2✔
408
                index = 0
2✔
409
                for t in txs:
2✔
410
                    if t.confirmations != 0:
2✔
411
                        res = self.cache.store_transaction(t, index, commit=False)
2✔
412
                        index += 1
2✔
413
                        # Failure to store transaction: stop caching transaction and store last tx block height - 1
414
                        if res is False:
2✔
415
                            if t.block_height:
×
416
                                last_block = t.block_height - 1
×
417
                            break
×
418
                self.cache.commit()
2✔
419
                self.cache.store_address(address, last_block, last_txid=last_txid, txs_complete=self.complete)
2✔
420

421
        all_txs = txs_cache + txs
2✔
422
        # If we have txs for this address update spent and balance information in cache
423
        if self.complete:
2✔
424
            all_txs = transaction_update_spents(all_txs, address)
2✔
425
            if caching_enabled:
2✔
426
                self.cache.store_address(address, last_block, last_txid=last_txid, txs_complete=True)
2✔
427
                for t in all_txs:
2✔
428
                    self.cache.store_transaction(t, commit=False)
2✔
429
                self.cache.commit()
2✔
430
        return all_txs
2✔
431

432
    def getrawtransaction(self, txid):
2✔
433
        """
434
        Get a raw transaction by its transaction hash
435

436
        :param txid: Transaction identification hash
437
        :type txid: str
438

439
        :return str: Raw transaction as hexstring
440
        """
441
        self.results_cache_n = 0
2✔
442
        rawtx = self.cache.getrawtransaction(bytes.fromhex(txid))
2✔
443
        if rawtx:
2✔
444
            self.results_cache_n = 1
2✔
445
            return rawtx
2✔
446
        return self._provider_execute('getrawtransaction', txid)
2✔
447

448
    def sendrawtransaction(self, rawtx):
2✔
449
        """
450
        Push a raw transaction to the network
451

452
        :param rawtx: Raw transaction as hexstring or bytes
453
        :type rawtx: str
454

455
        :return dict: Send transaction result
456
        """
457
        return self._provider_execute('sendrawtransaction', rawtx)
2✔
458

459
    def estimatefee(self, blocks=5, priority=''):
2✔
460
        """
461
        Estimate fee per kilobyte for a transaction for this network with expected confirmation within a certain
462
        amount of blocks
463

464
        :param blocks: Expected confirmation time in blocks.
465
        :type blocks: int
466
        :param priority: Priority for transaction: can be 'low', 'medium' or 'high'. Overwrites value supplied in 'blocks' argument
467
        :type priority: str
468

469
        :return int: Fee in the smallest network denominator (satoshi)
470
        """
471
        self.results_cache_n = 0
2✔
472
        if priority:
2✔
473
            if priority == 'low':
2✔
474
                blocks = 25
2✔
475
            elif priority == 'high':
×
476
                blocks = 2
×
477
        if self.min_providers <= 1:  # Disable cache if comparing providers
2✔
478
            fee = self.cache.estimatefee(blocks)
2✔
479
            if fee:
2✔
480
                self.results_cache_n = 1
2✔
481
                return fee
2✔
482
        fee = self._provider_execute('estimatefee', blocks)
2✔
483
        if not fee:  # pragma: no cover
484
            if self.network.fee_default:
485
                fee = self.network.fee_default
486
            else:
487
                raise ServiceError("Could not estimate fees, please define default fees in network settings")
488
        if fee < self.network.fee_min:
2✔
489
            fee = self.network.fee_min
2✔
490
        elif fee > self.network.fee_max:
2✔
491
            fee = self.network.fee_max
2✔
492
        self.cache.store_estimated_fee(blocks, fee)
2✔
493
        return fee
2✔
494

495
    def blockcount(self):
2✔
496
        """
497
        Get the latest block number: The block number of last block in longest chain on the Blockchain.
498

499
        Block count is cashed for BLOCK_COUNT_CACHE_TIME seconds to avoid to many calls to service providers.
500

501
        :return int:
502
        """
503

504
        blockcount = self.cache.blockcount()
2✔
505
        last_cache_blockcount = self.cache.blockcount(never_expires=True)
2✔
506
        if blockcount:
2✔
507
            self._blockcount = blockcount
2✔
508
            return blockcount
2✔
509

510
        current_timestamp = time.time()
2✔
511
        if self._blockcount_update < current_timestamp - BLOCK_COUNT_CACHE_TIME:
2✔
512
            new_count = self._provider_execute('blockcount')
2✔
513
            if last_cache_blockcount > new_count:
2✔
514
                _logger.warning(f"New block count ({new_count}) is lower than block count in cache "
×
515
                                f"({last_cache_blockcount}). Will try to find provider consensus")
516
                blockcounts = [last_cache_blockcount]
×
517
                for _ in range(5):
×
518
                    blockcounts.append(self._provider_execute('blockcount'))
×
519
                # return third last blockcount in list, assume last 2 and first 3 could be wrong
520
                self._blockcount = sorted(blockcounts)[-2]
×
521
                self._blockcount_update = current_timestamp
×
522
            elif not self._blockcount or (new_count and new_count > self._blockcount):
2✔
523
                self._blockcount = new_count
2✔
524
                self._blockcount_update = current_timestamp
2✔
525
            # Store result in cache
526
            if len(self.results) and list(self.results.keys())[0] != 'caching':
2✔
527
                self.cache.store_blockcount(self._blockcount)
2✔
528
        return self._blockcount
2✔
529

530
    def getblock(self, blockid, parse_transactions=True, page=1, limit=None):
2✔
531
        """
532
        Get block with specified block height or block hash from service providers.
533

534
        If parse_transaction is set to True a list of Transaction object will be returned otherwise a
535
        list of transaction ID's.
536

537
        Some providers require 1 or 2 extra request per transaction, so to avoid timeouts or rate limiting errors
538
        you can specify a page and limit for the transaction. For instance with page=2, limit=4 only transaction
539
        5 to 8 are returned to the Blocks's 'transaction' attribute.
540

541
        If you only use a local bcoin or bitcoind provider, make sure you set the limit to maximum (i.e. 9999)
542
        because all transactions are already downloaded when fetching the block.
543

544
        >>> from bitcoinlib.services.services import Service
545
        >>> srv = Service()
546
        >>> b = srv.getblock(0)
547
        >>> b
548
        <Block(000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f, 0, transactions: 1)>
549

550
        :param blockid: Hash or block height of block
551
        :type blockid: str, int
552
        :param parse_transactions: Return Transaction objects or just transaction ID's. Default is return txids.
553
        :type parse_transactions: bool
554
        :param page: Page number of transaction paging. Default is start from the beginning: 1
555
        :type page: int
556
        :param limit: Maximum amount of transaction to return. Default is 25 if parse transaction is enabled, otherwise returns all txid's (9999)
557
        :type limit: int
558

559
        :return Block:
560
        """
561
        if limit is None:
2✔
562
            limit = 25 if parse_transactions else 99999
2✔
563

564
        block = self.cache.getblock(blockid)
2✔
565
        is_last_page = False
2✔
566
        if block:
2✔
567
            # Block found get transactions from cache
568
            txs = self.cache.getblocktransactions(block.height, page, limit)
2✔
569
            if parse_transactions:
2✔
570
                block.transactions = txs
2✔
571
            else:
572
                block.transactions = [tx.txid for tx in txs]
×
573
            if block.transactions:
2✔
574
                self.results_cache_n = 1
2✔
575
            is_last_page = page*limit > block.tx_count
2✔
576
        if not block or (not len(block.transactions) and limit != 0) or (not is_last_page and len(block.transactions) < limit) or \
2✔
577
                (is_last_page and ((page-1)*limit - block.tx_count + len(block.transactions)) < 0):
578
            self.results_cache_n = 0
2✔
579
            bd = self._provider_execute('getblock', blockid, parse_transactions, page, limit)
2✔
580
            if not bd or isinstance(bd, bool):
2✔
581
                return False
×
582
            block = Block(bd['block_hash'], bd['version'], bd['prev_block'], bd['merkle_root'], bd['time'], bd['bits'],
2✔
583
                          bd['nonce'], bd['txs'], bd['height'], bd['depth'], self.network)
584
            block.tx_count = bd['tx_count']
2✔
585
            block.limit = limit
2✔
586
            block.page = page
2✔
587

588
            if parse_transactions and self.min_providers <= 1:
2✔
589
                index = (page-1)*limit
2✔
590
                for tx in block.transactions:
2✔
591
                    if isinstance(tx, Transaction):
2✔
592
                        self.cache.store_transaction(tx, index, commit=False)
2✔
593
                    index += 1
2✔
594
                self.cache.commit()
2✔
595
            self.complete = True if len(block.transactions) == block.tx_count else False
2✔
596
            self.cache.store_block(block)
2✔
597
        return block
2✔
598

599
    def getrawblock(self, blockid):
2✔
600
        """
601
        Get raw block as hexadecimal string for block with specified hash or block height.
602

603
        Not many providers offer this option, and it can be slow, so it is advised to use a local client such
604
        as bitcoind.
605

606
        :param blockid: Block hash or block height
607
        :type blockid: str, int
608

609
        :return str:
610
        """
611
        return self._provider_execute('getrawblock', blockid)
2✔
612

613
    def mempool(self, txid=''):
2✔
614
        """
615
        Get list of all transaction IDs in the current mempool
616

617
        A full list of transactions ID's will only be returned if a bcoin or bitcoind client is available. Otherwise,
618
        specify the txid option to verify if a transaction is added to the mempool.
619

620
        :param txid: Check if transaction with this hash exists in memory pool
621
        :type txid: str
622

623
        :return list:
624
        """
625
        return self._provider_execute('mempool', txid)
2✔
626

627
    def getcacheaddressinfo(self, address):
2✔
628
        """
629
        Get address information from cache. I.e. balance, number of transactions, number of utxo's, etc
630

631
        Cache will only be filled after all transactions for a specific address are retrieved (with gettransactions ie)
632

633
        :param address: address string
634
        :type address: str
635

636
        :return dict:
637
        """
638
        addr_dict = {'address': address}
2✔
639
        addr_rec = self.cache.getaddress(address)
2✔
640
        if isinstance(addr_rec, DbCacheAddress):
2✔
641
            addr_dict['balance'] = addr_rec.balance
2✔
642
            addr_dict['last_block'] = addr_rec.last_block
2✔
643
            addr_dict['n_txs'] = addr_rec.n_txs
2✔
644
            addr_dict['n_utxos'] = addr_rec.n_utxos
2✔
645
        return addr_dict
2✔
646

647
    def isspent(self, txid, output_n):
2✔
648
        """
649
        Check if the output with provided transaction ID and output number is spent.
650

651
        :param txid: Transaction ID hex
652
        :type txid: str
653
        :param output_n: Output number
654
        :type output_n: int
655

656
        :return bool:
657
        """
658
        t = self.cache.gettransaction(bytes.fromhex(txid))
2✔
659
        if t and len(t.outputs) > output_n and t.outputs[output_n].spent is not None:
2✔
660
            return t.outputs[output_n].spent
×
661
        else:
662
            return bool(self._provider_execute('isspent', txid, output_n))
2✔
663

664
    def getinfo(self):
2✔
665
        """
666
        Returns info about current network. Such as difficulty, latest block, mempool size and network hashrate.
667

668
        :return dict:
669
        """
670
        return self._provider_execute('getinfo')
2✔
671

672
    def getinputvalues(self, t):
2✔
673
        """
674
        Retrieve values for transaction inputs for given Transaction.
675

676
        Raw transactions as stored on the blockchain do not contain the input values but only the previous
677
        transaction hash and index number. This method retrieves the previous transaction and reads the value.
678

679
        :param t: Transaction
680
        :type t: Transaction
681

682
        :return Transaction:
683
        """
684
        prev_txs = []
2✔
685
        for i in t.inputs:
2✔
686
            if not i.value:
2✔
687
                if i.prev_txid not in prev_txs and i.prev_txid != 32 * b'\0':
2✔
688
                    prev_t = self.gettransaction(i.prev_txid.hex())
2✔
689
                else:
690
                    prev_t = [t for t in prev_txs if t.txid == i.prev_txid][0]
×
691
                i.value = prev_t.outputs[i.output_n_int].value
2✔
692
        return t
2✔
693

694

695
class Cache(object):
2✔
696
    """
697
    Store transaction, utxo and address information in database to increase speed and avoid duplicate calls to
698
    service providers.
699

700
    Once confirmed a transaction is immutable so we have to fetch it from a service provider only once. When checking
701
    for new transactions or utxo's for a certain address we only have to check the new blocks.
702

703
    This class is used by the Service class and normally you won't need to access it directly.
704

705
    """
706

707
    def __init__(self, network, db_uri=''):
2✔
708
        """
709
        Open Cache class
710

711
        :param network: Specify network used
712
        :type network: str, Network
713
        :param db_uri: Database to use for caching
714
        :type db_uri: str
715
        """
716
        self.session = None
2✔
717
        if SERVICE_CACHING_ENABLED:
2✔
718
            self.session = DbCache(db_uri=db_uri).session
2✔
719
        self.network = network
2✔
720

721
    def cache_enabled(self):
2✔
722
        """
723
        Check if caching is enabled. Returns False if SERVICE_CACHING_ENABLED is False or no session is defined.
724

725
        :return bool:
726
        """
727
        if not SERVICE_CACHING_ENABLED or not self.session:
2✔
728
            return False
2✔
729
        return True
2✔
730

731
    def commit(self):
2✔
732
        """
733
        Commit queries in self.session. Rollback if commit fails.
734

735
        :return:
736
        """
737
        if not self.session:
2✔
738
            return
2✔
739
        try:
2✔
740
            self.session.commit()
2✔
741
        except Exception:
×
742
            self.session.rollback()
×
743
            raise
×
744

745
    @staticmethod
2✔
746
    def _parse_db_transaction(db_tx):
2✔
747
        t = Transaction(locktime=db_tx.locktime, version=db_tx.version, network=db_tx.network_name,
2✔
748
                        fee=db_tx.fee, txid=db_tx.txid.hex(), date=db_tx.date, confirmations=db_tx.confirmations,
749
                        block_height=db_tx.block_height, status='confirmed', witness_type=db_tx.witness_type.value,
750
                        index=db_tx.index)
751
        if t.date and not t.date.tzinfo:
2✔
752
            t.date = t.date.replace(tzinfo=timezone.utc)
2✔
753
        for n in db_tx.nodes:
2✔
754
            if n.is_input:
2✔
755
                witness_type = None
2✔
756
                if n.ref_txid == b'\00' * 32:
2✔
757
                    t.coinbase = True
2✔
758
                    witness_type = db_tx.witness_type.value
2✔
759
                t.add_input(n.ref_txid.hex(), n.ref_index_n, unlocking_script=n.script, address=n.address,
2✔
760
                            sequence=n.sequence, value=n.value, index_n=n.index_n, witnesses=n.witnesses,
761
                            strict=False, witness_type=witness_type)
762
            else:
763
                t.add_output(n.value, n.address, lock_script=n.script, spent=n.spent, output_n=n.index_n,
2✔
764
                             spending_txid=None if not n.ref_txid else n.ref_txid.hex(),
765
                             spending_index_n=n.ref_index_n, strict=False)
766

767
        t.update_totals()
2✔
768
        t.size = len(t.raw())
2✔
769
        t.calc_weight_units()
2✔
770
        _logger.info("Retrieved transaction %s from cache" % t.txid)
2✔
771
        return t
2✔
772

773
    def gettransaction(self, txid):
2✔
774
        """
775
        Get transaction from cache. Returns False if not available
776

777
        :param txid: Transaction identification hash
778
        :type txid: bytes
779

780
        :return Transaction: A single Transaction object
781
        """
782
        if not self.cache_enabled():
2✔
783
            return False
2✔
784
        db_tx = self.session.query(DbCacheTransaction).filter_by(txid=txid, network_name=self.network.name).first()
2✔
785
        if not db_tx:
2✔
786
            return False
2✔
787
        db_tx.txid = txid
2✔
788
        t = self._parse_db_transaction(db_tx)
2✔
789
        if t.block_height:
2✔
790
            t.confirmations = (self.blockcount() - t.block_height) + 1
2✔
791
        return t
2✔
792

793
    def getaddress(self, address):
2✔
794
        """
795
        Get address information from cache, with links to transactions and utxo's and latest update information.
796

797
        :param address: Address string
798
        :type address: str
799

800
        :return DbCacheAddress: An address cache database object
801
        """
802
        if not self.cache_enabled():
2✔
803
            return
×
804
        return self.session.query(DbCacheAddress).filter_by(address=address, network_name=self.network.name).scalar()
2✔
805

806
    def gettransactions(self, address, after_txid='', limit=MAX_TRANSACTIONS):
2✔
807
        """
808
        Get transactions from cache. Returns empty list if no transactions are found or caching is disabled.
809

810
        :param address: Address string
811
        :type address: str
812
        :param after_txid: Transaction ID of last known transaction. Only check for transactions after given tx id. Default: Leave empty to return all transaction. If used only provide a single address
813
        :type after_txid: bytes
814
        :param limit: Maximum number of transactions to return
815
        :type limit: int
816

817
        :return list: List of Transaction objects
818
        """
819
        if not self.cache_enabled():
2✔
820
            return False
×
821
        db_addr = self.getaddress(address)
2✔
822
        txs = []
2✔
823
        if db_addr:
2✔
824
            if after_txid:
2✔
825
                after_tx = self.session.query(DbCacheTransaction).\
2✔
826
                    filter_by(txid=after_txid, network_name=self.network.name).scalar()
827
                if after_tx and db_addr.last_block and after_tx.block_height:
2✔
828
                    db_txs = self.session.query(DbCacheTransaction).join(DbCacheTransactionNode).\
2✔
829
                        filter(DbCacheTransactionNode.address == address,
830
                               DbCacheTransaction.block_height >= after_tx.block_height,
831
                               DbCacheTransaction.block_height <= db_addr.last_block).\
832
                        order_by(DbCacheTransaction.block_height, DbCacheTransaction.index).all()
833
                    db_txs2 = []
2✔
834
                    for d in db_txs:
2✔
835
                        db_txs2.append(d)
2✔
836
                        if d.txid == after_txid:
2✔
837
                            db_txs2 = []
2✔
838
                    db_txs = db_txs2
2✔
839
                else:
840
                    return []
×
841
            else:
842
                db_txs = self.session.query(DbCacheTransaction).join(DbCacheTransactionNode). \
2✔
843
                    filter(DbCacheTransactionNode.address == address). \
844
                    order_by(DbCacheTransaction.block_height, DbCacheTransaction.index).all()
845
            for db_tx in db_txs:
2✔
846
                t = self._parse_db_transaction(db_tx)
2✔
847
                if t:
2✔
848
                    if t.block_height:
2✔
849
                        t.confirmations = (self.blockcount() - t.block_height) + 1
2✔
850
                    txs.append(t)
2✔
851
                    if len(txs) >= limit:
2✔
852
                        break
2✔
853
                
854
            for tx in txs:
2✔
855
                tx.date = tx.date.replace(tzinfo=timezone.utc)
2✔
856

857
            return txs
2✔
858
        return []
2✔
859

860
    def getblocktransactions(self, height, page, limit):
2✔
861
        """
862
        Get range of transactions from a block
863

864
        :param height: Block height
865
        :type height: int
866
        :param page: Transaction page
867
        :type page: int
868
        :param limit: Number of transactions per page
869
        :type limit: int
870

871
        :return:
872
        """
873
        if not self.cache_enabled():
2✔
874
            return False
×
875
        n_from = (page-1) * limit
2✔
876
        n_to = page * limit
2✔
877
        db_txs = self.session.query(DbCacheTransaction).\
2✔
878
            filter(DbCacheTransaction.block_height == height, DbCacheTransaction.index >= n_from,
879
                   DbCacheTransaction.index < n_to).all()
880
        txs = []
2✔
881
        for db_tx in db_txs:
2✔
882
            t = self._parse_db_transaction(db_tx)
2✔
883
            if t:
2✔
884
                txs.append(t)
2✔
885
        return txs
2✔
886

887
    def getrawtransaction(self, txid):
2✔
888
        """
889
        Get a raw transaction string from the database cache if available
890

891
        :param txid: Transaction identification hash
892
        :type txid: bytes
893

894
        :return str: Raw transaction as hexstring
895
        """
896
        if not self.cache_enabled():
2✔
897
            return False
×
898
        tx = self.session.query(DbCacheTransaction).filter_by(txid=txid, network_name=self.network.name).first()
2✔
899
        if not tx:
2✔
900
            return False
2✔
901
        t = self._parse_db_transaction(tx)
2✔
902
        return t.raw_hex()
2✔
903

904
    def getutxos(self, address, after_txid=''):
2✔
905
        """
906
        Get list of unspent outputs (UTXO's) for specified address from database cache.
907

908
        Sorted from old to new, so the highest number of confirmations first.
909

910
        :param address: Address string
911
        :type address: str
912
        :param after_txid: Transaction ID of last known transaction. Only check for utxos after given tx id. Default: Leave empty to return all utxos.
913
        :type after_txid: bytes
914

915
        :return dict: UTXO's per address
916
        """
917
        if not self.cache_enabled():
2✔
918
            return False
×
919
        db_utxos = self.session.query(DbCacheTransactionNode.spent, DbCacheTransactionNode.index_n,
2✔
920
                                      DbCacheTransactionNode.value, DbCacheTransaction.confirmations,
921
                                      DbCacheTransaction.block_height, DbCacheTransaction.fee,
922
                                      DbCacheTransaction.date, DbCacheTransaction.txid).join(DbCacheTransaction). \
923
            order_by(DbCacheTransaction.block_height, DbCacheTransaction.index). \
924
            filter(DbCacheTransactionNode.address == address, DbCacheTransactionNode.is_input == False,
925
                   DbCacheTransaction.network_name == self.network.name).all()
926
        utxos = []
2✔
927
        for db_utxo in db_utxos:
2✔
928
            if db_utxo.spent is False:
2✔
929
                utxos.append({
2✔
930
                    'address': address,
931
                    'txid': db_utxo.txid.hex(),
932
                    'confirmations': db_utxo.confirmations,
933
                    'output_n': db_utxo.index_n,
934
                    'input_n': 0,
935
                    'block_height': db_utxo.block_height,
936
                    'fee': db_utxo.fee,
937
                    'size': 0,
938
                    'value': db_utxo.value,
939
                    'script': '',
940
                    'date': db_utxo.date
941
                })
942
            elif db_utxo.spent is None:
2✔
943
                return utxos
×
944
            if db_utxo.txid == after_txid:
2✔
945
                utxos = []
×
946
        return utxos
2✔
947

948
    def estimatefee(self, blocks):
2✔
949
        """
950
        Get fee estimation from cache for confirmation within specified amount of blocks.
951

952
        Stored in cache in three groups: low, medium and high fees.
953

954
        :param blocks: Expected confirmation time in blocks.
955
        :type blocks: int
956

957
        :return int: Fee in the smallest network denominator (satoshi)
958
        """
959
        if not self.cache_enabled():
2✔
960
            return False
×
961
        if blocks <= 1:
2✔
962
            varname = 'fee_high'
×
963
        elif blocks <= 5:
2✔
964
            varname = 'fee_medium'
2✔
965
        else:
966
            varname = 'fee_low'
2✔
967
        dbvar = self.session.query(DbCacheVars).filter_by(varname=varname, network_name=self.network.name).\
2✔
968
            filter(DbCacheVars.expires > datetime.now()).scalar()
969
        if dbvar:
2✔
970
            return int(dbvar.value)
2✔
971
        return False
2✔
972

973
    def blockcount(self, never_expires=False):
2✔
974
        """
975
        Get number of blocks on the current network from cache if recent data is available.
976

977
        :param never_expires: Always return latest blockcount found. Can be used to avoid return to old blocks if service providers are not up-to-date.
978
        :type never_expires: bool
979

980
        :return int:
981
        """
982
        if not self.cache_enabled():
2✔
983
            return False
2✔
984
        qr = self.session.query(DbCacheVars).filter_by(varname='blockcount', network_name=self.network.name)
2✔
985
        if not never_expires:
2✔
986
            qr = qr.filter(DbCacheVars.expires > datetime.now())
2✔
987
        dbvar = qr.scalar()
2✔
988
        if dbvar:
2✔
989
            return int(dbvar.value)
2✔
990
        return False
2✔
991

992
    def getblock(self, blockid):
2✔
993
        """
994
        Get specific block from database cache.
995

996
        :param blockid: Block height or block hash
997
        :type blockid: int, str
998

999
        :return Block:
1000
        """
1001
        if not self.cache_enabled():
2✔
1002
            return False
2✔
1003
        qr = self.session.query(DbCacheBlock)
2✔
1004
        if isinstance(blockid, int):
2✔
1005
            block = qr.filter_by(height=blockid, network_name=self.network.name).scalar()
2✔
1006
        else:
1007
            block = qr.filter_by(block_hash=to_bytes(blockid)).scalar()
2✔
1008
        if not block:
2✔
1009
            return False
2✔
1010
        b = Block(block_hash=block.block_hash, height=block.height, network=block.network_name,
2✔
1011
                  merkle_root=block.merkle_root, time=block.time, nonce=block.nonce,
1012
                  version=block.version, prev_block=block.prev_block, bits=block.bits)
1013
        b.tx_count = block.tx_count
2✔
1014
        _logger.info("Retrieved block with height %d from cache" % b.height)
2✔
1015
        return b
2✔
1016

1017
    def store_blockcount(self, blockcount):
2✔
1018
        """
1019
        Store network blockcount in cache for 60 seconds
1020

1021
        :param blockcount: Number of latest block
1022
        :type blockcount: int, str
1023

1024
        :return:
1025
        """
1026
        if not self.cache_enabled():
2✔
1027
            return
2✔
1028
        dbvar = DbCacheVars(varname='blockcount', network_name=self.network.name, value=str(blockcount), type='int',
2✔
1029
                            expires=datetime.now() + timedelta(seconds=60))
1030
        self.session.merge(dbvar)
2✔
1031
        self.commit()
2✔
1032

1033
    def store_transaction(self, t, index=None, commit=True):
2✔
1034
        """
1035
        Store transaction in cache. Use order number to determine order in a block
1036

1037
        :param t: Transaction
1038
        :type t: Transaction
1039
        :param index: Order in block
1040
        :type index: int
1041
        :param commit: Commit transaction to database. Default is True. Can be disabled if a larger number of transactions are added to cache, so you can commit outside this method.
1042
        :type commit: bool
1043

1044
        :return:
1045
        """
1046
        if not self.cache_enabled():
2✔
1047
            return
2✔
1048
        # Only store complete and confirmed transaction in cache
1049
        if not t.txid:    # pragma: no cover
1050
            _logger.info("Caching failure tx: Missing transaction hash")
1051
            return False
1052
        elif not t.date or not t.block_height or not t.network:
2✔
1053
            _logger.info("Caching failure tx: Incomplete transaction missing date, block height or network info")
1✔
1054
            return False
1✔
1055
        elif not t.coinbase and [i for i in t.inputs if not i.value]:
2✔
1056
            _logger.info("Caching failure tx: One the transaction inputs has value 0")
×
1057
            return False
×
1058
        # TODO: Check if inputs / outputs are complete? script, value, prev_txid, sequence, output/input_n
1059

1060
        txid = bytes.fromhex(t.txid)
2✔
1061
        if self.session.query(DbCacheTransaction).filter_by(txid=txid).count():
2✔
1062
            return
2✔
1063
        new_tx = DbCacheTransaction(txid=txid, date=t.date, confirmations=t.confirmations,
2✔
1064
                                    block_height=t.block_height, network_name=t.network.name,
1065
                                    fee=t.fee, index=index, version=t.version_int,
1066
                                    locktime=t.locktime, witness_type=t.witness_type)
1067
        self.session.add(new_tx)
2✔
1068
        for i in t.inputs:
2✔
1069
            if i.value is None or i.address is None or i.output_n is None:    # pragma: no cover
1070
                _logger.info("Caching failure tx: Input value, address or output_n missing")
1071
                return False
1072
            witnesses = int_to_varbyteint(len(i.witnesses)) + b''.join([bytes(varstr(w)) for w in i.witnesses])
2✔
1073
            new_node = DbCacheTransactionNode(txid=txid, address=i.address, index_n=i.index_n, value=i.value,
2✔
1074
                                              is_input=True, ref_txid=i.prev_txid, ref_index_n=i.output_n_int,
1075
                                              script=i.unlocking_script, sequence=i.sequence, witnesses=witnesses)
1076
            self.session.add(new_node)
2✔
1077
        for o in t.outputs:
2✔
1078
            if o.value is None or o.address is None or o.output_n is None:    # pragma: no cover
1079
                _logger.info("Caching failure tx: Output value, address or output_n missing")
1080
                return False
1081
            new_node = DbCacheTransactionNode(
2✔
1082
                txid=txid, address=o.address, index_n=o.output_n, value=o.value, is_input=False, spent=o.spent,
1083
                ref_txid=None if not o.spending_txid else bytes.fromhex(o.spending_txid),
1084
                ref_index_n=o.spending_index_n, script=o.lock_script)
1085
            self.session.add(new_node)
2✔
1086

1087
        if commit:
2✔
1088
            try:
2✔
1089
                self.commit()
2✔
1090
                _logger.info("Added transaction %s to cache" % t.txid)
2✔
1091
            except Exception as e:    # pragma: no cover
1092
                _logger.warning("Caching failure tx: %s" % e)
1093

1094
    def store_utxo(self, txid, index_n, commit=True):
2✔
1095
        """
1096
        Store utxo in cache. Updates only known transaction outputs for transactions which are fully cached
1097

1098
        :param txid: Transaction ID
1099
        :type txid: str
1100
        :param index_n: Index number of output
1101
        :type index_n: int
1102
        :param commit: Commit transaction to database. Default is True. Can be disabled if a larger number of transactions are added to cache, so you can commit outside this method.
1103
        :type commit: bool
1104

1105
        :return:
1106
        """
1107
        if not self.cache_enabled():
2✔
1108
            return False
×
1109
        txid = bytes.fromhex(txid)
2✔
1110
        result = self.session.query(DbCacheTransactionNode). \
2✔
1111
            filter(DbCacheTransactionNode.txid == txid, DbCacheTransactionNode.index_n == index_n,
1112
                   DbCacheTransactionNode.is_input == False).\
1113
            update({DbCacheTransactionNode.spent: False})
1114
        if commit:
2✔
1115
            try:
×
1116
                self.commit()
×
1117
            except Exception as e:    # pragma: no cover
1118
                _logger.warning("Caching failure utxo %s:%d: %s" % (txid.hex(), index_n, e))
1119

1120
    def store_address(self, address, last_block=None, balance=0, n_utxos=None, txs_complete=False, last_txid=None):
2✔
1121
        """
1122
        Store address information in cache
1123

1124
        :param address: Address string
1125
        :type address: str
1126
        :param last_block: Number or last block retrieved from service provider. For instance if address contains a large number of transactions and they will be retrieved in more then one request.
1127
        :type last_block: int
1128
        :param balance: Total balance of address in sathosis, or smallest network detominator
1129
        :type balance: int
1130
        :param n_utxos: Total number of UTXO's for this address
1131
        :type n_utxos: int
1132
        :param txs_complete: True if all transactions for this address are added to cache
1133
        :type txs_complete: bool
1134
        :param last_txid: Transaction ID of last transaction downloaded from blockchain
1135
        :type last_txid: bytes
1136

1137
 .       :return:
1138
        """
1139
        if not self.cache_enabled():
2✔
1140
            return
×
1141
        n_txs = None
2✔
1142
        if txs_complete:
2✔
1143
            n_txs = len(self.session.query(DbCacheTransaction).join(DbCacheTransactionNode).
2✔
1144
                        filter(DbCacheTransactionNode.address == address).all())
1145
            if n_utxos is None:
2✔
1146
                n_utxos = self.session.query(DbCacheTransactionNode).\
2✔
1147
                    filter(DbCacheTransactionNode.address == address, DbCacheTransactionNode.spent.is_(False),
1148
                           DbCacheTransactionNode.is_input.is_(False)).count()
1149
                if self.session.query(DbCacheTransactionNode).\
2✔
1150
                        filter(DbCacheTransactionNode.address == address, DbCacheTransactionNode.spent.is_(None),
1151
                               DbCacheTransactionNode.is_input.is_(False)).count():
1152
                    n_utxos = None
2✔
1153
            if not balance:
2✔
1154
                plusmin = self.session.query(DbCacheTransactionNode.is_input, func.sum(DbCacheTransactionNode.value)). \
2✔
1155
                    filter(DbCacheTransactionNode.address == address). \
1156
                    group_by(DbCacheTransactionNode.is_input).all()
1157
                balance = 0 if not plusmin else sum([(-p[1] if p[0] else p[1]) for p in plusmin])
2✔
1158
        db_addr = self.getaddress(address)
2✔
1159
        new_address = DbCacheAddress(
2✔
1160
            address=address, network_name=self.network.name,
1161
            last_block=last_block if last_block else getattr(db_addr, 'last_block', None),
1162
            balance=balance if balance is not None else getattr(db_addr, 'balance', None),
1163
            n_utxos=n_utxos if n_utxos is not None else getattr(db_addr, 'n_utxos', None),
1164
            n_txs=n_txs if n_txs is not None else getattr(db_addr, 'n_txs', None),
1165
            last_txid=last_txid if last_txid is not None else getattr(db_addr, 'last_txid', None))
1166
        self.session.merge(new_address)
2✔
1167
        try:
2✔
1168
            self.commit()
2✔
1169
        except Exception as e:    # pragma: no cover
1170
            _logger.warning("Caching failure addr: %s" % e)
1171

1172
    def store_estimated_fee(self, blocks, fee):
2✔
1173
        """
1174
        Store estimated fee retrieved from service providers in cache.
1175

1176
        :param blocks: Confirmation within x blocks
1177
        :type blocks: int
1178
        :param fee: Estimated fee in Sathosis
1179
        :type fee: int
1180

1181
        :return:
1182
        """
1183
        if not self.cache_enabled():
2✔
1184
            return
×
1185
        if blocks <= 1:
2✔
1186
            varname = 'fee_high'
×
1187
        elif blocks <= 5:
2✔
1188
            varname = 'fee_medium'
2✔
1189
        else:
1190
            varname = 'fee_low'
2✔
1191
        dbvar = DbCacheVars(varname=varname, network_name=self.network.name, value=str(fee), type='int',
2✔
1192
                            expires=datetime.now() + timedelta(seconds=600))
1193
        self.session.merge(dbvar)
2✔
1194
        self.commit()
2✔
1195

1196
    def store_block(self, block):
2✔
1197
        """
1198
        Store block in cache database
1199

1200
        :param block: Block
1201
        :type block: Block
1202

1203
        :return:
1204
        """
1205
        if not self.cache_enabled():
2✔
1206
            return
2✔
1207
        if not (block.height and block.block_hash and block.prev_block and block.merkle_root and
2✔
1208
                block.bits and block.version) \
1209
                and not block.block_hash == b'\x00\x00\x00\x00\x00\x19\xd6h\x9c\x08Z\xe1e\x83\x1e\x93O\xf7c\xaeF' \
1210
                                            b'\xa2\xa6\xc1r\xb3\xf1\xb6\n\x8c\xe2o':  # Bitcoin genesis block
1211
            _logger.info("Caching failure block: incomplete data")
×
1212
            return
×
1213

1214
        new_block = DbCacheBlock(
2✔
1215
            block_hash=block.block_hash, height=block.height, network_name=self.network.name,
1216
            version=block.version_int, prev_block=block.prev_block, bits=block.bits_int,
1217
            merkle_root=block.merkle_root, nonce=block.nonce_int, time=block.time, tx_count=block.tx_count)
1218
        self.session.merge(new_block)
2✔
1219
        try:
2✔
1220
            self.commit()
2✔
1221
        except Exception as e:    # pragma: no cover
1222
            _logger.warning("Caching failure block: %s" % e)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc