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)
|
@requires(WALLET_COMPONENT)
|
||||||
async def jsonrpc_support_create(
|
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):
|
preview=False, blocking=False):
|
||||||
"""
|
"""
|
||||||
Create a support or a tip for name claim.
|
Create a support or a tip for name claim.
|
||||||
|
@ -3988,12 +3990,18 @@ class Daemon(metaclass=JSONRPCServerType):
|
||||||
Usage:
|
Usage:
|
||||||
support_create (<claim_id> | --claim_id=<claim_id>) (<amount> | --amount=<amount>)
|
support_create (<claim_id> | --claim_id=<claim_id>) (<amount> | --amount=<amount>)
|
||||||
[--tip] [--account_id=<account_id>] [--wallet_id=<wallet_id>]
|
[--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>...]
|
[--preview] [--blocking] [--funding_account_ids=<funding_account_ids>...]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--claim_id=<claim_id> : (str) claim_id of the claim to support
|
--claim_id=<claim_id> : (str) claim_id of the claim to support
|
||||||
--amount=<amount> : (decimal) amount of support
|
--amount=<amount> : (decimal) amount of support
|
||||||
--tip : (bool) send support to claim owner, default: false.
|
--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
|
--account_id=<account_id> : (str) account to use for holding the transaction
|
||||||
--wallet_id=<wallet_id> : (str) restrict operation to specific wallet
|
--wallet_id=<wallet_id> : (str) restrict operation to specific wallet
|
||||||
--funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction
|
--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)
|
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
|
||||||
assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
|
assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
|
||||||
funding_accounts = wallet.get_accounts_or_all(funding_account_ids)
|
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)
|
amount = self.get_dewies_or_error("amount", amount)
|
||||||
claim = await self.ledger.get_claim_by_claim_id(wallet.accounts, claim_id)
|
claim = await self.ledger.get_claim_by_claim_id(wallet.accounts, claim_id)
|
||||||
claim_address = claim.get_address(self.ledger)
|
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()
|
claim_address = await account.receiving.get_or_create_usable_address()
|
||||||
|
|
||||||
tx = await Transaction.support(
|
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:
|
if not preview:
|
||||||
await self.broadcast_or_release(tx, blocking)
|
await self.broadcast_or_release(tx, blocking)
|
||||||
|
|
|
@ -7,6 +7,7 @@ from json import JSONEncoder
|
||||||
from google.protobuf.message import DecodeError
|
from google.protobuf.message import DecodeError
|
||||||
|
|
||||||
from lbry.schema.claim import Claim
|
from lbry.schema.claim import Claim
|
||||||
|
from lbry.schema.support import Support
|
||||||
from lbry.torrent.torrent_manager import TorrentSource
|
from lbry.torrent.torrent_manager import TorrentSource
|
||||||
from lbry.wallet import Wallet, Ledger, Account, Transaction, Output
|
from lbry.wallet import Wallet, Ledger, Account, Transaction, Output
|
||||||
from lbry.wallet.bip32 import PubKey
|
from lbry.wallet.bip32 import PubKey
|
||||||
|
@ -135,6 +136,8 @@ class JSONResponseEncoder(JSONEncoder):
|
||||||
return self.encode_output(obj)
|
return self.encode_output(obj)
|
||||||
if isinstance(obj, Claim):
|
if isinstance(obj, Claim):
|
||||||
return self.encode_claim(obj)
|
return self.encode_claim(obj)
|
||||||
|
if isinstance(obj, Support):
|
||||||
|
return obj.to_dict()
|
||||||
if isinstance(obj, PubKey):
|
if isinstance(obj, PubKey):
|
||||||
return obj.extended_key_string()
|
return obj.extended_key_string()
|
||||||
if isinstance(obj, datetime):
|
if isinstance(obj, datetime):
|
||||||
|
@ -220,22 +223,25 @@ class JSONResponseEncoder(JSONEncoder):
|
||||||
output['claims'] = [self.encode_output(o) for o in txo.claims]
|
output['claims'] = [self.encode_output(o) for o in txo.claims]
|
||||||
if txo.reposted_claim is not None:
|
if txo.reposted_claim is not None:
|
||||||
output['reposted_claim'] = self.encode_output(txo.reposted_claim)
|
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:
|
try:
|
||||||
output['value'] = txo.claim
|
output['value'] = txo.signable
|
||||||
output['value_type'] = txo.claim.claim_type
|
|
||||||
if self.include_protobuf:
|
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:
|
if txo.purchase_receipt is not None:
|
||||||
output['purchase_receipt'] = self.encode_output(txo.purchase_receipt)
|
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:
|
if txo.claim.is_channel:
|
||||||
output['has_signing_key'] = txo.has_private_key
|
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:
|
if txo.channel is not None:
|
||||||
output['signing_channel'] = self.encode_output(txo.channel)
|
output['signing_channel'] = self.encode_output(txo.channel)
|
||||||
output['is_channel_signature_valid'] = txo.is_signed_by(txo.channel, self.ledger)
|
output['is_channel_signature_valid'] = txo.is_signed_by(txo.channel, self.ledger)
|
||||||
else:
|
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
|
output['is_channel_signature_valid'] = False
|
||||||
except DecodeError:
|
except DecodeError:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -30,14 +30,10 @@ class Claim(Signable):
|
||||||
COLLECTION = 'collection'
|
COLLECTION = 'collection'
|
||||||
REPOST = 'repost'
|
REPOST = 'repost'
|
||||||
|
|
||||||
__slots__ = 'version',
|
__slots__ = ()
|
||||||
|
|
||||||
message_class = ClaimMessage
|
message_class = ClaimMessage
|
||||||
|
|
||||||
def __init__(self, message=None):
|
|
||||||
super().__init__(message)
|
|
||||||
self.version = 2
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def claim_type(self) -> str:
|
def claim_type(self) -> str:
|
||||||
return self.message.WhichOneof('type')
|
return self.message.WhichOneof('type')
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
from lbry.schema.base import Signable
|
from lbry.schema.base import Signable
|
||||||
|
from lbry.schema.types.v2.support_pb2 import Support as SupportMessage
|
||||||
|
|
||||||
|
|
||||||
class Support(Signable):
|
class Support(Signable):
|
||||||
__slots__ = ()
|
__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_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 = (
|
UPDATE_CLAIM_OPCODES = (
|
||||||
OP_UPDATE_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'), PUSH_SINGLE('claim'),
|
OP_UPDATE_CLAIM, PUSH_SINGLE('claim_name'), PUSH_SINGLE('claim_id'), PUSH_SINGLE('claim'),
|
||||||
OP_2DROP, OP_2DROP
|
OP_2DROP, OP_2DROP
|
||||||
|
@ -474,6 +485,8 @@ class OutputScript(Script):
|
||||||
CLAIM_NAME_SCRIPT,
|
CLAIM_NAME_SCRIPT,
|
||||||
SUPPORT_CLAIM_PUBKEY,
|
SUPPORT_CLAIM_PUBKEY,
|
||||||
SUPPORT_CLAIM_SCRIPT,
|
SUPPORT_CLAIM_SCRIPT,
|
||||||
|
SUPPORT_CLAIM_DATA_PUBKEY,
|
||||||
|
SUPPORT_CLAIM_DATA_SCRIPT,
|
||||||
UPDATE_CLAIM_PUBKEY,
|
UPDATE_CLAIM_PUBKEY,
|
||||||
UPDATE_CLAIM_SCRIPT,
|
UPDATE_CLAIM_SCRIPT,
|
||||||
SELL_CLAIM, SELL_SCRIPT,
|
SELL_CLAIM, SELL_SCRIPT,
|
||||||
|
@ -527,6 +540,16 @@ class OutputScript(Script):
|
||||||
'pubkey_hash': pubkey_hash
|
'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
|
@classmethod
|
||||||
def sell_script(cls, price):
|
def sell_script(cls, price):
|
||||||
return cls(template=cls.SELL_SCRIPT, values={
|
return cls(template=cls.SELL_SCRIPT, values={
|
||||||
|
@ -575,6 +598,10 @@ class OutputScript(Script):
|
||||||
def is_support_claim(self):
|
def is_support_claim(self):
|
||||||
return self.template.name.startswith('support_claim+')
|
return self.template.name.startswith('support_claim+')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_support_claim_data(self):
|
||||||
|
return self.template.name.startswith('support_claim+data+')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_sell_claim(self):
|
def is_sell_claim(self):
|
||||||
return self.template.name.startswith('sell_claim+')
|
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.crypto.base58 import Base58
|
||||||
from lbry.schema.url import normalize_name
|
from lbry.schema.url import normalize_name
|
||||||
from lbry.schema.claim import Claim
|
from lbry.schema.claim import Claim
|
||||||
|
from lbry.schema.base import Signable
|
||||||
from lbry.schema.purchase import Purchase
|
from lbry.schema.purchase import Purchase
|
||||||
|
from lbry.schema.support import Support
|
||||||
|
|
||||||
from .script import InputScript, OutputScript
|
from .script import InputScript, OutputScript
|
||||||
from .constants import COIN, NULL_HASH32
|
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',
|
'amount', 'script', 'is_internal_transfer', 'is_spent', 'is_my_output', 'is_my_input',
|
||||||
'channel', 'private_key', 'meta', 'sent_supports', 'sent_tips', 'received_tips',
|
'channel', 'private_key', 'meta', 'sent_supports', 'sent_tips', 'received_tips',
|
||||||
'purchase', 'purchased_claim', 'purchase_receipt',
|
'purchase', 'purchased_claim', 'purchase_receipt',
|
||||||
'reposted_claim', 'claims',
|
'reposted_claim', 'claims', '_signable'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, amount: int, script: OutputScript,
|
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.purchase_receipt: 'Output' = None # txo representing purchase receipt for this claim
|
||||||
self.reposted_claim: 'Output' = None # txo representing claim being reposted
|
self.reposted_claim: 'Output' = None # txo representing claim being reposted
|
||||||
self.claims: List['Output'] = None # resolved claims for collection
|
self.claims: List['Output'] = None # resolved claims for collection
|
||||||
|
self._signable: Optional[Signable] = None
|
||||||
self.meta = {}
|
self.meta = {}
|
||||||
|
|
||||||
def update_annotations(self, annotated: 'Output'):
|
def update_annotations(self, annotated: 'Output'):
|
||||||
|
@ -312,6 +315,10 @@ class Output(InputOutput):
|
||||||
def is_support(self) -> bool:
|
def is_support(self) -> bool:
|
||||||
return self.script.is_support_claim
|
return self.script.is_support_claim
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_support_data(self) -> bool:
|
||||||
|
return self.script.is_support_claim_data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def claim_hash(self) -> bytes:
|
def claim_hash(self) -> bytes:
|
||||||
if self.script.is_claim_name:
|
if self.script.is_claim_name:
|
||||||
|
@ -347,9 +354,33 @@ class Output(InputOutput):
|
||||||
def can_decode_claim(self):
|
def can_decode_claim(self):
|
||||||
try:
|
try:
|
||||||
return self.claim
|
return self.claim
|
||||||
except: # pylint: disable=bare-except
|
except Exception:
|
||||||
return False
|
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
|
@property
|
||||||
def permanent_url(self) -> str:
|
def permanent_url(self) -> str:
|
||||||
if self.script.is_claim_involved:
|
if self.script.is_claim_involved:
|
||||||
|
@ -361,22 +392,22 @@ class Output(InputOutput):
|
||||||
return self.private_key is not None
|
return self.private_key is not None
|
||||||
|
|
||||||
def get_signature_digest(self, ledger):
|
def get_signature_digest(self, ledger):
|
||||||
if self.claim.unsigned_payload:
|
if self.signable.unsigned_payload:
|
||||||
pieces = [
|
pieces = [
|
||||||
Base58.decode(self.get_address(ledger)),
|
Base58.decode(self.get_address(ledger)),
|
||||||
self.claim.unsigned_payload,
|
self.signable.unsigned_payload,
|
||||||
self.claim.signing_channel_hash[::-1]
|
self.signable.signing_channel_hash[::-1]
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
pieces = [
|
pieces = [
|
||||||
self.tx_ref.tx.inputs[0].txo_ref.hash,
|
self.tx_ref.tx.inputs[0].txo_ref.hash,
|
||||||
self.claim.signing_channel_hash,
|
self.signable.signing_channel_hash,
|
||||||
self.claim.to_message_bytes()
|
self.signable.to_message_bytes()
|
||||||
]
|
]
|
||||||
return sha256(b''.join(pieces))
|
return sha256(b''.join(pieces))
|
||||||
|
|
||||||
def get_encoded_signature(self):
|
def get_encoded_signature(self):
|
||||||
signature = hexlify(self.claim.signature)
|
signature = hexlify(self.signable.signature)
|
||||||
r = int(signature[:int(len(signature)/2)], 16)
|
r = int(signature[:int(len(signature)/2)], 16)
|
||||||
s = 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)
|
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):
|
def sign(self, channel: 'Output', first_input_id=None):
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.claim.signing_channel_hash = channel.claim_hash
|
self.signable.signing_channel_hash = channel.claim_hash
|
||||||
digest = sha256(b''.join([
|
digest = sha256(b''.join([
|
||||||
first_input_id or self.tx_ref.tx.inputs[0].txo_ref.hash,
|
first_input_id or self.tx_ref.tx.inputs[0].txo_ref.hash,
|
||||||
self.claim.signing_channel_hash,
|
self.signable.signing_channel_hash,
|
||||||
self.claim.to_message_bytes()
|
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()
|
self.script.generate()
|
||||||
|
|
||||||
def clear_signature(self):
|
def clear_signature(self):
|
||||||
self.channel = None
|
self.channel = None
|
||||||
self.claim.clear_signature()
|
self.signable.clear_signature()
|
||||||
|
|
||||||
async def generate_channel_private_key(self):
|
async def generate_channel_private_key(self):
|
||||||
self.private_key = await asyncio.get_event_loop().run_in_executor(
|
self.private_key = await asyncio.get_event_loop().run_in_executor(
|
||||||
|
@ -446,6 +477,14 @@ class Output(InputOutput):
|
||||||
)
|
)
|
||||||
return cls(amount, script)
|
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
|
@classmethod
|
||||||
def add_purchase_data(cls, purchase: Purchase) -> 'Output':
|
def add_purchase_data(cls, purchase: Purchase) -> 'Output':
|
||||||
script = OutputScript.return_data(purchase)
|
script = OutputScript.return_data(purchase)
|
||||||
|
@ -860,12 +899,20 @@ class Transaction:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def support(cls, claim_name: str, claim_id: str, amount: int, holding_address: str,
|
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)
|
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(
|
support_output = Output.pay_support_pubkey_hash(
|
||||||
amount, claim_name, claim_id, ledger.address_to_hash160(holding_address)
|
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
|
@classmethod
|
||||||
def purchase(cls, claim_id: str, amount: int, merchant_address: bytes,
|
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_tip'])
|
||||||
self.assertTrue(txs[1]['support_info'][0]['is_spent'])
|
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):
|
class CollectionCommands(CommandTestCase):
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue