This commit is contained in:
Lex Berezhny 2019-03-24 16:55:04 -04:00
parent 8fc4f4aa73
commit d47575e8e0
19 changed files with 1360 additions and 1177 deletions

View file

@ -21,10 +21,6 @@ CURRENCIES = {
'LBC': {'type': 'crypto'},
'USD': {'type': 'fiat'},
}
SLACK_WEBHOOK = (
'nUE0pUZ6Yl9bo29epl5moTSwnl5wo20ip2IlqzywMKZiIQSFZR5'
'AHx4mY0VmF0WQZ1ESEP9kMHZlp1WzJwWOoKN3ImR1M2yUAaMyqGZ='
)
HEADERS_FILE_SHA256_CHECKSUM = (
366295, 'b0c8197153a33ccbc52fb81a279588b6015b68b7726f73f6a2b81f7e25bfe4b9'
)

View file

@ -265,10 +265,7 @@ class WalletComponent(Component):
async def start(self):
log.info("Starting torba wallet")
storage = self.component_manager.get_component(DATABASE_COMPONENT)
lbrynet.schema.BLOCKCHAIN_NAME = self.conf.blockchain_name
self.wallet_manager = await LbryWalletManager.from_lbrynet_config(self.conf, storage)
self.wallet_manager.old_db = storage
self.wallet_manager = await LbryWalletManager.from_lbrynet_config(self.conf)
await self.wallet_manager.start()
async def stop(self):

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,13 @@
import os.path
from typing import List, Tuple
from decimal import Decimal
from binascii import hexlify, unhexlify
from google.protobuf.json_format import MessageToDict
from google.protobuf.message import DecodeError
from hachoir.parser import createParser as binary_file_parser
from hachoir.metadata import extractMetadata as binary_file_metadata
from hachoir.core.log import log as hachoir_log
from torba.client.hash import Base58
from torba.client.constants import COIN
@ -11,6 +15,10 @@ from torba.client.constants import COIN
from lbrynet.schema.types.v2.claim_pb2 import Claim as ClaimMessage, Fee as FeeMessage
from lbrynet.schema import compat
from lbrynet.schema.base import Signable
from lbrynet.schema.mime_types import guess_media_type
hachoir_log.use_print = False
class Claim(Signable):
@ -54,6 +62,9 @@ class Claim(Signable):
def channel(self) -> 'Channel':
return Channel(self)
def to_dict(self):
return MessageToDict(self.message, preserving_proto_field_name=True)
@classmethod
def from_bytes(cls, data: bytes) -> 'Claim':
try:
@ -208,225 +219,243 @@ class Fee:
self._fee.currency = FeeMessage.USD
class Channel:
class BaseClaimSubType:
__slots__ = '_claim', '_channel'
__slots__ = 'claim', 'message'
def __init__(self, claim: Claim = None):
self._claim = claim or Claim()
self._channel = self._claim.channel_message
def to_dict(self):
return MessageToDict(self._channel)
def __init__(self, claim: Claim):
self.claim = claim or Claim()
@property
def claim(self) -> Claim:
return self._claim
def title(self) -> str:
return self.message.title
@title.setter
def title(self, title: str):
self.message.title = title
@property
def description(self) -> str:
return self.message.description
@description.setter
def description(self, description: str):
self.message.description = description
@property
def language(self) -> str:
return self.message.language
@language.setter
def language(self, language: str):
self.message.language = language
@property
def thumbnail_url(self) -> str:
return self.message.thumbnail_url
@thumbnail_url.setter
def thumbnail_url(self, thumbnail_url: str):
self.message.thumbnail_url = thumbnail_url
@property
def tags(self) -> List:
return self._channel.tags
return self.message.tags
def to_dict(self):
return MessageToDict(self.message, preserving_proto_field_name=True)
def update(self, tags=None, clear_tags=False, **kwargs):
if clear_tags:
self.message.ClearField('tags')
if tags is not None:
if isinstance(tags, str):
self.tags.append(tags)
elif isinstance(tags, list):
self.tags.extend(tags)
else:
raise ValueError(f"Unknown tag type: {tags}")
for key, value in kwargs.items():
setattr(self, key, value)
class Channel(BaseClaimSubType):
__slots__ = ()
def __init__(self, claim: Claim = None):
super().__init__(claim)
self.message = self.claim.channel_message
@property
def public_key(self) -> str:
return hexlify(self._channel.public_key).decode()
return hexlify(self.message.public_key).decode()
@public_key.setter
def public_key(self, sd_public_key: str):
self._channel.public_key = unhexlify(sd_public_key.encode())
self.message.public_key = unhexlify(sd_public_key.encode())
@property
def public_key_bytes(self) -> bytes:
return self._channel.public_key
return self.message.public_key
@public_key_bytes.setter
def public_key_bytes(self, public_key: bytes):
self._channel.public_key = public_key
@property
def language(self) -> str:
return self._channel.language
@language.setter
def language(self, language: str):
self._channel.language = language
@property
def title(self) -> str:
return self._channel.title
@title.setter
def title(self, title: str):
self._channel.title = title
@property
def description(self) -> str:
return self._channel.description
@description.setter
def description(self, description: str):
self._channel.description = description
self.message.public_key = public_key
@property
def contact_email(self) -> str:
return self._channel.contact_email
return self.message.contact_email
@contact_email.setter
def contact_email(self, contact_email: str):
self._channel.contact_email = contact_email
self.message.contact_email = contact_email
@property
def homepage_url(self) -> str:
return self._channel.homepage_url
return self.message.homepage_url
@homepage_url.setter
def homepage_url(self, homepage_url: str):
self._channel.homepage_url = homepage_url
@property
def thumbnail_url(self) -> str:
return self._channel.thumbnail_url
@thumbnail_url.setter
def thumbnail_url(self, thumbnail_url: str):
self._channel.thumbnail_url = thumbnail_url
self.message.homepage_url = homepage_url
@property
def cover_url(self) -> str:
return self._channel.cover_url
return self.message.cover_url
@cover_url.setter
def cover_url(self, cover_url: str):
self._channel.cover_url = cover_url
self.message.cover_url = cover_url
class Stream:
class Stream(BaseClaimSubType):
__slots__ = '_claim', '_stream'
__slots__ = ()
def __init__(self, claim: Claim = None):
self._claim = claim or Claim()
self._stream = self._claim.stream_message
super().__init__(claim)
self.message = self.claim.stream_message
def to_dict(self):
return MessageToDict(self._stream)
def update(
self, file_path=None, duration=None,
fee_currency=None, fee_amount=None, fee_address=None,
video_height=None, video_width=None,
**kwargs):
@property
def claim(self) -> Claim:
return self._claim
super().update(**kwargs)
if video_height is not None:
self.video.height = video_height
if video_width is not None:
self.video.width = video_width
if file_path is not None:
self.media_type = guess_media_type(file_path)
if not os.path.isfile(file_path):
raise Exception(f"File does not exist: {file_path}")
self.file.size = os.path.getsize(file_path)
if self.file.size == 0:
raise Exception(f"Cannot publish empty file: {file_path}")
if fee_amount and fee_currency:
if fee_address:
self.fee.address = fee_address
if fee_currency.lower() == 'lbc':
self.fee.lbc = Decimal(fee_amount)
elif fee_currency.lower() == 'usd':
self.fee.usd = Decimal(fee_amount)
else:
raise Exception(f'Unknown currency type: {fee_currency}')
if duration is not None:
self.duration = duration
elif file_path is not None:
try:
file_metadata = binary_file_metadata(binary_file_parser(file_path))
self.duration = file_metadata.getValues('duration')[0].seconds
except:
pass
@property
def video(self) -> Video:
return Video(self._stream.video)
return Video(self.message.video)
@property
def file(self) -> File:
return File(self._stream.file)
return File(self.message.file)
@property
def fee(self) -> Fee:
return Fee(self._stream.fee)
return Fee(self.message.fee)
@property
def has_fee(self) -> bool:
return self._stream.HasField('fee')
@property
def tags(self) -> List:
return self._stream.tags
return self.message.HasField('fee')
@property
def hash(self) -> str:
return hexlify(self._stream.hash).decode()
return hexlify(self.message.hash).decode()
@hash.setter
def hash(self, sd_hash: str):
self._stream.hash = unhexlify(sd_hash.encode())
self.message.hash = unhexlify(sd_hash.encode())
@property
def hash_bytes(self) -> bytes:
return self._stream.hash
return self.message.hash
@hash_bytes.setter
def hash_bytes(self, hash: bytes):
self._stream.hash = hash
@property
def language(self) -> str:
return self._stream.language
@language.setter
def language(self, language: str):
self._stream.language = language
@property
def title(self) -> str:
return self._stream.title
@title.setter
def title(self, title: str):
self._stream.title = title
self.message.hash = hash
@property
def author(self) -> str:
return self._stream.author
return self.message.author
@author.setter
def author(self, author: str):
self._stream.author = author
@property
def description(self) -> str:
return self._stream.description
@description.setter
def description(self, description: str):
self._stream.description = description
self.message.author = author
@property
def media_type(self) -> str:
return self._stream.media_type
return self.message.media_type
@media_type.setter
def media_type(self, media_type: str):
self._stream.media_type = media_type
self.message.media_type = media_type
@property
def license(self) -> str:
return self._stream.license
return self.message.license
@license.setter
def license(self, license: str):
self._stream.license = license
self.message.license = license
@property
def license_url(self) -> str:
return self._stream.license_url
return self.message.license_url
@license_url.setter
def license_url(self, license_url: str):
self._stream.license_url = license_url
@property
def thumbnail_url(self) -> str:
return self._stream.thumbnail_url
@thumbnail_url.setter
def thumbnail_url(self, thumbnail_url: str):
self._stream.thumbnail_url = thumbnail_url
self.message.license_url = license_url
@property
def duration(self) -> int:
return self._stream.duration
return self.message.duration
@duration.setter
def duration(self, duration: int):
self._stream.duration = duration
self.message.duration = duration
@property
def release_time(self) -> int:
return self._stream.release_time
return self.message.release_time
@release_time.setter
def release_time(self, release_time: int):
self._stream.release_time = release_time
self.message.release_time = release_time

View file

@ -4,7 +4,7 @@ import typing
import logging
import binascii
from lbrynet.utils import generate_id
from lbrynet.extras.daemon.mime_types import guess_media_type
from lbrynet.schema.mime_types import guess_media_type
from lbrynet.stream.downloader import StreamDownloader
from lbrynet.stream.descriptor import StreamDescriptor
from lbrynet.stream.reflector.client import StreamReflectorClient

View file

@ -1,12 +1,16 @@
import json
import logging
import binascii
import typing
from hashlib import sha256
from string import hexdigits
from torba.client.baseaccount import BaseAccount
from torba.client.basetransaction import TXORef
if typing.TYPE_CHECKING:
from lbrynet.wallet import ledger
log = logging.getLogger(__name__)
@ -21,31 +25,32 @@ def validate_claim_id(claim_id):
class Account(BaseAccount):
ledger: 'ledger.MainNetLedger'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.certificates = {}
self.channel_keys = {}
@property
def hash(self) -> bytes:
h = sha256(json.dumps(self.to_dict(False)).encode())
for cert in sorted(self.certificates.keys()):
for cert in sorted(self.channel_keys.keys()):
h.update(cert.encode())
return h.digest()
def apply(self, d: dict):
super().apply(d)
self.certificates.update(d.get('certificates', {}))
self.channel_keys.update(d.get('certificates', {}))
def add_certificate_private_key(self, ref: TXORef, private_key):
assert ref.id not in self.certificates, 'Trying to add a duplicate certificate.'
self.certificates[ref.id] = private_key
def add_channel_private_key(self, ref: TXORef, private_key):
assert ref.id not in self.channel_keys, 'Trying to add a duplicate channel private key.'
self.channel_keys[ref.id] = private_key
def get_certificate_private_key(self, ref: TXORef):
return self.certificates.get(ref.id)
def get_channel_private_key(self, ref: TXORef):
return self.channel_keys.get(ref.id)
async def maybe_migrate_certificates(self):
if not self.certificates:
if not self.channel_keys:
return
addresses = {}
@ -59,7 +64,7 @@ class Account(BaseAccount):
}
double_hex_encoded_to_pop = []
for maybe_claim_id in list(self.certificates):
for maybe_claim_id in list(self.channel_keys):
if ':' not in maybe_claim_id:
try:
validate_claim_id(maybe_claim_id)
@ -71,7 +76,7 @@ class Account(BaseAccount):
maybe_claim_id_bytes = maybe_claim_id_bytes.encode()
decoded_double_hex = binascii.unhexlify(maybe_claim_id_bytes).decode()
validate_claim_id(decoded_double_hex)
if decoded_double_hex in self.certificates:
if decoded_double_hex in self.channel_keys:
log.warning("don't know how to migrate certificate %s", decoded_double_hex)
else:
log.info("claim id was double hex encoded, fixing it")
@ -80,9 +85,9 @@ class Account(BaseAccount):
continue
for double_encoded_claim_id, correct_claim_id in double_hex_encoded_to_pop:
self.certificates[correct_claim_id] = self.certificates.pop(double_encoded_claim_id)
self.channel_keys[correct_claim_id] = self.channel_keys.pop(double_encoded_claim_id)
for maybe_claim_id in list(self.certificates):
for maybe_claim_id in list(self.channel_keys):
results['total'] += 1
if ':' not in maybe_claim_id:
try:
@ -117,8 +122,8 @@ class Account(BaseAccount):
.format(maybe_claim_id)
)
tx_nout = '{txid}:{nout}'.format(**claim)
self.certificates[tx_nout] = self.certificates[maybe_claim_id]
del self.certificates[maybe_claim_id]
self.channel_keys[tx_nout] = self.channel_keys[maybe_claim_id]
del self.channel_keys[maybe_claim_id]
log.info(
"Migrated certificate with claim_id '%s' ('%s') to a new look up key %s.",
maybe_claim_id, txo.script.values['claim_name'], tx_nout
@ -186,18 +191,18 @@ class Account(BaseAccount):
@classmethod
def from_dict(cls, ledger, wallet, d: dict) -> 'Account':
account = super().from_dict(ledger, wallet, d)
account.certificates = d.get('certificates', {})
account.channel_keys = d.get('certificates', {})
return account
def to_dict(self, include_certificates=True):
def to_dict(self, include_channel_keys=True):
d = super().to_dict()
if include_certificates:
d['certificates'] = self.certificates
if include_channel_keys:
d['certificates'] = self.channel_keys
return d
async def get_details(self, **kwargs):
details = await super().get_details(**kwargs)
details['certificates'] = len(self.certificates)
details['certificates'] = len(self.channel_keys)
return details
def get_claim(self, claim_id=None, txid=None, nout=None):
@ -207,15 +212,15 @@ class Account(BaseAccount):
return self.ledger.db.get_claims(**{'account': self, 'txo.txid': txid, 'txo.position': nout})
@staticmethod
def constraint_utxos_sans_claims(constraints):
def constraint_spending_utxos(constraints):
constraints.update({'is_claim': 0, 'is_update': 0, 'is_support': 0})
def get_utxos(self, **constraints):
self.constraint_utxos_sans_claims(constraints)
self.constraint_spending_utxos(constraints)
return super().get_utxos(**constraints)
def get_utxo_count(self, **constraints):
self.constraint_utxos_sans_claims(constraints)
self.constraint_spending_utxos(constraints)
return super().get_utxo_count(**constraints)
def get_claims(self, **constraints):
@ -230,6 +235,12 @@ class Account(BaseAccount):
def get_channel_count(self, **constraints):
return self.ledger.db.get_channel_count(account=self, **constraints)
def get_supports(self, **constraints):
return self.ledger.db.get_supports(account=self, **constraints)
def get_support_count(self, **constraints):
return self.ledger.db.get_support_count(account=self, **constraints)
async def send_to_addresses(self, amount, addresses, broadcast=False):
tx_class = self.ledger.transaction_class
tx = await tx_class.create(

View file

@ -1,5 +1,9 @@
from typing import List
from torba.client.basedatabase import BaseDatabase
from lbrynet.wallet.transaction import Output
class WalletDatabase(BaseDatabase):
@ -48,7 +52,7 @@ class WalletDatabase(BaseDatabase):
row['claim_name'] = txo.claim_name
return row
async def get_txos(self, **constraints):
async def get_txos(self, **constraints) -> List[Output]:
my_account = constraints.get('my_account', constraints.get('account', None))
txos = await super().get_txos(**constraints)
@ -58,8 +62,8 @@ class WalletDatabase(BaseDatabase):
if txo.script.is_claim_name or txo.script.is_update_claim:
if txo.claim.is_signed:
channel_ids.add(txo.claim.signing_channel_id)
if txo.claim_name.startswith('@') and my_account is not None:
txo.private_key = my_account.get_certificate_private_key(txo.ref)
if txo.claim.is_channel and my_account is not None:
txo.private_key = my_account.get_channel_private_key(txo.ref)
if channel_ids:
channels = {
@ -77,11 +81,11 @@ class WalletDatabase(BaseDatabase):
@staticmethod
def constrain_claims(constraints):
constraints['claim_type__any'] = {'is_claim': 1, 'is_update': 1, 'is_support': 1}
constraints['claim_type__any'] = {'is_claim': 1, 'is_update': 1}
def get_claims(self, **constraints):
async def get_claims(self, **constraints) -> List[Output]:
self.constrain_claims(constraints)
return self.get_utxos(**constraints)
return await self.get_utxos(**constraints)
def get_claim_count(self, **constraints):
self.constrain_claims(constraints)
@ -100,22 +104,17 @@ class WalletDatabase(BaseDatabase):
self.constrain_channels(constraints)
return self.get_claim_count(**constraints)
async def get_certificates(self, private_key_accounts, exclude_without_key=False, **constraints):
channels = await self.get_channels(**constraints)
certificates = []
if private_key_accounts is not None:
for channel in channels:
if not channel.has_private_key:
private_key = None
for account in private_key_accounts:
private_key = account.get_certificate_private_key(channel.ref)
if private_key is not None:
break
if private_key is None and exclude_without_key:
continue
channel.private_key = private_key
certificates.append(channel)
return certificates
@staticmethod
def constrain_supports(constraints):
constraints['is_support'] = 1
def get_supports(self, **constraints):
self.constrain_supports(constraints)
return self.get_utxos(**constraints)
def get_support_count(self, **constraints):
self.constrain_supports(constraints)
return self.get_utxo_count(**constraints)
async def release_all_outputs(self, account):
await self.db.execute(

View file

@ -29,6 +29,8 @@ class MainNetLedger(BaseLedger):
network_class = Network
transaction_class = Transaction
db: WalletDatabase
secret_prefix = bytes((0x1c,))
pubkey_address_prefix = bytes((0x55,))
script_address_prefix = bytes((0x7a,))
@ -97,7 +99,7 @@ class MainNetLedger(BaseLedger):
log.info("Loaded account %s with %s LBC, %d receiving addresses (gap: %d), "
"%d change addresses (gap: %d), %d channels, %d certificates and %d claims. ",
account.id, balance, total_receiving, account.receiving.gap, total_change, account.change.gap,
channel_count, len(account.certificates), claim_count)
channel_count, len(account.channel_keys), claim_count)
class TestNetLedger(MainNetLedger):

View file

@ -155,7 +155,7 @@ class LbryWalletManager(BaseWalletManager):
return receiving_addresses, change_addresses
@classmethod
async def from_lbrynet_config(cls, settings, db):
async def from_lbrynet_config(cls, settings):
ledger_id = {
'lbrycrd_main': 'lbc_mainnet',
@ -233,24 +233,6 @@ class LbryWalletManager(BaseWalletManager):
await account.ledger.broadcast(tx)
return tx
async def send_claim_to_address(self, claim_id: str, destination_address: str, amount: Optional[int],
account=None):
account = account or self.default_account
claims = await account.get_claims(
claim_name_type__any={'is_claim': 1, 'is_update': 1}, # exclude is_supports
claim_id=claim_id
)
if not claims:
raise NameError(f"Claim not found: {claim_id}")
if not amount:
amount = claims[0].get_estimator(self.ledger).effective_amount
tx = await Transaction.update(
claims[0], claims[0].claim, amount,
destination_address.encode(), [account], account
)
await self.ledger.broadcast(tx)
return tx
def send_points_to_address(self, reserved: ReservedPoints, amount: int, account=None):
destination_address: bytes = reserved.identifier.encode('latin1')
return self.send_amount_to_address(amount, destination_address, account)
@ -392,44 +374,6 @@ class LbryWalletManager(BaseWalletManager):
def get_utxos(account: BaseAccount):
return account.get_utxos()
async def claim_name(self, account, name, amount, claim: Claim, certificate=None, claim_address=None):
claim_address = claim_address or await account.receiving.get_or_create_usable_address()
existing_claims = await account.get_claims(
claim_name_type__any={'is_claim': 1, 'is_update': 1}, # exclude is_supports
claim_name=name
)
inputs = []
if len(existing_claims) == 0:
claim_output = Output.pay_claim_name_pubkey_hash(
amount, name, claim, account.ledger.address_to_hash160(claim_address)
)
elif len(existing_claims) == 1:
previous_claim = existing_claims[0]
claim_output = Output.pay_update_claim_pubkey_hash(
amount, previous_claim.claim_name, previous_claim.claim_id,
claim, account.ledger.address_to_hash160(claim_address)
)
inputs = [Input.spend(previous_claim)]
else:
raise NameError(f"More than one other claim exists with the name '{name}'.")
if certificate:
claim_output.sign(certificate, first_input_id=b'placeholder')
tx = await Transaction.create(inputs, [claim_output], [account], account)
if certificate:
claim_output.sign(certificate)
await tx.sign([account])
await account.ledger.broadcast(tx)
await self.old_db.save_claims([self._old_get_temp_claim_info(
tx, tx.outputs[0], claim_address, claim, name, dewies_to_lbc(amount)
)])
# TODO: release reserved tx outputs in case anything fails by this point
return tx
async def support_claim(self, claim_name, claim_id, amount, account):
holding_address = await account.receiving.get_or_create_usable_address()
tx = await Transaction.support(claim_name, claim_id, amount, holding_address, [account], account)
@ -468,46 +412,6 @@ class LbryWalletManager(BaseWalletManager):
# TODO: release reserved tx outputs in case anything fails by this point
return tx
async def claim_new_channel(self, channel_name, amount, account):
address = await account.receiving.get_or_create_usable_address()
claim = Claim()
claim_output = Output.pay_claim_name_pubkey_hash(
amount, channel_name, claim, account.ledger.address_to_hash160(address)
)
key = claim_output.generate_channel_private_key()
claim_output.script.generate()
tx = await Transaction.create([], [claim_output], [account], account)
await account.ledger.broadcast(tx)
account.add_certificate_private_key(tx.outputs[0].ref, key.decode())
# TODO: release reserved tx outputs in case anything fails by this point
await self.old_db.save_claims([self._old_get_temp_claim_info(
tx, tx.outputs[0], address, claim, channel_name, dewies_to_lbc(amount)
)])
return tx
def _old_get_temp_claim_info(self, tx, txo, address, claim_dict, name, bid):
return {
"claim_id": txo.claim_id,
"name": name,
"amount": bid,
"address": address,
"txid": tx.id,
"nout": txo.position,
"value": claim_dict,
"height": -1,
"claim_sequence": -1,
}
def get_certificates(self, private_key_accounts, exclude_without_key=True, **constraints):
return self.db.get_certificates(
private_key_accounts=private_key_accounts,
exclude_without_key=exclude_without_key,
**constraints
)
def update_peer_address(self, peer, address):
pass # TODO: Data payments is disabled

View file

@ -11,7 +11,7 @@ from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
from ecdsa.util import sigencode_der
from torba.client.basetransaction import BaseTransaction, BaseInput, BaseOutput
from torba.client.basetransaction import BaseTransaction, BaseInput, BaseOutput, ReadOnlyList
from torba.client.hash import hash160, sha256, Base58
from lbrynet.schema.claim import Claim
from lbrynet.wallet.account import Account
@ -117,6 +117,7 @@ class Output(BaseOutput):
return True
def sign(self, channel: 'Output', first_input_id=None):
self.channel = channel
self.claim.signing_channel_hash = channel.claim_hash
digest = sha256(b''.join([
first_input_id or self.tx_ref.tx.inputs[0].txo_ref.id.encode(),
@ -129,8 +130,9 @@ class Output(BaseOutput):
def generate_channel_private_key(self):
private_key = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1, hashfunc=hashlib.sha256)
self.private_key = private_key.to_pem()
self.private_key = private_key.to_pem().decode()
self.claim.channel.public_key_bytes = private_key.get_verifying_key().to_der()
self.script.generate()
return self.private_key
def is_channel_private_key(self, private_key_pem):
@ -169,6 +171,9 @@ class Transaction(BaseTransaction):
input_class = Input
output_class = Output
outputs: ReadOnlyList[Output]
inputs: ReadOnlyList[Input]
@classmethod
def pay(cls, amount: int, address: bytes, funding_accounts: List[Account], change_account: Account):
ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account)
@ -176,42 +181,52 @@ class Transaction(BaseTransaction):
return cls.create([], [output], funding_accounts, change_account)
@classmethod
def claim(cls, name: str, claim: Claim, amount: int, holding_address: bytes,
funding_accounts: List[Account], change_account: Account):
def claim_create(
cls, name: str, claim: Claim, amount: int, holding_address: str,
funding_accounts: List[Account], change_account: Account, signing_channel: Output = None):
ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account)
claim_output = Output.pay_claim_name_pubkey_hash(
amount, name, claim, ledger.address_to_hash160(holding_address)
)
return cls.create([], [claim_output], funding_accounts, change_account)
if signing_channel is not None:
claim_output.sign(signing_channel, b'placeholder txid:nout')
return cls.create([], [claim_output], funding_accounts, change_account, sign=False)
@classmethod
def claim_update(
cls, previous_claim: Output, amount: int, holding_address: str,
funding_accounts: List[Account], change_account: Account, signing_channel: Output = None):
ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account)
updated_claim = Output.pay_update_claim_pubkey_hash(
amount, previous_claim.claim_name, previous_claim.claim_id,
previous_claim.claim, ledger.address_to_hash160(holding_address)
)
if signing_channel is not None:
updated_claim.sign(signing_channel, b'placeholder txid:nout')
return cls.create(
[Input.spend(previous_claim)], [updated_claim], funding_accounts, change_account, sign=False
)
@classmethod
def support(cls, claim_name: str, claim_id: str, amount: int, holding_address: str,
funding_accounts: List[Account], change_account: Account, signing_channel: Output = None):
ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account)
support_output = Output.pay_support_pubkey_hash(
amount, claim_name, claim_id, ledger.address_to_hash160(holding_address)
)
if signing_channel is not None:
support_output.sign(signing_channel, b'placeholder txid:nout')
return cls.create([], [support_output], funding_accounts, change_account, sign=False)
@classmethod
def purchase(cls, claim: Output, amount: int, merchant_address: bytes,
funding_accounts: List[Account], change_account: Account):
funding_accounts: List[Account], change_account: Account):
ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account)
claim_output = Output.purchase_claim_pubkey_hash(
amount, claim.claim_id, ledger.address_to_hash160(merchant_address)
)
return cls.create([], [claim_output], funding_accounts, change_account)
@classmethod
def update(cls, previous_claim: Output, claim: Claim, amount: int, holding_address: bytes,
funding_accounts: List[Account], change_account: Account):
ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account)
updated_claim = Output.pay_update_claim_pubkey_hash(
amount, previous_claim.claim_name, previous_claim.claim_id,
claim, ledger.address_to_hash160(holding_address)
)
return cls.create([Input.spend(previous_claim)], [updated_claim], funding_accounts, change_account)
@classmethod
def support(cls, claim_name: str, claim_id: str, amount: int, holding_address: bytes,
funding_accounts: List[Account], change_account: Account):
ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account)
output = Output.pay_support_pubkey_hash(
amount, claim_name, claim_id, ledger.address_to_hash160(holding_address)
)
return cls.create([], [output], funding_accounts, change_account)
@classmethod
def abandon(cls, claims: Iterable[Output], funding_accounts: Iterable[Account], change_account: Account):
return cls.create([Input.spend(txo) for txo in claims], [], funding_accounts, change_account)

View file

@ -38,5 +38,6 @@ setup(
'torba',
'pyyaml==3.13',
'docopt==0.6.2',
'hachoir',
],
)

View file

@ -19,9 +19,8 @@ class EpicAdventuresOfChris45(CommandTestCase):
# While making the spamdwich he wonders... has anyone on LBRY
# registered the @spam channel yet? "I should do that!" he
# exclaims and goes back to his computer to do just that!
channel = await self.out(self.daemon.jsonrpc_channel_new('@spam', "1.0"))
self.assertTrue(channel['success'])
await self.confirm_tx(channel['tx']['txid'])
tx = await self.create_channel('@spam', '1.0')
channel_id = tx['outputs'][0]['claim_id']
# Do we have it locally?
channels = await self.out(self.daemon.jsonrpc_channel_list())
@ -52,17 +51,12 @@ class EpicAdventuresOfChris45(CommandTestCase):
# And so, many hours later, Chris is finished writing his epic story
# about eels driving a hovercraft across the wetlands while eating spam
# and decides it's time to publish it to the @spam channel.
with tempfile.NamedTemporaryFile() as file:
file.write(b'blah blah blah...')
file.write(b'[insert long story about eels driving hovercraft]')
file.write(b'yada yada yada!')
file.write(b'the end')
file.flush()
claim1 = await self.out(self.daemon.jsonrpc_publish(
'hovercraft', '1.0', file_path=file.name, channel_id=channel['claim_id']
))
self.assertTrue(claim1['success'])
await self.confirm_tx(claim1['tx']['txid'])
tx = await self.create_claim(
'hovercraft', '1.0',
data=b'[insert long story about eels driving hovercraft]',
channel_id=channel_id
)
claim_id = tx['outputs'][0]['claim_id']
# He quickly checks the unconfirmed balance to make sure everything looks
# correct.
@ -84,21 +78,11 @@ class EpicAdventuresOfChris45(CommandTestCase):
# As people start reading his story they discover some typos and notify
# Chris who explains in despair "Oh! Noooooos!" but then remembers
# "No big deal! I can update my claim." And so he updates his claim.
with tempfile.NamedTemporaryFile() as file:
file.write(b'blah blah blah...')
file.write(b'[typo fixing sounds being made]')
file.write(b'yada yada yada!')
file.flush()
claim2 = await self.out(self.daemon.jsonrpc_publish(
'hovercraft', '1.0', file_path=file.name, channel_name='@spam'
))
self.assertTrue(claim2['success'])
self.assertEqual(claim2['claim_id'], claim1['claim_id'])
await self.confirm_tx(claim2['tx']['txid'])
await self.update_claim(claim_id, data=b'[typo fixing sounds being made]')
# After some soul searching Chris decides that his story needs more
# heart and a better ending. He takes down the story and begins the rewrite.
abandon = await self.out(self.daemon.jsonrpc_claim_abandon(claim1['claim_id'], blocking=False))
abandon = await self.out(self.daemon.jsonrpc_claim_abandon(claim_id, blocking=False))
self.assertTrue(abandon['success'])
await self.confirm_tx(abandon['tx']['txid'])
@ -134,17 +118,10 @@ class EpicAdventuresOfChris45(CommandTestCase):
# After Chris is done with all the "helping other people" stuff he decides that it's time to
# write a new story and publish it to lbry. All he needed was a fresh start and he came up with:
with tempfile.NamedTemporaryFile() as file:
file.write(b'Amazingly Original First Line')
file.write(b'Super plot for the grand novel')
file.write(b'Totally un-cliched ending')
file.write(b'**Audience Gasps**')
file.flush()
claim3 = await self.out(self.daemon.jsonrpc_publish(
'fresh-start', '1.0', file_path=file.name, channel_name='@spam'
))
self.assertTrue(claim3['success'])
await self.confirm_tx(claim3['tx']['txid'])
tx = await self.create_claim(
'fresh-start', '1.0', data=b'Amazingly Original First Line', channel_id=channel_id
)
claim_id2 = tx['outputs'][0]['claim_id']
await self.generate(5)
@ -154,7 +131,7 @@ class EpicAdventuresOfChris45(CommandTestCase):
# And voila, and bravo and encore! His Best Friend Ramsey read the story and immediately knew this was a hit
# Now to keep this claim winning on the lbry blockchain he immediately supports the claim
tx = await self.out(self.daemon.jsonrpc_claim_new_support(
'fresh-start', claim3['claim_id'], '0.2', account_id=ramsey_account_id
'fresh-start', claim_id2, '0.2', account_id=ramsey_account_id
))
await self.confirm_tx(tx['txid'])
@ -170,7 +147,7 @@ class EpicAdventuresOfChris45(CommandTestCase):
# Now he also wanted to support the original creator of the Award Winning Novel
# So he quickly decides to send a tip to him
tx = await self.out(
self.daemon.jsonrpc_claim_tip(claim3['claim_id'], '0.3', account_id=ramsey_account_id))
self.daemon.jsonrpc_claim_tip(claim_id2, '0.3', account_id=ramsey_account_id))
await self.confirm_tx(tx['txid'])
# And again checks if it went to the just right place
@ -181,7 +158,7 @@ class EpicAdventuresOfChris45(CommandTestCase):
await self.generate(5)
# Seeing the ravishing success of his novel Chris adds support to his claim too
tx = await self.out(self.daemon.jsonrpc_claim_new_support('fresh-start', claim3['claim_id'], '0.4'))
tx = await self.out(self.daemon.jsonrpc_claim_new_support('fresh-start', claim_id2, '0.4'))
await self.confirm_tx(tx['txid'])
# And check if his support showed up
@ -197,16 +174,9 @@ class EpicAdventuresOfChris45(CommandTestCase):
# his song, seeing as his novel had smashed all the records, he was the perfect candidate!
# .......
# Chris agrees.. 17 hours 43 minutes and 14 seconds later, he makes his publish
with tempfile.NamedTemporaryFile() as file:
file.write(b'The Whale amd The Bookmark')
file.write(b'I know right? Totally a hit song')
file.write(b'That\'s what goes around for songs these days anyways')
file.flush()
claim4 = await self.out(self.daemon.jsonrpc_publish(
'hit-song', '1.0', file_path=file.name, channel_id=channel['claim_id']
))
self.assertTrue(claim4['success'])
await self.confirm_tx(claim4['tx']['txid'])
tx = await self.out(self.daemon.jsonrpc_publish(
'hit-song', '1.0', data=b'The Whale and The Bookmark', channel_id=channel_id
))
await self.generate(5)
@ -215,7 +185,7 @@ class EpicAdventuresOfChris45(CommandTestCase):
# But sadly Ramsey wasn't so pleased. It was hard for him to tell Chris...
# Chris, though a bit heartbroken, abandoned the claim for now, but instantly started working on new hit lyrics
abandon = await self.out(self.daemon.jsonrpc_claim_abandon(txid=claim4['tx']['txid'], nout=0, blocking=False))
abandon = await self.out(self.daemon.jsonrpc_claim_abandon(txid=tx['txid'], nout=0, blocking=False))
self.assertTrue(abandon['success'])
await self.confirm_tx(abandon['tx']['txid'])

View file

@ -1,79 +1,224 @@
import hashlib
import tempfile
from binascii import unhexlify
import ecdsa
from lbrynet.wallet.transaction import Transaction, Output
from lbrynet.error import InsufficientFundsError
from lbrynet.schema.claim import Claim
from torba.client.errors import InsufficientFundsError
from lbrynet.schema.compat import OldClaimMessage
from integration.testcase import CommandTestCase
from torba.client.hash import sha256, Base58
class ChannelCommands(CommandTestCase):
async def test_create_channel_names(self):
# claim new name
await self.create_channel('@foo')
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 1)
await self.assertBalance(self.account, '8.991893')
# fail to claim duplicate
with self.assertRaisesRegex(Exception, "You already have a channel under the name '@foo'."):
await self.create_channel('@foo')
# fail to claim invalid name
with self.assertRaisesRegex(Exception, "Cannot make a new channel for a non channel name"):
await self.create_channel('foo')
# nothing's changed after failed attempts
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 1)
await self.assertBalance(self.account, '8.991893')
# succeed overriding duplicate restriction
await self.create_channel('@foo', allow_duplicate_name=True)
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 2)
await self.assertBalance(self.account, '7.983786')
async def test_channel_bids(self):
# enough funds
tx = await self.create_channel('@foo', '5.0')
claim_id = tx['outputs'][0]['claim_id']
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 1)
await self.assertBalance(self.account, '4.991893')
# bid preserved on update
tx = await self.update_channel(claim_id)
self.assertEqual(tx['outputs'][0]['amount'], '5.0')
# bid changed on update
tx = await self.update_channel(claim_id, bid='4.0')
self.assertEqual(tx['outputs'][0]['amount'], '4.0')
await self.assertBalance(self.account, '5.991447')
# not enough funds
with self.assertRaisesRegex(
InsufficientFundsError, "Not enough funds to cover this transaction."):
await self.create_channel('@foo2', '9.0')
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 1)
await self.assertBalance(self.account, '5.991447')
# spend exactly amount available, no change
tx = await self.create_channel('@foo3', '5.981266')
await self.assertBalance(self.account, '0.0')
self.assertEqual(len(tx['outputs']), 1) # no change
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 2)
async def test_setting_channel_fields(self):
values = {
'title': "Cool Channel",
'description': "Best channel on LBRY.",
'contact_email': "human@email.com",
'tags': ["cool", "awesome"],
'cover_url': "https://co.ol/cover.png",
'homepage_url': "https://co.ol",
'thumbnail_url': "https://co.ol/thumbnail.png",
'language': "en"
}
# create new channel with all fields set
tx = await self.out(self.create_channel('@bigchannel', **values))
txo = tx['outputs'][0]
self.assertEqual(
txo['value']['channel'],
{'public_key': txo['value']['channel']['public_key'], **values}
)
# create channel with nothing set
tx = await self.out(self.create_channel('@lightchannel'))
txo = tx['outputs'][0]
self.assertEqual(
txo['value']['channel'],
{'public_key': txo['value']['channel']['public_key']}
)
# create channel with just some tags
tx = await self.out(self.create_channel('@updatedchannel', tags='blah'))
txo = tx['outputs'][0]
claim_id = txo['claim_id']
public_key = txo['value']['channel']['public_key']
self.assertEqual(
txo['value']['channel'],
{'public_key': public_key, 'tags': ['blah']}
)
# update channel setting all fields
tx = await self.out(self.update_channel(claim_id, **values))
txo = tx['outputs'][0]
values['public_key'] = public_key
values['tags'].insert(0, 'blah') # existing tag
self.assertEqual(
txo['value']['channel'],
values
)
# clearing and settings tags
tx = await self.out(self.update_channel(claim_id, tags='single', clear_tags=True))
txo = tx['outputs'][0]
values['tags'] = ['single']
self.assertEqual(
txo['value']['channel'],
values
)
# reset signing key
tx = await self.out(self.update_channel(claim_id, new_signing_key=True))
txo = tx['outputs'][0]
self.assertNotEqual(
txo['value']['channel']['public_key'],
values['public_key']
)
# send channel to someone else
new_account = await self.daemon.jsonrpc_account_create('second account')
account2_id, account2 = new_account['id'], self.daemon.get_account_or_error(new_account['id'])
# before sending
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 3)
self.assertEqual(len(await self.daemon.jsonrpc_channel_list(account_id=account2_id)), 0)
other_address = await account2.receiving.get_or_create_usable_address()
tx = await self.out(self.update_channel(claim_id, claim_address=other_address))
# after sending
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 2)
self.assertEqual(len(await self.daemon.jsonrpc_channel_list(account_id=account2_id)), 1)
# shoud not have private key
txo = (await account2.get_channels())[0]
self.assertIsNone(txo.private_key)
# send the private key too
txoid = f"{tx['outputs'][0]['txid']}:{tx['outputs'][0]['nout']}"
account2.channel_keys[txoid] = self.account.channel_keys[txoid]
# now should have private key
txo = (await account2.get_channels())[0]
self.assertIsNotNone(txo.private_key)
class ClaimCommands(CommandTestCase):
async def test_create_update_and_abandon_claim(self):
await self.assertBalance(self.account, '10.0')
async def test_create_claim_names(self):
# claim new name
await self.create_claim('foo')
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 1)
await self.assertBalance(self.account, '8.993893')
claim = await self.make_claim(amount='2.5') # creates new claim
txs = await self.out(self.daemon.jsonrpc_transaction_list())
self.assertEqual(len(txs[0]['claim_info']), 1)
self.assertEqual(txs[0]['confirmations'], 1)
self.assertEqual(txs[0]['claim_info'][0]['balance_delta'], '-2.5')
self.assertEqual(txs[0]['claim_info'][0]['claim_id'], claim['claim_id'])
self.assertEqual(txs[0]['value'], '0.0')
self.assertEqual(txs[0]['fee'], '-0.020107')
await self.assertBalance(self.account, '7.479893')
# fail to claim duplicate
with self.assertRaisesRegex(Exception, "You already have a claim published under the name 'foo'."):
await self.create_claim('foo')
await self.make_claim(amount='1.0') # updates previous claim
txs = await self.out(self.daemon.jsonrpc_transaction_list())
self.assertEqual(len(txs[0]['update_info']), 1)
self.assertEqual(txs[0]['update_info'][0]['balance_delta'], '1.5')
self.assertEqual(txs[0]['update_info'][0]['claim_id'], claim['claim_id'])
self.assertEqual(txs[0]['value'], '0.0')
self.assertEqual(txs[0]['fee'], '-0.000182')
await self.assertBalance(self.account, '8.979711')
# fail claim starting with @
with self.assertRaisesRegex(Exception, "Claim names cannot start with @ symbol."):
await self.create_claim('@foo')
await self.out(self.daemon.jsonrpc_claim_abandon(claim['claim_id']))
txs = await self.out(self.daemon.jsonrpc_transaction_list())
self.assertEqual(len(txs[0]['abandon_info']), 1)
self.assertEqual(txs[0]['abandon_info'][0]['balance_delta'], '1.0')
self.assertEqual(txs[0]['abandon_info'][0]['claim_id'], claim['claim_id'])
self.assertEqual(txs[0]['value'], '0.0')
self.assertEqual(txs[0]['fee'], '-0.000107')
await self.assertBalance(self.account, '9.979604')
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 1)
await self.assertBalance(self.account, '8.993893')
async def test_update_claim_holding_address(self):
other_account_id = (await self.daemon.jsonrpc_account_create('second account'))['id']
other_account = self.daemon.get_account_or_error(other_account_id)
other_address = await other_account.receiving.get_or_create_usable_address()
# succeed overriding duplicate restriction
await self.create_claim('foo', allow_duplicate_name=True)
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 2)
await self.assertBalance(self.account, '7.987786')
await self.assertBalance(self.account, '10.0')
async def test_bids(self):
# enough funds
tx = await self.create_claim('foo', '2.0')
claim_id = tx['outputs'][0]['claim_id']
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 1)
await self.assertBalance(self.account, '7.993893')
# create the initial name claim
claim = await self.make_claim()
# bid preserved on update
tx = await self.update_claim(claim_id)
self.assertEqual(tx['outputs'][0]['amount'], '2.0')
self.assertEqual(len(await self.daemon.jsonrpc_claim_list_mine()), 1)
self.assertEqual(len(await self.daemon.jsonrpc_claim_list_mine(account_id=other_account_id)), 0)
tx = await self.daemon.jsonrpc_claim_send_to_address(
claim['claim_id'], other_address
)
await self.ledger.wait(tx)
self.assertEqual(len(await self.daemon.jsonrpc_claim_list_mine()), 0)
self.assertEqual(len(await self.daemon.jsonrpc_claim_list_mine(account_id=other_account_id)), 1)
# bid changed on update
tx = await self.update_claim(claim_id, bid='3.0')
self.assertEqual(tx['outputs'][0]['amount'], '3.0')
async def test_publishing_checks_all_accounts_for_certificate(self):
await self.assertBalance(self.account, '6.993384')
# not enough funds
with self.assertRaisesRegex(
InsufficientFundsError, "Not enough funds to cover this transaction."):
await self.create_claim('foo2', '9.0')
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 1)
await self.assertBalance(self.account, '6.993384')
# spend exactly amount available, no change
tx = await self.create_claim('foo3', '6.98527700')
await self.assertBalance(self.account, '0.0')
self.assertEqual(len(tx['outputs']), 1) # no change
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 2)
async def test_publishing_checks_all_accounts_for_channel(self):
account1_id, account1 = self.account.id, self.account
new_account = await self.daemon.jsonrpc_account_create('second account')
account2_id, account2 = new_account['id'], self.daemon.get_account_or_error(new_account['id'])
spam_channel = await self.out(self.daemon.jsonrpc_channel_new('@spam', '1.0'))
self.assertTrue(spam_channel['success'])
await self.confirm_tx(spam_channel['tx']['txid'])
await self.out(self.create_channel('@spam', '1.0'))
self.assertEqual('8.989893', await self.daemon.jsonrpc_account_balance())
result = await self.out(self.daemon.jsonrpc_wallet_send(
@ -84,9 +229,8 @@ class ClaimCommands(CommandTestCase):
self.assertEqual('3.989769', await self.daemon.jsonrpc_account_balance())
self.assertEqual('5.0', await self.daemon.jsonrpc_account_balance(account2_id))
baz_channel = await self.out(self.daemon.jsonrpc_channel_new('@baz', '1.0', account2_id))
self.assertTrue(baz_channel['success'])
await self.confirm_tx(baz_channel['tx']['txid'])
baz_tx = await self.out(self.create_channel('@baz', '1.0', account_id=account2_id))
baz_id = baz_tx['outputs'][0]['claim_id']
channels = await self.out(self.daemon.jsonrpc_channel_list(account1_id))
self.assertEqual(len(channels), 1)
@ -98,84 +242,158 @@ class ClaimCommands(CommandTestCase):
self.assertEqual(channels[0]['name'], '@baz')
# defaults to using all accounts to lookup channel
with tempfile.NamedTemporaryFile() as file:
file.write(b'hi!')
file.flush()
claim1 = await self.out(self.daemon.jsonrpc_publish(
'hovercraft', '1.0', file_path=file.name, channel_name='@baz'
))
self.assertTrue(claim1['success'])
await self.confirm_tx(claim1['tx']['txid'])
await self.create_claim('hovercraft1', channel_id=baz_id)
# uses only the specific accounts which contains the channel
with tempfile.NamedTemporaryFile() as file:
file.write(b'hi!')
file.flush()
claim1 = await self.out(self.daemon.jsonrpc_publish(
'hovercraft', '1.0', file_path=file.name,
channel_name='@baz', channel_account_id=[account2_id]
))
self.assertTrue(claim1['success'])
await self.confirm_tx(claim1['tx']['txid'])
await self.create_claim('hovercraft2', channel_id=baz_id, channel_account_id=[account2_id])
# fails when specifying account which does not contain channel
with tempfile.NamedTemporaryFile() as file:
file.write(b'hi!')
file.flush()
with self.assertRaisesRegex(ValueError, "Couldn't find channel with name '@baz'."):
await self.out(self.daemon.jsonrpc_publish(
'hovercraft', '1.0', file_path=file.name,
channel_name='@baz', channel_account_id=[account1_id]
))
with self.assertRaisesRegex(ValueError, "Couldn't find channel with channel_id"):
await self.create_claim(
'hovercraft3', channel_id=baz_id, channel_account_id=[account1_id]
)
async def test_updating_claim_includes_claim_value_in_balance_check(self):
async def test_setting_claim_fields(self):
values = {
'title': "Cool Channel",
'description': "Best channel on LBRY.",
'contact_email': "human@email.com",
'tags': ["cool", "awesome"],
'cover_url': "https://co.ol/cover.png",
'homepage_url': "https://co.ol",
'thumbnail_url': "https://co.ol/thumbnail.png",
'language': "en"
}
# create new channel with all fields set
tx = await self.out(self.create_channel('@bigchannel', **values))
txo = tx['outputs'][0]
self.assertEqual(
txo['value']['channel'],
{'public_key': txo['value']['channel']['public_key'], **values}
)
# create channel with nothing set
tx = await self.out(self.create_channel('@lightchannel'))
txo = tx['outputs'][0]
self.assertEqual(
txo['value']['channel'],
{'public_key': txo['value']['channel']['public_key']}
)
# create channel with just some tags
tx = await self.out(self.create_channel('@updatedchannel', tags='blah'))
txo = tx['outputs'][0]
claim_id = txo['claim_id']
public_key = txo['value']['channel']['public_key']
self.assertEqual(
txo['value']['channel'],
{'public_key': public_key, 'tags': ['blah']}
)
# update channel setting all fields
tx = await self.out(self.update_channel(claim_id, **values))
txo = tx['outputs'][0]
values['public_key'] = public_key
values['tags'].insert(0, 'blah') # existing tag
self.assertEqual(
txo['value']['channel'],
values
)
# clearing and settings tags
tx = await self.out(self.update_channel(claim_id, tags='single', clear_tags=True))
txo = tx['outputs'][0]
values['tags'] = ['single']
self.assertEqual(
txo['value']['channel'],
values
)
# reset signing key
tx = await self.out(self.update_channel(claim_id, new_signing_key=True))
txo = tx['outputs'][0]
self.assertNotEqual(
txo['value']['channel']['public_key'],
values['public_key']
)
# send channel to someone else
new_account = await self.daemon.jsonrpc_account_create('second account')
account2_id, account2 = new_account['id'], self.daemon.get_account_or_error(new_account['id'])
# before sending
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 3)
self.assertEqual(len(await self.daemon.jsonrpc_channel_list(account_id=account2_id)), 0)
other_address = await account2.receiving.get_or_create_usable_address()
tx = await self.out(self.update_channel(claim_id, claim_address=other_address))
# after sending
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 2)
self.assertEqual(len(await self.daemon.jsonrpc_channel_list(account_id=account2_id)), 1)
# shoud not have private key
txo = (await account2.get_channels())[0]
self.assertIsNone(txo.private_key)
# send the private key too
txoid = f"{tx['outputs'][0]['txid']}:{tx['outputs'][0]['nout']}"
account2.channel_keys[txoid] = self.account.channel_keys[txoid]
# now should have private key
txo = (await account2.get_channels())[0]
self.assertIsNotNone(txo.private_key)
async def test_create_update_and_abandon_claim(self):
await self.assertBalance(self.account, '10.0')
await self.make_claim(amount='9.0')
await self.assertBalance(self.account, '0.979893')
tx = await self.create_claim(bid='2.5') # creates new claim
claim_id = tx['outputs'][0]['claim_id']
txs = await self.out(self.daemon.jsonrpc_transaction_list())
self.assertEqual(len(txs[0]['claim_info']), 1)
self.assertEqual(txs[0]['confirmations'], 1)
self.assertEqual(txs[0]['claim_info'][0]['balance_delta'], '-2.5')
self.assertEqual(txs[0]['claim_info'][0]['claim_id'], claim_id)
self.assertEqual(txs[0]['value'], '0.0')
self.assertEqual(txs[0]['fee'], '-0.020107')
await self.assertBalance(self.account, '7.479893')
# update the same claim
await self.make_claim(amount='9.0')
await self.assertBalance(self.account, '0.979637')
await self.update_claim(claim_id, bid='1.0') # updates previous claim
txs = await self.out(self.daemon.jsonrpc_transaction_list())
self.assertEqual(len(txs[0]['update_info']), 1)
self.assertEqual(txs[0]['update_info'][0]['balance_delta'], '1.5')
self.assertEqual(txs[0]['update_info'][0]['claim_id'], claim_id)
self.assertEqual(txs[0]['value'], '0.0')
self.assertEqual(txs[0]['fee'], '-0.000184')
await self.assertBalance(self.account, '8.979709')
# update the claim a second time but use even more funds
await self.make_claim(amount='9.97')
await self.assertBalance(self.account, '0.009381')
# fails when specifying more than available
with tempfile.NamedTemporaryFile() as file:
file.write(b'hi!')
file.flush()
with self.assertRaisesRegex(
InsufficientFundsError,
"Please lower the bid value, the maximum amount"
" you can specify for this claim is 9.979307."
):
await self.out(self.daemon.jsonrpc_publish(
'hovercraft', '9.98', file_path=file.name
))
await self.out(self.daemon.jsonrpc_claim_abandon(claim_id))
txs = await self.out(self.daemon.jsonrpc_transaction_list())
self.assertEqual(len(txs[0]['abandon_info']), 1)
self.assertEqual(txs[0]['abandon_info'][0]['balance_delta'], '1.0')
self.assertEqual(txs[0]['abandon_info'][0]['claim_id'], claim_id)
self.assertEqual(txs[0]['value'], '0.0')
self.assertEqual(txs[0]['fee'], '-0.000107')
await self.assertBalance(self.account, '9.979602')
async def test_abandoning_claim_at_loss(self):
await self.assertBalance(self.account, '10.0')
claim = await self.make_claim(amount='0.0001')
tx = await self.create_claim(bid='0.0001')
await self.assertBalance(self.account, '9.979793')
await self.out(self.daemon.jsonrpc_claim_abandon(claim['claim_id']))
await self.out(self.daemon.jsonrpc_claim_abandon(tx['outputs'][0]['claim_id']))
await self.assertBalance(self.account, '9.97968399')
async def test_claim_show(self):
channel = await self.out(self.daemon.jsonrpc_channel_new('@abc', "1.0"))
self.assertTrue(channel['success'])
await self.confirm_tx(channel['tx']['txid'])
channel = await self.create_channel('@abc', '1.0')
channel_from_claim_show = await self.out(
self.daemon.jsonrpc_claim_show(txid=channel['tx']['txid'], nout=channel['output']['nout'])
self.daemon.jsonrpc_claim_show(txid=channel['txid'], nout=0)
)
self.assertEqual(channel_from_claim_show['value'], channel['output']['value'])
self.assertEqual(channel_from_claim_show['value'], channel['outputs'][0]['value'])
channel_from_claim_show = await self.out(
self.daemon.jsonrpc_claim_show(claim_id=channel['claim_id'])
self.daemon.jsonrpc_claim_show(claim_id=channel['outputs'][0]['claim_id'])
)
self.assertEqual(channel_from_claim_show['value'], channel['output']['value'])
self.assertEqual(channel_from_claim_show['value'], channel['outputs'][0]['value'])
abandon = await self.out(self.daemon.jsonrpc_claim_abandon(txid=channel['tx']['txid'], nout=0, blocking=False))
abandon = await self.out(self.daemon.jsonrpc_claim_abandon(txid=channel['txid'], nout=0, blocking=False))
self.assertTrue(abandon['success'])
await self.confirm_tx(abandon['tx']['txid'])
not_a_claim = await self.out(
@ -184,13 +402,10 @@ class ClaimCommands(CommandTestCase):
self.assertEqual(not_a_claim, 'claim not found')
async def test_claim_list(self):
channel = await self.out(self.daemon.jsonrpc_channel_new('@abc', "1.0"))
self.assertTrue(channel['success'])
await self.confirm_tx(channel['tx']['txid'])
claim = await self.make_claim(amount='0.0001', name='on-channel-claim', channel_name='@abc')
self.assertTrue(claim['success'])
unsigned_claim = await self.make_claim(amount='0.0001', name='unsigned')
self.assertTrue(claim['success'])
channel = await self.create_channel('@abc', '1.0')
channel_id = channel['outputs'][0]['claim_id']
claim = await self.create_claim('on-channel-claim', '0.0001', channel_id=channel_id)
unsigned_claim = await self.create_claim('unsigned', '0.0001')
channel_from_claim_list = await self.out(self.daemon.jsonrpc_claim_list('@abc'))
self.assertEqual(channel_from_claim_list['claims'][0]['value'], channel['output']['value'])
@ -373,8 +588,8 @@ class ClaimCommands(CommandTestCase):
# this test assumes that the lbrycrd forks normalization at height == 250 on regtest
c1 = await self.make_claim('ΣίσυφοςfiÆ', '0.1')
c2 = await self.make_claim('ΣΊΣΥΦΟσFIæ', '0.2')
c1 = await self.create_claim('ΣίσυφοςfiÆ', '0.1')
c2 = await self.create_claim('ΣΊΣΥΦΟσFIæ', '0.2')
r1 = await self.daemon.jsonrpc_resolve(urls='lbry://ΣίσυφοςfiÆ')
r2 = await self.daemon.jsonrpc_resolve(urls='lbry://ΣΊΣΥΦΟσFIæ')

View file

@ -4,7 +4,6 @@ import os
from integration.testcase import CommandTestCase
from lbrynet.blob_exchange.downloader import BlobDownloader
from lbrynet.error import InsufficientFundsError
class FileCommands(CommandTestCase):
@ -12,8 +11,8 @@ class FileCommands(CommandTestCase):
VERBOSITY = logging.WARN
async def test_file_management(self):
await self.make_claim('foo', '0.01')
await self.make_claim('foo2', '0.01')
await self.create_claim('foo', '0.01')
await self.create_claim('foo2', '0.01')
file1, file2 = self.daemon.jsonrpc_file_list('claim_name')
self.assertEqual(file1['claim_name'], 'foo')
@ -28,8 +27,8 @@ class FileCommands(CommandTestCase):
self.assertEqual(len(self.daemon.jsonrpc_file_list()), 1)
async def test_download_different_timeouts(self):
claim = await self.make_claim('foo', '0.01')
sd_hash = claim['output']['value']['stream']['hash']
tx = await self.create_claim('foo', '0.01')
sd_hash = tx['outputs'][0]['value']['stream']['hash']
await self.daemon.jsonrpc_file_delete(claim_name='foo')
all_except_sd = [
blob_hash for blob_hash in self.server.blob_manager.completed_blob_hashes if blob_hash != sd_hash
@ -49,7 +48,7 @@ class FileCommands(CommandTestCase):
await asyncio.sleep(0.01)
async def test_filename_conflicts_management_on_resume_download(self):
await self.make_claim('foo', '0.01', data=bytes([0]*(1<<23)))
await self.create_claim('foo', '0.01', data=bytes([0]*(1<<23)))
file_info = self.daemon.jsonrpc_file_list()[0]
original_path = os.path.join(self.daemon.conf.download_dir, file_info['file_name'])
await self.daemon.jsonrpc_file_delete(claim_name='foo')
@ -70,8 +69,8 @@ class FileCommands(CommandTestCase):
# this used to be inconsistent, if it becomes again it would create weird bugs, so worth checking
async def test_incomplete_downloads_erases_output_file_on_stop(self):
claim = await self.make_claim('foo', '0.01')
sd_hash = claim['output']['value']['stream']['hash']
tx = await self.create_claim('foo', '0.01')
sd_hash = tx['outputs'][0]['value']['stream']['hash']
file_info = self.daemon.jsonrpc_file_list()[0]
await self.daemon.jsonrpc_file_delete(claim_name='foo')
blobs = await self.server_storage.get_blobs_for_stream(
@ -89,8 +88,8 @@ class FileCommands(CommandTestCase):
self.assertFalse(os.path.isfile(os.path.join(self.daemon.conf.download_dir, file_info['file_name'])))
async def test_incomplete_downloads_retry(self):
claim = await self.make_claim('foo', '0.01')
sd_hash = claim['output']['value']['stream']['hash']
tx = await self.create_claim('foo', '0.01')
sd_hash = tx['outputs'][0]['value']['stream']['hash']
await self.daemon.jsonrpc_file_delete(claim_name='foo')
blobs = await self.server_storage.get_blobs_for_stream(
await self.server_storage.get_stream_hash_for_sd_hash(sd_hash)
@ -129,8 +128,8 @@ class FileCommands(CommandTestCase):
async def test_unban_recovers_stream(self):
BlobDownloader.BAN_TIME = .5 # fixme: temporary field, will move to connection manager or a conf
claim = await self.make_claim('foo', '0.01', data=bytes([0]*(1<<23)))
sd_hash = claim['output']['value']['stream']['hash']
tx = await self.create_claim('foo', '0.01', data=bytes([0]*(1<<23)))
sd_hash = tx['outputs'][0]['value']['stream']['hash']
missing_blob_hash = (await self.daemon.jsonrpc_blob_list(sd_hash=sd_hash))[-2]
await self.daemon.jsonrpc_file_delete(claim_name='foo')
# backup blob
@ -151,27 +150,30 @@ class FileCommands(CommandTestCase):
target_address = await self.blockchain.get_raw_change_address()
# FAIL: beyond available balance
await self.make_claim(
await self.create_claim(
'expensive', '0.01', data=b'pay me if you can',
fee={'currency': 'LBC', 'amount': 11.0, 'address': target_address})
fee_currency='LBC', fee_amount='11.0', fee_address=target_address
)
await self.daemon.jsonrpc_file_delete(claim_name='expensive')
response = await self.daemon.jsonrpc_get('lbry://expensive')
self.assertEqual(response['error'], 'fee of 11.00000 exceeds max available balance')
self.assertEqual(len(self.daemon.jsonrpc_file_list()), 0)
# FAIL: beyond maximum key fee
await self.make_claim(
await self.create_claim(
'maxkey', '0.01', data=b'no pay me, no',
fee={'currency': 'LBC', 'amount': 111.0, 'address': target_address})
fee_currency='LBC', fee_amount='111.0', fee_address=target_address
)
await self.daemon.jsonrpc_file_delete(claim_name='maxkey')
response = await self.daemon.jsonrpc_get('lbry://maxkey')
self.assertEqual(len(self.daemon.jsonrpc_file_list()), 0)
self.assertEqual(response['error'], 'fee of 111.00000 exceeds max configured to allow of 50.00000')
# PASS: purchase is successful
await self.make_claim(
await self.create_claim(
'icanpay', '0.01', data=b'I got the power!',
fee={'currency': 'LBC', 'amount': 1.0, 'address': target_address})
fee_currency='LBC', fee_amount='1.0', fee_address=target_address
)
await self.daemon.jsonrpc_file_delete(claim_name='icanpay')
await self.assertBalance(self.account, '9.925679')
response = await self.daemon.jsonrpc_get('lbry://icanpay')

View file

@ -4,7 +4,8 @@ from integration.testcase import CommandTestCase
class ResolveCommand(CommandTestCase):
async def test_resolve(self):
await self.make_channel('@abc', '0.01')
tx = await self.create_channel('@abc', '0.01')
channel_id = tx['outputs'][0]['claim_id']
# resolving a channel @abc
response = await self.resolve('lbry://@abc')
@ -14,8 +15,8 @@ class ResolveCommand(CommandTestCase):
self.assertEqual(response['lbry://@abc']['certificate']['name'], '@abc')
self.assertEqual(response['lbry://@abc']['claims_in_channel'], 0)
await self.make_claim('foo', '0.01', channel_name='@abc')
await self.make_claim('foo2', '0.01', channel_name='@abc')
await self.create_claim('foo', '0.01', channel_id=channel_id)
await self.create_claim('foo2', '0.01', channel_id=channel_id)
# resolving a channel @abc with some claims in it
response = await self.resolve('lbry://@abc')

View file

@ -62,7 +62,7 @@ class AccountSynchronization(AsyncioTestCase):
self.account.modified_on = 123.456
self.assertEqual(self.daemon.jsonrpc_sync_hash(), starting_hash)
self.assertEqual(self.daemon.jsonrpc_sync_apply('password')['hash'], starting_hash)
self.assertFalse(self.account.certificates)
self.assertFalse(self.account.channel_keys)
hash_w_cert = '974721f42dab42657b5911b7caf4af98ce4d3879eea6ac23d50c1d79bc5020ef'
add_cert = (
@ -78,9 +78,9 @@ class AccountSynchronization(AsyncioTestCase):
)
self.daemon.jsonrpc_sync_apply('password', data=add_cert)
self.assertEqual(self.daemon.jsonrpc_sync_hash(), hash_w_cert)
self.assertEqual(self.account.certificates, {'abcdefg1234:0': '---PRIVATE KEY---'})
self.assertEqual(self.account.channel_keys, {'abcdefg1234:0': '---PRIVATE KEY---'})
# applying the same diff is idempotent
self.daemon.jsonrpc_sync_apply('password', data=add_cert)
self.assertEqual(self.daemon.jsonrpc_sync_hash(), hash_w_cert)
self.assertEqual(self.account.certificates, {'abcdefg1234:0': '---PRIVATE KEY---'})
self.assertEqual(self.account.channel_keys, {'abcdefg1234:0': '---PRIVATE KEY---'})

View file

@ -155,29 +155,53 @@ class CommandTestCase(IntegrationTestCase):
to JSON and then back to a dictionary. """
return json.loads(jsonrpc_dumps_pretty(await awaitable, ledger=self.ledger))['result']
async def make_claim(self, name='hovercraft', amount='1.0', data=b'hi!',
channel_name=None, confirm=True, account_id=None, fee=None):
async def create_claim(self, name='hovercraft', bid='1.0', data=b'hi!', confirm=True, **kwargs):
with tempfile.NamedTemporaryFile() as file:
file.write(data)
file.flush()
claim = await self.out(self.daemon.jsonrpc_publish(
name, amount, file_path=file.name, channel_name=channel_name, account_id=account_id,
fee=fee
))
self.assertTrue(claim['success'])
claim = await self.out(
self.daemon.jsonrpc_publish(name, bid, file_path=file.name, **kwargs)
)
self.assertEqual(claim['outputs'][0]['name'], name)
if confirm:
await self.on_transaction_dict(claim['tx'])
await self.on_transaction_dict(claim)
await self.generate(1)
await self.on_transaction_dict(claim['tx'])
await self.on_transaction_dict(claim)
return claim
async def make_channel(self, name='@arena', amount='1.0', confirm=True, account_id=None):
channel = await self.out(self.daemon.jsonrpc_channel_new(name, amount, account_id))
self.assertTrue(channel['success'])
async def update_claim(self, claim_id, data=None, confirm=True, **kwargs):
if data:
with tempfile.NamedTemporaryFile() as file:
file.write(data)
file.flush()
claim = await self.out(
self.daemon.jsonrpc_claim_update(claim_id, file_path=file.name, **kwargs)
)
else:
claim = await self.out(self.daemon.jsonrpc_claim_update(claim_id, **kwargs))
self.assertIsNotNone(claim['outputs'][0]['name'])
if confirm:
await self.on_transaction_dict(channel['tx'])
await self.on_transaction_dict(claim)
await self.generate(1)
await self.on_transaction_dict(channel['tx'])
await self.on_transaction_dict(claim)
return claim
async def create_channel(self, name='@arena', bid='1.0', confirm=True, **kwargs):
channel = await self.out(self.daemon.jsonrpc_channel_create(name, bid, **kwargs))
self.assertEqual(channel['outputs'][0]['name'], name)
if confirm:
await self.on_transaction_dict(channel)
await self.generate(1)
await self.on_transaction_dict(channel)
return channel
async def update_channel(self, claim_id, confirm=True, **kwargs):
channel = await self.out(self.daemon.jsonrpc_channel_update(claim_id, **kwargs))
self.assertTrue(channel['outputs'][0]['name'].startswith('@'))
if confirm:
await self.on_transaction_dict(channel)
await self.generate(1)
await self.on_transaction_dict(channel)
return channel
async def resolve(self, uri):

View file

@ -1,5 +1,5 @@
import unittest
from lbrynet.extras.daemon import mime_types
from lbrynet.schema import mime_types
class TestMimeTypes(unittest.TestCase):