forked from LBRYCommunity/lbry-sdk
balance
This commit is contained in:
parent
9f9fdd2d1a
commit
22a04fab24
4 changed files with 180 additions and 179 deletions
|
@ -2,7 +2,7 @@ import logging
|
|||
from datetime import date
|
||||
from typing import Tuple, List, Optional, Union
|
||||
|
||||
from sqlalchemy import union, func, text, between, distinct
|
||||
from sqlalchemy import union, func, text, between, distinct, case
|
||||
from sqlalchemy.future import select, Select
|
||||
|
||||
from ...blockchain.transaction import (
|
||||
|
@ -421,7 +421,7 @@ def rows_to_txos(rows: List[dict], include_tx=True) -> List[Output]:
|
|||
tx_ref=TXRefImmutable.from_hash(row['tx_hash'], row['height'], row['timestamp']),
|
||||
position=row['txo_position'],
|
||||
)
|
||||
txo.spent_height = bool(row['spent_height'])
|
||||
txo.spent_height = row['spent_height']
|
||||
if 'is_my_input' in row:
|
||||
txo.is_my_input = bool(row['is_my_input'])
|
||||
if 'is_my_output' in row:
|
||||
|
@ -534,8 +534,47 @@ def get_txo_sum(**constraints):
|
|||
return result[0]['total'] or 0
|
||||
|
||||
|
||||
def get_balance(**constraints):
|
||||
return get_txo_sum(spent_height=0, **constraints)
|
||||
def get_balance(account_ids):
|
||||
my_addresses = select(AccountAddress.c.address).where(in_account_ids(account_ids))
|
||||
query = (
|
||||
select(
|
||||
func.sum(TXO.c.amount).label("total"),
|
||||
func.sum(case(
|
||||
[(TXO.c.txo_type != TXO_TYPES["other"], TXO.c.amount)],
|
||||
else_=0
|
||||
)).label("reserved"),
|
||||
func.sum(case(
|
||||
[(where_txo_type_in(CLAIM_TYPE_CODES), TXO.c.amount)],
|
||||
else_=0
|
||||
)).label("claims"),
|
||||
func.sum(case(
|
||||
[(where_txo_type_in(TXO_TYPES["support"]), TXO.c.amount)],
|
||||
else_=0
|
||||
)).label("supports"),
|
||||
func.sum(case(
|
||||
[(where_txo_type_in(TXO_TYPES["support"]) & (
|
||||
(TXI.c.address.isnot(None)) &
|
||||
(TXI.c.address.in_(my_addresses))
|
||||
), TXO.c.amount)],
|
||||
else_=0
|
||||
)).label("my_supports"),
|
||||
)
|
||||
.where((TXO.c.spent_height == 0) & (TXO.c.address.in_(my_addresses)))
|
||||
.select_from(
|
||||
TXO.join(TXI, (TXI.c.position == 0) & (TXI.c.tx_hash == TXO.c.tx_hash), isouter=True)
|
||||
)
|
||||
)
|
||||
result = context().fetchone(query)
|
||||
return {
|
||||
"total": result["total"],
|
||||
"available": result["total"] - result["reserved"],
|
||||
"reserved": result["reserved"],
|
||||
"reserved_subtotals": {
|
||||
"claims": result["claims"],
|
||||
"supports": result["my_supports"],
|
||||
"tips": result["supports"] - result["my_supports"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_report(account_ids):
|
||||
|
|
|
@ -755,13 +755,12 @@ class API:
|
|||
async def wallet_balance(
|
||||
self,
|
||||
wallet_id: str = None, # balance for specific wallet, other than default wallet
|
||||
confirmations=0 # only include transactions with this many confirmed blocks.
|
||||
) -> dict:
|
||||
"""
|
||||
Return the balance of a wallet
|
||||
|
||||
Usage:
|
||||
wallet balance [<wallet_id>] [--confirmations=<confirmations>]
|
||||
wallet balance [<wallet_id>]
|
||||
|
||||
"""
|
||||
wallet = self.wallets.get_or_default(wallet_id)
|
||||
|
@ -911,20 +910,17 @@ class API:
|
|||
self,
|
||||
account_id: str = None, # balance for specific account, default otherwise
|
||||
wallet_id: str = None, # restrict operation to specific wallet
|
||||
confirmations=0 # required confirmations of transactions included
|
||||
) -> dict:
|
||||
"""
|
||||
Return the balance of an account
|
||||
|
||||
Usage:
|
||||
account balance [<account_id>] [--wallet_id=<wallet_id>] [--confirmations=<confirmations>]
|
||||
account balance [<account_id>] [--wallet_id=<wallet_id>]
|
||||
|
||||
"""
|
||||
wallet = self.wallets.get_or_default(wallet_id)
|
||||
account = wallet.accounts.get_or_default(account_id)
|
||||
balance = await account.get_detailed_balance(
|
||||
confirmations=confirmations, reserved_subtotals=True,
|
||||
)
|
||||
balance = await account.get_balance()
|
||||
return dict_values_to_lbc(balance)
|
||||
|
||||
async def account_add(
|
||||
|
@ -1953,10 +1949,11 @@ class API:
|
|||
holding_account = wallet.accounts.get_or_default(stream_dict.pop('account_id'))
|
||||
funding_accounts = wallet.accounts.get_or_all(tx_dict.pop('fund_account_id'))
|
||||
signing_channel = None
|
||||
if 'channel_id' in stream_dict or 'channel_name' in stream_dict:
|
||||
channel_id = stream_dict.pop('channel_id')
|
||||
channel_name = stream_dict.pop('channel_name')
|
||||
if channel_id or channel_name:
|
||||
signing_channel = await wallet.channels.get_for_signing_or_none(
|
||||
channel_id=stream_dict.pop('channel_id', None),
|
||||
channel_name=stream_dict.pop('channel_name', None)
|
||||
channel_id=channel_id, channel_name=channel_name
|
||||
)
|
||||
holding_address = await holding_account.get_valid_receiving_address(
|
||||
stream_dict.pop('claim_address', None)
|
||||
|
@ -1999,103 +1996,63 @@ class API:
|
|||
{kwargs}
|
||||
|
||||
"""
|
||||
wallet = self.wallets.get_or_default(wallet_id)
|
||||
assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
|
||||
funding_accounts = wallet.accounts.get_or_all(fund_account_id)
|
||||
if account_id:
|
||||
account = wallet.get_account_or_error(account_id)
|
||||
accounts = [account]
|
||||
else:
|
||||
account = wallet.default_account
|
||||
accounts = wallet.accounts
|
||||
stream_dict, kwargs = pop_kwargs('stream_edit', extract_stream_edit(**stream_edit_and_tx_kwargs))
|
||||
tx_dict, kwargs = pop_kwargs('tx', extract_tx(**kwargs))
|
||||
assert_consumed_kwargs(kwargs)
|
||||
wallet = self.wallets.get_or_default_for_spending(tx_dict.pop('wallet_id'))
|
||||
holding_account = wallet.accounts.get_or_default(stream_dict.pop('account_id'))
|
||||
funding_accounts = wallet.accounts.get_or_all(tx_dict.pop('fund_account_id'))
|
||||
replace = stream_dict.pop('replace')
|
||||
|
||||
existing_claims = await self.ledger.get_claims(
|
||||
wallet=wallet, accounts=accounts, claim_id=claim_id
|
||||
)
|
||||
if len(existing_claims) != 1:
|
||||
account_ids = ', '.join(f"'{account.id}'" for account in accounts)
|
||||
old = await wallet.claims.get(claim_id=claim_id)
|
||||
if not old.claim.is_stream:
|
||||
raise Exception(
|
||||
f"Can't find the stream '{claim_id}' in account(s) {account_ids}."
|
||||
)
|
||||
old_txo = existing_claims[0]
|
||||
if not old_txo.claim.is_stream:
|
||||
raise Exception(
|
||||
f"A claim with id '{claim_id}' was found but it is not a stream claim."
|
||||
f"A claim with id '{claim_id}' was found but "
|
||||
f"it is not a stream claim."
|
||||
)
|
||||
|
||||
if bid is not None:
|
||||
amount = self.get_dewies_or_error('bid', bid, positive_value=True)
|
||||
amount = self.ledger.get_dewies_or_error('bid', bid, positive_value=True)
|
||||
else:
|
||||
amount = old_txo.amount
|
||||
amount = old.amount
|
||||
|
||||
if claim_address is not None:
|
||||
self.valid_address_or_error(claim_address)
|
||||
claim_address = stream_dict.pop('claim_address')
|
||||
if claim_address:
|
||||
holding_address = await holding_account.get_valid_receiving_address(claim_address)
|
||||
else:
|
||||
claim_address = old_txo.get_address(account.ledger)
|
||||
holding_address = old.get_address(self.ledger)
|
||||
|
||||
channel = None
|
||||
signing_channel = None
|
||||
channel_id = stream_dict.pop('channel_id')
|
||||
channel_name = stream_dict.pop('channel_name')
|
||||
clear_channel = stream_dict.pop('clear_channel')
|
||||
if channel_id or channel_name:
|
||||
channel = await self.get_channel_or_error(
|
||||
wallet, channel_account_id, channel_id, channel_name, for_signing=True)
|
||||
elif old_txo.claim.is_signed and not clear_channel and not replace:
|
||||
channel = old_txo.channel
|
||||
|
||||
fee_address = self.get_fee_address(kwargs, claim_address)
|
||||
if fee_address:
|
||||
kwargs['fee_address'] = fee_address
|
||||
|
||||
file_path, spec = await self._video_file_analyzer.verify_or_repair(
|
||||
validate_file, optimize_file, file_path, ignore_non_video=True
|
||||
signing_channel = await wallet.channels.get_for_signing_or_none(
|
||||
channel_id=channel_id, channel_name=channel_name
|
||||
)
|
||||
kwargs.update(spec)
|
||||
elif old.claim.is_signed and not clear_channel and not replace:
|
||||
signing_channel = old.channel
|
||||
|
||||
if replace:
|
||||
claim = Claim()
|
||||
claim.stream.message.source.CopyFrom(
|
||||
old_txo.claim.stream.message.source
|
||||
kwargs['fee_address'] = self.ledger.get_fee_address(kwargs, holding_address)
|
||||
|
||||
stream_dict.pop('validate_file')
|
||||
stream_dict.pop('optimize_file')
|
||||
# TODO: fix
|
||||
#file_path, spec = await self._video_file_analyzer.verify_or_repair(
|
||||
# validate_file, optimize_file, file_path, ignore_non_video=True
|
||||
#)
|
||||
#kwargs.update(spec)
|
||||
class FakeManagedStream:
|
||||
sd_hash = 'beef'
|
||||
async def create_file_stream(path):
|
||||
return FakeManagedStream()
|
||||
tx, fs = await wallet.streams.update(
|
||||
old=old, amount=amount, file_path=stream_dict.pop('file_path'),
|
||||
create_file_stream=create_file_stream, replace=replace,
|
||||
holding_address=holding_address, funding_accounts=funding_accounts,
|
||||
signing_channel=signing_channel, **remove_nulls(stream_dict)
|
||||
)
|
||||
stream_type = old_txo.claim.stream.stream_type
|
||||
if stream_type:
|
||||
old_stream_type = getattr(old_txo.claim.stream.message, stream_type)
|
||||
new_stream_type = getattr(claim.stream.message, stream_type)
|
||||
new_stream_type.CopyFrom(old_stream_type)
|
||||
claim.stream.update(file_path=file_path, **kwargs)
|
||||
else:
|
||||
claim = Claim.from_bytes(old_txo.claim.to_bytes())
|
||||
claim.stream.update(file_path=file_path, **kwargs)
|
||||
tx = await Transaction.claim_update(
|
||||
old_txo, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel
|
||||
)
|
||||
new_txo = tx.outputs[0]
|
||||
|
||||
stream_hash = None
|
||||
if not preview:
|
||||
old_stream = self.stream_manager.streams.get(old_txo.claim.stream.source.sd_hash, None)
|
||||
if file_path is not None:
|
||||
if old_stream:
|
||||
await self.stream_manager.delete_stream(old_stream, delete_file=False)
|
||||
file_stream = await self.stream_manager.create_stream(file_path)
|
||||
new_txo.claim.stream.source.sd_hash = file_stream.sd_hash
|
||||
new_txo.script.generate()
|
||||
stream_hash = file_stream.stream_hash
|
||||
elif old_stream:
|
||||
stream_hash = old_stream.stream_hash
|
||||
|
||||
if channel:
|
||||
new_txo.sign(channel)
|
||||
await tx.sign(funding_accounts)
|
||||
|
||||
if not preview:
|
||||
await self.broadcast_or_release(tx, blocking)
|
||||
await self.storage.save_claims([self._old_get_temp_claim_info(
|
||||
tx, new_txo, claim_address, new_txo.claim, new_txo.claim_name, dewies_to_lbc(amount)
|
||||
)])
|
||||
if stream_hash:
|
||||
await self.storage.save_content_claim(stream_hash, new_txo.id)
|
||||
self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('publish'))
|
||||
else:
|
||||
await account.ledger.release_tx(tx)
|
||||
|
||||
await self.service.maybe_broadcast_or_release(tx, tx_dict['preview'], tx_dict['no_wait'])
|
||||
return tx
|
||||
|
||||
async def stream_abandon(
|
||||
|
@ -2403,33 +2360,21 @@ class API:
|
|||
{kwargs}
|
||||
|
||||
"""
|
||||
wallet = self.wallets.get_or_default(wallet_id)
|
||||
assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
|
||||
funding_accounts = wallet.accounts.get_or_all(fund_account_id)
|
||||
amount = self.ledger.get_dewies_or_error("amount", amount)
|
||||
claim = await self.ledger.get_claim_by_claim_id(wallet.accounts, claim_id)
|
||||
claim_address = claim.get_address(self.ledger.ledger)
|
||||
tx_dict, kwargs = pop_kwargs('tx', extract_tx(**tx_kwargs))
|
||||
assert_consumed_kwargs(kwargs)
|
||||
wallet = self.wallets.get_or_default_for_spending(tx_dict.pop('wallet_id'))
|
||||
amount = self.ledger.get_dewies_or_error('amount', amount, positive_value=True)
|
||||
funding_accounts = wallet.accounts.get_or_all(tx_dict.pop('fund_account_id'))
|
||||
claim = await wallet.claims.get(claim_id=claim_id)
|
||||
claim_address = claim.get_address(self.ledger)
|
||||
if not tip:
|
||||
account = wallet.accounts.get_or_default(account_id)
|
||||
claim_address = await account.receiving.get_or_create_usable_address()
|
||||
holding_account = wallet.accounts.get_or_default(account_id)
|
||||
claim_address = await holding_account.receiving.get_or_create_usable_address()
|
||||
|
||||
tx = await Transaction.support(
|
||||
tx = await wallet.supports.create(
|
||||
claim.claim_name, claim_id, amount, claim_address, funding_accounts, funding_accounts[0]
|
||||
)
|
||||
|
||||
if not preview:
|
||||
await self.broadcast_or_release(tx, blocking)
|
||||
await self.storage.save_supports({claim_id: [{
|
||||
'txid': tx.id,
|
||||
'nout': tx.position,
|
||||
'address': claim_address,
|
||||
'claim_id': claim_id,
|
||||
'amount': dewies_to_lbc(amount)
|
||||
}]})
|
||||
self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('new_support'))
|
||||
else:
|
||||
await self.ledger.release_tx(tx)
|
||||
|
||||
await self.service.maybe_broadcast_or_release(tx, tx_dict['preview'], tx_dict['no_wait'])
|
||||
return tx
|
||||
|
||||
async def support_list(
|
||||
|
|
|
@ -431,14 +431,6 @@ class Account:
|
|||
def get_public_key(self, chain: int, index: int) -> PubKey:
|
||||
return self.address_managers[chain].get_public_key(index)
|
||||
|
||||
def get_balance(self, confirmations=0, include_claims=False, **constraints):
|
||||
if not include_claims:
|
||||
constraints.update({'txo_type__in': (TXO_TYPES['other'], TXO_TYPES['purchase'])})
|
||||
if confirmations > 0:
|
||||
height = self.ledger.headers.height - (confirmations-1)
|
||||
constraints.update({'height__lte': height, 'height__gt': 0})
|
||||
return self.db.get_balance(account=self, **constraints)
|
||||
|
||||
async def get_max_gap(self):
|
||||
change_gap = await self.change.get_max_gap()
|
||||
receiving_gap = await self.receiving.get_max_gap()
|
||||
|
@ -497,35 +489,5 @@ class Account:
|
|||
gap_changed = True
|
||||
return gap_changed
|
||||
|
||||
def get_support_summary(self):
|
||||
return self.db.get_supports_summary(account=self)
|
||||
|
||||
async def get_detailed_balance(self, confirmations=0, reserved_subtotals=False):
|
||||
tips_balance, supports_balance, claims_balance = 0, 0, 0
|
||||
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(txo_type__in=CLAIM_TYPE_CODES)
|
||||
for txo in await self.get_support_summary():
|
||||
if confirmations > 0 and not 0 < txo.tx_ref.height <= self.ledger.headers.height - (confirmations - 1):
|
||||
continue
|
||||
if txo.is_my_input:
|
||||
supports_balance += txo.amount
|
||||
else:
|
||||
tips_balance += txo.amount
|
||||
reserved = claims_balance + supports_balance + tips_balance
|
||||
else:
|
||||
reserved = await self.get_balance(
|
||||
confirmations=confirmations, include_claims=True, txo_type__gt=0
|
||||
)
|
||||
return {
|
||||
'total': total,
|
||||
'available': total - reserved,
|
||||
'reserved': reserved,
|
||||
'reserved_subtotals': {
|
||||
'claims': claims_balance,
|
||||
'supports': supports_balance,
|
||||
'tips': tips_balance
|
||||
} if reserved_subtotals else None
|
||||
}
|
||||
async def get_balance(self, **constraints):
|
||||
return await self.db.get_balance(account=self, **constraints)
|
||||
|
|
|
@ -388,11 +388,8 @@ class Wallet:
|
|||
f"Use --allow-duplicate-name flag to override."
|
||||
)
|
||||
|
||||
async def get_balance(self):
|
||||
balance = {"total": 0}
|
||||
for account in self.accounts:
|
||||
balance["total"] += await account.get_balance()
|
||||
return balance
|
||||
async def get_balance(self, **constraints):
|
||||
return await self.db.get_balance(accounts=self.accounts, **constraints)
|
||||
|
||||
|
||||
class AccountListManager:
|
||||
|
@ -533,7 +530,7 @@ class ClaimListManager(BaseListManager):
|
|||
else:
|
||||
updated_claim.clear_signature()
|
||||
return await self.wallet.create_transaction(
|
||||
[Input.spend(previous_claim)], [updated_claim], funding_accounts, change_account, sign=False
|
||||
[Input.spend(previous_claim)], [updated_claim], funding_accounts, change_account
|
||||
)
|
||||
|
||||
async def delete(self, claim_id=None, txid=None, nout=None):
|
||||
|
@ -554,7 +551,7 @@ class ClaimListManager(BaseListManager):
|
|||
key, value, constraints = 'name', claim_name, {'claim_name': claim_name}
|
||||
else:
|
||||
raise ValueError(f"Couldn't find {self.name} because an {self.name}_id or name was not provided.")
|
||||
claims = await self.list(**constraints)
|
||||
claims = await self.list(is_spent=False, **constraints)
|
||||
if len(claims) == 1:
|
||||
return claims[0]
|
||||
elif len(claims) > 1:
|
||||
|
@ -575,7 +572,8 @@ class ChannelListManager(ClaimListManager):
|
|||
|
||||
async def create(
|
||||
self, name: str, amount: int, holding_account: Account,
|
||||
funding_accounts: List[Account], save_key=True, **kwargs) -> Transaction:
|
||||
funding_accounts: List[Account], save_key=True, **kwargs
|
||||
) -> Transaction:
|
||||
|
||||
holding_address = await holding_account.receiving.get_or_create_usable_address()
|
||||
|
||||
|
@ -602,7 +600,8 @@ class ChannelListManager(ClaimListManager):
|
|||
async def update(
|
||||
self, old: Output, amount: int, new_signing_key: bool, replace: bool,
|
||||
holding_account: Account, funding_accounts: List[Account],
|
||||
save_key=True, **kwargs) -> Transaction:
|
||||
save_key=True, **kwargs
|
||||
) -> Transaction:
|
||||
|
||||
moving_accounts = False
|
||||
holding_address = old.get_address(self.wallet.ledger)
|
||||
|
@ -668,7 +667,8 @@ class StreamListManager(ClaimListManager):
|
|||
create_file_stream: Callable[[str], Awaitable[ManagedStream]],
|
||||
holding_address: str, funding_accounts: List[Account],
|
||||
signing_channel: Optional[Output] = None,
|
||||
preview=False, **kwargs) -> Tuple[Transaction, ManagedStream]:
|
||||
preview=False, **kwargs
|
||||
) -> Tuple[Transaction, ManagedStream]:
|
||||
|
||||
claim = Claim()
|
||||
claim.stream.update(file_path=file_path, sd_hash='0' * 96, **kwargs)
|
||||
|
@ -702,6 +702,61 @@ class StreamListManager(ClaimListManager):
|
|||
|
||||
return tx, file_stream
|
||||
|
||||
async def update(
|
||||
self, old: Output, amount: int, file_path: str,
|
||||
create_file_stream: Callable[[str], Awaitable[ManagedStream]],
|
||||
holding_address: str, funding_accounts: List[Account],
|
||||
signing_channel: Optional[Output] = None,
|
||||
preview=False, replace=False, **kwargs
|
||||
) -> Tuple[Transaction, ManagedStream]:
|
||||
|
||||
if replace:
|
||||
claim = Claim()
|
||||
# stream file metadata is not replaced
|
||||
claim.stream.message.source.CopyFrom(old.claim.stream.message.source)
|
||||
stream_type = old.claim.stream.stream_type
|
||||
if stream_type:
|
||||
old_stream_type = getattr(old.claim.stream.message, stream_type)
|
||||
new_stream_type = getattr(claim.stream.message, stream_type)
|
||||
new_stream_type.CopyFrom(old_stream_type)
|
||||
else:
|
||||
claim = Claim.from_bytes(old.claim.to_bytes())
|
||||
claim.stream.update(file_path=file_path, **kwargs)
|
||||
|
||||
# before creating file stream, create TX to ensure we have enough LBC
|
||||
tx = await super().update(
|
||||
old, claim, amount, holding_address,
|
||||
funding_accounts, funding_accounts[0],
|
||||
signing_channel
|
||||
)
|
||||
txo = tx.outputs[0]
|
||||
|
||||
file_stream = None
|
||||
try:
|
||||
|
||||
# we have enough LBC to create TX, now try create the file stream
|
||||
if not preview:
|
||||
old_stream = None # TODO: get old stream
|
||||
if file_path is not None:
|
||||
if old_stream is not None:
|
||||
# TODO: delete the old stream
|
||||
pass
|
||||
file_stream = await create_file_stream(file_path)
|
||||
claim.stream.source.sd_hash = file_stream.sd_hash
|
||||
txo.script.generate()
|
||||
|
||||
# creating TX and file stream was successful, now sign all the things
|
||||
if signing_channel is not None:
|
||||
txo.sign(signing_channel)
|
||||
await self.wallet.sign(tx)
|
||||
|
||||
except Exception as e:
|
||||
# creating file stream or something else went wrong, release txos
|
||||
await self.wallet.db.release_tx(tx)
|
||||
raise e
|
||||
|
||||
return tx, file_stream
|
||||
|
||||
async def list(self, **constraints) -> Result[Output]:
|
||||
return await self.wallet.db.get_streams(wallet=self.wallet, **constraints)
|
||||
|
||||
|
|
Loading…
Reference in a new issue