wip
This commit is contained in:
parent
323694fb12
commit
1ec8f0b0b4
5 changed files with 166 additions and 115 deletions
|
@ -6,7 +6,7 @@ from binascii import unhexlify
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from lbrynet.schema.constants import SECP256k1
|
from lbrynet.schema.claim import Claim
|
||||||
from torba.client.basemanager import BaseWalletManager
|
from torba.client.basemanager import BaseWalletManager
|
||||||
from torba.rpc.jsonrpc import CodeMessageError
|
from torba.rpc.jsonrpc import CodeMessageError
|
||||||
|
|
||||||
|
@ -394,15 +394,11 @@ class LbryWalletManager(BaseWalletManager):
|
||||||
def get_utxos(account: BaseAccount):
|
def get_utxos(account: BaseAccount):
|
||||||
return account.get_utxos()
|
return account.get_utxos()
|
||||||
|
|
||||||
async def claim_name(self, account, name, amount, claim_dict, certificate=None, claim_address=None):
|
async def claim_name(self, account, name, amount, claim: Claim, certificate=None, claim_address=None):
|
||||||
claim = ClaimDict.load_dict(claim_dict)
|
|
||||||
if not claim_address:
|
if not claim_address:
|
||||||
claim_address = await account.receiving.get_or_create_usable_address()
|
claim_address = await account.receiving.get_or_create_usable_address()
|
||||||
if certificate:
|
if certificate:
|
||||||
claim = claim.sign(
|
claim = claim.sign(certificate.claim_id, certificate.private_key)
|
||||||
certificate.private_key, claim_address, certificate.claim_id, curve=SECP256k1, name=name,
|
|
||||||
force_detached=False # TODO: delete it and make True default everywhere when its out
|
|
||||||
)
|
|
||||||
existing_claims = await account.get_claims(
|
existing_claims = await account.get_claims(
|
||||||
claim_name_type__any={'is_claim': 1, 'is_update': 1}, # exclude is_supports
|
claim_name_type__any={'is_claim': 1, 'is_update': 1}, # exclude is_supports
|
||||||
claim_name=name
|
claim_name=name
|
||||||
|
@ -417,9 +413,12 @@ class LbryWalletManager(BaseWalletManager):
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise NameError(f"More than one other claim exists with the name '{name}'.")
|
raise NameError(f"More than one other claim exists with the name '{name}'.")
|
||||||
|
if certificate:
|
||||||
|
claim.sign(certificate.claim_id, certificate.private_key, tx.inputs[0].txo_ref.id.encode())
|
||||||
|
tx._reset()
|
||||||
await account.ledger.broadcast(tx)
|
await account.ledger.broadcast(tx)
|
||||||
await self.old_db.save_claims([self._old_get_temp_claim_info(
|
await self.old_db.save_claims([self._old_get_temp_claim_info(
|
||||||
tx, tx.outputs[0], claim_address, claim_dict, name, dewies_to_lbc(amount)
|
tx, tx.outputs[0], claim_address, claim, name, dewies_to_lbc(amount)
|
||||||
)])
|
)])
|
||||||
# TODO: release reserved tx outputs in case anything fails by this point
|
# TODO: release reserved tx outputs in case anything fails by this point
|
||||||
return tx
|
return tx
|
||||||
|
|
|
@ -4,8 +4,7 @@ from typing import List, Iterable, Optional
|
||||||
|
|
||||||
from torba.client.basetransaction import BaseTransaction, BaseInput, BaseOutput
|
from torba.client.basetransaction import BaseTransaction, BaseInput, BaseOutput
|
||||||
from torba.client.hash import hash160
|
from torba.client.hash import hash160
|
||||||
from lbrynet.schema.decode import smart_decode
|
from lbrynet.schema.claim import Claim
|
||||||
from lbrynet.schema.claim import ClaimDict
|
|
||||||
from lbrynet.extras.wallet.account import Account
|
from lbrynet.extras.wallet.account import Account
|
||||||
from lbrynet.extras.wallet.script import InputScript, OutputScript
|
from lbrynet.extras.wallet.script import InputScript, OutputScript
|
||||||
|
|
||||||
|
@ -19,12 +18,12 @@ class Output(BaseOutput):
|
||||||
script: OutputScript
|
script: OutputScript
|
||||||
script_class = OutputScript
|
script_class = OutputScript
|
||||||
|
|
||||||
__slots__ = '_claim_dict', 'channel', 'private_key'
|
__slots__ = '_claim', 'channel', 'private_key'
|
||||||
|
|
||||||
def __init__(self, *args, channel: Optional['Output'] = None,
|
def __init__(self, *args, channel: Optional['Output'] = None,
|
||||||
private_key: Optional[str] = None, **kwargs) -> None:
|
private_key: Optional[str] = None, **kwargs) -> None:
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._claim_dict = None
|
self._claim = None
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.private_key = private_key
|
self.private_key = private_key
|
||||||
|
|
||||||
|
@ -60,17 +59,13 @@ class Output(BaseOutput):
|
||||||
raise ValueError('No claim_name associated.')
|
raise ValueError('No claim_name associated.')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def claim(self) -> ClaimDict:
|
def claim(self) -> Claim:
|
||||||
if self.is_claim:
|
if self.is_claim:
|
||||||
return smart_decode(self.script.values['claim'])
|
if self._claim is None:
|
||||||
|
self._claim = Claim.from_bytes(self.script.values['claim'])
|
||||||
|
return self._claim
|
||||||
raise ValueError('Only claim name and claim update have the claim payload.')
|
raise ValueError('Only claim name and claim update have the claim payload.')
|
||||||
|
|
||||||
@property
|
|
||||||
def claim_dict(self) -> dict:
|
|
||||||
if self._claim_dict is None:
|
|
||||||
self._claim_dict = self.claim.claim_dict
|
|
||||||
return self._claim_dict
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def permanent_url(self) -> str:
|
def permanent_url(self) -> str:
|
||||||
if self.script.is_claim_involved:
|
if self.script.is_claim_involved:
|
||||||
|
@ -124,11 +119,11 @@ class Transaction(BaseTransaction):
|
||||||
return cls.create([], [output], funding_accounts, change_account)
|
return cls.create([], [output], funding_accounts, change_account)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def claim(cls, name: str, meta: ClaimDict, amount: int, holding_address: bytes,
|
def claim(cls, name: str, meta: Claim, amount: int, holding_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)
|
ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account)
|
||||||
claim_output = Output.pay_claim_name_pubkey_hash(
|
claim_output = Output.pay_claim_name_pubkey_hash(
|
||||||
amount, name, meta.serialized, ledger.address_to_hash160(holding_address)
|
amount, name, meta.to_bytes(), ledger.address_to_hash160(holding_address)
|
||||||
)
|
)
|
||||||
return cls.create([], [claim_output], funding_accounts, change_account)
|
return cls.create([], [claim_output], funding_accounts, change_account)
|
||||||
|
|
||||||
|
@ -142,12 +137,12 @@ class Transaction(BaseTransaction):
|
||||||
return cls.create([], [claim_output], funding_accounts, change_account)
|
return cls.create([], [claim_output], funding_accounts, change_account)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update(cls, previous_claim: Output, meta: ClaimDict, amount: int, holding_address: bytes,
|
def update(cls, previous_claim: Output, meta: Claim, amount: int, holding_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)
|
ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account)
|
||||||
updated_claim = Output.pay_update_claim_pubkey_hash(
|
updated_claim = Output.pay_update_claim_pubkey_hash(
|
||||||
amount, previous_claim.claim_name, previous_claim.claim_id,
|
amount, previous_claim.claim_name, previous_claim.claim_id,
|
||||||
meta.serialized, ledger.address_to_hash160(holding_address)
|
meta.to_bytes(), ledger.address_to_hash160(holding_address)
|
||||||
)
|
)
|
||||||
return cls.create([Input.spend(previous_claim)], [updated_claim], funding_accounts, change_account)
|
return cls.create([Input.spend(previous_claim)], [updated_claim], funding_accounts, change_account)
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,15 @@ from collections import OrderedDict
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from binascii import hexlify, unhexlify
|
from binascii import hexlify, unhexlify
|
||||||
|
from hashlib import sha256
|
||||||
|
|
||||||
|
import ecdsa
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives.serialization import load_der_public_key
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ec
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
|
||||||
|
from ecdsa.util import sigencode_der
|
||||||
|
|
||||||
from google.protobuf import json_format # pylint: disable=no-name-in-module
|
from google.protobuf import json_format # pylint: disable=no-name-in-module
|
||||||
from google.protobuf.message import DecodeError as DecodeError_pb # pylint: disable=no-name-in-module,import-error
|
from google.protobuf.message import DecodeError as DecodeError_pb # pylint: disable=no-name-in-module,import-error
|
||||||
|
@ -204,10 +213,14 @@ class ClaimDict(OrderedDict):
|
||||||
|
|
||||||
class Claim:
|
class Claim:
|
||||||
|
|
||||||
__slots__ = '_claim',
|
__slots__ = '_claim', 'signature', 'certificate_id', 'signature_type', 'unsigned_payload'
|
||||||
|
|
||||||
def __init__(self, claim_message=None):
|
def __init__(self, claim_message=None):
|
||||||
self._claim = claim_message or ClaimMessage()
|
self._claim = claim_message or ClaimMessage()
|
||||||
|
self.signature = None
|
||||||
|
self.signature_type = 'SECP256k1'
|
||||||
|
self.certificate_id = None
|
||||||
|
self.unsigned_payload = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_undetermined(self):
|
def is_undetermined(self):
|
||||||
|
@ -221,6 +234,40 @@ class Claim:
|
||||||
def is_channel(self):
|
def is_channel(self):
|
||||||
return self._claim.WhichOneof('type') == 'channel'
|
return self._claim.WhichOneof('type') == 'channel'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_signed(self):
|
||||||
|
return self.signature is not None
|
||||||
|
|
||||||
|
def is_signed_by(self, channel: 'Channel', claim_address):
|
||||||
|
if self.unsigned_payload:
|
||||||
|
digest = sha256(b''.join([
|
||||||
|
claim_address, self.unsigned_payload, self.certificate_id
|
||||||
|
])).digest()
|
||||||
|
public_key = load_der_public_key(channel.public_key_bytes, default_backend())
|
||||||
|
hash = hashes.SHA256()
|
||||||
|
signature = hexlify(self.signature)
|
||||||
|
r = int(signature[:int(len(signature)/2)], 16)
|
||||||
|
s = int(signature[int(len(signature)/2):], 16)
|
||||||
|
encoded_sig = sigencode_der(r, s, len(signature)*4)
|
||||||
|
public_key.verify(encoded_sig, digest, ec.ECDSA(Prehashed(hash)))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
digest = sha256(b''.join([
|
||||||
|
self.certificate_id.encode(),
|
||||||
|
first_input_txid_nout.encode(),
|
||||||
|
self.to_bytes()
|
||||||
|
])).digest()
|
||||||
|
|
||||||
|
def sign(self, certificate_id: str, private_key_text: str, first_input_txid_nout):
|
||||||
|
digest = sha256(b''.join([
|
||||||
|
certificate_id.encode(),
|
||||||
|
first_input_txid_nout.encode(),
|
||||||
|
self.to_bytes()
|
||||||
|
])).digest()
|
||||||
|
private_key = ecdsa.SigningKey.from_pem(private_key_text, hashfunc="sha256")
|
||||||
|
self.signature = private_key.sign_digest_deterministic(digest, hashfunc="sha256")
|
||||||
|
self.certificate_id = certificate_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stream_message(self):
|
def stream_message(self):
|
||||||
if self.is_undetermined:
|
if self.is_undetermined:
|
||||||
|
@ -400,6 +447,95 @@ class Fee:
|
||||||
self._fee.currency = FeeMessage.USD
|
self._fee.currency = FeeMessage.USD
|
||||||
|
|
||||||
|
|
||||||
|
class Channel:
|
||||||
|
|
||||||
|
__slots__ = '_claim', '_channel'
|
||||||
|
|
||||||
|
def __init__(self, claim: Claim = None):
|
||||||
|
self._claim = claim or Claim()
|
||||||
|
self._channel = self._claim.channel_message
|
||||||
|
|
||||||
|
@property
|
||||||
|
def claim(self) -> Claim:
|
||||||
|
return self._claim
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tags(self) -> List:
|
||||||
|
return self._channel.tags
|
||||||
|
|
||||||
|
@property
|
||||||
|
def public_key(self) -> str:
|
||||||
|
return hexlify(self._channel.public_key).decode()
|
||||||
|
|
||||||
|
@public_key.setter
|
||||||
|
def public_key(self, sd_public_key: str):
|
||||||
|
self._channel.public_key = unhexlify(sd_public_key.encode())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def public_key_bytes(self) -> bytes:
|
||||||
|
return self._channel.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
|
||||||
|
|
||||||
|
@property
|
||||||
|
def contact_email(self) -> str:
|
||||||
|
return self._channel.contact_email
|
||||||
|
|
||||||
|
@contact_email.setter
|
||||||
|
def contact_email(self, contact_email: str):
|
||||||
|
self._channel.contact_email = contact_email
|
||||||
|
|
||||||
|
@property
|
||||||
|
def homepage_url(self) -> str:
|
||||||
|
return self._channel.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
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cover_url(self) -> str:
|
||||||
|
return self._channel.cover_url
|
||||||
|
|
||||||
|
@cover_url.setter
|
||||||
|
def cover_url(self, cover_url: str):
|
||||||
|
self._channel.cover_url = cover_url
|
||||||
|
|
||||||
|
|
||||||
class Stream:
|
class Stream:
|
||||||
|
|
||||||
__slots__ = '_claim', '_stream'
|
__slots__ = '_claim', '_stream'
|
||||||
|
@ -523,92 +659,3 @@ class Stream:
|
||||||
@release_time.setter
|
@release_time.setter
|
||||||
def release_time(self, release_time: int):
|
def release_time(self, release_time: int):
|
||||||
self._stream.release_time = release_time
|
self._stream.release_time = release_time
|
||||||
|
|
||||||
|
|
||||||
class Channel:
|
|
||||||
|
|
||||||
__slots__ = '_claim', '_channel'
|
|
||||||
|
|
||||||
def __init__(self, claim: Claim = None):
|
|
||||||
self._claim = claim or Claim()
|
|
||||||
self._channel = self._claim.channel_message
|
|
||||||
|
|
||||||
@property
|
|
||||||
def claim(self) -> Claim:
|
|
||||||
return self._claim
|
|
||||||
|
|
||||||
@property
|
|
||||||
def tags(self) -> List:
|
|
||||||
return self._channel.tags
|
|
||||||
|
|
||||||
@property
|
|
||||||
def public_key(self) -> str:
|
|
||||||
return hexlify(self._channel.public_key).decode()
|
|
||||||
|
|
||||||
@public_key.setter
|
|
||||||
def public_key(self, sd_public_key: str):
|
|
||||||
self._channel.public_key = unhexlify(sd_public_key.encode())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def public_key_bytes(self) -> bytes:
|
|
||||||
return self._channel.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
|
|
||||||
|
|
||||||
@property
|
|
||||||
def contact_email(self) -> str:
|
|
||||||
return self._channel.contact_email
|
|
||||||
|
|
||||||
@contact_email.setter
|
|
||||||
def contact_email(self, contact_email: str):
|
|
||||||
self._channel.contact_email = contact_email
|
|
||||||
|
|
||||||
@property
|
|
||||||
def homepage_url(self) -> str:
|
|
||||||
return self._channel.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
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cover_url(self) -> str:
|
|
||||||
return self._channel.cover_url
|
|
||||||
|
|
||||||
@cover_url.setter
|
|
||||||
def cover_url(self, cover_url: str):
|
|
||||||
self._channel.cover_url = cover_url
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import json
|
import json
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from lbrynet.schema.address import decode_address, encode_address
|
|
||||||
from lbrynet.schema.types.v1.legacy_claim_pb2 import Claim as OldClaimMessage
|
from lbrynet.schema.types.v1.legacy_claim_pb2 import Claim as OldClaimMessage
|
||||||
from lbrynet.schema.types.v1.metadata_pb2 import Metadata as MetadataMessage
|
from lbrynet.schema.types.v1.metadata_pb2 import Metadata as MetadataMessage
|
||||||
|
from lbrynet.schema.types.v1.certificate_pb2 import KeyType
|
||||||
from lbrynet.schema.types.v1.fee_pb2 import Fee as FeeMessage
|
from lbrynet.schema.types.v1.fee_pb2 import Fee as FeeMessage
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,6 +60,13 @@ def from_types_v1(claim, payload: bytes):
|
||||||
stream.fee.usd = Decimal(fee.amount)
|
stream.fee.usd = Decimal(fee.amount)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f'Unsupported currency: {currency}')
|
raise ValueError(f'Unsupported currency: {currency}')
|
||||||
|
if old.HasField('publisherSignature'):
|
||||||
|
sig = old.publisherSignature
|
||||||
|
claim.signature = sig.signature
|
||||||
|
claim.signature_type = KeyType.Name(sig.signatureType)
|
||||||
|
claim.certificate_id = sig.certificateId
|
||||||
|
old.ClearField("publisherSignature")
|
||||||
|
claim.unsigned_payload = old.SerializeToString()
|
||||||
elif old.claimType == 2:
|
elif old.claimType == 2:
|
||||||
channel = claim.channel
|
channel = claim.channel
|
||||||
channel.public_key_bytes = old.certificate.publicKey
|
channel.public_key_bytes = old.certificate.publicKey
|
||||||
|
|
|
@ -2,6 +2,7 @@ from unittest import TestCase
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
|
|
||||||
from lbrynet.schema import Claim
|
from lbrynet.schema import Claim
|
||||||
|
from lbrynet.schema.base import b58decode
|
||||||
|
|
||||||
|
|
||||||
class TestOldJSONSchemaCompatibility(TestCase):
|
class TestOldJSONSchemaCompatibility(TestCase):
|
||||||
|
@ -143,6 +144,8 @@ class TestTypesV1Compatibility(TestCase):
|
||||||
'3f17'
|
'3f17'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.assertTrue(stream.is_signed_by(channel, b58decode('bb4UAfujhmvTgyx7ufoEa4aevum6hKSW36')))
|
||||||
|
|
||||||
def test_unsigned_with_fee(self):
|
def test_unsigned_with_fee(self):
|
||||||
claim = Claim.from_bytes(unhexlify(
|
claim = Claim.from_bytes(unhexlify(
|
||||||
b'080110011ad6010801127c080410011a08727067206d69646922046d6964692a08727067206d696469322'
|
b'080110011ad6010801127c080410011a08727067206d69646922046d6964692a08727067206d696469322'
|
||||||
|
|
Loading…
Reference in a new issue