wip: implementation is now generic and supports multiple currencies

This commit is contained in:
Lex Berezhny 2018-04-30 03:04:52 -04:00 committed by Jack Robison
parent 83958604d5
commit 5e71dcbaf0
No known key found for this signature in database
GPG key ID: DF25C68FE0239BB2
29 changed files with 1214 additions and 797 deletions

View file

@ -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'

View file

@ -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()

View 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())

View file

@ -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

View file

View 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)

View file

@ -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'
)

View file

@ -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())

View file

@ -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

View file

@ -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()

View 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'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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))

View file

@ -0,0 +1,2 @@
from . import lbc
from . import bitcoin

View 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)

View file

@ -0,0 +1 @@
from .coin import LBC, LBCTestNet, LBCRegTest

View 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')

View 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

View file

@ -0,0 +1,5 @@
from lbrynet.wallet.basenetwork import BaseNetwork
class Network(BaseNetwork):
pass

View 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

View 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)

View file

@ -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
}
}

View file

@ -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')))

View file

@ -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

View file

@ -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)

View file

@ -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