Compare commits

...

39 Commits

Author SHA1 Message Date
PhilipWafula 1507ad3b55
Merge branch 'master' of gitlab.com:grassrootseconomics/cic-internal-integration into philip/multi-token-v1 2021-11-29 17:56:10 +03:00
PhilipWafula 29b2d7084a
Adds token decimals to transaction object. 2021-11-22 19:54:29 +03:00
PhilipWafula dc454eeec8
Removes superfluous logging. 2021-11-22 19:53:46 +03:00
PhilipWafula 25f43a8e79
Shifts to retrieving token decimals from transaction object. 2021-11-22 19:53:17 +03:00
PhilipWafula 7aa2565a3f
Corrects tx insertion to statement cache. 2021-11-22 19:52:20 +03:00
PhilipWafula 5f064bf713
Corrects statement and token listing. 2021-11-22 19:51:49 +03:00
PhilipWafula 22e142a2e3
Improves callback handle to handle multi-token ops. 2021-11-22 13:26:01 +03:00
PhilipWafula f52494e633
Adds missing proof to declarator 2021-11-22 13:25:39 +03:00
PhilipWafula 167d05667e
Bump cic-types 2021-11-22 13:25:16 +03:00
PhilipWafula c7dd285bbb
Updates statement funcs to adapt changes from multi-token env. 2021-11-22 13:24:56 +03:00
PhilipWafula 07c90cc7e5
Improves updating of session data. 2021-11-22 13:24:21 +03:00
PhilipWafula 098309f294
Updates notification handling to match multi-token env. 2021-11-22 13:23:54 +03:00
PhilipWafula 9addb901cc
Adds notion of last received and last token for ordering of the tokens list. 2021-11-22 13:22:04 +03:00
PhilipWafula b18bb959a5
Refactors cache to use versatile identifier values with multiple salts. 2021-11-22 13:21:29 +03:00
PhilipWafula 8e365ea586
Updates menu logic to adapt changes from multi-token env. 2021-11-22 13:17:39 +03:00
PhilipWafula c3ee108c8b
Adds util functions for polling against redis cache. 2021-11-22 13:16:44 +03:00
PhilipWafula b3fdd8b52c
Adds menu logic for token handling. 2021-11-22 13:16:19 +03:00
PhilipWafula eff588ad9e
Refactor balance retrieval to suite multi-token environment. 2021-11-22 13:16:01 +03:00
PhilipWafula 2c8acce80d
Add menu response handlers in view of changes to base logic 2021-11-22 13:15:24 +03:00
PhilipWafula 388f517ee3
Adds new menu navigation logic. 2021-11-22 13:14:57 +03:00
PhilipWafula 02f27cd5e8
Makes decimal explicitly declared for transactional operations. 2021-11-22 13:10:24 +03:00
PhilipWafula be31de0a8a
Adds auxiliary to handle multi-token ops. 2021-11-22 12:59:11 +03:00
PhilipWafula b3baa6acb6
Adds app password pepper override for pin encryption. 2021-11-22 12:47:29 +03:00
PhilipWafula 444f0af057
Adds translations for multi-token menus. 2021-11-22 12:42:57 +03:00
PhilipWafula 3cad3eb515
Updates state and transitions to incorporate multi token setup. 2021-11-22 12:42:26 +03:00
PhilipWafula 74c159d7d3
Updates transitions to include multi-token env setup. 2021-11-22 12:41:21 +03:00
PhilipWafula 6c447c24e9
Merge branch 'master' of gitlab.com:grassrootseconomics/cic-internal-integration into philip/multi-token-v1 2021-11-17 11:23:39 +03:00
PhilipWafula 4f99529924
Fixes encoding error. 2021-11-05 14:11:44 +03:00
PhilipWafula b0a4138e48
Merge branch 'master' of gitlab.com:grassrootseconomics/cic-internal-integration into philip/multi-token-v1 2021-11-03 10:33:17 +03:00
PhilipWafula 26e9e1ab2e
Adds missing env var values. 2021-11-02 17:01:20 +03:00
PhilipWafula 85c419e2db
Cleans up module. 2021-11-02 17:00:36 +03:00
PhilipWafula 1242a747da
Merge branch 'master' of gitlab.com:grassrootseconomics/cic-internal-integration into philip/multi-token-v1 2021-11-02 11:00:14 +03:00
PhilipWafula 8fd238b81d
Merge branch 'master' of gitlab.com:grassrootseconomics/cic-internal-integration into philip/multi-token-v1 2021-10-27 17:47:57 +03:00
PhilipWafula d2b811c124
Merge branch 'master' of gitlab.com:grassrootseconomics/cic-internal-integration into philip/multi-token-v1 2021-10-27 11:53:53 +03:00
PhilipWafula 103b7885b1
Handles token related callback handling. 2021-10-25 21:03:46 +03:00
PhilipWafula 97b80aaf38
Make cic-types optional. 2021-10-25 21:03:18 +03:00
PhilipWafula b34ce7866c
Handles token data processing. 2021-10-25 21:01:39 +03:00
PhilipWafula bbd43473b6
Adds token helper functions. 2021-10-25 21:01:25 +03:00
PhilipWafula 3a97cf95e8
Adds token metadata handler. 2021-10-25 21:01:00 +03:00
32 changed files with 1037 additions and 119 deletions

View File

@ -2,7 +2,7 @@
import json import json
import logging import logging
from typing import Optional from typing import Union, Optional
# third-party imports # third-party imports
from cic_eth.api import Api from cic_eth.api import Api
@ -14,7 +14,7 @@ from cic_ussd.account.transaction import from_wei
from cic_ussd.cache import cache_data_key, get_cached_data from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.error import CachedDataNotFoundError from cic_ussd.error import CachedDataNotFoundError
logg = logging.getLogger() logg = logging.getLogger(__file__)
def get_balances(address: str, def get_balances(address: str,
@ -43,7 +43,7 @@ def get_balances(address: str,
:return: A list containing balance data if called synchronously. | None :return: A list containing balance data if called synchronously. | None
:rtype: list | None :rtype: list | None
""" """
logg.debug(f'retrieving balance for address: {address}') logg.debug(f'retrieving {token_symbol} balance for address: {address}')
if asynchronous: if asynchronous:
cic_eth_api = Api( cic_eth_api = Api(
chain_str=chain_str, chain_str=chain_str,
@ -60,11 +60,13 @@ def get_balances(address: str,
return balance_request_task.get() return balance_request_task.get()
def calculate_available_balance(balances: dict) -> float: def calculate_available_balance(balances: dict, decimals: int) -> float:
"""This function calculates an account's balance at a specific point in time by computing the difference from the """This function calculates an account's balance at a specific point in time by computing the difference from the
outgoing balance and the sum of the incoming and network balances. outgoing balance and the sum of the incoming and network balances.
:param balances: incoming, network and outgoing balances. :param balances: incoming, network and outgoing balances.
:type balances: dict :type balances: dict
:param decimals:
:type decimals: int
:return: Token value of the available balance. :return: Token value of the available balance.
:rtype: float :rtype: float
""" """
@ -73,7 +75,7 @@ def calculate_available_balance(balances: dict) -> float:
network_balance = balances.get('balance_network') network_balance = balances.get('balance_network')
available_balance = (network_balance + incoming_balance) - outgoing_balance available_balance = (network_balance + incoming_balance) - outgoing_balance
return from_wei(value=available_balance) return from_wei(decimals=decimals, value=available_balance)
def get_adjusted_balance(balance: int, chain_str: str, timestamp: int, token_symbol: str): def get_adjusted_balance(balance: int, chain_str: str, timestamp: int, token_symbol: str):
@ -94,24 +96,25 @@ def get_adjusted_balance(balance: int, chain_str: str, timestamp: int, token_sym
return demurrage_api.get_adjusted_balance(token_symbol, balance, timestamp).result return demurrage_api.get_adjusted_balance(token_symbol, balance, timestamp).result
def get_cached_available_balance(blockchain_address: str) -> float: def get_cached_available_balance(decimals: int, identifier: Union[list, bytes]) -> float:
"""This function attempts to retrieve balance data from the redis cache. """This function attempts to retrieve balance data from the redis cache.
:param blockchain_address: Ethereum address of an account. :param decimals:
:type blockchain_address: str :type decimals: int
:param identifier: An identifier needed to create a unique pointer to a balances resource.
:type identifier: bytes | list
:raises CachedDataNotFoundError: No cached balance data could be found. :raises CachedDataNotFoundError: No cached balance data could be found.
:return: Operational balance of an account. :return: Operational balance of an account.
:rtype: float :rtype: float
""" """
identifier = bytes.fromhex(blockchain_address) key = cache_data_key(identifier=identifier, salt=MetadataPointer.BALANCES)
key = cache_data_key(identifier, salt=MetadataPointer.BALANCES)
cached_balances = get_cached_data(key=key) cached_balances = get_cached_data(key=key)
if cached_balances: if cached_balances:
return calculate_available_balance(json.loads(cached_balances)) return calculate_available_balance(balances=json.loads(cached_balances), decimals=decimals)
else: else:
raise CachedDataNotFoundError(f'No cached available balance for address: {blockchain_address}') raise CachedDataNotFoundError(f'No cached available balance at {key}')
def get_cached_adjusted_balance(identifier: bytes): def get_cached_adjusted_balance(identifier: Union[list, bytes]):
""" """
:param identifier: :param identifier:
:type identifier: :type identifier:
@ -120,3 +123,22 @@ def get_cached_adjusted_balance(identifier: bytes):
""" """
key = cache_data_key(identifier, MetadataPointer.BALANCES_ADJUSTED) key = cache_data_key(identifier, MetadataPointer.BALANCES_ADJUSTED)
return get_cached_data(key) return get_cached_data(key)
def get_account_tokens_balance(blockchain_address: str, chain_str: str, token_symbols_list: list):
"""
:param blockchain_address:
:type blockchain_address:
:param chain_str:
:type chain_str:
:param token_symbols_list:
:type token_symbols_list:
:return:
:rtype:
"""
for token_symbol in token_symbols_list:
get_balances(address=blockchain_address,
chain_str=chain_str,
token_symbol=token_symbol,
asynchronous=True,
callback_param=f'{blockchain_address},{token_symbol}')

View File

@ -69,7 +69,8 @@ def parse_statement_transactions(statement: list):
parsed_transactions = [] parsed_transactions = []
for transaction in statement: for transaction in statement:
action_tag = transaction.get('action_tag') action_tag = transaction.get('action_tag')
amount = from_wei(transaction.get('token_value')) decimals = transaction.get('token_decimals')
amount = from_wei(decimals, transaction.get('token_value'))
direction_tag = transaction.get('direction_tag') direction_tag = transaction.get('direction_tag')
token_symbol = transaction.get('token_symbol') token_symbol = transaction.get('token_symbol')
metadata_id = transaction.get('metadata_id') metadata_id = transaction.get('metadata_id')

View File

@ -1,19 +1,115 @@
# standard imports # standard imports
import hashlib
import json import json
import logging import logging
from typing import Dict, Optional from typing import Optional, Union
# external imports # external imports
from cic_eth.api import Api from cic_eth.api import Api
from cic_types.condiments import MetadataPointer from cic_types.condiments import MetadataPointer
# local imports # local imports
from cic_ussd.account.balance import get_cached_available_balance
from cic_ussd.account.chain import Chain from cic_ussd.account.chain import Chain
from cic_ussd.cache import cache_data_key, get_cached_data from cic_ussd.cache import cache_data, cache_data_key, get_cached_data
from cic_ussd.error import SeppukuError from cic_ussd.error import CachedDataNotFoundError, SeppukuError
from cic_ussd.metadata.tokens import query_token_info, query_token_metadata
from cic_ussd.processor.util import wait_for_cache
from cic_ussd.translation import translation_for
logg = logging.getLogger(__file__)
logg = logging.getLogger(__name__) def collate_token_metadata(token_info: dict, token_metadata: dict) -> dict:
"""
:param token_info:
:type token_info:
:param token_metadata:
:type token_metadata:
:return:
:rtype:
"""
logg.debug(f'Collating token info: {token_info} and token metadata: {token_metadata}')
description = token_info.get('description')
issuer = token_info.get('issuer')
location = token_metadata.get('location')
contact = token_metadata.get('contact')
return {
'description': description,
'issuer': issuer,
'location': location,
'contact': contact
}
def create_account_tokens_list(blockchain_address: str):
"""
:param blockchain_address:
:type blockchain_address:
:return:
:rtype:
"""
token_symbols_list = get_cached_token_symbol_list(blockchain_address=blockchain_address)
token_list_entries = []
if token_symbols_list:
logg.debug(f'Token symbols: {token_symbols_list} for account: {blockchain_address}')
for token_symbol in token_symbols_list:
entry = {}
logg.debug(f'Processing token data for: {token_symbol}')
key = cache_data_key([bytes.fromhex(blockchain_address), token_symbol.encode('utf-8')], MetadataPointer.TOKEN_DATA)
token_data = get_cached_data(key)
token_data = json.loads(token_data)
logg.debug(f'Retrieved token data: {token_data} for: {token_symbol}')
token_name = token_data.get('name')
entry['name'] = token_name
token_symbol = token_data.get('symbol')
entry['symbol'] = token_symbol
token_issuer = token_data.get('issuer')
entry['issuer'] = token_issuer
token_contact = token_data['contact'].get('phone')
entry['contact'] = token_contact
token_location = token_data.get('location')
entry['location'] = token_location
decimals = token_data.get('decimals')
identifier = [bytes.fromhex(blockchain_address), token_symbol.encode('utf-8')]
wait_for_cache(identifier, f'Cached available balance for token: {token_symbol}', MetadataPointer.BALANCES)
token_balance = get_cached_available_balance(decimals=decimals, identifier=identifier)
entry['balance'] = token_balance
token_list_entries.append(entry)
account_tokens_list = order_account_tokens_list(token_list_entries, bytes.fromhex(blockchain_address))
key = cache_data_key(bytes.fromhex(blockchain_address), MetadataPointer.TOKEN_DATA_LIST)
cache_data(key, json.dumps(account_tokens_list))
def get_active_token_symbol(blockchain_address: str):
"""
:param blockchain_address:
:type blockchain_address:
:return:
:rtype:
"""
identifier = bytes.fromhex(blockchain_address)
key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_ACTIVE)
active_token_symbol = get_cached_data(key)
if not active_token_symbol:
raise CachedDataNotFoundError('No active token set.')
return active_token_symbol
def get_cached_token_data(blockchain_address: str, token_symbol: str):
"""
:param blockchain_address:
:type blockchain_address:
:param token_symbol:
:type token_symbol:
:return:
:rtype:
"""
identifier = [bytes.fromhex(blockchain_address), token_symbol.encode('utf-8')]
key = cache_data_key(identifier, MetadataPointer.TOKEN_DATA)
logg.debug(f'Retrieving token data for: {token_symbol} at: {key}')
token_data = get_cached_data(key)
return json.loads(token_data)
def get_cached_default_token(chain_str: str) -> Optional[str]: def get_cached_default_token(chain_str: str) -> Optional[str]:
@ -49,6 +145,132 @@ def get_default_token_symbol():
raise SeppukuError(f'Could not retrieve default token for: {chain_str}') raise SeppukuError(f'Could not retrieve default token for: {chain_str}')
def get_cached_token_symbol_list(blockchain_address: str) -> Optional[list]:
"""
:param blockchain_address:
:type blockchain_address:
:return:
:rtype:
"""
key = cache_data_key(identifier=bytes.fromhex(blockchain_address), salt=MetadataPointer.TOKEN_SYMBOLS_LIST)
token_symbols_list = get_cached_data(key)
if token_symbols_list:
return json.loads(token_symbols_list)
return token_symbols_list
def get_cached_token_data_list(blockchain_address: str) -> Optional[list]:
"""
:param blockchain_address:
:type blockchain_address:
:return:
:rtype:
"""
key = cache_data_key(bytes.fromhex(blockchain_address), MetadataPointer.TOKEN_DATA_LIST)
token_data_list = get_cached_data(key)
if token_data_list:
return json.loads(token_data_list)
return token_data_list
def handle_token_symbol_list(blockchain_address: str, token_symbol: str):
"""
:param blockchain_address:
:type blockchain_address:
:param token_symbol:
:type token_symbol:
:return:
:rtype:
"""
token_symbol_list = get_cached_token_symbol_list(blockchain_address)
if token_symbol_list:
if token_symbol not in token_symbol_list:
token_symbol_list.append(token_symbol)
else:
token_symbol_list = [token_symbol]
identifier = bytes.fromhex(blockchain_address)
key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_SYMBOLS_LIST)
data = json.dumps(token_symbol_list)
cache_data(key, data)
def hashed_token_proof(token_proof: Union[dict, str]) -> str:
"""
:param token_proof:
:type token_proof:
:return:
:rtype:
"""
if isinstance(token_proof, dict):
token_proof = json.dumps(token_proof)
logg.debug(f'Hashing token proof: {token_proof}')
hash_object = hashlib.new("sha256")
hash_object.update(token_proof.encode('utf-8'))
return hash_object.digest().hex()
def order_account_tokens_list(account_tokens_list: list, identifier: bytes) -> list:
"""
:param account_tokens_list:
:type account_tokens_list:
:param identifier:
:type identifier:
:return:
:rtype:
"""
ordered_tokens_list = []
# get last sent token
key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_LAST_SENT)
last_sent_token_symbol = get_cached_data(key)
# get last received token
key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_LAST_RECEIVED)
last_received_token_symbol = get_cached_data(key)
last_sent_token_data, remaining_accounts_token_list = remove_from_account_tokens_list(account_tokens_list, last_sent_token_symbol)
if last_sent_token_data:
ordered_tokens_list.append(last_sent_token_data[0])
last_received_token_data, remaining_accounts_token_list = remove_from_account_tokens_list(remaining_accounts_token_list, last_received_token_symbol)
if last_received_token_data:
ordered_tokens_list.append(last_received_token_data[0])
# order the by balance
ordered_by_balance = sorted(remaining_accounts_token_list, key=lambda d: d['balance'], reverse=True)
return ordered_tokens_list + ordered_by_balance
def parse_token_list(account_token_list: list):
parsed_token_list = []
for i in range(len(account_token_list)):
token_symbol = account_token_list[i].get('symbol')
token_balance = account_token_list[i].get('balance')
token_data_repr = f'{i+1}. {token_symbol} {token_balance}'
parsed_token_list.append(token_data_repr)
return parsed_token_list
def process_token_data(blockchain_address: str, token_symbol: str):
"""
:param blockchain_address:
:type blockchain_address:
:param token_symbol:
:type token_symbol:
:return:
:rtype:
"""
logg.debug(f'Processing token data for token: {token_symbol}')
identifier = token_symbol.encode('utf-8')
query_token_metadata(identifier=identifier)
token_info = query_token_info(identifier=identifier)
hashed_token_info = hashed_token_proof(token_proof=token_info)
query_token_data(blockchain_address=blockchain_address,
hashed_proofs=[hashed_token_info],
token_symbols=[token_symbol])
def query_default_token(chain_str: str): def query_default_token(chain_str: str):
"""This function synchronously queries cic-eth for the deployed system's default token. """This function synchronously queries cic-eth for the deployed system's default token.
:param chain_str: Chain name and network id. :param chain_str: Chain name and network id.
@ -60,3 +282,60 @@ def query_default_token(chain_str: str):
cic_eth_api = Api(chain_str=chain_str) cic_eth_api = Api(chain_str=chain_str)
default_token_request_task = cic_eth_api.default_token() default_token_request_task = cic_eth_api.default_token()
return default_token_request_task.get() return default_token_request_task.get()
def query_token_data(blockchain_address: str, hashed_proofs: list, token_symbols: list):
""""""
logg.debug(f'Retrieving token metadata for tokens: {", ".join(token_symbols)}')
api = Api(callback_param=blockchain_address,
callback_queue='cic-ussd',
chain_str=Chain.spec.__str__(),
callback_task='cic_ussd.tasks.callback_handler.token_data_callback')
api.tokens(token_symbols=token_symbols, proof=hashed_proofs)
def remove_from_account_tokens_list(account_tokens_list: list, token_symbol: str):
"""
:param account_tokens_list:
:type account_tokens_list:
:param token_symbol:
:type token_symbol:
:return:
:rtype:
"""
removed_token_data = []
for i in range(len(account_tokens_list)):
if account_tokens_list[i]['symbol'] == token_symbol:
removed_token_data.append(account_tokens_list[i])
del account_tokens_list[i]
break
return removed_token_data, account_tokens_list
def set_active_token(blockchain_address: str, token_symbol: str):
"""
:param blockchain_address:
:type blockchain_address:
:param token_symbol:
:type token_symbol:
:return:
:rtype:
"""
logg.info(f'Active token set to: {token_symbol}')
key = cache_data_key(identifier=bytes.fromhex(blockchain_address), salt=MetadataPointer.TOKEN_ACTIVE)
cache_data(key=key, data=token_symbol)
def token_list_set(preferred_language: str, token_data_reprs: list):
"""
:param preferred_language:
:type preferred_language:
:param token_data_reprs:
:type token_data_reprs:
:return:
:rtype:
"""
if not token_data_reprs:
return translation_for('helpers.no_tokens_list', preferred_language)
return ''.join(f'{token_data_repr}\n' for token_data_repr in token_data_reprs)

View File

@ -1,7 +1,6 @@
# standard import # standard import
import decimal
import json
import logging import logging
from math import trunc
from typing import Dict, Tuple from typing import Dict, Tuple
# external import # external import
@ -9,8 +8,6 @@ from cic_eth.api import Api
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
# local import # local import
from cic_ussd.account.chain import Chain
from cic_ussd.account.tokens import get_cached_default_token
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase from cic_ussd.db.models.base import SessionBase
from cic_ussd.error import UnknownUssdRecipient from cic_ussd.error import UnknownUssdRecipient
@ -55,32 +52,32 @@ def aux_transaction_data(preferred_language: str, transaction: dict) -> dict:
return transaction return transaction
def from_wei(value: int) -> float: def from_wei(decimals: int, value: int) -> float:
"""This function converts values in Wei to a token in the cic network. """This function converts values in Wei to a token in the cic network.
:param decimals: The decimals required for wei values.
:type decimals: int
:param value: Value in Wei :param value: Value in Wei
:type value: int :type value: int
:return: SRF equivalent of value in Wei :return: SRF equivalent of value in Wei
:rtype: float :rtype: float
""" """
cached_token_data = json.loads(get_cached_default_token(Chain.spec.__str__())) value = float(value) / (10**decimals)
token_decimals: int = cached_token_data.get('decimals')
value = float(value) / (10**token_decimals)
return truncate(value=value, decimals=2) return truncate(value=value, decimals=2)
def to_wei(value: int) -> int: def to_wei(decimals: int, value: int) -> int:
"""This functions converts values from a token in the cic network to Wei. """This functions converts values from a token in the cic network to Wei.
:param decimals: The decimals required for wei values.
:type decimals: int
:param value: Value in SRF :param value: Value in SRF
:type value: int :type value: int
:return: Wei equivalent of value in SRF :return: Wei equivalent of value in SRF
:rtype: int :rtype: int
""" """
cached_token_data = json.loads(get_cached_default_token(Chain.spec.__str__())) return int(value * (10**decimals))
token_decimals: int = cached_token_data.get('decimals')
return int(value * (10**token_decimals))
def truncate(value: float, decimals: int): def truncate(value: float, decimals: int) -> float:
"""This function truncates a value to a specified number of decimals places. """This function truncates a value to a specified number of decimals places.
:param value: The value to be truncated. :param value: The value to be truncated.
:type value: float :type value: float
@ -89,9 +86,8 @@ def truncate(value: float, decimals: int):
:return: The truncated value. :return: The truncated value.
:rtype: int :rtype: int
""" """
decimal.getcontext().rounding = decimal.ROUND_DOWN stepper = 10.0**decimals
contextualized_value = decimal.Decimal(value) return trunc(stepper*value) / stepper
return round(contextualized_value, decimals)
def transaction_actors(transaction: dict) -> Tuple[Dict, Dict]: def transaction_actors(transaction: dict) -> Tuple[Dict, Dict]:
@ -104,14 +100,17 @@ def transaction_actors(transaction: dict) -> Tuple[Dict, Dict]:
""" """
destination_token_symbol = transaction.get('destination_token_symbol') destination_token_symbol = transaction.get('destination_token_symbol')
destination_token_value = transaction.get('destination_token_value') or transaction.get('to_value') destination_token_value = transaction.get('destination_token_value') or transaction.get('to_value')
destination_token_decimals = transaction.get('destination_token_decimals')
recipient_blockchain_address = transaction.get('recipient') recipient_blockchain_address = transaction.get('recipient')
sender_blockchain_address = transaction.get('sender') sender_blockchain_address = transaction.get('sender')
source_token_symbol = transaction.get('source_token_symbol') source_token_symbol = transaction.get('source_token_symbol')
source_token_value = transaction.get('source_token_value') or transaction.get('from_value') source_token_value = transaction.get('source_token_value') or transaction.get('from_value')
source_token_decimals = transaction.get('source_token_decimals')
recipient_transaction_data = { recipient_transaction_data = {
"token_symbol": destination_token_symbol, "token_symbol": destination_token_symbol,
"token_value": destination_token_value, "token_value": destination_token_value,
"token_decimals": destination_token_decimals,
"blockchain_address": recipient_blockchain_address, "blockchain_address": recipient_blockchain_address,
"role": "recipient", "role": "recipient",
} }
@ -119,6 +118,7 @@ def transaction_actors(transaction: dict) -> Tuple[Dict, Dict]:
"blockchain_address": sender_blockchain_address, "blockchain_address": sender_blockchain_address,
"token_symbol": source_token_symbol, "token_symbol": source_token_symbol,
"token_value": source_token_value, "token_value": source_token_value,
"token_decimals": source_token_decimals,
"role": "sender", "role": "sender",
} }
return recipient_transaction_data, sender_transaction_data return recipient_transaction_data, sender_transaction_data
@ -166,14 +166,16 @@ class OutgoingTransaction:
self.from_address = from_address self.from_address = from_address
self.to_address = to_address self.to_address = to_address
def transfer(self, amount: int, token_symbol: str): def transfer(self, amount: int, decimals: int, token_symbol: str):
"""This function initiates standard transfers between one account to another """This function initiates standard transfers between one account to another
:param amount: The amount of tokens to be sent :param amount: The amount of tokens to be sent
:type amount: int :type amount: int
:param decimals: The decimals for the token being transferred.
:type decimals: int
:param token_symbol: ERC20 token symbol of token to send :param token_symbol: ERC20 token symbol of token to send
:type token_symbol: str :type token_symbol: str
""" """
self.cic_eth_api.transfer(from_address=self.from_address, self.cic_eth_api.transfer(from_address=self.from_address,
to_address=self.to_address, to_address=self.to_address,
value=to_wei(value=amount), value=to_wei(decimals=decimals, value=amount),
token_symbol=token_symbol) token_symbol=token_symbol)

View File

@ -1,12 +1,13 @@
# standard imports # standard imports
import hashlib import hashlib
import logging import logging
from typing import Union
# external imports # external imports
from cic_types.condiments import MetadataPointer from cic_types.condiments import MetadataPointer
from redis import Redis from redis import Redis
logg = logging.getLogger() logg = logging.getLogger(__file__)
class Cache: class Cache:
@ -39,7 +40,7 @@ def get_cached_data(key: str):
return cache.get(name=key) return cache.get(name=key)
def cache_data_key(identifier: bytes, salt: MetadataPointer): def cache_data_key(identifier: Union[list, bytes], salt: MetadataPointer):
""" """
:param identifier: :param identifier:
:type identifier: :type identifier:
@ -49,6 +50,10 @@ def cache_data_key(identifier: bytes, salt: MetadataPointer):
:rtype: :rtype:
""" """
hash_object = hashlib.new("sha256") hash_object = hashlib.new("sha256")
hash_object.update(identifier) if isinstance(identifier, list):
for identity in identifier:
hash_object.update(identity)
else:
hash_object.update(identifier)
hash_object.update(salt.value.encode(encoding="utf-8")) hash_object.update(salt.value.encode(encoding="utf-8"))
return hash_object.digest().hex() return hash_object.digest().hex()

View File

@ -287,7 +287,36 @@
"display_key": "ussd.kenya.dob_edit_pin_authorization", "display_key": "ussd.kenya.dob_edit_pin_authorization",
"name": "dob_edit_pin_authorization", "name": "dob_edit_pin_authorization",
"parent": "metadata_management" "parent": "metadata_management"
},
"49": {
"description": "Menu to display first set of tokens in the account's token list.",
"display_key": "ussd.kenya.first_account_tokens_set",
"name": "first_account_tokens_set",
"parent": null
},
"50": {
"description": "Menu to display middle set of tokens in the account's token list.",
"display_key": "ussd.kenya.middle_account_tokens_set",
"name": "middle_account_tokens_set",
"parent": null
},
"51": {
"description": "Menu to display last set of tokens in the account's token list.",
"display_key": "ussd.kenya.last_account_tokens_set",
"name": "last_account_tokens_set",
"parent": null
},
"52": {
"description": "Pin entry menu for setting an active token.",
"display_key": "ussd.kenya.token_selection_pin_authorization",
"name": "token_selection_pin_authorization",
"parent": null
},
"53": {
"description": "Exit following a successful active token setting.",
"display_key": "ussd.kenya.exit_successful_token_selection",
"name": "exit_successful_token_selection",
"parent": null
} }
} }
} }

View File

@ -13,7 +13,7 @@ logg = logging.getLogger(__file__)
class UssdMetadataHandler(MetadataRequestsHandler): class UssdMetadataHandler(MetadataRequestsHandler):
def __init__(self, cic_type: MetadataPointer, identifier: bytes): def __init__(self, identifier: bytes, cic_type: MetadataPointer = None):
super().__init__(cic_type, identifier) super().__init__(cic_type, identifier)
def cache_metadata(self, data: str): def cache_metadata(self, data: str):

View File

@ -0,0 +1,54 @@
# standard imports
from typing import Dict, Optional
# external imports
import json
from cic_types.condiments import MetadataPointer
# local imports
from .base import UssdMetadataHandler
from cic_ussd.cache import cache_data
from cic_ussd.error import MetadataNotFoundError
class TokenMetadata(UssdMetadataHandler):
def __init__(self, identifier: bytes, **kwargs):
super(TokenMetadata, self).__init__(identifier=identifier, **kwargs)
def token_metadata_handler(metadata_client: TokenMetadata) -> Optional[Dict]:
"""
:param metadata_client:
:type metadata_client:
:return:
:rtype:
"""
result = metadata_client.query()
token_metadata = result.json()
if not token_metadata:
raise MetadataNotFoundError(f'No metadata found at: {metadata_client.metadata_pointer} for: {metadata_client.identifier.decode("utf-8")}')
cache_data(metadata_client.metadata_pointer, json.dumps(token_metadata))
return token_metadata
def query_token_metadata(identifier: bytes):
"""
:param identifier:
:type identifier:
:return:
:rtype:
"""
token_metadata_client = TokenMetadata(identifier=identifier, cic_type=MetadataPointer.TOKEN_META_SYMBOL)
return token_metadata_handler(token_metadata_client)
def query_token_info(identifier: bytes):
"""
:param identifier:
:type identifier:
:return:
:rtype:
"""
token_info_client = TokenMetadata(identifier=identifier, cic_type=MetadataPointer.TOKEN_PROOF_SYMBOL)
return token_metadata_handler(token_info_client)

View File

@ -9,6 +9,7 @@ from cic_types.condiments import MetadataPointer
# local imports # local imports
from cic_ussd.account.balance import (calculate_available_balance, from cic_ussd.account.balance import (calculate_available_balance,
get_account_tokens_balance,
get_adjusted_balance, get_adjusted_balance,
get_balances, get_balances,
get_cached_adjusted_balance, get_cached_adjusted_balance,
@ -21,13 +22,20 @@ from cic_ussd.account.statement import (
query_statement, query_statement,
statement_transaction_set statement_transaction_set
) )
from cic_ussd.account.tokens import get_default_token_symbol from cic_ussd.account.tokens import (create_account_tokens_list,
get_active_token_symbol,
get_cached_token_data,
get_cached_token_symbol_list,
get_cached_token_data_list,
parse_token_list,
token_list_set)
from cic_ussd.account.transaction import from_wei, to_wei from cic_ussd.account.transaction import from_wei, to_wei
from cic_ussd.cache import cache_data_key, cache_data from cic_ussd.cache import cache_data_key, cache_data
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
from cic_ussd.metadata import PersonMetadata from cic_ussd.metadata import PersonMetadata
from cic_ussd.phone_number import Support from cic_ussd.phone_number import Support
from cic_ussd.processor.util import parse_person_metadata from cic_ussd.processor.util import parse_person_metadata
from cic_ussd.session.ussd_session import save_session_data
from cic_ussd.translation import translation_for from cic_ussd.translation import translation_for
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -48,22 +56,27 @@ class MenuProcessor:
:return: :return:
:rtype: :rtype:
""" """
available_balance = get_cached_available_balance(self.account.blockchain_address)
adjusted_balance = get_cached_adjusted_balance(self.identifier) adjusted_balance = get_cached_adjusted_balance(self.identifier)
token_symbol = get_default_token_symbol() token_symbol = get_active_token_symbol(self.account.blockchain_address)
token_data = get_cached_token_data(self.account.blockchain_address, token_symbol)
preferred_language = get_cached_preferred_language(self.account.blockchain_address) preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language: if not preferred_language:
preferred_language = i18n.config.get('fallback') preferred_language = i18n.config.get('fallback')
with_available_balance = f'{self.display_key}.available_balance' with_available_balance = f'{self.display_key}.available_balance'
with_fees = f'{self.display_key}.with_fees' with_fees = f'{self.display_key}.with_fees'
decimals = token_data.get('decimals')
available_balance = get_cached_available_balance(decimals, [self.identifier, token_symbol.encode('utf-8')])
if not adjusted_balance: if not adjusted_balance:
return translation_for(key=with_available_balance, return translation_for(key=with_available_balance,
preferred_language=preferred_language, preferred_language=preferred_language,
available_balance=available_balance, available_balance=available_balance,
token_symbol=token_symbol) token_symbol=token_symbol)
adjusted_balance = json.loads(adjusted_balance) adjusted_balance = json.loads(adjusted_balance)
tax_wei = to_wei(int(available_balance)) - int(adjusted_balance)
tax = from_wei(int(tax_wei)) tax_wei = to_wei(decimals, int(available_balance)) - int(adjusted_balance)
tax = from_wei(decimals, int(tax_wei))
return translation_for(key=with_fees, return translation_for(key=with_fees,
preferred_language=preferred_language, preferred_language=preferred_language,
available_balance=available_balance, available_balance=available_balance,
@ -76,21 +89,25 @@ class MenuProcessor:
:rtype: :rtype:
""" """
cached_statement = get_cached_statement(self.account.blockchain_address) cached_statement = get_cached_statement(self.account.blockchain_address)
statement = json.loads(cached_statement) transaction_sets = []
statement_transactions = parse_statement_transactions(statement) if cached_statement:
transaction_sets = [statement_transactions[tx:tx + 3] for tx in range(0, len(statement_transactions), 3)] statement = json.loads(cached_statement)
statement_transactions = parse_statement_transactions(statement)
transaction_sets = [statement_transactions[tx:tx + 3] for tx in range(0, len(statement_transactions), 3)]
preferred_language = get_cached_preferred_language(self.account.blockchain_address) preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language: if not preferred_language:
preferred_language = i18n.config.get('fallback') preferred_language = i18n.config.get('fallback')
first_transaction_set = [] no_transaction_history = statement_transaction_set(preferred_language, transaction_sets)
middle_transaction_set = [] first_transaction_set = no_transaction_history
last_transaction_set = [] middle_transaction_set = no_transaction_history
last_transaction_set = no_transaction_history
if transaction_sets: if transaction_sets:
first_transaction_set = statement_transaction_set(preferred_language, transaction_sets[0]) first_transaction_set = statement_transaction_set(preferred_language, transaction_sets[0])
if len(transaction_sets) >= 2: if len(transaction_sets) >= 2:
middle_transaction_set = statement_transaction_set(preferred_language, transaction_sets[1]) middle_transaction_set = statement_transaction_set(preferred_language, transaction_sets[1])
if len(transaction_sets) >= 3: if len(transaction_sets) >= 3:
last_transaction_set = statement_transaction_set(preferred_language, transaction_sets[2]) last_transaction_set = statement_transaction_set(preferred_language, transaction_sets[2])
if self.display_key == 'ussd.kenya.first_transaction_set': if self.display_key == 'ussd.kenya.first_transaction_set':
return translation_for( return translation_for(
self.display_key, preferred_language, first_transaction_set=first_transaction_set self.display_key, preferred_language, first_transaction_set=first_transaction_set
@ -104,7 +121,42 @@ class MenuProcessor:
self.display_key, preferred_language, last_transaction_set=last_transaction_set self.display_key, preferred_language, last_transaction_set=last_transaction_set
) )
def help(self): def account_tokens(self) -> str:
cached_token_data_list = get_cached_token_data_list(self.account.blockchain_address)
token_data_list = parse_token_list(cached_token_data_list)
token_list_sets = [token_data_list[tds:tds + 3] for tds in range(0, len(token_data_list), 3)]
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
no_token_list = token_list_set(preferred_language, [])
first_account_tokens_set = no_token_list
middle_account_tokens_set = no_token_list
last_account_tokens_set = no_token_list
if token_list_sets:
data = {
'account_tokens_list': cached_token_data_list
}
save_session_data(data=data, queue='cic-ussd', session=self.session, ussd_session=self.ussd_session)
first_account_tokens_set = token_list_set(preferred_language, token_list_sets[0])
if len(token_list_sets) >= 2:
middle_account_tokens_set = token_list_set(preferred_language, token_list_sets[1])
if len(token_list_sets) >= 3:
last_account_tokens_set = token_list_set(preferred_language, token_list_sets[2])
if self.display_key == 'ussd.kenya.first_account_tokens_set':
return translation_for(
self.display_key, preferred_language, first_account_tokens_set=first_account_tokens_set
)
if self.display_key == 'ussd.kenya.middle_account_tokens_set':
return translation_for(
self.display_key, preferred_language, middle_account_tokens_set=middle_account_tokens_set
)
if self.display_key == 'ussd.kenya.last_account_tokens_set':
return translation_for(
self.display_key, preferred_language, last_account_tokens_set=last_account_tokens_set
)
def help(self) -> str:
preferred_language = get_cached_preferred_language(self.account.blockchain_address) preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language: if not preferred_language:
preferred_language = i18n.config.get('fallback') preferred_language = i18n.config.get('fallback')
@ -161,24 +213,28 @@ class MenuProcessor:
:rtype: :rtype:
""" """
chain_str = Chain.spec.__str__() chain_str = Chain.spec.__str__()
token_symbol = get_default_token_symbol() token_symbol = get_active_token_symbol(self.account.blockchain_address)
token_data = get_cached_token_data(self.account.blockchain_address, token_symbol)
decimals = token_data.get('decimals')
blockchain_address = self.account.blockchain_address blockchain_address = self.account.blockchain_address
balances = get_balances(blockchain_address, chain_str, token_symbol, False)[0] balances = get_balances(blockchain_address, chain_str, token_symbol, False)[0]
key = cache_data_key(self.identifier, MetadataPointer.BALANCES) key = cache_data_key([self.identifier, token_symbol.encode('utf-8')], MetadataPointer.BALANCES)
cache_data(key, json.dumps(balances)) cache_data(key, json.dumps(balances))
available_balance = calculate_available_balance(balances) available_balance = calculate_available_balance(balances, decimals)
now = datetime.now() now = datetime.now()
if (now - self.account.created).days >= 30: if (now - self.account.created).days >= 30:
if available_balance <= 0: if available_balance <= 0:
logg.info(f'Not retrieving adjusted balance, available balance: {available_balance} is insufficient.') logg.info(f'Not retrieving adjusted balance, available balance: {available_balance} is insufficient.')
else: else:
timestamp = int((now - timedelta(30)).timestamp()) timestamp = int((now - timedelta(30)).timestamp())
adjusted_balance = get_adjusted_balance(to_wei(int(available_balance)), chain_str, timestamp, token_symbol) adjusted_balance = get_adjusted_balance(to_wei(decimals, int(available_balance)), chain_str, timestamp, token_symbol)
key = cache_data_key(self.identifier, MetadataPointer.BALANCES_ADJUSTED) key = cache_data_key([self.identifier, token_symbol.encode('utf-8')], MetadataPointer.BALANCES_ADJUSTED)
cache_data(key, json.dumps(adjusted_balance)) cache_data(key, json.dumps(adjusted_balance))
query_statement(blockchain_address) query_statement(blockchain_address)
token_symbols_list = get_cached_token_symbol_list(blockchain_address)
get_account_tokens_balance(blockchain_address, chain_str, token_symbols_list)
create_account_tokens_list(blockchain_address)
preferred_language = get_cached_preferred_language(self.account.blockchain_address) preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language: if not preferred_language:
preferred_language = i18n.config.get('fallback') preferred_language = i18n.config.get('fallback')
@ -186,6 +242,20 @@ class MenuProcessor:
self.display_key, preferred_language, account_balance=available_balance, account_token_name=token_symbol self.display_key, preferred_language, account_balance=available_balance, account_token_name=token_symbol
) )
def token_selection_pin_authorization(self) -> str:
"""
:return:
:rtype:
"""
selected_token = self.ussd_session.get('data').get('selected_token')
token_name = selected_token.get('name')
token_symbol = selected_token.get('symbol')
token_issuer = selected_token.get('issuer')
token_contact = selected_token.get('contact')
token_location = selected_token.get('location')
token_data = f'{token_name} ({token_symbol})\n{token_issuer}\n{token_contact}\n{token_location}\n'
return self.pin_authorization(token_data=token_data)
def transaction_pin_authorization(self) -> str: def transaction_pin_authorization(self) -> str:
""" """
:return: :return:
@ -195,12 +265,14 @@ class MenuProcessor:
recipient = Account.get_by_phone_number(recipient_phone_number, self.session) recipient = Account.get_by_phone_number(recipient_phone_number, self.session)
tx_recipient_information = recipient.standard_metadata_id() tx_recipient_information = recipient.standard_metadata_id()
tx_sender_information = self.account.standard_metadata_id() tx_sender_information = self.account.standard_metadata_id()
token_symbol = get_default_token_symbol() token_symbol = get_active_token_symbol(self.account.blockchain_address)
token_data = get_cached_token_data(self.account.blockchain_address, token_symbol)
user_input = self.ussd_session.get('data').get('transaction_amount') user_input = self.ussd_session.get('data').get('transaction_amount')
transaction_amount = to_wei(value=int(user_input)) decimals = token_data.get('decimals')
transaction_amount = to_wei(decimals=decimals, value=int(user_input))
return self.pin_authorization( return self.pin_authorization(
recipient_information=tx_recipient_information, recipient_information=tx_recipient_information,
transaction_amount=from_wei(transaction_amount), transaction_amount=from_wei(decimals, transaction_amount),
token_symbol=token_symbol, token_symbol=token_symbol,
sender_information=tx_sender_information sender_information=tx_sender_information
) )
@ -210,21 +282,23 @@ class MenuProcessor:
:return: :return:
:rtype: :rtype:
""" """
available_balance = get_cached_available_balance(self.account.blockchain_address)
preferred_language = get_cached_preferred_language(self.account.blockchain_address) preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language: if not preferred_language:
preferred_language = i18n.config.get('fallback') preferred_language = i18n.config.get('fallback')
session_data = self.ussd_session.get('data') session_data = self.ussd_session.get('data')
token_symbol = get_active_token_symbol(self.account.blockchain_address)
token_data = get_cached_token_data(self.account.blockchain_address, token_symbol)
decimals = token_data.get('decimals')
available_balance = get_cached_available_balance(decimals, [self.identifier, token_symbol.encode('utf-8')])
transaction_amount = session_data.get('transaction_amount') transaction_amount = session_data.get('transaction_amount')
transaction_amount = to_wei(value=int(transaction_amount)) transaction_amount = to_wei(decimals=decimals, value=int(transaction_amount))
token_symbol = get_default_token_symbol()
recipient_phone_number = self.ussd_session.get('data').get('recipient_phone_number') recipient_phone_number = self.ussd_session.get('data').get('recipient_phone_number')
recipient = Account.get_by_phone_number(recipient_phone_number, self.session) recipient = Account.get_by_phone_number(recipient_phone_number, self.session)
tx_recipient_information = recipient.standard_metadata_id() tx_recipient_information = recipient.standard_metadata_id()
return translation_for( return translation_for(
self.display_key, self.display_key,
preferred_language, preferred_language,
amount=from_wei(transaction_amount), amount=from_wei(decimals, transaction_amount),
token_symbol=token_symbol, token_symbol=token_symbol,
recipient_information=tx_recipient_information, recipient_information=tx_recipient_information,
token_balance=available_balance token_balance=available_balance
@ -242,6 +316,14 @@ class MenuProcessor:
preferred_language = i18n.config.get('fallback') preferred_language = i18n.config.get('fallback')
return translation_for('ussd.kenya.exit_pin_blocked', preferred_language, support_phone=Support.phone_number) return translation_for('ussd.kenya.exit_pin_blocked', preferred_language, support_phone=Support.phone_number)
def exit_successful_token_selection(self) -> str:
selected_token = self.ussd_session.get('data').get('selected_token')
token_symbol = selected_token.get('symbol')
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
return translation_for(self.display_key,preferred_language,token_symbol=token_symbol)
def exit_successful_transaction(self): def exit_successful_transaction(self):
""" """
:return: :return:
@ -251,8 +333,10 @@ class MenuProcessor:
preferred_language = get_cached_preferred_language(self.account.blockchain_address) preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language: if not preferred_language:
preferred_language = i18n.config.get('fallback') preferred_language = i18n.config.get('fallback')
transaction_amount = to_wei(amount) token_symbol = get_active_token_symbol(self.account.blockchain_address)
token_symbol = get_default_token_symbol() token_data = get_cached_token_data(self.account.blockchain_address, token_symbol)
decimals = token_data.get('decimals')
transaction_amount = to_wei(decimals, amount)
recipient_phone_number = self.ussd_session.get('data').get('recipient_phone_number') recipient_phone_number = self.ussd_session.get('data').get('recipient_phone_number')
recipient = Account.get_by_phone_number(phone_number=recipient_phone_number, session=self.session) recipient = Account.get_by_phone_number(phone_number=recipient_phone_number, session=self.session)
tx_recipient_information = recipient.standard_metadata_id() tx_recipient_information = recipient.standard_metadata_id()
@ -260,7 +344,7 @@ class MenuProcessor:
return translation_for( return translation_for(
self.display_key, self.display_key,
preferred_language, preferred_language,
transaction_amount=from_wei(transaction_amount), transaction_amount=from_wei(decimals, transaction_amount),
token_symbol=token_symbol, token_symbol=token_symbol,
recipient_information=tx_recipient_information, recipient_information=tx_recipient_information,
sender_information=tx_sender_information sender_information=tx_sender_information
@ -294,6 +378,10 @@ def response(account: Account, display_key: str, menu_name: str, session: Sessio
if menu_name == 'transaction_pin_authorization': if menu_name == 'transaction_pin_authorization':
return menu_processor.transaction_pin_authorization() return menu_processor.transaction_pin_authorization()
if menu_name == 'token_selection_pin_authorization':
logg.debug(f'RESPONSE IS: {menu_processor.token_selection_pin_authorization()}')
return menu_processor.token_selection_pin_authorization()
if menu_name == 'exit_insufficient_balance': if menu_name == 'exit_insufficient_balance':
return menu_processor.exit_insufficient_balance() return menu_processor.exit_insufficient_balance()
@ -312,6 +400,9 @@ def response(account: Account, display_key: str, menu_name: str, session: Sessio
if 'transaction_set' in menu_name: if 'transaction_set' in menu_name:
return menu_processor.account_statement() return menu_processor.account_statement()
if 'account_tokens_set' in menu_name:
return menu_processor.account_tokens()
if menu_name == 'display_user_metadata': if menu_name == 'display_user_metadata':
return menu_processor.person_metadata() return menu_processor.person_metadata()
@ -321,6 +412,9 @@ def response(account: Account, display_key: str, menu_name: str, session: Sessio
if menu_name == 'exit_pin_blocked': if menu_name == 'exit_pin_blocked':
return menu_processor.exit_pin_blocked() return menu_processor.exit_pin_blocked()
if menu_name == 'exit_successful_token_selection':
return menu_processor.exit_successful_token_selection()
preferred_language = get_cached_preferred_language(account.blockchain_address) preferred_language = get_cached_preferred_language(account.blockchain_address)
return translation_for(display_key, preferred_language) return translation_for(display_key, preferred_language)

View File

@ -1,15 +1,22 @@
# standard imports # standard imports
import datetime import datetime
import json import json
import logging
import time
from typing import Union
# external imports # external imports
from cic_types.condiments import MetadataPointer
from cic_types.models.person import get_contact_data_from_vcard from cic_types.models.person import get_contact_data_from_vcard
from tinydb.table import Document from tinydb.table import Document
# local imports # local imports
from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.menu.ussd_menu import UssdMenu from cic_ussd.menu.ussd_menu import UssdMenu
from cic_ussd.translation import translation_for from cic_ussd.translation import translation_for
logg = logging.getLogger(__file__)
def latest_input(user_input: str) -> str: def latest_input(user_input: str) -> str:
""" """
@ -76,3 +83,66 @@ def resume_last_ussd_session(last_state: str) -> Document:
if last_state in non_reusable_states: if last_state in non_reusable_states:
return UssdMenu.find_by_name('start') return UssdMenu.find_by_name('start')
return UssdMenu.find_by_name(last_state) return UssdMenu.find_by_name(last_state)
def wait_for_cache(identifier: Union[list, bytes], resource_name: str, salt: MetadataPointer, interval: int = 1, max_retry: int = 5):
"""
:param identifier:
:type identifier:
:param interval:
:type interval:
:param resource_name:
:type resource_name:
:param salt:
:type salt:
:param max_retry:
:type max_retry:
:return:
:rtype:
"""
key = cache_data_key(identifier=identifier, salt=salt)
resource = get_cached_data(key)
counter = 0
while resource is None:
logg.debug(f'Waiting for: {resource_name} at: {key}. Checking after: {interval} ...')
time.sleep(interval)
counter += 1
resource = get_cached_data(key)
if resource is not None:
logg.debug(f'{resource_name} now available.')
break
else:
if counter == max_retry:
logg.debug(f'Could not find: {resource_name} within: {max_retry}')
break
def wait_for_session_data(resource_name: str, session_data_key: str, ussd_session: dict, interval: int = 1, max_retry: int = 5):
"""
:param interval:
:type interval:
:param resource_name:
:type resource_name:
:param session_data_key:
:type session_data_key:
:param ussd_session:
:type ussd_session:
:param max_retry:
:type max_retry:
:return:
:rtype:
"""
session_data = ussd_session.get('data').get(session_data_key)
counter = 0
while session_data is None:
logg.debug(f'Waiting for: {resource_name}. Checking after: {interval} ...')
time.sleep(interval)
counter += 1
session_data = ussd_session.get('data').get(session_data_key)
if session_data is not None:
logg.debug(f'{resource_name} now available.')
break
else:
if counter == max_retry:
logg.debug(f'Could not find: {resource_name} within: {max_retry}')
break

View File

@ -63,7 +63,6 @@ elif ssl == 0:
else: else:
ssl = True ssl = True
valid_service_codes = config.get('USSD_SERVICE_CODE').split(",")
def main(): def main():
# TODO: improve url building # TODO: improve url building
@ -79,7 +78,7 @@ def main():
session = uuid.uuid4().hex session = uuid.uuid4().hex
data = { data = {
'sessionId': session, 'sessionId': session,
'serviceCode': valid_service_codes[0], 'serviceCode': config.get('USSD_SERVICE_CODE'),
'phoneNumber': args.phone, 'phoneNumber': args.phone,
'text': "", 'text': "",
} }

View File

@ -13,7 +13,7 @@ from cic_ussd.cache import Cache
from cic_ussd.db.models.base import SessionBase from cic_ussd.db.models.base import SessionBase
from cic_ussd.db.models.ussd_session import UssdSession as DbUssdSession from cic_ussd.db.models.ussd_session import UssdSession as DbUssdSession
logg = logging.getLogger() logg = logging.getLogger(__file__)
class UssdSession: class UssdSession:
@ -239,11 +239,16 @@ def save_session_data(queue: Optional[str], session: Session, data: dict, ussd_s
:param ussd_session: A ussd session passed to the state machine. :param ussd_session: A ussd session passed to the state machine.
:type ussd_session: UssdSession :type ussd_session: UssdSession
""" """
logg.debug(f'Saving: {data} session data to: {ussd_session}')
cache = Cache.store cache = Cache.store
external_session_id = ussd_session.get('external_session_id') external_session_id = ussd_session.get('external_session_id')
existing_session_data = ussd_session.get('data') existing_session_data = ussd_session.get('data')
if existing_session_data: if existing_session_data:
data = {**existing_session_data, **data} # replace session data entry
keys = data.keys()
for key in keys:
existing_session_data[key] = data[key]
data = existing_session_data
in_redis_ussd_session = cache.get(external_session_id) in_redis_ussd_session = cache.get(external_session_id)
in_redis_ussd_session = json.loads(in_redis_ussd_session) in_redis_ussd_session = json.loads(in_redis_ussd_session)
create_or_update_session( create_or_update_session(

View File

@ -93,6 +93,30 @@ def menu_zero_zero_selected(state_machine_data: Tuple[str, dict, Account, Sessio
return user_input == '00' return user_input == '00'
def menu_eleven_selected(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""
This function checks that user input matches a string with value '11'
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
:return: A user input's match with '99'
:rtype: bool
"""
user_input, ussd_session, account, session = state_machine_data
return user_input == '11'
def menu_twenty_two_selected(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""
This function checks that user input matches a string with value '22'
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
:return: A user input's match with '99'
:rtype: bool
"""
user_input, ussd_session, account, session = state_machine_data
return user_input == '22'
def menu_ninety_nine_selected(state_machine_data: Tuple[str, dict, Account, Session]) -> bool: def menu_ninety_nine_selected(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
""" """
This function checks that user input matches a string with value '99' This function checks that user input matches a string with value '99'

View File

@ -3,7 +3,6 @@ user's pin.
""" """
# standard imports # standard imports
import json
import logging import logging
import re import re
from typing import Tuple from typing import Tuple
@ -16,6 +15,7 @@ from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase from cic_ussd.db.models.base import SessionBase
from cic_ussd.db.enum import AccountStatus from cic_ussd.db.enum import AccountStatus
from cic_ussd.encoder import create_password_hash, check_password_hash from cic_ussd.encoder import create_password_hash, check_password_hash
from cic_ussd.processor.util import wait_for_session_data
from cic_ussd.session.ussd_session import create_or_update_session, persist_ussd_session from cic_ussd.session.ussd_session import create_or_update_session, persist_ussd_session
@ -31,11 +31,8 @@ def is_valid_pin(state_machine_data: Tuple[str, dict, Account, Session]) -> bool
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, account, session = state_machine_data user_input, ussd_session, account, session = state_machine_data
pin_is_valid = False
matcher = r'^\d{4}$' matcher = r'^\d{4}$'
if re.match(matcher, user_input): return bool(re.match(matcher, user_input))
pin_is_valid = True
return pin_is_valid
def is_authorized_pin(state_machine_data: Tuple[str, dict, Account, Session]) -> bool: def is_authorized_pin(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
@ -68,7 +65,7 @@ def save_initial_pin_to_session_data(state_machine_data: Tuple[str, dict, Accoun
: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, session = state_machine_data user_input, ussd_session, account, session = state_machine_data
initial_pin = create_password_hash(user_input) initial_pin = create_password_hash(user_input)
if ussd_session.get('data'): if ussd_session.get('data'):
data = ussd_session.get('data') data = ussd_session.get('data')
@ -97,7 +94,8 @@ def pins_match(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
:return: A match between two pin values. :return: A match between two pin values.
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user, session = state_machine_data user_input, ussd_session, account, session = state_machine_data
wait_for_session_data('Initial pin', session_data_key='initial_pin', ussd_session=ussd_session)
initial_pin = ussd_session.get('data').get('initial_pin') initial_pin = ussd_session.get('data').get('initial_pin')
return check_password_hash(user_input, initial_pin) return check_password_hash(user_input, initial_pin)
@ -107,11 +105,12 @@ def complete_pin_change(state_machine_data: Tuple[str, dict, Account, Session]):
: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, session = state_machine_data user_input, ussd_session, account, session = state_machine_data
session = SessionBase.bind_session(session=session) session = SessionBase.bind_session(session=session)
wait_for_session_data('Initial pin', session_data_key='initial_pin', ussd_session=ussd_session)
password_hash = ussd_session.get('data').get('initial_pin') password_hash = ussd_session.get('data').get('initial_pin')
user.password_hash = password_hash account.password_hash = password_hash
session.add(user) session.add(account)
session.flush() session.flush()
SessionBase.release_session(session=session) SessionBase.release_session(session=session)
@ -134,6 +133,6 @@ def is_valid_new_pin(state_machine_data: Tuple[str, dict, Account, Session]) ->
:return: A match between two pin values. :return: A match between two pin values.
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, user, session = state_machine_data user_input, ussd_session, account, session = state_machine_data
is_old_pin = user.verify_password(password=user_input) is_old_pin = account.verify_password(password=user_input)
return is_valid_pin(state_machine_data=state_machine_data) and not is_old_pin return is_valid_pin(state_machine_data=state_machine_data) and not is_old_pin

View File

@ -6,7 +6,7 @@ from sqlalchemy.orm.session import Session
# local imports # local imports
from cic_ussd.account.metadata import get_cached_preferred_language from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.account.tokens import get_default_token_symbol from cic_ussd.account.tokens import get_active_token_symbol
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
from cic_ussd.notifications import Notifier from cic_ussd.notifications import Notifier
from cic_ussd.phone_number import Support from cic_ussd.phone_number import Support
@ -18,7 +18,7 @@ def upsell_unregistered_recipient(state_machine_data: Tuple[str, dict, Account,
notifier = Notifier() notifier = Notifier()
phone_number = ussd_session.get('data')['recipient_phone_number'] phone_number = ussd_session.get('data')['recipient_phone_number']
preferred_language = get_cached_preferred_language(account.blockchain_address) preferred_language = get_cached_preferred_language(account.blockchain_address)
token_symbol = get_default_token_symbol() token_symbol = get_active_token_symbol(account.blockchain_address)
tx_sender_information = account.standard_metadata_id() tx_sender_information = account.standard_metadata_id()
notifier.send_sms_notification('sms.upsell_unregistered_recipient', notifier.send_sms_notification('sms.upsell_unregistered_recipient',
phone_number, phone_number,

View File

@ -0,0 +1,69 @@
# standard imports
from typing import Tuple
# external imports
from sqlalchemy.orm.session import Session
# local imports
from cic_ussd.account.tokens import set_active_token
from cic_ussd.db.models.account import Account
from cic_ussd.processor.util import wait_for_session_data
from cic_ussd.session.ussd_session import save_session_data
def is_valid_token_selection(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
user_input, ussd_session, account, session = state_machine_data
session_data = ussd_session.get('data')
account_tokens_list = session_data.get('account_tokens_list')
if not account_tokens_list:
wait_for_session_data('Account token list', session_data_key='account_tokens_list', ussd_session=ussd_session)
if user_input not in ['00', '22']:
try:
user_input = int(user_input)
return user_input <= len(account_tokens_list)
except ValueError:
user_input = user_input.upper()
return any(token_data['symbol'] == user_input for token_data in account_tokens_list)
def process_token_selection(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
user_input, ussd_session, account, session = state_machine_data
account_tokens_list = ussd_session.get('data').get('account_tokens_list')
try:
user_input = int(user_input)
selected_token = account_tokens_list[user_input-1]
except ValueError:
user_input = user_input.upper()
selected_token = next(token_data for token_data in account_tokens_list if token_data['symbol'] == user_input)
data = {
'selected_token': selected_token
}
save_session_data(queue='cic-ussd', session=session, data=data, ussd_session=ussd_session)
def set_selected_active_token(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
user_input, ussd_session, account, session = state_machine_data
wait_for_session_data(resource_name='Selected token', session_data_key='selected_token', ussd_session=ussd_session)
selected_token = ussd_session.get('data').get('selected_token')
token_symbol = selected_token.get('symbol')
set_active_token(blockchain_address=account.blockchain_address, token_symbol=token_symbol)

View File

@ -5,18 +5,17 @@ from typing import Tuple
# third party imports # third party imports
import celery import celery
from phonenumbers.phonenumberutil import NumberParseException from phonenumbers.phonenumberutil import NumberParseException
from sqlalchemy.orm.session import Session
# local imports # local imports
from cic_ussd.account.balance import get_cached_available_balance from cic_ussd.account.balance import get_cached_available_balance
from cic_ussd.account.chain import Chain from cic_ussd.account.chain import Chain
from cic_ussd.account.tokens import get_default_token_symbol from cic_ussd.account.tokens import get_active_token_symbol, get_cached_token_data
from cic_ussd.account.transaction import OutgoingTransaction from cic_ussd.account.transaction import OutgoingTransaction
from cic_ussd.db.enum import AccountStatus
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase
from cic_ussd.phone_number import process_phone_number, E164Format from cic_ussd.phone_number import process_phone_number, E164Format
from cic_ussd.session.ussd_session import save_session_data from cic_ussd.session.ussd_session import save_session_data
from sqlalchemy.orm.session import Session
logg = logging.getLogger(__file__) logg = logging.getLogger(__file__)
@ -63,7 +62,11 @@ def has_sufficient_balance(state_machine_data: Tuple[str, dict, Account, Session
:rtype: bool :rtype: bool
""" """
user_input, ussd_session, account, session = state_machine_data user_input, ussd_session, account, session = state_machine_data
return int(user_input) <= get_cached_available_balance(account.blockchain_address) identifier = bytes.fromhex(account.blockchain_address)
token_symbol = get_active_token_symbol(account.blockchain_address)
token_data = get_cached_token_data(account.blockchain_address, token_symbol)
decimals = token_data.get('decimals')
return int(user_input) <= get_cached_available_balance(decimals, [identifier, token_symbol.encode('utf-8')])
def save_recipient_phone_to_session_data(state_machine_data: Tuple[str, dict, Account, Session]): def save_recipient_phone_to_session_data(state_machine_data: Tuple[str, dict, Account, Session]):
@ -122,9 +125,10 @@ def process_transaction_request(state_machine_data: Tuple[str, dict, Account, Se
to_address = recipient.blockchain_address to_address = recipient.blockchain_address
from_address = account.blockchain_address from_address = account.blockchain_address
amount = int(ussd_session.get('data').get('transaction_amount')) amount = int(ussd_session.get('data').get('transaction_amount'))
token_symbol = get_default_token_symbol() token_symbol = get_active_token_symbol(account.blockchain_address)
token_data = get_cached_token_data(account.blockchain_address, token_symbol)
decimals = token_data.get('decimals')
outgoing_tx_processor = OutgoingTransaction(chain_str=chain_str, outgoing_tx_processor = OutgoingTransaction(chain_str=chain_str,
from_address=from_address, from_address=from_address,
to_address=to_address) to_address=to_address)
outgoing_tx_processor.transfer(amount=amount, token_symbol=token_symbol) outgoing_tx_processor.transfer(amount=amount, decimals=decimals, token_symbol=token_symbol)

View File

@ -1,4 +1,5 @@
# standard imports # standard imports
import json import json
import logging import logging
from datetime import timedelta from datetime import timedelta
@ -7,7 +8,6 @@ from datetime import timedelta
import celery import celery
from cic_types.condiments import MetadataPointer from cic_types.condiments import MetadataPointer
# local imports # local imports
from cic_ussd.account.balance import get_balances, calculate_available_balance from cic_ussd.account.balance import get_balances, calculate_available_balance
from cic_ussd.account.statement import generate from cic_ussd.account.statement import generate
@ -15,8 +15,15 @@ from cic_ussd.cache import Cache, cache_data, cache_data_key, get_cached_data
from cic_ussd.account.chain import Chain from cic_ussd.account.chain import Chain
from cic_ussd.db.models.base import SessionBase from cic_ussd.db.models.base import SessionBase
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
from cic_ussd.processor.util import wait_for_cache
from cic_ussd.account.statement import filter_statement_transactions from cic_ussd.account.statement import filter_statement_transactions
from cic_ussd.account.transaction import transaction_actors from cic_ussd.account.transaction import transaction_actors
from cic_ussd.account.tokens import (collate_token_metadata,
get_cached_token_data,
get_default_token_symbol,
handle_token_symbol_list,
process_token_data,
set_active_token)
from cic_ussd.error import AccountCreationDataNotFound from cic_ussd.error import AccountCreationDataNotFound
from cic_ussd.tasks.base import CriticalSQLAlchemyTask from cic_ussd.tasks.base import CriticalSQLAlchemyTask
@ -58,6 +65,9 @@ def account_creation_callback(self, result: str, url: str, status_code: int):
session.close() session.close()
logg.debug(f'recorded account with identifier: {result}') logg.debug(f'recorded account with identifier: {result}')
token_symbol = get_default_token_symbol()
set_active_token(blockchain_address=result, token_symbol=token_symbol)
queue = self.request.delivery_info.get('routing_key') queue = self.request.delivery_info.get('routing_key')
s_phone_pointer = celery.signature( s_phone_pointer = celery.signature(
'cic_ussd.tasks.metadata.add_phone_pointer', [result, phone_number], queue=queue 'cic_ussd.tasks.metadata.add_phone_pointer', [result, phone_number], queue=queue
@ -88,8 +98,16 @@ def balances_callback(result: list, param: str, status_code: int):
raise ValueError(f'Unexpected status code: {status_code}.') raise ValueError(f'Unexpected status code: {status_code}.')
balances = result[0] balances = result[0]
identifier = bytes.fromhex(param) identifier = []
key = cache_data_key(identifier, MetadataPointer.BALANCES) param = param.split(',')
for identity in param:
try:
i = bytes.fromhex(identity)
identifier.append(i)
except ValueError:
i = identity.encode('utf-8')
identifier.append(i)
key = cache_data_key(identifier=identifier, salt=MetadataPointer.BALANCES)
cache_data(key, json.dumps(balances)) cache_data(key, json.dumps(balances))
@ -122,6 +140,38 @@ def statement_callback(self, result, param: str, status_code: int):
generate(param, queue, sender_transaction) generate(param, queue, sender_transaction)
@celery_app.task
def token_data_callback(result: dict, param: str, status_code: int):
"""
:param result:
:type result:
:param param:
:type param:
:param status_code:
:type status_code:
:return:
:rtype:
"""
if status_code != 0:
raise ValueError(f'Unexpected status code: {status_code}.')
token = result[0]
token_symbol = token.get('symbol')
identifier = token_symbol.encode('utf-8')
token_meta_key = cache_data_key(identifier, MetadataPointer.TOKEN_META_SYMBOL)
token_info_key = cache_data_key(identifier, MetadataPointer.TOKEN_PROOF_SYMBOL)
token_meta = get_cached_data(token_meta_key)
token_meta = json.loads(token_meta)
token_info = get_cached_data(token_info_key)
token_info = json.loads(token_info)
token_data = collate_token_metadata(token_info=token_info, token_metadata=token_meta)
token_data = {**token_data, **token}
token_data_key = cache_data_key([bytes.fromhex(param), identifier], MetadataPointer.TOKEN_DATA)
cache_data(token_data_key, json.dumps(token_data))
handle_token_symbol_list(blockchain_address=param, token_symbol=token_symbol)
@celery_app.task(bind=True) @celery_app.task(bind=True)
def transaction_balances_callback(self, result: list, param: dict, status_code: int): def transaction_balances_callback(self, result: list, param: dict, status_code: int):
""" """
@ -138,10 +188,15 @@ def transaction_balances_callback(self, result: list, param: dict, status_code:
""" """
if status_code != 0: if status_code != 0:
raise ValueError(f'Unexpected status code: {status_code}.') raise ValueError(f'Unexpected status code: {status_code}.')
balances_data = result[0] balances_data = result[0]
available_balance = calculate_available_balance(balances_data)
transaction = param transaction = param
token_symbol = transaction.get('token_symbol')
blockchain_address = transaction.get('blockchain_address')
identifier = [bytes.fromhex(blockchain_address), token_symbol.encode('utf-8')]
wait_for_cache(identifier, f'Cached token data for: {token_symbol}', MetadataPointer.TOKEN_DATA)
token_data = get_cached_token_data(blockchain_address, token_symbol)
decimals = token_data.get('decimals')
available_balance = calculate_available_balance(balances_data, decimals)
transaction['available_balance'] = available_balance transaction['available_balance'] = available_balance
queue = self.request.delivery_info.get('routing_key') queue = self.request.delivery_info.get('routing_key')
@ -175,6 +230,8 @@ def transaction_callback(result: dict, param: str, status_code: int):
source_token_symbol = result.get('source_token_symbol') source_token_symbol = result.get('source_token_symbol')
source_token_value = result.get('source_token_value') source_token_value = result.get('source_token_value')
process_token_data(blockchain_address=recipient_blockchain_address, token_symbol=destination_token_symbol)
recipient_metadata = { recipient_metadata = {
"alt_blockchain_address": sender_blockchain_address, "alt_blockchain_address": sender_blockchain_address,
"blockchain_address": recipient_blockchain_address, "blockchain_address": recipient_blockchain_address,

View File

@ -6,6 +6,7 @@ import logging
import celery import celery
# local imports # local imports
from cic_ussd.account.tokens import get_cached_token_data
from cic_ussd.account.transaction import from_wei from cic_ussd.account.transaction import from_wei
from cic_ussd.notifications import Notifier from cic_ussd.notifications import Notifier
from cic_ussd.phone_number import Support from cic_ussd.phone_number import Support
@ -25,11 +26,15 @@ def transaction(notification_data: dict):
""" """
role = notification_data.get('role') role = notification_data.get('role')
token_value = notification_data.get('token_value') token_value = notification_data.get('token_value')
amount = token_value if token_value == 0 else from_wei(token_value) token_symbol = notification_data.get('token_symbol')
blockchain_address = notification_data.get('blockchain_address')
token_data = get_cached_token_data(blockchain_address, token_symbol)
decimals = token_data.get('decimals')
amount = token_value if token_value == 0 else from_wei(decimals, token_value)
balance = notification_data.get('available_balance') balance = notification_data.get('available_balance')
phone_number = notification_data.get('phone_number') phone_number = notification_data.get('phone_number')
preferred_language = notification_data.get('preferred_language') preferred_language = notification_data.get('preferred_language')
token_symbol = notification_data.get('token_symbol')
alt_metadata_id = notification_data.get('alt_metadata_id') alt_metadata_id = notification_data.get('alt_metadata_id')
metadata_id = notification_data.get('metadata_id') metadata_id = notification_data.get('metadata_id')
transaction_type = notification_data.get('transaction_type') transaction_type = notification_data.get('transaction_type')

View File

@ -47,7 +47,8 @@ def cache_statement(parsed_transaction: dict, querying_party: str):
statement_transactions = [] statement_transactions = []
if cached_statement: if cached_statement:
statement_transactions = json.loads(cached_statement) statement_transactions = json.loads(cached_statement)
statement_transactions.append(parsed_transaction) if parsed_transaction not in statement_transactions:
statement_transactions.append(parsed_transaction)
data = json.dumps(statement_transactions) data = json.dumps(statement_transactions)
identifier = bytes.fromhex(querying_party) identifier = bytes.fromhex(querying_party)
key = cache_data_key(identifier, MetadataPointer.STATEMENT) key = cache_data_key(identifier, MetadataPointer.STATEMENT)
@ -74,6 +75,14 @@ def parse_transaction(transaction: dict) -> dict:
role = transaction.get('role') role = transaction.get('role')
alt_blockchain_address = transaction.get('alt_blockchain_address') alt_blockchain_address = transaction.get('alt_blockchain_address')
blockchain_address = transaction.get('blockchain_address') blockchain_address = transaction.get('blockchain_address')
identifier = bytes.fromhex(blockchain_address)
token_symbol = transaction.get('token_symbol')
if role == 'recipient':
key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_LAST_RECEIVED)
cache_data(key, token_symbol)
if role == 'sender':
key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_LAST_SENT)
cache_data(key, token_symbol)
account = validate_transaction_account(blockchain_address, role, session) account = validate_transaction_account(blockchain_address, role, session)
alt_account = session.query(Account).filter_by(blockchain_address=alt_blockchain_address).first() alt_account = session.query(Account).filter_by(blockchain_address=alt_blockchain_address).first()
if alt_account: if alt_account:

View File

@ -6,7 +6,7 @@ celery==4.4.7
cffi==1.14.6 cffi==1.14.6
cic-eth~=0.12.5a1 cic-eth~=0.12.5a1
cic-notify~=0.4.0a11 cic-notify~=0.4.0a11
cic-types~=0.2.1a2 cic-types~=0.2.1a7
confini>=0.3.6rc4,<0.5.0 confini>=0.3.6rc4,<0.5.0
phonenumbers==8.12.12 phonenumbers==8.12.12
psycopg2==2.8.6 psycopg2==2.8.6

View File

@ -0,0 +1,7 @@
[
"first_account_tokens_set",
"middle_account_tokens_set",
"last_account_tokens_set",
"token_selection_pin_authorization",
"exit_successful_token_selection"
]

View File

@ -8,7 +8,7 @@
{ {
"trigger": "scan_data", "trigger": "scan_data",
"source": "first_transaction_set", "source": "first_transaction_set",
"dest": "start", "dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected" "conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
}, },
{ {
@ -31,7 +31,7 @@
{ {
"trigger": "scan_data", "trigger": "scan_data",
"source": "middle_transaction_set", "source": "middle_transaction_set",
"dest": "start", "dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected" "conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
}, },
{ {
@ -48,7 +48,7 @@
{ {
"trigger": "scan_data", "trigger": "scan_data",
"source": "last_transaction_set", "source": "last_transaction_set",
"dest": "start", "dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected" "conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
}, },
{ {

View File

@ -58,5 +58,17 @@
"source": "exit_successful_transaction", "source": "exit_successful_transaction",
"dest": "exit", "dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_ninety_nine_selected" "conditions": "cic_ussd.state_machine.logic.menu.menu_ninety_nine_selected"
},
{
"trigger": "scan_data",
"source": "exit_successful_token_selection",
"dest": "start",
"conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
},
{
"trigger": "scan_data",
"source": "exit_successful_token_selection",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_ninety_nine_selected"
} }
] ]

View File

@ -8,15 +8,21 @@
{ {
"trigger": "scan_data", "trigger": "scan_data",
"source": "start", "source": "start",
"dest": "account_management", "dest": "first_account_tokens_set",
"conditions": "cic_ussd.state_machine.logic.menu.menu_two_selected" "conditions": "cic_ussd.state_machine.logic.menu.menu_two_selected"
}, },
{ {
"trigger": "scan_data", "trigger": "scan_data",
"source": "start", "source": "start",
"dest": "help", "dest": "account_management",
"conditions": "cic_ussd.state_machine.logic.menu.menu_three_selected" "conditions": "cic_ussd.state_machine.logic.menu.menu_three_selected"
}, },
{
"trigger": "scan_data",
"source": "start",
"dest": "help",
"conditions": "cic_ussd.state_machine.logic.menu.menu_four_selected"
},
{ {
"trigger": "scan_data", "trigger": "scan_data",
"source": "start", "source": "start",

View File

@ -0,0 +1,93 @@
[
{
"trigger": "scan_data",
"source": "first_account_tokens_set",
"dest": "middle_account_tokens_set",
"conditions": "cic_ussd.state_machine.logic.menu.menu_eleven_selected"
},
{
"trigger": "scan_data",
"source": "first_account_tokens_set",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
},
{
"trigger": "scan_data",
"source": "first_account_tokens_set",
"dest": "token_selection_pin_authorization",
"conditions": "cic_ussd.state_machine.logic.tokens.is_valid_token_selection",
"after": "cic_ussd.state_machine.logic.tokens.process_token_selection"
},
{
"trigger": "scan_data",
"source": "first_account_tokens_set",
"dest": "exit_invalid_menu_option"
},
{
"trigger": "scan_data",
"source": "middle_account_tokens_set",
"dest": "last_account_tokens_set",
"conditions": "cic_ussd.state_machine.logic.menu.menu_eleven_selected"
},
{
"trigger": "scan_data",
"source": "middle_account_tokens_set",
"dest": "first_account_tokens_set",
"conditions": "cic_ussd.state_machine.logic.menu.menu_twenty_two_selected"
},
{
"trigger": "scan_data",
"source": "middle_account_tokens_set",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
},
{
"trigger": "scan_data",
"source": "middle_account_tokens_set",
"dest": "token_selection_pin_authorization",
"conditions": "cic_ussd.state_machine.logic.tokens.is_valid_token_selection",
"after": "cic_ussd.state_machine.logic.tokens.process_token_selection"
},
{
"trigger": "scan_data",
"source": "middle_account_tokens_set",
"dest": "exit_invalid_menu_option"
},
{
"trigger": "scan_data",
"source": "last_account_tokens_set",
"dest": "middle_account_tokens_set",
"conditions": "cic_ussd.state_machine.logic.menu.menu_twenty_two_selected"
},
{
"trigger": "scan_data",
"source": "last_account_tokens_set",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
},
{
"trigger": "scan_data",
"source": "last_account_tokens_set",
"dest": "token_selection_pin_authorization",
"conditions": "cic_ussd.state_machine.logic.tokens.is_valid_token_selection",
"after": "cic_ussd.state_machine.logic.tokens.process_token_selection"
},
{
"trigger": "scan_data",
"source": "last_account_tokens_set",
"dest": "exit_invalid_menu_option"
},
{
"trigger": "scan_data",
"source": "token_selection_pin_authorization",
"dest": "exit_successful_token_selection",
"conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
"after": "cic_ussd.state_machine.logic.tokens.set_selected_active_token"
},
{
"trigger": "scan_data",
"source": "token_selection_pin_authorization",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
}
]

View File

@ -9,6 +9,8 @@ en:
Not provided Not provided
no_transaction_history: |- no_transaction_history: |-
No transaction history No transaction history
no_tokens_list: |-
No tokens to list
other: |- other: |-
Other Other
received: |- received: |-

View File

@ -9,6 +9,8 @@ sw:
Haijawekwa Haijawekwa
no_transaction_history: |- no_transaction_history: |-
Hamna ripoti ya matumizi Hamna ripoti ya matumizi
no_tokens_list: |-
Hamna sarafu nyingine
other: |- other: |-
Nyingine Nyingine
received: |- received: |-

View File

@ -33,14 +33,37 @@ en:
start: |- start: |-
CON Balance %{account_balance} %{account_token_name} CON Balance %{account_balance} %{account_token_name}
1. Send 1. Send
2. My Account 2. My Sarafu
3. Help 3. My Account
4. Help
enter_transaction_recipient: |- enter_transaction_recipient: |-
CON Enter phone number CON Enter phone number
0. Back 0. Back
enter_transaction_amount: |- enter_transaction_amount: |-
CON Enter amount CON Enter amount
0. Back 0. Back
first_account_tokens_set: |-
CON Choose a number or symbol from your balances:
%{first_account_tokens_set}
11. Next
00. Exit
middle_account_tokens_set: |-
CON Choose a number or symbol from your balances:
%{middle_account_tokens_set}
11. Next
22. Previous
00. Exit
last_account_tokens_set: |-
CON Choose a number or symbol from your balances:
%{last_account_tokens_set}
22. Previous
00. Exit
token_selection_pin_authorization:
first: |-
CON %{token_data}
Enter pin to select:
retry: |-
%{retry_pin_entry}
account_management: |- account_management: |-
CON My account CON My account
1. My profile 1. My profile
@ -73,7 +96,6 @@ en:
0. Back 0. Back
retry_pin_entry: |- retry_pin_entry: |-
CON Incorrect PIN entered, please try again. You have %{remaining_attempts} attempts remaining. CON Incorrect PIN entered, please try again. You have %{remaining_attempts} attempts remaining.
0. Back
enter_current_pin: enter_current_pin:
first: |- first: |-
CON Enter current PIN. CON Enter current PIN.
@ -203,6 +225,10 @@ en:
Your Sarafu-Network balances is: %{token_balance} Your Sarafu-Network balances is: %{token_balance}
00. Back 00. Back
99. Exit 99. Exit
exit_successful_token_selection: |-
CON Success! %{token_symbol} is your active Sarafu.
00. Back
99. Exit
invalid_service_code: |- invalid_service_code: |-
Please dial %{valid_service_code} to access Sarafu Network Please dial %{valid_service_code} to access Sarafu Network
help: |- help: |-

View File

@ -32,14 +32,37 @@ sw:
start: |- start: |-
CON Salio %{account_balance} %{account_token_name} CON Salio %{account_balance} %{account_token_name}
1. Tuma 1. Tuma
2. Akaunti yangu 2. Sarafu yangu
3. Usaidizi 3. Akaunti yangu
4. Usaidizi
enter_transaction_recipient: |- enter_transaction_recipient: |-
CON Weka nambari ya simu CON Weka nambari ya simu
0. Nyuma 0. Nyuma
enter_transaction_amount: |- enter_transaction_amount: |-
CON Weka kiwango CON Weka kiwango
0. Nyuma 0. Nyuma
first_account_tokens_set: |-
CON Chagua nambari au ishara kutoka kwa salio zako:
%{first_account_tokens_set}
11. Mbele
00. Ondoka
middle_account_tokens_set: |-
CON Chagua nambari au ishara kutoka kwa salio zako:
%{middle_account_tokens_set}
11. Mbele
22. Nyuma
00. Ondoka
last_account_tokens_set: |-
CON Chagua nambari au ishara kutoka kwa salio zako:
%{last_account_tokens_set}
22. Nyuma
00. Ondoka
token_selection_pin_authorization:
first: |-
CON %{token_data}
Weka nambari ya siri kuchagua:
retry: |-
%{retry_pin_entry}
account_management: |- account_management: |-
CON Akaunti yangu CON Akaunti yangu
1. Wasifu wangu 1. Wasifu wangu
@ -72,7 +95,6 @@ sw:
0. Nyuma 0. Nyuma
retry_pin_entry: |- retry_pin_entry: |-
CON Nambari uliyoweka si sahihi, jaribu tena. Una majaribio %{remaining_attempts} yaliyobaki. CON Nambari uliyoweka si sahihi, jaribu tena. Una majaribio %{remaining_attempts} yaliyobaki.
0. Back
enter_current_pin: enter_current_pin:
first: |- first: |-
CON Weka nambari ya siri. CON Weka nambari ya siri.
@ -202,6 +224,10 @@ sw:
Akaunti yako ya Sarafu ina salio ifuatayo: %{token_balance} Akaunti yako ya Sarafu ina salio ifuatayo: %{token_balance}
00. Nyuma 00. Nyuma
99. Ondoka 99. Ondoka
exit_successful_token_selection: |-
CON Chaguo lako limekamilika, %{token_symbol} ni sarafu itakayotumika.
00. Nyuma
99. Ondoka
invalid_service_code: |- invalid_service_code: |-
Bonyeza %{valid_service_code} kutumia mtandao wa Sarafu Bonyeza %{valid_service_code} kutumia mtandao wa Sarafu
help: |- help: |-

View File

@ -64,7 +64,8 @@ def wrapper(chain_spec: ChainSpec, rpc: EthHTTPConnection) -> Declarator:
return Declarator(chain_spec, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle, signer=signer) return Declarator(chain_spec, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle, signer=signer)
def write_to_declarator(contract_address: hex, contract_wrapper: Declarator, proof: any, rpc: EthHTTPConnection, signer_address: hex, token_address: hex): def write_to_declarator(contract_address: hex, contract_wrapper: Declarator, proof: any, rpc: EthHTTPConnection,
signer_address: hex, token_address: hex):
operation = contract_wrapper.add_declaration(contract_address, signer_address, token_address, proof) operation = contract_wrapper.add_declaration(contract_address, signer_address, token_address, proof)
results = rpc.do(operation[1]) results = rpc.do(operation[1])
rpc.wait(results) rpc.wait(results)
@ -90,13 +91,15 @@ if __name__ == '__main__':
token_meta_writer = MetadataRequestsHandler(cic_type=MetadataPointer.TOKEN_META, identifier=token_address_bytes) token_meta_writer = MetadataRequestsHandler(cic_type=MetadataPointer.TOKEN_META, identifier=token_address_bytes)
write_metadata(token_meta_writer, token_meta_data) write_metadata(token_meta_writer, token_meta_data)
token_meta_symbol_writer = MetadataRequestsHandler(cic_type=MetadataPointer.TOKEN_META_SYMBOL, identifier=token_symbol_bytes) token_meta_symbol_writer = MetadataRequestsHandler(cic_type=MetadataPointer.TOKEN_META_SYMBOL,
identifier=token_symbol_bytes)
write_metadata(token_meta_symbol_writer, token_meta_data) write_metadata(token_meta_symbol_writer, token_meta_data)
token_proof_writer = MetadataRequestsHandler(cic_type=MetadataPointer.TOKEN_PROOF, identifier=token_address_bytes) token_proof_writer = MetadataRequestsHandler(cic_type=MetadataPointer.TOKEN_PROOF, identifier=token_address_bytes)
write_metadata(token_proof_writer, token_proof_data) write_metadata(token_proof_writer, token_proof_data)
token_proof_symbol_writer = MetadataRequestsHandler(cic_type=MetadataPointer.TOKEN_PROOF_SYMBOL, identifier=token_symbol_bytes) token_proof_symbol_writer = MetadataRequestsHandler(cic_type=MetadataPointer.TOKEN_PROOF_SYMBOL,
identifier=token_symbol_bytes)
write_metadata(token_proof_symbol_writer, token_proof_data) write_metadata(token_proof_symbol_writer, token_proof_data)
rpc = EthHTTPConnection(url=config.get('RPC_PROVIDER'), chain_spec=chain_spec) rpc = EthHTTPConnection(url=config.get('RPC_PROVIDER'), chain_spec=chain_spec)
@ -112,3 +115,13 @@ if __name__ == '__main__':
rpc=rpc, rpc=rpc,
signer_address=args.signer_address, signer_address=args.signer_address,
token_address=args.e) token_address=args.e)
hashed_token_proof = hash_proof(args.token_symbol.encode('utf-8'))
identifier = bytes.fromhex(hashed_token_proof)
token_immutable_proof_writer = MetadataRequestsHandler(cic_type=MetadataPointer.NONE, identifier=identifier)
write_to_declarator(contract_address=args.address_declarator,
contract_wrapper=contract_wrapper,
proof=identifier,
rpc=rpc,
signer_address=args.signer_address,
token_address=args.e)

View File

@ -506,6 +506,7 @@ services:
DATABASE_DRIVER: ${DATABASE_DRIVER:-psycopg2} DATABASE_DRIVER: ${DATABASE_DRIVER:-psycopg2}
DATABASE_DEBUG: ${DATABASE_DEBUG:-0} DATABASE_DEBUG: ${DATABASE_DEBUG:-0}
DATABASE_POOL_SIZE: 0 DATABASE_POOL_SIZE: 0
APP_PASSWORD_PEPPER: ${APP_PASSWORD_PEPPER:-QYbzKff6NhiQzY3ygl2BkiKOpER8RE/Upqs/5aZWW+I=}
CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis} CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis}
CELERY_RESULT_URL: ${CELERY_BROKER_URL:-redis://redis} CELERY_RESULT_URL: ${CELERY_BROKER_URL:-redis://redis}
CHAIN_SPEC: ${CHAIN_SPEC:-evm:byzantium:8996:bloxberg} CHAIN_SPEC: ${CHAIN_SPEC:-evm:byzantium:8996:bloxberg}
@ -545,6 +546,7 @@ services:
DATABASE_DRIVER: ${DATABASE_DRIVER:-psycopg2} DATABASE_DRIVER: ${DATABASE_DRIVER:-psycopg2}
DATABASE_DEBUG: ${DATABASE_DEBUG:-0} DATABASE_DEBUG: ${DATABASE_DEBUG:-0}
DATABASE_POOL_SIZE: 0 DATABASE_POOL_SIZE: 0
SERVER_PORT: 9500
restart: on-failure restart: on-failure
ports: ports:
- ${DEV_CIC_USER_SERVER_PORT:-63415}:9500 - ${DEV_CIC_USER_SERVER_PORT:-63415}:9500
@ -574,11 +576,13 @@ services:
DATABASE_DRIVER: ${DATABASE_DRIVER:-psycopg2} DATABASE_DRIVER: ${DATABASE_DRIVER:-psycopg2}
DATABASE_DEBUG: ${DATABASE_DEBUG:-0} DATABASE_DEBUG: ${DATABASE_DEBUG:-0}
DATABASE_POOL_SIZE: 0 DATABASE_POOL_SIZE: 0
APP_PASSWORD_PEPPER: ${APP_PASSWORD_PEPPER:-QYbzKff6NhiQzY3ygl2BkiKOpER8RE/Upqs/5aZWW+I=}
CHAIN_SPEC: ${CHAIN_SPEC:-evm:byzantium:8996:bloxberg} CHAIN_SPEC: ${CHAIN_SPEC:-evm:byzantium:8996:bloxberg}
CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis} CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis}
CELERY_RESULT_URL: ${CELERY_RESULT_URL:-redis://redis} CELERY_RESULT_URL: ${CELERY_RESULT_URL:-redis://redis}
REDIS_PORT: 6379 REDIS_PORT: 6379
REDIS_HOST: redis REDIS_HOST: redis
SERVER_PORT: 9000
restart: on-failure restart: on-failure
depends_on: depends_on:
- postgres - postgres
@ -586,7 +590,7 @@ services:
- cic-eth-tasker - cic-eth-tasker
- cic-cache-tasker - cic-cache-tasker
ports: ports:
- ${DEV_CIC_USER_USSD_SERVER_PORT:-63315}:9500 - ${DEV_CIC_USER_USSD_SERVER_PORT:-63315}:9000
volumes: volumes:
- ./apps/contract-migration/testdata/pgp/:/usr/src/secrets/ - ./apps/contract-migration/testdata/pgp/:/usr/src/secrets/
command: "/root/start_cic_user_ussd_server.sh -vv" command: "/root/start_cic_user_ussd_server.sh -vv"