forked from LBRYCommunity/lbry-sdk
channel certificate and channel claim can be different accounts, also JSON RPC responses now include a field showing if the channel certificate is available
This commit is contained in:
parent
daac3eed09
commit
7c22111ae0
8 changed files with 88 additions and 92 deletions
|
@ -2291,15 +2291,38 @@ class Daemon(metaclass=JSONRPCServerType):
|
||||||
channel_private_key = ecdsa.SigningKey.from_pem(
|
channel_private_key = ecdsa.SigningKey.from_pem(
|
||||||
data['signing_private_key'], hashfunc=hashlib.sha256
|
data['signing_private_key'], hashfunc=hashlib.sha256
|
||||||
)
|
)
|
||||||
account: LBCAccount = await self.ledger.get_account_for_address(data['holding_address'])
|
public_key_der = channel_private_key.get_verifying_key().to_der()
|
||||||
if not account:
|
|
||||||
account = LBCAccount.from_dict(self.ledger, self.default_wallet, {
|
# check that the holding_address hasn't changed since the export was made
|
||||||
'name': f"Holding Account For Channel {data['name']}",
|
holding_address = data['holding_address']
|
||||||
'public_key': data['holding_public_key'],
|
channels, _, _ = await self.ledger.claim_search(
|
||||||
'address_generator': {'name': 'single-address'}
|
public_key_id=self.ledger.public_key_to_address(public_key_der)
|
||||||
})
|
)
|
||||||
if self.ledger.network.is_connected:
|
if channels and channels[0].get_address(self.ledger) != holding_address:
|
||||||
await self.ledger.subscribe_account(account)
|
holding_address = channels[0].get_address(self.ledger)
|
||||||
|
|
||||||
|
account: LBCAccount = await self.ledger.get_account_for_address(holding_address)
|
||||||
|
if account:
|
||||||
|
# Case 1: channel holding address is in one of the accounts we already have
|
||||||
|
# simply add the certificate to existing account
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# Case 2: channel holding address hasn't changed and thus is in the bundled read-only account
|
||||||
|
# create a single-address holding account to manage the channel
|
||||||
|
if holding_address == data['holding_address']:
|
||||||
|
account = LBCAccount.from_dict(self.ledger, self.default_wallet, {
|
||||||
|
'name': f"Holding Account For Channel {data['name']}",
|
||||||
|
'public_key': data['holding_public_key'],
|
||||||
|
'address_generator': {'name': 'single-address'}
|
||||||
|
})
|
||||||
|
if self.ledger.network.is_connected:
|
||||||
|
await self.ledger.subscribe_account(account)
|
||||||
|
# Case 3: the holding address has changed and we can't create or find an account for it
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
"Channel owning account has changed since the channel was exported and "
|
||||||
|
"it is not an account to which you have access."
|
||||||
|
)
|
||||||
account.add_channel_private_key(channel_private_key)
|
account.add_channel_private_key(channel_private_key)
|
||||||
self.default_wallet.save()
|
self.default_wallet.save()
|
||||||
return f"Added channel signing key for {data['name']}."
|
return f"Added channel signing key for {data['name']}."
|
||||||
|
@ -3751,17 +3774,19 @@ class Daemon(metaclass=JSONRPCServerType):
|
||||||
key, value = 'name', channel_name
|
key, value = 'name', channel_name
|
||||||
else:
|
else:
|
||||||
raise ValueError("Couldn't find channel because a channel_id or channel_name was not provided.")
|
raise ValueError("Couldn't find channel because a channel_id or channel_name was not provided.")
|
||||||
for account in self.get_accounts_or_all(account_ids):
|
channels = await self.ledger.get_channels(
|
||||||
channels = await account.get_channels(**{f'claim_{key}': value}, limit=1)
|
accounts=self.get_accounts_or_all(account_ids),
|
||||||
if len(channels) == 1:
|
**{f'claim_{key}': value}
|
||||||
if for_signing and channels[0].private_key is None:
|
)
|
||||||
raise Exception(f"Couldn't find private key for {key} '{value}'. ")
|
if len(channels) == 1:
|
||||||
return channels[0]
|
if for_signing and not channels[0].has_private_key:
|
||||||
elif len(channels) > 1:
|
raise Exception(f"Couldn't find private key for {key} '{value}'. ")
|
||||||
raise ValueError(
|
return channels[0]
|
||||||
f"Multiple channels found with channel_{key} '{value}', "
|
elif len(channels) > 1:
|
||||||
f"pass a channel_id to narrow it down."
|
raise ValueError(
|
||||||
)
|
f"Multiple channels found with channel_{key} '{value}', "
|
||||||
|
f"pass a channel_id to narrow it down."
|
||||||
|
)
|
||||||
raise ValueError(f"Couldn't find channel with channel_{key} '{value}'.")
|
raise ValueError(f"Couldn't find channel with channel_{key} '{value}'.")
|
||||||
|
|
||||||
def get_account_or_default(self, account_id: str, argument_name: str = "account", lbc_only=True) -> LBCAccount:
|
def get_account_or_default(self, account_id: str, argument_name: str = "account", lbc_only=True) -> LBCAccount:
|
||||||
|
|
|
@ -184,6 +184,8 @@ class JSONResponseEncoder(JSONEncoder):
|
||||||
output['value_type'] = txo.claim.claim_type
|
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.claim.to_bytes())
|
||||||
|
if txo.claim.is_channel:
|
||||||
|
output['has_signing_key'] = txo.has_private_key
|
||||||
if check_signature and txo.claim.is_signed:
|
if check_signature and txo.claim.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)
|
||||||
|
|
|
@ -6,6 +6,7 @@ from string import hexdigits
|
||||||
|
|
||||||
import ecdsa
|
import ecdsa
|
||||||
from lbry.wallet.dewies import dewies_to_lbc
|
from lbry.wallet.dewies import dewies_to_lbc
|
||||||
|
from lbry.wallet.constants import TXO_TYPES
|
||||||
|
|
||||||
from torba.client.baseaccount import BaseAccount, HierarchicalDeterministic
|
from torba.client.baseaccount import BaseAccount, HierarchicalDeterministic
|
||||||
|
|
||||||
|
@ -76,7 +77,7 @@ class Account(BaseAccount):
|
||||||
|
|
||||||
def get_balance(self, confirmations=0, include_claims=False, **constraints):
|
def get_balance(self, confirmations=0, include_claims=False, **constraints):
|
||||||
if not include_claims:
|
if not include_claims:
|
||||||
constraints.update({'is_claim': 0, 'is_update': 0, 'is_support': 0})
|
constraints.update({'txo_type': 0})
|
||||||
return super().get_balance(confirmations, **constraints)
|
return super().get_balance(confirmations, **constraints)
|
||||||
|
|
||||||
async def get_granular_balances(self, confirmations=0, reserved_subtotals=False):
|
async def get_granular_balances(self, confirmations=0, reserved_subtotals=False):
|
||||||
|
@ -84,7 +85,7 @@ class Account(BaseAccount):
|
||||||
get_total_balance = partial(self.get_balance, confirmations=confirmations, include_claims=True)
|
get_total_balance = partial(self.get_balance, confirmations=confirmations, include_claims=True)
|
||||||
total = await get_total_balance()
|
total = await get_total_balance()
|
||||||
if reserved_subtotals:
|
if reserved_subtotals:
|
||||||
claims_balance = await get_total_balance(claim_type__or={'is_claim': True, 'is_update': True})
|
claims_balance = await get_total_balance(txo_type__in=[TXO_TYPES['stream'], TXO_TYPES['channel']])
|
||||||
for amount, spent, from_me, to_me, height in await self.get_support_summary():
|
for amount, spent, from_me, to_me, height in await self.get_support_summary():
|
||||||
if confirmations > 0 and not 0 < height <= self.ledger.headers.height - (confirmations - 1):
|
if confirmations > 0 and not 0 < height <= self.ledger.headers.height - (confirmations - 1):
|
||||||
continue
|
continue
|
||||||
|
@ -96,8 +97,7 @@ class Account(BaseAccount):
|
||||||
reserved = claims_balance + supports_balance + tips_balance
|
reserved = claims_balance + supports_balance + tips_balance
|
||||||
else:
|
else:
|
||||||
reserved = await self.get_balance(
|
reserved = await self.get_balance(
|
||||||
confirmations=confirmations, include_claims=True,
|
confirmations=confirmations, include_claims=True, txo_type__gt=0
|
||||||
claim_type__or={'is_claim': True, 'is_support': True, 'is_update': True}
|
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
'total': dewies_to_lbc(total),
|
'total': dewies_to_lbc(total),
|
||||||
|
|
|
@ -3,6 +3,7 @@ from typing import List
|
||||||
from torba.client.basedatabase import BaseDatabase
|
from torba.client.basedatabase import BaseDatabase
|
||||||
|
|
||||||
from lbry.wallet.transaction import Output
|
from lbry.wallet.transaction import Output
|
||||||
|
from lbry.wallet.constants import TXO_TYPES
|
||||||
|
|
||||||
|
|
||||||
class WalletDatabase(BaseDatabase):
|
class WalletDatabase(BaseDatabase):
|
||||||
|
@ -17,15 +18,12 @@ class WalletDatabase(BaseDatabase):
|
||||||
script blob not null,
|
script blob not null,
|
||||||
is_reserved boolean not null default 0,
|
is_reserved boolean not null default 0,
|
||||||
|
|
||||||
|
txo_type integer not null default 0,
|
||||||
claim_id text,
|
claim_id text,
|
||||||
claim_name text,
|
claim_name text
|
||||||
is_claim boolean not null default 0,
|
|
||||||
is_update boolean not null default 0,
|
|
||||||
is_support boolean not null default 0,
|
|
||||||
is_buy boolean not null default 0,
|
|
||||||
is_sell boolean not null default 0
|
|
||||||
);
|
);
|
||||||
create index if not exists txo_claim_id_idx on txo (claim_id);
|
create index if not exists txo_claim_id_idx on txo (claim_id);
|
||||||
|
create index if not exists txo_txo_type_idx on txo (txo_type);
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CREATE_TABLES_QUERY = (
|
CREATE_TABLES_QUERY = (
|
||||||
|
@ -40,13 +38,10 @@ class WalletDatabase(BaseDatabase):
|
||||||
|
|
||||||
def txo_to_row(self, tx, address, txo):
|
def txo_to_row(self, tx, address, txo):
|
||||||
row = super().txo_to_row(tx, address, txo)
|
row = super().txo_to_row(tx, address, txo)
|
||||||
row.update({
|
if txo.is_claim:
|
||||||
'is_claim': txo.script.is_claim_name,
|
row['txo_type'] = TXO_TYPES.get(txo.claim.claim_type, 0)
|
||||||
'is_update': txo.script.is_update_claim,
|
elif txo.is_support:
|
||||||
'is_support': txo.script.is_support_claim,
|
row['txo_type'] = TXO_TYPES['support']
|
||||||
'is_buy': txo.script.is_buy_claim,
|
|
||||||
'is_sell': txo.script.is_sell_claim,
|
|
||||||
})
|
|
||||||
if txo.script.is_claim_involved:
|
if txo.script.is_claim_involved:
|
||||||
row['claim_id'] = txo.claim_id
|
row['claim_id'] = txo.claim_id
|
||||||
row['claim_name'] = txo.claim_name
|
row['claim_name'] = txo.claim_name
|
||||||
|
@ -87,7 +82,9 @@ class WalletDatabase(BaseDatabase):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def constrain_claims(constraints):
|
def constrain_claims(constraints):
|
||||||
constraints['claim_type__any'] = {'is_claim': 1, 'is_update': 1}
|
constraints['txo_type__in'] = [
|
||||||
|
TXO_TYPES['stream'], TXO_TYPES['channel']
|
||||||
|
]
|
||||||
|
|
||||||
async def get_claims(self, **constraints) -> List[Output]:
|
async def get_claims(self, **constraints) -> List[Output]:
|
||||||
self.constrain_claims(constraints)
|
self.constrain_claims(constraints)
|
||||||
|
@ -99,8 +96,7 @@ class WalletDatabase(BaseDatabase):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def constrain_streams(constraints):
|
def constrain_streams(constraints):
|
||||||
if 'claim_name' not in constraints or 'claim_id' not in constraints:
|
constraints['txo_type'] = TXO_TYPES['stream']
|
||||||
constraints['claim_name__not_like'] = '@%'
|
|
||||||
|
|
||||||
def get_streams(self, **constraints):
|
def get_streams(self, **constraints):
|
||||||
self.constrain_streams(constraints)
|
self.constrain_streams(constraints)
|
||||||
|
@ -112,8 +108,7 @@ class WalletDatabase(BaseDatabase):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def constrain_channels(constraints):
|
def constrain_channels(constraints):
|
||||||
if 'claim_name' not in constraints or 'claim_id' not in constraints:
|
constraints['txo_type'] = TXO_TYPES['channel']
|
||||||
constraints['claim_name__like'] = '@%'
|
|
||||||
|
|
||||||
def get_channels(self, **constraints):
|
def get_channels(self, **constraints):
|
||||||
self.constrain_channels(constraints)
|
self.constrain_channels(constraints)
|
||||||
|
@ -125,7 +120,7 @@ class WalletDatabase(BaseDatabase):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def constrain_supports(constraints):
|
def constrain_supports(constraints):
|
||||||
constraints['is_support'] = 1
|
constraints['txo_type'] = TXO_TYPES['support']
|
||||||
|
|
||||||
def get_supports(self, **constraints):
|
def get_supports(self, **constraints):
|
||||||
self.constrain_supports(constraints)
|
self.constrain_supports(constraints)
|
||||||
|
@ -144,12 +139,12 @@ class WalletDatabase(BaseDatabase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_supports_summary(self, account_id):
|
def get_supports_summary(self, account_id):
|
||||||
return self.db.execute_fetchall("""
|
return self.db.execute_fetchall(f"""
|
||||||
select txo.amount, exists(select * from txi where txi.txoid=txo.txoid) as spent,
|
select txo.amount, exists(select * from txi where txi.txoid=txo.txoid) as spent,
|
||||||
(txo.txid in
|
(txo.txid in
|
||||||
(select txi.txid from txi join pubkey_address a on txi.address = a.address
|
(select txi.txid from txi join pubkey_address a on txi.address = a.address
|
||||||
where a.account = ?)) as from_me,
|
where a.account = ?)) as from_me,
|
||||||
(txo.address in (select address from pubkey_address where account=?)) as to_me,
|
(txo.address in (select address from pubkey_address where account=?)) as to_me,
|
||||||
tx.height
|
tx.height
|
||||||
from txo join tx using (txid) where is_support=1
|
from txo join tx using (txid) where txo_type={TXO_TYPES['support']}
|
||||||
""", (account_id, account_id))
|
""", (account_id, account_id))
|
||||||
|
|
|
@ -110,7 +110,7 @@ class MainNetLedger(BaseLedger):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def constraint_spending_utxos(constraints):
|
def constraint_spending_utxos(constraints):
|
||||||
constraints.update({'is_claim': 0, 'is_update': 0, 'is_support': 0})
|
constraints['txo_type'] = 0
|
||||||
|
|
||||||
def get_utxos(self, **constraints):
|
def get_utxos(self, **constraints):
|
||||||
self.constraint_spending_utxos(constraints)
|
self.constraint_spending_utxos(constraints)
|
||||||
|
|
|
@ -499,44 +499,9 @@ class ChannelCommands(CommandTestCase):
|
||||||
claim_id = self.get_claim_id(tx)
|
claim_id = self.get_claim_id(tx)
|
||||||
channel_private_key = (await self.account.get_channels())[0].private_key
|
channel_private_key = (await self.account.get_channels())[0].private_key
|
||||||
exported_data = await self.out(self.daemon.jsonrpc_channel_export(claim_id))
|
exported_data = await self.out(self.daemon.jsonrpc_channel_export(claim_id))
|
||||||
|
|
||||||
daemon2 = await self.add_daemon()
|
daemon2 = await self.add_daemon()
|
||||||
|
|
||||||
# before importing channel
|
|
||||||
self.assertEqual(1, len(daemon2.default_wallet.accounts))
|
|
||||||
|
|
||||||
# importing channel which will create a new single key account
|
|
||||||
await daemon2.jsonrpc_channel_import(exported_data)
|
await daemon2.jsonrpc_channel_import(exported_data)
|
||||||
|
channels = await daemon2.jsonrpc_channel_list()
|
||||||
# after import
|
|
||||||
self.assertEqual(2, len(daemon2.default_wallet.accounts))
|
|
||||||
new_account = daemon2.default_wallet.accounts[1]
|
|
||||||
await daemon2.ledger._update_tasks.done.wait()
|
|
||||||
channels = await new_account.get_channels()
|
|
||||||
self.assertEqual(1, len(channels))
|
|
||||||
self.assertEqual(channel_private_key.to_string(), channels[0].private_key.to_string())
|
|
||||||
|
|
||||||
async def test_channel_export_import_into_existing_account(self):
|
|
||||||
tx = await self.channel_create('@foo', '1.0')
|
|
||||||
claim_id = self.get_claim_id(tx)
|
|
||||||
channel_private_key = (await self.account.get_channels())[0].private_key
|
|
||||||
exported_data = await self.out(self.daemon.jsonrpc_channel_export(claim_id))
|
|
||||||
|
|
||||||
daemon2 = await self.add_daemon(seed=self.account.seed)
|
|
||||||
await daemon2.ledger._update_tasks.done.wait() # will sync channel previously created
|
|
||||||
|
|
||||||
# before importing channel key, has channel without key
|
|
||||||
self.assertEqual(1, len(daemon2.default_wallet.accounts))
|
|
||||||
channels = await daemon2.default_account.get_channels()
|
|
||||||
self.assertEqual(1, len(channels))
|
|
||||||
self.assertIsNone(channels[0].private_key)
|
|
||||||
|
|
||||||
# importing channel will add it to existing account
|
|
||||||
await daemon2.jsonrpc_channel_import(exported_data)
|
|
||||||
|
|
||||||
# after import, still just one account but with private key now
|
|
||||||
self.assertEqual(1, len(daemon2.default_wallet.accounts))
|
|
||||||
channels = await daemon2.default_account.get_channels()
|
|
||||||
self.assertEqual(1, len(channels))
|
self.assertEqual(1, len(channels))
|
||||||
self.assertEqual(channel_private_key.to_string(), channels[0].private_key.to_string())
|
self.assertEqual(channel_private_key.to_string(), channels[0].private_key.to_string())
|
||||||
|
|
||||||
|
@ -653,6 +618,15 @@ class StreamCommands(ClaimTestCase):
|
||||||
'hovercraft5', '0.1', channel_name='@baz', channel_account_id=[account1_id]
|
'hovercraft5', '0.1', channel_name='@baz', channel_account_id=[account1_id]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# signing with channel works even if channel and certificate are in different accounts
|
||||||
|
await self.channel_update(
|
||||||
|
baz_id, account_id=account2_id,
|
||||||
|
claim_address=await self.daemon.jsonrpc_address_unused(account1_id)
|
||||||
|
)
|
||||||
|
await self.stream_create(
|
||||||
|
'hovercraft5', '0.1', channel_id=baz_id
|
||||||
|
)
|
||||||
|
|
||||||
async def test_preview_works_with_signed_streams(self):
|
async def test_preview_works_with_signed_streams(self):
|
||||||
await self.out(self.channel_create('@spam', '1.0'))
|
await self.out(self.channel_create('@spam', '1.0'))
|
||||||
signed = await self.out(self.stream_create('bar', '1.0', channel_name='@spam', preview=True, confirm=False))
|
signed = await self.out(self.stream_create('bar', '1.0', channel_name='@spam', preview=True, confirm=False))
|
||||||
|
|
|
@ -339,14 +339,18 @@ class BaseDatabase(SQLiteMixin):
|
||||||
'script': sqlite3.Binary(txo.script.source)
|
'script': sqlite3.Binary(txo.script.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
async def insert_transaction(self, tx):
|
@staticmethod
|
||||||
await self.db.execute(*self._insert_sql('tx', {
|
def tx_to_row(tx):
|
||||||
|
return {
|
||||||
'txid': tx.id,
|
'txid': tx.id,
|
||||||
'raw': sqlite3.Binary(tx.raw),
|
'raw': sqlite3.Binary(tx.raw),
|
||||||
'height': tx.height,
|
'height': tx.height,
|
||||||
'position': tx.position,
|
'position': tx.position,
|
||||||
'is_verified': tx.is_verified
|
'is_verified': tx.is_verified
|
||||||
}))
|
}
|
||||||
|
|
||||||
|
async def insert_transaction(self, tx):
|
||||||
|
await self.db.execute(*self._insert_sql('tx', self.tx_to_row(tx)))
|
||||||
|
|
||||||
async def update_transaction(self, tx):
|
async def update_transaction(self, tx):
|
||||||
await self.db.execute(*self._update_sql("tx", {
|
await self.db.execute(*self._update_sql("tx", {
|
||||||
|
@ -354,13 +358,7 @@ class BaseDatabase(SQLiteMixin):
|
||||||
}, 'txid = ?', (tx.id,)))
|
}, 'txid = ?', (tx.id,)))
|
||||||
|
|
||||||
def _transaction_io(self, conn: sqlite3.Connection, tx: BaseTransaction, address, txhash, history):
|
def _transaction_io(self, conn: sqlite3.Connection, tx: BaseTransaction, address, txhash, history):
|
||||||
conn.execute(*self._insert_sql('tx', {
|
conn.execute(*self._insert_sql('tx', self.tx_to_row(tx), replace=True))
|
||||||
'txid': tx.id,
|
|
||||||
'raw': sqlite3.Binary(tx.raw),
|
|
||||||
'height': tx.height,
|
|
||||||
'position': tx.position,
|
|
||||||
'is_verified': tx.is_verified
|
|
||||||
}, replace=True))
|
|
||||||
|
|
||||||
for txo in tx.outputs:
|
for txo in tx.outputs:
|
||||||
if txo.script.is_pay_pubkey_hash and txo.script.values['pubkey_hash'] == txhash:
|
if txo.script.is_pay_pubkey_hash and txo.script.values['pubkey_hash'] == txhash:
|
||||||
|
|
|
@ -230,6 +230,8 @@ class BaseLedger(metaclass=LedgerRegistry):
|
||||||
return self.release_outputs([txi.txo_ref.txo for txi in tx.inputs])
|
return self.release_outputs([txi.txo_ref.txo for txi in tx.inputs])
|
||||||
|
|
||||||
def constraint_account_or_all(self, constraints):
|
def constraint_account_or_all(self, constraints):
|
||||||
|
if 'accounts' in constraints:
|
||||||
|
return
|
||||||
account = constraints.pop('account', None)
|
account = constraints.pop('account', None)
|
||||||
if account:
|
if account:
|
||||||
constraints['accounts'] = [account]
|
constraints['accounts'] = [account]
|
||||||
|
|
Loading…
Add table
Reference in a new issue