Compare commits

...

42 Commits

Author SHA1 Message Date
Spencer Ofwiti
7990bf32b1 Merge branch 'master' into spencer/meta-multiple-hashes 2021-06-29 14:01:07 +03:00
3b6e031746 Merge branch 'philip/db-session-management' into 'master'
Philip/db session management

Closes cic-ussd#53

See merge request grassrootseconomics/cic-internal-integration!193
2021-06-29 10:49:25 +00:00
b1d5d45eef Philip/db session management 2021-06-29 10:49:25 +00:00
53317cb912 Merge branch 'philip/notify-queue-setting' into 'master'
Enable api level setting of queue value.

See merge request grassrootseconomics/cic-internal-integration!184
2021-06-29 10:21:43 +00:00
Spencer Ofwiti
8a0eee2cc6 Merge branch 'master' into spencer/meta-multiple-hashes 2021-06-29 09:20:35 +03:00
18382a1f35 Merge branch 'bvander/integration-notes' into 'master'
integration updates: meta imports

See merge request grassrootseconomics/cic-internal-integration!201
2021-06-28 22:40:54 +00:00
29e91fafab integration updates: meta imports 2021-06-28 22:40:54 +00:00
5b20a9a24a Merge branch 'bvander/better-data-seeding-container' into 'master'
add key store and vars

See merge request grassrootseconomics/cic-internal-integration!202
2021-06-28 22:40:04 +00:00
Spencer Ofwiti
4fd976b1ae Merge branch 'master' into spencer/meta-multiple-hashes 2021-06-28 18:37:26 +03:00
Spencer Ofwiti
f1be3b633c Merge branch 'lash/downgrade-chainsyncer' into 'master'
Downgrade chainsyncer

See merge request grassrootseconomics/cic-internal-integration!200
2021-06-28 15:36:56 +00:00
nolash
e59a71188c Downgrade chainsyncer 2021-06-28 17:18:52 +02:00
Spencer Ofwiti
7c439b5963 Get using multiple identifiers with automerge none. 2021-06-28 17:55:24 +03:00
Spencer Ofwiti
20ffaa3e68 Refactor meta server to take multiple identifiers. 2021-06-28 17:27:10 +03:00
Spencer Ofwiti
4be0b9d3ae Return 404 if resource is not found. 2021-06-28 10:26:09 +03:00
1d0eb06f2f Merge branch 'bvander/data-seeding-keystore' into 'master'
add keystore

See merge request grassrootseconomics/cic-internal-integration!197
2021-06-26 17:12:46 +00:00
57127132b5 add keystore 2021-06-26 09:50:58 -07:00
0bf2c35fcd add key store and vars 2021-06-26 09:50:03 -07:00
d046595764 Merge branch 'philip/remove-hardcoded-import-script-configs' into 'master'
Refactors hardcoded config vars.

See merge request grassrootseconomics/cic-internal-integration!196
2021-06-26 15:58:07 +00:00
9dd7ec88fd Refactors hardcoded config vars. 2021-06-26 18:51:25 +03:00
282fd2ff52 Merge branch 'bvander/better-data-seeding-container' into 'master'
fix paths for data seeding container

See merge request grassrootseconomics/cic-internal-integration!195
2021-06-25 23:06:25 +00:00
8f85598861 fix paths and stuff 2021-06-25 15:59:10 -07:00
8529c349ca Merge branch 'philip/fix-import-script-pointers' into 'master'
Philip/fix import script pointers

See merge request grassrootseconomics/cic-internal-integration!194
2021-06-25 15:36:06 +00:00
4368d2bf59 Philip/fix import script pointers 2021-06-25 15:36:05 +00:00
da3c812bf5 Merge branch 'philip/add-age-metadata' into 'master'
Philip/add age metadata

See merge request grassrootseconomics/cic-internal-integration!186
2021-06-23 13:25:09 +00:00
82b1e87462 Philip/add age metadata 2021-06-23 13:25:09 +00:00
e13c423daf Merge branch 'philip/custom-metadata' into 'master'
Philip/custom metadata

See merge request grassrootseconomics/cic-internal-integration!192
2021-06-23 08:54:34 +00:00
56b3bd751d Philip/custom metadata 2021-06-23 08:54:34 +00:00
4f41c5bacf Merge branch 'philip/notify-sender-id-resolution' into 'master'
Handle empty string defaults in kubernetes secrets.

See merge request grassrootseconomics/cic-internal-integration!183
2021-06-23 07:02:22 +00:00
07583f0c3b Handle empty string defaults in kubernetes secrets. 2021-06-23 07:02:22 +00:00
0ae912082c Merge branch 'philip/refactor-phone-number-input-handling' into 'master'
Refactors handling of phone number inputs during transactions

See merge request grassrootseconomics/cic-internal-integration!185
2021-06-23 06:44:01 +00:00
094f4d4298 Refactors handling of phone number inputs during transactions 2021-06-23 06:44:01 +00:00
Spencer Ofwiti
9471b1d8ab Merge branch 'spencer/fix-import-scripts-build' into 'master'
Fix build process for import scripts.

See merge request grassrootseconomics/cic-internal-integration!188
2021-06-23 05:49:06 +00:00
Spencer Ofwiti
57100366d8 Fix build process for import scripts. 2021-06-23 05:49:06 +00:00
71e0973020 Merge branch 'lash/check-import-ussd' into 'master'
Rehabilitate ussd import scripts

Closes #57

See merge request grassrootseconomics/cic-internal-integration!166
2021-06-23 04:29:39 +00:00
Louis Holbrook
12ab5c2f66 Rehabilitate ussd import scripts 2021-06-23 04:29:38 +00:00
a804552620 Merge branch 'bvander/add-venv-to-ignore' into 'master'
add .venv folder

See merge request grassrootseconomics/cic-internal-integration!191
2021-06-22 21:35:21 +00:00
0319fa6076 add .venv folder 2021-06-22 14:26:41 -07:00
91dfc51d54 Merge branch 'bvander/change-meta-key-dir' into 'master'
change pgp key dir

See merge request grassrootseconomics/cic-internal-integration!190
2021-06-22 21:25:58 +00:00
4fd861f080 change pgp key dir 2021-06-22 14:15:38 -07:00
Louis Holbrook
28de7a4eac Merge branch 'lash/loglines' into 'master'
Loglines and dep bump

See merge request grassrootseconomics/cic-internal-integration!187
2021-06-19 06:49:42 +00:00
Louis Holbrook
a31e79b0f7 Loglines and dep bump 2021-06-19 06:49:42 +00:00
eb2f71aee0 Enable api level setting of queue value. 2021-06-14 10:38:23 +03:00
60 changed files with 1167 additions and 531 deletions

2
.gitignore vendored
View File

@@ -11,3 +11,5 @@ build/
**/*sqlite
**/.nyc_output
**/coverage
**/.venv
.idea

View File

@@ -194,6 +194,7 @@ def main():
except UnknownContractError as e:
logg.exception('Registry contract connection failed for {}: {}'.format(config.get('CIC_REGISTRY_ADDRESS'), e))
sys.exit(1)
logg.info('connected contract registry {}'.format(config.get('CIC_REGISTRY_ADDRESS')))
trusted_addresses_src = config.get('CIC_TRUST_ADDRESS')
if trusted_addresses_src == None:

View File

@@ -16,10 +16,10 @@ moolb~=0.1.1b2
eth-address-index~=0.1.1a11
chainlib~=0.0.3rc2
hexathon~=0.0.1a7
chainsyncer[sql]~=0.0.2a5
chainsyncer[sql]==0.0.2a5
chainqueue~=0.0.2b3
sarafu-faucet==0.0.3a3
erc20-faucet==0.2.1a4
sarafu-faucet~=0.0.3a3
erc20-faucet~=0.2.1a5
coincurve==15.0.0
potaahto~=0.0.1a2
pycryptodome==3.10.1

View File

@@ -147,7 +147,7 @@ function handleClientMergeGet(db, digest, keystore) {
doh(e);
});
}).catch((e) => {
console.error('mesage', e);
console.error('message', e);
doh(e);
});
});

View File

@@ -87,7 +87,7 @@ async function startServer() {
http.createServer(processRequest).listen(config.get('SERVER_PORT'));
}
const re_digest = /^\/([a-fA-F0-9]{64})\/?$/;
const re_digest = /^([a-fA-F0-9]{64})\/?$/;
function parseDigest(url) {
const digest_test = url.match(re_digest);
if (digest_test === null) {
@@ -96,6 +96,42 @@ function parseDigest(url) {
return digest_test[1].toLowerCase();
}
function getIds(url: string): Array<string> {
const params: Array<string> = url.split('?')[1].split('&');
let ids: Array<string> = [];
for (let param of params) {
const splitParam: Array<string> = param.split('=');
if (splitParam[0] === 'id') {
ids.push(parseDigest(splitParam[1]));
}
}
return ids;
}
function generateResponseBody(digest: string, data: string | boolean): string {
let response = {
id: digest,
status: 0,
headers: {},
body: ''
}
if (typeof data === 'boolean' || data === undefined) {
response.body = `Metadata for identifier ${digest} not found!`;
response.status = 404;
response.headers = {"Content-Type": "text/plain"}
} else {
const responseContentLength = (new TextEncoder().encode(data)).length;
response.body = data;
response.status = 200;
response.headers = {
"Access-Control-Allow-Origin": "*",
"Content-Type": 'application/json',
"Content-Length": responseContentLength,
}
}
return JSON.stringify(response);
}
async function processRequest(req, res) {
let digest = undefined;
const headers = {
@@ -119,7 +155,16 @@ async function processRequest(req, res) {
}
try {
digest = parseDigest(req.url);
if (req.url.includes('id')) {
if (req.method !== 'GET') {
res.writeHead(405, {"Content-Type": "text/plain"});
res.end();
return;
}
digest = getIds(req.url);
} else {
digest = parseDigest(req.url.substring(1));
}
} catch(e) {
console.error('digest error: ' + e)
res.writeHead(400, {"Content-Type": "text/plain"});
@@ -162,7 +207,24 @@ async function processRequest(req, res) {
break;
case 'get:automerge:client':
content = await handlers.handleClientMergeGet(db, digest, keystore);
if (digest instanceof Array) {
let response = [];
for (let dg of digest) {
const metadata = await handlers.handleClientMergeGet(db, dg, keystore);
response.push(generateResponseBody(dg, metadata));
}
const responseContentLength = (new TextEncoder().encode(response.toString())).length;
res.writeHead(207, {
"Access-Control-Allow-Origin": "*",
"Content-Type": contentType,
"Content-Length": responseContentLength,
});
res.write(response.toString());
res.end();
return;
} else {
content = await handlers.handleClientMergeGet(db, digest, keystore);
}
break;
case 'post:automerge:server':
@@ -182,13 +244,30 @@ async function processRequest(req, res) {
// break;
case 'get:automerge:none':
r = await handlers.handleNoMergeGet(db, digest, keystore);
if (r == false) {
res.writeHead(404, {"Content-Type": "text/plain"});
if (digest instanceof Array) {
let response = [];
for (let dg of digest) {
const metadata = await handlers.handleNoMergeGet(db, dg, keystore);
response.push(generateResponseBody(dg, metadata));
}
const responseContentLength = (new TextEncoder().encode(response.toString())).length;
res.writeHead(207, {
"Access-Control-Allow-Origin": "*",
"Content-Type": contentType,
"Content-Length": responseContentLength,
});
res.write(response.toString());
res.end();
return;
} else {
r = await handlers.handleNoMergeGet(db, digest, keystore);
if (r == false) {
res.writeHead(404, {"Content-Type": "text/plain"});
res.end();
return;
}
content = r;
}
content = r;
break;
default:
@@ -205,7 +284,7 @@ async function processRequest(req, res) {
if (content === undefined) {
console.error('empty content', data);
res.writeHead(400, {"Content-Type": "text/plain"});
res.writeHead(404, {"Content-Type": "text/plain"});
res.end();
return;
}

View File

@@ -26,7 +26,7 @@ def get_sms_queue_tasks(app, task_prefix='cic_notify.tasks.sms.'):
for q in qs[host]:
if re.match(re_q, q['name']):
host_queues.append((host, q['name'],))
task_prefix_len = len(task_prefix)
queue_tasks = []
for (host, queue) in host_queues:
@@ -35,17 +35,18 @@ def get_sms_queue_tasks(app, task_prefix='cic_notify.tasks.sms.'):
for task in tasks:
if len(task) >= task_prefix_len and task[:task_prefix_len] == task_prefix:
queue_tasks.append((queue, task,))
return queue_tasks
class Api:
# 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
:type queue: str
"""
self.queue = queue
self.sms_tasks = get_sms_queue_tasks(app)
logg.debug('sms tasks {}'.format(self.sms_tasks))
@@ -61,13 +62,19 @@ class Api:
"""
signatures = []
for q in self.sms_tasks:
if not self.queue:
queue = q[0]
else:
queue = self.queue
signature = celery.signature(
q[1],
[
message,
recipient,
],
queue=q[0],
queue=queue,
)
signatures.append(signature)

View File

@@ -87,10 +87,18 @@ for key in config.store.keys():
module = importlib.import_module(config.store[key])
if key == 'TASKS_AFRICASTALKING':
africastalking_notifier = module.AfricasTalkingNotifier
api_sender_id = config.get('AFRICASTALKING_API_SENDER_ID')
logg.debug(f'SENDER ID VALUE IS: {api_sender_id}')
if not api_sender_id:
api_sender_id = None
logg.debug(f'SENDER ID RESOLVED TO NONE: {api_sender_id}')
africastalking_notifier.initialize(
config.get('AFRICASTALKING_API_USERNAME'),
config.get('AFRICASTALKING_API_KEY'),
config.get('AFRICASTALKING_API_SENDER_ID')
api_sender_id
)

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
from enum import IntEnum
# third party imports
from sqlalchemy import Column, Integer, String
# local imports
from cic_ussd.db.enum import AccountStatus
from cic_ussd.db.models.base import SessionBase
from cic_ussd.encoder import check_password_hash, create_password_hash
class AccountStatus(IntEnum):
PENDING = 1
ACTIVE = 2
LOCKED = 3
RESET = 4
# third party imports
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm.session import Session
class Account(SessionBase):
@@ -30,6 +24,21 @@ class Account(SessionBase):
account_status = Column(Integer)
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):
self.blockchain_address = blockchain_address
self.phone_number = phone_number

View File

@@ -275,6 +275,18 @@
"display_key": "ussd.kenya.new_pin_confirmation",
"name": "new_pin_confirmation",
"parent": "metadata_management"
},
"47": {
"description": "Year of birth entry menu.",
"display_key": "ussd.kenya.enter_date_of_birth",
"name": "enter_date_of_birth",
"parent": "metadata_management"
},
"48": {
"description": "Pin entry menu for changing year of birth data.",
"display_key": "ussd.kenya.dob_edit_pin_authorization",
"name": "dob_edit_pin_authorization",
"parent": "metadata_management"
}
}

View File

@@ -0,0 +1,12 @@
# standard imports
# external imports
# local imports
from .base import MetadataRequestsHandler
class CustomMetadata(MetadataRequestsHandler):
def __init__(self, identifier: bytes):
super().__init__(cic_type=':cic.custom', identifier=identifier)

View File

@@ -0,0 +1,12 @@
# standard imports
# external imports
# local imports
from .base import MetadataRequestsHandler
class PreferencesMetadata(MetadataRequestsHandler):
def __init__(self, identifier: bytes):
super().__init__(cic_type=':cic.preferences', identifier=identifier)

View File

@@ -6,11 +6,13 @@ import logging
import celery
import i18n
from cic_eth.api.api_task import Api
from sqlalchemy.orm.session import Session
from tinydb.table import Document
from typing import Optional
# local imports
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.task_tracker import TaskTracker
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()
def add_tasks_to_tracker(task_uuid):
"""
This function takes tasks spawned over api interfaces and records their creation time for tracking.
def add_tasks_to_tracker(session, task_uuid: str):
"""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.
:type task_uuid: str
"""
session = SessionBase.bind_session(session=session)
task_record = TaskTracker(task_uuid=task_uuid)
TaskTracker.session.add(task_record)
TaskTracker.session.commit()
session.add(task_record)
session.flush()
SessionBase.release_session(session=session)
def define_response_with_content(headers: list, response: str) -> tuple:
@@ -95,6 +100,7 @@ def create_or_update_session(
service_code: str,
user_input: str,
current_menu: str,
session,
session_data: Optional[dict] = None) -> InMemoryUssdSession:
"""
Handles the creation or updating of session as necessary.
@@ -108,12 +114,15 @@ def create_or_update_session(
:type user_input: str
:param current_menu: Menu name that is currently being displayed on the ussd session
: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.
:type session_data: dict.
:return: ussd session object
: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()
if existing_ussd_session:
@@ -132,20 +141,25 @@ def create_or_update_session(
current_menu=current_menu,
session_data=session_data
)
SessionBase.release_session(session=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.
:param phone_number: The phone number to be checked.
:type phone_number: str
:param session:
:type session:
:return: The user account status.
:rtype: str
"""
user = Account.session.query(Account).filter_by(phone_number=phone_number).first()
status = user.get_account_status()
Account.session.add(user)
Account.session.commit()
session = SessionBase.bind_session(session=session)
account = Account.get_by_phone_number(phone_number=phone_number, session=session)
status = account.get_account_status()
session.add(account)
session.flush()
SessionBase.release_session(session=session)
return status
@@ -165,6 +179,7 @@ def initiate_account_creation_request(chain_str: str,
external_session_id: str,
phone_number: str,
service_code: str,
session,
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
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
:param service_code: The service code dialed.
:type service_code: str
:param session:
:type session:
:param user_input: The input entered by the user.
:type user_input: str
: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
# 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_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,
service_code=service_code,
current_menu=current_menu.get('name'),
session=session,
user_input=user_input)
# 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)
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
:param ussd_session: An in db ussd session object.
:type ussd_session: UssdSession
:param user: A user object.
:type user: Account
:param account: A account object.
:type account: Account
:param session:
:type session:
:param user_input: The user's input.
:type user_input: str
:return: An in memory ussd menu object.
@@ -285,7 +305,13 @@ def process_current_menu(ussd_session: Optional[dict], user: Account, user_input
else:
# get current state
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
@@ -294,6 +320,7 @@ def process_menu_interaction_requests(chain_str: str,
phone_number: str,
queue: str,
service_code: str,
session,
user_input: str) -> str:
"""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.
@@ -308,25 +335,29 @@ def process_menu_interaction_requests(chain_str: str,
:type queue: str
:param service_code: The service dialed by the user making the request.
:type service_code: str
:param session:
:type session:
:param user_input: The inputs entered by the user.
:type user_input: str
:return: A response based on the request received.
:rtype: str
"""
# 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,
external_session_id=external_session_id,
phone_number=phone_number,
service_code=service_code,
session=session,
user_input=user_input)
else:
# get user
user = Account.session.query(Account).filter_by(phone_number=phone_number).first()
# get account
session = SessionBase.bind_session(session=session)
account = Account.get_by_phone_number(phone_number=phone_number, session=session)
# retrieve and cache user's metadata
blockchain_address = user.blockchain_address
blockchain_address = account.blockchain_address
s_query_person_metadata = celery.signature(
'cic_ussd.tasks.metadata.query_person_metadata',
[blockchain_address]
@@ -334,24 +365,25 @@ def process_menu_interaction_requests(chain_str: str,
s_query_person_metadata.apply_async(queue='cic-ussd')
# find any existing ussd session
existing_ussd_session = UssdSession.session.query(UssdSession).filter_by(
external_session_id=external_session_id).first()
existing_ussd_session = session.query(UssdSession).filter_by(external_session_id=external_session_id).first()
# validate user inputs
if existing_ussd_session:
current_menu = process_current_menu(
account=account,
session=session,
ussd_session=existing_ussd_session.to_json(),
user=user,
user_input=user_input
)
else:
current_menu = process_current_menu(
account=account,
session=session,
ussd_session=None,
user=user,
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:
# create or update the ussd session as appropriate
@@ -361,6 +393,7 @@ def process_menu_interaction_requests(chain_str: str,
service_code=service_code,
user_input=user_input,
current_menu=current_menu.get('name'),
session=session,
session_data=last_ussd_session.session_data
)
else:
@@ -369,15 +402,17 @@ def process_menu_interaction_requests(chain_str: str,
phone=phone_number,
service_code=service_code,
user_input=user_input,
current_menu=current_menu.get('name')
current_menu=current_menu.get('name'),
session=session
)
# define appropriate response
response = custom_display_text(
account=account,
display_key=current_menu.get('display_key'),
menu_name=current_menu.get('name'),
session=session,
ussd_session=ussd_session.to_json(),
user=user
)
# 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_task(external_session_id=external_session_id, queue=queue)
SessionBase.release_session(session=session)
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.
:param phone_number: The phone number belonging to the account to be unlocked.
:type phone_number: str
:param session:
:type session:
:return: The status of the pin reset.
:rtype: str
"""
user = Account.session.query(Account).filter_by(phone_number=phone_number).first()
user.reset_account_pin()
Account.session.add(user)
Account.session.commit()
session = SessionBase.bind_session(session=session)
account = Account.get_by_phone_number(phone_number=phone_number, session=session)
account.reset_account_pin()
session.add(account)
session.flush()
SessionBase.release_session(session=session)
response = f'Pin reset for user {phone_number} is successful!'
return response
@@ -438,11 +478,13 @@ def update_ussd_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
cache.
:param queue: The queue on which the celery task should run.
: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
temporarily.
: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'),
user_input=in_redis_ussd_session.get('user_input'),
current_menu=in_redis_ussd_session.get('state'),
session=session,
session_data=session_data
)
persist_session_to_db_task(external_session_id=external_session_id, queue=queue)

View File

@@ -8,6 +8,10 @@ import phonenumbers
from cic_ussd.db.models.account import Account
class E164Format:
region = None
def process_phone_number(phone_number: str, region: str):
"""This function parses any phone number for the provided region
:param phone_number: A string with a phone number.
@@ -29,19 +33,5 @@ def process_phone_number(phone_number: str, region: str):
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='KE')
user = Account.session.query(Account).filter_by(phone_number=phone_number).first()
return user
class Support:
phone_number = None

View File

@@ -1,25 +1,27 @@
# standard imports
import datetime
import logging
import json
import re
from typing import Optional
# third party imports
import celery
from sqlalchemy import desc
from cic_eth.api import Api
from sqlalchemy.orm.session import Session
from tinydb.table import Document
# local imports
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.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.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.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.state_machine import UssdStateMachine
from cic_ussd.conversions import to_wei, from_wei
@@ -61,47 +63,48 @@ def retrieve_token_symbol(chain_str: str = Chain.spec.__str__()):
raise SeppukuError(f'Could not retrieve default token for: {chain_str}')
def process_pin_authorization(display_key: str, user: Account, **kwargs) -> str:
"""
This method provides translation for all ussd menu entries that follow the pin authorization pattern.
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.
: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
: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.
:type kwargs
:return: A string value corresponding the ussd menu's text value.
:rtype: str
"""
remaining_attempts = 3
if user.failed_pin_attempts > 0:
if account.failed_pin_attempts > 0:
return translation_for(
key=f'{display_key}.retry',
preferred_language=user.preferred_language,
remaining_attempts=(remaining_attempts - user.failed_pin_attempts)
preferred_language=account.preferred_language,
remaining_attempts=(remaining_attempts - account.failed_pin_attempts)
)
else:
return translation_for(
key=f'{display_key}.first',
preferred_language=user.preferred_language,
preferred_language=account.preferred_language,
**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
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
:type display_key: str
:param user: The user requesting access to the ussd menu.
:type user: Account
:param session:
:type session:
:param ussd_session: A JSON serialized in-memory ussd session object
:type ussd_session: dict
:return: Corresponding translation text response
:rtype: str
"""
# 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
user_input = ussd_session.get('user_input').split('*')[-1]
@@ -111,13 +114,13 @@ def process_exit_insufficient_balance(display_key: str, user: Account, ussd_sess
token_symbol = retrieve_token_symbol()
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)
return translation_for(
key=display_key,
preferred_language=user.preferred_language,
preferred_language=account.preferred_language,
amount=from_wei(transaction_amount),
token_symbol=token_symbol,
recipient_information=tx_recipient_information,
@@ -125,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.
: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
:type display_key: str
:param user: The user requesting access to the ussd menu.
:type user: Account
:param session:
:type session:
:param ussd_session: A JSON serialized in-memory ussd session object
:type ussd_session: dict
:return: Corresponding translation text response
@@ -139,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')))
token_symbol = retrieve_token_symbol()
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_sender_information = define_account_tx_metadata(user=user)
tx_sender_information = define_account_tx_metadata(user=account)
return translation_for(
key=display_key,
preferred_language=user.preferred_language,
preferred_language=account.preferred_language,
transaction_amount=from_wei(transaction_amount),
token_symbol=token_symbol,
recipient_information=tx_recipient_information,
@@ -153,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
pre-transaction response menu that shows the details of the transaction.
:param user: The user requesting access to the ussd menu.
:type user: Account
: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
: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
text values.
:type ussd_session: UssdSession
@@ -168,16 +175,16 @@ def process_transaction_pin_authorization(user: Account, display_key: str, ussd_
"""
# compile response data
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_sender_information = define_account_tx_metadata(user=user)
tx_sender_information = define_account_tx_metadata(user=account)
token_symbol = retrieve_token_symbol()
user_input = ussd_session.get('session_data').get('transaction_amount')
transaction_amount = to_wei(value=int(user_input))
logg.debug('Requires integration to determine user tokens.')
return process_pin_authorization(
user=user,
account=account,
display_key=display_key,
recipient_information=tx_recipient_information,
transaction_amount=from_wei(transaction_amount),
@@ -186,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:
:type user:
:param display_key:
:type display_key:
:param ussd_session:
:type ussd_session:
:return:
:rtype:
"""
@@ -257,6 +262,10 @@ def process_display_user_metadata(user: Account, display_key: str):
contact_data = get_contact_data_from_vcard(vcard=user_metadata.get('vcard'))
logg.debug(f'{contact_data}')
full_name = f'{contact_data.get("given")} {contact_data.get("family")}'
date_of_birth = user_metadata.get('date_of_birth')
year_of_birth = date_of_birth.get('year')
present_year = datetime.datetime.now().year
age = present_year - year_of_birth
gender = user_metadata.get('gender')
products = ', '.join(user_metadata.get('products'))
location = user_metadata.get('location').get('area_name')
@@ -265,6 +274,7 @@ def process_display_user_metadata(user: Account, display_key: str):
key=display_key,
preferred_language=user.preferred_language,
full_name=full_name,
age=age,
gender=gender,
location=location,
products=products
@@ -289,15 +299,12 @@ def process_display_user_metadata(user: Account, display_key: str):
)
def process_account_statement(user: Account, display_key: str, ussd_session: dict):
def process_account_statement(user: Account, display_key: str):
"""
:param user:
:type user:
:param display_key:
:type display_key:
:param ussd_session:
:type ussd_session:
:return:
:rtype:
"""
@@ -399,23 +406,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
last_ussd_session = UssdSession.session\
.query(UssdSession)\
session = SessionBase.bind_session(session=session)
last_ussd_session = session.query(UssdSession)\
.filter_by(msisdn=phone_number)\
.order_by(desc(UssdSession.created))\
.first()
SessionBase.release_session(session=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
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.
It then returns the appropriate ussd menu text values.
:param user: The user requesting access to the ussd menu.
:type user: Account
:param account: The account requesting access to the ussd menu.
:type account: Account
:param session:
:type session:
:param user_input: The value a user enters in the ussd menu.
:type user_input: str
:param ussd_session: A JSON serialized in-memory ussd session object
@@ -423,22 +433,20 @@ def process_request(user_input: str, user: Account, ussd_session: Optional[dict]
:return: A ussd menu's corresponding text value.
:rtype: Document
"""
# retrieve metadata before any transition
key = generate_metadata_pointer(
identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address),
cic_type=':cic.person'
)
person_metadata = get_cached_data(key=key)
if ussd_session:
if user_input == "0":
return UssdMenu.parent_menu(menu_name=ussd_session.get('state'))
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)
else:
if user.has_valid_pin():
last_ussd_session = retrieve_most_recent_ussd_session(phone_number=user.phone_number)
if account.has_valid_pin():
last_ussd_session = retrieve_most_recent_ussd_session(phone_number=account.phone_number, session=session)
if last_ussd_session:
# get last state
@@ -452,33 +460,35 @@ def process_request(user_input: str, user: Account, ussd_session: Optional[dict]
'exit_pin_mismatch',
'exit_invalid_request',
'exit_successful_transaction'
] and person_metadata is not None:
]:
return UssdMenu.find_by_name(name='start')
else:
return UssdMenu.find_by_name(name=last_state)
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')
elif user.preferred_language is None:
elif account.preferred_language is None:
return UssdMenu.find_by_name(name='initial_language_selection')
else:
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.
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.
: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
: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.
:type user_input: str
:return: A string value corresponding the successive give a specific state in the state machine.
"""
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
return new_state
@@ -493,42 +503,63 @@ def process_exit_invalid_menu_option(display_key: str, preferred_language: str):
def custom_display_text(
account: Account,
display_key: str,
menu_name: str,
ussd_session: dict,
user: Account) -> str:
session: Session,
ussd_session: dict) -> str:
"""This function extracts the appropriate session data based on the current menu name. It then inserts them as
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
:type display_key: str
:param menu_name: The name by which a specific menu can be identified.
:type menu_name: str
:param user: The user in a running USSD session.
:type user: Account
:param session:
:type session:
:param ussd_session: A JSON serialized in-memory ussd session object
:type ussd_session: dict
:return: A string value corresponding the ussd menu's text value.
:rtype: str
"""
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':
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':
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':
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:
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:
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':
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:
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':
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':
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:
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
from sqlalchemy import desc
from sqlalchemy.orm.session import Session
# 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.validator import check_known_user
@@ -72,24 +75,26 @@ def get_account_creation_callback_request_data(env: dict) -> tuple:
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
requests responsible for returning an account's status and
:param env: A dictionary of values representing data sent on the api.
:type env: dict
:param phone_number: The phone of the user whose pin is being reset.
:type phone_number: str
:param session:
:type session:
:return: A response denoting the result of the request to reset the user's pin.
: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'
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':
status = get_account_status(phone_number=phone_number)
status = get_account_status(phone_number=phone_number, session=session)
response = {
'status': f'{status}'
}
@@ -97,16 +102,18 @@ def process_pin_reset_requests(env: dict, phone_number: str):
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
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.
:type env: dict
:param session:
:type session:
:return: A tuple containing a serialized list of blockchain addresses for locked accounts and corresponding message
for the response.
:rtype: tuple
"""
logg.debug('Authentication requires integration with cic-auth')
session = SessionBase.bind_session(session=session)
response = ''
if get_request_method(env) == 'GET':
@@ -123,12 +130,14 @@ def process_locked_accounts_requests(env: dict) -> tuple:
else:
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.failed_pin_attempts >= 3).order_by(desc(Account.updated)).offset(offset).limit(limit).all()
# convert lists to scalar blockchain addresses
locked_accounts = [blockchain_address for (blockchain_address, ) in locked_accounts]
SessionBase.release_session(session=session)
response = json.dumps(locked_accounts)
return response, '200 OK'
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
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'))
# create session for the life time of http request
SessionBase.session = SessionBase.create_session()
# handle requests from CICADA
def application(env, start_response):
"""Loads python code for application to be accessible over web server
: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')]
headers = [('Content-Type', 'text/plain')]
# create session for the life time of http request
session = SessionBase.create_session()
if get_request_endpoint(env) == '/pin':
phone_number = get_query_parameters(env=env, query_name='phoneNumber')
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)
SessionBase.session.close()
session.commit()
session.close()
start_response(message, headers)
return [response_bytes]
# 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)
start_response(message, headers)
SessionBase.session.close()
session.commit()
session.close()
return [response_bytes]

View File

@@ -26,7 +26,7 @@ from cic_ussd.metadata.base import Metadata
from cic_ussd.operations import (define_response_with_content,
process_menu_interaction_requests,
define_multilingual_responses)
from cic_ussd.phone_number import process_phone_number, Support
from cic_ussd.phone_number import process_phone_number, Support, E164Format
from cic_ussd.processor import get_default_token_data
from cic_ussd.redis import cache_data, create_cached_data_key, InMemoryStore
from cic_ussd.requests import (get_request_endpoint,
@@ -55,8 +55,6 @@ 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'))
# create session for the life time of http request
SessionBase.session = SessionBase.create_session()
# set up translations
i18n.load_path.append(config.get('APP_LOCALE_PATH'))
@@ -126,6 +124,7 @@ else:
valid_service_codes = config.get('APP_SERVICE_CODE').split(",")
E164Format.region = config.get('PHONE_NUMBER_REGION')
Support.phone_number = config.get('APP_SUPPORT_PHONE_NUMBER')
@@ -142,10 +141,13 @@ def application(env, start_response):
errors_headers = [('Content-Type', 'text/plain'), ('Content-Length', '0')]
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 env.get('CONTENT_TYPE') != 'application/x-www-form-urlencoded':
start_response('405 Play by the rules', errors_headers)
start_response('405 Urlencoded, please', errors_headers)
return []
post_data = env.get('wsgi.input').read()
@@ -168,7 +170,7 @@ def application(env, start_response):
# add validation for phone number
if phone_number:
phone_number = process_phone_number(phone_number=phone_number, region=config.get('PHONE_NUMBER_REGION'))
phone_number = process_phone_number(phone_number=phone_number, region=E164Format.region)
# validate ip address
if not check_ip(config=config, env=env):
@@ -205,14 +207,20 @@ def application(env, start_response):
phone_number=phone_number,
queue=args.q,
service_code=service_code,
session=session,
user_input=user_input)
response_bytes, headers = define_response_with_content(headers=headers, response=response)
start_response('200 OK,', headers)
SessionBase.session.close()
session.commit()
session.close()
return [response_bytes]
else:
logg.error('invalid query {}'.format(env))
for r in env:
logg.debug('{}: {}'.format(r, env))
session.close()
start_response('405 Play by the rules', errors_headers)
return []

View File

@@ -3,6 +3,7 @@ import logging
from typing import Tuple
# third-party imports
from sqlalchemy.orm.session import Session
# local imports
from cic_ussd.db.models.account import Account
@@ -10,11 +11,11 @@ from cic_ussd.db.models.account import Account
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
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.
: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.)')

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'
:rtype: bool
"""
user_input, ussd_session, user = state_machine_data
user_input, ussd_session, user, session = state_machine_data
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'
:rtype: bool
"""
user_input, ussd_session, user = state_machine_data
user_input, ussd_session, user, session = state_machine_data
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'
:rtype: bool
"""
user_input, ussd_session, user = state_machine_data
user_input, ussd_session, user, session = state_machine_data
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'
:rtype: bool
"""
user_input, ussd_session, user = state_machine_data
user_input, ussd_session, user, session = state_machine_data
return user_input == '4'
@@ -62,10 +62,22 @@ def menu_five_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
:return: A user input's match with '5'
:rtype: bool
"""
user_input, ussd_session, user = state_machine_data
user_input, ussd_session, user, session = state_machine_data
return user_input == '5'
def menu_six_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
"""
This function checks that user input matches a string with value '6'
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
:return: A user input's match with '6'
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
return user_input == '6'
def menu_zero_zero_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
"""
This function checks that user input matches a string with value '00'
@@ -74,7 +86,7 @@ def menu_zero_zero_selected(state_machine_data: Tuple[str, dict, Account]) -> bo
:return: A user input's match with '00'
:rtype: bool
"""
user_input, ussd_session, user = state_machine_data
user_input, ussd_session, user, session = state_machine_data
return user_input == '00'
@@ -86,5 +98,5 @@ def menu_ninety_nine_selected(state_machine_data: Tuple[str, dict, Account]) ->
:return: A user input's match with '99'
:rtype: bool
"""
user_input, ussd_session, user = state_machine_data
user_input, ussd_session, user, session = state_machine_data
return user_input == '99'

View File

@@ -9,11 +9,13 @@ import re
from typing import Tuple
# third party imports
import bcrypt
from sqlalchemy.orm.session import Session
# local imports
from cic_ussd.db.models.account import AccountStatus, Account
from cic_ussd.encoder import PasswordEncoder, create_password_hash, check_password_hash
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.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.redis import InMemoryStore
@@ -21,7 +23,7 @@ from cic_ussd.redis import InMemoryStore
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
numeric.
: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
:rtype: bool
"""
user_input, ussd_session, user = state_machine_data
user_input, ussd_session, user, session = state_machine_data
pin_is_valid = False
matcher = r'^\d{4}$'
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
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.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
:return: A match between two pin values.
: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)
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.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
:return: A match between two pin values.
: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
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.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
: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
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'),
user_input=user_input,
current_menu=in_redis_ussd_session.get('state'),
session=session,
session_data=session_data
)
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.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
:return: A match between two pin values.
: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')
logg.debug(f'USSD SESSION: {ussd_session}')
return check_password_hash(user_input, initial_pin)
def complete_pin_change(state_machine_data: Tuple[str, dict, Account]):
def complete_pin_change(state_machine_data: Tuple[str, dict, Account, Session]):
"""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.
: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')
user.password_hash = password_hash
Account.session.add(user)
Account.session.commit()
session.add(user)
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.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
:return: A match between two pin values.
: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
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.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
:return: A match between two pin values.
: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)
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]):
user_input, ussd_session, user = state_machine_data
user_input, ussd_session, user, session = state_machine_data
logg.debug('Requires integration to cic-notify.')
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.')
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.')

View File

@@ -5,13 +5,16 @@ from typing import Tuple
# third party imports
import celery
from sqlalchemy.orm.session import Session
# 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.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.phone_number import get_user_by_phone_number, process_phone_number
from cic_ussd.phone_number import process_phone_number, E164Format
from cic_ussd.processor import retrieve_token_symbol
from cic_ussd.redis import create_cached_data_key, get_cached_data
from cic_ussd.transactions import OutgoingTransactionProcessor
@@ -20,7 +23,7 @@ from cic_ussd.transactions import OutgoingTransactionProcessor
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
and is authorized to perform standard transactions.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
@@ -28,14 +31,17 @@ def is_valid_recipient(state_machine_data: Tuple[str, dict, Account]) -> bool:
:return: A user's validity
:rtype: bool
"""
user_input, ussd_session, user = state_machine_data
recipient = get_user_by_phone_number(phone_number=user_input)
is_not_initiator = process_phone_number(user_input, 'KE') != user.phone_number
user_input, ussd_session, user, session = state_machine_data
phone_number = process_phone_number(user_input, E164Format.region)
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
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
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
being attempted.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
@@ -43,14 +49,14 @@ def is_valid_transaction_amount(state_machine_data: Tuple[str, dict, Account]) -
:return: A transaction amount's validity
:rtype: bool
"""
user_input, ussd_session, user = state_machine_data
user_input, ussd_session, user, session = state_machine_data
try:
return int(user_input) > 0
except ValueError:
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
being attempted.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
@@ -58,10 +64,7 @@ def has_sufficient_balance(state_machine_data: Tuple[str, dict, Account]) -> boo
:return: An account balance's validity
:rtype: bool
"""
user_input, ussd_session, user = state_machine_data
balance_manager = BalanceManager(address=user.blockchain_address,
chain_str=Chain.spec.__str__(),
token_symbol='SRF')
user_input, ussd_session, user, session = state_machine_data
# get cached balance
key = create_cached_data_key(
identifier=bytes.fromhex(user.blockchain_address[2:]),
@@ -73,30 +76,37 @@ def has_sufficient_balance(state_machine_data: Tuple[str, dict, Account]) -> boo
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.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
: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['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:
:type state_machine_data:
:return:
: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
# retrieve and cache account's metadata
s_query_person_metadata = celery.signature(
'cic_ussd.tasks.metadata.query_person_metadata',
@@ -105,32 +115,36 @@ def retrieve_recipient_metadata(state_machine_data: Tuple[str, dict, Account]):
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.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
: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['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.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
: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
chain_str = Chain.spec.__str__()
# get user from 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
from_address = user.blockchain_address
amount = int(ussd_session.get('session_data').get('transaction_amount'))

View File

@@ -5,12 +5,14 @@ from typing import Tuple
# third-party imports
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 sqlalchemy.orm.session import Session
# local imports
from cic_ussd.chain import Chain
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.metadata import blockchain_address_to_metadata_pointer
from cic_ussd.operations import save_to_in_memory_ussd_session_data
@@ -19,37 +21,63 @@ from cic_ussd.redis import get_cached_data
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.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
: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'
Account.session.add(user)
Account.session.commit()
session.add(user)
session.flush()
SessionBase.release_session(session=session)
preferences_data = {
'preferred_language': 'en'
}
s = celery.signature(
'cic_ussd.tasks.metadata.add_preferences_metadata',
[user.blockchain_address, preferences_data]
)
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.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
"""
user_input, ussd_session, user = state_machine_data
user.preferred_language = 'sw'
Account.session.add(user)
Account.session.commit()
user_input, ussd_session, account, session = state_machine_data
session = SessionBase.bind_session(session=session)
account.preferred_language = 'sw'
session.add(account)
session.flush()
SessionBase.release_session(session=session)
preferences_data = {
'preferred_language': 'sw'
}
s = celery.signature(
'cic_ussd.tasks.metadata.add_preferences_metadata',
[account.blockchain_address, preferences_data]
)
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.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
"""
user_input, ussd_session, user = state_machine_data
user.activate_account()
Account.session.add(user)
Account.session.commit()
user_input, ussd_session, account, session = state_machine_data
session = SessionBase.bind_session(session=session)
account.activate_account()
session.add(account)
session.flush()
SessionBase.release_session(session=session)
def process_gender_user_input(user: Account, user_input: str):
@@ -61,6 +89,7 @@ def process_gender_user_input(user: Account, user_input: str):
:return:
:rtype:
"""
gender = ""
if user.preferred_language == 'en':
if user_input == '1':
gender = 'Male'
@@ -78,13 +107,13 @@ def process_gender_user_input(user: Account, user_input: str):
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.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
: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
current_state = ussd_session.get('state')
@@ -93,6 +122,9 @@ def save_metadata_attribute_to_session_data(state_machine_data: Tuple[str, dict,
if 'given_name' in current_state:
key = 'given_name'
if 'date_of_birth' in current_state:
key = 'date_of_birth'
if 'family_name' in current_state:
key = 'family_name'
@@ -114,7 +146,11 @@ def save_metadata_attribute_to_session_data(state_machine_data: Tuple[str, dict,
session_data = {
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):
@@ -130,6 +166,13 @@ def format_user_metadata(metadata: dict, user: Account):
given_name = metadata.get('given_name')
family_name = metadata.get('family_name')
if isinstance(metadata.get('date_of_birth'), dict):
date_of_birth = metadata.get('date_of_birth')
else:
date_of_birth = {
"year": int(metadata.get('date_of_birth')[:4])
}
# check whether there's existing location data
if isinstance(metadata.get('location'), dict):
location = metadata.get('location')
@@ -154,6 +197,7 @@ def format_user_metadata(metadata: dict, user: Account):
)
return {
"date_registered": date_registered,
"date_of_birth": date_of_birth,
"gender": gender,
"identities": identities,
"location": location,
@@ -166,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
:param state_machine_data: A tuple containing user input, a ussd session and user object.
: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
metadata = ussd_session.get('session_data')
@@ -187,8 +231,8 @@ def save_complete_user_metadata(state_machine_data: Tuple[str, dict, Account]):
s_create_person_metadata.apply_async(queue='cic-ussd')
def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, Account]):
user_input, ussd_session, user = state_machine_data
def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, Account, Session]):
user_input, ussd_session, user, session = state_machine_data
blockchain_address = user.blockchain_address
key = generate_metadata_pointer(
identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address),
@@ -201,12 +245,12 @@ def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, Account]):
given_name = ussd_session.get('session_data').get('given_name')
family_name = ussd_session.get('session_data').get('family_name')
date_of_birth = ussd_session.get('session_data').get('date_of_birth')
gender = ussd_session.get('session_data').get('gender')
location = ussd_session.get('session_data').get('location')
products = ussd_session.get('session_data').get('products')
# validate user metadata
person = Person()
user_metadata = json.loads(user_metadata)
# edit specific metadata attribute
@@ -214,6 +258,11 @@ def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, Account]):
user_metadata['given_name'] = given_name
if family_name:
user_metadata['family_name'] = family_name
if date_of_birth and len(date_of_birth) == 4:
year = int(date_of_birth[:4])
user_metadata['date_of_birth'] = {
'year': year
}
if gender:
user_metadata['gender'] = gender
if location:
@@ -233,8 +282,8 @@ def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, Account]):
s_edit_person_metadata.apply_async(queue='cic-ussd')
def get_user_metadata(state_machine_data: Tuple[str, dict, Account]):
user_input, ussd_session, user = state_machine_data
def get_user_metadata(state_machine_data: Tuple[str, dict, Account, Session]):
user_input, ussd_session, user, session = state_machine_data
blockchain_address = user.blockchain_address
s_get_user_metadata = celery.signature(
'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.
: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
key = generate_metadata_pointer(
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.
: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]+$"
valid_name = re.match(name_matcher, user_input)
if valid_name:
@@ -50,9 +50,21 @@ def is_valid_gender_selection(state_machine_data: Tuple[str, dict, Account]):
:return:
:rtype:
"""
user_input, ussd_session, user = state_machine_data
user_input, ussd_session, user, session = state_machine_data
selection_matcher = "^[1-2]$"
if re.match(selection_matcher, user_input):
return True
else:
return False
def is_valid_date(state_machine_data: Tuple[str, dict, Account]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
user_input, ussd_session, user, session = state_machine_data
# For MVP this value is defaulting to year
return len(user_input) == 4 and int(user_input) >= 1900

View File

@@ -53,13 +53,25 @@ def process_account_creation_callback(self, result: str, url: str, status_code:
session.add(user)
session.commit()
session.close()
queue = self.request.delivery_info.get('routing_key')
s = celery.signature(
# add phone number metadata lookup
s_phone_pointer = celery.signature(
'cic_ussd.tasks.metadata.add_phone_pointer',
[result, phone_number]
)
s.apply_async(queue=queue)
s_phone_pointer.apply_async(queue=queue)
# add custom metadata tags
custom_metadata = {
"tags": ["ussd", "individual"]
}
s_custom_metadata = celery.signature(
'cic_ussd.tasks.metadata.add_custom_metadata',
[result, custom_metadata]
)
s_custom_metadata.apply_async(queue=queue)
# expire cache
cache.expire(task_id, timedelta(seconds=180))

View File

@@ -7,8 +7,10 @@ from hexathon import strip_0x
# local imports
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
from cic_ussd.metadata.custom import CustomMetadata
from cic_ussd.metadata.person import PersonMetadata
from cic_ussd.metadata.phone import PhonePointerMetadata
from cic_ussd.metadata.preferences import PreferencesMetadata
from cic_ussd.tasks.base import CriticalMetadataTask
celery_app = celery.current_app
@@ -44,7 +46,7 @@ def create_person_metadata(blockchain_address: str, data: dict):
@celery_app.task
def edit_person_metadata(blockchain_address: str, data: bytes):
def edit_person_metadata(blockchain_address: str, data: dict):
identifier = blockchain_address_to_metadata_pointer(blockchain_address=blockchain_address)
person_metadata_client = PersonMetadata(identifier=identifier)
person_metadata_client.edit(data=data)
@@ -56,3 +58,17 @@ def add_phone_pointer(self, blockchain_address: str, phone_number: str):
stripped_address = strip_0x(blockchain_address)
phone_metadata_client = PhonePointerMetadata(identifier=identifier)
phone_metadata_client.create(data=stripped_address)
@celery_app.task()
def add_custom_metadata(blockchain_address: str, data: dict):
identifier = blockchain_address_to_metadata_pointer(blockchain_address=blockchain_address)
custom_metadata_client = CustomMetadata(identifier=identifier)
custom_metadata_client.create(data=data)
@celery_app.task()
def add_preferences_metadata(blockchain_address: str, data: dict):
identifier = blockchain_address_to_metadata_pointer(blockchain_address=blockchain_address)
custom_metadata_client = PreferencesMetadata(identifier=identifier)
custom_metadata_client.create(data=data)

View File

@@ -9,6 +9,7 @@ from confini import Config
# local imports
from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase
logg = logging.getLogger(__file__)
@@ -45,29 +46,21 @@ def check_request_content_length(config: Config, env: dict):
config.get('APP_MAX_BODY_LENGTH'))
def check_known_user(phone: str):
"""
This method attempts to ascertain whether the user already exists and is known to the system.
def check_known_user(phone_number: str, session):
"""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
memory.
:param phone: A valid phone number
:type phone: str
:param phone_number: A valid phone number
:type phone_number: str
:param session:
:type session:
:return: Is known phone number
:rtype: boolean
"""
user = Account.session.query(Account).filter_by(phone_number=phone).first()
return user 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
session = SessionBase.bind_session(session=session)
account = session.query(Account).filter_by(phone_number=phone_number).first()
SessionBase.release_session(session=session)
return account is not None
def check_request_method(env: dict):

View File

@@ -1,4 +1,4 @@
cic_base[full_graph]~=0.1.2b15
cic_base[full_graph]~=0.1.2b21
cic-eth~=0.11.0b16
cic-notify~=0.4.0a5
cic-types~=0.1.0a10
cic-types~=0.1.0a11

View File

@@ -7,6 +7,7 @@
"new_pin_confirmation",
"display_user_metadata",
"name_edit_pin_authorization",
"dob_edit_pin_authorization",
"gender_edit_pin_authorization",
"location_edit_pin_authorization",
"products_edit_pin_authorization",

View File

@@ -5,5 +5,6 @@
"enter_age",
"enter_location",
"enter_products",
"enter_date_of_birth",
"display_metadata_pin_authorization"
]

View File

@@ -0,0 +1,39 @@
[
{
"trigger": "scan_data",
"source": "enter_date_of_birth",
"dest": "dob_edit_pin_authorization",
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
"conditions": [
"cic_ussd.state_machine.logic.validator.has_cached_user_metadata",
"cic_ussd.state_machine.logic.validator.is_valid_date"
]
},
{
"trigger": "scan_data",
"source": "enter_date_of_birth",
"dest": "enter_gender",
"conditions": "cic_ussd.state_machine.logic.validator.is_valid_date",
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
"unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
},
{
"trigger": "scan_data",
"source": "enter_date_of_birth",
"dest": "exit_invalid_input",
"unless": "cic_ussd.state_machine.logic.validator.is_valid_date"
},
{
"trigger": "scan_data",
"source": "dob_edit_pin_authorization",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
"after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute"
},
{
"trigger": "scan_data",
"source": "dob_edit_pin_authorization",
"dest": "exit_pin_blocked",
"conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
}
]

View File

@@ -15,7 +15,7 @@
{
"trigger": "scan_data",
"source": "enter_family_name",
"dest": "enter_gender",
"dest": "enter_date_of_birth",
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
"unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
},

View File

@@ -14,21 +14,27 @@
{
"trigger": "scan_data",
"source": "metadata_management",
"dest": "enter_location",
"dest": "enter_date_of_birth",
"conditions": "cic_ussd.state_machine.logic.menu.menu_three_selected"
},
{
"trigger": "scan_data",
"source": "metadata_management",
"dest": "enter_products",
"dest": "enter_location",
"conditions": "cic_ussd.state_machine.logic.menu.menu_four_selected"
},
{
"trigger": "scan_data",
"source": "metadata_management",
"dest": "display_metadata_pin_authorization",
"dest": "enter_products",
"conditions": "cic_ussd.state_machine.logic.menu.menu_five_selected"
},
{
"trigger": "scan_data",
"source": "metadata_management",
"dest": "display_metadata_pin_authorization",
"conditions": "cic_ussd.state_machine.logic.menu.menu_six_selected"
},
{
"trigger": "scan_data",
"source": "display_metadata_pin_authorization",

View File

@@ -17,6 +17,9 @@ en:
enter_family_name: |-
CON Enter family name
0. Back
enter_date_of_birth: |-
CON Enter year of birth
0. Back
enter_gender: |-
CON Enter gender
1. Male
@@ -52,14 +55,16 @@ en:
CON My profile
1. Edit name
2. Edit gender
3. Edit location
4. Edit products
5. View my profile
3. Edit Age
4. Edit location
5. Edit products
6. View my profile
0. Back
display_user_metadata: |-
CON Your details are:
Name: %{full_name}
Gender: %{gender}
Age: %{age}
Location: %{location}
You sell: %{products}
0. Back
@@ -117,6 +122,13 @@ en:
retry: |-
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining
0. Back
dob_edit_pin_authorization:
first: |-
CON Please enter your PIN
0. Back
retry: |-
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining
0. Back
gender_edit_pin_authorization:
first: |-
CON Please enter your PIN

View File

@@ -17,6 +17,9 @@ sw:
enter_family_name: |-
CON Weka jina lako la mwisho
0. Nyuma
enter_date_of_birth: |-
CON Weka mwaka wa kuzaliwa
0. Nyuma
enter_gender: |-
CON Weka jinsia yako
1. Mwanaume
@@ -52,14 +55,16 @@ sw:
CON Wasifu wangu
1. Weka jina
2. Weka jinsia
3. Weka eneo
4. Weka bidhaa
5. Angalia wasifu wako
3 Weka umri
4. Weka eneo
5. Weka bidhaa
6. Angalia wasifu wako
0. Nyuma
display_user_metadata: |-
CON Wasifu wako una maelezo yafuatayo:
Jina: %{full_name}
Jinsia: %{gender}
Umri: %{age}
Eneo: %{location}
Unauza: %{products}
0. Nyuma
@@ -117,6 +122,13 @@ sw:
retry: |-
CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
0. Nyuma
dob_edit_pin_authorization:
first: |-
CON Tafadhali weka PIN yako
0. Nyuma
retry: |-
CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
0. Nyuma
gender_edit_pin_authorization:
first: |-
CON Tafadhali weka PIN yako

View File

@@ -76,6 +76,7 @@ if [[ -n "${ETH_PROVIDER}" ]]; then
>&2 echo "add deployer address as account index writer"
eth-accounts-index-writer $gas_price_arg -y $DEV_ETH_KEYSTORE_FILE -i $CIC_CHAIN_SPEC -p $ETH_PROVIDER -a $DEV_ACCOUNT_INDEX_ADDRESS -ww -vv $debug $DEV_ETH_ACCOUNT_CONTRACT_DEPLOYER
>&2 echo "deploy contract registry contract"
CIC_REGISTRY_ADDRESS=`eth-contract-registry-deploy $gas_price_arg -i $CIC_CHAIN_SPEC -y $DEV_ETH_KEYSTORE_FILE --identifier BancorRegistry --identifier AccountRegistry --identifier TokenRegistry --identifier AddressDeclarator --identifier Faucet --identifier TransferAuthorization -p $ETH_PROVIDER -vv -w`
eth-contract-registry-set $gas_price_arg -w -y $DEV_ETH_KEYSTORE_FILE -r $CIC_REGISTRY_ADDRESS -i $CIC_CHAIN_SPEC -p $ETH_PROVIDER -vv ContractRegistry $CIC_REGISTRY_ADDRESS
eth-contract-registry-set $gas_price_arg -w -y $DEV_ETH_KEYSTORE_FILE -r $CIC_REGISTRY_ADDRESS -i $CIC_CHAIN_SPEC -p $ETH_PROVIDER -vv AccountRegistry $DEV_ACCOUNT_INDEX_ADDRESS
@@ -87,7 +88,7 @@ if [[ -n "${ETH_PROVIDER}" ]]; then
eth-contract-registry-set $gas_price_arg -w -y $DEV_ETH_KEYSTORE_FILE -r $CIC_REGISTRY_ADDRESS -i $CIC_CHAIN_SPEC -p $ETH_PROVIDER -vv AddressDeclarator $DEV_DECLARATOR_ADDRESS
# Deploy transfer authorization contact
>&2 echo "deploy address declarator contract"
>&2 echo "deploy transfer auth contract"
DEV_TRANSFER_AUTHORIZATION_ADDRESS=`erc20-transfer-auth-deploy $gas_price_arg -y $DEV_ETH_KEYSTORE_FILE -i $CIC_CHAIN_SPEC -p $ETH_PROVIDER -w -vv`
eth-contract-registry-set $gas_price_arg -w -y $DEV_ETH_KEYSTORE_FILE -r $CIC_REGISTRY_ADDRESS -i $CIC_CHAIN_SPEC -p $ETH_PROVIDER -vv TransferAuthorization $DEV_TRANSFER_AUTHORIZATION_ADDRESS

View File

@@ -136,7 +136,7 @@ First, make a note of the **block height** before running anything:
To import, run to _completion_:
`python eth/import_users.py -v -c config -p <eth_provider> -r <cic_registry_address> -y ../keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c <datadir>`
`python eth/import_users.py -v -c config -p <eth_provider> -r <cic_registry_address> -y ../contract-migration/keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c <datadir>`
After the script completes, keystore files for all generated accouts will be found in `<datadir>/keystore`, all with `foo` as password (would set it empty, but believe it or not some interfaces out there won't work unless you have one).
@@ -150,7 +150,7 @@ Then run:
Run in sequence, in first terminal:
`python cic_eth/import_balance.py -v -c config -p <eth_provider> -r <cic_registry_address> --token-symbol <token_symbol> -y ../keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c --head out`
`python cic_eth/import_balance.py -v -c config -p <eth_provider> -r <cic_registry_address> --token-symbol <token_symbol> -y ../contract-migration/keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c --head out`
In another terminal:
@@ -226,7 +226,7 @@ The connection parameters for the `cic-ussd-server` is currently _hardcoded_ in
### Step 5 - Verify
`python verify.py -v -c config -r <cic_registry_address> -p <eth_provider> <datadir>`
`python verify.py -v -c config -r <cic_registry_address> -p <eth_provider> --token-symbol <token_symbol> <datadir>`
Included checks:
* Private key is in cic-eth keystore
@@ -262,3 +262,5 @@ Should exit with code 0 if all input data is found in the respective services.
- MacOS BigSur issue when installing psycopg2: ld: library not found for -lssl -> https://github.com/psycopg/psycopg2/issues/1115#issuecomment-831498953
- `cic_ussd` imports is poorly implemented, and consumes a lot of resources. Therefore it takes a long time to complete. Reducing the amount of polls for the phone pointer would go a long way to improve it.
- A strict constraint is maintained insistin the use of postgresql-12.

View File

@@ -76,6 +76,7 @@ args_override = {
'CIC_CHAIN_SPEC': getattr(args, 'i'),
'ETH_PROVIDER': getattr(args, 'p'),
'CIC_REGISTRY_ADDRESS': getattr(args, 'r'),
'KEYSTORE_FILE_PATH': getattr(args, 'key-file')
}
config.dict_override(args_override, 'cli flag')
config.censor('PASSWORD', 'DATABASE')

View File

@@ -194,8 +194,7 @@ if __name__ == '__main__':
f.write(json.dumps(o))
f.close()
#fi.write('{},{}\n'.format(new_address, old_address))
meta_key = generate_metadata_pointer(bytes.fromhex(new_address_clean), 'cic.person')
meta_key = generate_metadata_pointer(bytes.fromhex(new_address_clean), ':cic.person')
meta_filepath = os.path.join(meta_dir, '{}.json'.format(new_address_clean.upper()))
os.symlink(os.path.realpath(filepath), meta_filepath)
@@ -221,7 +220,7 @@ if __name__ == '__main__':
# custom data
custom_key = generate_metadata_pointer(phone.encode('utf-8'), ':cic.custom')
custom_key = generate_metadata_pointer(bytes.fromhex(new_address_clean), ':cic.custom')
custom_filepath = os.path.join(custom_dir, 'meta', custom_key)
filepath = os.path.join(

View File

@@ -31,13 +31,16 @@ function sendit(uid, envelope) {
const req = http.request(url + uid, opts, (res) => {
res.on('data', process.stdout.write);
res.on('end', () => {
if (!res.complete) {
console.log('The connection was terminated while the message was being sent.')
}
console.log('result', res.statusCode, res.headers);
});
});
if (!req.write(d)) {
console.error('foo', d);
process.exit(1);
}
req.on('error', (err) => {
console.log('ERROR when talking to meta', err)
})
req.write(d)
req.end();
}
@@ -55,6 +58,7 @@ function doOne(keystore, filePath) {
const s = new crdt.Syncable(uid, o);
s.setSigner(signer);
s.onwrap = (env) => {
console.log(`Sending uid: ${uid} and env: ${env} to meta`)
sendit(uid, env);
};
s.sign();
@@ -84,6 +88,7 @@ let batchCount = 0;
function importMeta(keystore) {
console.log('Running importMeta....')
let err;
let files;
@@ -94,6 +99,11 @@ function importMeta(keystore) {
setTimeout(importMeta, batchDelay, keystore);
return;
}
console.log(`Trying to read ${files.length} files`)
if (files === 0) {
console.log(`ERROR did not find any files under ${workDir}. \nLooks like there is no work for me, bailing!`)
process.exit(1)
}
let limit = batchSize;
if (files.length < limit) {
limit = files.length;
@@ -108,6 +118,7 @@ function importMeta(keystore) {
doOne(keystore, filePath);
count++;
batchCount++;
//console.log('done one', count, batchCount)
if (batchCount == batchSize) {
console.debug('reached batch size, breathing');
batchCount=0;

View File

@@ -0,0 +1,133 @@
const fs = require('fs');
const path = require('path');
const http = require('http');
const cic = require('@cicnet/cic-client-meta');
const crdt = require('@cicnet/crdt-meta');
//const conf = JSON.parse(fs.readFileSync('./cic.conf'));
const config = new crdt.Config('./config');
config.process();
console.log(config);
function sendit(uid, envelope) {
const d = envelope.toJSON();
const contentLength = (new TextEncoder().encode(d)).length;
const opts = {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Content-Length': contentLength,
'X-CIC-AUTOMERGE': 'client',
},
};
let url = config.get('META_URL');
url = url.replace(new RegExp('^(.+://[^/]+)/*$'), '$1/');
console.log('posting to url: ' + url + uid);
const req = http.request(url + uid, opts, (res) => {
res.on('data', process.stdout.write);
res.on('end', () => {
console.log('result', res.statusCode, res.headers);
});
});
if (!req.write(d)) {
console.error('foo', d);
process.exit(1);
}
req.end();
}
function doOne(keystore, filePath, identifier) {
const signer = new crdt.PGPSigner(keystore);
const o = JSON.parse(fs.readFileSync(filePath).toString());
cic.Custom.toKey(identifier).then((uid) => {
const s = new crdt.Syncable(uid, o);
s.setSigner(signer);
s.onwrap = (env) => {
sendit(identifier, env);
};
s.sign();
});
}
const privateKeyPath = path.join(config.get('PGP_EXPORTS_DIR'), config.get('PGP_PRIVATE_KEY_FILE'));
const publicKeyPath = path.join(config.get('PGP_EXPORTS_DIR'), config.get('PGP_PRIVATE_KEY_FILE'));
pk = fs.readFileSync(privateKeyPath);
pubk = fs.readFileSync(publicKeyPath);
new crdt.PGPKeyStore(
config.get('PGP_PASSPHRASE'),
pk,
pubk,
undefined,
undefined,
importMetaCustom,
);
const batchSize = 16;
const batchDelay = 1000;
const total = parseInt(process.argv[3]);
const dataDir = process.argv[2];
const workDir = path.join(dataDir, 'preferences/meta');
const userDir = path.join(dataDir, 'preferences/new');
let count = 0;
let batchCount = 0;
function importMetaCustom(keystore) {
let err;
let files;
try {
err, files = fs.readdirSync(workDir);
} catch {
console.error('source directory not yet ready', workDir);
setTimeout(importMetaCustom, batchDelay, keystore);
return;
}
let limit = batchSize;
if (files.length < limit) {
limit = files.length;
}
for (let i = 0; i < limit; i++) {
const file = files[i];
if (file.length < 3) {
console.debug('skipping file', file);
continue;
}
//const identifier = file.substr(0,file.length-5);
const identifier = file;
const filePath = path.join(workDir, file);
console.log(filePath);
//const address = fs.readFileSync(filePath).toString().substring(2).toUpperCase();
const custom = JSON.parse(fs.readFileSync(filePath).toString());
const customFilePath = path.join(
userDir,
identifier.substring(0, 2),
identifier.substring(2, 4),
identifier + '.json',
);
doOne(keystore, filePath, identifier);
fs.unlinkSync(filePath);
count++;
batchCount++;
if (batchCount == batchSize) {
console.debug('reached batch size, breathing');
batchCount=0;
setTimeout(importMetaCustom, batchDelay, keystore);
return;
}
}
if (count == total) {
return;
}
setTimeout(importMetaCustom, 100, keystore);
}

View File

@@ -70,6 +70,7 @@ args_override = {
'REDIS_DB': getattr(args, 'redis_db'),
'META_HOST': getattr(args, 'meta_host'),
'META_PORT': getattr(args, 'meta_port'),
'KEYSTORE_FILE_PATH': getattr(args, 'y')
}
config.dict_override(args_override, 'cli flag')
config.censor('PASSWORD', 'DATABASE')
@@ -114,7 +115,7 @@ def main():
conn = EthHTTPConnection(config.get('ETH_PROVIDER'))
ImportTask.balance_processor = BalanceProcessor(conn, chain_spec, config.get('CIC_REGISTRY_ADDRESS'), signer_address, signer)
ImportTask.balance_processor.init()
ImportTask.balance_processor.init(token_symbol)
# TODO get decimals from token
balances = {}
@@ -139,6 +140,7 @@ def main():
ImportTask.balances = balances
ImportTask.count = i
ImportTask.import_dir = user_dir
s = celery.signature(
'import_task.send_txs',

View File

@@ -39,6 +39,7 @@ elif args.vv:
config_dir = args.c
config = confini.Config(config_dir, os.environ.get('CONFINI_ENV_PREFIX'))
config.process()
logg.debug('config loaded from {}:\n{}'.format(args.c, config))
celery_app = celery.Celery(broker=config.get('CELERY_BROKER_URL'), backend=config.get('CELERY_RESULT_URL'))
@@ -62,9 +63,6 @@ def main():
)
s_import_pins.apply_async()
argv = ['worker', '-Q', 'cic-import-ussd', '--loglevel=DEBUG']
celery_app.worker_main(argv)
if __name__ == '__main__':
main()

View File

@@ -1,6 +1,7 @@
# standard imports
import os
import logging
import random
import urllib.parse
import urllib.error
import urllib.request
@@ -136,6 +137,42 @@ def generate_metadata(self, address, phone):
)
os.symlink(os.path.realpath(filepath), meta_filepath)
# write ussd data
ussd_data = {
'phone': phone,
'is_activated': 1,
'preferred_language': random.sample(['en', 'sw'], 1)[0],
'is_disabled': False
}
ussd_data_dir = os.path.join(self.import_dir, 'ussd')
ussd_data_file_path = os.path.join(ussd_data_dir, f'{old_address}.json')
f = open(ussd_data_file_path, 'w')
f.write(json.dumps(ussd_data))
f.close()
# write preferences data
preferences_dir = os.path.join(self.import_dir, 'preferences')
preferences_data = {
'preferred_language': ussd_data['preferred_language']
}
preferences_key = generate_metadata_pointer(bytes.fromhex(new_address_clean[2:]), ':cic.preferences')
preferences_filepath = os.path.join(preferences_dir, 'meta', preferences_key)
filepath = os.path.join(
preferences_dir,
'new',
preferences_key[:2].upper(),
preferences_key[2:4].upper(),
preferences_key.upper() + '.json'
)
os.makedirs(os.path.dirname(filepath), exist_ok=True)
f = open(filepath, 'w')
f.write(json.dumps(preferences_data))
f.close()
os.symlink(os.path.realpath(filepath), preferences_filepath)
logg.debug('found metadata {} for phone {}'.format(o, phone))
return address

View File

@@ -1,29 +1,21 @@
# standard imports
import os
import sys
import argparse
import json
import logging
import argparse
import uuid
import datetime
import os
import sys
import time
import urllib.request
from glob import glob
import uuid
from urllib.parse import urlencode
# third-party imports
import redis
import confini
# external imports
import celery
from hexathon import (
add_0x,
strip_0x,
)
from chainlib.eth.address import to_checksum
from cic_types.models.person import Person
from cic_eth.api.api_task import Api
from chainlib.chain import ChainSpec
from cic_types.processor import generate_metadata_pointer
import confini
import phonenumbers
import redis
from chainlib.chain import ChainSpec
from cic_types.models.person import Person
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
@@ -39,6 +31,9 @@ argparser.add_argument('--redis-db', dest='redis_db', type=int, help='redis db t
argparser.add_argument('--batch-size', dest='batch_size', default=100, type=int, help='burst size of sending transactions to node') # batch size should be slightly below cumulative gas limit worth, eg 80000 gas txs with 8000000 limit is a bit less than 100 batch size
argparser.add_argument('--batch-delay', dest='batch_delay', default=3, type=int, help='seconds delay between batches')
argparser.add_argument('--timeout', default=60.0, type=float, help='Callback timeout')
argparser.add_argument('--ussd-host', dest='ussd_host', type=str, help="host to ussd app responsible for processing ussd requests.")
argparser.add_argument('--ussd-port', dest='ussd_port', type=str, help="port to ussd app responsible for processing ussd requests.")
argparser.add_argument('--ussd-no-ssl', dest='ussd_no_ssl', help='do not use ssl (careful)', action='store_true')
argparser.add_argument('-q', type=str, default='cic-eth', help='Task queue')
argparser.add_argument('-v', action='store_true', help='Be verbose')
argparser.add_argument('-vv', action='store_true', help='Be more verbose')
@@ -72,6 +67,12 @@ ps = r.pubsub()
user_new_dir = os.path.join(args.user_dir, 'new')
os.makedirs(user_new_dir)
ussd_data_dir = os.path.join(args.user_dir, 'ussd')
os.makedirs(ussd_data_dir)
preferences_dir = os.path.join(args.user_dir, 'preferences')
os.makedirs(os.path.join(preferences_dir, 'meta'))
meta_dir = os.path.join(args.user_dir, 'meta')
os.makedirs(meta_dir)
@@ -86,22 +87,22 @@ chain_str = str(chain_spec)
batch_size = args.batch_size
batch_delay = args.batch_delay
db_configs = {
'database': config.get('DATABASE_NAME'),
'host': config.get('DATABASE_HOST'),
'port': config.get('DATABASE_PORT'),
'user': config.get('DATABASE_USER'),
'password': config.get('DATABASE_PASSWORD')
}
ussd_port = args.ussd_port
ussd_host = args.ussd_host
ussd_no_ssl = args.ussd_no_ssl
if ussd_no_ssl is True:
ussd_ssl = False
else:
ussd_ssl = True
def build_ussd_request(phone, host, port, service_code, username, password, ssl=False):
url = 'http'
if ssl:
url += 's'
url += '://{}:{}'.format(host, port)
url += '/?username={}&password={}'.format(username, password) #config.get('USSD_USER'), config.get('USSD_PASS'))
url += '://{}'.format(host)
if port:
url += ':{}'.format(port)
url += '/?username={}&password={}'.format(username, password)
logg.info('ussd service url {}'.format(url))
logg.info('ussd phone {}'.format(phone))
@@ -114,9 +115,10 @@ def build_ussd_request(phone, host, port, service_code, username, password, ssl=
'text': service_code,
}
req = urllib.request.Request(url)
data_str = json.dumps(data)
req.method=('POST')
data_str = urlencode(data)
data_bytes = data_str.encode('utf-8')
req.add_header('Content-Type', 'application/json')
req.add_header('Content-Type', 'application/x-www-form-urlencoded')
req.data = data_bytes
return req
@@ -126,7 +128,15 @@ def register_ussd(i, u):
phone_object = phonenumbers.parse(u.tel)
phone = phonenumbers.format_number(phone_object, phonenumbers.PhoneNumberFormat.E164)
logg.debug('tel {} {}'.format(u.tel, phone))
req = build_ussd_request(phone, 'localhost', 63315, '*483*46#', '', '')
req = build_ussd_request(
phone,
ussd_host,
ussd_port,
config.get('APP_SERVICE_CODE'),
'',
'',
ussd_ssl
)
response = urllib.request.urlopen(req)
response_data = response.read().decode('utf-8')
state = response_data[:3]
@@ -143,59 +153,57 @@ if __name__ == '__main__':
if y[len(y)-5:] != '.json':
continue
# handle json containing person object
filepath = None
if y[:15] != '_ussd_data.json':
filepath = os.path.join(x[0], y)
f = open(filepath, 'r')
try:
o = json.load(f)
except json.decoder.JSONDecodeError as e:
f.close()
logg.error('load error for {}: {}'.format(y, e))
continue
filepath = os.path.join(x[0], y)
f = open(filepath, 'r')
try:
o = json.load(f)
except json.decoder.JSONDecodeError as e:
f.close()
u = Person.deserialize(o)
logg.error('load error for {}: {}'.format(y, e))
continue
f.close()
u = Person.deserialize(o)
new_address = register_ussd(i, u)
new_address = register_ussd(i, u)
phone_object = phonenumbers.parse(u.tel)
phone = phonenumbers.format_number(phone_object, phonenumbers.PhoneNumberFormat.E164)
phone_object = phonenumbers.parse(u.tel)
phone = phonenumbers.format_number(phone_object, phonenumbers.PhoneNumberFormat.E164)
s_phone = celery.signature(
'import_task.resolve_phone',
[
phone,
],
queue='cic-import-ussd',
)
s_phone = celery.signature(
'import_task.resolve_phone',
[
phone,
],
queue='cic-import-ussd',
)
s_meta = celery.signature(
'import_task.generate_metadata',
[
phone,
],
queue='cic-import-ussd',
)
s_meta = celery.signature(
'import_task.generate_metadata',
[
phone,
],
queue='cic-import-ussd',
)
s_balance = celery.signature(
'import_task.opening_balance_tx',
[
phone,
i,
],
queue='cic-import-ussd',
)
s_balance = celery.signature(
'import_task.opening_balance_tx',
[
phone,
i,
],
queue='cic-import-ussd',
)
s_meta.link(s_balance)
s_phone.link(s_meta)
# block time plus a bit of time for ussd processing
s_phone.apply_async(countdown=7)
s_meta.link(s_balance)
s_phone.link(s_meta)
# block time plus a bit of time for ussd processing
s_phone.apply_async(countdown=7)
i += 1
sys.stdout.write('imported {} {}'.format(i, u).ljust(200) + "\r")
i += 1
sys.stdout.write('imported {} {}'.format(i, u).ljust(200) + "\r")
j += 1
if j == batch_size:
time.sleep(batch_delay)
j = 0
j += 1
if j == batch_size:
time.sleep(batch_delay)
j = 0

View File

@@ -31,9 +31,9 @@ elif args.vv:
config_dir = args.c
config = Config(config_dir, os.environ.get('CONFINI_ENV_PREFIX'))
config.process()
logg.debug('config loaded from {}:\n{}'.format(args.c, config))
user_old_dir = os.path.join(args.user_dir, 'old')
os.stat(user_old_dir)
ussd_data_dir = os.path.join(args.user_dir, 'ussd')
db_configs = {
'database': config.get('DATABASE_NAME'),
@@ -45,18 +45,15 @@ db_configs = {
celery_app = celery.Celery(broker=config.get('CELERY_BROKER_URL'), backend=config.get('CELERY_RESULT_URL'))
if __name__ == '__main__':
for x in os.walk(user_old_dir):
for x in os.walk(ussd_data_dir):
for y in x[2]:
if y[len(y) - 5:] != '.json':
continue
# handle ussd_data json object
if y[:15] == '_ussd_data.json':
if y[len(y) - 5:] == '.json':
filepath = os.path.join(x[0], y)
f = open(filepath, 'r')
try:
ussd_data = json.load(f)
logg.debug(f'LOADING USSD DATA: {ussd_data}')
except json.decoder.JSONDecodeError as e:
f.close()
logg.error('load error for {}: {}'.format(y, e))

View File

@@ -6,7 +6,7 @@ from eth_contract_registry import Registry
from eth_token_index import TokenUniqueSymbolIndex
from chainlib.eth.gas import OverrideGasOracle
from chainlib.eth.nonce import OverrideNonceOracle
from chainlib.eth.erc20 import ERC20
from eth_erc20 import ERC20
from chainlib.eth.tx import (
count,
TxFormat,
@@ -37,7 +37,7 @@ class BalanceProcessor:
self.value_multiplier = 1
def init(self):
def init(self, token_symbol):
# Get Token registry address
registry = Registry(self.chain_spec)
o = registry.address_of(self.registry_address, 'TokenRegistry')
@@ -46,10 +46,10 @@ class BalanceProcessor:
logg.info('found token index address {}'.format(self.token_index_address))
token_registry = TokenUniqueSymbolIndex(self.chain_spec)
o = token_registry.address_of(self.token_index_address, 'SRF')
o = token_registry.address_of(self.token_index_address, token_symbol)
r = self.conn.do(o)
self.token_address = token_registry.parse_address_of(r)
logg.info('found SRF token address {}'.format(self.token_address))
logg.info('found {} token address {}'.format(token_symbol, self.token_address))
tx_factory = ERC20(self.chain_spec)
o = tx_factory.decimals(self.token_address)

View File

@@ -22,3 +22,6 @@ TRANSITIONS=/usr/src/cic-ussd/transitions/
host =
port =
ssl =
[keystore]
file_path = keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c

View File

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

View File

@@ -1,5 +1,10 @@
[database]
name = sempo
host = localhost
port = 5432
user = postgres
NAME=sempo
USER=postgres
PASSWORD=
HOST=localhost
PORT=5432
ENGINE=postgresql
DRIVER=psycopg2
DEBUG=0
POOL_SIZE=1

View File

@@ -3,6 +3,7 @@ import argparse
import json
import logging
import os
import uuid
# third-party imports
import bcrypt
@@ -83,7 +84,7 @@ if __name__ == '__main__':
phone_object = phonenumbers.parse(u.tel)
phone = phonenumbers.format_number(phone_object, phonenumbers.PhoneNumberFormat.E164)
password_hash = generate_password_hash()
password_hash = uuid.uuid4().hex
pins_file.write(f'{phone},{password_hash}\n')
logg.info(f'Writing phone: {phone}, password_hash: {password_hash}')

View File

@@ -228,7 +228,6 @@ def prepareLocalFilePath(datadir, address):
if __name__ == '__main__':
base_dir = os.path.join(user_dir, 'old')
ussd_dir = os.path.join(user_dir, 'ussd')
os.makedirs(base_dir, exist_ok=True)
fa = open(os.path.join(user_dir, 'balances.csv'), 'w')
@@ -248,23 +247,11 @@ if __name__ == '__main__':
print(o)
ussd_data = {
'phone': phone,
'is_activated': 1,
'preferred_language': random.sample(['en', 'sw'], 1)[0],
'is_disabled': False
}
d = prepareLocalFilePath(base_dir, uid)
f = open('{}/{}'.format(d, uid + '.json'), 'w')
json.dump(o.serialize(), f)
f.close()
d = prepareLocalFilePath(ussd_dir, uid)
x = open('{}/{}'.format(d, uid + '_ussd_data.json'), 'w')
json.dump(ussd_data, x)
x.close()
pidx = genPhoneIndex(phone)
d = prepareLocalFilePath(os.path.join(user_dir, 'phone'), pidx)
f = open('{}/{}'.format(d, pidx), 'w')

View File

@@ -1,43 +1,19 @@
# syntax = docker/dockerfile:1.2
FROM python:3.8.6-slim-buster as compile-image
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
WORKDIR /root
RUN mkdir -vp /usr/local/etc/cic
COPY data-seeding/requirements.txt .
COPY data-seeding/ .
ARG EXTRA_INDEX_URL="https://pip.grassrootseconomics.net:8433"
RUN pip install --extra-index-url $EXTRA_INDEX_URL -r requirements.txt
# -------------- begin runtime container ----------------
FROM python:3.8.6-slim-buster as runtime-image
RUN apt-get update
RUN apt-get install -y --no-install-recommends gnupg libpq-dev
RUN apt-get install -y jq bash iputils-ping socat telnet dnsutils
COPY --from=compile-image /usr/local/bin/ /usr/local/bin/
COPY --from=compile-image /usr/local/etc/cic/ /usr/local/etc/cic/
COPY --from=compile-image /usr/local/lib/python3.8/site-packages/ \
/usr/local/lib/python3.8/site-packages/
WORKDIR root/
ENV EXTRA_INDEX_URL https://pip.grassrootseconomics.net:8433
# RUN useradd -u 1001 --create-home grassroots
# RUN adduser grassroots sudo && \
# echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
# WORKDIR /home/grassroots
COPY data-seeding/ .
# we copied these from the root build container.
# this is dumb though...I guess the compile image should have the same user
# RUN chown grassroots:grassroots -R /usr/local/lib/python3.8/site-packages/
# USER grassroots
ENTRYPOINT [ ]

View File

@@ -75,6 +75,7 @@ args_override = {
'CIC_CHAIN_SPEC': getattr(args, 'i'),
'ETH_PROVIDER': getattr(args, 'p'),
'CIC_REGISTRY_ADDRESS': getattr(args, 'r'),
'KEYSTORE_FILE_PATH': getattr(args, 'key-file')
}
config.dict_override(args_override, 'cli flag')
config.censor('PASSWORD', 'DATABASE')

View File

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

View File

@@ -0,0 +1 @@
{"address":"eb3907ecad74a0013c259d5874ae7f22dcbcc95c","crypto":{"cipher":"aes-128-ctr","ciphertext":"b0f70a8af4071faff2267374e2423cbc7a71012096fd2215866d8de7445cc215","cipherparams":{"iv":"9ac89383a7793226446dcb7e1b45cdf3"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"299f7b5df1d08a0a7b7f9c9eb44fe4798683b78da3513fcf9603fd913ab3336f"},"mac":"6f4ed36c11345a9a48353cd2f93f1f92958c96df15f3112a192bc994250e8d03"},"id":"61a9dd88-24a9-495c-9a51-152bd1bfaa5b","version":3}

View File

@@ -1,5 +1,5 @@
{
"name": "scripts",
"name": "data-seeding",
"lockfileVersion": 2,
"requires": true,
"packages": {
@@ -49,20 +49,20 @@
}
},
"node_modules/@ethereumjs/common": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-2.2.0.tgz",
"integrity": "sha512-PyQiTG00MJtBRkJmv46ChZL8u2XWxNBeAthznAUIUiefxPAXjbkuiCZOuncgJS34/XkMbNc9zMt/PlgKRBElig==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-2.3.0.tgz",
"integrity": "sha512-Fmi15MdVptsC85n6NcUXIFiiXCXWEfZNgPWP+OGAQOC6ZtdzoNawtxH/cYpIgEgSuIzfOeX3VKQP/qVI1wISHg==",
"dependencies": {
"crc-32": "^1.2.0",
"ethereumjs-util": "^7.0.9"
"ethereumjs-util": "^7.0.10"
}
},
"node_modules/@ethereumjs/tx": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-3.1.3.tgz",
"integrity": "sha512-DJBu6cbwYtiPTFeCUR8DF5p+PF0jxs+0rALJZiEcTz2tiRPIEkM72GEbrkGuqzENLCzBrJHT43O0DxSYTqeo+g==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-3.2.0.tgz",
"integrity": "sha512-D3X/XtZ3ldUg34hr99Jvj7NxW3NxVKdUKrwQnEWlAp4CmCQpvYoyn7NF4lk34rHEt7ScS+Agu01pcDHoOcd19A==",
"dependencies": {
"@ethereumjs/common": "^2.2.0",
"@ethereumjs/common": "^2.3.0",
"ethereumjs-util": "^7.0.10"
}
},
@@ -75,9 +75,9 @@
}
},
"node_modules/@types/node": {
"version": "14.14.41",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.41.tgz",
"integrity": "sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g=="
"version": "15.12.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.0.tgz",
"integrity": "sha512-+aHJvoCsVhO2ZCuT4o5JtcPrCPyDE3+1nvbDprYes+pPkEsbjH7AGUCNtjMOXS0fqH14t+B7yLzaqSz92FPWyw=="
},
"node_modules/@types/pbkdf2": {
"version": "3.1.0",
@@ -115,6 +115,10 @@
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi-regex": {
@@ -134,6 +138,9 @@
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/aproba": {
@@ -803,9 +810,9 @@
}
},
"node_modules/glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -816,6 +823,9 @@
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/graceful-fs": {
@@ -836,6 +846,7 @@
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
"deprecated": "this library is no longer supported",
"optional": true,
"dependencies": {
"ajv": "^6.12.3",
@@ -909,9 +920,9 @@
}
},
"node_modules/ignore-walk": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz",
"integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==",
"dependencies": {
"minimatch": "^3.0.4"
}
@@ -1037,6 +1048,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz",
"integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==",
"hasInstallScript": true,
"dependencies": {
"node-addon-api": "^2.0.0",
"node-gyp-build": "^4.2.0"
@@ -1056,21 +1068,21 @@
}
},
"node_modules/mime-db": {
"version": "1.47.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz",
"integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==",
"version": "1.48.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz",
"integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==",
"optional": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.30",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz",
"integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==",
"version": "2.1.31",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz",
"integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==",
"optional": true,
"dependencies": {
"mime-db": "1.47.0"
"mime-db": "1.48.0"
},
"engines": {
"node": ">= 0.6"
@@ -1215,6 +1227,7 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz",
"integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==",
"deprecated": "Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future",
"dependencies": {
"detect-libc": "^1.0.2",
"mkdirp": "^0.5.1",
@@ -1273,9 +1286,9 @@
}
},
"node_modules/npm-bundled": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
"integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz",
"integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==",
"dependencies": {
"npm-normalize-package-bin": "^1.0.1"
}
@@ -1426,6 +1439,14 @@
},
"engines": {
"node": ">= 8.0.0"
},
"peerDependencies": {
"pg-native": ">=2.0.0"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-connection-string": {
@@ -1444,7 +1465,10 @@
"node_modules/pg-pool": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.3.0.tgz",
"integrity": "sha512-0O5huCql8/D6PIRFAlmccjphLYWC+JIzvUhSzXSpGaf+tjTZc4nn+Lr7mLXBbFJfvwbP0ywDv73EiaBsxn7zdg=="
"integrity": "sha512-0O5huCql8/D6PIRFAlmccjphLYWC+JIzvUhSzXSpGaf+tjTZc4nn+Lr7mLXBbFJfvwbP0ywDv73EiaBsxn7zdg==",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.5.0",
@@ -1596,6 +1620,7 @@
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
"deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142",
"optional": true,
"dependencies": {
"aws-sign2": "~0.7.0",
@@ -1670,7 +1695,21 @@
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/safer-buffer": {
"version": "2.1.2",
@@ -1691,6 +1730,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.2.tgz",
"integrity": "sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg==",
"hasInstallScript": true,
"dependencies": {
"elliptic": "^6.5.2",
"node-addon-api": "^2.0.0",
@@ -1755,18 +1795,27 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.2.tgz",
"integrity": "sha512-1SdTNo+BVU211Xj1csWa8lV6KM0CtucDwRyA0VHl91wEH1Mgh7RxUpI4rVvG7OhHrzCSGaVyW5g8vKvlrk9DJA==",
"hasInstallScript": true,
"dependencies": {
"node-addon-api": "^3.0.0",
"node-pre-gyp": "^0.11.0"
},
"optionalDependencies": {
"node-gyp": "3.x"
},
"peerDependencies": {
"node-gyp": "3.x"
},
"peerDependenciesMeta": {
"node-gyp": {
"optional": true
}
}
},
"node_modules/sqlite3/node_modules/node-addon-api": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz",
"integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw=="
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
},
"node_modules/sshpk": {
"version": "1.16.1",
@@ -1784,11 +1833,6 @@
"safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0"
},
"bin": {
"sshpk-conv": "bin/sshpk-conv",
"sshpk-sign": "bin/sshpk-sign",
"sshpk-verify": "bin/sshpk-verify"
},
"engines": {
"node": ">=0.10.0"
}
@@ -1872,12 +1916,16 @@
"node_modules/transit-immutable-js": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/transit-immutable-js/-/transit-immutable-js-0.7.0.tgz",
"integrity": "sha1-mT4lCJtjEf9AIUD1VidtbSUwBdk="
"integrity": "sha1-mT4lCJtjEf9AIUD1VidtbSUwBdk=",
"peerDependencies": {
"immutable": ">= 3",
"transit-js": ">= 0.8"
}
},
"node_modules/transit-js": {
"version": "0.8.867",
"resolved": "https://registry.npmjs.org/transit-js/-/transit-js-0.8.867.tgz",
"integrity": "sha512-rOwB4K0z/WZ+E2bV42iN9UV3mvGzmwSv/IpMOKdnFpawPAZT0d1L7f91Y+tZQF7lXSDGk+oln4XyIQXo+pyTGA==",
"version": "0.8.874",
"resolved": "https://registry.npmjs.org/transit-js/-/transit-js-0.8.874.tgz",
"integrity": "sha512-IDJJGKRzUbJHmN0P15HBBa05nbKor3r2MmG6aSt0UxXIlJZZKcddTk67/U7WyAeW9Hv/VYI02IqLzolsC4sbPA==",
"engines": {
"node": ">= 0.10.0"
}
@@ -1923,6 +1971,7 @@
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
}
@@ -1977,6 +2026,9 @@
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/ansi-regex": {
@@ -2151,20 +2203,20 @@
}
},
"@ethereumjs/common": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-2.2.0.tgz",
"integrity": "sha512-PyQiTG00MJtBRkJmv46ChZL8u2XWxNBeAthznAUIUiefxPAXjbkuiCZOuncgJS34/XkMbNc9zMt/PlgKRBElig==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-2.3.0.tgz",
"integrity": "sha512-Fmi15MdVptsC85n6NcUXIFiiXCXWEfZNgPWP+OGAQOC6ZtdzoNawtxH/cYpIgEgSuIzfOeX3VKQP/qVI1wISHg==",
"requires": {
"crc-32": "^1.2.0",
"ethereumjs-util": "^7.0.9"
"ethereumjs-util": "^7.0.10"
}
},
"@ethereumjs/tx": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-3.1.3.tgz",
"integrity": "sha512-DJBu6cbwYtiPTFeCUR8DF5p+PF0jxs+0rALJZiEcTz2tiRPIEkM72GEbrkGuqzENLCzBrJHT43O0DxSYTqeo+g==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-3.2.0.tgz",
"integrity": "sha512-D3X/XtZ3ldUg34hr99Jvj7NxW3NxVKdUKrwQnEWlAp4CmCQpvYoyn7NF4lk34rHEt7ScS+Agu01pcDHoOcd19A==",
"requires": {
"@ethereumjs/common": "^2.2.0",
"@ethereumjs/common": "^2.3.0",
"ethereumjs-util": "^7.0.10"
}
},
@@ -2177,9 +2229,9 @@
}
},
"@types/node": {
"version": "14.14.41",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.41.tgz",
"integrity": "sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g=="
"version": "15.12.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.0.tgz",
"integrity": "sha512-+aHJvoCsVhO2ZCuT4o5JtcPrCPyDE3+1nvbDprYes+pPkEsbjH7AGUCNtjMOXS0fqH14t+B7yLzaqSz92FPWyw=="
},
"@types/pbkdf2": {
"version": "3.1.0",
@@ -2822,9 +2874,9 @@
}
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -2909,9 +2961,9 @@
}
},
"ignore-walk": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz",
"integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==",
"requires": {
"minimatch": "^3.0.4"
}
@@ -3037,18 +3089,18 @@
}
},
"mime-db": {
"version": "1.47.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz",
"integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==",
"version": "1.48.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz",
"integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==",
"optional": true
},
"mime-types": {
"version": "2.1.30",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz",
"integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==",
"version": "2.1.31",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz",
"integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==",
"optional": true,
"requires": {
"mime-db": "1.47.0"
"mime-db": "1.48.0"
}
},
"minimalistic-assert": {
@@ -3209,9 +3261,9 @@
}
},
"npm-bundled": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
"integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz",
"integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==",
"requires": {
"npm-normalize-package-bin": "^1.0.1"
}
@@ -3350,7 +3402,8 @@
"pg-pool": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.3.0.tgz",
"integrity": "sha512-0O5huCql8/D6PIRFAlmccjphLYWC+JIzvUhSzXSpGaf+tjTZc4nn+Lr7mLXBbFJfvwbP0ywDv73EiaBsxn7zdg=="
"integrity": "sha512-0O5huCql8/D6PIRFAlmccjphLYWC+JIzvUhSzXSpGaf+tjTZc4nn+Lr7mLXBbFJfvwbP0ywDv73EiaBsxn7zdg==",
"requires": {}
},
"pg-protocol": {
"version": "1.5.0",
@@ -3610,9 +3663,9 @@
},
"dependencies": {
"node-addon-api": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz",
"integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw=="
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
}
}
},
@@ -3696,12 +3749,13 @@
"transit-immutable-js": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/transit-immutable-js/-/transit-immutable-js-0.7.0.tgz",
"integrity": "sha1-mT4lCJtjEf9AIUD1VidtbSUwBdk="
"integrity": "sha1-mT4lCJtjEf9AIUD1VidtbSUwBdk=",
"requires": {}
},
"transit-js": {
"version": "0.8.867",
"resolved": "https://registry.npmjs.org/transit-js/-/transit-js-0.8.867.tgz",
"integrity": "sha512-rOwB4K0z/WZ+E2bV42iN9UV3mvGzmwSv/IpMOKdnFpawPAZT0d1L7f91Y+tZQF7lXSDGk+oln4XyIQXo+pyTGA=="
"version": "0.8.874",
"resolved": "https://registry.npmjs.org/transit-js/-/transit-js-0.8.874.tgz",
"integrity": "sha512-IDJJGKRzUbJHmN0P15HBBa05nbKor3r2MmG6aSt0UxXIlJZZKcddTk67/U7WyAeW9Hv/VYI02IqLzolsC4sbPA=="
},
"tunnel-agent": {
"version": "0.6.0",

View File

@@ -9,6 +9,7 @@ import sys
import urllib
import urllib.request
import uuid
import urllib.parse
# external imports
import celery
@@ -72,7 +73,7 @@ argparser.add_argument('--ussd-provider', type=str, dest='ussd_provider', defaul
argparser.add_argument('--skip-custodial', dest='skip_custodial', action='store_true', help='skip all custodial verifications')
argparser.add_argument('--exclude', action='append', type=str, default=[], help='skip specified verification')
argparser.add_argument('--include', action='append', type=str, help='include specified verification')
argparser.add_argument('--token-symbol', default='SRF', type=str, dest='token_symbol', help='Token symbol to use for trnsactions')
argparser.add_argument('--token-symbol', default='GFT', type=str, dest='token_symbol', help='Token symbol to use for trnsactions')
argparser.add_argument('-r', '--registry-address', type=str, dest='r', help='CIC Registry address')
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('-x', '--exit-on-error', dest='x', action='store_true', help='Halt exection on error')
@@ -185,9 +186,9 @@ def send_ussd_request(address, data_dir):
}
req = urllib.request.Request(config.get('_USSD_PROVIDER'))
data_str = json.dumps(data)
data_bytes = data_str.encode('utf-8')
req.add_header('Content-Type', 'application/json')
urlencoded_data = urllib.parse.urlencode(data)
data_bytes = urlencoded_data.encode('utf-8')
req.add_header('Content-Type', 'application/x-www-form-urlencoded')
req.data = data_bytes
response = urllib.request.urlopen(req)
return response.read().decode('utf-8')
@@ -388,10 +389,9 @@ class Verifier:
def verify_ussd_pins(self, address, balance):
response_data = send_ussd_request(address, self.data_dir)
if response_data[:11] != 'CON Balance':
if response_data[:11] != 'CON Balance' and response_data[:9] != 'CON Salio':
raise VerifierError(response_data, 'pins')
def verify(self, address, balance, debug_stem=None):
for k in active_tests:

View File

@@ -484,7 +484,7 @@ services:
restart_policy:
condition: on-failure
volumes:
- ${LOCAL_VOLUME_DIR:-/tmp/cic}/pgp:/tmp/cic/pgp
- ./apps/contract-migration/testdata/pgp/:/tmp/cic/pgp
# command: "/root/start_server.sh -vv"
cic-user-ussd-server: