mirror of
https://git.wownero.com/wownero/tippero.git
synced 2024-08-15 00:33:14 +00:00
437 lines
14 KiB
Python
437 lines
14 KiB
Python
#!/bin/python
|
|
#
|
|
# Cryptonote tipbot - utility functions
|
|
# Copyright 2014,2015 moneromooo
|
|
#
|
|
# The Cryptonote tipbot is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License as published
|
|
# by the Free Software Foundation; either version 2, or (at your option)
|
|
# any later version.
|
|
#
|
|
|
|
import redis
|
|
import hashlib
|
|
import json
|
|
import httplib
|
|
import time
|
|
import threading
|
|
import math
|
|
import string
|
|
import random
|
|
from Crypto.Random.random import getrandbits
|
|
from decimal import *
|
|
import tipbot.config as config
|
|
import tipbot.coinspecs as coinspecs
|
|
from tipbot.log import log_error, log_warn, log_info, log_log
|
|
from tipbot.link import Link
|
|
from tipbot.redisdb import *
|
|
|
|
registered_networks=dict()
|
|
networks=[]
|
|
cached_tipbot_balance=None
|
|
cached_tipbot_unlocked_balance=None
|
|
cached_tipbot_balance_timestamp=None
|
|
|
|
core_lock = threading.RLock()
|
|
|
|
def GetPassword(name):
|
|
try:
|
|
f = open('tipbot-password.txt', 'r')
|
|
for p in f:
|
|
p = p.strip("\r\n")
|
|
parts=p.split(':')
|
|
if parts[0]==name:
|
|
return parts[1]
|
|
except Exception,e:
|
|
log_error('could not fetch password: %s' % str(e))
|
|
raise
|
|
return "xxx"
|
|
finally:
|
|
f.close()
|
|
|
|
def IsParamPresent(parms,idx):
|
|
return len(parms) > idx
|
|
|
|
def GetParam(parms,idx):
|
|
if IsParamPresent(parms,idx):
|
|
return parms[idx]
|
|
return None
|
|
|
|
def GetPaymentID(link,random=False):
|
|
salt="2u3g55bkwrui32fi3g4bGR$j5g4ugnujb-"+coinspecs.name+"-";
|
|
if random:
|
|
salt = salt + "-" + str(time.time()) + "-" + str(getrandbits(128))
|
|
p = hashlib.sha256(salt+link.identity()).hexdigest();
|
|
try:
|
|
redis_hset("paymentid",p,link.identity())
|
|
except Exception,e:
|
|
log_error('GetPaymentID: failed to set payment ID for %s to redis: %s' % (link.identity(),str(e)))
|
|
return p
|
|
|
|
def GetRandomPaymentID(link):
|
|
return GetPaymentID(link, True)
|
|
|
|
def GetIdentityFromPaymentID(p):
|
|
if not redis_hexists("paymentid",p):
|
|
log_log('PaymentID %s not found' % p)
|
|
return None
|
|
identity = redis_hget("paymentid",p)
|
|
log_log('PaymentID %s => %s' % (p, str(identity)))
|
|
# HACK - grandfathering pre-network payment IDs
|
|
if identity.index(':') == -1:
|
|
log_warn('Pre-network payment ID found, assuming freenode')
|
|
identity = "freenode:"+identity
|
|
return identity
|
|
|
|
def IsAddressLengthValid(address):
|
|
if type(coinspecs.address_length[0]) == list:
|
|
for allist in coinspecs.address_length:
|
|
if len(address) >= allist[0] and len(address) <= allist[1]:
|
|
return True
|
|
else:
|
|
if len(address) >= coinspecs.address_length[0] and len(address) <= coinspecs.address_length[1]:
|
|
return True
|
|
return False
|
|
|
|
def IsValidAddress(address):
|
|
if not IsAddressLengthValid(address):
|
|
return False
|
|
for prefix in coinspecs.address_prefix:
|
|
if address.startswith(prefix):
|
|
return True
|
|
return False
|
|
|
|
def IsValidPaymentID(payment_id):
|
|
if len(payment_id)!=64:
|
|
return False
|
|
for char in payment_id:
|
|
if char not in string.hexdigits:
|
|
return False
|
|
return True
|
|
|
|
# Code taken from the Python documentation
|
|
def moneyfmt(value, places=2, curr='', sep=',', dp='.',
|
|
pos='', neg='-', trailneg=''):
|
|
"""Convert Decimal to a money formatted string.
|
|
|
|
places: required number of places after the decimal point
|
|
curr: optional currency symbol before the sign (may be blank)
|
|
sep: optional grouping separator (comma, period, space, or blank)
|
|
dp: decimal point indicator (comma or period)
|
|
only specify as blank when places is zero
|
|
pos: optional sign for positive numbers: '+', space or blank
|
|
neg: optional sign for negative numbers: '-', '(', space or blank
|
|
trailneg:optional trailing minus indicator: '-', ')', space or blank
|
|
|
|
>>> d = Decimal('-1234567.8901')
|
|
>>> moneyfmt(d, curr='$')
|
|
'-$1,234,567.89'
|
|
>>> moneyfmt(d, places=0, sep='.', dp='', neg='', trailneg='-')
|
|
'1.234.568-'
|
|
>>> moneyfmt(d, curr='$', neg='(', trailneg=')')
|
|
'($1,234,567.89)'
|
|
>>> moneyfmt(Decimal(123456789), sep=' ')
|
|
'123 456 789.00'
|
|
>>> moneyfmt(Decimal('-0.02'), neg='<', trailneg='>')
|
|
'<0.02>'
|
|
|
|
"""
|
|
q = Decimal(10) ** -places # 2 places --> '0.01'
|
|
sign, digits, exp = value.quantize(q).as_tuple()
|
|
result = []
|
|
digits = map(str, digits)
|
|
build, next = result.append, digits.pop
|
|
if sign:
|
|
build(trailneg)
|
|
for i in range(places):
|
|
build(next() if digits else '0')
|
|
build(dp)
|
|
if not digits:
|
|
build('0')
|
|
i = 0
|
|
while digits:
|
|
build(next())
|
|
i += 1
|
|
if i == 3 and digits:
|
|
i = 0
|
|
build(sep)
|
|
build(curr)
|
|
build(neg if sign else pos)
|
|
s = ''.join(reversed(result))
|
|
|
|
if dp in s:
|
|
s=s.strip('0').rstrip(dp)
|
|
if s=="" or s[0]==dp:
|
|
s="0"+s
|
|
return s
|
|
|
|
def AmountToString(amount):
|
|
if amount == None:
|
|
amount = 0
|
|
lamount=long(amount)
|
|
samount = None
|
|
if lamount == 0:
|
|
samount = "0 %s" % coinspecs.name
|
|
else:
|
|
places=long(0.5+math.log10(coinspecs.atomic_units))
|
|
for den in coinspecs.denominations:
|
|
if lamount < den[0]:
|
|
samount = moneyfmt(Decimal(lamount)/Decimal(den[1]),places=places) + " " + den[2]
|
|
break
|
|
if not samount:
|
|
samount = moneyfmt(Decimal(lamount)/Decimal(coinspecs.atomic_units),places=places) + " " + coinspecs.name
|
|
return samount
|
|
|
|
def TimeToString(seconds):
|
|
seconds=float(seconds)
|
|
if seconds < 1e-3:
|
|
return "%.2f microseconds" % (seconds*1e6)
|
|
if seconds < 1:
|
|
return "%.2f milliseconds" % (seconds*1e3)
|
|
if seconds < 60:
|
|
return "%.2f seconds" % (seconds)
|
|
if seconds < 3600:
|
|
return "%.2f minutes" % (seconds / 60)
|
|
if seconds < 3600 * 24:
|
|
return "%.2f hours" % (seconds / 3600)
|
|
if seconds < 3600 * 24 * 30.5:
|
|
return "%.2f days" % (seconds / (3600*24))
|
|
if seconds < 3600 * 24 * 365.25:
|
|
return "%.2f months" % (seconds / (3600*24*30.5))
|
|
if seconds < 3600 * 24 * 365.25 * 100:
|
|
return "%.2f years" % (seconds / (3600*24*365.25))
|
|
if seconds < 3600 * 24 * 365.25 * 1000:
|
|
return "%.2f centuries" % (seconds / (3600*24*365.25 * 100))
|
|
if seconds < 3600 * 24 * 365.25 * 1000000:
|
|
return "%.2f millenia" % (seconds / (3600*24*365.25 * 100))
|
|
return "like, forever, dude"
|
|
|
|
def StringToUnits(s):
|
|
try:
|
|
return long(Decimal(s)*long(coinspecs.atomic_units))
|
|
except Exception,e:
|
|
log_error('Failed to convert %s to units: %s' % (s,str(e)))
|
|
raise
|
|
|
|
def SendJSONRPCCommand(host,port,method,params):
|
|
try:
|
|
http = httplib.HTTPConnection(host,port,timeout=20)
|
|
except Exception,e:
|
|
log_error('SendJSONRPCCommand: Error connecting to %s:%u: %s' % (host, port, str(e)))
|
|
raise
|
|
d = dict(id="0",jsonrpc="2.0",method=method,params=params)
|
|
try:
|
|
j = json.dumps(d).encode()
|
|
except Exception,e:
|
|
log_error('SendJSONRPCCommand: Failed to encode JSON: %s' % str(e))
|
|
http.close()
|
|
raise
|
|
log_log('SendJSONRPCCommand: Sending json as body: %s' % j)
|
|
headers = None
|
|
try:
|
|
http.request("POST","/json_rpc",body=j)
|
|
except Exception,e:
|
|
log_error('SendJSONRPCCommand: Failed to post request: %s' % str(e))
|
|
http.close()
|
|
raise
|
|
response = http.getresponse()
|
|
if response.status != 200:
|
|
log_error('SendJSONRPCCommand: Error, received reply status %s' % str(response.status))
|
|
http.close()
|
|
raise RuntimeError("Error "+response.status)
|
|
s = response.read()
|
|
log_log('SendJSONRPCCommand: Received reply status %s: %s' % (response.status, str(s).replace('\r\n',' ').replace('\n',' ')))
|
|
try:
|
|
j = json.loads(s)
|
|
except Exception,e:
|
|
log_error('SendJSONRPCCommand: Failed to decode JSON: %s' % str(e))
|
|
http.close()
|
|
raise
|
|
http.close()
|
|
return j
|
|
|
|
def SendHTMLCommand(host,port,method):
|
|
try:
|
|
http = httplib.HTTPConnection(host,port,timeout=20)
|
|
except Exception,e:
|
|
log_error('SendHTMLCommand: Error connecting to %s:%u: %s' % (host, port, str(e)))
|
|
raise
|
|
headers = None
|
|
try:
|
|
http.request("POST","/"+method)
|
|
except Exception,e:
|
|
log_error('SendHTMLCommand: Failed to post request: %s' % str(e))
|
|
http.close()
|
|
raise
|
|
response = http.getresponse()
|
|
if response.status != 200:
|
|
log_error('SendHTMLCommand: Error, received reply status %s' % str(response.status))
|
|
http.close()
|
|
raise RuntimeError("Error "+response.status)
|
|
s = response.read()
|
|
log_log('SendHTMLCommand: Received reply status %s: %s' % (response.status,s.replace('\r\n',' ').replace('\n',' ')))
|
|
try:
|
|
j = json.loads(s)
|
|
except Exception,e:
|
|
log_error('SendHTMLCommand: Failed to decode JSON: %s' % str(e))
|
|
http.close()
|
|
raise
|
|
http.close()
|
|
return j
|
|
|
|
def SendWalletJSONRPCCommand(method,params):
|
|
return SendJSONRPCCommand(config.wallet_host,config.wallet_port,method,params)
|
|
|
|
def SendDaemonJSONRPCCommand(method,params):
|
|
return SendJSONRPCCommand(config.daemon_host,config.daemon_port,method,params)
|
|
|
|
def SendDaemonHTMLCommand(method):
|
|
return SendHTMLCommand(config.daemon_host,config.daemon_port,method)
|
|
|
|
def RetrieveTipbotBalance(force_refresh=False):
|
|
global cached_tipbot_balance, cached_tipbot_unlocked_balance, cached_tipbot_balance_timestamp
|
|
if not force_refresh and cached_tipbot_balance_timestamp and time.time()-cached_tipbot_balance_timestamp < config.tipbot_balance_cache_time:
|
|
return cached_tipbot_balance, cached_tipbot_unlocked_balance
|
|
|
|
j = SendWalletJSONRPCCommand("getbalance",None)
|
|
if not "result" in j:
|
|
log_error('RetrieveTipbotBalance: result not found in reply')
|
|
raise RuntimeError("")
|
|
return
|
|
result = j["result"]
|
|
if not "balance" in result:
|
|
log_error('RetrieveTipbotBalance: balance not found in result')
|
|
raise RuntimeError("")
|
|
return
|
|
if not "unlocked_balance" in result:
|
|
log_error('RetrieveTipbotBalance: unlocked_balance not found in result')
|
|
raise RuntimeError("")
|
|
return
|
|
balance = result["balance"]
|
|
unlocked_balance = result["unlocked_balance"]
|
|
log_log('RetrieveTipbotBalance: balance: %s' % str(balance))
|
|
log_log('RetrieveTipbotBalance: unlocked_balance: %s' % str(unlocked_balance))
|
|
pending = long(balance)-long(unlocked_balance)
|
|
if pending < 0:
|
|
log_error('RetrieveTipbotBalance: Negative pending balance! balance %s, unlocked %s' % (str(balance),str(unlocked_balance)))
|
|
raise RuntimeError("")
|
|
return
|
|
cached_tipbot_balance_timestamp=time.time()
|
|
cached_tipbot_balance=balance
|
|
cached_tipbot_unlocked_balance=unlocked_balance
|
|
return balance, unlocked_balance
|
|
|
|
def GetAccount(link_or_identity):
|
|
if isinstance(link_or_identity,Link):
|
|
identity=link_or_identity.identity()
|
|
else:
|
|
identity=link_or_identity
|
|
account = redis_hget('accounts',identity)
|
|
if account == None:
|
|
log_info('No account found for %s, creating new one' % identity)
|
|
next_account_id = long(redis_get('next_account_id') or 0)
|
|
account = next_account_id
|
|
if redis_hexists('accounts',account):
|
|
raise RuntimeError('Next account ID already exists (%d)', account)
|
|
redis_hset('accounts',identity,account)
|
|
next_account_id += 1
|
|
redis_set('next_account_id',next_account_id)
|
|
return account
|
|
|
|
def RetrieveBalance(link):
|
|
try:
|
|
account = GetAccount(link)
|
|
balance = redis_hget("balances",account) or 0
|
|
confirming = redis_hget("confirming_payments",account) or 0
|
|
return long(balance), long(confirming)
|
|
except Exception, e:
|
|
log_error('RetrieveBalance: exception: %s' % str(e))
|
|
raise
|
|
|
|
def LinkCore(link,other_identity):
|
|
try:
|
|
identity=link.identity()
|
|
if identity==other_identity:
|
|
return True, "same-identity"
|
|
links=redis_hget('links',identity)
|
|
if links:
|
|
if other_identity in links.split(chr(0)):
|
|
return True, "already"
|
|
links=links+chr(0)+other_identity
|
|
else:
|
|
links=other_identity
|
|
redis_hset('links',identity,links)
|
|
|
|
links=redis_hget('links',other_identity)
|
|
if links:
|
|
if identity in links.split(chr(0)):
|
|
# we have both
|
|
account=GetAccount(identity)
|
|
other_account=GetAccount(other_identity)
|
|
if account==other_account:
|
|
log_info('%s and %s already have the same account: %s' % (identity,other_identity,account))
|
|
return True, "same-account"
|
|
|
|
balance=long(redis_hget('balances',account))
|
|
log_info('Linking accounts %s (%s) and %s (%s)' % (account,identity,other_account,other_identity))
|
|
p=redis_pipeline()
|
|
p.hincrby('balances',other_account,balance)
|
|
p.hincrby('balances',account,-balance)
|
|
accounts=redis_hgetall('accounts')
|
|
for a in accounts:
|
|
if accounts[a]==account:
|
|
log_info('Changing %s\'s account from %s to %s' % (a,account,other_account))
|
|
p.hset('accounts',a,other_account)
|
|
p.execute()
|
|
return True, "linked"
|
|
except Exception,e:
|
|
log_error('Error linking %s and %s: %s' % (identity,other_identity,str(e)))
|
|
return False, "error"
|
|
|
|
return True, "ok"
|
|
|
|
def IdentityFromString(link,s):
|
|
if s.find(':') == -1:
|
|
network = link.network
|
|
nick=s
|
|
else:
|
|
parts=s.split(':')
|
|
network_name=parts[0]
|
|
network=GetNetworkByName(network_name)
|
|
if not network:
|
|
log_error('unknown network: %s' % network_name)
|
|
raise RuntimeError('Unknown network: %s' % network_name)
|
|
nick=parts[1]
|
|
return network.name+':'+network.canonicalize(nick)
|
|
|
|
def NetworkFromIdentity(identity):
|
|
return identity.split(':')[0]
|
|
|
|
def NickFromIdentity(identity):
|
|
return identity.split(':')[1]
|
|
|
|
def RegisterNetwork(name,type):
|
|
registered_networks[name]=type
|
|
|
|
def AddNetwork(network):
|
|
networks.append(network)
|
|
|
|
def GetNetworkByName(name):
|
|
for network in networks:
|
|
if network.name==name:
|
|
return network
|
|
return None
|
|
|
|
def GetNetworkByType(type):
|
|
for network in networks:
|
|
if isinstance(network,type):
|
|
return network
|
|
return None
|
|
|
|
def Lock():
|
|
return core_lock.acquire()
|
|
|
|
def Unlock():
|
|
core_lock.release()
|
|
return True
|
|
|