refactored wallet and mnemonic
This commit is contained in:
parent
7c4f943bcb
commit
be6ebf0047
7 changed files with 293 additions and 165 deletions
|
@ -1,3 +1,3 @@
|
|||
from .account import Account, AddressManager, SingleKey
|
||||
from .wallet import Wallet
|
||||
from .manager import WalletManager
|
||||
from .account import Account, SingleKey, HierarchicalDeterministic
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in a new issue