added support for signed supports
This commit is contained in:
parent
07f7a77ac0
commit
c03e30a01f
7 changed files with 154 additions and 49 deletions
|
@ -3980,7 +3980,9 @@ class Daemon(metaclass=JSONRPCServerType):
|
|||
|
||||
@requires(WALLET_COMPONENT)
|
||||
async def jsonrpc_support_create(
|
||||
self, claim_id, amount, tip=False, account_id=None, wallet_id=None, funding_account_ids=None,
|
||||
self, claim_id, amount, tip=False,
|
||||
channel_id=None, channel_name=None, channel_account_id=None,
|
||||
account_id=None, wallet_id=None, funding_account_ids=None,
|
||||
preview=False, blocking=False):
|
||||
"""
|
||||
Create a support or a tip for name claim.
|
||||
|
@ -3988,12 +3990,18 @@ class Daemon(metaclass=JSONRPCServerType):
|
|||
Usage:
|
||||
support_create (<claim_id> | --claim_id=<claim_id>) (<amount> | --amount=<amount>)
|
||||
[--tip] [--account_id=<account_id>] [--wallet_id=<wallet_id>]
|
||||
[--channel_id=<channel_id> | --channel_name=<channel_name>]
|
||||
[--channel_account_id=<channel_account_id>...]
|
||||
[--preview] [--blocking] [--funding_account_ids=<funding_account_ids>...]
|
||||
|
||||
Options:
|
||||
--claim_id=<claim_id> : (str) claim_id of the claim to support
|
||||
--amount=<amount> : (decimal) amount of support
|
||||
--tip : (bool) send support to claim owner, default: false.
|
||||
--channel_id=<channel_id> : (str) claim id of the supporters identity channel
|
||||
--channel_name=<channel_name> : (str) name of the supporters identity channel
|
||||
--channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in
|
||||
for channel certificates, defaults to all accounts.
|
||||
--account_id=<account_id> : (str) account to use for holding the transaction
|
||||
--wallet_id=<wallet_id> : (str) restrict operation to specific wallet
|
||||
--funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction
|
||||
|
@ -4005,6 +4013,7 @@ class Daemon(metaclass=JSONRPCServerType):
|
|||
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
|
||||
assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
|
||||
funding_accounts = wallet.get_accounts_or_all(funding_account_ids)
|
||||
channel = await self.get_channel_or_none(wallet, channel_account_id, channel_id, channel_name, for_signing=True)
|
||||
amount = self.get_dewies_or_error("amount", amount)
|
||||
claim = await self.ledger.get_claim_by_claim_id(wallet.accounts, claim_id)
|
||||
claim_address = claim.get_address(self.ledger)
|
||||
|
@ -4013,8 +4022,13 @@ class Daemon(metaclass=JSONRPCServerType):
|
|||
claim_address = await account.receiving.get_or_create_usable_address()
|
||||
|
||||
tx = await Transaction.support(
|
||||
claim.claim_name, claim_id, amount, claim_address, funding_accounts, funding_accounts[0]
|
||||
claim.claim_name, claim_id, amount, claim_address, funding_accounts, funding_accounts[0], channel
|
||||
)
|
||||
new_txo = tx.outputs[0]
|
||||
|
||||
if channel:
|
||||
new_txo.sign(channel)
|
||||
await tx.sign(funding_accounts)
|
||||
|
||||
if not preview:
|
||||
await self.broadcast_or_release(tx, blocking)
|
||||
|
|
|
@ -7,6 +7,7 @@ from json import JSONEncoder
|
|||
from google.protobuf.message import DecodeError
|
||||
|
||||
from lbry.schema.claim import Claim
|
||||
from lbry.schema.support import Support
|
||||
from lbry.torrent.torrent_manager import TorrentSource
|
||||
from lbry.wallet import Wallet, Ledger, Account, Transaction, Output
|
||||
from lbry.wallet.bip32 import PubKey
|
||||
|
@ -135,6 +136,8 @@ class JSONResponseEncoder(JSONEncoder):
|
|||
return self.encode_output(obj)
|
||||
if isinstance(obj, Claim):
|
||||
return self.encode_claim(obj)
|
||||
if isinstance(obj, Support):
|
||||
return obj.to_dict()
|
||||
if isinstance(obj, PubKey):
|
||||
return obj.extended_key_string()
|
||||
if isinstance(obj, datetime):
|
||||
|
@ -220,22 +223,25 @@ class JSONResponseEncoder(JSONEncoder):
|
|||
output['claims'] = [self.encode_output(o) for o in txo.claims]
|
||||
if txo.reposted_claim is not None:
|
||||
output['reposted_claim'] = self.encode_output(txo.reposted_claim)
|
||||
if txo.script.is_claim_name or txo.script.is_update_claim:
|
||||
if txo.script.is_claim_name or txo.script.is_update_claim or txo.script.is_support_claim_data:
|
||||
try:
|
||||
output['value'] = txo.claim
|
||||
output['value_type'] = txo.claim.claim_type
|
||||
output['value'] = txo.signable
|
||||
if self.include_protobuf:
|
||||
output['protobuf'] = hexlify(txo.claim.to_bytes())
|
||||
output['protobuf'] = hexlify(txo.signable.to_bytes())
|
||||
if txo.purchase_receipt is not None:
|
||||
output['purchase_receipt'] = self.encode_output(txo.purchase_receipt)
|
||||
if txo.script.is_claim_name or txo.script.is_update_claim:
|
||||
output['value_type'] = txo.claim.claim_type
|
||||
if txo.claim.is_channel:
|
||||
output['has_signing_key'] = txo.has_private_key
|
||||
if check_signature and txo.claim.is_signed:
|
||||
elif txo.script.is_support_claim_data:
|
||||
output['value_type'] = 'emoji'
|
||||
if check_signature and txo.signable.is_signed:
|
||||
if txo.channel is not None:
|
||||
output['signing_channel'] = self.encode_output(txo.channel)
|
||||
output['is_channel_signature_valid'] = txo.is_signed_by(txo.channel, self.ledger)
|
||||
else:
|
||||
output['signing_channel'] = {'channel_id': txo.claim.signing_channel_id}
|
||||
output['signing_channel'] = {'channel_id': txo.signable.signing_channel_id}
|
||||
output['is_channel_signature_valid'] = False
|
||||
except DecodeError:
|
||||
pass
|
||||
|
|
|
@ -30,14 +30,10 @@ class Claim(Signable):
|
|||
COLLECTION = 'collection'
|
||||
REPOST = 'repost'
|
||||
|
||||
__slots__ = 'version',
|
||||
__slots__ = ()
|
||||
|
||||
message_class = ClaimMessage
|
||||
|
||||
def __init__(self, message=None):
|
||||
super().__init__(message)
|
||||
self.version = 2
|
||||
|
||||
@property
|
||||
def claim_type(self) -> str:
|
||||
return self.message.WhichOneof('type')
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
from lbry.schema.base import Signable
|
||||
from lbry.schema.types.v2.support_pb2 import Support as SupportMessage
|
||||
|
||||
|
||||
class Support(Signable):
|
||||
__slots__ = ()
|
||||
message_class = None # TODO: add support protobufs
|
||||
message_class = SupportMessage
|
||||
|
||||
@property
|
||||
def emoji(self) -> str:
|
||||
return self.message.emoji
|
||||
|
||||
@emoji.setter
|
||||
def emoji(self, emoji: str):
|
||||
self.message.emoji = emoji
|
||||
|
|
|
@ -438,6 +438,17 @@ class OutputScript(Script):
|
|||
SUPPORT_CLAIM_OPCODES + PAY_SCRIPT_HASH.opcodes
|
||||
))
|
||||
|
||||
SUPPORT_CLAIM_DATA_OPCODES = (
|
||||
OP_SUPPORT_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'), PUSH_SINGLE('support'),
|
||||
OP_2DROP, OP_2DROP
|
||||
)
|
||||
SUPPORT_CLAIM_DATA_PUBKEY = Template('support_claim+data+pay_pubkey_hash', (
|
||||
SUPPORT_CLAIM_DATA_OPCODES + PAY_PUBKEY_HASH.opcodes
|
||||
))
|
||||
SUPPORT_CLAIM_DATA_SCRIPT = Template('support_claim+data+pay_script_hash', (
|
||||
SUPPORT_CLAIM_DATA_OPCODES + PAY_SCRIPT_HASH.opcodes
|
||||
))
|
||||
|
||||
UPDATE_CLAIM_OPCODES = (
|
||||
OP_UPDATE_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'), PUSH_SINGLE('claim'),
|
||||
OP_2DROP, OP_2DROP
|
||||
|
@ -474,6 +485,8 @@ class OutputScript(Script):
|
|||
CLAIM_NAME_SCRIPT,
|
||||
SUPPORT_CLAIM_PUBKEY,
|
||||
SUPPORT_CLAIM_SCRIPT,
|
||||
SUPPORT_CLAIM_DATA_PUBKEY,
|
||||
SUPPORT_CLAIM_DATA_SCRIPT,
|
||||
UPDATE_CLAIM_PUBKEY,
|
||||
UPDATE_CLAIM_SCRIPT,
|
||||
SELL_CLAIM, SELL_SCRIPT,
|
||||
|
@ -527,6 +540,16 @@ class OutputScript(Script):
|
|||
'pubkey_hash': pubkey_hash
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def pay_support_data_pubkey_hash(
|
||||
cls, claim_name: bytes, claim_id: bytes, support, pubkey_hash: bytes):
|
||||
return cls(template=cls.SUPPORT_CLAIM_DATA_PUBKEY, values={
|
||||
'claim_name': claim_name,
|
||||
'claim_id': claim_id,
|
||||
'support': support,
|
||||
'pubkey_hash': pubkey_hash
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def sell_script(cls, price):
|
||||
return cls(template=cls.SELL_SCRIPT, values={
|
||||
|
@ -575,6 +598,10 @@ class OutputScript(Script):
|
|||
def is_support_claim(self):
|
||||
return self.template.name.startswith('support_claim+')
|
||||
|
||||
@property
|
||||
def is_support_claim_data(self):
|
||||
return self.template.name.startswith('support_claim+data+')
|
||||
|
||||
@property
|
||||
def is_sell_claim(self):
|
||||
return self.template.name.startswith('sell_claim+')
|
||||
|
|
|
@ -19,7 +19,9 @@ from lbry.crypto.hash import hash160, sha256
|
|||
from lbry.crypto.base58 import Base58
|
||||
from lbry.schema.url import normalize_name
|
||||
from lbry.schema.claim import Claim
|
||||
from lbry.schema.base import Signable
|
||||
from lbry.schema.purchase import Purchase
|
||||
from lbry.schema.support import Support
|
||||
|
||||
from .script import InputScript, OutputScript
|
||||
from .constants import COIN, NULL_HASH32
|
||||
|
@ -211,7 +213,7 @@ class Output(InputOutput):
|
|||
'amount', 'script', 'is_internal_transfer', 'is_spent', 'is_my_output', 'is_my_input',
|
||||
'channel', 'private_key', 'meta', 'sent_supports', 'sent_tips', 'received_tips',
|
||||
'purchase', 'purchased_claim', 'purchase_receipt',
|
||||
'reposted_claim', 'claims',
|
||||
'reposted_claim', 'claims', '_signable'
|
||||
)
|
||||
|
||||
def __init__(self, amount: int, script: OutputScript,
|
||||
|
@ -239,6 +241,7 @@ class Output(InputOutput):
|
|||
self.purchase_receipt: 'Output' = None # txo representing purchase receipt for this claim
|
||||
self.reposted_claim: 'Output' = None # txo representing claim being reposted
|
||||
self.claims: List['Output'] = None # resolved claims for collection
|
||||
self._signable: Optional[Signable] = None
|
||||
self.meta = {}
|
||||
|
||||
def update_annotations(self, annotated: 'Output'):
|
||||
|
@ -312,6 +315,10 @@ class Output(InputOutput):
|
|||
def is_support(self) -> bool:
|
||||
return self.script.is_support_claim
|
||||
|
||||
@property
|
||||
def is_support_data(self) -> bool:
|
||||
return self.script.is_support_claim_data
|
||||
|
||||
@property
|
||||
def claim_hash(self) -> bytes:
|
||||
if self.script.is_claim_name:
|
||||
|
@ -347,9 +354,33 @@ class Output(InputOutput):
|
|||
def can_decode_claim(self):
|
||||
try:
|
||||
return self.claim
|
||||
except: # pylint: disable=bare-except
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@property
|
||||
def support(self) -> Support:
|
||||
if self.is_support_data:
|
||||
if not isinstance(self.script.values['support'], Support):
|
||||
self.script.values['support'] = Support.from_bytes(self.script.values['support'])
|
||||
return self.script.values['support']
|
||||
raise ValueError('Only supports with data can be represented as Supports.')
|
||||
|
||||
@property
|
||||
def can_decode_support(self):
|
||||
try:
|
||||
return self.support
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@property
|
||||
def signable(self) -> Signable:
|
||||
if self._signable is None:
|
||||
if self.is_claim:
|
||||
self._signable = self.claim
|
||||
elif self.is_support_data:
|
||||
self._signable = self.support
|
||||
return self._signable
|
||||
|
||||
@property
|
||||
def permanent_url(self) -> str:
|
||||
if self.script.is_claim_involved:
|
||||
|
@ -361,22 +392,22 @@ class Output(InputOutput):
|
|||
return self.private_key is not None
|
||||
|
||||
def get_signature_digest(self, ledger):
|
||||
if self.claim.unsigned_payload:
|
||||
if self.signable.unsigned_payload:
|
||||
pieces = [
|
||||
Base58.decode(self.get_address(ledger)),
|
||||
self.claim.unsigned_payload,
|
||||
self.claim.signing_channel_hash[::-1]
|
||||
self.signable.unsigned_payload,
|
||||
self.signable.signing_channel_hash[::-1]
|
||||
]
|
||||
else:
|
||||
pieces = [
|
||||
self.tx_ref.tx.inputs[0].txo_ref.hash,
|
||||
self.claim.signing_channel_hash,
|
||||
self.claim.to_message_bytes()
|
||||
self.signable.signing_channel_hash,
|
||||
self.signable.to_message_bytes()
|
||||
]
|
||||
return sha256(b''.join(pieces))
|
||||
|
||||
def get_encoded_signature(self):
|
||||
signature = hexlify(self.claim.signature)
|
||||
signature = hexlify(self.signable.signature)
|
||||
r = int(signature[:int(len(signature)/2)], 16)
|
||||
s = int(signature[int(len(signature)/2):], 16)
|
||||
return ecdsa.util.sigencode_der(r, s, len(signature)*4)
|
||||
|
@ -400,18 +431,18 @@ class Output(InputOutput):
|
|||
|
||||
def sign(self, channel: 'Output', first_input_id=None):
|
||||
self.channel = channel
|
||||
self.claim.signing_channel_hash = channel.claim_hash
|
||||
self.signable.signing_channel_hash = channel.claim_hash
|
||||
digest = sha256(b''.join([
|
||||
first_input_id or self.tx_ref.tx.inputs[0].txo_ref.hash,
|
||||
self.claim.signing_channel_hash,
|
||||
self.claim.to_message_bytes()
|
||||
self.signable.signing_channel_hash,
|
||||
self.signable.to_message_bytes()
|
||||
]))
|
||||
self.claim.signature = channel.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256)
|
||||
self.signable.signature = channel.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256)
|
||||
self.script.generate()
|
||||
|
||||
def clear_signature(self):
|
||||
self.channel = None
|
||||
self.claim.clear_signature()
|
||||
self.signable.clear_signature()
|
||||
|
||||
async def generate_channel_private_key(self):
|
||||
self.private_key = await asyncio.get_event_loop().run_in_executor(
|
||||
|
@ -446,6 +477,14 @@ class Output(InputOutput):
|
|||
)
|
||||
return cls(amount, script)
|
||||
|
||||
@classmethod
|
||||
def pay_support_data_pubkey_hash(
|
||||
cls, amount: int, claim_name: str, claim_id: str, support: Support, pubkey_hash: bytes) -> 'Output':
|
||||
script = OutputScript.pay_support_data_pubkey_hash(
|
||||
claim_name.encode(), unhexlify(claim_id)[::-1], support, pubkey_hash
|
||||
)
|
||||
return cls(amount, script)
|
||||
|
||||
@classmethod
|
||||
def add_purchase_data(cls, purchase: Purchase) -> 'Output':
|
||||
script = OutputScript.return_data(purchase)
|
||||
|
@ -860,12 +899,20 @@ class Transaction:
|
|||
|
||||
@classmethod
|
||||
def support(cls, claim_name: str, claim_id: str, amount: int, holding_address: str,
|
||||
funding_accounts: List['Account'], change_account: 'Account'):
|
||||
funding_accounts: List['Account'], change_account: 'Account', signing_channel: Output = None):
|
||||
ledger, _ = cls.ensure_all_have_same_ledger_and_wallet(funding_accounts, change_account)
|
||||
if signing_channel is not None:
|
||||
support = Support()
|
||||
support.emoji = '👍'
|
||||
support_output = Output.pay_support_data_pubkey_hash(
|
||||
amount, claim_name, claim_id, support, ledger.address_to_hash160(holding_address)
|
||||
)
|
||||
support_output.sign(signing_channel, b'placeholder txid:nout')
|
||||
else:
|
||||
support_output = Output.pay_support_pubkey_hash(
|
||||
amount, claim_name, claim_id, ledger.address_to_hash160(holding_address)
|
||||
)
|
||||
return cls.create([], [support_output], funding_accounts, change_account)
|
||||
return cls.create([], [support_output], funding_accounts, change_account, sign=False)
|
||||
|
||||
@classmethod
|
||||
def purchase(cls, claim_id: str, amount: int, merchant_address: bytes,
|
||||
|
|
|
@ -1886,6 +1886,12 @@ class SupportCommands(CommandTestCase):
|
|||
self.assertTrue(txs[1]['support_info'][0]['is_tip'])
|
||||
self.assertTrue(txs[1]['support_info'][0]['is_spent'])
|
||||
|
||||
async def test_signed_supports(self):
|
||||
channel_id = self.get_claim_id(await self.channel_create())
|
||||
stream_id = self.get_claim_id(await self.stream_create())
|
||||
tx = await self.support_create(stream_id, '0.3', channel_id=channel_id)
|
||||
self.assertTrue(tx['outputs'][0]['is_channel_signature_valid'])
|
||||
|
||||
|
||||
class CollectionCommands(CommandTestCase):
|
||||
|
||||
|
|
Loading…
Reference in a new issue