cic-eth: Complex balance
This commit is contained in:
parent
c6bcda8832
commit
ab7b5fbeb9
@ -30,7 +30,7 @@ class Api:
|
|||||||
:param queue: Name of worker queue to submit tasks to
|
:param queue: Name of worker queue to submit tasks to
|
||||||
:type queue: str
|
:type queue: str
|
||||||
"""
|
"""
|
||||||
def __init__(self, chain_str, queue='cic-eth', callback_param=None, callback_task='cic_eth.callbacks.noop', callback_queue=None):
|
def __init__(self, chain_str, queue='cic-eth', callback_param=None, callback_task='cic_eth.callbacks.noop.noop', callback_queue=None):
|
||||||
self.chain_str = chain_str
|
self.chain_str = chain_str
|
||||||
self.chain_spec = ChainSpec.from_chain_str(chain_str)
|
self.chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||||
self.callback_param = callback_param
|
self.callback_param = callback_param
|
||||||
@ -301,13 +301,15 @@ class Api:
|
|||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
def balance(self, address, token_symbol):
|
def balance(self, address, token_symbol, include_pending=True):
|
||||||
"""Calls the provided callback with the current token balance of the given address.
|
"""Calls the provided callback with the current token balance of the given address.
|
||||||
|
|
||||||
:param address: Ethereum address of holder
|
:param address: Ethereum address of holder
|
||||||
:type address: str, 0x-hex
|
:type address: str, 0x-hex
|
||||||
:param token_symbol: ERC20 token symbol of token to send
|
:param token_symbol: ERC20 token symbol of token to send
|
||||||
:type token_symbol: str
|
:type token_symbol: str
|
||||||
|
:param include_pending: If set, will include transactions that have not yet been fully processed
|
||||||
|
:type include_pending: bool
|
||||||
:returns: uuid of root task
|
:returns: uuid of root task
|
||||||
:rtype: celery.Task
|
:rtype: celery.Task
|
||||||
"""
|
"""
|
||||||
@ -330,14 +332,45 @@ class Api:
|
|||||||
],
|
],
|
||||||
queue=self.queue,
|
queue=self.queue,
|
||||||
)
|
)
|
||||||
|
s_result = celery.signature(
|
||||||
|
'cic_eth.queue.balance.assemble_balances',
|
||||||
|
[],
|
||||||
|
queue=self.queue,
|
||||||
|
)
|
||||||
|
|
||||||
|
last_in_chain = s_balance
|
||||||
|
if include_pending:
|
||||||
|
s_balance_incoming = celery.signature(
|
||||||
|
'cic_eth.queue.balance.balance_incoming',
|
||||||
|
[
|
||||||
|
address,
|
||||||
|
self.chain_str,
|
||||||
|
],
|
||||||
|
queue=self.queue,
|
||||||
|
)
|
||||||
|
s_balance_outgoing = celery.signature(
|
||||||
|
'cic_eth.queue.balance.balance_outgoing',
|
||||||
|
[
|
||||||
|
address,
|
||||||
|
self.chain_str,
|
||||||
|
],
|
||||||
|
queue=self.queue,
|
||||||
|
)
|
||||||
|
s_balance.link(s_balance_incoming)
|
||||||
|
s_balance_incoming.link(s_balance_outgoing)
|
||||||
|
last_in_chain = s_balance_outgoing
|
||||||
|
|
||||||
|
one = celery.chain(s_tokens, s_balance)
|
||||||
|
two = celery.chain(s_tokens, s_balance_incoming)
|
||||||
|
three = celery.chain(s_tokens, s_balance_outgoing)
|
||||||
|
|
||||||
|
t = None
|
||||||
if self.callback_param != None:
|
if self.callback_param != None:
|
||||||
s_balance.link(self.callback_success)
|
s_result.link(self.callback_success).on_error(self.callback_error)
|
||||||
s_tokens.link(s_balance).on_error(self.callback_error)
|
t = celery.chord([one, two, three])(s_result)
|
||||||
else:
|
else:
|
||||||
s_tokens.link(s_balance)
|
t = celery.chord([one, two, three])(s_result)
|
||||||
|
|
||||||
t = s_tokens.apply_async(queue=self.queue)
|
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,4 +18,4 @@ def noop(self, result, param, status_code):
|
|||||||
:rtype: bool
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
logg.info('noop callback {} {} {}'.format(result, param, status_code))
|
logg.info('noop callback {} {} {}'.format(result, param, status_code))
|
||||||
return True
|
return result
|
||||||
|
@ -4,20 +4,23 @@ import enum
|
|||||||
|
|
||||||
@enum.unique
|
@enum.unique
|
||||||
class StatusBits(enum.IntEnum):
|
class StatusBits(enum.IntEnum):
|
||||||
QUEUED = 0x01
|
"""Individual bit flags that are combined to define the state and legacy of a queued transaction
|
||||||
IN_NETWORK = 0x08
|
|
||||||
|
|
||||||
DEFERRED = 0x10
|
"""
|
||||||
GAS_ISSUES = 0x20
|
QUEUED = 0x01 # transaction should be sent to network
|
||||||
|
IN_NETWORK = 0x08 # transaction is in network
|
||||||
|
|
||||||
LOCAL_ERROR = 0x100
|
DEFERRED = 0x10 # an attempt to send the transaction to network has failed
|
||||||
NODE_ERROR = 0x200
|
GAS_ISSUES = 0x20 # transaction is pending sender account gas funding
|
||||||
NETWORK_ERROR = 0x400
|
|
||||||
UNKNOWN_ERROR = 0x800
|
|
||||||
|
|
||||||
FINAL = 0x1000
|
LOCAL_ERROR = 0x100 # errors that originate internally from the component
|
||||||
OBSOLETE = 0x2000
|
NODE_ERROR = 0x200 # errors originating in the node (invalid RLP input...)
|
||||||
MANUAL = 0x8000
|
NETWORK_ERROR = 0x400 # errors that originate from the network (REVERT)
|
||||||
|
UNKNOWN_ERROR = 0x800 # unclassified errors (the should not occur)
|
||||||
|
|
||||||
|
FINAL = 0x1000 # transaction processing has completed
|
||||||
|
OBSOLETE = 0x2000 # transaction has been replaced by a different transaction with higher fee
|
||||||
|
MANUAL = 0x8000 # transaction processing has been manually overridden
|
||||||
|
|
||||||
|
|
||||||
@enum.unique
|
@enum.unique
|
||||||
@ -79,6 +82,19 @@ class LockEnum(enum.IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
def status_str(v, bits_only=False):
|
def status_str(v, bits_only=False):
|
||||||
|
"""Render a human-readable string describing the status
|
||||||
|
|
||||||
|
If the bit field exactly matches a StatusEnum value, the StatusEnum label will be returned.
|
||||||
|
|
||||||
|
If a StatusEnum cannot be matched, the string will be postfixed with "*", unless explicitly instructed to return bit field labels only.
|
||||||
|
|
||||||
|
:param v: Status bit field
|
||||||
|
:type v: number
|
||||||
|
:param bits_only: Only render individual bit labels.
|
||||||
|
:type bits_only: bool
|
||||||
|
:returns: Status string
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
s = ''
|
s = ''
|
||||||
if not bits_only:
|
if not bits_only:
|
||||||
try:
|
try:
|
||||||
@ -100,14 +116,39 @@ def status_str(v, bits_only=False):
|
|||||||
|
|
||||||
|
|
||||||
def all_errors():
|
def all_errors():
|
||||||
|
"""Bit mask of all error states
|
||||||
|
|
||||||
|
:returns: Error flags
|
||||||
|
:rtype: number
|
||||||
|
"""
|
||||||
return StatusBits.LOCAL_ERROR | StatusBits.NODE_ERROR | StatusBits.NETWORK_ERROR | StatusBits.UNKNOWN_ERROR
|
return StatusBits.LOCAL_ERROR | StatusBits.NODE_ERROR | StatusBits.NETWORK_ERROR | StatusBits.UNKNOWN_ERROR
|
||||||
|
|
||||||
|
|
||||||
def is_error_status(v):
|
def is_error_status(v):
|
||||||
|
"""Check if value is an error state
|
||||||
|
|
||||||
|
:param v: Status bit field
|
||||||
|
:type v: number
|
||||||
|
:returns: True if error
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
return bool(v & all_errors())
|
return bool(v & all_errors())
|
||||||
|
|
||||||
|
|
||||||
|
def dead():
|
||||||
|
"""Bit mask defining whether a transaction is still likely to be processed on the network.
|
||||||
|
|
||||||
|
:returns: Bit mask
|
||||||
|
:rtype: number
|
||||||
|
"""
|
||||||
|
return StatusBits.FINAL | StatusBits.OBSOLETE
|
||||||
|
|
||||||
|
|
||||||
def is_alive(v):
|
def is_alive(v):
|
||||||
return bool(v & (StatusBits.FINAL | StatusBits.OBSOLETE) == 0)
|
"""Check if transaction is still likely to be processed on the network.
|
||||||
|
|
||||||
|
The contingency of "likely" refers to the case a transaction has been obsoleted after sent to the network, but the network still confirms the obsoleted transaction. The return value of this method will not change as a result of this, BUT the state itself will (as the FINAL bit will be set).
|
||||||
|
|
||||||
|
:returns:
|
||||||
|
"""
|
||||||
|
return bool(v & dead() == 0)
|
||||||
|
@ -287,7 +287,6 @@ class Otx(SessionBase):
|
|||||||
|
|
||||||
self.__set_status(StatusBits.IN_NETWORK, session)
|
self.__set_status(StatusBits.IN_NETWORK, session)
|
||||||
self.__reset_status(StatusBits.DEFERRED | StatusBits.QUEUED | StatusBits.LOCAL_ERROR | StatusBits.NODE_ERROR, session)
|
self.__reset_status(StatusBits.DEFERRED | StatusBits.QUEUED | StatusBits.LOCAL_ERROR | StatusBits.NODE_ERROR, session)
|
||||||
logg.debug('<<< status {}'.format(status_str(self.status)))
|
|
||||||
|
|
||||||
if self.tracing:
|
if self.tracing:
|
||||||
self.__state_log(session=session)
|
self.__state_log(session=session)
|
||||||
|
@ -187,7 +187,6 @@ def balance(tokens, holder_address, chain_str):
|
|||||||
"""
|
"""
|
||||||
#abi = ContractRegistry.abi('ERC20Token')
|
#abi = ContractRegistry.abi('ERC20Token')
|
||||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||||
balances = []
|
|
||||||
c = RpcClient(chain_spec)
|
c = RpcClient(chain_spec)
|
||||||
for t in tokens:
|
for t in tokens:
|
||||||
#token = CICRegistry.get_address(t['address'])
|
#token = CICRegistry.get_address(t['address'])
|
||||||
@ -195,9 +194,9 @@ def balance(tokens, holder_address, chain_str):
|
|||||||
#o = c.w3.eth.contract(abi=abi, address=t['address'])
|
#o = c.w3.eth.contract(abi=abi, address=t['address'])
|
||||||
o = CICRegistry.get_address(chain_spec, t['address']).contract
|
o = CICRegistry.get_address(chain_spec, t['address']).contract
|
||||||
b = o.functions.balanceOf(holder_address).call()
|
b = o.functions.balanceOf(holder_address).call()
|
||||||
logg.debug('balance {} for {}: {}'.format(t['address'], holder_address, b))
|
t['balance_network'] = b
|
||||||
balances.append(b)
|
|
||||||
return b
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task(bind=True)
|
@celery_app.task(bind=True)
|
||||||
@ -326,7 +325,7 @@ def resolve_tokens_by_symbol(token_symbols, chain_str):
|
|||||||
token = CICRegistry.get_token(chain_spec, token_symbol)
|
token = CICRegistry.get_token(chain_spec, token_symbol)
|
||||||
tokens.append({
|
tokens.append({
|
||||||
'address': token.address(),
|
'address': token.address(),
|
||||||
#'converters': [],
|
'converters': [],
|
||||||
})
|
})
|
||||||
return tokens
|
return tokens
|
||||||
|
|
||||||
|
@ -344,7 +344,6 @@ def send(self, txs, chain_str):
|
|||||||
|
|
||||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||||
|
|
||||||
|
|
||||||
tx_hex = txs[0]
|
tx_hex = txs[0]
|
||||||
logg.debug('send transaction {}'.format(tx_hex))
|
logg.debug('send transaction {}'.format(tx_hex))
|
||||||
|
|
||||||
|
120
apps/cic-eth/cic_eth/queue/balance.py
Normal file
120
apps/cic-eth/cic_eth/queue/balance.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# standard imports
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# third-party imports
|
||||||
|
import celery
|
||||||
|
from hexathon import strip_0x
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from cic_registry.chain import ChainSpec
|
||||||
|
from cic_eth.db import SessionBase
|
||||||
|
from cic_eth.db.models.otx import Otx
|
||||||
|
from cic_eth.db.models.tx import TxCache
|
||||||
|
from cic_eth.db.enum import (
|
||||||
|
StatusBits,
|
||||||
|
dead,
|
||||||
|
)
|
||||||
|
|
||||||
|
celery_app = celery.current_app
|
||||||
|
|
||||||
|
logg = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
def __balance_outgoing_compatible(token_address, holder_address, chain_str):
|
||||||
|
session = SessionBase.create_session()
|
||||||
|
q = session.query(TxCache.from_value)
|
||||||
|
q = q.join(Otx)
|
||||||
|
q = q.filter(TxCache.sender==holder_address)
|
||||||
|
status_compare = dead()
|
||||||
|
q = q.filter(Otx.status.op('&')(status_compare)==0)
|
||||||
|
q = q.filter(TxCache.source_token_address==token_address)
|
||||||
|
delta = 0
|
||||||
|
for r in q.all():
|
||||||
|
delta += int(r[0])
|
||||||
|
session.close()
|
||||||
|
return delta
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task()
|
||||||
|
def balance_outgoing(tokens, holder_address, chain_str):
|
||||||
|
"""Retrieve accumulated value of unprocessed transactions sent from the given address.
|
||||||
|
|
||||||
|
:param tokens: list of token spec dicts with addresses to retrieve balances for
|
||||||
|
:type tokens: list of str, 0x-hex
|
||||||
|
:param holder_address: Sender address
|
||||||
|
:type holder_address: str, 0x-hex
|
||||||
|
:param chain_str: Chain spec string representation
|
||||||
|
:type chain_str: str
|
||||||
|
:returns: Tokens dicts with outgoing balance added
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||||
|
for t in tokens:
|
||||||
|
b = __balance_outgoing_compatible(t['address'], holder_address, chain_str)
|
||||||
|
t['balance_outgoing'] = b
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
def __balance_incoming_compatible(token_address, receiver_address, chain_str):
|
||||||
|
session = SessionBase.create_session()
|
||||||
|
q = session.query(TxCache.to_value)
|
||||||
|
q = q.join(Otx)
|
||||||
|
q = q.filter(TxCache.recipient==receiver_address)
|
||||||
|
status_compare = dead()
|
||||||
|
q = q.filter(Otx.status.op('&')(status_compare)==0)
|
||||||
|
# TODO: this can change the result for the recipient if tx is later obsoleted and resubmission is delayed.
|
||||||
|
q = q.filter(Otx.status.op('&')(StatusBits.IN_NETWORK)==StatusBits.IN_NETWORK)
|
||||||
|
q = q.filter(TxCache.destination_token_address==token_address)
|
||||||
|
delta = 0
|
||||||
|
for r in q.all():
|
||||||
|
delta += int(r[0])
|
||||||
|
session.close()
|
||||||
|
return delta
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task()
|
||||||
|
def balance_incoming(tokens, receipient_address, chain_str):
|
||||||
|
"""Retrieve accumulated value of unprocessed transactions to be received by the given address.
|
||||||
|
|
||||||
|
:param tokens: list of token spec dicts with addresses to retrieve balances for
|
||||||
|
:type tokens: list of str, 0x-hex
|
||||||
|
:param holder_address: Recipient address
|
||||||
|
:type holder_address: str, 0x-hex
|
||||||
|
:param chain_str: Chain spec string representation
|
||||||
|
:type chain_str: str
|
||||||
|
:returns: Tokens dicts with outgoing balance added
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||||
|
for t in tokens:
|
||||||
|
b = __balance_incoming_compatible(t['address'], receipient_address, chain_str)
|
||||||
|
t['balance_incoming'] = b
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task()
|
||||||
|
def assemble_balances(balances_collection):
|
||||||
|
"""Combines token spec dicts with individual balances into a single token spec dict.
|
||||||
|
|
||||||
|
A "balance" means any field that is keyed with a string starting with "balance_"
|
||||||
|
|
||||||
|
:param balances_collection: Token spec dicts
|
||||||
|
:type balances_collection: list of lists of dicts
|
||||||
|
:returns: Single token spec dict per token with all balances
|
||||||
|
:rtype: list of dicts
|
||||||
|
"""
|
||||||
|
tokens = {}
|
||||||
|
for c in balances_collection:
|
||||||
|
for b in c:
|
||||||
|
address = b['address']
|
||||||
|
if tokens.get(address) == None:
|
||||||
|
tokens[address] = {
|
||||||
|
'address': address,
|
||||||
|
'converters': b['converters'],
|
||||||
|
}
|
||||||
|
for k in b.keys():
|
||||||
|
if k[:8] == 'balance_':
|
||||||
|
tokens[address][k] = b[k]
|
||||||
|
return list(tokens.values())
|
@ -5,6 +5,7 @@ import datetime
|
|||||||
|
|
||||||
# third-party imports
|
# third-party imports
|
||||||
import celery
|
import celery
|
||||||
|
from hexathon import strip_0x
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
from sqlalchemy import not_
|
from sqlalchemy import not_
|
||||||
from sqlalchemy import tuple_
|
from sqlalchemy import tuple_
|
||||||
@ -12,6 +13,7 @@ from sqlalchemy import func
|
|||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
from cic_registry import CICRegistry
|
from cic_registry import CICRegistry
|
||||||
|
from cic_registry.chain import ChainSpec
|
||||||
from cic_eth.db.models.otx import Otx
|
from cic_eth.db.models.otx import Otx
|
||||||
from cic_eth.db.models.otx import OtxStateLog
|
from cic_eth.db.models.otx import OtxStateLog
|
||||||
from cic_eth.db.models.tx import TxCache
|
from cic_eth.db.models.tx import TxCache
|
||||||
@ -22,6 +24,7 @@ from cic_eth.db.enum import (
|
|||||||
LockEnum,
|
LockEnum,
|
||||||
StatusBits,
|
StatusBits,
|
||||||
is_alive,
|
is_alive,
|
||||||
|
dead,
|
||||||
)
|
)
|
||||||
from cic_eth.eth.util import unpack_signed_raw_tx # TODO: should not be in same sub-path as package that imports queue.tx
|
from cic_eth.eth.util import unpack_signed_raw_tx # TODO: should not be in same sub-path as package that imports queue.tx
|
||||||
from cic_eth.error import NotLocalTxError
|
from cic_eth.error import NotLocalTxError
|
||||||
@ -687,5 +690,3 @@ def get_account_tx(address, as_sender=True, as_recipient=True, counterpart=None)
|
|||||||
|
|
||||||
return txs
|
return txs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ def celery_includes():
|
|||||||
'cic_eth.eth.tx',
|
'cic_eth.eth.tx',
|
||||||
'cic_eth.ext.tx',
|
'cic_eth.ext.tx',
|
||||||
'cic_eth.queue.tx',
|
'cic_eth.queue.tx',
|
||||||
|
'cic_eth.queue.balance',
|
||||||
'cic_eth.admin.ctrl',
|
'cic_eth.admin.ctrl',
|
||||||
'cic_eth.admin.nonce',
|
'cic_eth.admin.nonce',
|
||||||
'cic_eth.eth.account',
|
'cic_eth.eth.account',
|
||||||
|
@ -29,27 +29,6 @@ def test_account_api(
|
|||||||
assert t.successful()
|
assert t.successful()
|
||||||
|
|
||||||
|
|
||||||
def test_balance_api(
|
|
||||||
default_chain_spec,
|
|
||||||
default_chain_registry,
|
|
||||||
init_w3,
|
|
||||||
cic_registry,
|
|
||||||
init_database,
|
|
||||||
bancor_tokens,
|
|
||||||
bancor_registry,
|
|
||||||
celery_session_worker,
|
|
||||||
):
|
|
||||||
|
|
||||||
token = CICRegistry.get_address(default_chain_spec, bancor_tokens[0])
|
|
||||||
|
|
||||||
api = Api(str(default_chain_spec), callback_param='balance', callback_task='cic_eth.callbacks.noop.noop', queue=None)
|
|
||||||
t = api.balance(init_w3.eth.accounts[2], token.symbol())
|
|
||||||
t.get()
|
|
||||||
for r in t.collect():
|
|
||||||
print(r)
|
|
||||||
assert t.successful()
|
|
||||||
|
|
||||||
|
|
||||||
def test_transfer_api(
|
def test_transfer_api(
|
||||||
default_chain_spec,
|
default_chain_spec,
|
||||||
init_w3,
|
init_w3,
|
||||||
|
40
apps/cic-eth/tests/functional/test_balance.py
Normal file
40
apps/cic-eth/tests/functional/test_balance.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# standard imports
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
import web3
|
||||||
|
from cic_eth.api.api_task import Api
|
||||||
|
|
||||||
|
logg = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
def test_balance_complex_api(
|
||||||
|
default_chain_spec,
|
||||||
|
init_database,
|
||||||
|
init_w3,
|
||||||
|
cic_registry,
|
||||||
|
dummy_token,
|
||||||
|
dummy_token_registered,
|
||||||
|
celery_session_worker,
|
||||||
|
init_eth_tester,
|
||||||
|
):
|
||||||
|
|
||||||
|
chain_str = str(default_chain_spec)
|
||||||
|
|
||||||
|
api = Api(chain_str, queue=None, callback_param='foo')
|
||||||
|
|
||||||
|
a = web3.Web3.toChecksumAddress('0x' + os.urandom(20).hex())
|
||||||
|
t = api.balance(a, 'DUM')
|
||||||
|
t.get()
|
||||||
|
r = None
|
||||||
|
for c in t.collect():
|
||||||
|
r = c[1]
|
||||||
|
assert t.successful()
|
||||||
|
logg.debug(r)
|
||||||
|
|
||||||
|
assert r[0].get('balance_incoming') != None
|
||||||
|
assert r[0].get('balance_outgoing') != None
|
||||||
|
assert r[0].get('balance_network') != None
|
||||||
|
|
||||||
|
logg.debug('r {}'.format(r))
|
232
apps/cic-eth/tests/tasks/test_balance_complex.py
Normal file
232
apps/cic-eth/tests/tasks/test_balance_complex.py
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
# standard imports
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# third-party imports
|
||||||
|
from cic_registry import CICRegistry
|
||||||
|
import celery
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from cic_eth.eth.rpc import RpcClient
|
||||||
|
from cic_eth.db.models.otx import Otx
|
||||||
|
from cic_eth.eth.util import unpack_signed_raw_tx
|
||||||
|
|
||||||
|
#logg = logging.getLogger(__name__)
|
||||||
|
logg = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
def test_balance_complex(
|
||||||
|
default_chain_spec,
|
||||||
|
init_database,
|
||||||
|
init_w3,
|
||||||
|
cic_registry,
|
||||||
|
dummy_token_gifted,
|
||||||
|
celery_session_worker,
|
||||||
|
init_eth_tester,
|
||||||
|
):
|
||||||
|
|
||||||
|
chain_str = str(default_chain_spec)
|
||||||
|
token_data = {
|
||||||
|
'address': dummy_token_gifted,
|
||||||
|
'converters': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
tx_hashes = []
|
||||||
|
for i in range(3):
|
||||||
|
s = celery.signature(
|
||||||
|
'cic_eth.eth.token.transfer',
|
||||||
|
[
|
||||||
|
[token_data],
|
||||||
|
init_w3.eth.accounts[0],
|
||||||
|
init_w3.eth.accounts[1],
|
||||||
|
1000*(i+1),
|
||||||
|
chain_str,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
t = s.apply_async()
|
||||||
|
t.get()
|
||||||
|
r = None
|
||||||
|
for c in t.collect():
|
||||||
|
r = c[1]
|
||||||
|
assert t.successful()
|
||||||
|
tx_hashes.append(r)
|
||||||
|
|
||||||
|
otx = Otx.load(r)
|
||||||
|
|
||||||
|
s_send = celery.signature(
|
||||||
|
'cic_eth.eth.tx.send',
|
||||||
|
[
|
||||||
|
[otx.signed_tx],
|
||||||
|
chain_str,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
t = s_send.apply_async()
|
||||||
|
t.get()
|
||||||
|
for r in t.collect():
|
||||||
|
pass
|
||||||
|
assert t.successful()
|
||||||
|
init_eth_tester.mine_block()
|
||||||
|
|
||||||
|
|
||||||
|
# here insert block sync to get state of balance
|
||||||
|
|
||||||
|
s_balance_base = celery.signature(
|
||||||
|
'cic_eth.eth.token.balance',
|
||||||
|
[
|
||||||
|
[token_data],
|
||||||
|
init_w3.eth.accounts[0],
|
||||||
|
chain_str,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
s_balance_out = celery.signature(
|
||||||
|
'cic_eth.queue.balance.balance_outgoing',
|
||||||
|
[
|
||||||
|
init_w3.eth.accounts[0],
|
||||||
|
chain_str,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
s_balance_in = celery.signature(
|
||||||
|
'cic_eth.queue.balance.balance_incoming',
|
||||||
|
[
|
||||||
|
init_w3.eth.accounts[0],
|
||||||
|
chain_str,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
s_balance_out.link(s_balance_in)
|
||||||
|
s_balance_base.link(s_balance_out)
|
||||||
|
t = s_balance_base.apply_async()
|
||||||
|
t.get()
|
||||||
|
r = None
|
||||||
|
for c in t.collect():
|
||||||
|
r = c[1]
|
||||||
|
assert t.successful()
|
||||||
|
|
||||||
|
assert r[0]['balance_network'] > 0
|
||||||
|
assert r[0]['balance_incoming'] == 0
|
||||||
|
assert r[0]['balance_outgoing'] > 0
|
||||||
|
|
||||||
|
s_balance_base = celery.signature(
|
||||||
|
'cic_eth.eth.token.balance',
|
||||||
|
[
|
||||||
|
init_w3.eth.accounts[1],
|
||||||
|
chain_str,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
s_balance_out = celery.signature(
|
||||||
|
'cic_eth.queue.balance.balance_outgoing',
|
||||||
|
[
|
||||||
|
[token_data],
|
||||||
|
init_w3.eth.accounts[1],
|
||||||
|
chain_str,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
s_balance_in = celery.signature(
|
||||||
|
'cic_eth.queue.balance.balance_incoming',
|
||||||
|
[
|
||||||
|
init_w3.eth.accounts[1],
|
||||||
|
chain_str,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
s_balance_base.link(s_balance_in)
|
||||||
|
s_balance_out.link(s_balance_base)
|
||||||
|
t = s_balance_out.apply_async()
|
||||||
|
t.get()
|
||||||
|
r = None
|
||||||
|
for c in t.collect():
|
||||||
|
r = c[1]
|
||||||
|
assert t.successful()
|
||||||
|
|
||||||
|
assert r[0]['balance_network'] > 0
|
||||||
|
assert r[0]['balance_incoming'] > 0
|
||||||
|
assert r[0]['balance_outgoing'] == 0
|
||||||
|
|
||||||
|
# Set confirmed status in backend
|
||||||
|
for tx_hash in tx_hashes:
|
||||||
|
rcpt = init_w3.eth.getTransactionReceipt(tx_hash)
|
||||||
|
assert rcpt['status'] == 1
|
||||||
|
otx = Otx.load(tx_hash, session=init_database)
|
||||||
|
otx.success(block=rcpt['blockNumber'], session=init_database)
|
||||||
|
init_database.add(otx)
|
||||||
|
init_database.commit()
|
||||||
|
|
||||||
|
|
||||||
|
s_balance_base = celery.signature(
|
||||||
|
'cic_eth.eth.token.balance',
|
||||||
|
[
|
||||||
|
init_w3.eth.accounts[1],
|
||||||
|
chain_str,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
s_balance_out = celery.signature(
|
||||||
|
'cic_eth.queue.balance.balance_outgoing',
|
||||||
|
[
|
||||||
|
[token_data],
|
||||||
|
init_w3.eth.accounts[1],
|
||||||
|
chain_str,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
s_balance_in = celery.signature(
|
||||||
|
'cic_eth.queue.balance.balance_incoming',
|
||||||
|
[
|
||||||
|
init_w3.eth.accounts[1],
|
||||||
|
chain_str,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
s_balance_base.link(s_balance_in)
|
||||||
|
s_balance_out.link(s_balance_base)
|
||||||
|
t = s_balance_out.apply_async()
|
||||||
|
t.get()
|
||||||
|
r = None
|
||||||
|
for c in t.collect():
|
||||||
|
r = c[1]
|
||||||
|
assert t.successful()
|
||||||
|
assert r[0]['balance_network'] > 0
|
||||||
|
assert r[0]['balance_incoming'] == 0
|
||||||
|
assert r[0]['balance_outgoing'] == 0
|
||||||
|
|
||||||
|
|
||||||
|
s_balance_base = celery.signature(
|
||||||
|
'cic_eth.eth.token.balance',
|
||||||
|
[
|
||||||
|
init_w3.eth.accounts[0],
|
||||||
|
chain_str,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
s_balance_out = celery.signature(
|
||||||
|
'cic_eth.queue.balance.balance_outgoing',
|
||||||
|
[
|
||||||
|
[token_data],
|
||||||
|
init_w3.eth.accounts[0],
|
||||||
|
chain_str,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
s_balance_in = celery.signature(
|
||||||
|
'cic_eth.queue.balance.balance_incoming',
|
||||||
|
[
|
||||||
|
init_w3.eth.accounts[0],
|
||||||
|
chain_str,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
s_balance_base.link(s_balance_in)
|
||||||
|
s_balance_out.link(s_balance_base)
|
||||||
|
t = s_balance_out.apply_async()
|
||||||
|
t.get()
|
||||||
|
r = None
|
||||||
|
for c in t.collect():
|
||||||
|
r = c[1]
|
||||||
|
assert t.successful()
|
||||||
|
assert r[0]['balance_network'] > 0
|
||||||
|
assert r[0]['balance_incoming'] == 0
|
||||||
|
assert r[0]['balance_outgoing'] == 0
|
||||||
|
|
||||||
|
|
158
apps/cic-eth/tests/unit/queue/test_balances.py
Normal file
158
apps/cic-eth/tests/unit/queue/test_balances.py
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
# standard imports
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# third-party imports
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from cic_eth.db.models.otx import Otx
|
||||||
|
from cic_eth.db.models.tx import TxCache
|
||||||
|
from cic_eth.queue.balance import (
|
||||||
|
balance_outgoing,
|
||||||
|
balance_incoming,
|
||||||
|
assemble_balances,
|
||||||
|
)
|
||||||
|
|
||||||
|
logg = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
def test_assemble():
|
||||||
|
|
||||||
|
token_foo = '0x' + os.urandom(20).hex()
|
||||||
|
token_bar = '0x' + os.urandom(20).hex()
|
||||||
|
b = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'address': token_foo,
|
||||||
|
'converters': [],
|
||||||
|
'balance_foo': 42,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'address': token_bar,
|
||||||
|
'converters': [],
|
||||||
|
'balance_baz': 666,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'address': token_foo,
|
||||||
|
'converters': [],
|
||||||
|
'balance_bar': 13,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
'address': token_bar,
|
||||||
|
'converters': [],
|
||||||
|
'balance_xyzzy': 1337,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
r = assemble_balances(b)
|
||||||
|
logg.debug('r {}'.format(r))
|
||||||
|
|
||||||
|
assert r[0]['address'] == token_foo
|
||||||
|
assert r[1]['address'] == token_bar
|
||||||
|
assert r[0].get('balance_foo') != None
|
||||||
|
assert r[0].get('balance_bar') != None
|
||||||
|
assert r[1].get('balance_baz') != None
|
||||||
|
assert r[1].get('balance_xyzzy') != None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_outgoing_balance(
|
||||||
|
default_chain_spec,
|
||||||
|
init_database,
|
||||||
|
):
|
||||||
|
|
||||||
|
chain_str = str(default_chain_spec)
|
||||||
|
recipient = '0x' + os.urandom(20).hex()
|
||||||
|
tx_hash = '0x' + os.urandom(32).hex()
|
||||||
|
signed_tx = '0x' + os.urandom(128).hex()
|
||||||
|
otx = Otx.add(0, recipient, tx_hash, signed_tx, session=init_database)
|
||||||
|
init_database.add(otx)
|
||||||
|
init_database.commit()
|
||||||
|
|
||||||
|
token_address = '0x' + os.urandom(20).hex()
|
||||||
|
sender = '0x' + os.urandom(20).hex()
|
||||||
|
txc = TxCache(
|
||||||
|
tx_hash,
|
||||||
|
sender,
|
||||||
|
recipient,
|
||||||
|
token_address,
|
||||||
|
token_address,
|
||||||
|
1000,
|
||||||
|
1000,
|
||||||
|
)
|
||||||
|
init_database.add(txc)
|
||||||
|
init_database.commit()
|
||||||
|
|
||||||
|
token_data = {
|
||||||
|
'address': token_address,
|
||||||
|
'converters': [],
|
||||||
|
}
|
||||||
|
b = balance_outgoing([token_data], sender, chain_str)
|
||||||
|
assert b[0]['balance_outgoing'] == 1000
|
||||||
|
|
||||||
|
otx.sent(session=init_database)
|
||||||
|
init_database.commit()
|
||||||
|
|
||||||
|
b = balance_outgoing([token_data], sender, chain_str)
|
||||||
|
assert b[0]['balance_outgoing'] == 1000
|
||||||
|
|
||||||
|
otx.success(block=1024, session=init_database)
|
||||||
|
init_database.commit()
|
||||||
|
|
||||||
|
b = balance_outgoing([token_data], sender, chain_str)
|
||||||
|
assert b[0]['balance_outgoing'] == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip()
|
||||||
|
def test_incoming_balance(
|
||||||
|
default_chain_spec,
|
||||||
|
init_database,
|
||||||
|
):
|
||||||
|
|
||||||
|
chain_str = str(default_chain_spec)
|
||||||
|
recipient = '0x' + os.urandom(20).hex()
|
||||||
|
tx_hash = '0x' + os.urandom(32).hex()
|
||||||
|
signed_tx = '0x' + os.urandom(128).hex()
|
||||||
|
otx = Otx.add(0, recipient, tx_hash, signed_tx, session=init_database)
|
||||||
|
init_database.add(otx)
|
||||||
|
init_database.commit()
|
||||||
|
|
||||||
|
token_address = '0x' + os.urandom(20).hex()
|
||||||
|
sender = '0x' + os.urandom(20).hex()
|
||||||
|
txc = TxCache(
|
||||||
|
tx_hash,
|
||||||
|
sender,
|
||||||
|
recipient,
|
||||||
|
token_address,
|
||||||
|
token_address,
|
||||||
|
1000,
|
||||||
|
1000,
|
||||||
|
)
|
||||||
|
init_database.add(txc)
|
||||||
|
init_database.commit()
|
||||||
|
|
||||||
|
token_data = {
|
||||||
|
'address': token_address,
|
||||||
|
'converters': [],
|
||||||
|
}
|
||||||
|
b = balance_incoming([token_data], recipient, chain_str)
|
||||||
|
assert b[0]['balance_incoming'] == 0
|
||||||
|
|
||||||
|
otx.sent(session=init_database)
|
||||||
|
init_database.commit()
|
||||||
|
|
||||||
|
b = balance_incoming([token_data], recipient, chain_str)
|
||||||
|
assert b[0]['balance_incoming'] == 1000
|
||||||
|
|
||||||
|
otx.success(block=1024, session=init_database)
|
||||||
|
init_database.commit()
|
||||||
|
|
||||||
|
b = balance_incoming([token_data], recipient, chain_str)
|
||||||
|
assert b[0]['balance_incoming'] == 0
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user