diff --git a/lbry/lbry/extras/daemon/Daemon.py b/lbry/lbry/extras/daemon/Daemon.py index 5d9082509..86211b266 100644 --- a/lbry/lbry/extras/daemon/Daemon.py +++ b/lbry/lbry/extras/daemon/Daemon.py @@ -2291,15 +2291,38 @@ class Daemon(metaclass=JSONRPCServerType): channel_private_key = ecdsa.SigningKey.from_pem( data['signing_private_key'], hashfunc=hashlib.sha256 ) - account: LBCAccount = await self.ledger.get_account_for_address(data['holding_address']) - if not account: - 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) + public_key_der = channel_private_key.get_verifying_key().to_der() + + # check that the holding_address hasn't changed since the export was made + holding_address = data['holding_address'] + channels, _, _ = await self.ledger.claim_search( + public_key_id=self.ledger.public_key_to_address(public_key_der) + ) + if channels and channels[0].get_address(self.ledger) != holding_address: + 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) self.default_wallet.save() return f"Added channel signing key for {data['name']}." @@ -3751,17 +3774,19 @@ class Daemon(metaclass=JSONRPCServerType): key, value = 'name', channel_name else: 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 account.get_channels(**{f'claim_{key}': value}, limit=1) - if len(channels) == 1: - if for_signing and channels[0].private_key is None: - raise Exception(f"Couldn't find private key for {key} '{value}'. ") - return channels[0] - elif len(channels) > 1: - raise ValueError( - f"Multiple channels found with channel_{key} '{value}', " - f"pass a channel_id to narrow it down." - ) + channels = await self.ledger.get_channels( + accounts=self.get_accounts_or_all(account_ids), + **{f'claim_{key}': value} + ) + if len(channels) == 1: + if for_signing and not channels[0].has_private_key: + raise Exception(f"Couldn't find private key for {key} '{value}'. ") + return channels[0] + elif len(channels) > 1: + 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}'.") def get_account_or_default(self, account_id: str, argument_name: str = "account", lbc_only=True) -> LBCAccount: diff --git a/lbry/lbry/extras/daemon/json_response_encoder.py b/lbry/lbry/extras/daemon/json_response_encoder.py index 3283b1fb7..4e0780bb7 100644 --- a/lbry/lbry/extras/daemon/json_response_encoder.py +++ b/lbry/lbry/extras/daemon/json_response_encoder.py @@ -184,6 +184,8 @@ class JSONResponseEncoder(JSONEncoder): output['value_type'] = txo.claim.claim_type if self.include_protobuf: 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 txo.channel is not None: output['signing_channel'] = self.encode_output(txo.channel) diff --git a/lbry/lbry/wallet/account.py b/lbry/lbry/wallet/account.py index 9cd9ab0d1..b83689478 100644 --- a/lbry/lbry/wallet/account.py +++ b/lbry/lbry/wallet/account.py @@ -6,6 +6,7 @@ from string import hexdigits import ecdsa from lbry.wallet.dewies import dewies_to_lbc +from lbry.wallet.constants import TXO_TYPES from torba.client.baseaccount import BaseAccount, HierarchicalDeterministic @@ -76,7 +77,7 @@ class Account(BaseAccount): def get_balance(self, confirmations=0, include_claims=False, **constraints): 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) 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) total = await get_total_balance() 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(): if confirmations > 0 and not 0 < height <= self.ledger.headers.height - (confirmations - 1): continue @@ -96,8 +97,7 @@ class Account(BaseAccount): reserved = claims_balance + supports_balance + tips_balance else: reserved = await self.get_balance( - confirmations=confirmations, include_claims=True, - claim_type__or={'is_claim': True, 'is_support': True, 'is_update': True} + confirmations=confirmations, include_claims=True, txo_type__gt=0 ) return { 'total': dewies_to_lbc(total), diff --git a/lbry/lbry/wallet/database.py b/lbry/lbry/wallet/database.py index de720d5f4..bd26c9344 100644 --- a/lbry/lbry/wallet/database.py +++ b/lbry/lbry/wallet/database.py @@ -3,6 +3,7 @@ from typing import List from torba.client.basedatabase import BaseDatabase from lbry.wallet.transaction import Output +from lbry.wallet.constants import TXO_TYPES class WalletDatabase(BaseDatabase): @@ -17,15 +18,12 @@ class WalletDatabase(BaseDatabase): script blob not null, is_reserved boolean not null default 0, + txo_type integer not null default 0, claim_id 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 + claim_name text ); 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 = ( @@ -40,13 +38,10 @@ class WalletDatabase(BaseDatabase): def txo_to_row(self, tx, address, txo): row = super().txo_to_row(tx, address, txo) - row.update({ - 'is_claim': txo.script.is_claim_name, - 'is_update': txo.script.is_update_claim, - 'is_support': txo.script.is_support_claim, - 'is_buy': txo.script.is_buy_claim, - 'is_sell': txo.script.is_sell_claim, - }) + if txo.is_claim: + row['txo_type'] = TXO_TYPES.get(txo.claim.claim_type, 0) + elif txo.is_support: + row['txo_type'] = TXO_TYPES['support'] if txo.script.is_claim_involved: row['claim_id'] = txo.claim_id row['claim_name'] = txo.claim_name @@ -87,7 +82,9 @@ class WalletDatabase(BaseDatabase): @staticmethod 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]: self.constrain_claims(constraints) @@ -99,8 +96,7 @@ class WalletDatabase(BaseDatabase): @staticmethod def constrain_streams(constraints): - if 'claim_name' not in constraints or 'claim_id' not in constraints: - constraints['claim_name__not_like'] = '@%' + constraints['txo_type'] = TXO_TYPES['stream'] def get_streams(self, **constraints): self.constrain_streams(constraints) @@ -112,8 +108,7 @@ class WalletDatabase(BaseDatabase): @staticmethod def constrain_channels(constraints): - if 'claim_name' not in constraints or 'claim_id' not in constraints: - constraints['claim_name__like'] = '@%' + constraints['txo_type'] = TXO_TYPES['channel'] def get_channels(self, **constraints): self.constrain_channels(constraints) @@ -125,7 +120,7 @@ class WalletDatabase(BaseDatabase): @staticmethod def constrain_supports(constraints): - constraints['is_support'] = 1 + constraints['txo_type'] = TXO_TYPES['support'] def get_supports(self, **constraints): self.constrain_supports(constraints) @@ -144,12 +139,12 @@ class WalletDatabase(BaseDatabase): ) 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, (txo.txid in (select txi.txid from txi join pubkey_address a on txi.address = a.address where a.account = ?)) as from_me, (txo.address in (select address from pubkey_address where account=?)) as to_me, 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)) diff --git a/lbry/lbry/wallet/ledger.py b/lbry/lbry/wallet/ledger.py index d43da338e..082ee7e5f 100644 --- a/lbry/lbry/wallet/ledger.py +++ b/lbry/lbry/wallet/ledger.py @@ -110,7 +110,7 @@ class MainNetLedger(BaseLedger): @staticmethod 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): self.constraint_spending_utxos(constraints) diff --git a/lbry/tests/integration/test_claim_commands.py b/lbry/tests/integration/test_claim_commands.py index 3ca2a2710..4eaa2a996 100644 --- a/lbry/tests/integration/test_claim_commands.py +++ b/lbry/tests/integration/test_claim_commands.py @@ -499,44 +499,9 @@ class ChannelCommands(CommandTestCase): 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() - - # 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) - - # 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() + channels = await daemon2.jsonrpc_channel_list() self.assertEqual(1, len(channels)) 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] ) + # 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): 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)) diff --git a/torba/torba/client/basedatabase.py b/torba/torba/client/basedatabase.py index 42b3c0a55..a6f9b0eca 100644 --- a/torba/torba/client/basedatabase.py +++ b/torba/torba/client/basedatabase.py @@ -339,14 +339,18 @@ class BaseDatabase(SQLiteMixin): 'script': sqlite3.Binary(txo.script.source) } - async def insert_transaction(self, tx): - await self.db.execute(*self._insert_sql('tx', { + @staticmethod + def tx_to_row(tx): + return { 'txid': tx.id, 'raw': sqlite3.Binary(tx.raw), 'height': tx.height, 'position': tx.position, '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): await self.db.execute(*self._update_sql("tx", { @@ -354,13 +358,7 @@ class BaseDatabase(SQLiteMixin): }, 'txid = ?', (tx.id,))) def _transaction_io(self, conn: sqlite3.Connection, tx: BaseTransaction, address, txhash, history): - conn.execute(*self._insert_sql('tx', { - 'txid': tx.id, - 'raw': sqlite3.Binary(tx.raw), - 'height': tx.height, - 'position': tx.position, - 'is_verified': tx.is_verified - }, replace=True)) + conn.execute(*self._insert_sql('tx', self.tx_to_row(tx), replace=True)) for txo in tx.outputs: if txo.script.is_pay_pubkey_hash and txo.script.values['pubkey_hash'] == txhash: diff --git a/torba/torba/client/baseledger.py b/torba/torba/client/baseledger.py index cba9a8d9a..bebc771c0 100644 --- a/torba/torba/client/baseledger.py +++ b/torba/torba/client/baseledger.py @@ -230,6 +230,8 @@ class BaseLedger(metaclass=LedgerRegistry): return self.release_outputs([txi.txo_ref.txo for txi in tx.inputs]) def constraint_account_or_all(self, constraints): + if 'accounts' in constraints: + return account = constraints.pop('account', None) if account: constraints['accounts'] = [account]