commit
daa024115b
@ -21,11 +21,8 @@ COINDAEMON_TRUSTED_PASSWORD = 'somepassword'
|
||||
# The available options are:
|
||||
# scrypt, sha256d, scrypt-jane and quark
|
||||
# If the option does not meet either of these criteria stratum defaults to scrypt
|
||||
# Until AutoReward Selecting Code has been implemented the below options are used to select the type of coin
|
||||
# For Reward type there is POW and POS. please ensure you choose the currect type.
|
||||
# For Coins which support TX Messages please enter yes in the TX selection
|
||||
COINDAEMON_ALGO = 'scrypt'
|
||||
COINDAEMON_Reward = 'POW'
|
||||
COINDAEMON_TX = 'no'
|
||||
# ******************** BASIC SETTINGS ***************
|
||||
# Backup Coin Daemon address's (consider having at least 1 backup)
|
||||
@ -191,3 +188,11 @@ NOTIFY_EMAIL_SERVER = 'localhost' # E-Mail Sender
|
||||
NOTIFY_EMAIL_USERNAME = '' # E-Mail server SMTP Logon
|
||||
NOTIFY_EMAIL_PASSWORD = ''
|
||||
NOTIFY_EMAIL_USETLS = True
|
||||
|
||||
#### Memcache ####
|
||||
# Memcahce is a requirement. Enter the settings below
|
||||
MEMCACHE_HOST = "localhost" # hostname or IP that runs memcached
|
||||
MEMCACHE_PORT = 11211 # Port
|
||||
MEMCACHE_TIMEOUT = 900 # Key timeout
|
||||
MEMCACHE_PREFIX = "stratum_" # Prefix for keys
|
||||
|
||||
|
||||
@ -5,6 +5,8 @@ import struct
|
||||
import util
|
||||
import merkletree
|
||||
import halfnode
|
||||
from coinbasetx import CoinbaseTransactionPOW
|
||||
from coinbasetx import CoinbaseTransactionPOS
|
||||
from coinbasetx import CoinbaseTransaction
|
||||
import lib.logger
|
||||
log = lib.logger.get_logger('block_template')
|
||||
@ -55,10 +57,10 @@ class BlockTemplate(halfnode.CBlock):
|
||||
txhashes = [None] + [ util.ser_uint256(int(t['hash'], 16)) for t in data['transactions'] ]
|
||||
mt = merkletree.MerkleTree(txhashes)
|
||||
if settings.COINDAEMON_Reward == 'POW':
|
||||
coinbase = self.coinbase_transaction_class(self.timestamper, self.coinbaser, data['coinbasevalue'], data['coinbaseaux']['flags'], data['height'],
|
||||
coinbase = CoinbaseTransactionPOW(self.timestamper, self.coinbaser, data['coinbasevalue'], data['coinbaseaux']['flags'], data['height'],
|
||||
settings.COINBASE_EXTRAS)
|
||||
else:
|
||||
coinbase = self.coinbase_transaction_class(self.timestamper, self.coinbaser, data['coinbasevalue'], data['coinbaseaux']['flags'], data['height'],
|
||||
coinbase = CoinbaseTransactionPOS(self.timestamper, self.coinbaser, data['coinbasevalue'], data['coinbaseaux']['flags'], data['height'],
|
||||
settings.COINBASE_EXTRAS, data['curtime'])
|
||||
|
||||
self.height = data['height']
|
||||
|
||||
@ -6,8 +6,8 @@ import settings
|
||||
import lib.logger
|
||||
log = lib.logger.get_logger('coinbasetx')
|
||||
|
||||
if settings.COINDAEMON_Reward == 'POW':
|
||||
class CoinbaseTransaction(halfnode.CTransaction):
|
||||
#if settings.COINDAEMON_Reward == 'POW':
|
||||
class CoinbaseTransactionPOW(halfnode.CTransaction):
|
||||
'''Construct special transaction used for coinbase tx.
|
||||
It also implements quick serialization using pre-cached
|
||||
scriptSig template.'''
|
||||
@ -17,8 +17,8 @@ if settings.COINDAEMON_Reward == 'POW':
|
||||
extranonce_size = struct.calcsize(extranonce_type)
|
||||
|
||||
def __init__(self, timestamper, coinbaser, value, flags, height, data):
|
||||
super(CoinbaseTransaction, self).__init__()
|
||||
log.debug("Got to CoinBaseTX")
|
||||
super(CoinbaseTransactionPOW, self).__init__()
|
||||
log.debug("Got to CoinBaseTX")
|
||||
#self.extranonce = 0
|
||||
|
||||
if len(self.extranonce_placeholder) != self.extranonce_size:
|
||||
@ -45,7 +45,7 @@ if settings.COINDAEMON_Reward == 'POW':
|
||||
self.vout.append(tx_out)
|
||||
|
||||
# Two parts of serialized coinbase, just put part1 + extranonce + part2 to have final serialized tx
|
||||
self._serialized = super(CoinbaseTransaction, self).serialize().split(self.extranonce_placeholder)
|
||||
self._serialized = super(CoinbaseTransactionPOW, self).serialize().split(self.extranonce_placeholder)
|
||||
|
||||
def set_extranonce(self, extranonce):
|
||||
if len(extranonce) != self.extranonce_size:
|
||||
@ -53,8 +53,56 @@ if settings.COINDAEMON_Reward == 'POW':
|
||||
|
||||
(part1, part2) = self.vin[0]._scriptSig_template
|
||||
self.vin[0].scriptSig = part1 + extranonce + part2
|
||||
elif settings.COINDAEMON_Reward == 'POS':
|
||||
class CoinbaseTransaction(halfnode.CTransaction):
|
||||
#elif settings.COINDAEMON_Reward == 'POS':
|
||||
class CoinbaseTransactionPOS(halfnode.CTransaction):
|
||||
'''Construct special transaction used for coinbase tx.
|
||||
It also implements quick serialization using pre-cached
|
||||
scriptSig template.'''
|
||||
|
||||
extranonce_type = '>Q'
|
||||
extranonce_placeholder = struct.pack(extranonce_type, int('f000000ff111111f', 16))
|
||||
extranonce_size = struct.calcsize(extranonce_type)
|
||||
|
||||
def __init__(self, timestamper, coinbaser, value, flags, height, data, ntime):
|
||||
super(CoinbaseTransactionPOS, self).__init__()
|
||||
log.debug("Got to CoinBaseTX")
|
||||
#self.extranonce = 0
|
||||
|
||||
if len(self.extranonce_placeholder) != self.extranonce_size:
|
||||
raise Exception("Extranonce placeholder don't match expected length!")
|
||||
|
||||
tx_in = halfnode.CTxIn()
|
||||
tx_in.prevout.hash = 0L
|
||||
tx_in.prevout.n = 2**32-1
|
||||
tx_in._scriptSig_template = (
|
||||
util.ser_number(height) + binascii.unhexlify(flags) + util.ser_number(int(timestamper.time())) + \
|
||||
chr(self.extranonce_size),
|
||||
util.ser_string(coinbaser.get_coinbase_data() + data)
|
||||
)
|
||||
|
||||
tx_in.scriptSig = tx_in._scriptSig_template[0] + self.extranonce_placeholder + tx_in._scriptSig_template[1]
|
||||
|
||||
tx_out = halfnode.CTxOut()
|
||||
tx_out.nValue = value
|
||||
tx_out.scriptPubKey = coinbaser.get_script_pubkey()
|
||||
|
||||
self.nTime = ntime
|
||||
if settings.COINDAEMON_SHA256_TX == 'yes':
|
||||
self.strTxComment = "http://github.com/ahmedbodi/stratum-mining"
|
||||
self.vin.append(tx_in)
|
||||
self.vout.append(tx_out)
|
||||
|
||||
# Two parts of serialized coinbase, just put part1 + extranonce + part2 to have final serialized tx
|
||||
self._serialized = super(CoinbaseTransactionPOS, self).serialize().split(self.extranonce_placeholder)
|
||||
|
||||
def set_extranonce(self, extranonce):
|
||||
if len(extranonce) != self.extranonce_size:
|
||||
raise Exception("Incorrect extranonce size")
|
||||
|
||||
(part1, part2) = self.vin[0]._scriptSig_template
|
||||
self.vin[0].scriptSig = part1 + extranonce + part2
|
||||
#else:
|
||||
class CoinbaseTransaction(halfnode.CTransaction):
|
||||
'''Construct special transaction used for coinbase tx.
|
||||
It also implements quick serialization using pre-cached
|
||||
scriptSig template.'''
|
||||
@ -86,54 +134,6 @@ elif settings.COINDAEMON_Reward == 'POS':
|
||||
tx_out.nValue = value
|
||||
tx_out.scriptPubKey = coinbaser.get_script_pubkey()
|
||||
|
||||
self.nTime = ntime
|
||||
if settings.COINDAEMON_SHA256_TX == 'yes':
|
||||
self.strTxComment = "http://github.com/ahmedbodi/stratum-mining"
|
||||
self.vin.append(tx_in)
|
||||
self.vout.append(tx_out)
|
||||
|
||||
# Two parts of serialized coinbase, just put part1 + extranonce + part2 to have final serialized tx
|
||||
self._serialized = super(CoinbaseTransaction, self).serialize().split(self.extranonce_placeholder)
|
||||
|
||||
def set_extranonce(self, extranonce):
|
||||
if len(extranonce) != self.extranonce_size:
|
||||
raise Exception("Incorrect extranonce size")
|
||||
|
||||
(part1, part2) = self.vin[0]._scriptSig_template
|
||||
self.vin[0].scriptSig = part1 + extranonce + part2
|
||||
else:
|
||||
class CoinbaseTransaction(halfnode.CTransaction):
|
||||
'''Construct special transaction used for coinbase tx.
|
||||
It also implements quick serialization using pre-cached
|
||||
scriptSig template.'''
|
||||
|
||||
extranonce_type = '>Q'
|
||||
extranonce_placeholder = struct.pack(extranonce_type, int('f000000ff111111f', 16))
|
||||
extranonce_size = struct.calcsize(extranonce_type)
|
||||
|
||||
def __init__(self, timestamper, coinbaser, value, flags, height, data, ntime):
|
||||
super(CoinbaseTransaction, self).__init__()
|
||||
log.debug("Got to CoinBaseTX")
|
||||
#self.extranonce = 0
|
||||
|
||||
if len(self.extranonce_placeholder) != self.extranonce_size:
|
||||
raise Exception("Extranonce placeholder don't match expected length!")
|
||||
|
||||
tx_in = halfnode.CTxIn()
|
||||
tx_in.prevout.hash = 0L
|
||||
tx_in.prevout.n = 2**32-1
|
||||
tx_in._scriptSig_template = (
|
||||
util.ser_number(height) + binascii.unhexlify(flags) + util.ser_number(int(timestamper.time())) + \
|
||||
chr(self.extranonce_size),
|
||||
util.ser_string(coinbaser.get_coinbase_data() + data)
|
||||
)
|
||||
|
||||
tx_in.scriptSig = tx_in._scriptSig_template[0] + self.extranonce_placeholder + tx_in._scriptSig_template[1]
|
||||
|
||||
tx_out = halfnode.CTxOut()
|
||||
tx_out.nValue = value
|
||||
tx_out.scriptPubKey = coinbaser.get_script_pubkey()
|
||||
|
||||
self.nTime = ntime
|
||||
self.vin.append(tx_in)
|
||||
self.vout.append(tx_out)
|
||||
|
||||
@ -21,27 +21,27 @@ log = lib.logger.get_logger('halfnode')
|
||||
log.debug("Got to Halfnode")
|
||||
|
||||
if settings.COINDAEMON_ALGO == 'scrypt':
|
||||
log.debug("########################################### Loading LTC Scrypt #########################################################")
|
||||
import ltc_scrypt
|
||||
log.debug("########################################### Loading LTC Scrypt #########################################################")
|
||||
import ltc_scrypt
|
||||
elif settings.COINDAEMON_ALGO == 'quark':
|
||||
log.debug("########################################### Loading Quark Support #########################################################")
|
||||
import quark_hash
|
||||
log.debug("########################################### Loading Quark Support #########################################################")
|
||||
import quark_hash
|
||||
else:
|
||||
log.debug("########################################### Loading SHA256 Support ######################################################")
|
||||
log.debug("########################################### Loading SHA256 Support ######################################################")
|
||||
|
||||
if settings.COINDAEMON_Reward == 'POS':
|
||||
log.debug("########################################### Loading POS Support #########################################################")
|
||||
pass
|
||||
else:
|
||||
log.debug("########################################### Loading POW Support ######################################################")
|
||||
pass
|
||||
#if settings.COINDAEMON_Reward == 'POS':
|
||||
# log.debug("########################################### Loading POS Support #########################################################")
|
||||
# pass
|
||||
#else:
|
||||
# log.debug("########################################### Loading POW Support ######################################################")
|
||||
# pass
|
||||
|
||||
if settings.COINDAEMON_TX == 'yes':
|
||||
log.debug("########################################### Loading SHA256 Transaction Message Support #########################################################")
|
||||
pass
|
||||
log.debug("########################################### Loading SHA256 Transaction Message Support #########################################################")
|
||||
pass
|
||||
else:
|
||||
log.debug("########################################### NOT Loading SHA256 Transaction Message Support ######################################################")
|
||||
pass
|
||||
log.debug("########################################### NOT Loading SHA256 Transaction Message Support ######################################################")
|
||||
pass
|
||||
|
||||
|
||||
MY_VERSION = 31402
|
||||
|
||||
24
lib/util.py
24
lib/util.py
@ -211,15 +211,15 @@ def ser_number(n):
|
||||
s.append(n)
|
||||
return bytes(s)
|
||||
|
||||
if settings.COINDAEMON_Reward == 'POW':
|
||||
def script_to_address(addr):
|
||||
d = address_to_pubkeyhash(addr)
|
||||
if not d:
|
||||
raise ValueError('invalid address')
|
||||
(ver, pubkeyhash) = d
|
||||
return b'\x76\xa9\x14' + pubkeyhash + b'\x88\xac'
|
||||
else:
|
||||
def script_to_pubkey(key):
|
||||
if len(key) == 66: key = binascii.unhexlify(key)
|
||||
if len(key) != 33: raise Exception('Invalid Address')
|
||||
return b'\x21' + key + b'\xac'
|
||||
#if settings.COINDAEMON_Reward == 'POW':
|
||||
def script_to_address(addr):
|
||||
d = address_to_pubkeyhash(addr)
|
||||
if not d:
|
||||
raise ValueError('invalid address')
|
||||
(ver, pubkeyhash) = d
|
||||
return b'\x76\xa9\x14' + pubkeyhash + b'\x88\xac'
|
||||
#else:
|
||||
def script_to_pubkey(key):
|
||||
if len(key) == 66: key = binascii.unhexlify(key)
|
||||
if len(key) != 33: raise Exception('Invalid Address')
|
||||
return b'\x21' + key + b'\xac'
|
||||
|
||||
24
mining/Cache.py
Normal file
24
mining/Cache.py
Normal file
@ -0,0 +1,24 @@
|
||||
''' A simple wrapper for pylibmc. It can be overwritten with simple hashing if necessary '''
|
||||
import lib.settings as settings
|
||||
import lib.logger
|
||||
log = lib.logger.get_logger('Cache')
|
||||
|
||||
import pylibmc
|
||||
|
||||
class Cache():
|
||||
def __init__(self):
|
||||
# Open a new connection
|
||||
self.mc = pylibmc.Client([settings.MEMCACHE_HOST + ":" + str(settings.MEMCACHE_PORT)], binary=True)
|
||||
log.info("Caching initialized")
|
||||
|
||||
def set(self, key, value, time=settings.MEMCACHE_TIMEOUT):
|
||||
return self.mc.set(settings.MEMCACHE_PREFIX + str(key), value, time)
|
||||
|
||||
def get(self, key):
|
||||
return self.mc.get(settings.MEMCACHE_PREFIX + str(key))
|
||||
|
||||
def delete(self, key):
|
||||
return self.mc.delete(settings.MEMCACHE_PREFIX + str(key))
|
||||
|
||||
def exists(self, key):
|
||||
return str(key) in self.mc.get(settings.MEMCACHE_PREFIX + str(key))
|
||||
@ -3,6 +3,7 @@ import time
|
||||
from datetime import datetime
|
||||
import Queue
|
||||
import signal
|
||||
import Cache
|
||||
|
||||
import lib.settings as settings
|
||||
|
||||
@ -19,8 +20,7 @@ class DBInterface():
|
||||
self.q = Queue.Queue()
|
||||
self.queueclock = None
|
||||
|
||||
self.usercache = {}
|
||||
self.clearusercache()
|
||||
self.cache = Cache.Cache()
|
||||
|
||||
self.nextStatsUpdate = 0
|
||||
|
||||
@ -67,11 +67,6 @@ class DBInterface():
|
||||
return DB_None.DB_None()
|
||||
|
||||
|
||||
def clearusercache(self):
|
||||
log.debug("DBInterface.clearusercache called")
|
||||
self.usercache = {}
|
||||
self.usercacheclock = reactor.callLater(settings.DB_USERCACHE_TIME , self.clearusercache)
|
||||
|
||||
def scheduleImport(self):
|
||||
# This schedule's the Import
|
||||
if settings.DATABASE_DRIVER == "sqlite":
|
||||
@ -163,19 +158,16 @@ class DBInterface():
|
||||
# Force username and password to be strings
|
||||
username = str(username)
|
||||
password = str(password)
|
||||
wid = username + ":-:" + password
|
||||
|
||||
if wid in self.usercache:
|
||||
if not settings.USERS_CHECK_PASSWORD and self.user_exists(username):
|
||||
return True
|
||||
elif not settings.USERS_CHECK_PASSWORD and self.user_exists(username):
|
||||
self.usercache[wid] = 1
|
||||
elif self.cache.get(username) == password:
|
||||
return True
|
||||
elif self.dbi.check_password(username, password):
|
||||
self.usercache[wid] = 1
|
||||
self.cache.set(username, password)
|
||||
return True
|
||||
elif settings.USERS_AUTOADD == True:
|
||||
self.insert_user(username, password)
|
||||
self.usercache[wid] = 1
|
||||
self.cache.set(username, password)
|
||||
return True
|
||||
|
||||
log.info("Authentication for %s failed" % username)
|
||||
@ -188,6 +180,8 @@ class DBInterface():
|
||||
return self.dbi.get_user(id)
|
||||
|
||||
def user_exists(self, username):
|
||||
if self.cache.get(username) is not None:
|
||||
return True
|
||||
user = self.dbi.get_user(username)
|
||||
return user is not None
|
||||
|
||||
@ -195,11 +189,13 @@ class DBInterface():
|
||||
return self.dbi.insert_user(username, password)
|
||||
|
||||
def delete_user(self, username):
|
||||
self.mc.delete(username)
|
||||
self.usercache = {}
|
||||
return self.dbi.delete_user(username)
|
||||
|
||||
def update_user(self, username, password):
|
||||
self.usercache = {}
|
||||
self.mc.delete(username)
|
||||
self.mc.set(username, password)
|
||||
return self.dbi.update_user(username, password)
|
||||
|
||||
def update_worker_diff(self, username, diff):
|
||||
|
||||
@ -59,6 +59,72 @@ class DB_Mysql_Vardiff(DB_Mysql.DB_Mysql):
|
||||
)
|
||||
|
||||
self.dbh.commit()
|
||||
|
||||
def found_block(self, data):
|
||||
# for database compatibility we are converting our_worker to Y/N format
|
||||
if data[5]:
|
||||
data[5] = 'Y'
|
||||
else:
|
||||
data[5] = 'N'
|
||||
|
||||
# Check for the share in the database before updating it
|
||||
# Note: We can't use DUPLICATE KEY because solution is not a key
|
||||
|
||||
self.execute(
|
||||
"""
|
||||
Select `id` from `shares`
|
||||
WHERE `solution` = %(solution)s
|
||||
LIMIT 1
|
||||
""",
|
||||
{
|
||||
"solution": data[2]
|
||||
}
|
||||
)
|
||||
|
||||
shareid = self.dbc.fetchone()
|
||||
|
||||
if shareid[0] > 0:
|
||||
# Note: difficulty = -1 here
|
||||
self.execute(
|
||||
"""
|
||||
UPDATE `shares`
|
||||
SET `upstream_result` = %(result)s
|
||||
WHERE `solution` = %(solution)s
|
||||
AND `id` = %(id)s
|
||||
LIMIT 1
|
||||
""",
|
||||
{
|
||||
"result": data[5],
|
||||
"solution": data[2],
|
||||
"id": shareid[0]
|
||||
}
|
||||
)
|
||||
|
||||
self.dbh.commit()
|
||||
else:
|
||||
self.execute(
|
||||
"""
|
||||
INSERT INTO `shares`
|
||||
(time, rem_host, username, our_result,
|
||||
upstream_result, reason, solution)
|
||||
VALUES
|
||||
(FROM_UNIXTIME(%(time)s), %(host)s,
|
||||
%(uname)s,
|
||||
%(lres)s, %(result)s, %(reason)s, %(solution)s)
|
||||
""",
|
||||
{
|
||||
"time": v[4],
|
||||
"host": v[6],
|
||||
"uname": v[0],
|
||||
"lres": v[5],
|
||||
"result": v[5],
|
||||
"reason": v[9],
|
||||
"solution": v[2]
|
||||
}
|
||||
)
|
||||
|
||||
self.dbh.commit()
|
||||
|
||||
|
||||
def update_worker_diff(self, username, diff):
|
||||
log.debug("Setting difficulty for %s to %s", username, diff)
|
||||
|
||||
@ -44,17 +44,16 @@ def setup(on_startup):
|
||||
if isinstance(result, dict):
|
||||
# litecoind implements version 1 of getblocktemplate
|
||||
if result['version'] >= 1:
|
||||
result = (yield bitcoin_rpc.getinfo())
|
||||
if isinstance(result,dict):
|
||||
if 'stake' in result and settings.COINDAEMON_Reward == 'POS':
|
||||
log.info("CoinD looks to be a POS Coin, Config for POS looks correct")
|
||||
break
|
||||
elif 'stake' not in result and settings.COINDAEMON_Reward == 'POW':
|
||||
log.info("CoinD looks to be a POW Coin, Config looks to be correct")
|
||||
break
|
||||
else:
|
||||
log.error("Wrong Algo Selected, Switch to appropriate POS/POW in config.py!")
|
||||
reactor.stop()
|
||||
result = (yield bitcoin_rpc.getdifficulty())
|
||||
if isinstance(result,dict):
|
||||
if 'proof-of-stake' in result:
|
||||
settings.COINDAEMON_Reward = 'POS'
|
||||
log.info("Coin detected as POS")
|
||||
break;
|
||||
else:
|
||||
settings.COINDAEMON_Reward = 'POW'
|
||||
log.info("Coin detected as POW")
|
||||
break;
|
||||
else:
|
||||
log.error("Block Version mismatch: %s" % result['version'])
|
||||
|
||||
|
||||
48
requirements.txt
Normal file
48
requirements.txt
Normal file
@ -0,0 +1,48 @@
|
||||
# This File is used to create a list of requirements needed for testing stratum-mining or to create a clone install
|
||||
BeautifulSoup==3.2.1
|
||||
#Brlapi==0.5.7
|
||||
#GnuPGInterface==0.3.2
|
||||
MySQL-python==1.2.3
|
||||
#PAM==0.4.2
|
||||
#Pyste==0.9.10
|
||||
#SOAPpy==0.12.0
|
||||
Twisted==12.0.0
|
||||
#Twisted-Conch==12.0.0
|
||||
#Twisted-Core==12.0.0
|
||||
#Twisted-Lore==12.0.0
|
||||
#Twisted-Mail==12.0.0
|
||||
#Twisted-Names==12.0.0
|
||||
#Twisted-News==12.0.0
|
||||
#Twisted-Runner==12.0.0
|
||||
#Twisted-Web==12.0.0
|
||||
#Twisted-Words==12.0.0
|
||||
#apt-xapian-index==0.45
|
||||
argparse==1.2.1
|
||||
autobahn==0.6.5
|
||||
#chardet==2.0.1
|
||||
defer==1.0.6
|
||||
distribute==0.6.28
|
||||
ecdsa==0.10
|
||||
feedparser==5.1.2
|
||||
fpconst==0.7.2
|
||||
httplib2==0.7.4
|
||||
#louis==2.4.1
|
||||
#ltc-scrypt==1.0
|
||||
#numpy==1.6.2
|
||||
pyOpenSSL==0.13
|
||||
pyasn1==0.1.3
|
||||
pycrypto==2.6
|
||||
#pycurl==7.19.0
|
||||
pyserial==2.5
|
||||
#python-apt==0.8.8.2
|
||||
#python-debian==0.1.21
|
||||
#python-debianbts==1.11
|
||||
python-memcached==1.48
|
||||
pyxdg==0.19
|
||||
#reportbug==6.4.4
|
||||
simplejson==2.5.2
|
||||
#stratum==0.2.13
|
||||
#uTidylib==0.2
|
||||
#unattended-upgrades==0.1
|
||||
#wsgiref==0.1.2
|
||||
zope.interface==3.6.1
|
||||
Loading…
Reference in New Issue
Block a user