Merge remote-tracking branch 'origin/master' into lash/allowance

This commit is contained in:
nolash 2021-06-30 16:45:06 +02:00
commit 2e3f8a0bf1
58 changed files with 735 additions and 1720 deletions

View File

@ -16,6 +16,7 @@ import cic_base.config
import cic_base.log import cic_base.log
import cic_base.argparse import cic_base.argparse
import cic_base.rpc import cic_base.rpc
from cic_base.eth.syncer import chain_interface
from cic_eth_registry import CICRegistry from cic_eth_registry import CICRegistry
from cic_eth_registry.error import UnknownContractError from cic_eth_registry.error import UnknownContractError
from chainlib.chain import ChainSpec from chainlib.chain import ChainSpec
@ -28,10 +29,8 @@ from hexathon import (
strip_0x, strip_0x,
) )
from chainsyncer.backend.sql import SQLBackend from chainsyncer.backend.sql import SQLBackend
from chainsyncer.driver import ( from chainsyncer.driver.head import HeadSyncer
HeadSyncer, from chainsyncer.driver.history import HistorySyncer
HistorySyncer,
)
from chainsyncer.db.models.base import SessionBase from chainsyncer.db.models.base import SessionBase
# local imports # local imports
@ -113,10 +112,10 @@ def main():
logg.info('resuming sync session {}'.format(syncer_backend)) logg.info('resuming sync session {}'.format(syncer_backend))
for syncer_backend in syncer_backends: for syncer_backend in syncer_backends:
syncers.append(HistorySyncer(syncer_backend)) syncers.append(HistorySyncer(syncer_backend, chain_interface))
syncer_backend = SQLBackend.live(chain_spec, block_offset+1) syncer_backend = SQLBackend.live(chain_spec, block_offset+1)
syncers.append(HeadSyncer(syncer_backend)) syncers.append(HeadSyncer(syncer_backend, chain_interface))
trusted_addresses_src = config.get('CIC_TRUST_ADDRESS') trusted_addresses_src = config.get('CIC_TRUST_ADDRESS')
if trusted_addresses_src == None: if trusted_addresses_src == None:

View File

@ -1,12 +1,13 @@
cic-base~=0.1.2b10 cic-base==0.1.3a3+build.4aa03607
alembic==1.4.2 alembic==1.4.2
confini~=0.3.6rc3 confini~=0.3.6rc3
uwsgi==2.0.19.1 uwsgi==2.0.19.1
moolb~=0.1.0 moolb~=0.1.0
cic-eth-registry~=0.5.5a4 cic-eth-registry~=0.5.6a1
SQLAlchemy==1.3.20 SQLAlchemy==1.3.20
semver==2.13.0 semver==2.13.0
psycopg2==2.8.6 psycopg2==2.8.6
celery==4.4.7 celery==4.4.7
redis==3.5.3 redis==3.5.3
chainsyncer[sql]~=0.0.2a4 chainsyncer[sql]~=0.0.3a3
erc20-faucet~=0.2.2a1

View File

@ -2,6 +2,7 @@
import os import os
import argparse import argparse
import logging import logging
import re
import alembic import alembic
from alembic.config import Config as AlembicConfig from alembic.config import Config as AlembicConfig
@ -23,6 +24,8 @@ argparser = argparse.ArgumentParser()
argparser.add_argument('-c', type=str, default=config_dir, help='config file') argparser.add_argument('-c', type=str, default=config_dir, help='config file')
argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration') argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration')
argparser.add_argument('--migrations-dir', dest='migrations_dir', default=migrationsdir, type=str, help='path to alembic migrations directory') argparser.add_argument('--migrations-dir', dest='migrations_dir', default=migrationsdir, type=str, help='path to alembic migrations directory')
argparser.add_argument('--reset', action='store_true', help='downgrade before upgrading')
argparser.add_argument('-f', action='store_true', help='force action')
argparser.add_argument('-v', action='store_true', help='be verbose') argparser.add_argument('-v', action='store_true', help='be verbose')
argparser.add_argument('-vv', action='store_true', help='be more verbose') argparser.add_argument('-vv', action='store_true', help='be more verbose')
args = argparser.parse_args() args = argparser.parse_args()
@ -53,4 +56,10 @@ ac = AlembicConfig(os.path.join(migrations_dir, 'alembic.ini'))
ac.set_main_option('sqlalchemy.url', dsn) ac.set_main_option('sqlalchemy.url', dsn)
ac.set_main_option('script_location', migrations_dir) ac.set_main_option('script_location', migrations_dir)
if args.reset:
if not args.f:
if not re.match(r'[yY][eE]?[sS]?', input('EEK! this will DELETE the existing db. are you sure??')):
logg.error('user chickened out on requested reset, bailing')
sys.exit(1)
alembic.command.downgrade(ac, 'base')
alembic.command.upgrade(ac, 'head') alembic.command.upgrade(ac, 'head')

View File

@ -6,6 +6,5 @@ sqlparse==0.4.1
pytest-celery==0.0.0a1 pytest-celery==0.0.0a1
eth_tester==0.5.0b3 eth_tester==0.5.0b3
py-evm==0.3.0a20 py-evm==0.3.0a20
web3==5.12.2 cic_base[full]==0.1.3a3+build.4aa03607
cic-eth-registry~=0.5.5a3 sarafu-faucet~=0.0.4a1
cic-base[full]==0.1.2b8

2
apps/cic-eth/MANIFEST.in Normal file
View File

@ -0,0 +1,2 @@
include *requirements.txt

View File

@ -562,13 +562,13 @@ class AdminApi:
tx['source_token_symbol'] = source_token.symbol tx['source_token_symbol'] = source_token.symbol
o = erc20_c.balance_of(tx['source_token'], tx['sender'], sender_address=self.call_address) o = erc20_c.balance_of(tx['source_token'], tx['sender'], sender_address=self.call_address)
r = self.rpc.do(o) r = self.rpc.do(o)
tx['sender_token_balance'] = erc20_c.parse_balance_of(r) tx['sender_token_balance'] = erc20_c.parse_balance(r)
if destination_token != None: if destination_token != None:
tx['destination_token_symbol'] = destination_token.symbol tx['destination_token_symbol'] = destination_token.symbol
o = erc20_c.balance_of(tx['destination_token'], tx['recipient'], sender_address=self.call_address) o = erc20_c.balance_of(tx['destination_token'], tx['recipient'], sender_address=self.call_address)
r = self.rpc.do(o) r = self.rpc.do(o)
tx['recipient_token_balance'] = erc20_c.parse_balance_of(r) tx['recipient_token_balance'] = erc20_c.parse_balance(r)
#tx['recipient_token_balance'] = destination_token.function('balanceOf')(tx['recipient']).call() #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 # TODO: this can mean either not subitted or culled, need to check other txs with same nonce to determine which

View File

@ -1,136 +0,0 @@
# standard imports
import os
import re
import logging
import argparse
import json
# third-party imports
import web3
import confini
import celery
from json.decoder import JSONDecodeError
from cic_registry.chain import ChainSpec
# local imports
from cic_eth.db import dsn_from_config
from cic_eth.db.models.base import SessionBase
from cic_eth.eth.util import unpack_signed_raw_tx
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
rootdir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
dbdir = os.path.join(rootdir, 'cic_eth', 'db')
migrationsdir = os.path.join(dbdir, 'migrations')
config_dir = os.path.join('/usr/local/etc/cic-eth')
argparser = argparse.ArgumentParser()
argparser.add_argument('-c', type=str, default=config_dir, help='config file')
argparser.add_argument('-i', '--chain-spec', dest='i', type=str, help='chain spec')
argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration')
argparser.add_argument('-q', type=str, default='cic-eth', help='queue name for worker tasks')
argparser.add_argument('-v', action='store_true', help='be verbose')
argparser.add_argument('-vv', action='store_true', help='be more verbose')
args = argparser.parse_args()
if args.vv:
logging.getLogger().setLevel(logging.DEBUG)
elif args.v:
logging.getLogger().setLevel(logging.INFO)
config = confini.Config(args.c, args.env_prefix)
config.process()
args_override = {
'CIC_CHAIN_SPEC': getattr(args, 'i'),
}
config.censor('PASSWORD', 'DATABASE')
config.censor('PASSWORD', 'SSL')
logg.debug('config:\n{}'.format(config))
dsn = dsn_from_config(config)
SessionBase.connect(dsn)
celery_app = celery.Celery(backend=config.get('CELERY_RESULT_URL'), broker=config.get('CELERY_BROKER_URL'))
queue = args.q
re_something = r'^/something/?'
chain_spec = ChainSpec.from_chain_str(config.get('CIC_CHAIN_SPEC'))
def process_something(session, env):
r = re.match(re_something, env.get('PATH_INFO'))
if not r:
return None
#if env.get('CONTENT_TYPE') != 'application/json':
# raise AttributeError('content type')
#if env.get('REQUEST_METHOD') != 'POST':
# raise AttributeError('method')
#post_data = json.load(env.get('wsgi.input'))
#return ('text/plain', 'foo'.encode('utf-8'),)
# uwsgi application
def application(env, start_response):
for k in env.keys():
logg.debug('env {} {}'.format(k, env[k]))
headers = []
content = b''
err = None
session = SessionBase.create_session()
for handler in [
process_something,
]:
try:
r = handler(session, env)
except AttributeError as e:
logg.error('handler fail attribute {}'.format(e))
err = '400 Impertinent request'
break
except JSONDecodeError as e:
logg.error('handler fail json {}'.format(e))
err = '400 Invalid data format'
break
except KeyError as e:
logg.error('handler fail key {}'.format(e))
err = '400 Invalid JSON'
break
except ValueError as e:
logg.error('handler fail value {}'.format(e))
err = '400 Invalid data'
break
except RuntimeError as e:
logg.error('task fail value {}'.format(e))
err = '500 Task failed, sorry I cannot tell you more'
break
if r != None:
(mime_type, content) = r
break
session.close()
if err != None:
headers.append(('Content-Type', 'text/plain, charset=UTF-8',))
start_response(err, headers)
session.close()
return [content]
headers.append(('Content-Length', str(len(content))),)
headers.append(('Access-Control-Allow-Origin', '*',));
if len(content) == 0:
headers.append(('Content-Type', 'text/plain, charset=UTF-8',))
start_response('404 Looked everywhere, sorry', headers)
else:
headers.append(('Content-Type', mime_type,))
start_response('200 OK', headers)
return [content]

View File

@ -15,6 +15,7 @@ import cic_base.config
import cic_base.log import cic_base.log
import cic_base.argparse import cic_base.argparse
import cic_base.rpc import cic_base.rpc
from cic_base.eth.syncer import chain_interface
from cic_eth_registry.error import UnknownContractError from cic_eth_registry.error import UnknownContractError
from chainlib.chain import ChainSpec from chainlib.chain import ChainSpec
from chainlib.eth.constant import ZERO_ADDRESS from chainlib.eth.constant import ZERO_ADDRESS
@ -26,10 +27,8 @@ from hexathon import (
strip_0x, strip_0x,
) )
from chainsyncer.backend.sql import SQLBackend from chainsyncer.backend.sql import SQLBackend
from chainsyncer.driver import ( from chainsyncer.driver.head import HeadSyncer
HeadSyncer, from chainsyncer.driver.history import HistorySyncer
HistorySyncer,
)
from chainsyncer.db.models.base import SessionBase from chainsyncer.db.models.base import SessionBase
# local imports # local imports
@ -80,6 +79,7 @@ chain_spec = ChainSpec.from_chain_str(config.get('CIC_CHAIN_SPEC'))
cic_base.rpc.setup(chain_spec, config.get('ETH_PROVIDER')) cic_base.rpc.setup(chain_spec, config.get('ETH_PROVIDER'))
def main(): def main():
# connect to celery # connect to celery
celery.Celery(broker=config.get('CELERY_BROKER_URL'), backend=config.get('CELERY_RESULT_URL')) celery.Celery(broker=config.get('CELERY_BROKER_URL'), backend=config.get('CELERY_RESULT_URL'))
@ -121,11 +121,11 @@ def main():
for syncer_backend in syncer_backends: for syncer_backend in syncer_backends:
try: try:
syncers.append(HistorySyncer(syncer_backend)) syncers.append(HistorySyncer(syncer_backend, chain_interface))
logg.info('Initializing HISTORY syncer on backend {}'.format(syncer_backend)) logg.info('Initializing HISTORY syncer on backend {}'.format(syncer_backend))
except AttributeError: except AttributeError:
logg.info('Initializing HEAD syncer on backend {}'.format(syncer_backend)) logg.info('Initializing HEAD syncer on backend {}'.format(syncer_backend))
syncers.append(HeadSyncer(syncer_backend)) syncers.append(HeadSyncer(syncer_backend, chain_interface))
connect_registry(rpc, chain_spec, config.get('CIC_REGISTRY_ADDRESS')) connect_registry(rpc, chain_spec, config.get('CIC_REGISTRY_ADDRESS'))

View File

@ -9,8 +9,8 @@ import semver
version = ( version = (
0, 0,
11, 11,
0, 1,
'beta.16', 'alpha.2',
) )
version_object = semver.VersionInfo( version_object = semver.VersionInfo(

View File

@ -1,25 +1,25 @@
cic-base~=0.1.2b15 cic-base==0.1.3a3+build.4aa03607
celery==4.4.7 celery==4.4.7
crypto-dev-signer~=0.4.14b3 crypto-dev-signer~=0.4.14b6
confini~=0.3.6rc3 confini~=0.3.6rc3
cic-eth-registry~=0.5.5a7 cic-eth-registry~=0.5.6a1
redis==3.5.3 redis==3.5.3
alembic==1.4.2 alembic==1.4.2
websockets==8.1 websockets==8.1
requests~=2.24.0 requests~=2.24.0
eth_accounts_index~=0.0.11a12 eth_accounts_index~=0.0.12a1
erc20-transfer-authorization~=0.3.1a7 erc20-transfer-authorization~=0.3.2a1
uWSGI==2.0.19.1 uWSGI==2.0.19.1
semver==2.13.0 semver==2.13.0
websocket-client==0.57.0 websocket-client==0.57.0
moolb~=0.1.1b2 moolb~=0.1.1b2
eth-address-index~=0.1.1a11 eth-address-index~=0.1.2a1
chainlib~=0.0.3rc2 chainlib-eth~=0.0.5a1
hexathon~=0.0.1a7 hexathon~=0.0.1a7
chainsyncer[sql]==0.0.2a5 chainsyncer[sql]~=0.0.3a3
chainqueue~=0.0.2b3 chainqueue~=0.0.2b5
sarafu-faucet~=0.0.3a3 sarafu-faucet~=0.0.4a1
erc20-faucet~=0.2.1a5 erc20-faucet~=0.2.2a1
coincurve==15.0.0 coincurve==15.0.0
potaahto~=0.0.1a2 potaahto~=0.0.1a2
pycryptodome==3.10.1 pycryptodome==3.10.1

View File

@ -2,6 +2,8 @@
import os import os
import argparse import argparse
import logging import logging
import re
import sys
import alembic import alembic
from alembic.config import Config as AlembicConfig from alembic.config import Config as AlembicConfig
@ -23,6 +25,8 @@ argparser = argparse.ArgumentParser()
argparser.add_argument('-c', type=str, default=config_dir, help='config file') argparser.add_argument('-c', type=str, default=config_dir, help='config file')
argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration') argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration')
argparser.add_argument('--migrations-dir', dest='migrations_dir', default=migrationsdir, type=str, help='path to alembic migrations directory') argparser.add_argument('--migrations-dir', dest='migrations_dir', default=migrationsdir, type=str, help='path to alembic migrations directory')
argparser.add_argument('--reset', action='store_true', help='downgrade before upgrading')
argparser.add_argument('-f', action='store_true', help='force action')
argparser.add_argument('-v', action='store_true', help='be verbose') argparser.add_argument('-v', action='store_true', help='be verbose')
argparser.add_argument('-vv', action='store_true', help='be more verbose') argparser.add_argument('-vv', action='store_true', help='be more verbose')
args = argparser.parse_args() args = argparser.parse_args()
@ -53,4 +57,10 @@ ac = AlembicConfig(os.path.join(migrations_dir, 'alembic.ini'))
ac.set_main_option('sqlalchemy.url', dsn) ac.set_main_option('sqlalchemy.url', dsn)
ac.set_main_option('script_location', migrations_dir) ac.set_main_option('script_location', migrations_dir)
if args.reset:
if not args.f:
if not re.match(r'[yY][eE]?[sS]?', input('EEK! this will DELETE the existing db. are you sure??')):
logg.error('user chickened out on requested reset, bailing')
sys.exit(1)
alembic.command.downgrade(ac, 'base')
alembic.command.upgrade(ac, 'head') alembic.command.upgrade(ac, 'head')

View File

@ -41,11 +41,12 @@ def get_sms_queue_tasks(app, task_prefix='cic_notify.tasks.sms.'):
class Api: class Api:
# TODO: Implement callback strategy # TODO: Implement callback strategy
def __init__(self, queue='cic-notify'): def __init__(self, queue=None):
""" """
:param queue: The queue on which to execute notification tasks :param queue: The queue on which to execute notification tasks
:type queue: str :type queue: str
""" """
self.queue = queue
self.sms_tasks = get_sms_queue_tasks(app) self.sms_tasks = get_sms_queue_tasks(app)
logg.debug('sms tasks {}'.format(self.sms_tasks)) logg.debug('sms tasks {}'.format(self.sms_tasks))
@ -61,13 +62,19 @@ class Api:
""" """
signatures = [] signatures = []
for q in self.sms_tasks: for q in self.sms_tasks:
if not self.queue:
queue = q[0]
else:
queue = self.queue
signature = celery.signature( signature = celery.signature(
q[1], q[1],
[ [
message, message,
recipient, recipient,
], ],
queue=q[0], queue=queue,
) )
signatures.append(signature) signatures.append(signature)

View File

@ -9,7 +9,7 @@ import semver
logg = logging.getLogger() logg = logging.getLogger()
version = (0, 4, 0, 'alpha.5') version = (0, 4, 0, 'alpha.6')
version_object = semver.VersionInfo( version_object = semver.VersionInfo(
major=version[0], major=version[0],

View File

@ -1 +1 @@
cic_base[full_graph]~=0.1.2a61 cic_base[full_graph]==0.1.3a3+build.4aa03607

View File

@ -2,4 +2,3 @@ pytest~=6.0.1
pytest-celery~=0.0.0a1 pytest-celery~=0.0.0a1
pytest-mock~=3.3.1 pytest-mock~=3.3.1
pysqlite3~=0.4.3 pysqlite3~=0.4.3

View File

@ -0,0 +1,9 @@
# standard import
from enum import IntEnum
class AccountStatus(IntEnum):
PENDING = 1
ACTIVE = 2
LOCKED = 3
RESET = 4

View File

@ -1,19 +1,13 @@
# standard imports # standard imports
from enum import IntEnum
# third party imports
from sqlalchemy import Column, Integer, String
# local imports # local imports
from cic_ussd.db.enum import AccountStatus
from cic_ussd.db.models.base import SessionBase from cic_ussd.db.models.base import SessionBase
from cic_ussd.encoder import check_password_hash, create_password_hash from cic_ussd.encoder import check_password_hash, create_password_hash
# third party imports
class AccountStatus(IntEnum): from sqlalchemy import Column, Integer, String
PENDING = 1 from sqlalchemy.orm.session import Session
ACTIVE = 2
LOCKED = 3
RESET = 4
class Account(SessionBase): class Account(SessionBase):
@ -30,6 +24,21 @@ class Account(SessionBase):
account_status = Column(Integer) account_status = Column(Integer)
preferred_language = Column(String) preferred_language = Column(String)
@staticmethod
def get_by_phone_number(phone_number: str, session: Session):
"""Retrieves an account from a phone number.
:param phone_number: The E164 format of a phone number.
:type phone_number:str
:param session:
:type session:
:return: An account object.
:rtype: Account
"""
session = SessionBase.bind_session(session=session)
account = session.query(Account).filter_by(phone_number=phone_number).first()
SessionBase.release_session(session=session)
return account
def __init__(self, blockchain_address, phone_number): def __init__(self, blockchain_address, phone_number):
self.blockchain_address = blockchain_address self.blockchain_address = blockchain_address
self.phone_number = phone_number self.phone_number = phone_number

View File

@ -6,11 +6,13 @@ import logging
import celery import celery
import i18n import i18n
from cic_eth.api.api_task import Api from cic_eth.api.api_task import Api
from sqlalchemy.orm.session import Session
from tinydb.table import Document from tinydb.table import Document
from typing import Optional from typing import Optional
# local imports # local imports
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase
from cic_ussd.db.models.ussd_session import UssdSession from cic_ussd.db.models.ussd_session import UssdSession
from cic_ussd.db.models.task_tracker import TaskTracker from cic_ussd.db.models.task_tracker import TaskTracker
from cic_ussd.menu.ussd_menu import UssdMenu from cic_ussd.menu.ussd_menu import UssdMenu
@ -22,15 +24,18 @@ from cic_ussd.validator import check_known_user, validate_response_type
logg = logging.getLogger() logg = logging.getLogger()
def add_tasks_to_tracker(task_uuid): def add_tasks_to_tracker(session, task_uuid: str):
""" """This function takes tasks spawned over api interfaces and records their creation time for tracking.
This function takes tasks spawned over api interfaces and records their creation time for tracking. :param session:
:type session:
:param task_uuid: The uuid for an initiated task. :param task_uuid: The uuid for an initiated task.
:type task_uuid: str :type task_uuid: str
""" """
session = SessionBase.bind_session(session=session)
task_record = TaskTracker(task_uuid=task_uuid) task_record = TaskTracker(task_uuid=task_uuid)
TaskTracker.session.add(task_record) session.add(task_record)
TaskTracker.session.commit() session.flush()
SessionBase.release_session(session=session)
def define_response_with_content(headers: list, response: str) -> tuple: def define_response_with_content(headers: list, response: str) -> tuple:
@ -95,6 +100,7 @@ def create_or_update_session(
service_code: str, service_code: str,
user_input: str, user_input: str,
current_menu: str, current_menu: str,
session,
session_data: Optional[dict] = None) -> InMemoryUssdSession: session_data: Optional[dict] = None) -> InMemoryUssdSession:
""" """
Handles the creation or updating of session as necessary. Handles the creation or updating of session as necessary.
@ -108,12 +114,15 @@ def create_or_update_session(
:type user_input: str :type user_input: str
:param current_menu: Menu name that is currently being displayed on the ussd session :param current_menu: Menu name that is currently being displayed on the ussd session
:type current_menu: str :type current_menu: str
:param session:
:type session:
:param session_data: Any additional data that was persisted during the user's interaction with the system. :param session_data: Any additional data that was persisted during the user's interaction with the system.
:type session_data: dict. :type session_data: dict.
:return: ussd session object :return: ussd session object
:rtype: InMemoryUssdSession :rtype: InMemoryUssdSession
""" """
existing_ussd_session = UssdSession.session.query(UssdSession).filter_by( session = SessionBase.bind_session(session=session)
existing_ussd_session = session.query(UssdSession).filter_by(
external_session_id=external_session_id).first() external_session_id=external_session_id).first()
if existing_ussd_session: if existing_ussd_session:
@ -132,20 +141,25 @@ def create_or_update_session(
current_menu=current_menu, current_menu=current_menu,
session_data=session_data session_data=session_data
) )
SessionBase.release_session(session=session)
return ussd_session return ussd_session
def get_account_status(phone_number) -> str: def get_account_status(phone_number, session: Session) -> str:
"""Get the status of a user's account. """Get the status of a user's account.
:param phone_number: The phone number to be checked. :param phone_number: The phone number to be checked.
:type phone_number: str :type phone_number: str
:param session:
:type session:
:return: The user account status. :return: The user account status.
:rtype: str :rtype: str
""" """
user = Account.session.query(Account).filter_by(phone_number=phone_number).first() session = SessionBase.bind_session(session=session)
status = user.get_account_status() account = Account.get_by_phone_number(phone_number=phone_number, session=session)
Account.session.add(user) status = account.get_account_status()
Account.session.commit() session.add(account)
session.flush()
SessionBase.release_session(session=session)
return status return status
@ -165,6 +179,7 @@ def initiate_account_creation_request(chain_str: str,
external_session_id: str, external_session_id: str,
phone_number: str, phone_number: str,
service_code: str, service_code: str,
session,
user_input: str) -> str: user_input: str) -> str:
"""This function issues a task to create a blockchain account on cic-eth. It then creates a record of the ussd """This function issues a task to create a blockchain account on cic-eth. It then creates a record of the ussd
session corresponding to the creation of the account and returns a response denoting that the user's account is session corresponding to the creation of the account and returns a response denoting that the user's account is
@ -177,6 +192,8 @@ def initiate_account_creation_request(chain_str: str,
:type phone_number: str :type phone_number: str
:param service_code: The service code dialed. :param service_code: The service code dialed.
:type service_code: str :type service_code: str
:param session:
:type session:
:param user_input: The input entered by the user. :param user_input: The input entered by the user.
:type user_input: str :type user_input: str
:return: A response denoting that the account is being created. :return: A response denoting that the account is being created.
@ -190,7 +207,7 @@ def initiate_account_creation_request(chain_str: str,
creation_task_id = cic_eth_api.create_account().id creation_task_id = cic_eth_api.create_account().id
# record task initiation time # record task initiation time
add_tasks_to_tracker(task_uuid=creation_task_id) add_tasks_to_tracker(task_uuid=creation_task_id, session=session)
# cache account creation data # cache account creation data
cache_account_creation_task_id(phone_number=phone_number, task_id=creation_task_id) cache_account_creation_task_id(phone_number=phone_number, task_id=creation_task_id)
@ -204,6 +221,7 @@ def initiate_account_creation_request(chain_str: str,
phone=phone_number, phone=phone_number,
service_code=service_code, service_code=service_code,
current_menu=current_menu.get('name'), current_menu=current_menu.get('name'),
session=session,
user_input=user_input) user_input=user_input)
# define response to relay to user # define response to relay to user
@ -268,12 +286,14 @@ def cache_account_creation_task_id(phone_number: str, task_id: str):
redis_cache.persist(name=task_id) redis_cache.persist(name=task_id)
def process_current_menu(ussd_session: Optional[dict], user: Account, user_input: str) -> Document: def process_current_menu(account: Account, session: Session, ussd_session: Optional[dict], user_input: str) -> Document:
"""This function checks user input and returns a corresponding ussd menu """This function checks user input and returns a corresponding ussd menu
:param ussd_session: An in db ussd session object. :param ussd_session: An in db ussd session object.
:type ussd_session: UssdSession :type ussd_session: UssdSession
:param user: A user object. :param account: A account object.
:type user: Account :type account: Account
:param session:
:type session:
:param user_input: The user's input. :param user_input: The user's input.
:type user_input: str :type user_input: str
:return: An in memory ussd menu object. :return: An in memory ussd menu object.
@ -285,7 +305,13 @@ def process_current_menu(ussd_session: Optional[dict], user: Account, user_input
else: else:
# get current state # get current state
latest_input = get_latest_input(user_input=user_input) latest_input = get_latest_input(user_input=user_input)
current_menu = process_request(ussd_session=ussd_session, user_input=latest_input, user=user) session = SessionBase.bind_session(session=session)
current_menu = process_request(
account=account,
session=session,
ussd_session=ussd_session,
user_input=latest_input)
SessionBase.release_session(session=session)
return current_menu return current_menu
@ -294,6 +320,7 @@ def process_menu_interaction_requests(chain_str: str,
phone_number: str, phone_number: str,
queue: str, queue: str,
service_code: str, service_code: str,
session,
user_input: str) -> str: user_input: str) -> str:
"""This function handles requests intended for interaction with ussd menu, it checks whether a user matching the """This function handles requests intended for interaction with ussd menu, it checks whether a user matching the
provided phone number exists and in the absence of which it creates an account for the user. provided phone number exists and in the absence of which it creates an account for the user.
@ -308,25 +335,29 @@ def process_menu_interaction_requests(chain_str: str,
:type queue: str :type queue: str
:param service_code: The service dialed by the user making the request. :param service_code: The service dialed by the user making the request.
:type service_code: str :type service_code: str
:param session:
:type session:
:param user_input: The inputs entered by the user. :param user_input: The inputs entered by the user.
:type user_input: str :type user_input: str
:return: A response based on the request received. :return: A response based on the request received.
:rtype: str :rtype: str
""" """
# check whether the user exists # check whether the user exists
if not check_known_user(phone=phone_number): if not check_known_user(phone_number=phone_number, session=session):
response = initiate_account_creation_request(chain_str=chain_str, response = initiate_account_creation_request(chain_str=chain_str,
external_session_id=external_session_id, external_session_id=external_session_id,
phone_number=phone_number, phone_number=phone_number,
service_code=service_code, service_code=service_code,
session=session,
user_input=user_input) user_input=user_input)
else: else:
# get user # get account
user = Account.session.query(Account).filter_by(phone_number=phone_number).first() session = SessionBase.bind_session(session=session)
account = Account.get_by_phone_number(phone_number=phone_number, session=session)
# retrieve and cache user's metadata # retrieve and cache user's metadata
blockchain_address = user.blockchain_address blockchain_address = account.blockchain_address
s_query_person_metadata = celery.signature( s_query_person_metadata = celery.signature(
'cic_ussd.tasks.metadata.query_person_metadata', 'cic_ussd.tasks.metadata.query_person_metadata',
[blockchain_address] [blockchain_address]
@ -334,24 +365,25 @@ def process_menu_interaction_requests(chain_str: str,
s_query_person_metadata.apply_async(queue='cic-ussd') s_query_person_metadata.apply_async(queue='cic-ussd')
# find any existing ussd session # find any existing ussd session
existing_ussd_session = UssdSession.session.query(UssdSession).filter_by( existing_ussd_session = session.query(UssdSession).filter_by(external_session_id=external_session_id).first()
external_session_id=external_session_id).first()
# validate user inputs # validate user inputs
if existing_ussd_session: if existing_ussd_session:
current_menu = process_current_menu( current_menu = process_current_menu(
account=account,
session=session,
ussd_session=existing_ussd_session.to_json(), ussd_session=existing_ussd_session.to_json(),
user=user,
user_input=user_input user_input=user_input
) )
else: else:
current_menu = process_current_menu( current_menu = process_current_menu(
account=account,
session=session,
ussd_session=None, ussd_session=None,
user=user,
user_input=user_input user_input=user_input
) )
last_ussd_session = retrieve_most_recent_ussd_session(phone_number=user.phone_number) last_ussd_session = retrieve_most_recent_ussd_session(phone_number=account.phone_number, session=session)
if last_ussd_session: if last_ussd_session:
# create or update the ussd session as appropriate # create or update the ussd session as appropriate
@ -361,6 +393,7 @@ def process_menu_interaction_requests(chain_str: str,
service_code=service_code, service_code=service_code,
user_input=user_input, user_input=user_input,
current_menu=current_menu.get('name'), current_menu=current_menu.get('name'),
session=session,
session_data=last_ussd_session.session_data session_data=last_ussd_session.session_data
) )
else: else:
@ -369,15 +402,17 @@ def process_menu_interaction_requests(chain_str: str,
phone=phone_number, phone=phone_number,
service_code=service_code, service_code=service_code,
user_input=user_input, user_input=user_input,
current_menu=current_menu.get('name') current_menu=current_menu.get('name'),
session=session
) )
# define appropriate response # define appropriate response
response = custom_display_text( response = custom_display_text(
account=account,
display_key=current_menu.get('display_key'), display_key=current_menu.get('display_key'),
menu_name=current_menu.get('name'), menu_name=current_menu.get('name'),
session=session,
ussd_session=ussd_session.to_json(), ussd_session=ussd_session.to_json(),
user=user
) )
# check that the response from the processor is valid # check that the response from the processor is valid
@ -386,21 +421,26 @@ def process_menu_interaction_requests(chain_str: str,
# persist session to db # persist session to db
persist_session_to_db_task(external_session_id=external_session_id, queue=queue) persist_session_to_db_task(external_session_id=external_session_id, queue=queue)
SessionBase.release_session(session=session)
return response return response
def reset_pin(phone_number: str) -> str: def reset_pin(phone_number: str, session: Session) -> str:
"""Reset account status from Locked to Pending. """Reset account status from Locked to Pending.
:param phone_number: The phone number belonging to the account to be unlocked. :param phone_number: The phone number belonging to the account to be unlocked.
:type phone_number: str :type phone_number: str
:param session:
:type session:
:return: The status of the pin reset. :return: The status of the pin reset.
:rtype: str :rtype: str
""" """
user = Account.session.query(Account).filter_by(phone_number=phone_number).first() session = SessionBase.bind_session(session=session)
user.reset_account_pin() account = Account.get_by_phone_number(phone_number=phone_number, session=session)
Account.session.add(user) account.reset_account_pin()
Account.session.commit() session.add(account)
session.flush()
SessionBase.release_session(session=session)
response = f'Pin reset for user {phone_number} is successful!' response = f'Pin reset for user {phone_number} is successful!'
return response return response
@ -438,11 +478,13 @@ def update_ussd_session(
return session return session
def save_to_in_memory_ussd_session_data(queue: str, session_data: dict, ussd_session: dict): def save_to_in_memory_ussd_session_data(queue: str, session: Session, session_data: dict, ussd_session: dict):
"""This function is used to save information to the session data attribute of a ussd session object in the redis """This function is used to save information to the session data attribute of a ussd session object in the redis
cache. cache.
:param queue: The queue on which the celery task should run. :param queue: The queue on which the celery task should run.
:type queue: str :type queue: str
:param session:
:type session:
:param session_data: A dictionary containing data for a specific ussd session in redis that needs to be saved :param session_data: A dictionary containing data for a specific ussd session in redis that needs to be saved
temporarily. temporarily.
:type session_data: dict :type session_data: dict
@ -473,7 +515,7 @@ def save_to_in_memory_ussd_session_data(queue: str, session_data: dict, ussd_ses
service_code=in_redis_ussd_session.get('service_code'), service_code=in_redis_ussd_session.get('service_code'),
user_input=in_redis_ussd_session.get('user_input'), user_input=in_redis_ussd_session.get('user_input'),
current_menu=in_redis_ussd_session.get('state'), current_menu=in_redis_ussd_session.get('state'),
session=session,
session_data=session_data session_data=session_data
) )
persist_session_to_db_task(external_session_id=external_session_id, queue=queue) persist_session_to_db_task(external_session_id=external_session_id, queue=queue)

View File

@ -33,19 +33,5 @@ def process_phone_number(phone_number: str, region: str):
return parsed_phone_number return parsed_phone_number
def get_user_by_phone_number(phone_number: str) -> Optional[Account]:
"""This function queries the database for a user based on the provided phone number.
:param phone_number: A valid phone number.
:type phone_number: str
:return: A user object matching a given phone number
:rtype: Account|None
"""
# consider adding region to user's metadata
phone_number = process_phone_number(phone_number=phone_number, region=E164Format.region)
user = Account.session.query(Account).filter_by(phone_number=phone_number).first()
return user
class Support: class Support:
phone_number = None phone_number = None

View File

@ -2,25 +2,26 @@
import datetime import datetime
import logging import logging
import json import json
import re
from typing import Optional from typing import Optional
# third party imports # third party imports
import celery
from sqlalchemy import desc from sqlalchemy import desc
from cic_eth.api import Api from cic_eth.api import Api
from sqlalchemy.orm.session import Session
from tinydb.table import Document from tinydb.table import Document
# local imports # local imports
from cic_ussd.account import define_account_tx_metadata, retrieve_account_statement from cic_ussd.account import define_account_tx_metadata, retrieve_account_statement
from cic_ussd.balance import BalanceManager, compute_operational_balance, get_cached_operational_balance from cic_ussd.balance import BalanceManager, compute_operational_balance, get_cached_operational_balance
from cic_ussd.chain import Chain from cic_ussd.chain import Chain
from cic_ussd.db.models.account import AccountStatus, Account from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase
from cic_ussd.db.models.ussd_session import UssdSession from cic_ussd.db.models.ussd_session import UssdSession
from cic_ussd.error import MetadataNotFoundError, SeppukuError from cic_ussd.db.enum import AccountStatus
from cic_ussd.error import SeppukuError
from cic_ussd.menu.ussd_menu import UssdMenu from cic_ussd.menu.ussd_menu import UssdMenu
from cic_ussd.metadata import blockchain_address_to_metadata_pointer from cic_ussd.metadata import blockchain_address_to_metadata_pointer
from cic_ussd.phone_number import get_user_by_phone_number, Support from cic_ussd.phone_number import Support
from cic_ussd.redis import cache_data, create_cached_data_key, get_cached_data from cic_ussd.redis import cache_data, create_cached_data_key, get_cached_data
from cic_ussd.state_machine import UssdStateMachine from cic_ussd.state_machine import UssdStateMachine
from cic_ussd.conversions import to_wei, from_wei from cic_ussd.conversions import to_wei, from_wei
@ -62,47 +63,48 @@ def retrieve_token_symbol(chain_str: str = Chain.spec.__str__()):
raise SeppukuError(f'Could not retrieve default token for: {chain_str}') raise SeppukuError(f'Could not retrieve default token for: {chain_str}')
def process_pin_authorization(display_key: str, user: Account, **kwargs) -> str: def process_pin_authorization(account: Account, display_key: str, **kwargs) -> str:
""" """This method provides translation for all ussd menu entries that follow the pin authorization pattern.
This method provides translation for all ussd menu entries that follow the pin authorization pattern. :param account: The account in a running USSD session.
:type account: Account
:param display_key: The path in the translation files defining an appropriate ussd response :param display_key: The path in the translation files defining an appropriate ussd response
:type display_key: str :type display_key: str
:param user: The user in a running USSD session.
:type user: Account
:param kwargs: Any additional information required by the text values in the internationalization files. :param kwargs: Any additional information required by the text values in the internationalization files.
:type kwargs :type kwargs
:return: A string value corresponding the ussd menu's text value. :return: A string value corresponding the ussd menu's text value.
:rtype: str :rtype: str
""" """
remaining_attempts = 3 remaining_attempts = 3
if user.failed_pin_attempts > 0: if account.failed_pin_attempts > 0:
return translation_for( return translation_for(
key=f'{display_key}.retry', key=f'{display_key}.retry',
preferred_language=user.preferred_language, preferred_language=account.preferred_language,
remaining_attempts=(remaining_attempts - user.failed_pin_attempts) remaining_attempts=(remaining_attempts - account.failed_pin_attempts)
) )
else: else:
return translation_for( return translation_for(
key=f'{display_key}.first', key=f'{display_key}.first',
preferred_language=user.preferred_language, preferred_language=account.preferred_language,
**kwargs **kwargs
) )
def process_exit_insufficient_balance(display_key: str, user: Account, ussd_session: dict): def process_exit_insufficient_balance(account: Account, display_key: str, session: Session, ussd_session: dict):
"""This function processes the exit menu letting users their account balance is insufficient to perform a specific """This function processes the exit menu letting users their account balance is insufficient to perform a specific
transaction. transaction.
:param account: The account requesting access to the ussd menu.
:type account: Account
:param display_key: The path in the translation files defining an appropriate ussd response :param display_key: The path in the translation files defining an appropriate ussd response
:type display_key: str :type display_key: str
:param user: The user requesting access to the ussd menu. :param session:
:type user: Account :type session:
:param ussd_session: A JSON serialized in-memory ussd session object :param ussd_session: A JSON serialized in-memory ussd session object
:type ussd_session: dict :type ussd_session: dict
:return: Corresponding translation text response :return: Corresponding translation text response
:rtype: str :rtype: str
""" """
# get account balance # get account balance
operational_balance = get_cached_operational_balance(blockchain_address=user.blockchain_address) operational_balance = get_cached_operational_balance(blockchain_address=account.blockchain_address)
# compile response data # compile response data
user_input = ussd_session.get('user_input').split('*')[-1] user_input = ussd_session.get('user_input').split('*')[-1]
@ -112,13 +114,13 @@ def process_exit_insufficient_balance(display_key: str, user: Account, ussd_sess
token_symbol = retrieve_token_symbol() token_symbol = retrieve_token_symbol()
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number') recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
recipient = get_user_by_phone_number(phone_number=recipient_phone_number) recipient = Account.get_by_phone_number(phone_number=recipient_phone_number, session=session)
tx_recipient_information = define_account_tx_metadata(user=recipient) tx_recipient_information = define_account_tx_metadata(user=recipient)
return translation_for( return translation_for(
key=display_key, key=display_key,
preferred_language=user.preferred_language, preferred_language=account.preferred_language,
amount=from_wei(transaction_amount), amount=from_wei(transaction_amount),
token_symbol=token_symbol, token_symbol=token_symbol,
recipient_information=tx_recipient_information, recipient_information=tx_recipient_information,
@ -126,12 +128,14 @@ def process_exit_insufficient_balance(display_key: str, user: Account, ussd_sess
) )
def process_exit_successful_transaction(display_key: str, user: Account, ussd_session: dict): def process_exit_successful_transaction(account: Account, display_key: str, session: Session, ussd_session: dict):
"""This function processes the exit menu after a successful initiation for a transfer of tokens. """This function processes the exit menu after a successful initiation for a transfer of tokens.
:param account: The account requesting access to the ussd menu.
:type account: Account
:param display_key: The path in the translation files defining an appropriate ussd response :param display_key: The path in the translation files defining an appropriate ussd response
:type display_key: str :type display_key: str
:param user: The user requesting access to the ussd menu. :param session:
:type user: Account :type session:
:param ussd_session: A JSON serialized in-memory ussd session object :param ussd_session: A JSON serialized in-memory ussd session object
:type ussd_session: dict :type ussd_session: dict
:return: Corresponding translation text response :return: Corresponding translation text response
@ -140,13 +144,13 @@ def process_exit_successful_transaction(display_key: str, user: Account, ussd_se
transaction_amount = to_wei(int(ussd_session.get('session_data').get('transaction_amount'))) transaction_amount = to_wei(int(ussd_session.get('session_data').get('transaction_amount')))
token_symbol = retrieve_token_symbol() token_symbol = retrieve_token_symbol()
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number') recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
recipient = get_user_by_phone_number(phone_number=recipient_phone_number) recipient = Account.get_by_phone_number(phone_number=recipient_phone_number, session=session)
tx_recipient_information = define_account_tx_metadata(user=recipient) tx_recipient_information = define_account_tx_metadata(user=recipient)
tx_sender_information = define_account_tx_metadata(user=user) tx_sender_information = define_account_tx_metadata(user=account)
return translation_for( return translation_for(
key=display_key, key=display_key,
preferred_language=user.preferred_language, preferred_language=account.preferred_language,
transaction_amount=from_wei(transaction_amount), transaction_amount=from_wei(transaction_amount),
token_symbol=token_symbol, token_symbol=token_symbol,
recipient_information=tx_recipient_information, recipient_information=tx_recipient_information,
@ -154,13 +158,15 @@ def process_exit_successful_transaction(display_key: str, user: Account, ussd_se
) )
def process_transaction_pin_authorization(user: Account, display_key: str, ussd_session: dict): def process_transaction_pin_authorization(account: Account, display_key: str, session: Session, ussd_session: dict):
"""This function processes pin authorization where making a transaction is concerned. It constructs a """This function processes pin authorization where making a transaction is concerned. It constructs a
pre-transaction response menu that shows the details of the transaction. pre-transaction response menu that shows the details of the transaction.
:param user: The user requesting access to the ussd menu. :param account: The account requesting access to the ussd menu.
:type user: Account :type account: Account
:param display_key: The path in the translation files defining an appropriate ussd response :param display_key: The path in the translation files defining an appropriate ussd response
:type display_key: str :type display_key: str
:param session:
:type session:
:param ussd_session: The USSD session determining what user data needs to be extracted and added to the menu's :param ussd_session: The USSD session determining what user data needs to be extracted and added to the menu's
text values. text values.
:type ussd_session: UssdSession :type ussd_session: UssdSession
@ -169,16 +175,16 @@ def process_transaction_pin_authorization(user: Account, display_key: str, ussd_
""" """
# compile response data # compile response data
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number') recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
recipient = get_user_by_phone_number(phone_number=recipient_phone_number) recipient = Account.get_by_phone_number(phone_number=recipient_phone_number, session=session)
tx_recipient_information = define_account_tx_metadata(user=recipient) tx_recipient_information = define_account_tx_metadata(user=recipient)
tx_sender_information = define_account_tx_metadata(user=user) tx_sender_information = define_account_tx_metadata(user=account)
token_symbol = retrieve_token_symbol() token_symbol = retrieve_token_symbol()
user_input = ussd_session.get('session_data').get('transaction_amount') user_input = ussd_session.get('session_data').get('transaction_amount')
transaction_amount = to_wei(value=int(user_input)) transaction_amount = to_wei(value=int(user_input))
logg.debug('Requires integration to determine user tokens.') logg.debug('Requires integration to determine user tokens.')
return process_pin_authorization( return process_pin_authorization(
user=user, account=account,
display_key=display_key, display_key=display_key,
recipient_information=tx_recipient_information, recipient_information=tx_recipient_information,
transaction_amount=from_wei(transaction_amount), transaction_amount=from_wei(transaction_amount),
@ -187,14 +193,12 @@ def process_transaction_pin_authorization(user: Account, display_key: str, ussd_
) )
def process_account_balances(user: Account, display_key: str, ussd_session: dict): def process_account_balances(user: Account, display_key: str):
""" """
:param user: :param user:
:type user: :type user:
:param display_key: :param display_key:
:type display_key: :type display_key:
:param ussd_session:
:type ussd_session:
:return: :return:
:rtype: :rtype:
""" """
@ -290,19 +294,18 @@ def process_display_user_metadata(user: Account, display_key: str):
preferred_language=user.preferred_language, preferred_language=user.preferred_language,
full_name=absent, full_name=absent,
gender=absent, gender=absent,
age=absent,
location=absent, location=absent,
products=absent products=absent
) )
def process_account_statement(user: Account, display_key: str, ussd_session: dict): def process_account_statement(user: Account, display_key: str):
""" """
:param user: :param user:
:type user: :type user:
:param display_key: :param display_key:
:type display_key: :type display_key:
:param ussd_session:
:type ussd_session:
:return: :return:
:rtype: :rtype:
""" """
@ -404,23 +407,26 @@ def process_start_menu(display_key: str, user: Account):
) )
def retrieve_most_recent_ussd_session(phone_number: str) -> UssdSession: def retrieve_most_recent_ussd_session(phone_number: str, session: Session) -> UssdSession:
# get last ussd session based on user phone number # get last ussd session based on user phone number
last_ussd_session = UssdSession.session\ session = SessionBase.bind_session(session=session)
.query(UssdSession)\ last_ussd_session = session.query(UssdSession)\
.filter_by(msisdn=phone_number)\ .filter_by(msisdn=phone_number)\
.order_by(desc(UssdSession.created))\ .order_by(desc(UssdSession.created))\
.first() .first()
SessionBase.release_session(session=session)
return last_ussd_session return last_ussd_session
def process_request(user_input: str, user: Account, ussd_session: Optional[dict] = None) -> Document: def process_request(account: Account, session, user_input: str, ussd_session: Optional[dict] = None) -> Document:
"""This function assesses a request based on the user from the request comes, the session_id and the user's """This function assesses a request based on the user from the request comes, the session_id and the user's
input. It determines whether the request translates to a return to an existing session by checking whether the input. It determines whether the request translates to a return to an existing session by checking whether the
provided session id exists in the database or whether the creation of a new ussd session object is warranted. provided session id exists in the database or whether the creation of a new ussd session object is warranted.
It then returns the appropriate ussd menu text values. It then returns the appropriate ussd menu text values.
:param user: The user requesting access to the ussd menu. :param account: The account requesting access to the ussd menu.
:type user: Account :type account: Account
:param session:
:type session:
:param user_input: The value a user enters in the ussd menu. :param user_input: The value a user enters in the ussd menu.
:type user_input: str :type user_input: str
:param ussd_session: A JSON serialized in-memory ussd session object :param ussd_session: A JSON serialized in-memory ussd session object
@ -433,11 +439,15 @@ def process_request(user_input: str, user: Account, ussd_session: Optional[dict]
if user_input == "0": if user_input == "0":
return UssdMenu.parent_menu(menu_name=ussd_session.get('state')) return UssdMenu.parent_menu(menu_name=ussd_session.get('state'))
else: else:
successive_state = next_state(ussd_session=ussd_session, user=user, user_input=user_input) successive_state = next_state(
account=account,
session=session,
ussd_session=ussd_session,
user_input=user_input)
return UssdMenu.find_by_name(name=successive_state) return UssdMenu.find_by_name(name=successive_state)
else: else:
if user.has_valid_pin(): if account.has_valid_pin():
last_ussd_session = retrieve_most_recent_ussd_session(phone_number=user.phone_number) last_ussd_session = retrieve_most_recent_ussd_session(phone_number=account.phone_number, session=session)
if last_ussd_session: if last_ussd_session:
# get last state # get last state
@ -456,28 +466,30 @@ def process_request(user_input: str, user: Account, ussd_session: Optional[dict]
else: else:
return UssdMenu.find_by_name(name=last_state) return UssdMenu.find_by_name(name=last_state)
else: else:
if user.failed_pin_attempts >= 3 and user.get_account_status() == AccountStatus.LOCKED.name: if account.failed_pin_attempts >= 3 and account.get_account_status() == AccountStatus.LOCKED.name:
return UssdMenu.find_by_name(name='exit_pin_blocked') return UssdMenu.find_by_name(name='exit_pin_blocked')
elif user.preferred_language is None: elif account.preferred_language is None:
return UssdMenu.find_by_name(name='initial_language_selection') return UssdMenu.find_by_name(name='initial_language_selection')
else: else:
return UssdMenu.find_by_name(name='initial_pin_entry') return UssdMenu.find_by_name(name='initial_pin_entry')
def next_state(ussd_session: dict, user: Account, user_input: str) -> str: def next_state(account: Account, session, ussd_session: dict, user_input: str) -> str:
"""This function navigates the state machine based on the ussd session object and user inputs it receives. """This function navigates the state machine based on the ussd session object and user inputs it receives.
It checks the user input and provides the successive state in the state machine. It then updates the session's It checks the user input and provides the successive state in the state machine. It then updates the session's
state attribute with the new state. state attribute with the new state.
:param account: The account requesting access to the ussd menu.
:type account: Account
:param session:
:type session:
:param ussd_session: A JSON serialized in-memory ussd session object :param ussd_session: A JSON serialized in-memory ussd session object
:type ussd_session: dict :type ussd_session: dict
:param user: The user requesting access to the ussd menu.
:type user: Account
:param user_input: The value a user enters in the ussd menu. :param user_input: The value a user enters in the ussd menu.
:type user_input: str :type user_input: str
:return: A string value corresponding the successive give a specific state in the state machine. :return: A string value corresponding the successive give a specific state in the state machine.
""" """
state_machine = UssdStateMachine(ussd_session=ussd_session) state_machine = UssdStateMachine(ussd_session=ussd_session)
state_machine.scan_data((user_input, ussd_session, user)) state_machine.scan_data((user_input, ussd_session, account, session))
new_state = state_machine.state new_state = state_machine.state
return new_state return new_state
@ -492,42 +504,63 @@ def process_exit_invalid_menu_option(display_key: str, preferred_language: str):
def custom_display_text( def custom_display_text(
account: Account,
display_key: str, display_key: str,
menu_name: str, menu_name: str,
ussd_session: dict, session: Session,
user: Account) -> str: ussd_session: dict) -> str:
"""This function extracts the appropriate session data based on the current menu name. It then inserts them as """This function extracts the appropriate session data based on the current menu name. It then inserts them as
keywords in the i18n function. keywords in the i18n function.
:param account: The account in a running USSD session.
:type account: Account
:param display_key: The path in the translation files defining an appropriate ussd response :param display_key: The path in the translation files defining an appropriate ussd response
:type display_key: str :type display_key: str
:param menu_name: The name by which a specific menu can be identified. :param menu_name: The name by which a specific menu can be identified.
:type menu_name: str :type menu_name: str
:param user: The user in a running USSD session. :param session:
:type user: Account :type session:
:param ussd_session: A JSON serialized in-memory ussd session object :param ussd_session: A JSON serialized in-memory ussd session object
:type ussd_session: dict :type ussd_session: dict
:return: A string value corresponding the ussd menu's text value. :return: A string value corresponding the ussd menu's text value.
:rtype: str :rtype: str
""" """
if menu_name == 'transaction_pin_authorization': if menu_name == 'transaction_pin_authorization':
return process_transaction_pin_authorization(display_key=display_key, user=user, ussd_session=ussd_session) return process_transaction_pin_authorization(
account=account,
display_key=display_key,
session=session,
ussd_session=ussd_session)
elif menu_name == 'exit_insufficient_balance': elif menu_name == 'exit_insufficient_balance':
return process_exit_insufficient_balance(display_key=display_key, user=user, ussd_session=ussd_session) return process_exit_insufficient_balance(
account=account,
display_key=display_key,
session=session,
ussd_session=ussd_session)
elif menu_name == 'exit_successful_transaction': elif menu_name == 'exit_successful_transaction':
return process_exit_successful_transaction(display_key=display_key, user=user, ussd_session=ussd_session) return process_exit_successful_transaction(
account=account,
display_key=display_key,
session=session,
ussd_session=ussd_session)
elif menu_name == 'start': elif menu_name == 'start':
return process_start_menu(display_key=display_key, user=user) return process_start_menu(display_key=display_key, user=account)
elif 'pin_authorization' in menu_name: elif 'pin_authorization' in menu_name:
return process_pin_authorization(display_key=display_key, user=user) return process_pin_authorization(
account=account,
display_key=display_key,
session=session)
elif 'enter_current_pin' in menu_name: elif 'enter_current_pin' in menu_name:
return process_pin_authorization(display_key=display_key, user=user) return process_pin_authorization(
account=account,
display_key=display_key,
session=session)
elif menu_name == 'account_balances': elif menu_name == 'account_balances':
return process_account_balances(display_key=display_key, user=user, ussd_session=ussd_session) return process_account_balances(display_key=display_key, user=account)
elif 'transaction_set' in menu_name: elif 'transaction_set' in menu_name:
return process_account_statement(display_key=display_key, user=user, ussd_session=ussd_session) return process_account_statement(display_key=display_key, user=account)
elif menu_name == 'display_user_metadata': elif menu_name == 'display_user_metadata':
return process_display_user_metadata(display_key=display_key, user=user) return process_display_user_metadata(display_key=display_key, user=account)
elif menu_name == 'exit_invalid_menu_option': elif menu_name == 'exit_invalid_menu_option':
return process_exit_invalid_menu_option(display_key=display_key, preferred_language=user.preferred_language) return process_exit_invalid_menu_option(display_key=display_key, preferred_language=account.preferred_language)
else: else:
return translation_for(key=display_key, preferred_language=user.preferred_language) return translation_for(key=display_key, preferred_language=account.preferred_language)

View File

@ -8,9 +8,12 @@ from urllib.parse import urlparse, parse_qs
# third-party imports # third-party imports
from sqlalchemy import desc from sqlalchemy import desc
from sqlalchemy.orm.session import Session
# local imports # local imports
from cic_ussd.db.models.account import AccountStatus, Account from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase
from cic_ussd.db.enum import AccountStatus
from cic_ussd.operations import get_account_status, reset_pin from cic_ussd.operations import get_account_status, reset_pin
from cic_ussd.validator import check_known_user from cic_ussd.validator import check_known_user
@ -72,24 +75,26 @@ def get_account_creation_callback_request_data(env: dict) -> tuple:
return status, task_id, result return status, task_id, result
def process_pin_reset_requests(env: dict, phone_number: str): def process_pin_reset_requests(env: dict, phone_number: str, session: Session):
"""This function processes requests that are responsible for the pin reset functionality. It processes GET and PUT """This function processes requests that are responsible for the pin reset functionality. It processes GET and PUT
requests responsible for returning an account's status and requests responsible for returning an account's status and
:param env: A dictionary of values representing data sent on the api. :param env: A dictionary of values representing data sent on the api.
:type env: dict :type env: dict
:param phone_number: The phone of the user whose pin is being reset. :param phone_number: The phone of the user whose pin is being reset.
:type phone_number: str :type phone_number: str
:param session:
:type session:
:return: A response denoting the result of the request to reset the user's pin. :return: A response denoting the result of the request to reset the user's pin.
:rtype: str :rtype: str
""" """
if not check_known_user(phone=phone_number): if not check_known_user(phone_number=phone_number, session=session):
return f'No user matching {phone_number} was found.', '404 Not Found' return f'No user matching {phone_number} was found.', '404 Not Found'
if get_request_method(env) == 'PUT': if get_request_method(env) == 'PUT':
return reset_pin(phone_number=phone_number), '200 OK' return reset_pin(phone_number=phone_number, session=session), '200 OK'
if get_request_method(env) == 'GET': if get_request_method(env) == 'GET':
status = get_account_status(phone_number=phone_number) status = get_account_status(phone_number=phone_number, session=session)
response = { response = {
'status': f'{status}' 'status': f'{status}'
} }
@ -97,16 +102,18 @@ def process_pin_reset_requests(env: dict, phone_number: str):
return response, '200 OK' return response, '200 OK'
def process_locked_accounts_requests(env: dict) -> tuple: def process_locked_accounts_requests(env: dict, session: Session) -> tuple:
"""This function authenticates staff requests and returns a serialized JSON formatted list of blockchain addresses """This function authenticates staff requests and returns a serialized JSON formatted list of blockchain addresses
of accounts for which the PIN has been locked due to too many failed attempts. of accounts for which the PIN has been locked due to too many failed attempts.
:param env: A dictionary of values representing data sent on the api. :param env: A dictionary of values representing data sent on the api.
:type env: dict :type env: dict
:param session:
:type session:
:return: A tuple containing a serialized list of blockchain addresses for locked accounts and corresponding message :return: A tuple containing a serialized list of blockchain addresses for locked accounts and corresponding message
for the response. for the response.
:rtype: tuple :rtype: tuple
""" """
logg.debug('Authentication requires integration with cic-auth') session = SessionBase.bind_session(session=session)
response = '' response = ''
if get_request_method(env) == 'GET': if get_request_method(env) == 'GET':
@ -123,12 +130,14 @@ def process_locked_accounts_requests(env: dict) -> tuple:
else: else:
limit = r[1] limit = r[1]
locked_accounts = Account.session.query(Account.blockchain_address).filter( locked_accounts = session.query(Account.blockchain_address).filter(
Account.account_status == AccountStatus.LOCKED.value, Account.account_status == AccountStatus.LOCKED.value,
Account.failed_pin_attempts >= 3).order_by(desc(Account.updated)).offset(offset).limit(limit).all() Account.failed_pin_attempts >= 3).order_by(desc(Account.updated)).offset(offset).limit(limit).all()
# convert lists to scalar blockchain addresses # convert lists to scalar blockchain addresses
locked_accounts = [blockchain_address for (blockchain_address, ) in locked_accounts] locked_accounts = [blockchain_address for (blockchain_address, ) in locked_accounts]
SessionBase.release_session(session=session)
response = json.dumps(locked_accounts) response = json.dumps(locked_accounts)
return response, '200 OK' return response, '200 OK'
return response, '405 Play by the rules' return response, '405 Play by the rules'

View File

@ -36,11 +36,8 @@ logg.debug('config loaded from {}:\n{}'.format(args.c, config))
# set up db # set up db
data_source_name = dsn_from_config(config) data_source_name = dsn_from_config(config)
SessionBase.connect(data_source_name, pool_size=int(config.get('DATABASE_POOL_SIZE')), debug=config.true('DATABASE_DEBUG')) SessionBase.connect(data_source_name, pool_size=int(config.get('DATABASE_POOL_SIZE')), debug=config.true('DATABASE_DEBUG'))
# create session for the life time of http request
SessionBase.session = SessionBase.create_session()
# handle requests from CICADA
def application(env, start_response): def application(env, start_response):
"""Loads python code for application to be accessible over web server """Loads python code for application to be accessible over web server
:param env: Object containing server and request information :param env: Object containing server and request information
@ -55,19 +52,24 @@ def application(env, start_response):
errors_headers = [('Content-Type', 'text/plain'), ('Content-Length', '0')] errors_headers = [('Content-Type', 'text/plain'), ('Content-Length', '0')]
headers = [('Content-Type', 'text/plain')] headers = [('Content-Type', 'text/plain')]
# create session for the life time of http request
session = SessionBase.create_session()
if get_request_endpoint(env) == '/pin': if get_request_endpoint(env) == '/pin':
phone_number = get_query_parameters(env=env, query_name='phoneNumber') phone_number = get_query_parameters(env=env, query_name='phoneNumber')
phone_number = quote_plus(phone_number) phone_number = quote_plus(phone_number)
response, message = process_pin_reset_requests(env=env, phone_number=phone_number) response, message = process_pin_reset_requests(env=env, phone_number=phone_number, session=session)
response_bytes, headers = define_response_with_content(headers=errors_headers, response=response) response_bytes, headers = define_response_with_content(headers=errors_headers, response=response)
SessionBase.session.close() session.commit()
session.close()
start_response(message, headers) start_response(message, headers)
return [response_bytes] return [response_bytes]
# handle requests for locked accounts # handle requests for locked accounts
response, message = process_locked_accounts_requests(env=env) response, message = process_locked_accounts_requests(env=env, session=session)
response_bytes, headers = define_response_with_content(headers=headers, response=response) response_bytes, headers = define_response_with_content(headers=headers, response=response)
start_response(message, headers) start_response(message, headers)
SessionBase.session.close() session.commit()
session.close()
return [response_bytes] return [response_bytes]

View File

@ -55,8 +55,6 @@ data_source_name = dsn_from_config(config)
SessionBase.connect(data_source_name, SessionBase.connect(data_source_name,
pool_size=int(config.get('DATABASE_POOL_SIZE')), pool_size=int(config.get('DATABASE_POOL_SIZE')),
debug=config.true('DATABASE_DEBUG')) debug=config.true('DATABASE_DEBUG'))
# create session for the life time of http request
SessionBase.session = SessionBase.create_session()
# set up translations # set up translations
i18n.load_path.append(config.get('APP_LOCALE_PATH')) i18n.load_path.append(config.get('APP_LOCALE_PATH'))
@ -143,6 +141,9 @@ def application(env, start_response):
errors_headers = [('Content-Type', 'text/plain'), ('Content-Length', '0')] errors_headers = [('Content-Type', 'text/plain'), ('Content-Length', '0')]
headers = [('Content-Type', 'text/plain')] headers = [('Content-Type', 'text/plain')]
# create session for the life time of http request
session = SessionBase.create_session()
if get_request_method(env=env) == 'POST' and get_request_endpoint(env=env) == '/': if get_request_method(env=env) == 'POST' and get_request_endpoint(env=env) == '/':
if env.get('CONTENT_TYPE') != 'application/x-www-form-urlencoded': if env.get('CONTENT_TYPE') != 'application/x-www-form-urlencoded':
@ -206,17 +207,20 @@ def application(env, start_response):
phone_number=phone_number, phone_number=phone_number,
queue=args.q, queue=args.q,
service_code=service_code, service_code=service_code,
session=session,
user_input=user_input) user_input=user_input)
response_bytes, headers = define_response_with_content(headers=headers, response=response) response_bytes, headers = define_response_with_content(headers=headers, response=response)
start_response('200 OK,', headers) start_response('200 OK,', headers)
SessionBase.session.close() session.commit()
session.close()
return [response_bytes] return [response_bytes]
else: else:
logg.error('invalid query {}'.format(env)) logg.error('invalid query {}'.format(env))
for r in env: for r in env:
logg.debug('{}: {}'.format(r, env)) logg.debug('{}: {}'.format(r, env))
session.close()
start_response('405 Play by the rules', errors_headers) start_response('405 Play by the rules', errors_headers)
return [] return []

View File

@ -3,6 +3,7 @@ import logging
from typing import Tuple from typing import Tuple
# third-party imports # third-party imports
from sqlalchemy.orm.session import Session
# local imports # local imports
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
@ -10,11 +11,11 @@ from cic_ussd.db.models.account import Account
logg = logging.getLogger(__file__) logg = logging.getLogger(__file__)
def process_mini_statement_request(state_machine_data: Tuple[str, dict, Account]): def process_mini_statement_request(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function compiles a brief statement of a user's last three inbound and outbound transactions and send the """This function compiles a brief statement of a user's last three inbound and outbound transactions and send the
same as a message on their selected avenue for notification. same as a message on their selected avenue for notification.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: str :type state_machine_data: str
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
logg.debug('This section requires integration with cic-eth. (The last 6 transactions would be sent as an sms.)') logg.debug('This section requires integration with cic-eth. (The last 6 transactions would be sent as an sms.)')

View File

@ -16,7 +16,7 @@ def menu_one_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
:return: A user input's match with '1' :return: A user input's match with '1'
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
return user_input == '1' return user_input == '1'
@ -27,7 +27,7 @@ def menu_two_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
:return: A user input's match with '2' :return: A user input's match with '2'
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
return user_input == '2' return user_input == '2'
@ -38,7 +38,7 @@ def menu_three_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
:return: A user input's match with '3' :return: A user input's match with '3'
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
return user_input == '3' return user_input == '3'
@ -50,7 +50,7 @@ def menu_four_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
:return: A user input's match with '4' :return: A user input's match with '4'
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
return user_input == '4' return user_input == '4'
@ -62,7 +62,7 @@ def menu_five_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
:return: A user input's match with '5' :return: A user input's match with '5'
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
return user_input == '5' return user_input == '5'
@ -74,7 +74,7 @@ def menu_six_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
:return: A user input's match with '6' :return: A user input's match with '6'
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
return user_input == '6' return user_input == '6'
@ -86,7 +86,7 @@ def menu_zero_zero_selected(state_machine_data: Tuple[str, dict, Account]) -> bo
:return: A user input's match with '00' :return: A user input's match with '00'
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
return user_input == '00' return user_input == '00'
@ -98,5 +98,5 @@ def menu_ninety_nine_selected(state_machine_data: Tuple[str, dict, Account]) ->
:return: A user input's match with '99' :return: A user input's match with '99'
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
return user_input == '99' return user_input == '99'

View File

@ -9,11 +9,13 @@ import re
from typing import Tuple from typing import Tuple
# third party imports # third party imports
import bcrypt from sqlalchemy.orm.session import Session
# local imports # local imports
from cic_ussd.db.models.account import AccountStatus, Account from cic_ussd.db.models.account import Account
from cic_ussd.encoder import PasswordEncoder, create_password_hash, check_password_hash from cic_ussd.db.models.base import SessionBase
from cic_ussd.db.enum import AccountStatus
from cic_ussd.encoder import create_password_hash, check_password_hash
from cic_ussd.operations import persist_session_to_db_task, create_or_update_session from cic_ussd.operations import persist_session_to_db_task, create_or_update_session
from cic_ussd.redis import InMemoryStore from cic_ussd.redis import InMemoryStore
@ -21,7 +23,7 @@ from cic_ussd.redis import InMemoryStore
logg = logging.getLogger(__file__) logg = logging.getLogger(__file__)
def is_valid_pin(state_machine_data: Tuple[str, dict, Account]) -> bool: def is_valid_pin(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""This function checks a pin's validity by ensuring it has a length of for characters and the characters are """This function checks a pin's validity by ensuring it has a length of for characters and the characters are
numeric. numeric.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
@ -29,7 +31,7 @@ def is_valid_pin(state_machine_data: Tuple[str, dict, Account]) -> bool:
:return: A pin's validity :return: A pin's validity
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
pin_is_valid = False pin_is_valid = False
matcher = r'^\d{4}$' matcher = r'^\d{4}$'
if re.match(matcher, user_input): if re.match(matcher, user_input):
@ -37,34 +39,34 @@ def is_valid_pin(state_machine_data: Tuple[str, dict, Account]) -> bool:
return pin_is_valid return pin_is_valid
def is_authorized_pin(state_machine_data: Tuple[str, dict, Account]) -> bool: def is_authorized_pin(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""This function checks whether the user input confirming a specific pin matches the initial pin entered. """This function checks whether the user input confirming a specific pin matches the initial pin entered.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple :type state_machine_data: tuple
:return: A match between two pin values. :return: A match between two pin values.
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
return user.verify_password(password=user_input) return user.verify_password(password=user_input)
def is_locked_account(state_machine_data: Tuple[str, dict, Account]) -> bool: def is_locked_account(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""This function checks whether a user's account is locked due to too many failed attempts. """This function checks whether a user's account is locked due to too many failed attempts.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple :type state_machine_data: tuple
:return: A match between two pin values. :return: A match between two pin values.
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
return user.get_account_status() == AccountStatus.LOCKED.name return user.get_account_status() == AccountStatus.LOCKED.name
def save_initial_pin_to_session_data(state_machine_data: Tuple[str, dict, Account]): def save_initial_pin_to_session_data(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function hashes a pin and stores it in session data. """This function hashes a pin and stores it in session data.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple :type state_machine_data: tuple
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
# define redis cache entry point # define redis cache entry point
cache = InMemoryStore.cache cache = InMemoryStore.cache
@ -93,54 +95,56 @@ def save_initial_pin_to_session_data(state_machine_data: Tuple[str, dict, Accoun
service_code=in_redis_ussd_session.get('service_code'), service_code=in_redis_ussd_session.get('service_code'),
user_input=user_input, user_input=user_input,
current_menu=in_redis_ussd_session.get('state'), current_menu=in_redis_ussd_session.get('state'),
session=session,
session_data=session_data session_data=session_data
) )
persist_session_to_db_task(external_session_id=external_session_id, queue='cic-ussd') persist_session_to_db_task(external_session_id=external_session_id, queue='cic-ussd')
def pins_match(state_machine_data: Tuple[str, dict, Account]) -> bool: def pins_match(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""This function checks whether the user input confirming a specific pin matches the initial pin entered. """This function checks whether the user input confirming a specific pin matches the initial pin entered.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple :type state_machine_data: tuple
:return: A match between two pin values. :return: A match between two pin values.
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
initial_pin = ussd_session.get('session_data').get('initial_pin') initial_pin = ussd_session.get('session_data').get('initial_pin')
logg.debug(f'USSD SESSION: {ussd_session}')
return check_password_hash(user_input, initial_pin) return check_password_hash(user_input, initial_pin)
def complete_pin_change(state_machine_data: Tuple[str, dict, Account]): def complete_pin_change(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function persists the user's pin to the database """This function persists the user's pin to the database
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple :type state_machine_data: tuple
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
session = SessionBase.bind_session(session=session)
password_hash = ussd_session.get('session_data').get('initial_pin') password_hash = ussd_session.get('session_data').get('initial_pin')
user.password_hash = password_hash user.password_hash = password_hash
Account.session.add(user) session.add(user)
Account.session.commit() session.flush()
SessionBase.release_session(session=session)
def is_blocked_pin(state_machine_data: Tuple[str, dict, Account]) -> bool: def is_blocked_pin(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""This function checks whether the user input confirming a specific pin matches the initial pin entered. """This function checks whether the user input confirming a specific pin matches the initial pin entered.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple :type state_machine_data: tuple
:return: A match between two pin values. :return: A match between two pin values.
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
return user.get_account_status() == AccountStatus.LOCKED.name return user.get_account_status() == AccountStatus.LOCKED.name
def is_valid_new_pin(state_machine_data: Tuple[str, dict, Account]) -> bool: def is_valid_new_pin(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""This function checks whether the user's new pin is a valid pin and that it isn't the same as the old one. """This function checks whether the user's new pin is a valid pin and that it isn't the same as the old one.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple :type state_machine_data: tuple
:return: A match between two pin values. :return: A match between two pin values.
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
is_old_pin = user.verify_password(password=user_input) is_old_pin = user.verify_password(password=user_input)
return is_valid_pin(state_machine_data=state_machine_data) and not is_old_pin return is_valid_pin(state_machine_data=state_machine_data) and not is_old_pin

View File

@ -9,15 +9,15 @@ logg = logging.getLogger()
def send_terms_to_user_if_required(state_machine_data: Tuple[str, dict, Account]): def send_terms_to_user_if_required(state_machine_data: Tuple[str, dict, Account]):
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
logg.debug('Requires integration to cic-notify.') logg.debug('Requires integration to cic-notify.')
def process_mini_statement_request(state_machine_data: Tuple[str, dict, Account]): def process_mini_statement_request(state_machine_data: Tuple[str, dict, Account]):
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
logg.debug('Requires integration to cic-notify.') logg.debug('Requires integration to cic-notify.')
def upsell_unregistered_recipient(state_machine_data: Tuple[str, dict, Account]): def upsell_unregistered_recipient(state_machine_data: Tuple[str, dict, Account]):
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
logg.debug('Requires integration to cic-notify.') logg.debug('Requires integration to cic-notify.')

View File

@ -5,13 +5,16 @@ from typing import Tuple
# third party imports # third party imports
import celery import celery
from sqlalchemy.orm.session import Session
# local imports # local imports
from cic_ussd.balance import BalanceManager, compute_operational_balance from cic_ussd.balance import compute_operational_balance
from cic_ussd.chain import Chain from cic_ussd.chain import Chain
from cic_ussd.db.models.account import AccountStatus, Account from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase
from cic_ussd.db.enum import AccountStatus
from cic_ussd.operations import save_to_in_memory_ussd_session_data from cic_ussd.operations import save_to_in_memory_ussd_session_data
from cic_ussd.phone_number import get_user_by_phone_number, process_phone_number, E164Format from cic_ussd.phone_number import process_phone_number, E164Format
from cic_ussd.processor import retrieve_token_symbol from cic_ussd.processor import retrieve_token_symbol
from cic_ussd.redis import create_cached_data_key, get_cached_data from cic_ussd.redis import create_cached_data_key, get_cached_data
from cic_ussd.transactions import OutgoingTransactionProcessor from cic_ussd.transactions import OutgoingTransactionProcessor
@ -20,7 +23,7 @@ from cic_ussd.transactions import OutgoingTransactionProcessor
logg = logging.getLogger(__file__) logg = logging.getLogger(__file__)
def is_valid_recipient(state_machine_data: Tuple[str, dict, Account]) -> bool: def is_valid_recipient(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""This function checks that a user exists, is not the initiator of the transaction, has an active account status """This function checks that a user exists, is not the initiator of the transaction, has an active account status
and is authorized to perform standard transactions. and is authorized to perform standard transactions.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
@ -28,15 +31,17 @@ def is_valid_recipient(state_machine_data: Tuple[str, dict, Account]) -> bool:
:return: A user's validity :return: A user's validity
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
phone_number = process_phone_number(user_input, E164Format.region) phone_number = process_phone_number(user_input, E164Format.region)
recipient = get_user_by_phone_number(phone_number=user_input) session = SessionBase.bind_session(session=session)
recipient = Account.get_by_phone_number(phone_number=phone_number, session=session)
SessionBase.release_session(session=session)
is_not_initiator = phone_number != user.phone_number is_not_initiator = phone_number != user.phone_number
has_active_account_status = user.get_account_status() == AccountStatus.ACTIVE.name 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 return is_not_initiator and has_active_account_status and recipient is not None
def is_valid_transaction_amount(state_machine_data: Tuple[str, dict, Account]) -> bool: def is_valid_transaction_amount(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""This function checks that the transaction amount provided is valid as per the criteria for the transaction """This function checks that the transaction amount provided is valid as per the criteria for the transaction
being attempted. being attempted.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
@ -44,14 +49,14 @@ def is_valid_transaction_amount(state_machine_data: Tuple[str, dict, Account]) -
:return: A transaction amount's validity :return: A transaction amount's validity
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
try: try:
return int(user_input) > 0 return int(user_input) > 0
except ValueError: except ValueError:
return False return False
def has_sufficient_balance(state_machine_data: Tuple[str, dict, Account]) -> bool: def has_sufficient_balance(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""This function checks that the transaction amount provided is valid as per the criteria for the transaction """This function checks that the transaction amount provided is valid as per the criteria for the transaction
being attempted. being attempted.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
@ -59,10 +64,7 @@ def has_sufficient_balance(state_machine_data: Tuple[str, dict, Account]) -> boo
:return: An account balance's validity :return: An account balance's validity
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
balance_manager = BalanceManager(address=user.blockchain_address,
chain_str=Chain.spec.__str__(),
token_symbol='SRF')
# get cached balance # get cached balance
key = create_cached_data_key( key = create_cached_data_key(
identifier=bytes.fromhex(user.blockchain_address[2:]), identifier=bytes.fromhex(user.blockchain_address[2:]),
@ -74,30 +76,37 @@ def has_sufficient_balance(state_machine_data: Tuple[str, dict, Account]) -> boo
return int(user_input) <= operational_balance return int(user_input) <= operational_balance
def save_recipient_phone_to_session_data(state_machine_data: Tuple[str, dict, Account]): def save_recipient_phone_to_session_data(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function saves the phone number corresponding the intended recipients blockchain account. """This function saves the phone number corresponding the intended recipients blockchain account.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: str :type state_machine_data: str
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
session_data = ussd_session.get('session_data') or {} session_data = ussd_session.get('session_data') or {}
session_data['recipient_phone_number'] = user_input recipient_phone_number = process_phone_number(phone_number=user_input, region=E164Format.region)
session_data['recipient_phone_number'] = recipient_phone_number
save_to_in_memory_ussd_session_data(queue='cic-ussd', session_data=session_data, ussd_session=ussd_session) save_to_in_memory_ussd_session_data(
queue='cic-ussd',
session=session,
session_data=session_data,
ussd_session=ussd_session)
def retrieve_recipient_metadata(state_machine_data: Tuple[str, dict, Account]): def retrieve_recipient_metadata(state_machine_data: Tuple[str, dict, Account, Session]):
""" """
:param state_machine_data: :param state_machine_data:
:type state_machine_data: :type state_machine_data:
:return: :return:
:rtype: :rtype:
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
recipient = get_user_by_phone_number(phone_number=user_input) recipient_phone_number = process_phone_number(phone_number=user_input, region=E164Format.region)
recipient = Account.get_by_phone_number(phone_number=recipient_phone_number, session=session)
blockchain_address = recipient.blockchain_address blockchain_address = recipient.blockchain_address
# retrieve and cache account's metadata # retrieve and cache account's metadata
s_query_person_metadata = celery.signature( s_query_person_metadata = celery.signature(
'cic_ussd.tasks.metadata.query_person_metadata', 'cic_ussd.tasks.metadata.query_person_metadata',
@ -106,32 +115,36 @@ def retrieve_recipient_metadata(state_machine_data: Tuple[str, dict, Account]):
s_query_person_metadata.apply_async(queue='cic-ussd') s_query_person_metadata.apply_async(queue='cic-ussd')
def save_transaction_amount_to_session_data(state_machine_data: Tuple[str, dict, Account]): def save_transaction_amount_to_session_data(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function saves the phone number corresponding the intended recipients blockchain account. """This function saves the phone number corresponding the intended recipients blockchain account.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: str :type state_machine_data: str
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
session_data = ussd_session.get('session_data') or {} session_data = ussd_session.get('session_data') or {}
session_data['transaction_amount'] = user_input session_data['transaction_amount'] = user_input
save_to_in_memory_ussd_session_data(queue='cic-ussd', session_data=session_data, ussd_session=ussd_session) save_to_in_memory_ussd_session_data(
queue='cic-ussd',
session=session,
session_data=session_data,
ussd_session=ussd_session)
def process_transaction_request(state_machine_data: Tuple[str, dict, Account]): def process_transaction_request(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function saves the phone number corresponding the intended recipients blockchain account. """This function saves the phone number corresponding the intended recipients blockchain account.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: str :type state_machine_data: str
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
# retrieve token symbol # retrieve token symbol
chain_str = Chain.spec.__str__() chain_str = Chain.spec.__str__()
# get user from phone number # get user from phone number
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number') recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
recipient = get_user_by_phone_number(phone_number=recipient_phone_number) recipient = Account.get_by_phone_number(phone_number=recipient_phone_number, session=session)
to_address = recipient.blockchain_address to_address = recipient.blockchain_address
from_address = user.blockchain_address from_address = user.blockchain_address
amount = int(ussd_session.get('session_data').get('transaction_amount')) amount = int(ussd_session.get('session_data').get('transaction_amount'))

View File

@ -5,12 +5,14 @@ from typing import Tuple
# third-party imports # third-party imports
import celery import celery
from cic_types.models.person import Person, generate_metadata_pointer from cic_types.models.person import generate_metadata_pointer
from cic_types.models.person import generate_vcard_from_contact_data, manage_identity_data from cic_types.models.person import generate_vcard_from_contact_data, manage_identity_data
from sqlalchemy.orm.session import Session
# local imports # local imports
from cic_ussd.chain import Chain from cic_ussd.chain import Chain
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase
from cic_ussd.error import MetadataNotFoundError from cic_ussd.error import MetadataNotFoundError
from cic_ussd.metadata import blockchain_address_to_metadata_pointer from cic_ussd.metadata import blockchain_address_to_metadata_pointer
from cic_ussd.operations import save_to_in_memory_ussd_session_data from cic_ussd.operations import save_to_in_memory_ussd_session_data
@ -19,15 +21,17 @@ from cic_ussd.redis import get_cached_data
logg = logging.getLogger(__file__) logg = logging.getLogger(__file__)
def change_preferred_language_to_en(state_machine_data: Tuple[str, dict, Account]): def change_preferred_language_to_en(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function changes the user's preferred language to english. """This function changes the user's preferred language to english.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple :type state_machine_data: tuple
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
session = SessionBase.bind_session(session=session)
user.preferred_language = 'en' user.preferred_language = 'en'
Account.session.add(user) session.add(user)
Account.session.commit() session.flush()
SessionBase.release_session(session=session)
preferences_data = { preferences_data = {
'preferred_language': 'en' 'preferred_language': 'en'
@ -40,15 +44,17 @@ def change_preferred_language_to_en(state_machine_data: Tuple[str, dict, Account
s.apply_async(queue='cic-ussd') s.apply_async(queue='cic-ussd')
def change_preferred_language_to_sw(state_machine_data: Tuple[str, dict, Account]): def change_preferred_language_to_sw(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function changes the user's preferred language to swahili. """This function changes the user's preferred language to swahili.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple :type state_machine_data: tuple
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, account, session = state_machine_data
user.preferred_language = 'sw' session = SessionBase.bind_session(session=session)
Account.session.add(user) account.preferred_language = 'sw'
Account.session.commit() session.add(account)
session.flush()
SessionBase.release_session(session=session)
preferences_data = { preferences_data = {
'preferred_language': 'sw' 'preferred_language': 'sw'
@ -56,20 +62,22 @@ def change_preferred_language_to_sw(state_machine_data: Tuple[str, dict, Account
s = celery.signature( s = celery.signature(
'cic_ussd.tasks.metadata.add_preferences_metadata', 'cic_ussd.tasks.metadata.add_preferences_metadata',
[user.blockchain_address, preferences_data] [account.blockchain_address, preferences_data]
) )
s.apply_async(queue='cic-ussd') s.apply_async(queue='cic-ussd')
def update_account_status_to_active(state_machine_data: Tuple[str, dict, Account]): def update_account_status_to_active(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function sets user's account to active. """This function sets user's account to active.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple :type state_machine_data: tuple
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, account, session = state_machine_data
user.activate_account() session = SessionBase.bind_session(session=session)
Account.session.add(user) account.activate_account()
Account.session.commit() session.add(account)
session.flush()
SessionBase.release_session(session=session)
def process_gender_user_input(user: Account, user_input: str): def process_gender_user_input(user: Account, user_input: str):
@ -81,6 +89,7 @@ def process_gender_user_input(user: Account, user_input: str):
:return: :return:
:rtype: :rtype:
""" """
gender = ""
if user.preferred_language == 'en': if user.preferred_language == 'en':
if user_input == '1': if user_input == '1':
gender = 'Male' gender = 'Male'
@ -98,13 +107,13 @@ def process_gender_user_input(user: Account, user_input: str):
return gender return gender
def save_metadata_attribute_to_session_data(state_machine_data: Tuple[str, dict, Account]): def save_metadata_attribute_to_session_data(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function saves first name data to the ussd session in the redis cache. """This function saves first name data to the ussd session in the redis cache.
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple :type state_machine_data: tuple
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
session = SessionBase.bind_session(session=session)
# get current menu # get current menu
current_state = ussd_session.get('state') current_state = ussd_session.get('state')
@ -137,7 +146,11 @@ def save_metadata_attribute_to_session_data(state_machine_data: Tuple[str, dict,
session_data = { session_data = {
key: user_input key: user_input
} }
save_to_in_memory_ussd_session_data(queue='cic-ussd', session_data=session_data, ussd_session=ussd_session) save_to_in_memory_ussd_session_data(
queue='cic-ussd',
session=session,
session_data=session_data,
ussd_session=ussd_session)
def format_user_metadata(metadata: dict, user: Account): def format_user_metadata(metadata: dict, user: Account):
@ -197,12 +210,12 @@ def format_user_metadata(metadata: dict, user: Account):
} }
def save_complete_user_metadata(state_machine_data: Tuple[str, dict, Account]): def save_complete_user_metadata(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function persists elements of the user metadata stored in session data """This function persists elements of the user metadata stored in session data
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple :type state_machine_data: tuple
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
# get session data # get session data
metadata = ussd_session.get('session_data') metadata = ussd_session.get('session_data')
@ -218,8 +231,8 @@ def save_complete_user_metadata(state_machine_data: Tuple[str, dict, Account]):
s_create_person_metadata.apply_async(queue='cic-ussd') s_create_person_metadata.apply_async(queue='cic-ussd')
def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, Account]): def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, Account, Session]):
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
blockchain_address = user.blockchain_address blockchain_address = user.blockchain_address
key = generate_metadata_pointer( key = generate_metadata_pointer(
identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address), identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address),
@ -269,8 +282,8 @@ def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, Account]):
s_edit_person_metadata.apply_async(queue='cic-ussd') s_edit_person_metadata.apply_async(queue='cic-ussd')
def get_user_metadata(state_machine_data: Tuple[str, dict, Account]): def get_user_metadata(state_machine_data: Tuple[str, dict, Account, Session]):
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
blockchain_address = user.blockchain_address blockchain_address = user.blockchain_address
s_get_user_metadata = celery.signature( s_get_user_metadata = celery.signature(
'cic_ussd.tasks.metadata.query_person_metadata', 'cic_ussd.tasks.metadata.query_person_metadata',

View File

@ -19,7 +19,7 @@ def has_cached_user_metadata(state_machine_data: Tuple[str, dict, Account]):
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: str :type state_machine_data: str
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
# check for user metadata in cache # check for user metadata in cache
key = generate_metadata_pointer( key = generate_metadata_pointer(
identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address), identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address),
@ -34,7 +34,7 @@ def is_valid_name(state_machine_data: Tuple[str, dict, Account]):
:param state_machine_data: A tuple containing user input, a ussd session and user object. :param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: str :type state_machine_data: str
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
name_matcher = "^[a-zA-Z]+$" name_matcher = "^[a-zA-Z]+$"
valid_name = re.match(name_matcher, user_input) valid_name = re.match(name_matcher, user_input)
if valid_name: if valid_name:
@ -50,7 +50,7 @@ def is_valid_gender_selection(state_machine_data: Tuple[str, dict, Account]):
:return: :return:
:rtype: :rtype:
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
selection_matcher = "^[1-2]$" selection_matcher = "^[1-2]$"
if re.match(selection_matcher, user_input): if re.match(selection_matcher, user_input):
return True return True
@ -65,6 +65,6 @@ def is_valid_date(state_machine_data: Tuple[str, dict, Account]):
:return: :return:
:rtype: :rtype:
""" """
user_input, ussd_session, user = state_machine_data user_input, ussd_session, user, session = state_machine_data
# For MVP this value is defaulting to year # For MVP this value is defaulting to year
return len(user_input) == 4 and int(user_input) >= 1900 return len(user_input) == 4 and int(user_input) >= 1900

View File

@ -9,6 +9,7 @@ from confini import Config
# local imports # local imports
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase
logg = logging.getLogger(__file__) logg = logging.getLogger(__file__)
@ -45,29 +46,21 @@ def check_request_content_length(config: Config, env: dict):
config.get('APP_MAX_BODY_LENGTH')) config.get('APP_MAX_BODY_LENGTH'))
def check_known_user(phone: str): def check_known_user(phone_number: str, session):
""" """This method attempts to ascertain whether the user already exists and is known to the system.
This method attempts to ascertain whether the user already exists and is known to the system.
It sends a get request to the platform application and attempts to retrieve the user's data which it persists in It sends a get request to the platform application and attempts to retrieve the user's data which it persists in
memory. memory.
:param phone: A valid phone number :param phone_number: A valid phone number
:type phone: str :type phone_number: str
:param session:
:type session:
:return: Is known phone number :return: Is known phone number
:rtype: boolean :rtype: boolean
""" """
user = Account.session.query(Account).filter_by(phone_number=phone).first() session = SessionBase.bind_session(session=session)
return user is not None account = session.query(Account).filter_by(phone_number=phone_number).first()
SessionBase.release_session(session=session)
return account is not None
def check_phone_number(number: str):
"""
Checks whether phone number is present
:param number: A valid phone number
:type number: str
:return: Phone number presence
:rtype: boolean
"""
return number is not None
def check_request_method(env: dict): def check_request_method(env: dict):

View File

@ -1,4 +1,4 @@
cic_base[full_graph]~=0.1.2b21 cic_base[full_graph]==0.1.3a3+build.4aa03607
cic-eth~=0.11.0b16 cic-eth~=0.11.1a2
cic-notify~=0.4.0a5 cic-notify~=0.4.0a6
cic-types~=0.1.0a11 cic-types~=0.1.0a11

View File

@ -124,46 +124,6 @@ def second_profile_management_session_id() -> str:
return session_id() 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') @pytest.fixture(scope='session')
def first_profile_management_session_id_1() -> str: def first_profile_management_session_id_1() -> str:
return session_id() return session_id()

View File

@ -0,0 +1,25 @@
# INTEGRATION TESTING
This folder contains integration tests.
## OVERVIEW
There are four files defining the integration tests.
* **test_account_creation**: Tests account sign up process.
* **test_transactions**: Tests transactions between two accounts.
* **test_profile_management**: Tests that account metadata can be edited.
* **test_account_management**: Tests that account management functionalities are intact.
## REQUIREMENTS
In order to run the transaction tests, please ensure that the faucet amount is set to a non-zero value, ideally `50000000`
which is the value set in the config file `.config/test/integration.ini`.
This implies setting the `DEV_FAUCET_AMOUNT` to a non-zero value before bringing up the contract-migration image:
```shell
export DEV_FAUCET_AMOUNT=50000000
RUN_MASK=1 docker-compose up contract-migration
RUN_MASK=2 docker-compose up contract-migration
```

View File

@ -214,12 +214,13 @@ stages:
status_code: status_code:
- 200 - 200
headers: headers:
Content-Length: '28' Content-Length: '51'
Content-Type: "text/plain" Content-Type: "text/plain"
verify_response_with: verify_response_with:
function: ext.validator:validate_response function: ext.validator:validate_response
extra_kwargs: extra_kwargs:
expected_response: "CON Enter first name\n0. Back" expected_response: "CON Balance {gift_value} {token_symbol}\n1. Send\n2. My Account\n3. Help"
delay_before: 10
- name: Pin number confirmation [{second_account_pin_number} - second account] - name: Pin number confirmation [{second_account_pin_number} - second account]
request: request:
@ -232,227 +233,6 @@ stages:
headers: headers:
content-type: "application/x-www-form-urlencoded" content-type: "application/x-www-form-urlencoded"
method: POST 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: response:
status_code: status_code:
- 200 - 200

View File

@ -31,7 +31,6 @@ stages:
status_code: status_code:
- 200 - 200
headers: headers:
Content-Length: '51'
Content-Type: "text/plain" Content-Type: "text/plain"
verify_response_with: verify_response_with:
function: ext.validator:validate_response function: ext.validator:validate_response

View File

@ -170,7 +170,7 @@ stages:
verify_response_with: verify_response_with:
function: ext.validator:validate_response function: ext.validator:validate_response
extra_kwargs: 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" expected_response: "CON {second_account_phone_number} will receive 17.00 {token_symbol} from {first_account_phone_number}.\nPlease enter your PIN to confirm.\n0. Back"
- name: Enter transcation amount [second account] - name: Enter transcation amount [second account]
request: request:
@ -191,7 +191,7 @@ stages:
verify_response_with: verify_response_with:
function: ext.validator:validate_response function: ext.validator:validate_response
extra_kwargs: 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" expected_response: "CON {first_account_phone_number} atapokea 25.00 {token_symbol} kutoka kwa {second_account_phone_number}.\nTafadhali weka nambari yako ya siri kudhibitisha.\n0. Nyuma"
- name: Pin to authorize transaction [first account] - name: Pin to authorize transaction [first account]
request: request:
@ -212,7 +212,7 @@ stages:
verify_response_with: verify_response_with:
function: ext.validator:validate_response function: ext.validator:validate_response
extra_kwargs: 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" expected_response: "CON Your request has been sent. {second_account_phone_number} will receive 17.00 {token_symbol} from {first_account_phone_number}.\n00. Back\n99. Exit"
- name: Pin to authorize transaction [second account] - name: Pin to authorize transaction [second account]
request: request:
@ -233,7 +233,7 @@ stages:
verify_response_with: verify_response_with:
function: ext.validator:validate_response function: ext.validator:validate_response
extra_kwargs: 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" expected_response: "CON Ombi lako limetumwa. {first_account_phone_number} atapokea 25.00 {token_symbol} kutoka kwa {second_account_phone_number}.\n00. Nyuma\n99. Ondoka"
- name: Verify balance changes [first account] - name: Verify balance changes [first account]
delay_before: 10 delay_before: 10

View File

@ -12,7 +12,7 @@
{ {
"trigger": "scan_data", "trigger": "scan_data",
"source": "enter_date_of_birth", "source": "enter_date_of_birth",
"dest": "enter_gender", "dest": "enter_location",
"conditions": "cic_ussd.state_machine.logic.validator.is_valid_date", "conditions": "cic_ussd.state_machine.logic.validator.is_valid_date",
"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" "unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"

View File

@ -2,7 +2,7 @@
{ {
"trigger": "scan_data", "trigger": "scan_data",
"source": "enter_gender", "source": "enter_gender",
"dest": "enter_location", "dest": "enter_date_of_birth",
"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",
"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" "unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"

View File

@ -15,7 +15,7 @@
{ {
"trigger": "scan_data", "trigger": "scan_data",
"source": "enter_family_name", "source": "enter_family_name",
"dest": "enter_date_of_birth", "dest": "enter_gender",
"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" "unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
}, },

View File

@ -55,7 +55,7 @@ en:
CON My profile CON My profile
1. Edit name 1. Edit name
2. Edit gender 2. Edit gender
3. Edit Age 3. Edit age
4. Edit location 4. Edit location
5. Edit products 5. Edit products
6. View my profile 6. View my profile

View File

@ -55,7 +55,7 @@ sw:
CON Wasifu wangu CON Wasifu wangu
1. Weka jina 1. Weka jina
2. Weka jinsia 2. Weka jinsia
3 Weka umri 3. Weka umri
4. Weka eneo 4. Weka eneo
5. Weka bidhaa 5. Weka bidhaa
6. Angalia wasifu wako 6. Angalia wasifu wako

View File

@ -171,7 +171,11 @@ Then, in sequence, run in first terminal:
In second terminal: In second terminal:
`python cic_ussd/import_users.py -v -c config out` `python cic_ussd/import_users.py -v --ussd-host <user_ussd_server_host> --ussd-port <user_ussd_server_port> -c config out`
In the event that you are running the command in a local environment you may want to consider passing the `--ussd-no-ssl` flag i.e:
`python cic_ussd/import_users.py -v --ussd-host <user_ussd_server_host> --ussd-port <user_ussd_server_port> --ussd-no-ssl -c config out`
@ -199,6 +203,13 @@ If _number of users_ is omitted the script will run until manually interrupted.
If you imported using `cic_ussd`, the phone pointer is _already added_ and this script will do nothing. If you imported using `cic_ussd`, the phone pointer is _already added_ and this script will do nothing.
### Importing preferences metadata
`node cic_meta/import_meta_preferences.js <datadir> <number_of_users>`
If you used the `cic_ussd/import_user.py` script to import your users, preferences metadata is generated and will be imported.
##### Importing pins and ussd data (optional) ##### 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: 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:

View File

@ -18,19 +18,17 @@ from hexathon import (
add_0x, add_0x,
) )
from chainsyncer.backend.memory import MemBackend from chainsyncer.backend.memory import MemBackend
from chainsyncer.driver import HeadSyncer from chainsyncer.driver.head import HeadSyncer
from chainlib.eth.connection import EthHTTPConnection from chainlib.eth.connection import EthHTTPConnection
from chainlib.eth.block import ( from chainlib.eth.block import (
block_latest, block_latest,
block_by_number,
Block,
) )
from chainlib.hash import keccak256_string_to_hex from chainlib.hash import keccak256_string_to_hex
from chainlib.eth.address import to_checksum_address from chainlib.eth.address import to_checksum_address
from chainlib.eth.gas import OverrideGasOracle from chainlib.eth.gas import OverrideGasOracle
from chainlib.eth.nonce import RPCNonceOracle from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import TxFactory from chainlib.eth.tx import TxFactory
from chainlib.jsonrpc import jsonrpc_template from chainlib.jsonrpc import JSONRPCRequest
from chainlib.eth.error import EthException from chainlib.eth.error import EthException
from chainlib.chain import ChainSpec from chainlib.chain import ChainSpec
from chainlib.eth.constant import ZERO_ADDRESS from chainlib.eth.constant import ZERO_ADDRESS
@ -38,6 +36,7 @@ from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
from crypto_dev_signer.keystore.dict import DictKeystore from crypto_dev_signer.keystore.dict import DictKeystore
from cic_types.models.person import Person from cic_types.models.person import Person
from eth_erc20 import ERC20 from eth_erc20 import ERC20
from cic_base.eth.syncer import chain_interface
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
@ -70,13 +69,14 @@ elif args.vv == True:
config_dir = os.path.join(args.c) config_dir = os.path.join(args.c)
os.makedirs(config_dir, 0o777, True) os.makedirs(config_dir, 0o777, True)
config = confini.Config(config_dir, args.env_prefix) config = confini.Config(config_dir, args.env_prefix)
config.process()
# override args # override args
config.process()
logg.debug('config loaded from {}:\n{}'.format(config_dir, config))
args_override = { args_override = {
'CIC_CHAIN_SPEC': getattr(args, 'i'), 'CIC_CHAIN_SPEC': getattr(args, 'i'),
'ETH_PROVIDER': getattr(args, 'p'), 'ETH_PROVIDER': getattr(args, 'p'),
'CIC_REGISTRY_ADDRESS': getattr(args, 'r'), 'CIC_REGISTRY_ADDRESS': getattr(args, 'r'),
'KEYSTORE_FILE_PATH': getattr(args, 'key-file') 'KEYSTORE_FILE_PATH': getattr(args, 'y'),
} }
config.dict_override(args_override, 'cli flag') config.dict_override(args_override, 'cli flag')
config.censor('PASSWORD', 'DATABASE') config.censor('PASSWORD', 'DATABASE')
@ -185,27 +185,6 @@ class Handler:
# logg.error('key record not found in imports: {}'.format(e).ljust(200)) # logg.error('key record not found in imports: {}'.format(e).ljust(200))
#class BlockGetter:
#
# def __init__(self, conn, gas_oracle, nonce_oracle, chain_spec):
# self.conn = conn
# self.tx_factory = ERC20(signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle, chain_id=chain_id)
#
#
# def get(self, n):
# o = block_by_number(n)
# r = self.conn.do(o)
# b = None
# try:
# b = Block(r)
# except TypeError as e:
# if r == None:
# logg.debug('block not found {}'.format(n))
# else:
# logg.error('block retrieve error {}'.format(e))
# return b
def progress_callback(block_number, tx_index): def progress_callback(block_number, tx_index):
sys.stdout.write(str(block_number).ljust(200) + "\n") sys.stdout.write(str(block_number).ljust(200) + "\n")
@ -227,10 +206,12 @@ def main():
data += eth_abi.encode_single('bytes32', b'TokenRegistry').hex() data += eth_abi.encode_single('bytes32', b'TokenRegistry').hex()
txf.set_code(tx, data) txf.set_code(tx, data)
o = jsonrpc_template() j = JSONRPCRequest()
o = j.template()
o['method'] = 'eth_call' o['method'] = 'eth_call'
o['params'].append(txf.normalize(tx)) o['params'].append(txf.normalize(tx))
o['params'].append('latest') o['params'].append('latest')
o = j.finalize(o)
r = conn.do(o) r = conn.do(o)
token_index_address = to_checksum_address(eth_abi.decode_single('address', bytes.fromhex(strip_0x(r)))) token_index_address = to_checksum_address(eth_abi.decode_single('address', bytes.fromhex(strip_0x(r))))
logg.info('found token index address {}'.format(token_index_address)) logg.info('found token index address {}'.format(token_index_address))
@ -244,10 +225,11 @@ def main():
z = h.digest() z = h.digest()
data += eth_abi.encode_single('bytes32', z).hex() data += eth_abi.encode_single('bytes32', z).hex()
txf.set_code(tx, data) txf.set_code(tx, data)
o = jsonrpc_template() o = j.template()
o['method'] = 'eth_call' o['method'] = 'eth_call'
o['params'].append(txf.normalize(tx)) o['params'].append(txf.normalize(tx))
o['params'].append('latest') o['params'].append('latest')
o = j.finalize(o)
r = conn.do(o) r = conn.do(o)
try: try:
sarafu_token_address = to_checksum_address(eth_abi.decode_single('address', bytes.fromhex(strip_0x(r)))) sarafu_token_address = to_checksum_address(eth_abi.decode_single('address', bytes.fromhex(strip_0x(r))))
@ -305,7 +287,7 @@ def main():
f.close() f.close()
syncer_backend.set(block_offset, 0) syncer_backend.set(block_offset, 0)
syncer = HeadSyncer(syncer_backend, block_callback=progress_callback) syncer = HeadSyncer(syncer_backend, chain_interface, block_callback=progress_callback)
handler = Handler(conn, chain_spec, user_dir, balances, sarafu_token_address, signer, gas_oracle, nonce_oracle) handler = Handler(conn, chain_spec, user_dir, balances, sarafu_token_address, signer, gas_oracle, nonce_oracle)
syncer.add_filter(handler) syncer.add_filter(handler)
syncer.loop(1, conn) syncer.loop(1, conn)

View File

@ -7,4 +7,5 @@ approval_escrow_address =
chain_spec = evm:bloxberg:8996 chain_spec = evm:bloxberg:8996
tx_retry_delay = tx_retry_delay =
trust_address = 0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C trust_address = 0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C
user_ussd_svc_service_port= user_ussd_svc_service_port =

View File

@ -1,8 +1,2 @@
[eth] [eth]
#ws_provider = ws://localhost:8546
#ttp_provider = http://localhost:8545
provider = http://localhost:63545 provider = http://localhost:63545
gas_provider_address =
#chain_id =
abi_dir = /usr/local/share/cic/solidity/abi
account_accounts_index_writer =

View File

@ -204,9 +204,9 @@ def gen():
])) ]))
if random.randint(0, 1): if random.randint(0, 1):
# fake.local_latitude() # fake.local_latitude()
p.location['latitude'] = (random.random() + 180) - 90 p.location['latitude'] = (random.random() * 180) - 90
# fake.local_latitude() # fake.local_latitude()
p.location['longitude'] = (random.random() + 360) - 180 p.location['longitude'] = (random.random() * 360) - 179
return (old_blockchain_checksum_address, phone, p) return (old_blockchain_checksum_address, phone, p)

View File

@ -1,19 +1,21 @@
# syntax = docker/dockerfile:1.2 # syntax = docker/dockerfile:1.2
FROM python:3.8.6-slim-buster as compile-image #FROM python:3.8.6-slim-buster as compile-image
FROM registry.gitlab.com/grassrootseconomics/cic-base-images:python-3.8.6-dev-5ab8bf45
WORKDIR /root
RUN apt-get update
RUN apt-get install -y --no-install-recommends git gcc g++ libpq-dev gawk jq telnet wget openssl iputils-ping gnupg socat bash procps make python2 cargo
RUN mkdir -vp /usr/local/etc/cic RUN mkdir -vp /usr/local/etc/cic
COPY data-seeding/package.json \
data-seeding/package-lock.json \
.
RUN npm install
COPY data-seeding/requirements.txt . COPY data-seeding/requirements.txt .
ARG EXTRA_INDEX_URL="https://pip.grassrootseconomics.net:8433"
ARG GITLAB_PYTHON_REGISTRY="https://gitlab.com/api/v4/projects/27624814/packages/pypi/simple"
RUN pip install --extra-index-url $GITLAB_PYTHON_REGISTRY --extra-index-url $EXTRA_INDEX_URL -r requirements.txt
COPY data-seeding/ . COPY data-seeding/ .
ARG EXTRA_INDEX_URL="https://pip.grassrootseconomics.net:8433"
RUN pip install --extra-index-url $EXTRA_INDEX_URL -r requirements.txt
ENTRYPOINT [ ] ENTRYPOINT [ ]

View File

@ -18,25 +18,24 @@ from hexathon import (
add_0x, add_0x,
) )
from chainsyncer.backend.memory import MemBackend from chainsyncer.backend.memory import MemBackend
from chainsyncer.driver import HeadSyncer from chainsyncer.driver.head import HeadSyncer
from chainlib.eth.connection import EthHTTPConnection from chainlib.eth.connection import EthHTTPConnection
from chainlib.eth.block import ( from chainlib.eth.block import (
block_latest, block_latest,
block_by_number,
Block,
) )
from chainlib.hash import keccak256_string_to_hex from chainlib.hash import keccak256_string_to_hex
from chainlib.eth.address import to_checksum_address from chainlib.eth.address import to_checksum_address
from chainlib.eth.gas import OverrideGasOracle from chainlib.eth.gas import OverrideGasOracle
from chainlib.eth.nonce import RPCNonceOracle from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import TxFactory from chainlib.eth.tx import TxFactory
from chainlib.jsonrpc import jsonrpc_template from chainlib.jsonrpc import JSONRPCRequest
from chainlib.eth.error import EthException from chainlib.eth.error import EthException
from chainlib.chain import ChainSpec from chainlib.chain import ChainSpec
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
from crypto_dev_signer.keystore.dict import DictKeystore from crypto_dev_signer.keystore.dict import DictKeystore
from cic_types.models.person import Person from cic_types.models.person import Person
from eth_erc20 import ERC20 from eth_erc20 import ERC20
from cic_base.eth.syncer import chain_interface
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
@ -75,7 +74,7 @@ args_override = {
'CIC_CHAIN_SPEC': getattr(args, 'i'), 'CIC_CHAIN_SPEC': getattr(args, 'i'),
'ETH_PROVIDER': getattr(args, 'p'), 'ETH_PROVIDER': getattr(args, 'p'),
'CIC_REGISTRY_ADDRESS': getattr(args, 'r'), 'CIC_REGISTRY_ADDRESS': getattr(args, 'r'),
'KEYSTORE_FILE_PATH': getattr(args, 'key-file') 'KEYSTORE_FILE_PATH': getattr(args, 'y')
} }
config.dict_override(args_override, 'cli flag') config.dict_override(args_override, 'cli flag')
config.censor('PASSWORD', 'DATABASE') config.censor('PASSWORD', 'DATABASE')
@ -184,27 +183,6 @@ class Handler:
# logg.error('key record not found in imports: {}'.format(e).ljust(200)) # logg.error('key record not found in imports: {}'.format(e).ljust(200))
#class BlockGetter:
#
# def __init__(self, conn, gas_oracle, nonce_oracle, chain_spec):
# self.conn = conn
# self.tx_factory = ERC20(signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle, chain_id=chain_id)
#
#
# def get(self, n):
# o = block_by_number(n)
# r = self.conn.do(o)
# b = None
# try:
# b = Block(r)
# except TypeError as e:
# if r == None:
# logg.debug('block not found {}'.format(n))
# else:
# logg.error('block retrieve error {}'.format(e))
# return b
def progress_callback(block_number, tx_index): def progress_callback(block_number, tx_index):
sys.stdout.write(str(block_number).ljust(200) + "\n") sys.stdout.write(str(block_number).ljust(200) + "\n")
@ -226,10 +204,12 @@ def main():
data += eth_abi.encode_single('bytes32', b'TokenRegistry').hex() data += eth_abi.encode_single('bytes32', b'TokenRegistry').hex()
txf.set_code(tx, data) txf.set_code(tx, data)
o = jsonrpc_template() j = JSONRPCRequest()
o = j.template()
o['method'] = 'eth_call' o['method'] = 'eth_call'
o['params'].append(txf.normalize(tx)) o['params'].append(txf.normalize(tx))
o['params'].append('latest') o['params'].append('latest')
o = j.finalize(o)
r = conn.do(o) r = conn.do(o)
token_index_address = to_checksum_address(eth_abi.decode_single('address', bytes.fromhex(strip_0x(r)))) token_index_address = to_checksum_address(eth_abi.decode_single('address', bytes.fromhex(strip_0x(r))))
logg.info('found token index address {}'.format(token_index_address)) logg.info('found token index address {}'.format(token_index_address))
@ -243,10 +223,11 @@ def main():
z = h.digest() z = h.digest()
data += eth_abi.encode_single('bytes32', z).hex() data += eth_abi.encode_single('bytes32', z).hex()
txf.set_code(tx, data) txf.set_code(tx, data)
o = jsonrpc_template() o = j.template()
o['method'] = 'eth_call' o['method'] = 'eth_call'
o['params'].append(txf.normalize(tx)) o['params'].append(txf.normalize(tx))
o['params'].append('latest') o['params'].append('latest')
o = j.finalize(o)
r = conn.do(o) r = conn.do(o)
try: try:
sarafu_token_address = to_checksum_address(eth_abi.decode_single('address', bytes.fromhex(strip_0x(r)))) sarafu_token_address = to_checksum_address(eth_abi.decode_single('address', bytes.fromhex(strip_0x(r))))
@ -300,7 +281,7 @@ def main():
f.close() f.close()
syncer_backend.set(block_offset, 0) syncer_backend.set(block_offset, 0)
syncer = HeadSyncer(syncer_backend, block_callback=progress_callback) syncer = HeadSyncer(syncer_backend, chain_interface, block_callback=progress_callback)
handler = Handler(conn, chain_spec, user_dir, balances, sarafu_token_address, signer, gas_oracle, nonce_oracle) handler = Handler(conn, chain_spec, user_dir, balances, sarafu_token_address, signer, gas_oracle, nonce_oracle)
syncer.add_filter(handler) syncer.add_filter(handler)
syncer.loop(1, conn) syncer.loop(1, conn)

View File

@ -59,7 +59,7 @@ config.process()
args_override = { args_override = {
'CIC_REGISTRY_ADDRESS': getattr(args, 'r'), 'CIC_REGISTRY_ADDRESS': getattr(args, 'r'),
'CIC_CHAIN_SPEC': getattr(args, 'i'), 'CIC_CHAIN_SPEC': getattr(args, 'i'),
'KEYSTORE_FILE_PATH': getattr(args, 'key-file') 'KEYSTORE_FILE_PATH': getattr(args, 'y')
} }
config.dict_override(args_override, 'cli') config.dict_override(args_override, 'cli')
config.add(args.user_dir, '_USERDIR', True) config.add(args.user_dir, '_USERDIR', True)

View File

@ -1,5 +1,5 @@
cic-base[full_graph]==0.1.2b15 cic_base[full_graph]==0.1.3a3+build.4aa03607
sarafu-faucet==0.0.3a3 sarafu-faucet==0.0.4a1
cic-eth==0.11.0b16 cic-eth==0.11.1a1
cic-types==0.1.0a11 cic-types==0.1.0a13
crypto-dev-signer==0.4.14b3 crypto-dev-signer==0.4.14b6

View File

@ -25,7 +25,7 @@ from chainlib.eth.gas import (
) )
from chainlib.eth.tx import TxFactory from chainlib.eth.tx import TxFactory
from chainlib.hash import keccak256_string_to_hex from chainlib.hash import keccak256_string_to_hex
from chainlib.jsonrpc import jsonrpc_template from chainlib.jsonrpc import JSONRPCRequest
from cic_types.models.person import ( from cic_types.models.person import (
Person, Person,
generate_metadata_pointer, generate_metadata_pointer,
@ -264,9 +264,11 @@ class Verifier:
data += eth_abi.encode_single('address', address).hex() data += eth_abi.encode_single('address', address).hex()
tx = self.tx_factory.set_code(tx, data) tx = self.tx_factory.set_code(tx, data)
tx = self.tx_factory.normalize(tx) tx = self.tx_factory.normalize(tx)
o = jsonrpc_template() j = JSONRPCRequest()
o = j.template()
o['method'] = 'eth_call' o['method'] = 'eth_call'
o['params'].append(tx) o['params'].append(tx)
o = j.finalize(o)
r = self.conn.do(o) r = self.conn.do(o)
logg.debug('index check for {}: {}'.format(address, r)) logg.debug('index check for {}: {}'.format(address, r))
n = eth_abi.decode_single('uint256', bytes.fromhex(strip_0x(r))) n = eth_abi.decode_single('uint256', bytes.fromhex(strip_0x(r)))
@ -429,10 +431,12 @@ def main():
data += eth_abi.encode_single('bytes32', b'TokenRegistry').hex() data += eth_abi.encode_single('bytes32', b'TokenRegistry').hex()
txf.set_code(tx, data) txf.set_code(tx, data)
o = jsonrpc_template() j = JSONRPCRequest()
o = j.template()
o['method'] = 'eth_call' o['method'] = 'eth_call'
o['params'].append(txf.normalize(tx)) o['params'].append(txf.normalize(tx))
o['params'].append('latest') o['params'].append('latest')
o = j.finalize(o)
r = conn.do(o) r = conn.do(o)
token_index_address = to_checksum_address(eth_abi.decode_single('address', bytes.fromhex(strip_0x(r)))) token_index_address = to_checksum_address(eth_abi.decode_single('address', bytes.fromhex(strip_0x(r))))
logg.info('found token index address {}'.format(token_index_address)) logg.info('found token index address {}'.format(token_index_address))
@ -441,10 +445,11 @@ def main():
data += eth_abi.encode_single('bytes32', b'AccountRegistry').hex() data += eth_abi.encode_single('bytes32', b'AccountRegistry').hex()
txf.set_code(tx, data) txf.set_code(tx, data)
o = jsonrpc_template() o = j.template()
o['method'] = 'eth_call' o['method'] = 'eth_call'
o['params'].append(txf.normalize(tx)) o['params'].append(txf.normalize(tx))
o['params'].append('latest') o['params'].append('latest')
o = j.finalize(o)
r = conn.do(o) r = conn.do(o)
account_index_address = to_checksum_address(eth_abi.decode_single('address', bytes.fromhex(strip_0x(r)))) account_index_address = to_checksum_address(eth_abi.decode_single('address', bytes.fromhex(strip_0x(r))))
logg.info('found account index address {}'.format(account_index_address)) logg.info('found account index address {}'.format(account_index_address))
@ -453,10 +458,11 @@ def main():
data += eth_abi.encode_single('bytes32', b'Faucet').hex() data += eth_abi.encode_single('bytes32', b'Faucet').hex()
txf.set_code(tx, data) txf.set_code(tx, data)
o = jsonrpc_template() o = j.template()
o['method'] = 'eth_call' o['method'] = 'eth_call'
o['params'].append(txf.normalize(tx)) o['params'].append(txf.normalize(tx))
o['params'].append('latest') o['params'].append('latest')
o = j.finalize(o)
r = conn.do(o) r = conn.do(o)
faucet_address = to_checksum_address(eth_abi.decode_single('address', bytes.fromhex(strip_0x(r)))) faucet_address = to_checksum_address(eth_abi.decode_single('address', bytes.fromhex(strip_0x(r))))
logg.info('found faucet {}'.format(faucet_address)) logg.info('found faucet {}'.format(faucet_address))
@ -471,10 +477,11 @@ def main():
z = h.digest() z = h.digest()
data += eth_abi.encode_single('bytes32', z).hex() data += eth_abi.encode_single('bytes32', z).hex()
txf.set_code(tx, data) txf.set_code(tx, data)
o = jsonrpc_template() o = j.template()
o['method'] = 'eth_call' o['method'] = 'eth_call'
o['params'].append(txf.normalize(tx)) o['params'].append(txf.normalize(tx))
o['params'].append('latest') o['params'].append('latest')
o = j.finalize(o)
r = conn.do(o) r = conn.do(o)
sarafu_token_address = to_checksum_address(eth_abi.decode_single('address', bytes.fromhex(strip_0x(r)))) sarafu_token_address = to_checksum_address(eth_abi.decode_single('address', bytes.fromhex(strip_0x(r))))
logg.info('found token address {}'.format(sarafu_token_address)) logg.info('found token address {}'.format(sarafu_token_address))

View File

@ -0,0 +1 @@
cic-base==0.1.3a3+build.4aa03607

View File

@ -0,0 +1 @@
requirements-magic~=0.0.2

View File

@ -0,0 +1,21 @@
#!/bin/bash
which pyreq-merge &> /dev/null
if [ $? -gt 0 ]; then
>&2 echo pyreq-merge missing, please install requirements
exit 1
fi
t=$(mktemp)
>&2 echo using tmp $t
repos=(../../cic-cache ../../cic-eth ../../cic-ussd ../../data-seeding ../../cic-notify)
for r in ${repos[@]}; do
f="$r/requirements.txt"
>&2 echo updating $f
f="$r/test_requirements.txt"
>&2 echo updating $f
pyreq-update $f base_requirement.txt > $t
cp $t $f
done

View File

@ -387,44 +387,6 @@ services:
# command: "/root/start_retry.sh -q cic-eth -vv" # command: "/root/start_retry.sh -q cic-eth -vv"
# cic-eth-server:
# build:
# context: apps/
# dockerfile: cic-eth/docker/Dockerfile
# environment:
# CIC_CHAIN_SPEC: $CIC_CHAIN_SPEC
# CELERY_BROKER_URL: $CELERY_BROKER_URL
# CELERY_RESULT_URL: $CELERY_RESULT_URL
# SERVER_PORT: 8000
# depends_on:
# - eth
# - postgres
# - redis
# ports:
# - ${HTTP_PORT_CIC_ETH:-63314}:8000
# deploy:
# restart_policy:
# condition: on-failure
# volumes:
# - contract-config:/tmp/cic/config/:ro
# command:
# - /bin/bash
# - -c
# - |
# if [[ -f /tmp/cic/config/.env ]]; then source /tmp/cic/config/.env; fi
# "/usr/local/bin/uwsgi" \
# --wsgi-file /usr/src/cic-eth/cic_eth/runnable/server_agent.py \
# --http :80 \
# --pyargv -vv
## entrypoint:
## - "/usr/local/bin/uwsgi"
## - "--wsgi-file"
## - "/usr/src/cic-eth/cic_eth/runnable/server_agent.py"
## - "--http"
## - ":80"
# # command: "--pyargv -vv"
cic-notify-tasker: cic-notify-tasker:
build: build: