Compare commits
3 Commits
lash/cic-c
...
lash/sessi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26db8b3baf | ||
|
|
4ff3b13251
|
||
|
|
7c33c120e9
|
@@ -29,8 +29,8 @@ def upgrade():
|
|||||||
sa.Column('source_token', sa.String(42), nullable=False),
|
sa.Column('source_token', sa.String(42), nullable=False),
|
||||||
sa.Column('destination_token', sa.String(42), nullable=False),
|
sa.Column('destination_token', sa.String(42), nullable=False),
|
||||||
sa.Column('success', sa.Boolean, nullable=False),
|
sa.Column('success', sa.Boolean, nullable=False),
|
||||||
sa.Column('from_value', sa.NUMERIC(), nullable=False),
|
sa.Column('from_value', sa.BIGINT(), nullable=False),
|
||||||
sa.Column('to_value', sa.NUMERIC(), nullable=False),
|
sa.Column('to_value', sa.BIGINT(), nullable=False),
|
||||||
sa.Column('date_block', sa.DateTime, nullable=False),
|
sa.Column('date_block', sa.DateTime, nullable=False),
|
||||||
)
|
)
|
||||||
op.create_table(
|
op.create_table(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[database]
|
[database]
|
||||||
NAME=cic_cache
|
NAME=cic-eth
|
||||||
USER=postgres
|
USER=postgres
|
||||||
PASSWORD=
|
PASSWORD=
|
||||||
HOST=localhost
|
HOST=localhost
|
||||||
|
|||||||
@@ -1,25 +1,12 @@
|
|||||||
# standard imports
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
# external imports
|
|
||||||
import celery
|
import celery
|
||||||
|
|
||||||
# local imports
|
|
||||||
from cic_eth.db.models.debug import Debug
|
|
||||||
from cic_eth.db.models.base import SessionBase
|
|
||||||
from cic_eth.task import CriticalSQLAlchemyTask
|
|
||||||
|
|
||||||
celery_app = celery.current_app
|
celery_app = celery.current_app
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task(base=CriticalSQLAlchemyTask)
|
@celery_app.task()
|
||||||
def alert(chained_input, tag, txt):
|
def out_tmp(tag, txt):
|
||||||
session = SessionBase.create_session()
|
f = open('/tmp/err.{}.txt'.format(tag), "w")
|
||||||
|
f.write(txt)
|
||||||
o = Debug(tag, txt)
|
f.close()
|
||||||
session.add(o)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return chained_input
|
|
||||||
|
|||||||
@@ -489,9 +489,8 @@ class Api:
|
|||||||
],
|
],
|
||||||
queue=self.queue,
|
queue=self.queue,
|
||||||
)
|
)
|
||||||
s_local.link(s_brief)
|
|
||||||
if self.callback_param != None:
|
if self.callback_param != None:
|
||||||
s_brief.link(self.callback_success).on_error(self.callback_error)
|
s_assemble.link(self.callback_success).on_error(self.callback_error)
|
||||||
|
|
||||||
t = None
|
t = None
|
||||||
if external_task != None:
|
if external_task != None:
|
||||||
@@ -516,10 +515,11 @@ class Api:
|
|||||||
c = celery.chain(s_external_get, s_external_process)
|
c = celery.chain(s_external_get, s_external_process)
|
||||||
t = celery.chord([s_local, c])(s_brief)
|
t = celery.chord([s_local, c])(s_brief)
|
||||||
else:
|
else:
|
||||||
t = s_local.apply_async(queue=self.queue)
|
t = s_local.apply_sync()
|
||||||
|
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
def ping(self, r):
|
def ping(self, r):
|
||||||
"""A noop callback ping for testing purposes.
|
"""A noop callback ping for testing purposes.
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
"""debug output
|
|
||||||
|
|
||||||
Revision ID: f738d9962fdf
|
|
||||||
Revises: ec40ac0974c1
|
|
||||||
Create Date: 2021-03-04 08:32:43.281214
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'f738d9962fdf'
|
|
||||||
down_revision = 'ec40ac0974c1'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.create_table(
|
|
||||||
'debug',
|
|
||||||
sa.Column('id', sa.Integer, primary_key=True),
|
|
||||||
sa.Column('tag', sa.String, nullable=False),
|
|
||||||
sa.Column('description', sa.String, nullable=False),
|
|
||||||
sa.Column('date_created', sa.DateTime, nullable=False),
|
|
||||||
)
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
op.drop_table('debug')
|
|
||||||
pass
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""debug output
|
|
||||||
|
|
||||||
Revision ID: f738d9962fdf
|
|
||||||
Revises: ec40ac0974c1
|
|
||||||
Create Date: 2021-03-04 08:32:43.281214
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'f738d9962fdf'
|
|
||||||
down_revision = 'ec40ac0974c1'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.create_table(
|
|
||||||
'debug',
|
|
||||||
sa.Column('id', sa.Integer, primary_key=True),
|
|
||||||
sa.Column('tag', sa.String, nullable=False),
|
|
||||||
sa.Column('description', sa.String, nullable=False),
|
|
||||||
sa.Column('date_created', sa.DateTime, nullable=False),
|
|
||||||
)
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
op.drop_table('debug')
|
|
||||||
pass
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
import datetime
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# external imports
|
|
||||||
from sqlalchemy import Column, String, DateTime
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
from .base import SessionBase
|
|
||||||
|
|
||||||
|
|
||||||
class Debug(SessionBase):
|
|
||||||
|
|
||||||
__tablename__ = 'debug'
|
|
||||||
|
|
||||||
date_created = Column(DateTime, default=datetime.datetime.utcnow)
|
|
||||||
tag = Column(String)
|
|
||||||
description = Column(String)
|
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, tag, description):
|
|
||||||
self.tag = tag
|
|
||||||
self.description = description
|
|
||||||
@@ -88,7 +88,7 @@ class Nonce(SessionBase):
|
|||||||
#session.execute('UNLOCK TABLE nonce')
|
#session.execute('UNLOCK TABLE nonce')
|
||||||
#conn.close()
|
#conn.close()
|
||||||
session.commit()
|
session.commit()
|
||||||
# session.commit()
|
session.commit()
|
||||||
|
|
||||||
SessionBase.release_session(session)
|
SessionBase.release_session(session)
|
||||||
return nonce
|
return nonce
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
# external imports
|
# third-party imports
|
||||||
from sqlalchemy import Column, Enum, String, Integer, DateTime, Text, or_, ForeignKey
|
from sqlalchemy import Column, Enum, String, Integer, DateTime, Text, or_, ForeignKey
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
|
from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
|
||||||
|
|
||||||
|
|||||||
194
apps/cic-eth/cic_eth/eth/request.py
Normal file
194
apps/cic-eth/cic_eth/eth/request.py
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
# standard imports
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# third-party imports
|
||||||
|
import web3
|
||||||
|
import celery
|
||||||
|
from erc20_approval_escrow import TransferApproval
|
||||||
|
from cic_registry import CICRegistry
|
||||||
|
from cic_registry.chain import ChainSpec
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from cic_eth.db.models.tx import TxCache
|
||||||
|
from cic_eth.db.models.base import SessionBase
|
||||||
|
from cic_eth.eth import RpcClient
|
||||||
|
from cic_eth.eth.factory import TxFactory
|
||||||
|
from cic_eth.eth.task import sign_and_register_tx
|
||||||
|
from cic_eth.eth.util import unpack_signed_raw_tx
|
||||||
|
from cic_eth.eth.task import create_check_gas_and_send_task
|
||||||
|
from cic_eth.error import TokenCountError
|
||||||
|
|
||||||
|
celery_app = celery.current_app
|
||||||
|
logg = logging.getLogger()
|
||||||
|
|
||||||
|
contract_function_signatures = {
|
||||||
|
'request': 'b0addede',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TransferRequestTxFactory(TxFactory):
|
||||||
|
"""Factory for creating Transfer request transactions using the TransferApproval contract backend
|
||||||
|
"""
|
||||||
|
def request(
|
||||||
|
self,
|
||||||
|
token_address,
|
||||||
|
beneficiary_address,
|
||||||
|
amount,
|
||||||
|
chain_spec,
|
||||||
|
):
|
||||||
|
"""Create a new TransferApproval.request transaction
|
||||||
|
|
||||||
|
:param token_address: Token to create transfer request for
|
||||||
|
:type token_address: str, 0x-hex
|
||||||
|
:param beneficiary_address: Beneficiary of token transfer
|
||||||
|
:type beneficiary_address: str, 0x-hex
|
||||||
|
:param amount: Amount of tokens to transfer
|
||||||
|
:type amount: number
|
||||||
|
:param chain_spec: Chain spec
|
||||||
|
:type chain_spec: cic_registry.chain.ChainSpec
|
||||||
|
:returns: Transaction in standard Ethereum format
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
transfer_approval = CICRegistry.get_contract(chain_spec, 'TransferApproval', 'TransferAuthorization')
|
||||||
|
fn = transfer_approval.function('createRequest')
|
||||||
|
tx_approval_buildable = fn(beneficiary_address, token_address, amount)
|
||||||
|
transfer_approval_gas = transfer_approval.gas('createRequest')
|
||||||
|
|
||||||
|
tx_approval = tx_approval_buildable.buildTransaction({
|
||||||
|
'from': self.address,
|
||||||
|
'gas': transfer_approval_gas,
|
||||||
|
'gasPrice': self.gas_price,
|
||||||
|
'chainId': chain_spec.chain_id(),
|
||||||
|
'nonce': self.next_nonce(),
|
||||||
|
})
|
||||||
|
return tx_approval
|
||||||
|
|
||||||
|
|
||||||
|
def unpack_transfer_approval_request(data):
|
||||||
|
"""Verifies that a transaction is an "TransferApproval.request" transaction, and extracts call parameters from it.
|
||||||
|
|
||||||
|
:param data: Raw input data from Ethereum transaction.
|
||||||
|
:type data: str, 0x-hex
|
||||||
|
:raises ValueError: Function signature does not match AccountRegister.add
|
||||||
|
:returns: Parsed parameters
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
f = data[2:10]
|
||||||
|
if f != contract_function_signatures['request']:
|
||||||
|
raise ValueError('Invalid transfer request data ({})'.format(f))
|
||||||
|
|
||||||
|
d = data[10:]
|
||||||
|
return {
|
||||||
|
'to': web3.Web3.toChecksumAddress('0x' + d[64-40:64]),
|
||||||
|
'token': web3.Web3.toChecksumAddress('0x' + d[128-40:128]),
|
||||||
|
'amount': int(d[128:], 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task(bind=True)
|
||||||
|
def transfer_approval_request(self, tokens, holder_address, receiver_address, value, chain_str):
|
||||||
|
"""Creates a new transfer approval
|
||||||
|
|
||||||
|
:param tokens: Token to generate transfer request for
|
||||||
|
:type tokens: list with single token spec as dict
|
||||||
|
:param holder_address: Address to generate transfer on behalf of
|
||||||
|
:type holder_address: str, 0x-hex
|
||||||
|
:param receiver_address: Address to transfser tokens to
|
||||||
|
:type receiver_address: str, 0x-hex
|
||||||
|
:param value: Amount of tokens to transfer
|
||||||
|
:type value: number
|
||||||
|
:param chain_spec: Chain spec string representation
|
||||||
|
:type chain_spec: str
|
||||||
|
:raises cic_eth.error.TokenCountError: More than one token in tokens argument
|
||||||
|
:returns: Raw signed transaction
|
||||||
|
:rtype: list with transaction as only element
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(tokens) != 1:
|
||||||
|
raise TokenCountError
|
||||||
|
|
||||||
|
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||||
|
|
||||||
|
queue = self.request.delivery_info['routing_key']
|
||||||
|
|
||||||
|
t = tokens[0]
|
||||||
|
|
||||||
|
c = RpcClient(holder_address)
|
||||||
|
|
||||||
|
txf = TransferRequestTxFactory(holder_address, c)
|
||||||
|
|
||||||
|
tx_transfer = txf.request(t['address'], receiver_address, value, chain_spec)
|
||||||
|
(tx_hash_hex, tx_signed_raw_hex) = sign_and_register_tx(tx_transfer, chain_str, queue, 'cic_eth.eth.request.otx_cache_transfer_approval_request')
|
||||||
|
|
||||||
|
gas_budget = tx_transfer['gas'] * tx_transfer['gasPrice']
|
||||||
|
|
||||||
|
s = create_check_gas_and_send_task(
|
||||||
|
[tx_signed_raw_hex],
|
||||||
|
chain_str,
|
||||||
|
holder_address,
|
||||||
|
gas_budget,
|
||||||
|
[tx_hash_hex],
|
||||||
|
queue,
|
||||||
|
)
|
||||||
|
s.apply_async()
|
||||||
|
return [tx_signed_raw_hex]
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task()
|
||||||
|
def otx_cache_transfer_approval_request(
|
||||||
|
tx_hash_hex,
|
||||||
|
tx_signed_raw_hex,
|
||||||
|
chain_str,
|
||||||
|
):
|
||||||
|
"""Generates and commits transaction cache metadata for an TransferApproval.request transaction
|
||||||
|
|
||||||
|
:param tx_hash_hex: Transaction hash
|
||||||
|
:type tx_hash_hex: str, 0x-hex
|
||||||
|
:param tx_signed_raw_hex: Raw signed transaction
|
||||||
|
:type tx_signed_raw_hex: str, 0x-hex
|
||||||
|
:param chain_str: Chain spec string representation
|
||||||
|
:type chain_str: str
|
||||||
|
:returns: Transaction hash and id of cache element in storage backend, respectively
|
||||||
|
:rtype: tuple
|
||||||
|
"""
|
||||||
|
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||||
|
tx_signed_raw_bytes = bytes.fromhex(tx_signed_raw_hex[2:])
|
||||||
|
tx = unpack_signed_raw_tx(tx_signed_raw_bytes, chain_spec.chain_id())
|
||||||
|
logg.debug('in otx acche transfer approval request')
|
||||||
|
(txc, cache_id) = cache_transfer_approval_request_data(tx_hash_hex, tx)
|
||||||
|
return txc
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task()
|
||||||
|
def cache_transfer_approval_request_data(
|
||||||
|
tx_hash_hex,
|
||||||
|
tx,
|
||||||
|
):
|
||||||
|
"""Helper function for otx_cache_transfer_approval_request
|
||||||
|
|
||||||
|
:param tx_hash_hex: Transaction hash
|
||||||
|
:type tx_hash_hex: str, 0x-hex
|
||||||
|
:param tx: Signed raw transaction
|
||||||
|
:type tx: str, 0x-hex
|
||||||
|
:returns: Transaction hash and id of cache element in storage backend, respectively
|
||||||
|
:rtype: tuple
|
||||||
|
"""
|
||||||
|
tx_data = unpack_transfer_approval_request(tx['data'])
|
||||||
|
logg.debug('tx approval request data {}'.format(tx_data))
|
||||||
|
logg.debug('tx approval request {}'.format(tx))
|
||||||
|
|
||||||
|
session = SessionBase.create_session()
|
||||||
|
tx_cache = TxCache(
|
||||||
|
tx_hash_hex,
|
||||||
|
tx['from'],
|
||||||
|
tx_data['to'],
|
||||||
|
tx_data['token'],
|
||||||
|
tx_data['token'],
|
||||||
|
tx_data['amount'],
|
||||||
|
tx_data['amount'],
|
||||||
|
)
|
||||||
|
session.add(tx_cache)
|
||||||
|
session.commit()
|
||||||
|
cache_id = tx_cache.id
|
||||||
|
session.close()
|
||||||
|
return (tx_hash_hex, cache_id)
|
||||||
@@ -69,6 +69,7 @@ def check_gas(self, tx_hashes, chain_str, txs=[], address=None, gas_required=Non
|
|||||||
for i in range(len(tx_hashes)):
|
for i in range(len(tx_hashes)):
|
||||||
o = get_tx(tx_hashes[i])
|
o = get_tx(tx_hashes[i])
|
||||||
txs.append(o['signed_tx'])
|
txs.append(o['signed_tx'])
|
||||||
|
logg.debug('ooooo {}'.format(o))
|
||||||
if address == None:
|
if address == None:
|
||||||
address = o['address']
|
address = o['address']
|
||||||
|
|
||||||
@@ -177,7 +178,12 @@ class ParityNodeHandler:
|
|||||||
def handle(self, exception, tx_hash_hex, tx_hex):
|
def handle(self, exception, tx_hash_hex, tx_hex):
|
||||||
meth = self.handle_default
|
meth = self.handle_default
|
||||||
if isinstance(exception, (ValueError)):
|
if isinstance(exception, (ValueError)):
|
||||||
|
# s_debug = celery.signature(
|
||||||
|
# 'cic_eth.admin.debug.out_tmp',
|
||||||
|
# [tx_hash_hex, '{}: {}'.format(tx_hash_hex, exception)],
|
||||||
|
# queue=queue,
|
||||||
|
# )
|
||||||
|
# s_debug.apply_async()
|
||||||
earg = exception.args[0]
|
earg = exception.args[0]
|
||||||
if earg['code'] == -32010:
|
if earg['code'] == -32010:
|
||||||
logg.debug('skipping lock for code {}'.format(earg['code']))
|
logg.debug('skipping lock for code {}'.format(earg['code']))
|
||||||
@@ -185,15 +191,14 @@ class ParityNodeHandler:
|
|||||||
elif earg['code'] == -32602:
|
elif earg['code'] == -32602:
|
||||||
meth = self.handle_invalid_encoding
|
meth = self.handle_invalid_encoding
|
||||||
else:
|
else:
|
||||||
# TODO: move to status log db comment field
|
|
||||||
meth = self.handle_invalid
|
meth = self.handle_invalid
|
||||||
elif isinstance(exception, (requests.exceptions.ConnectionError)):
|
elif isinstance(exception, (requests.exceptions.ConnectionError)):
|
||||||
meth = self.handle_connection
|
meth = self.handle_connection
|
||||||
(t, e_fn, message) = meth(tx_hash_hex, tx_hex, str(exception))
|
(t, e_fn, message) = meth(tx_hash_hex, tx_hex)
|
||||||
return (t, e_fn, '{} {}'.format(message, exception))
|
return (t, e_fn, '{} {}'.format(message, exception))
|
||||||
|
|
||||||
|
|
||||||
def handle_connection(self, tx_hash_hex, tx_hex, debugstr=None):
|
def handle_connection(self, tx_hash_hex, tx_hex):
|
||||||
s_set_sent = celery.signature(
|
s_set_sent = celery.signature(
|
||||||
'cic_eth.queue.tx.set_sent_status',
|
'cic_eth.queue.tx.set_sent_status',
|
||||||
[
|
[
|
||||||
@@ -206,7 +211,7 @@ class ParityNodeHandler:
|
|||||||
return (t, TemporaryTxError, 'Sendfail {}'.format(tx_hex_string(tx_hex, self.chain_spec.chain_id())))
|
return (t, TemporaryTxError, 'Sendfail {}'.format(tx_hex_string(tx_hex, self.chain_spec.chain_id())))
|
||||||
|
|
||||||
|
|
||||||
def handle_invalid_encoding(self, tx_hash_hex, tx_hex, debugstr=None):
|
def handle_invalid_encoding(self, tx_hash_hex, tx_hex):
|
||||||
tx_bytes = bytes.fromhex(tx_hex[2:])
|
tx_bytes = bytes.fromhex(tx_hex[2:])
|
||||||
tx = unpack_signed_raw_tx(tx_bytes, self.chain_spec.chain_id())
|
tx = unpack_signed_raw_tx(tx_bytes, self.chain_spec.chain_id())
|
||||||
s_lock = celery.signature(
|
s_lock = celery.signature(
|
||||||
@@ -253,7 +258,7 @@ class ParityNodeHandler:
|
|||||||
return (t, PermanentTxError, 'Reject invalid encoding {}'.format(tx_hex_string(tx_hex, self.chain_spec.chain_id())))
|
return (t, PermanentTxError, 'Reject invalid encoding {}'.format(tx_hex_string(tx_hex, self.chain_spec.chain_id())))
|
||||||
|
|
||||||
|
|
||||||
def handle_invalid_parameters(self, tx_hash_hex, tx_hex, debugstr=None):
|
def handle_invalid_parameters(self, tx_hash_hex, tx_hex):
|
||||||
s_sync = celery.signature(
|
s_sync = celery.signature(
|
||||||
'cic_eth.eth.tx.sync_tx',
|
'cic_eth.eth.tx.sync_tx',
|
||||||
[
|
[
|
||||||
@@ -266,7 +271,7 @@ class ParityNodeHandler:
|
|||||||
return (t, PermanentTxError, 'Reject invalid parameters {}'.format(tx_hex_string(tx_hex, self.chain_spec.chain_id())))
|
return (t, PermanentTxError, 'Reject invalid parameters {}'.format(tx_hex_string(tx_hex, self.chain_spec.chain_id())))
|
||||||
|
|
||||||
|
|
||||||
def handle_invalid(self, tx_hash_hex, tx_hex, debugstr=None):
|
def handle_invalid(self, tx_hash_hex, tx_hex):
|
||||||
tx_bytes = bytes.fromhex(tx_hex[2:])
|
tx_bytes = bytes.fromhex(tx_hex[2:])
|
||||||
tx = unpack_signed_raw_tx(tx_bytes, self.chain_spec.chain_id())
|
tx = unpack_signed_raw_tx(tx_bytes, self.chain_spec.chain_id())
|
||||||
s_lock = celery.signature(
|
s_lock = celery.signature(
|
||||||
@@ -284,16 +289,6 @@ class ParityNodeHandler:
|
|||||||
[],
|
[],
|
||||||
queue=self.queue,
|
queue=self.queue,
|
||||||
)
|
)
|
||||||
s_debug = celery.signature(
|
|
||||||
'cic_eth.admin.debug.alert',
|
|
||||||
[
|
|
||||||
tx_hash_hex,
|
|
||||||
tx_hash_hex,
|
|
||||||
debugstr,
|
|
||||||
],
|
|
||||||
queue=queue,
|
|
||||||
)
|
|
||||||
s_set_reject.link(s_debug)
|
|
||||||
s_lock.link(s_set_reject)
|
s_lock.link(s_set_reject)
|
||||||
t = s_lock.apply_async()
|
t = s_lock.apply_async()
|
||||||
return (t, PermanentTxError, 'Reject invalid {}'.format(tx_hex_string(tx_hex, self.chain_spec.chain_id())))
|
return (t, PermanentTxError, 'Reject invalid {}'.format(tx_hex_string(tx_hex, self.chain_spec.chain_id())))
|
||||||
|
|||||||
@@ -149,9 +149,6 @@ def tx_collate(tx_batches, chain_str, offset, limit, newest_first=True):
|
|||||||
txs_by_block = {}
|
txs_by_block = {}
|
||||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||||
|
|
||||||
if isinstance(tx_batches, dict):
|
|
||||||
tx_batches = [tx_batches]
|
|
||||||
|
|
||||||
for b in tx_batches:
|
for b in tx_batches:
|
||||||
for v in b.values():
|
for v in b.values():
|
||||||
tx = None
|
tx = None
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ from cic_eth.task import CriticalSQLAlchemyTask
|
|||||||
from cic_eth.eth.util import unpack_signed_raw_tx # TODO: should not be in same sub-path as package that imports queue.tx
|
from cic_eth.eth.util import unpack_signed_raw_tx # TODO: should not be in same sub-path as package that imports queue.tx
|
||||||
from cic_eth.error import NotLocalTxError
|
from cic_eth.error import NotLocalTxError
|
||||||
from cic_eth.error import LockedError
|
from cic_eth.error import LockedError
|
||||||
from cic_eth.db.enum import status_str
|
|
||||||
|
|
||||||
celery_app = celery.current_app
|
celery_app = celery.current_app
|
||||||
#logg = celery_app.log.get_default_logger()
|
#logg = celery_app.log.get_default_logger()
|
||||||
@@ -406,7 +405,7 @@ def get_tx_cache(tx_hash):
|
|||||||
'tx_hash': otx.tx_hash,
|
'tx_hash': otx.tx_hash,
|
||||||
'signed_tx': otx.signed_tx,
|
'signed_tx': otx.signed_tx,
|
||||||
'nonce': otx.nonce,
|
'nonce': otx.nonce,
|
||||||
'status': status_str(otx.status),
|
'status': StatusEnum(otx.status).name,
|
||||||
'status_code': otx.status,
|
'status_code': otx.status,
|
||||||
'source_token': txc.source_token_address,
|
'source_token': txc.source_token_address,
|
||||||
'destination_token': txc.destination_token_address,
|
'destination_token': txc.destination_token_address,
|
||||||
|
|||||||
@@ -2,4 +2,3 @@ from .callback import CallbackFilter
|
|||||||
from .tx import TxFilter
|
from .tx import TxFilter
|
||||||
from .gas import GasFilter
|
from .gas import GasFilter
|
||||||
from .register import RegistrationFilter
|
from .register import RegistrationFilter
|
||||||
from .transferauth import TransferAuthFilter
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# standard imports
|
# standard imports
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
# external imports
|
# third-party imports
|
||||||
from cic_registry.chain import ChainSpec
|
from cic_registry.chain import ChainSpec
|
||||||
from hexathon import add_0x
|
from hexathon import add_0x
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ class GasFilter(SyncFilter):
|
|||||||
self.chain_spec = chain_spec
|
self.chain_spec = chain_spec
|
||||||
|
|
||||||
|
|
||||||
def filter(self, conn, block, tx, session):
|
def filter(self, conn, block, tx, session): #rcpt, chain_str, session=None):
|
||||||
tx_hash_hex = add_0x(tx.hash)
|
tx_hash_hex = add_0x(tx.hash)
|
||||||
if tx.value > 0:
|
if tx.value > 0:
|
||||||
logg.debug('gas refill tx {}'.format(tx_hash_hex))
|
logg.debug('gas refill tx {}'.format(tx_hash_hex))
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# external imports
|
|
||||||
import celery
|
|
||||||
from hexathon import (
|
|
||||||
strip_0x,
|
|
||||||
add_0x,
|
|
||||||
)
|
|
||||||
from chainlib.eth.address import to_checksum
|
|
||||||
from .base import SyncFilter
|
|
||||||
|
|
||||||
|
|
||||||
logg = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
transfer_request_signature = 'ed71262a'
|
|
||||||
|
|
||||||
def unpack_create_request(data):
|
|
||||||
|
|
||||||
data = strip_0x(data)
|
|
||||||
cursor = 0
|
|
||||||
f = data[cursor:cursor+8]
|
|
||||||
cursor += 8
|
|
||||||
|
|
||||||
if f != transfer_request_signature:
|
|
||||||
raise ValueError('Invalid create request data ({})'.format(f))
|
|
||||||
|
|
||||||
o = {}
|
|
||||||
o['sender'] = data[cursor+24:cursor+64]
|
|
||||||
cursor += 64
|
|
||||||
o['recipient'] = data[cursor+24:cursor+64]
|
|
||||||
cursor += 64
|
|
||||||
o['token'] = data[cursor+24:cursor+64]
|
|
||||||
cursor += 64
|
|
||||||
o['value'] = int(data[cursor:], 16)
|
|
||||||
return o
|
|
||||||
|
|
||||||
|
|
||||||
class TransferAuthFilter(SyncFilter):
|
|
||||||
|
|
||||||
def __init__(self, registry, chain_spec, queue=None):
|
|
||||||
self.queue = queue
|
|
||||||
self.chain_spec = chain_spec
|
|
||||||
self.transfer_request_contract = registry.get_contract(self.chain_spec, 'TransferAuthorization')
|
|
||||||
|
|
||||||
|
|
||||||
def filter(self, conn, block, tx, session): #rcpt, chain_str, session=None):
|
|
||||||
|
|
||||||
if tx.payload == None:
|
|
||||||
logg.debug('no payload')
|
|
||||||
return False
|
|
||||||
|
|
||||||
payloadlength = len(tx.payload)
|
|
||||||
if payloadlength != 8+256:
|
|
||||||
logg.debug('{} below minimum length for a transfer auth call'.format(payloadlength))
|
|
||||||
logg.debug('payload {}'.format(tx.payload))
|
|
||||||
return False
|
|
||||||
|
|
||||||
recipient = tx.inputs[0]
|
|
||||||
if recipient != self.transfer_request_contract.address():
|
|
||||||
logg.debug('not our transfer auth contract address {}'.format(recipient))
|
|
||||||
return False
|
|
||||||
|
|
||||||
o = unpack_create_request(tx.payload)
|
|
||||||
|
|
||||||
sender = add_0x(to_checksum(o['sender']))
|
|
||||||
recipient = add_0x(to_checksum(recipient))
|
|
||||||
token = add_0x(to_checksum(o['token']))
|
|
||||||
s = celery.signature(
|
|
||||||
'cic_eth.eth.token.approve',
|
|
||||||
[
|
|
||||||
[
|
|
||||||
{
|
|
||||||
'address': token,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
sender,
|
|
||||||
recipient,
|
|
||||||
o['value'],
|
|
||||||
str(self.chain_spec),
|
|
||||||
],
|
|
||||||
queue=self.queue,
|
|
||||||
)
|
|
||||||
t = s.apply_async()
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'cic-eth transfer auth filter'
|
|
||||||
@@ -26,6 +26,7 @@ from cic_eth.eth import bancor
|
|||||||
from cic_eth.eth import token
|
from cic_eth.eth import token
|
||||||
from cic_eth.eth import tx
|
from cic_eth.eth import tx
|
||||||
from cic_eth.eth import account
|
from cic_eth.eth import account
|
||||||
|
from cic_eth.eth import request
|
||||||
from cic_eth.admin import debug
|
from cic_eth.admin import debug
|
||||||
from cic_eth.admin import ctrl
|
from cic_eth.admin import ctrl
|
||||||
from cic_eth.eth.rpc import RpcClient
|
from cic_eth.eth.rpc import RpcClient
|
||||||
@@ -39,7 +40,6 @@ from cic_eth.callbacks import redis
|
|||||||
from cic_eth.db.models.base import SessionBase
|
from cic_eth.db.models.base import SessionBase
|
||||||
from cic_eth.db.models.otx import Otx
|
from cic_eth.db.models.otx import Otx
|
||||||
from cic_eth.db import dsn_from_config
|
from cic_eth.db import dsn_from_config
|
||||||
from cic_eth.ext import tx
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.WARNING)
|
logging.basicConfig(level=logging.WARNING)
|
||||||
logg = logging.getLogger()
|
logg = logging.getLogger()
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ from cic_eth.runnable.daemons.filters import (
|
|||||||
GasFilter,
|
GasFilter,
|
||||||
TxFilter,
|
TxFilter,
|
||||||
RegistrationFilter,
|
RegistrationFilter,
|
||||||
TransferAuthFilter,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
script_dir = os.path.realpath(os.path.dirname(__file__))
|
script_dir = os.path.realpath(os.path.dirname(__file__))
|
||||||
@@ -147,8 +146,6 @@ def main():
|
|||||||
|
|
||||||
gas_filter = GasFilter(chain_spec, config.get('_CELERY_QUEUE'))
|
gas_filter = GasFilter(chain_spec, config.get('_CELERY_QUEUE'))
|
||||||
|
|
||||||
transfer_auth_filter = TransferAuthFilter(registry, chain_spec, config.get('_CELERY_QUEUE'))
|
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
for syncer in syncers:
|
for syncer in syncers:
|
||||||
logg.debug('running syncer index {}'.format(i))
|
logg.debug('running syncer index {}'.format(i))
|
||||||
@@ -156,7 +153,6 @@ def main():
|
|||||||
syncer.add_filter(registration_filter)
|
syncer.add_filter(registration_filter)
|
||||||
# TODO: the two following filter functions break the filter loop if return uuid. Pro: less code executed. Con: Possibly unintuitive flow break
|
# TODO: the two following filter functions break the filter loop if return uuid. Pro: less code executed. Con: Possibly unintuitive flow break
|
||||||
syncer.add_filter(tx_filter)
|
syncer.add_filter(tx_filter)
|
||||||
syncer.add_filter(transfer_auth_filter)
|
|
||||||
for cf in callback_filters:
|
for cf in callback_filters:
|
||||||
syncer.add_filter(cf)
|
syncer.add_filter(cf)
|
||||||
|
|
||||||
|
|||||||
@@ -55,25 +55,62 @@ SessionBase.connect(dsn)
|
|||||||
celery_app = celery.Celery(backend=config.get('CELERY_RESULT_URL'), broker=config.get('CELERY_BROKER_URL'))
|
celery_app = celery.Celery(backend=config.get('CELERY_RESULT_URL'), broker=config.get('CELERY_BROKER_URL'))
|
||||||
queue = args.q
|
queue = args.q
|
||||||
|
|
||||||
re_something = r'^/something/?'
|
re_transfer_approval_request = r'^/transferrequest/?'
|
||||||
|
|
||||||
chain_spec = ChainSpec.from_chain_str(config.get('CIC_CHAIN_SPEC'))
|
chain_spec = ChainSpec.from_chain_str(config.get('CIC_CHAIN_SPEC'))
|
||||||
|
|
||||||
|
|
||||||
def process_something(session, env):
|
def process_transfer_approval_request(session, env):
|
||||||
r = re.match(re_something, env.get('PATH_INFO'))
|
r = re.match(re_transfer_approval_request, env.get('PATH_INFO'))
|
||||||
if not r:
|
if not r:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
#if env.get('CONTENT_TYPE') != 'application/json':
|
if env.get('CONTENT_TYPE') != 'application/json':
|
||||||
# raise AttributeError('content type')
|
raise AttributeError('content type')
|
||||||
|
|
||||||
#if env.get('REQUEST_METHOD') != 'POST':
|
if env.get('REQUEST_METHOD') != 'POST':
|
||||||
# raise AttributeError('method')
|
raise AttributeError('method')
|
||||||
|
|
||||||
#post_data = json.load(env.get('wsgi.input'))
|
post_data = json.load(env.get('wsgi.input'))
|
||||||
|
token_address = web3.Web3.toChecksumAddress(post_data['token_address'])
|
||||||
#return ('text/plain', 'foo'.encode('utf-8'),)
|
holder_address = web3.Web3.toChecksumAddress(post_data['holder_address'])
|
||||||
|
beneficiary_address = web3.Web3.toChecksumAddress(post_data['beneficiary_address'])
|
||||||
|
value = int(post_data['value'])
|
||||||
|
|
||||||
|
logg.debug('transfer approval request token {} to {} from {} value {}'.format(
|
||||||
|
token_address,
|
||||||
|
beneficiary_address,
|
||||||
|
holder_address,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
s = celery.signature(
|
||||||
|
'cic_eth.eth.request.transfer_approval_request',
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'address': token_address,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
holder_address,
|
||||||
|
beneficiary_address,
|
||||||
|
value,
|
||||||
|
config.get('CIC_CHAIN_SPEC'),
|
||||||
|
],
|
||||||
|
queue=queue,
|
||||||
|
)
|
||||||
|
t = s.apply_async()
|
||||||
|
r = t.get()
|
||||||
|
tx_raw_bytes = bytes.fromhex(r[0][2:])
|
||||||
|
tx = unpack_signed_raw_tx(tx_raw_bytes, chain_spec.chain_id())
|
||||||
|
for r in t.collect():
|
||||||
|
logg.debug('result {}'.format(r))
|
||||||
|
|
||||||
|
if not t.successful():
|
||||||
|
raise RuntimeError(tx['hash'])
|
||||||
|
|
||||||
|
return ('text/plain', tx['hash'].encode('utf-8'),)
|
||||||
|
|
||||||
|
|
||||||
# uwsgi application
|
# uwsgi application
|
||||||
@@ -88,7 +125,7 @@ def application(env, start_response):
|
|||||||
|
|
||||||
session = SessionBase.create_session()
|
session = SessionBase.create_session()
|
||||||
for handler in [
|
for handler in [
|
||||||
process_something,
|
process_transfer_approval_request,
|
||||||
]:
|
]:
|
||||||
try:
|
try:
|
||||||
r = handler(session, env)
|
r = handler(session, env)
|
||||||
@@ -23,11 +23,8 @@ from hexathon import add_0x
|
|||||||
# local imports
|
# local imports
|
||||||
from cic_eth.api import AdminApi
|
from cic_eth.api import AdminApi
|
||||||
from cic_eth.eth.rpc import RpcClient
|
from cic_eth.eth.rpc import RpcClient
|
||||||
from cic_eth.db.enum import (
|
from cic_eth.db.enum import StatusEnum
|
||||||
StatusEnum,
|
from cic_eth.db.enum import LockEnum
|
||||||
status_str,
|
|
||||||
LockEnum,
|
|
||||||
)
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.WARNING)
|
logging.basicConfig(level=logging.WARNING)
|
||||||
logg = logging.getLogger()
|
logg = logging.getLogger()
|
||||||
@@ -122,7 +119,7 @@ def render_tx(o, **kwargs):
|
|||||||
|
|
||||||
for v in o.get('status_log', []):
|
for v in o.get('status_log', []):
|
||||||
d = datetime.datetime.fromisoformat(v[0])
|
d = datetime.datetime.fromisoformat(v[0])
|
||||||
e = status_str(v[1])
|
e = StatusEnum(v[1]).name
|
||||||
content += '{}: {}\n'.format(d, e)
|
content += '{}: {}\n'.format(d, e)
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ version = (
|
|||||||
0,
|
0,
|
||||||
10,
|
10,
|
||||||
0,
|
0,
|
||||||
'alpha.38',
|
'alpha.36',
|
||||||
)
|
)
|
||||||
|
|
||||||
version_object = semver.VersionInfo(
|
version_object = semver.VersionInfo(
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
cic-base~=0.1.1a10
|
|
||||||
web3==5.12.2
|
web3==5.12.2
|
||||||
celery==4.4.7
|
celery==4.4.7
|
||||||
crypto-dev-signer~=0.4.13rc4
|
crypto-dev-signer~=0.4.13rc3
|
||||||
confini~=0.3.6rc3
|
confini~=0.3.6rc3
|
||||||
cic-registry~=0.5.3a22
|
cic-registry~=0.5.3a22
|
||||||
cic-bancor~=0.0.6
|
cic-bancor~=0.0.6
|
||||||
@@ -10,7 +9,7 @@ alembic==1.4.2
|
|||||||
websockets==8.1
|
websockets==8.1
|
||||||
requests~=2.24.0
|
requests~=2.24.0
|
||||||
eth_accounts_index~=0.0.10a10
|
eth_accounts_index~=0.0.10a10
|
||||||
erc20-transfer-authorization~=0.3.0a10
|
erc20-approval-escrow~=0.3.0a5
|
||||||
erc20-single-shot-faucet~=0.2.0a6
|
erc20-single-shot-faucet~=0.2.0a6
|
||||||
rlp==2.0.1
|
rlp==2.0.1
|
||||||
uWSGI==2.0.19.1
|
uWSGI==2.0.19.1
|
||||||
@@ -22,3 +21,4 @@ eth-address-index~=0.1.0a8
|
|||||||
chainlib~=0.0.1a19
|
chainlib~=0.0.1a19
|
||||||
hexathon~=0.0.1a3
|
hexathon~=0.0.1a3
|
||||||
chainsyncer~=0.0.1a19
|
chainsyncer~=0.0.1a19
|
||||||
|
cic-base==0.1.1a10
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ def celery_includes():
|
|||||||
return [
|
return [
|
||||||
'cic_eth.eth.bancor',
|
'cic_eth.eth.bancor',
|
||||||
'cic_eth.eth.token',
|
'cic_eth.eth.token',
|
||||||
|
'cic_eth.eth.request',
|
||||||
'cic_eth.eth.tx',
|
'cic_eth.eth.tx',
|
||||||
'cic_eth.ext.tx',
|
'cic_eth.ext.tx',
|
||||||
'cic_eth.queue.tx',
|
'cic_eth.queue.tx',
|
||||||
'cic_eth.queue.balance',
|
'cic_eth.queue.balance',
|
||||||
'cic_eth.admin.ctrl',
|
'cic_eth.admin.ctrl',
|
||||||
'cic_eth.admin.nonce',
|
'cic_eth.admin.nonce',
|
||||||
'cic_eth.admin.debug',
|
|
||||||
'cic_eth.eth.account',
|
'cic_eth.eth.account',
|
||||||
'cic_eth.callbacks.noop',
|
'cic_eth.callbacks.noop',
|
||||||
'cic_eth.callbacks.http',
|
'cic_eth.callbacks.http',
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
# external imports
|
|
||||||
import celery
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
from cic_eth.db.models.debug import Debug
|
|
||||||
|
|
||||||
|
|
||||||
def test_debug_alert(
|
|
||||||
init_database,
|
|
||||||
celery_session_worker,
|
|
||||||
):
|
|
||||||
|
|
||||||
s = celery.signature(
|
|
||||||
'cic_eth.admin.debug.alert',
|
|
||||||
[
|
|
||||||
'foo',
|
|
||||||
'bar',
|
|
||||||
'baz',
|
|
||||||
],
|
|
||||||
queue=None,
|
|
||||||
)
|
|
||||||
t = s.apply_async()
|
|
||||||
r = t.get()
|
|
||||||
assert r == 'foo'
|
|
||||||
|
|
||||||
q = init_database.query(Debug)
|
|
||||||
q = q.filter(Debug.tag=='bar')
|
|
||||||
o = q.first()
|
|
||||||
assert o.description == 'baz'
|
|
||||||
76
apps/cic-eth/tests/tasks/test_transfer_approval.py
Normal file
76
apps/cic-eth/tests/tasks/test_transfer_approval.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# standard imports
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
# third-party imports
|
||||||
|
from erc20_approval_escrow import TransferApproval
|
||||||
|
import celery
|
||||||
|
import sha3
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from cic_eth.eth.token import TokenTxFactory
|
||||||
|
|
||||||
|
logg = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
# BUG: transaction receipt only found sometimes
|
||||||
|
def test_transfer_approval(
|
||||||
|
default_chain_spec,
|
||||||
|
transfer_approval,
|
||||||
|
bancor_tokens,
|
||||||
|
w3_account_roles,
|
||||||
|
eth_empty_accounts,
|
||||||
|
cic_registry,
|
||||||
|
init_database,
|
||||||
|
celery_session_worker,
|
||||||
|
init_eth_tester,
|
||||||
|
init_w3,
|
||||||
|
):
|
||||||
|
|
||||||
|
s = celery.signature(
|
||||||
|
'cic_eth.eth.request.transfer_approval_request',
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'address': bancor_tokens[0],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
w3_account_roles['eth_account_sarafu_owner'],
|
||||||
|
eth_empty_accounts[0],
|
||||||
|
1024,
|
||||||
|
str(default_chain_spec),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
s_send = celery.signature(
|
||||||
|
'cic_eth.eth.tx.send',
|
||||||
|
[
|
||||||
|
str(default_chain_spec),
|
||||||
|
],
|
||||||
|
|
||||||
|
)
|
||||||
|
s.link(s_send)
|
||||||
|
t = s.apply_async()
|
||||||
|
|
||||||
|
tx_signed_raws = t.get()
|
||||||
|
for r in t.collect():
|
||||||
|
logg.debug('result {}'.format(r))
|
||||||
|
|
||||||
|
assert t.successful()
|
||||||
|
|
||||||
|
init_eth_tester.mine_block()
|
||||||
|
|
||||||
|
h = sha3.keccak_256()
|
||||||
|
tx_signed_raw = tx_signed_raws[0]
|
||||||
|
tx_signed_raw_bytes = bytes.fromhex(tx_signed_raw[2:])
|
||||||
|
h.update(tx_signed_raw_bytes)
|
||||||
|
tx_hash = h.digest()
|
||||||
|
rcpt = init_w3.eth.getTransactionReceipt(tx_hash)
|
||||||
|
|
||||||
|
assert rcpt.status == 1
|
||||||
|
|
||||||
|
a = TransferApproval(init_w3, transfer_approval)
|
||||||
|
assert a.last_serial() == 1
|
||||||
|
|
||||||
|
logg.debug('requests {}'.format(a.requests(1)['serial']))
|
||||||
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# local imports
|
|
||||||
from cic_eth.db.models.debug import Debug
|
|
||||||
|
|
||||||
|
|
||||||
def test_debug(
|
|
||||||
init_database,
|
|
||||||
):
|
|
||||||
|
|
||||||
o = Debug('foo', 'bar')
|
|
||||||
init_database.add(o)
|
|
||||||
init_database.commit()
|
|
||||||
|
|
||||||
q = init_database.query(Debug)
|
|
||||||
q = q.filter(Debug.tag=='foo')
|
|
||||||
o = q.first()
|
|
||||||
assert o.description == 'bar'
|
|
||||||
@@ -7,8 +7,10 @@ PASSWORD_PEPPER=QYbzKff6NhiQzY3ygl2BkiKOpER8RE/Upqs/5aZWW+I=
|
|||||||
SERVICE_CODE=*483*46#
|
SERVICE_CODE=*483*46#
|
||||||
|
|
||||||
[ussd]
|
[ussd]
|
||||||
MENU_FILE=/usr/src/data/ussd_menu.json
|
MENU_FILE=/usr/local/lib/python3.8/site-packages/cic_ussd/db/ussd_menu.json
|
||||||
|
|
||||||
[statemachine]
|
[statemachine]
|
||||||
STATES=/usr/src/cic-ussd/states/
|
STATES=/usr/src/cic-ussd/states/
|
||||||
TRANSITIONS=/usr/src/cic-ussd/transitions/
|
TRANSITIONS=/usr/src/cic-ussd/transitions/
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,2 @@
|
|||||||
[cic]
|
[cic]
|
||||||
engine = evm
|
chain_spec = Bloxberg:8995
|
||||||
common_name = bloxberg
|
|
||||||
network_id = 8996
|
|
||||||
meta_url = http://localhost:63380
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[database]
|
[database]
|
||||||
NAME=cic_ussd
|
NAME=cic_ussd
|
||||||
USER=postgres
|
USER=postgres
|
||||||
PASSWORD=
|
PASSWORD=password
|
||||||
HOST=localhost
|
HOST=localhost
|
||||||
PORT=5432
|
PORT=5432
|
||||||
ENGINE=postgresql
|
ENGINE=postgresql
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
[pgp]
|
|
||||||
export_dir = /usr/src/pgp/keys/
|
|
||||||
keys_path = /usr/src/secrets/
|
|
||||||
private_keys = privatekeys_meta.asc
|
|
||||||
passphrase =
|
|
||||||
@@ -7,8 +7,8 @@ PASSWORD_PEPPER=QYbzKff6NhiQzY3ygl2BkiKOpER8RE/Upqs/5aZWW+I=
|
|||||||
SERVICE_CODE=*483*46#
|
SERVICE_CODE=*483*46#
|
||||||
|
|
||||||
[ussd]
|
[ussd]
|
||||||
MENU_FILE=/usr/local/lib/python3.8/site-packages/cic_ussd/db/ussd_menu.json
|
MENU_FILE=cic_ussd/db/ussd_menu.json
|
||||||
|
|
||||||
[statemachine]
|
[statemachine]
|
||||||
STATES=/usr/src/cic-ussd/states/
|
STATES=states/
|
||||||
TRANSITIONS=/usr/src/cic-ussd/transitions/
|
TRANSITIONS=transitions/
|
||||||
|
|||||||
@@ -1,5 +1,2 @@
|
|||||||
[cic]
|
[cic]
|
||||||
engine = evm
|
chain_spec = Bloxberg:8995
|
||||||
common_name = bloxberg
|
|
||||||
network_id = 8996
|
|
||||||
meta_url = http://localhost:63380
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
[pgp]
|
|
||||||
export_dir = /usr/src/pgp/keys/
|
|
||||||
keys_path = /usr/src/secrets/
|
|
||||||
private_keys = privatekeys_meta.asc
|
|
||||||
passphrase =
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
import json
|
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
from cic_eth.api import Api
|
|
||||||
from cic_types.models.person import Person
|
|
||||||
from cic_types.processor import generate_metadata_pointer
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
from cic_ussd.chain import Chain
|
|
||||||
from cic_ussd.db.models.user import User
|
|
||||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
|
|
||||||
from cic_ussd.redis import get_cached_data
|
|
||||||
|
|
||||||
|
|
||||||
def define_account_tx_metadata(user: User):
|
|
||||||
# get sender metadata
|
|
||||||
identifier = blockchain_address_to_metadata_pointer(
|
|
||||||
blockchain_address=user.blockchain_address
|
|
||||||
)
|
|
||||||
key = generate_metadata_pointer(
|
|
||||||
identifier=identifier,
|
|
||||||
cic_type='cic.person'
|
|
||||||
)
|
|
||||||
account_metadata = get_cached_data(key=key)
|
|
||||||
|
|
||||||
if account_metadata:
|
|
||||||
account_metadata = json.loads(account_metadata)
|
|
||||||
person = Person()
|
|
||||||
deserialized_person = person.deserialize(metadata=account_metadata)
|
|
||||||
given_name = deserialized_person.given_name
|
|
||||||
family_name = deserialized_person.family_name
|
|
||||||
phone_number = deserialized_person.tel
|
|
||||||
|
|
||||||
return f'{given_name} {family_name} {phone_number}'
|
|
||||||
else:
|
|
||||||
phone_number = user.phone_number
|
|
||||||
return phone_number
|
|
||||||
|
|
||||||
|
|
||||||
def retrieve_account_statement(blockchain_address: str):
|
|
||||||
chain_str = Chain.spec.__str__()
|
|
||||||
cic_eth_api = Api(
|
|
||||||
chain_str=chain_str,
|
|
||||||
callback_queue='cic-ussd',
|
|
||||||
callback_task='cic_ussd.tasks.callback_handler.process_statement_callback',
|
|
||||||
callback_param=blockchain_address
|
|
||||||
)
|
|
||||||
result = cic_eth_api.list(address=blockchain_address, limit=9)
|
|
||||||
39
apps/cic-ussd/cic_ussd/accounts.py
Normal file
39
apps/cic-ussd/cic_ussd/accounts.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# standard imports
|
||||||
|
import logging
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
# third-party imports
|
||||||
|
from cic_eth.api import Api
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from cic_ussd.transactions import from_wei
|
||||||
|
|
||||||
|
logg = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
class BalanceManager:
|
||||||
|
|
||||||
|
def __init__(self, address: str, chain_str: str, token_symbol: str):
|
||||||
|
"""
|
||||||
|
:param address: Ethereum address of account whose balance is being queried
|
||||||
|
:type address: str, 0x-hex
|
||||||
|
:param chain_str: The chain name and network id.
|
||||||
|
:type chain_str: str
|
||||||
|
:param token_symbol: ERC20 token symbol of whose balance is being queried
|
||||||
|
:type token_symbol: str
|
||||||
|
"""
|
||||||
|
self.address = address
|
||||||
|
self.chain_str = chain_str
|
||||||
|
self.token_symbol = token_symbol
|
||||||
|
|
||||||
|
def get_operational_balance(self) -> float:
|
||||||
|
"""This question queries cic-eth for an account's balance
|
||||||
|
:return: The current balance of the account as reflected on the blockchain.
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
cic_eth_api = Api(chain_str=self.chain_str, callback_task=None)
|
||||||
|
balance_request_task = cic_eth_api.balance(address=self.address, token_symbol=self.token_symbol)
|
||||||
|
balance_request_task_results = balance_request_task.collect()
|
||||||
|
balance_result = deque(balance_request_task_results, maxlen=1).pop()
|
||||||
|
balance = from_wei(value=balance_result[-1])
|
||||||
|
return balance
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
import celery
|
|
||||||
from cic_eth.api import Api
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
from cic_ussd.error import CachedDataNotFoundError
|
|
||||||
from cic_ussd.redis import create_cached_data_key, get_cached_data
|
|
||||||
from cic_ussd.conversions import from_wei
|
|
||||||
|
|
||||||
logg = logging.getLogger()
|
|
||||||
|
|
||||||
|
|
||||||
class BalanceManager:
|
|
||||||
|
|
||||||
def __init__(self, address: str, chain_str: str, token_symbol: str):
|
|
||||||
"""
|
|
||||||
:param address: Ethereum address of account whose balance is being queried
|
|
||||||
:type address: str, 0x-hex
|
|
||||||
:param chain_str: The chain name and network id.
|
|
||||||
:type chain_str: str
|
|
||||||
:param token_symbol: ERC20 token symbol of whose balance is being queried
|
|
||||||
:type token_symbol: str
|
|
||||||
"""
|
|
||||||
self.address = address
|
|
||||||
self.chain_str = chain_str
|
|
||||||
self.token_symbol = token_symbol
|
|
||||||
|
|
||||||
def get_balances(self, asynchronous: bool = False) -> Union[celery.Task, dict]:
|
|
||||||
"""
|
|
||||||
This function queries cic-eth for an account's balances, It provides a means to receive the balance either
|
|
||||||
asynchronously or synchronously depending on the provided value for teh asynchronous parameter. It returns a
|
|
||||||
dictionary containing network, outgoing and incoming balances.
|
|
||||||
:param asynchronous: Boolean value checking whether to return balances asynchronously
|
|
||||||
:type asynchronous: bool
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
if asynchronous:
|
|
||||||
cic_eth_api = Api(
|
|
||||||
chain_str=self.chain_str,
|
|
||||||
callback_queue='cic-ussd',
|
|
||||||
callback_task='cic_ussd.tasks.callback_handler.process_balances_callback',
|
|
||||||
callback_param=''
|
|
||||||
)
|
|
||||||
cic_eth_api.balance(address=self.address, token_symbol=self.token_symbol)
|
|
||||||
else:
|
|
||||||
cic_eth_api = Api(chain_str=self.chain_str)
|
|
||||||
balance_request_task = cic_eth_api.balance(
|
|
||||||
address=self.address,
|
|
||||||
token_symbol=self.token_symbol)
|
|
||||||
return balance_request_task.get()[0]
|
|
||||||
|
|
||||||
|
|
||||||
def compute_operational_balance(balances: dict) -> float:
|
|
||||||
"""This function calculates the right balance given incoming and outgoing
|
|
||||||
:param balances:
|
|
||||||
:type balances:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
incoming_balance = balances.get('balance_incoming')
|
|
||||||
outgoing_balance = balances.get('balance_outgoing')
|
|
||||||
network_balance = balances.get('balance_network')
|
|
||||||
|
|
||||||
operational_balance = (network_balance + incoming_balance) - outgoing_balance
|
|
||||||
return from_wei(value=operational_balance)
|
|
||||||
|
|
||||||
|
|
||||||
def get_cached_operational_balance(blockchain_address: str):
|
|
||||||
"""
|
|
||||||
:param blockchain_address:
|
|
||||||
:type blockchain_address:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
key = create_cached_data_key(
|
|
||||||
identifier=bytes.fromhex(blockchain_address[2:]),
|
|
||||||
salt='cic.balances_data'
|
|
||||||
)
|
|
||||||
cached_balance = get_cached_data(key=key)
|
|
||||||
if cached_balance:
|
|
||||||
operational_balance = compute_operational_balance(balances=json.loads(cached_balance))
|
|
||||||
return operational_balance
|
|
||||||
else:
|
|
||||||
raise CachedDataNotFoundError('Cached operational balance not found.')
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# local imports
|
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
from chainlib.chain import ChainSpec
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
|
|
||||||
|
|
||||||
class Chain:
|
|
||||||
spec: ChainSpec = None
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
import decimal
|
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
|
|
||||||
|
|
||||||
def truncate(value: float, decimals: int):
|
|
||||||
"""This function truncates a value to a specified number of decimals places.
|
|
||||||
:param value: The value to be truncated.
|
|
||||||
:type value: float
|
|
||||||
:param decimals: The number of decimals for the value to be truncated to
|
|
||||||
:type decimals: int
|
|
||||||
:return: The truncated value.
|
|
||||||
:rtype: int
|
|
||||||
"""
|
|
||||||
decimal.getcontext().rounding = decimal.ROUND_DOWN
|
|
||||||
contextualized_value = decimal.Decimal(value)
|
|
||||||
return round(contextualized_value, decimals)
|
|
||||||
|
|
||||||
|
|
||||||
def from_wei(value: int) -> float:
|
|
||||||
"""This function converts values in Wei to a token in the cic network.
|
|
||||||
:param value: Value in Wei
|
|
||||||
:type value: int
|
|
||||||
:return: SRF equivalent of value in Wei
|
|
||||||
:rtype: float
|
|
||||||
"""
|
|
||||||
value = float(value) / 1e+6
|
|
||||||
return truncate(value=value, decimals=2)
|
|
||||||
|
|
||||||
|
|
||||||
def to_wei(value: int) -> int:
|
|
||||||
"""This functions converts values from a token in the cic network to Wei.
|
|
||||||
:param value: Value in SRF
|
|
||||||
:type value: int
|
|
||||||
:return: Wei equivalent of value in SRF
|
|
||||||
:rtype: int
|
|
||||||
"""
|
|
||||||
return int(value * 1e+6)
|
|
||||||
@@ -1,237 +1,213 @@
|
|||||||
{
|
{
|
||||||
"ussd_menu": {
|
"ussd_menu": {
|
||||||
"1": {
|
"1": {
|
||||||
"description": "Entry point for users to select their preferred language.",
|
"description": "The self signup process has been initiated and the account is being created",
|
||||||
"display_key": "ussd.kenya.initial_language_selection",
|
|
||||||
"name": "initial_language_selection",
|
|
||||||
"parent": null
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"description": "Entry point for users to enter a pin to secure their account.",
|
|
||||||
"display_key": "ussd.kenya.initial_pin_entry",
|
|
||||||
"name": "initial_pin_entry",
|
|
||||||
"parent": null
|
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"description": "Pin confirmation entry menu.",
|
|
||||||
"display_key": "ussd.kenya.initial_pin_confirmation",
|
|
||||||
"name": "initial_pin_confirmation",
|
|
||||||
"parent": "initial_pin_entry"
|
|
||||||
},
|
|
||||||
"4": {
|
|
||||||
"description": "The signup process has been initiated and the account is being created.",
|
|
||||||
"display_key": "ussd.kenya.account_creation_prompt",
|
"display_key": "ussd.kenya.account_creation_prompt",
|
||||||
"name": "account_creation_prompt",
|
"name": "account_creation_prompt",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
|
"2": {
|
||||||
|
"description": "Start menu. This is the entry point for users to select their preferred language",
|
||||||
|
"display_key": "ussd.kenya.initial_language_selection",
|
||||||
|
"name": "initial_language_selection",
|
||||||
|
"parent": null
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"description": "PIN setup entry menu",
|
||||||
|
"display_key": "ussd.kenya.initial_pin_entry",
|
||||||
|
"name": "initial_pin_entry",
|
||||||
|
"parent": "initial_language_selection"
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"description": "Confirm new PIN menu",
|
||||||
|
"display_key": "ussd.kenya.initial_pin_confirmation",
|
||||||
|
"name": "initial_pin_confirmation",
|
||||||
|
"parent": "initial_pin_entry"
|
||||||
|
},
|
||||||
"5": {
|
"5": {
|
||||||
"description": "Entry point for activated users.",
|
"description": "Start menu. This is the entry point for activated users",
|
||||||
"display_key": "ussd.kenya.start",
|
"display_key": "ussd.kenya.start",
|
||||||
"name": "start",
|
"name": "start",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"6": {
|
"6": {
|
||||||
"description": "Given name entry menu.",
|
"description": "Send Token recipient entry",
|
||||||
"display_key": "ussd.kenya.enter_given_name",
|
|
||||||
"name": "enter_given_name",
|
|
||||||
"parent": "metadata_management"
|
|
||||||
},
|
|
||||||
"7": {
|
|
||||||
"description": "Family name entry menu.",
|
|
||||||
"display_key": "ussd.kenya.enter_family_name",
|
|
||||||
"name": "enter_family_name",
|
|
||||||
"parent": "metadata_management"
|
|
||||||
},
|
|
||||||
"8": {
|
|
||||||
"description": "Gender entry menu.",
|
|
||||||
"display_key": "ussd.kenya.enter_gender",
|
|
||||||
"name": "enter_gender",
|
|
||||||
"parent": "metadata_management"
|
|
||||||
},
|
|
||||||
"9": {
|
|
||||||
"description": "Age entry menu.",
|
|
||||||
"display_key": "ussd.kenya.enter_gender",
|
|
||||||
"name": "enter_gender",
|
|
||||||
"parent": "metadata_management"
|
|
||||||
},
|
|
||||||
"10": {
|
|
||||||
"description": "Location entry menu.",
|
|
||||||
"display_key": "ussd.kenya.enter_location",
|
|
||||||
"name": "enter_location",
|
|
||||||
"parent": "metadata_management"
|
|
||||||
},
|
|
||||||
"11": {
|
|
||||||
"description": "Products entry menu.",
|
|
||||||
"display_key": "ussd.kenya.enter_products",
|
|
||||||
"name": "enter_products",
|
|
||||||
"parent": "metadata_management"
|
|
||||||
},
|
|
||||||
"12": {
|
|
||||||
"description": "Entry point for activated users.",
|
|
||||||
"display_key": "ussd.kenya.start",
|
|
||||||
"name": "start",
|
|
||||||
"parent": null
|
|
||||||
},
|
|
||||||
"13": {
|
|
||||||
"description": "Send Token recipient entry.",
|
|
||||||
"display_key": "ussd.kenya.enter_transaction_recipient",
|
"display_key": "ussd.kenya.enter_transaction_recipient",
|
||||||
"name": "enter_transaction_recipient",
|
"name": "enter_transaction_recipient",
|
||||||
"parent": "start"
|
"parent": "start"
|
||||||
},
|
},
|
||||||
"14": {
|
"7": {
|
||||||
"description": "Send Token amount prompt menu.",
|
"description": "Send Token amount prompt menu",
|
||||||
"display_key": "ussd.kenya.enter_transaction_amount",
|
"display_key": "ussd.kenya.enter_transaction_amount",
|
||||||
"name": "enter_transaction_amount",
|
"name": "enter_transaction_amount",
|
||||||
"parent": "start"
|
"parent": "start"
|
||||||
},
|
},
|
||||||
"15": {
|
"8": {
|
||||||
"description": "Pin entry for authorization to send token.",
|
"description": "PIN entry for authorization to send token",
|
||||||
"display_key": "ussd.kenya.transaction_pin_authorization",
|
"display_key": "ussd.kenya.transaction_pin_authorization",
|
||||||
"name": "transaction_pin_authorization",
|
"name": "transaction_pin_authorization",
|
||||||
"parent": "start"
|
"parent": "start"
|
||||||
},
|
},
|
||||||
"16": {
|
"9": {
|
||||||
"description": "Manage account menu.",
|
"description": "Terminal of a menu flow where an SMS is expected after.",
|
||||||
"display_key": "ussd.kenya.account_management",
|
"display_key": "ussd.kenya.complete",
|
||||||
"name": "account_management",
|
"name": "complete",
|
||||||
|
"parent": null
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"description": "Help menu",
|
||||||
|
"display_key": "ussd.kenya.help",
|
||||||
|
"name": "help",
|
||||||
"parent": "start"
|
"parent": "start"
|
||||||
},
|
},
|
||||||
"17": {
|
"11": {
|
||||||
"description": "Manage metadata menu.",
|
"description": "Manage account menu",
|
||||||
"display_key": "ussd.kenya.metadata_management",
|
"display_key": "ussd.kenya.profile_management",
|
||||||
"name": "metadata_management",
|
"name": "profile_management",
|
||||||
"parent": "start"
|
"parent": "start"
|
||||||
},
|
},
|
||||||
"18": {
|
"12": {
|
||||||
"description": "Manage user's preferred language menu.",
|
"description": "Manage business directory info",
|
||||||
"display_key": "ussd.kenya.select_preferred_language",
|
"display_key": "ussd.kenya.select_preferred_language",
|
||||||
"name": "select_preferred_language",
|
"name": "select_preferred_language",
|
||||||
"parent": "account_management"
|
"parent": "account_management"
|
||||||
},
|
},
|
||||||
"19": {
|
"13": {
|
||||||
"description": "Retrieve mini-statement menu.",
|
"description": "About business directory info",
|
||||||
"display_key": "ussd.kenya.mini_statement_pin_authorization",
|
"display_key": "ussd.kenya.mini_statement_pin_authorization",
|
||||||
"name": "mini_statement_pin_authorization",
|
"name": "mini_statement_pin_authorization",
|
||||||
"parent": "account_management"
|
"parent": "account_management"
|
||||||
},
|
},
|
||||||
"20": {
|
"14": {
|
||||||
"description": "Manage user's pin menu.",
|
"description": "Change business directory info",
|
||||||
"display_key": "ussd.kenya.enter_current_pin",
|
"display_key": "ussd.kenya.enter_current_pin",
|
||||||
"name": "enter_current_pin",
|
"name": "enter_current_pin",
|
||||||
"parent": "account_management"
|
"parent": "account_management"
|
||||||
},
|
},
|
||||||
"21": {
|
"15": {
|
||||||
"description": "New pin entry menu.",
|
"description": "New PIN entry menu",
|
||||||
"display_key": "ussd.kenya.enter_new_pin",
|
"display_key": "ussd.kenya.enter_new_pin",
|
||||||
"name": "enter_new_pin",
|
"name": "enter_new_pin",
|
||||||
"parent": "account_management"
|
"parent": "account_management"
|
||||||
},
|
},
|
||||||
|
"16": {
|
||||||
|
"description": "First name entry menu",
|
||||||
|
"display_key": "ussd.kenya.enter_first_name",
|
||||||
|
"name": "enter_first_name",
|
||||||
|
"parent": "profile_management"
|
||||||
|
},
|
||||||
|
"17": {
|
||||||
|
"description": "Last name entry menu",
|
||||||
|
"display_key": "ussd.kenya.enter_last_name",
|
||||||
|
"name": "enter_last_name",
|
||||||
|
"parent": "profile_management"
|
||||||
|
},
|
||||||
|
"18": {
|
||||||
|
"description": "Gender entry menu",
|
||||||
|
"display_key": "ussd.kenya.enter_gender",
|
||||||
|
"name": "enter_gender",
|
||||||
|
"parent": "profile_management"
|
||||||
|
},
|
||||||
|
"19": {
|
||||||
|
"description": "Location entry menu",
|
||||||
|
"display_key": "ussd.kenya.enter_location",
|
||||||
|
"name": "enter_location",
|
||||||
|
"parent": "profile_management"
|
||||||
|
},
|
||||||
|
"20": {
|
||||||
|
"description": "Business profile entry menu",
|
||||||
|
"display_key": "ussd.kenya.enter_business_profile",
|
||||||
|
"name": "enter_business_profile",
|
||||||
|
"parent": "profile_management"
|
||||||
|
},
|
||||||
|
"21": {
|
||||||
|
"description": "Menu to display a user's entire profile",
|
||||||
|
"display_key": "ussd.kenya.display_user_profile_data",
|
||||||
|
"name": "display_user_profile_data",
|
||||||
|
"parent": "profile_management"
|
||||||
|
},
|
||||||
"22": {
|
"22": {
|
||||||
"description": "Pin entry menu.",
|
"description": "Pin authorization to change name",
|
||||||
"display_key": "ussd.kenya.standard_pin_authorization",
|
"display_key": "ussd.kenya.name_management_pin_authorization",
|
||||||
"name": "standard_pin_authorization",
|
"name": "name_management_pin_authorization",
|
||||||
"parent": "start"
|
"parent": "profile_management"
|
||||||
},
|
},
|
||||||
"23": {
|
"23": {
|
||||||
"description": "Exit menu.",
|
"description": "Pin authorization to change gender",
|
||||||
|
"display_key": "ussd.kenya.gender_management_pin_authorization",
|
||||||
|
"name": "gender_management_pin_authorization",
|
||||||
|
"parent": "profile_management"
|
||||||
|
},
|
||||||
|
"24": {
|
||||||
|
"description": "Pin authorization to change location",
|
||||||
|
"display_key": "ussd.kenya.location_management_pin_authorization",
|
||||||
|
"name": "location_management_pin_authorization",
|
||||||
|
"parent": "profile_management"
|
||||||
|
},
|
||||||
|
"26": {
|
||||||
|
"description": "Pin authorization to display user's profile",
|
||||||
|
"display_key": "ussd.kenya.view_profile_pin_authorization",
|
||||||
|
"name": "view_profile_pin_authorization",
|
||||||
|
"parent": "profile_management"
|
||||||
|
},
|
||||||
|
"27": {
|
||||||
|
"description": "Exit menu",
|
||||||
"display_key": "ussd.kenya.exit",
|
"display_key": "ussd.kenya.exit",
|
||||||
"name": "exit",
|
"name": "exit",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"24": {
|
"28": {
|
||||||
"description": "Invalid menu option.",
|
"description": "Invalid menu option",
|
||||||
"display_key": "ussd.kenya.exit_invalid_menu_option",
|
"display_key": "ussd.kenya.exit_invalid_menu_option",
|
||||||
"name": "exit_invalid_menu_option",
|
"name": "exit_invalid_menu_option",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"25": {
|
"29": {
|
||||||
"description": "Pin policy violation.",
|
"description": "PIN policy violation",
|
||||||
"display_key": "ussd.kenya.exit_invalid_pin",
|
"display_key": "ussd.kenya.exit_invalid_pin",
|
||||||
"name": "exit_invalid_pin",
|
"name": "exit_invalid_pin",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"26": {
|
"30": {
|
||||||
"description": "Pin mismatch. New pin and the new pin confirmation do not match",
|
"description": "PIN mismatch. New PIN and the new PIN confirmation do not match",
|
||||||
"display_key": "ussd.kenya.exit_pin_mismatch",
|
"display_key": "ussd.kenya.exit_pin_mismatch",
|
||||||
"name": "exit_pin_mismatch",
|
"name": "exit_pin_mismatch",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"27": {
|
"31": {
|
||||||
"description": "Ussd pin blocked Menu",
|
"description": "Ussd PIN Blocked Menu",
|
||||||
"display_key": "ussd.kenya.exit_pin_blocked",
|
"display_key": "ussd.kenya.exit_pin_blocked",
|
||||||
"name": "exit_pin_blocked",
|
"name": "exit_pin_blocked",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"28": {
|
"32": {
|
||||||
"description": "Key params missing in request.",
|
"description": "Key params missing in request",
|
||||||
"display_key": "ussd.kenya.exit_invalid_request",
|
"display_key": "ussd.kenya.exit_invalid_request",
|
||||||
"name": "exit_invalid_request",
|
"name": "exit_invalid_request",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"29": {
|
"33": {
|
||||||
"description": "The user did not select a choice.",
|
"description": "The user did not select a choice",
|
||||||
"display_key": "ussd.kenya.exit_invalid_input",
|
"display_key": "ussd.kenya.exit_invalid_input",
|
||||||
"name": "exit_invalid_input",
|
"name": "exit_invalid_input",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"30": {
|
"34": {
|
||||||
"description": "Exit following unsuccessful transaction due to insufficient account balance.",
|
|
||||||
"display_key": "ussd.kenya.exit_insufficient_balance",
|
|
||||||
"name": "exit_insufficient_balance",
|
|
||||||
"parent": null
|
|
||||||
},
|
|
||||||
"31": {
|
|
||||||
"description": "Exit following a successful transaction.",
|
"description": "Exit following a successful transaction.",
|
||||||
"display_key": "ussd.kenya.exit_successful_transaction",
|
"display_key": "ussd.kenya.exit_successful_transaction",
|
||||||
"name": "exit_successful_transaction",
|
"name": "exit_successful_transaction",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"32": {
|
|
||||||
"description": "End of a menu flow.",
|
|
||||||
"display_key": "ussd.kenya.complete",
|
|
||||||
"name": "complete",
|
|
||||||
"parent": null
|
|
||||||
},
|
|
||||||
"33": {
|
|
||||||
"description": "Pin entry menu to view account balances.",
|
|
||||||
"display_key": "ussd.kenya.account_balances_pin_authorization",
|
|
||||||
"name": "account_balances_pin_authorization",
|
|
||||||
"parent": "account_management"
|
|
||||||
},
|
|
||||||
"34": {
|
|
||||||
"description": "Pin entry menu to view account statement.",
|
|
||||||
"display_key": "ussd.kenya.account_statement_pin_authorization",
|
|
||||||
"name": "account_statement_pin_authorization",
|
|
||||||
"parent": "account_management"
|
|
||||||
},
|
|
||||||
"35": {
|
"35": {
|
||||||
"description": "Menu to display account balances.",
|
"description": "Manage account menu",
|
||||||
"display_key": "ussd.kenya.account_balances",
|
"display_key": "ussd.kenya.account_management",
|
||||||
"name": "account_balances",
|
"name": "account_management",
|
||||||
"parent": "account_management"
|
"parent": "start"
|
||||||
},
|
},
|
||||||
"36": {
|
"36": {
|
||||||
"description": "Menu to display first set of transactions in statement.",
|
"description": "Exit following insufficient balance to perform a transaction.",
|
||||||
"display_key": "ussd.kenya.first_transaction_set",
|
"display_key": "ussd.kenya.exit_insufficient_balance",
|
||||||
"name": "first_transaction_set",
|
"name": "exit_insufficient_balance",
|
||||||
"parent": null
|
|
||||||
},
|
|
||||||
"37": {
|
|
||||||
"description": "Menu to display middle set of transactions in statement.",
|
|
||||||
"display_key": "ussd.kenya.middle_transaction_set",
|
|
||||||
"name": "middle_transaction_set",
|
|
||||||
"parent": null
|
|
||||||
},
|
|
||||||
"38": {
|
|
||||||
"description": "Menu to display last set of transactions in statement.",
|
|
||||||
"display_key": "ussd.kenya.last_transaction_set",
|
|
||||||
"name": "last_transaction_set",
|
|
||||||
"parent": null
|
|
||||||
},
|
|
||||||
"39": {
|
|
||||||
"description": "Menu to instruct users to call the office.",
|
|
||||||
"display_key": "ussd.key.help",
|
|
||||||
"name": "help",
|
|
||||||
"parent": null
|
"parent": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,17 +17,3 @@ class ActionDataNotFoundError(OSError):
|
|||||||
"""Raised when action data matching a specific task uuid is not found in the redis cache"""
|
"""Raised when action data matching a specific task uuid is not found in the redis cache"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UserMetadataNotFoundError(OSError):
|
|
||||||
"""Raised when metadata is expected but not available in cache."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedMethodError(OSError):
|
|
||||||
"""Raised when the method passed to the make request function is unsupported."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CachedDataNotFoundError(OSError):
|
|
||||||
"""Raised when the method passed to the make request function is unsupported."""
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
import requests
|
|
||||||
from chainlib.eth.address import to_checksum
|
|
||||||
from hexathon import add_0x
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
from cic_ussd.error import UnsupportedMethodError
|
|
||||||
|
|
||||||
|
|
||||||
def make_request(method: str, url: str, data: any = None, headers: dict = None):
|
|
||||||
"""
|
|
||||||
:param method:
|
|
||||||
:type method:
|
|
||||||
:param url:
|
|
||||||
:type url:
|
|
||||||
:param data:
|
|
||||||
:type data:
|
|
||||||
:param headers:
|
|
||||||
:type headers:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
if method == 'GET':
|
|
||||||
result = requests.get(url=url)
|
|
||||||
elif method == 'POST':
|
|
||||||
result = requests.post(url=url, data=data, headers=headers)
|
|
||||||
elif method == 'PUT':
|
|
||||||
result = requests.put(url=url, data=data, headers=headers)
|
|
||||||
else:
|
|
||||||
raise UnsupportedMethodError(f'Unsupported method: {method}')
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def blockchain_address_to_metadata_pointer(blockchain_address: str):
|
|
||||||
"""
|
|
||||||
:param blockchain_address:
|
|
||||||
:type blockchain_address:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
return bytes.fromhex(blockchain_address[2:])
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from typing import Optional
|
|
||||||
from urllib.request import Request, urlopen
|
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
import gnupg
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
|
|
||||||
logg = logging.getLogger()
|
|
||||||
|
|
||||||
|
|
||||||
class Signer:
|
|
||||||
"""
|
|
||||||
:cvar gpg_path:
|
|
||||||
:type gpg_path:
|
|
||||||
:cvar gpg_passphrase:
|
|
||||||
:type gpg_passphrase:
|
|
||||||
:cvar key_file_path:
|
|
||||||
:type key_file_path:
|
|
||||||
|
|
||||||
"""
|
|
||||||
gpg_path: str = None
|
|
||||||
gpg_passphrase: str = None
|
|
||||||
key_file_path: str = None
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.gpg = gnupg.GPG(gnupghome=self.gpg_path)
|
|
||||||
|
|
||||||
# parse key file data
|
|
||||||
key_file = open(self.key_file_path, 'r')
|
|
||||||
self.key_data = key_file.read()
|
|
||||||
key_file.close()
|
|
||||||
|
|
||||||
def get_operational_key(self):
|
|
||||||
"""
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
# import key data into keyring
|
|
||||||
self.gpg.import_keys(key_data=self.key_data)
|
|
||||||
gpg_keys = self.gpg.list_keys()
|
|
||||||
key_algorithm = gpg_keys[0].get('algo')
|
|
||||||
key_id = gpg_keys[0].get("keyid")
|
|
||||||
logg.info(f'using signing key: {key_id}, algorithm: {key_algorithm}')
|
|
||||||
return gpg_keys[0]
|
|
||||||
|
|
||||||
def sign_digest(self, data: bytes):
|
|
||||||
"""
|
|
||||||
:param data:
|
|
||||||
:type data:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
data = json.loads(data)
|
|
||||||
digest = data['digest']
|
|
||||||
key_id = self.get_operational_key().get('keyid')
|
|
||||||
signature = self.gpg.sign(digest, passphrase=self.gpg_passphrase, keyid=key_id)
|
|
||||||
return str(signature)
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
import requests
|
|
||||||
from cic_types.models.person import generate_metadata_pointer, Person
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
from cic_ussd.chain import Chain
|
|
||||||
from cic_ussd.metadata import make_request
|
|
||||||
from cic_ussd.metadata.signer import Signer
|
|
||||||
from cic_ussd.redis import cache_data
|
|
||||||
|
|
||||||
logg = logging.getLogger()
|
|
||||||
|
|
||||||
|
|
||||||
class UserMetadata:
|
|
||||||
"""
|
|
||||||
:cvar base_url:
|
|
||||||
:type base_url:
|
|
||||||
"""
|
|
||||||
base_url = None
|
|
||||||
|
|
||||||
def __init__(self, identifier: bytes):
|
|
||||||
"""
|
|
||||||
:param identifier:
|
|
||||||
:type identifier:
|
|
||||||
"""
|
|
||||||
self. headers = {
|
|
||||||
'X-CIC-AUTOMERGE': 'server',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
self.identifier = identifier
|
|
||||||
self.metadata_pointer = generate_metadata_pointer(
|
|
||||||
identifier=self.identifier,
|
|
||||||
cic_type='cic.person'
|
|
||||||
)
|
|
||||||
if self.base_url:
|
|
||||||
self.url = os.path.join(self.base_url, self.metadata_pointer)
|
|
||||||
|
|
||||||
def create(self, data: dict):
|
|
||||||
try:
|
|
||||||
data = json.dumps(data).encode('utf-8')
|
|
||||||
result = make_request(method='POST', url=self.url, data=data, headers=self.headers)
|
|
||||||
metadata = result.content
|
|
||||||
self.edit(data=metadata, engine='pgp')
|
|
||||||
logg.info(f'Get sign material response status: {result.status_code}')
|
|
||||||
result.raise_for_status()
|
|
||||||
except requests.exceptions.HTTPError as error:
|
|
||||||
raise RuntimeError(error)
|
|
||||||
|
|
||||||
def edit(self, data: bytes, engine: str):
|
|
||||||
"""
|
|
||||||
:param data:
|
|
||||||
:type data:
|
|
||||||
:param engine:
|
|
||||||
:type engine:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
cic_meta_signer = Signer()
|
|
||||||
signature = cic_meta_signer.sign_digest(data=data)
|
|
||||||
algorithm = cic_meta_signer.get_operational_key().get('algo')
|
|
||||||
formatted_data = {
|
|
||||||
'm': data.decode('utf-8'),
|
|
||||||
's': {
|
|
||||||
'engine': engine,
|
|
||||||
'algo': algorithm,
|
|
||||||
'data': signature,
|
|
||||||
'digest': json.loads(data).get('digest'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
formatted_data = json.dumps(formatted_data).encode('utf-8')
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = make_request(method='PUT', url=self.url, data=formatted_data, headers=self.headers)
|
|
||||||
logg.info(f'Signed content submission status: {result.status_code}.')
|
|
||||||
result.raise_for_status()
|
|
||||||
except requests.exceptions.HTTPError as error:
|
|
||||||
raise RuntimeError(error)
|
|
||||||
|
|
||||||
def query(self):
|
|
||||||
result = make_request(method='GET', url=self.url)
|
|
||||||
status = result.status_code
|
|
||||||
logg.info(f'Get latest data status: {status}')
|
|
||||||
try:
|
|
||||||
if status == 200:
|
|
||||||
response_data = result.content
|
|
||||||
data = json.loads(response_data.decode())
|
|
||||||
|
|
||||||
# validate data
|
|
||||||
person = Person()
|
|
||||||
deserialized_person = person.deserialize(metadata=json.loads(data))
|
|
||||||
|
|
||||||
cache_data(key=self.metadata_pointer, data=json.dumps(deserialized_person.serialize()))
|
|
||||||
elif status == 404:
|
|
||||||
logg.info('The data is not available and might need to be added.')
|
|
||||||
result.raise_for_status()
|
|
||||||
except requests.exceptions.HTTPError as error:
|
|
||||||
raise RuntimeError(error)
|
|
||||||
@@ -5,6 +5,7 @@ import logging
|
|||||||
# third party imports
|
# third party imports
|
||||||
import celery
|
import celery
|
||||||
import i18n
|
import i18n
|
||||||
|
import phonenumbers
|
||||||
from cic_eth.api.api_task import Api
|
from cic_eth.api.api_task import Api
|
||||||
from tinydb.table import Document
|
from tinydb.table import Document
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -238,7 +239,7 @@ def persist_session_to_db_task(external_session_id: str, queue: str):
|
|||||||
:type queue: str
|
:type queue: str
|
||||||
"""
|
"""
|
||||||
s_persist_session_to_db = celery.signature(
|
s_persist_session_to_db = celery.signature(
|
||||||
'cic_ussd.tasks.ussd_session.persist_session_to_db',
|
'cic_ussd.tasks.ussd.persist_session_to_db',
|
||||||
[external_session_id]
|
[external_session_id]
|
||||||
)
|
)
|
||||||
s_persist_session_to_db.apply_async(queue=queue)
|
s_persist_session_to_db.apply_async(queue=queue)
|
||||||
@@ -452,3 +453,37 @@ def save_to_in_memory_ussd_session_data(queue: str, session_data: dict, ussd_ses
|
|||||||
)
|
)
|
||||||
persist_session_to_db_task(external_session_id=external_session_id, queue=queue)
|
persist_session_to_db_task(external_session_id=external_session_id, queue=queue)
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
:type phone_number: str
|
||||||
|
:param region: Caller defined region
|
||||||
|
:type region: str
|
||||||
|
:return: The parsed phone number value based on the defined region
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
if not isinstance(phone_number, str):
|
||||||
|
try:
|
||||||
|
phone_number = str(int(phone_number))
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
phone_number_object = phonenumbers.parse(phone_number, region)
|
||||||
|
parsed_phone_number = phonenumbers.format_number(phone_number_object, phonenumbers.PhoneNumberFormat.E164)
|
||||||
|
|
||||||
|
return parsed_phone_number
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_by_phone_number(phone_number: str) -> Optional[User]:
|
||||||
|
"""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: User|None
|
||||||
|
"""
|
||||||
|
# consider adding region to user's metadata
|
||||||
|
phone_number = process_phone_number(phone_number=phone_number, region='KE')
|
||||||
|
user = User.session.query(User).filter_by(phone_number=phone_number).first()
|
||||||
|
return user
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
import phonenumbers
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
from cic_ussd.db.models.user import User
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
|
||||||
:type phone_number: str
|
|
||||||
:param region: Caller defined region
|
|
||||||
:type region: str
|
|
||||||
:return: The parsed phone number value based on the defined region
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
if not isinstance(phone_number, str):
|
|
||||||
try:
|
|
||||||
phone_number = str(int(phone_number))
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
phone_number_object = phonenumbers.parse(phone_number, region)
|
|
||||||
parsed_phone_number = phonenumbers.format_number(phone_number_object, phonenumbers.PhoneNumberFormat.E164)
|
|
||||||
|
|
||||||
return parsed_phone_number
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_by_phone_number(phone_number: str) -> Optional[User]:
|
|
||||||
"""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: User|None
|
|
||||||
"""
|
|
||||||
# consider adding region to user's metadata
|
|
||||||
phone_number = process_phone_number(phone_number=phone_number, region='KE')
|
|
||||||
user = User.session.query(User).filter_by(phone_number=phone_number).first()
|
|
||||||
return user
|
|
||||||
@@ -1,26 +1,17 @@
|
|||||||
# standard imports
|
# standard imports
|
||||||
import logging
|
import logging
|
||||||
import json
|
|
||||||
import re
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
# third party imports
|
# third party imports
|
||||||
import celery
|
|
||||||
from cic_types.models.person import Person
|
|
||||||
from tinydb.table import Document
|
from tinydb.table import Document
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
from cic_ussd.account import define_account_tx_metadata, retrieve_account_statement
|
from cic_ussd.accounts import BalanceManager
|
||||||
from cic_ussd.balance import BalanceManager, compute_operational_balance, get_cached_operational_balance
|
|
||||||
from cic_ussd.chain import Chain
|
|
||||||
from cic_ussd.db.models.user import AccountStatus, User
|
from cic_ussd.db.models.user import AccountStatus, User
|
||||||
from cic_ussd.db.models.ussd_session import UssdSession
|
from cic_ussd.db.models.ussd_session import UssdSession
|
||||||
from cic_ussd.menu.ussd_menu import UssdMenu
|
from cic_ussd.menu.ussd_menu import UssdMenu
|
||||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
|
|
||||||
from cic_ussd.phone_number import get_user_by_phone_number
|
|
||||||
from cic_ussd.redis import cache_data, create_cached_data_key, get_cached_data
|
|
||||||
from cic_ussd.state_machine import UssdStateMachine
|
from cic_ussd.state_machine import UssdStateMachine
|
||||||
from cic_ussd.conversions import to_wei, from_wei
|
from cic_ussd.transactions import to_wei, from_wei
|
||||||
from cic_ussd.translation import translation_for
|
from cic_ussd.translation import translation_for
|
||||||
|
|
||||||
logg = logging.getLogger(__name__)
|
logg = logging.getLogger(__name__)
|
||||||
@@ -66,17 +57,17 @@ def process_exit_insufficient_balance(display_key: str, user: User, ussd_session
|
|||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
# get account balance
|
# get account balance
|
||||||
operational_balance = get_cached_operational_balance(blockchain_address=user.blockchain_address)
|
balance_manager = BalanceManager(address=user.blockchain_address,
|
||||||
|
chain_str=UssdStateMachine.chain_str,
|
||||||
|
token_symbol='SRF')
|
||||||
|
balance = balance_manager.get_operational_balance()
|
||||||
|
|
||||||
# compile response data
|
# compile response data
|
||||||
user_input = ussd_session.get('user_input').split('*')[-1]
|
user_input = ussd_session.get('user_input').split('*')[-1]
|
||||||
transaction_amount = to_wei(value=int(user_input))
|
transaction_amount = to_wei(value=int(user_input))
|
||||||
token_symbol = 'SRF'
|
token_symbol = 'SRF'
|
||||||
|
|
||||||
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
|
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
|
||||||
recipient = get_user_by_phone_number(phone_number=recipient_phone_number)
|
tx_recipient_information = recipient_phone_number
|
||||||
|
|
||||||
tx_recipient_information = define_account_tx_metadata(user=recipient)
|
|
||||||
|
|
||||||
return translation_for(
|
return translation_for(
|
||||||
key=display_key,
|
key=display_key,
|
||||||
@@ -84,7 +75,7 @@ def process_exit_insufficient_balance(display_key: str, user: User, ussd_session
|
|||||||
amount=from_wei(transaction_amount),
|
amount=from_wei(transaction_amount),
|
||||||
token_symbol=token_symbol,
|
token_symbol=token_symbol,
|
||||||
recipient_information=tx_recipient_information,
|
recipient_information=tx_recipient_information,
|
||||||
token_balance=operational_balance
|
token_balance=balance
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -102,9 +93,9 @@ def process_exit_successful_transaction(display_key: str, user: User, ussd_sessi
|
|||||||
transaction_amount = to_wei(int(ussd_session.get('session_data').get('transaction_amount')))
|
transaction_amount = to_wei(int(ussd_session.get('session_data').get('transaction_amount')))
|
||||||
token_symbol = 'SRF'
|
token_symbol = 'SRF'
|
||||||
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
|
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
|
||||||
recipient = get_user_by_phone_number(phone_number=recipient_phone_number)
|
sender_phone_number = user.phone_number
|
||||||
tx_recipient_information = define_account_tx_metadata(user=recipient)
|
tx_recipient_information = recipient_phone_number
|
||||||
tx_sender_information = define_account_tx_metadata(user=user)
|
tx_sender_information = sender_phone_number
|
||||||
|
|
||||||
return translation_for(
|
return translation_for(
|
||||||
key=display_key,
|
key=display_key,
|
||||||
@@ -131,10 +122,9 @@ def process_transaction_pin_authorization(user: User, display_key: str, ussd_ses
|
|||||||
"""
|
"""
|
||||||
# compile response data
|
# compile response data
|
||||||
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
|
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
|
||||||
recipient = get_user_by_phone_number(phone_number=recipient_phone_number)
|
tx_recipient_information = recipient_phone_number
|
||||||
tx_recipient_information = define_account_tx_metadata(user=recipient)
|
tx_sender_information = user.phone_number
|
||||||
tx_sender_information = define_account_tx_metadata(user=user)
|
logg.debug('Requires integration with cic-meta to get user name.')
|
||||||
|
|
||||||
token_symbol = 'SRF'
|
token_symbol = 'SRF'
|
||||||
user_input = ussd_session.get('user_input').split('*')[-1]
|
user_input = ussd_session.get('user_input').split('*')[-1]
|
||||||
transaction_amount = to_wei(value=int(user_input))
|
transaction_amount = to_wei(value=int(user_input))
|
||||||
@@ -149,122 +139,6 @@ def process_transaction_pin_authorization(user: User, display_key: str, ussd_ses
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def process_account_balances(user: User, display_key: str, ussd_session: dict):
|
|
||||||
"""
|
|
||||||
:param user:
|
|
||||||
:type user:
|
|
||||||
:param display_key:
|
|
||||||
:type display_key:
|
|
||||||
:param ussd_session:
|
|
||||||
:type ussd_session:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
# retrieve cached balance
|
|
||||||
operational_balance = get_cached_operational_balance(blockchain_address=user.blockchain_address)
|
|
||||||
|
|
||||||
logg.debug('Requires call to retrieve tax and bonus amounts')
|
|
||||||
tax = ''
|
|
||||||
bonus = ''
|
|
||||||
|
|
||||||
return translation_for(
|
|
||||||
key=display_key,
|
|
||||||
preferred_language=user.preferred_language,
|
|
||||||
operational_balance=operational_balance,
|
|
||||||
tax=tax,
|
|
||||||
bonus=bonus,
|
|
||||||
token_symbol='SRF'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def format_transactions(transactions: list, preferred_language: str):
|
|
||||||
|
|
||||||
formatted_transactions = ''
|
|
||||||
if len(transactions) > 0:
|
|
||||||
for transaction in transactions:
|
|
||||||
recipient_phone_number = transaction.get('recipient_phone_number')
|
|
||||||
sender_phone_number = transaction.get('sender_phone_number')
|
|
||||||
value = transaction.get('to_value')
|
|
||||||
timestamp = transaction.get('timestamp')
|
|
||||||
action_tag = transaction.get('action_tag')
|
|
||||||
token_symbol = 'SRF'
|
|
||||||
|
|
||||||
if action_tag == 'SENT' or action_tag == 'ULITUMA':
|
|
||||||
formatted_transactions += f'{action_tag} {value} {token_symbol} {recipient_phone_number} {timestamp}.\n'
|
|
||||||
else:
|
|
||||||
formatted_transactions += f'{action_tag} {value} {token_symbol} {sender_phone_number} {timestamp}. \n'
|
|
||||||
return formatted_transactions
|
|
||||||
else:
|
|
||||||
if preferred_language == 'en':
|
|
||||||
formatted_transactions = 'Empty'
|
|
||||||
else:
|
|
||||||
formatted_transactions = 'Hamna historia'
|
|
||||||
return formatted_transactions
|
|
||||||
|
|
||||||
|
|
||||||
def process_account_statement(user: User, display_key: str, ussd_session: dict):
|
|
||||||
"""
|
|
||||||
:param user:
|
|
||||||
:type user:
|
|
||||||
:param display_key:
|
|
||||||
:type display_key:
|
|
||||||
:param ussd_session:
|
|
||||||
:type ussd_session:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
# retrieve cached statement
|
|
||||||
identifier = blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address)
|
|
||||||
key = create_cached_data_key(identifier=identifier, salt='cic.statement')
|
|
||||||
transactions = get_cached_data(key=key)
|
|
||||||
|
|
||||||
first_transaction_set = []
|
|
||||||
middle_transaction_set = []
|
|
||||||
last_transaction_set = []
|
|
||||||
|
|
||||||
transactions = json.loads(transactions)
|
|
||||||
|
|
||||||
if len(transactions) > 6:
|
|
||||||
last_transaction_set += transactions[6:]
|
|
||||||
middle_transaction_set += transactions[3:][:3]
|
|
||||||
first_transaction_set += transactions[:3]
|
|
||||||
# there are probably much cleaner and operational inexpensive ways to do this so find them
|
|
||||||
elif 4 < len(transactions) < 7:
|
|
||||||
middle_transaction_set += transactions[3:]
|
|
||||||
first_transaction_set += transactions[:3]
|
|
||||||
else:
|
|
||||||
first_transaction_set += transactions[:3]
|
|
||||||
|
|
||||||
if display_key == 'ussd.kenya.first_transaction_set':
|
|
||||||
return translation_for(
|
|
||||||
key=display_key,
|
|
||||||
preferred_language=user.preferred_language,
|
|
||||||
first_transaction_set=format_transactions(
|
|
||||||
transactions=first_transaction_set,
|
|
||||||
preferred_language=user.preferred_language
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif display_key == 'ussd.kenya.middle_transaction_set':
|
|
||||||
return translation_for(
|
|
||||||
key=display_key,
|
|
||||||
preferred_language=user.preferred_language,
|
|
||||||
middle_transaction_set=format_transactions(
|
|
||||||
transactions=middle_transaction_set,
|
|
||||||
preferred_language=user.preferred_language
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
elif display_key == 'ussd.kenya.last_transaction_set':
|
|
||||||
return translation_for(
|
|
||||||
key=display_key,
|
|
||||||
preferred_language=user.preferred_language,
|
|
||||||
last_transaction_set=format_transactions(
|
|
||||||
transactions=last_transaction_set,
|
|
||||||
preferred_language=user.preferred_language
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def process_start_menu(display_key: str, user: User):
|
def process_start_menu(display_key: str, user: User):
|
||||||
"""This function gets data on an account's balance and token in order to append it to the start of the start menu's
|
"""This function gets data on an account's balance and token in order to append it to the start of the start menu's
|
||||||
title. It passes said arguments to the translation function and returns the appropriate corresponding text from the
|
title. It passes said arguments to the translation function and returns the appropriate corresponding text from the
|
||||||
@@ -276,41 +150,16 @@ def process_start_menu(display_key: str, user: User):
|
|||||||
:return: Corresponding translation text response
|
:return: Corresponding translation text response
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
chain_str = Chain.spec.__str__()
|
balance_manager = BalanceManager(address=user.blockchain_address,
|
||||||
blockchain_address = user.blockchain_address
|
chain_str=UssdStateMachine.chain_str,
|
||||||
balance_manager = BalanceManager(address=blockchain_address,
|
|
||||||
chain_str=chain_str,
|
|
||||||
token_symbol='SRF')
|
token_symbol='SRF')
|
||||||
|
balance = balance_manager.get_operational_balance()
|
||||||
# get balances synchronously for display on start menu
|
|
||||||
balances_data = balance_manager.get_balances()
|
|
||||||
|
|
||||||
key = create_cached_data_key(
|
|
||||||
identifier=bytes.fromhex(blockchain_address[2:]),
|
|
||||||
salt='cic.balances_data'
|
|
||||||
)
|
|
||||||
cache_data(key=key, data=json.dumps(balances_data))
|
|
||||||
|
|
||||||
# get operational balance
|
|
||||||
operational_balance = compute_operational_balance(balances=balances_data)
|
|
||||||
|
|
||||||
# retrieve and cache account's metadata
|
|
||||||
s_query_user_metadata = celery.signature(
|
|
||||||
'cic_ussd.tasks.metadata.query_user_metadata',
|
|
||||||
[blockchain_address]
|
|
||||||
)
|
|
||||||
s_query_user_metadata.apply_async(queue='cic-ussd')
|
|
||||||
|
|
||||||
# retrieve and cache account's statement
|
|
||||||
retrieve_account_statement(blockchain_address=blockchain_address)
|
|
||||||
|
|
||||||
# TODO [Philip]: figure out how to get token symbol from a metadata layer of sorts.
|
|
||||||
token_symbol = 'SRF'
|
token_symbol = 'SRF'
|
||||||
|
logg.debug("Requires integration to determine user's balance and token.")
|
||||||
return translation_for(
|
return translation_for(
|
||||||
key=display_key,
|
key=display_key,
|
||||||
preferred_language=user.preferred_language,
|
preferred_language=user.preferred_language,
|
||||||
account_balance=operational_balance,
|
account_balance=balance,
|
||||||
account_token_name=token_symbol
|
account_token_name=token_symbol
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -392,11 +241,5 @@ def custom_display_text(
|
|||||||
return process_exit_successful_transaction(display_key=display_key, user=user, ussd_session=ussd_session)
|
return process_exit_successful_transaction(display_key=display_key, user=user, ussd_session=ussd_session)
|
||||||
elif menu_name == 'start':
|
elif menu_name == 'start':
|
||||||
return process_start_menu(display_key=display_key, user=user)
|
return process_start_menu(display_key=display_key, user=user)
|
||||||
elif 'pin_authorization' in menu_name:
|
|
||||||
return process_pin_authorization(display_key=display_key, user=user)
|
|
||||||
elif menu_name == 'account_balances':
|
|
||||||
return process_account_balances(display_key=display_key, user=user, ussd_session=ussd_session)
|
|
||||||
elif 'transaction_set' in menu_name:
|
|
||||||
return process_account_statement(display_key=display_key, user=user, ussd_session=ussd_session)
|
|
||||||
else:
|
else:
|
||||||
return translation_for(key=display_key, preferred_language=user.preferred_language)
|
return translation_for(key=display_key, preferred_language=user.preferred_language)
|
||||||
|
|||||||
@@ -1,52 +1,6 @@
|
|||||||
# standard imports
|
|
||||||
import hashlib
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# third-party imports
|
# third-party imports
|
||||||
from redis import Redis
|
from redis import Redis
|
||||||
|
|
||||||
logg = logging.getLogger()
|
|
||||||
|
|
||||||
|
|
||||||
class InMemoryStore:
|
class InMemoryStore:
|
||||||
cache: Redis = None
|
cache: Redis = None
|
||||||
|
|
||||||
|
|
||||||
def cache_data(key: str, data: str):
|
|
||||||
"""
|
|
||||||
:param key:
|
|
||||||
:type key:
|
|
||||||
:param data:
|
|
||||||
:type data:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
cache = InMemoryStore.cache
|
|
||||||
cache.set(name=key, value=data)
|
|
||||||
cache.persist(name=key)
|
|
||||||
|
|
||||||
|
|
||||||
def get_cached_data(key: str):
|
|
||||||
"""
|
|
||||||
:param key:
|
|
||||||
:type key:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
cache = InMemoryStore.cache
|
|
||||||
return cache.get(name=key)
|
|
||||||
|
|
||||||
|
|
||||||
def create_cached_data_key(identifier: bytes, salt: str):
|
|
||||||
"""
|
|
||||||
:param identifier:
|
|
||||||
:type identifier:
|
|
||||||
:param salt:
|
|
||||||
:type salt:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
hash_object = hashlib.new("sha256")
|
|
||||||
hash_object.update(identifier)
|
|
||||||
hash_object.update(salt.encode(encoding="utf-8"))
|
|
||||||
return hash_object.digest().hex()
|
|
||||||
|
|||||||
@@ -12,18 +12,14 @@ import redis
|
|||||||
|
|
||||||
# third-party imports
|
# third-party imports
|
||||||
from confini import Config
|
from confini import Config
|
||||||
from chainlib.chain import ChainSpec
|
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
from cic_ussd.chain import Chain
|
|
||||||
from cic_ussd.db import dsn_from_config
|
from cic_ussd.db import dsn_from_config
|
||||||
from cic_ussd.db.models.base import SessionBase
|
from cic_ussd.db.models.base import SessionBase
|
||||||
from cic_ussd.encoder import PasswordEncoder
|
from cic_ussd.encoder import PasswordEncoder
|
||||||
from cic_ussd.files.local_files import create_local_file_data_stores, json_file_parser
|
from cic_ussd.files.local_files import create_local_file_data_stores, json_file_parser
|
||||||
from cic_ussd.menu.ussd_menu import UssdMenu
|
from cic_ussd.menu.ussd_menu import UssdMenu
|
||||||
from cic_ussd.metadata.signer import Signer
|
|
||||||
from cic_ussd.metadata.user import UserMetadata
|
|
||||||
from cic_ussd.operations import (define_response_with_content,
|
from cic_ussd.operations import (define_response_with_content,
|
||||||
process_menu_interaction_requests,
|
process_menu_interaction_requests,
|
||||||
define_multilingual_responses)
|
define_multilingual_responses)
|
||||||
@@ -63,7 +59,6 @@ config.censor('PASSWORD', 'DATABASE')
|
|||||||
# define log levels
|
# define log levels
|
||||||
if args.vv:
|
if args.vv:
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
logging.getLogger('sqlalchemy.engine').setLevel(logging.DEBUG)
|
|
||||||
elif args.v:
|
elif args.v:
|
||||||
logging.getLogger().setLevel(logging.INFO)
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
|
|
||||||
@@ -97,14 +92,6 @@ InMemoryStore.cache = redis.StrictRedis(host=config.get('REDIS_HOSTNAME'),
|
|||||||
decode_responses=True)
|
decode_responses=True)
|
||||||
InMemoryUssdSession.redis_cache = InMemoryStore.cache
|
InMemoryUssdSession.redis_cache = InMemoryStore.cache
|
||||||
|
|
||||||
# define metadata URL
|
|
||||||
UserMetadata.base_url = config.get('CIC_META_URL')
|
|
||||||
|
|
||||||
# define signer values
|
|
||||||
Signer.gpg_path = config.get('PGP_EXPORT_DIR')
|
|
||||||
Signer.gpg_passphrase = config.get('PGP_PASSPHRASE')
|
|
||||||
Signer.key_file_path = f"{config.get('PGP_KEYS_PATH')}{config.get('PGP_PRIVATE_KEYS')}"
|
|
||||||
|
|
||||||
# initialize celery app
|
# initialize celery app
|
||||||
celery.Celery(backend=config.get('CELERY_RESULT_URL'), broker=config.get('CELERY_BROKER_URL'))
|
celery.Celery(backend=config.get('CELERY_RESULT_URL'), broker=config.get('CELERY_BROKER_URL'))
|
||||||
|
|
||||||
@@ -112,13 +99,7 @@ celery.Celery(backend=config.get('CELERY_RESULT_URL'), broker=config.get('CELERY
|
|||||||
states = json_file_parser(filepath=config.get('STATEMACHINE_STATES'))
|
states = json_file_parser(filepath=config.get('STATEMACHINE_STATES'))
|
||||||
transitions = json_file_parser(filepath=config.get('STATEMACHINE_TRANSITIONS'))
|
transitions = json_file_parser(filepath=config.get('STATEMACHINE_TRANSITIONS'))
|
||||||
|
|
||||||
chain_spec = ChainSpec(
|
UssdStateMachine.chain_str = config.get('CIC_CHAIN_SPEC')
|
||||||
common_name=config.get('CIC_COMMON_NAME'),
|
|
||||||
engine=config.get('CIC_ENGINE'),
|
|
||||||
network_id=config.get('CIC_NETWORK_ID')
|
|
||||||
)
|
|
||||||
|
|
||||||
Chain.spec = chain_spec
|
|
||||||
UssdStateMachine.states = states
|
UssdStateMachine.states = states
|
||||||
UssdStateMachine.transitions = transitions
|
UssdStateMachine.transitions = transitions
|
||||||
|
|
||||||
@@ -171,8 +152,7 @@ def application(env, start_response):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
# handle menu interaction requests
|
# handle menu interaction requests
|
||||||
chain_str = chain_spec.__str__()
|
response = process_menu_interaction_requests(chain_str=config.get('CIC_CHAIN_SPEC'),
|
||||||
response = process_menu_interaction_requests(chain_str=chain_str,
|
|
||||||
external_session_id=external_session_id,
|
external_session_id=external_session_id,
|
||||||
phone_number=phone_number,
|
phone_number=phone_number,
|
||||||
queue=args.q,
|
queue=args.q,
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ from confini import Config
|
|||||||
# local imports
|
# local imports
|
||||||
from cic_ussd.db import dsn_from_config
|
from cic_ussd.db import dsn_from_config
|
||||||
from cic_ussd.db.models.base import SessionBase
|
from cic_ussd.db.models.base import SessionBase
|
||||||
from cic_ussd.metadata.signer import Signer
|
|
||||||
from cic_ussd.metadata.user import UserMetadata
|
|
||||||
from cic_ussd.redis import InMemoryStore
|
from cic_ussd.redis import InMemoryStore
|
||||||
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
|
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
|
||||||
|
|
||||||
@@ -61,14 +59,6 @@ InMemoryStore.cache = redis.StrictRedis(host=config.get('REDIS_HOSTNAME'),
|
|||||||
decode_responses=True)
|
decode_responses=True)
|
||||||
InMemoryUssdSession.redis_cache = InMemoryStore.cache
|
InMemoryUssdSession.redis_cache = InMemoryStore.cache
|
||||||
|
|
||||||
# define metadata URL
|
|
||||||
UserMetadata.base_url = config.get('CIC_META_URL')
|
|
||||||
|
|
||||||
# define signer values
|
|
||||||
Signer.gpg_path = config.get('PGP_EXPORT_DIR')
|
|
||||||
Signer.gpg_passphrase = config.get('PGP_PASSPHRASE')
|
|
||||||
Signer.key_file_path = f"{config.get('PGP_KEYS_PATH')}{config.get('PGP_PRIVATE_KEYS')}"
|
|
||||||
|
|
||||||
# set up celery
|
# set up celery
|
||||||
current_app = celery.Celery(__name__)
|
current_app = celery.Celery(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
# standard imports
|
# standard imports
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
# third party imports
|
# third party imports
|
||||||
import celery
|
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
from cic_ussd.balance import BalanceManager, compute_operational_balance
|
from cic_ussd.accounts import BalanceManager
|
||||||
from cic_ussd.chain import Chain
|
|
||||||
from cic_ussd.db.models.user import AccountStatus, User
|
from cic_ussd.db.models.user import AccountStatus, User
|
||||||
from cic_ussd.operations import save_to_in_memory_ussd_session_data
|
from cic_ussd.operations import get_user_by_phone_number, save_to_in_memory_ussd_session_data
|
||||||
from cic_ussd.phone_number import get_user_by_phone_number
|
from cic_ussd.state_machine.state_machine import UssdStateMachine
|
||||||
from cic_ussd.redis import create_cached_data_key, get_cached_data
|
|
||||||
from cic_ussd.transactions import OutgoingTransactionProcessor
|
from cic_ussd.transactions import OutgoingTransactionProcessor
|
||||||
|
|
||||||
|
|
||||||
@@ -31,7 +27,22 @@ def is_valid_recipient(state_machine_data: Tuple[str, dict, User]) -> bool:
|
|||||||
recipient = get_user_by_phone_number(phone_number=user_input)
|
recipient = get_user_by_phone_number(phone_number=user_input)
|
||||||
is_not_initiator = user_input != user.phone_number
|
is_not_initiator = user_input != user.phone_number
|
||||||
has_active_account_status = user.get_account_status() == AccountStatus.ACTIVE.name
|
has_active_account_status = user.get_account_status() == AccountStatus.ACTIVE.name
|
||||||
return is_not_initiator and has_active_account_status and recipient is not None
|
logg.debug('This section requires implementation of checks for user roles and authorization status of an account.')
|
||||||
|
return is_not_initiator and has_active_account_status
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_token_agent(state_machine_data: Tuple[str, dict, User]) -> 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 exchange transactions.
|
||||||
|
:param state_machine_data: A tuple containing user input, a ussd session and user object.
|
||||||
|
:type state_machine_data: tuple
|
||||||
|
:return: A user's validity
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
user_input, ussd_session, user = state_machine_data
|
||||||
|
# is_token_agent = AccountRole.TOKEN_AGENT.value in user.get_user_roles()
|
||||||
|
logg.debug('This section requires implementation of user roles and authorization to facilitate exchanges.')
|
||||||
|
return is_valid_recipient(state_machine_data=state_machine_data)
|
||||||
|
|
||||||
|
|
||||||
def is_valid_transaction_amount(state_machine_data: Tuple[str, dict, User]) -> bool:
|
def is_valid_transaction_amount(state_machine_data: Tuple[str, dict, User]) -> bool:
|
||||||
@@ -59,17 +70,10 @@ def has_sufficient_balance(state_machine_data: Tuple[str, dict, User]) -> bool:
|
|||||||
"""
|
"""
|
||||||
user_input, ussd_session, user = state_machine_data
|
user_input, ussd_session, user = state_machine_data
|
||||||
balance_manager = BalanceManager(address=user.blockchain_address,
|
balance_manager = BalanceManager(address=user.blockchain_address,
|
||||||
chain_str=Chain.spec.__str__(),
|
chain_str=UssdStateMachine.chain_str,
|
||||||
token_symbol='SRF')
|
token_symbol='SRF')
|
||||||
# get cached balance
|
balance = balance_manager.get_operational_balance()
|
||||||
key = create_cached_data_key(
|
return int(user_input) <= balance
|
||||||
identifier=bytes.fromhex(user.blockchain_address[2:]),
|
|
||||||
salt='cic.balances_data'
|
|
||||||
)
|
|
||||||
cached_balance = get_cached_data(key=key)
|
|
||||||
operational_balance = compute_operational_balance(balances=json.loads(cached_balance))
|
|
||||||
|
|
||||||
return int(user_input) <= operational_balance
|
|
||||||
|
|
||||||
|
|
||||||
def save_recipient_phone_to_session_data(state_machine_data: Tuple[str, dict, User]):
|
def save_recipient_phone_to_session_data(state_machine_data: Tuple[str, dict, User]):
|
||||||
@@ -84,25 +88,6 @@ def save_recipient_phone_to_session_data(state_machine_data: Tuple[str, dict, Us
|
|||||||
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_data=session_data, ussd_session=ussd_session)
|
||||||
|
|
||||||
|
|
||||||
def retrieve_recipient_metadata(state_machine_data: Tuple[str, dict, User]):
|
|
||||||
"""
|
|
||||||
:param state_machine_data:
|
|
||||||
:type state_machine_data:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
user_input, ussd_session, user = state_machine_data
|
|
||||||
|
|
||||||
recipient = get_user_by_phone_number(phone_number=user_input)
|
|
||||||
blockchain_address = recipient.blockchain_address
|
|
||||||
# retrieve and cache account's metadata
|
|
||||||
s_query_user_metadata = celery.signature(
|
|
||||||
'cic_ussd.tasks.metadata.query_user_metadata',
|
|
||||||
[blockchain_address]
|
|
||||||
)
|
|
||||||
s_query_user_metadata.apply_async(queue='cic-ussd')
|
|
||||||
|
|
||||||
|
|
||||||
def save_transaction_amount_to_session_data(state_machine_data: Tuple[str, dict, User]):
|
def save_transaction_amount_to_session_data(state_machine_data: Tuple[str, dict, User]):
|
||||||
"""This function saves the phone number corresponding the intended recipients blockchain account.
|
"""This function saves the phone number corresponding the intended recipients blockchain account.
|
||||||
:param state_machine_data: A tuple containing user input, a ussd session and user object.
|
:param state_machine_data: A tuple containing user input, a ussd session and user object.
|
||||||
@@ -128,8 +113,7 @@ def process_transaction_request(state_machine_data: Tuple[str, dict, User]):
|
|||||||
to_address = recipient.blockchain_address
|
to_address = recipient.blockchain_address
|
||||||
from_address = user.blockchain_address
|
from_address = user.blockchain_address
|
||||||
amount = int(ussd_session.get('session_data').get('transaction_amount'))
|
amount = int(ussd_session.get('session_data').get('transaction_amount'))
|
||||||
chain_str = Chain.spec.__str__()
|
outgoing_tx_processor = OutgoingTransactionProcessor(chain_str=UssdStateMachine.chain_str,
|
||||||
outgoing_tx_processor = OutgoingTransactionProcessor(chain_str=chain_str,
|
|
||||||
from_address=from_address,
|
from_address=from_address,
|
||||||
to_address=to_address)
|
to_address=to_address)
|
||||||
outgoing_tx_processor.process_outgoing_transfer_transaction(amount=amount)
|
outgoing_tx_processor.process_outgoing_transfer_transaction(amount=amount)
|
||||||
|
|||||||
@@ -1,20 +1,10 @@
|
|||||||
# standard imports
|
# standard imports
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Tuple
|
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_vcard_from_contact_data, manage_identity_data
|
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
from cic_ussd.chain import Chain
|
|
||||||
from cic_ussd.db.models.user import User
|
from cic_ussd.db.models.user import User
|
||||||
from cic_ussd.error import UserMetadataNotFoundError
|
|
||||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
|
|
||||||
from cic_ussd.operations import save_to_in_memory_ussd_session_data
|
from cic_ussd.operations import save_to_in_memory_ussd_session_data
|
||||||
from cic_ussd.redis import get_cached_data
|
|
||||||
|
|
||||||
logg = logging.getLogger(__file__)
|
logg = logging.getLogger(__file__)
|
||||||
|
|
||||||
@@ -52,29 +42,7 @@ def update_account_status_to_active(state_machine_data: Tuple[str, dict, User]):
|
|||||||
User.session.commit()
|
User.session.commit()
|
||||||
|
|
||||||
|
|
||||||
def process_gender_user_input(user: User, user_input: str):
|
def save_profile_attribute_to_session_data(state_machine_data: Tuple[str, dict, User]):
|
||||||
"""
|
|
||||||
:param user:
|
|
||||||
:type user:
|
|
||||||
:param user_input:
|
|
||||||
:type user_input:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
if user.preferred_language == 'en':
|
|
||||||
if user_input == '1':
|
|
||||||
gender = 'Male'
|
|
||||||
else:
|
|
||||||
gender = 'Female'
|
|
||||||
else:
|
|
||||||
if user_input == '1':
|
|
||||||
gender = 'Mwanaume'
|
|
||||||
else:
|
|
||||||
gender = 'Mwanamke'
|
|
||||||
return gender
|
|
||||||
|
|
||||||
|
|
||||||
def save_metadata_attribute_to_session_data(state_machine_data: Tuple[str, dict, User]):
|
|
||||||
"""This function saves first name data to the ussd session in the redis cache.
|
"""This function saves first name data to the ussd session in the redis cache.
|
||||||
:param state_machine_data: A tuple containing user input, a ussd session and user object.
|
:param state_machine_data: A tuple containing user input, a ussd session and user object.
|
||||||
:type state_machine_data: tuple
|
:type state_machine_data: tuple
|
||||||
@@ -86,17 +54,16 @@ def save_metadata_attribute_to_session_data(state_machine_data: Tuple[str, dict,
|
|||||||
|
|
||||||
# define session data key from current state
|
# define session data key from current state
|
||||||
key = ''
|
key = ''
|
||||||
if 'given_name' in current_state:
|
if 'first_name' in current_state:
|
||||||
key = 'given_name'
|
key = 'first_name'
|
||||||
elif 'family_name' in current_state:
|
elif 'last_name' in current_state:
|
||||||
key = 'family_name'
|
key = 'last_name'
|
||||||
elif 'gender' in current_state:
|
elif 'gender' in current_state:
|
||||||
key = 'gender'
|
key = 'gender'
|
||||||
user_input = process_gender_user_input(user=user, user_input=user_input)
|
|
||||||
elif 'location' in current_state:
|
elif 'location' in current_state:
|
||||||
key = 'location'
|
key = 'location'
|
||||||
elif 'products' in current_state:
|
elif 'business_profile' in current_state:
|
||||||
key = 'products'
|
key = 'business_profile'
|
||||||
|
|
||||||
# check if there is existing session data
|
# check if there is existing session data
|
||||||
if ussd_session.get('session_data'):
|
if ussd_session.get('session_data'):
|
||||||
@@ -109,120 +76,14 @@ def save_metadata_attribute_to_session_data(state_machine_data: Tuple[str, dict,
|
|||||||
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_data=session_data, ussd_session=ussd_session)
|
||||||
|
|
||||||
|
|
||||||
def format_user_metadata(metadata: dict, user: User):
|
def persist_profile_data(state_machine_data: Tuple[str, dict, User]):
|
||||||
"""
|
"""This function persists elements of the user profile stored in session data
|
||||||
:param metadata:
|
|
||||||
:type metadata:
|
|
||||||
:param user:
|
|
||||||
:type user:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
gender = metadata.get('gender')
|
|
||||||
given_name = metadata.get('given_name')
|
|
||||||
family_name = metadata.get('family_name')
|
|
||||||
location = {
|
|
||||||
"area_name": metadata.get('location')
|
|
||||||
}
|
|
||||||
products = []
|
|
||||||
if metadata.get('products'):
|
|
||||||
products = metadata.get('products').split(',')
|
|
||||||
phone_number = user.phone_number
|
|
||||||
date_registered = int(user.created.replace().timestamp())
|
|
||||||
blockchain_address = user.blockchain_address
|
|
||||||
chain_spec = f'{Chain.spec.common_name()}:{Chain.spec.network_id()}'
|
|
||||||
identities = manage_identity_data(
|
|
||||||
blockchain_address=blockchain_address,
|
|
||||||
blockchain_type=Chain.spec.engine(),
|
|
||||||
chain_spec=chain_spec
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"date_registered": date_registered,
|
|
||||||
"gender": gender,
|
|
||||||
"identities": identities,
|
|
||||||
"location": location,
|
|
||||||
"products": products,
|
|
||||||
"vcard": generate_vcard_from_contact_data(
|
|
||||||
family_name=family_name,
|
|
||||||
given_name=given_name,
|
|
||||||
tel=phone_number
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def save_complete_user_metadata(state_machine_data: Tuple[str, dict, User]):
|
|
||||||
"""This function persists elements of the user metadata stored in session data
|
|
||||||
:param state_machine_data: A tuple containing user input, a ussd session and user object.
|
:param state_machine_data: A tuple containing user input, a ussd session and user object.
|
||||||
:type state_machine_data: tuple
|
:type state_machine_data: tuple
|
||||||
"""
|
"""
|
||||||
user_input, ussd_session, user = state_machine_data
|
user_input, ussd_session, user = state_machine_data
|
||||||
|
|
||||||
# get session data
|
# get session data
|
||||||
metadata = ussd_session.get('session_data')
|
profile_data = ussd_session.get('session_data')
|
||||||
|
logg.debug('This section requires implementation of user metadata.')
|
||||||
|
|
||||||
# format metadata appropriately
|
|
||||||
user_metadata = format_user_metadata(metadata=metadata, user=user)
|
|
||||||
|
|
||||||
blockchain_address = user.blockchain_address
|
|
||||||
s_create_user_metadata = celery.signature(
|
|
||||||
'cic_ussd.tasks.metadata.create_user_metadata',
|
|
||||||
[blockchain_address, user_metadata]
|
|
||||||
)
|
|
||||||
s_create_user_metadata.apply_async(queue='cic-ussd')
|
|
||||||
|
|
||||||
|
|
||||||
def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, User]):
|
|
||||||
user_input, ussd_session, user = state_machine_data
|
|
||||||
blockchain_address = user.blockchain_address
|
|
||||||
key = generate_metadata_pointer(
|
|
||||||
identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address),
|
|
||||||
cic_type='cic.person'
|
|
||||||
)
|
|
||||||
user_metadata = get_cached_data(key=key)
|
|
||||||
|
|
||||||
if not user_metadata:
|
|
||||||
raise UserMetadataNotFoundError(f'Expected user metadata but found none in cache for key: {blockchain_address}')
|
|
||||||
|
|
||||||
given_name = ussd_session.get('session_data').get('given_name')
|
|
||||||
family_name = ussd_session.get('session_data').get('family_name')
|
|
||||||
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)
|
|
||||||
deserialized_person = person.deserialize(metadata=user_metadata)
|
|
||||||
|
|
||||||
# edit specific metadata attribute
|
|
||||||
if given_name:
|
|
||||||
deserialized_person.given_name = given_name
|
|
||||||
elif family_name:
|
|
||||||
deserialized_person.family_name = family_name
|
|
||||||
elif gender:
|
|
||||||
deserialized_person.gender = gender
|
|
||||||
elif location:
|
|
||||||
# get existing location metadata:
|
|
||||||
location_data = user_metadata.get('location')
|
|
||||||
location_data['area_name'] = location
|
|
||||||
deserialized_person.location = location_data
|
|
||||||
elif products:
|
|
||||||
deserialized_person.products = products
|
|
||||||
|
|
||||||
edited_metadata = deserialized_person.serialize()
|
|
||||||
|
|
||||||
s_edit_user_metadata = celery.signature(
|
|
||||||
'cic_ussd.tasks.metadata.edit_user_metadata',
|
|
||||||
[blockchain_address, edited_metadata, 'pgp']
|
|
||||||
)
|
|
||||||
s_edit_user_metadata.apply_async(queue='cic-ussd')
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_metadata(state_machine_data: Tuple[str, dict, User]):
|
|
||||||
user_input, ussd_session, user = state_machine_data
|
|
||||||
blockchain_address = user.blockchain_address
|
|
||||||
s_get_user_metadata = celery.signature(
|
|
||||||
'cic_ussd.tasks.metadata.query_user_metadata',
|
|
||||||
[blockchain_address]
|
|
||||||
)
|
|
||||||
s_get_user_metadata.apply_async(queue='cic-ussd')
|
|
||||||
|
|||||||
@@ -3,30 +3,55 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
from cic_types.models.person import generate_metadata_pointer
|
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
from cic_ussd.db.models.user import User
|
from cic_ussd.db.models.user import User
|
||||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
|
|
||||||
from cic_ussd.redis import get_cached_data
|
|
||||||
|
|
||||||
logg = logging.getLogger()
|
logg = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
def has_cached_user_metadata(state_machine_data: Tuple[str, dict, User]):
|
def has_complete_profile_data(state_machine_data: Tuple[str, dict, User]):
|
||||||
"""This function checks whether the attributes of the user's metadata constituting a profile are filled out.
|
"""This function checks whether the attributes of the user's metadata constituting a profile are filled out.
|
||||||
:param state_machine_data: A tuple containing user input, a ussd session and user object.
|
:param state_machine_data: A tuple containing user input, a ussd session and user object.
|
||||||
:type state_machine_data: str
|
:type state_machine_data: str
|
||||||
"""
|
"""
|
||||||
user_input, ussd_session, user = state_machine_data
|
user_input, ussd_session, user = state_machine_data
|
||||||
# check for user metadata in cache
|
logg.debug('This section requires implementation of user metadata.')
|
||||||
key = generate_metadata_pointer(
|
|
||||||
identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address),
|
|
||||||
cic_type='cic.person'
|
def has_empty_username_data(state_machine_data: Tuple[str, dict, User]):
|
||||||
)
|
"""This function checks whether the aspects of the user's name metadata is filled out.
|
||||||
user_metadata = get_cached_data(key=key)
|
:param state_machine_data: A tuple containing user input, a ussd session and user object.
|
||||||
return user_metadata is not None
|
:type state_machine_data: str
|
||||||
|
"""
|
||||||
|
user_input, ussd_session, user = state_machine_data
|
||||||
|
logg.debug('This section requires implementation of user metadata.')
|
||||||
|
|
||||||
|
|
||||||
|
def has_empty_gender_data(state_machine_data: Tuple[str, dict, User]):
|
||||||
|
"""This function checks whether the aspects of the user's gender metadata is filled out.
|
||||||
|
: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
|
||||||
|
logg.debug('This section requires implementation of user metadata.')
|
||||||
|
|
||||||
|
|
||||||
|
def has_empty_location_data(state_machine_data: Tuple[str, dict, User]):
|
||||||
|
"""This function checks whether the aspects of the user's location metadata is filled out.
|
||||||
|
: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
|
||||||
|
logg.debug('This section requires implementation of user metadata.')
|
||||||
|
|
||||||
|
|
||||||
|
def has_empty_business_profile_data(state_machine_data: Tuple[str, dict, User]):
|
||||||
|
"""This function checks whether the aspects of the user's business profile metadata is filled out.
|
||||||
|
: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
|
||||||
|
logg.debug('This section requires implementation of user metadata.')
|
||||||
|
|
||||||
|
|
||||||
def is_valid_name(state_machine_data: Tuple[str, dict, User]):
|
def is_valid_name(state_machine_data: Tuple[str, dict, User]):
|
||||||
@@ -41,18 +66,3 @@ def is_valid_name(state_machine_data: Tuple[str, dict, User]):
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_valid_gender_selection(state_machine_data: Tuple[str, dict, User]):
|
|
||||||
"""
|
|
||||||
:param state_machine_data:
|
|
||||||
:type state_machine_data:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
user_input, ussd_session, user = state_machine_data
|
|
||||||
selection_matcher = "^[1-2]$"
|
|
||||||
if re.match(selection_matcher, user_input):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|||||||
@@ -14,11 +14,15 @@ class UssdStateMachine(Machine):
|
|||||||
menu as well as providing a means for navigating through these states based on different user inputs.
|
menu as well as providing a means for navigating through these states based on different user inputs.
|
||||||
It defines different helper functions that co-ordinate with the stakeholder components of the ussd menu: i.e the
|
It defines different helper functions that co-ordinate with the stakeholder components of the ussd menu: i.e the
|
||||||
User, UssdSession, UssdMenu to facilitate user interaction with ussd menu.
|
User, UssdSession, UssdMenu to facilitate user interaction with ussd menu.
|
||||||
|
|
||||||
|
:cvar chain_str: The chain name and network id.
|
||||||
|
:type chain_str: str
|
||||||
:cvar states: A list of pre-defined states.
|
:cvar states: A list of pre-defined states.
|
||||||
:type states: list
|
:type states: list
|
||||||
:cvar transitions: A list of pre-defined transitions.
|
:cvar transitions: A list of pre-defined transitions.
|
||||||
:type transitions: list
|
:type transitions: list
|
||||||
"""
|
"""
|
||||||
|
chain_str = None
|
||||||
states = []
|
states = []
|
||||||
transitions = []
|
transitions = []
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import celery
|
|||||||
|
|
||||||
celery_app = celery.current_app
|
celery_app = celery.current_app
|
||||||
# export external celery task modules
|
# export external celery task modules
|
||||||
from .logger import *
|
from .foo import log_it_plz
|
||||||
from .ussd_session import *
|
from .ussd import persist_session_to_db
|
||||||
from .callback_handler import *
|
from .callback_handler import process_account_creation_callback
|
||||||
from .metadata import *
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
import celery
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
|
|
||||||
|
|
||||||
class CriticalTask(celery.Task):
|
|
||||||
retry_jitter = True
|
|
||||||
retry_backoff = True
|
|
||||||
retry_backoff_max = 8
|
|
||||||
|
|
||||||
|
|
||||||
class CriticalSQLAlchemyTask(CriticalTask):
|
|
||||||
autoretry_for = (
|
|
||||||
sqlalchemy.exc.DatabaseError,
|
|
||||||
sqlalchemy.exc.TimeoutError,
|
|
||||||
)
|
|
||||||
@@ -1,26 +1,23 @@
|
|||||||
# standard imports
|
# standard imports
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
# third-party imports
|
# third-party imports
|
||||||
import celery
|
import celery
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
from cic_ussd.conversions import from_wei
|
|
||||||
from cic_ussd.db.models.base import SessionBase
|
from cic_ussd.db.models.base import SessionBase
|
||||||
from cic_ussd.db.models.user import User
|
from cic_ussd.db.models.user import User
|
||||||
from cic_ussd.account import define_account_tx_metadata
|
|
||||||
from cic_ussd.error import ActionDataNotFoundError
|
from cic_ussd.error import ActionDataNotFoundError
|
||||||
from cic_ussd.redis import InMemoryStore, cache_data, create_cached_data_key
|
from cic_ussd.redis import InMemoryStore
|
||||||
from cic_ussd.tasks.base import CriticalSQLAlchemyTask
|
|
||||||
from cic_ussd.transactions import IncomingTransactionProcessor
|
from cic_ussd.transactions import IncomingTransactionProcessor
|
||||||
|
|
||||||
logg = logging.getLogger(__file__)
|
logg = logging.getLogger(__file__)
|
||||||
celery_app = celery.current_app
|
celery_app = celery.current_app
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task(bind=True, base=CriticalSQLAlchemyTask)
|
@celery_app.task(bind=True)
|
||||||
def process_account_creation_callback(self, result: str, url: str, status_code: int):
|
def process_account_creation_callback(self, result: str, url: str, status_code: int):
|
||||||
"""This function defines a task that creates a user and
|
"""This function defines a task that creates a user and
|
||||||
:param result: The blockchain address for the created account
|
:param result: The blockchain address for the created account
|
||||||
@@ -52,14 +49,14 @@ def process_account_creation_callback(self, result: str, url: str, status_code:
|
|||||||
user = User(blockchain_address=result, phone_number=phone_number)
|
user = User(blockchain_address=result, phone_number=phone_number)
|
||||||
session.add(user)
|
session.add(user)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.close()
|
|
||||||
|
|
||||||
# expire cache
|
# expire cache
|
||||||
cache.expire(task_id, timedelta(seconds=180))
|
cache.expire(task_id, timedelta(seconds=30))
|
||||||
|
session.close()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
cache.expire(task_id, timedelta(seconds=30))
|
||||||
session.close()
|
session.close()
|
||||||
cache.expire(task_id, timedelta(seconds=180))
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
session.close()
|
session.close()
|
||||||
@@ -68,8 +65,9 @@ def process_account_creation_callback(self, result: str, url: str, status_code:
|
|||||||
|
|
||||||
@celery_app.task
|
@celery_app.task
|
||||||
def process_incoming_transfer_callback(result: dict, param: str, status_code: int):
|
def process_incoming_transfer_callback(result: dict, param: str, status_code: int):
|
||||||
|
logg.debug(f'PARAM: {param}, RESULT: {result}, STATUS_CODE: {status_code}')
|
||||||
session = SessionBase.create_session()
|
session = SessionBase.create_session()
|
||||||
if status_code == 0:
|
if result and status_code == 0:
|
||||||
|
|
||||||
# collect result data
|
# collect result data
|
||||||
recipient_blockchain_address = result.get('recipient')
|
recipient_blockchain_address = result.get('recipient')
|
||||||
@@ -95,123 +93,22 @@ def process_incoming_transfer_callback(result: dict, param: str, status_code: in
|
|||||||
value=value)
|
value=value)
|
||||||
|
|
||||||
if param == 'tokengift':
|
if param == 'tokengift':
|
||||||
incoming_tx_processor.process_token_gift_incoming_transactions()
|
logg.debug('Name information would require integration with cic meta.')
|
||||||
|
incoming_tx_processor.process_token_gift_incoming_transactions(first_name="")
|
||||||
elif param == 'transfer':
|
elif param == 'transfer':
|
||||||
|
logg.debug('Name information would require integration with cic meta.')
|
||||||
if sender_user:
|
if sender_user:
|
||||||
sender_information = define_account_tx_metadata(user=sender_user)
|
sender_information = f'{sender_user.phone_number}, {""}, {""}'
|
||||||
incoming_tx_processor.process_transfer_incoming_transaction(
|
incoming_tx_processor.process_transfer_incoming_transaction(sender_information=sender_information)
|
||||||
sender_information=sender_information,
|
|
||||||
recipient_blockchain_address=recipient_blockchain_address
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
logg.warning(
|
logg.warning(
|
||||||
f'Tx with sender: {sender_blockchain_address} was received but has no matching user in the system.'
|
f'Tx with sender: {sender_blockchain_address} was received but has no matching user in the system.'
|
||||||
)
|
)
|
||||||
incoming_tx_processor.process_transfer_incoming_transaction(
|
incoming_tx_processor.process_transfer_incoming_transaction(
|
||||||
sender_information='GRASSROOTS ECONOMICS',
|
sender_information=sender_blockchain_address)
|
||||||
recipient_blockchain_address=recipient_blockchain_address
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
session.close()
|
session.close()
|
||||||
raise ValueError(f'Unexpected transaction param: {param}.')
|
raise ValueError(f'Unexpected transaction param: {param}.')
|
||||||
else:
|
else:
|
||||||
session.close()
|
session.close()
|
||||||
raise ValueError(f'Unexpected status code: {status_code}.')
|
raise ValueError(f'Unexpected status code: {status_code}.')
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task
|
|
||||||
def process_balances_callback(result: list, param: str, status_code: int):
|
|
||||||
if status_code == 0:
|
|
||||||
balances_data = result[0]
|
|
||||||
blockchain_address = balances_data.get('address')
|
|
||||||
key = create_cached_data_key(
|
|
||||||
identifier=bytes.fromhex(blockchain_address[2:]),
|
|
||||||
salt='cic.balances_data'
|
|
||||||
)
|
|
||||||
cache_data(key=key, data=json.dumps(balances_data))
|
|
||||||
else:
|
|
||||||
raise ValueError(f'Unexpected status code: {status_code}.')
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: clean up this handler
|
|
||||||
def define_transaction_action_tag(
|
|
||||||
preferred_language: str,
|
|
||||||
sender_blockchain_address: str,
|
|
||||||
param: str):
|
|
||||||
# check if out going ot incoming transaction
|
|
||||||
if sender_blockchain_address == param:
|
|
||||||
# check preferred language
|
|
||||||
if preferred_language == 'en':
|
|
||||||
action_tag = 'SENT'
|
|
||||||
else:
|
|
||||||
action_tag = 'ULITUMA'
|
|
||||||
else:
|
|
||||||
if preferred_language == 'en':
|
|
||||||
action_tag = 'RECEIVED'
|
|
||||||
else:
|
|
||||||
action_tag = 'ULIPOKEA'
|
|
||||||
return action_tag
|
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task
|
|
||||||
def process_statement_callback(result, param: str, status_code: int):
|
|
||||||
if status_code == 0:
|
|
||||||
# create session
|
|
||||||
session = SessionBase.create_session()
|
|
||||||
processed_transactions = []
|
|
||||||
|
|
||||||
# process transaction data to cache
|
|
||||||
for transaction in result:
|
|
||||||
sender_blockchain_address = transaction.get('sender')
|
|
||||||
recipient_address = transaction.get('recipient')
|
|
||||||
source_token = transaction.get('source_token')
|
|
||||||
|
|
||||||
# filter out any transactions that are "gassy"
|
|
||||||
if '0x0000000000000000000000000000000000000000' in source_token:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# describe a processed transaction
|
|
||||||
processed_transaction = {}
|
|
||||||
|
|
||||||
# check if sender is in the system
|
|
||||||
sender: User = session.query(User).filter_by(blockchain_address=sender_blockchain_address).first()
|
|
||||||
if sender:
|
|
||||||
processed_transaction['sender_phone_number'] = sender.phone_number
|
|
||||||
|
|
||||||
action_tag = define_transaction_action_tag(
|
|
||||||
preferred_language=sender.preferred_language,
|
|
||||||
sender_blockchain_address=sender_blockchain_address,
|
|
||||||
param=param
|
|
||||||
)
|
|
||||||
processed_transaction['action_tag'] = action_tag
|
|
||||||
|
|
||||||
else:
|
|
||||||
processed_transaction['sender_phone_number'] = 'GRASSROOTS ECONOMICS'
|
|
||||||
|
|
||||||
# check if recipient is in the system
|
|
||||||
recipient: User = session.query(User).filter_by(blockchain_address=recipient_address).first()
|
|
||||||
if recipient:
|
|
||||||
processed_transaction['recipient_phone_number'] = recipient.phone_number
|
|
||||||
|
|
||||||
else:
|
|
||||||
logg.warning(f'Tx with recipient not found in cic-ussd')
|
|
||||||
|
|
||||||
# add transaction values
|
|
||||||
processed_transaction['to_value'] = from_wei(value=transaction.get('to_value'))
|
|
||||||
processed_transaction['from_value'] = from_wei(value=transaction.get('from_value'))
|
|
||||||
|
|
||||||
raw_timestamp = transaction.get('timestamp')
|
|
||||||
timestamp = datetime.utcfromtimestamp(raw_timestamp).strftime('%d/%m/%y, %H:%M')
|
|
||||||
processed_transaction['timestamp'] = timestamp
|
|
||||||
|
|
||||||
processed_transactions.append(processed_transaction)
|
|
||||||
|
|
||||||
# cache account statement
|
|
||||||
identifier = bytes.fromhex(param[2:])
|
|
||||||
key = create_cached_data_key(identifier=identifier, salt='cic.statement')
|
|
||||||
data = json.dumps(processed_transactions)
|
|
||||||
|
|
||||||
# cache statement data
|
|
||||||
cache_data(key=key, data=data)
|
|
||||||
else:
|
|
||||||
raise ValueError(f'Unexpected status code: {status_code}.')
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
import celery
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
|
|
||||||
from cic_ussd.metadata.user import UserMetadata
|
|
||||||
|
|
||||||
celery_app = celery.current_app
|
|
||||||
logg = logging.getLogger()
|
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task
|
|
||||||
def query_user_metadata(blockchain_address: str):
|
|
||||||
"""
|
|
||||||
:param blockchain_address:
|
|
||||||
:type blockchain_address:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
identifier = blockchain_address_to_metadata_pointer(blockchain_address=blockchain_address)
|
|
||||||
user_metadata_client = UserMetadata(identifier=identifier)
|
|
||||||
user_metadata_client.query()
|
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task
|
|
||||||
def create_user_metadata(blockchain_address: str, data: dict):
|
|
||||||
"""
|
|
||||||
:param blockchain_address:
|
|
||||||
:type blockchain_address:
|
|
||||||
:param data:
|
|
||||||
:type data:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
identifier = blockchain_address_to_metadata_pointer(blockchain_address=blockchain_address)
|
|
||||||
user_metadata_client = UserMetadata(identifier=identifier)
|
|
||||||
user_metadata_client.create(data=data)
|
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task
|
|
||||||
def edit_user_metadata(blockchain_address: str, data: bytes, engine: str):
|
|
||||||
identifier = blockchain_address_to_metadata_pointer(blockchain_address=blockchain_address)
|
|
||||||
user_metadata_client = UserMetadata(identifier=identifier)
|
|
||||||
user_metadata_client.edit(data=data, engine=engine)
|
|
||||||
@@ -11,13 +11,12 @@ from cic_ussd.db.models.base import SessionBase
|
|||||||
from cic_ussd.db.models.ussd_session import UssdSession
|
from cic_ussd.db.models.ussd_session import UssdSession
|
||||||
from cic_ussd.error import SessionNotFoundError
|
from cic_ussd.error import SessionNotFoundError
|
||||||
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
|
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
|
||||||
from cic_ussd.tasks.base import CriticalSQLAlchemyTask
|
|
||||||
|
|
||||||
celery_app = celery.current_app
|
celery_app = celery.current_app
|
||||||
logg = get_logger(__file__)
|
logg = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task(base=CriticalSQLAlchemyTask)
|
@celery_app.task
|
||||||
def persist_session_to_db(external_session_id: str):
|
def persist_session_to_db(external_session_id: str):
|
||||||
"""
|
"""
|
||||||
This task initiates the saving of the session object to the database and it's removal from the in-memory storage.
|
This task initiates the saving of the session object to the database and it's removal from the in-memory storage.
|
||||||
@@ -63,10 +62,11 @@ def persist_session_to_db(external_session_id: str):
|
|||||||
in_db_ussd_session.set_data(key=key, value=value, session=session)
|
in_db_ussd_session.set_data(key=key, value=value, session=session)
|
||||||
|
|
||||||
session.add(in_db_ussd_session)
|
session.add(in_db_ussd_session)
|
||||||
session.commit()
|
|
||||||
session.close()
|
|
||||||
InMemoryUssdSession.redis_cache.expire(external_session_id, timedelta(minutes=1))
|
InMemoryUssdSession.redis_cache.expire(external_session_id, timedelta(minutes=1))
|
||||||
else:
|
else:
|
||||||
session.close()
|
session.close()
|
||||||
raise SessionNotFoundError('Session does not exist!')
|
raise SessionNotFoundError('Session does not exist!')
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
session.close()
|
||||||
|
|
||||||
@@ -7,7 +7,6 @@ from datetime import datetime
|
|||||||
from cic_eth.api import Api
|
from cic_eth.api import Api
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
from cic_ussd.balance import get_cached_operational_balance
|
|
||||||
from cic_ussd.notifications import Notifier
|
from cic_ussd.notifications import Notifier
|
||||||
|
|
||||||
|
|
||||||
@@ -36,7 +35,7 @@ def from_wei(value: int) -> float:
|
|||||||
:return: SRF equivalent of value in Wei
|
:return: SRF equivalent of value in Wei
|
||||||
:rtype: float
|
:rtype: float
|
||||||
"""
|
"""
|
||||||
value = float(value) / 1e+6
|
value = float(value) / 1e+18
|
||||||
return truncate(value=value, decimals=2)
|
return truncate(value=value, decimals=2)
|
||||||
|
|
||||||
|
|
||||||
@@ -68,9 +67,11 @@ class IncomingTransactionProcessor:
|
|||||||
self.token_symbol = token_symbol
|
self.token_symbol = token_symbol
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
def process_token_gift_incoming_transactions(self):
|
def process_token_gift_incoming_transactions(self, first_name: str):
|
||||||
"""This function processes incoming transactions with a "tokengift" param, it collects all appropriate data to
|
"""This function processes incoming transactions with a "tokengift" param, it collects all appropriate data to
|
||||||
send out notifications to users when their accounts are successfully created.
|
send out notifications to users when their accounts are successfully created.
|
||||||
|
:param first_name: The first name of the recipient of the token gift transaction.
|
||||||
|
:type first_name: str
|
||||||
|
|
||||||
"""
|
"""
|
||||||
balance = from_wei(value=self.value)
|
balance = from_wei(value=self.value)
|
||||||
@@ -79,22 +80,20 @@ class IncomingTransactionProcessor:
|
|||||||
phone_number=self.phone_number,
|
phone_number=self.phone_number,
|
||||||
preferred_language=self.preferred_language,
|
preferred_language=self.preferred_language,
|
||||||
balance=balance,
|
balance=balance,
|
||||||
|
first_name=first_name,
|
||||||
token_symbol=self.token_symbol)
|
token_symbol=self.token_symbol)
|
||||||
|
|
||||||
def process_transfer_incoming_transaction(self, sender_information: str, recipient_blockchain_address: str):
|
def process_transfer_incoming_transaction(self, sender_information: str):
|
||||||
"""This function processes incoming transactions with the "transfer" param and issues notifications to users
|
"""This function processes incoming transactions with the "transfer" param and issues notifications to users
|
||||||
about reception of funds into their accounts.
|
about reception of funds into their accounts.
|
||||||
:param sender_information: A string with a user's full name and phone number.
|
:param sender_information: A string with a user's full name and phone number.
|
||||||
:type sender_information: str
|
:type sender_information: str
|
||||||
:param recipient_blockchain_address:
|
|
||||||
type recipient_blockchain_address: str
|
|
||||||
"""
|
"""
|
||||||
key = 'sms.received_tokens'
|
key = 'sms.received_tokens'
|
||||||
amount = from_wei(value=self.value)
|
amount = from_wei(value=self.value)
|
||||||
timestamp = datetime.now().strftime('%d-%m-%y, %H:%M %p')
|
timestamp = datetime.now().strftime('%d-%m-%y, %H:%M %p')
|
||||||
|
|
||||||
operational_balance = get_cached_operational_balance(blockchain_address=recipient_blockchain_address)
|
logg.debug('Balance requires implementation of cic-eth integration with balance.')
|
||||||
|
|
||||||
notifier.send_sms_notification(key=key,
|
notifier.send_sms_notification(key=key,
|
||||||
phone_number=self.phone_number,
|
phone_number=self.phone_number,
|
||||||
preferred_language=self.preferred_language,
|
preferred_language=self.preferred_language,
|
||||||
@@ -102,7 +101,7 @@ class IncomingTransactionProcessor:
|
|||||||
token_symbol=self.token_symbol,
|
token_symbol=self.token_symbol,
|
||||||
tx_sender_information=sender_information,
|
tx_sender_information=sender_information,
|
||||||
timestamp=timestamp,
|
timestamp=timestamp,
|
||||||
balance=operational_balance)
|
balance='')
|
||||||
|
|
||||||
|
|
||||||
class OutgoingTransactionProcessor:
|
class OutgoingTransactionProcessor:
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ def validate_phone_number(phone: str):
|
|||||||
|
|
||||||
|
|
||||||
def validate_response_type(processor_response: str) -> bool:
|
def validate_response_type(processor_response: str) -> bool:
|
||||||
"""1*3443*3443*Philip*Wanga*1*Juja*Software Developer*2*3
|
"""
|
||||||
This function checks the prefix for a corresponding menu's text from the response offered by the Ussd Processor and
|
This function checks the prefix for a corresponding menu's text from the response offered by the Ussd Processor and
|
||||||
determines whether the response should prompt the end of a ussd session or the
|
determines whether the response should prompt the end of a ussd session or the
|
||||||
:param processor_response: A ussd menu's text value.
|
:param processor_response: A ussd menu's text value.
|
||||||
|
|||||||
@@ -11,12 +11,8 @@ RUN apk update && \
|
|||||||
apk add git linux-headers postgresql-dev gnupg bash
|
apk add git linux-headers postgresql-dev gnupg bash
|
||||||
RUN apk add --update musl-dev gcc libffi-dev
|
RUN apk add --update musl-dev gcc libffi-dev
|
||||||
|
|
||||||
# create secrets directory
|
|
||||||
RUN mkdir -vp pgp/keys
|
|
||||||
|
|
||||||
# create application directory
|
# create application directory
|
||||||
RUN mkdir -vp cic-ussd
|
RUN mkdir -vp cic-ussd
|
||||||
RUN mkdir -vp data
|
|
||||||
|
|
||||||
COPY cic-ussd/setup.cfg \
|
COPY cic-ussd/setup.cfg \
|
||||||
cic-ussd/setup.py \
|
cic-ussd/setup.py \
|
||||||
@@ -32,7 +28,6 @@ RUN cd cic-ussd && \
|
|||||||
|
|
||||||
# copy all necessary files
|
# copy all necessary files
|
||||||
COPY cic-ussd/cic_ussd/ cic-ussd/cic_ussd/
|
COPY cic-ussd/cic_ussd/ cic-ussd/cic_ussd/
|
||||||
COPY cic-ussd/cic_ussd/db/ussd_menu.json data/
|
|
||||||
COPY cic-ussd/scripts/ cic-ussd/scripts/
|
COPY cic-ussd/scripts/ cic-ussd/scripts/
|
||||||
COPY cic-ussd/states/ cic-ussd/states/
|
COPY cic-ussd/states/ cic-ussd/states/
|
||||||
COPY cic-ussd/transitions/ cic-ussd/transitions/
|
COPY cic-ussd/transitions/ cic-ussd/transitions/
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ betterpath==0.2.2
|
|||||||
billiard==3.6.3.0
|
billiard==3.6.3.0
|
||||||
celery==4.4.7
|
celery==4.4.7
|
||||||
cffi==1.14.3
|
cffi==1.14.3
|
||||||
chainlib~=0.0.1a15
|
cic-eth~=0.10.0a22
|
||||||
cic-eth==0.10.0a38
|
|
||||||
cic-notify==0.3.1
|
cic-notify==0.3.1
|
||||||
cic-types==0.1.0a8
|
|
||||||
click==7.1.2
|
click==7.1.2
|
||||||
confini==0.3.5
|
confini~=0.3.6a1
|
||||||
cryptography==3.2.1
|
cryptography==3.2.1
|
||||||
faker==4.17.1
|
faker==4.17.1
|
||||||
iniconfig==1.1.1
|
iniconfig==1.1.1
|
||||||
@@ -36,7 +34,6 @@ python-i18n==0.3.9
|
|||||||
pytz==2020.1
|
pytz==2020.1
|
||||||
PyYAML==5.3.1
|
PyYAML==5.3.1
|
||||||
redis==3.5.3
|
redis==3.5.3
|
||||||
requests==2.24.0
|
|
||||||
semver==2.13.0
|
semver==2.13.0
|
||||||
six==1.15.0
|
six==1.15.0
|
||||||
SQLAlchemy==1.3.20
|
SQLAlchemy==1.3.20
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ packages =
|
|||||||
cic_ussd.db.models
|
cic_ussd.db.models
|
||||||
cic_ussd.files
|
cic_ussd.files
|
||||||
cic_ussd.menu
|
cic_ussd.menu
|
||||||
cic_ussd.metadata
|
|
||||||
cic_ussd.runnable
|
cic_ussd.runnable
|
||||||
cic_ussd.session
|
cic_ussd.session
|
||||||
cic_ussd.state_machine
|
cic_ussd.state_machine
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
[
|
[
|
||||||
"account_management",
|
"account_management",
|
||||||
"metadata_management",
|
"profile_management",
|
||||||
"select_preferred_language",
|
"select_preferred_language",
|
||||||
"enter_current_pin",
|
"enter_current_pin",
|
||||||
|
"mini_statement_inquiry_pin_authorization",
|
||||||
"enter_new_pin",
|
"enter_new_pin",
|
||||||
"new_pin_confirmation",
|
"new_pin_confirmation",
|
||||||
"display_user_metadata",
|
"display_user_profile_data"
|
||||||
"standard_pin_authorization",
|
|
||||||
"account_balances_pin_authorization",
|
|
||||||
"account_statement_pin_authorization",
|
|
||||||
"account_balances"
|
|
||||||
]
|
]
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
[
|
|
||||||
"first_transaction_set",
|
|
||||||
"middle_transaction_set",
|
|
||||||
"last_transaction_set"
|
|
||||||
]
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[
|
|
||||||
"enter_given_name",
|
|
||||||
"enter_family_name",
|
|
||||||
"enter_gender",
|
|
||||||
"enter_age",
|
|
||||||
"enter_location",
|
|
||||||
"enter_products"
|
|
||||||
]
|
|
||||||
8
apps/cic-ussd/states/user_profile_states.json
Normal file
8
apps/cic-ussd/states/user_profile_states.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[
|
||||||
|
"enter_first_name",
|
||||||
|
"enter_last_name",
|
||||||
|
"enter_gender",
|
||||||
|
"enter_location",
|
||||||
|
"enter_business_profile",
|
||||||
|
"view_profile_pin_authorization"
|
||||||
|
]
|
||||||
@@ -3,5 +3,4 @@ pytest-alembic==0.2.5
|
|||||||
pytest-celery==0.0.0a1
|
pytest-celery==0.0.0a1
|
||||||
pytest-cov==2.10.1
|
pytest-cov==2.10.1
|
||||||
pytest-mock==3.3.1
|
pytest-mock==3.3.1
|
||||||
pytest-redis==2.0.0
|
pytest-redis==2.0.0
|
||||||
requests-mock==1.8.0
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
import os
|
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
from cic_ussd.db import dsn_from_config
|
|
||||||
|
|
||||||
|
|
||||||
def test_dsn_from_config(load_config):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
# test dsn for sqlite engine
|
|
||||||
dsn = dsn_from_config(load_config)
|
|
||||||
scheme = f'{load_config.get("DATABASE_ENGINE")}+{load_config.get("DATABASE_DRIVER")}'
|
|
||||||
assert dsn == f'{scheme}:///{load_config.get("DATABASE_NAME")}'
|
|
||||||
|
|
||||||
# test dsn for other db formats
|
|
||||||
overrides = {
|
|
||||||
'DATABASE_PASSWORD': 'password',
|
|
||||||
'DATABASE_DRIVER': 'psycopg2',
|
|
||||||
'DATABASE_ENGINE': 'postgresql'
|
|
||||||
}
|
|
||||||
load_config.dict_override(dct=overrides, dct_description='Override values to test different db formats.')
|
|
||||||
|
|
||||||
scheme = f'{load_config.get("DATABASE_ENGINE")}+{load_config.get("DATABASE_DRIVER")}'
|
|
||||||
|
|
||||||
dsn = dsn_from_config(load_config)
|
|
||||||
assert dsn == f"{scheme}://{load_config.get('DATABASE_USER')}:{load_config.get('DATABASE_PASSWORD')}@{load_config.get('DATABASE_HOST')}:{load_config.get('DATABASE_PORT')}/{load_config.get('DATABASE_NAME')}"
|
|
||||||
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
import json
|
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
import pytest
|
|
||||||
import requests
|
|
||||||
import requests_mock
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
from cic_ussd.error import UnsupportedMethodError
|
|
||||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer, make_request
|
|
||||||
|
|
||||||
|
|
||||||
def test_make_request(define_metadata_pointer_url, mock_meta_get_response, mock_meta_post_response, person_metadata):
|
|
||||||
with requests_mock.Mocker(real_http=False) as request_mocker:
|
|
||||||
request_mocker.register_uri(
|
|
||||||
'GET',
|
|
||||||
define_metadata_pointer_url,
|
|
||||||
status_code=200,
|
|
||||||
reason='OK',
|
|
||||||
content=json.dumps(mock_meta_get_response).encode('utf-8')
|
|
||||||
)
|
|
||||||
response = make_request(method='GET', url=define_metadata_pointer_url)
|
|
||||||
assert response.content == requests.get(define_metadata_pointer_url).content
|
|
||||||
|
|
||||||
with requests_mock.Mocker(real_http=False) as request_mocker:
|
|
||||||
request_mocker.register_uri(
|
|
||||||
'POST',
|
|
||||||
define_metadata_pointer_url,
|
|
||||||
status_code=201,
|
|
||||||
reason='CREATED',
|
|
||||||
content=json.dumps(mock_meta_post_response).encode('utf-8')
|
|
||||||
)
|
|
||||||
response = make_request(
|
|
||||||
method='POST',
|
|
||||||
url=define_metadata_pointer_url,
|
|
||||||
data=json.dumps(person_metadata).encode('utf-8'),
|
|
||||||
headers={
|
|
||||||
'X-CIC-AUTOMERGE': 'server',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assert response.content == requests.post(define_metadata_pointer_url).content
|
|
||||||
|
|
||||||
with requests_mock.Mocker(real_http=False) as request_mocker:
|
|
||||||
request_mocker.register_uri(
|
|
||||||
'PUT',
|
|
||||||
define_metadata_pointer_url,
|
|
||||||
status_code=200,
|
|
||||||
reason='OK'
|
|
||||||
)
|
|
||||||
response = make_request(
|
|
||||||
method='PUT',
|
|
||||||
url=define_metadata_pointer_url,
|
|
||||||
data=json.dumps(person_metadata).encode('utf-8'),
|
|
||||||
headers={
|
|
||||||
'X-CIC-AUTOMERGE': 'server',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assert response.content == requests.put(define_metadata_pointer_url).content
|
|
||||||
|
|
||||||
with pytest.raises(UnsupportedMethodError) as error:
|
|
||||||
with requests_mock.Mocker(real_http=False) as request_mocker:
|
|
||||||
request_mocker.register_uri(
|
|
||||||
'DELETE',
|
|
||||||
define_metadata_pointer_url,
|
|
||||||
status_code=200,
|
|
||||||
reason='OK'
|
|
||||||
)
|
|
||||||
make_request(
|
|
||||||
method='DELETE',
|
|
||||||
url=define_metadata_pointer_url
|
|
||||||
)
|
|
||||||
assert str(error.value) == 'Unsupported method: DELETE'
|
|
||||||
|
|
||||||
|
|
||||||
def test_blockchain_address_to_metadata_pointer(create_activated_user):
|
|
||||||
blockchain_address = create_activated_user.blockchain_address
|
|
||||||
assert type(blockchain_address_to_metadata_pointer(blockchain_address)) == bytes
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
from cic_ussd.metadata.signer import Signer
|
|
||||||
|
|
||||||
|
|
||||||
def test_client(load_config, setup_metadata_signer, person_metadata):
|
|
||||||
signer = Signer()
|
|
||||||
# get gpg used
|
|
||||||
digest = 'a4337bc45a8fc544c03f52dc550cd6e1e87021bc896588bd79e901e2'
|
|
||||||
person_metadata['digest'] = digest
|
|
||||||
gpg = signer.gpg
|
|
||||||
|
|
||||||
# check that key data was loaded
|
|
||||||
assert signer.key_data is not None
|
|
||||||
|
|
||||||
# check that correct operational key is returned
|
|
||||||
gpg.import_keys(key_data=signer.key_data)
|
|
||||||
gpg_keys = gpg.list_keys()
|
|
||||||
assert signer.get_operational_key() == gpg_keys[0]
|
|
||||||
|
|
||||||
# check that correct signature is returned
|
|
||||||
key_id = signer.get_operational_key().get('keyid')
|
|
||||||
signature = gpg.sign(message=digest, passphrase=load_config.get('KEYS_PASSPHRASE'), keyid=key_id)
|
|
||||||
assert str(signature) == signer.sign_digest(data=person_metadata)
|
|
||||||
|
|
||||||
# remove tmp gpg file
|
|
||||||
shutil.rmtree(Signer.gpg_path)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
import json
|
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
import pytest
|
|
||||||
import requests_mock
|
|
||||||
from cic_types.models.person import generate_metadata_pointer
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
|
|
||||||
from cic_ussd.metadata.signer import Signer
|
|
||||||
from cic_ussd.metadata.user import UserMetadata
|
|
||||||
from cic_ussd.redis import get_cached_data
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_metadata(create_activated_user, define_metadata_pointer_url, load_config):
|
|
||||||
UserMetadata.base_url = load_config.get('CIC_META_URL')
|
|
||||||
identifier = blockchain_address_to_metadata_pointer(blockchain_address=create_activated_user.blockchain_address)
|
|
||||||
user_metadata_client = UserMetadata(identifier=identifier)
|
|
||||||
|
|
||||||
assert user_metadata_client.url == define_metadata_pointer_url
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_user_metadata(caplog,
|
|
||||||
create_activated_user,
|
|
||||||
define_metadata_pointer_url,
|
|
||||||
load_config,
|
|
||||||
mock_meta_post_response,
|
|
||||||
person_metadata):
|
|
||||||
identifier = blockchain_address_to_metadata_pointer(blockchain_address=create_activated_user.blockchain_address)
|
|
||||||
user_metadata_client = UserMetadata(identifier=identifier)
|
|
||||||
|
|
||||||
with requests_mock.Mocker(real_http=False) as request_mocker:
|
|
||||||
request_mocker.register_uri(
|
|
||||||
'POST',
|
|
||||||
define_metadata_pointer_url,
|
|
||||||
status_code=201,
|
|
||||||
reason='CREATED',
|
|
||||||
content=json.dumps(mock_meta_post_response).encode('utf-8')
|
|
||||||
)
|
|
||||||
user_metadata_client.create(data=person_metadata)
|
|
||||||
assert 'Get signed material response status: 201' in caplog.text
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError) as error:
|
|
||||||
with requests_mock.Mocker(real_http=False) as request_mocker:
|
|
||||||
request_mocker.register_uri(
|
|
||||||
'POST',
|
|
||||||
define_metadata_pointer_url,
|
|
||||||
status_code=400,
|
|
||||||
reason='BAD REQUEST'
|
|
||||||
)
|
|
||||||
user_metadata_client.create(data=person_metadata)
|
|
||||||
assert str(error.value) == f'400 Client Error: BAD REQUEST for url: {define_metadata_pointer_url}'
|
|
||||||
|
|
||||||
|
|
||||||
def test_edit_user_metadata(caplog,
|
|
||||||
create_activated_user,
|
|
||||||
define_metadata_pointer_url,
|
|
||||||
load_config,
|
|
||||||
person_metadata,
|
|
||||||
setup_metadata_signer):
|
|
||||||
Signer.gpg_passphrase = load_config.get('KEYS_PASSPHRASE')
|
|
||||||
identifier = blockchain_address_to_metadata_pointer(blockchain_address=create_activated_user.blockchain_address)
|
|
||||||
user_metadata_client = UserMetadata(identifier=identifier)
|
|
||||||
with requests_mock.Mocker(real_http=False) as request_mocker:
|
|
||||||
request_mocker.register_uri(
|
|
||||||
'PUT',
|
|
||||||
define_metadata_pointer_url,
|
|
||||||
status_code=200,
|
|
||||||
reason='OK'
|
|
||||||
)
|
|
||||||
user_metadata_client.edit(data=person_metadata, engine='pgp')
|
|
||||||
assert 'Signed content submission status: 200' in caplog.text
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError) as error:
|
|
||||||
with requests_mock.Mocker(real_http=False) as request_mocker:
|
|
||||||
request_mocker.register_uri(
|
|
||||||
'PUT',
|
|
||||||
define_metadata_pointer_url,
|
|
||||||
status_code=400,
|
|
||||||
reason='BAD REQUEST'
|
|
||||||
)
|
|
||||||
user_metadata_client.edit(data=person_metadata, engine='pgp')
|
|
||||||
assert str(error.value) == f'400 Client Error: BAD REQUEST for url: {define_metadata_pointer_url}'
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_user_metadata(caplog,
|
|
||||||
create_activated_user,
|
|
||||||
define_metadata_pointer_url,
|
|
||||||
init_redis_cache,
|
|
||||||
load_config,
|
|
||||||
person_metadata,
|
|
||||||
setup_metadata_signer):
|
|
||||||
identifier = blockchain_address_to_metadata_pointer(blockchain_address=create_activated_user.blockchain_address)
|
|
||||||
user_metadata_client = UserMetadata(identifier=identifier)
|
|
||||||
with requests_mock.Mocker(real_http=False) as request_mocker:
|
|
||||||
request_mocker.register_uri(
|
|
||||||
'GET',
|
|
||||||
define_metadata_pointer_url,
|
|
||||||
status_code=200,
|
|
||||||
content=json.dumps(person_metadata).encode('utf-8'),
|
|
||||||
reason='OK'
|
|
||||||
)
|
|
||||||
user_metadata_client.query()
|
|
||||||
assert 'Get latest data status: 200' in caplog.text
|
|
||||||
key = generate_metadata_pointer(
|
|
||||||
identifier=identifier,
|
|
||||||
cic_type='cic.person'
|
|
||||||
)
|
|
||||||
cached_user_metadata = get_cached_data(key=key)
|
|
||||||
assert cached_user_metadata
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError) as error:
|
|
||||||
with requests_mock.Mocker(real_http=False) as request_mocker:
|
|
||||||
request_mocker.register_uri(
|
|
||||||
'GET',
|
|
||||||
define_metadata_pointer_url,
|
|
||||||
status_code=404,
|
|
||||||
reason='NOT FOUND'
|
|
||||||
)
|
|
||||||
user_metadata_client.query()
|
|
||||||
assert 'The data is not available and might need to be added.' in caplog.text
|
|
||||||
assert str(error.value) == f'400 Client Error: NOT FOUND for url: {define_metadata_pointer_url}'
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
import json
|
|
||||||
|
|
||||||
# third-party-imports
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
from cic_ussd.chain import Chain
|
|
||||||
from cic_ussd.redis import InMemoryStore
|
|
||||||
from cic_ussd.state_machine.logic.user import (
|
|
||||||
change_preferred_language_to_en,
|
|
||||||
change_preferred_language_to_sw,
|
|
||||||
edit_user_metadata_attribute,
|
|
||||||
format_user_metadata,
|
|
||||||
get_user_metadata,
|
|
||||||
save_complete_user_metadata,
|
|
||||||
process_gender_user_input,
|
|
||||||
save_profile_attribute_to_session_data,
|
|
||||||
update_account_status_to_active)
|
|
||||||
|
|
||||||
|
|
||||||
def test_change_preferred_language(create_pending_user, create_in_db_ussd_session):
|
|
||||||
state_machine_data = ('', create_in_db_ussd_session, create_pending_user)
|
|
||||||
assert create_pending_user.preferred_language is None
|
|
||||||
change_preferred_language_to_en(state_machine_data)
|
|
||||||
assert create_pending_user.preferred_language == 'en'
|
|
||||||
change_preferred_language_to_sw(state_machine_data)
|
|
||||||
assert create_pending_user.preferred_language == 'sw'
|
|
||||||
|
|
||||||
|
|
||||||
def test_update_account_status_to_active(create_pending_user, create_in_db_ussd_session):
|
|
||||||
state_machine_data = ('', create_in_db_ussd_session, create_pending_user)
|
|
||||||
update_account_status_to_active(state_machine_data)
|
|
||||||
assert create_pending_user.get_account_status() == 'ACTIVE'
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("current_state, expected_key, expected_result, user_input", [
|
|
||||||
("enter_given_name", "given_name", "John", "John"),
|
|
||||||
("enter_family_name", "family_name", "Doe", "Doe"),
|
|
||||||
("enter_gender", "gender", "Male", "1"),
|
|
||||||
("enter_location", "location", "Kangemi", "Kangemi"),
|
|
||||||
("enter_products", "products", "Mandazi", "Mandazi"),
|
|
||||||
])
|
|
||||||
def test_save_save_profile_attribute_to_session_data(current_state,
|
|
||||||
expected_key,
|
|
||||||
expected_result,
|
|
||||||
user_input,
|
|
||||||
celery_session_worker,
|
|
||||||
create_activated_user,
|
|
||||||
create_in_db_ussd_session,
|
|
||||||
create_in_redis_ussd_session):
|
|
||||||
create_in_db_ussd_session.state = current_state
|
|
||||||
serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
|
|
||||||
state_machine_data = (user_input, serialized_in_db_ussd_session, create_activated_user)
|
|
||||||
in_memory_ussd_session = InMemoryStore.cache.get('AT974186')
|
|
||||||
in_memory_ussd_session = json.loads(in_memory_ussd_session)
|
|
||||||
assert in_memory_ussd_session.get('session_data') == {}
|
|
||||||
serialized_in_db_ussd_session['state'] = current_state
|
|
||||||
save_profile_attribute_to_session_data(state_machine_data=state_machine_data)
|
|
||||||
|
|
||||||
in_memory_ussd_session = InMemoryStore.cache.get('AT974186')
|
|
||||||
in_memory_ussd_session = json.loads(in_memory_ussd_session)
|
|
||||||
|
|
||||||
assert in_memory_ussd_session.get('session_data')[expected_key] == expected_result
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("preferred_language, user_input, expected_gender_value", [
|
|
||||||
("en", "1", "Male"),
|
|
||||||
("en", "2", "Female"),
|
|
||||||
("sw", "1", "Mwanaume"),
|
|
||||||
("sw", "2", "Mwanamke"),
|
|
||||||
])
|
|
||||||
def test_process_gender_user_input(create_activated_user, expected_gender_value, preferred_language, user_input):
|
|
||||||
create_activated_user.preferred_language = preferred_language
|
|
||||||
gender = process_gender_user_input(user=create_activated_user, user_input=user_input)
|
|
||||||
assert gender == expected_gender_value
|
|
||||||
|
|
||||||
|
|
||||||
def test_format_user_metadata(create_activated_user,
|
|
||||||
complete_user_metadata,
|
|
||||||
setup_chain_spec):
|
|
||||||
from cic_types.models.person import Person
|
|
||||||
formatted_user_metadata = format_user_metadata(metadata=complete_user_metadata, user=create_activated_user)
|
|
||||||
person = Person()
|
|
||||||
user_metadata = person.deserialize(metadata=formatted_user_metadata)
|
|
||||||
assert formatted_user_metadata == user_metadata.serialize()
|
|
||||||
|
|
||||||
|
|
||||||
def test_save_complete_user_metadata(celery_session_worker,
|
|
||||||
complete_user_metadata,
|
|
||||||
create_activated_user,
|
|
||||||
create_in_redis_ussd_session,
|
|
||||||
mocker,
|
|
||||||
setup_chain_spec,
|
|
||||||
ussd_session_data):
|
|
||||||
ussd_session = create_in_redis_ussd_session.get(ussd_session_data.get('external_session_id'))
|
|
||||||
ussd_session = json.loads(ussd_session)
|
|
||||||
ussd_session['session_data'] = complete_user_metadata
|
|
||||||
user_metadata = format_user_metadata(metadata=ussd_session.get('session_data'), user=create_activated_user)
|
|
||||||
state_machine_data = ('', ussd_session, create_activated_user)
|
|
||||||
mocked_create_metadata_task = mocker.patch('cic_ussd.tasks.metadata.create_user_metadata.apply_async')
|
|
||||||
save_complete_user_metadata(state_machine_data=state_machine_data)
|
|
||||||
mocked_create_metadata_task.assert_called_with(
|
|
||||||
(user_metadata, create_activated_user.blockchain_address),
|
|
||||||
{},
|
|
||||||
queue='cic-ussd'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_edit_user_metadata_attribute(celery_session_worker,
|
|
||||||
cached_user_metadata,
|
|
||||||
create_activated_user,
|
|
||||||
create_in_redis_ussd_session,
|
|
||||||
init_redis_cache,
|
|
||||||
mocker,
|
|
||||||
person_metadata,
|
|
||||||
setup_chain_spec,
|
|
||||||
ussd_session_data):
|
|
||||||
ussd_session = create_in_redis_ussd_session.get(ussd_session_data.get('external_session_id'))
|
|
||||||
ussd_session = json.loads(ussd_session)
|
|
||||||
|
|
||||||
assert person_metadata['location']['area_name'] == 'kayaba'
|
|
||||||
|
|
||||||
# appropriately format session
|
|
||||||
ussd_session['session_data'] = {
|
|
||||||
'location': 'nairobi'
|
|
||||||
}
|
|
||||||
state_machine_data = ('', ussd_session, create_activated_user)
|
|
||||||
|
|
||||||
mocked_edit_metadata = mocker.patch('cic_ussd.tasks.metadata.edit_user_metadata.apply_async')
|
|
||||||
edit_user_metadata_attribute(state_machine_data=state_machine_data)
|
|
||||||
person_metadata['location']['area_name'] = 'nairobi'
|
|
||||||
mocked_edit_metadata.assert_called_with(
|
|
||||||
(create_activated_user.blockchain_address, person_metadata, Chain.spec.engine()),
|
|
||||||
{},
|
|
||||||
queue='cic-ussd'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_user_metadata_attribute(celery_session_worker,
|
|
||||||
create_activated_user,
|
|
||||||
create_in_redis_ussd_session,
|
|
||||||
mocker,
|
|
||||||
ussd_session_data):
|
|
||||||
ussd_session = create_in_redis_ussd_session.get(ussd_session_data.get('external_session_id'))
|
|
||||||
ussd_session = json.loads(ussd_session)
|
|
||||||
state_machine_data = ('', ussd_session, create_activated_user)
|
|
||||||
|
|
||||||
mocked_get_metadata = mocker.patch('cic_ussd.tasks.metadata.query_user_metadata.apply_async')
|
|
||||||
get_user_metadata(state_machine_data=state_machine_data)
|
|
||||||
mocked_get_metadata.assert_called_with(
|
|
||||||
(create_activated_user.blockchain_address,),
|
|
||||||
{},
|
|
||||||
queue='cic-ussd'
|
|
||||||
)
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
import json
|
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
import pytest
|
|
||||||
from cic_types.models.person import generate_metadata_pointer
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
|
|
||||||
from cic_ussd.redis import cache_data
|
|
||||||
from cic_ussd.state_machine.logic.validator import (is_valid_name,
|
|
||||||
is_valid_gender_selection,
|
|
||||||
has_cached_user_metadata)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("user_input, expected_result", [
|
|
||||||
("Arya", True),
|
|
||||||
("1234", False)
|
|
||||||
])
|
|
||||||
def test_is_valid_name(create_in_db_ussd_session, create_pending_user, user_input, expected_result):
|
|
||||||
serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
|
|
||||||
state_machine_data = (user_input, serialized_in_db_ussd_session, create_pending_user)
|
|
||||||
result = is_valid_name(state_machine_data=state_machine_data)
|
|
||||||
assert result is expected_result
|
|
||||||
|
|
||||||
|
|
||||||
def test_has_cached_user_metadata(create_in_db_ussd_session,
|
|
||||||
create_activated_user,
|
|
||||||
init_redis_cache,
|
|
||||||
person_metadata):
|
|
||||||
serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
|
|
||||||
state_machine_data = ('', serialized_in_db_ussd_session, create_activated_user)
|
|
||||||
result = has_cached_user_metadata(state_machine_data=state_machine_data)
|
|
||||||
assert result is False
|
|
||||||
# cache metadata
|
|
||||||
user = create_activated_user
|
|
||||||
key = generate_metadata_pointer(
|
|
||||||
identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address),
|
|
||||||
cic_type='cic.person'
|
|
||||||
)
|
|
||||||
cache_data(key=key, data=json.dumps(person_metadata))
|
|
||||||
result = has_cached_user_metadata(state_machine_data=state_machine_data)
|
|
||||||
assert result
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("user_input, expected_result", [
|
|
||||||
("1", True),
|
|
||||||
("2", True),
|
|
||||||
("3", False)
|
|
||||||
])
|
|
||||||
def test_is_valid_gender_selection(create_in_db_ussd_session, create_pending_user, user_input, expected_result):
|
|
||||||
serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
|
|
||||||
state_machine_data = (user_input, serialized_in_db_ussd_session, create_pending_user)
|
|
||||||
result = is_valid_gender_selection(state_machine_data=state_machine_data)
|
|
||||||
assert result is expected_result
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# standard imports
|
|
||||||
|
|
||||||
# third-party imports
|
|
||||||
|
|
||||||
# local imports
|
|
||||||
from cic_ussd.balance import BalanceManager
|
|
||||||
from cic_ussd.chain import Chain
|
|
||||||
|
|
||||||
|
|
||||||
def test_balance_manager(create_valid_tx_recipient, load_config, mocker, setup_chain_spec):
|
|
||||||
chain_str = Chain.spec.__str__()
|
|
||||||
balance_manager = BalanceManager(
|
|
||||||
address=create_valid_tx_recipient.blockchain_address,
|
|
||||||
chain_str=chain_str,
|
|
||||||
token_symbol='SRF'
|
|
||||||
)
|
|
||||||
balance_manager.get_balances = mocker.MagicMock()
|
|
||||||
balance_manager.get_balances()
|
|
||||||
|
|
||||||
balance_manager.get_balances.assert_called_once()
|
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
# third-party imports
|
|
||||||
from cic_types.pytest import *
|
|
||||||
|
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
from tests.fixtures.config import *
|
from tests.fixtures.config import *
|
||||||
from tests.fixtures.db import *
|
from tests.fixtures.db import *
|
||||||
@@ -12,3 +8,41 @@ from tests.fixtures.redis import *
|
|||||||
from tests.fixtures.callback import *
|
from tests.fixtures.callback import *
|
||||||
from tests.fixtures.requests import *
|
from tests.fixtures.requests import *
|
||||||
from tests.fixtures.mocks import *
|
from tests.fixtures.mocks import *
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
3
apps/cic-ussd/tests/fixtures/celery.py
vendored
3
apps/cic-ussd/tests/fixtures/celery.py
vendored
@@ -12,8 +12,7 @@ def celery_includes():
|
|||||||
return [
|
return [
|
||||||
'cic_ussd.tasks.ussd',
|
'cic_ussd.tasks.ussd',
|
||||||
'cic_ussd.tasks.callback_handler',
|
'cic_ussd.tasks.callback_handler',
|
||||||
'cic_notify.tasks.sms',
|
'cic_notify.tasks.sms'
|
||||||
'cic_ussd.tasks.metadata'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
34
apps/cic-ussd/tests/fixtures/config.py
vendored
34
apps/cic-ussd/tests/fixtures/config.py
vendored
@@ -2,24 +2,18 @@
|
|||||||
import i18n
|
import i18n
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import tempfile
|
|
||||||
|
|
||||||
# third party imports
|
# third party imports
|
||||||
import pytest
|
import pytest
|
||||||
from chainlib.chain import ChainSpec
|
|
||||||
from confini import Config
|
from confini import Config
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
from cic_ussd.chain import Chain
|
|
||||||
from cic_ussd.db import dsn_from_config
|
from cic_ussd.db import dsn_from_config
|
||||||
from cic_ussd.encoder import PasswordEncoder
|
|
||||||
from cic_ussd.files.local_files import create_local_file_data_stores, json_file_parser
|
from cic_ussd.files.local_files import create_local_file_data_stores, json_file_parser
|
||||||
from cic_ussd.menu.ussd_menu import UssdMenu
|
from cic_ussd.menu.ussd_menu import UssdMenu
|
||||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
|
|
||||||
from cic_ussd.metadata.signer import Signer
|
|
||||||
from cic_ussd.metadata.user import UserMetadata
|
|
||||||
from cic_ussd.state_machine import UssdStateMachine
|
from cic_ussd.state_machine import UssdStateMachine
|
||||||
|
from cic_ussd.encoder import PasswordEncoder
|
||||||
|
|
||||||
|
|
||||||
logg = logging.getLogger()
|
logg = logging.getLogger()
|
||||||
@@ -108,29 +102,3 @@ def uwsgi_env():
|
|||||||
'uwsgi.node': b'mango-habanero'
|
'uwsgi.node': b'mango-habanero'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def setup_metadata_signer(load_config):
|
|
||||||
temp_dir = tempfile.mkdtemp(dir='/tmp')
|
|
||||||
logg.debug(f'Created temp dir: {temp_dir}')
|
|
||||||
Signer.gpg_path = temp_dir
|
|
||||||
Signer.gpg_passphrase = load_config.get('PGP_PASSPHRASE')
|
|
||||||
Signer.key_file_path = f"{load_config.get('PGP_KEYS_PATH')}{load_config.get('PGP_PRIVATE_KEYS')}"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def define_metadata_pointer_url(load_config, create_activated_user):
|
|
||||||
identifier = blockchain_address_to_metadata_pointer(blockchain_address=create_activated_user.blockchain_address)
|
|
||||||
UserMetadata.base_url = load_config.get('CIC_META_URL')
|
|
||||||
user_metadata_client = UserMetadata(identifier=identifier)
|
|
||||||
return user_metadata_client.url
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def setup_chain_spec(load_config):
|
|
||||||
chain_spec = ChainSpec(
|
|
||||||
common_name=load_config.get('CIC_COMMON_NAME'),
|
|
||||||
engine=load_config.get('CIC_ENGINE'),
|
|
||||||
network_id=load_config.get('CIC_NETWORK_ID')
|
|
||||||
)
|
|
||||||
Chain.spec = chain_spec
|
|
||||||
|
|||||||
48
apps/cic-ussd/tests/fixtures/mocks.py
vendored
48
apps/cic-ussd/tests/fixtures/mocks.py
vendored
@@ -1,6 +1,4 @@
|
|||||||
# standard imports
|
# standard imports
|
||||||
import json
|
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
# third-party imports
|
# third-party imports
|
||||||
import pytest
|
import pytest
|
||||||
@@ -10,49 +8,7 @@ from cic_ussd.translation import translation_for
|
|||||||
from cic_ussd.transactions import truncate
|
from cic_ussd.transactions import truncate
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function', autouse=True)
|
||||||
def mock_meta_post_response():
|
|
||||||
return {
|
|
||||||
'name': 'cic',
|
|
||||||
'version': '1',
|
|
||||||
'ext': {
|
|
||||||
'network': {
|
|
||||||
'name': 'pgp',
|
|
||||||
'version': '2'
|
|
||||||
},
|
|
||||||
'engine': {
|
|
||||||
'name': 'automerge',
|
|
||||||
'version': '0.14.1'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'payload': '["~#iL",[["~#iM",["ops",["^0",[["^1",["action","set","obj","00000000-0000-0000-0000-000000000000",'
|
|
||||||
'"key","id","value","7e2f58335a69ac82f9a965a8fc35403c8585ea601946d858ee97684a285bf857"]],["^1",'
|
|
||||||
'["action","set","obj","00000000-0000-0000-0000-000000000000","key","timestamp","value",'
|
|
||||||
'1613487781]], '
|
|
||||||
'["^1",["action","set","obj","00000000-0000-0000-0000-000000000000","key","data","value",'
|
|
||||||
'"{\\"foo\\": '
|
|
||||||
'\\"bar\\", \\"xyzzy\\": 42}"]]]],"actor","2b738a75-2aad-4ac8-ae8d-294a5ea4afad","seq",1,"deps",'
|
|
||||||
'["^1", '
|
|
||||||
'[]],"message","Initialization","undoable",false]],["^1",["ops",["^0",[["^1",["action","makeMap",'
|
|
||||||
'"obj","a921a5ae-0554-497a-ac2e-4e829d8a12b6"]],["^1",["action","set","obj",'
|
|
||||||
'"a921a5ae-0554-497a-ac2e-4e829d8a12b6","key","digest","value","W10="]],["^1",["action","link",'
|
|
||||||
'"obj", '
|
|
||||||
'"00000000-0000-0000-0000-000000000000","key","signature","value",'
|
|
||||||
'"a921a5ae-0554-497a-ac2e-4e829d8a12b6"]]]],"actor","2b738a75-2aad-4ac8-ae8d-294a5ea4afad","seq",2,'
|
|
||||||
'"deps",["^1",[]],"message","sign"]]]]',
|
|
||||||
'digest': 'W10='
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def mock_meta_get_response():
|
|
||||||
return {
|
|
||||||
"foo": "bar",
|
|
||||||
"xyzzy": 42
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def mock_notifier_api(mocker):
|
def mock_notifier_api(mocker):
|
||||||
messages = []
|
messages = []
|
||||||
|
|
||||||
@@ -87,7 +43,7 @@ def mock_outgoing_transactions(mocker):
|
|||||||
|
|
||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function')
|
||||||
def mock_balance(mocker):
|
def mock_balance(mocker):
|
||||||
mocked_operational_balance = mocker.patch('cic_ussd.accounts.BalanceManager.get_balances')
|
mocked_operational_balance = mocker.patch('cic_ussd.accounts.BalanceManager.get_operational_balance')
|
||||||
|
|
||||||
def _mock_operational_balance(balance: int):
|
def _mock_operational_balance(balance: int):
|
||||||
mocked_operational_balance.return_value = truncate(value=balance, decimals=2)
|
mocked_operational_balance.return_value = truncate(value=balance, decimals=2)
|
||||||
|
|||||||
28
apps/cic-ussd/tests/fixtures/user.py
vendored
28
apps/cic-ussd/tests/fixtures/user.py
vendored
@@ -1,17 +1,13 @@
|
|||||||
# standard imports
|
# standard imports
|
||||||
import json
|
|
||||||
import uuid
|
|
||||||
from random import randint
|
from random import randint
|
||||||
|
import uuid
|
||||||
|
|
||||||
# third party imports
|
# third party imports
|
||||||
import pytest
|
import pytest
|
||||||
from cic_types.models.person import generate_metadata_pointer
|
|
||||||
from faker import Faker
|
from faker import Faker
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
from cic_ussd.db.models.user import AccountStatus, User
|
from cic_ussd.db.models.user import AccountStatus, User
|
||||||
from cic_ussd.redis import cache_data
|
|
||||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
|
|
||||||
|
|
||||||
|
|
||||||
fake = Faker()
|
fake = Faker()
|
||||||
@@ -96,25 +92,3 @@ def create_locked_accounts(init_database, set_fernet_key):
|
|||||||
user.account_status = AccountStatus.LOCKED.value
|
user.account_status = AccountStatus.LOCKED.value
|
||||||
user.session.add(user)
|
user.session.add(user)
|
||||||
user.session.commit()
|
user.session.commit()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def complete_user_metadata(create_activated_user):
|
|
||||||
return {
|
|
||||||
"date_registered": create_activated_user.created,
|
|
||||||
"family_name": "Snow",
|
|
||||||
"given_name": "Name",
|
|
||||||
"gender": 'Male',
|
|
||||||
"location": "Kangemi",
|
|
||||||
"products": "Mandazi"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def cached_user_metadata(create_activated_user, init_redis_cache, person_metadata):
|
|
||||||
user_metadata = json.dumps(person_metadata)
|
|
||||||
key = generate_metadata_pointer(
|
|
||||||
identifier=blockchain_address_to_metadata_pointer(blockchain_address=create_activated_user.blockchain_address),
|
|
||||||
cic_type='cic.person'
|
|
||||||
)
|
|
||||||
cache_data(key=key, data=user_metadata)
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import pytest
|
|||||||
# local imports
|
# local imports
|
||||||
from cic_ussd.db.models.user import User
|
from cic_ussd.db.models.user import User
|
||||||
from cic_ussd.error import ActionDataNotFoundError
|
from cic_ussd.error import ActionDataNotFoundError
|
||||||
from cic_ussd.conversions import from_wei
|
from cic_ussd.transactions import from_wei
|
||||||
|
|
||||||
logg = logging.getLogger()
|
logg = logging.getLogger()
|
||||||
|
|
||||||
@@ -155,7 +155,6 @@ def test_unsuccessful_incoming_transaction_recipient_not_found(celery_session_wo
|
|||||||
def test_successful_incoming_transaction_sender_not_found(caplog,
|
def test_successful_incoming_transaction_sender_not_found(caplog,
|
||||||
celery_session_worker,
|
celery_session_worker,
|
||||||
create_valid_tx_recipient,
|
create_valid_tx_recipient,
|
||||||
mock_notifier_api,
|
|
||||||
successful_incoming_transfer_callback):
|
successful_incoming_transfer_callback):
|
||||||
result = successful_incoming_transfer_callback.get('RESULT')
|
result = successful_incoming_transfer_callback.get('RESULT')
|
||||||
param = successful_incoming_transfer_callback.get('PARAM')
|
param = successful_incoming_transfer_callback.get('PARAM')
|
||||||
@@ -15,7 +15,7 @@ def test_persist_session_to_db_task(
|
|||||||
create_in_redis_ussd_session):
|
create_in_redis_ussd_session):
|
||||||
external_session_id = ussd_session_data.get('external_session_id')
|
external_session_id = ussd_session_data.get('external_session_id')
|
||||||
s_persist_session_to_db = celery.signature(
|
s_persist_session_to_db = celery.signature(
|
||||||
'cic_ussd.tasks.ussd_session.persist_session_to_db',
|
'cic_ussd.tasks.ussd.persist_session_to_db',
|
||||||
[external_session_id]
|
[external_session_id]
|
||||||
)
|
)
|
||||||
result = s_persist_session_to_db.apply_async()
|
result = s_persist_session_to_db.apply_async()
|
||||||
@@ -38,7 +38,7 @@ def test_session_not_found_error(
|
|||||||
with pytest.raises(SessionNotFoundError) as error:
|
with pytest.raises(SessionNotFoundError) as error:
|
||||||
external_session_id = 'SomeRandomValue'
|
external_session_id = 'SomeRandomValue'
|
||||||
s_persist_session_to_db = celery.signature(
|
s_persist_session_to_db = celery.signature(
|
||||||
'cic_ussd.tasks.ussd_session.persist_session_to_db',
|
'cic_ussd.tasks.ussd.persist_session_to_db',
|
||||||
[external_session_id]
|
[external_session_id]
|
||||||
)
|
)
|
||||||
result = s_persist_session_to_db.apply_async()
|
result = s_persist_session_to_db.apply_async()
|
||||||
@@ -5,6 +5,7 @@ import json
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
|
from cic_ussd.state_machine import UssdStateMachine
|
||||||
from cic_ussd.state_machine.logic.transaction import (has_sufficient_balance,
|
from cic_ussd.state_machine.logic.transaction import (has_sufficient_balance,
|
||||||
is_valid_recipient,
|
is_valid_recipient,
|
||||||
is_valid_transaction_amount,
|
is_valid_transaction_amount,
|
||||||
@@ -99,8 +100,8 @@ def test_process_transaction_request(create_valid_tx_recipient,
|
|||||||
create_valid_tx_sender,
|
create_valid_tx_sender,
|
||||||
load_config,
|
load_config,
|
||||||
mock_outgoing_transactions,
|
mock_outgoing_transactions,
|
||||||
setup_chain_spec,
|
|
||||||
ussd_session_data):
|
ussd_session_data):
|
||||||
|
UssdStateMachine.chain_str = load_config.get('CIC_CHAIN_SPEC')
|
||||||
ussd_session_data['session_data'] = {
|
ussd_session_data['session_data'] = {
|
||||||
'recipient_phone_number': create_valid_tx_recipient.phone_number,
|
'recipient_phone_number': create_valid_tx_recipient.phone_number,
|
||||||
'transaction_amount': '50'
|
'transaction_amount': '50'
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# standard imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# third-party-imports
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from cic_ussd.redis import InMemoryStore
|
||||||
|
from cic_ussd.state_machine.logic.user import (
|
||||||
|
change_preferred_language_to_en,
|
||||||
|
change_preferred_language_to_sw,
|
||||||
|
save_profile_attribute_to_session_data,
|
||||||
|
update_account_status_to_active)
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_preferred_language(create_pending_user, create_in_db_ussd_session):
|
||||||
|
state_machine_data = ('', create_in_db_ussd_session, create_pending_user)
|
||||||
|
assert create_pending_user.preferred_language is None
|
||||||
|
change_preferred_language_to_en(state_machine_data)
|
||||||
|
assert create_pending_user.preferred_language == 'en'
|
||||||
|
change_preferred_language_to_sw(state_machine_data)
|
||||||
|
assert create_pending_user.preferred_language == 'sw'
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_account_status_to_active(create_pending_user, create_in_db_ussd_session):
|
||||||
|
state_machine_data = ('', create_in_db_ussd_session, create_pending_user)
|
||||||
|
update_account_status_to_active(state_machine_data)
|
||||||
|
assert create_pending_user.get_account_status() == 'ACTIVE'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("current_state, expected_key, expected_result, user_input", [
|
||||||
|
("enter_first_name", "first_name", "John", "John"),
|
||||||
|
("enter_last_name", "last_name", "Doe", "Doe"),
|
||||||
|
("enter_location", "location", "Kangemi", "Kangemi"),
|
||||||
|
("enter_business_profile", "business_profile", "Mandazi", "Mandazi")
|
||||||
|
])
|
||||||
|
def test_save_profile_attribute_to_session_data(current_state,
|
||||||
|
expected_key,
|
||||||
|
expected_result,
|
||||||
|
user_input,
|
||||||
|
celery_session_worker,
|
||||||
|
create_activated_user,
|
||||||
|
create_in_db_ussd_session,
|
||||||
|
create_in_redis_ussd_session):
|
||||||
|
create_in_db_ussd_session.state = current_state
|
||||||
|
serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
|
||||||
|
state_machine_data = (user_input, serialized_in_db_ussd_session, create_activated_user)
|
||||||
|
in_memory_ussd_session = InMemoryStore.cache.get('AT974186')
|
||||||
|
in_memory_ussd_session = json.loads(in_memory_ussd_session)
|
||||||
|
assert in_memory_ussd_session.get('session_data') == {}
|
||||||
|
serialized_in_db_ussd_session['state'] = current_state
|
||||||
|
save_profile_attribute_to_session_data(state_machine_data=state_machine_data)
|
||||||
|
|
||||||
|
in_memory_ussd_session = InMemoryStore.cache.get('AT974186')
|
||||||
|
in_memory_ussd_session = json.loads(in_memory_ussd_session)
|
||||||
|
|
||||||
|
assert in_memory_ussd_session.get('session_data')[expected_key] == expected_result
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# standard imports
|
||||||
|
|
||||||
|
# third-party imports
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from cic_ussd.state_machine.logic.validator import (is_valid_name,
|
||||||
|
has_complete_profile_data,
|
||||||
|
has_empty_username_data,
|
||||||
|
has_empty_gender_data,
|
||||||
|
has_empty_location_data,
|
||||||
|
has_empty_business_profile_data)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("user_input, expected_result", [
|
||||||
|
("Arya", True),
|
||||||
|
("1234", False)
|
||||||
|
])
|
||||||
|
def test_is_valid_name(create_in_db_ussd_session, create_pending_user, user_input, expected_result):
|
||||||
|
serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
|
||||||
|
state_machine_data = (user_input, serialized_in_db_ussd_session, create_pending_user)
|
||||||
|
result = is_valid_name(state_machine_data=state_machine_data)
|
||||||
|
assert result is expected_result
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_complete_profile_data(caplog,
|
||||||
|
create_in_db_ussd_session,
|
||||||
|
create_activated_user):
|
||||||
|
serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
|
||||||
|
state_machine_data = ('', serialized_in_db_ussd_session, create_activated_user)
|
||||||
|
has_complete_profile_data(state_machine_data=state_machine_data)
|
||||||
|
assert 'This section requires implementation of user metadata.' in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_empty_username_data(caplog,
|
||||||
|
create_in_db_ussd_session,
|
||||||
|
create_activated_user):
|
||||||
|
state_machine_data = ('', create_in_db_ussd_session, create_activated_user)
|
||||||
|
has_empty_username_data(state_machine_data=state_machine_data)
|
||||||
|
assert 'This section requires implementation of user metadata.' in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_empty_gender_data(caplog,
|
||||||
|
create_in_db_ussd_session,
|
||||||
|
create_activated_user):
|
||||||
|
serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
|
||||||
|
state_machine_data = ('', serialized_in_db_ussd_session, create_activated_user)
|
||||||
|
has_empty_gender_data(state_machine_data=state_machine_data)
|
||||||
|
assert 'This section requires implementation of user metadata.' in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_empty_location_data(caplog,
|
||||||
|
create_in_db_ussd_session,
|
||||||
|
create_activated_user):
|
||||||
|
serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
|
||||||
|
state_machine_data = ('', serialized_in_db_ussd_session, create_activated_user)
|
||||||
|
has_empty_location_data(state_machine_data=state_machine_data)
|
||||||
|
assert 'This section requires implementation of user metadata.' in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_empty_business_profile_data(caplog,
|
||||||
|
create_in_db_ussd_session,
|
||||||
|
create_activated_user):
|
||||||
|
serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
|
||||||
|
state_machine_data = ('', serialized_in_db_ussd_session, create_activated_user)
|
||||||
|
has_empty_business_profile_data(state_machine_data=state_machine_data)
|
||||||
|
assert 'This section requires implementation of user metadata.' in caplog.text
|
||||||
19
apps/cic-ussd/tests/unit/cic_ussd/test_accounts.py
Normal file
19
apps/cic-ussd/tests/unit/cic_ussd/test_accounts.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# standard imports
|
||||||
|
|
||||||
|
# third-party imports
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from cic_ussd.accounts import BalanceManager
|
||||||
|
|
||||||
|
|
||||||
|
def test_balance_manager(mocker, load_config, create_valid_tx_recipient):
|
||||||
|
|
||||||
|
balance_manager = BalanceManager(
|
||||||
|
address=create_valid_tx_recipient.blockchain_address,
|
||||||
|
chain_str=load_config.get('CIC_CHAIN_SPEC'),
|
||||||
|
token_symbol='SRF'
|
||||||
|
)
|
||||||
|
balance_manager.get_operational_balance = mocker.MagicMock()
|
||||||
|
balance_manager.get_operational_balance()
|
||||||
|
|
||||||
|
balance_manager.get_operational_balance.assert_called_once()
|
||||||
@@ -18,7 +18,6 @@ def test_send_sms_notification(celery_session_worker,
|
|||||||
recipient,
|
recipient,
|
||||||
set_locale_files,
|
set_locale_files,
|
||||||
mock_notifier_api):
|
mock_notifier_api):
|
||||||
|
|
||||||
notifier = Notifier()
|
notifier = Notifier()
|
||||||
notifier.queue = None
|
notifier.queue = None
|
||||||
|
|
||||||
@@ -28,9 +27,3 @@ def test_send_sms_notification(celery_session_worker,
|
|||||||
assert messages[0].get('message') == expected_message
|
assert messages[0].get('message') == expected_message
|
||||||
assert messages[0].get('recipient') == recipient
|
assert messages[0].get('recipient') == recipient
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user