forked from LBRYCommunity/lbry-sdk
Merge pull request #3575 from lbryio/spend_time_locked
added `account_deposit` command which is able to deposit time locked transaction into wallet
This commit is contained in:
commit
d0c5b32a90
7 changed files with 187 additions and 77 deletions
|
@ -28,6 +28,7 @@ from lbry.wallet import (
|
|||
from lbry.wallet.dewies import dewies_to_lbc, lbc_to_dewies, dict_values_to_lbc
|
||||
from lbry.wallet.constants import TXO_TYPES, CLAIM_TYPE_NAMES
|
||||
from lbry.wallet.bip32 import PrivateKey
|
||||
from lbry.crypto.base58 import Base58
|
||||
|
||||
from lbry import utils
|
||||
from lbry.conf import Config, Setting, NOT_SET
|
||||
|
@ -1872,6 +1873,48 @@ class Daemon(metaclass=JSONRPCServerType):
|
|||
outputs=outputs, broadcast=broadcast
|
||||
)
|
||||
|
||||
@requires("wallet")
|
||||
async def jsonrpc_account_deposit(
|
||||
self, txid, nout, redeem_script, private_key,
|
||||
to_account=None, wallet_id=None, preview=False, blocking=False
|
||||
):
|
||||
"""
|
||||
Spend a time locked transaction into your account.
|
||||
|
||||
Usage:
|
||||
account_deposit <txid> <nout> <redeem_script> <private_key>
|
||||
[<to_account> | --to_account=<to_account>]
|
||||
[--wallet_id=<wallet_id>] [--preview] [--blocking]
|
||||
|
||||
Options:
|
||||
--txid=<txid> : (str) id of the transaction
|
||||
--nout=<nout> : (int) output number in the transaction
|
||||
--redeem_script=<redeem_script> : (str) redeem script for output
|
||||
--private_key=<private_key> : (str) private key to sign transaction
|
||||
--to_account=<to_account> : (str) deposit to this account
|
||||
--wallet_id=<wallet_id> : (str) limit operation to specific wallet.
|
||||
--preview : (bool) do not broadcast the transaction
|
||||
--blocking : (bool) wait until tx has synced
|
||||
|
||||
Returns: {Transaction}
|
||||
"""
|
||||
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
|
||||
account = wallet.get_account_or_default(to_account)
|
||||
other_tx = await self.wallet_manager.get_transaction(txid)
|
||||
tx = await Transaction.spend_time_lock(
|
||||
other_tx.outputs[nout], unhexlify(redeem_script), account
|
||||
)
|
||||
pk = PrivateKey.from_bytes(
|
||||
account.ledger, Base58.decode_check(private_key)[1:-1]
|
||||
)
|
||||
await tx.sign([account], {pk.address: pk})
|
||||
if not preview:
|
||||
await self.broadcast_or_release(tx, blocking)
|
||||
self.component_manager.loop.create_task(self.analytics_manager.send_credits_sent())
|
||||
else:
|
||||
await self.ledger.release_tx(tx)
|
||||
return tx
|
||||
|
||||
@requires(WALLET_COMPONENT)
|
||||
def jsonrpc_account_send(self, amount, addresses, account_id=None, wallet_id=None, preview=False, blocking=False):
|
||||
"""
|
||||
|
|
|
@ -215,6 +215,10 @@ class PrivateKey(_KeyBase):
|
|||
private_key = cPrivateKey.from_int(key_int)
|
||||
return cls(ledger, private_key, bytes((0,)*32), 0, 0)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, ledger, key_bytes) -> 'PrivateKey':
|
||||
return cls(ledger, cPrivateKey(key_bytes), bytes((0,)*32), 0, 0)
|
||||
|
||||
@cachedproperty
|
||||
def private_key_bytes(self):
|
||||
""" Return the serialized private key (no leading zero byte). """
|
||||
|
|
|
@ -17,6 +17,7 @@ OP_HASH160 = 0xa9
|
|||
OP_EQUALVERIFY = 0x88
|
||||
OP_CHECKSIG = 0xac
|
||||
OP_CHECKMULTISIG = 0xae
|
||||
OP_CHECKLOCKTIMEVERIFY = 0xb1
|
||||
OP_EQUAL = 0x87
|
||||
OP_PUSHDATA1 = 0x4c
|
||||
OP_PUSHDATA2 = 0x4d
|
||||
|
@ -276,7 +277,7 @@ class Template:
|
|||
elif isinstance(opcode, PUSH_INTEGER):
|
||||
data = values[opcode.name]
|
||||
source.write_many(push_data(
|
||||
data.to_bytes((data.bit_length() + 7) // 8, byteorder='little')
|
||||
data.to_bytes((data.bit_length() + 8) // 8, byteorder='little', signed=True)
|
||||
))
|
||||
elif isinstance(opcode, PUSH_SUBSCRIPT):
|
||||
data = values[opcode.name]
|
||||
|
@ -357,19 +358,27 @@ class InputScript(Script):
|
|||
REDEEM_PUBKEY_HASH = Template('pubkey_hash', (
|
||||
PUSH_SINGLE('signature'), PUSH_SINGLE('pubkey')
|
||||
))
|
||||
REDEEM_SCRIPT = Template('script', (
|
||||
MULTI_SIG_SCRIPT = Template('multi_sig', (
|
||||
SMALL_INTEGER('signatures_count'), PUSH_MANY('pubkeys'), SMALL_INTEGER('pubkeys_count'),
|
||||
OP_CHECKMULTISIG
|
||||
))
|
||||
REDEEM_SCRIPT_HASH = Template('script_hash', (
|
||||
OP_0, PUSH_MANY('signatures'), PUSH_SUBSCRIPT('script', REDEEM_SCRIPT)
|
||||
REDEEM_SCRIPT_HASH_MULTI_SIG = Template('script_hash+multi_sig', (
|
||||
OP_0, PUSH_MANY('signatures'), PUSH_SUBSCRIPT('script', MULTI_SIG_SCRIPT)
|
||||
))
|
||||
TIME_LOCK_SCRIPT = Template('timelock', (
|
||||
PUSH_INTEGER('height'), OP_CHECKLOCKTIMEVERIFY, OP_DROP,
|
||||
# rest is identical to OutputScript.PAY_PUBKEY_HASH:
|
||||
OP_DUP, OP_HASH160, PUSH_SINGLE('pubkey_hash'), OP_EQUALVERIFY, OP_CHECKSIG
|
||||
))
|
||||
REDEEM_SCRIPT_HASH_TIME_LOCK = Template('script_hash+timelock', (
|
||||
PUSH_SINGLE('signature'), PUSH_SINGLE('pubkey'), PUSH_SUBSCRIPT('script', TIME_LOCK_SCRIPT)
|
||||
))
|
||||
|
||||
templates = [
|
||||
REDEEM_PUBKEY,
|
||||
REDEEM_PUBKEY_HASH,
|
||||
REDEEM_SCRIPT_HASH,
|
||||
REDEEM_SCRIPT
|
||||
REDEEM_SCRIPT_HASH_TIME_LOCK,
|
||||
REDEEM_SCRIPT_HASH_MULTI_SIG,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
|
@ -380,19 +389,37 @@ class InputScript(Script):
|
|||
})
|
||||
|
||||
@classmethod
|
||||
def redeem_script_hash(cls, signatures, pubkeys):
|
||||
return cls(template=cls.REDEEM_SCRIPT_HASH, values={
|
||||
def redeem_multi_sig_script_hash(cls, signatures, pubkeys):
|
||||
return cls(template=cls.REDEEM_SCRIPT_HASH_MULTI_SIG, values={
|
||||
'signatures': signatures,
|
||||
'script': cls.redeem_script(signatures, pubkeys)
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def redeem_script(cls, signatures, pubkeys):
|
||||
return cls(template=cls.REDEEM_SCRIPT, values={
|
||||
'script': cls(template=cls.MULTI_SIG_SCRIPT, values={
|
||||
'signatures_count': len(signatures),
|
||||
'pubkeys': pubkeys,
|
||||
'pubkeys_count': len(pubkeys)
|
||||
})
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def redeem_time_lock_script_hash(cls, signature, pubkey, height=None, pubkey_hash=None, script_source=None):
|
||||
if height and pubkey_hash:
|
||||
script = cls(template=cls.TIME_LOCK_SCRIPT, values={
|
||||
'height': height,
|
||||
'pubkey_hash': pubkey_hash
|
||||
})
|
||||
elif script_source:
|
||||
script = cls(source=script_source, template=cls.TIME_LOCK_SCRIPT)
|
||||
script.parse(script.template)
|
||||
else:
|
||||
raise ValueError("script_source or both height and pubkey_hash are required.")
|
||||
return cls(template=cls.REDEEM_SCRIPT_HASH_TIME_LOCK, values={
|
||||
'signature': signature,
|
||||
'pubkey': pubkey,
|
||||
'script': script
|
||||
})
|
||||
|
||||
@property
|
||||
def is_script_hash(self):
|
||||
return self.template.name.startswith('script_hash+')
|
||||
|
||||
|
||||
class OutputScript(Script):
|
||||
|
@ -460,21 +487,6 @@ class OutputScript(Script):
|
|||
UPDATE_CLAIM_OPCODES + PAY_SCRIPT_HASH.opcodes
|
||||
))
|
||||
|
||||
SELL_SCRIPT = Template('sell_script', (
|
||||
OP_VERIFY, OP_DROP, OP_DROP, OP_DROP, PUSH_INTEGER('price'), OP_PRICECHECK
|
||||
))
|
||||
SELL_CLAIM = Template('sell_claim+pay_script_hash', (
|
||||
OP_SELL_CLAIM, PUSH_SINGLE('claim_id'), PUSH_SUBSCRIPT('sell_script', SELL_SCRIPT),
|
||||
PUSH_SUBSCRIPT('receive_script', InputScript.REDEEM_SCRIPT), OP_2DROP, OP_2DROP
|
||||
) + PAY_SCRIPT_HASH.opcodes)
|
||||
|
||||
BUY_CLAIM = Template('buy_claim+pay_script_hash', (
|
||||
OP_BUY_CLAIM, PUSH_SINGLE('sell_id'),
|
||||
PUSH_SINGLE('claim_id'), PUSH_SINGLE('claim_version'),
|
||||
PUSH_SINGLE('owner_pubkey_hash'), PUSH_SINGLE('negotiation_signature'),
|
||||
OP_2DROP, OP_2DROP, OP_2DROP,
|
||||
) + PAY_SCRIPT_HASH.opcodes)
|
||||
|
||||
templates = [
|
||||
PAY_PUBKEY_FULL,
|
||||
PAY_PUBKEY_HASH,
|
||||
|
@ -489,8 +501,6 @@ class OutputScript(Script):
|
|||
SUPPORT_CLAIM_DATA_SCRIPT,
|
||||
UPDATE_CLAIM_PUBKEY,
|
||||
UPDATE_CLAIM_SCRIPT,
|
||||
SELL_CLAIM, SELL_SCRIPT,
|
||||
BUY_CLAIM,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
|
@ -550,30 +560,6 @@ class OutputScript(Script):
|
|||
'pubkey_hash': pubkey_hash
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def sell_script(cls, price):
|
||||
return cls(template=cls.SELL_SCRIPT, values={
|
||||
'price': price,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def sell_claim(cls, claim_id, price, signatures, pubkeys):
|
||||
return cls(template=cls.SELL_CLAIM, values={
|
||||
'claim_id': claim_id,
|
||||
'sell_script': OutputScript.sell_script(price),
|
||||
'receive_script': InputScript.redeem_script(signatures, pubkeys)
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def buy_claim(cls, sell_id, claim_id, claim_version, owner_pubkey_hash, negotiation_signature):
|
||||
return cls(template=cls.BUY_CLAIM, values={
|
||||
'sell_id': sell_id,
|
||||
'claim_id': claim_id,
|
||||
'claim_version': claim_version,
|
||||
'owner_pubkey_hash': owner_pubkey_hash,
|
||||
'negotiation_signature': negotiation_signature,
|
||||
})
|
||||
|
||||
@property
|
||||
def is_pay_pubkey_hash(self):
|
||||
return self.template.name.endswith('pay_pubkey_hash')
|
||||
|
@ -602,17 +588,6 @@ class OutputScript(Script):
|
|||
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+')
|
||||
|
||||
@property
|
||||
def is_buy_claim(self):
|
||||
return self.template.name.startswith('buy_claim+')
|
||||
|
||||
@property
|
||||
def is_claim_involved(self):
|
||||
return any((
|
||||
self.is_claim_name, self.is_support_claim, self.is_update_claim,
|
||||
self.is_sell_claim, self.is_buy_claim
|
||||
))
|
||||
return any((self.is_claim_name, self.is_support_claim, self.is_update_claim))
|
||||
|
|
|
@ -145,6 +145,14 @@ class Input(InputOutput):
|
|||
script = InputScript.redeem_pubkey_hash(cls.NULL_SIGNATURE, cls.NULL_PUBLIC_KEY)
|
||||
return cls(txo.ref, script)
|
||||
|
||||
@classmethod
|
||||
def spend_time_lock(cls, txo: 'Output', script_source: bytes) -> 'Input':
|
||||
""" Create an input to spend time lock script."""
|
||||
script = InputScript.redeem_time_lock_script_hash(
|
||||
cls.NULL_SIGNATURE, cls.NULL_PUBLIC_KEY, script_source=script_source
|
||||
)
|
||||
return cls(txo.ref, script)
|
||||
|
||||
@property
|
||||
def amount(self) -> int:
|
||||
""" Amount this input adds to the transaction. """
|
||||
|
@ -710,6 +718,9 @@ class Transaction:
|
|||
stream.write_compact_size(len(self._inputs))
|
||||
for i, txin in enumerate(self._inputs):
|
||||
if signing_input == i:
|
||||
if txin.script.is_script_hash:
|
||||
txin.serialize_to(stream, txin.script.values['script'].source)
|
||||
else:
|
||||
assert txin.txo_ref.txo is not None
|
||||
txin.serialize_to(stream, txin.txo_ref.txo.script.source)
|
||||
else:
|
||||
|
@ -854,16 +865,19 @@ class Transaction:
|
|||
def signature_hash_type(hash_type):
|
||||
return hash_type
|
||||
|
||||
async def sign(self, funding_accounts: Iterable['Account']):
|
||||
async def sign(self, funding_accounts: Iterable['Account'], extra_keys: dict = None):
|
||||
self._reset()
|
||||
ledger, wallet = self.ensure_all_have_same_ledger_and_wallet(funding_accounts)
|
||||
for i, txi in enumerate(self._inputs):
|
||||
assert txi.script is not None
|
||||
assert txi.txo_ref.txo is not None
|
||||
txo_script = txi.txo_ref.txo.script
|
||||
if txo_script.is_pay_pubkey_hash:
|
||||
address = ledger.hash160_to_address(txo_script.values['pubkey_hash'])
|
||||
if txo_script.is_pay_pubkey_hash or txo_script.is_pay_script_hash:
|
||||
if 'pubkey_hash' in txo_script.values:
|
||||
address = ledger.hash160_to_address(txo_script.values.get('pubkey_hash', ''))
|
||||
private_key = await ledger.get_private_key_for_address(wallet, address)
|
||||
else:
|
||||
private_key = next(iter(extra_keys.values()))
|
||||
assert private_key is not None, 'Cannot find private key for signing output.'
|
||||
tx = self._serialize_for_signature(i)
|
||||
txi.script.values['signature'] = \
|
||||
|
@ -937,6 +951,15 @@ class Transaction:
|
|||
data = Output.add_purchase_data(Purchase(claim_id))
|
||||
return cls.create([], [payment, data], funding_accounts, change_account)
|
||||
|
||||
@classmethod
|
||||
async def spend_time_lock(cls, time_locked_txo: Output, script: bytes, account: 'Account'):
|
||||
txi = Input.spend_time_lock(time_locked_txo, script)
|
||||
txi.sequence = 0xFFFFFFFE
|
||||
tx = await cls.create([txi], [], [account], account, sign=False)
|
||||
tx.locktime = txi.script.values['script'].values['height']
|
||||
tx._reset()
|
||||
return tx
|
||||
|
||||
@property
|
||||
def my_inputs(self):
|
||||
for txi in self.inputs:
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
from binascii import unhexlify
|
||||
from binascii import hexlify, unhexlify
|
||||
|
||||
from lbry.testcase import CommandTestCase
|
||||
from lbry.wallet.script import InputScript
|
||||
from lbry.wallet.dewies import dewies_to_lbc
|
||||
from lbry.wallet.account import DeterministicChannelKeyManager
|
||||
from lbry.crypto.hash import hash160
|
||||
from lbry.crypto.base58 import Base58
|
||||
|
||||
|
||||
def extract(d, keys):
|
||||
|
@ -289,3 +292,25 @@ class AccountManagement(CommandTestCase):
|
|||
self.assertTrue(channel2c.has_private_key)
|
||||
self.assertTrue(channel3c.has_private_key)
|
||||
|
||||
async def test_time_locked_transactions(self):
|
||||
address = await self.account.receiving.get_or_create_usable_address()
|
||||
private_key = await self.ledger.get_private_key_for_address(self.wallet, address)
|
||||
|
||||
script = InputScript(
|
||||
template=InputScript.TIME_LOCK_SCRIPT,
|
||||
values={'height': 210, 'pubkey_hash': self.ledger.address_to_hash160(address)}
|
||||
)
|
||||
script_address = self.ledger.hash160_to_script_address(hash160(script.source))
|
||||
script_source = hexlify(script.source).decode()
|
||||
|
||||
await self.assertBalance(self.account, '10.0')
|
||||
tx = await self.daemon.jsonrpc_account_send('4.0', script_address)
|
||||
await self.confirm_tx(tx.id)
|
||||
await self.generate(510)
|
||||
await self.assertBalance(self.account, '5.999877')
|
||||
tx = await self.daemon.jsonrpc_account_deposit(
|
||||
tx.id, 0, script_source,
|
||||
Base58.encode_check(self.ledger.private_key_to_wif(private_key.private_key_bytes))
|
||||
)
|
||||
await self.confirm_tx(tx.id)
|
||||
await self.assertBalance(self.account, '9.9997545')
|
||||
|
|
|
@ -130,12 +130,12 @@ class TestRedeemScriptHash(unittest.TestCase):
|
|||
|
||||
def redeem_script_hash(self, sigs, pubkeys):
|
||||
# this checks that factory function correctly sets up the script
|
||||
src1 = InputScript.redeem_script_hash(
|
||||
src1 = InputScript.redeem_multi_sig_script_hash(
|
||||
[unhexlify(sig) for sig in sigs],
|
||||
[unhexlify(pubkey) for pubkey in pubkeys]
|
||||
)
|
||||
subscript1 = src1.values['script']
|
||||
self.assertEqual(src1.template.name, 'script_hash')
|
||||
self.assertEqual(src1.template.name, 'script_hash+multi_sig')
|
||||
self.assertListEqual([hexlify(v) for v in src1.values['signatures']], sigs)
|
||||
self.assertListEqual([hexlify(p) for p in subscript1.values['pubkeys']], pubkeys)
|
||||
self.assertEqual(subscript1.values['signatures_count'], len(sigs))
|
||||
|
@ -143,7 +143,7 @@ class TestRedeemScriptHash(unittest.TestCase):
|
|||
# now we test that it will round trip
|
||||
src2 = InputScript(src1.source)
|
||||
subscript2 = src2.values['script']
|
||||
self.assertEqual(src2.template.name, 'script_hash')
|
||||
self.assertEqual(src2.template.name, 'script_hash+multi_sig')
|
||||
self.assertListEqual([hexlify(v) for v in src2.values['signatures']], sigs)
|
||||
self.assertListEqual([hexlify(p) for p in subscript2.values['pubkeys']], pubkeys)
|
||||
self.assertEqual(subscript2.values['signatures_count'], len(sigs))
|
||||
|
|
|
@ -262,6 +262,46 @@ class TestTransactionSerialization(unittest.TestCase):
|
|||
tx._reset()
|
||||
self.assertEqual(tx.raw, raw)
|
||||
|
||||
def test_redeem_scripthash_transaction(self):
|
||||
raw = unhexlify(
|
||||
"0200000001409223c2405238fdc516d4f2e8aa57637ce52d3b1ac42b26f1accdcda9697e79010000008a4"
|
||||
"730440220033d5286f161da717d9d1bc3c2bc28da7636b38fc0c6aefb1e0864212f05282c02205df3ce13"
|
||||
"5e79c76d44489212f77ad4e3a838562e601e6377704fa6206a6ae44f012102261773e7eebe9da80a5653d"
|
||||
"865cc600362f8e7b2b598661139dd902b5b01ea101f03aaf30ab17576a914a3328f18ac1892a6667f713d"
|
||||
"7020ff3437d973c888acfeffffff0180ed3e17000000001976a914353352b7ce1e3c9c05ffcd6ae97609d"
|
||||
"e2999744488accdf50a00"
|
||||
)
|
||||
tx = Transaction(raw)
|
||||
self.assertEqual(tx.id, 'e466881128889d1cc4110627753051c22e72a81d11229a1a1337da06940bebcf')
|
||||
self.assertEqual(tx.version, 2)
|
||||
self.assertEqual(tx.locktime, 718285,)
|
||||
self.assertEqual(len(tx.inputs), 1)
|
||||
self.assertEqual(len(tx.outputs), 1)
|
||||
|
||||
txin = tx.inputs[0]
|
||||
self.assertEqual(
|
||||
txin.txo_ref.id,
|
||||
'797e69a9cdcdacf1262bc41a3b2de57c6357aae8f2d416c5fd385240c2239240:1'
|
||||
)
|
||||
self.assertEqual(txin.txo_ref.position, 1)
|
||||
self.assertEqual(txin.sequence, 4294967294)
|
||||
self.assertIsNone(txin.coinbase)
|
||||
self.assertEqual(txin.script.template.name, 'script_hash+timelock')
|
||||
self.assertEqual(
|
||||
hexlify(txin.script.values['signature']),
|
||||
b'30440220033d5286f161da717d9d1bc3c2bc28da7636b38fc0c6aefb1e0864212f'
|
||||
b'05282c02205df3ce135e79c76d44489212f77ad4e3a838562e601e6377704fa620'
|
||||
b'6a6ae44f01'
|
||||
)
|
||||
self.assertEqual(
|
||||
hexlify(txin.script.values['pubkey']),
|
||||
b'02261773e7eebe9da80a5653d865cc600362f8e7b2b598661139dd902b5b01ea10'
|
||||
)
|
||||
script = txin.script.values['script']
|
||||
self.assertEqual(script.template.name, 'timelock')
|
||||
self.assertEqual(script.values['height'], 717738)
|
||||
self.assertEqual(hexlify(script.values['pubkey_hash']), b'a3328f18ac1892a6667f713d7020ff3437d973c8')
|
||||
|
||||
|
||||
class TestTransactionSigning(AsyncioTestCase):
|
||||
|
||||
|
|
Loading…
Reference in a new issue