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'
|
||||
LBRYUM_WALLET = 'lbryum'
|
||||
PTC_WALLET = 'ptc'
|
||||
TORBA_WALLET = 'torba'
|
||||
|
||||
PROTOCOL_PREFIX = 'lbry'
|
||||
APP_NAME = 'LBRY'
|
||||
|
|
|
@ -6,17 +6,14 @@ from binascii import hexlify
|
|||
|
||||
from twisted.internet import defer, reactor, threads
|
||||
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.database.storage import SQLiteStorage
|
||||
|
||||
from lbrynet.wallet import set_wallet_manager
|
||||
from lbrynet.wallet.wallet import Wallet
|
||||
from lbrynet.wallet.basecoin import CoinRegistry
|
||||
from lbrynet.wallet.manager import WalletManager
|
||||
from lbrynet.wallet.transaction import Transaction, Output
|
||||
from lbrynet.wallet.constants import COIN, REGTEST_CHAIN
|
||||
from lbrynet.wallet.hash import hash160_to_address, address_to_hash_160
|
||||
from lbrynet.wallet.constants import COIN
|
||||
|
||||
|
||||
class WalletTestCase(unittest.TestCase):
|
||||
|
@ -27,11 +24,6 @@ class WalletTestCase(unittest.TestCase):
|
|||
logging.getLogger('lbrynet').setLevel(logging.INFO)
|
||||
self.data_path = tempfile.mkdtemp()
|
||||
self.db = SQLiteStorage(self.data_path)
|
||||
self.config = {
|
||||
'chain': REGTEST_CHAIN,
|
||||
'wallet_path': self.data_path,
|
||||
'default_servers': [('localhost', 50001)]
|
||||
}
|
||||
CallLaterManager.setup(reactor.callLater)
|
||||
self.service = BaseLbryServiceStack(self.VERBOSE)
|
||||
return self.service.startup()
|
||||
|
@ -52,37 +44,30 @@ class StartupTests(WalletTestCase):
|
|||
|
||||
@defer.inlineCallbacks
|
||||
def test_balance(self):
|
||||
wallet = Wallet(chain=REGTEST_CHAIN)
|
||||
manager = WalletManager(self.config, wallet)
|
||||
set_wallet_manager(manager)
|
||||
yield manager.start()
|
||||
yield self.lbrycrd.generate(1)
|
||||
yield threads.deferToThread(time.sleep, 1)
|
||||
#yield wallet.network.on_header.first
|
||||
address = manager.get_least_used_receiving_address()
|
||||
coin_id = 'lbc_regtest'
|
||||
manager = WalletManager.from_config({
|
||||
'ledgers': {coin_id: {'default_servers': [('localhost', 50001)]}}
|
||||
})
|
||||
wallet = manager.create_wallet(None, CoinRegistry.get_coin_class(coin_id))
|
||||
ledger = manager.ledgers.values()[0]
|
||||
account = wallet.default_account
|
||||
coin = account.coin
|
||||
yield manager.start_ledgers()
|
||||
address = account.get_least_used_receiving_address()
|
||||
sendtxid = yield self.lbrycrd.sendtoaddress(address, 2.5)
|
||||
yield self.lbrycrd.generate(1)
|
||||
#yield manager.wallet.history.on_transaction.
|
||||
yield threads.deferToThread(time.sleep, 10)
|
||||
tx = manager.ledger.transactions.values()[0]
|
||||
print(tx.to_python_source())
|
||||
print(address)
|
||||
output = None
|
||||
for txo in tx.outputs:
|
||||
other = hash160_to_address(txo.script.values['pubkey_hash'], 'regtest')
|
||||
if other == address:
|
||||
output = txo
|
||||
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())
|
||||
utxo = account.get_unspent_utxos()[0]
|
||||
address2 = account.get_least_used_receiving_address()
|
||||
tx_class = ledger.transaction_class
|
||||
Input, Output = tx_class.input_class, tx_class.output_class
|
||||
tx = tx_class()\
|
||||
.add_inputs([Input.spend(utxo)])\
|
||||
.add_outputs([Output.pay_pubkey_hash(2.49*COIN, coin.address_to_hash160(address2))])\
|
||||
.sign(account)
|
||||
|
||||
yield self.lbrycrd.decoderawtransaction(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
|
||||
|
||||
from lbrynet.wallet.constants import CENT, MAXIMUM_FEE_PER_BYTE
|
||||
from lbrynet.wallet.transaction import Transaction, Output
|
||||
from lbrynet.wallet.coins.lbc.lbc import LBRYCredits
|
||||
from lbrynet.wallet.coins.bitcoin import Bitcoin
|
||||
from lbrynet.wallet.coinselection import CoinSelector, MAXIMUM_TRIES
|
||||
from lbrynet.wallet.constants import CENT
|
||||
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
|
||||
|
@ -15,20 +17,18 @@ def search(*args, **kwargs):
|
|||
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):
|
||||
|
||||
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):
|
||||
self.assertIsNone(CoinSelector([], 0, 0).select())
|
||||
|
||||
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)]
|
||||
selector = CoinSelector(big_pool, 101 * CENT, 0)
|
||||
self.assertIsNone(selector.select())
|
||||
|
@ -39,7 +39,7 @@ class TestCoinSelectionTests(unittest.TestCase):
|
|||
self.assertEqual(selector.tries, 201)
|
||||
|
||||
def test_exact_match(self):
|
||||
fee = utxo(CENT).spend(fake=True).fee
|
||||
fee = utxo(CENT).spend().fee
|
||||
utxo_pool = [
|
||||
utxo(CENT + fee),
|
||||
utxo(CENT),
|
||||
|
@ -74,7 +74,9 @@ class TestOfficialBitcoinCoinSelectionTests(unittest.TestCase):
|
|||
# https://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf
|
||||
|
||||
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):
|
||||
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 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.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):
|
||||
|
@ -100,12 +102,12 @@ class TestRedeemPubKeyHash(unittest.TestCase):
|
|||
|
||||
def redeem_pubkey_hash(self, sig, pubkey):
|
||||
# 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(hexlify(src1.values['signature']), sig)
|
||||
self.assertEqual(hexlify(src1.values['pubkey']), pubkey)
|
||||
# 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(hexlify(src2.values['signature']), sig)
|
||||
self.assertEqual(hexlify(src2.values['pubkey']), pubkey)
|
||||
|
@ -128,7 +130,7 @@ class TestRedeemScriptHash(unittest.TestCase):
|
|||
|
||||
def redeem_script_hash(self, sigs, pubkeys):
|
||||
# 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(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['pubkeys_count'], len(pubkeys))
|
||||
# now we test that it will round trip
|
||||
src2 = InputScript(src1.source)
|
||||
src2 = BaseInputScript(src1.source)
|
||||
subscript2 = src2.values['script']
|
||||
self.assertEqual(src2.template.name, 'script_hash')
|
||||
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):
|
||||
# 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(hexlify(src1.values['pubkey_hash']), pubkey_hash)
|
||||
# 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(hexlify(src2.values['pubkey_hash']), pubkey_hash)
|
||||
return hexlify(src1.source)
|
||||
|
@ -201,11 +203,11 @@ class TestPayScriptHash(unittest.TestCase):
|
|||
|
||||
def pay_script_hash(self, script_hash):
|
||||
# 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(hexlify(src1.values['script_hash']), script_hash)
|
||||
# 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(hexlify(src2.values['script_hash']), script_hash)
|
||||
return hexlify(src1.source)
|
||||
|
@ -221,7 +223,8 @@ class TestPayClaimNamePubkeyHash(unittest.TestCase):
|
|||
|
||||
def pay_claim_name_pubkey_hash(self, name, claim, pubkey_hash):
|
||||
# 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.values['claim_name'], name)
|
||||
self.assertEqual(hexlify(src1.values['claim']), claim)
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from binascii import hexlify, unhexlify
|
||||
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 import set_wallet_manager
|
||||
from lbrynet.wallet.bip32 import PrivateKey
|
||||
from lbrynet.wallet.mnemonic import Mnemonic
|
||||
from lbrynet.wallet.wallet import Wallet
|
||||
|
||||
|
||||
NULL_HASH = '\x00'*32
|
||||
|
@ -13,68 +14,78 @@ FEE_PER_BYTE = 50
|
|||
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):
|
||||
|
||||
def setUp(self):
|
||||
set_wallet_manager(WalletManager({
|
||||
'fee_per_byte': FEE_PER_BYTE,
|
||||
'fee_per_name_char': FEE_PER_CHAR
|
||||
}))
|
||||
self.wallet = get_lbc_wallet()
|
||||
self.coin = self.wallet.coins[0]
|
||||
WalletManager([self.wallet], {})
|
||||
|
||||
@staticmethod
|
||||
def get_output():
|
||||
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 io_fee(self, io):
|
||||
return self.coin.get_input_output_fee(io)
|
||||
|
||||
def test_output_size_and_fee(self):
|
||||
txo = self.get_output()
|
||||
txo = get_output()
|
||||
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):
|
||||
txi = self.get_input()
|
||||
txi = get_input()
|
||||
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):
|
||||
tx = self.get_transaction()
|
||||
tx = get_transaction()
|
||||
base_size = tx.size - 1 - tx.inputs[0].size
|
||||
self.assertEqual(tx.size, 204)
|
||||
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):
|
||||
# fee based on claim name is the larger fee
|
||||
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
|
||||
self.assertEqual(tx.size, 4225)
|
||||
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
|
||||
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
|
||||
self.assertEqual(tx.size, 4214)
|
||||
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):
|
||||
|
@ -92,7 +103,7 @@ class TestTransactionSerialization(unittest.TestCase):
|
|||
self.assertEqual(len(tx.outputs), 1)
|
||||
|
||||
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.sequence, 0xFFFFFFFF)
|
||||
self.assertTrue(coinbase.is_coinbase)
|
||||
|
@ -125,7 +136,7 @@ class TestTransactionSerialization(unittest.TestCase):
|
|||
self.assertEqual(len(tx.outputs), 1)
|
||||
|
||||
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.sequence, 0)
|
||||
self.assertTrue(coinbase.is_coinbase)
|
||||
|
@ -166,9 +177,9 @@ class TestTransactionSerialization(unittest.TestCase):
|
|||
self.assertEqual(len(tx.inputs), 1)
|
||||
self.assertEqual(len(tx.outputs), 2)
|
||||
|
||||
txin = tx.inputs[0] # type: Input
|
||||
txin = tx.inputs[0]
|
||||
self.assertEqual(
|
||||
hexlify(txin.output_tx_hash[::-1]),
|
||||
hexlify(txin.output_txid[::-1]),
|
||||
b'1dfd535b6c8550ebe95abceb877f92f76f30e5ba4d3483b043386027b3e13324'
|
||||
)
|
||||
self.assertEqual(txin.output_index, 0)
|
||||
|
@ -186,7 +197,7 @@ class TestTransactionSerialization(unittest.TestCase):
|
|||
)
|
||||
|
||||
# Claim
|
||||
out0 = tx.outputs[0] # type: Output
|
||||
out0 = tx.outputs[0]
|
||||
self.assertEqual(out0.amount, 10000000)
|
||||
self.assertEqual(out0.index, 0)
|
||||
self.assertTrue(out0.script.is_pay_pubkey_hash)
|
||||
|
@ -199,7 +210,7 @@ class TestTransactionSerialization(unittest.TestCase):
|
|||
)
|
||||
|
||||
# Change
|
||||
out1 = tx.outputs[1] # type: Output
|
||||
out1 = tx.outputs[1]
|
||||
self.assertEqual(out1.amount, 189977100)
|
||||
self.assertEqual(out1.index, 1)
|
||||
self.assertTrue(out1.script.is_pay_pubkey_hash)
|
||||
|
@ -215,15 +226,27 @@ class TestTransactionSerialization(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):
|
||||
tx = Transaction()
|
||||
Output.pay_pubkey_hash(Transaction(), 0, CENT, NULL_HASH).spend(fake=True)
|
||||
tx.add_inputs([self.get_input()])
|
||||
Output.pay_pubkey_hash(tx, 0, CENT, NULL_HASH)
|
||||
tx = self.get_tx()
|
||||
lbc = LBC()
|
||||
wallet = Wallet('Main', [lbc], [Account.from_seed(
|
||||
lbc, 'carbon smart garage balance margin twelve chest sword toast envelope '
|
||||
'bottom stomach absent'
|
||||
)])
|
||||
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 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 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()
|
||||
set_wallet_manager(WalletManager(wallet=wallet))
|
||||
account = wallet.default_account # type: Account
|
||||
self.assertIsInstance(account, Account)
|
||||
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)
|
||||
self.assertEqual(wallet.name, 'Wallet')
|
||||
self.assertEqual(wallet.coins, [])
|
||||
self.assertEqual(wallet.accounts, [])
|
||||
|
||||
account1 = wallet.generate_account(LBC)
|
||||
account2 = wallet.generate_account(LBC)
|
||||
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()
|
||||
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)
|
||||
self.assertEqual(len(account1.receiving_keys.addresses), 20)
|
||||
self.assertEqual(len(account1.change_keys.addresses), 6)
|
||||
self.assertEqual(len(account2.receiving_keys.addresses), 20)
|
||||
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):
|
||||
account = Account.generate_from_seed(
|
||||
"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 = {
|
||||
def test_load_and_save_wallet(self):
|
||||
wallet_dict = {
|
||||
'name': 'Main Wallet',
|
||||
'accounts': {
|
||||
0: {
|
||||
'accounts': [
|
||||
{
|
||||
'coin': 'lbc_mainnet',
|
||||
'seed':
|
||||
"carbon smart garage balance margin twelve chest sword toast envelope botto"
|
||||
"m stomach absent",
|
||||
'encrypted': False,
|
||||
'private_key':
|
||||
"xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7DRNLEoB8HoirMgH969"
|
||||
"NrgL8jNzLEegqFzPRWM37GXd4uE8uuRkx4LAe",
|
||||
'LprvXPsFZUGgrX1X9HiyxABZSf6hWJK7kHv4zGZRyyiHbBq5Wu94cE1DMvttnpLYReTPNW4eYwX9dWMvTz3PrB'
|
||||
'wwbRafEeA1ZXL69U2egM4QJdq',
|
||||
'public_key':
|
||||
"xpub661MyMwAqRbcGWtPvbWh9sc2BCfw2cTeVDYF23o3N1t6UZ5wv3EMmDgp66FxHuDtWdft3B"
|
||||
"5eL5xQtyzAtkdmhhC95gjRjLzSTdkho95asu9",
|
||||
'Lpub2hkYkGHXktBhLpwUhKKogyuJ1M7Gt9EkjFTVKyDqZiZpWdhLuCoT1eKDfXfysMFfG4SzfXXcA2SsHzrjHK'
|
||||
'Ea5aoCNRBAhjT5NPLV6hXtvEi',
|
||||
'receiving_gap': 10,
|
||||
'receiving_keys': [
|
||||
'02c68e2d1cf85404c86244ffa279f4c5cd00331e996d30a86d6e46480e3a9220f4',
|
||||
'03c5a997d0549875d23b8e4bbc7b4d316d962587483f3a2e62ddd90a21043c4941'],
|
||||
'03c5a997d0549875d23b8e4bbc7b4d316d962587483f3a2e62ddd90a21043c4941'
|
||||
],
|
||||
'change_gap': 10,
|
||||
'change_keys': [
|
||||
'021460e8d728eee325d0d43128572b2e2bacdc027e420451df100cf9f2154ea5ab']
|
||||
'021460e8d728eee325d0d43128572b2e2bacdc027e420451df100cf9f2154ea5ab'
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
wallet = Wallet.from_json(wallet_data)
|
||||
set_wallet_manager(WalletManager(wallet=wallet))
|
||||
storage = WalletStorage(default=wallet_dict)
|
||||
wallet = Wallet.from_storage(storage)
|
||||
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
|
||||
self.assertIsInstance(account, Account)
|
||||
|
||||
|
@ -91,8 +92,5 @@ class TestWalletAccount(unittest.TestCase):
|
|||
account.change_keys.addresses[0],
|
||||
'bFpHENtqugKKHDshKFq2Mnb59Y2bx4vKgL'
|
||||
)
|
||||
|
||||
self.assertDictEqual(
|
||||
wallet_data['accounts'][0],
|
||||
account.to_json()
|
||||
)
|
||||
wallet_dict['coins'] = {'lbc_mainnet': {'fee_per_name_char': 200000, 'fee_per_byte': 50}}
|
||||
self.assertDictEqual(wallet_dict, wallet.to_dict())
|
||||
|
|
|
@ -1,10 +1 @@
|
|||
_wallet_manager = None
|
||||
|
||||
|
||||
def set_wallet_manager(wallet_manager):
|
||||
global _wallet_manager
|
||||
_wallet_manager = wallet_manager
|
||||
|
||||
|
||||
def get_wallet_manager():
|
||||
return _wallet_manager
|
||||
import coins
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
import itertools
|
||||
from typing import Dict, Generator
|
||||
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.bip32 import PrivateKey, PubKey, from_extended_key_string
|
||||
from lbrynet.wallet.hash import double_sha256, aes_encrypt, aes_decrypt
|
||||
|
||||
from lbryschema.address import public_key_to_address
|
||||
|
||||
|
||||
class KeyChain:
|
||||
|
||||
def __init__(self, parent_key, child_keys, gap):
|
||||
self.coin = parent_key.coin
|
||||
self.parent_key = parent_key # type: PubKey
|
||||
self.child_keys = child_keys
|
||||
self.minimum_gap = gap
|
||||
self.addresses = [
|
||||
public_key_to_address(key)
|
||||
self.coin.public_key_to_address(key)
|
||||
for key in child_keys
|
||||
]
|
||||
|
||||
|
@ -23,9 +24,8 @@ class KeyChain:
|
|||
def has_gap(self):
|
||||
if len(self.addresses) < self.minimum_gap:
|
||||
return False
|
||||
ledger = get_wallet_manager().ledger
|
||||
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 True
|
||||
|
||||
|
@ -44,71 +44,77 @@ class KeyChain:
|
|||
|
||||
class Account:
|
||||
|
||||
def __init__(self, seed, encrypted, private_key, public_key, **kwargs):
|
||||
self.seed = seed
|
||||
self.encrypted = encrypted
|
||||
def __init__(self, coin, seed, encrypted, private_key, public_key,
|
||||
receiving_keys=None, receiving_gap=20,
|
||||
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.public_key = public_key # type: PubKey
|
||||
self.receiving_gap = kwargs.get('receiving_gap', 20)
|
||||
self.receiving_keys = kwargs.get('receiving_keys') or \
|
||||
KeyChain(self.public_key.child(0), [], self.receiving_gap)
|
||||
self.change_gap = kwargs.get('change_gap', 6)
|
||||
self.change_keys = kwargs.get('change_keys') or \
|
||||
KeyChain(self.public_key.child(1), [], self.change_gap)
|
||||
self.keychains = [
|
||||
self.receiving_keys, # child: 0
|
||||
self.change_keys # child: 1
|
||||
]
|
||||
self.keychains = (
|
||||
KeyChain(public_key.child(0), receiving_keys or [], receiving_gap),
|
||||
KeyChain(public_key.child(1), change_keys or [], change_gap)
|
||||
)
|
||||
self.receiving_keys, self.change_keys = self.keychains
|
||||
|
||||
@classmethod
|
||||
def generate(cls):
|
||||
def generate(cls, coin): # type: (BaseCoin) -> Account
|
||||
seed = Mnemonic().make_seed()
|
||||
return cls.generate_from_seed(seed)
|
||||
return cls.from_seed(coin, seed)
|
||||
|
||||
@classmethod
|
||||
def generate_from_seed(cls, seed):
|
||||
private_key = cls.get_private_key_from_seed(seed)
|
||||
def from_seed(cls, coin, seed): # type: (BaseCoin, str) -> Account
|
||||
private_key = cls.get_private_key_from_seed(coin, seed)
|
||||
return cls(
|
||||
seed=seed, encrypted=False,
|
||||
coin=coin, seed=seed, encrypted=False,
|
||||
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
|
||||
def from_json(cls, json_data):
|
||||
data = json_data.copy()
|
||||
if not data['encrypted']:
|
||||
data['private_key'] = from_extended_key_string(data['private_key'])
|
||||
data['public_key'] = from_extended_key_string(data['public_key'])
|
||||
data['receiving_keys'] = KeyChain(
|
||||
data['public_key'].child(0),
|
||||
[unhexlify(k) for k in data['receiving_keys']],
|
||||
data['receiving_gap']
|
||||
def from_dict(cls, coin, d): # type: (BaseCoin, Dict) -> Account
|
||||
if not d['encrypted']:
|
||||
private_key = from_extended_key_string(coin, d['private_key'])
|
||||
public_key = private_key.public_key
|
||||
else:
|
||||
private_key = d['private_key']
|
||||
public_key = from_extended_key_string(coin, d['public_key'])
|
||||
return cls(
|
||||
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 {
|
||||
'coin': self.coin.get_id(),
|
||||
'seed': self.seed,
|
||||
'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(),
|
||||
'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_gap': self.change_gap
|
||||
'change_gap': self.change_keys.minimum_gap
|
||||
}
|
||||
|
||||
def decrypt(self, password):
|
||||
assert self.encrypted, "Key is not encrypted."
|
||||
secret = double_sha256(password)
|
||||
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
|
||||
|
||||
def encrypt(self, password):
|
||||
|
@ -118,13 +124,9 @@ class Account:
|
|||
self.private_key = aes_encrypt(secret, self.private_key.extended_key_string())
|
||||
self.encrypted = True
|
||||
|
||||
@staticmethod
|
||||
def get_private_key_from_seed(seed):
|
||||
return PrivateKey.from_seed(Mnemonic.mnemonic_to_seed(seed))
|
||||
|
||||
@property
|
||||
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):
|
||||
assert not self.encrypted, "Cannot get private key on encrypted wallet account."
|
||||
|
@ -139,3 +141,47 @@ class Account:
|
|||
for keychain in self.keychains
|
||||
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 hashlib
|
||||
from binascii import hexlify
|
||||
from typing import List, Dict, Type
|
||||
from binascii import unhexlify
|
||||
from operator import itemgetter
|
||||
|
||||
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.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.hash import double_sha256, pow_hash
|
||||
|
||||
|
@ -17,43 +21,76 @@ log = logging.getLogger(__name__)
|
|||
|
||||
class Address:
|
||||
|
||||
def __init__(self, address):
|
||||
self.address = address
|
||||
self.transactions = []
|
||||
def __init__(self, pubkey_hash):
|
||||
self.pubkey_hash = pubkey_hash
|
||||
self.transactions = [] # type: List[BaseTransaction]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.transactions)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.transactions)
|
||||
|
||||
def add_transaction(self, 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.db = db
|
||||
self.addresses = {}
|
||||
self.transactions = {}
|
||||
self.headers = BlockchainHeaders(self.headers_path, self.config.get('chain', MAIN_CHAIN))
|
||||
self.addresses = {} # type: Dict[str, Address]
|
||||
self.transactions = {} # type: Dict[str, BaseTransaction]
|
||||
self.headers = Headers(self)
|
||||
self._on_transaction_controller = StreamController()
|
||||
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
|
||||
def headers_path(self):
|
||||
filename = 'blockchain_headers'
|
||||
if self.config.get('chain', MAIN_CHAIN) != MAIN_CHAIN:
|
||||
filename = '{}_headers'.format(self.config['chain'])
|
||||
return os.path.join(self.config.get('wallet_path', ''), filename)
|
||||
def transaction_class(self):
|
||||
return self.coin_class.transaction_class
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_dict):
|
||||
return cls(json_dict)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def load(self):
|
||||
txs = yield self.db.get_transactions()
|
||||
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()
|
||||
for tx_hash, address_hash, input_output, amount, height in txios:
|
||||
tx = self.transactions[tx_hash]
|
||||
address = self.addresses.get(address_hash)
|
||||
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)
|
||||
address.add_transaction(tx)
|
||||
|
||||
|
@ -68,10 +105,11 @@ class Ledger:
|
|||
age = tx_age
|
||||
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.addresses.setdefault(address, [])
|
||||
self.addresses[address].append(transaction)
|
||||
self._on_transaction_controller.add(transaction)
|
||||
|
||||
def has_address(self, address):
|
||||
|
@ -109,20 +147,109 @@ class Ledger:
|
|||
transaction_counts.sort(key=itemgetter(1))
|
||||
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):
|
||||
self.path = path
|
||||
self.chain = chain
|
||||
self.max_target = CHAINS[chain]['max_target']
|
||||
self.target_timespan = CHAINS[chain]['target_timespan']
|
||||
self.genesis_bits = CHAINS[chain]['genesis_bits']
|
||||
def stop(self):
|
||||
return self.network.stop()
|
||||
|
||||
@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_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):
|
||||
if not os.path.exists(self.path):
|
||||
|
@ -134,13 +261,13 @@ class BlockchainHeaders:
|
|||
return len(self) - 1
|
||||
|
||||
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):
|
||||
if 0 <= height < len(self):
|
||||
with open(self.path, 'rb') as f:
|
||||
f.seek(height * HEADER_SIZE)
|
||||
return f.read(HEADER_SIZE)
|
||||
f.seek(height * self.ledger.header_size)
|
||||
return f.read(self.ledger.header_size)
|
||||
|
||||
def __len__(self):
|
||||
if self._size is None:
|
||||
|
@ -168,7 +295,7 @@ class BlockchainHeaders:
|
|||
previous_header = header
|
||||
|
||||
with open(self.path, 'r+b') as f:
|
||||
f.seek(start * HEADER_SIZE)
|
||||
f.seek(start * self.ledger.header_size)
|
||||
f.write(headers)
|
||||
f.truncate()
|
||||
|
||||
|
@ -179,9 +306,9 @@ class BlockchainHeaders:
|
|||
self._on_change_controller.add(change)
|
||||
|
||||
def _iterate_headers(self, height, headers):
|
||||
assert len(headers) % HEADER_SIZE == 0
|
||||
for idx in range(len(headers) / HEADER_SIZE):
|
||||
start, end = idx * HEADER_SIZE, (idx + 1) * HEADER_SIZE
|
||||
assert len(headers) % self.ledger.header_size == 0
|
||||
for idx in range(len(headers) / self.ledger.header_size):
|
||||
start, end = idx * self.ledger.header_size, (idx + 1) * self.ledger.header_size
|
||||
header = headers[start:end]
|
||||
yield self._deserialize(height+idx, header)
|
||||
|
||||
|
@ -239,10 +366,9 @@ class BlockchainHeaders:
|
|||
""" See: lbrycrd/src/lbry.cpp """
|
||||
|
||||
if height == 0:
|
||||
return self.genesis_bits, self.max_target
|
||||
return self.ledger.genesis_bits, self.ledger.max_target
|
||||
|
||||
# bits to target
|
||||
if self.chain != REGTEST_CHAIN:
|
||||
if self.ledger.verify_bits_to_target:
|
||||
bits = last['bits']
|
||||
bitsN = (bits >> 24) & 0xff
|
||||
assert 0x03 <= bitsN <= 0x1f, \
|
||||
|
@ -252,7 +378,7 @@ class BlockchainHeaders:
|
|||
"Second part of bits should be in [0x8000, 0x7fffff] but it was {}".format(bitsBase)
|
||||
|
||||
# new target
|
||||
retargetTimespan = self.target_timespan
|
||||
retargetTimespan = self.ledger.target_timespan
|
||||
nActualTimespan = last['timestamp'] - first['timestamp']
|
||||
|
||||
nModulatedTimespan = retargetTimespan + (nActualTimespan - retargetTimespan) // 8
|
||||
|
@ -267,7 +393,7 @@ class BlockchainHeaders:
|
|||
nModulatedTimespan = nMaxTimespan
|
||||
|
||||
# Retarget
|
||||
bnPowLimit = _ArithUint256(self.max_target)
|
||||
bnPowLimit = _ArithUint256(self.ledger.max_target)
|
||||
bnNew = _ArithUint256.SetCompact(last['bits'])
|
||||
bnNew *= nModulatedTimespan
|
||||
bnNew //= nModulatedTimespan
|
|
@ -10,7 +10,7 @@ from twisted.protocols.basic import LineOnlyReceiver
|
|||
from errors import RemoteServiceException, ProtocolException
|
||||
from errors import TransportException
|
||||
|
||||
from .stream import StreamController
|
||||
from lbrynet.wallet.stream import StreamController
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
|
@ -18,6 +18,8 @@ log = logging.getLogger()
|
|||
def unicode2bytes(string):
|
||||
if isinstance(string, six.text_type):
|
||||
return string.encode('iso-8859-1')
|
||||
elif isinstance(string, list):
|
||||
return [unicode2bytes(s) for s in string]
|
||||
return string
|
||||
|
||||
|
||||
|
@ -125,7 +127,7 @@ class StratumClientFactory(protocol.ClientFactory):
|
|||
return client
|
||||
|
||||
|
||||
class Network:
|
||||
class BaseNetwork:
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
|
@ -2,8 +2,8 @@ from itertools import chain
|
|||
from binascii import hexlify
|
||||
from collections import namedtuple
|
||||
|
||||
from .bcd_data_stream import BCDataStream
|
||||
from .util import subclass_tuple
|
||||
from lbrynet.wallet.bcd_data_stream import BCDataStream
|
||||
from lbrynet.wallet.util import subclass_tuple
|
||||
|
||||
# bitcoin opcodes
|
||||
OP_0 = 0x00
|
||||
|
@ -21,11 +21,6 @@ OP_PUSHDATA4 = 0x4e
|
|||
OP_2DROP = 0x6d
|
||||
OP_DROP = 0x75
|
||||
|
||||
# lbry custom opcodes
|
||||
OP_CLAIM_NAME = 0xb5
|
||||
OP_SUPPORT_CLAIM = 0xb6
|
||||
OP_UPDATE_CLAIM = 0xb7
|
||||
|
||||
|
||||
# template matching opcodes (not real opcodes)
|
||||
# base class for PUSH_DATA related opcodes
|
||||
|
@ -289,12 +284,7 @@ class Script(object):
|
|||
|
||||
@classmethod
|
||||
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):
|
||||
tokens = self.tokens
|
||||
|
@ -313,7 +303,7 @@ class Script(object):
|
|||
self.source = self.template.generate(self.values)
|
||||
|
||||
|
||||
class InputScript(Script):
|
||||
class BaseInputScript(Script):
|
||||
""" Input / redeem script templates (aka scriptSig) """
|
||||
|
||||
__slots__ = ()
|
||||
|
@ -362,7 +352,7 @@ class InputScript(Script):
|
|||
})
|
||||
|
||||
|
||||
class OutputScript(Script):
|
||||
class BaseOutputScript(Script):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
|
@ -374,48 +364,9 @@ class OutputScript(Script):
|
|||
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 = [
|
||||
PAY_PUBKEY_HASH,
|
||||
PAY_SCRIPT_HASH,
|
||||
CLAIM_NAME_PUBKEY,
|
||||
CLAIM_NAME_SCRIPT,
|
||||
SUPPORT_CLAIM_PUBKEY,
|
||||
SUPPORT_CLAIM_SCRIPT,
|
||||
UPDATE_CLAIM_PUBKEY,
|
||||
UPDATE_CLAIM_SCRIPT
|
||||
]
|
||||
|
||||
@classmethod
|
||||
|
@ -430,14 +381,6 @@ class OutputScript(Script):
|
|||
'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
|
||||
def is_pay_pubkey_hash(self):
|
||||
return self.template.name.endswith('pay_pubkey_hash')
|
||||
|
@ -445,19 +388,3 @@ class OutputScript(Script):
|
|||
@property
|
||||
def is_pay_script_hash(self):
|
||||
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 logging
|
||||
from binascii import hexlify
|
||||
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.hash import sha256, hash160_to_address, claim_id_hash
|
||||
from lbrynet.wallet.script import InputScript, OutputScript
|
||||
from lbrynet.wallet.wallet import Wallet
|
||||
from lbrynet.wallet.hash import sha256
|
||||
from lbrynet.wallet.account import Account
|
||||
from lbrynet.wallet.util import ReadOnlyList
|
||||
|
||||
|
||||
log = logging.getLogger()
|
||||
|
@ -19,11 +17,6 @@ NULL_HASH = '\x00'*32
|
|||
|
||||
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
|
||||
def size(self):
|
||||
""" Size of this input / output in bytes. """
|
||||
|
@ -35,30 +28,39 @@ class InputOutput(object):
|
|||
raise NotImplemented
|
||||
|
||||
|
||||
class Input(InputOutput):
|
||||
class BaseInput(InputOutput):
|
||||
|
||||
script_class = None
|
||||
|
||||
NULL_SIGNATURE = '0'*72
|
||||
NULL_PUBLIC_KEY = '0'*33
|
||||
|
||||
def __init__(self, output_or_txid_index, script, sequence=0xFFFFFFFF):
|
||||
if isinstance(output_or_txid_index, Output):
|
||||
self.output = output_or_txid_index # type: Output
|
||||
if isinstance(output_or_txid_index, BaseOutput):
|
||||
self.output = output_or_txid_index # type: BaseOutput
|
||||
self.output_txid = self.output.transaction.hash
|
||||
self.output_index = self.output.index
|
||||
else:
|
||||
self.output = None # type: Output
|
||||
self.output = None # type: BaseOutput
|
||||
self.output_txid, self.output_index = output_or_txid_index
|
||||
self.sequence = sequence
|
||||
self.is_coinbase = self.output_txid == NULL_HASH
|
||||
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):
|
||||
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
|
||||
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
|
||||
def amount(self):
|
||||
""" Amount this input adds to the transaction. """
|
||||
|
@ -82,7 +84,7 @@ class Input(InputOutput):
|
|||
sequence = stream.read_uint32()
|
||||
return cls(
|
||||
(txid, index),
|
||||
InputScript(script) if not txid == NULL_HASH else script,
|
||||
cls.script_class(script) if not txid == NULL_HASH else script,
|
||||
sequence
|
||||
)
|
||||
|
||||
|
@ -98,86 +100,48 @@ class Input(InputOutput):
|
|||
stream.write_string(self.script.source)
|
||||
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):
|
||||
self.transaction = transaction # type: Transaction
|
||||
self.index = index # type: int
|
||||
def __init__(self, amount, script):
|
||||
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
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.effective_amount < other.effective_amount
|
||||
|
||||
def _add_and_return(self):
|
||||
self.transaction.add_outputs([self])
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def pay_pubkey_hash(cls, transaction, index, amount, pubkey_hash):
|
||||
return cls(
|
||||
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)
|
||||
def pay_pubkey_hash(cls, amount, pubkey_hash):
|
||||
return cls(amount, cls.script_class.pay_pubkey_hash(pubkey_hash))
|
||||
|
||||
@property
|
||||
def effective_amount(self):
|
||||
""" Amount minus fees it would take to spend this output. """
|
||||
if self._effective_amount is None:
|
||||
txi = self.spend()
|
||||
self._effective_amount = txi.effective_amount
|
||||
self._effective_amount = self.input_class.spend(self).effective_amount
|
||||
return self._effective_amount
|
||||
|
||||
@classmethod
|
||||
def deserialize_from(cls, stream, transaction, index):
|
||||
def deserialize_from(cls, stream):
|
||||
return cls(
|
||||
transaction=transaction,
|
||||
index=index,
|
||||
amount=stream.read_uint64(),
|
||||
script=OutputScript(stream.read_string())
|
||||
script=cls.script_class(stream.read_string())
|
||||
)
|
||||
|
||||
def serialize_to(self, stream):
|
||||
stream.write_uint64(self.amount)
|
||||
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):
|
||||
self._raw = raw
|
||||
|
@ -186,8 +150,8 @@ class Transaction:
|
|||
self.version = version # type: int
|
||||
self.locktime = locktime # type: int
|
||||
self.height = height # type: int
|
||||
self.inputs = [] # type: List[Input]
|
||||
self.outputs = [] # type: List[Output]
|
||||
self._inputs = [] # type: List[BaseInput]
|
||||
self._outputs = [] # type: List[BaseOutput]
|
||||
self.is_saved = is_saved # type: bool
|
||||
if raw is not None:
|
||||
self._deserialize()
|
||||
|
@ -211,19 +175,30 @@ class Transaction:
|
|||
return self._raw
|
||||
|
||||
def _reset(self):
|
||||
self._raw = None
|
||||
self._hash = None
|
||||
self._id = None
|
||||
|
||||
def get_claim_id(self, output_index):
|
||||
script = self.outputs[output_index]
|
||||
assert script.script.is_claim_name(), 'Not a name claim.'
|
||||
return claim_id_hash(self.hash, output_index)
|
||||
self._hash = None
|
||||
self._raw = None
|
||||
|
||||
@property
|
||||
def is_complete(self):
|
||||
s, r = self.signature_count()
|
||||
return r == s
|
||||
def inputs(self): # type: () -> ReadOnlyList[BaseInput]
|
||||
return ReadOnlyList(self._inputs)
|
||||
|
||||
@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
|
||||
def fee(self):
|
||||
|
@ -240,30 +215,15 @@ class Transaction:
|
|||
""" Size in bytes of transaction meta data and all outputs; without inputs. """
|
||||
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):
|
||||
stream = BCDataStream()
|
||||
stream.write_uint32(self.version)
|
||||
if with_inputs:
|
||||
stream.write_compact_size(len(self.inputs))
|
||||
for txin in self.inputs:
|
||||
stream.write_compact_size(len(self._inputs))
|
||||
for txin in self._inputs:
|
||||
txin.serialize_to(stream)
|
||||
stream.write_compact_size(len(self.outputs))
|
||||
for txout in self.outputs:
|
||||
stream.write_compact_size(len(self._outputs))
|
||||
for txout in self._outputs:
|
||||
txout.serialize_to(stream)
|
||||
stream.write_uint32(self.locktime)
|
||||
return stream.get_bytes()
|
||||
|
@ -271,14 +231,14 @@ class Transaction:
|
|||
def _serialize_for_signature(self, signing_input):
|
||||
stream = BCDataStream()
|
||||
stream.write_uint32(self.version)
|
||||
stream.write_compact_size(len(self.inputs))
|
||||
for i, txin in enumerate(self.inputs):
|
||||
stream.write_compact_size(len(self._inputs))
|
||||
for i, txin in enumerate(self._inputs):
|
||||
if signing_input == i:
|
||||
txin.serialize_to(stream, txin.output.script.source)
|
||||
else:
|
||||
txin.serialize_to(stream, b'')
|
||||
stream.write_compact_size(len(self.outputs))
|
||||
for txout in self.outputs:
|
||||
stream.write_compact_size(len(self._outputs))
|
||||
for txout in self._outputs:
|
||||
txout.serialize_to(stream)
|
||||
stream.write_uint32(self.locktime)
|
||||
stream.write_uint32(1) # signature hash type: SIGHASH_ALL
|
||||
|
@ -289,58 +249,37 @@ class Transaction:
|
|||
stream = BCDataStream(self._raw)
|
||||
self.version = stream.read_uint32()
|
||||
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()
|
||||
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()
|
||||
|
||||
def add_inputs(self, inputs):
|
||||
self.inputs.extend(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):
|
||||
def sign(self, account): # type: (Account) -> BaseTransaction
|
||||
for i, txi in enumerate(self._inputs):
|
||||
txo_script = txi.output.script
|
||||
if txo_script.is_pay_pubkey_hash:
|
||||
address = hash160_to_address(txo_script.values['pubkey_hash'], wallet.chain)
|
||||
private_key = wallet.get_private_key_for_address(address)
|
||||
address = account.coin.hash160_to_address(txo_script.values['pubkey_hash'])
|
||||
private_key = account.get_private_key_for_address(address)
|
||||
tx = self._serialize_for_signature(i)
|
||||
txi.script.values['signature'] = private_key.sign(tx)+six.int2byte(1)
|
||||
txi.script.values['pubkey'] = private_key.public_key.pubkey_bytes
|
||||
txi.script.generate()
|
||||
self._reset()
|
||||
return True
|
||||
return self
|
||||
|
||||
def sort(self):
|
||||
# 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.outputs.sort(key=lambda o: (o[2], pay_script(o[0], o[1])))
|
||||
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])))
|
||||
|
||||
@property
|
||||
def input_sum(self):
|
||||
return sum(i.amount for i in self.inputs)
|
||||
return sum(i.amount for i in self._inputs)
|
||||
|
||||
@property
|
||||
def output_sum(self):
|
||||
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()
|
||||
return sum(o.amount for o in self._outputs)
|
|
@ -10,14 +10,14 @@
|
|||
|
||||
import struct
|
||||
import hashlib
|
||||
from binascii import unhexlify
|
||||
from six import int2byte, byte2int
|
||||
|
||||
import ecdsa
|
||||
import ecdsa.ellipticcurve as EC
|
||||
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
|
||||
|
||||
|
||||
|
@ -30,7 +30,9 @@ class _KeyBase(object):
|
|||
|
||||
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)):
|
||||
raise TypeError('chain code must be raw bytes')
|
||||
if len(chain_code) != 32:
|
||||
|
@ -42,6 +44,7 @@ class _KeyBase(object):
|
|||
if parent is not None:
|
||||
if not isinstance(parent, type(self)):
|
||||
raise TypeError('parent key has bad type')
|
||||
self.coin = coin
|
||||
self.chain_code = chain_code
|
||||
self.n = n
|
||||
self.depth = depth
|
||||
|
@ -83,8 +86,8 @@ class _KeyBase(object):
|
|||
class PubKey(_KeyBase):
|
||||
""" A BIP32 public key. """
|
||||
|
||||
def __init__(self, pubkey, chain_code, n, depth, parent=None):
|
||||
super(PubKey, self).__init__(chain_code, n, depth, parent)
|
||||
def __init__(self, coin, pubkey, chain_code, n, depth, parent=None):
|
||||
super(PubKey, self).__init__(coin, chain_code, n, depth, parent)
|
||||
if isinstance(pubkey, ecdsa.VerifyingKey):
|
||||
self.verifying_key = pubkey
|
||||
else:
|
||||
|
@ -126,7 +129,7 @@ class PubKey(_KeyBase):
|
|||
@cachedproperty
|
||||
def address(self):
|
||||
""" 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):
|
||||
return self.verifying_key.pubkey.point
|
||||
|
@ -150,7 +153,7 @@ class PubKey(_KeyBase):
|
|||
|
||||
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):
|
||||
""" Return the key's identifier as 20 bytes. """
|
||||
|
@ -158,7 +161,10 @@ class PubKey(_KeyBase):
|
|||
|
||||
def extended_key(self):
|
||||
""" 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):
|
||||
|
@ -180,8 +186,8 @@ class PrivateKey(_KeyBase):
|
|||
|
||||
HARDENED = 1 << 31
|
||||
|
||||
def __init__(self, privkey, chain_code, n, depth, parent=None):
|
||||
super(PrivateKey, self).__init__(chain_code, n, depth, parent)
|
||||
def __init__(self, coin, privkey, chain_code, n, depth, parent=None):
|
||||
super(PrivateKey, self).__init__(coin, chain_code, n, depth, parent)
|
||||
if isinstance(privkey, ecdsa.SigningKey):
|
||||
self.signing_key = privkey
|
||||
else:
|
||||
|
@ -206,11 +212,11 @@ class PrivateKey(_KeyBase):
|
|||
return exponent
|
||||
|
||||
@classmethod
|
||||
def from_seed(cls, seed):
|
||||
def from_seed(cls, coin, seed):
|
||||
# This hard-coded message string seems to be coin-independent...
|
||||
hmac = hmac_sha512(b'Bitcoin seed', seed)
|
||||
privkey, chain_code = hmac[:32], hmac[32:]
|
||||
return cls(privkey, chain_code, 0, 0)
|
||||
return cls(coin, privkey, chain_code, 0, 0)
|
||||
|
||||
@cachedproperty
|
||||
def private_key_bytes(self):
|
||||
|
@ -222,7 +228,7 @@ class PrivateKey(_KeyBase):
|
|||
""" Return the corresponding extended public key. """
|
||||
verifying_key = self.signing_key.get_verifying_key()
|
||||
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)
|
||||
|
||||
def ec_point(self):
|
||||
|
@ -234,7 +240,7 @@ class PrivateKey(_KeyBase):
|
|||
|
||||
def wif(self):
|
||||
""" 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):
|
||||
""" The public key as a P2PKH address. """
|
||||
|
@ -261,7 +267,7 @@ class PrivateKey(_KeyBase):
|
|||
|
||||
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):
|
||||
""" 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):
|
||||
"""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):
|
||||
|
@ -283,7 +292,7 @@ def _exponent_to_bytes(exponent):
|
|||
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."""
|
||||
if not isinstance(ekey, (bytes, bytearray)):
|
||||
raise TypeError('extended key must be raw bytes')
|
||||
|
@ -295,21 +304,21 @@ def _from_extended_key(ekey):
|
|||
n, = struct.unpack('>I', ekey[9:13])
|
||||
chain_code = ekey[13:45]
|
||||
|
||||
if ekey[:4] == unhexlify("0488b21e"):
|
||||
if ekey[:4] == coin.extended_public_key_prefix:
|
||||
pubkey = ekey[45:]
|
||||
key = PubKey(pubkey, chain_code, n, depth)
|
||||
elif ekey[:4] == unhexlify("0488ade4"):
|
||||
key = PubKey(coin, pubkey, chain_code, n, depth)
|
||||
elif ekey[:4] == coin.extended_private_key_prefix:
|
||||
if ekey[45] is not int2byte(0):
|
||||
raise ValueError('invalid extended private key prefix byte')
|
||||
privkey = ekey[46:]
|
||||
key = PrivateKey(privkey, chain_code, n, depth)
|
||||
key = PrivateKey(coin, privkey, chain_code, n, depth)
|
||||
else:
|
||||
raise ValueError('version bytes unrecognised')
|
||||
|
||||
return key
|
||||
|
||||
|
||||
def from_extended_key_string(ekey_str):
|
||||
def from_extended_key_string(coin, ekey_str):
|
||||
"""Given an extended key string, such as
|
||||
|
||||
xpub6BsnM1W2Y7qLMiuhi7f7dbAwQZ5Cz5gYJCRzTNainXzQXYjFwtuQXHd
|
||||
|
@ -317,4 +326,4 @@ def from_extended_key_string(ekey_str):
|
|||
|
||||
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
|
||||
NEW_SEED_VERSION = 11 # 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
|
||||
|
||||
|
||||
MAXIMUM_FEE_PER_BYTE = 50
|
||||
MAXIMUM_FEE_PER_NAME_CHAR = 200000
|
||||
COINBASE_MATURITY = 100
|
||||
CENT = 1000000
|
||||
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
|
||||
|
||||
NO_SIGNATURE = 'ff'
|
||||
|
||||
NULL_HASH = '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
HEADER_SIZE = 112
|
||||
BLOCKS_PER_CHUNK = 96
|
||||
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'}
|
||||
NODES_RETRY_INTERVAL = 60
|
||||
SERVER_RETRY_INTERVAL = 10
|
||||
MAX_BATCH_QUERY_SIZE = 500
|
||||
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 hashlib
|
||||
import hmac
|
||||
import struct
|
||||
from binascii import hexlify, unhexlify
|
||||
|
||||
from .util import bytes_to_int, int_to_bytes
|
||||
from .constants import CHAINS, MAIN_CHAIN
|
||||
|
||||
_sha256 = hashlib.sha256
|
||||
_sha512 = hashlib.sha512
|
||||
|
@ -77,26 +75,6 @@ def hex_str_to_hash(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):
|
||||
return base64.b64encode(aes.encryptData(secret, value.encode('utf8')))
|
||||
|
||||
|
|
|
@ -1,141 +1,83 @@
|
|||
import logging
|
||||
from binascii import unhexlify
|
||||
from operator import itemgetter
|
||||
import functools
|
||||
from typing import List, Dict, Type
|
||||
from twisted.internet import defer
|
||||
|
||||
from lbrynet.wallet.wallet import Wallet
|
||||
from lbrynet.wallet.ledger import Ledger
|
||||
from lbrynet.wallet.protocol import Network
|
||||
from lbrynet.wallet.transaction import Transaction
|
||||
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__)
|
||||
from lbrynet.wallet.account import AccountsView
|
||||
from lbrynet.wallet.basecoin import CoinRegistry
|
||||
from lbrynet.wallet.baseledger import BaseLedger
|
||||
from lbrynet.wallet.wallet import Wallet, WalletStorage
|
||||
|
||||
|
||||
class WalletManager:
|
||||
|
||||
def __init__(self, config=None, wallet=None, ledger=None, network=None):
|
||||
self.config = config or {}
|
||||
self.ledger = ledger or Ledger(self.config)
|
||||
self.wallet = wallet or Wallet()
|
||||
self.wallets = [self.wallet]
|
||||
self.network = network or Network(self.config)
|
||||
self.network.on_header.listen(self.process_header)
|
||||
self.network.on_status.listen(self.process_status)
|
||||
def __init__(self, wallets=None, ledgers=None):
|
||||
self.wallets = wallets or [] # type: List[Wallet]
|
||||
self.ledgers = ledgers or {} # type: Dict[Type[BaseLedger],BaseLedger]
|
||||
self.running = False
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config):
|
||||
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
|
||||
def fee_per_byte(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):
|
||||
def default_wallet(self):
|
||||
for wallet in self.wallets:
|
||||
for address in wallet.addresses:
|
||||
if not self.ledger.has_address(address):
|
||||
yield address
|
||||
return wallet
|
||||
|
||||
def get_least_used_receiving_address(self, max_transactions=1000):
|
||||
return self._get_least_used_address(
|
||||
self.wallet.default_account.receiving_keys.addresses,
|
||||
self.wallet.default_account.receiving_keys,
|
||||
max_transactions
|
||||
@property
|
||||
def default_account(self):
|
||||
for wallet in self.wallets:
|
||||
return wallet.default_account
|
||||
|
||||
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):
|
||||
return self._get_least_used_address(
|
||||
self.wallet.default_account.change_keys.addresses,
|
||||
self.wallet.default_account.change_keys,
|
||||
max_transactions
|
||||
)
|
||||
def create_wallet(self, path, coin_class):
|
||||
storage = WalletStorage(path)
|
||||
wallet = Wallet.from_storage(storage, self)
|
||||
self.wallets.append(wallet)
|
||||
self.create_account(wallet, coin_class)
|
||||
return wallet
|
||||
|
||||
def _get_least_used_address(self, addresses, sequence, max_transactions):
|
||||
address = self.ledger.get_least_used_address(addresses, max_transactions)
|
||||
if address:
|
||||
return address
|
||||
address = sequence.generate_next_address()
|
||||
self.subscribe_history(address)
|
||||
return address
|
||||
def create_account(self, wallet, coin_class):
|
||||
ledger = self.get_or_create_ledger(coin_class.get_id())
|
||||
return wallet.generate_account(ledger)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def start(self):
|
||||
first_connection = self.network.on_connected.first
|
||||
self.network.start()
|
||||
yield first_connection
|
||||
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
|
||||
def start_ledgers(self):
|
||||
self.running = True
|
||||
yield defer.DeferredList([
|
||||
l.start() for l in self.ledgers.values()
|
||||
])
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def update_history(self, address):
|
||||
history = yield self.network.get_history(address)
|
||||
for hash in map(itemgetter('tx_hash'), history):
|
||||
transaction = self.ledger.get_transaction(hash)
|
||||
if not transaction:
|
||||
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)
|
||||
def stop_ledgers(self):
|
||||
yield defer.DeferredList([
|
||||
l.stop() for l in self.ledgers.values()
|
||||
])
|
||||
self.running = False
|
||||
|
|
|
@ -1,4 +1,17 @@
|
|||
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):
|
||||
|
@ -17,6 +30,15 @@ class cachedproperty(object):
|
|||
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):
|
||||
""" Interprets a big-endian sequence of bytes as an integer. """
|
||||
return int(hexlify(be_bytes), 16)
|
||||
|
|
|
@ -1,110 +1,150 @@
|
|||
import stat
|
||||
import json
|
||||
import os
|
||||
from typing import List, Dict
|
||||
|
||||
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:
|
||||
""" 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):
|
||||
self.name = kwargs.get('name', 'Wallet')
|
||||
self.chain = kwargs.get('chain', MAIN_CHAIN)
|
||||
self.accounts = kwargs.get('accounts') or {0: Account.generate()}
|
||||
def __init__(self, name='Wallet', coins=None, accounts=None, storage=None):
|
||||
self.name = name
|
||||
self.coins = coins or [] # type: List[BaseCoin]
|
||||
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
|
||||
def from_json(cls, json_data):
|
||||
if 'accounts' in json_data:
|
||||
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 from_storage(cls, storage, manager): # type: (WalletStorage, 'WalletManager') -> Wallet
|
||||
json_dict = storage.read()
|
||||
|
||||
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 {
|
||||
'name': self.name,
|
||||
'chain': self.chain,
|
||||
'accounts': {
|
||||
a_id: a.to_json() for
|
||||
a_id, a in self.accounts.items()
|
||||
}
|
||||
'coins': {c.get_id(): c.to_dict() for c in self.coins},
|
||||
'accounts': [a.to_dict() for a in self.accounts]
|
||||
}
|
||||
|
||||
def save(self):
|
||||
self.storage.write(self.to_dict())
|
||||
|
||||
@property
|
||||
def default_account(self):
|
||||
return self.accounts.get(0, None)
|
||||
for account in self.accounts:
|
||||
return account
|
||||
|
||||
@property
|
||||
def addresses(self):
|
||||
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():
|
||||
def get_account_private_key_for_address(self, address):
|
||||
for account in self.accounts:
|
||||
private_key = account.get_private_key_for_address(address)
|
||||
if private_key is not None:
|
||||
return private_key
|
||||
return account, private_key
|
||||
|
||||
|
||||
class EphemeralWalletStorage(dict):
|
||||
class WalletStorage:
|
||||
|
||||
LATEST_VERSION = 2
|
||||
|
||||
def save(self):
|
||||
return json.dumps(self, indent=4, sort_keys=True)
|
||||
DEFAULT = {
|
||||
'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):
|
||||
if old in self:
|
||||
old_value = self[old]
|
||||
del self[old]
|
||||
if new not in self:
|
||||
self[new] = old_value
|
||||
if old in json_dict:
|
||||
json_dict[new] = json_dict[old]
|
||||
del json_dict[old]
|
||||
|
||||
if self.get('version', 1) == 1: # upgrade from version 1 to version 2
|
||||
# TODO: `addr_history` should actually be imported into SQLStorage and removed from wallet.
|
||||
version = json_dict.pop('version', -1)
|
||||
|
||||
if version == 1: # upgrade from version 1 to version 2
|
||||
_rename_property('addr_history', 'history')
|
||||
_rename_property('use_encryption', 'encrypted')
|
||||
_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):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
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()
|
||||
json_data = json.dumps(json_dict, indent=4, sort_keys=True)
|
||||
if self.path is None:
|
||||
return json_data
|
||||
|
||||
temp_path = "%s.tmp.%s" % (self.path, os.getpid())
|
||||
with open(temp_path, "w") as f:
|
||||
|
@ -116,12 +156,9 @@ class PermanentWalletStorage(EphemeralWalletStorage):
|
|||
mode = os.stat(self.path).st_mode
|
||||
else:
|
||||
mode = stat.S_IREAD | stat.S_IWRITE
|
||||
|
||||
try:
|
||||
os.rename(temp_path, self.path)
|
||||
except:
|
||||
os.remove(self.path)
|
||||
os.rename(temp_path, self.path)
|
||||
os.chmod(self.path, mode)
|
||||
|
||||
return json_data
|
||||
|
|
Loading…
Add table
Reference in a new issue