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(
|
||||
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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in a new issue