PyBitmessage/src/api.py

1482 lines
60 KiB
Python
Raw Normal View History

# pylint: disable=too-many-locals,too-many-lines,no-self-use,too-many-public-methods,too-many-branches
# pylint: disable=too-many-statements
"""
src/api.py
==========
2016-05-01 08:34:04 +02:00
# Copyright (c) 2012-2016 Jonathan Warren
2019-02-05 15:16:30 +01:00
# Copyright (c) 2012-2019 The Bitmessage developers
2014-01-13 01:56:30 +01:00
This is not what you run to run the Bitmessage API. Instead, enable the API
( https://bitmessage.org/wiki/API ) and optionally enable daemon mode
( https://bitmessage.org/wiki/Daemon ) then run bitmessagemain.py.
2014-01-13 01:56:30 +01:00
"""
from __future__ import absolute_import
2017-06-03 02:53:13 +02:00
import base64
import errno
2018-04-08 17:28:08 +02:00
import hashlib
2014-01-13 01:56:30 +01:00
import json
import random # nosec
import socket
import subprocess
import threading
2018-04-08 17:28:08 +02:00
import time
2017-06-03 02:53:13 +02:00
from binascii import hexlify, unhexlify
2018-04-08 17:28:08 +02:00
from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
from struct import pack
2014-01-13 01:56:30 +01:00
from version import softwareVersion
import defaults
2014-01-13 01:56:30 +01:00
import helper_inbox
import helper_sent
import helper_threading
import network.stats
import proofofwork
import queues
import shared
import shutdown
import state
from addresses import addBMIfNotPresent, calculateInventoryHash, decodeAddress, decodeVarint, varintDecodeError
from bmconfigparser import BMConfigParser
2014-01-13 01:56:30 +01:00
from debug import logger
from helper_ackPayload import genAckPayload
from helper_sql import SqlBulkExecute, sqlExecute, sqlQuery, sqlStoredProcedure
from inventory import Inventory
2014-01-13 01:56:30 +01:00
str_chan = '[chan]'
class APIError(Exception):
"""APIError exception class"""
2014-01-13 01:56:30 +01:00
def __init__(self, error_number, error_message):
super(APIError, self).__init__()
self.error_number = error_number
self.error_message = error_message
2018-04-08 17:28:08 +02:00
2014-01-13 01:56:30 +01:00
def __str__(self):
return "API Error %04i: %s" % (self.error_number, self.error_message)
class StoppableXMLRPCServer(SimpleXMLRPCServer):
"""A SimpleXMLRPCServer that honours state.shutdown"""
allow_reuse_address = True
def serve_forever(self):
"""Start the SimpleXMLRPCServer"""
# pylint: disable=arguments-differ
while state.shutdown == 0:
self.handle_request()
# This thread, of which there is only one, runs the API.
class singleAPI(threading.Thread, helper_threading.StoppableThread):
2018-09-05 12:56:06 +02:00
"""API thread"""
def __init__(self):
threading.Thread.__init__(self, name="singleAPI")
self.initStop()
def stopThread(self):
super(singleAPI, self).stopThread()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((
BMConfigParser().get('bitmessagesettings', 'apiinterface'),
BMConfigParser().getint('bitmessagesettings', 'apiport')
))
s.shutdown(socket.SHUT_RDWR)
s.close()
except BaseException:
pass
def run(self):
port = BMConfigParser().getint('bitmessagesettings', 'apiport')
try:
2018-09-05 12:56:06 +02:00
getattr(errno, 'WSAEADDRINUSE')
except AttributeError:
errno.WSAEADDRINUSE = errno.EADDRINUSE
for attempt in range(50):
try:
if attempt > 0:
port = random.randint(32767, 65535)
se = StoppableXMLRPCServer(
(BMConfigParser().get(
'bitmessagesettings', 'apiinterface'),
port),
MySimpleXMLRPCRequestHandler, True, True)
except socket.error as e:
if e.errno in (errno.EADDRINUSE, errno.WSAEADDRINUSE):
continue
else:
if attempt > 0:
BMConfigParser().set(
"bitmessagesettings", "apiport", str(port))
BMConfigParser().save()
break
se.register_introspection_functions()
apiNotifyPath = BMConfigParser().safeGet(
'bitmessagesettings', 'apinotifypath')
if apiNotifyPath:
logger.info('Trying to call %s', apiNotifyPath)
try:
subprocess.call([apiNotifyPath, "startingUp"])
except OSError:
logger.warning(
'Failed to call %s, removing apinotifypath setting',
apiNotifyPath)
BMConfigParser().remove_option(
'bitmessagesettings', 'apinotifypath')
se.serve_forever()
2014-01-13 01:56:30 +01:00
class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
"""
This is one of several classes that constitute the API
This class was written by Vaibhav Bhatia. Modified by Jonathan Warren (Atheros).
http://code.activestate.com/recipes/501148-xmlrpc-serverclient-which-does-cookie-handling-and/
"""
2014-01-13 01:56:30 +01:00
def do_POST(self):
"""
Handles the HTTP POST request.
Attempts to interpret all HTTP POST requests as XML-RPC calls,
which are forwarded to the server's _dispatch method for handling.
2014-01-13 01:56:30 +01:00
Note: this method is the same as in SimpleXMLRPCRequestHandler,
just hacked to handle cookies
"""
2014-01-13 01:56:30 +01:00
# Check that the path is legal
if not self.is_rpc_path_valid():
self.report_404()
return
try:
# Get arguments by reading body of request.
# We read this in chunks to avoid straining
# socket.read(); around the 10 or 15Mb mark, some platforms
# begin to have problems (bug #792570).
max_chunk_size = 10 * 1024 * 1024
size_remaining = int(self.headers["content-length"])
L = []
while size_remaining:
chunk_size = min(size_remaining, max_chunk_size)
L.append(self.rfile.read(chunk_size))
size_remaining -= len(L[-1])
data = ''.join(L)
# In previous versions of SimpleXMLRPCServer, _dispatch
# could be overridden in this class, instead of in
# SimpleXMLRPCDispatcher. To maintain backwards compatibility,
# check to see if a subclass implements _dispatch and dispatch
# using that method if present.
response = self.server._marshaled_dispatch( # pylint: disable=protected-access
2014-01-13 01:56:30 +01:00
data, getattr(self, '_dispatch', None)
)
except BaseException: # This should only happen if the module is buggy
2014-01-13 01:56:30 +01:00
# internal error, report as HTTP server error
self.send_response(500)
self.end_headers()
else:
# got a valid XML RPC response
self.send_response(200)
self.send_header("Content-type", "text/xml")
self.send_header("Content-length", str(len(response)))
# HACK :start -> sends cookies here
if self.cookies:
for cookie in self.cookies:
self.send_header('Set-Cookie', cookie.output(header=''))
# HACK :end
self.end_headers()
self.wfile.write(response)
# shut down the connection
self.wfile.flush()
self.connection.shutdown(1)
def APIAuthenticateClient(self):
"""Predicate to check for valid API credentials in the request header"""
2014-01-13 01:56:30 +01:00
if 'Authorization' in self.headers:
# handle Basic authentication
_, encstr = self.headers.get('Authorization').split()
2018-04-08 17:28:08 +02:00
emailid, password = encstr.decode('base64').split(':')
return (
emailid == BMConfigParser().get('bitmessagesettings', 'apiusername') and
password == BMConfigParser().get('bitmessagesettings', 'apipassword')
2018-04-08 17:28:08 +02:00
)
2014-01-13 01:56:30 +01:00
else:
2018-04-08 17:28:08 +02:00
logger.warning(
'Authentication failed because header lacks'
' Authentication field')
2014-01-13 01:56:30 +01:00
time.sleep(2)
return False
def _decode(self, text, decode_type):
try:
2017-06-03 02:53:13 +02:00
if decode_type == 'hex':
return unhexlify(text)
elif decode_type == 'base64':
return base64.b64decode(text)
2014-01-13 01:56:30 +01:00
except Exception as e:
2018-04-08 17:28:08 +02:00
raise APIError(
22, "Decode error - %s. Had trouble while decoding string: %r"
% (e, text)
)
return None
2014-01-13 01:56:30 +01:00
def _verifyAddress(self, address):
2018-04-08 17:28:08 +02:00
status, addressVersionNumber, streamNumber, ripe = \
decodeAddress(address)
2014-01-13 01:56:30 +01:00
if status != 'success':
2018-04-08 17:28:08 +02:00
logger.warning(
'API Error 0007: Could not decode address %s. Status: %s.',
address, status
)
2014-01-13 01:56:30 +01:00
if status == 'checksumfailed':
raise APIError(8, 'Checksum failed for address: ' + address)
if status == 'invalidcharacters':
raise APIError(9, 'Invalid characters in address: ' + address)
if status == 'versiontoohigh':
raise APIError(10, 'Address version number too high (or zero) in address: ' + address)
2014-08-27 09:14:32 +02:00
if status == 'varintmalformed':
raise APIError(26, 'Malformed varint in address: ' + address)
raise APIError(7, 'Could not decode address: %s : %s' % (address, status))
2014-01-13 01:56:30 +01:00
if addressVersionNumber < 2 or addressVersionNumber > 4:
2018-04-08 17:28:08 +02:00
raise APIError(
11, 'The address version number currently must be 2, 3 or 4.'
' Others aren\'t supported. Check the address.'
)
2014-01-13 01:56:30 +01:00
if streamNumber != 1:
2018-04-08 17:28:08 +02:00
raise APIError(
12, 'The stream number must be 1. Others aren\'t supported.'
' Check the address.'
)
2014-01-13 01:56:30 +01:00
return (status, addressVersionNumber, streamNumber, ripe)
2018-04-08 17:28:08 +02:00
# Request Handlers
def HandleListAddresses(self, method):
"""Handle a request to list addresses"""
data = '{"addresses":['
for addressInKeysFile in BMConfigParser().addresses():
status, addressVersionNumber, streamNumber, hash01 = decodeAddress( # pylint: disable=unused-variable
addressInKeysFile)
if len(data) > 20:
data += ','
if BMConfigParser().has_option(addressInKeysFile, 'chan'):
chan = BMConfigParser().getboolean(addressInKeysFile, 'chan')
else:
chan = False
label = BMConfigParser().get(addressInKeysFile, 'label')
if method == 'listAddresses2':
2017-06-03 02:53:13 +02:00
label = base64.b64encode(label)
2018-04-08 17:28:08 +02:00
data += json.dumps({
'label': label,
'address': addressInKeysFile,
'stream': streamNumber,
'enabled':
BMConfigParser().getboolean(addressInKeysFile, 'enabled'),
'chan': chan
}, indent=4, separators=(',', ': '))
data += ']}'
return data
def HandleListAddressBookEntries(self, params):
"""Handle a request to list address book entries"""
if len(params) == 1:
label, = params
label = self._decode(label, "base64")
2018-04-08 17:28:08 +02:00
queryreturn = sqlQuery(
"SELECT label, address from addressbook WHERE label = ?",
label)
elif len(params) > 1:
raise APIError(0, "Too many paremeters, max 1")
else:
2018-04-08 17:28:08 +02:00
queryreturn = sqlQuery("SELECT label, address from addressbook")
data = '{"addresses":['
for row in queryreturn:
label, address = row
label = shared.fixPotentiallyInvalidUTF8Data(label)
if len(data) > 20:
data += ','
2018-04-08 17:28:08 +02:00
data += json.dumps({
'label': base64.b64encode(label),
'address': address}, indent=4, separators=(',', ': '))
data += ']}'
return data
def HandleAddAddressBookEntry(self, params):
"""Handle a request to add an address book entry"""
if len(params) != 2:
raise APIError(0, "I need label and address")
address, label = params
label = self._decode(label, "base64")
address = addBMIfNotPresent(address)
self._verifyAddress(address)
2018-04-08 17:28:08 +02:00
queryreturn = sqlQuery(
"SELECT address FROM addressbook WHERE address=?", address)
if queryreturn != []:
2018-04-08 17:28:08 +02:00
raise APIError(
16, 'You already have this address in your address book.')
sqlExecute("INSERT INTO addressbook VALUES(?,?)", label, address)
2018-04-08 17:28:08 +02:00
queues.UISignalQueue.put(('rerenderMessagelistFromLabels', ''))
queues.UISignalQueue.put(('rerenderMessagelistToLabels', ''))
queues.UISignalQueue.put(('rerenderAddressBook', ''))
return "Added address %s to address book" % address
def HandleDeleteAddressBookEntry(self, params):
"""Handle a request to delete an address book entry"""
if len(params) != 1:
raise APIError(0, "I need an address")
address, = params
address = addBMIfNotPresent(address)
self._verifyAddress(address)
sqlExecute('DELETE FROM addressbook WHERE address=?', address)
2018-04-08 17:28:08 +02:00
queues.UISignalQueue.put(('rerenderMessagelistFromLabels', ''))
queues.UISignalQueue.put(('rerenderMessagelistToLabels', ''))
queues.UISignalQueue.put(('rerenderAddressBook', ''))
return "Deleted address book entry for %s if it existed" % address
def HandleCreateRandomAddress(self, params):
"""Handle a request to create a random address"""
if not params:
raise APIError(0, 'I need parameters!')
elif len(params) == 1:
label, = params
eighteenByteRipe = False
nonceTrialsPerByte = BMConfigParser().get(
'bitmessagesettings', 'defaultnoncetrialsperbyte')
payloadLengthExtraBytes = BMConfigParser().get(
'bitmessagesettings', 'defaultpayloadlengthextrabytes')
elif len(params) == 2:
label, eighteenByteRipe = params
nonceTrialsPerByte = BMConfigParser().get(
'bitmessagesettings', 'defaultnoncetrialsperbyte')
payloadLengthExtraBytes = BMConfigParser().get(
'bitmessagesettings', 'defaultpayloadlengthextrabytes')
elif len(params) == 3:
label, eighteenByteRipe, totalDifficulty = params
nonceTrialsPerByte = int(
defaults.networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty)
payloadLengthExtraBytes = BMConfigParser().get(
'bitmessagesettings', 'defaultpayloadlengthextrabytes')
elif len(params) == 4:
2018-04-08 17:28:08 +02:00
label, eighteenByteRipe, totalDifficulty, \
smallMessageDifficulty = params
nonceTrialsPerByte = int(
defaults.networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty)
payloadLengthExtraBytes = int(
defaults.networkDefaultPayloadLengthExtraBytes * smallMessageDifficulty)
else:
raise APIError(0, 'Too many parameters!')
label = self._decode(label, "base64")
try:
unicode(label, 'utf-8')
except BaseException:
raise APIError(17, 'Label is not valid UTF-8 data.')
queues.apiAddressGeneratorReturnQueue.queue.clear()
streamNumberForAddress = 1
queues.addressGeneratorQueue.put((
2018-04-08 17:28:08 +02:00
'createRandomAddress', 4, streamNumberForAddress, label, 1, "",
eighteenByteRipe, nonceTrialsPerByte, payloadLengthExtraBytes
))
return queues.apiAddressGeneratorReturnQueue.get()
def HandleCreateDeterministicAddresses(self, params):
"""Handle a request to create a deterministic address"""
if not params:
raise APIError(0, 'I need parameters!')
elif len(params) == 1:
passphrase, = params
2014-01-13 01:56:30 +01:00
numberOfAddresses = 1
addressVersionNumber = 0
streamNumber = 0
eighteenByteRipe = False
nonceTrialsPerByte = BMConfigParser().get(
'bitmessagesettings', 'defaultnoncetrialsperbyte')
payloadLengthExtraBytes = BMConfigParser().get(
'bitmessagesettings', 'defaultpayloadlengthextrabytes')
elif len(params) == 2:
passphrase, numberOfAddresses = params
addressVersionNumber = 0
streamNumber = 0
eighteenByteRipe = False
nonceTrialsPerByte = BMConfigParser().get(
'bitmessagesettings', 'defaultnoncetrialsperbyte')
payloadLengthExtraBytes = BMConfigParser().get(
'bitmessagesettings', 'defaultpayloadlengthextrabytes')
elif len(params) == 3:
passphrase, numberOfAddresses, addressVersionNumber = params
streamNumber = 0
eighteenByteRipe = False
nonceTrialsPerByte = BMConfigParser().get(
'bitmessagesettings', 'defaultnoncetrialsperbyte')
payloadLengthExtraBytes = BMConfigParser().get(
'bitmessagesettings', 'defaultpayloadlengthextrabytes')
elif len(params) == 4:
2018-04-08 17:28:08 +02:00
passphrase, numberOfAddresses, addressVersionNumber, \
streamNumber = params
eighteenByteRipe = False
nonceTrialsPerByte = BMConfigParser().get(
'bitmessagesettings', 'defaultnoncetrialsperbyte')
payloadLengthExtraBytes = BMConfigParser().get(
'bitmessagesettings', 'defaultpayloadlengthextrabytes')
elif len(params) == 5:
2018-04-08 17:28:08 +02:00
passphrase, numberOfAddresses, addressVersionNumber, \
streamNumber, eighteenByteRipe = params
nonceTrialsPerByte = BMConfigParser().get(
'bitmessagesettings', 'defaultnoncetrialsperbyte')
payloadLengthExtraBytes = BMConfigParser().get(
'bitmessagesettings', 'defaultpayloadlengthextrabytes')
elif len(params) == 6:
2018-04-08 17:28:08 +02:00
passphrase, numberOfAddresses, addressVersionNumber, \
streamNumber, eighteenByteRipe, totalDifficulty = params
nonceTrialsPerByte = int(
defaults.networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty)
payloadLengthExtraBytes = BMConfigParser().get(
'bitmessagesettings', 'defaultpayloadlengthextrabytes')
elif len(params) == 7:
2018-04-08 17:28:08 +02:00
passphrase, numberOfAddresses, addressVersionNumber, \
streamNumber, eighteenByteRipe, totalDifficulty, \
smallMessageDifficulty = params
nonceTrialsPerByte = int(
defaults.networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty)
payloadLengthExtraBytes = int(
defaults.networkDefaultPayloadLengthExtraBytes * smallMessageDifficulty)
else:
raise APIError(0, 'Too many parameters!')
if not passphrase:
raise APIError(1, 'The specified passphrase is blank.')
if not isinstance(eighteenByteRipe, bool):
2018-04-08 17:28:08 +02:00
raise APIError(
23, 'Bool expected in eighteenByteRipe, saw %s instead' %
type(eighteenByteRipe))
passphrase = self._decode(passphrase, "base64")
2018-04-08 17:28:08 +02:00
# 0 means "just use the proper addressVersionNumber"
if addressVersionNumber == 0:
2014-01-13 01:56:30 +01:00
addressVersionNumber = 4
if addressVersionNumber != 3 and addressVersionNumber != 4:
2018-04-08 17:28:08 +02:00
raise APIError(
2, 'The address version number currently must be 3, 4, or 0'
' (which means auto-select). %i isn\'t supported.' %
addressVersionNumber)
if streamNumber == 0: # 0 means "just use the most available stream"
2014-01-13 01:56:30 +01:00
streamNumber = 1
if streamNumber != 1:
2018-04-08 17:28:08 +02:00
raise APIError(
3, 'The stream number must be 1 (or 0 which means'
' auto-select). Others aren\'t supported.')
if numberOfAddresses == 0:
2018-04-08 17:28:08 +02:00
raise APIError(
4, 'Why would you ask me to generate 0 addresses for you?')
if numberOfAddresses > 999:
2018-04-08 17:28:08 +02:00
raise APIError(
5, 'You have (accidentally?) specified too many addresses to'
' make. Maximum 999. This check only exists to prevent'
' mischief; if you really want to create more addresses than'
' this, contact the Bitmessage developers and we can modify'
' the check or you can do it yourself by searching the source'
' code for this message.')
queues.apiAddressGeneratorReturnQueue.queue.clear()
2018-04-08 17:28:08 +02:00
logger.debug(
'Requesting that the addressGenerator create %s addresses.',
numberOfAddresses)
queues.addressGeneratorQueue.put((
'createDeterministicAddresses', addressVersionNumber, streamNumber,
'unused API address', numberOfAddresses, passphrase,
eighteenByteRipe, nonceTrialsPerByte, payloadLengthExtraBytes
))
data = '{"addresses":['
queueReturn = queues.apiAddressGeneratorReturnQueue.get()
for item in queueReturn:
if len(data) > 20:
data += ','
data += "\"" + item + "\""
data += ']}'
return data
def HandleGetDeterministicAddress(self, params):
"""Handle a request to get a deterministic address"""
if len(params) != 3:
raise APIError(0, 'I need exactly 3 parameters.')
passphrase, addressVersionNumber, streamNumber = params
numberOfAddresses = 1
eighteenByteRipe = False
if not passphrase:
raise APIError(1, 'The specified passphrase is blank.')
passphrase = self._decode(passphrase, "base64")
if addressVersionNumber != 3 and addressVersionNumber != 4:
2018-04-08 17:28:08 +02:00
raise APIError(
2, 'The address version number currently must be 3 or 4. %i'
' isn\'t supported.' % addressVersionNumber)
if streamNumber != 1:
2018-04-08 17:28:08 +02:00
raise APIError(
3, ' The stream number must be 1. Others aren\'t supported.')
queues.apiAddressGeneratorReturnQueue.queue.clear()
2018-04-08 17:28:08 +02:00
logger.debug(
'Requesting that the addressGenerator create %s addresses.',
numberOfAddresses)
queues.addressGeneratorQueue.put((
'getDeterministicAddress', addressVersionNumber, streamNumber,
'unused API address', numberOfAddresses, passphrase,
eighteenByteRipe
))
return queues.apiAddressGeneratorReturnQueue.get()
def HandleCreateChan(self, params):
"""Handle a request to create a chan"""
if not params:
raise APIError(0, 'I need parameters.')
elif len(params) == 1:
passphrase, = params
passphrase = self._decode(passphrase, "base64")
if not passphrase:
raise APIError(1, 'The specified passphrase is blank.')
# It would be nice to make the label the passphrase but it is
# possible that the passphrase contains non-utf-8 characters.
try:
unicode(passphrase, 'utf-8')
label = str_chan + ' ' + passphrase
except BaseException:
label = str_chan + ' ' + repr(passphrase)
addressVersionNumber = 4
streamNumber = 1
queues.apiAddressGeneratorReturnQueue.queue.clear()
2018-04-08 17:28:08 +02:00
logger.debug(
'Requesting that the addressGenerator create chan %s.', passphrase)
queues.addressGeneratorQueue.put((
'createChan', addressVersionNumber, streamNumber, label,
passphrase, True
))
queueReturn = queues.apiAddressGeneratorReturnQueue.get()
if not queueReturn:
raise APIError(24, 'Chan address is already present.')
address = queueReturn[0]
return address
def HandleJoinChan(self, params):
"""Handle a request to join a chan"""
if len(params) < 2:
raise APIError(0, 'I need two parameters.')
elif len(params) == 2:
2018-04-08 17:28:08 +02:00
passphrase, suppliedAddress = params
passphrase = self._decode(passphrase, "base64")
if not passphrase:
raise APIError(1, 'The specified passphrase is blank.')
# It would be nice to make the label the passphrase but it is
# possible that the passphrase contains non-utf-8 characters.
try:
unicode(passphrase, 'utf-8')
label = str_chan + ' ' + passphrase
except BaseException:
label = str_chan + ' ' + repr(passphrase)
status, addressVersionNumber, streamNumber, toRipe = self._verifyAddress( # pylint: disable=unused-variable
suppliedAddress)
suppliedAddress = addBMIfNotPresent(suppliedAddress)
queues.apiAddressGeneratorReturnQueue.queue.clear()
2018-04-08 17:28:08 +02:00
queues.addressGeneratorQueue.put((
'joinChan', suppliedAddress, label, passphrase, True
))
addressGeneratorReturnValue = \
queues.apiAddressGeneratorReturnQueue.get()
2018-04-08 17:28:08 +02:00
if addressGeneratorReturnValue[0] == \
'chan name does not match address':
raise APIError(18, 'Chan name does not match address.')
if not addressGeneratorReturnValue:
raise APIError(24, 'Chan address is already present.')
return "success"
def HandleLeaveChan(self, params):
"""Handle a request to leave a chan"""
if not params:
raise APIError(0, 'I need parameters.')
elif len(params) == 1:
address, = params
# pylint: disable=unused-variable
status, addressVersionNumber, streamNumber, toRipe = self._verifyAddress(address)
address = addBMIfNotPresent(address)
if not BMConfigParser().has_section(address):
2018-04-08 17:28:08 +02:00
raise APIError(
13, 'Could not find this address in your keys.dat file.')
if not BMConfigParser().safeGetBoolean(address, 'chan'):
2018-04-08 17:28:08 +02:00
raise APIError(
25, 'Specified address is not a chan address.'
' Use deleteAddress API call instead.')
BMConfigParser().remove_section(address)
with open(state.appdata + 'keys.dat', 'wb') as configfile:
BMConfigParser