import calendar
from decimal import Decimal
import decimal
import logging
import cPickle
from collections import namedtuple
from datetime import datetime, timedelta
from flask import current_app
from sqlalchemy.schema import CheckConstraint
from .model_lib import base
from .filters import sig_round
from . import db, currencies, chains, algos, cache
[docs]def make_upper_lower(trim=None, span=None, offset=None, clip=None, fmt="dt"):
""" Generates upper and lower bounded datetime objects. """
dt = datetime.utcnow()
if span is None:
span = timedelta(minutes=10)
if trim is not None:
trim_seconds = trim.total_seconds()
stamp = calendar.timegm(dt.utctimetuple())
stamp = (stamp // trim_seconds) * trim_seconds
dt = datetime.utcfromtimestamp(stamp)
if offset is None:
offset = timedelta(minutes=0)
if clip is None:
clip = timedelta(minutes=0)
upper = dt - offset - clip
lower = dt - span - offset
if fmt == "both":
return lower, upper, calendar.timegm(lower.utctimetuple()), calendar.timegm(upper.utctimetuple())
elif fmt == "stamp":
calendar.timegm(lower.utctimetuple()), calendar.timegm(upper.utctimetuple())
return lower, upper
[docs]class TradeRequest(base):
"""
Used to provide info necessary to external applications for trading currencies
Created rows will be checked + updated externally
"""
id = db.Column(db.Integer, primary_key=True)
# 3-8 letter code for the currency to be traded
currency = db.Column(db.String, nullable=False)
# Quantity of currency to be traded
quantity = db.Column(db.Numeric, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
type = db.Column(db.Enum("sell", "buy", name="req_type"), nullable=False)
# These values should only be updated by sctrader
exchanged_quantity = db.Column(db.Numeric, default=None)
# Fees from fulfilling this tr
fees = db.Column(db.Numeric, default=None)
_status = db.Column(db.SmallInteger, default=0)
[docs] def distribute(self):
assert self.type in ["buy", "sell"], "Invalid type!"
assert self.exchanged_quantity > 0
# Check config to see if we're charging exchange fees or not
payable_amount = self.exchanged_quantity
if current_app.config.get('charge_autoex_fees', False):
payable_amount -= self.fees
credits = self.credits # Do caching here, avoid multiple lookups
if not credits:
current_app.logger.warn("Trade request #{} has no attached credits"
.format(self.id))
else:
# calculate user payouts based on percentage of the total
# exchanged value
if self.type == "sell":
portions = {c.id: c.amount for c in credits}
elif self.type == "buy":
portions = {c.id: c.sell_amount for c in credits}
amounts = distributor(payable_amount, portions)
for credit in credits:
if self.type == "sell":
assert credit.sell_amount is None
credit.sell_amount = amounts[credit.id]
elif self.type == "buy":
assert credit.buy_amount is None
credit.buy_amount = amounts[credit.id]
# Mark the credit ready for payout to users
credit.payable = True
current_app.logger.info(
"Successfully pushed trade result for request id {:,} and "
"amount {:,} to {:,} credits.".
format(self.id, self.exchanged_quantity, len(credits)))
self._status = 6
@property
def credits(self):
if self.type == "sell":
return self.sell_credits
return self.buy_credits
@property
def status(self):
if self._status == 0:
return "Processing Trade Request"
elif self._status == 1:
return "Pending Exchange Deposit"
elif self._status == 2:
return "Trading on Exchange"
elif self._status == 4:
return "Pending Exchange Withdrawal"
elif self._status == 6:
return "Complete"
return "Error"
@classmethod
[docs] def create(cls, currency, quantity):
tr = cls(currency=currency,
quantity=quantity)
# add and flush
db.session.add(tr)
return tr
[docs]class ChainPayout(base):
# The share chain that contributed this portion of shares to the block
chainid = db.Column(db.Integer, primary_key=True)
block_id = db.Column(db.Integer, db.ForeignKey('block.id'), primary_key=True)
block = db.relationship('Block', foreign_keys=[block_id], backref='chain_payouts')
# Placeholder for the point at which the block was solved in this share chain.
solve_slice = db.Column(db.Integer)
# Shares on this chain. Used to get portion of total block
chain_shares = db.Column(db.Numeric, nullable=False)
# Payout shares. The number of shares computed to payout users
payout_shares = db.Column(db.Numeric, nullable=False)
# Total portion that this chain recieved
amount = db.Column(db.Numeric)
# total going to pool from donations
donations = db.Column(db.Numeric)
# total going to pool from fees
fees = db.Column(db.Numeric)
@property
def config_obj(self):
return chains[self.chainid]
@property
def hashes(self):
hps = current_app.algos[self.block.algo].hashes_per_share
return hps * self.chain_shares
@property
def mhashes(self):
return self.hashes / 1000000
[docs] def make_credit_obj(self, user, address, currency, shares):
""" Makes the appropriate credit object given a few details. Payout
amount too be calculated. """
# If they're trying to get paid to invalid currency pay it to the
# pool
key = (user, address, currency)
# If there's already a payout object with this information
if key in self.credits:
self.credits[key].shares += shares
return
p = Credit.make_credit(
user=user,
block=self.block,
sharechain_id=self.chainid,
currency=currency.key,
source=0,
address=address)
p.shares = shares
db.session.add(p)
self.credits[key] = p
[docs] def distribute(self):
share_distrib = {}
total_shares = 0
for key, credit in self.credits.iteritems():
share_distrib[key] = credit.shares
total_shares += credit.shares
assert total_shares == self.payout_shares, "Chain had payout share count mismatch at distribution time!"
credit_distrib = distributor(self.amount, share_distrib)
for key in share_distrib:
self.credits[key].amount = credit_distrib[key]
[docs]class Block(base):
""" This class stores metadata on all blocks found by the pool """
# the hash of the block for orphan checking
id = db.Column(db.Integer, primary_key=True)
hash = db.Column(db.String(64), unique=True)
height = db.Column(db.Integer, nullable=False)
# User who discovered block
user = db.Column(db.String)
worker = db.Column(db.String)
# When block was found
found_at = db.Column(db.DateTime, nullable=False)
# # Time started on block
time_started = db.Column(db.DateTime, nullable=False)
# Is block now orphaned?
orphan = db.Column(db.Boolean, default=False)
# Is the block matured?
mature = db.Column(db.Boolean, default=False)
# Block total value (includes transaction fees)
total_value = db.Column(db.Numeric)
# Associated transaction fees
transaction_fees = db.Column(db.Numeric)
# Difficulty of block when solved
difficulty = db.Column(db.Float, nullable=False)
# 3-8 letter code for the currency that was mined
currency = db.Column(db.String, nullable=False)
# Will be == currency if currency is was merge mined
merged = db.Column(db.Boolean, nullable=False)
# The hashing algorith mused to solve the block
algo = db.Column(db.String, nullable=False)
standard_join = ['status', 'merged', 'currency', 'worker', 'explorer_link',
'luck', 'total_value', 'difficulty', 'duration',
'found_at', 'time_started']
def __str__(self):
return "<{} h:{} hsh:{}>".format(self.currency, self.height, self.hash)
@property
def algo_obj(self):
return algos[self.algo]
@property
def currency_obj(self):
return currencies[self.currency]
@property
def contributed(self):
""" Total fees + donations associated with this block """
return sum([(bp.donations + bp.fees) for bp in self.chain_payouts]) or 0
@property
def average_hashrate(self):
t = self.duration.total_seconds()
if not t:
return 0.0
return (float(sum([bp.chain_shares for bp in self.chain_payouts])) *
float(self.currency_obj.algo.hashes_per_share) / t)
@property
def hashes_to_solve(self):
return (sum([bp.chain_shares for bp in self.chain_payouts]) *
self.currency_obj.algo.hashes_per_share)
@property
def shares_to_solve(self):
""" Total shares that were required to solve the block """
return sum([bp.chain_shares for bp in self.chain_payouts])
@property
def status(self):
if self.mature:
return "Mature"
if self.orphan:
return "Orphan"
confirms = self.confirms_remaining
if confirms is not None:
return "{} Confirms Remaining".format(confirms)
else:
return "Pending confirmation"
@property
def explorer_link(self):
# XXX: Currently not supported
return False
@property
def luck(self):
hps = current_app.algos[self.algo].hashes_per_share
return ((self.difficulty * (2 ** 32)) / ((float(self.shares_to_solve) or 1) * hps)) * 100
@property
def timestamp(self):
return calendar.timegm(self.found_at.utctimetuple())
@property
def duration(self):
seconds = round((self.found_at - self.time_started).total_seconds())
formatted_time = timedelta(seconds=seconds)
return formatted_time
@property
def confirms_remaining(self):
data = cache.get("{}_data".format(self.currency)) or {}
if data.get('height'):
return (self.height +
self.currency_obj.block_mature_confirms -
data['height'])
return None
[docs] def chain_distrib(self):
chain_data = {}
total = 0
for chain_payout in self.chain_payouts:
total += chain_payout.chain_shares
chain_data.setdefault(chain_payout.config_obj, [chain_payout.chain_shares, 0])
for data in chain_data.itervalues():
data[1] = data[0] / total * 100
return chain_data
[docs] def chain_profitability(self):
""" Creates a dictionary that is keyed by chainid to represent the BTC
earned per number of shares for every share chain that helped solve
this block """
# Get a some credit totals
cache_key_name = "chain_profitability_{}".format(self.hash)
chain_data = cache.cache._client.get(cache_key_name)
if chain_data:
chain_data = cPickle.loads(chain_data)
return chain_data
uncacheable = False
chain_data = {}
for chain_payout in self.chain_payouts:
chain = chain_data.setdefault(
chain_payout.chainid,
dict(btc_total=0,
amount_total=0,
amount_sold=0,
obj=chain_payout)
)
for credit in (Credit.query.with_polymorphic(CreditExchange).
filter_by(block_id=self.id)):
if not credit.sharechain_id:
continue
chain = chain_data[credit.sharechain_id]
chain['amount_total'] += credit.amount
if credit.type == 1 and credit.sell_amount > 0:
chain['amount_sold'] += credit.amount
chain['btc_total'] += credit.sell_amount
elif credit.type == 1:
uncacheable = True
# We're gonna need to be pretty precise here
with decimal.localcontext(decimal.BasicContext) as ctx:
ctx.rounding = decimal.ROUND_DOWN
ctx.prec = 100
for chain_id, data in chain_data.iteritems():
# determine what percent was sold
sold_perc = data['amount_sold'] / data['amount_total']
# Determine shares that accounted for that sale quantity
data['sold_shares'] = data.pop('obj').chain_shares * sold_perc
if uncacheable is False:
cache.cache._client.set(cache_key_name,
cPickle.dumps(chain_data))
cache.cache._client.expire(cache_key_name, 86400 * 4)
return chain_data
[docs]class Transaction(base):
id = db.Column(db.Integer, primary_key=True)
txid = db.Column(db.String(64), unique=True)
confirmed = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
currency = db.Column(db.String, nullable=False)
network_fee = db.Column(db.Numeric)
standard_join = ['txid', 'confirmed', 'created_at', 'currency', '__dont_mongo']
@property
def url_for(self):
return "/transaction/{}".format(self.txid)
@property
def status(self):
if self.confirmed:
return "Confirmed"
return "Pending"
@property
def timestamp(self):
return calendar.timegm(self.created_at.utctimetuple())
@property
def currency_obj(self):
return currencies[self.currency]
[docs]class Credit(base):
""" A credit for currency directly crediting a users balance. These
have no intermediary exchanges. """
id = db.Column(db.Integer, primary_key=True)
block_id = db.Column(db.Integer, db.ForeignKey('block.id'))
block = db.relationship('Block', foreign_keys=[block_id], backref='credits')
user = db.Column(db.String)
sharechain_id = db.Column(db.SmallInteger)
address = db.Column(db.String, nullable=False)
currency = db.Column(db.String, nullable=False)
amount = db.Column(db.Numeric, CheckConstraint('amount > 0', 'min_credit_amount'))
fee_perc = db.Column(db.SmallInteger, default=0)
pd_perc = db.Column(db.SmallInteger, default=0)
type = db.Column(db.SmallInteger)
payable = db.Column(db.Boolean, default=False)
source = db.Column(db.SmallInteger)
payout = db.relationship('Payout', backref='credits')
payout_id = db.Column(db.Integer, db.ForeignKey('payout.id'))
__table_args__ = (
db.Index('payable_idx', 'payable'),
db.Index('user_idx', 'user'),
)
__mapper_args__ = {
'polymorphic_identity': 0,
'polymorphic_on': type
}
standard_join = ['status', 'created_at', 'explorer_link',
'text_perc_applied', 'mined', 'height',
'transaction_id']
@classmethod
[docs] def make_credit(self, currency, block, **kwargs):
assert isinstance(currency, basestring)
# Create a payout entry indicating this needs to be exchanged or not
if currency != block.currency:
cls = CreditExchange
else:
cls = Credit
p = cls(block=block,
currency=currency,
**kwargs)
return p
@property
def payable_amount(self):
return self.amount
@property
def currency_obj(self):
return currencies[self.currency]
@property
def sharechain_title(self):
return current_app.powerpools[self.pp_sharechain_id].title
@property
def cut_perc(self):
cut_perc = (self.pd_perc or 0) + (self.fee_perc or 0)
if cut_perc == 0:
return Decimal('0')
else:
return Decimal(cut_perc) / 100
@property
def hr_fee_perc(self):
return round(float(self.fee_perc or 0), 2)
@property
def hr_pd_perc(self):
return round(float(self.pd_perc or 0), 2)
@property
def perc_applied(self):
return (self.cut_perc * self.mined).quantize(current_app.SATOSHI)
@property
def text_perc_applied(self):
if self.cut_perc < 0:
return "BONUS {}".format(sig_round(self.perc_applied))
else:
return "{}".format(sig_round(self.perc_applied * -1))
@property
def mined(self):
return self.amount / (1 - self.cut_perc)
@property
def height(self):
return self.block.height
@property
def status(self):
if self.block.orphan:
return "Block Orphaned"
if not self.block.mature:
return "Pending Block Confirmation"
if self.payable:
if self.payout:
if self.payout.transaction:
return ('Payout <a href="{}">{}...</a>'
.format(self.payout.transaction.url_for,
self.payout.transaction.txid[:15],
self.payout.transaction.status))
return "Payout Pending"
return "Pending batching for payout"
[docs]class CreditExchange(Credit):
""" A credit that needs a sale and a buy to get to the correct currency
"""
id = db.Column(db.Integer, db.ForeignKey('credit.id'), primary_key=True)
sell_req_id = db.Column(db.Integer, db.ForeignKey('trade_request.id'))
sell_req = db.relationship('TradeRequest', foreign_keys=[sell_req_id],
backref='sell_credits')
sell_amount = db.Column(db.Numeric)
buy_req_id = db.Column(db.Integer, db.ForeignKey('trade_request.id'))
buy_req = db.relationship('TradeRequest', foreign_keys=[buy_req_id],
backref='buy_credits')
buy_amount = db.Column(db.Numeric)
@property
def payable_amount(self):
return self.buy_amount
@property
def status(self):
if self.block.orphan:
return "Block Orphaned"
if not self.block.mature:
return "Pending Block Confirmation"
if self.payable:
if self.payout:
if self.payout.transaction:
return ('Payout <a href="{}">{}...</a>'
.format(self.payout.transaction.url_for,
self.payout.transaction.txid[:15],
self.payout.transaction.status))
return "Payout Pending"
return "Pending batching for payout"
# Don't say we're purchasing if we'd be shown as purchasing BTC to
# avoid confusion
btc = self.currency == "BTC"
if self.buy_req and not btc:
return "Purchasing desired currency"
if self.sell_req and self.sell_req._status == 6:
if btc:
return "Pending Payout"
else:
return "Sold on exchange, pending purchase"
return "Pending sale on exchange"
@property
def final_amount(self):
return self.buy_amount
__mapper_args__ = {
'polymorphic_identity': 1
}
[docs]class Payout(base):
id = db.Column(db.Integer, primary_key=True)
transaction_id = db.Column(db.Integer, db.ForeignKey('transaction.id'))
transaction = db.relationship('Transaction', backref='payouts')
user = db.Column(db.String)
address = db.Column(db.String, nullable=False)
currency = db.Column(db.String, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
amount = db.Column(db.Numeric, CheckConstraint('amount > 0',
'min_payout_amount'))
count = db.Column(db.SmallInteger)
@property
def currency_obj(self):
return currencies[self.currency]
@property
def payout_currency(self):
return self.currency
@property
def status(self):
if self.transaction_id:
if self.transaction.confirmed is False:
return "Funds Sent - Pending TX confirmation"
else:
return "Complete"
return "Payout pending"
@property
def timestamp(self):
return calendar.timegm(self.found_at.utctimetuple())
@classmethod
def average_combine(cls, *lst):
""" Takes an iterable and combines the values. Usually either returns
an average or a sum. Can assume at least one item in list """
return sum(lst) / len(lst)
@classmethod
def sum_combine(cls, *lst):
""" Takes a query list and combines the values. Usually either returns
an average or a sum. Can assume at least one item in ql """
return sum(lst)
[docs]class TimeSlice(object):
""" An time abstracted data sample that pertains to a single worker.
Currently used to represent accepted and rejected shares. """
@property
def end_time(self):
return self.time + timedelta(
seconds=self.span_config[self.span]['slice'].total_seconds())
@property
def item_key(self):
return self.key(**{k: getattr(self, k) for k in self.keys})
@classmethod
[docs] def create(cls, user, worker, algo, value, time):
# XXX: Unused I think, need to be cut
dt = cls.floor_time(time)
slc = cls(user=user, value=value, time=dt, worker=worker)
db.session.add(slc)
return slc
@classmethod
[docs] def add_value(cls, user, value, time, worker):
dt = cls.floor_time(time)
slc = cls.query.with_lockmode('update').filter_by(
user=user, time=dt, worker=worker).one()
slc.value += value
@classmethod
[docs] def floor_time(cls, time, span, stamp=False):
seconds = cls.span_config[span]['slice'].total_seconds()
if isinstance(time, datetime):
time = calendar.timegm(time.utctimetuple())
time = (time // seconds) * seconds
if stamp:
return int(time)
return datetime.utcfromtimestamp(time)
@classmethod
[docs] def compress(cls, span, delete=True):
# If we're trying to compress the largest slice boundary
if span == len(cls.span_config) - 1:
raise Exception("Can't compress this!")
upper_span = span + 1
# the timestamp of the slice currently being processed
current_slice = None
# dictionary of lists keyed by item_hash
items = {}
def create_upper():
# add a time slice for each user in this pending period
for key, slices in items.iteritems():
# Allows us to use different combining methods. Ie averaging or
# adding
new_val = cls.combine(*[slc.value for slc in slices])
# put it in the database
upper = cls.query.filter_by(time=current_slice, span=upper_span, **key._asdict()).with_lockmode('update').first()
# wasn't in the db? create it
if not upper:
upper = cls(time=current_slice, value=new_val, span=upper_span, **key._asdict())
db.session.add(upper)
else:
upper.value = cls.combine(upper.value, new_val)
if delete:
for slc in slices:
db.session.delete(slc)
items.clear()
db.session.commit()
db.session.expire_all()
# get the minute shares that are old enough to be compressed and
# deleted
upper_time = cls.floor_time(datetime.utcnow(), upper_span) - cls.span_config[span]['window']
lower_time = upper_time - cls.span_config[span]['window']
# while there are some old slices to combine
while cls.query.filter(cls.time <= upper_time).filter_by(span=span).count() > 0:
# traverse slices that are old enough to combine in time order
found_slices = 0
chunk_slices = 0
for slc in cls.query.filter(cls.time < upper_time).filter(cls.time >= lower_time).filter_by(span=span).order_by(cls.time):
slice_time = cls.floor_time(slc.time, upper_span)
if current_slice is None:
current_slice = slice_time
# we've encountered the next time slice, so commit the pending one
if slice_time != current_slice:
logging.info("Processing slice {} with {:,} members"
.format(current_slice, chunk_slices))
chunk_slices = 0
create_upper()
current_slice = slice_time
# add the one min shares for this user the list of pending shares
# to be grouped together
key = slc.item_key
items.setdefault(key, [])
items[key].append(slc)
found_slices += 1
chunk_slices += 1
create_upper()
# Move the time span window backwards
logging.info("Found {:,} slices to combine from {} to {}!"
.format(found_slices, upper_time, lower_time))
upper_time -= cls.span_config[span]['window']
lower_time -= cls.span_config[span]['window']
@classmethod
[docs] def get_span(cls, lower=None, upper=None, stamp=False, ret_query=False,
slice_size=None, **kwargs):
""" A utility to grab a group of slices and automatically compress
smaller slices into larger slices
address, worker, and algo are just filters.
They may be a single string or list of strings.
upper and lower are datetimes.
"""
query = db.session.query(cls)
# Allow us to filter by any of the keys
for key in cls.keys:
if key in kwargs:
vals = kwargs.pop(key)
if vals:
query = query.filter(getattr(cls, key).in_(vals))
if kwargs:
raise ValueError("Extra unused parameters {}".format(kwargs))
if lower:
query = query.filter(cls.time >= lower)
# Attempt automatic slice size detection... Doesn't work too well
if slice_size is None:
if lower:
query = query.filter(cls.time >= lower)
# Determine which slice size we will use
time_in_past = datetime.utcnow() - lower
slice_size = None
for i, cfg in enumerate(cls.span_config):
if cfg['window'] > time_in_past:
slice_size = i
break
slice_size = i
else:
slice_size = len(cls.span_config) - 1
if upper:
query = query.filter(cls.time <= upper)
if ret_query:
return query
buckets = {}
for slc in query:
time = cls.floor_time(slc.time, slice_size, stamp=stamp)
key = slc.item_key
buckets.setdefault(key, {'data': slc.item_key._asdict(), 'values': {}})
buckets[key]['values'].setdefault(time, 0)
buckets[key]['values'][time] = cls.combine(buckets[key]['values'][time], slc.value)
return buckets.values()
[docs]class ShareSlice(TimeSlice, base):
SHARE_TYPES = ["acc", "low", "dup", "stale"]
time = db.Column(db.DateTime, primary_key=True)
user = db.Column(db.String, primary_key=True)
worker = db.Column(db.String, primary_key=True)
algo = db.Column(db.String, primary_key=True)
share_type = db.Column(db.Enum(*SHARE_TYPES, name="share_type"),
primary_key=True)
span = db.Column(db.SmallInteger, nullable=False)
value = db.Column(db.Float)
combine = sum_combine
keys = ['user', 'worker', 'algo', 'share_type']
key = namedtuple('Key', keys)
span_config = [dict(window=timedelta(hours=1), slice=timedelta(minutes=1)),
dict(window=timedelta(days=1), slice=timedelta(minutes=5)),
dict(window=timedelta(days=30), slice=timedelta(hours=1))]
[docs]class DeviceSlice(TimeSlice, base):
""" An data sample that pertains to a single workers device. Currently
used to temperature and hashrate. """
time = db.Column(db.DateTime, primary_key=True)
user = db.Column(db.String, primary_key=True)
worker = db.Column(db.String, primary_key=True)
device = db.Column(db.SmallInteger, primary_key=True)
stat_val = db.Column(db.SmallInteger, nullable=False, primary_key=True)
from_db = {0: "hashrate", 1: "temperature"}
to_db = {"hashrate": 0, "temperature": 1}
[docs] def get_stat(self, stat):
return self.from_db[stat]
[docs] def set_stat(self, stat):
self.stat_val = self.to_db[stat]
stat = property(get_stat, set_stat)
__table_args__ = (
db.Index('time_idx', 'time'),
)
span = db.Column(db.SmallInteger, nullable=False)
value = db.Column(db.Float)
combine = average_combine
keys = ['user', 'worker', 'device', 'stat_val']
key = namedtuple('Key', keys)
span_config = [dict(window=timedelta(hours=1), slice=timedelta(minutes=1)),
dict(window=timedelta(days=1), slice=timedelta(minutes=5)),
dict(window=timedelta(days=30), slice=timedelta(hours=1))]
################################################################################
# User account related objects
################################################################################
[docs]class UserSettings(base):
user = db.Column(db.String, primary_key=True)
pdonation_perc = db.Column(db.Numeric, default=Decimal('0'))
spayout_perc = db.Column(db.Numeric)
spayout_addr = db.Column(db.String)
spayout_curr = db.Column(db.String)
anon = db.Column(db.Boolean, default=False)
addresses = db.relationship("PayoutAddress")
[docs] def apply(self, shares, user_currency, block_currency, valid_currencies):
""" Given a share amount, a currency we're paying out, and the valid
exchangeable currencies we return a new distribution of shares among
some number of addresses. """
# Handle converting their payout address into one for the block type if
# the user has specified one. If they haven't and the block can't payout
# the user_currency then it will get caught downstream and converted to
# the pools payout information
main_address = self.user
main_currency = user_currency
for addr in self.addresses:
if addr.currency == block_currency:
main_address = addr.address
main_currency = addr.currency
# Handle special payout splitting if the special payout currency is
# payable from this block currency (and spayout is defined of course)
if (self.spayout_addr and
self.spayout_perc and
currencies[self.spayout_curr] in valid_currencies):
ret = distributor(
shares,
{
0: 1 - self.spayout_perc,
1: self.spayout_perc
})
return ((main_address, main_currency, ret[0]),
(self.spayout_addr, self.spayout_curr, ret[1]))
return ((main_address, main_currency, shares), )
@property
def exchangeable_addresses(self):
return {pa_obj.currency: pa_obj.address for pa_obj in self.addresses if pa_obj.exchangeable}
@property
def unexchangeable_addresses(self):
return {pa_obj.currency: pa_obj.address for pa_obj in self.addresses if not pa_obj.exchangeable}
@property
def hr_perc(self):
return self.hr_pdonation_perc
@property
def hr_pdonation_perc(self):
if self.pdonation_perc:
return (self.pdonation_perc * 100).quantize(Decimal('0.01'))
@property
def hr_spayout_perc(self):
if self.spayout_perc:
return (self.spayout_perc * 100).quantize(Decimal('0.01'))
@classmethod
[docs] def update(cls, address, set_addrs, del_addrs, pdonate_perc, spayout_perc,
spayout_addr, spayout_curr, del_spayout_addr, anon):
user = cls.query.filter_by(user=address).first()
if not user:
UserSettings.create(address, pdonate_perc, spayout_perc,
spayout_addr, spayout_curr, del_spayout_addr,
anon, set_addrs)
else:
user.pdonation_perc = pdonate_perc
user.anon = anon
if del_spayout_addr:
user.spayout_perc = None
user.spayout_addr = None
user.spayout_curr = None
else:
user.spayout_perc = spayout_perc
user.spayout_addr = spayout_addr
user.spayout_curr = spayout_curr
# Set addresses
for address in user.addresses:
# Update existing
for currency, addr in set_addrs.items():
if address.currency == currency:
address.currency = currency
address.address = addr
# Pop the currencies we just updated
set_addrs.pop(currency)
# Add new currencies
for currency, addr in set_addrs.iteritems():
user.addresses.append(PayoutAddress.create(addr, currency))
# Delete addresses
for address in user.addresses:
for currency in del_addrs:
if address.currency == currency:
db.session.delete(address)
db.session.commit()
return user
@classmethod
[docs] def create(cls, user, pdonate_perc, spayout_perc, spayout_addr,
spayout_curr, del_spayout_addr, anon, set_addrs):
user = cls(user=user,
pdonation_perc=pdonate_perc,
anon=anon)
if not del_spayout_addr:
user.spayout_perc = spayout_perc
user.spayout_addr = spayout_addr
user.spayout_curr = spayout_curr
db.session.add(user)
for currency, addr in set_addrs.iteritems():
user.addresses.append(PayoutAddress.create(addr, currency))
return user
[docs]class PayoutAddress(base):
address = db.Column(db.String, primary_key=True)
user = db.Column(db.String, db.ForeignKey('user_settings.user'), primary_key=True)
# Abbreviated currency name. EG 'LTC'
currency = db.Column(db.String)
@classmethod
[docs] def create(cls, address, currency):
pa = cls(currency=currency,
address=address)
db.session.add(pa)
return pa
@property
def exchangeable(self):
return currencies[self.currency].sellable
from .scheduler import distributor