diff --git a/lbry/wallet/__init__.py b/lbry/wallet/__init__.py index 266bec4ad..3e6b218ff 100644 --- a/lbry/wallet/__init__.py +++ b/lbry/wallet/__init__.py @@ -1,3 +1,3 @@ -from .account import Account, AddressManager, SingleKey from .wallet import Wallet from .manager import WalletManager +from .account import Account, SingleKey, HierarchicalDeterministic diff --git a/lbry/wallet/account.py b/lbry/wallet/account.py index 30bbba51e..7984eafd5 100644 --- a/lbry/wallet/account.py +++ b/lbry/wallet/account.py @@ -6,35 +6,23 @@ import asyncio import random from functools import partial from hashlib import sha256 -from string import hexdigits from typing import Type, Dict, Tuple, Optional, Any, List import ecdsa +from lbry.constants import COIN +from lbry.db import Database, CLAIM_TYPE_CODES, TXO_TYPES +from lbry.blockchain import Ledger, Transaction, Input, Output from lbry.error import InvalidPasswordError from lbry.crypto.crypt import aes_encrypt, aes_decrypt from lbry.crypto.bip32 import PrivateKey, PubKey, from_extended_key_string -from lbry.constants import COIN -from lbry.blockchain.transaction import Transaction, Input, Output -from lbry.blockchain.ledger import Ledger -from lbry.db import Database -from lbry.db.constants import CLAIM_TYPE_CODES, TXO_TYPES -from .mnemonic import Mnemonic +from . import mnemonic log = logging.getLogger(__name__) -def validate_claim_id(claim_id): - if not len(claim_id) == 40: - raise Exception("Incorrect claimid length: %i" % len(claim_id)) - if isinstance(claim_id, bytes): - claim_id = claim_id.decode('utf-8') - if set(claim_id).difference(hexdigits): - raise Exception("Claim id is not hex encoded") - - class AddressManager: name: str @@ -48,8 +36,7 @@ class AddressManager: self.address_generator_lock = asyncio.Lock() @classmethod - def from_dict(cls, account: 'Account', d: dict) \ - -> Tuple['AddressManager', 'AddressManager']: + def from_dict(cls, account: 'Account', d: dict) -> Tuple['AddressManager', 'AddressManager']: raise NotImplementedError @classmethod @@ -222,24 +209,21 @@ class SingleKey(AddressManager): class Account: - - mnemonic_class = Mnemonic - private_key_class = PrivateKey - public_key_class = PubKey address_generators: Dict[str, Type[AddressManager]] = { SingleKey.name: SingleKey, HierarchicalDeterministic.name: HierarchicalDeterministic, } - def __init__(self, ledger: 'Ledger', db: 'Database', name: str, - seed: str, private_key_string: str, encrypted: bool, - private_key: Optional[PrivateKey], public_key: PubKey, + def __init__(self, ledger: Ledger, db: Database, name: str, + phrase: str, language: str, private_key_string: str, + encrypted: bool, private_key: Optional[PrivateKey], public_key: PubKey, address_generator: dict, modified_on: float, channel_keys: dict) -> None: self.ledger = ledger self.db = db self.id = public_key.address self.name = name - self.seed = seed + self.phrase = phrase + self.language = language self.modified_on = modified_on self.private_key_string = private_key_string self.init_vectors: Dict[str, bytes] = {} @@ -251,6 +235,7 @@ class Account: self.receiving, self.change = self.address_generator.from_dict(self, address_generator) self.address_managers = {am.chain_number: am for am in {self.receiving, self.change}} self.channel_keys = channel_keys + self._channel_keys_deserialized = {} def get_init_vector(self, key) -> Optional[bytes]: init_vector = self.init_vectors.get(key, None) @@ -259,42 +244,40 @@ class Account: return init_vector @classmethod - def generate(cls, ledger: 'Ledger', db: 'Database', - name: str = None, address_generator: dict = None): - return cls.from_dict(ledger, db, { + async def generate( + cls, ledger: Ledger, db: Database, + name: str = None, language: str = 'en', + address_generator: dict = None): + return await cls.from_dict(ledger, db, { 'name': name, - 'seed': cls.mnemonic_class().make_seed(), + 'seed': await mnemonic.generate_phrase(language), + 'language': language, 'address_generator': address_generator or {} }) @classmethod - def get_private_key_from_seed(cls, ledger: 'Ledger', seed: str, password: str): - return cls.private_key_class.from_seed( - ledger, cls.mnemonic_class.mnemonic_to_seed(seed, password or 'lbryum') - ) - - @classmethod - def keys_from_dict(cls, ledger: 'Ledger', d: dict) \ - -> Tuple[str, Optional[PrivateKey], PubKey]: - seed = d.get('seed', '') + async def keys_from_dict(cls, ledger: Ledger, d: dict) -> Tuple[str, Optional[PrivateKey], PubKey]: + phrase = d.get('seed', '') private_key_string = d.get('private_key', '') private_key = None public_key = None encrypted = d.get('encrypted', False) if not encrypted: - if seed: - private_key = cls.get_private_key_from_seed(ledger, seed, '') + if phrase: + private_key = PrivateKey.from_seed( + ledger, await mnemonic.derive_key_from_phrase(phrase) + ) public_key = private_key.public_key elif private_key_string: private_key = from_extended_key_string(ledger, private_key_string) public_key = private_key.public_key if public_key is None: public_key = from_extended_key_string(ledger, d['public_key']) - return seed, private_key, public_key + return phrase, private_key, public_key @classmethod - def from_dict(cls, ledger: 'Ledger', db: 'Database', d: dict): - seed, private_key, public_key = cls.keys_from_dict(ledger, d) + async def from_dict(cls, ledger: Ledger, db: Database, d: dict): + phrase, private_key, public_key = await cls.keys_from_dict(ledger, d) name = d.get('name') if not name: name = f'Account #{public_key.address}' @@ -302,7 +285,8 @@ class Account: ledger=ledger, db=db, name=name, - seed=seed, + phrase=phrase, + language=d.get('lang', 'en'), private_key_string=d.get('private_key', ''), encrypted=d.get('encrypted', False), private_key=private_key, @@ -313,7 +297,7 @@ class Account: ) def to_dict(self, encrypt_password: str = None, include_channel_keys: bool = True): - private_key_string, seed = self.private_key_string, self.seed + private_key_string, phrase = self.private_key_string, self.phrase if not self.encrypted and self.private_key: private_key_string = self.private_key.extended_key_string() if not self.encrypted and encrypt_password: @@ -321,11 +305,12 @@ class Account: private_key_string = aes_encrypt( encrypt_password, private_key_string, self.get_init_vector('private_key') ) - if seed: - seed = aes_encrypt(encrypt_password, self.seed, self.get_init_vector('seed')) + if phrase: + phrase = aes_encrypt(encrypt_password, self.phrase, self.get_init_vector('phrase')) d = { 'name': self.name, - 'seed': seed, + 'seed': phrase, + 'lang': self.language, 'encrypted': bool(self.encrypted or encrypt_password), 'private_key': private_key_string, 'public_key': self.public_key.extended_key_string(), @@ -367,21 +352,21 @@ class Account: 'address_generator': self.address_generator.to_dict(self.receiving, self.change) } if show_seed: - details['seed'] = self.seed + details['seed'] = self.phrase details['certificates'] = len(self.channel_keys) return details def decrypt(self, password: str) -> bool: assert self.encrypted, "Key is not encrypted." try: - seed = self._decrypt_seed(password) + phrase = self._decrypt_phrase(password) except (ValueError, InvalidPasswordError): return False try: private_key = self._decrypt_private_key_string(password) except (TypeError, ValueError, InvalidPasswordError): return False - self.seed = seed + self.phrase = phrase self.private_key = private_key self.private_key_string = "" self.encrypted = False @@ -397,24 +382,20 @@ class Account: self.ledger, private_key_string ) - def _decrypt_seed(self, password: str) -> str: - if not self.seed: + def _decrypt_phrase(self, password: str) -> str: + if not self.phrase: return "" - seed, self.init_vectors['seed'] = aes_decrypt(password, self.seed) - if not seed: + phrase, self.init_vectors['phrase'] = aes_decrypt(password, self.phrase) + if not phrase: return "" - try: - Mnemonic().mnemonic_decode(seed) - except IndexError: - # failed to decode the seed, this either means it decrypted and is invalid - # or that we hit an edge case where an incorrect password gave valid padding - raise ValueError("Failed to decode seed.") - return seed + if not mnemonic.is_phrase_valid(self.language, phrase): + raise ValueError("Failed to decode seed phrase.") + return phrase def encrypt(self, password: str) -> bool: assert not self.encrypted, "Key is already encrypted." - if self.seed: - self.seed = aes_encrypt(password, self.seed, self.get_init_vector('seed')) + if self.phrase: + self.phrase = aes_encrypt(password, self.phrase, self.get_init_vector('phrase')) if isinstance(self.private_key, PrivateKey): self.private_key_string = aes_encrypt( password, self.private_key.extended_key_string(), self.get_init_vector('private_key') @@ -504,12 +485,20 @@ class Account: public_key_bytes = private_key.get_verifying_key().to_der() channel_pubkey_hash = self.ledger.public_key_to_address(public_key_bytes) self.channel_keys[channel_pubkey_hash] = private_key.to_pem().decode() + self._channel_keys_deserialized[channel_pubkey_hash] = private_key - def get_channel_private_key(self, public_key_bytes): + async def get_channel_private_key(self, public_key_bytes): channel_pubkey_hash = self.ledger.public_key_to_address(public_key_bytes) + private_key = self._channel_keys_deserialized.get(channel_pubkey_hash) + if private_key: + return private_key private_key_pem = self.channel_keys.get(channel_pubkey_hash) if private_key_pem: - return ecdsa.SigningKey.from_pem(private_key_pem, hashfunc=sha256) + private_key = await asyncio.get_running_loop().run_in_executor( + None, ecdsa.SigningKey.from_pem, private_key_pem, sha256 + ) + self._channel_keys_deserialized[channel_pubkey_hash] = private_key + return private_key async def maybe_migrate_certificates(self): def to_der(private_key_pem): diff --git a/lbry/wallet/manager.py b/lbry/wallet/manager.py index cd40150da..ce79c1a70 100644 --- a/lbry/wallet/manager.py +++ b/lbry/wallet/manager.py @@ -29,10 +29,19 @@ class WalletManager: for wallet in self.wallets.values(): return wallet - def get_or_default(self, wallet_id: Optional[str]) -> Optional[Wallet]: + def get_or_default(self, wallet_id: Optional[str]) -> Wallet: if wallet_id: return self[wallet_id] - return self.default + wallet = self.default + if not wallet: + raise ValueError("No wallets available.") + return wallet + + def get_or_default_for_spending(self, wallet_id: Optional[str]) -> Wallet: + wallet = self.get_or_default(wallet_id) + if wallet.is_locked: + raise ValueError("Cannot spend funds with locked wallet, unlock first.") + return wallet @property def path(self): @@ -72,7 +81,7 @@ class WalletManager: create_account=self.ledger.conf.create_default_account ) elif not default_wallet.has_accounts and self.ledger.conf.create_default_account: - default_wallet.accounts.generate() + await default_wallet.accounts.generate() def add(self, wallet: Wallet) -> Wallet: self.wallets[wallet.id] = wallet @@ -92,11 +101,16 @@ class WalletManager: wallet = await Wallet.from_path(self.ledger, self.db, wallet_path) return self.add(wallet) - async def create(self, wallet_id: str, name: str, create_account=False, single_key=False) -> Wallet: + async def create( + self, wallet_id: str, name: str, + create_account=False, language='en', single_key=False) -> Wallet: if wallet_id in self.wallets: raise Exception(f"Wallet with id '{wallet_id}' is already loaded and cannot be created.") wallet_path = os.path.join(self.path, wallet_id) if os.path.exists(wallet_path): raise Exception(f"Wallet at path '{wallet_path}' already exists, use 'wallet_add' to load wallet.") - wallet = await Wallet.create(self.ledger, self.db, wallet_path, name, create_account, single_key) + wallet = await Wallet.create( + self.ledger, self.db, wallet_path, name, + create_account, language, single_key + ) return self.add(wallet) diff --git a/lbry/wallet/mnemonic.py b/lbry/wallet/mnemonic.py index 590710bc3..f29e2d201 100644 --- a/lbry/wallet/mnemonic.py +++ b/lbry/wallet/mnemonic.py @@ -12,19 +12,19 @@ def get_languages(): return words.languages -def normalize(mnemonic: str) -> str: - return ' '.join(unicodedata.normalize('NFKD', mnemonic).lower().split()) +def normalize(phrase: str) -> str: + return ' '.join(unicodedata.normalize('NFKD', phrase).lower().split()) -def is_valid(language, mnemonic): +def is_phrase_valid(language, phrase): local_words = getattr(words, language) - for word in normalize(mnemonic).split(): + for word in normalize(phrase).split(): if word not in local_words: return False - return bool(mnemonic) + return bool(phrase) -def sync_generate(language: str) -> str: +def sync_generate_phrase(language: str) -> str: local_words = getattr(words, language) entropy = randbits(132) nonce = 0 @@ -41,17 +41,17 @@ def sync_generate(language: str) -> str: return seed -def sync_to_seed(mnemonic: str) -> bytes: - return hashlib.pbkdf2_hmac('sha512', normalize(mnemonic).encode(), b'lbryum', 2048) +def sync_derive_key_from_phrase(phrase: str) -> bytes: + return hashlib.pbkdf2_hmac('sha512', normalize(phrase).encode(), b'lbryum', 2048) -async def generate(language: str) -> str: +async def generate_phrase(language: str) -> str: return await asyncio.get_running_loop().run_in_executor( - None, sync_generate, language + None, sync_generate_phrase, language ) -async def to_seed(mnemonic: str) -> bytes: +async def derive_key_from_phrase(phrase: str) -> bytes: return await asyncio.get_running_loop().run_in_executor( - None, sync_to_seed, mnemonic + None, sync_derive_key_from_phrase, phrase ) diff --git a/lbry/wallet/sync.py b/lbry/wallet/sync.py index 7f3059303..b523effc2 100644 --- a/lbry/wallet/sync.py +++ b/lbry/wallet/sync.py @@ -7,7 +7,6 @@ from collections import defaultdict from binascii import hexlify, unhexlify from typing import List, Optional, DefaultDict, NamedTuple -import pylru from lbry.crypto.hash import double_sha256, sha256 from lbry.service.api import Client @@ -80,7 +79,7 @@ class SPVSync(Sync): self._on_ready_controller = EventController() self.on_ready = self._on_ready_controller.stream - self._tx_cache = pylru.lrucache(100000) + #self._tx_cache = pylru.lrucache(100000) self._update_tasks = TaskGroup() self._other_tasks = TaskGroup() # that we dont need to start self._header_processing_lock = asyncio.Lock() diff --git a/lbry/wallet/wallet.py b/lbry/wallet/wallet.py index c20228e24..9f12d7903 100644 --- a/lbry/wallet/wallet.py +++ b/lbry/wallet/wallet.py @@ -3,7 +3,7 @@ import json import zlib import asyncio import logging -from typing import List, Sequence, Tuple, Optional, Iterable +from typing import Awaitable, Callable, List, Tuple, Optional, Iterable, Union from hashlib import sha256 from operator import attrgetter from decimal import Decimal @@ -19,6 +19,7 @@ from lbry.crypto.bip32 import PubKey, PrivateKey from lbry.schema.claim import Claim from lbry.schema.purchase import Purchase from lbry.error import InsufficientFundsError, KeyFeeAboveMaxAllowedError +from lbry.stream.managed_stream import ManagedStream from .account import Account, SingleKey, HierarchicalDeterministic from .coinselection import CoinSelector, OutputEffectiveAmountEstimator @@ -61,10 +62,12 @@ class Wallet: return os.path.basename(self.storage.path) if self.storage.path else self.name @classmethod - async def create(cls, ledger: Ledger, db: Database, path: str, name: str, create_account=False, single_key=False): + async def create( + cls, ledger: Ledger, db: Database, path: str, name: str, + create_account=False, language='en', single_key=False): wallet = cls(ledger, db, name, WalletStorage(path), {}) if create_account: - wallet.accounts.generate(address_generator={ + await wallet.accounts.generate(language=language, address_generator={ 'name': SingleKey.name if single_key else HierarchicalDeterministic.name }) await wallet.save() @@ -88,7 +91,7 @@ class Wallet: preferences=json_dict.get('preferences', {}), ) for account_dict in json_dict.get('accounts', []): - wallet.accounts.add_from_dict(account_dict) + await wallet.accounts.add_from_dict(account_dict) return wallet def to_dict(self, encrypt_password: str = None): @@ -135,13 +138,13 @@ class Wallet: decompressed = zlib.decompress(decrypted) return json.loads(decompressed) - def merge(self, password: str, data: str) -> List[Account]: + async def merge(self, password: str, data: str) -> List[Account]: assert not self.is_locked, "Cannot sync apply on a locked wallet." added_accounts = [] decrypted_data = self.unpack(password, data) self.preferences.merge(decrypted_data.get('preferences', {})) for account_dict in decrypted_data['accounts']: - _, _, pubkey = Account.keys_from_dict(self.ledger, account_dict) + _, _, pubkey = await Account.keys_from_dict(self.ledger, account_dict) account_id = pubkey.address local_match = None for local_account in self.accounts: @@ -182,18 +185,18 @@ class Wallet: def is_encrypted(self) -> bool: return self.is_locked or self.preferences.get(ENCRYPT_ON_DISK, False) - def decrypt(self): + async def decrypt(self): assert not self.is_locked, "Cannot decrypt a locked wallet, unlock first." self.preferences[ENCRYPT_ON_DISK] = False - self.save() + await self.save() return True - def encrypt(self, password): + async def encrypt(self, password): assert not self.is_locked, "Cannot re-encrypt a locked wallet, unlock first." assert password, "Cannot encrypt with blank password." self.encryption_password = password self.preferences[ENCRYPT_ON_DISK] = True - self.save() + await self.save() return True @property @@ -232,7 +235,7 @@ class Wallet: if await account.save_max_gap(): gap_changed = True if gap_changed: - self.save() + await self.save() async def get_effective_amount_estimators(self, funding_accounts: Iterable[Account]): estimators = [] @@ -260,9 +263,9 @@ class Wallet: **constraints ), self.ledger) - async def create_transaction(self, inputs: Iterable[Input], outputs: Iterable[Output], - funding_accounts: Iterable[Account], change_account: Account, - sign: bool = True): + async def create_transaction( + self, inputs: Iterable[Input], outputs: Iterable[Output], + funding_accounts: Iterable[Account], change_account: Account): """ Find optimal set of inputs when only outputs are provided; add change outputs if only inputs are provided or if inputs are greater than outputs. """ @@ -318,11 +321,7 @@ class Wallet: # less than the fee, after 5 attempts we give up and go home cost += cost_of_change + 1 - if sign: - await self.sign(tx) - except Exception as e: - log.exception('Failed to create transaction:') await self.db.release_tx(tx) raise e @@ -374,6 +373,15 @@ class Wallet: 'Failed to display wallet state, please file issue ' 'for this bug along with the traceback you see below:') + async def verify_duplicate(self, name: str, allow_duplicate: bool): + if not allow_duplicate: + claims, _ = await self.claims.list(claim_name=name) + if len(claims) > 0: + raise Exception( + f"You already have a claim published under the name '{name}'. " + f"Use --allow-duplicate-name flag to override." + ) + class AccountListManager: __slots__ = 'wallet', '_accounts' @@ -399,13 +407,15 @@ class AccountListManager: for account in self: return account - def generate(self, name: str = None, address_generator: dict = None) -> Account: - account = Account.generate(self.wallet.ledger, self.wallet.db, name, address_generator) + async def generate(self, name: str = None, language: str = 'en', address_generator: dict = None) -> Account: + account = await Account.generate( + self.wallet.ledger, self.wallet.db, name, language, address_generator + ) self._accounts.append(account) return account - def add_from_dict(self, account_dict: dict) -> Account: - account = Account.from_dict(self.wallet.ledger, self.wallet.db, account_dict) + async def add_from_dict(self, account_dict: dict) -> Account: + account = await Account.from_dict(self.wallet.ledger, self.wallet.db, account_dict) self._accounts.append(account) return account @@ -424,7 +434,9 @@ class AccountListManager: return self.default return self[account_id] - def get_or_all(self, account_ids: List[str]) -> List[Account]: + def get_or_all(self, account_ids: Union[List[str], str]) -> List[Account]: + if account_ids and isinstance(account_ids, str): + account_ids = [account_ids] return [self[account_id] for account_id in account_ids] if account_ids else self._accounts async def get_account_details(self, **kwargs): @@ -437,16 +449,15 @@ class AccountListManager: class BaseListManager: - __slots__ = 'wallet', 'db' + __slots__ = 'wallet', def __init__(self, wallet: Wallet): self.wallet = wallet - self.db = wallet.db async def create(self, **kwargs) -> Transaction: raise NotImplementedError - async def delete(self, **constraints): + async def delete(self, **constraints) -> Transaction: raise NotImplementedError async def list(self, **constraints) -> Tuple[List[Output], Optional[int]]: @@ -463,21 +474,39 @@ class ClaimListManager(BaseListManager): name = 'claim' __slots__ = () - async def create( + async def _create( self, name: str, claim: Claim, amount: int, holding_address: str, - funding_accounts: List[Account], change_account: Account, signing_channel: Output = None): - claim_output = Output.pay_claim_name_pubkey_hash( + funding_accounts: List[Account], change_account: Account, + signing_channel: Output = None) -> Transaction: + txo = Output.pay_claim_name_pubkey_hash( amount, name, claim, self.wallet.ledger.address_to_hash160(holding_address) ) if signing_channel is not None: - claim_output.sign(signing_channel, b'placeholder txid:nout') - return await self.wallet.create_transaction( - [], [claim_output], funding_accounts, change_account, sign=False + txo.sign(signing_channel, b'placeholder txid:nout') + tx = await self.wallet.create_transaction( + [], [txo], funding_accounts, change_account ) + return tx + + async def create( + self, name: str, claim: Claim, amount: int, holding_address: str, + funding_accounts: List[Account], change_account: Account, + signing_channel: Output = None) -> Transaction: + tx = await self._create( + name, claim, amount, holding_address, + funding_accounts, change_account, + signing_channel + ) + txo = tx.outputs[0] + if signing_channel is not None: + txo.sign(signing_channel) + await self.wallet.sign(tx) + return tx async def update( self, previous_claim: Output, claim: Claim, amount: int, holding_address: str, - funding_accounts: List[Account], change_account: Account, signing_channel: Output = None): + funding_accounts: List[Account], change_account: Account, + signing_channel: Output = None) -> Transaction: updated_claim = Output.pay_update_claim_pubkey_hash( amount, previous_claim.claim_name, previous_claim.claim_id, claim, self.wallet.ledger.address_to_hash160(holding_address) @@ -497,7 +526,7 @@ class ClaimListManager(BaseListManager): ) async def list(self, **constraints) -> Tuple[List[Output], Optional[int]]: - return await self.db.get_claims(wallet=self.wallet, **constraints) + return await self.wallet.db.get_claims(wallet=self.wallet, **constraints) async def get(self, claim_id=None, claim_name=None, txid=None, nout=None) -> Output: if txid is not None and nout is not None: @@ -523,59 +552,157 @@ class ClaimListManager(BaseListManager): return await self.get(claim_id, claim_name, txid, nout) +class ChannelListManager(ClaimListManager): + name = 'channel' + __slots__ = () + + async def create( + self, name: str, amount: int, holding_account: Account, + funding_accounts: List[Account], save_key=True, **kwargs) -> Transaction: + + holding_address = await holding_account.receiving.get_or_create_usable_address() + + claim = Claim() + claim.channel.update(**kwargs) + txo = Output.pay_claim_name_pubkey_hash( + amount, name, claim, self.wallet.ledger.address_to_hash160(holding_address) + ) + + await txo.generate_channel_private_key() + + tx = await self.wallet.create_transaction( + [], [txo], funding_accounts, funding_accounts[0] + ) + + await self.wallet.sign(tx) + + if save_key: + holding_account.add_channel_private_key(txo.private_key) + await self.wallet.save() + + return tx + + async def update( + self, old: Output, amount: int, new_signing_key: bool, replace: bool, + holding_account: Account, funding_accounts: List[Account], + save_key=True, **kwargs) -> Transaction: + + moving_accounts = False + holding_address = old.get_address(self.wallet.ledger) + if holding_account: + old_account = await self.wallet.get_account_for_address(holding_address) + if holding_account.id != old_account.id: + holding_address = await holding_account.receiving.get_or_create_usable_address() + moving_accounts = True + elif new_signing_key: + holding_account = await self.wallet.get_account_for_address(holding_address) + + if replace: + claim = Claim() + claim.channel.public_key_bytes = old.claim.channel.public_key_bytes + else: + claim = Claim.from_bytes(old.claim.to_bytes()) + claim.channel.update(**kwargs) + + txo = Output.pay_update_claim_pubkey_hash( + amount, old.claim_name, old.claim_id, claim, + self.wallet.ledger.address_to_hash160(holding_address) + ) + + if new_signing_key: + await txo.generate_channel_private_key() + else: + txo.private_key = old.private_key + + tx = await self.wallet.create_transaction( + [Input.spend(old)], [txo], funding_accounts, funding_accounts[0] + ) + + await self.wallet.sign(tx) + + if any((new_signing_key, moving_accounts)) and save_key: + holding_account.add_channel_private_key(txo.private_key) + await self.wallet.save() + + return tx + + async def list(self, **constraints) -> Tuple[List[Output], Optional[int]]: + return await self.wallet.db.get_channels(wallet=self.wallet, **constraints) + + async def get_for_signing(self, channel_id=None, channel_name=None) -> Output: + channel = await self.get(claim_id=channel_id, claim_name=channel_name) + if not channel.has_private_key: + raise Exception( + f"Couldn't find private key for channel '{channel.claim_name}', " + f"can't use channel for signing. " + ) + return channel + + async def get_for_signing_or_none(self, channel_id=None, channel_name=None) -> Optional[Output]: + if channel_id or channel_name: + return await self.get_for_signing(channel_id, channel_name) + + class StreamListManager(ClaimListManager): __slots__ = () - async def create(self, *args, **kwargs): - return await super().create(*args, **kwargs) + async def create( + self, name: str, amount: int, file_path: str, + create_file_stream: Callable[[str], Awaitable[ManagedStream]], + holding_address: str, funding_accounts: List[Account], + signing_channel: Optional[Output] = None, + preview=False, **kwargs) -> Tuple[Transaction, ManagedStream]: + + claim = Claim() + claim.stream.update(file_path=file_path, sd_hash='0' * 96, **kwargs) + + # before creating file stream, create TX to ensure we have enough LBC + tx = await self._create( + name, claim, amount, holding_address, + funding_accounts, funding_accounts[0], + signing_channel + ) + txo = tx.outputs[0] + + file_stream = None + try: + + # we have enough LBC to create TX, now try create the file stream + if not preview: + file_stream = await create_file_stream(file_path) + claim.stream.source.sd_hash = file_stream.sd_hash + txo.script.generate() + + # creating TX and file stream was successful, now sign all the things + if signing_channel is not None: + txo.sign(signing_channel) + await self.wallet.sign(tx) + + except Exception as e: + # creating file stream or something else went wrong, release txos + await self.wallet.db.release_tx(tx) + raise e + + return tx, file_stream async def list(self, **constraints) -> Tuple[List[Output], Optional[int]]: - return await self.db.get_streams(wallet=self.wallet, **constraints) + return await self.wallet.db.get_streams(wallet=self.wallet, **constraints) class CollectionListManager(ClaimListManager): __slots__ = () - async def create(self, *args, **kwargs): - return await super().create(*args, **kwargs) - - async def list(self, **constraints) -> Tuple[List[Output], Optional[int]]: - return await self.db.get_collections(wallet=self.wallet, **constraints) - - -class ChannelListManager(ClaimListManager): - name = 'channel' - __slots__ = () - - async def create(self, name: str, amount: int, account: Account, funding_accounts: List[Account], - claim_address: str, preview=False, **kwargs): + async def create( + self, name: str, amount: int, holding_address: str, funding_accounts: List[Account], + channel: Optional[Output] = None, **kwargs) -> Transaction: claim = Claim() - claim.channel.update(**kwargs) - tx = await super().create( - name, claim, amount, claim_address, funding_accounts, funding_accounts[0] + claim.collection.update(**kwargs) + return await super().create( + name, claim, amount, holding_address, funding_accounts, funding_accounts[0], channel ) - txo = tx.outputs[0] - txo.generate_channel_private_key() - await self.wallet.sign(tx) - if not preview: - account.add_channel_private_key(txo.private_key) - await self.wallet.save() - return tx async def list(self, **constraints) -> Tuple[List[Output], Optional[int]]: - return await self.db.get_channels(wallet=self.wallet, **constraints) - - async def get_for_signing(self, **kwargs) -> Output: - channel = await self.get(**kwargs) - if not channel.has_private_key: - raise Exception( - f"Couldn't find private key for channel '{channel.claim_name}', can't use channel for signing. " - ) - return channel - - async def get_for_signing_or_none(self, **kwargs) -> Optional[Output]: - if any(kwargs.values()): - return await self.get_for_signing(**kwargs) + return await self.wallet.db.get_collections(wallet=self.wallet, **constraints) class SupportListManager(BaseListManager): @@ -591,7 +718,7 @@ class SupportListManager(BaseListManager): ) async def list(self, **constraints) -> Tuple[List[Output], Optional[int]]: - return await self.db.get_supports(**constraints) + return await self.wallet.db.get_supports(**constraints) async def get(self, **constraints) -> Output: raise NotImplementedError @@ -645,7 +772,7 @@ class PurchaseListManager(BaseListManager): ) async def list(self, **constraints) -> Tuple[List[Output], Optional[int]]: - return await self.db.get_purchases(**constraints) + return await self.wallet.db.get_purchases(**constraints) async def get(self, **constraints) -> Output: raise NotImplementedError diff --git a/lbry/wallet/words/__init__.py b/lbry/wallet/words/__init__.py index 1d013275d..500390464 100644 --- a/lbry/wallet/words/__init__.py +++ b/lbry/wallet/words/__init__.py @@ -2,7 +2,6 @@ from .english import words as en from .french import words as fr from .italian import words as it from .japanese import words as ja -from .portuguese import words as pt from .spanish import words as es -from .chinese_simplified import words as zh -languages = 'en', 'fr', 'it', 'ja', 'pt', 'es', 'zh +from .chinese import words as zh +languages = 'en', 'fr', 'it', 'ja', 'es', 'zh'