#!/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 SetPaymentID(link,pid): try: redis_hset("paymentid",pid,link.identity()) except Exception,e: log_error('GetPaymentID: failed to set payment ID for %s to redis: %s' % (link.identity(),str(e))) return 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=config.rpc_timeout) 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=config.rpc_timeout) 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