From fa581fac0078a46c2b50969f6e7286beb203bf55 Mon Sep 17 00:00:00 2001 From: moneromooo Date: Sun, 8 Mar 2015 17:23:23 +0000 Subject: [PATCH] First version, adapted from my CN tipbot --- README | 98 ++++++++++++++++++ monero/__init__.py | 0 monero/log.py | 48 +++++++++ monero/monero.py | 63 ++++++++++++ monero/payment.py | 237 +++++++++++++++++++++++++++++++++++++++++++ monero/redisdb.py | 153 ++++++++++++++++++++++++++++ monero/utils.py | 243 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 842 insertions(+) create mode 100644 README create mode 100644 monero/__init__.py create mode 100644 monero/log.py create mode 100644 monero/monero.py create mode 100644 monero/payment.py create mode 100644 monero/redisdb.py create mode 100644 monero/utils.py diff --git a/README b/README new file mode 100644 index 0000000..1d5553f --- /dev/null +++ b/README @@ -0,0 +1,98 @@ +monero-python is a Python wrapper around the Monero RPC API. +It allows receiving and sending Monero using a Python API. +Licensed as BSD. + +Requirements: + - Monero + - Python 2.x + - Redis + +The API consists of a single class, Monero, with a number of methods. +Calling raw module functions is undocumented and subject to change. + + * Initialization: + + monero=Monero(daemon_host,daemon_port,wallet_host,wallet_port,redis_host,redis_port) + + * Checking the current balance: + + full_balance,unlocked_balance = monero.GetWalletBalance() + + The full balance is the total amount (in atomic units, of which a + monero contains 1e12, or 1000000000000) in the wallet. The unlocked + balance is the portion of it that may be used now. Part of the + balance may be locked if it was just mined or received (including + as part of change to an outgoing transaction). Those locked funds + will become unlocked as new blocks are found by the network. + + * Create a new payment ID for a recipient: + + payment_id=monero.GetPaymentID(recipient) + + recipient is a string representing a user, destination, etc. The + semantics of it are left to the API user. Numbers can be used if + they're passed as strings. CheckForDeposits and other such functions + will return that recipient when the associated payment ID is found + in a transaction. + Payment IDs are deterministic, so asking for a payment ID for a + given recipient will always yield the same payment ID. + + * Get the recipient corresponding to a payment ID: + + recipient=monero.GetRecipient(payment_id) + + Returns the recipient associated with the given payment ID, or None + if there is none. If there is one, it is the string that was passed + to a previous call to GetPaymentID. + + * Check for new deposits: + + payments=monero.CheckForDeposits() + + payments will be either None, in case of an error, or an array + containing payment information. This array may be empty if no new + transactions were found. Deposits are checked for all payment IDs + that were created with GetPaymentID. + Each member of the array has the following fields: + tx_hash (string): the transaction hash for that transaction + amount (long): the amount of atomic units received + confirmed (boolean): whether that transaction has enough confirmations + confirmations (int): number of blocks received on top of the block with this transaction + payment_id (string): the payment id associated with that transaction, None if none + recipient (string): the recipient corresponding to that payment id, or None + + * Get full transaction history: + + payments=monero.GetDepositHistory(paymentid=[]) + + Returns the same data format as CheckForDeposits, but for all time, + rather that just new ones. This retrieves all deposits matching the + given filters. + A payment ID of None will return history for all the payment IDs created + by GetPaymentID. [] will return all history, regardless of payment ID. + A payment ID string will return history for this particular payment ID. + A list of payment IDs will return history for all these payment IDs. + Since a list is returned, it is easy to perform extra steps with the list, + such as accumulating amounts, eg: + total = sum(tx['amount'] for tx in payments) + + + * Send monero to an address: + + tx_hash=monero.Send(address,amount,paymentid=None,mixin=None) + + Sends some monero to the address. The amount is in atomic units. + The payment ID and mixin are optional. It is recommended to use + a mixin equal to 3 or higher. The network will take a fee for this + transaction, so you will be unable to send the full balance. At + the time of writing, the network fee is 0.01 monero per kilobyte. + + * Send monero to several addresses at once: + + tx_hash=monero.SendMany(address,amount,paymentid=None,mixin=None) + + Sends some monero to a number of addresses. The recipient parameter + is a dict containing the amounts (in atomic units) as a value of + the address in the key (ie, {'4....': 823900000000}). + Other comments from Send also apply. + diff --git a/monero/__init__.py b/monero/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/monero/log.py b/monero/log.py new file mode 100644 index 0000000..258c0a9 --- /dev/null +++ b/monero/log.py @@ -0,0 +1,48 @@ +#!/bin/python +# +# oython-monero - logging +# Copyright 2014 moneromooo +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +import time + +def log(stype,msg): + header = "%s\t%s\t" % (time.ctime(time.time()),stype) + print "%s%s" % (header, str(msg).replace("\n","\n"+header)) + +def log_error(msg): + log("ERROR",msg) + +def log_warn(msg): + log("WARNING",msg) + +def log_info(msg): + log("INFO",msg) + +def log_log(msg): + log("LOG",msg) + diff --git a/monero/monero.py b/monero/monero.py new file mode 100644 index 0000000..9997d3f --- /dev/null +++ b/monero/monero.py @@ -0,0 +1,63 @@ +#!/bin/python +# +# python-monero +# Copyright 2015 moneromooo +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +import utils +import redisdb +import payment + +class Monero: + def __init__(self,daemon_host,daemon_port,wallet_host,wallet_port,redis_host,redis_port): + redisdb.connect_to_redis(redis_host,redis_port) + self.daemon_host=daemon_host + self.daemon_port=daemon_port + self.wallet_host=wallet_host + self.wallet_port=wallet_port + + def GetWalletBalance(self): + return utils.RetrieveWalletBalance(self.wallet_host,self.wallet_port) + + def GetPaymentID(self,recipient): + return utils.GetPaymentIDFromUserID(recipient) + + def GetRecipient(self,payment_id): + return utils.GetUserIDFromPaymentID(payment_id) + + def CheckForDeposits(self,confirmations=6): + return payment.CheckForDeposits(self.daemon_host,self.daemon_port,self.wallet_host,self.wallet_port,confirmations) + + def GetDepositHistory(self,paymentid=[]): + return payment.GetDepositHistory(self.daemon_host,self.daemon_port,self.wallet_host,self.wallet_port,paymentid) + + def Send(self,address,amount,paymentid=None,mixin=3): + return payment.Send(self.wallet_host,self.wallet_port,address,amount,paymentid,mixin) + + def SendMany(self,recipients,paymentid=None,mixin=3): + return payment.SendMany(self.wallet_host,self.wallet_port,recipients,paymentid,mixin) + diff --git a/monero/payment.py b/monero/payment.py new file mode 100644 index 0000000..3c183e3 --- /dev/null +++ b/monero/payment.py @@ -0,0 +1,237 @@ +#!/bin/python +# +# python-monero - payment +# Copyright 2014,2015 moneromooo +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +import redis +import time +from log import log_error, log_warn, log_info, log_log +from utils import * +from redisdb import * + +def GetWalletAddress(wallet_host,wallet_port): + try: + j = SendJSONRPCCommand(wallet_host,wallet_port,"getaddress",None) + if not "result" in j: + log_error('GetWalletAddress: No result found in getaddress reply') + return None + result = j["result"] + if not "address" in result: + log_error('GetWalletAddress: No address found in getaddress reply') + return None + return result["address"] + except Exception,e: + log_error("GetWalletAddress: Error retrieving wallet address: %s" % (str(e))) + return None + +def UpdateCoin(daemon_host,daemon_port,wallet_host,wallet_port,full_history=False,paymentid=None,config_confirmations=6): + txs=[] + executed_txs=[] + + try: + try: + if full_history: + scan_block_height = 0 + else: + scan_block_height = redis_get("scan_block_height") + scan_block_height = long(scan_block_height) + except Exception,e: + log_error('Failed to get scan_block_height: %s' % str(e)) + return None + + try: + j = SendHTMLCommand(daemon_host,daemon_port,"getheight") + except Exception,e: + log_error('UpdateCoin: error getting height: %s' % str(e)) + return None + if not "height" in j: + log_error('UpdateCoin: error getting height: height not found in %s' % str(j)) + return None + try: + height=long(j["height"]) + except Exception,e: + log_error('UpdateCoin: error getting height: %s' % str(e)) + return None + + if paymentid != None: + if isinstance(paymentid,list): + payment_ids = paymentid + else: + payment_ids = [paymentid] + else: + full_payment_ids = redis_hgetall("paymentid") + #print 'Got full payment ids: %s' % str(full_payment_ids) + payment_ids = [] + for pid in full_payment_ids: + payment_ids.append(pid) + + #print 'Got payment ids: %s' % str(payment_ids) + params = { + "payment_ids": payment_ids, + "min_block_height": scan_block_height + } + j = SendJSONRPCCommand(wallet_host,wallet_port,"get_bulk_payments",params) + #print 'Got j: %s' % str(j) + if "result" in j: + result = j["result"] + cp = redis_pipeline() + if not full_history: + cp.delete('confirming_payments') + if "payments" in result: + payments = result["payments"] + new_payments = [] + n_confirming = 0 + new_scan_block_height = scan_block_height + for p in payments: + payment_id=p["payment_id"] + tx_hash = p["tx_hash"] + bh = p["block_height"] + ut = p["block_height"] + amount=p["amount"] + if not full_history and redis_sismember("processed_txs",tx_hash): + continue + log_log('UpdateCoin: Looking at payment %s' % str(p)) + confirmations = height-1-bh + confirmations_needed = max(config_confirmations,ut-height) + if confirmations >= confirmations_needed: + log_info('Payment %s is now confirmed' % str(p)) + new_payments.append(p) + if new_scan_block_height and bh > new_scan_block_height: + new_scan_block_height = bh + else: + log_info('Payment %s has %d/%d confirmations' % (str(p),confirmations,confirmations_needed)) + n_confirming += 1 + new_scan_block_height = None + try: + recipient = GetUserIDFromPaymentID(payment_id) + if not recipient: + raise RuntimeError('Payment ID %s not found' % payment_id) + log_info('UpdateCoin: Found payment %s to %s for %s' % (tx_hash,recipient, AmountToString(amount))) + if not full_history: + cp.hincrby('confirming_payments',recipient,amount) + txs.append({'tx_hash':tx_hash,'amount':amount,'confirmed':False,'confirmations':confirmations,'payment_id':payment_id,'recipient':recipient}) + except Exception,e: + log_error('UpdateCoin: No identity found for payment id %s, tx hash %s, amount %s: %s' % (payment_id, tx_hash, amount, str(e))) + payments=new_payments + log_info('UpdateCoin: Got %d mature payments and %d confirming payments' % (len(payments),n_confirming)) + if len(payments) > 0: + if not full_history and new_scan_block_height and new_scan_block_height > scan_block_height: + scan_block_height = new_scan_block_height + log_log('UpdateCoin: increasing scan_block_height to %d' % scan_block_height) + try: + pipe = redis_pipeline() + if not full_history: + pipe.set("scan_block_height", scan_block_height) + log_log('UpdateCoin: processing payments') + for p in payments: + payment_id=p["payment_id"] + tx_hash=p["tx_hash"] + amount=p["amount"] + bh = p["block_height"] + ut = p["unlock_time"] + confirmations = height-1-bh + try: + recipient = GetUserIDFromPaymentID(payment_id) + if not recipient: + raise RuntimeError('Payment ID %s not found' % payment_id) + log_info('UpdateCoin: Found payment %s to %s for %s' % (tx_hash,recipient, AmountToString(amount))) + if not full_history: + pipe.sadd("processed_txs",tx_hash) + txs.append({'tx_hash':tx_hash,'amount':amount,'confirmed':True,'confirmations':confirmations,'payment_id':payment_id,'recipient':recipient}) + except Exception,e: + log_error('UpdateCoin: No identity found for payment id %s, tx hash %s, amount %s: %s' % (payment_id, tx_hash, amount, str(e))) + log_log('UpdateCoin: Executing received payments pipeline') + if not full_history: + pipe.execute() + except Exception,e: + log_error('UpdateCoin: failed to set scan_block_height: %s' % str(e)) + if not full_history: + cp.execute() + executed_txs=txs + else: + log_error('UpdateCoin: No results in get_bulk_payments reply') + except Exception,e: + log_error('UpdateCoin: Failed to get bulk payments: %s' % str(e)) + return executed_txs + +def SendMany(wallet_host,wallet_port,recipients,paymentid=None,mixin=None): + for address in recipients: + if not IsValidAddress(address): + log_error("Invalid address: %s" % address) + return + amount = recipients[address] + if amount <= 0: + log_error("Invalid amount: %s" % str(amount)) + return + + if paymentid != None: + if not IsValidPaymentID(paymentid): + log_error("Invalid payment ID") + return + + if mixin < 0: + log_error("Invalid mixin: %d") + return + + log_info("Pay: sending %s, payment id %s, mixin %d" % (str(recipients), str(paymentid),mixin)) + + try: + destinations=[] + for address in recipients: + destinations.append({'address':address,'amount':recipients[address]}) + params = { + 'destinations':destinations, + 'payment_id': paymentid, + 'fee': 0, + 'mixin': mixin, + 'unlock_time': 0, + } + j = SendJSONRPCCommand(wallet_host,wallet_port,"transfer",params) + except Exception,e: + log_error('Withdraw: Error in transfer: %s' % str(e)) + return + if not "result" in j: + log_error('Withdraw: No result in transfer reply') + return + result = j["result"] + if not "tx_hash" in result: + log_error('Withdraw: No tx_hash in transfer reply') + return + tx_hash = result["tx_hash"] + log_info('tx sent, tx_hash %s' % (str(tx_hash))) + return tx_hash + +def Send(wallet_host,wallet_port,address,amount,paymentid=None,mixin=None): + return SendMany(wallet_host,wallet_port,{address: amount},paymentid,mixin) + +def CheckForDeposits(daemon_host,daemon_port,wallet_host,wallet_port,confirmations): + return UpdateCoin(daemon_host,daemon_port,wallet_host,wallet_port,full_history=False,config_confirmations=confirmations) + +def GetDepositHistory(daemon_host,daemon_port,wallet_host,wallet_port,paymentid=[]): + return UpdateCoin(daemon_host,daemon_port,wallet_host,wallet_port,full_history=True,paymentid=paymentid) + diff --git a/monero/redisdb.py b/monero/redisdb.py new file mode 100644 index 0000000..cef62f4 --- /dev/null +++ b/monero/redisdb.py @@ -0,0 +1,153 @@ +#!/bin/python +# +# python-monero +# Copyright 2014,2015 moneromooo +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +import redis +from log import log_error, log_warn, log_info, log_log + +redisdb = None + +def connect_to_redis(host,port): + log_info('Connecting to Redis at %s:%u' % (host, port)) + try: + global redisdb + redisdb = redis.Redis(host=host,port=port) + return redisdb + except Exception, e: + log_error( 'Error initializing redis: %s' % str(e)) + exit() + +def redis_pipeline(): + return redisdb.pipeline() + +def redis_exists(k): + return redisdb.exists(k) + +def redis_get(k): + return redisdb.get(k) + +def redis_set(k,v): + return redisdb.set(k,v) + +def redis_hexists(t,k): + return redisdb.hexists(t,k) + +def redis_hget(t,k): + return redisdb.hget(t,k) + +def redis_hgetall(t): + return redisdb.hgetall(t) + +def redis_hset(t,k,v): + return redisdb.hset(t,k,v) + +def redis_hincrby(t,k,v): + return redisdb.hincrby(t,k,v) + +def redis_hdel(t,k): + return redisdb.hdel(t,k) + +def redis_incrby(k,v): + return redisdb.incrby(k,v) + +def redis_sadd(k,v): + return redisdb.sadd(k,v) + +def redis_smembers(k): + return redisdb.smembers(k) + +def redis_sismember(k,v): + return redisdb.sismember(k,v) + +def redis_rpush(k,v): + return redisdb.rpush(k,v) + +def redis_lpop(k): + return redisdb.lpop(k) + +def redis_llen(k): + return redisdb.llen(k) + +def redis_lindex(k,n): + return redisdb.lindex(k,n) + +def redis_lset(k,i,v): + return redisdb.lset(k,i,v) + +def redis_zincrby(t,k,s): + return redisdb.zincrby(t,k,s) + +def redis_zscore(t,k): + return redisdb.zscore(t,k) + +def redis_zrangebylex(t,x0,x1): + return redisdb.zrangebylex(t,x0,x1) + +def redis_keys(s): + return redisdb.keys(s) + +def redis_delete(k): + return redisdb.delete(k) + + +def CompatibilityCheck(): + try: + r = redis.Redis() + if not r.pipeline: raise RuntimeError('pipeline call not found') + p = r.pipeline() + if not p.exists: raise RuntimeError('exists call not found') + if not p.get: raise RuntimeError('get call not found') + if not p.set: raise RuntimeError('set call not found') + if not p.hexists: raise RuntimeError('hexists call not found') + if not p.hget: raise RuntimeError('hget call not found') + if not p.hgetall: raise RuntimeError('hgetall call not found') + if not p.hset: raise RuntimeError('hset call not found') + if not p.hincrby: raise RuntimeError('hincrby call not found') + if not p.hdel: raise RuntimeError('hdel call not found') + if not p.incrby: raise RuntimeError('incrby call not found') + if not p.sadd: raise RuntimeError('sadd call not found') + if not p.smembers: raise RuntimeError('smembers call not found') + if not p.sismember: raise RuntimeError('sismember call not found') + if not p.rpush: raise RuntimeError('rpush call not found') + if not p.lpop: raise RuntimeError('lpop call not found') + if not p.llen: raise RuntimeError('llen call not found') + if not p.lindex: raise RuntimeError('lindex call not found') + if not p.lset: raise RuntimeError('lset call not found') + if not p.zincrby: raise RuntimeError('zincrby call not found') + if not p.zscore: raise RuntimeError('zscore call not found') + if not p.zrangebylex: raise RuntimeError('zrangebylex call not found') + if not p.keys: raise RuntimeError('keys call not found') + if not p.execute: raise RuntimeError('execute call not found') + if not p.delete: raise RuntimeError('delete call not found') + except Exception,e: + log_error('Error checking redis compatibility: %s' % str(e)) + exit(1) + +CompatibilityCheck() + diff --git a/monero/utils.py b/monero/utils.py new file mode 100644 index 0000000..eac9d3b --- /dev/null +++ b/monero/utils.py @@ -0,0 +1,243 @@ +#!/bin/python +# +# python-monero - utility functions +# Copyright 2014,2015 moneromooo +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +import redis +import hashlib +import json +import httplib +import time +import string +from decimal import * +from log import log_error, log_warn, log_info, log_log +from redisdb import * + +address_length = [95, 95] # min/max size of addresses +address_prefix = ['4', '9'] # allowed prefixes of addresses + +cached_wallet_balance=None +cached_wallet_unlocked_balance=None +cached_wallet_balance_timestamp=None + +def GetPaymentIDFromUserID(user_id): + salt="nveuweaiirv-" + p = hashlib.sha256(salt+user_id).hexdigest() + try: + redis_hset("paymentid",p,link.identity()) + except Exception,e: + log_error('GetPaymentIDFromUserID: failed to set payment ID for %s to redis: %s' % (user_id,str(e))) + return p + +def GetUserIDFromPaymentID(paymentid): + if not redis_hexists("paymentid",paymentid): + log_log('PaymentID %s not found' % paymentid) + return None + identity = redis_hget("paymentid",paymentid) + log_log('PaymentID %s => %s' % (paymentid, str(identity))) + return identity + + +def IsValidAddress(address): + if len(address) < address_length[0] or len(address) > address_length[1]: + return False + for prefix in 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 + +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 RetrieveWalletBalance(wallet_host,wallet_port,force_refresh=False): + global cached_wallet_balance, cached_wallet_unlocked_balance, cached_wallet_balance_timestamp + if not force_refresh and cached_wallet_balance_timestamp and time.time()-cached_wallet_balance_timestamp < 35: + return cached_wallet_balance, cached_wallet_unlocked_balance + + j = SendJSONRPCCommand(wallet_host,wallet_port,"getbalance",None) + if not "result" in j: + log_error('RetrieveWalletBalance: result not found in reply') + raise RuntimeError("") + return + result = j["result"] + if not "balance" in result: + log_error('RetrieveWalletBalance: balance not found in result') + raise RuntimeError("") + return + if not "unlocked_balance" in result: + log_error('RetrieveWalletBalance: unlocked_balance not found in result') + raise RuntimeError("") + return + balance = result["balance"] + unlocked_balance = result["unlocked_balance"] + log_log('RetrieveWalletBalance: balance: %s' % str(balance)) + log_log('RetrieveWalletBalance: unlocked_balance: %s' % str(unlocked_balance)) + pending = long(balance)-long(unlocked_balance) + if pending < 0: + log_error('RetrieveWalletBalance: Negative pending balance! balance %s, unlocked %s' % (str(balance),str(unlocked_balance))) + raise RuntimeError("") + return + cached_wallet_balance_timestamp=time.time() + cached_wallet_balance=balance + cached_wallet_unlocked_balance=unlocked_balance + return balance, unlocked_balance + +# 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 = moneyfmt(Decimal(lamount)/Decimal(1e12),places=12) + " monero" + return samount +