forked from LBRYCommunity/lbry-sdk
wip: implementation is now generic and supports multiple currencies
This commit is contained in:
parent
83958604d5
commit
5e71dcbaf0
29 changed files with 1214 additions and 797 deletions
|
@ -29,6 +29,7 @@ ENV_NAMESPACE = 'LBRY_'
|
||||||
LBRYCRD_WALLET = 'lbrycrd'
|
LBRYCRD_WALLET = 'lbrycrd'
|
||||||
LBRYUM_WALLET = 'lbryum'
|
LBRYUM_WALLET = 'lbryum'
|
||||||
PTC_WALLET = 'ptc'
|
PTC_WALLET = 'ptc'
|
||||||
|
TORBA_WALLET = 'torba'
|
||||||
|
|
||||||
PROTOCOL_PREFIX = 'lbry'
|
PROTOCOL_PREFIX = 'lbry'
|
||||||
APP_NAME = 'LBRY'
|
APP_NAME = 'LBRY'
|
||||||
|
|
|
@ -6,17 +6,14 @@ from binascii import hexlify
|
||||||
|
|
||||||
from twisted.internet import defer, reactor, threads
|
from twisted.internet import defer, reactor, threads
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
from orchstr8.wrapper import BaseLbryServiceStack
|
from orchstr8.services import BaseLbryServiceStack
|
||||||
|
|
||||||
from lbrynet.core.call_later_manager import CallLaterManager
|
from lbrynet.core.call_later_manager import CallLaterManager
|
||||||
from lbrynet.database.storage import SQLiteStorage
|
from lbrynet.database.storage import SQLiteStorage
|
||||||
|
|
||||||
from lbrynet.wallet import set_wallet_manager
|
from lbrynet.wallet.basecoin import CoinRegistry
|
||||||
from lbrynet.wallet.wallet import Wallet
|
|
||||||
from lbrynet.wallet.manager import WalletManager
|
from lbrynet.wallet.manager import WalletManager
|
||||||
from lbrynet.wallet.transaction import Transaction, Output
|
from lbrynet.wallet.constants import COIN
|
||||||
from lbrynet.wallet.constants import COIN, REGTEST_CHAIN
|
|
||||||
from lbrynet.wallet.hash import hash160_to_address, address_to_hash_160
|
|
||||||
|
|
||||||
|
|
||||||
class WalletTestCase(unittest.TestCase):
|
class WalletTestCase(unittest.TestCase):
|
||||||
|
@ -27,11 +24,6 @@ class WalletTestCase(unittest.TestCase):
|
||||||
logging.getLogger('lbrynet').setLevel(logging.INFO)
|
logging.getLogger('lbrynet').setLevel(logging.INFO)
|
||||||
self.data_path = tempfile.mkdtemp()
|
self.data_path = tempfile.mkdtemp()
|
||||||
self.db = SQLiteStorage(self.data_path)
|
self.db = SQLiteStorage(self.data_path)
|
||||||
self.config = {
|
|
||||||
'chain': REGTEST_CHAIN,
|
|
||||||
'wallet_path': self.data_path,
|
|
||||||
'default_servers': [('localhost', 50001)]
|
|
||||||
}
|
|
||||||
CallLaterManager.setup(reactor.callLater)
|
CallLaterManager.setup(reactor.callLater)
|
||||||
self.service = BaseLbryServiceStack(self.VERBOSE)
|
self.service = BaseLbryServiceStack(self.VERBOSE)
|
||||||
return self.service.startup()
|
return self.service.startup()
|
||||||
|
@ -52,37 +44,30 @@ class StartupTests(WalletTestCase):
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_balance(self):
|
def test_balance(self):
|
||||||
wallet = Wallet(chain=REGTEST_CHAIN)
|
coin_id = 'lbc_regtest'
|
||||||
manager = WalletManager(self.config, wallet)
|
manager = WalletManager.from_config({
|
||||||
set_wallet_manager(manager)
|
'ledgers': {coin_id: {'default_servers': [('localhost', 50001)]}}
|
||||||
yield manager.start()
|
})
|
||||||
yield self.lbrycrd.generate(1)
|
wallet = manager.create_wallet(None, CoinRegistry.get_coin_class(coin_id))
|
||||||
yield threads.deferToThread(time.sleep, 1)
|
ledger = manager.ledgers.values()[0]
|
||||||
#yield wallet.network.on_header.first
|
account = wallet.default_account
|
||||||
address = manager.get_least_used_receiving_address()
|
coin = account.coin
|
||||||
|
yield manager.start_ledgers()
|
||||||
|
address = account.get_least_used_receiving_address()
|
||||||
sendtxid = yield self.lbrycrd.sendtoaddress(address, 2.5)
|
sendtxid = yield self.lbrycrd.sendtoaddress(address, 2.5)
|
||||||
yield self.lbrycrd.generate(1)
|
yield self.lbrycrd.generate(1)
|
||||||
#yield manager.wallet.history.on_transaction.
|
#yield manager.wallet.history.on_transaction.
|
||||||
yield threads.deferToThread(time.sleep, 10)
|
yield threads.deferToThread(time.sleep, 10)
|
||||||
tx = manager.ledger.transactions.values()[0]
|
utxo = account.get_unspent_utxos()[0]
|
||||||
print(tx.to_python_source())
|
address2 = account.get_least_used_receiving_address()
|
||||||
print(address)
|
tx_class = ledger.transaction_class
|
||||||
output = None
|
Input, Output = tx_class.input_class, tx_class.output_class
|
||||||
for txo in tx.outputs:
|
tx = tx_class()\
|
||||||
other = hash160_to_address(txo.script.values['pubkey_hash'], 'regtest')
|
.add_inputs([Input.spend(utxo)])\
|
||||||
if other == address:
|
.add_outputs([Output.pay_pubkey_hash(2.49*COIN, coin.address_to_hash160(address2))])\
|
||||||
output = txo
|
.sign(account)
|
||||||
break
|
|
||||||
|
|
||||||
address2 = manager.get_least_used_receiving_address()
|
|
||||||
tx = Transaction()
|
|
||||||
tx.add_inputs([output.spend()])
|
|
||||||
Output.pay_pubkey_hash(tx, 0, 2.49*COIN, address_to_hash_160(address2))
|
|
||||||
print(tx.to_python_source())
|
|
||||||
tx.sign(wallet)
|
|
||||||
print(tx.to_python_source())
|
|
||||||
|
|
||||||
yield self.lbrycrd.decoderawtransaction(hexlify(tx.raw))
|
yield self.lbrycrd.decoderawtransaction(hexlify(tx.raw))
|
||||||
yield self.lbrycrd.sendrawtransaction(hexlify(tx.raw))
|
yield self.lbrycrd.sendrawtransaction(hexlify(tx.raw))
|
||||||
|
|
||||||
yield manager.stop()
|
yield manager.stop_ledgers()
|
||||||
|
|
99
lbrynet/tests/unit/wallet/test_account.py
Normal file
99
lbrynet/tests/unit/wallet/test_account.py
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
from twisted.trial import unittest
|
||||||
|
|
||||||
|
from lbrynet.wallet.coins.lbc import LBC
|
||||||
|
from lbrynet.wallet.manager import WalletManager
|
||||||
|
from lbrynet.wallet.wallet import Account
|
||||||
|
|
||||||
|
|
||||||
|
class TestAccount(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
coin = LBC()
|
||||||
|
ledger = coin.ledger_class
|
||||||
|
WalletManager([], {ledger: ledger(coin)}).install()
|
||||||
|
self.coin = coin
|
||||||
|
|
||||||
|
def test_generate_account(self):
|
||||||
|
account = Account.generate(self.coin)
|
||||||
|
self.assertEqual(account.coin, self.coin)
|
||||||
|
self.assertIsNotNone(account.seed)
|
||||||
|
self.assertEqual(account.public_key.coin, self.coin)
|
||||||
|
self.assertEqual(account.private_key.public_key, account.public_key)
|
||||||
|
|
||||||
|
self.assertEqual(len(account.receiving_keys.child_keys), 0)
|
||||||
|
self.assertEqual(len(account.receiving_keys.addresses), 0)
|
||||||
|
self.assertEqual(len(account.change_keys.child_keys), 0)
|
||||||
|
self.assertEqual(len(account.change_keys.addresses), 0)
|
||||||
|
|
||||||
|
account.ensure_enough_addresses()
|
||||||
|
self.assertEqual(len(account.receiving_keys.child_keys), 20)
|
||||||
|
self.assertEqual(len(account.receiving_keys.addresses), 20)
|
||||||
|
self.assertEqual(len(account.change_keys.child_keys), 6)
|
||||||
|
self.assertEqual(len(account.change_keys.addresses), 6)
|
||||||
|
|
||||||
|
def test_generate_account_from_seed(self):
|
||||||
|
account = Account.from_seed(
|
||||||
|
self.coin,
|
||||||
|
"carbon smart garage balance margin twelve chest sword toast envelope bottom stomach ab"
|
||||||
|
"sent"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
account.private_key.extended_key_string(),
|
||||||
|
'LprvXPsFZUGgrX1X9HiyxABZSf6hWJK7kHv4zGZRyyiHbBq5Wu94cE1DMvttnpLYReTPNW4eYwX9dWMvTz3PrB'
|
||||||
|
'wwbRafEeA1ZXL69U2egM4QJdq'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
account.public_key.extended_key_string(),
|
||||||
|
'Lpub2hkYkGHXktBhLpwUhKKogyuJ1M7Gt9EkjFTVKyDqZiZpWdhLuCoT1eKDfXfysMFfG4SzfXXcA2SsHzrjHK'
|
||||||
|
'Ea5aoCNRBAhjT5NPLV6hXtvEi'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
account.receiving_keys.generate_next_address(),
|
||||||
|
'bCqJrLHdoiRqEZ1whFZ3WHNb33bP34SuGx'
|
||||||
|
)
|
||||||
|
private_key = account.get_private_key_for_address('bCqJrLHdoiRqEZ1whFZ3WHNb33bP34SuGx')
|
||||||
|
self.assertEqual(
|
||||||
|
private_key.extended_key_string(),
|
||||||
|
'LprvXTnmVLXGKvRGo2ihBE6LJ771G3VVpAx2zhTJvjnx5P3h6iZ4VJX8PvwTcgzJZ1hqXX61Wpn4pQoP6n2wgp'
|
||||||
|
'S8xjzCM6H2uGzCXuAMy5H9vtA'
|
||||||
|
)
|
||||||
|
self.assertIsNone(account.get_private_key_for_address('BcQjRlhDOIrQez1WHfz3whnB33Bp34sUgX'))
|
||||||
|
|
||||||
|
def test_load_and_save_account(self):
|
||||||
|
account_data = {
|
||||||
|
'seed':
|
||||||
|
"carbon smart garage balance margin twelve chest sword toast envelope bottom stomac"
|
||||||
|
"h absent",
|
||||||
|
'encrypted': False,
|
||||||
|
'private_key':
|
||||||
|
'LprvXPsFZUGgrX1X9HiyxABZSf6hWJK7kHv4zGZRyyiHbBq5Wu94cE1DMvttnpLYReTPNW4eYwX9dWMvTz3PrB'
|
||||||
|
'wwbRafEeA1ZXL69U2egM4QJdq',
|
||||||
|
'public_key':
|
||||||
|
'Lpub2hkYkGHXktBhLpwUhKKogyuJ1M7Gt9EkjFTVKyDqZiZpWdhLuCoT1eKDfXfysMFfG4SzfXXcA2SsHzrjHK'
|
||||||
|
'Ea5aoCNRBAhjT5NPLV6hXtvEi',
|
||||||
|
'receiving_gap': 10,
|
||||||
|
'receiving_keys': [
|
||||||
|
'02c68e2d1cf85404c86244ffa279f4c5cd00331e996d30a86d6e46480e3a9220f4',
|
||||||
|
'03c5a997d0549875d23b8e4bbc7b4d316d962587483f3a2e62ddd90a21043c4941'
|
||||||
|
],
|
||||||
|
'change_gap': 10,
|
||||||
|
'change_keys': [
|
||||||
|
'021460e8d728eee325d0d43128572b2e2bacdc027e420451df100cf9f2154ea5ab'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
account = Account.from_dict(self.coin, account_data)
|
||||||
|
|
||||||
|
self.assertEqual(len(account.receiving_keys.addresses), 2)
|
||||||
|
self.assertEqual(
|
||||||
|
account.receiving_keys.addresses[0],
|
||||||
|
'bCqJrLHdoiRqEZ1whFZ3WHNb33bP34SuGx'
|
||||||
|
)
|
||||||
|
self.assertEqual(len(account.change_keys.addresses), 1)
|
||||||
|
self.assertEqual(
|
||||||
|
account.change_keys.addresses[0],
|
||||||
|
'bFpHENtqugKKHDshKFq2Mnb59Y2bx4vKgL'
|
||||||
|
)
|
||||||
|
|
||||||
|
account_data['coin'] = 'lbc_mainnet'
|
||||||
|
self.assertDictEqual(account_data, account.to_dict())
|
|
@ -1,10 +1,12 @@
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from lbrynet.wallet.constants import CENT, MAXIMUM_FEE_PER_BYTE
|
from lbrynet.wallet.coins.lbc.lbc import LBRYCredits
|
||||||
from lbrynet.wallet.transaction import Transaction, Output
|
from lbrynet.wallet.coins.bitcoin import Bitcoin
|
||||||
from lbrynet.wallet.coinselection import CoinSelector, MAXIMUM_TRIES
|
from lbrynet.wallet.coinselection import CoinSelector, MAXIMUM_TRIES
|
||||||
|
from lbrynet.wallet.constants import CENT
|
||||||
from lbrynet.wallet.manager import WalletManager
|
from lbrynet.wallet.manager import WalletManager
|
||||||
from lbrynet.wallet import set_wallet_manager
|
|
||||||
|
from .test_transaction import get_output as utxo
|
||||||
|
|
||||||
|
|
||||||
NULL_HASH = '\x00'*32
|
NULL_HASH = '\x00'*32
|
||||||
|
@ -15,20 +17,18 @@ def search(*args, **kwargs):
|
||||||
return [o.amount for o in selection] if selection else selection
|
return [o.amount for o in selection] if selection else selection
|
||||||
|
|
||||||
|
|
||||||
def utxo(amount):
|
|
||||||
return Output.pay_pubkey_hash(Transaction(), 0, amount, NULL_HASH)
|
|
||||||
|
|
||||||
|
|
||||||
class TestCoinSelectionTests(unittest.TestCase):
|
class TestCoinSelectionTests(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
set_wallet_manager(WalletManager({'fee_per_byte': MAXIMUM_FEE_PER_BYTE}))
|
WalletManager([], {
|
||||||
|
LBRYCredits.ledger_class: LBRYCredits.ledger_class(LBRYCredits),
|
||||||
|
}).install()
|
||||||
|
|
||||||
def test_empty_coins(self):
|
def test_empty_coins(self):
|
||||||
self.assertIsNone(CoinSelector([], 0, 0).select())
|
self.assertIsNone(CoinSelector([], 0, 0).select())
|
||||||
|
|
||||||
def test_skip_binary_search_if_total_not_enough(self):
|
def test_skip_binary_search_if_total_not_enough(self):
|
||||||
fee = utxo(CENT).spend(fake=True).fee
|
fee = utxo(CENT).spend().fee
|
||||||
big_pool = [utxo(CENT+fee) for _ in range(100)]
|
big_pool = [utxo(CENT+fee) for _ in range(100)]
|
||||||
selector = CoinSelector(big_pool, 101 * CENT, 0)
|
selector = CoinSelector(big_pool, 101 * CENT, 0)
|
||||||
self.assertIsNone(selector.select())
|
self.assertIsNone(selector.select())
|
||||||
|
@ -39,7 +39,7 @@ class TestCoinSelectionTests(unittest.TestCase):
|
||||||
self.assertEqual(selector.tries, 201)
|
self.assertEqual(selector.tries, 201)
|
||||||
|
|
||||||
def test_exact_match(self):
|
def test_exact_match(self):
|
||||||
fee = utxo(CENT).spend(fake=True).fee
|
fee = utxo(CENT).spend().fee
|
||||||
utxo_pool = [
|
utxo_pool = [
|
||||||
utxo(CENT + fee),
|
utxo(CENT + fee),
|
||||||
utxo(CENT),
|
utxo(CENT),
|
||||||
|
@ -74,7 +74,9 @@ class TestOfficialBitcoinCoinSelectionTests(unittest.TestCase):
|
||||||
# https://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf
|
# https://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
set_wallet_manager(WalletManager({'fee_per_byte': 0}))
|
WalletManager([], {
|
||||||
|
Bitcoin.ledger_class: Bitcoin.ledger_class(Bitcoin),
|
||||||
|
}).install()
|
||||||
|
|
||||||
def make_hard_case(self, utxos):
|
def make_hard_case(self, utxos):
|
||||||
target = 0
|
target = 0
|
||||||
|
|
0
lbrynet/tests/unit/wallet/test_ledger.py
Normal file
0
lbrynet/tests/unit/wallet/test_ledger.py
Normal file
|
@ -1,9 +1,11 @@
|
||||||
from binascii import hexlify, unhexlify
|
from binascii import hexlify, unhexlify
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
from lbrynet.wallet.script import Template, ParseError, tokenize, push_data
|
|
||||||
from lbrynet.wallet.script import PUSH_SINGLE, PUSH_MANY, OP_HASH160, OP_EQUAL
|
|
||||||
from lbrynet.wallet.script import InputScript, OutputScript
|
|
||||||
from lbrynet.wallet.bcd_data_stream import BCDataStream
|
from lbrynet.wallet.bcd_data_stream import BCDataStream
|
||||||
|
from lbrynet.wallet.basescript import Template, ParseError, tokenize, push_data
|
||||||
|
from lbrynet.wallet.basescript import PUSH_SINGLE, PUSH_MANY, OP_HASH160, OP_EQUAL
|
||||||
|
from lbrynet.wallet.basescript import BaseInputScript, BaseOutputScript
|
||||||
|
from lbrynet.wallet.coins.lbc.script import OutputScript
|
||||||
|
|
||||||
|
|
||||||
def parse(opcodes, source):
|
def parse(opcodes, source):
|
||||||
|
@ -100,12 +102,12 @@ class TestRedeemPubKeyHash(unittest.TestCase):
|
||||||
|
|
||||||
def redeem_pubkey_hash(self, sig, pubkey):
|
def redeem_pubkey_hash(self, sig, pubkey):
|
||||||
# this checks that factory function correctly sets up the script
|
# this checks that factory function correctly sets up the script
|
||||||
src1 = InputScript.redeem_pubkey_hash(unhexlify(sig), unhexlify(pubkey))
|
src1 = BaseInputScript.redeem_pubkey_hash(unhexlify(sig), unhexlify(pubkey))
|
||||||
self.assertEqual(src1.template.name, 'pubkey_hash')
|
self.assertEqual(src1.template.name, 'pubkey_hash')
|
||||||
self.assertEqual(hexlify(src1.values['signature']), sig)
|
self.assertEqual(hexlify(src1.values['signature']), sig)
|
||||||
self.assertEqual(hexlify(src1.values['pubkey']), pubkey)
|
self.assertEqual(hexlify(src1.values['pubkey']), pubkey)
|
||||||
# now we test that it will round trip
|
# now we test that it will round trip
|
||||||
src2 = InputScript(src1.source)
|
src2 = BaseInputScript(src1.source)
|
||||||
self.assertEqual(src2.template.name, 'pubkey_hash')
|
self.assertEqual(src2.template.name, 'pubkey_hash')
|
||||||
self.assertEqual(hexlify(src2.values['signature']), sig)
|
self.assertEqual(hexlify(src2.values['signature']), sig)
|
||||||
self.assertEqual(hexlify(src2.values['pubkey']), pubkey)
|
self.assertEqual(hexlify(src2.values['pubkey']), pubkey)
|
||||||
|
@ -128,7 +130,7 @@ class TestRedeemScriptHash(unittest.TestCase):
|
||||||
|
|
||||||
def redeem_script_hash(self, sigs, pubkeys):
|
def redeem_script_hash(self, sigs, pubkeys):
|
||||||
# this checks that factory function correctly sets up the script
|
# this checks that factory function correctly sets up the script
|
||||||
src1 = InputScript.redeem_script_hash(
|
src1 = BaseInputScript.redeem_script_hash(
|
||||||
[unhexlify(sig) for sig in sigs],
|
[unhexlify(sig) for sig in sigs],
|
||||||
[unhexlify(pubkey) for pubkey in pubkeys]
|
[unhexlify(pubkey) for pubkey in pubkeys]
|
||||||
)
|
)
|
||||||
|
@ -139,7 +141,7 @@ class TestRedeemScriptHash(unittest.TestCase):
|
||||||
self.assertEqual(subscript1.values['signatures_count'], len(sigs))
|
self.assertEqual(subscript1.values['signatures_count'], len(sigs))
|
||||||
self.assertEqual(subscript1.values['pubkeys_count'], len(pubkeys))
|
self.assertEqual(subscript1.values['pubkeys_count'], len(pubkeys))
|
||||||
# now we test that it will round trip
|
# now we test that it will round trip
|
||||||
src2 = InputScript(src1.source)
|
src2 = BaseInputScript(src1.source)
|
||||||
subscript2 = src2.values['script']
|
subscript2 = src2.values['script']
|
||||||
self.assertEqual(src2.template.name, 'script_hash')
|
self.assertEqual(src2.template.name, 'script_hash')
|
||||||
self.assertEqual([hexlify(v) for v in src2.values['signatures']], sigs)
|
self.assertEqual([hexlify(v) for v in src2.values['signatures']], sigs)
|
||||||
|
@ -181,11 +183,11 @@ class TestPayPubKeyHash(unittest.TestCase):
|
||||||
|
|
||||||
def pay_pubkey_hash(self, pubkey_hash):
|
def pay_pubkey_hash(self, pubkey_hash):
|
||||||
# this checks that factory function correctly sets up the script
|
# this checks that factory function correctly sets up the script
|
||||||
src1 = OutputScript.pay_pubkey_hash(unhexlify(pubkey_hash))
|
src1 = BaseOutputScript.pay_pubkey_hash(unhexlify(pubkey_hash))
|
||||||
self.assertEqual(src1.template.name, 'pay_pubkey_hash')
|
self.assertEqual(src1.template.name, 'pay_pubkey_hash')
|
||||||
self.assertEqual(hexlify(src1.values['pubkey_hash']), pubkey_hash)
|
self.assertEqual(hexlify(src1.values['pubkey_hash']), pubkey_hash)
|
||||||
# now we test that it will round trip
|
# now we test that it will round trip
|
||||||
src2 = OutputScript(src1.source)
|
src2 = BaseOutputScript(src1.source)
|
||||||
self.assertEqual(src2.template.name, 'pay_pubkey_hash')
|
self.assertEqual(src2.template.name, 'pay_pubkey_hash')
|
||||||
self.assertEqual(hexlify(src2.values['pubkey_hash']), pubkey_hash)
|
self.assertEqual(hexlify(src2.values['pubkey_hash']), pubkey_hash)
|
||||||
return hexlify(src1.source)
|
return hexlify(src1.source)
|
||||||
|
@ -201,11 +203,11 @@ class TestPayScriptHash(unittest.TestCase):
|
||||||
|
|
||||||
def pay_script_hash(self, script_hash):
|
def pay_script_hash(self, script_hash):
|
||||||
# this checks that factory function correctly sets up the script
|
# this checks that factory function correctly sets up the script
|
||||||
src1 = OutputScript.pay_script_hash(unhexlify(script_hash))
|
src1 = BaseOutputScript.pay_script_hash(unhexlify(script_hash))
|
||||||
self.assertEqual(src1.template.name, 'pay_script_hash')
|
self.assertEqual(src1.template.name, 'pay_script_hash')
|
||||||
self.assertEqual(hexlify(src1.values['script_hash']), script_hash)
|
self.assertEqual(hexlify(src1.values['script_hash']), script_hash)
|
||||||
# now we test that it will round trip
|
# now we test that it will round trip
|
||||||
src2 = OutputScript(src1.source)
|
src2 = BaseOutputScript(src1.source)
|
||||||
self.assertEqual(src2.template.name, 'pay_script_hash')
|
self.assertEqual(src2.template.name, 'pay_script_hash')
|
||||||
self.assertEqual(hexlify(src2.values['script_hash']), script_hash)
|
self.assertEqual(hexlify(src2.values['script_hash']), script_hash)
|
||||||
return hexlify(src1.source)
|
return hexlify(src1.source)
|
||||||
|
@ -221,7 +223,8 @@ class TestPayClaimNamePubkeyHash(unittest.TestCase):
|
||||||
|
|
||||||
def pay_claim_name_pubkey_hash(self, name, claim, pubkey_hash):
|
def pay_claim_name_pubkey_hash(self, name, claim, pubkey_hash):
|
||||||
# this checks that factory function correctly sets up the script
|
# this checks that factory function correctly sets up the script
|
||||||
src1 = OutputScript.pay_claim_name_pubkey_hash(name, unhexlify(claim), unhexlify(pubkey_hash))
|
src1 = OutputScript.pay_claim_name_pubkey_hash(
|
||||||
|
name, unhexlify(claim), unhexlify(pubkey_hash))
|
||||||
self.assertEqual(src1.template.name, 'claim_name+pay_pubkey_hash')
|
self.assertEqual(src1.template.name, 'claim_name+pay_pubkey_hash')
|
||||||
self.assertEqual(src1.values['claim_name'], name)
|
self.assertEqual(src1.values['claim_name'], name)
|
||||||
self.assertEqual(hexlify(src1.values['claim']), claim)
|
self.assertEqual(hexlify(src1.values['claim']), claim)
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
from binascii import hexlify, unhexlify
|
from binascii import hexlify, unhexlify
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
from lbrynet.wallet.constants import CENT
|
|
||||||
from lbrynet.wallet.transaction import Transaction, Input, Output
|
from lbrynet.wallet.account import Account
|
||||||
|
from lbrynet.wallet.coins.lbc import LBC
|
||||||
|
from lbrynet.wallet.coins.lbc.transaction import Transaction, Output, Input
|
||||||
|
from lbrynet.wallet.constants import CENT, COIN
|
||||||
from lbrynet.wallet.manager import WalletManager
|
from lbrynet.wallet.manager import WalletManager
|
||||||
from lbrynet.wallet import set_wallet_manager
|
from lbrynet.wallet.wallet import Wallet
|
||||||
from lbrynet.wallet.bip32 import PrivateKey
|
|
||||||
from lbrynet.wallet.mnemonic import Mnemonic
|
|
||||||
|
|
||||||
|
|
||||||
NULL_HASH = '\x00'*32
|
NULL_HASH = '\x00'*32
|
||||||
|
@ -13,68 +14,78 @@ FEE_PER_BYTE = 50
|
||||||
FEE_PER_CHAR = 200000
|
FEE_PER_CHAR = 200000
|
||||||
|
|
||||||
|
|
||||||
|
def get_output(amount=CENT, pubkey_hash=NULL_HASH):
|
||||||
|
return Transaction() \
|
||||||
|
.add_outputs([Output.pay_pubkey_hash(amount, pubkey_hash)]) \
|
||||||
|
.outputs[0]
|
||||||
|
|
||||||
|
|
||||||
|
def get_input():
|
||||||
|
return Input.spend(get_output())
|
||||||
|
|
||||||
|
|
||||||
|
def get_transaction(txo=None):
|
||||||
|
return Transaction() \
|
||||||
|
.add_inputs([get_input()]) \
|
||||||
|
.add_outputs([txo or Output.pay_pubkey_hash(CENT, NULL_HASH)])
|
||||||
|
|
||||||
|
|
||||||
|
def get_claim_transaction(claim_name, claim=''):
|
||||||
|
return get_transaction(
|
||||||
|
Output.pay_claim_name_pubkey_hash(CENT, claim_name, claim, NULL_HASH)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_lbc_wallet():
|
||||||
|
lbc = LBC.from_dict({
|
||||||
|
'fee_per_byte': FEE_PER_BYTE,
|
||||||
|
'fee_per_name_char': FEE_PER_CHAR
|
||||||
|
})
|
||||||
|
return Wallet('Main', [lbc], [Account.generate(lbc)])
|
||||||
|
|
||||||
|
|
||||||
class TestSizeAndFeeEstimation(unittest.TestCase):
|
class TestSizeAndFeeEstimation(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
set_wallet_manager(WalletManager({
|
self.wallet = get_lbc_wallet()
|
||||||
'fee_per_byte': FEE_PER_BYTE,
|
self.coin = self.wallet.coins[0]
|
||||||
'fee_per_name_char': FEE_PER_CHAR
|
WalletManager([self.wallet], {})
|
||||||
}))
|
|
||||||
|
|
||||||
@staticmethod
|
def io_fee(self, io):
|
||||||
def get_output():
|
return self.coin.get_input_output_fee(io)
|
||||||
return Output.pay_pubkey_hash(Transaction(), 1, CENT, NULL_HASH)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_input(cls):
|
|
||||||
return cls.get_output().spend(fake=True)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_transaction(cls):
|
|
||||||
tx = Transaction()
|
|
||||||
Output.pay_pubkey_hash(tx, 1, CENT, NULL_HASH)
|
|
||||||
tx.add_inputs([cls.get_input()])
|
|
||||||
return tx
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_claim_transaction(cls, claim_name, claim=''):
|
|
||||||
tx = Transaction()
|
|
||||||
Output.pay_claim_name_pubkey_hash(tx, 1, CENT, claim_name, claim, NULL_HASH)
|
|
||||||
tx.add_inputs([cls.get_input()])
|
|
||||||
return tx
|
|
||||||
|
|
||||||
def test_output_size_and_fee(self):
|
def test_output_size_and_fee(self):
|
||||||
txo = self.get_output()
|
txo = get_output()
|
||||||
self.assertEqual(txo.size, 46)
|
self.assertEqual(txo.size, 46)
|
||||||
self.assertEqual(txo.fee, 46 * FEE_PER_BYTE)
|
self.assertEqual(self.io_fee(txo), 46 * FEE_PER_BYTE)
|
||||||
|
|
||||||
def test_input_size_and_fee(self):
|
def test_input_size_and_fee(self):
|
||||||
txi = self.get_input()
|
txi = get_input()
|
||||||
self.assertEqual(txi.size, 148)
|
self.assertEqual(txi.size, 148)
|
||||||
self.assertEqual(txi.fee, 148 * FEE_PER_BYTE)
|
self.assertEqual(self.io_fee(txi), 148 * FEE_PER_BYTE)
|
||||||
|
|
||||||
def test_transaction_size_and_fee(self):
|
def test_transaction_size_and_fee(self):
|
||||||
tx = self.get_transaction()
|
tx = get_transaction()
|
||||||
base_size = tx.size - 1 - tx.inputs[0].size
|
base_size = tx.size - 1 - tx.inputs[0].size
|
||||||
self.assertEqual(tx.size, 204)
|
self.assertEqual(tx.size, 204)
|
||||||
self.assertEqual(tx.base_size, base_size)
|
self.assertEqual(tx.base_size, base_size)
|
||||||
self.assertEqual(tx.base_fee, FEE_PER_BYTE * base_size)
|
self.assertEqual(self.coin.get_transaction_base_fee(tx), FEE_PER_BYTE * base_size)
|
||||||
|
|
||||||
def test_claim_name_transaction_size_and_fee(self):
|
def test_claim_name_transaction_size_and_fee(self):
|
||||||
# fee based on claim name is the larger fee
|
# fee based on claim name is the larger fee
|
||||||
claim_name = 'verylongname'
|
claim_name = 'verylongname'
|
||||||
tx = self.get_claim_transaction(claim_name, '0'*4000)
|
tx = get_claim_transaction(claim_name, '0'*4000)
|
||||||
base_size = tx.size - 1 - tx.inputs[0].size
|
base_size = tx.size - 1 - tx.inputs[0].size
|
||||||
self.assertEqual(tx.size, 4225)
|
self.assertEqual(tx.size, 4225)
|
||||||
self.assertEqual(tx.base_size, base_size)
|
self.assertEqual(tx.base_size, base_size)
|
||||||
self.assertEqual(tx.base_fee, len(claim_name) * FEE_PER_CHAR)
|
self.assertEqual(self.coin.get_transaction_base_fee(tx), len(claim_name) * FEE_PER_CHAR)
|
||||||
# fee based on total bytes is the larger fee
|
# fee based on total bytes is the larger fee
|
||||||
claim_name = 'a'
|
claim_name = 'a'
|
||||||
tx = self.get_claim_transaction(claim_name, '0'*4000)
|
tx = get_claim_transaction(claim_name, '0'*4000)
|
||||||
base_size = tx.size - 1 - tx.inputs[0].size
|
base_size = tx.size - 1 - tx.inputs[0].size
|
||||||
self.assertEqual(tx.size, 4214)
|
self.assertEqual(tx.size, 4214)
|
||||||
self.assertEqual(tx.base_size, base_size)
|
self.assertEqual(tx.base_size, base_size)
|
||||||
self.assertEqual(tx.base_fee, FEE_PER_BYTE * base_size)
|
self.assertEqual(self.coin.get_transaction_base_fee(tx), FEE_PER_BYTE * base_size)
|
||||||
|
|
||||||
|
|
||||||
class TestTransactionSerialization(unittest.TestCase):
|
class TestTransactionSerialization(unittest.TestCase):
|
||||||
|
@ -92,7 +103,7 @@ class TestTransactionSerialization(unittest.TestCase):
|
||||||
self.assertEqual(len(tx.outputs), 1)
|
self.assertEqual(len(tx.outputs), 1)
|
||||||
|
|
||||||
coinbase = tx.inputs[0]
|
coinbase = tx.inputs[0]
|
||||||
self.assertEqual(coinbase.output_tx_hash, NULL_HASH)
|
self.assertEqual(coinbase.output_txid, NULL_HASH)
|
||||||
self.assertEqual(coinbase.output_index, 0xFFFFFFFF)
|
self.assertEqual(coinbase.output_index, 0xFFFFFFFF)
|
||||||
self.assertEqual(coinbase.sequence, 0xFFFFFFFF)
|
self.assertEqual(coinbase.sequence, 0xFFFFFFFF)
|
||||||
self.assertTrue(coinbase.is_coinbase)
|
self.assertTrue(coinbase.is_coinbase)
|
||||||
|
@ -125,7 +136,7 @@ class TestTransactionSerialization(unittest.TestCase):
|
||||||
self.assertEqual(len(tx.outputs), 1)
|
self.assertEqual(len(tx.outputs), 1)
|
||||||
|
|
||||||
coinbase = tx.inputs[0]
|
coinbase = tx.inputs[0]
|
||||||
self.assertEqual(coinbase.output_tx_hash, NULL_HASH)
|
self.assertEqual(coinbase.output_txid, NULL_HASH)
|
||||||
self.assertEqual(coinbase.output_index, 0xFFFFFFFF)
|
self.assertEqual(coinbase.output_index, 0xFFFFFFFF)
|
||||||
self.assertEqual(coinbase.sequence, 0)
|
self.assertEqual(coinbase.sequence, 0)
|
||||||
self.assertTrue(coinbase.is_coinbase)
|
self.assertTrue(coinbase.is_coinbase)
|
||||||
|
@ -166,9 +177,9 @@ class TestTransactionSerialization(unittest.TestCase):
|
||||||
self.assertEqual(len(tx.inputs), 1)
|
self.assertEqual(len(tx.inputs), 1)
|
||||||
self.assertEqual(len(tx.outputs), 2)
|
self.assertEqual(len(tx.outputs), 2)
|
||||||
|
|
||||||
txin = tx.inputs[0] # type: Input
|
txin = tx.inputs[0]
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
hexlify(txin.output_tx_hash[::-1]),
|
hexlify(txin.output_txid[::-1]),
|
||||||
b'1dfd535b6c8550ebe95abceb877f92f76f30e5ba4d3483b043386027b3e13324'
|
b'1dfd535b6c8550ebe95abceb877f92f76f30e5ba4d3483b043386027b3e13324'
|
||||||
)
|
)
|
||||||
self.assertEqual(txin.output_index, 0)
|
self.assertEqual(txin.output_index, 0)
|
||||||
|
@ -186,7 +197,7 @@ class TestTransactionSerialization(unittest.TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Claim
|
# Claim
|
||||||
out0 = tx.outputs[0] # type: Output
|
out0 = tx.outputs[0]
|
||||||
self.assertEqual(out0.amount, 10000000)
|
self.assertEqual(out0.amount, 10000000)
|
||||||
self.assertEqual(out0.index, 0)
|
self.assertEqual(out0.index, 0)
|
||||||
self.assertTrue(out0.script.is_pay_pubkey_hash)
|
self.assertTrue(out0.script.is_pay_pubkey_hash)
|
||||||
|
@ -199,7 +210,7 @@ class TestTransactionSerialization(unittest.TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Change
|
# Change
|
||||||
out1 = tx.outputs[1] # type: Output
|
out1 = tx.outputs[1]
|
||||||
self.assertEqual(out1.amount, 189977100)
|
self.assertEqual(out1.amount, 189977100)
|
||||||
self.assertEqual(out1.index, 1)
|
self.assertEqual(out1.index, 1)
|
||||||
self.assertTrue(out1.script.is_pay_pubkey_hash)
|
self.assertTrue(out1.script.is_pay_pubkey_hash)
|
||||||
|
@ -215,15 +226,27 @@ class TestTransactionSerialization(unittest.TestCase):
|
||||||
|
|
||||||
class TestTransactionSigning(unittest.TestCase):
|
class TestTransactionSigning(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.private_key = PrivateKey.from_seed(Mnemonic.mnemonic_to_seed(
|
|
||||||
'program leader library giant team normal suspect crater pair miracle sweet until absent'
|
|
||||||
))
|
|
||||||
|
|
||||||
def test_sign(self):
|
def test_sign(self):
|
||||||
tx = Transaction()
|
lbc = LBC()
|
||||||
Output.pay_pubkey_hash(Transaction(), 0, CENT, NULL_HASH).spend(fake=True)
|
wallet = Wallet('Main', [lbc], [Account.from_seed(
|
||||||
tx.add_inputs([self.get_input()])
|
lbc, 'carbon smart garage balance margin twelve chest sword toast envelope '
|
||||||
Output.pay_pubkey_hash(tx, 0, CENT, NULL_HASH)
|
'bottom stomach absent'
|
||||||
tx = self.get_tx()
|
)])
|
||||||
|
account = wallet.default_account
|
||||||
|
|
||||||
|
address1 = account.receiving_keys.generate_next_address()
|
||||||
|
address2 = account.receiving_keys.generate_next_address()
|
||||||
|
pubkey_hash1 = account.coin.address_to_hash160(address1)
|
||||||
|
pubkey_hash2 = account.coin.address_to_hash160(address2)
|
||||||
|
|
||||||
|
tx = Transaction() \
|
||||||
|
.add_inputs([Input.spend(get_output(2*COIN, pubkey_hash1))]) \
|
||||||
|
.add_outputs([Output.pay_pubkey_hash(1.9*COIN, pubkey_hash2)]) \
|
||||||
|
.sign(account)
|
||||||
|
|
||||||
|
print(hexlify(tx.inputs[0].script.values['signature']))
|
||||||
|
self.assertEqual(
|
||||||
|
hexlify(tx.inputs[0].script.values['signature']),
|
||||||
|
b'304402200dafa26ad7cf38c5a971c8a25ce7d85a076235f146126762296b1223c42ae21e022020ef9eeb8'
|
||||||
|
b'398327891008c5c0be4357683f12cb22346691ff23914f457bf679601'
|
||||||
|
)
|
||||||
|
|
|
@ -1,83 +1,84 @@
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
from lbrynet.wallet.wallet import Account, Wallet
|
|
||||||
|
from lbrynet.wallet.coins.bitcoin import BTC
|
||||||
|
from lbrynet.wallet.coins.lbc import LBC
|
||||||
from lbrynet.wallet.manager import WalletManager
|
from lbrynet.wallet.manager import WalletManager
|
||||||
from lbrynet.wallet import set_wallet_manager
|
from lbrynet.wallet.wallet import Account, Wallet, WalletStorage
|
||||||
|
|
||||||
|
|
||||||
class TestWalletAccount(unittest.TestCase):
|
class TestWalletCreation(unittest.TestCase):
|
||||||
|
|
||||||
def test_wallet_automatically_creates_default_account(self):
|
def setUp(self):
|
||||||
|
WalletManager([], {
|
||||||
|
LBC.ledger_class: LBC.ledger_class(LBC),
|
||||||
|
BTC.ledger_class: BTC.ledger_class(BTC)
|
||||||
|
}).install()
|
||||||
|
self.coin = LBC()
|
||||||
|
|
||||||
|
def test_create_wallet_and_accounts(self):
|
||||||
wallet = Wallet()
|
wallet = Wallet()
|
||||||
set_wallet_manager(WalletManager(wallet=wallet))
|
self.assertEqual(wallet.name, 'Wallet')
|
||||||
account = wallet.default_account # type: Account
|
self.assertEqual(wallet.coins, [])
|
||||||
self.assertIsInstance(account, Account)
|
self.assertEqual(wallet.accounts, [])
|
||||||
self.assertEqual(len(account.receiving_keys.child_keys), 0)
|
|
||||||
self.assertEqual(len(account.receiving_keys.addresses), 0)
|
account1 = wallet.generate_account(LBC)
|
||||||
self.assertEqual(len(account.change_keys.child_keys), 0)
|
account2 = wallet.generate_account(LBC)
|
||||||
self.assertEqual(len(account.change_keys.addresses), 0)
|
account3 = wallet.generate_account(BTC)
|
||||||
|
self.assertEqual(wallet.default_account, account1)
|
||||||
|
self.assertEqual(len(wallet.coins), 2)
|
||||||
|
self.assertEqual(len(wallet.accounts), 3)
|
||||||
|
self.assertIsInstance(wallet.coins[0], LBC)
|
||||||
|
self.assertIsInstance(wallet.coins[1], BTC)
|
||||||
|
|
||||||
|
self.assertEqual(len(account1.receiving_keys.addresses), 0)
|
||||||
|
self.assertEqual(len(account1.change_keys.addresses), 0)
|
||||||
|
self.assertEqual(len(account2.receiving_keys.addresses), 0)
|
||||||
|
self.assertEqual(len(account2.change_keys.addresses), 0)
|
||||||
|
self.assertEqual(len(account3.receiving_keys.addresses), 0)
|
||||||
|
self.assertEqual(len(account3.change_keys.addresses), 0)
|
||||||
wallet.ensure_enough_addresses()
|
wallet.ensure_enough_addresses()
|
||||||
self.assertEqual(len(account.receiving_keys.child_keys), 20)
|
self.assertEqual(len(account1.receiving_keys.addresses), 20)
|
||||||
self.assertEqual(len(account.receiving_keys.addresses), 20)
|
self.assertEqual(len(account1.change_keys.addresses), 6)
|
||||||
self.assertEqual(len(account.change_keys.child_keys), 6)
|
self.assertEqual(len(account2.receiving_keys.addresses), 20)
|
||||||
self.assertEqual(len(account.change_keys.addresses), 6)
|
self.assertEqual(len(account2.change_keys.addresses), 6)
|
||||||
|
self.assertEqual(len(account3.receiving_keys.addresses), 20)
|
||||||
|
self.assertEqual(len(account3.change_keys.addresses), 6)
|
||||||
|
|
||||||
def test_generate_account_from_seed(self):
|
def test_load_and_save_wallet(self):
|
||||||
account = Account.generate_from_seed(
|
wallet_dict = {
|
||||||
"carbon smart garage balance margin twelve chest sword toast envelope bottom stomach ab"
|
|
||||||
"sent"
|
|
||||||
) # type: Account
|
|
||||||
self.assertEqual(
|
|
||||||
account.private_key.extended_key_string(),
|
|
||||||
"xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7DRNLEoB8HoirMgH969NrgL8jNzLEeg"
|
|
||||||
"qFzPRWM37GXd4uE8uuRkx4LAe",
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
account.public_key.extended_key_string(),
|
|
||||||
"xpub661MyMwAqRbcGWtPvbWh9sc2BCfw2cTeVDYF23o3N1t6UZ5wv3EMmDgp66FxHuDtWdft3B5eL5xQtyzAtk"
|
|
||||||
"dmhhC95gjRjLzSTdkho95asu9",
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
account.receiving_keys.generate_next_address(),
|
|
||||||
'bCqJrLHdoiRqEZ1whFZ3WHNb33bP34SuGx'
|
|
||||||
)
|
|
||||||
private_key = account.get_private_key_for_address('bCqJrLHdoiRqEZ1whFZ3WHNb33bP34SuGx')
|
|
||||||
self.assertEqual(
|
|
||||||
private_key.extended_key_string(),
|
|
||||||
'xprv9vwXVierUTT4hmoe3dtTeBfbNv1ph2mm8RWXARU6HsZjBaAoFaS2FRQu4fptRAyJWhJW42dmsEaC1nKnVK'
|
|
||||||
'KTMhq3TVEHsNj1ca3ciZMKktT'
|
|
||||||
)
|
|
||||||
self.assertIsNone(account.get_private_key_for_address('BcQjRlhDOIrQez1WHfz3whnB33Bp34sUgX'))
|
|
||||||
|
|
||||||
def test_load_and_save_account(self):
|
|
||||||
wallet_data = {
|
|
||||||
'name': 'Main Wallet',
|
'name': 'Main Wallet',
|
||||||
'accounts': {
|
'accounts': [
|
||||||
0: {
|
{
|
||||||
|
'coin': 'lbc_mainnet',
|
||||||
'seed':
|
'seed':
|
||||||
"carbon smart garage balance margin twelve chest sword toast envelope botto"
|
"carbon smart garage balance margin twelve chest sword toast envelope botto"
|
||||||
"m stomach absent",
|
"m stomach absent",
|
||||||
'encrypted': False,
|
'encrypted': False,
|
||||||
'private_key':
|
'private_key':
|
||||||
"xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7DRNLEoB8HoirMgH969"
|
'LprvXPsFZUGgrX1X9HiyxABZSf6hWJK7kHv4zGZRyyiHbBq5Wu94cE1DMvttnpLYReTPNW4eYwX9dWMvTz3PrB'
|
||||||
"NrgL8jNzLEegqFzPRWM37GXd4uE8uuRkx4LAe",
|
'wwbRafEeA1ZXL69U2egM4QJdq',
|
||||||
'public_key':
|
'public_key':
|
||||||
"xpub661MyMwAqRbcGWtPvbWh9sc2BCfw2cTeVDYF23o3N1t6UZ5wv3EMmDgp66FxHuDtWdft3B"
|
'Lpub2hkYkGHXktBhLpwUhKKogyuJ1M7Gt9EkjFTVKyDqZiZpWdhLuCoT1eKDfXfysMFfG4SzfXXcA2SsHzrjHK'
|
||||||
"5eL5xQtyzAtkdmhhC95gjRjLzSTdkho95asu9",
|
'Ea5aoCNRBAhjT5NPLV6hXtvEi',
|
||||||
'receiving_gap': 10,
|
'receiving_gap': 10,
|
||||||
'receiving_keys': [
|
'receiving_keys': [
|
||||||
'02c68e2d1cf85404c86244ffa279f4c5cd00331e996d30a86d6e46480e3a9220f4',
|
'02c68e2d1cf85404c86244ffa279f4c5cd00331e996d30a86d6e46480e3a9220f4',
|
||||||
'03c5a997d0549875d23b8e4bbc7b4d316d962587483f3a2e62ddd90a21043c4941'],
|
'03c5a997d0549875d23b8e4bbc7b4d316d962587483f3a2e62ddd90a21043c4941'
|
||||||
|
],
|
||||||
'change_gap': 10,
|
'change_gap': 10,
|
||||||
'change_keys': [
|
'change_keys': [
|
||||||
'021460e8d728eee325d0d43128572b2e2bacdc027e420451df100cf9f2154ea5ab']
|
'021460e8d728eee325d0d43128572b2e2bacdc027e420451df100cf9f2154ea5ab'
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
wallet = Wallet.from_json(wallet_data)
|
storage = WalletStorage(default=wallet_dict)
|
||||||
set_wallet_manager(WalletManager(wallet=wallet))
|
wallet = Wallet.from_storage(storage)
|
||||||
self.assertEqual(wallet.name, 'Main Wallet')
|
self.assertEqual(wallet.name, 'Main Wallet')
|
||||||
|
self.assertEqual(len(wallet.coins), 1)
|
||||||
|
self.assertIsInstance(wallet.coins[0], LBC)
|
||||||
|
self.assertEqual(len(wallet.accounts), 1)
|
||||||
account = wallet.default_account
|
account = wallet.default_account
|
||||||
self.assertIsInstance(account, Account)
|
self.assertIsInstance(account, Account)
|
||||||
|
|
||||||
|
@ -91,8 +92,5 @@ class TestWalletAccount(unittest.TestCase):
|
||||||
account.change_keys.addresses[0],
|
account.change_keys.addresses[0],
|
||||||
'bFpHENtqugKKHDshKFq2Mnb59Y2bx4vKgL'
|
'bFpHENtqugKKHDshKFq2Mnb59Y2bx4vKgL'
|
||||||
)
|
)
|
||||||
|
wallet_dict['coins'] = {'lbc_mainnet': {'fee_per_name_char': 200000, 'fee_per_byte': 50}}
|
||||||
self.assertDictEqual(
|
self.assertDictEqual(wallet_dict, wallet.to_dict())
|
||||||
wallet_data['accounts'][0],
|
|
||||||
account.to_json()
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,10 +1 @@
|
||||||
_wallet_manager = None
|
import coins
|
||||||
|
|
||||||
|
|
||||||
def set_wallet_manager(wallet_manager):
|
|
||||||
global _wallet_manager
|
|
||||||
_wallet_manager = wallet_manager
|
|
||||||
|
|
||||||
|
|
||||||
def get_wallet_manager():
|
|
||||||
return _wallet_manager
|
|
||||||
|
|
|
@ -1,21 +1,22 @@
|
||||||
|
import itertools
|
||||||
|
from typing import Dict, Generator
|
||||||
from binascii import hexlify, unhexlify
|
from binascii import hexlify, unhexlify
|
||||||
from itertools import chain
|
|
||||||
from lbrynet.wallet import get_wallet_manager
|
from lbrynet.wallet.basecoin import BaseCoin
|
||||||
from lbrynet.wallet.mnemonic import Mnemonic
|
from lbrynet.wallet.mnemonic import Mnemonic
|
||||||
from lbrynet.wallet.bip32 import PrivateKey, PubKey, from_extended_key_string
|
from lbrynet.wallet.bip32 import PrivateKey, PubKey, from_extended_key_string
|
||||||
from lbrynet.wallet.hash import double_sha256, aes_encrypt, aes_decrypt
|
from lbrynet.wallet.hash import double_sha256, aes_encrypt, aes_decrypt
|
||||||
|
|
||||||
from lbryschema.address import public_key_to_address
|
|
||||||
|
|
||||||
|
|
||||||
class KeyChain:
|
class KeyChain:
|
||||||
|
|
||||||
def __init__(self, parent_key, child_keys, gap):
|
def __init__(self, parent_key, child_keys, gap):
|
||||||
|
self.coin = parent_key.coin
|
||||||
self.parent_key = parent_key # type: PubKey
|
self.parent_key = parent_key # type: PubKey
|
||||||
self.child_keys = child_keys
|
self.child_keys = child_keys
|
||||||
self.minimum_gap = gap
|
self.minimum_gap = gap
|
||||||
self.addresses = [
|
self.addresses = [
|
||||||
public_key_to_address(key)
|
self.coin.public_key_to_address(key)
|
||||||
for key in child_keys
|
for key in child_keys
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -23,9 +24,8 @@ class KeyChain:
|
||||||
def has_gap(self):
|
def has_gap(self):
|
||||||
if len(self.addresses) < self.minimum_gap:
|
if len(self.addresses) < self.minimum_gap:
|
||||||
return False
|
return False
|
||||||
ledger = get_wallet_manager().ledger
|
|
||||||
for address in self.addresses[-self.minimum_gap:]:
|
for address in self.addresses[-self.minimum_gap:]:
|
||||||
if ledger.is_address_old(address):
|
if self.coin.ledger.is_address_old(address):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -44,71 +44,77 @@ class KeyChain:
|
||||||
|
|
||||||
class Account:
|
class Account:
|
||||||
|
|
||||||
def __init__(self, seed, encrypted, private_key, public_key, **kwargs):
|
def __init__(self, coin, seed, encrypted, private_key, public_key,
|
||||||
self.seed = seed
|
receiving_keys=None, receiving_gap=20,
|
||||||
self.encrypted = encrypted
|
change_keys=None, change_gap=6):
|
||||||
|
self.coin = coin # type: BaseCoin
|
||||||
|
self.seed = seed # type: str
|
||||||
|
self.encrypted = encrypted # type: bool
|
||||||
self.private_key = private_key # type: PrivateKey
|
self.private_key = private_key # type: PrivateKey
|
||||||
self.public_key = public_key # type: PubKey
|
self.public_key = public_key # type: PubKey
|
||||||
self.receiving_gap = kwargs.get('receiving_gap', 20)
|
self.keychains = (
|
||||||
self.receiving_keys = kwargs.get('receiving_keys') or \
|
KeyChain(public_key.child(0), receiving_keys or [], receiving_gap),
|
||||||
KeyChain(self.public_key.child(0), [], self.receiving_gap)
|
KeyChain(public_key.child(1), change_keys or [], change_gap)
|
||||||
self.change_gap = kwargs.get('change_gap', 6)
|
)
|
||||||
self.change_keys = kwargs.get('change_keys') or \
|
self.receiving_keys, self.change_keys = self.keychains
|
||||||
KeyChain(self.public_key.child(1), [], self.change_gap)
|
|
||||||
self.keychains = [
|
|
||||||
self.receiving_keys, # child: 0
|
|
||||||
self.change_keys # child: 1
|
|
||||||
]
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate(cls):
|
def generate(cls, coin): # type: (BaseCoin) -> Account
|
||||||
seed = Mnemonic().make_seed()
|
seed = Mnemonic().make_seed()
|
||||||
return cls.generate_from_seed(seed)
|
return cls.from_seed(coin, seed)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_from_seed(cls, seed):
|
def from_seed(cls, coin, seed): # type: (BaseCoin, str) -> Account
|
||||||
private_key = cls.get_private_key_from_seed(seed)
|
private_key = cls.get_private_key_from_seed(coin, seed)
|
||||||
return cls(
|
return cls(
|
||||||
seed=seed, encrypted=False,
|
coin=coin, seed=seed, encrypted=False,
|
||||||
private_key=private_key,
|
private_key=private_key,
|
||||||
public_key=private_key.public_key,
|
public_key=private_key.public_key
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_private_key_from_seed(coin, seed): # type: (BaseCoin, str) -> PrivateKey
|
||||||
|
return PrivateKey.from_seed(coin, Mnemonic.mnemonic_to_seed(seed))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, json_data):
|
def from_dict(cls, coin, d): # type: (BaseCoin, Dict) -> Account
|
||||||
data = json_data.copy()
|
if not d['encrypted']:
|
||||||
if not data['encrypted']:
|
private_key = from_extended_key_string(coin, d['private_key'])
|
||||||
data['private_key'] = from_extended_key_string(data['private_key'])
|
public_key = private_key.public_key
|
||||||
data['public_key'] = from_extended_key_string(data['public_key'])
|
else:
|
||||||
data['receiving_keys'] = KeyChain(
|
private_key = d['private_key']
|
||||||
data['public_key'].child(0),
|
public_key = from_extended_key_string(coin, d['public_key'])
|
||||||
[unhexlify(k) for k in data['receiving_keys']],
|
return cls(
|
||||||
data['receiving_gap']
|
coin=coin,
|
||||||
|
seed=d['seed'],
|
||||||
|
encrypted=d['encrypted'],
|
||||||
|
private_key=private_key,
|
||||||
|
public_key=public_key,
|
||||||
|
receiving_keys=map(unhexlify, d['receiving_keys']),
|
||||||
|
receiving_gap=d['receiving_gap'],
|
||||||
|
change_keys=map(unhexlify, d['change_keys']),
|
||||||
|
change_gap=d['change_gap']
|
||||||
)
|
)
|
||||||
data['change_keys'] = KeyChain(
|
|
||||||
data['public_key'].child(1),
|
|
||||||
[unhexlify(k) for k in data['change_keys']],
|
|
||||||
data['change_gap']
|
|
||||||
)
|
|
||||||
return cls(**data)
|
|
||||||
|
|
||||||
def to_json(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
|
'coin': self.coin.get_id(),
|
||||||
'seed': self.seed,
|
'seed': self.seed,
|
||||||
'encrypted': self.encrypted,
|
'encrypted': self.encrypted,
|
||||||
'private_key': self.private_key.extended_key_string(),
|
'private_key': self.private_key if self.encrypted else
|
||||||
|
self.private_key.extended_key_string(),
|
||||||
'public_key': self.public_key.extended_key_string(),
|
'public_key': self.public_key.extended_key_string(),
|
||||||
'receiving_keys': [hexlify(k) for k in self.receiving_keys.child_keys],
|
'receiving_keys': [hexlify(k) for k in self.receiving_keys.child_keys],
|
||||||
'receiving_gap': self.receiving_gap,
|
'receiving_gap': self.receiving_keys.minimum_gap,
|
||||||
'change_keys': [hexlify(k) for k in self.change_keys.child_keys],
|
'change_keys': [hexlify(k) for k in self.change_keys.child_keys],
|
||||||
'change_gap': self.change_gap
|
'change_gap': self.change_keys.minimum_gap
|
||||||
}
|
}
|
||||||
|
|
||||||
def decrypt(self, password):
|
def decrypt(self, password):
|
||||||
assert self.encrypted, "Key is not encrypted."
|
assert self.encrypted, "Key is not encrypted."
|
||||||
secret = double_sha256(password)
|
secret = double_sha256(password)
|
||||||
self.seed = aes_decrypt(secret, self.seed)
|
self.seed = aes_decrypt(secret, self.seed)
|
||||||
self.private_key = from_extended_key_string(aes_decrypt(secret, self.private_key))
|
self.private_key = from_extended_key_string(self.coin, aes_decrypt(secret, self.private_key))
|
||||||
self.encrypted = False
|
self.encrypted = False
|
||||||
|
|
||||||
def encrypt(self, password):
|
def encrypt(self, password):
|
||||||
|
@ -118,13 +124,9 @@ class Account:
|
||||||
self.private_key = aes_encrypt(secret, self.private_key.extended_key_string())
|
self.private_key = aes_encrypt(secret, self.private_key.extended_key_string())
|
||||||
self.encrypted = True
|
self.encrypted = True
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_private_key_from_seed(seed):
|
|
||||||
return PrivateKey.from_seed(Mnemonic.mnemonic_to_seed(seed))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def addresses(self):
|
def addresses(self):
|
||||||
return chain(self.receiving_keys.addresses, self.change_keys.addresses)
|
return itertools.chain(self.receiving_keys.addresses, self.change_keys.addresses)
|
||||||
|
|
||||||
def get_private_key_for_address(self, address):
|
def get_private_key_for_address(self, address):
|
||||||
assert not self.encrypted, "Cannot get private key on encrypted wallet account."
|
assert not self.encrypted, "Cannot get private key on encrypted wallet account."
|
||||||
|
@ -139,3 +141,47 @@ class Account:
|
||||||
for keychain in self.keychains
|
for keychain in self.keychains
|
||||||
for address in keychain.ensure_enough_addresses()
|
for address in keychain.ensure_enough_addresses()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def addresses_without_history(self):
|
||||||
|
for address in self.addresses:
|
||||||
|
if not self.coin.ledger.has_address(address):
|
||||||
|
yield address
|
||||||
|
|
||||||
|
def get_least_used_receiving_address(self, max_transactions=1000):
|
||||||
|
return self._get_least_used_address(
|
||||||
|
self.receiving_keys.addresses,
|
||||||
|
self.receiving_keys,
|
||||||
|
max_transactions
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_least_used_change_address(self, max_transactions=100):
|
||||||
|
return self._get_least_used_address(
|
||||||
|
self.change_keys.addresses,
|
||||||
|
self.change_keys,
|
||||||
|
max_transactions
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_least_used_address(self, addresses, keychain, max_transactions):
|
||||||
|
ledger = self.coin.ledger
|
||||||
|
address = ledger.get_least_used_address(addresses, max_transactions)
|
||||||
|
if address:
|
||||||
|
return address
|
||||||
|
address = keychain.generate_next_address()
|
||||||
|
ledger.subscribe_history(address)
|
||||||
|
return address
|
||||||
|
|
||||||
|
def get_unspent_utxos(self):
|
||||||
|
return [
|
||||||
|
utxo
|
||||||
|
for address in self.addresses
|
||||||
|
for utxo in self.coin.ledger.get_unspent_outputs(address)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsView:
|
||||||
|
|
||||||
|
def __init__(self, accounts):
|
||||||
|
self._accounts_generator = accounts
|
||||||
|
|
||||||
|
def __iter__(self): # type: () -> Generator[Account]
|
||||||
|
return self._accounts_generator()
|
||||||
|
|
83
lbrynet/wallet/basecoin.py
Normal file
83
lbrynet/wallet/basecoin.py
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import six
|
||||||
|
from typing import Dict, Type
|
||||||
|
from .hash import hash160, double_sha256, Base58
|
||||||
|
|
||||||
|
|
||||||
|
class CoinRegistry(type):
|
||||||
|
coins = {} # type: Dict[str, Type[BaseCoin]]
|
||||||
|
|
||||||
|
def __new__(mcs, name, bases, attrs):
|
||||||
|
cls = super(CoinRegistry, mcs).__new__(mcs, name, bases, attrs) # type: Type[BaseCoin]
|
||||||
|
if not (name == 'BaseCoin' and not bases):
|
||||||
|
coin_id = cls.get_id()
|
||||||
|
assert coin_id not in mcs.coins, 'Coin with id "{}" already registered.'.format(coin_id)
|
||||||
|
mcs.coins[coin_id] = cls
|
||||||
|
assert cls.ledger_class.coin_class is None, (
|
||||||
|
"Ledger ({}) which this coin ({}) references is already referenced by another "
|
||||||
|
"coin ({}). One to one relationship between a coin and a ledger is strictly and "
|
||||||
|
"automatically enforced. Make sure that coin_class=None in the ledger and that "
|
||||||
|
"another Coin isn't already referencing this Ledger."
|
||||||
|
).format(cls.ledger_class.__name__, name, cls.ledger_class.coin_class.__name__)
|
||||||
|
# create back reference from ledger to the coin
|
||||||
|
cls.ledger_class.coin_class = cls
|
||||||
|
return cls
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_coin_class(mcs, coin_id): # type: (str) -> Type[BaseCoin]
|
||||||
|
return mcs.coins[coin_id]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_ledger_class(mcs, coin_id): # type: (str) -> Type[BaseLedger]
|
||||||
|
return mcs.coins[coin_id].ledger_class
|
||||||
|
|
||||||
|
|
||||||
|
class BaseCoin(six.with_metaclass(CoinRegistry)):
|
||||||
|
|
||||||
|
name = None
|
||||||
|
symbol = None
|
||||||
|
network = None
|
||||||
|
|
||||||
|
ledger_class = None # type: Type[BaseLedger]
|
||||||
|
transaction_class = None # type: Type[BaseTransaction]
|
||||||
|
|
||||||
|
secret_prefix = None
|
||||||
|
pubkey_address_prefix = None
|
||||||
|
script_address_prefix = None
|
||||||
|
extended_public_key_prefix = None
|
||||||
|
extended_private_key_prefix = None
|
||||||
|
|
||||||
|
def __init__(self, ledger, fee_per_byte):
|
||||||
|
self.ledger = ledger
|
||||||
|
self.fee_per_byte = fee_per_byte
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_id(cls):
|
||||||
|
return '{}_{}'.format(cls.symbol.lower(), cls.network.lower())
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {'fee_per_byte': self.fee_per_byte}
|
||||||
|
|
||||||
|
def get_input_output_fee(self, io):
|
||||||
|
""" Fee based on size of the input / output. """
|
||||||
|
return self.fee_per_byte * io.size
|
||||||
|
|
||||||
|
def get_transaction_base_fee(self, tx):
|
||||||
|
""" Fee for the transaction header and all outputs; without inputs. """
|
||||||
|
return self.fee_per_byte * tx.base_size
|
||||||
|
|
||||||
|
def hash160_to_address(self, h160):
|
||||||
|
raw_address = self.pubkey_address_prefix + h160
|
||||||
|
return Base58.encode(raw_address + double_sha256(raw_address)[0:4])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def address_to_hash160(address):
|
||||||
|
bytes = Base58.decode(address)
|
||||||
|
prefix, pubkey_bytes, addr_checksum = bytes[0], bytes[1:21], bytes[21:]
|
||||||
|
return pubkey_bytes
|
||||||
|
|
||||||
|
def public_key_to_address(self, public_key):
|
||||||
|
return self.hash160_to_address(hash160(public_key))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def private_key_to_wif(private_key):
|
||||||
|
return b'\x1c' + private_key + b'\x01'
|
|
@ -2,13 +2,17 @@ import os
|
||||||
import logging
|
import logging
|
||||||
import hashlib
|
import hashlib
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
|
from typing import List, Dict, Type
|
||||||
|
from binascii import unhexlify
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
from twisted.internet import threads, defer
|
from twisted.internet import threads, defer
|
||||||
|
|
||||||
|
from lbrynet.wallet.account import Account, AccountsView
|
||||||
|
from lbrynet.wallet.basecoin import BaseCoin
|
||||||
|
from lbrynet.wallet.basetransaction import BaseTransaction, BaseInput, BaseOutput
|
||||||
|
from lbrynet.wallet.basenetwork import BaseNetwork
|
||||||
from lbrynet.wallet.stream import StreamController, execute_serially
|
from lbrynet.wallet.stream import StreamController, execute_serially
|
||||||
from lbrynet.wallet.transaction import Transaction
|
|
||||||
from lbrynet.wallet.constants import CHAINS, MAIN_CHAIN, REGTEST_CHAIN, HEADER_SIZE
|
|
||||||
from lbrynet.wallet.util import hex_to_int, int_to_hex, rev_hex, hash_encode
|
from lbrynet.wallet.util import hex_to_int, int_to_hex, rev_hex, hash_encode
|
||||||
from lbrynet.wallet.hash import double_sha256, pow_hash
|
from lbrynet.wallet.hash import double_sha256, pow_hash
|
||||||
|
|
||||||
|
@ -17,43 +21,76 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Address:
|
class Address:
|
||||||
|
|
||||||
def __init__(self, address):
|
def __init__(self, pubkey_hash):
|
||||||
self.address = address
|
self.pubkey_hash = pubkey_hash
|
||||||
self.transactions = []
|
self.transactions = [] # type: List[BaseTransaction]
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.transactions)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.transactions)
|
||||||
|
|
||||||
def add_transaction(self, transaction):
|
def add_transaction(self, transaction):
|
||||||
self.transactions.append(transaction)
|
self.transactions.append(transaction)
|
||||||
|
|
||||||
|
def get_unspent_utxos(self):
|
||||||
|
inputs, outputs, utxos = [], [], []
|
||||||
|
for tx in self:
|
||||||
|
for txi in tx.inputs:
|
||||||
|
inputs.append((txi.output_txid, txi.output_index))
|
||||||
|
for txo in tx.outputs:
|
||||||
|
if txo.script.is_pay_pubkey_hash and txo.script.values['pubkey_hash'] == self.pubkey_hash:
|
||||||
|
outputs.append((txo, txo.transaction.hash, txo.index))
|
||||||
|
for output in set(outputs):
|
||||||
|
if output[1:] not in inputs:
|
||||||
|
yield output[0]
|
||||||
|
|
||||||
class Ledger:
|
|
||||||
|
|
||||||
def __init__(self, config=None, db=None):
|
class BaseLedger:
|
||||||
|
|
||||||
|
# coin_class is automatically set by BaseCoin metaclass
|
||||||
|
# when it creates the Coin classes, there is a 1..1 relationship
|
||||||
|
# between a coin and a ledger (at the class level) but a 1..* relationship
|
||||||
|
# at instance level. Only one Ledger instance should exist per coin class,
|
||||||
|
# but many coin instances can exist linking back to the single Ledger instance.
|
||||||
|
coin_class = None # type: Type[BaseCoin]
|
||||||
|
network_class = None # type: Type[BaseNetwork]
|
||||||
|
|
||||||
|
verify_bits_to_target = True
|
||||||
|
|
||||||
|
def __init__(self, accounts, config=None, network=None, db=None):
|
||||||
|
self.accounts = accounts # type: AccountsView
|
||||||
self.config = config or {}
|
self.config = config or {}
|
||||||
self.db = db
|
self.db = db
|
||||||
self.addresses = {}
|
self.addresses = {} # type: Dict[str, Address]
|
||||||
self.transactions = {}
|
self.transactions = {} # type: Dict[str, BaseTransaction]
|
||||||
self.headers = BlockchainHeaders(self.headers_path, self.config.get('chain', MAIN_CHAIN))
|
self.headers = Headers(self)
|
||||||
self._on_transaction_controller = StreamController()
|
self._on_transaction_controller = StreamController()
|
||||||
self.on_transaction = self._on_transaction_controller.stream
|
self.on_transaction = self._on_transaction_controller.stream
|
||||||
|
self.network = network or self.network_class(self.config)
|
||||||
|
self.network.on_header.listen(self.process_header)
|
||||||
|
self.network.on_status.listen(self.process_status)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def headers_path(self):
|
def transaction_class(self):
|
||||||
filename = 'blockchain_headers'
|
return self.coin_class.transaction_class
|
||||||
if self.config.get('chain', MAIN_CHAIN) != MAIN_CHAIN:
|
|
||||||
filename = '{}_headers'.format(self.config['chain'])
|
@classmethod
|
||||||
return os.path.join(self.config.get('wallet_path', ''), filename)
|
def from_json(cls, json_dict):
|
||||||
|
return cls(json_dict)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def load(self):
|
def load(self):
|
||||||
txs = yield self.db.get_transactions()
|
txs = yield self.db.get_transactions()
|
||||||
for tx_hash, raw, height in txs:
|
for tx_hash, raw, height in txs:
|
||||||
self.transactions[tx_hash] = Transaction(raw, height)
|
self.transactions[tx_hash] = self.transaction_class(raw, height)
|
||||||
txios = yield self.db.get_transaction_inputs_and_outputs()
|
txios = yield self.db.get_transaction_inputs_and_outputs()
|
||||||
for tx_hash, address_hash, input_output, amount, height in txios:
|
for tx_hash, address_hash, input_output, amount, height in txios:
|
||||||
tx = self.transactions[tx_hash]
|
tx = self.transactions[tx_hash]
|
||||||
address = self.addresses.get(address_hash)
|
address = self.addresses.get(address_hash)
|
||||||
if address is None:
|
if address is None:
|
||||||
address = self.addresses[address_hash] = Address(address_hash)
|
address = self.addresses[address_hash] = Address(self.coin_class.address_to_hash160(address_hash))
|
||||||
tx.add_txio(address, input_output, amount)
|
tx.add_txio(address, input_output, amount)
|
||||||
address.add_transaction(tx)
|
address.add_transaction(tx)
|
||||||
|
|
||||||
|
@ -68,10 +105,11 @@ class Ledger:
|
||||||
age = tx_age
|
age = tx_age
|
||||||
return age > age_limit
|
return age > age_limit
|
||||||
|
|
||||||
def add_transaction(self, address, transaction):
|
def add_transaction(self, address, transaction): # type: (str, BaseTransaction) -> None
|
||||||
|
if address not in self.addresses:
|
||||||
|
self.addresses[address] = Address(self.coin_class.address_to_hash160(address))
|
||||||
|
self.addresses[address].add_transaction(transaction)
|
||||||
self.transactions.setdefault(hexlify(transaction.id), transaction)
|
self.transactions.setdefault(hexlify(transaction.id), transaction)
|
||||||
self.addresses.setdefault(address, [])
|
|
||||||
self.addresses[address].append(transaction)
|
|
||||||
self._on_transaction_controller.add(transaction)
|
self._on_transaction_controller.add(transaction)
|
||||||
|
|
||||||
def has_address(self, address):
|
def has_address(self, address):
|
||||||
|
@ -109,20 +147,109 @@ class Ledger:
|
||||||
transaction_counts.sort(key=itemgetter(1))
|
transaction_counts.sort(key=itemgetter(1))
|
||||||
return transaction_counts[0]
|
return transaction_counts[0]
|
||||||
|
|
||||||
|
def get_unspent_outputs(self, address):
|
||||||
|
if address in self.addresses:
|
||||||
|
return list(self.addresses[address].get_unspent_utxos())
|
||||||
|
return []
|
||||||
|
|
||||||
class BlockchainHeaders:
|
@defer.inlineCallbacks
|
||||||
|
def start(self):
|
||||||
|
first_connection = self.network.on_connected.first
|
||||||
|
self.network.start()
|
||||||
|
yield first_connection
|
||||||
|
self.headers.touch()
|
||||||
|
yield self.update_headers()
|
||||||
|
yield self.network.subscribe_headers()
|
||||||
|
yield self.update_accounts()
|
||||||
|
|
||||||
def __init__(self, path, chain=MAIN_CHAIN):
|
def stop(self):
|
||||||
self.path = path
|
return self.network.stop()
|
||||||
self.chain = chain
|
|
||||||
self.max_target = CHAINS[chain]['max_target']
|
|
||||||
self.target_timespan = CHAINS[chain]['target_timespan']
|
|
||||||
self.genesis_bits = CHAINS[chain]['genesis_bits']
|
|
||||||
|
|
||||||
|
@execute_serially
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def update_headers(self):
|
||||||
|
while True:
|
||||||
|
height_sought = len(self.headers)
|
||||||
|
headers = yield self.network.get_headers(height_sought)
|
||||||
|
log.info("received {} headers starting at {} height".format(headers['count'], height_sought))
|
||||||
|
if headers['count'] <= 0:
|
||||||
|
break
|
||||||
|
yield self.headers.connect(height_sought, headers['hex'].decode('hex'))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def process_header(self, response):
|
||||||
|
header = response[0]
|
||||||
|
if self.update_headers.is_running:
|
||||||
|
return
|
||||||
|
if header['height'] == len(self.headers):
|
||||||
|
# New header from network directly connects after the last local header.
|
||||||
|
yield self.headers.connect(len(self.headers), header['hex'].decode('hex'))
|
||||||
|
elif header['height'] > len(self.headers):
|
||||||
|
# New header is several heights ahead of local, do download instead.
|
||||||
|
yield self.update_headers()
|
||||||
|
|
||||||
|
@execute_serially
|
||||||
|
def update_accounts(self):
|
||||||
|
return defer.DeferredList([
|
||||||
|
self.update_account(a) for a in self.accounts
|
||||||
|
])
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def update_account(self, account): # type: (Account) -> defer.Defferred
|
||||||
|
# Before subscribing, download history for any addresses that don't have any,
|
||||||
|
# this avoids situation where we're getting status updates to addresses we know
|
||||||
|
# need to update anyways. Continue to get history and create more addresses until
|
||||||
|
# all missing addresses are created and history for them is fully restored.
|
||||||
|
account.ensure_enough_addresses()
|
||||||
|
addresses = list(account.addresses_without_history())
|
||||||
|
while addresses:
|
||||||
|
yield defer.DeferredList([
|
||||||
|
self.update_history(a) for a in addresses
|
||||||
|
])
|
||||||
|
addresses = account.ensure_enough_addresses()
|
||||||
|
|
||||||
|
# By this point all of the addresses should be restored and we
|
||||||
|
# can now subscribe all of them to receive updates.
|
||||||
|
yield defer.DeferredList([
|
||||||
|
self.subscribe_history(address)
|
||||||
|
for address in account.addresses
|
||||||
|
])
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def update_history(self, address):
|
||||||
|
history = yield self.network.get_history(address)
|
||||||
|
for hash in map(itemgetter('tx_hash'), history):
|
||||||
|
transaction = self.get_transaction(hash)
|
||||||
|
if not transaction:
|
||||||
|
raw = yield self.network.get_transaction(hash)
|
||||||
|
transaction = self.transaction_class(unhexlify(raw))
|
||||||
|
self.add_transaction(address, transaction)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def subscribe_history(self, address):
|
||||||
|
status = yield self.network.subscribe_address(address)
|
||||||
|
if status != self.get_status(address):
|
||||||
|
self.update_history(address)
|
||||||
|
|
||||||
|
def process_status(self, response):
|
||||||
|
address, status = response
|
||||||
|
if status != self.get_status(address):
|
||||||
|
self.update_history(address)
|
||||||
|
|
||||||
|
|
||||||
|
class Headers:
|
||||||
|
|
||||||
|
def __init__(self, ledger):
|
||||||
|
self.ledger = ledger
|
||||||
|
self._size = None
|
||||||
self._on_change_controller = StreamController()
|
self._on_change_controller = StreamController()
|
||||||
self.on_changed = self._on_change_controller.stream
|
self.on_changed = self._on_change_controller.stream
|
||||||
|
|
||||||
self._size = None
|
@property
|
||||||
|
def path(self):
|
||||||
|
wallet_path = self.ledger.config.get('wallet_path', '')
|
||||||
|
filename = '{}_headers'.format(self.ledger.coin_class.get_id())
|
||||||
|
return os.path.join(wallet_path, filename)
|
||||||
|
|
||||||
def touch(self):
|
def touch(self):
|
||||||
if not os.path.exists(self.path):
|
if not os.path.exists(self.path):
|
||||||
|
@ -134,13 +261,13 @@ class BlockchainHeaders:
|
||||||
return len(self) - 1
|
return len(self) - 1
|
||||||
|
|
||||||
def sync_read_length(self):
|
def sync_read_length(self):
|
||||||
return os.path.getsize(self.path) / HEADER_SIZE
|
return os.path.getsize(self.path) / self.ledger.header_size
|
||||||
|
|
||||||
def sync_read_header(self, height):
|
def sync_read_header(self, height):
|
||||||
if 0 <= height < len(self):
|
if 0 <= height < len(self):
|
||||||
with open(self.path, 'rb') as f:
|
with open(self.path, 'rb') as f:
|
||||||
f.seek(height * HEADER_SIZE)
|
f.seek(height * self.ledger.header_size)
|
||||||
return f.read(HEADER_SIZE)
|
return f.read(self.ledger.header_size)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
if self._size is None:
|
if self._size is None:
|
||||||
|
@ -168,7 +295,7 @@ class BlockchainHeaders:
|
||||||
previous_header = header
|
previous_header = header
|
||||||
|
|
||||||
with open(self.path, 'r+b') as f:
|
with open(self.path, 'r+b') as f:
|
||||||
f.seek(start * HEADER_SIZE)
|
f.seek(start * self.ledger.header_size)
|
||||||
f.write(headers)
|
f.write(headers)
|
||||||
f.truncate()
|
f.truncate()
|
||||||
|
|
||||||
|
@ -179,9 +306,9 @@ class BlockchainHeaders:
|
||||||
self._on_change_controller.add(change)
|
self._on_change_controller.add(change)
|
||||||
|
|
||||||
def _iterate_headers(self, height, headers):
|
def _iterate_headers(self, height, headers):
|
||||||
assert len(headers) % HEADER_SIZE == 0
|
assert len(headers) % self.ledger.header_size == 0
|
||||||
for idx in range(len(headers) / HEADER_SIZE):
|
for idx in range(len(headers) / self.ledger.header_size):
|
||||||
start, end = idx * HEADER_SIZE, (idx + 1) * HEADER_SIZE
|
start, end = idx * self.ledger.header_size, (idx + 1) * self.ledger.header_size
|
||||||
header = headers[start:end]
|
header = headers[start:end]
|
||||||
yield self._deserialize(height+idx, header)
|
yield self._deserialize(height+idx, header)
|
||||||
|
|
||||||
|
@ -239,10 +366,9 @@ class BlockchainHeaders:
|
||||||
""" See: lbrycrd/src/lbry.cpp """
|
""" See: lbrycrd/src/lbry.cpp """
|
||||||
|
|
||||||
if height == 0:
|
if height == 0:
|
||||||
return self.genesis_bits, self.max_target
|
return self.ledger.genesis_bits, self.ledger.max_target
|
||||||
|
|
||||||
# bits to target
|
if self.ledger.verify_bits_to_target:
|
||||||
if self.chain != REGTEST_CHAIN:
|
|
||||||
bits = last['bits']
|
bits = last['bits']
|
||||||
bitsN = (bits >> 24) & 0xff
|
bitsN = (bits >> 24) & 0xff
|
||||||
assert 0x03 <= bitsN <= 0x1f, \
|
assert 0x03 <= bitsN <= 0x1f, \
|
||||||
|
@ -252,7 +378,7 @@ class BlockchainHeaders:
|
||||||
"Second part of bits should be in [0x8000, 0x7fffff] but it was {}".format(bitsBase)
|
"Second part of bits should be in [0x8000, 0x7fffff] but it was {}".format(bitsBase)
|
||||||
|
|
||||||
# new target
|
# new target
|
||||||
retargetTimespan = self.target_timespan
|
retargetTimespan = self.ledger.target_timespan
|
||||||
nActualTimespan = last['timestamp'] - first['timestamp']
|
nActualTimespan = last['timestamp'] - first['timestamp']
|
||||||
|
|
||||||
nModulatedTimespan = retargetTimespan + (nActualTimespan - retargetTimespan) // 8
|
nModulatedTimespan = retargetTimespan + (nActualTimespan - retargetTimespan) // 8
|
||||||
|
@ -267,7 +393,7 @@ class BlockchainHeaders:
|
||||||
nModulatedTimespan = nMaxTimespan
|
nModulatedTimespan = nMaxTimespan
|
||||||
|
|
||||||
# Retarget
|
# Retarget
|
||||||
bnPowLimit = _ArithUint256(self.max_target)
|
bnPowLimit = _ArithUint256(self.ledger.max_target)
|
||||||
bnNew = _ArithUint256.SetCompact(last['bits'])
|
bnNew = _ArithUint256.SetCompact(last['bits'])
|
||||||
bnNew *= nModulatedTimespan
|
bnNew *= nModulatedTimespan
|
||||||
bnNew //= nModulatedTimespan
|
bnNew //= nModulatedTimespan
|
|
@ -10,7 +10,7 @@ from twisted.protocols.basic import LineOnlyReceiver
|
||||||
from errors import RemoteServiceException, ProtocolException
|
from errors import RemoteServiceException, ProtocolException
|
||||||
from errors import TransportException
|
from errors import TransportException
|
||||||
|
|
||||||
from .stream import StreamController
|
from lbrynet.wallet.stream import StreamController
|
||||||
|
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
|
@ -18,6 +18,8 @@ log = logging.getLogger()
|
||||||
def unicode2bytes(string):
|
def unicode2bytes(string):
|
||||||
if isinstance(string, six.text_type):
|
if isinstance(string, six.text_type):
|
||||||
return string.encode('iso-8859-1')
|
return string.encode('iso-8859-1')
|
||||||
|
elif isinstance(string, list):
|
||||||
|
return [unicode2bytes(s) for s in string]
|
||||||
return string
|
return string
|
||||||
|
|
||||||
|
|
||||||
|
@ -125,7 +127,7 @@ class StratumClientFactory(protocol.ClientFactory):
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
||||||
class Network:
|
class BaseNetwork:
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
|
@ -2,8 +2,8 @@ from itertools import chain
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
from .bcd_data_stream import BCDataStream
|
from lbrynet.wallet.bcd_data_stream import BCDataStream
|
||||||
from .util import subclass_tuple
|
from lbrynet.wallet.util import subclass_tuple
|
||||||
|
|
||||||
# bitcoin opcodes
|
# bitcoin opcodes
|
||||||
OP_0 = 0x00
|
OP_0 = 0x00
|
||||||
|
@ -21,11 +21,6 @@ OP_PUSHDATA4 = 0x4e
|
||||||
OP_2DROP = 0x6d
|
OP_2DROP = 0x6d
|
||||||
OP_DROP = 0x75
|
OP_DROP = 0x75
|
||||||
|
|
||||||
# lbry custom opcodes
|
|
||||||
OP_CLAIM_NAME = 0xb5
|
|
||||||
OP_SUPPORT_CLAIM = 0xb6
|
|
||||||
OP_UPDATE_CLAIM = 0xb7
|
|
||||||
|
|
||||||
|
|
||||||
# template matching opcodes (not real opcodes)
|
# template matching opcodes (not real opcodes)
|
||||||
# base class for PUSH_DATA related opcodes
|
# base class for PUSH_DATA related opcodes
|
||||||
|
@ -289,11 +284,6 @@ class Script(object):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_source_with_template(cls, source, template):
|
def from_source_with_template(cls, source, template):
|
||||||
if template in InputScript.templates:
|
|
||||||
return InputScript(source, template_hint=template)
|
|
||||||
elif template in OutputScript.templates:
|
|
||||||
return OutputScript(source, template_hint=template)
|
|
||||||
else:
|
|
||||||
return cls(source, template_hint=template)
|
return cls(source, template_hint=template)
|
||||||
|
|
||||||
def parse(self, template_hint=None):
|
def parse(self, template_hint=None):
|
||||||
|
@ -313,7 +303,7 @@ class Script(object):
|
||||||
self.source = self.template.generate(self.values)
|
self.source = self.template.generate(self.values)
|
||||||
|
|
||||||
|
|
||||||
class InputScript(Script):
|
class BaseInputScript(Script):
|
||||||
""" Input / redeem script templates (aka scriptSig) """
|
""" Input / redeem script templates (aka scriptSig) """
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
@ -362,7 +352,7 @@ class InputScript(Script):
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class OutputScript(Script):
|
class BaseOutputScript(Script):
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
|
@ -374,48 +364,9 @@ class OutputScript(Script):
|
||||||
OP_HASH160, PUSH_SINGLE('script_hash'), OP_EQUAL
|
OP_HASH160, PUSH_SINGLE('script_hash'), OP_EQUAL
|
||||||
))
|
))
|
||||||
|
|
||||||
CLAIM_NAME_OPCODES = (
|
|
||||||
OP_CLAIM_NAME, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim'),
|
|
||||||
OP_2DROP, OP_DROP
|
|
||||||
)
|
|
||||||
CLAIM_NAME_PUBKEY = Template('claim_name+pay_pubkey_hash', (
|
|
||||||
CLAIM_NAME_OPCODES + PAY_PUBKEY_HASH.opcodes
|
|
||||||
))
|
|
||||||
CLAIM_NAME_SCRIPT = Template('claim_name+pay_script_hash', (
|
|
||||||
CLAIM_NAME_OPCODES + PAY_SCRIPT_HASH.opcodes
|
|
||||||
))
|
|
||||||
|
|
||||||
SUPPORT_CLAIM_OPCODES = (
|
|
||||||
OP_SUPPORT_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'),
|
|
||||||
OP_2DROP, OP_DROP
|
|
||||||
)
|
|
||||||
SUPPORT_CLAIM_PUBKEY = Template('support_claim+pay_pubkey_hash', (
|
|
||||||
SUPPORT_CLAIM_OPCODES + PAY_PUBKEY_HASH.opcodes
|
|
||||||
))
|
|
||||||
SUPPORT_CLAIM_SCRIPT = Template('support_claim+pay_script_hash', (
|
|
||||||
SUPPORT_CLAIM_OPCODES + PAY_SCRIPT_HASH.opcodes
|
|
||||||
))
|
|
||||||
|
|
||||||
UPDATE_CLAIM_OPCODES = (
|
|
||||||
OP_UPDATE_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'), PUSH_SINGLE('claim'),
|
|
||||||
OP_2DROP, OP_2DROP
|
|
||||||
)
|
|
||||||
UPDATE_CLAIM_PUBKEY = Template('update_claim+pay_pubkey_hash', (
|
|
||||||
UPDATE_CLAIM_OPCODES + PAY_PUBKEY_HASH.opcodes
|
|
||||||
))
|
|
||||||
UPDATE_CLAIM_SCRIPT = Template('update_claim+pay_script_hash', (
|
|
||||||
UPDATE_CLAIM_OPCODES + PAY_SCRIPT_HASH.opcodes
|
|
||||||
))
|
|
||||||
|
|
||||||
templates = [
|
templates = [
|
||||||
PAY_PUBKEY_HASH,
|
PAY_PUBKEY_HASH,
|
||||||
PAY_SCRIPT_HASH,
|
PAY_SCRIPT_HASH,
|
||||||
CLAIM_NAME_PUBKEY,
|
|
||||||
CLAIM_NAME_SCRIPT,
|
|
||||||
SUPPORT_CLAIM_PUBKEY,
|
|
||||||
SUPPORT_CLAIM_SCRIPT,
|
|
||||||
UPDATE_CLAIM_PUBKEY,
|
|
||||||
UPDATE_CLAIM_SCRIPT
|
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -430,14 +381,6 @@ class OutputScript(Script):
|
||||||
'script_hash': script_hash
|
'script_hash': script_hash
|
||||||
})
|
})
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def pay_claim_name_pubkey_hash(cls, claim_name, claim, pubkey_hash):
|
|
||||||
return cls(template=cls.CLAIM_NAME_PUBKEY, values={
|
|
||||||
'claim_name': claim_name,
|
|
||||||
'claim': claim,
|
|
||||||
'pubkey_hash': pubkey_hash
|
|
||||||
})
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_pay_pubkey_hash(self):
|
def is_pay_pubkey_hash(self):
|
||||||
return self.template.name.endswith('pay_pubkey_hash')
|
return self.template.name.endswith('pay_pubkey_hash')
|
||||||
|
@ -445,19 +388,3 @@ class OutputScript(Script):
|
||||||
@property
|
@property
|
||||||
def is_pay_script_hash(self):
|
def is_pay_script_hash(self):
|
||||||
return self.template.name.endswith('pay_script_hash')
|
return self.template.name.endswith('pay_script_hash')
|
||||||
|
|
||||||
@property
|
|
||||||
def is_claim_name(self):
|
|
||||||
return self.template.name.startswith('claim_name+')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_support_claim(self):
|
|
||||||
return self.template.name.startswith('support_claim+')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_update_claim(self):
|
|
||||||
return self.template.name.startswith('update_claim+')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_claim_involved(self):
|
|
||||||
return self.is_claim_name or self.is_support_claim or self.is_update_claim
|
|
|
@ -1,14 +1,12 @@
|
||||||
import io
|
|
||||||
import six
|
import six
|
||||||
import logging
|
import logging
|
||||||
from binascii import hexlify
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from lbrynet.wallet import get_wallet_manager
|
from lbrynet.wallet.basescript import BaseInputScript, BaseOutputScript
|
||||||
from lbrynet.wallet.bcd_data_stream import BCDataStream
|
from lbrynet.wallet.bcd_data_stream import BCDataStream
|
||||||
from lbrynet.wallet.hash import sha256, hash160_to_address, claim_id_hash
|
from lbrynet.wallet.hash import sha256
|
||||||
from lbrynet.wallet.script import InputScript, OutputScript
|
from lbrynet.wallet.account import Account
|
||||||
from lbrynet.wallet.wallet import Wallet
|
from lbrynet.wallet.util import ReadOnlyList
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
@ -19,11 +17,6 @@ NULL_HASH = '\x00'*32
|
||||||
|
|
||||||
class InputOutput(object):
|
class InputOutput(object):
|
||||||
|
|
||||||
@property
|
|
||||||
def fee(self):
|
|
||||||
""" Fee based on size of the input / output. """
|
|
||||||
return get_wallet_manager().fee_per_byte * self.size
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self):
|
def size(self):
|
||||||
""" Size of this input / output in bytes. """
|
""" Size of this input / output in bytes. """
|
||||||
|
@ -35,30 +28,39 @@ class InputOutput(object):
|
||||||
raise NotImplemented
|
raise NotImplemented
|
||||||
|
|
||||||
|
|
||||||
class Input(InputOutput):
|
class BaseInput(InputOutput):
|
||||||
|
|
||||||
|
script_class = None
|
||||||
|
|
||||||
NULL_SIGNATURE = '0'*72
|
NULL_SIGNATURE = '0'*72
|
||||||
NULL_PUBLIC_KEY = '0'*33
|
NULL_PUBLIC_KEY = '0'*33
|
||||||
|
|
||||||
def __init__(self, output_or_txid_index, script, sequence=0xFFFFFFFF):
|
def __init__(self, output_or_txid_index, script, sequence=0xFFFFFFFF):
|
||||||
if isinstance(output_or_txid_index, Output):
|
if isinstance(output_or_txid_index, BaseOutput):
|
||||||
self.output = output_or_txid_index # type: Output
|
self.output = output_or_txid_index # type: BaseOutput
|
||||||
self.output_txid = self.output.transaction.hash
|
self.output_txid = self.output.transaction.hash
|
||||||
self.output_index = self.output.index
|
self.output_index = self.output.index
|
||||||
else:
|
else:
|
||||||
self.output = None # type: Output
|
self.output = None # type: BaseOutput
|
||||||
self.output_txid, self.output_index = output_or_txid_index
|
self.output_txid, self.output_index = output_or_txid_index
|
||||||
self.sequence = sequence
|
self.sequence = sequence
|
||||||
self.is_coinbase = self.output_txid == NULL_HASH
|
self.is_coinbase = self.output_txid == NULL_HASH
|
||||||
self.coinbase = script if self.is_coinbase else None
|
self.coinbase = script if self.is_coinbase else None
|
||||||
self.script = script if not self.is_coinbase else None # type: InputScript
|
self.script = script if not self.is_coinbase else None # type: BaseInputScript
|
||||||
|
|
||||||
def link_output(self, output):
|
def link_output(self, output):
|
||||||
assert self.output is None
|
assert self.output is None
|
||||||
assert self.output_txid == output.transaction.id
|
assert self.output_txid == output.transaction.hash
|
||||||
assert self.output_index == output.index
|
assert self.output_index == output.index
|
||||||
self.output = output
|
self.output = output
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def spend(cls, output):
|
||||||
|
""" Create an input to spend the output."""
|
||||||
|
assert output.script.is_pay_pubkey_hash, 'Attempting to spend unsupported output.'
|
||||||
|
script = cls.script_class.redeem_pubkey_hash(cls.NULL_SIGNATURE, cls.NULL_PUBLIC_KEY)
|
||||||
|
return cls(output, script)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def amount(self):
|
def amount(self):
|
||||||
""" Amount this input adds to the transaction. """
|
""" Amount this input adds to the transaction. """
|
||||||
|
@ -82,7 +84,7 @@ class Input(InputOutput):
|
||||||
sequence = stream.read_uint32()
|
sequence = stream.read_uint32()
|
||||||
return cls(
|
return cls(
|
||||||
(txid, index),
|
(txid, index),
|
||||||
InputScript(script) if not txid == NULL_HASH else script,
|
cls.script_class(script) if not txid == NULL_HASH else script,
|
||||||
sequence
|
sequence
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -98,86 +100,48 @@ class Input(InputOutput):
|
||||||
stream.write_string(self.script.source)
|
stream.write_string(self.script.source)
|
||||||
stream.write_uint32(self.sequence)
|
stream.write_uint32(self.sequence)
|
||||||
|
|
||||||
def to_python_source(self):
|
|
||||||
return (
|
|
||||||
u"InputScript(\n"
|
|
||||||
u" (output_txid=unhexlify('{}'), output_index={}),\n"
|
|
||||||
u" script=unhexlify('{}')\n"
|
|
||||||
u" # tokens: {}\n"
|
|
||||||
u")").format(
|
|
||||||
hexlify(self.output_txid), self.output_index,
|
|
||||||
hexlify(self.coinbase) if self.is_coinbase else hexlify(self.script.source),
|
|
||||||
repr(self.script.tokens)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
class BaseOutput(InputOutput):
|
||||||
|
|
||||||
class Output(InputOutput):
|
script_class = None
|
||||||
|
|
||||||
def __init__(self, transaction, index, amount, script):
|
def __init__(self, amount, script):
|
||||||
self.transaction = transaction # type: Transaction
|
|
||||||
self.index = index # type: int
|
|
||||||
self.amount = amount # type: int
|
self.amount = amount # type: int
|
||||||
self.script = script # type: OutputScript
|
self.script = script # type: BaseOutputScript
|
||||||
|
self.transaction = None # type: BaseTransaction
|
||||||
|
self.index = None # type: int
|
||||||
self._effective_amount = None # type: int
|
self._effective_amount = None # type: int
|
||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
return self.effective_amount < other.effective_amount
|
return self.effective_amount < other.effective_amount
|
||||||
|
|
||||||
def _add_and_return(self):
|
|
||||||
self.transaction.add_outputs([self])
|
|
||||||
return self
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pay_pubkey_hash(cls, transaction, index, amount, pubkey_hash):
|
def pay_pubkey_hash(cls, amount, pubkey_hash):
|
||||||
return cls(
|
return cls(amount, cls.script_class.pay_pubkey_hash(pubkey_hash))
|
||||||
transaction, index, amount,
|
|
||||||
OutputScript.pay_pubkey_hash(pubkey_hash)
|
|
||||||
)._add_and_return()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def pay_claim_name_pubkey_hash(cls, transaction, index, amount, claim_name, claim, pubkey_hash):
|
|
||||||
return cls(
|
|
||||||
transaction, index, amount,
|
|
||||||
OutputScript.pay_claim_name_pubkey_hash(claim_name, claim, pubkey_hash)
|
|
||||||
)._add_and_return()
|
|
||||||
|
|
||||||
def spend(self, signature=Input.NULL_SIGNATURE, pubkey=Input.NULL_PUBLIC_KEY):
|
|
||||||
""" Create the input to spend this output."""
|
|
||||||
assert self.script.is_pay_pubkey_hash, 'Attempting to spend unsupported output.'
|
|
||||||
script = InputScript.redeem_pubkey_hash(signature, pubkey)
|
|
||||||
return Input(self, script)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def effective_amount(self):
|
def effective_amount(self):
|
||||||
""" Amount minus fees it would take to spend this output. """
|
""" Amount minus fees it would take to spend this output. """
|
||||||
if self._effective_amount is None:
|
if self._effective_amount is None:
|
||||||
txi = self.spend()
|
self._effective_amount = self.input_class.spend(self).effective_amount
|
||||||
self._effective_amount = txi.effective_amount
|
|
||||||
return self._effective_amount
|
return self._effective_amount
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def deserialize_from(cls, stream, transaction, index):
|
def deserialize_from(cls, stream):
|
||||||
return cls(
|
return cls(
|
||||||
transaction=transaction,
|
|
||||||
index=index,
|
|
||||||
amount=stream.read_uint64(),
|
amount=stream.read_uint64(),
|
||||||
script=OutputScript(stream.read_string())
|
script=cls.script_class(stream.read_string())
|
||||||
)
|
)
|
||||||
|
|
||||||
def serialize_to(self, stream):
|
def serialize_to(self, stream):
|
||||||
stream.write_uint64(self.amount)
|
stream.write_uint64(self.amount)
|
||||||
stream.write_string(self.script.source)
|
stream.write_string(self.script.source)
|
||||||
|
|
||||||
def to_python_source(self):
|
|
||||||
return (
|
|
||||||
u"OutputScript(tx, index={}, amount={},\n"
|
|
||||||
u" script=unhexlify('{}')\n"
|
|
||||||
u" # tokens: {}\n"
|
|
||||||
u")").format(
|
|
||||||
self.index, self.amount, hexlify(self.script.source), repr(self.script.tokens))
|
|
||||||
|
|
||||||
|
class BaseTransaction:
|
||||||
|
|
||||||
class Transaction:
|
input_class = None
|
||||||
|
output_class = None
|
||||||
|
|
||||||
def __init__(self, raw=None, version=1, locktime=0, height=None, is_saved=False):
|
def __init__(self, raw=None, version=1, locktime=0, height=None, is_saved=False):
|
||||||
self._raw = raw
|
self._raw = raw
|
||||||
|
@ -186,8 +150,8 @@ class Transaction:
|
||||||
self.version = version # type: int
|
self.version = version # type: int
|
||||||
self.locktime = locktime # type: int
|
self.locktime = locktime # type: int
|
||||||
self.height = height # type: int
|
self.height = height # type: int
|
||||||
self.inputs = [] # type: List[Input]
|
self._inputs = [] # type: List[BaseInput]
|
||||||
self.outputs = [] # type: List[Output]
|
self._outputs = [] # type: List[BaseOutput]
|
||||||
self.is_saved = is_saved # type: bool
|
self.is_saved = is_saved # type: bool
|
||||||
if raw is not None:
|
if raw is not None:
|
||||||
self._deserialize()
|
self._deserialize()
|
||||||
|
@ -211,19 +175,30 @@ class Transaction:
|
||||||
return self._raw
|
return self._raw
|
||||||
|
|
||||||
def _reset(self):
|
def _reset(self):
|
||||||
self._raw = None
|
|
||||||
self._hash = None
|
|
||||||
self._id = None
|
self._id = None
|
||||||
|
self._hash = None
|
||||||
def get_claim_id(self, output_index):
|
self._raw = None
|
||||||
script = self.outputs[output_index]
|
|
||||||
assert script.script.is_claim_name(), 'Not a name claim.'
|
|
||||||
return claim_id_hash(self.hash, output_index)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_complete(self):
|
def inputs(self): # type: () -> ReadOnlyList[BaseInput]
|
||||||
s, r = self.signature_count()
|
return ReadOnlyList(self._inputs)
|
||||||
return r == s
|
|
||||||
|
@property
|
||||||
|
def outputs(self): # type: () -> ReadOnlyList[BaseOutput]
|
||||||
|
return ReadOnlyList(self._outputs)
|
||||||
|
|
||||||
|
def add_inputs(self, inputs):
|
||||||
|
self._inputs.extend(inputs)
|
||||||
|
self._reset()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_outputs(self, outputs):
|
||||||
|
for txo in outputs:
|
||||||
|
txo.transaction = self
|
||||||
|
txo.index = len(self._outputs)
|
||||||
|
self._outputs.append(txo)
|
||||||
|
self._reset()
|
||||||
|
return self
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fee(self):
|
def fee(self):
|
||||||
|
@ -240,30 +215,15 @@ class Transaction:
|
||||||
""" Size in bytes of transaction meta data and all outputs; without inputs. """
|
""" Size in bytes of transaction meta data and all outputs; without inputs. """
|
||||||
return len(self._serialize(with_inputs=False))
|
return len(self._serialize(with_inputs=False))
|
||||||
|
|
||||||
@property
|
|
||||||
def base_fee(self):
|
|
||||||
""" Fee for the transaction header and all outputs; without inputs. """
|
|
||||||
byte_fee = get_wallet_manager().fee_per_byte * self.base_size
|
|
||||||
return max(byte_fee, self.claim_name_fee)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def claim_name_fee(self):
|
|
||||||
char_fee = get_wallet_manager().fee_per_name_char
|
|
||||||
fee = 0
|
|
||||||
for output in self.outputs:
|
|
||||||
if output.script.is_claim_name:
|
|
||||||
fee += len(output.script.values['claim_name']) * char_fee
|
|
||||||
return fee
|
|
||||||
|
|
||||||
def _serialize(self, with_inputs=True):
|
def _serialize(self, with_inputs=True):
|
||||||
stream = BCDataStream()
|
stream = BCDataStream()
|
||||||
stream.write_uint32(self.version)
|
stream.write_uint32(self.version)
|
||||||
if with_inputs:
|
if with_inputs:
|
||||||
stream.write_compact_size(len(self.inputs))
|
stream.write_compact_size(len(self._inputs))
|
||||||
for txin in self.inputs:
|
for txin in self._inputs:
|
||||||
txin.serialize_to(stream)
|
txin.serialize_to(stream)
|
||||||
stream.write_compact_size(len(self.outputs))
|
stream.write_compact_size(len(self._outputs))
|
||||||
for txout in self.outputs:
|
for txout in self._outputs:
|
||||||
txout.serialize_to(stream)
|
txout.serialize_to(stream)
|
||||||
stream.write_uint32(self.locktime)
|
stream.write_uint32(self.locktime)
|
||||||
return stream.get_bytes()
|
return stream.get_bytes()
|
||||||
|
@ -271,14 +231,14 @@ class Transaction:
|
||||||
def _serialize_for_signature(self, signing_input):
|
def _serialize_for_signature(self, signing_input):
|
||||||
stream = BCDataStream()
|
stream = BCDataStream()
|
||||||
stream.write_uint32(self.version)
|
stream.write_uint32(self.version)
|
||||||
stream.write_compact_size(len(self.inputs))
|
stream.write_compact_size(len(self._inputs))
|
||||||
for i, txin in enumerate(self.inputs):
|
for i, txin in enumerate(self._inputs):
|
||||||
if signing_input == i:
|
if signing_input == i:
|
||||||
txin.serialize_to(stream, txin.output.script.source)
|
txin.serialize_to(stream, txin.output.script.source)
|
||||||
else:
|
else:
|
||||||
txin.serialize_to(stream, b'')
|
txin.serialize_to(stream, b'')
|
||||||
stream.write_compact_size(len(self.outputs))
|
stream.write_compact_size(len(self._outputs))
|
||||||
for txout in self.outputs:
|
for txout in self._outputs:
|
||||||
txout.serialize_to(stream)
|
txout.serialize_to(stream)
|
||||||
stream.write_uint32(self.locktime)
|
stream.write_uint32(self.locktime)
|
||||||
stream.write_uint32(1) # signature hash type: SIGHASH_ALL
|
stream.write_uint32(1) # signature hash type: SIGHASH_ALL
|
||||||
|
@ -289,58 +249,37 @@ class Transaction:
|
||||||
stream = BCDataStream(self._raw)
|
stream = BCDataStream(self._raw)
|
||||||
self.version = stream.read_uint32()
|
self.version = stream.read_uint32()
|
||||||
input_count = stream.read_compact_size()
|
input_count = stream.read_compact_size()
|
||||||
self.inputs = [Input.deserialize_from(stream) for _ in range(input_count)]
|
self.add_inputs([
|
||||||
|
self.input_class.deserialize_from(stream) for _ in range(input_count)
|
||||||
|
])
|
||||||
output_count = stream.read_compact_size()
|
output_count = stream.read_compact_size()
|
||||||
self.outputs = [Output.deserialize_from(stream, self, i) for i in range(output_count)]
|
self.add_outputs([
|
||||||
|
self.output_class.deserialize_from(stream) for _ in range(output_count)
|
||||||
|
])
|
||||||
self.locktime = stream.read_uint32()
|
self.locktime = stream.read_uint32()
|
||||||
|
|
||||||
def add_inputs(self, inputs):
|
def sign(self, account): # type: (Account) -> BaseTransaction
|
||||||
self.inputs.extend(inputs)
|
for i, txi in enumerate(self._inputs):
|
||||||
self._reset()
|
|
||||||
|
|
||||||
def add_outputs(self, outputs):
|
|
||||||
self.outputs.extend(outputs)
|
|
||||||
self._reset()
|
|
||||||
|
|
||||||
def sign(self, wallet): # type: (Wallet) -> bool
|
|
||||||
for i, txi in enumerate(self.inputs):
|
|
||||||
txo_script = txi.output.script
|
txo_script = txi.output.script
|
||||||
if txo_script.is_pay_pubkey_hash:
|
if txo_script.is_pay_pubkey_hash:
|
||||||
address = hash160_to_address(txo_script.values['pubkey_hash'], wallet.chain)
|
address = account.coin.hash160_to_address(txo_script.values['pubkey_hash'])
|
||||||
private_key = wallet.get_private_key_for_address(address)
|
private_key = account.get_private_key_for_address(address)
|
||||||
tx = self._serialize_for_signature(i)
|
tx = self._serialize_for_signature(i)
|
||||||
txi.script.values['signature'] = private_key.sign(tx)+six.int2byte(1)
|
txi.script.values['signature'] = private_key.sign(tx)+six.int2byte(1)
|
||||||
txi.script.values['pubkey'] = private_key.public_key.pubkey_bytes
|
txi.script.values['pubkey'] = private_key.public_key.pubkey_bytes
|
||||||
txi.script.generate()
|
txi.script.generate()
|
||||||
self._reset()
|
self._reset()
|
||||||
return True
|
return self
|
||||||
|
|
||||||
def sort(self):
|
def sort(self):
|
||||||
# See https://github.com/kristovatlas/rfc/blob/master/bips/bip-li01.mediawiki
|
# See https://github.com/kristovatlas/rfc/blob/master/bips/bip-li01.mediawiki
|
||||||
self.inputs.sort(key=lambda i: (i['prevout_hash'], i['prevout_n']))
|
self._inputs.sort(key=lambda i: (i['prevout_hash'], i['prevout_n']))
|
||||||
self.outputs.sort(key=lambda o: (o[2], pay_script(o[0], o[1])))
|
self._outputs.sort(key=lambda o: (o[2], pay_script(o[0], o[1])))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def input_sum(self):
|
def input_sum(self):
|
||||||
return sum(i.amount for i in self.inputs)
|
return sum(i.amount for i in self._inputs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def output_sum(self):
|
def output_sum(self):
|
||||||
return sum(o.amount for o in self.outputs)
|
return sum(o.amount for o in self._outputs)
|
||||||
|
|
||||||
def to_python_source(self):
|
|
||||||
s = io.StringIO()
|
|
||||||
s.write(u'tx = Transaction(version={}, locktime={}, height={})\n'.format(
|
|
||||||
self.version, self.locktime, self.height
|
|
||||||
))
|
|
||||||
for txi in self.inputs:
|
|
||||||
s.write(u'tx.add_input(')
|
|
||||||
s.write(txi.to_python_source())
|
|
||||||
s.write(u')\n')
|
|
||||||
for txo in self.outputs:
|
|
||||||
s.write(u'tx.add_output(')
|
|
||||||
s.write(txo.to_python_source())
|
|
||||||
s.write(u')\n')
|
|
||||||
s.write(u'# tx.id: unhexlify("{}")\n'.format(hexlify(self.id)))
|
|
||||||
s.write(u'# tx.raw: unhexlify("{}")\n'.format(hexlify(self.raw)))
|
|
||||||
return s.getvalue()
|
|
|
@ -10,14 +10,14 @@
|
||||||
|
|
||||||
import struct
|
import struct
|
||||||
import hashlib
|
import hashlib
|
||||||
from binascii import unhexlify
|
|
||||||
from six import int2byte, byte2int
|
from six import int2byte, byte2int
|
||||||
|
|
||||||
import ecdsa
|
import ecdsa
|
||||||
import ecdsa.ellipticcurve as EC
|
import ecdsa.ellipticcurve as EC
|
||||||
import ecdsa.numbertheory as NT
|
import ecdsa.numbertheory as NT
|
||||||
|
|
||||||
from .hash import Base58, hmac_sha512, hash160, double_sha256, public_key_to_address
|
from .basecoin import BaseCoin
|
||||||
|
from .hash import Base58, hmac_sha512, hash160, double_sha256
|
||||||
from .util import cachedproperty, bytes_to_int, int_to_bytes
|
from .util import cachedproperty, bytes_to_int, int_to_bytes
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,7 +30,9 @@ class _KeyBase(object):
|
||||||
|
|
||||||
CURVE = ecdsa.SECP256k1
|
CURVE = ecdsa.SECP256k1
|
||||||
|
|
||||||
def __init__(self, chain_code, n, depth, parent):
|
def __init__(self, coin, chain_code, n, depth, parent):
|
||||||
|
if not isinstance(coin, BaseCoin):
|
||||||
|
raise TypeError('invalid coin')
|
||||||
if not isinstance(chain_code, (bytes, bytearray)):
|
if not isinstance(chain_code, (bytes, bytearray)):
|
||||||
raise TypeError('chain code must be raw bytes')
|
raise TypeError('chain code must be raw bytes')
|
||||||
if len(chain_code) != 32:
|
if len(chain_code) != 32:
|
||||||
|
@ -42,6 +44,7 @@ class _KeyBase(object):
|
||||||
if parent is not None:
|
if parent is not None:
|
||||||
if not isinstance(parent, type(self)):
|
if not isinstance(parent, type(self)):
|
||||||
raise TypeError('parent key has bad type')
|
raise TypeError('parent key has bad type')
|
||||||
|
self.coin = coin
|
||||||
self.chain_code = chain_code
|
self.chain_code = chain_code
|
||||||
self.n = n
|
self.n = n
|
||||||
self.depth = depth
|
self.depth = depth
|
||||||
|
@ -83,8 +86,8 @@ class _KeyBase(object):
|
||||||
class PubKey(_KeyBase):
|
class PubKey(_KeyBase):
|
||||||
""" A BIP32 public key. """
|
""" A BIP32 public key. """
|
||||||
|
|
||||||
def __init__(self, pubkey, chain_code, n, depth, parent=None):
|
def __init__(self, coin, pubkey, chain_code, n, depth, parent=None):
|
||||||
super(PubKey, self).__init__(chain_code, n, depth, parent)
|
super(PubKey, self).__init__(coin, chain_code, n, depth, parent)
|
||||||
if isinstance(pubkey, ecdsa.VerifyingKey):
|
if isinstance(pubkey, ecdsa.VerifyingKey):
|
||||||
self.verifying_key = pubkey
|
self.verifying_key = pubkey
|
||||||
else:
|
else:
|
||||||
|
@ -126,7 +129,7 @@ class PubKey(_KeyBase):
|
||||||
@cachedproperty
|
@cachedproperty
|
||||||
def address(self):
|
def address(self):
|
||||||
""" The public key as a P2PKH address. """
|
""" The public key as a P2PKH address. """
|
||||||
return public_key_to_address(self.pubkey_bytes, 'regtest')
|
return self.coin.public_key_to_address(self.pubkey_bytes)
|
||||||
|
|
||||||
def ec_point(self):
|
def ec_point(self):
|
||||||
return self.verifying_key.pubkey.point
|
return self.verifying_key.pubkey.point
|
||||||
|
@ -150,7 +153,7 @@ class PubKey(_KeyBase):
|
||||||
|
|
||||||
verkey = ecdsa.VerifyingKey.from_public_point(point, curve=curve)
|
verkey = ecdsa.VerifyingKey.from_public_point(point, curve=curve)
|
||||||
|
|
||||||
return PubKey(verkey, R, n, self.depth + 1, self)
|
return PubKey(self.coin, verkey, R, n, self.depth + 1, self)
|
||||||
|
|
||||||
def identifier(self):
|
def identifier(self):
|
||||||
""" Return the key's identifier as 20 bytes. """
|
""" Return the key's identifier as 20 bytes. """
|
||||||
|
@ -158,7 +161,10 @@ class PubKey(_KeyBase):
|
||||||
|
|
||||||
def extended_key(self):
|
def extended_key(self):
|
||||||
""" Return a raw extended public key. """
|
""" Return a raw extended public key. """
|
||||||
return self._extended_key(unhexlify("0488b21e"), self.pubkey_bytes)
|
return self._extended_key(
|
||||||
|
self.coin.extended_public_key_prefix,
|
||||||
|
self.pubkey_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LowSValueSigningKey(ecdsa.SigningKey):
|
class LowSValueSigningKey(ecdsa.SigningKey):
|
||||||
|
@ -180,8 +186,8 @@ class PrivateKey(_KeyBase):
|
||||||
|
|
||||||
HARDENED = 1 << 31
|
HARDENED = 1 << 31
|
||||||
|
|
||||||
def __init__(self, privkey, chain_code, n, depth, parent=None):
|
def __init__(self, coin, privkey, chain_code, n, depth, parent=None):
|
||||||
super(PrivateKey, self).__init__(chain_code, n, depth, parent)
|
super(PrivateKey, self).__init__(coin, chain_code, n, depth, parent)
|
||||||
if isinstance(privkey, ecdsa.SigningKey):
|
if isinstance(privkey, ecdsa.SigningKey):
|
||||||
self.signing_key = privkey
|
self.signing_key = privkey
|
||||||
else:
|
else:
|
||||||
|
@ -206,11 +212,11 @@ class PrivateKey(_KeyBase):
|
||||||
return exponent
|
return exponent
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_seed(cls, seed):
|
def from_seed(cls, coin, seed):
|
||||||
# This hard-coded message string seems to be coin-independent...
|
# This hard-coded message string seems to be coin-independent...
|
||||||
hmac = hmac_sha512(b'Bitcoin seed', seed)
|
hmac = hmac_sha512(b'Bitcoin seed', seed)
|
||||||
privkey, chain_code = hmac[:32], hmac[32:]
|
privkey, chain_code = hmac[:32], hmac[32:]
|
||||||
return cls(privkey, chain_code, 0, 0)
|
return cls(coin, privkey, chain_code, 0, 0)
|
||||||
|
|
||||||
@cachedproperty
|
@cachedproperty
|
||||||
def private_key_bytes(self):
|
def private_key_bytes(self):
|
||||||
|
@ -222,7 +228,7 @@ class PrivateKey(_KeyBase):
|
||||||
""" Return the corresponding extended public key. """
|
""" Return the corresponding extended public key. """
|
||||||
verifying_key = self.signing_key.get_verifying_key()
|
verifying_key = self.signing_key.get_verifying_key()
|
||||||
parent_pubkey = self.parent.public_key if self.parent else None
|
parent_pubkey = self.parent.public_key if self.parent else None
|
||||||
return PubKey(verifying_key, self.chain_code, self.n, self.depth,
|
return PubKey(self.coin, verifying_key, self.chain_code, self.n, self.depth,
|
||||||
parent_pubkey)
|
parent_pubkey)
|
||||||
|
|
||||||
def ec_point(self):
|
def ec_point(self):
|
||||||
|
@ -234,7 +240,7 @@ class PrivateKey(_KeyBase):
|
||||||
|
|
||||||
def wif(self):
|
def wif(self):
|
||||||
""" Return the private key encoded in Wallet Import Format. """
|
""" Return the private key encoded in Wallet Import Format. """
|
||||||
return b'\x1c' + self.private_key_bytes + b'\x01'
|
return self.coin.private_key_to_wif(self.private_key_bytes)
|
||||||
|
|
||||||
def address(self):
|
def address(self):
|
||||||
""" The public key as a P2PKH address. """
|
""" The public key as a P2PKH address. """
|
||||||
|
@ -261,7 +267,7 @@ class PrivateKey(_KeyBase):
|
||||||
|
|
||||||
privkey = _exponent_to_bytes(exponent)
|
privkey = _exponent_to_bytes(exponent)
|
||||||
|
|
||||||
return PrivateKey(privkey, R, n, self.depth + 1, self)
|
return PrivateKey(self.coin, privkey, R, n, self.depth + 1, self)
|
||||||
|
|
||||||
def sign(self, data):
|
def sign(self, data):
|
||||||
""" Produce a signature for piece of data by double hashing it and signing the hash. """
|
""" Produce a signature for piece of data by double hashing it and signing the hash. """
|
||||||
|
@ -275,7 +281,10 @@ class PrivateKey(_KeyBase):
|
||||||
|
|
||||||
def extended_key(self):
|
def extended_key(self):
|
||||||
"""Return a raw extended private key."""
|
"""Return a raw extended private key."""
|
||||||
return self._extended_key(unhexlify("0488ade4"), b'\0' + self.private_key_bytes)
|
return self._extended_key(
|
||||||
|
self.coin.extended_private_key_prefix,
|
||||||
|
b'\0' + self.private_key_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _exponent_to_bytes(exponent):
|
def _exponent_to_bytes(exponent):
|
||||||
|
@ -283,7 +292,7 @@ def _exponent_to_bytes(exponent):
|
||||||
return (int2byte(0)*32 + int_to_bytes(exponent))[-32:]
|
return (int2byte(0)*32 + int_to_bytes(exponent))[-32:]
|
||||||
|
|
||||||
|
|
||||||
def _from_extended_key(ekey):
|
def _from_extended_key(coin, ekey):
|
||||||
"""Return a PubKey or PrivateKey from an extended key raw bytes."""
|
"""Return a PubKey or PrivateKey from an extended key raw bytes."""
|
||||||
if not isinstance(ekey, (bytes, bytearray)):
|
if not isinstance(ekey, (bytes, bytearray)):
|
||||||
raise TypeError('extended key must be raw bytes')
|
raise TypeError('extended key must be raw bytes')
|
||||||
|
@ -295,21 +304,21 @@ def _from_extended_key(ekey):
|
||||||
n, = struct.unpack('>I', ekey[9:13])
|
n, = struct.unpack('>I', ekey[9:13])
|
||||||
chain_code = ekey[13:45]
|
chain_code = ekey[13:45]
|
||||||
|
|
||||||
if ekey[:4] == unhexlify("0488b21e"):
|
if ekey[:4] == coin.extended_public_key_prefix:
|
||||||
pubkey = ekey[45:]
|
pubkey = ekey[45:]
|
||||||
key = PubKey(pubkey, chain_code, n, depth)
|
key = PubKey(coin, pubkey, chain_code, n, depth)
|
||||||
elif ekey[:4] == unhexlify("0488ade4"):
|
elif ekey[:4] == coin.extended_private_key_prefix:
|
||||||
if ekey[45] is not int2byte(0):
|
if ekey[45] is not int2byte(0):
|
||||||
raise ValueError('invalid extended private key prefix byte')
|
raise ValueError('invalid extended private key prefix byte')
|
||||||
privkey = ekey[46:]
|
privkey = ekey[46:]
|
||||||
key = PrivateKey(privkey, chain_code, n, depth)
|
key = PrivateKey(coin, privkey, chain_code, n, depth)
|
||||||
else:
|
else:
|
||||||
raise ValueError('version bytes unrecognised')
|
raise ValueError('version bytes unrecognised')
|
||||||
|
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
|
||||||
def from_extended_key_string(ekey_str):
|
def from_extended_key_string(coin, ekey_str):
|
||||||
"""Given an extended key string, such as
|
"""Given an extended key string, such as
|
||||||
|
|
||||||
xpub6BsnM1W2Y7qLMiuhi7f7dbAwQZ5Cz5gYJCRzTNainXzQXYjFwtuQXHd
|
xpub6BsnM1W2Y7qLMiuhi7f7dbAwQZ5Cz5gYJCRzTNainXzQXYjFwtuQXHd
|
||||||
|
@ -317,4 +326,4 @@ def from_extended_key_string(ekey_str):
|
||||||
|
|
||||||
return a PubKey or PrivateKey.
|
return a PubKey or PrivateKey.
|
||||||
"""
|
"""
|
||||||
return _from_extended_key(Base58.decode_check(ekey_str))
|
return _from_extended_key(coin, Base58.decode_check(ekey_str))
|
||||||
|
|
2
lbrynet/wallet/coins/__init__.py
Normal file
2
lbrynet/wallet/coins/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
from . import lbc
|
||||||
|
from . import bitcoin
|
43
lbrynet/wallet/coins/bitcoin.py
Normal file
43
lbrynet/wallet/coins/bitcoin.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
from six import int2byte
|
||||||
|
from binascii import unhexlify
|
||||||
|
from lbrynet.wallet.baseledger import BaseLedger
|
||||||
|
from lbrynet.wallet.basenetwork import BaseNetwork
|
||||||
|
from lbrynet.wallet.basescript import BaseInputScript, BaseOutputScript
|
||||||
|
from lbrynet.wallet.basetransaction import BaseTransaction, BaseInput, BaseOutput
|
||||||
|
from lbrynet.wallet.basecoin import BaseCoin
|
||||||
|
|
||||||
|
|
||||||
|
class Ledger(BaseLedger):
|
||||||
|
network_class = BaseNetwork
|
||||||
|
|
||||||
|
|
||||||
|
class Input(BaseInput):
|
||||||
|
script_class = BaseInputScript
|
||||||
|
|
||||||
|
|
||||||
|
class Output(BaseOutput):
|
||||||
|
script_class = BaseOutputScript
|
||||||
|
|
||||||
|
|
||||||
|
class Transaction(BaseTransaction):
|
||||||
|
input_class = BaseInput
|
||||||
|
output_class = BaseOutput
|
||||||
|
|
||||||
|
|
||||||
|
class BTC(BaseCoin):
|
||||||
|
name = 'Bitcoin'
|
||||||
|
symbol = 'BTC'
|
||||||
|
network = 'mainnet'
|
||||||
|
|
||||||
|
ledger_class = Ledger
|
||||||
|
transaction_class = Transaction
|
||||||
|
|
||||||
|
pubkey_address_prefix = int2byte(0x00)
|
||||||
|
script_address_prefix = int2byte(0x05)
|
||||||
|
extended_public_key_prefix = unhexlify('0488b21e')
|
||||||
|
extended_private_key_prefix = unhexlify('0488ade4')
|
||||||
|
|
||||||
|
default_fee_per_byte = 50
|
||||||
|
|
||||||
|
def __init__(self, ledger, fee_per_byte=default_fee_per_byte):
|
||||||
|
super(BTC, self).__init__(ledger, fee_per_byte)
|
1
lbrynet/wallet/coins/lbc/__init__.py
Normal file
1
lbrynet/wallet/coins/lbc/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .coin import LBC, LBCTestNet, LBCRegTest
|
67
lbrynet/wallet/coins/lbc/coin.py
Normal file
67
lbrynet/wallet/coins/lbc/coin.py
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
from six import int2byte
|
||||||
|
from binascii import unhexlify
|
||||||
|
|
||||||
|
from lbrynet.wallet.basecoin import BaseCoin
|
||||||
|
|
||||||
|
from .ledger import MainNetLedger, TestNetLedger, RegTestLedger
|
||||||
|
from .transaction import Transaction
|
||||||
|
|
||||||
|
|
||||||
|
class LBC(BaseCoin):
|
||||||
|
name = 'LBRY Credits'
|
||||||
|
symbol = 'LBC'
|
||||||
|
network = 'mainnet'
|
||||||
|
|
||||||
|
ledger_class = MainNetLedger
|
||||||
|
transaction_class = Transaction
|
||||||
|
|
||||||
|
secret_prefix = int2byte(0x1c)
|
||||||
|
pubkey_address_prefix = int2byte(0x55)
|
||||||
|
script_address_prefix = int2byte(0x7a)
|
||||||
|
extended_public_key_prefix = unhexlify('019c354f')
|
||||||
|
extended_private_key_prefix = unhexlify('019c3118')
|
||||||
|
|
||||||
|
default_fee_per_byte = 50
|
||||||
|
default_fee_per_name_char = 200000
|
||||||
|
|
||||||
|
def __init__(self, ledger, fee_per_byte=default_fee_per_byte,
|
||||||
|
fee_per_name_char=default_fee_per_name_char):
|
||||||
|
super(LBC, self).__init__(ledger, fee_per_byte)
|
||||||
|
self.fee_per_name_char = fee_per_name_char
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
coin_dict = super(LBC, self).to_dict()
|
||||||
|
coin_dict['fee_per_name_char'] = self.fee_per_name_char
|
||||||
|
return coin_dict
|
||||||
|
|
||||||
|
def get_transaction_base_fee(self, tx):
|
||||||
|
""" Fee for the transaction header and all outputs; without inputs. """
|
||||||
|
return max(
|
||||||
|
super(LBC, self).get_transaction_base_fee(tx),
|
||||||
|
self.get_transaction_claim_name_fee(tx)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_transaction_claim_name_fee(self, tx):
|
||||||
|
fee = 0
|
||||||
|
for output in tx.outputs:
|
||||||
|
if output.script.is_claim_name:
|
||||||
|
fee += len(output.script.values['claim_name']) * self.fee_per_name_char
|
||||||
|
return fee
|
||||||
|
|
||||||
|
|
||||||
|
class LBCTestNet(LBC):
|
||||||
|
network = 'testnet'
|
||||||
|
ledger_class = TestNetLedger
|
||||||
|
pubkey_address_prefix = int2byte(111)
|
||||||
|
script_address_prefix = int2byte(196)
|
||||||
|
extended_public_key_prefix = unhexlify('043587cf')
|
||||||
|
extended_private_key_prefix = unhexlify('04358394')
|
||||||
|
|
||||||
|
|
||||||
|
class LBCRegTest(LBC):
|
||||||
|
network = 'regtest'
|
||||||
|
ledger_class = RegTestLedger
|
||||||
|
pubkey_address_prefix = int2byte(111)
|
||||||
|
script_address_prefix = int2byte(196)
|
||||||
|
extended_public_key_prefix = unhexlify('043587cf')
|
||||||
|
extended_private_key_prefix = unhexlify('04358394')
|
28
lbrynet/wallet/coins/lbc/ledger.py
Normal file
28
lbrynet/wallet/coins/lbc/ledger.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from lbrynet.wallet.baseledger import BaseLedger
|
||||||
|
|
||||||
|
from .network import Network
|
||||||
|
|
||||||
|
|
||||||
|
class LBCLedger(BaseLedger):
|
||||||
|
network_class = Network
|
||||||
|
header_size = 112
|
||||||
|
max_target = 0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
|
||||||
|
genesis_hash = '9c89283ba0f3227f6c03b70216b9f665f0118d5e0fa729cedf4fb34d6a34f463'
|
||||||
|
genesis_bits = 0x1f00ffff
|
||||||
|
target_timespan = 150
|
||||||
|
|
||||||
|
|
||||||
|
class MainNetLedger(LBCLedger):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestNetLedger(LBCLedger):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RegTestLedger(LBCLedger):
|
||||||
|
max_target = 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
|
||||||
|
genesis_hash = '6e3fcf1299d4ec5d79c3a4c91d624a4acf9e2e173d95a1a0504f677669687556'
|
||||||
|
genesis_bits = 0x207fffff
|
||||||
|
target_timespan = 1
|
||||||
|
verify_bits_to_target = False
|
5
lbrynet/wallet/coins/lbc/network.py
Normal file
5
lbrynet/wallet/coins/lbc/network.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from lbrynet.wallet.basenetwork import BaseNetwork
|
||||||
|
|
||||||
|
|
||||||
|
class Network(BaseNetwork):
|
||||||
|
pass
|
80
lbrynet/wallet/coins/lbc/script.py
Normal file
80
lbrynet/wallet/coins/lbc/script.py
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
from lbrynet.wallet.basescript import BaseInputScript, BaseOutputScript, Template
|
||||||
|
from lbrynet.wallet.basescript import PUSH_SINGLE, OP_DROP, OP_2DROP
|
||||||
|
|
||||||
|
|
||||||
|
class InputScript(BaseInputScript):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OutputScript(BaseOutputScript):
|
||||||
|
|
||||||
|
# lbry custom opcodes
|
||||||
|
OP_CLAIM_NAME = 0xb5
|
||||||
|
OP_SUPPORT_CLAIM = 0xb6
|
||||||
|
OP_UPDATE_CLAIM = 0xb7
|
||||||
|
|
||||||
|
CLAIM_NAME_OPCODES = (
|
||||||
|
OP_CLAIM_NAME, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim'),
|
||||||
|
OP_2DROP, OP_DROP
|
||||||
|
)
|
||||||
|
CLAIM_NAME_PUBKEY = Template('claim_name+pay_pubkey_hash', (
|
||||||
|
CLAIM_NAME_OPCODES + BaseOutputScript.PAY_PUBKEY_HASH.opcodes
|
||||||
|
))
|
||||||
|
CLAIM_NAME_SCRIPT = Template('claim_name+pay_script_hash', (
|
||||||
|
CLAIM_NAME_OPCODES + BaseOutputScript.PAY_SCRIPT_HASH.opcodes
|
||||||
|
))
|
||||||
|
|
||||||
|
SUPPORT_CLAIM_OPCODES = (
|
||||||
|
OP_SUPPORT_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'),
|
||||||
|
OP_2DROP, OP_DROP
|
||||||
|
)
|
||||||
|
SUPPORT_CLAIM_PUBKEY = Template('support_claim+pay_pubkey_hash', (
|
||||||
|
SUPPORT_CLAIM_OPCODES + BaseOutputScript.PAY_PUBKEY_HASH.opcodes
|
||||||
|
))
|
||||||
|
SUPPORT_CLAIM_SCRIPT = Template('support_claim+pay_script_hash', (
|
||||||
|
SUPPORT_CLAIM_OPCODES + BaseOutputScript.PAY_SCRIPT_HASH.opcodes
|
||||||
|
))
|
||||||
|
|
||||||
|
UPDATE_CLAIM_OPCODES = (
|
||||||
|
OP_UPDATE_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'), PUSH_SINGLE('claim'),
|
||||||
|
OP_2DROP, OP_2DROP
|
||||||
|
)
|
||||||
|
UPDATE_CLAIM_PUBKEY = Template('update_claim+pay_pubkey_hash', (
|
||||||
|
UPDATE_CLAIM_OPCODES + BaseOutputScript.PAY_PUBKEY_HASH.opcodes
|
||||||
|
))
|
||||||
|
UPDATE_CLAIM_SCRIPT = Template('update_claim+pay_script_hash', (
|
||||||
|
UPDATE_CLAIM_OPCODES + BaseOutputScript.PAY_SCRIPT_HASH.opcodes
|
||||||
|
))
|
||||||
|
|
||||||
|
templates = BaseOutputScript.templates + [
|
||||||
|
CLAIM_NAME_PUBKEY,
|
||||||
|
CLAIM_NAME_SCRIPT,
|
||||||
|
SUPPORT_CLAIM_PUBKEY,
|
||||||
|
SUPPORT_CLAIM_SCRIPT,
|
||||||
|
UPDATE_CLAIM_PUBKEY,
|
||||||
|
UPDATE_CLAIM_SCRIPT
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def pay_claim_name_pubkey_hash(cls, claim_name, claim, pubkey_hash):
|
||||||
|
return cls(template=cls.CLAIM_NAME_PUBKEY, values={
|
||||||
|
'claim_name': claim_name,
|
||||||
|
'claim': claim,
|
||||||
|
'pubkey_hash': pubkey_hash
|
||||||
|
})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_claim_name(self):
|
||||||
|
return self.template.name.startswith('claim_name+')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_support_claim(self):
|
||||||
|
return self.template.name.startswith('support_claim+')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_update_claim(self):
|
||||||
|
return self.template.name.startswith('update_claim+')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_claim_involved(self):
|
||||||
|
return self.is_claim_name or self.is_support_claim or self.is_update_claim
|
34
lbrynet/wallet/coins/lbc/transaction.py
Normal file
34
lbrynet/wallet/coins/lbc/transaction.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from lbrynet.wallet.basetransaction import BaseTransaction, BaseInput, BaseOutput
|
||||||
|
from lbrynet.wallet.hash import hash160
|
||||||
|
|
||||||
|
from .script import InputScript, OutputScript
|
||||||
|
|
||||||
|
|
||||||
|
def claim_id_hash(txid, n):
|
||||||
|
return hash160(txid + struct.pack('>I', n))
|
||||||
|
|
||||||
|
|
||||||
|
class Input(BaseInput):
|
||||||
|
script_class = InputScript
|
||||||
|
|
||||||
|
|
||||||
|
class Output(BaseOutput):
|
||||||
|
script_class = OutputScript
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def pay_claim_name_pubkey_hash(cls, amount, claim_name, claim, pubkey_hash):
|
||||||
|
script = cls.script_class.pay_claim_name_pubkey_hash(claim_name, claim, pubkey_hash)
|
||||||
|
return cls(amount, script)
|
||||||
|
|
||||||
|
|
||||||
|
class Transaction(BaseTransaction):
|
||||||
|
|
||||||
|
input_class = Input
|
||||||
|
output_class = Output
|
||||||
|
|
||||||
|
def get_claim_id(self, output_index):
|
||||||
|
output = self._outputs[output_index]
|
||||||
|
assert output.script.is_claim_name(), 'Not a name claim.'
|
||||||
|
return claim_id_hash(self.hash, output_index)
|
|
@ -1,5 +1,3 @@
|
||||||
from lbrynet import __version__
|
|
||||||
LBRYUM_VERSION = __version__
|
|
||||||
PROTOCOL_VERSION = '0.10' # protocol version requested
|
PROTOCOL_VERSION = '0.10' # protocol version requested
|
||||||
NEW_SEED_VERSION = 11 # lbryum versions >= 2.0
|
NEW_SEED_VERSION = 11 # lbryum versions >= 2.0
|
||||||
OLD_SEED_VERSION = 4 # lbryum versions < 2.0
|
OLD_SEED_VERSION = 4 # lbryum versions < 2.0
|
||||||
|
@ -9,73 +7,19 @@ SEED_PREFIX = '01' # Electrum standard wallet
|
||||||
SEED_PREFIX_2FA = '101' # extended seed for two-factor authentication
|
SEED_PREFIX_2FA = '101' # extended seed for two-factor authentication
|
||||||
|
|
||||||
|
|
||||||
MAXIMUM_FEE_PER_BYTE = 50
|
|
||||||
MAXIMUM_FEE_PER_NAME_CHAR = 200000
|
|
||||||
COINBASE_MATURITY = 100
|
COINBASE_MATURITY = 100
|
||||||
CENT = 1000000
|
CENT = 1000000
|
||||||
COIN = 100*CENT
|
COIN = 100*CENT
|
||||||
|
|
||||||
# supported types of transaction outputs
|
|
||||||
TYPE_ADDRESS = 1
|
|
||||||
TYPE_PUBKEY = 2
|
|
||||||
TYPE_SCRIPT = 4
|
|
||||||
TYPE_CLAIM = 8
|
|
||||||
TYPE_SUPPORT = 16
|
|
||||||
TYPE_UPDATE = 32
|
|
||||||
|
|
||||||
# claim related constants
|
|
||||||
EXPIRATION_BLOCKS = 262974
|
|
||||||
RECOMMENDED_CLAIMTRIE_HASH_CONFIRMS = 1
|
RECOMMENDED_CLAIMTRIE_HASH_CONFIRMS = 1
|
||||||
|
|
||||||
NO_SIGNATURE = 'ff'
|
NO_SIGNATURE = 'ff'
|
||||||
|
|
||||||
NULL_HASH = '0000000000000000000000000000000000000000000000000000000000000000'
|
NULL_HASH = '0000000000000000000000000000000000000000000000000000000000000000'
|
||||||
HEADER_SIZE = 112
|
|
||||||
BLOCKS_PER_CHUNK = 96
|
|
||||||
CLAIM_ID_SIZE = 20
|
CLAIM_ID_SIZE = 20
|
||||||
|
|
||||||
HEADERS_URL = "https://s3.amazonaws.com/lbry-blockchain-headers/blockchain_headers_latest"
|
|
||||||
|
|
||||||
DEFAULT_PORTS = {'t': '50001', 's': '50002', 'h': '8081', 'g': '8082'}
|
DEFAULT_PORTS = {'t': '50001', 's': '50002', 'h': '8081', 'g': '8082'}
|
||||||
NODES_RETRY_INTERVAL = 60
|
NODES_RETRY_INTERVAL = 60
|
||||||
SERVER_RETRY_INTERVAL = 10
|
SERVER_RETRY_INTERVAL = 10
|
||||||
MAX_BATCH_QUERY_SIZE = 500
|
MAX_BATCH_QUERY_SIZE = 500
|
||||||
proxy_modes = ['socks4', 'socks5', 'http']
|
proxy_modes = ['socks4', 'socks5', 'http']
|
||||||
|
|
||||||
# Chain Properties
|
|
||||||
# see: https://github.com/lbryio/lbrycrd/blob/master/src/chainparams.cpp
|
|
||||||
MAIN_CHAIN = 'main'
|
|
||||||
TESTNET_CHAIN = 'testnet'
|
|
||||||
REGTEST_CHAIN = 'regtest'
|
|
||||||
CHAINS = {
|
|
||||||
MAIN_CHAIN: {
|
|
||||||
'pubkey_address': 0,
|
|
||||||
'script_address': 5,
|
|
||||||
'pubkey_address_prefix': 85,
|
|
||||||
'script_address_prefix': 122,
|
|
||||||
'genesis_hash': '9c89283ba0f3227f6c03b70216b9f665f0118d5e0fa729cedf4fb34d6a34f463',
|
|
||||||
'max_target': 0x0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
|
|
||||||
'genesis_bits': 0x1f00ffff,
|
|
||||||
'target_timespan': 150
|
|
||||||
},
|
|
||||||
TESTNET_CHAIN: {
|
|
||||||
'pubkey_address': 0,
|
|
||||||
'script_address': 5,
|
|
||||||
'pubkey_address_prefix': 111,
|
|
||||||
'script_address_prefix': 196,
|
|
||||||
'genesis_hash': '9c89283ba0f3227f6c03b70216b9f665f0118d5e0fa729cedf4fb34d6a34f463',
|
|
||||||
'max_target': 0x0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
|
|
||||||
'genesis_bits': 0x1f00ffff,
|
|
||||||
'target_timespan': 150
|
|
||||||
},
|
|
||||||
REGTEST_CHAIN: {
|
|
||||||
'pubkey_address': 0,
|
|
||||||
'script_address': 5,
|
|
||||||
'pubkey_address_prefix': 111,
|
|
||||||
'script_address_prefix': 196,
|
|
||||||
'genesis_hash': '6e3fcf1299d4ec5d79c3a4c91d624a4acf9e2e173d95a1a0504f677669687556',
|
|
||||||
'max_target': 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
|
|
||||||
'genesis_bits': 0x207fffff,
|
|
||||||
'target_timespan': 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -13,11 +13,9 @@ import aes
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import struct
|
|
||||||
from binascii import hexlify, unhexlify
|
from binascii import hexlify, unhexlify
|
||||||
|
|
||||||
from .util import bytes_to_int, int_to_bytes
|
from .util import bytes_to_int, int_to_bytes
|
||||||
from .constants import CHAINS, MAIN_CHAIN
|
|
||||||
|
|
||||||
_sha256 = hashlib.sha256
|
_sha256 = hashlib.sha256
|
||||||
_sha512 = hashlib.sha512
|
_sha512 = hashlib.sha512
|
||||||
|
@ -77,26 +75,6 @@ def hex_str_to_hash(x):
|
||||||
return reversed(unhexlify(x))
|
return reversed(unhexlify(x))
|
||||||
|
|
||||||
|
|
||||||
def public_key_to_address(public_key, chain=MAIN_CHAIN):
|
|
||||||
return hash160_to_address(hash160(public_key), chain)
|
|
||||||
|
|
||||||
|
|
||||||
def hash160_to_address(h160, chain=MAIN_CHAIN):
|
|
||||||
prefix = CHAINS[chain]['pubkey_address_prefix']
|
|
||||||
raw_address = six.int2byte(prefix) + h160
|
|
||||||
return Base58.encode(raw_address + double_sha256(raw_address)[0:4])
|
|
||||||
|
|
||||||
|
|
||||||
def address_to_hash_160(address):
|
|
||||||
bytes = Base58.decode(address)
|
|
||||||
prefix, pubkey_bytes, addr_checksum = bytes[0], bytes[1:21], bytes[21:]
|
|
||||||
return pubkey_bytes
|
|
||||||
|
|
||||||
|
|
||||||
def claim_id_hash(txid, n):
|
|
||||||
return hash160(txid + struct.pack('>I', n))
|
|
||||||
|
|
||||||
|
|
||||||
def aes_encrypt(secret, value):
|
def aes_encrypt(secret, value):
|
||||||
return base64.b64encode(aes.encryptData(secret, value.encode('utf8')))
|
return base64.b64encode(aes.encryptData(secret, value.encode('utf8')))
|
||||||
|
|
||||||
|
|
|
@ -1,141 +1,83 @@
|
||||||
import logging
|
import functools
|
||||||
from binascii import unhexlify
|
from typing import List, Dict, Type
|
||||||
from operator import itemgetter
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from lbrynet.wallet.wallet import Wallet
|
from lbrynet.wallet.account import AccountsView
|
||||||
from lbrynet.wallet.ledger import Ledger
|
from lbrynet.wallet.basecoin import CoinRegistry
|
||||||
from lbrynet.wallet.protocol import Network
|
from lbrynet.wallet.baseledger import BaseLedger
|
||||||
from lbrynet.wallet.transaction import Transaction
|
from lbrynet.wallet.wallet import Wallet, WalletStorage
|
||||||
from lbrynet.wallet.stream import execute_serially
|
|
||||||
from lbrynet.wallet.constants import MAXIMUM_FEE_PER_BYTE, MAXIMUM_FEE_PER_NAME_CHAR
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class WalletManager:
|
class WalletManager:
|
||||||
|
|
||||||
def __init__(self, config=None, wallet=None, ledger=None, network=None):
|
def __init__(self, wallets=None, ledgers=None):
|
||||||
self.config = config or {}
|
self.wallets = wallets or [] # type: List[Wallet]
|
||||||
self.ledger = ledger or Ledger(self.config)
|
self.ledgers = ledgers or {} # type: Dict[Type[BaseLedger],BaseLedger]
|
||||||
self.wallet = wallet or Wallet()
|
self.running = False
|
||||||
self.wallets = [self.wallet]
|
|
||||||
self.network = network or Network(self.config)
|
@classmethod
|
||||||
self.network.on_header.listen(self.process_header)
|
def from_config(cls, config):
|
||||||
self.network.on_status.listen(self.process_status)
|
wallets = []
|
||||||
|
manager = cls(wallets)
|
||||||
|
for coin_id, ledger_config in config.get('ledgers', {}).items():
|
||||||
|
manager.get_or_create_ledger(coin_id, ledger_config)
|
||||||
|
for wallet_path in config.get('wallets', []):
|
||||||
|
wallet_storage = WalletStorage(wallet_path)
|
||||||
|
wallet = Wallet.from_storage(wallet_storage, manager)
|
||||||
|
wallets.append(wallet)
|
||||||
|
return manager
|
||||||
|
|
||||||
|
def get_or_create_ledger(self, coin_id, ledger_config=None):
|
||||||
|
coin_class = CoinRegistry.get_coin_class(coin_id)
|
||||||
|
ledger_class = coin_class.ledger_class
|
||||||
|
ledger = self.ledgers.get(ledger_class)
|
||||||
|
if ledger is None:
|
||||||
|
ledger = ledger_class(self.get_accounts_view(coin_class), ledger_config or {})
|
||||||
|
self.ledgers[ledger_class] = ledger
|
||||||
|
return ledger
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fee_per_byte(self):
|
def default_wallet(self):
|
||||||
return self.config.get('fee_per_byte', MAXIMUM_FEE_PER_BYTE)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fee_per_name_char(self):
|
|
||||||
return self.config.get('fee_per_name_char', MAXIMUM_FEE_PER_NAME_CHAR)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def addresses_without_history(self):
|
|
||||||
for wallet in self.wallets:
|
for wallet in self.wallets:
|
||||||
for address in wallet.addresses:
|
return wallet
|
||||||
if not self.ledger.has_address(address):
|
|
||||||
yield address
|
|
||||||
|
|
||||||
def get_least_used_receiving_address(self, max_transactions=1000):
|
@property
|
||||||
return self._get_least_used_address(
|
def default_account(self):
|
||||||
self.wallet.default_account.receiving_keys.addresses,
|
for wallet in self.wallets:
|
||||||
self.wallet.default_account.receiving_keys,
|
return wallet.default_account
|
||||||
max_transactions
|
|
||||||
|
def get_accounts(self, coin_class):
|
||||||
|
for wallet in self.wallets:
|
||||||
|
for account in wallet.accounts:
|
||||||
|
if account.coin.__class__ is coin_class:
|
||||||
|
yield account
|
||||||
|
|
||||||
|
def get_accounts_view(self, coin_class):
|
||||||
|
return AccountsView(
|
||||||
|
functools.partial(self.get_accounts, coin_class)
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_least_used_change_address(self, max_transactions=100):
|
def create_wallet(self, path, coin_class):
|
||||||
return self._get_least_used_address(
|
storage = WalletStorage(path)
|
||||||
self.wallet.default_account.change_keys.addresses,
|
wallet = Wallet.from_storage(storage, self)
|
||||||
self.wallet.default_account.change_keys,
|
self.wallets.append(wallet)
|
||||||
max_transactions
|
self.create_account(wallet, coin_class)
|
||||||
)
|
return wallet
|
||||||
|
|
||||||
def _get_least_used_address(self, addresses, sequence, max_transactions):
|
def create_account(self, wallet, coin_class):
|
||||||
address = self.ledger.get_least_used_address(addresses, max_transactions)
|
ledger = self.get_or_create_ledger(coin_class.get_id())
|
||||||
if address:
|
return wallet.generate_account(ledger)
|
||||||
return address
|
|
||||||
address = sequence.generate_next_address()
|
|
||||||
self.subscribe_history(address)
|
|
||||||
return address
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def start(self):
|
def start_ledgers(self):
|
||||||
first_connection = self.network.on_connected.first
|
self.running = True
|
||||||
self.network.start()
|
yield defer.DeferredList([
|
||||||
yield first_connection
|
l.start() for l in self.ledgers.values()
|
||||||
self.ledger.headers.touch()
|
|
||||||
yield self.update_headers()
|
|
||||||
yield self.network.subscribe_headers()
|
|
||||||
yield self.update_wallet()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
return self.network.stop()
|
|
||||||
|
|
||||||
@execute_serially
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def update_headers(self):
|
|
||||||
while True:
|
|
||||||
height_sought = len(self.ledger.headers)
|
|
||||||
headers = yield self.network.get_headers(height_sought)
|
|
||||||
log.info("received {} headers starting at {} height".format(headers['count'], height_sought))
|
|
||||||
if headers['count'] <= 0:
|
|
||||||
break
|
|
||||||
yield self.ledger.headers.connect(height_sought, headers['hex'].decode('hex'))
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def process_header(self, response):
|
|
||||||
header = response[0]
|
|
||||||
if self.update_headers.is_running:
|
|
||||||
return
|
|
||||||
if header['height'] == len(self.ledger.headers):
|
|
||||||
# New header from network directly connects after the last local header.
|
|
||||||
yield self.ledger.headers.connect(len(self.ledger.headers), header['hex'].decode('hex'))
|
|
||||||
elif header['height'] > len(self.ledger.headers):
|
|
||||||
# New header is several heights ahead of local, do download instead.
|
|
||||||
yield self.update_headers()
|
|
||||||
|
|
||||||
@execute_serially
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def update_wallet(self):
|
|
||||||
# Before subscribing, download history for any addresses that don't have any,
|
|
||||||
# this avoids situation where we're getting status updates to addresses we know
|
|
||||||
# need to update anyways. Continue to get history and create more addresses until
|
|
||||||
# all missing addresses are created and history for them is fully restored.
|
|
||||||
self.wallet.ensure_enough_addresses()
|
|
||||||
addresses = list(self.addresses_without_history)
|
|
||||||
while addresses:
|
|
||||||
yield defer.gatherResults([
|
|
||||||
self.update_history(a) for a in addresses
|
|
||||||
])
|
|
||||||
addresses = self.wallet.ensure_enough_addresses()
|
|
||||||
|
|
||||||
# By this point all of the addresses should be restored and we
|
|
||||||
# can now subscribe all of them to receive updates.
|
|
||||||
yield defer.gatherResults([
|
|
||||||
self.subscribe_history(address)
|
|
||||||
for address in self.wallet.addresses
|
|
||||||
])
|
])
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def update_history(self, address):
|
def stop_ledgers(self):
|
||||||
history = yield self.network.get_history(address)
|
yield defer.DeferredList([
|
||||||
for hash in map(itemgetter('tx_hash'), history):
|
l.stop() for l in self.ledgers.values()
|
||||||
transaction = self.ledger.get_transaction(hash)
|
])
|
||||||
if not transaction:
|
self.running = False
|
||||||
raw = yield self.network.get_transaction(hash)
|
|
||||||
transaction = Transaction(unhexlify(raw))
|
|
||||||
self.ledger.add_transaction(address, transaction)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def subscribe_history(self, address):
|
|
||||||
status = yield self.network.subscribe_address(address)
|
|
||||||
if status != self.ledger.get_status(address):
|
|
||||||
self.update_history(address)
|
|
||||||
|
|
||||||
def process_status(self, response):
|
|
||||||
address, status = response
|
|
||||||
if status != self.ledger.get_status(address):
|
|
||||||
self.update_history(address)
|
|
||||||
|
|
|
@ -1,4 +1,17 @@
|
||||||
from binascii import unhexlify, hexlify
|
from binascii import unhexlify, hexlify
|
||||||
|
from collections import Sequence
|
||||||
|
|
||||||
|
|
||||||
|
class ReadOnlyList(Sequence):
|
||||||
|
|
||||||
|
def __init__(self, lst):
|
||||||
|
self.lst = lst
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.lst[key]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.lst)
|
||||||
|
|
||||||
|
|
||||||
def subclass_tuple(name, base):
|
def subclass_tuple(name, base):
|
||||||
|
@ -17,6 +30,15 @@ class cachedproperty(object):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class classproperty(object):
|
||||||
|
|
||||||
|
def __init__(self, f):
|
||||||
|
self.f = f
|
||||||
|
|
||||||
|
def __get__(self, obj, owner):
|
||||||
|
return self.f(owner)
|
||||||
|
|
||||||
|
|
||||||
def bytes_to_int(be_bytes):
|
def bytes_to_int(be_bytes):
|
||||||
""" Interprets a big-endian sequence of bytes as an integer. """
|
""" Interprets a big-endian sequence of bytes as an integer. """
|
||||||
return int(hexlify(be_bytes), 16)
|
return int(hexlify(be_bytes), 16)
|
||||||
|
|
|
@ -1,110 +1,150 @@
|
||||||
import stat
|
import stat
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from typing import List, Dict
|
||||||
|
|
||||||
from lbrynet.wallet.account import Account
|
from lbrynet.wallet.account import Account
|
||||||
from lbrynet.wallet.constants import MAIN_CHAIN
|
from lbrynet.wallet.basecoin import CoinRegistry, BaseCoin
|
||||||
|
from lbrynet.wallet.baseledger import BaseLedger
|
||||||
|
|
||||||
|
|
||||||
|
def inflate_coin(manager, coin_id, coin_dict):
|
||||||
|
# type: ('WalletManager', str, Dict) -> BaseCoin
|
||||||
|
coin_class = CoinRegistry.get_coin_class(coin_id)
|
||||||
|
ledger = manager.get_or_create_ledger(coin_id)
|
||||||
|
return coin_class(ledger, **coin_dict)
|
||||||
|
|
||||||
|
|
||||||
class Wallet:
|
class Wallet:
|
||||||
|
""" The primary role of Wallet is to encapsulate a collection
|
||||||
|
of accounts (seed/private keys) and the spending rules / settings
|
||||||
|
for the coins attached to those accounts. Wallets are represented
|
||||||
|
by physical files on the filesystem.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, name='Wallet', coins=None, accounts=None, storage=None):
|
||||||
self.name = kwargs.get('name', 'Wallet')
|
self.name = name
|
||||||
self.chain = kwargs.get('chain', MAIN_CHAIN)
|
self.coins = coins or [] # type: List[BaseCoin]
|
||||||
self.accounts = kwargs.get('accounts') or {0: Account.generate()}
|
self.accounts = accounts or [] # type: List[Account]
|
||||||
|
self.storage = storage or WalletStorage()
|
||||||
|
|
||||||
|
def get_or_create_coin(self, ledger, coin_dict=None): # type: (BaseLedger, Dict) -> BaseCoin
|
||||||
|
for coin in self.coins:
|
||||||
|
if coin.__class__ is ledger.coin_class:
|
||||||
|
return coin
|
||||||
|
coin = ledger.coin_class(ledger, **(coin_dict or {}))
|
||||||
|
self.coins.append(coin)
|
||||||
|
return coin
|
||||||
|
|
||||||
|
def generate_account(self, ledger): # type: (BaseLedger) -> Account
|
||||||
|
coin = self.get_or_create_coin(ledger)
|
||||||
|
account = Account.generate(coin)
|
||||||
|
self.accounts.append(account)
|
||||||
|
return account
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, json_data):
|
def from_storage(cls, storage, manager): # type: (WalletStorage, 'WalletManager') -> Wallet
|
||||||
if 'accounts' in json_data:
|
json_dict = storage.read()
|
||||||
json_data = json_data.copy()
|
|
||||||
json_data['accounts'] = {
|
|
||||||
a_id: Account.from_json(a) for
|
|
||||||
a_id, a in json_data['accounts'].items()
|
|
||||||
}
|
|
||||||
return cls(**json_data)
|
|
||||||
|
|
||||||
def to_json(self):
|
coins = {}
|
||||||
|
for coin_id, coin_dict in json_dict.get('coins', {}).items():
|
||||||
|
coins[coin_id] = inflate_coin(manager, coin_id, coin_dict)
|
||||||
|
|
||||||
|
accounts = []
|
||||||
|
for account_dict in json_dict.get('accounts', []):
|
||||||
|
coin_id = account_dict['coin']
|
||||||
|
coin = coins.get(coin_id)
|
||||||
|
if coin is None:
|
||||||
|
coin = coins[coin_id] = inflate_coin(manager, coin_id, {})
|
||||||
|
account = Account.from_dict(coin, account_dict)
|
||||||
|
accounts.append(account)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
name=json_dict.get('name', 'Wallet'),
|
||||||
|
coins=list(coins.values()),
|
||||||
|
accounts=accounts,
|
||||||
|
storage=storage
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'chain': self.chain,
|
'coins': {c.get_id(): c.to_dict() for c in self.coins},
|
||||||
'accounts': {
|
'accounts': [a.to_dict() for a in self.accounts]
|
||||||
a_id: a.to_json() for
|
|
||||||
a_id, a in self.accounts.items()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self.storage.write(self.to_dict())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_account(self):
|
def default_account(self):
|
||||||
return self.accounts.get(0, None)
|
for account in self.accounts:
|
||||||
|
return account
|
||||||
|
|
||||||
@property
|
def get_account_private_key_for_address(self, address):
|
||||||
def addresses(self):
|
for account in self.accounts:
|
||||||
for account in self.accounts.values():
|
|
||||||
for address in account.addresses:
|
|
||||||
yield address
|
|
||||||
|
|
||||||
def ensure_enough_addresses(self):
|
|
||||||
return [
|
|
||||||
address
|
|
||||||
for account in self.accounts.values()
|
|
||||||
for address in account.ensure_enough_addresses()
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_private_key_for_address(self, address):
|
|
||||||
for account in self.accounts.values():
|
|
||||||
private_key = account.get_private_key_for_address(address)
|
private_key = account.get_private_key_for_address(address)
|
||||||
if private_key is not None:
|
if private_key is not None:
|
||||||
return private_key
|
return account, private_key
|
||||||
|
|
||||||
|
|
||||||
class EphemeralWalletStorage(dict):
|
class WalletStorage:
|
||||||
|
|
||||||
LATEST_VERSION = 2
|
LATEST_VERSION = 2
|
||||||
|
|
||||||
def save(self):
|
DEFAULT = {
|
||||||
return json.dumps(self, indent=4, sort_keys=True)
|
'version': LATEST_VERSION,
|
||||||
|
'name': 'Wallet',
|
||||||
|
'coins': {},
|
||||||
|
'accounts': []
|
||||||
|
}
|
||||||
|
|
||||||
def upgrade(self):
|
def __init__(self, path=None, default=None):
|
||||||
|
self.path = path
|
||||||
|
self._default = default or self.DEFAULT.copy()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default(self):
|
||||||
|
return self._default.copy()
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
if self.path and self.path.exists(self.path):
|
||||||
|
with open(self.path, "r") as f:
|
||||||
|
json_data = f.read()
|
||||||
|
json_dict = json.loads(json_data)
|
||||||
|
if json_dict.get('version') == self.LATEST_VERSION and \
|
||||||
|
set(json_dict) == set(self._default):
|
||||||
|
return json_dict
|
||||||
|
else:
|
||||||
|
return self.upgrade(json_dict)
|
||||||
|
else:
|
||||||
|
return self.default
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def upgrade(cls, json_dict):
|
||||||
|
json_dict = json_dict.copy()
|
||||||
|
|
||||||
def _rename_property(old, new):
|
def _rename_property(old, new):
|
||||||
if old in self:
|
if old in json_dict:
|
||||||
old_value = self[old]
|
json_dict[new] = json_dict[old]
|
||||||
del self[old]
|
del json_dict[old]
|
||||||
if new not in self:
|
|
||||||
self[new] = old_value
|
|
||||||
|
|
||||||
if self.get('version', 1) == 1: # upgrade from version 1 to version 2
|
version = json_dict.pop('version', -1)
|
||||||
# TODO: `addr_history` should actually be imported into SQLStorage and removed from wallet.
|
|
||||||
|
if version == 1: # upgrade from version 1 to version 2
|
||||||
_rename_property('addr_history', 'history')
|
_rename_property('addr_history', 'history')
|
||||||
_rename_property('use_encryption', 'encrypted')
|
_rename_property('use_encryption', 'encrypted')
|
||||||
_rename_property('gap_limit', 'gap_limit_for_receiving')
|
_rename_property('gap_limit', 'gap_limit_for_receiving')
|
||||||
self['version'] = 2
|
|
||||||
|
|
||||||
self.save()
|
upgraded = cls.DEFAULT
|
||||||
|
upgraded.update(json_dict)
|
||||||
|
return json_dict
|
||||||
|
|
||||||
|
def write(self, json_dict):
|
||||||
|
|
||||||
class PermanentWalletStorage(EphemeralWalletStorage):
|
json_data = json.dumps(json_dict, indent=4, sort_keys=True)
|
||||||
|
if self.path is None:
|
||||||
def __init__(self, *args, **kwargs):
|
return json_data
|
||||||
super(PermanentWalletStorage, self).__init__(*args, **kwargs)
|
|
||||||
self.path = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_path(cls, path):
|
|
||||||
if os.path.exists(path):
|
|
||||||
with open(path, "r") as f:
|
|
||||||
json_data = f.read()
|
|
||||||
json_dict = json.loads(json_data)
|
|
||||||
storage = cls(**json_dict)
|
|
||||||
if 'version' in storage and storage['version'] != storage.LATEST_VERSION:
|
|
||||||
storage.upgrade()
|
|
||||||
else:
|
|
||||||
storage = cls()
|
|
||||||
storage.path = path
|
|
||||||
return storage
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
json_data = super(PermanentWalletStorage, self).save()
|
|
||||||
|
|
||||||
temp_path = "%s.tmp.%s" % (self.path, os.getpid())
|
temp_path = "%s.tmp.%s" % (self.path, os.getpid())
|
||||||
with open(temp_path, "w") as f:
|
with open(temp_path, "w") as f:
|
||||||
|
@ -116,12 +156,9 @@ class PermanentWalletStorage(EphemeralWalletStorage):
|
||||||
mode = os.stat(self.path).st_mode
|
mode = os.stat(self.path).st_mode
|
||||||
else:
|
else:
|
||||||
mode = stat.S_IREAD | stat.S_IWRITE
|
mode = stat.S_IREAD | stat.S_IWRITE
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.rename(temp_path, self.path)
|
os.rename(temp_path, self.path)
|
||||||
except:
|
except:
|
||||||
os.remove(self.path)
|
os.remove(self.path)
|
||||||
os.rename(temp_path, self.path)
|
os.rename(temp_path, self.path)
|
||||||
os.chmod(self.path, mode)
|
os.chmod(self.path, mode)
|
||||||
|
|
||||||
return json_data
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue