From 277033f3b59d5c9b6bb9b648a89ed13b2173eb7f Mon Sep 17 00:00:00 2001 From: Geoff Turk <3742109-geoffturk@users.noreply.gitlab.com> Date: Thu, 27 May 2021 10:38:07 +0000 Subject: [PATCH 01/15] Fix traffic script --- .../scripts => data-seeding}/cic_eth/traffic/cmd/cache.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/{contract-migration/scripts => data-seeding}/cic_eth/traffic/cmd/cache.py (100%) diff --git a/apps/contract-migration/scripts/cic_eth/traffic/cmd/cache.py b/apps/data-seeding/cic_eth/traffic/cmd/cache.py similarity index 100% rename from apps/contract-migration/scripts/cic_eth/traffic/cmd/cache.py rename to apps/data-seeding/cic_eth/traffic/cmd/cache.py From fbf73512380e6ab3f66eb09401d94f6d34526812 Mon Sep 17 00:00:00 2001 From: Louis Holbrook Date: Mon, 31 May 2021 15:34:16 +0000 Subject: [PATCH 02/15] cic-eth: Reach 90% test coverage --- apps/cic-eth/.coveragerc | 2 + apps/cic-eth/.gitlab-ci.yml | 4 +- apps/cic-eth/cic_eth/admin/nonce.py | 80 ++-- apps/cic-eth/cic_eth/api/api_admin.py | 70 ++-- apps/cic-eth/cic_eth/api/api_task.py | 256 ++++++------ apps/cic-eth/cic_eth/db/util.py | 8 - apps/cic-eth/cic_eth/eth/gas.py | 38 +- apps/cic-eth/cic_eth/eth/tx.py | 12 +- apps/cic-eth/cic_eth/queue/query.py | 2 +- apps/cic-eth/cic_eth/registry.py | 13 +- .../cic_eth/runnable/daemons/filters/gas.py | 9 +- .../runnable/daemons/filters/register.py | 3 +- .../runnable/daemons/filters/transferauth.py | 18 +- apps/cic-eth/cic_eth/stat.py | 6 +- apps/cic-eth/cic_eth/task.py | 12 +- apps/cic-eth/requirements.txt | 10 +- apps/cic-eth/tests/check/test_check_db.py | 8 + apps/cic-eth/tests/check/test_check_gas.py | 20 + apps/cic-eth/tests/check/test_check_redis.py | 16 + apps/cic-eth/tests/check/test_check_signer.py | 13 + apps/cic-eth/tests/conftest.py | 28 ++ .../tests/filters/test_filter_bogus.py | 38 ++ apps/cic-eth/tests/filters/test_gas_filter.py | 101 +++++ .../tests/filters/test_register_filter.py | 78 ++++ .../tests/filters/test_transferauth_filter.py | 79 ++++ apps/cic-eth/tests/filters/test_tx_filter.py | 3 +- apps/cic-eth/tests/fixtures_celery.py | 8 +- apps/cic-eth/tests/fixtures_contract.py | 77 ++++ apps/cic-eth/tests/task/api/test_admin.py | 351 ++++++++-------- .../tests/task/api/test_admin_noncritical.py | 373 ++++++++++++++++++ apps/cic-eth/tests/task/api/test_app.py | 61 ++- .../tests/task/api/test_app_noncritical.py | 19 + apps/cic-eth/tests/task/test_task_account.py | 1 + apps/cic-eth/tests/task/test_task_admin.py | 88 +++++ apps/cic-eth/tests/task/test_task_gas.py | 286 ++++++++++++++ apps/cic-eth/tests/task/test_task_tx.py | 73 ++-- apps/cic-eth/tests/task/test_task_tx_misc.py | 170 ++++++++ apps/cic-eth/tests/testdata/Bogus.bin | 1 + apps/cic-eth/tests/testdata/Bogus.sol | 10 + apps/cic-eth/tests/unit/ext/test_address.py | 1 + .../tests/unit/test_registry_connect.py | 22 ++ apps/cic-eth/tests/unit/test_stat.py | 18 + 42 files changed, 2037 insertions(+), 449 deletions(-) delete mode 100644 apps/cic-eth/cic_eth/db/util.py create mode 100644 apps/cic-eth/tests/check/test_check_db.py create mode 100644 apps/cic-eth/tests/check/test_check_gas.py create mode 100644 apps/cic-eth/tests/check/test_check_redis.py create mode 100644 apps/cic-eth/tests/check/test_check_signer.py create mode 100644 apps/cic-eth/tests/filters/test_filter_bogus.py create mode 100644 apps/cic-eth/tests/filters/test_gas_filter.py create mode 100644 apps/cic-eth/tests/filters/test_register_filter.py create mode 100644 apps/cic-eth/tests/filters/test_transferauth_filter.py create mode 100644 apps/cic-eth/tests/fixtures_contract.py create mode 100644 apps/cic-eth/tests/task/api/test_admin_noncritical.py create mode 100644 apps/cic-eth/tests/task/api/test_app_noncritical.py create mode 100644 apps/cic-eth/tests/task/test_task_admin.py create mode 100644 apps/cic-eth/tests/task/test_task_gas.py create mode 100644 apps/cic-eth/tests/task/test_task_tx_misc.py create mode 100644 apps/cic-eth/tests/testdata/Bogus.bin create mode 100644 apps/cic-eth/tests/testdata/Bogus.sol create mode 100644 apps/cic-eth/tests/unit/test_registry_connect.py create mode 100644 apps/cic-eth/tests/unit/test_stat.py diff --git a/apps/cic-eth/.coveragerc b/apps/cic-eth/.coveragerc index 63f983d6..47acdac6 100644 --- a/apps/cic-eth/.coveragerc +++ b/apps/cic-eth/.coveragerc @@ -5,3 +5,5 @@ omit = cic_eth/db/migrations/* cic_eth/sync/head.py cic_eth/sync/mempool.py + cic_eth/queue/state.py + *redis*.py diff --git a/apps/cic-eth/.gitlab-ci.yml b/apps/cic-eth/.gitlab-ci.yml index ab8e077a..80c2ded4 100644 --- a/apps/cic-eth/.gitlab-ci.yml +++ b/apps/cic-eth/.gitlab-ci.yml @@ -24,9 +24,7 @@ test-mr-cic-eth: image: $CI_REGISTRY_IMAGE/$APP_NAME-test:latest script: - cd apps/$APP_NAME/ - - pytest tests/unit/ - - pytest tests/task/ - - pytest tests/filters/ + - pytest -x --cov=cic_eth --cov-fail-under=90 --cov-report term-missing tests needs: ["build-mr-cic-eth"] build-push-cic-eth: diff --git a/apps/cic-eth/cic_eth/admin/nonce.py b/apps/cic-eth/cic_eth/admin/nonce.py index b150cbed..a55c8921 100644 --- a/apps/cic-eth/cic_eth/admin/nonce.py +++ b/apps/cic-eth/cic_eth/admin/nonce.py @@ -4,11 +4,18 @@ import logging # external imports import celery from chainlib.chain import ChainSpec -from chainlib.eth.tx import unpack -from chainqueue.query import get_tx -from chainqueue.state import set_cancel +from chainlib.connection import RPCConnection +from chainlib.eth.tx import ( + unpack, + TxFactory, + ) +from chainlib.eth.gas import OverrideGasOracle +from chainqueue.sql.query import get_tx +from chainqueue.sql.state import set_cancel from chainqueue.db.models.otx import Otx from chainqueue.db.models.tx import TxCache +from hexathon import strip_0x +from potaahto.symbols import snake_and_camel # local imports from cic_eth.db.models.base import SessionBase @@ -21,13 +28,14 @@ from cic_eth.admin.ctrl import ( ) from cic_eth.queue.tx import queue_create from cic_eth.eth.gas import create_check_gas_task +from cic_eth.task import BaseTask celery_app = celery.current_app logg = logging.getLogger() -@celery_app.task(bind=True) -def shift_nonce(self, chain_str, tx_hash_orig_hex, delta=1): +@celery_app.task(bind=True, base=BaseTask) +def shift_nonce(self, chainspec_dict, tx_hash_orig_hex, delta=1): """Shift all transactions with nonces higher than the offset by the provided position delta. Transactions who are replaced by transactions that move nonces will be marked as OVERRIDDEN. @@ -38,25 +46,29 @@ def shift_nonce(self, chain_str, tx_hash_orig_hex, delta=1): :type tx_hash_orig_hex: str, 0x-hex :param delta: Amount """ + chain_spec = ChainSpec.from_dict(chainspec_dict) + rpc = RPCConnection.connect(chain_spec, 'default') + rpc_signer = RPCConnection.connect(chain_spec, 'signer') queue = None try: queue = self.request.delivery_info.get('routing_key') except AttributeError: pass - chain_spec = ChainSpec.from_chain_str(chain_str) - tx_brief = get_tx(tx_hash_orig_hex) - tx_raw = bytes.fromhex(strip_0x(tx_brief['signed_tx'][2:])) + session = BaseTask.session_func() + tx_brief = get_tx(chain_spec, tx_hash_orig_hex, session=session) + tx_raw = bytes.fromhex(strip_0x(tx_brief['signed_tx'])) tx = unpack(tx_raw, chain_spec) nonce = tx_brief['nonce'] address = tx['from'] - logg.debug('shifting nonce {} position(s) for address {}, offset {}'.format(delta, address, nonce)) + logg.debug('shifting nonce {} position(s) for address {}, offset {}, hash {}'.format(delta, address, nonce, tx['hash'])) - lock_queue(None, chain_str, address) - lock_send(None, chain_str, address) + lock_queue(None, chain_spec.asdict(), address=address) + lock_send(None, chain_spec.asdict(), address=address) + + set_cancel(chain_spec, strip_0x(tx['hash']), manual=True, session=session) - session = SessionBase.create_session() q = session.query(Otx) q = q.join(TxCache) q = q.filter(TxCache.sender==address) @@ -69,49 +81,57 @@ def shift_nonce(self, chain_str, tx_hash_orig_hex, delta=1): for otx in otxs: tx_raw = bytes.fromhex(strip_0x(otx.signed_tx)) tx_new = unpack(tx_raw, chain_spec) + tx_new = snake_and_camel(tx_new) tx_previous_hash_hex = tx_new['hash'] tx_previous_nonce = tx_new['nonce'] - del(tx_new['hash']) - del(tx_new['hash_unsigned']) + tx_new['gas_price'] += 1 + tx_new['gasPrice'] = tx_new['gas_price'] tx_new['nonce'] -= delta - (tx_hash_hex, tx_signed_raw_hex) = sign_tx(tx_new, chain_str) + logg.debug('tx_new {}'.format(tx_new)) + + del(tx_new['hash']) + del(tx_new['hash_unsigned']) + del(tx_new['hashUnsigned']) + + gas_oracle = OverrideGasOracle(limit=tx_new['gas'], price=tx_new['gas_price'] + 1) # TODO: it should be possible to merely set this price here and if missing in the existing struct then fill it in (chainlib.eth.tx) + c = TxFactory(chain_spec, signer=rpc_signer, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.build_raw(tx_new) logg.debug('tx {} -> {} nonce {} -> {}'.format(tx_previous_hash_hex, tx_hash_hex, tx_previous_nonce, tx_new['nonce'])) otx = Otx( - nonce=tx_new['nonce'], - address=tx_new['from'], - tx_hash=tx_hash_hex, - signed_tx=tx_signed_raw_hex, - ) + tx_new['nonce'], + tx_hash_hex, + tx_signed_raw_hex, + ) session.add(otx) - session.commit() # TODO: cancel all first, then replace. Otherwise we risk two non-locked states for two different nonces. - set_cancel(tx_previous_hash_hex, True) + set_cancel(chain_spec, strip_0x(tx_previous_hash_hex), manual=True, session=session) - TxCache.clone(tx_previous_hash_hex, tx_hash_hex) + TxCache.clone(tx_previous_hash_hex, tx_hash_hex, session=session) tx_hashes.append(tx_hash_hex) txs.append(tx_signed_raw_hex) + session.commit() session.close() - s = create_check_gas_and_send_task( + s = create_check_gas_task( txs, - chain_str, + chain_spec, tx_new['from'], - tx_new['gas'], - tx_hashes, - queue, + gas=tx_new['gas'], + tx_hashes_hex=tx_hashes, + queue=queue, ) s_unlock_send = celery.signature( 'cic_eth.admin.ctrl.unlock_send', [ - chain_str, + chain_spec.asdict(), tx_new['from'], ], queue=queue, @@ -119,7 +139,7 @@ def shift_nonce(self, chain_str, tx_hash_orig_hex, delta=1): s_unlock_direct = celery.signature( 'cic_eth.admin.ctrl.unlock_queue', [ - chain_str, + chain_spec.asdict(), tx_new['from'], ], queue=queue, diff --git a/apps/cic-eth/cic_eth/api/api_admin.py b/apps/cic-eth/cic_eth/api/api_admin.py index 5862dd65..519262c7 100644 --- a/apps/cic-eth/cic_eth/api/api_admin.py +++ b/apps/cic-eth/cic_eth/api/api_admin.py @@ -8,6 +8,7 @@ from chainlib.eth.constant import ( ZERO_ADDRESS, ) from cic_eth_registry import CICRegistry +from cic_eth_registry.erc20 import ERC20Token from cic_eth_registry.error import UnknownContractError from chainlib.eth.address import to_checksum_address from chainlib.eth.contract import code @@ -30,13 +31,14 @@ from chainqueue.db.enum import ( status_str, ) from chainqueue.error import TxStateChangeError +from chainqueue.query import get_tx +from eth_erc20 import ERC20 # local imports from cic_eth.db.models.base import SessionBase from cic_eth.db.models.role import AccountRole from cic_eth.db.models.nonce import Nonce from cic_eth.error import InitializationError -from cic_eth.queue.query import get_tx app = celery.current_app @@ -188,6 +190,7 @@ class AdminApi: s_manual = celery.signature( 'cic_eth.queue.state.set_manual', [ + chain_spec.asdict(), tx_hash_hex, ], queue=self.queue, @@ -206,8 +209,9 @@ class AdminApi: s.link(s_gas) return s_manual.apply_async() - - def check_nonce(self, address): + + + def check_nonce(self, chain_spec, address): s = celery.signature( 'cic_eth.queue.query.get_account_tx', [ @@ -228,13 +232,12 @@ class AdminApi: s_get_tx = celery.signature( 'cic_eth.queue.query.get_tx', [ - chain_spec.asdict(), + chain_spec.asdict(), k, ], queue=self.queue, ) tx = s_get_tx.apply_async().get() - #tx = get_tx(k) logg.debug('checking nonce {} (previous {})'.format(tx['nonce'], last_nonce)) nonce_otx = tx['nonce'] if not is_alive(tx['status']) and tx['status'] & local_fail > 0: @@ -242,7 +245,9 @@ class AdminApi: blocking_tx = k blocking_nonce = nonce_otx elif nonce_otx - last_nonce > 1: - logg.error('nonce gap; {} followed {} for account {}'.format(nonce_otx, last_nonce, tx['from'])) + logg.debug('tx {}'.format(tx)) + tx_obj = unpack(bytes.fromhex(strip_0x(tx['signed_tx'])), chain_spec) + logg.error('nonce gap; {} followed {} for account {}'.format(nonce_otx, last_nonce, tx_obj['from'])) blocking_tx = k blocking_nonce = nonce_otx break @@ -256,12 +261,13 @@ class AdminApi: 'blocking': blocking_nonce, }, 'tx': { - 'blocking': blocking_tx, - } + 'blocking': add_0x(blocking_tx), } + } - def fix_nonce(self, address, nonce, chain_spec): + # TODO: is risky since it does not validate that there is actually a nonce problem? + def fix_nonce(self, chain_spec, address, nonce): s = celery.signature( 'cic_eth.queue.query.get_account_tx', [ @@ -275,15 +281,17 @@ class AdminApi: txs = s.apply_async().get() tx_hash_hex = None + session = SessionBase.create_session() for k in txs.keys(): - tx_dict = get_tx(k) + tx_dict = get_tx(chain_spec, k, session=session) if tx_dict['nonce'] == nonce: tx_hash_hex = k + session.close() s_nonce = celery.signature( 'cic_eth.admin.nonce.shift_nonce', [ - self.rpc.chain_spec.asdict(), + chain_spec.asdict(), tx_hash_hex, ], queue=self.queue @@ -388,12 +396,13 @@ class AdminApi: t = s.apply_async() tx = t.get() - + source_token = None if tx['source_token'] != ZERO_ADDRESS: + source_token_declaration = None if registry != None: try: - source_token = registry.by_address(tx['source_token']) + source_token_declaration = registry.by_address(tx['source_token'], sender_address=self.call_address) except UnknownContractError: logg.warning('unknown source token contract {} (direct)'.format(tx['source_token'])) else: @@ -406,16 +415,21 @@ class AdminApi: queue=self.queue ) t = s.apply_async() - source_token = t.get() - if source_token == None: - logg.warning('unknown source token contract {} (task pool)'.format(tx['source_token'])) + source_token_declaration = t.get() + + if source_token_declaration != None: + logg.warning('found declarator record for source token {} but not checking validity'.format(tx['source_token'])) + source_token = ERC20Token(chain_spec, self.rpc, tx['source_token']) + logg.debug('source token set tup {}'.format(source_token)) + destination_token = None if tx['destination_token'] != ZERO_ADDRESS: + destination_token_declaration = None if registry != None: try: - destination_token = registry.by_address(tx['destination_token']) + destination_token_declaration = registry.by_address(tx['destination_token'], sender_address=self.call_address) except UnknownContractError: logg.warning('unknown destination token contract {}'.format(tx['destination_token'])) else: @@ -428,10 +442,10 @@ class AdminApi: queue=self.queue ) t = s.apply_async() - destination_token = t.get() - if destination_token == None: - logg.warning('unknown destination token contract {} (task pool)'.format(tx['destination_token'])) - + destination_token_declaration = t.get() + if destination_token_declaration != None: + logg.warning('found declarator record for destination token {} but not checking validity'.format(tx['destination_token'])) + destination_token = ERC20Token(chain_spec, self.rpc, tx['destination_token']) tx['sender_description'] = 'Custodial account' tx['recipient_description'] = 'Custodial account' @@ -543,13 +557,19 @@ class AdminApi: if role != None: tx['recipient_description'] = role + erc20_c = ERC20(chain_spec) if source_token != None: - tx['source_token_symbol'] = source_token.symbol() - tx['sender_token_balance'] = source_token.function('balanceOf')(tx['sender']).call() + tx['source_token_symbol'] = source_token.symbol + o = erc20_c.balance_of(tx['source_token'], tx['sender'], sender_address=self.call_address) + r = self.rpc.do(o) + tx['sender_token_balance'] = erc20_c.parse_balance_of(r) if destination_token != None: - tx['destination_token_symbol'] = destination_token.symbol() - tx['recipient_token_balance'] = source_token.function('balanceOf')(tx['recipient']).call() + tx['destination_token_symbol'] = destination_token.symbol + o = erc20_c.balance_of(tx['destination_token'], tx['recipient'], sender_address=self.call_address) + r = self.rpc.do(o) + tx['recipient_token_balance'] = erc20_c.parse_balance_of(r) + #tx['recipient_token_balance'] = destination_token.function('balanceOf')(tx['recipient']).call() # TODO: this can mean either not subitted or culled, need to check other txs with same nonce to determine which tx['network_status'] = 'Not in node' diff --git a/apps/cic-eth/cic_eth/api/api_task.py b/apps/cic-eth/cic_eth/api/api_task.py index 93a4d1bf..aea00002 100644 --- a/apps/cic-eth/cic_eth/api/api_task.py +++ b/apps/cic-eth/cic_eth/api/api_task.py @@ -74,134 +74,134 @@ class Api: return s_token.apply_async() - def convert_transfer(self, from_address, to_address, target_return, minimum_return, from_token_symbol, to_token_symbol): - """Executes a chain of celery tasks that performs conversion between two ERC20 tokens, and transfers to a specified receipient after convert has completed. - - :param from_address: Ethereum address of sender - :type from_address: str, 0x-hex - :param to_address: Ethereum address of receipient - :type to_address: str, 0x-hex - :param target_return: Estimated return from conversion - :type target_return: int - :param minimum_return: The least value of destination token return to allow - :type minimum_return: int - :param from_token_symbol: ERC20 token symbol of token being converted - :type from_token_symbol: str - :param to_token_symbol: ERC20 token symbol of token to receive - :type to_token_symbol: str - :returns: uuid of root task - :rtype: celery.Task - """ - raise NotImplementedError('out of service until new DEX migration is done') - s_check = celery.signature( - 'cic_eth.admin.ctrl.check_lock', - [ - [from_token_symbol, to_token_symbol], - self.chain_spec.asdict(), - LockEnum.QUEUE, - from_address, - ], - queue=self.queue, - ) - s_nonce = celery.signature( - 'cic_eth.eth.nonce.reserve_nonce', - [ - self.chain_spec.asdict(), - ], - queue=self.queue, - ) - s_tokens = celery.signature( - 'cic_eth.eth.erc20.resolve_tokens_by_symbol', - [ - self.chain_str, - ], - queue=self.queue, - ) - s_convert = celery.signature( - 'cic_eth.eth.bancor.convert_with_default_reserve', - [ - from_address, - target_return, - minimum_return, - to_address, - self.chain_spec.asdict(), - ], - queue=self.queue, - ) - s_nonce.link(s_tokens) - s_check.link(s_nonce) - if self.callback_param != None: - s_convert.link(self.callback_success) - s_tokens.link(s_convert).on_error(self.callback_error) - else: - s_tokens.link(s_convert) - - t = s_check.apply_async(queue=self.queue) - return t - - - def convert(self, from_address, target_return, minimum_return, from_token_symbol, to_token_symbol): - """Executes a chain of celery tasks that performs conversion between two ERC20 tokens. - - :param from_address: Ethereum address of sender - :type from_address: str, 0x-hex - :param target_return: Estimated return from conversion - :type target_return: int - :param minimum_return: The least value of destination token return to allow - :type minimum_return: int - :param from_token_symbol: ERC20 token symbol of token being converted - :type from_token_symbol: str - :param to_token_symbol: ERC20 token symbol of token to receive - :type to_token_symbol: str - :returns: uuid of root task - :rtype: celery.Task - """ - raise NotImplementedError('out of service until new DEX migration is done') - s_check = celery.signature( - 'cic_eth.admin.ctrl.check_lock', - [ - [from_token_symbol, to_token_symbol], - self.chain_spec.asdict(), - LockEnum.QUEUE, - from_address, - ], - queue=self.queue, - ) - s_nonce = celery.signature( - 'cic_eth.eth.nonce.reserve_nonce', - [ - self.chain_spec.asdict(), - ], - queue=self.queue, - ) - s_tokens = celery.signature( - 'cic_eth.eth.erc20.resolve_tokens_by_symbol', - [ - self.chain_spec.asdict(), - ], - queue=self.queue, - ) - s_convert = celery.signature( - 'cic_eth.eth.bancor.convert_with_default_reserve', - [ - from_address, - target_return, - minimum_return, - from_address, - self.chain_spec.asdict(), - ], - queue=self.queue, - ) - s_nonce.link(s_tokens) - s_check.link(s_nonce) - if self.callback_param != None: - s_convert.link(self.callback_success) - s_tokens.link(s_convert).on_error(self.callback_error) - else: - s_tokens.link(s_convert) - - t = s_check.apply_async(queue=self.queue) - return t +# def convert_transfer(self, from_address, to_address, target_return, minimum_return, from_token_symbol, to_token_symbol): +# """Executes a chain of celery tasks that performs conversion between two ERC20 tokens, and transfers to a specified receipient after convert has completed. +# +# :param from_address: Ethereum address of sender +# :type from_address: str, 0x-hex +# :param to_address: Ethereum address of receipient +# :type to_address: str, 0x-hex +# :param target_return: Estimated return from conversion +# :type target_return: int +# :param minimum_return: The least value of destination token return to allow +# :type minimum_return: int +# :param from_token_symbol: ERC20 token symbol of token being converted +# :type from_token_symbol: str +# :param to_token_symbol: ERC20 token symbol of token to receive +# :type to_token_symbol: str +# :returns: uuid of root task +# :rtype: celery.Task +# """ +# raise NotImplementedError('out of service until new DEX migration is done') +# s_check = celery.signature( +# 'cic_eth.admin.ctrl.check_lock', +# [ +# [from_token_symbol, to_token_symbol], +# self.chain_spec.asdict(), +# LockEnum.QUEUE, +# from_address, +# ], +# queue=self.queue, +# ) +# s_nonce = celery.signature( +# 'cic_eth.eth.nonce.reserve_nonce', +# [ +# self.chain_spec.asdict(), +# ], +# queue=self.queue, +# ) +# s_tokens = celery.signature( +# 'cic_eth.eth.erc20.resolve_tokens_by_symbol', +# [ +# self.chain_str, +# ], +# queue=self.queue, +# ) +# s_convert = celery.signature( +# 'cic_eth.eth.bancor.convert_with_default_reserve', +# [ +# from_address, +# target_return, +# minimum_return, +# to_address, +# self.chain_spec.asdict(), +# ], +# queue=self.queue, +# ) +# s_nonce.link(s_tokens) +# s_check.link(s_nonce) +# if self.callback_param != None: +# s_convert.link(self.callback_success) +# s_tokens.link(s_convert).on_error(self.callback_error) +# else: +# s_tokens.link(s_convert) +# +# t = s_check.apply_async(queue=self.queue) +# return t +# +# +# def convert(self, from_address, target_return, minimum_return, from_token_symbol, to_token_symbol): +# """Executes a chain of celery tasks that performs conversion between two ERC20 tokens. +# +# :param from_address: Ethereum address of sender +# :type from_address: str, 0x-hex +# :param target_return: Estimated return from conversion +# :type target_return: int +# :param minimum_return: The least value of destination token return to allow +# :type minimum_return: int +# :param from_token_symbol: ERC20 token symbol of token being converted +# :type from_token_symbol: str +# :param to_token_symbol: ERC20 token symbol of token to receive +# :type to_token_symbol: str +# :returns: uuid of root task +# :rtype: celery.Task +# """ +# raise NotImplementedError('out of service until new DEX migration is done') +# s_check = celery.signature( +# 'cic_eth.admin.ctrl.check_lock', +# [ +# [from_token_symbol, to_token_symbol], +# self.chain_spec.asdict(), +# LockEnum.QUEUE, +# from_address, +# ], +# queue=self.queue, +# ) +# s_nonce = celery.signature( +# 'cic_eth.eth.nonce.reserve_nonce', +# [ +# self.chain_spec.asdict(), +# ], +# queue=self.queue, +# ) +# s_tokens = celery.signature( +# 'cic_eth.eth.erc20.resolve_tokens_by_symbol', +# [ +# self.chain_spec.asdict(), +# ], +# queue=self.queue, +# ) +# s_convert = celery.signature( +# 'cic_eth.eth.bancor.convert_with_default_reserve', +# [ +# from_address, +# target_return, +# minimum_return, +# from_address, +# self.chain_spec.asdict(), +# ], +# queue=self.queue, +# ) +# s_nonce.link(s_tokens) +# s_check.link(s_nonce) +# if self.callback_param != None: +# s_convert.link(self.callback_success) +# s_tokens.link(s_convert).on_error(self.callback_error) +# else: +# s_tokens.link(s_convert) +# +# t = s_check.apply_async(queue=self.queue) +# return t def transfer(self, from_address, to_address, value, token_symbol): diff --git a/apps/cic-eth/cic_eth/db/util.py b/apps/cic-eth/cic_eth/db/util.py deleted file mode 100644 index c7620626..00000000 --- a/apps/cic-eth/cic_eth/db/util.py +++ /dev/null @@ -1,8 +0,0 @@ -import math - -def num_serialize(n): - if n == 0: - return b'\x00' - binlog = math.log2(n) - bytelength = int(binlog / 8 + 1) - return n.to_bytes(bytelength, 'big') diff --git a/apps/cic-eth/cic_eth/eth/gas.py b/apps/cic-eth/cic_eth/eth/gas.py index 258054c0..49e89c72 100644 --- a/apps/cic-eth/cic_eth/eth/gas.py +++ b/apps/cic-eth/cic_eth/eth/gas.py @@ -57,10 +57,12 @@ celery_app = celery.current_app logg = logging.getLogger() +MAXIMUM_FEE_UNITS = 8000000 + class MaxGasOracle: def gas(code=None): - return 8000000 + return MAXIMUM_FEE_UNITS def create_check_gas_task(tx_signed_raws_hex, chain_spec, holder_address, gas=None, tx_hashes_hex=None, queue=None): @@ -150,7 +152,7 @@ def cache_gas_data( @celery_app.task(bind=True, throws=(OutOfGasError), base=CriticalSQLAlchemyAndWeb3Task) -def check_gas(self, tx_hashes, chain_spec_dict, txs=[], address=None, gas_required=None): +def check_gas(self, tx_hashes, chain_spec_dict, txs=[], address=None, gas_required=MAXIMUM_FEE_UNITS): """Check the gas level of the sender address of a transaction. If the account balance is not sufficient for the required gas, gas refill is requested and OutOfGasError raiser. @@ -170,24 +172,30 @@ def check_gas(self, tx_hashes, chain_spec_dict, txs=[], address=None, gas_requir :return: Signed raw transaction data list :rtype: param txs, unchanged """ - if len(txs) == 0: - for i in range(len(tx_hashes)): - o = get_tx(tx_hashes[i]) - txs.append(o['signed_tx']) - if address == None: - address = o['address'] + chain_spec = ChainSpec.from_dict(chain_spec_dict) + logg.debug('txs {} tx_hashes {}'.format(txs, tx_hashes)) + + addresspass = None + if len(txs) == 0: + addresspass = [] + for i in range(len(tx_hashes)): + o = get_tx(chain_spec_dict, tx_hashes[i]) + txs.append(o['signed_tx']) + logg.debug('sender {}'.format(o)) + tx = unpack(bytes.fromhex(strip_0x(o['signed_tx'])), chain_spec) + if address == None: + address = tx['from'] + elif address != tx['from']: + raise ValueError('txs passed to check gas must all have same sender; had {} got {}'.format(address, tx['from'])) + addresspass.append(address) - #if not web3.Web3.isChecksumAddress(address): if not is_checksum_address(address): raise ValueError('invalid address {}'.format(address)) - chain_spec = ChainSpec.from_dict(chain_spec_dict) - queue = self.request.delivery_info.get('routing_key') conn = RPCConnection.connect(chain_spec) - # TODO: it should not be necessary to pass address explicitly, if not passed should be derived from the tx gas_balance = 0 try: o = balance(address) @@ -198,6 +206,9 @@ def check_gas(self, tx_hashes, chain_spec_dict, txs=[], address=None, gas_requir conn.disconnect() raise EthError('gas_balance call for {}: {}'.format(address, e)) + if gas_required == None: + gas_required = MAXIMUM_FEE_UNITS + logg.debug('address {} has gas {} needs {}'.format(address, gas_balance, gas_required)) session = SessionBase.create_session() gas_provider = AccountRole.get_address('GAS_GIFTER', session=session) @@ -268,7 +279,8 @@ def check_gas(self, tx_hashes, chain_spec_dict, txs=[], address=None, gas_requir queue=queue, ) ready_tasks.append(s) - celery.group(ready_tasks)() + t = celery.group(ready_tasks)() + logg.debug('group {}'.format(t)) return txs diff --git a/apps/cic-eth/cic_eth/eth/tx.py b/apps/cic-eth/cic_eth/eth/tx.py index 4883e872..6602fe67 100644 --- a/apps/cic-eth/cic_eth/eth/tx.py +++ b/apps/cic-eth/cic_eth/eth/tx.py @@ -21,6 +21,7 @@ from chainqueue.db.models.tx import Otx from chainqueue.db.models.tx import TxCache from chainqueue.db.enum import StatusBits from chainqueue.error import NotLocalTxError +from potaahto.symbols import snake_and_camel # local imports from cic_eth.db import SessionBase @@ -58,6 +59,9 @@ def hashes_to_txs(self, tx_hashes): if len(tx_hashes) == 0: raise ValueError('no transaction to send') + for i in range(len(tx_hashes)): + tx_hashes[i] = strip_0x(tx_hashes[i]) + queue = self.request.delivery_info['routing_key'] session = SessionBase.create_session() @@ -148,7 +152,7 @@ def send(self, txs, chain_spec_dict): @celery_app.task(bind=True, throws=(NotFoundEthException,), base=CriticalWeb3Task) def sync_tx(self, tx_hash_hex, chain_spec_dict): - """Force update of network status of a simgle transaction + """Force update of network status of a single transaction :param tx_hash_hex: Transaction hash :type tx_hash_hex: str, 0x-hex @@ -173,12 +177,14 @@ def sync_tx(self, tx_hash_hex, chain_spec_dict): # TODO: apply receipt in tx object to validate and normalize input if rcpt != None: + rcpt = snake_and_camel(rcpt) success = rcpt['status'] == 1 - logg.debug('sync tx {} mined block {} success {}'.format(tx_hash_hex, rcpt['blockNumber'], success)) + logg.debug('sync tx {} mined block {} tx index {} success {}'.format(tx_hash_hex, rcpt['blockNumber'], rcpt['transactionIndex'], success)) s = celery.signature( 'cic_eth.queue.state.set_final', [ + chain_spec_dict, tx_hash_hex, rcpt['blockNumber'], rcpt['transactionIndex'], @@ -186,12 +192,14 @@ def sync_tx(self, tx_hash_hex, chain_spec_dict): ], queue=queue, ) + # TODO: it's not entirely clear how we can reliable determine that its in mempool without explicitly checking else: logg.debug('sync tx {} mempool'.format(tx_hash_hex)) s = celery.signature( 'cic_eth.queue.state.set_sent', [ + chain_spec_dict, tx_hash_hex, ], queue=queue, diff --git a/apps/cic-eth/cic_eth/queue/query.py b/apps/cic-eth/cic_eth/queue/query.py index 1572289b..44ed7547 100644 --- a/apps/cic-eth/cic_eth/queue/query.py +++ b/apps/cic-eth/cic_eth/queue/query.py @@ -37,7 +37,7 @@ def get_tx_cache(chain_spec_dict, tx_hash): def get_tx(chain_spec_dict, tx_hash): chain_spec = ChainSpec.from_dict(chain_spec_dict) session = SessionBase.create_session() - r = chainqueue.query.get_tx(chain_spec, tx_hash) + r = chainqueue.query.get_tx(chain_spec, tx_hash, session=session) session.close() return r diff --git a/apps/cic-eth/cic_eth/registry.py b/apps/cic-eth/cic_eth/registry.py index 2b5e2b94..b46b2008 100644 --- a/apps/cic-eth/cic_eth/registry.py +++ b/apps/cic-eth/cic_eth/registry.py @@ -5,29 +5,30 @@ import logging from cic_eth_registry import CICRegistry from cic_eth_registry.lookup.declarator import AddressDeclaratorLookup from cic_eth_registry.lookup.tokenindex import TokenIndexLookup +from chainlib.eth.constant import ZERO_ADDRESS logg = logging.getLogger() -def connect_token_registry(rpc, chain_spec): +def connect_token_registry(rpc, chain_spec, sender_address=ZERO_ADDRESS): registry = CICRegistry(chain_spec, rpc) - token_registry_address = registry.by_name('TokenRegistry') + token_registry_address = registry.by_name('TokenRegistry', sender_address=sender_address) logg.debug('using token registry address {}'.format(token_registry_address)) lookup = TokenIndexLookup(chain_spec, token_registry_address) CICRegistry.add_lookup(lookup) -def connect_declarator(rpc, chain_spec, trusted_addresses): +def connect_declarator(rpc, chain_spec, trusted_addresses, sender_address=ZERO_ADDRESS): registry = CICRegistry(chain_spec, rpc) - declarator_address = registry.by_name('AddressDeclarator') + declarator_address = registry.by_name('AddressDeclarator', sender_address=sender_address) logg.debug('using declarator address {}'.format(declarator_address)) lookup = AddressDeclaratorLookup(chain_spec, declarator_address, trusted_addresses) CICRegistry.add_lookup(lookup) -def connect(rpc, chain_spec, registry_address): +def connect(rpc, chain_spec, registry_address, sender_address=ZERO_ADDRESS): CICRegistry.address = registry_address registry = CICRegistry(chain_spec, rpc) - registry_address = registry.by_name('ContractRegistry') + registry_address = registry.by_name('ContractRegistry', sender_address=sender_address) return registry diff --git a/apps/cic-eth/cic_eth/runnable/daemons/filters/gas.py b/apps/cic-eth/cic_eth/runnable/daemons/filters/gas.py index b0219093..4b17f86c 100644 --- a/apps/cic-eth/cic_eth/runnable/daemons/filters/gas.py +++ b/apps/cic-eth/cic_eth/runnable/daemons/filters/gas.py @@ -17,7 +17,8 @@ from cic_eth.db.models.base import SessionBase from cic_eth.eth.gas import create_check_gas_task from .base import SyncFilter -logg = logging.getLogger().getChild(__name__) +#logg = logging.getLogger().getChild(__name__) +logg = logging.getLogger() class GasFilter(SyncFilter): @@ -27,11 +28,11 @@ class GasFilter(SyncFilter): self.chain_spec = chain_spec - def filter(self, conn, block, tx, session): + def filter(self, conn, block, tx, db_session): if tx.value > 0: tx_hash_hex = add_0x(tx.hash) logg.debug('gas refill tx {}'.format(tx_hash_hex)) - session = SessionBase.bind_session(session) + session = SessionBase.bind_session(db_session) q = session.query(TxCache.recipient) q = q.join(Otx) q = q.filter(Otx.tx_hash==strip_0x(tx_hash_hex)) @@ -56,7 +57,7 @@ class GasFilter(SyncFilter): tx_hashes_hex=list(txs.keys()), queue=self.queue, ) - s.apply_async() + return s.apply_async() def __str__(self): diff --git a/apps/cic-eth/cic_eth/runnable/daemons/filters/register.py b/apps/cic-eth/cic_eth/runnable/daemons/filters/register.py index dc1bd778..23953229 100644 --- a/apps/cic-eth/cic_eth/runnable/daemons/filters/register.py +++ b/apps/cic-eth/cic_eth/runnable/daemons/filters/register.py @@ -50,7 +50,8 @@ class RegistrationFilter(SyncFilter): queue=self.queue, ) s_nonce.link(s_gift) - s_nonce.apply_async() + t = s_nonce.apply_async() + return t def __str__(self): diff --git a/apps/cic-eth/cic_eth/runnable/daemons/filters/transferauth.py b/apps/cic-eth/cic_eth/runnable/daemons/filters/transferauth.py index d6646b6c..3ba77791 100644 --- a/apps/cic-eth/cic_eth/runnable/daemons/filters/transferauth.py +++ b/apps/cic-eth/cic_eth/runnable/daemons/filters/transferauth.py @@ -32,7 +32,7 @@ class TransferAuthFilter(SyncFilter): self.transfer_request_contract = registry.by_name('TransferAuthorization', sender_address=call_address) - def filter(self, conn, block, tx, session): #rcpt, chain_str, session=None): + def filter(self, conn, block, tx, db_session): #rcpt, chain_str, session=None): if tx.payload == None: logg.debug('no payload') @@ -45,16 +45,17 @@ class TransferAuthFilter(SyncFilter): return False recipient = tx.inputs[0] - if recipient != self.transfer_request_contract.address(): + #if recipient != self.transfer_request_contract.address(): + if recipient != self.transfer_request_contract: logg.debug('not our transfer auth contract address {}'.format(recipient)) return False r = TransferAuthorization.parse_create_request_request(tx.payload) - - sender = abi_decode_single(ABIContractType.ADDRESS, r[0]) - recipient = abi_decode_single(ABIContractType.ADDRESS, r[1]) - token = abi_decode_single(ABIContractType.ADDRESS, r[2]) - value = abi_decode_single(ABIContractType.UINT256, r[3]) + + sender = r[0] + recipient = r[1] + token = r[2] + value = r[3] token_data = { 'address': token, @@ -64,6 +65,7 @@ class TransferAuthFilter(SyncFilter): 'cic_eth.eth.nonce.reserve_nonce', [ [token_data], + self.chain_spec.asdict(), sender, ], queue=self.queue, @@ -80,7 +82,7 @@ class TransferAuthFilter(SyncFilter): ) s_nonce.link(s_approve) t = s_nonce.apply_async() - return True + return t def __str__(self): diff --git a/apps/cic-eth/cic_eth/stat.py b/apps/cic-eth/cic_eth/stat.py index d3a07fdb..e674e120 100644 --- a/apps/cic-eth/cic_eth/stat.py +++ b/apps/cic-eth/cic_eth/stat.py @@ -20,7 +20,11 @@ def init_chain_stat(rpc, block_start=0): if block_start == 0: o = block_latest() r = rpc.do(o) - block_start = int(r, 16) + try: + block_start = int(r, 16) + except TypeError: + block_start = int(r) + logg.debug('blockstart {}'.format(block_start)) for i in range(BLOCK_SAMPLES): o = block_by_number(block_start-10+i) diff --git a/apps/cic-eth/cic_eth/task.py b/apps/cic-eth/cic_eth/task.py index 2275971c..fa542cd2 100644 --- a/apps/cic-eth/cic_eth/task.py +++ b/apps/cic-eth/cic_eth/task.py @@ -20,7 +20,8 @@ import liveness.linux from cic_eth.error import SeppukuError from cic_eth.db.models.base import SessionBase -logg = logging.getLogger().getChild(__name__) +#logg = logging.getLogger().getChild(__name__) +logg = logging.getLogger() celery_app = celery.current_app @@ -118,12 +119,13 @@ def registry(): return CICRegistry.address -@celery_app.task() -def registry_address_lookup(chain_spec_dict, address, connection_tag='default'): +@celery_app.task(bind=True, base=BaseTask) +def registry_address_lookup(self, chain_spec_dict, address, connection_tag='default'): chain_spec = ChainSpec.from_dict(chain_spec_dict) conn = RPCConnection.connect(chain_spec, tag=connection_tag) registry = CICRegistry(chain_spec, conn) - return registry.by_address(address) + r = registry.by_address(address, sender_address=self.call_address) + return r @celery_app.task(throws=(UnknownContractError,)) @@ -131,7 +133,7 @@ def registry_name_lookup(chain_spec_dict, name, connection_tag='default'): chain_spec = ChainSpec.from_dict(chain_spec_dict) conn = RPCConnection.connect(chain_spec, tag=connection_tag) registry = CICRegistry(chain_spec, conn) - return registry.by_name(name) + return registry.by_name(name, sender_address=self.call_address) @celery_app.task() diff --git a/apps/cic-eth/requirements.txt b/apps/cic-eth/requirements.txt index fddcd155..a7ed3ae5 100644 --- a/apps/cic-eth/requirements.txt +++ b/apps/cic-eth/requirements.txt @@ -1,23 +1,23 @@ -cic-base~=0.1.2b11 +cic-base~=0.1.2b14 celery==4.4.7 crypto-dev-signer~=0.4.14b3 confini~=0.3.6rc3 -cic-eth-registry~=0.5.5a4 +cic-eth-registry~=0.5.5a7 redis==3.5.3 alembic==1.4.2 websockets==8.1 requests~=2.24.0 eth_accounts_index~=0.0.11a12 -erc20-transfer-authorization~=0.3.1a6 +erc20-transfer-authorization~=0.3.1a7 uWSGI==2.0.19.1 semver==2.13.0 websocket-client==0.57.0 moolb~=0.1.1b2 eth-address-index~=0.1.1a11 -chainlib~=0.0.3a2 +chainlib~=0.0.3rc2 hexathon~=0.0.1a7 chainsyncer[sql]~=0.0.2a4 -chainqueue~=0.0.2a2 +chainqueue~=0.0.2b1 sarafu-faucet==0.0.3a3 erc20-faucet==0.2.1a4 coincurve==15.0.0 diff --git a/apps/cic-eth/tests/check/test_check_db.py b/apps/cic-eth/tests/check/test_check_db.py new file mode 100644 index 00000000..941f8892 --- /dev/null +++ b/apps/cic-eth/tests/check/test_check_db.py @@ -0,0 +1,8 @@ +# local imports +from cic_eth.check.db import health + +def test_check_health( + init_database, + ): + + assert health() diff --git a/apps/cic-eth/tests/check/test_check_gas.py b/apps/cic-eth/tests/check/test_check_gas.py new file mode 100644 index 00000000..ea0ffb88 --- /dev/null +++ b/apps/cic-eth/tests/check/test_check_gas.py @@ -0,0 +1,20 @@ +# local imports +from cic_eth.check.gas import health +from cic_eth.db.models.role import AccountRole + +def test_check_gas( + config, + init_database, + default_chain_spec, + eth_rpc, + custodial_roles, + whoever, + ): + + config.add(str(default_chain_spec), 'CIC_CHAIN_SPEC', exists_ok=True) + config.add(100, 'ETH_GAS_GIFTER_MINIMUM_BALANCE', exists_ok=True) + assert health(config=config) + + AccountRole.set('GAS_GIFTER', whoever, session=init_database) + init_database.commit() + assert not health(config=config) diff --git a/apps/cic-eth/tests/check/test_check_redis.py b/apps/cic-eth/tests/check/test_check_redis.py new file mode 100644 index 00000000..02c3ba77 --- /dev/null +++ b/apps/cic-eth/tests/check/test_check_redis.py @@ -0,0 +1,16 @@ +# external imports +import pytest + +# local imports +from cic_eth.check.redis import health + + +def test_check_redis( + config, + have_redis, + ): + + if have_redis != None: + pytest.skip('cannot connect to redis, skipping test: {}'.format(have_redis)) + + assert health(unit='test', config=config) diff --git a/apps/cic-eth/tests/check/test_check_signer.py b/apps/cic-eth/tests/check/test_check_signer.py new file mode 100644 index 00000000..5572e195 --- /dev/null +++ b/apps/cic-eth/tests/check/test_check_signer.py @@ -0,0 +1,13 @@ +# local imports +from cic_eth.check.signer import health + + +def test_check_signer( + default_chain_spec, + config, + eth_signer, + eth_rpc, + ): + + config.add(str(default_chain_spec), 'CIC_CHAIN_SPEC', exists_ok=True) + assert health(config=config) diff --git a/apps/cic-eth/tests/conftest.py b/apps/cic-eth/tests/conftest.py index bc7a82bb..169a6c71 100644 --- a/apps/cic-eth/tests/conftest.py +++ b/apps/cic-eth/tests/conftest.py @@ -2,9 +2,11 @@ import os import sys import logging +import uuid # external imports from eth_erc20 import ERC20 +import redis # local imports from cic_eth.api import Api @@ -19,6 +21,7 @@ from tests.fixtures_config import * from tests.fixtures_database import * from tests.fixtures_celery import * from tests.fixtures_role import * +from tests.fixtures_contract import * from chainlib.eth.pytest import * from eth_contract_registry.pytest import * from cic_eth_registry.pytest.fixtures_contracts import * @@ -55,3 +58,28 @@ def default_token( ): BaseTask.default_token_symbol = foo_token_symbol BaseTask.default_token_address = foo_token + + + +@pytest.fixture(scope='session') +def have_redis( + config, + ): + + r = redis.Redis( + host = config.get('REDIS_HOST'), + port = config.get('REDIS_PORT'), + db = config.get('REDIS_DB'), + ) + k = str(uuid.uuid4()) + try: + r.set(k, 'foo') + r.delete(k) + except redis.exceptions.ConnectionError as e: + return e + except TypeError as e: + return e + + return None + + diff --git a/apps/cic-eth/tests/filters/test_filter_bogus.py b/apps/cic-eth/tests/filters/test_filter_bogus.py new file mode 100644 index 00000000..30edbc70 --- /dev/null +++ b/apps/cic-eth/tests/filters/test_filter_bogus.py @@ -0,0 +1,38 @@ +# local imports +from cic_eth.runnable.daemons.filters.gas import GasFilter +from cic_eth.runnable.daemons.filters.transferauth import TransferAuthFilter +from cic_eth.runnable.daemons.filters.callback import CallbackFilter +from cic_eth.runnable.daemons.filters.straggler import StragglerFilter +from cic_eth.runnable.daemons.filters.tx import TxFilter +from cic_eth.runnable.daemons.filters.register import RegistrationFilter + + +# Hit tx mismatch paths on all filters +def test_filter_bogus( + init_database, + bogus_tx_block, + default_chain_spec, + eth_rpc, + eth_signer, + transfer_auth, + cic_registry, + contract_roles, + register_lookups, + ): + + fltrs = [ + TransferAuthFilter(cic_registry, default_chain_spec, eth_rpc, call_address=contract_roles['CONTRACT_DEPLOYER']), + GasFilter(default_chain_spec, queue=None), + TxFilter(default_chain_spec, None), + CallbackFilter(default_chain_spec, None, None, caller_address=contract_roles['CONTRACT_DEPLOYER']), + StragglerFilter(default_chain_spec, None), + RegistrationFilter(default_chain_spec, queue=None), + ] + + for fltr in fltrs: + r = None + try: + r = fltr.filter(eth_rpc, bogus_tx_block[0], bogus_tx_block[1], db_session=init_database) + except: + pass + assert not r diff --git a/apps/cic-eth/tests/filters/test_gas_filter.py b/apps/cic-eth/tests/filters/test_gas_filter.py new file mode 100644 index 00000000..8f91d2fd --- /dev/null +++ b/apps/cic-eth/tests/filters/test_gas_filter.py @@ -0,0 +1,101 @@ +# external imports +from chainlib.connection import RPCConnection +from chainlib.eth.nonce import OverrideNonceOracle +from chainqueue.tx import create as queue_create +from chainlib.eth.tx import ( + TxFormat, + unpack, + Tx, + ) +from chainlib.eth.gas import ( + Gas, + OverrideGasOracle, + ) +from chainlib.eth.block import ( + block_latest, + block_by_number, + Block, + ) +from chainqueue.state import ( + set_waitforgas, + ) +from hexathon import strip_0x +from chainqueue.db.models.otx import Otx +from chainqueue.db.enum import StatusBits + +# local imports +from cic_eth.runnable.daemons.filters.gas import GasFilter +from cic_eth.eth.gas import cache_gas_data + + +def test_filter_gas( + default_chain_spec, + init_database, + eth_rpc, + eth_signer, + agent_roles, + celery_session_worker, + ): + + rpc = RPCConnection.connect(default_chain_spec, 'default') + nonce_oracle = OverrideNonceOracle(agent_roles['ALICE'], 42) + gas_oracle = OverrideGasOracle(price=1000000000, limit=21000) + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.create(agent_roles['ALICE'], agent_roles['BOB'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + + queue_create( + default_chain_spec, + 42, + agent_roles['ALICE'], + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_gas_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + set_waitforgas(default_chain_spec, tx_hash_hex, session=init_database) + init_database.commit() + tx_hash_hex_wait = tx_hash_hex + otx = Otx.load(tx_hash_hex_wait, session=init_database) + assert otx.status & StatusBits.GAS_ISSUES == StatusBits.GAS_ISSUES + + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.create(agent_roles['BOB'], agent_roles['ALICE'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + + queue_create( + default_chain_spec, + 43, + agent_roles['BOB'], + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_gas_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + + fltr = GasFilter(default_chain_spec, queue=None) + + o = block_latest() + r = eth_rpc.do(o) + o = block_by_number(r, include_tx=False) + r = eth_rpc.do(o) + block = Block(r) + block.txs = [tx_hash_hex] + + tx_signed_raw_bytes = bytes.fromhex(strip_0x(tx_signed_raw_hex)) + tx_src = unpack(tx_signed_raw_bytes, default_chain_spec) + tx = Tx(tx_src, block=block) + t = fltr.filter(eth_rpc, block, tx, db_session=init_database) + + t.get_leaf() + assert t.successful() + init_database.commit() + + otx = Otx.load(tx_hash_hex_wait, session=init_database) + assert otx.status & StatusBits.QUEUED == StatusBits.QUEUED diff --git a/apps/cic-eth/tests/filters/test_register_filter.py b/apps/cic-eth/tests/filters/test_register_filter.py new file mode 100644 index 00000000..9842bc00 --- /dev/null +++ b/apps/cic-eth/tests/filters/test_register_filter.py @@ -0,0 +1,78 @@ +# external imports +from eth_accounts_index.registry import AccountRegistry +from chainlib.connection import RPCConnection +from chainlib.eth.nonce import RPCNonceOracle +from chainlib.eth.gas import OverrideGasOracle +from chainlib.eth.tx import( + receipt, + unpack, + Tx, + ) +from chainlib.eth.block import ( + block_latest, + block_by_number, + Block, + ) +from erc20_faucet import Faucet +from hexathon import strip_0x +from chainqueue.query import get_account_tx + +# local imports +from cic_eth.runnable.daemons.filters.register import RegistrationFilter + + +def test_register_filter( + default_chain_spec, + init_database, + eth_rpc, + eth_signer, + account_registry, + faucet, + register_lookups, + contract_roles, + agent_roles, + cic_registry, + init_celery_tasks, + celery_session_worker, + caplog, + ): + + nonce_oracle = RPCNonceOracle(contract_roles['ACCOUNT_REGISTRY_WRITER'], conn=eth_rpc) + gas_oracle = OverrideGasOracle(limit=AccountRegistry.gas(), conn=eth_rpc) + + c = AccountRegistry(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, o) = c.add(account_registry, contract_roles['ACCOUNT_REGISTRY_WRITER'], agent_roles['ALICE']) + r = eth_rpc.do(o) + tx_signed_raw_bytes = bytes.fromhex(strip_0x(o['params'][0])) + + o = receipt(tx_hash_hex) + rcpt = eth_rpc.do(o) + assert rcpt['status'] == 1 + + o = block_latest() + r = eth_rpc.do(o) + o = block_by_number(r, include_tx=False) + r = eth_rpc.do(o) + block = Block(r) + block.txs = [tx_hash_hex] + + tx_src = unpack(tx_signed_raw_bytes, default_chain_spec) + tx = Tx(tx_src, block=block, rcpt=rcpt) + tx.apply_receipt(rcpt) + + fltr = RegistrationFilter(default_chain_spec, queue=None) + t = fltr.filter(eth_rpc, block, tx, db_session=init_database) + + t.get_leaf() + assert t.successful() + + gift_txs = get_account_tx(default_chain_spec.asdict(), agent_roles['ALICE'], as_sender=True, session=init_database) + ks = list(gift_txs.keys()) + assert len(ks) == 1 + + tx_raw_signed_hex = strip_0x(gift_txs[ks[0]]) + tx_raw_signed_bytes = bytes.fromhex(tx_raw_signed_hex) + gift_tx = unpack(tx_raw_signed_bytes, default_chain_spec) + + gift = Faucet.parse_give_to_request(gift_tx['data']) + assert gift[0] == agent_roles['ALICE'] diff --git a/apps/cic-eth/tests/filters/test_transferauth_filter.py b/apps/cic-eth/tests/filters/test_transferauth_filter.py new file mode 100644 index 00000000..25822b83 --- /dev/null +++ b/apps/cic-eth/tests/filters/test_transferauth_filter.py @@ -0,0 +1,79 @@ +# external imports +from erc20_transfer_authorization import TransferAuthorization +from eth_erc20 import ERC20 +from chainlib.connection import RPCConnection +from chainlib.eth.nonce import RPCNonceOracle +from chainlib.eth.gas import OverrideGasOracle +from chainlib.eth.tx import ( + receipt, + unpack, + Tx, + ) +from chainlib.eth.block import ( + block_latest, + block_by_number, + Block, + ) +from hexathon import strip_0x +from chainqueue.query import get_account_tx + +# local imports +from cic_eth.runnable.daemons.filters.transferauth import TransferAuthFilter + + +def test_filter_transferauth( + default_chain_spec, + init_database, + eth_rpc, + eth_signer, + agent_roles, + contract_roles, + transfer_auth, + foo_token, + celery_session_worker, + register_lookups, + init_custodial, + cic_registry, + ): + + rpc = RPCConnection.connect(default_chain_spec, 'default') + nonce_oracle = RPCNonceOracle(contract_roles['CONTRACT_DEPLOYER'], eth_rpc) + gas_oracle = OverrideGasOracle(limit=200000, conn=eth_rpc) + c = TransferAuthorization(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, o) = c.create_request(transfer_auth, contract_roles['CONTRACT_DEPLOYER'], agent_roles['ALICE'], agent_roles['BOB'], foo_token, 1024) + + r = rpc.do(o) + tx_signed_raw_bytes = bytes.fromhex(strip_0x(o['params'][0])) + + o = receipt(tx_hash_hex) + r = rpc.do(o) + assert r['status'] == 1 + + o = block_latest() + r = eth_rpc.do(o) + o = block_by_number(r, include_tx=False) + r = eth_rpc.do(o) + block = Block(r) + block.txs = [tx_hash_hex] + + #tx_signed_raw_bytes = bytes.fromhex(strip_0x(tx_signed_raw_hex)) + tx_src = unpack(tx_signed_raw_bytes, default_chain_spec) + tx = Tx(tx_src, block=block) + + fltr = TransferAuthFilter(cic_registry, default_chain_spec, eth_rpc, call_address=contract_roles['CONTRACT_DEPLOYER']) + t = fltr.filter(eth_rpc, block, tx, db_session=init_database) + + t.get_leaf() + assert t.successful() + + approve_txs = get_account_tx(default_chain_spec.asdict(), agent_roles['ALICE'], as_sender=True, session=init_database) + ks = list(approve_txs.keys()) + assert len(ks) == 1 + + tx_raw_signed_hex = strip_0x(approve_txs[ks[0]]) + tx_raw_signed_bytes = bytes.fromhex(tx_raw_signed_hex) + approve_tx = unpack(tx_raw_signed_bytes, default_chain_spec) + + c = ERC20(default_chain_spec) + approve = c.parse_approve_request(approve_tx['data']) + assert approve[0] == agent_roles['BOB'] diff --git a/apps/cic-eth/tests/filters/test_tx_filter.py b/apps/cic-eth/tests/filters/test_tx_filter.py index bd706e5d..06d2c94e 100644 --- a/apps/cic-eth/tests/filters/test_tx_filter.py +++ b/apps/cic-eth/tests/filters/test_tx_filter.py @@ -23,7 +23,6 @@ from chainqueue.state import ( set_ready, set_sent, ) - from hexathon import strip_0x # local imports @@ -31,7 +30,7 @@ from cic_eth.runnable.daemons.filters.tx import TxFilter from cic_eth.eth.gas import cache_gas_data -def test_tx( +def test_filter_tx( default_chain_spec, init_database, eth_rpc, diff --git a/apps/cic-eth/tests/fixtures_celery.py b/apps/cic-eth/tests/fixtures_celery.py index 26f640d3..188f973f 100644 --- a/apps/cic-eth/tests/fixtures_celery.py +++ b/apps/cic-eth/tests/fixtures_celery.py @@ -22,7 +22,6 @@ def init_celery_tasks( @pytest.fixture(scope='session') def celery_includes(): return [ -# 'cic_eth.eth.bancor', 'cic_eth.eth.erc20', 'cic_eth.eth.tx', 'cic_eth.ext.tx', @@ -47,8 +46,8 @@ def celery_config(): bq = tempfile.mkdtemp() bp = tempfile.mkdtemp() rq = tempfile.mkdtemp() - logg.debug('celery broker queue {} processed {}'.format(bq, bp)) - logg.debug('celery backend store {}'.format(rq)) + logg.debug('celery broker session queue {} processed {}'.format(bq, bp)) + logg.debug('celery backend session store {}'.format(rq)) yield { 'broker_url': 'filesystem://', 'broker_transport_options': { @@ -58,12 +57,11 @@ def celery_config(): }, 'result_backend': 'file://{}'.format(rq), } - logg.debug('cleaning up celery filesystem backend files {} {} {}'.format(bq, bp, rq)) + logg.debug('cleaning up celery session filesystem backend files {} {} {}'.format(bq, bp, rq)) shutil.rmtree(bq) shutil.rmtree(bp) shutil.rmtree(rq) - @pytest.fixture(scope='session') def celery_worker_parameters(): return { diff --git a/apps/cic-eth/tests/fixtures_contract.py b/apps/cic-eth/tests/fixtures_contract.py new file mode 100644 index 00000000..b09b551b --- /dev/null +++ b/apps/cic-eth/tests/fixtures_contract.py @@ -0,0 +1,77 @@ +# standard imports +import os + +# external imports +import pytest +from chainlib.eth.contract import ( + ABIContractEncoder, + ABIContractType, + ) +from chainlib.eth.nonce import RPCNonceOracle +from chainlib.eth.gas import OverrideGasOracle +from chainlib.eth.block import ( + block_latest, + block_by_number, + Block, + ) +from chainlib.eth.tx import ( + receipt, + TxFactory, + TxFormat, + unpack, + Tx, + ) +from hexathon import strip_0x + +script_dir = os.path.dirname(os.path.realpath(__file__)) +root_dir = os.path.dirname(script_dir) + + +@pytest.fixture(scope='function') +def bogus_tx_block( + default_chain_spec, + eth_rpc, + eth_signer, + contract_roles, + ): + + nonce_oracle = RPCNonceOracle(contract_roles['CONTRACT_DEPLOYER'], conn=eth_rpc) + gas_oracle = OverrideGasOracle(limit=2000000, conn=eth_rpc) + + f = open(os.path.join(script_dir, 'testdata', 'Bogus.bin'), 'r') + bytecode = f.read() + f.close() + + c = TxFactory(default_chain_spec, signer=eth_signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle) + tx = c.template(contract_roles['CONTRACT_DEPLOYER'], None, use_nonce=True) + tx = c.set_code(tx, bytecode) + (tx_hash_hex, o) = c.build(tx) + + r = eth_rpc.do(o) + + o = receipt(tx_hash_hex) + r = eth_rpc.do(o) + + contract_address = r['contract_address'] + + enc = ABIContractEncoder() + enc.method('poke') + data = enc.get() + tx = c.template(contract_roles['CONTRACT_DEPLOYER'], contract_address, use_nonce=True) + tx = c.set_code(tx, data) + (tx_hash_hex, o) = c.finalize(tx, TxFormat.JSONRPC) + r = eth_rpc.do(o) + tx_signed_raw_hex = strip_0x(o['params'][0]) + + o = block_latest() + r = eth_rpc.do(o) + o = block_by_number(r, include_tx=False) + r = eth_rpc.do(o) + block = Block(r) + block.txs = [tx_hash_hex] + + tx_signed_raw_bytes = bytes.fromhex(strip_0x(tx_signed_raw_hex)) + tx_src = unpack(tx_signed_raw_bytes, default_chain_spec) + tx = Tx(tx_src, block=block) + + return (block, tx) diff --git a/apps/cic-eth/tests/task/api/test_admin.py b/apps/cic-eth/tests/task/api/test_admin.py index a37e94a9..95b1a060 100644 --- a/apps/cic-eth/tests/task/api/test_admin.py +++ b/apps/cic-eth/tests/task/api/test_admin.py @@ -9,8 +9,14 @@ from chainlib.eth.tx import ( unpack, TxFormat, ) -from chainlib.eth.nonce import RPCNonceOracle -from chainlib.eth.gas import Gas +from chainlib.eth.nonce import ( + RPCNonceOracle, + OverrideNonceOracle, + ) +from chainlib.eth.gas import ( + Gas, + OverrideGasOracle, + ) from chainlib.eth.address import to_checksum_address from hexathon import ( strip_0x, @@ -23,7 +29,15 @@ from chainqueue.db.enum import ( StatusBits, status_str, ) -from chainqueue.query import get_tx +from chainqueue.state import ( + set_fubar, + set_ready, + set_reserved, + ) +from chainqueue.query import ( + get_tx, + get_nonce_tx_cache, + ) # local imports from cic_eth.api import AdminApi @@ -36,150 +50,6 @@ from cic_eth.queue.tx import queue_create logg = logging.getLogger() -#def test_resend_inplace( -# default_chain_spec, -# init_database, -# init_w3, -# celery_session_worker, -# ): -# -# chain_str = str(default_chain_spec) -# c = RpcClient(default_chain_spec) -# -# sigs = [] -# -# gas_provider = c.gas_provider() -# -# s_nonce = celery.signature( -# 'cic_eth.eth.nonce.reserve_nonce', -# [ -# init_w3.eth.accounts[0], -# gas_provider, -# ], -# queue=None, -# ) -# s_refill = celery.signature( -# 'cic_eth.eth.gas.refill_gas', -# [ -# chain_str, -# ], -# queue=None, -# ) -# s_nonce.link(s_refill) -# t = s_nonce.apply_async() -# t.get() -# for r in t.collect(): -# pass -# assert t.successful() -# -# q = init_database.query(Otx) -# q = q.join(TxCache) -# q = q.filter(TxCache.recipient==init_w3.eth.accounts[0]) -# o = q.first() -# tx_raw = o.signed_tx -# -# tx_dict = unpack(bytes.fromhex(tx_raw), default_chain_spec) -# gas_price_before = tx_dict['gasPrice'] -# -# s = celery.signature( -# 'cic_eth.admin.ctrl.lock_send', -# [ -# chain_str, -# init_w3.eth.accounts[0], -# ], -# queue=None, -# ) -# t = s.apply_async() -# t.get() -# assert t.successful() -# -# api = AdminApi(c, queue=None) -# t = api.resend(tx_dict['hash'], chain_str, unlock=True) -# t.get() -# i = 0 -# tx_hash_new_hex = None -# for r in t.collect(): -# tx_hash_new_hex = r[1] -# assert t.successful() -# -# tx_raw_new = get_tx(tx_hash_new_hex) -# logg.debug('get {}'.format(tx_raw_new)) -# tx_dict_new = unpack(bytes.fromhex(tx_raw_new['signed_tx']), default_chain_spec) -# assert tx_hash_new_hex != tx_dict['hash'] -# assert tx_dict_new['gasPrice'] > gas_price_before -# -# tx_dict_after = get_tx(tx_dict['hash']) -# -# logg.debug('logggg {}'.format(status_str(tx_dict_after['status']))) -# assert tx_dict_after['status'] & StatusBits.MANUAL - - -#def test_check_fix_nonce( -# default_chain_spec, -# init_database, -# init_eth_account_roles, -# init_w3, -# eth_empty_accounts, -# celery_session_worker, -# ): -# -# chain_str = str(default_chain_spec) -# -# sigs = [] -# for i in range(5): -# s = celery.signature( -# 'cic_eth.eth.gas.refill_gas', -# [ -# eth_empty_accounts[i], -# chain_str, -# ], -# queue=None, -# ) -# sigs.append(s) -# -# t = celery.group(sigs)() -# txs = t.get() -# assert t.successful() -# -# tx_hash = web3.Web3.keccak(hexstr=txs[2]) -# c = RpcClient(default_chain_spec) -# api = AdminApi(c, queue=None) -# address = init_eth_account_roles['eth_account_gas_provider'] -# nonce_spec = api.check_nonce(address) -# assert nonce_spec['nonce']['network'] == 0 -# assert nonce_spec['nonce']['queue'] == 4 -# assert nonce_spec['nonce']['blocking'] == None -# -# s_set = celery.signature( -# 'cic_eth.queue.tx.set_rejected', -# [ -# tx_hash.hex(), -# ], -# queue=None, -# ) -# t = s_set.apply_async() -# t.get() -# t.collect() -# assert t.successful() -# -# -# nonce_spec = api.check_nonce(address) -# assert nonce_spec['nonce']['blocking'] == 2 -# assert nonce_spec['tx']['blocking'] == tx_hash.hex() -# -# t = api.fix_nonce(address, nonce_spec['nonce']['blocking']) -# t.get() -# t.collect() -# assert t.successful() -# -# for tx in txs[3:]: -# tx_hash = web3.Web3.keccak(hexstr=tx) -# tx_dict = get_tx(tx_hash.hex()) -# assert tx_dict['status'] == StatusEnum.OVERRIDDEN -# -# - - def test_have_account( default_chain_spec, custodial_roles, @@ -243,28 +113,6 @@ def test_tag_account( assert AccountRole.get_address('bar', init_database) == agent_roles['CAROL'] -#def test_ready( -# init_database, -# agent_roles, -# eth_rpc, -# ): -# -# api = AdminApi(eth_rpc) -# -# with pytest.raises(InitializationError): -# api.ready() -# -# bogus_account = os.urandom(20) -# bogus_account_hex = '0x' + bogus_account.hex() -# -# api.tag_account('ETH_GAS_PROVIDER_ADDRESS', web3.Web3.toChecksumAddress(bogus_account_hex)) -# with pytest.raises(KeyError): -# api.ready() -# -# api.tag_account('ETH_GAS_PROVIDER_ADDRESS', eth_empty_accounts[0]) -# api.ready() - - def test_tx( default_chain_spec, cic_registry, @@ -286,3 +134,168 @@ def test_tx( api = AdminApi(eth_rpc, queue=None, call_address=contract_roles['DEFAULT']) tx = api.tx(default_chain_spec, tx_hash=tx_hash_hex) logg.warning('code missing to verify tx contents {}'.format(tx)) + + +def test_check_nonce_gap( + default_chain_spec, + init_database, + eth_rpc, + eth_signer, + agent_roles, + contract_roles, + celery_session_worker, + caplog, + ): + + # NOTE: this only works as long as agents roles start at nonce 0 + nonce_oracle = OverrideNonceOracle(agent_roles['ALICE'], 0) + gas_oracle = OverrideGasOracle(limit=21000, conn=eth_rpc) + + tx_hashes = [] + txs = [] + + j = 0 + for i in range(10): + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.create(agent_roles['ALICE'], agent_roles['BOB'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + if i == 3: + j = 1 + nonce_oracle = OverrideNonceOracle(agent_roles['ALICE'], i+1) + + queue_create( + default_chain_spec, + i+j, + agent_roles['ALICE'], + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_gas_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + tx_hashes.append(tx_hash_hex) + txs.append(tx_signed_raw_hex) + + + init_database.commit() + + api = AdminApi(eth_rpc, queue=None, call_address=contract_roles['DEFAULT']) + r = api.check_nonce(default_chain_spec, agent_roles['ALICE']) + + assert r['nonce']['blocking'] == 4 + assert r['tx']['blocking'] == tx_hashes[3] # one less because there is a gap + + +def test_check_nonce_localfail( + default_chain_spec, + init_database, + eth_rpc, + eth_signer, + agent_roles, + contract_roles, + celery_session_worker, + caplog, + ): + + # NOTE: this only works as long as agents roles start at nonce 0 + nonce_oracle = OverrideNonceOracle(agent_roles['ALICE'], 0) + gas_oracle = OverrideGasOracle(limit=21000, conn=eth_rpc) + + tx_hashes = [] + txs = [] + + j = 0 + for i in range(10): + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.create(agent_roles['ALICE'], agent_roles['BOB'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + + queue_create( + default_chain_spec, + i, + agent_roles['ALICE'], + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_gas_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + tx_hashes.append(tx_hash_hex) + txs.append(tx_signed_raw_hex) + + set_ready(default_chain_spec, tx_hashes[4], session=init_database) + set_reserved(default_chain_spec, tx_hashes[4], session=init_database) + set_fubar(default_chain_spec, tx_hashes[4], session=init_database) + + init_database.commit() + + api = AdminApi(eth_rpc, queue=None, call_address=contract_roles['DEFAULT']) + r = api.check_nonce(default_chain_spec, agent_roles['ALICE']) + + assert r['nonce']['blocking'] == 4 + assert r['tx']['blocking'] == tx_hashes[4] + + +def test_fix_nonce( + default_chain_spec, + init_database, + eth_rpc, + eth_signer, + agent_roles, + contract_roles, + celery_session_worker, + init_celery_tasks, + caplog, + ): + + nonce_oracle = OverrideNonceOracle(agent_roles['ALICE'], 0) + gas_oracle = OverrideGasOracle(limit=21000, conn=eth_rpc) + + tx_hashes = [] + txs = [] + + for i in range(10): + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.create(agent_roles['ALICE'], agent_roles['BOB'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + + queue_create( + default_chain_spec, + i, + agent_roles['ALICE'], + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_gas_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + tx_hashes.append(tx_hash_hex) + txs.append(tx_signed_raw_hex) + + init_database.commit() + + api = AdminApi(eth_rpc, queue=None, call_address=contract_roles['DEFAULT']) + t = api.fix_nonce(default_chain_spec, agent_roles['ALICE'], 3) + r = t.get_leaf() + assert t.successful() + + init_database.commit() + + txs = get_nonce_tx_cache(default_chain_spec, 3, agent_roles['ALICE'], session=init_database) + ks = txs.keys() + assert len(ks) == 2 + for k in ks: + hsh = add_0x(k) + otx = Otx.load(hsh, session=init_database) + init_database.refresh(otx) + logg.debug('checking nonce {} tx {} status {}'.format(3, otx.tx_hash, otx.status)) + if add_0x(k) == tx_hashes[3]: + assert otx.status & StatusBits.OBSOLETE == StatusBits.OBSOLETE + else: + assert otx.status == 1 diff --git a/apps/cic-eth/tests/task/api/test_admin_noncritical.py b/apps/cic-eth/tests/task/api/test_admin_noncritical.py new file mode 100644 index 00000000..fc10959d --- /dev/null +++ b/apps/cic-eth/tests/task/api/test_admin_noncritical.py @@ -0,0 +1,373 @@ +# standard imports +import logging +import io +import json + +# external imports +import pytest +from chainlib.connection import RPCConnection +from chainlib.eth.nonce import ( + nonce, + OverrideNonceOracle, + RPCNonceOracle, + ) +from chainqueue.tx import create as queue_create +from chainlib.eth.tx import ( + raw, + receipt, + TxFormat, + Tx, + ) +from chainlib.eth.block import block_latest +from chainlib.eth.gas import ( + Gas, + OverrideGasOracle, + ) +from chainqueue.state import ( + set_reserved, + set_sent, + set_ready, + ) +from chainqueue.db.models.otx import Otx +from chainqueue.db.enum import StatusBits +from chainqueue.query import get_nonce_tx_cache +from eth_erc20 import ERC20 +from cic_eth_registry import CICRegistry + +# local imports +from cic_eth.api.api_admin import AdminApi +from cic_eth.eth.gas import cache_gas_data +from cic_eth.eth.erc20 import cache_transfer_data + +logg = logging.getLogger() + + +def test_admin_api_tx( + default_chain_spec, + init_database, + init_celery_tasks, + eth_rpc, + eth_signer, + agent_roles, + contract_roles, + custodial_roles, + celery_session_worker, + foo_token, + address_declarator, + cic_registry, + register_tokens, + register_lookups, + caplog, + ): + + nonce_oracle = RPCNonceOracle(custodial_roles['FOO_TOKEN_GIFTER'], conn=eth_rpc) + gas_oracle = OverrideGasOracle(limit=100000, conn=eth_rpc) + + o = nonce(custodial_roles['FOO_TOKEN_GIFTER']) + r = eth_rpc.do(o) + gifter_nonce = int(r, 16) + + #c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + c = ERC20(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.transfer(foo_token, custodial_roles['FOO_TOKEN_GIFTER'], agent_roles['ALICE'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + queue_create( + default_chain_spec, + gifter_nonce, # will only work if agent starts at 0 + agent_roles['ALICE'], + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_transfer_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + + init_database.commit() + + o = raw(tx_signed_raw_hex) + eth_rpc.do(o) + + o = receipt(tx_hash_hex) + r = eth_rpc.do(o) + assert r['status'] == 1 + + set_ready(default_chain_spec, tx_hash_hex, session=init_database) + set_reserved(default_chain_spec, tx_hash_hex, session=init_database) + set_sent(default_chain_spec, tx_hash_hex, session=init_database) + + # lookup by transaction hash, without registry + api = AdminApi(eth_rpc, queue=None, call_address=contract_roles['CONTRACT_DEPLOYER']) + tx = api.tx(default_chain_spec, tx_hash=tx_hash_hex) + logg.debug('deployed {}'.format(contract_roles['CONTRACT_DEPLOYER'])) + assert tx['tx_hash'] == tx_hash_hex + + # lookup by RLP transaction, without registry + tx = api.tx(default_chain_spec, tx_raw=tx_signed_raw_hex) + assert tx['tx_hash'] == tx_hash_hex + + # lookup by transaction hash, with registry + registry = CICRegistry(default_chain_spec, eth_rpc) + tx = api.tx(default_chain_spec, tx_hash=tx_hash_hex, registry=registry) + assert tx['tx_hash'] == tx_hash_hex + + # lookup by transaction hash, using writer + buf = io.StringIO() + api.tx(default_chain_spec, tx_hash=tx_hash_hex, renderer=json.dumps, w=buf) + tx = json.loads(buf.getvalue()) + assert tx['tx_hash'] == tx_hash_hex + + +def test_admin_api_account( + default_chain_spec, + init_database, + eth_rpc, + eth_signer, + agent_roles, + contract_roles, + celery_session_worker, + caplog, + ): + + nonce_oracle = OverrideNonceOracle(agent_roles['ALICE'], 42) + gas_oracle = OverrideGasOracle(limit=21000, conn=eth_rpc) + + tx_hashes_alice = [] + txs_alice = [] + + for i in range(3): + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.create(agent_roles['ALICE'], agent_roles['BOB'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + queue_create( + default_chain_spec, + 42+i, + agent_roles['ALICE'], + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_gas_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + tx_hashes_alice.append(tx_hash_hex) + txs_alice.append(tx_signed_raw_hex) + + init_database.commit() + + nonce_oracle = OverrideNonceOracle(agent_roles['BOB'], 13) + tx_hashes_bob = [] + txs_bob = [] + + for i in range(2): + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.create(agent_roles['BOB'], agent_roles['ALICE'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + queue_create( + default_chain_spec, + 13+i, + agent_roles['BOB'], + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_gas_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + tx_hashes_bob.append(tx_hash_hex) + txs_bob.append(tx_signed_raw_hex) + + init_database.commit() + + + api = AdminApi(eth_rpc, queue=None, call_address=contract_roles['CONTRACT_DEPLOYER']) + r = api.account(default_chain_spec, agent_roles['ALICE']) + assert len(r) == 5 + + api = AdminApi(eth_rpc, queue=None, call_address=contract_roles['CONTRACT_DEPLOYER']) + r = api.account(default_chain_spec, agent_roles['ALICE'], include_sender=False) + assert len(r) == 2 + + api = AdminApi(eth_rpc, queue=None, call_address=contract_roles['CONTRACT_DEPLOYER']) + r = api.account(default_chain_spec, agent_roles['ALICE'], include_recipient=False) + assert len(r) == 3 + + +def test_admin_api_account_writer( + default_chain_spec, + init_database, + eth_rpc, + eth_signer, + agent_roles, + contract_roles, + celery_session_worker, + caplog, + ): + + nonce_oracle = OverrideNonceOracle(agent_roles['ALICE'], 42) + gas_oracle = OverrideGasOracle(limit=21000, conn=eth_rpc) + + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.create(agent_roles['ALICE'], agent_roles['BOB'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + queue_create( + default_chain_spec, + 42, + agent_roles['ALICE'], + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_gas_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + + init_database.commit() + + buf = io.StringIO() + api = AdminApi(eth_rpc, queue=None, call_address=contract_roles['CONTRACT_DEPLOYER']) + api.account(default_chain_spec, agent_roles['ALICE'], renderer=json.dumps, w=buf) + + # TODO: improve eval + tx = json.loads(buf.getvalue()) + assert tx['tx_hash'] == tx_hash_hex + + +def test_registry( + eth_rpc, + cic_registry, + contract_roles, + celery_session_worker, + ): + + api = AdminApi(eth_rpc, queue=None, call_address=contract_roles['CONTRACT_DEPLOYER']) + t = api.registry() + r = t.get_leaf() + assert r == cic_registry + + +def test_proxy_do( + default_chain_spec, + eth_rpc, + contract_roles, + celery_session_worker, + ): + + o = block_latest() + r = eth_rpc.do(o) + + api = AdminApi(eth_rpc, queue=None, call_address=contract_roles['CONTRACT_DEPLOYER']) + t = api.proxy_do(default_chain_spec, o) + rr = t.get_leaf() + + assert r == rr + + +def test_resend_inplace( + init_database, + default_chain_spec, + eth_rpc, + eth_signer, + agent_roles, + contract_roles, + celery_session_worker, + ): + + rpc = RPCConnection.connect(default_chain_spec, 'default') + nonce_oracle = OverrideNonceOracle(agent_roles['ALICE'], 42) + gas_oracle = OverrideGasOracle(price=1000000000, limit=21000) + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.create(agent_roles['ALICE'], agent_roles['BOB'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + + queue_create( + default_chain_spec, + 42, + agent_roles['ALICE'], + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_gas_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + + set_ready(default_chain_spec, tx_hash_hex, session=init_database) + set_reserved(default_chain_spec, tx_hash_hex, session=init_database) + set_sent(default_chain_spec, tx_hash_hex, session=init_database) + + init_database.commit() + + api = AdminApi(eth_rpc, queue=None, call_address=contract_roles['CONTRACT_DEPLOYER']) + t = api.resend(tx_hash_hex, default_chain_spec, unlock=True) + r = t.get_leaf() + assert t.successful() + + + otx = Otx.load(tx_hash_hex, session=init_database) + assert otx.status & StatusBits.OBSOLETE == StatusBits.OBSOLETE + + txs = get_nonce_tx_cache(default_chain_spec, otx.nonce, agent_roles['ALICE'], session=init_database) + assert len(txs) == 2 + + + +@pytest.mark.xfail() +def test_resend_clone( + init_database, + default_chain_spec, + eth_rpc, + eth_signer, + agent_roles, + contract_roles, + celery_session_worker, + ): + + rpc = RPCConnection.connect(default_chain_spec, 'default') + nonce_oracle = OverrideNonceOracle(agent_roles['ALICE'], 42) + gas_oracle = OverrideGasOracle(price=1000000000, limit=21000) + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.create(agent_roles['ALICE'], agent_roles['BOB'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + + queue_create( + default_chain_spec, + 42, + agent_roles['ALICE'], + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_gas_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + + set_ready(default_chain_spec, tx_hash_hex, session=init_database) + set_reserved(default_chain_spec, tx_hash_hex, session=init_database) + set_sent(default_chain_spec, tx_hash_hex, session=init_database) + + init_database.commit() + + api = AdminApi(eth_rpc, queue=None, call_address=contract_roles['CONTRACT_DEPLOYER']) + t = api.resend(tx_hash_hex, default_chain_spec, in_place=False) + r = t.get_leaf() + assert t.successful() + + otx = Otx.load(tx_hash_hex, session=init_database) + assert otx.status & StatusBits.IN_NETWORK == StatusBits.IN_NETWORK + assert otx.status & StatusBits.OBSOLETE == StatusBits.OBSOLETE + + txs = get_nonce_tx_cache(default_chain_spec, otx.nonce, agent_roles['ALICE'], session=init_database) + assert len(txs) == 1 + + txs = get_nonce_tx_cache(default_chain_spec, otx.nonce + 1, agent_roles['ALICE'], session=init_database) + assert len(txs) == 1 + + otx = Otx.load(txs[0], session=init_database) + assert otx.status == 0 diff --git a/apps/cic-eth/tests/task/api/test_app.py b/apps/cic-eth/tests/task/api/test_app.py index 1d3e64aa..321009f6 100644 --- a/apps/cic-eth/tests/task/api/test_app.py +++ b/apps/cic-eth/tests/task/api/test_app.py @@ -8,11 +8,20 @@ import pytest import celery from cic_eth_registry.erc20 import ERC20Token from chainlib.chain import ChainSpec +from eth_accounts_index import AccountsIndex +from chainlib.eth.tx import ( + transaction, + ) +from chainqueue.state import ( + set_reserved, + ) # local imports from cic_eth.api import Api +from cic_eth.queue.query import get_tx -logg = logging.getLogger(__name__) +#logg = logging.getLogger(__name__) +logg = logging.getLogger() def test_account_api( @@ -29,6 +38,47 @@ def test_account_api( assert t.successful() +def test_account_api_register( + default_chain_spec, + init_database, + account_registry, + faucet, + custodial_roles, + cic_registry, + register_lookups, + eth_rpc, + celery_session_worker, + ): + api = Api(str(default_chain_spec), callback_param='accounts', callback_task='cic_eth.callbacks.noop.noop', queue=None) + t = api.create_account('') + register_tx_hash = t.get_leaf() + assert t.successful() + + set_reserved(default_chain_spec, register_tx_hash, session=init_database) + + tx = get_tx(default_chain_spec.asdict(), register_tx_hash) + s = celery.signature( + 'cic_eth.eth.tx.send', + [ + [tx['signed_tx']], + default_chain_spec.asdict(), + ], + queue=None + ) + t = s.apply_async() + r = t.get_leaf() + assert t.successful() + + o = transaction(register_tx_hash) + tx_src = eth_rpc.do(o) + + c = AccountsIndex(default_chain_spec) + address = c.parse_add_request(tx_src['data']) + o = c.have(account_registry, address[0], sender_address=custodial_roles['CONTRACT_DEPLOYER']) + r = eth_rpc.do(o) + assert c.parse_have(r) + + def test_transfer_api( default_chain_spec, eth_rpc, @@ -37,16 +87,15 @@ def test_transfer_api( custodial_roles, agent_roles, cic_registry, - register_tokens, + token_registry, register_lookups, celery_session_worker, + register_tokens, + foo_token_symbol, ): - #token = CICRegistry.get_address(default_chain_spec, bancor_tokens[0]) - foo_token_cache = ERC20Token(default_chain_spec, eth_rpc, foo_token) - api = Api(str(default_chain_spec), callback_param='transfer', callback_task='cic_eth.callbacks.noop.noop', queue=None) - t = api.transfer(custodial_roles['FOO_TOKEN_GIFTER'], agent_roles['ALICE'], 1024, foo_token_cache.symbol) + t = api.transfer(custodial_roles['FOO_TOKEN_GIFTER'], agent_roles['ALICE'], 1, foo_token_symbol) t.get_leaf() assert t.successful() diff --git a/apps/cic-eth/tests/task/api/test_app_noncritical.py b/apps/cic-eth/tests/task/api/test_app_noncritical.py new file mode 100644 index 00000000..7ebb8700 --- /dev/null +++ b/apps/cic-eth/tests/task/api/test_app_noncritical.py @@ -0,0 +1,19 @@ +# local imports +from cic_eth.api.api_task import Api +from cic_eth.task import BaseTask + +def test_default_token( + default_chain_spec, + foo_token, + default_token, + token_registry, + register_tokens, + register_lookups, + cic_registry, + celery_session_worker, + ): + + api = Api(str(default_chain_spec), queue=None) + t = api.default_token() + r = t.get_leaf() + assert r['address'] == foo_token diff --git a/apps/cic-eth/tests/task/test_task_account.py b/apps/cic-eth/tests/task/test_task_account.py index a384babb..d1c1c3d2 100644 --- a/apps/cic-eth/tests/task/test_task_account.py +++ b/apps/cic-eth/tests/task/test_task_account.py @@ -156,6 +156,7 @@ def test_gift( eth_signer, init_celery_tasks, cic_registry, + register_lookups, celery_session_worker, ): diff --git a/apps/cic-eth/tests/task/test_task_admin.py b/apps/cic-eth/tests/task/test_task_admin.py new file mode 100644 index 00000000..7efa8283 --- /dev/null +++ b/apps/cic-eth/tests/task/test_task_admin.py @@ -0,0 +1,88 @@ +# standard imports +import logging + +# external imports +import celery +from chainlib.connection import RPCConnection +from chainlib.eth.nonce import OverrideNonceOracle +from chainqueue.tx import ( + create as queue_create, + ) +from chainlib.eth.gas import ( + Gas, + OverrideGasOracle, + ) +from chainlib.eth.tx import TxFormat +from chainqueue.query import get_nonce_tx_cache +from chainqueue.db.models.otx import Otx +from chainqueue.db.enum import StatusBits +from hexathon import add_0x + +# local imports +from cic_eth.admin.nonce import shift_nonce +from cic_eth.eth.gas import cache_gas_data + +logg = logging.getLogger() + + +def test_shift_nonce( + default_chain_spec, + init_database, + eth_rpc, + eth_signer, + agent_roles, + celery_session_worker, + caplog, + ): + + nonce_oracle = OverrideNonceOracle(agent_roles['ALICE'], 42) + gas_oracle = OverrideGasOracle(limit=21000, conn=eth_rpc) + + tx_hashes = [] + txs = [] + + for i in range(10): + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.create(agent_roles['ALICE'], agent_roles['BOB'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + queue_create( + default_chain_spec, + 42+i, + agent_roles['ALICE'], + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_gas_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + tx_hashes.append(tx_hash_hex) + txs.append(tx_signed_raw_hex) + + init_database.commit() + + s = celery.signature( + 'cic_eth.admin.nonce.shift_nonce', + [ + default_chain_spec.asdict(), + tx_hashes[3], + ], + queue=None + ) + t = s.apply_async() + r = t.get_leaf() + assert t.successful() + init_database.commit() + + + for i in range(42+3, 42+10): + txs = get_nonce_tx_cache(default_chain_spec, i, agent_roles['ALICE'], session=init_database) + for k in txs.keys(): + hsh = add_0x(k) + otx = Otx.load(hsh, session=init_database) + logg.debug('checking nonce {} tx {} status {}'.format(i, otx.tx_hash, otx.status)) + if add_0x(k) == tx_hashes[i-42]: + assert otx.status & StatusBits.OBSOLETE == StatusBits.OBSOLETE + else: + assert otx.status == 1 diff --git a/apps/cic-eth/tests/task/test_task_gas.py b/apps/cic-eth/tests/task/test_task_gas.py new file mode 100644 index 00000000..3bd3dd80 --- /dev/null +++ b/apps/cic-eth/tests/task/test_task_gas.py @@ -0,0 +1,286 @@ +# standard imports +import logging + +# external imports +import celery +import pytest +from chainlib.connection import RPCConnection +from chainlib.eth.nonce import ( + OverrideNonceOracle, + RPCNonceOracle, + ) +from chainlib.eth.gas import ( + OverrideGasOracle, + Gas, + ) +from chainlib.eth.tx import ( + unpack, + TxFormat, + ) +from chainlib.eth.constant import ( + MINIMUM_FEE_UNITS, + MINIMUM_FEE_PRICE, + ) +from chainqueue.tx import create as queue_create +from chainqueue.query import get_tx +from chainqueue.db.enum import StatusBits +from chainqueue.state import ( + set_ready, + set_reserved, + set_sent, + ) +from chainqueue.db.models.otx import Otx +from hexathon import strip_0x + +# local imports +from cic_eth.eth.gas import cache_gas_data +from cic_eth.error import OutOfGasError + +logg = logging.getLogger() + + +def test_task_check_gas_ok( + default_chain_spec, + eth_rpc, + eth_signer, + init_database, + agent_roles, + custodial_roles, + celery_session_worker, + ): + + rpc = RPCConnection.connect(default_chain_spec, 'default') + nonce_oracle = RPCNonceOracle(agent_roles['ALICE'], conn=eth_rpc) + gas_oracle = OverrideGasOracle(price=1000000000, limit=21000) + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.create(agent_roles['ALICE'], agent_roles['BOB'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + + queue_create( + default_chain_spec, + 0, + agent_roles['ALICE'], + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_gas_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + + init_database.commit() + + s = celery.signature( + 'cic_eth.eth.gas.check_gas', + [ + [ + tx_hash_hex, + ], + default_chain_spec.asdict(), + [], + None, + 8000000, + ], + queue=None + ) + t = s.apply_async() + t.get_leaf() + assert t.successful() + + init_database.commit() + + tx = get_tx(default_chain_spec, tx_hash_hex, session=init_database) + assert tx['status'] & StatusBits.QUEUED == StatusBits.QUEUED + + +def test_task_check_gas_insufficient( + default_chain_spec, + eth_rpc, + eth_signer, + init_database, + agent_roles, + custodial_roles, + celery_session_worker, + whoever, + ): + + rpc = RPCConnection.connect(default_chain_spec, 'default') + nonce_oracle = OverrideNonceOracle(whoever, 42) + gas_oracle = OverrideGasOracle(price=1000000000, limit=21000) + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.create(whoever, agent_roles['BOB'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + + queue_create( + default_chain_spec, + 42, + whoever, + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_gas_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + + init_database.commit() + + s = celery.signature( + 'cic_eth.eth.gas.check_gas', + [ + [ + tx_hash_hex, + ], + default_chain_spec.asdict(), + [], + None, + None, + ], + queue=None + ) + t = s.apply_async() + try: + r = t.get_leaf() + except OutOfGasError: + pass + + init_database.commit() + + tx = get_tx(default_chain_spec, tx_hash_hex, session=init_database) + assert tx['status'] & StatusBits.GAS_ISSUES == StatusBits.GAS_ISSUES + + +def test_task_check_gas_low( + default_chain_spec, + eth_rpc, + eth_signer, + init_database, + agent_roles, + custodial_roles, + celery_session_worker, + whoever, + ): + + gas_oracle = OverrideGasOracle(price=MINIMUM_FEE_PRICE, limit=MINIMUM_FEE_UNITS) + nonce_oracle = RPCNonceOracle(custodial_roles['GAS_GIFTER'], conn=eth_rpc) + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, o) = c.create(custodial_roles['GAS_GIFTER'], whoever, 100 * (10 ** 6)) + r = eth_rpc.do(o) + + rpc = RPCConnection.connect(default_chain_spec, 'default') + nonce_oracle = RPCNonceOracle(whoever, conn=eth_rpc) + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.create(whoever, agent_roles['BOB'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + + queue_create( + default_chain_spec, + 0, + whoever, + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_gas_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + + init_database.commit() + + s = celery.signature( + 'cic_eth.eth.gas.check_gas', + [ + [ + tx_hash_hex, + ], + default_chain_spec.asdict(), + ], + [], + None, + None, + queue=None + ) + t = s.apply_async() + t.get_leaf() + assert t.successful() + + init_database.commit() + + tx = get_tx(default_chain_spec, tx_hash_hex, session=init_database) + assert tx['status'] & StatusBits.QUEUED == StatusBits.QUEUED + + +@pytest.mark.parametrize( + '_gas_price,_gas_factor', + [ + (None, 1.1), + (MINIMUM_FEE_PRICE * 1.1, 0.9), + (None, 1.3), + ] + ) +def test_task_resend_explicit( + default_chain_spec, + init_database, + eth_rpc, + eth_signer, + agent_roles, + custodial_roles, + celery_session_worker, + _gas_price, + _gas_factor, + ): + + rpc = RPCConnection.connect(default_chain_spec, 'default') + nonce_oracle = RPCNonceOracle(agent_roles['ALICE'], conn=eth_rpc) + gas_oracle = OverrideGasOracle(price=1000000000, limit=21000) + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.create(agent_roles['ALICE'], agent_roles['BOB'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + + queue_create( + default_chain_spec, + 0, + agent_roles['ALICE'], + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_gas_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + tx_before = unpack(bytes.fromhex(strip_0x(tx_signed_raw_hex)), default_chain_spec) + + init_database.commit() + + set_ready(default_chain_spec, tx_hash_hex, session=init_database) + set_reserved(default_chain_spec, tx_hash_hex, session=init_database) + set_sent(default_chain_spec, tx_hash_hex, session=init_database) + + s = celery.signature( + 'cic_eth.eth.gas.resend_with_higher_gas', + [ + tx_hash_hex, + default_chain_spec.asdict(), + _gas_price, + _gas_factor, + ], + queue=None + ) + t = s.apply_async() + r = t.get_leaf() + assert t.successful() + + q = init_database.query(Otx) + q = q.filter(Otx.tx_hash==strip_0x(r)) + otx = q.first() + if otx == None: + raise NotLocalTxError(r) + + tx_after = unpack(bytes.fromhex(strip_0x(otx.signed_tx)), default_chain_spec) + logg.debug('gasprices before {} after {}'.format(tx_before['gasPrice'], tx_after['gasPrice'])) + assert tx_after['gasPrice'] > tx_before['gasPrice'] + diff --git a/apps/cic-eth/tests/task/test_task_tx.py b/apps/cic-eth/tests/task/test_task_tx.py index 939b800b..ecf68992 100644 --- a/apps/cic-eth/tests/task/test_task_tx.py +++ b/apps/cic-eth/tests/task/test_task_tx.py @@ -4,16 +4,27 @@ import logging # external imports import pytest import celery -from chainlib.eth.gas import Gas +from chainlib.eth.gas import ( + OverrideGasOracle, + Gas, + ) from chainlib.eth.nonce import RPCNonceOracle from chainlib.eth.tx import ( TxFormat, unpack, transaction, receipt, + raw, ) from hexathon import strip_0x from chainqueue.db.models.otx import Otx +from chainqueue.tx import create as queue_create +from chainqueue.state import ( + set_reserved, + set_ready, + set_sent, + ) +from chainqueue.db.enum import StatusBits # local imports from cic_eth.queue.tx import register_tx @@ -60,15 +71,6 @@ def test_tx_send( def test_sync_tx( - default_chain_spec, - eth_rpc, - eth_signer, - celery_session_worker, - ): - pass - - -def test_resend_with_higher_gas( init_database, default_chain_spec, eth_rpc, @@ -77,31 +79,48 @@ def test_resend_with_higher_gas( celery_session_worker, ): - nonce_oracle = RPCNonceOracle(agent_roles['ALICE'], eth_rpc) - c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle) - (tx_hash_hex, tx_signed_raw_hex) = c.create(agent_roles['ALICE'], agent_roles['BOB'], 1024, tx_format=TxFormat.RLP_SIGNED) - register_tx(tx_hash_hex, tx_signed_raw_hex, default_chain_spec, None, session=init_database) - cache_gas_data(tx_hash_hex, tx_signed_raw_hex, default_chain_spec.asdict()) - tx_before = unpack(bytes.fromhex(strip_0x(tx_signed_raw_hex)), default_chain_spec) + nonce_oracle = RPCNonceOracle(agent_roles['ALICE'], conn=eth_rpc) + gas_oracle = OverrideGasOracle(price=1000000000, limit=21000) + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex, tx_signed_raw_hex) = c.create(agent_roles['ALICE'], agent_roles['BOB'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + + queue_create( + default_chain_spec, + 42, + agent_roles['ALICE'], + tx_hash_hex, + tx_signed_raw_hex, + session=init_database, + ) + cache_gas_data( + tx_hash_hex, + tx_signed_raw_hex, + default_chain_spec.asdict(), + ) + set_ready(default_chain_spec, tx_hash_hex, session=init_database) + set_reserved(default_chain_spec, tx_hash_hex, session=init_database) + set_sent(default_chain_spec, tx_hash_hex, session=init_database) + + o = raw(tx_signed_raw_hex) + r = eth_rpc.do(o) + + o = receipt(tx_hash_hex) + r = eth_rpc.do(o) + assert r['status'] == 1 s = celery.signature( - 'cic_eth.eth.gas.resend_with_higher_gas', + 'cic_eth.eth.tx.sync_tx', [ tx_hash_hex, default_chain_spec.asdict(), ], - queue=None, + queue=None ) t = s.apply_async() r = t.get_leaf() + assert t.successful() - q = init_database.query(Otx) - q = q.filter(Otx.tx_hash==strip_0x(r)) - otx = q.first() - if otx == None: - raise NotLocalTxError(r) - - tx_after = unpack(bytes.fromhex(strip_0x(otx.signed_tx)), default_chain_spec) - logg.debug('gasprices before {} after {}'.format(tx_before['gasPrice'], tx_after['gasPrice'])) - assert tx_after['gasPrice'] > tx_before['gasPrice'] + init_database.commit() + o = Otx.load(tx_hash_hex, session=init_database) + assert o.status & StatusBits.FINAL == StatusBits.FINAL diff --git a/apps/cic-eth/tests/task/test_task_tx_misc.py b/apps/cic-eth/tests/task/test_task_tx_misc.py new file mode 100644 index 00000000..992abb83 --- /dev/null +++ b/apps/cic-eth/tests/task/test_task_tx_misc.py @@ -0,0 +1,170 @@ +# standard imports +import os +import logging + +# external imports +import pytest +import celery +from chainqueue.tx import create as queue_create +from chainlib.eth.nonce import ( + RPCNonceOracle, + OverrideNonceOracle, + ) +from chainlib.eth.gas import ( + OverrideGasOracle, + Gas, + ) +from chainlib.eth.tx import ( + TxFormat, + unpack, + receipt, + ) +from hexathon import ( + add_0x, + strip_0x, + ) +from chainqueue.state import ( + set_reserved, + set_ready, + ) + +logg = logging.getLogger() + + +def test_hashes_to_txs( + init_database, + default_chain_spec, + agent_roles, + eth_rpc, + eth_signer, + celery_session_worker, + ): + + nonce_oracle = OverrideNonceOracle(agent_roles['ALICE'], 42) + gas_oracle = OverrideGasOracle(price=1000000000, limit=21000) + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex_one, tx_signed_raw_hex_one) = c.create(agent_roles['ALICE'], agent_roles['BOB'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + + queue_create( + default_chain_spec, + 42, + agent_roles['ALICE'], + tx_hash_hex_one, + tx_signed_raw_hex_one, + session=init_database, + ) + + #nonce_oracle = OverrideNonceOracle(agent_roles['ALICE'], 43) + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex_two, tx_signed_raw_hex_two) = c.create(agent_roles['ALICE'], agent_roles['CAROL'], 200 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + + queue_create( + default_chain_spec, + 43, + agent_roles['ALICE'], + tx_hash_hex_two, + tx_signed_raw_hex_two, + session=init_database, + ) + + init_database.commit() + + bogus_one = add_0x(os.urandom(32).hex()) + bogus_two = add_0x(os.urandom(32).hex()) + + yarrgs = [ + bogus_one, + tx_hash_hex_two, + bogus_two, + tx_hash_hex_one, + ] + s = celery.signature( + 'cic_eth.eth.tx.hashes_to_txs', + [ + yarrgs, + ], + queue=None, + ) + t = s.apply_async() + r = t.get_leaf() + assert t.successful() + assert len(r) == 2 + + logg.debug('r {}'.format(r)) + txs = [ + tx_signed_raw_hex_two, + tx_signed_raw_hex_one, + ] + for tx in r: + txs.remove(add_0x(tx)) + assert len(txs) == 0 + + + +def test_double_send( + init_database, + default_chain_spec, + agent_roles, + eth_rpc, + eth_signer, + celery_session_worker, + ): + + nonce_oracle = RPCNonceOracle(agent_roles['ALICE'], conn=eth_rpc) + gas_oracle = OverrideGasOracle(price=1000000000, limit=21000) + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex_one, tx_signed_raw_hex_one) = c.create(agent_roles['ALICE'], agent_roles['BOB'], 100 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + + queue_create( + default_chain_spec, + 42, + agent_roles['ALICE'], + tx_hash_hex_one, + tx_signed_raw_hex_one, + session=init_database, + ) + set_ready(default_chain_spec, tx_hash_hex_one, session=init_database) + set_reserved(default_chain_spec, tx_hash_hex_one, session=init_database) + + c = Gas(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) + (tx_hash_hex_two, tx_signed_raw_hex_two) = c.create(agent_roles['ALICE'], agent_roles['CAROL'], 200 * (10 ** 6), tx_format=TxFormat.RLP_SIGNED) + + queue_create( + default_chain_spec, + 43, + agent_roles['ALICE'], + tx_hash_hex_two, + tx_signed_raw_hex_two, + session=init_database, + ) + + set_ready(default_chain_spec, tx_hash_hex_two, session=init_database) + set_reserved(default_chain_spec, tx_hash_hex_two, session=init_database) + init_database.commit() + + yarrgs = [ + tx_signed_raw_hex_one, + tx_signed_raw_hex_two, + ] + s = celery.signature( + 'cic_eth.eth.tx.send', + [ + yarrgs, + default_chain_spec.asdict(), + ], + queue=None + ) + t = s.apply_async() + r = t.get_leaf() + assert t.successful() + + o = receipt(tx_hash_hex_one) + r = eth_rpc.do(o) + assert r['status'] == 1 + + o = receipt(tx_hash_hex_two) + r = eth_rpc.do(o) + assert r['status'] == 1 + + + diff --git a/apps/cic-eth/tests/testdata/Bogus.bin b/apps/cic-eth/tests/testdata/Bogus.bin new file mode 100644 index 00000000..eacfe1b4 --- /dev/null +++ b/apps/cic-eth/tests/testdata/Bogus.bin @@ -0,0 +1 @@ +60806040526000805534801561001457600080fd5b50610181806100246000396000f3fe608060405234801561001057600080fd5b5060043610610053576000357c0100000000000000000000000000000000000000000000000000000000900480630dbe671f146100585780631817835814610076575b600080fd5b610060610080565b60405161006d91906100ae565b60405180910390f35b61007e610086565b005b60005481565b600080815480929190610098906100d3565b9190505550565b6100a8816100c9565b82525050565b60006020820190506100c3600083018461009f565b92915050565b6000819050919050565b60006100de826100c9565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8214156101115761011061011c565b5b600182019050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fdfea264697066735822122034ad8e91e864f030d47f5b93e281869206c1b203c36dc79a209ac9c9c16e577564736f6c63430008040033 \ No newline at end of file diff --git a/apps/cic-eth/tests/testdata/Bogus.sol b/apps/cic-eth/tests/testdata/Bogus.sol new file mode 100644 index 00000000..17e3bb88 --- /dev/null +++ b/apps/cic-eth/tests/testdata/Bogus.sol @@ -0,0 +1,10 @@ +pragma solidity ^0.8.0; + +contract Bogus { + + uint256 public a = 0; + + function poke() public { + a++; + } +} diff --git a/apps/cic-eth/tests/unit/ext/test_address.py b/apps/cic-eth/tests/unit/ext/test_address.py index 2a768928..b7d92b69 100644 --- a/apps/cic-eth/tests/unit/ext/test_address.py +++ b/apps/cic-eth/tests/unit/ext/test_address.py @@ -19,6 +19,7 @@ def test_translate( agent_roles, cic_registry, init_celery_tasks, + register_lookups, ): nonce_oracle = RPCNonceOracle(contract_roles['CONTRACT_DEPLOYER'], eth_rpc) diff --git a/apps/cic-eth/tests/unit/test_registry_connect.py b/apps/cic-eth/tests/unit/test_registry_connect.py new file mode 100644 index 00000000..62f14c1e --- /dev/null +++ b/apps/cic-eth/tests/unit/test_registry_connect.py @@ -0,0 +1,22 @@ +# local imports +from cic_eth.registry import * + +def test_registry_connect( + eth_rpc, + default_chain_spec, + address_declarator, + token_registry, + contract_roles, + purge_lookups, + registry, + agent_roles, + ): + + r = connect(eth_rpc, default_chain_spec, registry, sender_address=contract_roles['CONTRACT_DEPLOYER']) + + connect_declarator(eth_rpc, default_chain_spec, [agent_roles['ALICE']], sender_address=contract_roles['CONTRACT_DEPLOYER']) + r.by_name('AddressDeclarator', sender_address=contract_roles['CONTRACT_DEPLOYER']) + + connect_token_registry(eth_rpc, default_chain_spec, sender_address=contract_roles['CONTRACT_DEPLOYER']) + r.by_name('TokenRegistry', sender_address=contract_roles['CONTRACT_DEPLOYER']) + diff --git a/apps/cic-eth/tests/unit/test_stat.py b/apps/cic-eth/tests/unit/test_stat.py new file mode 100644 index 00000000..5da49fdc --- /dev/null +++ b/apps/cic-eth/tests/unit/test_stat.py @@ -0,0 +1,18 @@ +# standard imports +import datetime + +# local imports +from cic_eth.stat import init_chain_stat + + +def test_chain_stat( + eth_rpc, + init_eth_tester, + ): + + now = int(datetime.datetime.now().timestamp()) + 1 + for i in range(11): + init_eth_tester.time_travel(now + (i * 2)) + + s = init_chain_stat(eth_rpc, block_start=0) + assert s.block_average() == 2 From e2946052e0d1f51feab0dcec40524187a107ab84 Mon Sep 17 00:00:00 2001 From: Geoff Turk Date: Wed, 2 Jun 2021 14:53:53 +0200 Subject: [PATCH 03/15] Add more better mocked data --- apps/data-seeding/create_import_users.py | 130 +++++++++++++++-------- 1 file changed, 83 insertions(+), 47 deletions(-) diff --git a/apps/data-seeding/create_import_users.py b/apps/data-seeding/create_import_users.py index 231dcc4d..58e0f137 100644 --- a/apps/data-seeding/create_import_users.py +++ b/apps/data-seeding/create_import_users.py @@ -2,27 +2,24 @@ # standard imports import json -import time import datetime import random import logging import os -import base64 import hashlib -import sys import argparse import random # external imports -import vobject import celery from faker import Faker +from collections import OrderedDict import confini from cic_types.models.person import ( - Person, - generate_vcard_from_contact_data, - get_contact_data_from_vcard, - ) + Person, + generate_vcard_from_contact_data, + get_contact_data_from_vcard, +) from chainlib.eth.address import to_checksum_address import phonenumbers @@ -32,17 +29,21 @@ logg = logging.getLogger() fake = Faker(['sl', 'en_US', 'no', 'de', 'ro']) script_dir = os.path.realpath(os.path.dirname(__file__)) -#config_dir = os.environ.get('CONFINI_DIR', '/usr/local/etc/cic') +# config_dir = os.environ.get('CONFINI_DIR', '/usr/local/etc/cic') config_dir = os.environ.get('CONFINI_DIR', os.path.join(script_dir, 'config')) argparser = argparse.ArgumentParser() argparser.add_argument('-c', type=str, default=config_dir, help='Config dir') -argparser.add_argument('--tag', type=str, action='append', help='Tags to add to record') -argparser.add_argument('--gift-threshold', type=int, help='If set, users will be funded with additional random balance (in token integer units)') +argparser.add_argument('--tag', type=str, action='append', + help='Tags to add to record') +argparser.add_argument('--gift-threshold', type=int, + help='If set, users will be funded with additional random balance (in token integer units)') argparser.add_argument('-v', action='store_true', help='Be verbose') argparser.add_argument('-vv', action='store_true', help='Be more verbose') -argparser.add_argument('--dir', default='out', type=str, help='path to users export dir tree') -argparser.add_argument('user_count', type=int, help='amount of users to generate') +argparser.add_argument('--dir', default='out', type=str, + help='path to users export dir tree') +argparser.add_argument('user_count', type=int, + help='amount of users to generate') args = argparser.parse_args() if args.v: @@ -60,22 +61,23 @@ dt_then = dt_now - datetime.timedelta(weeks=150) ts_now = int(dt_now.timestamp()) ts_then = int(dt_then.timestamp()) -celery_app = celery.Celery(broker=config.get('CELERY_BROKER_URL'), backend=config.get('CELERY_RESULT_URL')) +celery_app = celery.Celery(broker=config.get( + 'CELERY_BROKER_URL'), backend=config.get('CELERY_RESULT_URL')) gift_max = args.gift_threshold or 0 gift_factor = (10**6) categories = [ - "food/water", - "fuel/energy", - "education", - "health", - "shop", - "environment", - "transport", - "farming/labor", - "savingsgroup", - ] + "food/water", + "fuel/energy", + "education", + "health", + "shop", + "environment", + "transport", + "farming/labor", + "savingsgroup", +] phone_idx = [] @@ -88,6 +90,7 @@ if tags == None or len(tags) == 0: random.seed() + def genPhoneIndex(phone): h = hashlib.new('sha256') h.update(phone.encode('utf-8')) @@ -141,16 +144,16 @@ def genDob(): if random.random() > 0.5: dob['month'] = dob_src.month dob['day'] = dob_src.day - + return dob def gen(): old_blockchain_address = '0x' + os.urandom(20).hex() - old_blockchain_checksum_address = to_checksum_address(old_blockchain_address) + old_blockchain_checksum_address = to_checksum_address( + old_blockchain_address) gender = random.choice(['female', 'male', 'other']) phone = genPhone() - city = fake.city_name() v = genPersonal(phone) contact_data = get_contact_data_from_vcard(v) @@ -161,31 +164,63 @@ def gen(): p.date_of_birth = genDob() p.gender = gender p.identities = { - 'evm': { - 'oldchain:1': [ - old_blockchain_checksum_address, - ], - }, - } - p.location['area_name'] = city + 'evm': { + 'oldchain:1': [ + old_blockchain_checksum_address, + ], + }, + } + p.products = [fake.random_element(elements=OrderedDict( + [('fruit', 0.25), + ('maji', 0.05), + ('milk', 0.1), + ('teacher', 0.1), + ('doctor', 0.05), + ('boutique', 0.15), + ('recycling', 0.05), + ('farmer', 0.05), + ('oil', 0.05), + ('pastor', 0.1), + ('chama', 0.03), + ('pastor', 0.01), + ('bzrTsuZieaq', 0.01) + ]))] + p.location['area_name'] = fake.random_element(elements=OrderedDict( + [('mnarani', 0.05), + ('chilumani', 0.1), + ('madewani', 0.1), + ('kisauni', 0.05), + ('bangla', 0.1), + ('kabiro', 0.1), + ('manyani', 0.05), + ('ruben', 0.15), + ('makupa', 0.05), + ('kingston', 0.05), + ('rangala', 0.05), + ('homabay', 0.1), + ('nakuru', 0.03), + ('kajiado', 0.01), + ('zurtWicKtily', 0.01) + ])) if random.randint(0, 1): - p.location['latitude'] = (random.random() + 180) - 90 #fake.local_latitude() - p.location['longitude'] = (random.random() + 360) - 180 #fake.local_latitude() + # fake.local_latitude() + p.location['latitude'] = (random.random() + 180) - 90 + # fake.local_latitude() + p.location['longitude'] = (random.random() + 360) - 180 - return (old_blockchain_checksum_address, phone, p) def prepareLocalFilePath(datadir, address): parts = [ - address[:2], - address[2:4], - ] + address[:2], + address[2:4], + ] dirs = '{}/{}/{}'.format( - datadir, - parts[0], - parts[1], - ) + datadir, + parts[0], + parts[1], + ) os.makedirs(dirs, exist_ok=True) return dirs @@ -238,9 +273,10 @@ if __name__ == '__main__': ft.write('{}:{}\n'.format(eth, ','.join(tags))) amount = genAmount() - fa.write('{},{}\n'.format(eth,amount)) - logg.debug('pidx {}, uid {}, eth {}, amount {}, phone {}'.format(pidx, uid, eth, amount, phone)) - + fa.write('{},{}\n'.format(eth, amount)) + logg.debug('pidx {}, uid {}, eth {}, amount {}, phone {}'.format( + pidx, uid, eth, amount, phone)) + i += 1 ft.close() From 41a96b5584fef9daa0763c9bfbdbc7b3c2f7fbec Mon Sep 17 00:00:00 2001 From: nolash Date: Wed, 2 Jun 2021 17:11:15 +0200 Subject: [PATCH 04/15] Int comparisons on block numbers in cic cache lookup --- apps/cic-cache/cic_cache/runnable/daemons/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cic-cache/cic_cache/runnable/daemons/query.py b/apps/cic-cache/cic_cache/runnable/daemons/query.py index bafde932..b2576690 100644 --- a/apps/cic-cache/cic_cache/runnable/daemons/query.py +++ b/apps/cic-cache/cic_cache/runnable/daemons/query.py @@ -89,7 +89,7 @@ def process_transactions_all_data(session, env): offset = r[1] end = r[2] - if r[2] < r[1]: + if int(r[2]) < int(r[1]): raise ValueError('cart before the horse, dude') c = DataCache(session) From 0ad0f9981c914add8237917f9dde0b08a44ddd8d Mon Sep 17 00:00:00 2001 From: Louis Holbrook Date: Wed, 2 Jun 2021 16:09:39 +0000 Subject: [PATCH 05/15] Upgrade cic-eth package for imports --- apps/cic-eth/cic_eth/version.py | 2 +- apps/data-seeding/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cic-eth/cic_eth/version.py b/apps/cic-eth/cic_eth/version.py index 18ac1dd1..b233a1e3 100644 --- a/apps/cic-eth/cic_eth/version.py +++ b/apps/cic-eth/cic_eth/version.py @@ -10,7 +10,7 @@ version = ( 0, 11, 0, - 'beta.14', + 'beta.15', ) version_object = semver.VersionInfo( diff --git a/apps/data-seeding/requirements.txt b/apps/data-seeding/requirements.txt index fcc526d4..f0b55efb 100644 --- a/apps/data-seeding/requirements.txt +++ b/apps/data-seeding/requirements.txt @@ -1,5 +1,5 @@ cic-base[full_graph]==0.1.2b11 sarafu-faucet==0.0.3a3 -cic-eth==0.11.0b14 +cic-eth==0.11.0b15 cic-types==0.1.0a11 crypto-dev-signer==0.4.14b3 From 634d3fb4010e4856396dfc2fc1cb546aedbf6db7 Mon Sep 17 00:00:00 2001 From: Louis Holbrook Date: Wed, 2 Jun 2021 17:55:46 +0000 Subject: [PATCH 06/15] Bring import readme up-to-date --- apps/data-seeding/README.md | 68 +++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/apps/data-seeding/README.md b/apps/data-seeding/README.md index 32dde8d6..00db7d94 100644 --- a/apps/data-seeding/README.md +++ b/apps/data-seeding/README.md @@ -89,12 +89,7 @@ After this step is run, you can find top-level ethereum addresses (like the cic #### Custodial provisions -response_data = send_ussd_request(address, self.data_dir) - state = response_data[:3] - out = response_data[4:] - m = '{} {}'.format(state, out[:7]) - if m != 'CON Welcome': - raise VerifierError(response_data, 'ussd') + This step is _only_ needed if you are importing using `cic_eth` or `cic_ussd` `RUN_MASK=2 docker-compose up contract-migration` @@ -128,12 +123,16 @@ The keystore file used for transferring external opening balances tracker is rel All external balance transactions are saved in raw wire format in `/txs`, with transaction hash as file name. +If the contract migrations have been executed with the default "giftable" token contract, then the `token_symbol` in the `import_balance` scripts should be set to `GFT`. + #### Alternative 1 - Sovereign wallet import - `eth` -First, make a note of the **block height** before running anything. +First, make a note of the **block height** before running anything: + +`eth-info -p http://localhost:63545` To import, run to _completion_: @@ -143,7 +142,7 @@ After the script completes, keystore files for all generated accouts will be fou Then run: -`python eth/import_balance.py -v -c config -r -p --offset -y ../keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c ` +`python eth/import_balance.py -v -c config -r -p --token-symbol --offset -y ../keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c ` @@ -151,7 +150,7 @@ Then run: Run in sequence, in first terminal: -`python cic_eth/import_balance.py -v -c config -p -r -y ../keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c --head out` +`python cic_eth/import_balance.py -v -c config -p -r --token-symbol -y ../keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c --head out` In another terminal: @@ -168,14 +167,40 @@ If you have previously run the `cic_ussd` import incompletely, it could be a goo Then, in sequence, run in first terminal: -`python cic_eth/import_balance.py -v -c config -p -r -y ../keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c out` +`python cic_eth/import_balance.py -v -c config -p -r --token-symbol -y ../keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c out` In second terminal: `python cic_ussd/import_users.py -v -c config out` + +### Step 4 - Metadata import (optional) + +The metadata import scripts can be run at any time after step 1 has been completed. + + +#### Importing user metadata + +To import the main user metadata structs, run: + +`node cic_meta/import_meta.js ` + +Monitors a folder for output from the `import_users.py` script, adding the metadata found to the `cic-meta` service. + +If _number of users_ is omitted the script will run until manually interrupted. + + + +#### Importing phone pointer + +`node cic_meta/import_meta_phone.js ` + +If you imported using `cic_ussd`, the phone pointer is _already added_ and this script will do nothing. + + ##### Importing pins and ussd data (optional) + Once the user imports are complete the next step should be importing the user's pins and auxiliary ussd data. This can be done in 3 steps: In one terminal run: @@ -199,29 +224,6 @@ The balance script is a celery task worker, and will not exit by itself in its c The connection parameters for the `cic-ussd-server` is currently _hardcoded_ in the `import_users.py` script file. -### Step 4 - Metadata import (optional) - -The metadata import scripts can be run at any time after step 1 has been completed. - - -#### Importing user metadata - -To import the main user metadata structs, run: - -`node cic_meta/import_meta.js ` - -Monitors a folder for output from the `import_users.py` script, adding the metadata found to the `cic-meta` service. - -If _number of users_ is omitted the script will run until manually interrupted. - - -#### Importing phone pointer - -`node cic_meta/import_meta_phone.js ` - -If you imported using `cic_ussd`, the phone pointer is _already added_ and this script will do nothing. - - ### Step 5 - Verify `python verify.py -v -c config -r -p ` From ed9e03289001f052babba2be373e09ba02978ed0 Mon Sep 17 00:00:00 2001 From: Philip Wafula Date: Thu, 3 Jun 2021 13:40:51 +0000 Subject: [PATCH 07/15] Philip/management integration tests --- apps/cic-meta/scripts/server/handlers.ts | 4 +- .../cic_notify/tasks/sms/africastalking.py | 2 +- apps/cic-notify/setup.cfg | 1 + apps/cic-ussd/.config/test/integration.ini | 4 + apps/cic-ussd/cic_ussd/db/ussd_menu.json | 32 +- apps/cic-ussd/cic_ussd/metadata/base.py | 44 +- apps/cic-ussd/cic_ussd/metadata/signer.py | 3 +- apps/cic-ussd/cic_ussd/processor.py | 9 +- .../cic_ussd/state_machine/logic/pin.py | 17 +- .../cic_ussd/state_machine/logic/user.py | 63 +- apps/cic-ussd/cic_ussd/translation.py | 2 + apps/cic-ussd/requirements.txt | 6 +- .../states/account_management_states.json | 5 +- apps/cic-ussd/test_requirements.txt | 8 +- apps/cic-ussd/tests/conftest.py | 1 + apps/cic-ussd/tests/fixtures/integration.py | 249 +++ apps/cic-ussd/tests/helpers/accounts.py | 26 + .../tests/integration/ext/validator.py | 11 + apps/cic-ussd/tests/integration/run.sh | 2 + .../test_account_creation.tavern.yaml | 466 +++++ .../test_account_management.tavern.yaml | 587 ++++++ .../test_profile_management.tavern.yaml | 1573 +++++++++++++++++ .../integration/test_transactions.tavern.yaml | 282 +++ .../gender_setting_transitions.json | 12 +- .../location_setting_transitions.json | 12 +- .../transitions/name_setting_transitions.json | 41 +- .../transitions/pin_setting_transitions.json | 6 +- .../products_setting_transitions.json | 9 +- .../user_metadata_transitions.json | 2 +- apps/cic-ussd/var/lib/locale/ussd.en.yml | 35 +- apps/cic-ussd/var/lib/locale/ussd.sw.yml | 47 +- 31 files changed, 3437 insertions(+), 124 deletions(-) create mode 100644 apps/cic-ussd/.config/test/integration.ini create mode 100644 apps/cic-ussd/tests/fixtures/integration.py create mode 100644 apps/cic-ussd/tests/helpers/accounts.py create mode 100644 apps/cic-ussd/tests/integration/ext/validator.py create mode 100644 apps/cic-ussd/tests/integration/run.sh create mode 100644 apps/cic-ussd/tests/integration/test_account_creation.tavern.yaml create mode 100644 apps/cic-ussd/tests/integration/test_account_management.tavern.yaml create mode 100644 apps/cic-ussd/tests/integration/test_profile_management.tavern.yaml create mode 100644 apps/cic-ussd/tests/integration/test_transactions.tavern.yaml diff --git a/apps/cic-meta/scripts/server/handlers.ts b/apps/cic-meta/scripts/server/handlers.ts index 90dc622a..08bf2d9b 100644 --- a/apps/cic-meta/scripts/server/handlers.ts +++ b/apps/cic-meta/scripts/server/handlers.ts @@ -31,7 +31,7 @@ function handleNoMergeGet(db, digest, keystore) { doh(e); }); }).catch((e) => { - console.error('mesage', e); + console.error('message', e); doh(e); }); }) @@ -46,7 +46,7 @@ function handleServerMergePost(data, db, digest, keystore, signer) { let e = undefined; let s = undefined; if (v === undefined) { - s = new Syncable(digest, data); + s = new Syncable(digest, o); s.onwrap = (e) => { whohoo(e.toJSON()); }; diff --git a/apps/cic-notify/cic_notify/tasks/sms/africastalking.py b/apps/cic-notify/cic_notify/tasks/sms/africastalking.py index af48ac58..e52b030f 100644 --- a/apps/cic-notify/cic_notify/tasks/sms/africastalking.py +++ b/apps/cic-notify/cic_notify/tasks/sms/africastalking.py @@ -56,7 +56,7 @@ class AfricasTalkingNotifier: response = self.api_client.send(message=message, recipients=[recipient]) logg.debug(f'africastalking response no-sender-id {response}') - recipients = response.get('Recipients') + recipients = response.get('SMSMessageData').get('Recipients') if len(recipients) != 1: status = response.get('SMSMessageData').get('Message') diff --git a/apps/cic-notify/setup.cfg b/apps/cic-notify/setup.cfg index d4606598..ea3a2f05 100644 --- a/apps/cic-notify/setup.cfg +++ b/apps/cic-notify/setup.cfg @@ -28,6 +28,7 @@ packages = cic_notify cic_notify.db cic_notify.db.models + cic_notify.ext cic_notify.tasks.sms cic_notify.runnable scripts = diff --git a/apps/cic-ussd/.config/test/integration.ini b/apps/cic-ussd/.config/test/integration.ini new file mode 100644 index 00000000..4fc189e9 --- /dev/null +++ b/apps/cic-ussd/.config/test/integration.ini @@ -0,0 +1,4 @@ +[test] +gift_value = 50.00 +server_url = http://localhost:63315/ +token_symbol = GFT diff --git a/apps/cic-ussd/cic_ussd/db/ussd_menu.json b/apps/cic-ussd/cic_ussd/db/ussd_menu.json index 0f0d24eb..ecc98173 100644 --- a/apps/cic-ussd/cic_ussd/db/ussd_menu.json +++ b/apps/cic-ussd/cic_ussd/db/ussd_menu.json @@ -238,13 +238,43 @@ "description": "Menu to display a user's entire profile", "display_key": "ussd.kenya.display_user_metadata", "name": "display_user_metadata", - "parent": "account_management" + "parent": "metadata_management" }, "41": { "description": "The recipient is not in the system", "display_key": "ussd.kenya.exit_invalid_recipient", "name": "exit_invalid_recipient", "parent": null + }, + "42": { + "description": "Pin entry menu for changing name data.", + "display_key": "ussd.kenya.name_edit_pin_authorization", + "name": "name_edit_pin_authorization", + "parent": "metadata_management" + }, + "43": { + "description": "Pin entry menu for changing gender data.", + "display_key": "ussd.kenya.gender_edit_pin_authorization", + "name": "gender_edit_pin_authorization", + "parent": "metadata_management" + }, + "44": { + "description": "Pin entry menu for changing location data.", + "display_key": "ussd.kenya.location_edit_pin_authorization", + "name": "location_edit_pin_authorization", + "parent": "metadata_management" + }, + "45": { + "description": "Pin entry menu for changing products data.", + "display_key": "ussd.kenya.products_edit_pin_authorization", + "name": "products_edit_pin_authorization", + "parent": "metadata_management" + }, + "46": { + "description": "Pin confirmation for pin change.", + "display_key": "ussd.kenya.new_pin_confirmation", + "name": "new_pin_confirmation", + "parent": "metadata_management" } } diff --git a/apps/cic-ussd/cic_ussd/metadata/base.py b/apps/cic-ussd/cic_ussd/metadata/base.py index a60538de..b4cb3691 100644 --- a/apps/cic-ussd/cic_ussd/metadata/base.py +++ b/apps/cic-ussd/cic_ussd/metadata/base.py @@ -78,28 +78,27 @@ class MetadataRequestsHandler(Metadata): :param data: The data to be stored in the metadata server. :type data: dict|str """ - data = json.dumps(data).encode('utf-8') + data = json.dumps(data) result = make_request(method='POST', url=self.url, data=data, headers=self.headers) metadata_http_error_handler(result=result) - metadata = result.content + metadata = result.json() self.edit(data=metadata) - def edit(self, data: bytes): + def edit(self, data: Union[Dict, str]): """ This function is responsible for editing data in the metadata server corresponding to a unique pointer. :param data: The data to be edited in the metadata server. - :type data: bytes + :type data: dict """ cic_meta_signer = Signer() signature = cic_meta_signer.sign_digest(data=data) algorithm = cic_meta_signer.get_operational_key().get('algo') - decoded_data = data.decode('utf-8') formatted_data = { - 'm': data.decode('utf-8'), + 'm': json.dumps(data), 's': { 'engine': self.engine, 'algo': algorithm, 'data': signature, - 'digest': json.loads(data).get('digest'), + 'digest': data.get('digest'), } } formatted_data = json.dumps(formatted_data) @@ -110,19 +109,32 @@ class MetadataRequestsHandler(Metadata): decoded_identifier = self.identifier.decode("utf-8") except UnicodeDecodeError: decoded_identifier = self.identifier.hex() - logg.info(f'identifier: {decoded_identifier}. metadata pointer: {self.metadata_pointer} set to: {decoded_data}.') + logg.info(f'identifier: {decoded_identifier}. metadata pointer: {self.metadata_pointer} set to: {data}.') def query(self): - """This function is responsible for querying the metadata server for data corresponding to a unique pointer.""" + """ + :return: + :rtype: + """ + # retrieve the metadata result = make_request(method='GET', url=self.url) metadata_http_error_handler(result=result) - response_data = result.json() - data = json.loads(response_data) - if not isinstance(data, dict): - raise ValueError(f'Invalid data object: {data}.') + + # json serialize retrieved data + result_data = result.json() + + # validate result data format + if not isinstance(result_data, dict): + raise ValueError(f'Invalid result data object: {result_data}.') + if result.status_code == 200 and self.cic_type == ':cic.person': + # validate person metadata person = Person() - deserialized_person = person.deserialize(person_data=data) - data = json.dumps(deserialized_person.serialize()) - cache_data(self.metadata_pointer, data=data) + person_data = person.deserialize(person_data=result_data) + + # format new person data for caching + data = json.dumps(person_data.serialize()) + + # cache metadata + cache_data(key=self.metadata_pointer, data=data) logg.debug(f'caching: {data} with key: {self.metadata_pointer}') diff --git a/apps/cic-ussd/cic_ussd/metadata/signer.py b/apps/cic-ussd/cic_ussd/metadata/signer.py index dc187d29..39544a32 100644 --- a/apps/cic-ussd/cic_ussd/metadata/signer.py +++ b/apps/cic-ussd/cic_ussd/metadata/signer.py @@ -47,14 +47,13 @@ class Signer: logg.debug(f'using signing key: {key_id}, algorithm: {key_algorithm}') return gpg_keys[0] - def sign_digest(self, data: bytes): + def sign_digest(self, data: dict): """ :param data: :type data: :return: :rtype: """ - data = json.loads(data) digest = data['digest'] key_id = self.get_operational_key().get('keyid') signature = self.gpg.sign(digest, passphrase=self.gpg_passphrase, keyid=key_id) diff --git a/apps/cic-ussd/cic_ussd/processor.py b/apps/cic-ussd/cic_ussd/processor.py index 864cd53f..3958e748 100644 --- a/apps/cic-ussd/cic_ussd/processor.py +++ b/apps/cic-ussd/cic_ussd/processor.py @@ -251,9 +251,9 @@ def process_display_user_metadata(user: Account, display_key: str): identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address), cic_type=':cic.person' ) - user_metadata = get_cached_data(key) - if user_metadata: - user_metadata = json.loads(user_metadata) + cached_metadata = get_cached_data(key) + if cached_metadata: + user_metadata = json.loads(cached_metadata) contact_data = get_contact_data_from_vcard(vcard=user_metadata.get('vcard')) logg.debug(f'{contact_data}') full_name = f'{contact_data.get("given")} {contact_data.get("family")}' @@ -433,7 +433,8 @@ def process_request(user_input: str, user: Account, ussd_session: Optional[dict] 'exit_invalid_pin', 'exit_invalid_new_pin', 'exit_pin_mismatch', - 'exit_invalid_request' + 'exit_invalid_request', + 'exit_successful_transaction' ] and person_metadata is not None: return UssdMenu.find_by_name(name='start') else: diff --git a/apps/cic-ussd/cic_ussd/state_machine/logic/pin.py b/apps/cic-ussd/cic_ussd/state_machine/logic/pin.py index 3577699d..5f51e602 100644 --- a/apps/cic-ussd/cic_ussd/state_machine/logic/pin.py +++ b/apps/cic-ussd/cic_ussd/state_machine/logic/pin.py @@ -13,7 +13,7 @@ import bcrypt # local imports from cic_ussd.db.models.account import AccountStatus, Account -from cic_ussd.encoder import PasswordEncoder, create_password_hash +from cic_ussd.encoder import PasswordEncoder, create_password_hash, check_password_hash from cic_ussd.operations import persist_session_to_db_task, create_or_update_session from cic_ussd.redis import InMemoryStore @@ -78,9 +78,13 @@ def save_initial_pin_to_session_data(state_machine_data: Tuple[str, dict, Accoun # set initial pin data initial_pin = create_password_hash(user_input) - session_data = { - 'initial_pin': initial_pin - } + if ussd_session.get('session_data'): + session_data = ussd_session.get('session_data') + session_data['initial_pin'] = initial_pin + else: + session_data = { + 'initial_pin': initial_pin + } # create new in memory ussd session with current ussd session data create_or_update_session( @@ -103,9 +107,8 @@ def pins_match(state_machine_data: Tuple[str, dict, Account]) -> bool: """ user_input, ussd_session, user = state_machine_data initial_pin = ussd_session.get('session_data').get('initial_pin') - fernet = PasswordEncoder(PasswordEncoder.key) - initial_pin = fernet.decrypt(initial_pin.encode()) - return bcrypt.checkpw(user_input.encode(), initial_pin) + logg.debug(f'USSD SESSION: {ussd_session}') + return check_password_hash(user_input, initial_pin) def complete_pin_change(state_machine_data: Tuple[str, dict, Account]): diff --git a/apps/cic-ussd/cic_ussd/state_machine/logic/user.py b/apps/cic-ussd/cic_ussd/state_machine/logic/user.py index da8a87db..d2db5d29 100644 --- a/apps/cic-ussd/cic_ussd/state_machine/logic/user.py +++ b/apps/cic-ussd/cic_ussd/state_machine/logic/user.py @@ -64,13 +64,17 @@ def process_gender_user_input(user: Account, user_input: str): if user.preferred_language == 'en': if user_input == '1': gender = 'Male' - else: + elif user_input == '2': gender = 'Female' + elif user_input == '3': + gender = 'Other' else: if user_input == '1': gender = 'Mwanaume' - else: + elif user_input == '2': gender = 'Mwanamke' + elif user_input == '3': + gender = 'Nyingine' return gender @@ -88,14 +92,18 @@ def save_metadata_attribute_to_session_data(state_machine_data: Tuple[str, dict, key = '' if 'given_name' in current_state: key = 'given_name' - elif 'family_name' in current_state: + + if 'family_name' in current_state: key = 'family_name' - elif 'gender' in current_state: + + if 'gender' in current_state: key = 'gender' user_input = process_gender_user_input(user=user, user_input=user_input) - elif 'location' in current_state: + + if 'location' in current_state: key = 'location' - elif 'products' in current_state: + + if 'products' in current_state: key = 'products' # check if there is existing session data @@ -121,12 +129,20 @@ def format_user_metadata(metadata: dict, user: Account): gender = metadata.get('gender') given_name = metadata.get('given_name') family_name = metadata.get('family_name') - location = { - "area_name": metadata.get('location') - } - products = [] - if metadata.get('products'): + + # check whether there's existing location data + if isinstance(metadata.get('location'), dict): + location = metadata.get('location') + else: + location = { + "area_name": metadata.get('location') + } + # check whether it is a list + if isinstance(metadata.get('products'), list): + products = metadata.get('products') + else: products = metadata.get('products').split(',') + phone_number = user.phone_number date_registered = int(user.created.replace().timestamp()) blockchain_address = user.blockchain_address @@ -192,28 +208,27 @@ def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, Account]): # validate user metadata person = Person() user_metadata = json.loads(user_metadata) - deserialized_person = person.deserialize(person_data=user_metadata) # edit specific metadata attribute if given_name: - deserialized_person.given_name = given_name - elif family_name: - deserialized_person.family_name = family_name - elif gender: - deserialized_person.gender = gender - elif location: + user_metadata['given_name'] = given_name + if family_name: + user_metadata['family_name'] = family_name + if gender: + user_metadata['gender'] = gender + if location: # get existing location metadata: location_data = user_metadata.get('location') location_data['area_name'] = location - deserialized_person.location = location_data - elif products: - deserialized_person.products = products + user_metadata['location'] = location_data + if products: + user_metadata['products'] = products - edited_metadata = deserialized_person.serialize() + user_metadata = format_user_metadata(metadata=user_metadata, user=user) s_edit_person_metadata = celery.signature( - 'cic_ussd.tasks.metadata.edit_person_metadata', - [blockchain_address, edited_metadata] + 'cic_ussd.tasks.metadata.create_person_metadata', + [blockchain_address, user_metadata] ) s_edit_person_metadata.apply_async(queue='cic-ussd') diff --git a/apps/cic-ussd/cic_ussd/translation.py b/apps/cic-ussd/cic_ussd/translation.py index 35fd1cd1..e512a5b3 100644 --- a/apps/cic-ussd/cic_ussd/translation.py +++ b/apps/cic-ussd/cic_ussd/translation.py @@ -19,4 +19,6 @@ def translation_for(key: str, preferred_language: Optional[str] = None, **kwargs """ if preferred_language: i18n.set('locale', preferred_language) + else: + i18n.set('locale', i18n.config.get('fallback')) return i18n.t(key, **kwargs) diff --git a/apps/cic-ussd/requirements.txt b/apps/cic-ussd/requirements.txt index 793d5021..b01d9d47 100644 --- a/apps/cic-ussd/requirements.txt +++ b/apps/cic-ussd/requirements.txt @@ -1,4 +1,4 @@ -cic_base[full_graph]~=0.1.2b2 -cic-eth~=0.11.0b9 -cic-notify~=0.4.0a4 +cic_base[full_graph]~=0.1.2b14 +cic-eth~=0.11.0b15 +cic-notify~=0.4.0a5 cic-types~=0.1.0a10 diff --git a/apps/cic-ussd/states/account_management_states.json b/apps/cic-ussd/states/account_management_states.json index 419ba157..456edd92 100644 --- a/apps/cic-ussd/states/account_management_states.json +++ b/apps/cic-ussd/states/account_management_states.json @@ -6,7 +6,10 @@ "enter_new_pin", "new_pin_confirmation", "display_user_metadata", - "standard_pin_authorization", + "name_edit_pin_authorization", + "gender_edit_pin_authorization", + "location_edit_pin_authorization", + "products_edit_pin_authorization", "account_balances_pin_authorization", "account_statement_pin_authorization", "account_balances" diff --git a/apps/cic-ussd/test_requirements.txt b/apps/cic-ussd/test_requirements.txt index 1ffd94fc..76aa808a 100644 --- a/apps/cic-ussd/test_requirements.txt +++ b/apps/cic-ussd/test_requirements.txt @@ -1,7 +1,11 @@ -pytest==6.0.1 +Faker==8.1.2 +faker-e164==0.1.0 +pytest==6.2.4 pytest-alembic==0.2.5 pytest-celery==0.0.0a1 pytest-cov==2.10.1 pytest-mock==3.3.1 +pytest-ordering==0.6 pytest-redis==2.0.0 -requests-mock==1.8.0 \ No newline at end of file +requests-mock==1.8.0 +tavern==1.14.2 \ No newline at end of file diff --git a/apps/cic-ussd/tests/conftest.py b/apps/cic-ussd/tests/conftest.py index 09c8ffc7..fa9389e3 100644 --- a/apps/cic-ussd/tests/conftest.py +++ b/apps/cic-ussd/tests/conftest.py @@ -6,6 +6,7 @@ from cic_types.pytest import * from tests.fixtures.config import * from tests.fixtures.db import * from tests.fixtures.celery import * +from tests.fixtures.integration import * from tests.fixtures.user import * from tests.fixtures.ussd_session import * from tests.fixtures.redis import * diff --git a/apps/cic-ussd/tests/fixtures/integration.py b/apps/cic-ussd/tests/fixtures/integration.py new file mode 100644 index 00000000..b4488dcc --- /dev/null +++ b/apps/cic-ussd/tests/fixtures/integration.py @@ -0,0 +1,249 @@ +# standard imports + +# external imports + +import pytest +from faker import Faker + +# local imports + +# test imports +from tests.helpers.accounts import phone_number, pin_number, session_id + + +fake = Faker() + + +@pytest.fixture(scope='function') +def generate_phone_number() -> str: + return phone_number() + + +@pytest.fixture(scope='function') +def generate_session_id() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def first_account_phone_number() -> str: + return phone_number() + + +@pytest.fixture(scope='session') +def second_account_phone_number() -> str: + return phone_number() + + +@pytest.fixture(scope='session') +def first_account_pin_number() -> str: + return pin_number() + + +@pytest.fixture(scope='session') +def second_account_pin_number() -> str: + return pin_number() + + +@pytest.fixture(scope='session') +def first_metadata_entry_session_id() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def second_metadata_entry_session_id() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def first_transaction_session_id() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def second_transaction_session_id() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def first_account_given_name() -> str: + return fake.first_name() + + +@pytest.fixture(scope='session') +def second_account_given_name() -> str: + return fake.first_name() + + +@pytest.fixture(scope='session') +def first_account_family_name() -> str: + return fake.last_name() + + +@pytest.fixture(scope='session') +def second_account_family_name() -> str: + return fake.last_name() + + +@pytest.fixture(scope='session') +def first_account_location() -> str: + return fake.city() + + +@pytest.fixture(scope='session') +def second_account_location() -> str: + return fake.city() + + +@pytest.fixture(scope='session') +def first_account_product() -> str: + return fake.color_name() + + +@pytest.fixture(scope='session') +def second_account_product() -> str: + return fake.color_name() + + +@pytest.fixture(scope='session') +def first_account_verify_balance_session_id() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def second_account_verify_balance_session_id() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def first_profile_management_session_id() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def second_profile_management_session_id() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def first_account_change_given_name() -> str: + return fake.first_name() + + +@pytest.fixture(scope='session') +def second_account_change_given_name() -> str: + return fake.first_name() + + +@pytest.fixture(scope='session') +def first_account_change_family_name() -> str: + return fake.last_name() + + +@pytest.fixture(scope='session') +def second_account_change_family_name() -> str: + return fake.last_name() + + +@pytest.fixture(scope='session') +def first_account_change_location() -> str: + return fake.city() + + +@pytest.fixture(scope='session') +def second_account_change_location() -> str: + return fake.city() + + +@pytest.fixture(scope='session') +def first_account_change_product() -> str: + return fake.color_name() + + +@pytest.fixture(scope='session') +def second_account_change_product() -> str: + return fake.color_name() + + +@pytest.fixture(scope='session') +def first_profile_management_session_id_1() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def second_profile_management_session_id_1() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def first_profile_management_session_id_2() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def second_profile_management_session_id_2() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def first_profile_management_session_id_3() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def second_profile_management_session_id_3() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def first_profile_management_session_id_4() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def second_profile_management_session_id_4() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def first_account_management_session_id() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def second_account_management_session_id() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def first_account_management_session_id_1() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def second_account_management_session_id_1() -> str: + return session_id() + + +@pytest.fixture(scope='session') +def first_account_new_pin_number() -> str: + return pin_number() + + +@pytest.fixture(scope='session') +def second_account_new_pin_number() -> str: + return pin_number() + + +@pytest.fixture(scope='session') +def gift_value(load_config): + return load_config.get('TEST_GIFT_VALUE') + + +@pytest.fixture(scope='session') +def server_url(load_config): + return load_config.get('TEST_SERVER_URL') + + +@pytest.fixture(scope='session') +def token_symbol(load_config): + return load_config.get('TEST_TOKEN_SYMBOL') diff --git a/apps/cic-ussd/tests/helpers/accounts.py b/apps/cic-ussd/tests/helpers/accounts.py new file mode 100644 index 00000000..9ba9bd78 --- /dev/null +++ b/apps/cic-ussd/tests/helpers/accounts.py @@ -0,0 +1,26 @@ +# standard imports +import random +import uuid + +# external imports +from faker import Faker +from faker_e164.providers import E164Provider + +# local imports + +# test imports + +fake = Faker() +fake.add_provider(E164Provider) + + +def phone_number() -> str: + return fake.e164('KE') + + +def session_id() -> str: + return uuid.uuid4().hex + + +def pin_number() -> int: + return random.randint(1000, 9999) diff --git a/apps/cic-ussd/tests/integration/ext/validator.py b/apps/cic-ussd/tests/integration/ext/validator.py new file mode 100644 index 00000000..cb8a1a3a --- /dev/null +++ b/apps/cic-ussd/tests/integration/ext/validator.py @@ -0,0 +1,11 @@ +import logging + + +logg = logging.getLogger() +logg.setLevel(logging.DEBUG) + + +def validate_response(response, expected_response): + """Makes sure that the response received matches the expected response""" + logg.debug(f'RESPONSE: {response.content.decode("utf-8")}') + assert response.content.decode('utf-8') == expected_response diff --git a/apps/cic-ussd/tests/integration/run.sh b/apps/cic-ussd/tests/integration/run.sh new file mode 100644 index 00000000..a6339c09 --- /dev/null +++ b/apps/cic-ussd/tests/integration/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +PYTHONPATH=. py.test --debug -vv --log-level debug -s --log-cli-level debug diff --git a/apps/cic-ussd/tests/integration/test_account_creation.tavern.yaml b/apps/cic-ussd/tests/integration/test_account_creation.tavern.yaml new file mode 100644 index 00000000..4fa83dd5 --- /dev/null +++ b/apps/cic-ussd/tests/integration/test_account_creation.tavern.yaml @@ -0,0 +1,466 @@ +test_name: Test the creation of accounts through the cic_user_ussd_server entrypoint. +marks: + - usefixtures: + - gift_value + - server_url + - token_symbol + - generate_session_id + - first_account_phone_number + - second_account_phone_number + - first_account_pin_number + - second_account_pin_number + - first_account_family_name + - second_account_family_name + - first_account_given_name + - second_account_given_name + - first_account_location + - second_account_location + - first_account_product + - second_account_product + - first_metadata_entry_session_id + - second_metadata_entry_session_id + - first + +stages: + - name: Initiate account creation process [first account]. + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{generate_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '175' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "END Your account is being created. You will receive an SMS when your account is ready.\nAkaunti yako ya Sarafu inatayarishwa. Utapokea ujumbe wa SMS akaunti yako ikiwa tayari.\n" + + - name: Initiate account creation process [second account]. + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{generate_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '175' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "END Your account is being created. You will receive an SMS when your account is ready.\nAkaunti yako ya Sarafu inatayarishwa. Utapokea ujumbe wa SMS akaunti yako ikiwa tayari.\n" + delay_after: 5 + + - name: Initaite account metadata entry [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_metadata_entry_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '61' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Welcome to Sarafu Network\n1. English\n2. Kiswahili\n3. Help" + + - name: Initaite account metadata entry [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_metadata_entry_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '61' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Welcome to Sarafu Network\n1. English\n2. Kiswahili\n3. Help" + + - name: Select preferred language [English] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_metadata_entry_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '64' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Please enter a new four number PIN for your account.\n0. Back" + + - name: Select preferred language [Kiswahili] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_metadata_entry_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '71' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Tafadhali weka pin mpya yenye nambari nne kwa akaunti yako\n0. Nyuma" + + - name: Enter pin number [{first_account_pin_number} - first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_metadata_entry_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "1*{first_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '44' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Enter your four number PIN again\n0. Back" + + - name: Enter pin number [second_account_pin_number - second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_metadata_entry_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*{second_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '31' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Weka PIN yako tena\n0. Nyuma" + + - name: Pin number confirmation [first_account_pin_number - first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_metadata_entry_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "1*{first_account_pin_number}*{first_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '28' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Enter first name\n0. Back" + + - name: Pin number confirmation [{second_account_pin_number} - second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_metadata_entry_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*{second_account_pin_number}*{second_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '37' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Weka jina lako la kwanza\n0. Nyuma" + + - name: Enter first name [first_account_given_name - first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_metadata_entry_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "1*{first_account_pin_number}*{first_account_pin_number}*{first_account_given_name}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '29' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Enter family name\n0. Back" + + - name: Enter first name [second_account_given_name - second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_metadata_entry_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*{second_account_pin_number}*{second_account_pin_number}*{second_account_given_name}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '37' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Weka jina lako la mwisho\n0. Nyuma" + + - name: Enter last name [first_account_family_name - first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_metadata_entry_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "1*{first_account_pin_number}*{first_account_pin_number}*{first_account_given_name}*{first_account_family_name}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '51' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Enter gender\n1. Male\n2. Female\n3. Other\n0. Back" + + - name: Enter last name [second_account_family_name - second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_metadata_entry_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*{second_account_pin_number}*{second_account_pin_number}*{second_account_given_name}*{second_account_family_name}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '64' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Weka jinsia yako\n1. Mwanaume\n2. Mwanamke\n3. Nyngine\n0. Nyuma" + + - name: Select gender [Male - first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_metadata_entry_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "1*{first_account_pin_number}*{first_account_pin_number}*{first_account_given_name}*{first_account_family_name}*1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '31' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Enter your location\n0. Back" + + - name: Select gender [Female - second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_metadata_entry_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*{second_account_pin_number}*{second_account_pin_number}*{second_account_given_name}*{second_account_family_name}*2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '27' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Weka eneo lako\n0. Nyuma" + + - name: Enter location [first_account_location - first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_metadata_entry_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "1*{first_account_pin_number}*{first_account_pin_number}*{first_account_given_name}*{first_account_family_name}*1*{first_account_location}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '55' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Please enter a product or service you offer\n0. Back" + + - name: Enter location [second_account_location - second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_metadata_entry_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*{second_account_pin_number}*{second_account_pin_number}*{second_account_given_name}*{second_account_family_name}*2*{second_account_location}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '42' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Weka bidhaa ama huduma unauza\n0. Nyuma" + + - name: Enter product [first_account_product - first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_metadata_entry_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "1*{first_account_pin_number}*{first_account_pin_number}*{first_account_given_name}*{first_account_family_name}*1*{first_account_location}*{first_account_product}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '51' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Balance {gift_value} {token_symbol}\n1. Send\n2. My Account\n3. Help" + delay_before: 10 + + - name: Enter product [second_account_product - second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_metadata_entry_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*{second_account_pin_number}*{second_account_pin_number}*{second_account_given_name}*{second_account_family_name}*2*{second_account_location}*{second_account_product}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '56' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Salio {gift_value} {token_symbol}\n1. Tuma\n2. Akaunti yangu\n3. Usaidizi" + delay_before: 10 diff --git a/apps/cic-ussd/tests/integration/test_account_management.tavern.yaml b/apps/cic-ussd/tests/integration/test_account_management.tavern.yaml new file mode 100644 index 00000000..03a5a4df --- /dev/null +++ b/apps/cic-ussd/tests/integration/test_account_management.tavern.yaml @@ -0,0 +1,587 @@ +test_name: Test performing account management operations. +marks: + - usefixtures: + - server_url + - token_symbol + - first_account_pin_number + - second_account_pin_number + - first_account_phone_number + - second_account_phone_number + - first_account_management_session_id + - second_account_management_session_id + - first_account_management_session_id_1 + - second_account_management_session_id_1 + - first_account_new_pin_number + - second_account_new_pin_number + - fourth + +stages: + - name: Account management start menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_account_management_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '51' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Balance 58.00 {token_symbol}\n1. Send\n2. My Account\n3. Help" + + - name: Account management start menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_account_management_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '56' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Salio 42.00 {token_symbol}\n1. Tuma\n2. Akaunti yangu\n3. Usaidizi" + + - name: Account management menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_account_management_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '105' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON My account\n1. My profile\n2. Change language\n3. Check balance\n4. Check statement\n5. Change PIN\n0. Back" + + - name: Account management menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_account_management_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '148' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Akaunti yangu\n1. Wasifu wangu\n2. Chagua lugha utakayotumia\n3. Angalia salio\n4. Angalia taarifa ya matumizi\n5. Badilisha nambari ya siri\n0. Nyuma" + + - name: Language change [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_account_management_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "2*2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '51' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Choose language\n1. English\n2. Kiswahili\n0. Back" + + - name: Language change [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_account_management_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '51' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Chagua lugha\n1. Kingereza\n2. Kiswahili\n0. Nyuma" + + - name: Select language [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_account_management_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "2*2*2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '30' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "END Asante kwa kutumia huduma." + + - name: Select language [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_account_management_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*2*1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '36' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "END Thank you for using the service." + + - name: Second account management start menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_account_management_session_id_1}" + phoneNumber: "{first_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '56' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Salio 58.00 {token_symbol}\n1. Tuma\n2. Akaunti yangu\n3. Usaidizi" + + - name: Second account management start menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_account_management_session_id_1}" + phoneNumber: "{second_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '51' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Balance 42.00 {token_symbol}\n1. Send\n2. My Account\n3. Help" + + - name: Second account management menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_account_management_session_id_1}" + phoneNumber: "{first_account_phone_number}" + text: "2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '148' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Akaunti yangu\n1. Wasifu wangu\n2. Chagua lugha utakayotumia\n3. Angalia salio\n4. Angalia taarifa ya matumizi\n5. Badilisha nambari ya siri\n0. Nyuma" + + - name: Second account management menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_account_management_session_id_1}" + phoneNumber: "{second_account_phone_number}" + text: "2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '105' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON My account\n1. My profile\n2. Change language\n3. Check balance\n4. Check statement\n5. Change PIN\n0. Back" + + - name: Check balance [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_account_management_session_id_1}" + phoneNumber: "{first_account_phone_number}" + text: "2*3" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '49' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Tafadhali weka PIN yako kuona salio.\n0. Nyuma" + + - name: Check balance [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_account_management_session_id_1}" + phoneNumber: "{second_account_phone_number}" + text: "2*3" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '50' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Please enter your PIN to view balances\n0. Back" + + - name: Display balances [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_account_management_session_id_1}" + phoneNumber: "{first_account_phone_number}" + text: "2*3*{first_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Salio zako ni zifuatazo:\n salio: 58.00 {token_symbol}\n ushuru: {token_symbol}\n tuzo: {token_symbol}\n0. Nyuma" + + - name: Display balances [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_account_management_session_id_1}" + phoneNumber: "{second_account_phone_number}" + text: "2*3*{second_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Your balances are as follows:\n balance: 42.00 {token_symbol}\n fees: {token_symbol}\n rewards: {token_symbol}\n0. Back" + + - name: Resume account management menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_account_management_session_id_1}" + phoneNumber: "{first_account_phone_number}" + text: "2*3*{first_account_pin_number}*0" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '148' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Akaunti yangu\n1. Wasifu wangu\n2. Chagua lugha utakayotumia\n3. Angalia salio\n4. Angalia taarifa ya matumizi\n5. Badilisha nambari ya siri\n0. Nyuma" + + - name: Resume account management menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_account_management_session_id_1}" + phoneNumber: "{second_account_phone_number}" + text: "2*3*{second_account_pin_number}*0" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '105' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON My account\n1. My profile\n2. Change language\n3. Check balance\n4. Check statement\n5. Change PIN\n0. Back" + + - name: Change pin number [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_account_management_session_id_1}" + phoneNumber: "{first_account_phone_number}" + text: "2*3*{first_account_pin_number}*0*5" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '34' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Weka nambari ya siri.\n0. Nyuma" + + - name: Change pin number [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_account_management_session_id_1}" + phoneNumber: "{second_account_phone_number}" + text: "2*3*{second_account_pin_number}*0*5" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '30' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Enter current PIN.\n0. Back" + + - name: Enter old pin [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_account_management_session_id_1}" + phoneNumber: "{first_account_phone_number}" + text: "2*3*{first_account_pin_number}*0*5*{first_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '38' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Weka nambari ya siri mpya\n0. Nyuma" + + - name: Enter old pin [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_account_management_session_id_1}" + phoneNumber: "{second_account_phone_number}" + text: "2*3*{second_account_pin_number}*0*5*{second_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '42' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Enter your new four number PIN\n0. Back" + + - name: Enter new pin [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_account_management_session_id_1}" + phoneNumber: "{first_account_phone_number}" + text: "2*3*{first_account_pin_number}*0*5*{first_account_pin_number}*{first_account_new_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '31' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Weka PIN yako tena\n0. Nyuma" + + - name: Enter new pin [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_account_management_session_id_1}" + phoneNumber: "{second_account_phone_number}" + text: "2*3*{second_account_pin_number}*0*5*{second_account_pin_number}*{second_account_new_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '48' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Enter your new four number PIN again\n0. Back" + + - name: Enter new pin confirmation [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_account_management_session_id_1}" + phoneNumber: "{first_account_phone_number}" + text: "2*3*{first_account_pin_number}*0*5*{first_account_pin_number}*{first_account_new_pin_number}*{first_account_new_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '91' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Ombi lako limetumwa. Utapokea uthibitishaji wa SMS kwa muda mfupi.\n00. Nyuma\n99. Ondoka" + + - name: Enter new pin confirmation [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_account_management_session_id_1}" + phoneNumber: "{second_account_phone_number}" + text: "2*3*{second_account_pin_number}*0*5*{second_account_pin_number}*{second_account_new_pin_number}*{second_account_new_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '82' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Your request has been sent. You will receive an SMS shortly.\n00. Back\n99. Exit" diff --git a/apps/cic-ussd/tests/integration/test_profile_management.tavern.yaml b/apps/cic-ussd/tests/integration/test_profile_management.tavern.yaml new file mode 100644 index 00000000..274f0265 --- /dev/null +++ b/apps/cic-ussd/tests/integration/test_profile_management.tavern.yaml @@ -0,0 +1,1573 @@ +test_name: Test editing account profile data. +marks: + - usefixtures: + - server_url + - token_symbol + - first_account_pin_number + - second_account_pin_number + - first_account_phone_number + - second_account_phone_number + - first_account_family_name + - second_account_family_name + - first_account_given_name + - second_account_given_name + - first_account_location + - second_account_location + - first_account_product + - second_account_product + - first_profile_management_session_id + - second_profile_management_session_id + - first_account_change_family_name + - second_account_change_family_name + - first_account_change_given_name + - second_account_change_given_name + - first_account_change_location + - second_account_change_location + - first_account_change_product + - second_account_change_product + - first_profile_management_session_id_1 + - second_profile_management_session_id_1 + - first_profile_management_session_id_2 + - second_profile_management_session_id_2 + - first_profile_management_session_id_3 + - second_profile_management_session_id_3 + - first_profile_management_session_id_4 + - second_profile_management_session_id_4 + - third + +stages: + - name: Profile management start menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '51' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Balance 58.00 {token_symbol}\n1. Send\n2. My Account\n3. Help" + + - name: Profile management start menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '56' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Salio 42.00 {token_symbol}\n1. Tuma\n2. Akaunti yangu\n3. Usaidizi" + + - name: Account management menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '105' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON My account\n1. My profile\n2. Change language\n3. Check balance\n4. Check statement\n5. Change PIN\n0. Back" + + - name: Account management menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '148' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Akaunti yangu\n1. Wasifu wangu\n2. Chagua lugha utakayotumia\n3. Angalia salio\n4. Angalia taarifa ya matumizi\n5. Badilisha nambari ya siri\n0. Nyuma" + + - name: Profile management menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "2*1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '103' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON My profile\n1. Edit name\n2. Edit gender\n3. Edit location\n4. Edit products\n5. View my profile\n0. Back" + + - name: Profile management menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '104' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Wasifu wangu\n1. Weka jina\n2. Weka jinsia\n3. Weka eneo\n4. Weka bidhaa\n5. Angalia wasifu wako\n0. Nyuma" + + - name: Enter pin to view profile [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*5" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '33' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Please enter your PIN\n0. Back" + + - name: Enter pin to view profile [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*5" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '36' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Tafadhali weka PIN yako\n0. Nyuma" + + - name: Display profile [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*5*{first_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Your details are:\n Name: {first_account_given_name} {first_account_family_name}\n Gender: Male\n Location: {first_account_location}\n You sell: {first_account_product}\n0. Back" + + - name: Display profile [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*5*{second_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Wasifu wako una maelezo yafuatayo:\n Jina: {second_account_given_name} {second_account_family_name}\n Jinsia: Mwanamke\n Eneo: {second_account_location}\n Unauza: {second_account_product}\n0. Nyuma" + + - name: Second profile management menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*5*{first_account_pin_number}*0" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '103' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON My profile\n1. Edit name\n2. Edit gender\n3. Edit location\n4. Edit products\n5. View my profile\n0. Back" + + - name: Second profile management menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*5*{second_account_pin_number}*0" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '104' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Wasifu wangu\n1. Weka jina\n2. Weka jinsia\n3. Weka eneo\n4. Weka bidhaa\n5. Angalia wasifu wako\n0. Nyuma" + + - name: Edit name [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*5*{second_account_pin_number}*0*1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '28' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Enter first name\n0. Back" + + - name: Edit name [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*5*{second_account_pin_number}*0*1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '37' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Weka jina lako la kwanza\n0. Nyuma" + + - name: Enter given name [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*5*{second_account_pin_number}*0*1*{first_account_change_given_name}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '29' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Enter family name\n0. Back" + + - name: Enter given name [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*5*{second_account_pin_number}*0*1*{second_account_change_given_name}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '37' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Weka jina lako la mwisho\n0. Nyuma" + + - name: Enter family name [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*5*{second_account_pin_number}*0*1*{first_account_change_given_name}*{first_account_change_family_name}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '33' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Please enter your PIN\n0. Back" + + - name: Enter family name [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*5*{second_account_pin_number}*0*1*{second_account_change_given_name}*{second_account_change_family_name}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '36' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Tafadhali weka PIN yako\n0. Nyuma" + + - name: Enter name change pin [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*5*{second_account_pin_number}*0*1*{first_account_change_given_name}*{first_account_change_family_name}*{first_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '36' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "END Thank you for using the service." + + - name: Enter name change pin [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*5*{second_account_pin_number}*0*1*{second_account_change_given_name}*{second_account_change_family_name}*{second_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '30' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "END Asante kwa kutumia huduma." + + - name: Second profile management start menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_1}" + phoneNumber: "{first_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '51' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Balance 58.00 {token_symbol}\n1. Send\n2. My Account\n3. Help" + + - name: Second profile management start menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_1}" + phoneNumber: "{second_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '56' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Salio 42.00 {token_symbol}\n1. Tuma\n2. Akaunti yangu\n3. Usaidizi" + + - name: Second account management menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_1}" + phoneNumber: "{first_account_phone_number}" + text: "2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '105' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON My account\n1. My profile\n2. Change language\n3. Check balance\n4. Check statement\n5. Change PIN\n0. Back" + + - name: Second account management [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_1}" + phoneNumber: "{second_account_phone_number}" + text: "2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '148' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Akaunti yangu\n1. Wasifu wangu\n2. Chagua lugha utakayotumia\n3. Angalia salio\n4. Angalia taarifa ya matumizi\n5. Badilisha nambari ya siri\n0. Nyuma" + + - name: Second profile management menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_1}" + phoneNumber: "{first_account_phone_number}" + text: "2*1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '103' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON My profile\n1. Edit name\n2. Edit gender\n3. Edit location\n4. Edit products\n5. View my profile\n0. Back" + + - name: Second profile management menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_1}" + phoneNumber: "{second_account_phone_number}" + text: "2*1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '104' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Wasifu wangu\n1. Weka jina\n2. Weka jinsia\n3. Weka eneo\n4. Weka bidhaa\n5. Angalia wasifu wako\n0. Nyuma" + + - name: Gender change [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_1}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '51' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Enter gender\n1. Male\n2. Female\n3. Other\n0. Back" + + - name: Gender change [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_1}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '64' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Weka jinsia yako\n1. Mwanaume\n2. Mwanamke\n3. Nyngine\n0. Nyuma" + + - name: Select gender [female - first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_1}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*2*2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '33' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Please enter your PIN\n0. Back" + + - name: Select gender [male - second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_1}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*2*1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '36' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Tafadhali weka PIN yako\n0. Nyuma" + + - name: Enter gender change pin [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_1}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*2*2*{first_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '36' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "END Thank you for using the service." + + - name: Enter gender change pin [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_1}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*2*1*{second_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '30' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "END Asante kwa kutumia huduma." + + - name: Third profile management start menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_2}" + phoneNumber: "{first_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '51' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Balance 58.00 {token_symbol}\n1. Send\n2. My Account\n3. Help" + + - name: Third profile management start menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_2}" + phoneNumber: "{second_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '56' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Salio 42.00 {token_symbol}\n1. Tuma\n2. Akaunti yangu\n3. Usaidizi" + + - name: Third account management menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_2}" + phoneNumber: "{first_account_phone_number}" + text: "2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '105' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON My account\n1. My profile\n2. Change language\n3. Check balance\n4. Check statement\n5. Change PIN\n0. Back" + + - name: Third account management [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_2}" + phoneNumber: "{second_account_phone_number}" + text: "2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '148' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Akaunti yangu\n1. Wasifu wangu\n2. Chagua lugha utakayotumia\n3. Angalia salio\n4. Angalia taarifa ya matumizi\n5. Badilisha nambari ya siri\n0. Nyuma" + + - name: Third profile management menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_2}" + phoneNumber: "{first_account_phone_number}" + text: "2*1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '103' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON My profile\n1. Edit name\n2. Edit gender\n3. Edit location\n4. Edit products\n5. View my profile\n0. Back" + + - name: Third profile management menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_2}" + phoneNumber: "{second_account_phone_number}" + text: "2*1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '104' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Wasifu wangu\n1. Weka jina\n2. Weka jinsia\n3. Weka eneo\n4. Weka bidhaa\n5. Angalia wasifu wako\n0. Nyuma" + + - name: Location change [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_2}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*3" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '31' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Enter your location\n0. Back" + + - name: Location change [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_2}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*3" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '27' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Weka eneo lako\n0. Nyuma" + + - name: Enter location change [first_account_change_location - first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_2}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*3*{first_account_change_location}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '33' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Please enter your PIN\n0. Back" + + - name: Enter location change [second_account_change_location - second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_2}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*3*{second_account_change_location}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '36' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Tafadhali weka PIN yako\n0. Nyuma" + + - name: Enter location change pin [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_2}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*3*{first_account_change_location}*{first_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '36' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "END Thank you for using the service." + + - name: Enter location change pin [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_2}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*3*{second_account_change_location}*{second_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '30' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "END Asante kwa kutumia huduma." + + - name: Fourth profile management start menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_3}" + phoneNumber: "{first_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '51' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Balance 58.00 {token_symbol}\n1. Send\n2. My Account\n3. Help" + + - name: Fourth profile management start menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_3}" + phoneNumber: "{second_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '56' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Salio 42.00 {token_symbol}\n1. Tuma\n2. Akaunti yangu\n3. Usaidizi" + + - name: Fourth account management menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_3}" + phoneNumber: "{first_account_phone_number}" + text: "2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '105' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON My account\n1. My profile\n2. Change language\n3. Check balance\n4. Check statement\n5. Change PIN\n0. Back" + + - name: Fourth account management menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_3}" + phoneNumber: "{second_account_phone_number}" + text: "2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '148' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Akaunti yangu\n1. Wasifu wangu\n2. Chagua lugha utakayotumia\n3. Angalia salio\n4. Angalia taarifa ya matumizi\n5. Badilisha nambari ya siri\n0. Nyuma" + + - name: Fourth profile management menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_3}" + phoneNumber: "{first_account_phone_number}" + text: "2*1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '103' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON My profile\n1. Edit name\n2. Edit gender\n3. Edit location\n4. Edit products\n5. View my profile\n0. Back" + + - name: Fourth profile management menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_3}" + phoneNumber: "{second_account_phone_number}" + text: "2*1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '104' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Wasifu wangu\n1. Weka jina\n2. Weka jinsia\n3. Weka eneo\n4. Weka bidhaa\n5. Angalia wasifu wako\n0. Nyuma" + + - name: Product change [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_3}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*4" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '55' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Please enter a product or service you offer\n0. Back" + + - name: Product change [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_3}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*4" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '42' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Weka bidhaa ama huduma unauza\n0. Nyuma" + + - name: Enter product change [first_account_change_product - first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_3}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*4*{first_account_change_product}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '33' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Please enter your PIN\n0. Back" + + - name: Enter product change [second_account_change_product - second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_3}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*4*{second_account_change_product}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '36' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Tafadhali weka PIN yako\n0. Nyuma" + + - name: Enter product change pin [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_3}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*4*{first_account_change_product}*{first_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '36' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "END Thank you for using the service." + + - name: Enter product change pin [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_3}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*4*{second_account_change_product}*{second_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '30' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "END Asante kwa kutumia huduma." + + - name: Fifth profile managment start menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_4}" + phoneNumber: "{first_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '51' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Balance 58.00 {token_symbol}\n1. Send\n2. My Account\n3. Help" + + - name: Fifth profile managment start menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_4}" + phoneNumber: "{second_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '56' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Salio 42.00 {token_symbol}\n1. Tuma\n2. Akaunti yangu\n3. Usaidizi" + + - name: Fifth account management menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_4}" + phoneNumber: "{first_account_phone_number}" + text: "2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '105' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON My account\n1. My profile\n2. Change language\n3. Check balance\n4. Check statement\n5. Change PIN\n0. Back" + + - name: Fifth account management menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_4}" + phoneNumber: "{second_account_phone_number}" + text: "2" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '148' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Akaunti yangu\n1. Wasifu wangu\n2. Chagua lugha utakayotumia\n3. Angalia salio\n4. Angalia taarifa ya matumizi\n5. Badilisha nambari ya siri\n0. Nyuma" + + - name: Fifth profile management menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_4}" + phoneNumber: "{first_account_phone_number}" + text: "2*1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '103' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON My profile\n1. Edit name\n2. Edit gender\n3. Edit location\n4. Edit products\n5. View my profile\n0. Back" + + - name: Fifth profile management menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_4}" + phoneNumber: "{second_account_phone_number}" + text: "2*1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '104' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Wasifu wangu\n1. Weka jina\n2. Weka jinsia\n3. Weka eneo\n4. Weka bidhaa\n5. Angalia wasifu wako\n0. Nyuma" + + - name: Second enter pin to view profile [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_4}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*5" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '33' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Please enter your PIN\n0. Back" + + - name: Second enter pin to view profile [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_4}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*5" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '36' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Tafadhali weka PIN yako\n0. Nyuma" + + - name: Second display profile [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_4}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*5*{first_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Your details are:\n Name: {first_account_change_given_name} {first_account_change_family_name}\n Gender: Female\n Location: {first_account_change_location}\n You sell: {first_account_change_product}\n0. Back" + + - name: Second display profile [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_4}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*5*{second_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Wasifu wako una maelezo yafuatayo:\n Jina: {second_account_change_given_name} {second_account_change_family_name}\n Jinsia: Mwanaume\n Eneo: {second_account_change_location}\n Unauza: {second_account_change_product}\n0. Nyuma" + + - name: Return to profile management menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_4}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*5*{first_account_pin_number}*0" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '103' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON My profile\n1. Edit name\n2. Edit gender\n3. Edit location\n4. Edit products\n5. View my profile\n0. Back" + + - name: Return to profile management menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_4}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*5*{second_account_pin_number}*0" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '104' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Wasifu wangu\n1. Weka jina\n2. Weka jinsia\n3. Weka eneo\n4. Weka bidhaa\n5. Angalia wasifu wako\n0. Nyuma" + + - name: Resume start menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_profile_management_session_id_4}" + phoneNumber: "{first_account_phone_number}" + text: "2*1*5*{first_account_pin_number}*0*0" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '51' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Balance 58.00 {token_symbol}\n1. Send\n2. My Account\n3. Help" + + - name: Resume start menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_profile_management_session_id_4}" + phoneNumber: "{second_account_phone_number}" + text: "2*1*5*{second_account_pin_number}*0*0" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '56' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Salio 42.00 {token_symbol}\n1. Tuma\n2. Akaunti yangu\n3. Usaidizi" \ No newline at end of file diff --git a/apps/cic-ussd/tests/integration/test_transactions.tavern.yaml b/apps/cic-ussd/tests/integration/test_transactions.tavern.yaml new file mode 100644 index 00000000..9a50f012 --- /dev/null +++ b/apps/cic-ussd/tests/integration/test_transactions.tavern.yaml @@ -0,0 +1,282 @@ +test_name: Test that the two test accounts can trade with each other. +marks: + - usefixtures: + - gift_value + - server_url + - token_symbol + - first_account_family_name + - second_account_family_name + - first_account_given_name + - second_account_given_name + - first_account_phone_number + - second_account_phone_number + - first_account_pin_number + - second_account_pin_number + - first_transaction_session_id + - second_transaction_session_id + - first_account_verify_balance_session_id + - second_account_verify_balance_session_id + - second + +stages: + - name: Transactions start menu [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_transaction_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '51' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Balance {gift_value} {token_symbol}\n1. Send\n2. My Account\n3. Help" + + - name: Transactions start menu [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_transaction_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '56' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Salio {gift_value} {token_symbol}\n1. Tuma\n2. Akaunti yangu\n3. Usaidizi" + + - name: Initate transcation attempt [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_transaction_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '30' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Enter phone number\n0. Back" + + - name: Initate transcation attempt [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_transaction_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "1" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '33' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Weka nambari ya simu\n0. Nyuma" + + - name: Enter phone number [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_transaction_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "1*{second_account_phone_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '24' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Enter amount\n0. Back" + + - name: Enter phone number [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_transaction_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "1*{first_account_phone_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '25' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Weka kiwango\n0. Nyuma" + + - name: Enter transcation amount [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_transaction_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "1*{second_account_phone_number}*17" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON {second_account_given_name} {second_account_family_name} {second_account_phone_number} will receive 17.00 {token_symbol} from {first_account_given_name} {first_account_family_name} {first_account_phone_number}.\nPlease enter your PIN to confirm.\n0. Back" + + - name: Enter transcation amount [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_transaction_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "1*{first_account_phone_number}*25" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON {first_account_given_name} {first_account_family_name} {first_account_phone_number} atapokea 25.00 {token_symbol} kutoka kwa {second_account_given_name} {second_account_family_name} {second_account_phone_number}.\nTafadhali weka nambari yako ya siri kudhibitisha.\n0. Nyuma" + + - name: Pin to authorize transaction [first account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_transaction_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "1*{second_account_phone_number}*17*{first_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Your request has been sent. {second_account_given_name} {second_account_family_name} {second_account_phone_number} will receive 17.00 {token_symbol} from {first_account_given_name} {first_account_family_name} {first_account_phone_number}.\n00. Back\n99. Exit" + + - name: Pin to authorize transaction [second account] + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_transaction_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "1*{first_account_phone_number}*25*{second_account_pin_number}" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Ombi lako limetumwa. {first_account_given_name} {first_account_family_name} {first_account_phone_number} atapokea 25.00 {token_symbol} kutoka kwa {second_account_given_name} {second_account_family_name} {second_account_phone_number}.\n00. Nyuma\n99. Ondoka" + + - name: Verify balance changes [first account] + delay_before: 10 + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{first_account_verify_balance_session_id}" + phoneNumber: "{first_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '51' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Balance 58.00 {token_symbol}\n1. Send\n2. My Account\n3. Help" + + - name: Verify balance changes [second account] + delay_before: 10 + request: + url: "{server_url}" + data: + serviceCode: "*483*46#" + sessionId: "{second_account_verify_balance_session_id}" + phoneNumber: "{second_account_phone_number}" + text: "" + headers: + content-type: "application/x-www-form-urlencoded" + method: POST + response: + status_code: + - 200 + headers: + Content-Length: '56' + Content-Type: "text/plain" + verify_response_with: + function: ext.validator:validate_response + extra_kwargs: + expected_response: "CON Salio 42.00 {token_symbol}\n1. Tuma\n2. Akaunti yangu\n3. Usaidizi" \ No newline at end of file diff --git a/apps/cic-ussd/transitions/gender_setting_transitions.json b/apps/cic-ussd/transitions/gender_setting_transitions.json index 951be716..7881cdd7 100644 --- a/apps/cic-ussd/transitions/gender_setting_transitions.json +++ b/apps/cic-ussd/transitions/gender_setting_transitions.json @@ -4,12 +4,13 @@ "source": "enter_gender", "dest": "enter_location", "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data", - "conditions": "cic_ussd.state_machine.logic.validator.is_valid_gender_selection" + "conditions": "cic_ussd.state_machine.logic.validator.is_valid_gender_selection", + "unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata" }, { "trigger": "scan_data", "source": "enter_gender", - "dest": "standard_pin_authorization", + "dest": "gender_edit_pin_authorization", "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data", "conditions": [ "cic_ussd.state_machine.logic.validator.has_cached_user_metadata", @@ -18,15 +19,14 @@ }, { "trigger": "scan_data", - "source": "standard_pin_authorization", + "source": "gender_edit_pin_authorization", "dest": "exit", "conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin", - "after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute", - "unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata" + "after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute" }, { "trigger": "scan_data", - "source": "standard_pin_authorization", + "source": "gender_edit_pin_authorization", "dest": "exit_pin_blocked", "conditions": "cic_ussd.state_machine.logic.pin.is_locked_account" }, diff --git a/apps/cic-ussd/transitions/location_setting_transitions.json b/apps/cic-ussd/transitions/location_setting_transitions.json index d4197339..fa6ea52d 100644 --- a/apps/cic-ussd/transitions/location_setting_transitions.json +++ b/apps/cic-ussd/transitions/location_setting_transitions.json @@ -3,26 +3,26 @@ "trigger": "scan_data", "source": "enter_location", "dest": "enter_products", - "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data" + "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data", + "unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata" }, { "trigger": "scan_data", "source": "enter_location", - "dest": "standard_pin_authorization", + "dest": "location_edit_pin_authorization", "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data", "conditions": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata" }, { "trigger": "scan_data", - "source": "standard_pin_authorization", + "source": "location_edit_pin_authorization", "dest": "exit", "conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin", - "after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute", - "unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata" + "after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute" }, { "trigger": "scan_data", - "source": "standard_pin_authorization", + "source": "location_edit_pin_authorization", "dest": "exit_pin_blocked", "conditions": "cic_ussd.state_machine.logic.pin.is_locked_account" } diff --git a/apps/cic-ussd/transitions/name_setting_transitions.json b/apps/cic-ussd/transitions/name_setting_transitions.json index 1cda4b84..38b5e58c 100644 --- a/apps/cic-ussd/transitions/name_setting_transitions.json +++ b/apps/cic-ussd/transitions/name_setting_transitions.json @@ -7,49 +7,28 @@ }, { "trigger": "scan_data", - "source": "enter_given_name", - "dest": "standard_pin_authorization", + "source": "enter_family_name", + "dest": "name_edit_pin_authorization", "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data", "conditions": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata" }, - { - "trigger": "scan_data", - "source": "standard_pin_authorization", - "dest": "exit", - "conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin", - "after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute", - "unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata" - }, - { - "trigger": "scan_data", - "source": "standard_pin_authorization", - "dest": "exit_pin_blocked", - "conditions": "cic_ussd.state_machine.logic.pin.is_locked_account" - }, { "trigger": "scan_data", "source": "enter_family_name", "dest": "enter_gender", - "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data" - }, - { - "trigger": "scan_data", - "source": "enter_family_name", - "dest": "standard_pin_authorization", "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data", - "conditions": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata" - }, - { - "trigger": "scan_data", - "source": "standard_pin_authorization", - "dest": "exit", - "conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin", - "after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute", "unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata" }, { "trigger": "scan_data", - "source": "standard_pin_authorization", + "source": "name_edit_pin_authorization", + "dest": "exit", + "conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin", + "after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute" + }, + { + "trigger": "scan_data", + "source": "name_edit_pin_authorization", "dest": "exit_pin_blocked", "conditions": "cic_ussd.state_machine.logic.pin.is_locked_account" } diff --git a/apps/cic-ussd/transitions/pin_setting_transitions.json b/apps/cic-ussd/transitions/pin_setting_transitions.json index 2d904be0..6b5e797b 100644 --- a/apps/cic-ussd/transitions/pin_setting_transitions.json +++ b/apps/cic-ussd/transitions/pin_setting_transitions.json @@ -9,14 +9,14 @@ "trigger": "scan_data", "source": "enter_current_pin", "dest": "exit_pin_blocked", - "conditions": "cic_ussd.state_machine.logic.menu.is_blocked_pin" + "conditions": "cic_ussd.state_machine.logic.pin.is_blocked_pin" }, { "trigger": "scan_data", "source": "enter_new_pin", "dest": "new_pin_confirmation", "after": "cic_ussd.state_machine.logic.pin.save_initial_pin_to_session_data", - "conditions": "cic_ussd.state_machine.logic.menu.is_valid_new_pin" + "conditions": "cic_ussd.state_machine.logic.pin.is_valid_new_pin" }, { "trigger": "scan_data", @@ -28,7 +28,7 @@ "source": "new_pin_confirmation", "dest": "complete", "conditions": "cic_ussd.state_machine.logic.pin.pins_match", - "after": "cic_ussd.state_machine.logic.menu.complete_pin_change" + "after": "cic_ussd.state_machine.logic.pin.complete_pin_change" }, { "trigger": "scan_data", diff --git a/apps/cic-ussd/transitions/products_setting_transitions.json b/apps/cic-ussd/transitions/products_setting_transitions.json index b4bb8b1d..ac105842 100644 --- a/apps/cic-ussd/transitions/products_setting_transitions.json +++ b/apps/cic-ussd/transitions/products_setting_transitions.json @@ -2,7 +2,7 @@ { "trigger": "scan_data", "source": "enter_products", - "dest": "standard_pin_authorization", + "dest": "products_edit_pin_authorization", "conditions": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata", "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data" }, @@ -13,18 +13,19 @@ "after": [ "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data", "cic_ussd.state_machine.logic.user.save_complete_user_metadata" - ] + ], + "unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata" }, { "trigger": "scan_data", - "source": "standard_pin_authorization", + "source": "products_edit_pin_authorization", "dest": "exit", "conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin", "after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute" }, { "trigger": "scan_data", - "source": "standard_pin_authorization", + "source": "products_edit_pin_authorization", "dest": "exit_pin_blocked", "conditions": "cic_ussd.state_machine.logic.pin.is_locked_account" } diff --git a/apps/cic-ussd/transitions/user_metadata_transitions.json b/apps/cic-ussd/transitions/user_metadata_transitions.json index 7ee8cab0..dff068f6 100644 --- a/apps/cic-ussd/transitions/user_metadata_transitions.json +++ b/apps/cic-ussd/transitions/user_metadata_transitions.json @@ -8,7 +8,7 @@ { "trigger": "scan_data", "source": "metadata_management", - "dest": "enter_age", + "dest": "enter_gender", "conditions": "cic_ussd.state_machine.logic.menu.menu_two_selected" }, { diff --git a/apps/cic-ussd/var/lib/locale/ussd.en.yml b/apps/cic-ussd/var/lib/locale/ussd.en.yml index 8e9c3e42..d1f0653b 100644 --- a/apps/cic-ussd/var/lib/locale/ussd.en.yml +++ b/apps/cic-ussd/var/lib/locale/ussd.en.yml @@ -6,7 +6,7 @@ en: 2. Kiswahili 3. Help initial_pin_entry: |- - CON Please enter a new four number PIN for your account. + CON Please enter a new four number PIN for your account. 0. Back initial_pin_confirmation: |- CON Enter your four number PIN again @@ -76,7 +76,10 @@ en: CON Enter current PIN. You have %{remaining_attempts} attempts remaining. 0. Back enter_new_pin: |- - CON Enter new PIN again + CON Enter your new four number PIN + 0. Back + new_pin_confirmation: |- + CON Enter your new four number PIN again 0. Back transaction_pin_authorization: first: |- @@ -107,6 +110,34 @@ en: retry: |- CON Please enter your PIN. You have %{remaining_attempts} attempts remaining 0. Back + name_edit_pin_authorization: + first: |- + CON Please enter your PIN + 0. Back + retry: |- + CON Please enter your PIN. You have %{remaining_attempts} attempts remaining + 0. Back + gender_edit_pin_authorization: + first: |- + CON Please enter your PIN + 0. Back + retry: |- + CON Please enter your PIN. You have %{remaining_attempts} attempts remaining + 0. Back + location_edit_pin_authorization: + first: |- + CON Please enter your PIN + 0. Back + retry: |- + CON Please enter your PIN. You have %{remaining_attempts} attempts remaining + 0. Back + products_edit_pin_authorization: + first: |- + CON Please enter your PIN + 0. Back + retry: |- + CON Please enter your PIN. You have %{remaining_attempts} attempts remaining + 0. Back account_balances: |- CON Your balances are as follows: balance: %{operational_balance} %{token_symbol} diff --git a/apps/cic-ussd/var/lib/locale/ussd.sw.yml b/apps/cic-ussd/var/lib/locale/ussd.sw.yml index 57fea950..a40db2b7 100644 --- a/apps/cic-ussd/var/lib/locale/ussd.sw.yml +++ b/apps/cic-ussd/var/lib/locale/ussd.sw.yml @@ -21,7 +21,7 @@ sw: CON Weka jinsia yako 1. Mwanaume 2. Mwanamke - 3. Nyngine + 3. Nyngine 0. Nyuma enter_location: |- CON Weka eneo lako @@ -61,7 +61,7 @@ sw: Jina: %{full_name} Jinsia: %{gender} Eneo: %{location} - Unauza: %{user_bio} + Unauza: %{products} 0. Nyuma select_preferred_language: |- CON Chagua lugha @@ -78,6 +78,9 @@ sw: enter_new_pin: |- CON Weka nambari ya siri mpya 0. Nyuma + new_pin_confirmation: |- + CON Weka PIN yako tena + 0. Nyuma transaction_pin_authorization: first: |- CON %{recipient_information} atapokea %{transaction_amount} %{token_symbol} kutoka kwa %{sender_information}. @@ -86,9 +89,9 @@ sw: retry: |- CON Weka nambari ya siri. Una majaribio %{remaining_attempts} yaliyobaki. 0. Nyuma - standard_pin_authorization: + display_metadata_pin_authorization: first: |- - CON Tafadhali weka PIN yako. + CON Tafadhali weka PIN yako 0. Nyuma retry: |- CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki. @@ -107,12 +110,40 @@ sw: retry: |- CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki. 0. Nyuma + name_edit_pin_authorization: + first: |- + CON Tafadhali weka PIN yako + 0. Nyuma + retry: |- + CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki. + 0. Nyuma + gender_edit_pin_authorization: + first: |- + CON Tafadhali weka PIN yako + 0. Nyuma + retry: |- + CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki. + 0. Nyuma + location_edit_pin_authorization: + first: |- + CON Tafadhali weka PIN yako + 0. Nyuma + retry: |- + CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki. + 0. Nyuma + products_edit_pin_authorization: + first: |- + CON Tafadhali weka PIN yako + 0. Nyuma + retry: |- + CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki. + 0. Nyuma account_balances: |- CON Salio zako ni zifuatazo: - salio: %{operational_balance} - ushuru: %{tax} - tuzo: %{bonus} - 0. Back + salio: %{operational_balance} %{token_symbol} + ushuru: %{tax} %{token_symbol} + tuzo: %{bonus} %{token_symbol} + 0. Nyuma first_transaction_set: |- CON %{first_transaction_set} 1. Mbele From 096ed9bc2700110dac8608d45785dbf93ad21206 Mon Sep 17 00:00:00 2001 From: Louis Holbrook Date: Thu, 3 Jun 2021 13:51:55 +0000 Subject: [PATCH 08/15] Update queue/syncer module structure --- apps/cic-eth/cic_eth/api/api_admin.py | 2 +- apps/cic-eth/cic_eth/ext/tx.py | 2 +- apps/cic-eth/cic_eth/queue/query.py | 14 +++++------ apps/cic-eth/cic_eth/queue/state.py | 24 +++++++++---------- apps/cic-eth/cic_eth/queue/tx.py | 4 ++-- .../cic_eth/runnable/daemons/filters/gas.py | 2 +- .../runnable/daemons/filters/straggler.py | 2 +- apps/cic-eth/requirements.txt | 4 ++-- apps/cic-eth/tests/filters/test_gas_filter.py | 4 ++-- .../tests/filters/test_register_filter.py | 2 +- .../tests/filters/test_straggler_filter.py | 4 ++-- .../tests/filters/test_transferauth_filter.py | 2 +- apps/cic-eth/tests/filters/test_tx_filter.py | 4 ++-- apps/cic-eth/tests/task/api/test_admin.py | 4 ++-- .../tests/task/api/test_admin_noncritical.py | 6 ++--- apps/cic-eth/tests/task/api/test_app.py | 2 +- apps/cic-eth/tests/task/test_task_admin.py | 4 ++-- apps/cic-eth/tests/task/test_task_gas.py | 6 ++--- apps/cic-eth/tests/task/test_task_tx.py | 4 ++-- apps/cic-eth/tests/task/test_task_tx_misc.py | 4 ++-- 20 files changed, 50 insertions(+), 50 deletions(-) diff --git a/apps/cic-eth/cic_eth/api/api_admin.py b/apps/cic-eth/cic_eth/api/api_admin.py index 519262c7..c5c1e142 100644 --- a/apps/cic-eth/cic_eth/api/api_admin.py +++ b/apps/cic-eth/cic_eth/api/api_admin.py @@ -31,7 +31,7 @@ from chainqueue.db.enum import ( status_str, ) from chainqueue.error import TxStateChangeError -from chainqueue.query import get_tx +from chainqueue.sql.query import get_tx from eth_erc20 import ERC20 # local imports diff --git a/apps/cic-eth/cic_eth/ext/tx.py b/apps/cic-eth/cic_eth/ext/tx.py index dc9f0ad2..007b4136 100644 --- a/apps/cic-eth/cic_eth/ext/tx.py +++ b/apps/cic-eth/cic_eth/ext/tx.py @@ -19,7 +19,7 @@ from cic_eth_registry import CICRegistry from cic_eth_registry.erc20 import ERC20Token from chainqueue.db.models.otx import Otx from chainqueue.db.enum import StatusEnum -from chainqueue.query import get_tx_cache +from chainqueue.sql.query import get_tx_cache from eth_erc20 import ERC20 # local imports diff --git a/apps/cic-eth/cic_eth/queue/query.py b/apps/cic-eth/cic_eth/queue/query.py index 44ed7547..9ce3a17f 100644 --- a/apps/cic-eth/cic_eth/queue/query.py +++ b/apps/cic-eth/cic_eth/queue/query.py @@ -5,7 +5,7 @@ import datetime import celery from chainlib.chain import ChainSpec from chainlib.eth.tx import unpack -import chainqueue.query +import chainqueue.sql.query from chainqueue.db.enum import ( StatusEnum, is_alive, @@ -28,7 +28,7 @@ celery_app = celery.current_app def get_tx_cache(chain_spec_dict, tx_hash): chain_spec = ChainSpec.from_dict(chain_spec_dict) session = SessionBase.create_session() - r = chainqueue.query.get_tx_cache(chain_spec, tx_hash, session=session) + r = chainqueue.sql.query.get_tx_cache(chain_spec, tx_hash, session=session) session.close() return r @@ -37,7 +37,7 @@ def get_tx_cache(chain_spec_dict, tx_hash): def get_tx(chain_spec_dict, tx_hash): chain_spec = ChainSpec.from_dict(chain_spec_dict) session = SessionBase.create_session() - r = chainqueue.query.get_tx(chain_spec, tx_hash, session=session) + r = chainqueue.sql.query.get_tx(chain_spec, tx_hash, session=session) session.close() return r @@ -46,7 +46,7 @@ def get_tx(chain_spec_dict, tx_hash): def get_account_tx(chain_spec_dict, address, as_sender=True, as_recipient=True, counterpart=None): chain_spec = ChainSpec.from_dict(chain_spec_dict) session = SessionBase.create_session() - r = chainqueue.query.get_account_tx(chain_spec, address, as_sender=True, as_recipient=True, counterpart=None, session=session) + r = chainqueue.sql.query.get_account_tx(chain_spec, address, as_sender=True, as_recipient=True, counterpart=None, session=session) session.close() return r @@ -55,17 +55,17 @@ def get_account_tx(chain_spec_dict, address, as_sender=True, as_recipient=True, def get_upcoming_tx_nolock(chain_spec_dict, status=StatusEnum.READYSEND, not_status=None, recipient=None, before=None, limit=0, session=None): chain_spec = ChainSpec.from_dict(chain_spec_dict) session = SessionBase.create_session() - r = chainqueue.query.get_upcoming_tx(chain_spec, status, not_status=not_status, recipient=recipient, before=before, limit=limit, session=session, decoder=unpack) + r = chainqueue.sql.query.get_upcoming_tx(chain_spec, status, not_status=not_status, recipient=recipient, before=before, limit=limit, session=session, decoder=unpack) session.close() return r def get_status_tx(chain_spec, status, not_status=None, before=None, exact=False, limit=0, session=None): - return chainqueue.query.get_status_tx_cache(chain_spec, status, not_status=not_status, before=before, exact=exact, limit=limit, session=session, decoder=unpack) + return chainqueue.sql.query.get_status_tx_cache(chain_spec, status, not_status=not_status, before=before, exact=exact, limit=limit, session=session, decoder=unpack) def get_paused_tx(chain_spec, status=None, sender=None, session=None, decoder=None): - return chainqueue.query.get_paused_tx_cache(chain_spec, status=status, sender=sender, session=session, decoder=unpack) + return chainqueue.sql.query.get_paused_tx_cache(chain_spec, status=status, sender=sender, session=session, decoder=unpack) def get_nonce_tx(chain_spec, nonce, sender): diff --git a/apps/cic-eth/cic_eth/queue/state.py b/apps/cic-eth/cic_eth/queue/state.py index 89d5ce40..72bd505d 100644 --- a/apps/cic-eth/cic_eth/queue/state.py +++ b/apps/cic-eth/cic_eth/queue/state.py @@ -1,6 +1,6 @@ # external imports from chainlib.chain import ChainSpec -import chainqueue.state +import chainqueue.sql.state # local imports import celery @@ -14,7 +14,7 @@ celery_app = celery.current_app def set_sent(chain_spec_dict, tx_hash, fail=False): chain_spec = ChainSpec.from_dict(chain_spec_dict) session = SessionBase.create_session() - r = chainqueue.state.set_sent(chain_spec, tx_hash, fail, session=session) + r = chainqueue.sql.state.set_sent(chain_spec, tx_hash, fail, session=session) session.close() return r @@ -23,7 +23,7 @@ def set_sent(chain_spec_dict, tx_hash, fail=False): def set_final(chain_spec_dict, tx_hash, block=None, tx_index=None, fail=False): chain_spec = ChainSpec.from_dict(chain_spec_dict) session = SessionBase.create_session() - r = chainqueue.state.set_final(chain_spec, tx_hash, block=block, tx_index=tx_index, fail=fail, session=session) + r = chainqueue.sql.state.set_final(chain_spec, tx_hash, block=block, tx_index=tx_index, fail=fail, session=session) session.close() return r @@ -32,7 +32,7 @@ def set_final(chain_spec_dict, tx_hash, block=None, tx_index=None, fail=False): def set_cancel(chain_spec_dict, tx_hash, manual=False): chain_spec = ChainSpec.from_dict(chain_spec_dict) session = SessionBase.create_session() - r = chainqueue.state.set_cancel(chain_spec, tx_hash, manual, session=session) + r = chainqueue.sql.state.set_cancel(chain_spec, tx_hash, manual, session=session) session.close() return r @@ -41,7 +41,7 @@ def set_cancel(chain_spec_dict, tx_hash, manual=False): def set_rejected(chain_spec_dict, tx_hash): chain_spec = ChainSpec.from_dict(chain_spec_dict) session = SessionBase.create_session() - r = chainqueue.state.set_rejected(chain_spec, tx_hash, session=session) + r = chainqueue.sql.state.set_rejected(chain_spec, tx_hash, session=session) session.close() return r @@ -50,7 +50,7 @@ def set_rejected(chain_spec_dict, tx_hash): def set_fubar(chain_spec_dict, tx_hash): chain_spec = ChainSpec.from_dict(chain_spec_dict) session = SessionBase.create_session() - r = chainqueue.state.set_fubar(chain_spec, tx_hash, session=session) + r = chainqueue.sql.state.set_fubar(chain_spec, tx_hash, session=session) session.close() return r @@ -59,7 +59,7 @@ def set_fubar(chain_spec_dict, tx_hash): def set_manual(chain_spec_dict, tx_hash): chain_spec = ChainSpec.from_dict(chain_spec_dict) session = SessionBase.create_session() - r = chainqueue.state.set_manual(chain_spec, tx_hash, session=session) + r = chainqueue.sql.state.set_manual(chain_spec, tx_hash, session=session) session.close() return r @@ -68,7 +68,7 @@ def set_manual(chain_spec_dict, tx_hash): def set_ready(chain_spec_dict, tx_hash): chain_spec = ChainSpec.from_dict(chain_spec_dict) session = SessionBase.create_session() - r = chainqueue.state.set_ready(chain_spec, tx_hash, session=session) + r = chainqueue.sql.state.set_ready(chain_spec, tx_hash, session=session) session.close() return r @@ -77,7 +77,7 @@ def set_ready(chain_spec_dict, tx_hash): def set_reserved(chain_spec_dict, tx_hash): chain_spec = ChainSpec.from_dict(chain_spec_dict) session = SessionBase.create_session() - r = chainqueue.state.set_reserved(chain_spec, tx_hash, session=session) + r = chainqueue.sql.state.set_reserved(chain_spec, tx_hash, session=session) session.close() return r @@ -86,7 +86,7 @@ def set_reserved(chain_spec_dict, tx_hash): def set_waitforgas(chain_spec_dict, tx_hash): chain_spec = ChainSpec.from_dict(chain_spec_dict) session = SessionBase.create_session() - r = chainqueue.state.set_waitforgas(chain_spec, tx_hash, session=session) + r = chainqueue.sql.state.set_waitforgas(chain_spec, tx_hash, session=session) session.close() return r @@ -95,7 +95,7 @@ def set_waitforgas(chain_spec_dict, tx_hash): def get_state_log(chain_spec_dict, tx_hash): chain_spec = ChainSpec.from_dict(chain_spec_dict) session = SessionBase.create_session() - r = chainqueue.state.get_state_log(chain_spec, tx_hash, session=session) + r = chainqueue.sql.state.get_state_log(chain_spec, tx_hash, session=session) session.close() return r @@ -104,6 +104,6 @@ def get_state_log(chain_spec_dict, tx_hash): def obsolete(chain_spec_dict, tx_hash, final): chain_spec = ChainSpec.from_dict(chain_spec_dict) session = SessionBase.create_session() - r = chainqueue.state.obsolete_by_cache(chain_spec, tx_hash, final, session=session) + r = chainqueue.sql.state.obsolete_by_cache(chain_spec, tx_hash, final, session=session) session.close() return r diff --git a/apps/cic-eth/cic_eth/queue/tx.py b/apps/cic-eth/cic_eth/queue/tx.py index 841f45b5..2afa9187 100644 --- a/apps/cic-eth/cic_eth/queue/tx.py +++ b/apps/cic-eth/cic_eth/queue/tx.py @@ -15,14 +15,14 @@ from sqlalchemy import tuple_ from sqlalchemy import func from chainlib.chain import ChainSpec from chainlib.eth.tx import unpack -import chainqueue.state +import chainqueue.sql.state from chainqueue.db.enum import ( StatusEnum, StatusBits, is_alive, dead, ) -from chainqueue.tx import create +from chainqueue.sql.tx import create from chainqueue.error import NotLocalTxError from chainqueue.db.enum import status_str diff --git a/apps/cic-eth/cic_eth/runnable/daemons/filters/gas.py b/apps/cic-eth/cic_eth/runnable/daemons/filters/gas.py index 4b17f86c..702fd8d7 100644 --- a/apps/cic-eth/cic_eth/runnable/daemons/filters/gas.py +++ b/apps/cic-eth/cic_eth/runnable/daemons/filters/gas.py @@ -10,7 +10,7 @@ from chainlib.eth.tx import unpack from chainqueue.db.enum import StatusBits from chainqueue.db.models.tx import TxCache from chainqueue.db.models.otx import Otx -from chainqueue.query import get_paused_tx_cache as get_paused_tx +from chainqueue.sql.query import get_paused_tx_cache as get_paused_tx # local imports from cic_eth.db.models.base import SessionBase diff --git a/apps/cic-eth/cic_eth/runnable/daemons/filters/straggler.py b/apps/cic-eth/cic_eth/runnable/daemons/filters/straggler.py index 79d50cde..a8c5c88d 100644 --- a/apps/cic-eth/cic_eth/runnable/daemons/filters/straggler.py +++ b/apps/cic-eth/cic_eth/runnable/daemons/filters/straggler.py @@ -3,7 +3,7 @@ import logging # external imports import celery -from chainqueue.state import obsolete_by_cache +from chainqueue.sql.state import obsolete_by_cache logg = logging.getLogger() diff --git a/apps/cic-eth/requirements.txt b/apps/cic-eth/requirements.txt index a7ed3ae5..af704c62 100644 --- a/apps/cic-eth/requirements.txt +++ b/apps/cic-eth/requirements.txt @@ -16,8 +16,8 @@ moolb~=0.1.1b2 eth-address-index~=0.1.1a11 chainlib~=0.0.3rc2 hexathon~=0.0.1a7 -chainsyncer[sql]~=0.0.2a4 -chainqueue~=0.0.2b1 +chainsyncer[sql]~=0.0.2a5 +chainqueue~=0.0.2b3 sarafu-faucet==0.0.3a3 erc20-faucet==0.2.1a4 coincurve==15.0.0 diff --git a/apps/cic-eth/tests/filters/test_gas_filter.py b/apps/cic-eth/tests/filters/test_gas_filter.py index 8f91d2fd..2e30cc79 100644 --- a/apps/cic-eth/tests/filters/test_gas_filter.py +++ b/apps/cic-eth/tests/filters/test_gas_filter.py @@ -1,7 +1,7 @@ # external imports from chainlib.connection import RPCConnection from chainlib.eth.nonce import OverrideNonceOracle -from chainqueue.tx import create as queue_create +from chainqueue.sql.tx import create as queue_create from chainlib.eth.tx import ( TxFormat, unpack, @@ -16,7 +16,7 @@ from chainlib.eth.block import ( block_by_number, Block, ) -from chainqueue.state import ( +from chainqueue.sql.state import ( set_waitforgas, ) from hexathon import strip_0x diff --git a/apps/cic-eth/tests/filters/test_register_filter.py b/apps/cic-eth/tests/filters/test_register_filter.py index 9842bc00..ce4543bb 100644 --- a/apps/cic-eth/tests/filters/test_register_filter.py +++ b/apps/cic-eth/tests/filters/test_register_filter.py @@ -15,7 +15,7 @@ from chainlib.eth.block import ( ) from erc20_faucet import Faucet from hexathon import strip_0x -from chainqueue.query import get_account_tx +from chainqueue.sql.query import get_account_tx # local imports from cic_eth.runnable.daemons.filters.register import RegistrationFilter diff --git a/apps/cic-eth/tests/filters/test_straggler_filter.py b/apps/cic-eth/tests/filters/test_straggler_filter.py index 27dc0b75..0c8b5c07 100644 --- a/apps/cic-eth/tests/filters/test_straggler_filter.py +++ b/apps/cic-eth/tests/filters/test_straggler_filter.py @@ -17,8 +17,8 @@ from chainlib.eth.block import ( ) from chainqueue.db.models.otx import Otx from chainqueue.db.enum import StatusBits -from chainqueue.tx import create as queue_create -from chainqueue.state import ( +from chainqueue.sql.tx import create as queue_create +from chainqueue.sql.state import ( set_reserved, set_ready, set_sent, diff --git a/apps/cic-eth/tests/filters/test_transferauth_filter.py b/apps/cic-eth/tests/filters/test_transferauth_filter.py index 25822b83..4d2266c1 100644 --- a/apps/cic-eth/tests/filters/test_transferauth_filter.py +++ b/apps/cic-eth/tests/filters/test_transferauth_filter.py @@ -15,7 +15,7 @@ from chainlib.eth.block import ( Block, ) from hexathon import strip_0x -from chainqueue.query import get_account_tx +from chainqueue.sql.query import get_account_tx # local imports from cic_eth.runnable.daemons.filters.transferauth import TransferAuthFilter diff --git a/apps/cic-eth/tests/filters/test_tx_filter.py b/apps/cic-eth/tests/filters/test_tx_filter.py index 06d2c94e..82d56f65 100644 --- a/apps/cic-eth/tests/filters/test_tx_filter.py +++ b/apps/cic-eth/tests/filters/test_tx_filter.py @@ -17,8 +17,8 @@ from chainlib.eth.block import ( ) from chainqueue.db.models.otx import Otx from chainqueue.db.enum import StatusBits -from chainqueue.tx import create as queue_create -from chainqueue.state import ( +from chainqueue.sql.tx import create as queue_create +from chainqueue.sql.state import ( set_reserved, set_ready, set_sent, diff --git a/apps/cic-eth/tests/task/api/test_admin.py b/apps/cic-eth/tests/task/api/test_admin.py index 95b1a060..62f3c49d 100644 --- a/apps/cic-eth/tests/task/api/test_admin.py +++ b/apps/cic-eth/tests/task/api/test_admin.py @@ -29,12 +29,12 @@ from chainqueue.db.enum import ( StatusBits, status_str, ) -from chainqueue.state import ( +from chainqueue.sql.state import ( set_fubar, set_ready, set_reserved, ) -from chainqueue.query import ( +from chainqueue.sql.query import ( get_tx, get_nonce_tx_cache, ) diff --git a/apps/cic-eth/tests/task/api/test_admin_noncritical.py b/apps/cic-eth/tests/task/api/test_admin_noncritical.py index fc10959d..2e41036c 100644 --- a/apps/cic-eth/tests/task/api/test_admin_noncritical.py +++ b/apps/cic-eth/tests/task/api/test_admin_noncritical.py @@ -11,7 +11,7 @@ from chainlib.eth.nonce import ( OverrideNonceOracle, RPCNonceOracle, ) -from chainqueue.tx import create as queue_create +from chainqueue.sql.tx import create as queue_create from chainlib.eth.tx import ( raw, receipt, @@ -23,14 +23,14 @@ from chainlib.eth.gas import ( Gas, OverrideGasOracle, ) -from chainqueue.state import ( +from chainqueue.sql.state import ( set_reserved, set_sent, set_ready, ) from chainqueue.db.models.otx import Otx from chainqueue.db.enum import StatusBits -from chainqueue.query import get_nonce_tx_cache +from chainqueue.sql.query import get_nonce_tx_cache from eth_erc20 import ERC20 from cic_eth_registry import CICRegistry diff --git a/apps/cic-eth/tests/task/api/test_app.py b/apps/cic-eth/tests/task/api/test_app.py index 321009f6..1e081584 100644 --- a/apps/cic-eth/tests/task/api/test_app.py +++ b/apps/cic-eth/tests/task/api/test_app.py @@ -12,7 +12,7 @@ from eth_accounts_index import AccountsIndex from chainlib.eth.tx import ( transaction, ) -from chainqueue.state import ( +from chainqueue.sql.state import ( set_reserved, ) diff --git a/apps/cic-eth/tests/task/test_task_admin.py b/apps/cic-eth/tests/task/test_task_admin.py index 7efa8283..463d48e7 100644 --- a/apps/cic-eth/tests/task/test_task_admin.py +++ b/apps/cic-eth/tests/task/test_task_admin.py @@ -5,7 +5,7 @@ import logging import celery from chainlib.connection import RPCConnection from chainlib.eth.nonce import OverrideNonceOracle -from chainqueue.tx import ( +from chainqueue.sql.tx import ( create as queue_create, ) from chainlib.eth.gas import ( @@ -13,7 +13,7 @@ from chainlib.eth.gas import ( OverrideGasOracle, ) from chainlib.eth.tx import TxFormat -from chainqueue.query import get_nonce_tx_cache +from chainqueue.sql.query import get_nonce_tx_cache from chainqueue.db.models.otx import Otx from chainqueue.db.enum import StatusBits from hexathon import add_0x diff --git a/apps/cic-eth/tests/task/test_task_gas.py b/apps/cic-eth/tests/task/test_task_gas.py index 3bd3dd80..c525724a 100644 --- a/apps/cic-eth/tests/task/test_task_gas.py +++ b/apps/cic-eth/tests/task/test_task_gas.py @@ -21,10 +21,10 @@ from chainlib.eth.constant import ( MINIMUM_FEE_UNITS, MINIMUM_FEE_PRICE, ) -from chainqueue.tx import create as queue_create -from chainqueue.query import get_tx +from chainqueue.sql.tx import create as queue_create +from chainqueue.sql.query import get_tx from chainqueue.db.enum import StatusBits -from chainqueue.state import ( +from chainqueue.sql.state import ( set_ready, set_reserved, set_sent, diff --git a/apps/cic-eth/tests/task/test_task_tx.py b/apps/cic-eth/tests/task/test_task_tx.py index ecf68992..db2b36ed 100644 --- a/apps/cic-eth/tests/task/test_task_tx.py +++ b/apps/cic-eth/tests/task/test_task_tx.py @@ -18,8 +18,8 @@ from chainlib.eth.tx import ( ) from hexathon import strip_0x from chainqueue.db.models.otx import Otx -from chainqueue.tx import create as queue_create -from chainqueue.state import ( +from chainqueue.sql.tx import create as queue_create +from chainqueue.sql.state import ( set_reserved, set_ready, set_sent, diff --git a/apps/cic-eth/tests/task/test_task_tx_misc.py b/apps/cic-eth/tests/task/test_task_tx_misc.py index 992abb83..94850900 100644 --- a/apps/cic-eth/tests/task/test_task_tx_misc.py +++ b/apps/cic-eth/tests/task/test_task_tx_misc.py @@ -5,7 +5,7 @@ import logging # external imports import pytest import celery -from chainqueue.tx import create as queue_create +from chainqueue.sql.tx import create as queue_create from chainlib.eth.nonce import ( RPCNonceOracle, OverrideNonceOracle, @@ -23,7 +23,7 @@ from hexathon import ( add_0x, strip_0x, ) -from chainqueue.state import ( +from chainqueue.sql.state import ( set_reserved, set_ready, ) From 1c17048981bd79124b5fc1d01287c7fe66c0ebb9 Mon Sep 17 00:00:00 2001 From: Blair Vanderlugt Date: Thu, 3 Jun 2021 17:19:55 +0000 Subject: [PATCH 09/15] Update README.md --- apps/data-seeding/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/data-seeding/README.md b/apps/data-seeding/README.md index 00db7d94..cb33352a 100644 --- a/apps/data-seeding/README.md +++ b/apps/data-seeding/README.md @@ -259,4 +259,6 @@ Should exit with code 0 if all input data is found in the respective services. - Running the balance script should be _optional_ in all cases, but is currently required in the case of `cic_ussd` because it is needed to generate the metadata. An improvement would be moving the task to `import_users.py`, for a different queue than the balance tx handler. +- MacOS BigSur issue when installing psycopg2: ld: library not found for -lssl -> https://github.com/psycopg/psycopg2/issues/1115#issuecomment-831498953 + - `cic_ussd` imports is poorly implemented, and consumes a lot of resources. Therefore it takes a long time to complete. Reducing the amount of polls for the phone pointer would go a long way to improve it. From 022db0419867c43da1c07743eb2d29e7bd20ad1d Mon Sep 17 00:00:00 2001 From: Blair Vanderlugt Date: Thu, 3 Jun 2021 17:22:47 +0000 Subject: [PATCH 10/15] no local file mounts for config files --- apps/cic-eth/cic_eth/runnable/daemons/dispatcher.py | 2 +- apps/cic-eth/cic_eth/version.py | 2 +- apps/cic-eth/requirements.txt | 2 +- apps/data-seeding/requirements.txt | 4 ++-- docker-compose.yml | 6 +----- service-configs/.gitkeep | 0 6 files changed, 6 insertions(+), 10 deletions(-) delete mode 100644 service-configs/.gitkeep diff --git a/apps/cic-eth/cic_eth/runnable/daemons/dispatcher.py b/apps/cic-eth/cic_eth/runnable/daemons/dispatcher.py index 95dc7031..08a54185 100644 --- a/apps/cic-eth/cic_eth/runnable/daemons/dispatcher.py +++ b/apps/cic-eth/cic_eth/runnable/daemons/dispatcher.py @@ -21,7 +21,7 @@ from chainqueue.db.enum import ( StatusBits, ) from chainqueue.error import NotLocalTxError -from chainqueue.state import set_reserved +from chainqueue.sql.state import set_reserved # local imports import cic_eth diff --git a/apps/cic-eth/cic_eth/version.py b/apps/cic-eth/cic_eth/version.py index b233a1e3..0edda257 100644 --- a/apps/cic-eth/cic_eth/version.py +++ b/apps/cic-eth/cic_eth/version.py @@ -10,7 +10,7 @@ version = ( 0, 11, 0, - 'beta.15', + 'beta.16', ) version_object = semver.VersionInfo( diff --git a/apps/cic-eth/requirements.txt b/apps/cic-eth/requirements.txt index af704c62..3f56ce0c 100644 --- a/apps/cic-eth/requirements.txt +++ b/apps/cic-eth/requirements.txt @@ -1,4 +1,4 @@ -cic-base~=0.1.2b14 +cic-base~=0.1.2b15 celery==4.4.7 crypto-dev-signer~=0.4.14b3 confini~=0.3.6rc3 diff --git a/apps/data-seeding/requirements.txt b/apps/data-seeding/requirements.txt index f0b55efb..4f2cb559 100644 --- a/apps/data-seeding/requirements.txt +++ b/apps/data-seeding/requirements.txt @@ -1,5 +1,5 @@ -cic-base[full_graph]==0.1.2b11 +cic-base[full_graph]==0.1.2b15 sarafu-faucet==0.0.3a3 -cic-eth==0.11.0b15 +cic-eth==0.11.0b16 cic-types==0.1.0a11 crypto-dev-signer==0.4.14b3 diff --git a/docker-compose.yml b/docker-compose.yml index 1a1eae37..a6a4ba89 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,11 +6,7 @@ volumes: bee-data: {} signer-data: {} bloxberg-data: {} - contract-config: - driver_opts: - type: local - o: bind - device: ./service-configs + contract-config: {} networks: default: diff --git a/service-configs/.gitkeep b/service-configs/.gitkeep deleted file mode 100644 index e69de29b..00000000 From 1605e532164b88f9d91f0ca0da1d19aed0729ae5 Mon Sep 17 00:00:00 2001 From: Blair Vanderlugt Date: Thu, 3 Jun 2021 11:43:13 -0700 Subject: [PATCH 11/15] pull the req out of the container and bump em --- apps/contract-migration/docker/Dockerfile | 14 ++++---------- apps/contract-migration/requirements.txt | 4 ++++ 2 files changed, 8 insertions(+), 10 deletions(-) create mode 100644 apps/contract-migration/requirements.txt diff --git a/apps/contract-migration/docker/Dockerfile b/apps/contract-migration/docker/Dockerfile index 19ba1c13..a15ac1c8 100644 --- a/apps/contract-migration/docker/Dockerfile +++ b/apps/contract-migration/docker/Dockerfile @@ -51,19 +51,13 @@ ENV PATH $NVM_DIR/versions/node//v$NODE_VERSION/bin:$PATH # WORKDIR /home/grassroots # USER grassroots +COPY contract-migration/requirements.txt . + ARG pip_extra_args="" ARG pip_index_url=https://pypi.org/simple ARG pip_extra_index_url=https://pip.grassrootseconomics.net:8433 -ARG cic_base_version=0.1.2b11 -ARG cic_eth_version=0.11.0b14 -ARG sarafu_token_version=0.0.1a8 -ARG sarafu_faucet_version=0.0.3a3 -RUN pip install --index-url https://pypi.org/simple --extra-index-url $pip_extra_index_url \ - cic-base[full_graph]==$cic_base_version \ - cic-eth==$cic_eth_version \ - sarafu-faucet==$sarafu_faucet_version \ - sarafu-token==$sarafu_token_version \ - cic-eth==$cic_eth_version +RUN pip install --index-url https://pypi.org/simple \ + --extra-index-url $pip_extra_index_url -r requirements.txt # -------------- begin runtime container ---------------- FROM python:3.8.6-slim-buster as runtime-image diff --git a/apps/contract-migration/requirements.txt b/apps/contract-migration/requirements.txt new file mode 100644 index 00000000..291ced7a --- /dev/null +++ b/apps/contract-migration/requirements.txt @@ -0,0 +1,4 @@ + cic-base[full_graph]==0.1.2b15 + sarafu-faucet==0.0.3a3 + sarafu-token==0.0.1a8 + cic-eth==0.11.0b16 From 007d7a5121b969bd42cc2c9629aee31a4a19d801 Mon Sep 17 00:00:00 2001 From: PhilipWafula Date: Fri, 4 Jun 2021 11:12:04 +0300 Subject: [PATCH 12/15] Bumps cic-eth and cic-base versions. --- apps/cic-ussd/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cic-ussd/requirements.txt b/apps/cic-ussd/requirements.txt index b01d9d47..ddaa7abf 100644 --- a/apps/cic-ussd/requirements.txt +++ b/apps/cic-ussd/requirements.txt @@ -1,4 +1,4 @@ -cic_base[full_graph]~=0.1.2b14 -cic-eth~=0.11.0b15 +cic_base[full_graph]~=0.1.2b15 +cic-eth~=0.11.0b16 cic-notify~=0.4.0a5 cic-types~=0.1.0a10 From fab9b0c5207cdb6c2d0beb950cb4b6b3c7c1f464 Mon Sep 17 00:00:00 2001 From: nolash Date: Fri, 4 Jun 2021 22:00:06 +0200 Subject: [PATCH 13/15] Add long timeout to first account create in contract migration part 2 --- apps/contract-migration/seed_cic_eth.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/contract-migration/seed_cic_eth.sh b/apps/contract-migration/seed_cic_eth.sh index fee0d3e5..505857ee 100755 --- a/apps/contract-migration/seed_cic_eth.sh +++ b/apps/contract-migration/seed_cic_eth.sh @@ -47,7 +47,7 @@ EOF >&2 echo "create account for gas gifter" old_gas_provider=$DEV_ETH_ACCOUNT_GAS_PROVIDER -DEV_ETH_ACCOUNT_GAS_GIFTER=`cic-eth-create $debug --redis-host-callback=$REDIS_HOST --redis-port-callback=$REDIS_PORT --no-register` +DEV_ETH_ACCOUNT_GAS_GIFTER=`cic-eth-create --timeout 120 $debug --redis-host-callback=$REDIS_HOST --redis-port-callback=$REDIS_PORT --no-register` echo DEV_ETH_ACCOUNT_GAS_GIFTER=$DEV_ETH_ACCOUNT_GAS_GIFTER >> $env_out_file cic-eth-tag -i $CIC_CHAIN_SPEC GAS_GIFTER $DEV_ETH_ACCOUNT_GAS_GIFTER From 77d9936e39c6a325bc00e1664653bdcaf9b10668 Mon Sep 17 00:00:00 2001 From: Philip Wafula Date: Mon, 7 Jun 2021 07:47:02 +0000 Subject: [PATCH 14/15] Philip/refactor signup steps. --- apps/cic-ussd/cic_ussd/processor.py | 19 ++++++++++++++++++- .../state_machine/logic/transaction.py | 4 ++-- .../transitions/signup_transitions.json | 16 +--------------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/apps/cic-ussd/cic_ussd/processor.py b/apps/cic-ussd/cic_ussd/processor.py index 864cd53f..b31564ca 100644 --- a/apps/cic-ussd/cic_ussd/processor.py +++ b/apps/cic-ussd/cic_ussd/processor.py @@ -270,7 +270,24 @@ def process_display_user_metadata(user: Account, display_key: str): products=products ) else: - raise MetadataNotFoundError(f'Expected person metadata but found none in cache for key: {key}') + # TODO [Philip]: All these translations could be moved to translation files. + logg.warning(f'Expected person metadata but found none in cache for key: {key}') + + absent = '' + if user.preferred_language == 'en': + absent = 'Not provided' + elif user.preferred_language == 'sw': + absent = 'Haijawekwa' + + return translation_for( + key=display_key, + preferred_language=user.preferred_language, + full_name=absent, + gender=absent, + location=absent, + products=absent + ) + def process_account_statement(user: Account, display_key: str, ussd_session: dict): diff --git a/apps/cic-ussd/cic_ussd/state_machine/logic/transaction.py b/apps/cic-ussd/cic_ussd/state_machine/logic/transaction.py index 2c4bbf02..84c866ee 100644 --- a/apps/cic-ussd/cic_ussd/state_machine/logic/transaction.py +++ b/apps/cic-ussd/cic_ussd/state_machine/logic/transaction.py @@ -11,7 +11,7 @@ from cic_ussd.balance import BalanceManager, compute_operational_balance from cic_ussd.chain import Chain from cic_ussd.db.models.account import AccountStatus, Account from cic_ussd.operations import save_to_in_memory_ussd_session_data -from cic_ussd.phone_number import get_user_by_phone_number +from cic_ussd.phone_number import get_user_by_phone_number, process_phone_number from cic_ussd.processor import retrieve_token_symbol from cic_ussd.redis import create_cached_data_key, get_cached_data from cic_ussd.transactions import OutgoingTransactionProcessor @@ -30,7 +30,7 @@ def is_valid_recipient(state_machine_data: Tuple[str, dict, Account]) -> bool: """ user_input, ussd_session, user = state_machine_data recipient = get_user_by_phone_number(phone_number=user_input) - is_not_initiator = user_input != user.phone_number + is_not_initiator = process_phone_number(user_input, 'KE') != user.phone_number has_active_account_status = user.get_account_status() == AccountStatus.ACTIVE.name return is_not_initiator and has_active_account_status and recipient is not None diff --git a/apps/cic-ussd/transitions/signup_transitions.json b/apps/cic-ussd/transitions/signup_transitions.json index 193ec132..731c8131 100644 --- a/apps/cic-ussd/transitions/signup_transitions.json +++ b/apps/cic-ussd/transitions/signup_transitions.json @@ -36,26 +36,12 @@ "source": "initial_pin_entry", "dest": "exit_invalid_pin" }, - { - "trigger": "scan_data", - "source": "initial_pin_confirmation", - "dest": "start", - "conditions": [ - "cic_ussd.state_machine.logic.pin.pins_match", - "cic_ussd.state_machine.logic.validator.has_cached_user_metadata" - ], - "after": [ - "cic_ussd.state_machine.logic.pin.complete_pin_change", - "cic_ussd.state_machine.logic.user.get_user_metadata", - "cic_ussd.state_machine.logic.user.update_account_status_to_active" - ] - }, { "trigger": "scan_data", "source": "initial_pin_confirmation", "unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata", "conditions": "cic_ussd.state_machine.logic.pin.pins_match", - "dest": "enter_given_name", + "dest": "start", "after": [ "cic_ussd.state_machine.logic.pin.complete_pin_change", "cic_ussd.state_machine.logic.user.update_account_status_to_active" From 0da617d29ee675d760966c3fb0dc174326f732d0 Mon Sep 17 00:00:00 2001 From: Philip Wafula Date: Mon, 7 Jun 2021 08:02:03 +0000 Subject: [PATCH 15/15] Philip/add support phone number --- apps/cic-ussd/.config/app.ini | 1 + apps/cic-ussd/.config/test/app.ini | 1 + apps/cic-ussd/cic_ussd/phone_number.py | 4 ++++ apps/cic-ussd/cic_ussd/processor.py | 12 +++++++++++- .../runnable/daemons/cic_user_ussd_server.py | 4 +++- 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/cic-ussd/.config/app.ini b/apps/cic-ussd/.config/app.ini index 95a36052..1a0b5e4f 100644 --- a/apps/cic-ussd/.config/app.ini +++ b/apps/cic-ussd/.config/app.ini @@ -5,6 +5,7 @@ LOCALE_PATH=/usr/src/cic-ussd/var/lib/locale/ MAX_BODY_LENGTH=1024 PASSWORD_PEPPER=QYbzKff6NhiQzY3ygl2BkiKOpER8RE/Upqs/5aZWW+I= SERVICE_CODE=*483*46#,*483*061#,*384*96# +SUPPORT_PHONE_NUMBER=0757628885 [phone_number] REGION=KE diff --git a/apps/cic-ussd/.config/test/app.ini b/apps/cic-ussd/.config/test/app.ini index 38bc95c4..7ea1b696 100644 --- a/apps/cic-ussd/.config/test/app.ini +++ b/apps/cic-ussd/.config/test/app.ini @@ -5,6 +5,7 @@ LOCALE_PATH=var/lib/locale/ MAX_BODY_LENGTH=1024 PASSWORD_PEPPER=QYbzKff6NhiQzY3ygl2BkiKOpER8RE/Upqs/5aZWW+I= SERVICE_CODE=*483*46# +SUPPORT_PHONE_NUMBER=0757628885 [ussd] MENU_FILE=/usr/local/lib/python3.8/site-packages/cic_ussd/db/ussd_menu.json diff --git a/apps/cic-ussd/cic_ussd/phone_number.py b/apps/cic-ussd/cic_ussd/phone_number.py index 0a48b0d2..2c476fa0 100644 --- a/apps/cic-ussd/cic_ussd/phone_number.py +++ b/apps/cic-ussd/cic_ussd/phone_number.py @@ -41,3 +41,7 @@ def get_user_by_phone_number(phone_number: str) -> Optional[Account]: phone_number = process_phone_number(phone_number=phone_number, region='KE') user = Account.session.query(Account).filter_by(phone_number=phone_number).first() return user + + +class Support: + phone_number = None diff --git a/apps/cic-ussd/cic_ussd/processor.py b/apps/cic-ussd/cic_ussd/processor.py index 864cd53f..b483466c 100644 --- a/apps/cic-ussd/cic_ussd/processor.py +++ b/apps/cic-ussd/cic_ussd/processor.py @@ -19,7 +19,7 @@ from cic_ussd.db.models.ussd_session import UssdSession from cic_ussd.error import MetadataNotFoundError, SeppukuError from cic_ussd.menu.ussd_menu import UssdMenu from cic_ussd.metadata import blockchain_address_to_metadata_pointer -from cic_ussd.phone_number import get_user_by_phone_number +from cic_ussd.phone_number import get_user_by_phone_number, Support from cic_ussd.redis import cache_data, create_cached_data_key, get_cached_data from cic_ussd.state_machine import UssdStateMachine from cic_ussd.conversions import to_wei, from_wei @@ -466,6 +466,14 @@ def next_state(ussd_session: dict, user: Account, user_input: str) -> str: return new_state +def process_exit_invalid_menu_option(display_key: str, preferred_language: str): + return translation_for( + key=display_key, + preferred_language=preferred_language, + support_phone=Support.phone_number + ) + + def custom_display_text( display_key: str, menu_name: str, @@ -502,5 +510,7 @@ def custom_display_text( return process_account_statement(display_key=display_key, user=user, ussd_session=ussd_session) elif menu_name == 'display_user_metadata': return process_display_user_metadata(display_key=display_key, user=user) + elif menu_name == 'exit_invalid_menu_option': + return process_exit_invalid_menu_option(display_key=display_key, preferred_language=user.preferred_language) else: return translation_for(key=display_key, preferred_language=user.preferred_language) diff --git a/apps/cic-ussd/cic_ussd/runnable/daemons/cic_user_ussd_server.py b/apps/cic-ussd/cic_ussd/runnable/daemons/cic_user_ussd_server.py index 92c4147f..6aad181f 100644 --- a/apps/cic-ussd/cic_ussd/runnable/daemons/cic_user_ussd_server.py +++ b/apps/cic-ussd/cic_ussd/runnable/daemons/cic_user_ussd_server.py @@ -26,7 +26,7 @@ from cic_ussd.metadata.base import Metadata from cic_ussd.operations import (define_response_with_content, process_menu_interaction_requests, define_multilingual_responses) -from cic_ussd.phone_number import process_phone_number +from cic_ussd.phone_number import process_phone_number, Support from cic_ussd.processor import get_default_token_data from cic_ussd.redis import cache_data, create_cached_data_key, InMemoryStore from cic_ussd.requests import (get_request_endpoint, @@ -126,6 +126,8 @@ else: valid_service_codes = config.get('APP_SERVICE_CODE').split(",") +Support.phone_number = config.get('APP_SUPPORT_PHONE_NUMBER') + def application(env, start_response): """Loads python code for application to be accessible over web server