-
Notifications
You must be signed in to change notification settings - Fork 8
/
armory-daemon.py
executable file
·290 lines (243 loc) · 11.6 KB
/
armory-daemon.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
################################################################################
#
# Copyright (C) 2012, Ian Coleman
# Distributed under the GNU Affero General Public License (AGPL v3)
# See http://www.gnu.org/licenses/agpl.html
#
################################################################################
# This is a json-rpc interface to armory - http://bitcoinarmory.com/
#
# Where possible this follows conventions established by the Satoshi client.
# Does not require armory to be installed or running, this is a standalone application.
# Requires bitcoind process to be running before starting armory-daemon.
# Requires an armory watch-only wallet to be in the same folder as the
# armory-daemon script.
# Works with testnet, use --testnet flag when starting the script.
#
# BEWARE:
# This is relatively untested, please use caution. There should be no chance for
# irreversible damage to be done by this software, but it is still in the early
# development stage so treat it with the appropriate level of skepticism.
# Many thanks must go to etotheipi who started the armory client, and who has
# provided immense amounts of help with this. This app is mostly chunks
# of code taken from armory and refurbished into an rpc client.
# See the bitcontalk thread for more details about this software:
# https://bitcointalk.org/index.php?topic=92496.0
from twisted.web import server
from twisted.internet import reactor
from txjsonrpc.web import jsonrpc
from twisted.cred.checkers import FilePasswordDB
from txjsonrpc.auth import wrapResource
import datetime
import decimal
import os
import sys
RPC_PORT = 7070
STANDARD_FEE = 0.0005 # BTC
class Wallet_Json_Rpc_Server(jsonrpc.JSONRPC):
def __init__(self, wallet):
self.wallet = wallet
def jsonrpc_getnewaddress(self):
addr = self.wallet.getNextUnusedAddress()
return addr.getAddrStr()
def jsonrpc_getbalance(self):
int_balance = self.wallet.getBalance()
decimal_balance = decimal.Decimal(int_balance) / decimal.Decimal(ONE_BTC)
return float(decimal_balance)
def jsonrpc_getreceivedbyaddress(self, address):
if CLI_OPTIONS.offline:
raise ValueError('Cannot get received amount when offline')
# Only gets correct amount for addresses in the wallet, otherwise 0
addr160 = addrStr_to_hash160(address)
txs = self.wallet.getAddrTxLedger(addr160)
balance = sum([x.getValue() for x in txs if x.getValue() > 0])
decimal_balance = decimal.Decimal(balance) / decimal.Decimal(ONE_BTC)
float_balance = float(decimal_balance)
return float_balance
def jsonrpc_sendtoaddress(self, bitcoinaddress, amount):
if CLI_OPTIONS.offline:
raise ValueError('Cannot create transactions when offline')
return self.create_unsigned_transaction(bitcoinaddress, amount)
def jsonrpc_listtransactions(self, tx_count=10, from_tx=0):
#TODO this needs more work
# - populate the rest of the values in tx_info
# - fee
# - blocktime
# - timereceived
# Thanks to unclescrooge for inclusions - https://bitcointalk.org/index.php?topic=92496.msg1282975#msg1282975
# NOTE that this does not use 'account' like in the Satoshi client
final_tx_list = []
all_txs = self.wallet.getTxLedger('blk')
txs = all_txs[from_tx:]
for i in range(len(txs)):
tx = txs[i]
account = ''
txHashBin = tx.getTxHash()#hex_to_binary(tx.getTxHash())
cppTx = TheBDM.getTxByHash(txHashBin)
pytx = PyTx().unserialize(cppTx.serialize())
for txout in pytx.outputs:
scrType = getTxOutScriptType(txout.binScript)
if not scrType in (TXOUT_SCRIPT_STANDARD, TXOUT_SCRIPT_COINBASE):
continue
address = hash160_to_addrStr(TxOutScriptExtractAddr160(txout.binScript))
if self.wallet.hasAddr(address) == False:
continue
else:
break
if tx.getValue() < 0:
category = 'send'
else:
category = 'receive'
amount = float(decimal.Decimal(tx.getValue()) / decimal.Decimal(ONE_BTC))
confirmations = TheBDM.getTopBlockHeader().getBlockHeight() - tx.getBlockNum()+1
blockhash = 'TODO'
blockindex = 'TODO'#tx.getBlockNum()
txid = str(binary_to_hex(tx.getTxHash()))
time = 'TODO'#tx.getTxTime()
tx_info = {
'account':account,
'address':address,
'category':category,
'amount':amount,
#'fee': -0,
'confirmations':confirmations,
'blockhash':blockhash,
'blockindex':blockindex,
#'blocktime': blocktime,
'txid':txid,
'time:':time,
#'timereceived': timereceived
}
final_tx_list.append(tx_info)
if len(final_tx_list) >= tx_count:
break
return final_tx_list
# https://bitcointalk.org/index.php?topic=92496.msg1126310#msg1126310
def create_unsigned_transaction(self, bitcoinaddress_str, amount_to_send_btc):
# Get unspent TxOutList and select the coins
addr160_recipient = addrStr_to_hash160(bitcoinaddress_str)
totalSend, fee = long(amount_to_send_btc * ONE_BTC), (STANDARD_FEE * ONE_BTC)
spendBal = self.wallet.getBalance('Spendable')
utxoList = self.wallet.getTxOutList('Spendable')
utxoSelect = PySelectCoins(utxoList, totalSend, fee)
minFeeRec = calcMinSuggestedFees(utxoSelect, totalSend, fee)[1]
if fee<minFeeRec:
if totalSend + minFeeRec > spendBal:
raise NotEnoughCoinsError, "You can't afford the fee!"
utxoSelect = PySelectCoins(utxoList, totalSend, minFeeRec)
fee = minFeeRec
if len(utxoSelect)==0:
raise CoinSelectError, "Somehow, coin selection failed. This shouldn't happen"
totalSelected = sum([u.getValue() for u in utxoSelect])
totalChange = totalSelected - (totalSend + fee)
outputPairs = []
outputPairs.append( [addr160_recipient, totalSend] )
if totalChange > 0:
outputPairs.append( [self.wallet.getNextUnusedAddress().getAddr160(), totalChange] )
random.shuffle(outputPairs)
txdp = PyTxDistProposal().createFromTxOutSelection(utxoSelect, outputPairs)
return txdp.serializeAscii()
class Armory_Daemon():
def __init__(self):
sys.stdout.write("\nReading wallet file")
self.wallet = self.find_wallet()
use_blockchain = not CLI_OPTIONS.offline
if(use_blockchain):
sys.stdout.write("\nLoading blockchain")
self.loadBlockchain()
sys.stdout.write("\nInitialising server")
resource = Wallet_Json_Rpc_Server(self.wallet)
secured_resource = self.set_auth(resource)
reactor.listenTCP(RPC_PORT, server.Site(secured_resource))
if(use_blockchain):
self.NetworkingFactory = ArmoryClientFactory( \
func_loseConnect=self.showOfflineMsg, \
func_madeConnect=self.showOnlineMsg, \
func_newTx=self.handleIncomingTxFunc)
reactor.connectTCP('127.0.0.1', BITCOIN_PORT, self.NetworkingFactory)
reactor.callLater(5, self.Heartbeat)
self.start()
def set_auth(self, resource):
passwordfile = "credentials.txt" # TODO change to bitcoin credentials
checker = FilePasswordDB(passwordfile)
realmName = "Armory JSON-RPC App"
wrapper = wrapResource(resource, [checker], realmName=realmName)
return wrapper
def start(self):
sys.stdout.write("\nServer started")
reactor.run()
def handleIncomingTxFunc(self, pytxObj):
# Cut down version from ArmoryQt.py
TheBDM.addNewZeroConfTx(pytxObj.serialize(), long(RightNow()), True)
TheBDM.rescanWalletZeroConf(self.wallet.cppWallet)
# TODO set up a 'subscribe' feature so these notifications can be
# pushed out to interested parties.
# TODO something useful with this information
#message = "New TX"
#sys.stdout.write("\n" + message) # Gets too noisy
def showOfflineMsg(self):
sys.stdout.write("\n%s Offline - not tracking blockchain" % datetime.now().isoformat())
def showOnlineMsg(self):
sys.stdout.write("\n%s Online - tracking blockchain" % datetime.now().isoformat())
def find_wallet(self):
fnames = os.listdir(os.getcwd())
for fname in fnames:
is_wallet = fname[-7:] == ".wallet"
is_watchonly = fname.find("watchonly") > -1
is_backup = fname.find("backup") > -1
if(is_wallet and is_watchonly and not is_backup):
wallet = PyBtcWallet().readWalletFile(fname)
sys.stdout.write("\nUsing wallet file %s" % fname)
return wallet
raise ValueError('Unable to locate a watch-only wallet in %s' % os.getcwd())
def loadBlockchain(self):
TheBDM.loadBlockchain()
# Thanks to unclescrooge for inclusions - https://bitcointalk.org/index.php?topic=92496.msg1282975#msg1282975
self.latestBlockNum = TheBDM.getTopBlockHeader().getBlockHeight()
# Now that theb blockchain is loaded, let's populate the wallet info
if TheBDM.isInitialized():
mempoolfile = os.path.join(ARMORY_HOME_DIR,'mempool.bin')
self.checkMemoryPoolCorruption(mempoolfile)
TheBDM.enableZeroConf(mempoolfile)
# self.statusBar().showMessage('Syncing wallets with blockchain...')
sys.stdout.write("\nSyncing wallets with blockchain")
sys.stdout.write("\nSyncing wallet: %s" % self.wallet.uniqueIDB58)
self.wallet.setBlockchainSyncFlag(BLOCKCHAIN_READONLY)
self.wallet.syncWithBlockchain()
def checkMemoryPoolCorruption(self, mempoolname):
if not os.path.exists(mempoolname):
return
memfile = open(mempoolname, 'r')
memdata = memfile.read()
memfile.close()
binunpacker = BinaryUnpacker(memdata)
try:
while binunpacker.getRemainingSize() > 0:
binunpacker.get(UINT64)
PyTx().unserialize(binunpacker)
except:
os.remove(mempoolname);
#LOGWARN('Memory pool file was corrupt. Deleted. (no further action is needed)')
def Heartbeat(self, nextBeatSec=2):
"""
This method is invoked when the app is initialized, and will
run every 2 seconds, or whatever is specified in the nextBeatSec
argument.
"""
# Check for new blocks in the blk000X.dat file
if TheBDM.isInitialized():
sys.stdout.write(".")
sys.stdout.flush()
newBlks = TheBDM.readBlkFileUpdate()
self.topTimestamp = TheBDM.getTopBlockHeader().getTimestamp()
if newBlks>0:
self.latestBlockNum = TheBDM.getTopBlockHeader().getBlockHeight()
didAffectUs = False
prevLedgerSize = len(self.wallet.getTxLedger())
self.wallet.syncWithBlockchain()
TheBDM.rescanWalletZeroConf(self.wallet.cppWallet)
self.wallet.checkWalletLockTimeout()
reactor.callLater(nextBeatSec, self.Heartbeat)
if __name__ == "__main__":
from armoryengine import *
rpc_server = Armory_Daemon()