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:
Lex Berezhny 2019-09-13 09:16:17 -04:00
parent daac3eed09
commit 7c22111ae0
8 changed files with 88 additions and 92 deletions

View file

@ -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:

View file

@ -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)

View file

@ -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),

View file

@ -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))

View file

@ -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)

View file

@ -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))

View file

@ -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:

View file

@ -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]