forked from goatpig/armoryd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
armoryd.py
3630 lines (3132 loc) · 145 KB
/
armoryd.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
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
################################################################################
# #
# Copyright (C) 2011-2015, Armory Technologies, Inc. #
# Distributed under the GNU Affero General Public License (AGPL v3) #
# See LICENSE or http://www.gnu.org/licenses/agpl.html #
# #
################################################################################
# #
# Original copyright transferred from from Ian Coleman (2012) #
# Special thanks to Ian Coleman who created the original incarnation of #
# this file and then transferred the rights to me so I could integrate it #
# into the Armory project. And even more thanks to him for his advice #
# on upgrading its security features and other capabilities. #
# #
################################################################################
#####
# ORIGINAL comments from Ian Coleman, circa July 2012:
#
# 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 armoryd.
# Requires an armory wallet (can be watching only) to be in the same folder as
# the armoryd script. Works with testnet and regtest, use --testnet and --regtest
# flags respectively 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
#####
################################################################################
# To assist users, they can access, via JSON, a dictionary with information on
# all the functions available via JSON. The dictionary will use the funct name
# as the key and another dictionary as the value. The resultant dict will have a
# description (string), a list of strings with parameter info, and the return
# value (string). The following example provides a visual guide.
#
# {
# "functA" : {
# "Description" : "A funct that does a thing. This has mult lines."
# "Parameters" : [
# "paramA - Descrip of paramA. Mult lines exist."
# "paramB - A random string."
# ]
# "Return Value" : "Description of the return value. Mult lines!"
# }
#
# Devs will have to remember to add the doc string when new functs are added. In
# addition, devs will have to remember to use the following format for the
# docstring, lest they risk screwing up the resultant dictionary or being unable
# to run armoryd. Note that parameters must have a single dash separating them,
# along with a single space before and after the dash!
#
# DESCRIPTION:
# A funct that does a thing.
# This has mult lines.
# PARAMETERS:
# paramA - Descrip of paramA.
# Mult lines exist.
# paramB - A random string.
# RETURN:
# Description of the return value.
# Mult lines!
################################################################################
################################################################################
#
# Random JSON notes should be placed here as desired.
#
# - If a returned string has a newline char, JSON will convert it to the string
# "\n" (minus quotation marks).
# - In general, if you need to pass an actual newline via the command line, type
# $'\n' instead of \n or \\n. (Newlines in files can be left alone.)
# - The code sometimes returns "bitcoinrpc_jsonrpc.authproxy.JSONRPCException"
# if values are returned as binary data. This is something to keep in mind if
# bugs occur.
# - When all else fails, and you have no clue how to deal via JSON, read RFC
# 4627 and/or the Python manual's section on JSON.
#
################################################################################
import base64
from collections import defaultdict
import decimal
from inspect import *
import json
import sys
from twisted.cred.checkers import FilePasswordDB
from twisted.internet import reactor
from twisted.internet.protocol import ClientFactory # REMOVE IN 0.93
from twisted.web import server
from txjsonrpc.auth import wrapResource
from txjsonrpc.web import jsonrpc
from armoryengine.ALL import *
from armoryengine.Decorators import EmailOutput, catchErrsForJSON
from armoryengine.PyBtcWalletRecovery import *
from armoryengine.Block import PyBlockHeader
# Some non-twisted json imports from jgarzik's code and his UniversalEncoder
class UniversalEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, decimal.Decimal):
return float(obj)
return json.JSONEncoder.default(self, obj)
ARMORYD_CONF_FILE = os.path.join(ARMORY_HOME_DIR, 'armoryd.conf')
# Define some specific errors that can be thrown and caught
class UnrecognizedCommand(Exception): pass
class NotEnoughCoinsError(Exception): pass
class CoinSelectError(Exception): pass
class WalletUnlockNeeded(Exception): pass
class InvalidBitcoinAddress(Exception): pass
class PrivateKeyNotFound(Exception): pass
class WalletDoesNotExist(Exception): pass
class LockboxDoesNotExist(Exception): pass
class AddressNotInWallet(Exception): pass
class BlockchainNotReady(Exception): pass
class InvalidTransaction(Exception): pass
class IncompleteTransaction(Exception): pass
# A dictionary that includes the names of all functions an armoryd user can
# call from the armoryd server. Implemented on the server side so that a client
# can know what exactly the server can run. See the note above regarding the
# docstring format.
jsonFunctDict = {}
NOT_IMPLEMENTED = '--Not Implemented--'
################################################################################
# Utility function that takes a list of wallet paths, gets the paths and adds
# the wallets to a wallet set (actually a dictionary, with the wallet ID as the
# key and the wallet as the value), along with adding the wallet ID to a
# separate set.
def addMultWallets(inWltPaths, inWltMap):
'''Function that adds multiple wallets to an armoryd server.'''
newWltList = []
for aWlt in inWltPaths:
# Logic basically taken from loadWalletsAndSettings()
try:
wltLoad = PyBtcWallet().readWalletFile(aWlt)
wltID = wltLoad.uniqueIDB58
wltLoad.fillAddressPool()
# For now, no wallets are excluded. If this changes....
#if aWlt in wltExclude or wltID in wltExclude:
# continue
# A directory can have multiple versions of the same
# wallet. We'd prefer to skip watch-only wallets.
if wltID in inWltMap.keys():
LOGWARN('***WARNING: Duplicate wallet (%s) detected' % wltID)
wo1 = inWltMap[wltID].watchingOnly
wo2 = wltLoad.watchingOnly
if wo1 and not wo2:
prevWltPath = inWltMap[wltID].walletPath
inWltMap[wltID] = wltLoad
LOGWARN('First wallet is more useful than the second one...')
LOGWARN(' Wallet 1 (loaded): %s', aWlt)
LOGWARN(' Wallet 2 (skipped): %s', prevWltPath)
else:
LOGWARN('Second wallet is more useful than the first one...')
LOGWARN(' Wallet 1 (skipped): %s', aWlt)
LOGWARN(' Wallet 2 (loaded): %s', \
inWltMap[wltID].walletPath)
else:
# Update the wallet structs.
inWltMap[wltID] = wltLoad
newWltList.append(wltID)
except:
LOGEXCEPT('***WARNING: Unable to load wallet %s. Skipping.', aWlt)
raise
return newWltList
################################################################################
# Utility function that takes a list of lockbox paths, gets the paths and adds
# the lockboxes to a lockbox set (actually a dictionary, with the lockbox ID as
# the key and the lockbox as the value), along with adding the lockboxy ID to a
# separate set.
def addMultLockboxes(inLBPaths, inLboxMap):
'''Function that adds multiple lockboxes to an armoryd server.'''
newLBList = []
for curLBFile in inLBPaths:
try:
curLBList = readLockboxesFile(curLBFile)
for curLB in curLBList:
lbID = curLB.uniqueIDB58
if lbID in inLboxMap.keys():
LOGINFO('***WARNING: Duplicate lockbox (%s) detected' % lbID)
else:
inLboxMap[lbID] = curLB
newLBList.append(lbID)
except:
LOGEXCEPT('***WARNING: Unable to load lockbox file %s. Skipping.', \
curLBFile)
raise
return newLBList
class Armory_Json_Rpc_Server(jsonrpc.JSONRPC):
#############################################################################
def __init__(self, wallet, lockbox=None, inWltMap=None, inLBMap=None, \
inLBCppWalletMap=None, armoryHomeDir=ARMORY_HOME_DIR, \
addrByte=ADDRBYTE):
# Save the incoming info. If the user didn't pass in a wallet set, put the
# wallet in the set (actually a dictionary w/ the wallet ID as the key).
self.addressMetaData = {}
self.curWlt = wallet
self.curLB = lockbox
self.signerType = SIGNER_DEFAULT
# Dicts, sets and lists (and other container types?), if used as a default
# argument, actually become references and subsequent calls to __init__
# will not necessarily be empty objects/maps. The proper way to initialize
# is to check for None and set to the proper type.
if inWltMap == None:
inWltMap = {}
if inLBMap == None:
inLBMap = {}
if inLBCppWalletMap == None:
inLBCppWalletMap = {}
self.serverWltMap = inWltMap # Dict
self.serverLBMap = inLBMap # Dict
self.serverLBCppWalletMap = inLBCppWalletMap # Dict
self.armoryHomeDir = armoryHomeDir
if wallet != None:
wltID = wallet.uniqueIDB58
if self.serverWltMap.get(wltID) == None:
self.serverWltMap[wltID] = wallet
# If any variables rely on whether or not Testnet in a Box is running,
# we'll set everything up here.
self.addrByte = addrByte
#############################################################################
@catchErrsForJSON
def jsonrpc_getreceivedfromsigner(self, *sigBlock):
"""
DESCRIPTION:
Verify that a message (RFC 2440: clearsign or Base64) has been signed by
a Bitcoin address and get the amount of coins sent to the current wallet
by the message's signer.
PARAMETERS:
sigBlock - Message with the RFC 2440 message to be verified. The message
must be enclosed in quotation marks.
RETURN:
A dictionary with verified message and the amount of money sent to the
current wallet by the signer.
"""
retDict = {}
# We must deal with a quirk. Non-escaped spaces (i.e., spaces that aren't
# \u0020) will cause the CL parser to split the sig into multiple lines.
# We need to combine the lines. (NB: Strip the final space too!)
signedMsg = (''.join((str(piece) + ' ') for piece in sigBlock))[:-1]
verification = self.jsonrpc_verifysignature(signedMsg)
retDict['message'] = verification['message']
retDict['amount'] = \
self.jsonrpc_getreceivedfromaddress(verification['address'])
return retDict
#############################################################################
@catchErrsForJSON
def jsonrpc_sendasciitransaction(self, txASCIIFile):
"""
DESCRIPTION:
Broadcast to the bitcoin network the signed tx in the txASCIIFile
PARAMETERS:
txASCIIFile - The path to a file with a signed transacion.
RETURN:
The transaction id of the tx that was broadcast
"""
return self.jsonrpc_sendasciitransactionraw(self.readFile(txASCIIFile))
#############################################################################
@catchErrsForJSON
def jsonrpc_sendasciitransactionraw(self, txASCII):
"""
DESCRIPTION:
Broadcast to the bitcoin network the signed tx in the txASCII.
Unlike `sendasciitransaction`, this takes raw data, not a file path.
PARAMETERS:
txASCII - A signed transaction in ASCII-armored form.
RETURN:
The transaction id of the tx that was broadcast
"""
# Try to decipher the Tx and make sure it's actually signed.
txObj = UnsignedTransaction().unserializeAscii(txASCII)
if not txObj:
raise InvalidTransaction, "file does not contain a valid tx"
if not txObj.verifySigsAllInputs(txObj.signerType):
raise IncompleteTransaction, "transaction needs more signatures"
pytx = txObj.getSignedPyTx()
try:
TheBDM.bdv().broadcastZC(pytx.serialize())
except:
LOGERROR("failed to broadcast tx %s" % pytx.getHashHex(BIGENDIAN))
return "failed to broadcast tx"
return pytx.getHashHex(BIGENDIAN)
#############################################################################
# The following is a fake example of a message that can be sent into
# verifysignature(). The example is included primarily to show command line
# formatting. Messages are the same type as those generated by Bitcoin Core.
# python armoryd.py verifysignature \"-----BEGIN BITCOIN SIGNED MESSAGE-----$'\n'Comment: Hello.$'\n'-----BEGIN BITCOIN SIGNATURE-----$'\n'$'\n'junkjunkjunk$'\n'-----END BITCOIN SIGNATURE-----\"
@catchErrsForJSON
def jsonrpc_verifysignature(self, *sigBlock):
"""
DESCRIPTION:
Take a message (RFC 2440: clearsign or Base64) signed by a Bitcoin address
and verify the message.
PARAMETERS:
sigBlock - Message with the RFC 2440 message to be verified. The message
must be enclosed in quotation marks.
RETURN:
A dictionary with verified message and the Base58 address of the signer.
"""
retDict = {}
# We must deal with a couple of quirks. First, non-escaped spaces (i.e.,
# spaces that aren't \u0020) will cause the CL parser to split the sig
# into multiple lines. We need to combine the lines. Second, the quotation
# marks used to prevent Armory from treating the sig like a CL arg need
# to be removed. (NB: The final space must be stripped too.)
signedMsg = (''.join((str(piece) + ' ') for piece in sigBlock))[1:-2]
# Get the signature block's signature and message. The signature must be
# formatted for clearsign or Base64 persuant to RFC 2440.
sig, msg = readSigBlock(signedMsg)
retDict['message'] = msg
addrB58 = verifySignature(sig, msg, 'v1', ord(self.addrByte) )
retDict['address'] = addrB58
return retDict
#############################################################################
@catchErrsForJSON
def jsonrpc_getreceivedfromaddress(self, sender):
"""
DESCRIPTION:
Return the number of coins received from a particular sender.
PARAMETERS:
sender - Base58 address of the sender to the current wallet.
RETURN:
Number of Bitcoins sent by the sender to the current wallet.
"""
totalReceived = 0.0
ledgerEntries = self.curWlt.getTxLedger('blk')
for entry in ledgerEntries:
cppTx = TheBDM.bdv().getTxByHash(entry.getTxHash())
if cppTx.isInitialized():
# Only consider the first for determining received from address
# This function should assume it is online, and actually request the previous
# TxOut script from the BDM -- which guarantees we know the sender.
# Use TheBDM.getSenderScrAddr(txin). This takes a C++ txin (which we have)
# and it will grab the TxOut being spent by that TxIn and return the
# scraddr of it. This will succeed 100% of the time.
cppTxin = cppTx.getTxInCopy(0)
txInAddr = scrAddr_to_addrStr(TheBDM.bdv().getSenderScrAddr(cppTxin))
fromSender = sender == txInAddr
if fromSender:
txBinary = cppTx.serialize()
pyTx = PyTx().unserialize(txBinary)
for txout in pyTx.outputs:
if self.curWlt.hasAddr(script_to_addrStr(txout.getScript())):
totalReceived += txout.value
return AmountToJSON(totalReceived)
#############################################################################
# backupFilePath is the file to backup the current wallet to.
# It does not necessarily exist yet.
@catchErrsForJSON
def jsonrpc_backupwallet(self, backupFilePath):
"""
DESCRIPTION:
Back up the current wallet to a file at a given location. The backup will
occur only if the file does not exist yet.
PARAMETERS:
backupFilePath - Path to the location where the backup will be saved.
RETURN:
A dictionary indicating whether or not the backup succeeded or failed,
with the reason for failure given if applicable.
"""
retVal = {}
if os.path.isfile(backupFilePath):
retVal = {}
retVal['Error'] = 'File %s already exists. Will not overwrite.' % \
backupFilePath
else:
if not self.curWlt.backupWalletFile(backupFilePath):
# If we have a failure here, we probably won't know why. Not much
# to do other than ask the user to check the armoryd server.
retVal['Error'] = "Backup failed. Check the armoryd server logs."
else:
retVal['Result'] = "Backup succeeded."
return retVal
#############################################################################
# Get a list of UTXOs for the currently loaded wallet.
@catchErrsForJSON
def jsonrpc_listunspent(self):
"""
DESCRIPTION:
Get a list of unspent transactions for the currently loaded wallet. By
default, zero-conf UTXOs are included.
PARAMETERS:
None
RETURN:
A dictionary listing information about each UTXO in the currently loaded
wallet. The dictionary is similar to the one returned by the bitcoind
call of the same name.
"""
# Return a dictionary with a string as the key and a wallet B58 value as
# the value.
utxoList = self.curWlt.getFullUTXOList()
curTxOut = 0
totBal = 0
utxoOutList = []
if TheBDM.getState()==BDM_BLOCKCHAIN_READY:
for u in utxoList:
curUTXODict = {}
curTxOut += 1
curTxOutStr = 'utxo%05d' % curTxOut
utxoVal = AmountToJSON(u.getValue())
curUTXODict['txid'] = binary_to_hex(u.getTxHash(), \
BIGENDIAN, LITTLEENDIAN)
curUTXODict['vout'] = u.getTxOutIndex()
try:
curUTXODict['address'] = script_to_addrStr(u.getScript())
except:
LOGEXCEPT('Error parse UTXO script -- multisig or non-standard')
curUTXODict['address'] = ''
curUTXODict['scriptPubKey'] = binary_to_hex(u.getScript())
curUTXODict['amount'] = utxoVal
curUTXODict['confirmations'] = u.getNumConfirm()
curUTXODict['priority'] = utxoVal * u.getNumConfirm()
utxoOutList.append(curUTXODict)
totBal += utxoVal
else:
LOGERROR('Blockchain not ready. Values will not be reported.')
# Maybe we'll add more later, but for now, return what we have.
return utxoOutList
#############################################################################
# Get a dictionary with UTXOs for the wallet associated with the Base58
# address passed into the function. By default, zero-conf UTXOs are included.
# The basic layout of the dictionary is as follows.
# {
# addrbalance : {
# Address : Balance
# }
# numutxo : int
# totalbalance : float
# utxolist : {
# Same information as listunspent
# }
# }
@catchErrsForJSON
def jsonrpc_listaddrunspent(self, inB58):
"""
DESCRIPTION:
Get a list of unspent transactions for the currently loaded wallet that
are associated with a given, comma-separated list of Base58 addresses from
the wallet. By default, zero-conf UTXOs are included.
PARAMETERS:
inB58 - The Base58 address to check against the current wallet.
RETURN:
A dictionary containing all UTXOs for the currently loaded wallet
associated with the given Base58 address, along with information about
each UTXO.
"""
# TODO: We should probably add paging to this...
totalTxOuts = 0
totalBal = 0
utxoDict = {}
utxoList = []
# Get the UTXO balance & list for each address.
# The strip() makes it possible to supply addresses with
# spaces after or before each comma
addrList = [a.strip() for a in inB58.split(",")]
curTxOut = 0
topBlk = TheBDM.getTopBlockHeight()
addrBalanceMap = {}
utxoEntries = []
for addrStr in addrList:
atype,a160 = addrStr_to_hash160(addrStr)
if atype==ADDRBYTE:
# Already checked it's registered, regardless if in a loaded wallet
utxoList = getUnspentTxOutsForAddr160List([a160])
elif atype==P2SHBYTE:
# For P2SH, we'll require we have a loaded lockbox
lbox = self.getLockboxByP2SHAddrStr(addrStr)
if not lbox:
raise BitcoindError('Import lockbox before getting P2SH unspent')
# We simply grab the UTXO list for the lbox, both p2sh and multisig
cppWallet = self.serverLBCppWalletMap[lbox.uniqueIDB58]
utxoList = cppWallet.getFullUTXOList()
else:
raise NetworkIDError('Addr for the wrong network!')
# Place each UTXO in the return dict. Each entry should specify which
# address is associated with which UTXO.
# (DR: For 0.93, this ought to be merged with the listunspent code, and
# maybe moved around a bit to make the info easier to process.)
utxoListBal = 0
for u in utxoList:
curTxOut += 1
curUTXODict = {}
# Get the UTXO info.
curTxOutStr = 'utxo%05d' % curTxOut
utxoVal = AmountToJSON(u.getValue())
curUTXODict['txid'] = binary_to_hex(u.getOutPoint().getTxHash(), \
BIGENDIAN, LITTLEENDIAN)
curUTXODict['vout'] = u.getTxOutIndex()
try:
curUTXODict['address'] = script_to_addrStr(u.getScript())
except:
LOGEXCEPT('Error parse UTXO script -- multisig or non-standard')
curUTXODict['address'] = ''
curUTXODict['scriptPubKey'] = binary_to_hex(u.getScript())
curUTXODict['amount'] = utxoVal
curUTXODict['confirmations'] = u.getNumConfirm(TheBDM.getTopBlockHeight())
curUTXODict['priority'] = utxoVal * u.getNumConfirm(TheBDM.getTopBlockHeight())
utxoEntries.append(curUTXODict)
totalTxOuts += 1
utxoListBal += u.getValue()
totalBal += u.getValue()
# Add up the UTXO balances for each address and add it to the UTXO
# entry dict, then add the UTXO entry dict to the master dict.
addrBalanceMap[addrStr] = AmountToJSON(utxoListBal)
# Let's round out the master dict with more info.
utxoDict['utxolist'] = utxoEntries
utxoDict['numutxo'] = totalTxOuts
utxoDict['addrbalance'] = addrBalanceMap
utxoDict['totalbalance'] = AmountToJSON(totalBal)
return utxoDict
#############################################################################
@catchErrsForJSON
def jsonrpc_importprivkey(self, privKey):
"""
DESCRIPTION:
Import a private key into the current wallet.
PARAMETERS:
privKey - A private key in any format supported by Armory, including
Base58 private keys supported by bitcoind (uncompressed public
key support only).
RETURN:
A string of the private key's accompanying hexadecimal public key.
"""
# Convert string to binary
retDict = {}
privKey = str(privKey)
privKeyValid = True
if self.curWlt.useEncryption and self.curWlt.isLocked:
raise WalletUnlockNeeded
# Make sure the key is one we can support
try:
binPrivKey, privKeyType = parsePrivateKeyData(privKey)
except:
(errType, errVal) = sys.exc_info()[:2]
LOGEXCEPT('Error parsing incoming private key.')
LOGERROR('Error Type: %s' % errType)
LOGERROR('Error Value: %s' % errVal)
retDict['Error'] = 'Error type %s while parsing incoming private ' \
'key.' % errType
privKeyValid = False
if privKeyValid:
self.curWlt.isEnabled = False
thePubKey = self.curWlt.importExternalAddressData(\
privKey=SecureBinaryData(binPrivKey))
if thePubKey != None:
retDict['PubKey'] = thePubKey.toHexStr()
else:
LOGERROR('Attempt to import a private key failed.')
retDict['Error'] = 'Attempt to import your private key failed. ' \
'Check if the key is already in your wallet.'
return retDict
#############################################################################
@catchErrsForJSON
def jsonrpc_getrawtransaction(self, txHash, verbose=0, endianness=BIGENDIAN):
"""
DESCRIPTION:
Get the raw transaction string for a given transaction hash.
PARAMETERS:
txHash - A string representing the hex value of a transaction ID.
verbose - (Default=0) Integer indicating whether or not the result should
be more verbose.
endianness - (Default=BIGENDIAN) Indicates the endianness of the ID.
RETURN:
A dictionary with the decoded raw transaction and relevant information.
"""
rawTx = None
cppTx = TheBDM.bdv().getTxByHash(hex_to_binary(txHash, endianness))
if cppTx.isInitialized():
txBinary = cppTx.serialize()
pyTx = PyTx().unserialize(txBinary)
rawTx = binary_to_hex(pyTx.serialize())
if verbose:
result = self.jsonrpc_decoderawtransaction(rawTx)
result['hex'] = rawTx
else:
result = rawTx
else:
LOGERROR('Tx hash not recognized by TheBDM: %s' % txHash)
result = None
return result
#############################################################################
# NOTE: For now, the "includemempool" option isn't included. It seems to be a
# dead option on bitcoind. If this ever changes, it can be implemented.
@catchErrsForJSON
def jsonrpc_gettxout(self, txHash, n, binary=0):
"""
DESCRIPTION:
Get the TxOut entries for a given transaction hash.
PARAMETERS:
txHash - A string representing the hex value of a transaction ID.
n - The TxOut index to obtain.
binary - (Default=0) Boolean value indicating whether or not the resultant binary
script should be in binary form or converted to a hex string.
RETURN:
A dictionary with the Bitcoin amount for the TxOut and the TxOut script in
hex string form (default) or binary form.
"""
n = int(n)
txOut = None
cppTx = TheBDM.bdv().getTxByHash(hex_to_binary(txHash, BIGENDIAN))
if cppTx.isInitialized():
txBinary = cppTx.serialize()
pyTx = PyTx().unserialize(txBinary)
if n < len(pyTx.outputs):
# If the user doesn't want binary data, return a formatted string,
# otherwise return a hex string with the raw TxOut data.
result = {}
txOut = pyTx.outputs[n]
result = {'value' : AmountToJSON(txOut.value),
'script' : txOut.binScript if int(binary) else binary_to_hex(txOut.binScript)}
else:
LOGERROR('Tx output index is invalid: #%d' % n)
else:
LOGERROR('Tx hash not recognized by TheBDM: %s' % binary_to_hex(txHash))
return result
#############################################################################
@catchErrsForJSON
def jsonrpc_encryptwallet(self, passphrase):
"""
DESCRIPTION:
Encrypt a wallet with a given passphrase.
PARAMETERS:
passphrase - The wallet's new passphrase.
RETURN:
A string indicating that the encryption was successful.
"""
retStr = 'Wallet %s has been encrypted.' % self.curWlt.uniqueIDB58
if self.curWlt.isLocked:
raise WalletUnlockNeeded
else:
try:
self.sbdPassphrase = SecureBinaryData(str(passphrase))
self.curWlt.changeWalletEncryption(securePassphrase=self.sbdPassphrase)
self.curWlt.lock()
finally:
self.sbdPassphrase.destroy() # Ensure SBD is destroyed.
return retStr
#############################################################################
@catchErrsForJSON
def jsonrpc_walletpassphrase(self, passphrase, timeout=10):
"""
DESCRIPTION:
Unlock a wallet with a given passphrase and unlock time length.
PARAMETERS:
passphrase - The wallet's current passphrase.
timeout - (Default=10) The time, in seconds, that the wallet will be
unlocked.
RETURN:
A string indicating if the wallet was unlocked or if it was already
unlocked.
"""
retStr = 'Wallet %s is already unlocked.' % self.curWlt
if self.curWlt.isLocked:
try:
self.sbdPassphrase = SecureBinaryData(str(passphrase))
self.curWlt.unlock(securePassphrase=self.sbdPassphrase,
tempKeyLifetime=int(timeout))
retStr = 'Wallet %s has been unlocked.' % self.curWlt.uniqueIDB58
finally:
self.sbdPassphrase.destroy() # Ensure SBD is destroyed.
return retStr
#############################################################################
@catchErrsForJSON
def jsonrpc_walletlock(self):
"""
DESCRIPTION:
Lock a wallet.
PARAMETERS:
None
RETURN:
A string indicating whether or not the wallet is locked.
"""
# Lock the wallet. It should lock but we'll check to be safe.
self.curWlt.lock()
retStr = 'Wallet is %slocked.' % '' if self.curWlt.isLocked else 'not '
return retStr
#############################################################################
@catchErrsForJSON
def getScriptAddrStrs(self, txOut):
addrList = []
scriptType = getTxOutScriptType(txOut.binScript)
if scriptType in CPP_TXOUT_STDSINGLESIG:
M = 1
addrList = [script_to_addrStr(txOut.binScript)]
elif scriptType == CPP_TXOUT_P2SH:
M = -1
addrList = [script_to_addrStr(txOut.binScript)]
elif scriptType==CPP_TXOUT_MULTISIG:
M, N, addr160List, pub65List = getMultisigScriptInfo(txOut.binScript)
addrList = [hash160_to_addrStr(a160) for a160 in addr160List]
elif scriptType == CPP_TXOUT_NONSTANDARD:
M = -1
opStringList = convertScriptToOpStrings(txOut.binScript)
return { 'asm' : ' '.join(opStringList),
'hex' : binary_to_hex(txOut.binScript),
'reqSigs' : M,
'type' : CPP_TXOUT_SCRIPT_NAMES[scriptType],
'addresses' : addrList }
#############################################################################
@catchErrsForJSON
def jsonrpc_decoderawtransaction(self, hexString):
"""
DESCRIPTION:
Decode a raw transaction hex string.
PARAMETERS:
hexString - A string representing, in hex form, a raw transaction.
RETURN:
A dictionary containing the decoded transaction's information.
"""
pyTx = PyTx().unserialize(hex_to_binary(hexString))
#####
# Accumulate TxIn info
vinList = []
for txin in pyTx.inputs:
prevHash = txin.outpoint.txHash
scrType = getTxInScriptType(txin)
# ACR: What is asm, and why is basically just binScript?
oplist = convertScriptToOpStrings(txin.binScript)
scriptSigDict = { 'asm' : ' '.join(oplist),
'hex' : binary_to_hex(txin.binScript) }
if not scrType == CPP_TXIN_COINBASE:
vinList.append( { 'txid' : binary_to_hex(prevHash, BIGENDIAN),
'vout' : txin.outpoint.txOutIndex,
'scriptSig' : scriptSigDict,
'sequence' : txin.intSeq})
else:
vinList.append( { 'coinbase' : binary_to_hex(txin.binScript),
'sequence' : txin.intSeq })
#####
# Accumulate TxOut info
voutList = []
for n,txout in enumerate(pyTx.outputs):
voutList.append( { 'value' : AmountToJSON(txout.value),
'n' : n,
'scriptPubKey' : self.getScriptAddrStrs(txout) } )
#####
# Accumulate all the data to return
result = { 'txid' : pyTx.getHashHex(BIGENDIAN),
'version' : pyTx.version,
'locktime' : pyTx.lockTime,
'vin' : vinList,
'vout' : voutList }
return result
#############################################################################
@catchErrsForJSON
def jsonrpc_getnewaddress(self):
"""
DESCRIPTION:
Get a new Base58 address from the currently loaded wallet.
PARAMETERS:
None
RETURN:
The wallet's next unused public address in Base58 form.
"""
addr = self.curWlt.getNextUnusedAddress()
return addr.getAddrStr()
#############################################################################
@catchErrsForJSON
def jsonrpc_dumpprivkey(self, addr58, keyFormat='Base58'):
"""
DESCRIPTION:
Dump the private key for a given Base58 address associated with the
currently loaded wallet.
PARAMETERS:
addr58 - A Base58 public address associated with the current wallet.
keyFormat - (Default=Base58) The desired format of the output. "Base58"
and "Hex" are the available formats.
RETURN:
The 32 byte private key, encoded as requested by the user.
"""
# Cannot dump the private key for a locked wallet
if self.curWlt.isLocked:
raise WalletUnlockNeeded
# The first byte must be the correct net byte, and the
# last 4 bytes must be the correct checksum
if not checkAddrStrValid(addr58):
raise InvalidBitcoinAddress
retVal = ''
addr160 = addrStr_to_hash160(addr58, False)[1]
pyBtcAddress = self.curWlt.getAddrByHash160(addr160)
if pyBtcAddress == None:
raise PrivateKeyNotFound
try:
self.privKey = SecureBinaryData(pyBtcAddress.serializePlainPrivateKey())
if keyFormat.lower() == 'base58':
retVal = privKey_to_base58(self.privKey.toBinStr())
elif keyFormat.lower() == 'hex':
retVal = binary_to_hex(self.privKey.toBinStr())
else:
retVal = 'ERROR: Requested format (%s) is invalid.' % keyFormat
finally:
self.privKey.destroy()
return retVal
#############################################################################
@catchErrsForJSON
def jsonrpc_getwalletinfo(self, inWltID=None):
"""
DESCRIPTION:
Get information on the currently loaded wallet.
PARAMETERS:
inWltID - (Default=None) If used, armoryd will get info for the wallet for
the provided Base58 wallet ID instead of the current wallet.
RETURN:
A dictionary with information on the current wallet.
"""
wltInfo = {}
self.isReady = TheBDM.getState() == BDM_BLOCKCHAIN_READY
self.wltToUse = self.curWlt
# If we're not getting info on the currently loaded wallet, check to make
# sure the incoming wallet ID points to an actual wallet.
if inWltID:
if self.serverWltMap[inWltID] != None:
self.wltToUse = self.serverWltMap[inWltID]
else:
raise WalletDoesNotExist
return self.wltToUse.toJSONMap()
#############################################################################
@catchErrsForJSON
def jsonrpc_getbalance(self, baltype='spendable'):
"""
DESCRIPTION:
Get the balance of the currently loaded wallet.
PARAMETERS:
baltype - (Default=spendable) A string indicating the balance type to
retrieve from the current wallet.
RETURN:
The current wallet balance (BTC), or -1 if an error occurred.
"""
retVal = -1
# Proceed only if the blockchain's good. Wallet value could be unreliable
# otherwise.
if TheBDM.getState()==BDM_BLOCKCHAIN_READY:
if not baltype in ['spendable', 'spend', 'unconf', 'unconfirmed', \
'total', 'ultimate', 'unspent', 'full']:
LOGERROR('Unrecognized getbalance string: "%s"', baltype)
else:
retVal = AmountToJSON(self.curWlt.getBalance(baltype))
else:
raise BlockchainNotReady('Wallet is not loaded yet.')
return retVal
#############################################################################
@catchErrsForJSON
def jsonrpc_getaddrbalance(self, inB58, baltype='spendable'):
"""
DESCRIPTION:
Get the balance of a Base58 address associated with the currently
loaded wallet.
PARAMETERS:
inB58 - The Base58 address associated with the current wallet.
baltype - (Default=spendable) A string indicating the balance type to
retrieve from the current wallet.
RETURN:
The current wallet balance (BTC), or -1 if an error occurred.
"""
retVal = -1
if not baltype in ['spendable','spend', 'unconf', 'unconfirmed', \
'ultimate','unspent', 'full']: