Compare commits

..

93 Commits

Author SHA1 Message Date
e04d090b06 Adds account management integration tests. 2021-06-02 16:56:08 +03:00
122e012329 Adds fixtures. 2021-06-02 16:55:41 +03:00
f7bfea6563 Minor fix to menu. 2021-06-02 16:55:24 +03:00
ac09875258 Add account management integration tests. 2021-06-02 12:21:42 +03:00
0017247363 Clean up refactors. 2021-06-02 12:20:56 +03:00
d82d9979a5 Adds final management integration tests. 2021-06-02 07:43:16 +03:00
40b1e8272b Improves products transitions. 2021-06-02 07:42:35 +03:00
25e4aaf9f6 Refactors fixtures to management tests. 2021-06-02 07:42:00 +03:00
c90f1c59ec Adds tests for gender change. 2021-05-31 19:36:23 +03:00
0dbd8d63b2 Cleans up menus. 2021-05-31 15:22:36 +03:00
9da3b7a099 Refactors translations. 2021-05-31 15:22:12 +03:00
fbf736ba98 Adds fixtures. 2021-05-31 15:21:56 +03:00
8de6e9876e Improves management tests. 2021-05-31 15:21:36 +03:00
b2ab1465d7 Refactors accounts tests to match changes to menu. 2021-05-31 15:21:10 +03:00
23015f8b98 Adds management integration tests. 2021-05-27 19:07:23 +03:00
ea614e07af Minor fix in transaction tests. 2021-05-27 19:07:02 +03:00
f35e7010ba Adds management testing fixtures. 2021-05-27 19:06:11 +03:00
23ed64836e Minor bug fixes. 2021-05-27 19:04:24 +03:00
866eb7397b Bumps cic-notify version. 2021-05-27 19:03:59 +03:00
a05df2280b Refactors state machine logic. 2021-05-27 19:03:43 +03:00
9e4ad4c650 Refactors how meta handler interacts with cic-meta to fix bugs. 2021-05-27 19:01:48 +03:00
5b89c151f9 Updates cic-notify to fix minor bugs. 2021-05-27 19:00:41 +03:00
5bfe9fcd4a Refactors post handler to store object. 2021-05-27 18:57:40 +03:00
2608535200 Refactors transitions and ussd menus to fix bugs. 2021-05-27 18:55:17 +03:00
f39468d41f Merge branch 'master' of gitlab.com:grassrootseconomics/cic-internal-integration into philip/management-integration-tests 2021-05-19 20:46:01 +03:00
1676addbeb Merge branch 'philip/meta-cluster-bug' into 'master'
Refactors to handle error in metadata handling

Closes cic-ussd#45

See merge request grassrootseconomics/cic-internal-integration!160
2021-05-19 16:25:10 +00:00
1efc25ac15 Refactors to handle error in metadata handling 2021-05-19 16:25:10 +00:00
Louis Holbrook
db2ec0dcfa Merge branch 'philip/notify-errors' into 'master'
Philip/notify errors

Closes cic-notify#4

See merge request grassrootseconomics/cic-internal-integration!161
2021-05-19 16:13:06 +00:00
5148e6428b Philip/notify errors 2021-05-19 16:13:06 +00:00
Louis Holbrook
0c186ed968 Merge branch 'lash/rehabilitate-tests-eth' into 'master'
Fix outdated module names in cic-eth tests

See merge request grassrootseconomics/cic-internal-integration!162
2021-05-19 15:11:08 +00:00
Louis Holbrook
c44439bd90 Fix outdated module names in cic-eth tests 2021-05-19 15:11:08 +00:00
eee895ea71 Merge branch 'willruddick-master-patch-91858' into 'master'
small updates. note other gender

See merge request grassrootseconomics/cic-internal-integration!154
2021-05-19 10:26:52 +00:00
Louis Holbrook
a5ca898532 Merge branch 'lash/update-contracts-in-migration-2' into 'master'
imports: Fix stale dep in sovereign import users script

See merge request grassrootseconomics/cic-internal-integration!149
2021-05-19 09:57:03 +00:00
Louis Holbrook
6d8508aebf imports: Fix stale dep in sovereign import users script 2021-05-19 09:57:02 +00:00
Louis Holbrook
f8f66984d2 Merge branch 'lash/no-ussd-contamination' into 'master'
Isolate ussd-related data files in imports

See merge request grassrootseconomics/cic-internal-integration!150
2021-05-19 09:55:24 +00:00
Louis Holbrook
0f02dd1b7c Isolate ussd-related data files in imports 2021-05-19 09:55:24 +00:00
2be23b9390 Merge branch 'master' of gitlab.com:grassrootseconomics/cic-internal-integration into philip/management-integration-tests 2021-05-19 11:52:59 +03:00
e93851af76 Adds dep for ordering pytest tests. 2021-05-19 11:50:06 +03:00
1e89f01d5f Refactors run command to run all tavern tests. 2021-05-19 11:49:46 +03:00
6c1c05335e Separates integration tests into individual files. 2021-05-19 11:49:26 +03:00
5929a6c0bb Renames file containing all integration test fixtures. 2021-05-19 11:48:19 +03:00
63a4a82ab0 Merge branch 'philip/replicate-wills-changes' into 'master'
Replicates changes in broken MR by will.

See merge request grassrootseconomics/cic-internal-integration!158
2021-05-19 08:25:19 +00:00
949c1070a9 Replicates changes in broken MR by will. 2021-05-19 11:19:29 +03:00
5d9fbe9b64 Merge branch 'willruddick-master-patch-28332' into 'master'
shortened and changed service code

See merge request grassrootseconomics/cic-internal-integration!152
2021-05-19 07:47:09 +00:00
873a3f082a shortened and changed service code 2021-05-19 07:47:09 +00:00
7b408cf564 Merge branch 'willruddick-master-patch-28450' into 'master'
small changes, note the 'other' gender

See merge request grassrootseconomics/cic-internal-integration!151
2021-05-19 07:33:37 +00:00
Louis Holbrook
9dfbd7034c Merge branch 'lash/decimals-in-api' into 'master'
cic-eth-tasker: Add decimals and token name to default token api call return struct

Closes cic-eth#123

See merge request grassrootseconomics/cic-internal-integration!148
2021-05-19 06:59:43 +00:00
Louis Holbrook
235f5cede8 cic-eth-tasker: Add decimals and token name to default token api call return struct 2021-05-19 06:59:42 +00:00
Geoff Turk
0a59539f9a Merge branch 'lash/cache-data-api' into 'master'
cic-cache: Add data API

Closes cic-cache#11

See merge request grassrootseconomics/cic-internal-integration!157
2021-05-18 17:13:57 +00:00
Louis Holbrook
60b36945df cic-cache: Add data API 2021-05-18 17:13:57 +00:00
f72313aea9 Adds transaction integration tests. 2021-05-17 22:51:02 +03:00
43fd7465e5 Adds exit to be handled. 2021-05-17 22:50:44 +03:00
30eb9f517b Adds fixtures for integration tests. 2021-05-17 22:50:07 +03:00
f3e06dcd92 Adds helper functions for integration tests. 2021-05-17 22:49:29 +03:00
dae6526677 Merge branch 'philip/remove-notify-config-defaults' into 'master'
Refactors at configs.

See merge request grassrootseconomics/cic-internal-integration!156
2021-05-17 16:47:30 +00:00
1e94a516c2 Refactors at configs. 2021-05-17 19:41:07 +03:00
c0f578db75 Adds transaction tests. 2021-05-17 16:00:19 +03:00
5d2e5013f3 Adds helpers and fixtures for transaction tests 2021-05-17 16:00:06 +03:00
e8512ebbae small updates. note other gender 2021-05-17 11:27:45 +00:00
f2c955c60b small changes, note the 'other' gender 2021-05-17 11:06:10 +00:00
06d9612c6c Refactors to use fixtures for test configs. 2021-05-15 13:57:54 +03:00
07fef8df40 Moves test configurations to config file. 2021-05-15 13:56:58 +03:00
17b3b27d81 Merge branch 'philip/import-pins-script' into 'master'
Philip/import pins script

See merge request grassrootseconomics/cic-internal-integration!109
2021-05-15 07:40:34 +00:00
1cb172b8bf Philip/import pins script 2021-05-15 07:40:34 +00:00
Louis Holbrook
9d47e4c764 Merge branch 'lash/descriptive-documentation' into 'master'
cic-eth system documentation

See merge request grassrootseconomics/cic-internal-integration!122
2021-05-15 04:36:55 +00:00
Louis Holbrook
c68cc318ab cic-eth system documentation 2021-05-15 04:36:54 +00:00
1bd281f2a2 Adds convenience script for running integration tests. 2021-05-14 10:31:48 +03:00
7ab278d098 Adds integration tests for accounts. 2021-05-14 10:30:50 +03:00
15d44c859e Adds external functions for validation. 2021-05-14 10:30:05 +03:00
6d541d38bc Updates pytest version to work with tavern. 2021-05-14 10:29:09 +03:00
08567436f2 Adds common variable definitions to use across integration tests. 2021-05-14 10:28:46 +03:00
091b1e9f16 Imports accounts fixtures. 2021-05-14 10:28:15 +03:00
85837e1fec Adds helper functions to for testing accounts. 2021-05-14 10:27:46 +03:00
a17da7b91b Adds accounts fixtures. 2021-05-14 10:27:20 +03:00
c8adfb7f19 Adds requirements for tavern testing. 2021-05-14 00:38:55 +03:00
Louis Holbrook
af99ac823a Merge branch 'lash/custom-offset' into 'master'
cic-cache-tracker, cic-eth-tracker: Add optional and customizable history start for trackers

Closes #50

See merge request grassrootseconomics/cic-internal-integration!143
2021-05-13 16:37:44 +00:00
Louis Holbrook
06652eb30f cic-cache-tracker, cic-eth-tracker: Add optional and customizable history start for trackers 2021-05-13 16:37:44 +00:00
Louis Holbrook
f66f913307 Merge branch 'lash/update-contracts-in-migration-2' into 'master'
Upgrade accounts index

See merge request grassrootseconomics/cic-internal-integration!146
2021-05-13 16:29:02 +00:00
nolash
8bf1364864 Upgrade acoutns index 2021-05-13 18:00:59 +02:00
0d6d7179eb Merge branch 'philip/default-token-bug-fix' into 'master'
Fix hard-coded token symbols.

Closes cic-ussd#42

See merge request grassrootseconomics/cic-internal-integration!142
2021-05-12 11:26:20 +00:00
e7f48f3ce0 Refactors to fix hard-coded token symbols. 2021-05-12 12:51:55 +03:00
Louis Holbrook
b252fab018 Merge branch 'lash/catch-no-contract-crash' into 'master'
cic-eth-tracker: Catch bogus transfers where token address is no contract

See merge request grassrootseconomics/cic-internal-integration!141
2021-05-12 08:10:46 +00:00
nolash
4667916d80 Catch bogus transfers where token address is no contract 2021-05-12 08:48:50 +02:00
1f668384cc Merge branch 'philip/fix-africastalking-parser' into 'master'
Philip/fix africastalking parser

Closes cic-ussd#41

See merge request grassrootseconomics/cic-internal-integration!140
2021-05-11 10:58:00 +00:00
123dc55687 Philip/fix africastalking parser 2021-05-11 10:58:00 +00:00
nolash
0b4d8d5937 Add registry to cic-eth-info tool 2021-05-05 19:04:56 +02:00
Louis Holbrook
ed6bef4052 Merge branch 'lash/cache-faucet' into 'master'
Add faucet filter to cic-cache

Closes cic-cache#13

See merge request grassrootseconomics/cic-internal-integration!134
2021-05-05 16:25:21 +00:00
Louis Holbrook
6a8a356f09 Add faucet filter to cic-cache 2021-05-05 16:25:21 +00:00
5ec0b67496 Merge branch 'bvander/fix-user-pip-install' into 'master'
should have dropped the user flag

See merge request grassrootseconomics/cic-internal-integration!136
2021-05-02 19:38:57 +00:00
7d935bcbc3 should have dropped the user flag 2021-05-02 12:32:05 -07:00
fd69a3c6bb Update .cic-template.yml 2021-05-02 17:31:34 +00:00
Louis Holbrook
298bcf89e5 Merge branch 'lash/chainlib-erc20-split' into 'master'
Update componens for chainlib erc20 split

See merge request grassrootseconomics/cic-internal-integration!135
2021-05-02 17:11:03 +00:00
Louis Holbrook
5d3d773f41 Update componens for chainlib erc20 split 2021-05-02 17:11:03 +00:00
117 changed files with 5250 additions and 482 deletions

View File

@@ -1,22 +1,28 @@
# standard imports
import logging
import datetime
# third-party imports
# external imports
import moolb
# local imports
from cic_cache.db import list_transactions_mined
from cic_cache.db import list_transactions_account_mined
from cic_cache.db.list import (
list_transactions_mined,
list_transactions_account_mined,
list_transactions_mined_with_data,
)
logg = logging.getLogger()
class BloomCache:
class Cache:
def __init__(self, session):
self.session = session
class BloomCache(Cache):
@staticmethod
def __get_filter_size(n):
n = 8192 * 8
@@ -87,3 +93,43 @@ class BloomCache:
f_blocktx.add(block + tx)
logg.debug('added block {} tx {} lo {} hi {}'.format(r[0], r[1], lowest_block, highest_block))
return (lowest_block, highest_block, f_block.to_bytes(), f_blocktx.to_bytes(),)
class DataCache(Cache):
def load_transactions_with_data(self, offset, end):
rows = list_transactions_mined_with_data(self.session, offset, end)
tx_cache = []
highest_block = -1;
lowest_block = -1;
date_is_str = None # stick this in startup
for r in rows:
if highest_block == -1:
highest_block = r['block_number']
lowest_block = r['block_number']
tx_type = 'unknown'
if r['value'] != None:
tx_type = '{}.{}'.format(r['domain'], r['value'])
if date_is_str == None:
date_is_str = type(r['date_block']).__name__ == 'str'
o = {
'block_number': r['block_number'],
'tx_hash': r['tx_hash'],
'date_block': r['date_block'],
'sender': r['sender'],
'recipient': r['recipient'],
'from_value': int(r['from_value']),
'to_value': int(r['to_value']),
'source_token': r['source_token'],
'destination_token': r['destination_token'],
'tx_type': tx_type,
}
if date_is_str:
o['date_block'] = datetime.datetime.fromisoformat(r['date_block'])
tx_cache.append(o)
return (lowest_block, highest_block, tx_cache)

View File

@@ -28,6 +28,26 @@ def list_transactions_mined(
return r
def list_transactions_mined_with_data(
session,
offset,
end,
):
"""Executes db query to return all confirmed transactions according to the specified offset and limit.
:param offset: Offset in data set to return transactions from
:type offset: int
:param limit: Max number of transactions to retrieve
:type limit: int
:result: Result set
:rtype: SQLAlchemy.ResultProxy
"""
s = "SELECT tx_hash, block_number, date_block, sender, recipient, from_value, to_value, source_token, destination_token, domain, value FROM tx LEFT JOIN tag_tx_link ON tx.id = tag_tx_link.tx_id LEFT JOIN tag ON tag_tx_link.tag_id = tag.id WHERE block_number >= {} AND block_number <= {} ORDER BY block_number ASC, tx_index ASC".format(offset, end)
r = session.execute(s)
return r
def list_transactions_account_mined(
session,
address,

View File

@@ -1 +1,2 @@
from .erc20 import *
from .faucet import *

View File

@@ -0,0 +1,73 @@
# standard imports
import logging
# external imports
from erc20_faucet import Faucet
from chainlib.eth.address import to_checksum_address
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.status import Status
from hexathon import strip_0x
# local imports
import cic_cache.db as cic_cache_db
from .base import TagSyncFilter
#logg = logging.getLogger().getChild(__name__)
logg = logging.getLogger()
class FaucetFilter(TagSyncFilter):
def __init__(self, chain_spec, sender_address=ZERO_ADDRESS):
super(FaucetFilter, self).__init__('give_to', domain='faucet')
self.chain_spec = chain_spec
self.sender_address = sender_address
def filter(self, conn, block, tx, db_session=None):
try:
data = strip_0x(tx.payload)
except ValueError:
return False
logg.debug('data {}'.format(data))
if Faucet.method_for(data[:8]) == None:
return False
token_sender = tx.inputs[0]
token_recipient = data[64+8-40:]
logg.debug('token recipient {}'.format(token_recipient))
f = Faucet(self.chain_spec)
o = f.token(token_sender, sender_address=self.sender_address)
r = conn.do(o)
token = f.parse_token(r)
f = Faucet(self.chain_spec)
o = f.token_amount(token_sender, sender_address=self.sender_address)
r = conn.do(o)
token_value = f.parse_token_amount(r)
cic_cache_db.add_transaction(
db_session,
tx.hash,
block.number,
tx.index,
to_checksum_address(token_sender),
to_checksum_address(token_recipient),
token,
token,
token_value,
token_value,
tx.status == Status.SUCCESS,
block.timestamp,
)
db_session.flush()
cic_cache_db.tag_transaction(
db_session,
tx.hash,
self.tag_name,
domain=self.tag_domain,
)
db_session.commit()
return True

View File

@@ -0,0 +1,110 @@
# standard imports
import logging
import json
import re
import base64
# local imports
from cic_cache.cache import (
BloomCache,
DataCache,
)
logg = logging.getLogger(__name__)
re_transactions_all_bloom = r'/tx/(\d+)?/?(\d+)/?'
re_transactions_account_bloom = r'/tx/user/((0x)?[a-fA-F0-9]+)/?(\d+)?/?(\d+)/?'
re_transactions_all_data = r'/txa/(\d+)/(\d+)/?'
DEFAULT_LIMIT = 100
def process_transactions_account_bloom(session, env):
r = re.match(re_transactions_account_bloom, env.get('PATH_INFO'))
if not r:
return None
address = r[1]
if r[2] == None:
address = '0x' + address
offset = DEFAULT_LIMIT
if r.lastindex > 2:
offset = r[3]
limit = 0
if r.lastindex > 3:
limit = r[4]
c = BloomCache(session)
(lowest_block, highest_block, bloom_filter_block, bloom_filter_tx) = c.load_transactions_account(address, offset, limit)
o = {
'alg': 'sha256',
'low': lowest_block,
'high': highest_block,
'block_filter': base64.b64encode(bloom_filter_block).decode('utf-8'),
'blocktx_filter': base64.b64encode(bloom_filter_tx).decode('utf-8'),
'filter_rounds': 3,
}
j = json.dumps(o)
return ('application/json', j.encode('utf-8'),)
def process_transactions_all_bloom(session, env):
r = re.match(re_transactions_all_bloom, env.get('PATH_INFO'))
if not r:
return None
offset = DEFAULT_LIMIT
if r.lastindex > 0:
offset = r[1]
limit = 0
if r.lastindex > 1:
limit = r[2]
c = BloomCache(session)
(lowest_block, highest_block, bloom_filter_block, bloom_filter_tx) = c.load_transactions(offset, limit)
o = {
'alg': 'sha256',
'low': lowest_block,
'high': highest_block,
'block_filter': base64.b64encode(bloom_filter_block).decode('utf-8'),
'blocktx_filter': base64.b64encode(bloom_filter_tx).decode('utf-8'),
'filter_rounds': 3,
}
j = json.dumps(o)
return ('application/json', j.encode('utf-8'),)
def process_transactions_all_data(session, env):
r = re.match(re_transactions_all_data, env.get('PATH_INFO'))
if not r:
return None
if env.get('HTTP_X_CIC_CACHE_MODE') != 'all':
return None
offset = r[1]
end = r[2]
if r[2] < r[1]:
raise ValueError('cart before the horse, dude')
c = DataCache(session)
(lowest_block, highest_block, tx_cache) = c.load_transactions_with_data(offset, end)
for r in tx_cache:
r['date_block'] = r['date_block'].timestamp()
o = {
'low': lowest_block,
'high': highest_block,
'data': tx_cache,
}
j = json.dumps(o)
return ('application/json', j.encode('utf-8'),)

View File

@@ -1,18 +1,20 @@
# standard imports
import os
import re
import logging
import argparse
import json
import base64
# third-party imports
# external imports
import confini
# local imports
from cic_cache import BloomCache
from cic_cache.db import dsn_from_config
from cic_cache.db.models.base import SessionBase
from cic_cache.runnable.daemons.query import (
process_transactions_account_bloom,
process_transactions_all_bloom,
process_transactions_all_data,
)
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
@@ -44,72 +46,6 @@ logg.debug('config:\n{}'.format(config))
dsn = dsn_from_config(config)
SessionBase.connect(dsn, config.true('DATABASE_DEBUG'))
re_transactions_all_bloom = r'/tx/(\d+)?/?(\d+)/?'
re_transactions_account_bloom = r'/tx/user/((0x)?[a-fA-F0-9]+)/?(\d+)?/?(\d+)/?'
DEFAULT_LIMIT = 100
def process_transactions_account_bloom(session, env):
r = re.match(re_transactions_account_bloom, env.get('PATH_INFO'))
if not r:
return None
address = r[1]
if r[2] == None:
address = '0x' + address
offset = DEFAULT_LIMIT
if r.lastindex > 2:
offset = r[3]
limit = 0
if r.lastindex > 3:
limit = r[4]
c = BloomCache(session)
(lowest_block, highest_block, bloom_filter_block, bloom_filter_tx) = c.load_transactions_account(address, offset, limit)
o = {
'alg': 'sha256',
'low': lowest_block,
'high': highest_block,
'block_filter': base64.b64encode(bloom_filter_block).decode('utf-8'),
'blocktx_filter': base64.b64encode(bloom_filter_tx).decode('utf-8'),
'filter_rounds': 3,
}
j = json.dumps(o)
return ('application/json', j.encode('utf-8'),)
def process_transactions_all_bloom(session, env):
r = re.match(re_transactions_all_bloom, env.get('PATH_INFO'))
if not r:
return None
offset = DEFAULT_LIMIT
if r.lastindex > 0:
offset = r[1]
limit = 0
if r.lastindex > 1:
limit = r[2]
c = BloomCache(session)
(lowest_block, highest_block, bloom_filter_block, bloom_filter_tx) = c.load_transactions(offset, limit)
o = {
'alg': 'sha256',
'low': lowest_block,
'high': highest_block,
'block_filter': base64.b64encode(bloom_filter_block).decode('utf-8'),
'blocktx_filter': base64.b64encode(bloom_filter_tx).decode('utf-8'),
'filter_rounds': 3,
}
j = json.dumps(o)
return ('application/json', j.encode('utf-8'),)
# uwsgi application
def application(env, start_response):
@@ -119,10 +55,16 @@ def application(env, start_response):
session = SessionBase.create_session()
for handler in [
process_transactions_all_data,
process_transactions_all_bloom,
process_transactions_account_bloom,
]:
r = handler(session, env)
r = None
try:
r = handler(session, env)
except ValueError as e:
start_response('400 {}'.format(str(e)))
return []
if r != None:
(mime_type, content) = r
break

View File

@@ -41,16 +41,26 @@ from cic_cache.db import (
)
from cic_cache.runnable.daemons.filters import (
ERC20TransferFilter,
FaucetFilter,
)
script_dir = os.path.realpath(os.path.dirname(__file__))
def add_block_args(argparser):
argparser.add_argument('--history-start', type=int, default=0, dest='history_start', help='Start block height for initial history sync')
argparser.add_argument('--no-history', action='store_true', dest='no_history', help='Skip initial history sync')
return argparser
logg = cic_base.log.create()
argparser = cic_base.argparse.create(script_dir, cic_base.argparse.full_template)
#argparser = cic_base.argparse.add(argparser, add_traffic_args, 'traffic')
argparser = cic_base.argparse.add(argparser, add_block_args, 'block')
args = cic_base.argparse.parse(argparser, logg)
config = cic_base.config.create(args.c, args, args.env_prefix)
config.add(args.history_start, 'SYNCER_HISTORY_START', True)
config.add(args.no_history, '_NO_HISTORY', True)
cic_base.config.log(config)
dsn = dsn_from_config(config)
@@ -59,7 +69,6 @@ SessionBase.connect(dsn, debug=config.true('DATABASE_DEBUG'))
chain_spec = ChainSpec.from_chain_str(config.get('CIC_CHAIN_SPEC'))
#RPCConnection.register_location(config.get('ETH_PROVIDER'), chain_spec, 'default')
cic_base.rpc.setup(chain_spec, config.get('ETH_PROVIDER'))
@@ -71,6 +80,7 @@ def register_filter_tags(filters, session):
session.commit()
logg.info('added tag name "{}" domain "{}"'.format(tag[0], tag[1]))
except sqlalchemy.exc.IntegrityError:
session.rollback()
logg.debug('already have tag name "{}" domain "{}"'.format(tag[0], tag[1]))
@@ -82,7 +92,7 @@ def main():
r = rpc.do(o)
block_offset = int(strip_0x(r), 16) + 1
logg.debug('starting at block {}'.format(block_offset))
logg.debug('current block height {}'.format(block_offset))
syncers = []
@@ -91,8 +101,13 @@ def main():
syncer_backends = SQLBackend.resume(chain_spec, block_offset)
if len(syncer_backends) == 0:
logg.info('found no backends to resume')
syncer_backends.append(SQLBackend.initial(chain_spec, block_offset))
initial_block_start = config.get('SYNCER_HISTORY_START')
initial_block_offset = block_offset
if config.get('_NO_HISTORY'):
initial_block_start = block_offset
initial_block_offset += 1
syncer_backends.append(SQLBackend.initial(chain_spec, initial_block_offset, start_block_height=initial_block_start))
logg.info('found no backends to resume, adding initial sync from history start {} end {}'.format(initial_block_start, initial_block_offset))
else:
for syncer_backend in syncer_backends:
logg.info('resuming sync session {}'.format(syncer_backend))
@@ -112,9 +127,11 @@ def main():
logg.info('using trusted address {}'.format(address))
erc20_transfer_filter = ERC20TransferFilter(chain_spec)
faucet_filter = FaucetFilter(chain_spec)
filters = [
erc20_transfer_filter,
faucet_filter,
]
session = SessionBase.create_session()

View File

@@ -1,2 +1,2 @@
[eth]
provider = ws://localhost:63546
provider = http://localhost:63545

View File

@@ -1,2 +1,3 @@
[syncer]
loop_interval = 1
history_start = 0

View File

@@ -1,2 +1,3 @@
[syncer]
loop_interval = 5
history_start = 0

View File

@@ -17,7 +17,7 @@ RUN apt-get update && \
# Copy shared requirements from top of mono-repo
RUN echo "copying root req file ${root_requirement_file}"
RUN pip install $pip_extra_index_url_flag cic-base[full_graph]==0.1.2a76
RUN pip install $pip_extra_index_url_flag cic-base[full_graph]==0.1.2b9
COPY cic-cache/requirements.txt ./
COPY cic-cache/setup.cfg \

View File

@@ -1,4 +1,4 @@
cic-base~=0.1.2b8
cic-base~=0.1.2b10
alembic==1.4.2
confini~=0.3.6rc3
uwsgi==2.0.19.1

View File

@@ -88,3 +88,16 @@ def txs(
tx_hash_first,
tx_hash_second,
]
@pytest.fixture(scope='function')
def tag_txs(
init_database,
txs,
):
db.add_tag(init_database, 'taag', domain='test')
init_database.commit()
db.tag_transaction(init_database, txs[1], 'taag', domain='test')

View File

@@ -22,7 +22,7 @@ from cic_cache.runnable.daemons.filters.erc20 import ERC20TransferFilter
logg = logging.getLogger()
def test_cache(
def test_erc20_filter(
eth_rpc,
foo_token,
init_database,

View File

@@ -0,0 +1,71 @@
# standard imports
import logging
# external imports
from chainlib.chain import ChainSpec
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.block import (
block_by_hash,
Block,
)
from chainlib.eth.tx import (
receipt,
unpack,
transaction,
Tx,
)
from hexathon import strip_0x
from erc20_faucet.faucet import SingleShotFaucet
from sqlalchemy import text
# local imports
from cic_cache.db import add_tag
from cic_cache.runnable.daemons.filters.faucet import FaucetFilter
logg = logging.getLogger()
def test_filter_faucet(
eth_rpc,
eth_signer,
foo_token,
faucet_noregistry,
init_database,
list_defaults,
contract_roles,
agent_roles,
tags,
):
chain_spec = ChainSpec('foo', 'bar', 42, 'baz')
fltr = FaucetFilter(chain_spec, contract_roles['CONTRACT_DEPLOYER'])
add_tag(init_database, fltr.tag_name, domain=fltr.tag_domain)
nonce_oracle = RPCNonceOracle(agent_roles['ALICE'], eth_rpc)
c = SingleShotFaucet(chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle)
(tx_hash_hex, o) = c.give_to(faucet_noregistry, agent_roles['ALICE'], agent_roles['ALICE'])
r = eth_rpc.do(o)
tx_src = unpack(bytes.fromhex(strip_0x(o['params'][0])), chain_spec)
o = receipt(r)
r = eth_rpc.do(o)
rcpt = Tx.src_normalize(r)
assert r['status'] == 1
o = block_by_hash(r['block_hash'])
r = eth_rpc.do(o)
block_object = Block(r)
tx = Tx(tx_src, block_object)
tx.apply_receipt(rcpt)
r = fltr.filter(eth_rpc, block_object, tx, init_database)
assert r
s = text("SELECT x.tx_hash FROM tag a INNER JOIN tag_tx_link l ON l.tag_id = a.id INNER JOIN tx x ON x.id = l.tx_id WHERE a.domain = :a AND a.value = :b")
r = init_database.execute(s, {'a': fltr.tag_domain, 'b': fltr.tag_name}).fetchone()
assert r[0] == tx.hash

View File

@@ -0,0 +1,31 @@
# standard imports
import json
# external imports
import pytest
# local imports
from cic_cache.runnable.daemons.query import process_transactions_all_data
def test_api_all_data(
init_database,
txs,
):
env = {
'PATH_INFO': '/txa/410000/420000',
'HTTP_X_CIC_CACHE_MODE': 'all',
}
j = process_transactions_all_data(init_database, env)
o = json.loads(j[1])
assert len(o['data']) == 2
env = {
'PATH_INFO': '/txa/420000/410000',
'HTTP_X_CIC_CACHE_MODE': 'all',
}
with pytest.raises(ValueError):
j = process_transactions_all_data(init_database, env)

View File

@@ -9,6 +9,7 @@ import pytest
# local imports
from cic_cache import BloomCache
from cic_cache.cache import DataCache
logg = logging.getLogger()
@@ -33,3 +34,23 @@ def test_cache(
assert b[0] == list_defaults['block'] - 1
def test_cache_data(
init_database,
list_defaults,
list_actors,
list_tokens,
txs,
tag_txs,
):
session = init_database
c = DataCache(session)
b = c.load_transactions_with_data(410000, 420000)
assert len(b[2]) == 2
assert b[2][0]['tx_hash'] == txs[1]
assert b[2][1]['tx_type'] == 'unknown'
assert b[2][0]['tx_type'] == 'test.taag'

View File

@@ -16,4 +16,6 @@ def default_token(self):
return {
'symbol': self.default_token_symbol,
'address': self.default_token_address,
'name': self.default_token_name,
'decimals': self.default_token_decimals,
}

View File

@@ -20,7 +20,8 @@ from chainlib.eth.tx import (
)
from chainlib.chain import ChainSpec
from chainlib.error import JSONRPCException
from eth_accounts_index.registry import AccountRegistry # TODO, use interface module instead (needs gas limit method)
from eth_accounts_index.registry import AccountRegistry
from eth_accounts_index import AccountsIndex
from sarafu_faucet import MinterFaucet
from chainqueue.db.models.tx import TxCache
@@ -127,12 +128,12 @@ def register(self, account_address, chain_spec_dict, writer_address=None):
if writer_address == ZERO_ADDRESS:
session.close()
raise RoleMissingError('call address for resgistering {}'.format(account_address))
account_registry_address = registry.by_name('AccountsIndex', sender_address=call_address)
account_registry_address = registry.by_name('AccountRegistry', sender_address=call_address)
# Generate and sign transaction
rpc_signer = RPCConnection.connect(chain_spec, 'signer')
nonce_oracle = CustodialTaskNonceOracle(writer_address, self.request.root_id, session=session) #, default_nonce)
gas_oracle = self.create_gas_oracle(rpc, AccountsIndex.gas)
gas_oracle = self.create_gas_oracle(rpc, AccountRegistry.gas)
account_registry = AccountsIndex(chain_spec, signer=rpc_signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle)
(tx_hash_hex, tx_signed_raw_hex) = account_registry.add(account_registry_address, writer_address, account_address, tx_format=TxFormat.RLP_SIGNED)
rpc_signer.disconnect()

View File

@@ -3,16 +3,19 @@ import logging
# external imports
import celery
from cic_eth_registry.error import UnknownContractError
from cic_eth_registry.error import (
UnknownContractError,
NotAContractError,
)
from chainlib.status import Status as TxStatus
from chainlib.eth.address import to_checksum_address
from chainlib.eth.error import RequestMismatchException
from chainlib.eth.constant import ZERO_ADDRESS
from chainlib.eth.erc20 import ERC20
from hexathon import (
strip_0x,
add_0x,
)
from eth_erc20 import ERC20
from erc20_faucet import Faucet
# local imports
@@ -69,7 +72,9 @@ class CallbackFilter(SyncFilter):
#transfer_data['token_address'] = tx.inputs[0]
faucet_contract = tx.inputs[0]
o = Faucet.token(faucet_contract, sender_address=self.caller_address)
c = Faucet(self.chain_spec)
o = c.token(faucet_contract, sender_address=self.caller_address)
r = conn.do(o)
transfer_data['token_address'] = add_0x(c.parse_token(r))
@@ -124,8 +129,7 @@ class CallbackFilter(SyncFilter):
(transfer_type, transfer_data) = parser(tx, conn)
if transfer_type == None:
continue
else:
pass
break
except RequestMismatchException:
continue
@@ -168,7 +172,9 @@ class CallbackFilter(SyncFilter):
t = self.call_back(transfer_type, result)
logg.info('callback success task id {} tx {} queue {}'.format(t, tx.hash, t.queue))
except UnknownContractError:
logg.debug('callback filter {}:{} skipping "transfer" method on unknown contract {} tx {}'.format(tx.queue, tx.method, transfer_data['to'], tx.hash))
logg.debug('callback filter {}:{} skipping "transfer" method on unknown contract {} tx {}'.format(self.queue, self.method, transfer_data['to'], tx.hash))
except NotAContractError:
logg.debug('callback filter {}:{} skipping "transfer" on non-contract address {} tx {}'.format(self.queue, self.method, transfer_data['to'], tx.hash))
def __str__(self):

View File

@@ -14,7 +14,7 @@ from .base import SyncFilter
logg = logging.getLogger().getChild(__name__)
account_registry_add_log_hash = '0x5ed3bdd47b9af629827a8d129aa39c870b10c03f0153fe9ddb8e84b665061acd'
account_registry_add_log_hash = '0x9cc987676e7d63379f176ea50df0ae8d2d9d1141d1231d4ce15b5965f73c9430'
class RegistrationFilter(SyncFilter):

View File

@@ -30,7 +30,7 @@ class TxFilter(SyncFilter):
if otx == None:
logg.debug('tx {} not found locally, skipping'.format(tx_hash_hex))
return None
logg.info('tx filter match on {}'.format(otx.tx_hash))
logg.debug('otx filter match on {}'.format(otx.tx_hash))
db_session.flush()
SessionBase.release_session(db_session)
s_final_state = celery.signature(

View File

@@ -22,6 +22,7 @@ from chainlib.eth.connection import (
from chainlib.chain import ChainSpec
from chainqueue.db.models.otx import Otx
from cic_eth_registry.error import UnknownContractError
from cic_eth_registry.erc20 import ERC20Token
import liveness.linux
@@ -36,7 +37,7 @@ from cic_eth.eth import (
from cic_eth.admin import (
debug,
ctrl,
token
token,
)
from cic_eth.queue import (
query,
@@ -75,7 +76,6 @@ argparser.add_argument('-c', type=str, default=config_dir, help='config file')
argparser.add_argument('-q', type=str, default='cic-eth', help='queue name for worker tasks')
argparser.add_argument('-r', type=str, help='CIC registry address')
argparser.add_argument('--default-token-symbol', dest='default_token_symbol', type=str, help='Symbol of default token to use')
argparser.add_argument('--abi-dir', dest='abi_dir', type=str, help='Directory containing bytecode and abi')
argparser.add_argument('--trace-queue-status', default=None, dest='trace_queue_status', action='store_true', help='set to perist all queue entry status changes to storage')
argparser.add_argument('-i', '--chain-spec', dest='i', type=str, help='chain spec')
argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration')
@@ -121,20 +121,25 @@ broker = config.get('CELERY_BROKER_URL')
if broker[:4] == 'file':
bq = tempfile.mkdtemp()
bp = tempfile.mkdtemp()
current_app.conf.update({
conf_update = {
'broker_url': broker,
'broker_transport_options': {
'data_folder_in': bq,
'data_folder_out': bq,
'data_folder_processed': bp,
},
},
)
}
if config.true('CELERY_DEBUG'):
conf_update['result_extended'] = True
current_app.conf.update(conf_update)
logg.warning('celery broker dirs queue i/o {} processed {}, will NOT be deleted on shutdown'.format(bq, bp))
else:
current_app.conf.update({
'broker_url': broker,
})
conf_update = {
'broker_url': broker,
}
if config.true('CELERY_DEBUG'):
conf_update['result_extended'] = True
current_app.conf.update(conf_update)
result = config.get('CELERY_RESULT_URL')
if result[:4] == 'file':
@@ -203,6 +208,11 @@ def main():
BaseTask.default_token_symbol = config.get('CIC_DEFAULT_TOKEN_SYMBOL')
BaseTask.default_token_address = registry.by_name(BaseTask.default_token_symbol)
default_token = ERC20Token(chain_spec, rpc, BaseTask.default_token_address)
default_token.load(rpc)
BaseTask.default_token_decimals = default_token.decimals
BaseTask.default_token_name = default_token.name
BaseTask.run_dir = config.get('CIC_RUN_DIR')
logg.info('default token set to {} {}'.format(BaseTask.default_token_symbol, BaseTask.default_token_address))

View File

@@ -51,15 +51,23 @@ from cic_eth.registry import (
script_dir = os.path.realpath(os.path.dirname(__file__))
def add_block_args(argparser):
argparser.add_argument('--history-start', type=int, default=0, dest='history_start', help='Start block height for initial history sync')
argparser.add_argument('--no-history', action='store_true', dest='no_history', help='Skip initial history sync')
return argparser
logg = cic_base.log.create()
argparser = cic_base.argparse.create(script_dir, cic_base.argparse.full_template)
#argparser = cic_base.argparse.add(argparser, add_traffic_args, 'traffic')
argparser = cic_base.argparse.add(argparser, add_block_args, 'block')
args = cic_base.argparse.parse(argparser, logg)
config = cic_base.config.create(args.c, args, args.env_prefix)
config.add(args.y, '_KEYSTORE_FILE', True)
config.add(args.q, '_CELERY_QUEUE', True)
config.add(args.history_start, 'SYNCER_HISTORY_START', True)
config.add(args.no_history, '_NO_HISTORY', True)
cic_base.config.log(config)
@@ -69,9 +77,9 @@ SessionBase.connect(dsn, pool_size=16, debug=config.true('DATABASE_DEBUG'))
chain_spec = ChainSpec.from_chain_str(config.get('CIC_CHAIN_SPEC'))
#RPCConnection.register_location(config.get('ETH_PROVIDER'), chain_spec, 'default')
cic_base.rpc.setup(chain_spec, config.get('ETH_PROVIDER'))
def main():
# connect to celery
celery.Celery(broker=config.get('CELERY_BROKER_URL'), backend=config.get('CELERY_RESULT_URL'))
@@ -89,7 +97,7 @@ def main():
stat = init_chain_stat(rpc, block_start=block_current)
loop_interval = stat.block_average()
logg.debug('starting at block {}'.format(block_offset))
logg.debug('current block height {}'.format(block_offset))
syncers = []
@@ -98,8 +106,13 @@ def main():
syncer_backends = SQLBackend.resume(chain_spec, block_offset)
if len(syncer_backends) == 0:
logg.info('found no backends to resume')
syncer_backends.append(SQLBackend.initial(chain_spec, block_offset))
initial_block_start = config.get('SYNCER_HISTORY_START')
initial_block_offset = block_offset
if config.get('_NO_HISTORY'):
initial_block_start = block_offset
initial_block_offset += 1
syncer_backends.append(SQLBackend.initial(chain_spec, initial_block_offset, start_block_height=initial_block_start))
logg.info('found no backends to resume, adding initial sync from history start {} end {}'.format(initial_block_start, initial_block_offset))
else:
for syncer_backend in syncer_backends:
logg.info('resuming sync session {}'.format(syncer_backend))
@@ -155,7 +168,6 @@ def main():
for cf in callback_filters:
syncer.add_filter(cf)
#r = syncer.loop(int(config.get('SYNCER_LOOP_INTERVAL')), rpc)
r = syncer.loop(int(loop_interval), rpc)
sys.stderr.write("sync {} done at block {}\n".format(syncer, r))

View File

@@ -12,7 +12,10 @@ import confini
import celery
# local imports
from cic_eth.api import Api
from cic_eth.api import (
Api,
AdminApi,
)
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
@@ -53,13 +56,20 @@ celery_app = celery.Celery(broker=config.get('CELERY_BROKER_URL'), backend=confi
queue = args.q
api = Api(config.get('CIC_CHAIN_SPEC'), queue=queue)
admin_api = AdminApi(None)
def main():
t = admin_api.registry()
registry_address = t.get()
print('Registry: {}'.format(registry_address))
t = api.default_token()
token_info = t.get()
print('Default token symbol: {}'.format(token_info['symbol']))
print('Default token address: {}'.format(token_info['address']))
logg.debug('Default token name: {}'.format(token_info['name']))
logg.debug('Default token decimals: {}'.format(token_info['decimals']))
if __name__ == '__main__':
main()

View File

@@ -33,6 +33,8 @@ class BaseTask(celery.Task):
create_gas_oracle = RPCGasOracle
default_token_address = None
default_token_symbol = None
default_token_name = None
default_token_decimals = None
run_dir = '/run'
def create_session(self):

View File

@@ -10,7 +10,7 @@ version = (
0,
11,
0,
'beta.12',
'beta.13',
)
version_object = semver.VersionInfo(

View File

@@ -1,3 +1,4 @@
[celery]
broker_url = redis://
result_url = redis://
debug = 0

View File

@@ -1,3 +1,4 @@
[celery]
broker_url = redis://localhost:63379
result_url = redis://localhost:63379
debug = 0

View File

@@ -1,2 +1,3 @@
[SYNCER]
loop_interval =
history_start = 0

View File

@@ -1,2 +1,3 @@
[SYNCER]
loop_interval =
history_start = 0

View File

@@ -0,0 +1,22 @@
@node cic-eth-accounts
@section Accounts
Accounts are private keys in the signer component keyed by "addresses," a one-way transformation of a public key. Data can be signed by using the account as identifier for corresponding RPC requests.
Any account to be managed by @code{cic-eth} must be created by the corresponding task. This is because @code{cic-eth} creates a @code{nonce} entry for each newly created account, and guarantees that every nonce will only be used once in its threaded environment.
The calling code receives the account address upon creation. It never receives or has access to the private key.
@subsection Signer RPC
The signer is expected to handle a subset of the standard JSON-RPC:
@table @code
@item personal_newAccount(password)
Creates a new account, returning the account address.
@item eth_signTransactions(tx_dict)
Sign the transaction represented as a dictionary.
@item eth_sign(address, message)
Signs an arbtirary message with the standard Ethereum prefix.
@end table

View File

@@ -0,0 +1,60 @@
@node cic-eth system maintenance
@appendix Admin API
The admin API is still in an early stage of refinement. User friendliness can be considerably improved.
All of the API calls are celery task proxies, and return @code{Celery.AsyncResult} unless otherwise noted.
In contrast to the client API module, this API does not currently implement a pluggable callback.
@appendixsection registry
Returns the @code{ContractRegistry} this instance of @code{cic-eth-tasker} is running on.
@appendixsection proxy-do
Execute an arbitary JSON-RPC request using the @code{cic-eth-tasker} blockchain node RPC connection.
@appendixsection default_token
Returns the default token symbol and address.
@appendixsection lock
Set lock bits, globally or per address
@appendixsection unlock
Opposite of lock
@appendixsection get_lock
Get the current state of a lock
@appendixsection tag_account
Associate an identifier with an account address (@xref{cic-eth system accounts})
@appendixsection have_account
Check whether a private key exists in the keystore able to sign on behalf of the given account (it actually performs a signature).
@appendixsection resend
Clone or resend a transaction
@appendixsection check_nonce
Returns diagnostics for nonce sequences per account, e.g. detect nonce gaps that block execution of further transactions.
@appendixsection fix_nonce
Re-orders all nonces by shifting all transaction nonces after the given transaction down by one. This has the additional effect of obsoleting the given transaction. Can be used to close gaps in the nonce sequencing. Use with care!
@appendixsection account
Return brief transaction info lists per account
@appendixsection tx
Return a complex transaction metadata object for a single transaction. The object assembles state from both the blockchain node and the custodial queue system.

View File

@@ -0,0 +1,18 @@
\input texinfo
@setfilename index.html
@settitle CIC custodial services reference deployment
@copying
Released 2021 under GPL3
@end copying
@titlepage
@title CIC custodial services reference deployment
@author Louis Holbrook
@end titlepage
@c
@contents
@include index.texi

View File

@@ -0,0 +1,4 @@
@node cic-eth Appendix Task chains
@appendix Task chains
TBC - explain here how to generate these chain diagrams

View File

@@ -0,0 +1,108 @@
@node cic-eth configuration
@section Configuration
(refer to @code{cic-base} for a general overview of the config pipeline)
Configuration parameters are grouped by configuration filename.
@subsection cic
@table @var
@item registry_address
Ethereum address of the @var{ContractRegistry} contract
@item chain_spec
String representation of the connected blockchain according to the @var{chainlib} @var{ChainSpec} format.
@item tx_retry_delay
Minimum time in seconds to wait before retrying a transaction
@item trust_address
Comma-separated list of one or more ethereum addresses regarded as trusted for describing other resources, Used by @var{cic-eth-registry} in the context of the @var{AddressDeclarator}.
@item defalt_token_symbol
Fallback token to operate on when no other context is given.
@item health_modules
Comma-separated list of methods to execute liveness tests against. (see ...)
@item run_dir
Directory to use for session-scoped variables for @var{cic-eth} daemon parent processes.
@end table
@subsection celery
@table @var
@item broker_url
Message broker URL
@item result_url
Result backend URL
@item debug
Boolean value. If set, the amount of available context for a task in the result backend will be maximized@footnote{This is a @emph{required} setting for the task graph documenter to enabled it to display task names in the graph}.
@end table
@subsection database
See ref cic-base when ready
@subsection eth
@table @var
@item provider
Address of default RPC endpoint for transactions and state queries.
@item gas_gifter_minimum_balance
The minimum gas balance that must be held by the @code{GAS GIFTER} token before the queue processing shuts down@footnote{You should really make sure that this threshold is never hit}
@end table
@subsection redis
Defines connection to the redis server used outside of the context of @var{celery}. This is usually the same server, but should be a different db.
@table @var
@item host
Redis hostname
@item port
Redis port
@item db
Redis db
@end table
@subsection signer
Parameters
@table @var
@item socket_path
The connection string for the signer JSON-RPC service.@footnote{The @var{crypto-dev-signer} supports UNIX socket or a HTTP(S) connections}
@item secret
If set, this password is used to add obfuscation on top of the encryption already applied by the signer for the keystore.
@end table
@subsection ssl
Certificate information for https api callbacks.
@table @var
@item enable_client
Boolean value. If set, client certificate will be used to authenticate the callback request.
@item cert_file
Client certificate file in PEM or DER format
@item key_file
Client key file in PEM or DER format
@item password
Password for unlocking the client key
@item ca_file
Certificate authority bundle, to verify the certificate sent by the callback server.
@end table
@subsection syncer
@table @var
@item loop_interval
Seconds to pause before each execution of the @var{chainsyncer} poll loop.
@end table

View File

@@ -0,0 +1,46 @@
@node cic-eth-dependencies
@section Dependencies
This application is written in Python 3.8. It is tightly coupled with @code{python-celery}, which provides the task worker ecosystem. It also uses @code{SQLAlchemy} which provides useful abstractions for persistent storage though SQL, and @code{alembic} for database schema migrations.
There is currently also a somewhat explicit coupling with @code{Redis}, which is used as message broker for @code{python-celery}. @code{Redis} is also explicitly used by some CLI tools to retrieve results from command execution. This coupling may be relaxed in the future to allow other key-value pubsub solutions instead.
@subsection Generalized project dependencies
The core features are built around four main independent components that have been developed for the purpose of this project, but are separated and maintained as general-purpose libraries.
@table @samp
@item chainlib
A cross-chain library prototype that can provide encodings for transactions on a Solidity-based EVM contract network.
@item chainqueue
Queue manager that guarantees delivery of outgoing blockchain transactions.
@item chainsyncer
Monitors blockchains and guarantees execution of an arbitrary count of pluggable code objects for each block transaction.
@item crypto-dev-signer
An keystore capable of signing for the EVM chain through a standard Ethereum JSON-RPC interface.
@end table
@anchor{cic-eth-dependencies-smart-contracts}
@subsection Smart contract dependencies
The Smart contracts needed by the network must be discoverable through a single entry point called the Contract Registry. The contract registry is expected to reference itself in its records. The authenticity of the contract registry must be guaranteed by external sources of trust.
The contract registry maps contract addresses to well-known identifiers. The contracts are as follows:
@table @code
@item ContractRegistry (points to self)
Resolves plaintext identifiers to contract addresses.
@item AccountRegistry
An append-only store of accounts hosted by the custodial system
@item TokenRegistry
Unique symbol-to-address mappings for token contracts
@item AddressDeclarator
Reverse address to resource lookup
@item TokenAuthorization
Escrow contract for external spending on behalf of custodial users
@item Faucet
Called by newly created accounts to receive initial token balance
@end table
The dependency @code{cic-eth-registry} abstracts and facilitates lookups of resources on the blockchain network. In its current state it resolves tokens by symbol or address, and contracts by common-name identifiers. In the @code{cic-eth} code all lookups for EVM network resources will be performed through this dependency.

View File

@@ -0,0 +1,49 @@
@node cic-eth-incoming
@section Incoming transactions
All transactions in mined blocks will be passed to a selection of plugin filters to the @code{chainsyncer} component. Each of these filters are individual python module files in @code{cic_eth.runnable.daemons.filters}. This section describes their function.
The status bits refer to the bits definining the @code{chainqueue} state.
@subsection tx
Looks up the transaction in the local queue, and if found it sets the @code{FINAL} state bit. If the contract code execution was unsuccessful, the @code{NETWORK ERROR} state bit is also set.
@subsection gas
If the transaction is a gas token transfer, it checks if the recipient is a custodial account awaiting gas refill to execute a transaction (the queue item will have the @code{GAS ISSUES} bit set). If this is the case, the transaction will be activated by setting the @code{QUEUED} bit.
@subsection register
If the transaction is an account registration@footnote{The contract keyed by @var{AccountRegistry} in the @var{ContractRegistry} contract}, a Faucet transaction will be triggered for the registered account@footnote{The faucet contract used in the reference implementation will verify whether the account calling it is registered in the @var{AccountRegistry}. Thus it cannot be called before the account registration has succeeded.}
@subsection callback
Executes, in order, Celery tasks defined in the configuration variable @var{TASKS_TRANSFER_CALLBACKS}. Each of these tasks are registered as individual filters in the @code{chainsyncer} component, with the corresponding execution guarantees.
The callbacks will receive the following arguments
@enumerate
@item @strong{result}
A complex representation of the transaction (see section ?)
@item @strong{transfertype}
A string describing the type of transaction found@footnote{See appendix ? for an overview of possible values}
@item @strong{status}
0 if contract code executed successfully. Any other value is an error@footnote{The values 1-1024 are reserved for system specific errors. In the current implementation only a general error state with value 1 is defined. See appendix ?.}
@end enumerate
@subsection transferauth
If a valid transfer authorization request has been made, a token @emph{allowance}@footnote{@code{approve} for ERC20 tokens} transaction is executed on behalf of the custodial account, with the @var{TransferAuthorization} contract as spender.
@subsection convert
If the transaction is a token conversion, @emph{and} there is a pending transfer registered for the conversion, the corresponding token transfer transaction will be executed. Not currently implemented

View File

@@ -0,0 +1,14 @@
@top cic-eth
@include intro.texi
@include dependencies.texi
@include configuration.texi
@include system.texi
@include interacting.texi
@include outgoing.texi
@include incoming.texi
@include services.texi
@include tools.texi
@include admin.texi
@include chains.texi
@include transfertypes.texi

View File

@@ -0,0 +1,109 @@
@node cic-eth-interacting
@section Interacting with the system
The API to the @var{cic-eth} component is a proxy for executing @emph{chains of Celery tasks}. The tasks that compose individual chains are documented in @ref{cic-eth Appendix Task chains,the Task Chain appendix}, which also describes a CLI tool that can generate graph representationso of them.
There are two API classes, @var{Api} and @var{AdminApi}. The former is described later in this section, the latter described in @ref{cic-eth system maintenance,the Admin API appendix}.
@subsection Interface
API calls are constructed by creating @emph{Celery task signatures} and linking them together, sequentially and/or in parallell. In turn, the tasks themselves may spawn other asynchronous tasks. This means that the code in @file{cic_eth.api.*} does not necessarily specify the full task graph that will be executed for any one command.
The operational guarantee that tasks will be executed, not forgotten, and retried under certain circumstances is deferred to @var{Celery}. On top of this, the @var{chainqueue} component ensures that semantic state changes that the @code{Celery} tasks ask of it are valid.
@anchor{cic-eth-locking}
@subsection Locking
All methods that make a change to the blockchain network must pass @emph{locking layer checks}. Locks may be applied on a global or per-address basis. Lock states are defined by a combination of bit flags. The implemented lock bits are:
@table @var
@item INIT
The system has not yet been initialized. In this state, writes are limited to creating unregistered accounts only.
@item QUEUE
Items may not be added to the queue
@item SEND
Queued items may not be attempted sent to the network
@item CREATE (global-only)
New accounts may not be created
@item STICKY
Until reset, no other part of the locking state can be reset
@end table
@subsection Callback
All API calls provide the option to attach a callback to the end of the task chain. This callback will be executed regardless of whether task chain execution succeeded or not.
Refer to @file{cic-eth.callbacks.noop.noop} for the expected callback signature.
@subsection API Methods that change state
@subsubsection create_account
Creates a new account in the keystore, optionally registering the account with the @var{AccountRegistry} contract.
@subsubsection transfer
Attempts to execute a token transaction between two addresses. It is the caller's responsibility to check whether the token balance is sufficient for the transactions.
@subsubsection refill_gas
Executes a gas token transfer to a custodial address from the @var{GAS GIFTER} system account.
@subsubsection convert
Converts a token to another token for the given custodial account. Currently not implemented.
@anchor{cic-eth-convert-and-transfer}
@subsubsection convert_and_transfer
Same as convert, but will automatically execute a token transfer to another custodial account when conversion has been completed. Currently not implemented.
@subsection Read-only API methods
@subsubsection balance
Retrieves a complex balance statement of a single account, including:
@itemize
@item The network balance at the current block height
@item Value reductions due to by pending outgoing transactions
@item Value increments due to by pending incoming transactions
@end itemize
Only the first of these balance items has guaranteed finality. The reduction by outgoing transaction can be reasonably be assumed to eventually become final. The same applies for the increment by incoming transaction, @emph{unless} the transfer is part of a multiple-transaction operation. For example, a @ref{cic-eth-convert-and-transfer,convert_and_transfer} operation may fail in the convert stage and/or may yield less tokens then expected after conversion.
@subsubsection list
Returns an aggregate iist of all token value changes for a given address. As not all value transfers are a result of literal value transfer contract calls (e.g. @var{transfer} and @var{transferFrom} in @var{ERC20}), this data may come from a number of sources, including:
@itemize
@item Literal value transfers within the custodial system
@item Literal value transfers from or to an external address
@item Faucet invocations (token minting)
@item Demurrage and redistribution built into the token contract
@end itemize
@subsubsection default_token
Return the symbol and address of the token used by default in the network.
@subsubsection ping
Convenience method for the caller to check whether the @var{cic-eth} engine is alive.

View File

@@ -0,0 +1,74 @@
@node cic-eth-outgoing
@section Outgoing transactions
@strong{Important! A pre-requisite for proper functioning of the component is that no other agent is sending transactions to the network for any of the keys in the keystore.}
The term @var{state bit} refers to the bits definining the @code{chainqueue} state.
@subsection Lock
Any task that changes blockchain state @strong{must} apply a @code{QUEUE} lock for the address it operates on. This is to ensure that transactions are sent to the network in order.@footnote{If too many transactions arrive out of order to the blockchain node, it may arbitrarily prune those that cannot directly be included in a block. This puts unnecessary strain (and reliance) on the transaction retry mechanism.}
This lock will be released once the blockchain node confirms handover of the transaction.@footnote{This is the responsibility of the @var{dispatcher} service}
@subsection Nonce
A separate task step is executed for binding a transaction nonce to a Celery task root id, which uniquely identifies the task chain. This provides atomicity of the nonce across the parallell task environment, and also recoverability in case unexpected program interruption.
The nonce of a permanently failed task must be @emph{manually} unlocked. Celery tasks that involve nonces who permanently fail are to be considered @emph{critical anomalies} and should not happen. The queue locking mechanism is designed to prevent the amount of out-of-sequence transactions for an account to escalate.
@subsection Choosing fee prices
@code{cic-eth} uses the @code{chainlib} module to resolve gas price lookups.
Optimizing gas price discovery should be the responsibility of the chainlib layer. It already accommodates using an separate RPC for the @code{eth_gasPrice} call.@footnote{A sample implementation of a gas price tracker speaking JSON-RPC (also built using chainlib/chainsyncer) can be found at @url{https://gitlab.com/nolash/eth-stat-syncer}.}
@subsection Choosing gas limits
To determine the gas limit of a transaction, normally the EVM node will be used to perform a dry-run exection of the inputs against the current chain state.
As the current state of the custodial system should only rely on known, trusted contract bytecode, there is no real need for this mechanism. The @code{chainlib}-based contract interfaces are expected to provide a method call that return safe gas limit values for contract interactions.@footnote{Of course, this method call may in turn conceal more sophisticated gas limit heuristics.}
Note that it is still the responsibility of @code{cic-eth} to make sure that the gas limit of the network is sufficient to allow execution of all needed contracts.
@subsection Gas refills
If the gas balance of a custodial account is below a certain threshold, a gas refill task will be spawned. The gas will be transferred from the @code{GAS GIFTER} system account.
In the event that the balance is insufficient even for the imminent transaction@footnote{This will of course be the case when an account is first created, whereupon it has a balance of 0. The subsequent faucet call will spawn a gas refill task.}, execution of the transaction will be deferred until the gas refill transaction is completed. In this case the transaction will be marked with the @code{GAS ISSUES} state bit.
The value chosen for the gas refill threshold should ideally allow enough of a margin to avoid the need of deferring transactions in the future.
@subsection Queueing transactions
Once the lock, nonce and gas processing parts has been completed, the transaction will be queued for sending. This means that the @code{QUEUED} state bit is set. From here the @ref{cic-eth-services-dispatcher,dispatcher service} takes over responsibility.
@subsection Retrying transactions
There are three conditions create the need to defer and retry transactions.
The first is communication problems with the blockchain node itself, for example if it is overloaded or being restarted. As far as possible, retries of this nature will be left to the Celery task workers. There may be cases, however, where it is appropriate to hand the responsibility to the @code{chainqueue} instead. In this case, the queue item will have the @code{NODE ERROR} state bit set.
The second condition occurs when transactions take too long to be confirmed by the network. In this case, the transaction will be re-submitted, but with a higher gas price.
The third condition occurs when the blockchain node purges the transaction from the mempool before it is sent to the network. @code{cic-eth} does not distinguish this case from the second, as the issue is solved using the same mechanism.
@subsubsection Transaction obsoletion
"Re-submitting" a transaction means creating a transaction with a previously used nonce for an account address.
When this happens, The @code{chainqueue} will still contain all previous transactions with the same nonce. The transaction being superseded will have the @code{OBSOLETED} state bit set.
Once a transaction has been mined, all other transactions with the same node will have the @code{OBSOLETED} and @code{FINAL} state bits set.
@subsection Unexpected conditions
Any unexpected condition exposing the need for urgent code improvement and/or manual intervention will be signalled by marking the transaction with the @code{FUBAR} state bit set.

View File

@@ -1,24 +0,0 @@
@node cic-eth
@chapter cic-eth
@section Overview
@code{cic-eth} is the heart of the custodial account component. It is a combination of python-celery task queues and daemons that sign, dispatch and monitor blockchain transactions, aswell as triggering tasks contingent on other transactions.
@subsection Dependencies
The @code{cic-registry} module is used as a cache for contracts and tokens on the network.
A web3 JSON-RPC service that transparently proxies a keystore and provides transaction and message signing. The current development version uses the python web3 middleware feature to route methodsi involving the keystore to the module @code{crypto-dev-signer}, which is hosted on @file{pypi.org}.
@subsection What does it do
@subsection Tasks
Two main categories exist for tasks, @code{eth} and @code{queue}.
The @code{eth} tasks provide means to construct and decode Ethereum transactions, as well as interfacing the underlying key store.
Tasks in the @code{queue} module operate on the state of transactions queued for processing by @code{cic-eth}.

View File

@@ -0,0 +1,50 @@
@node cic-eth-services
@section Services
There are four daemons that together orchestrate all of the aforementioned recipes. This section will provide a high level description of them.
Each of them have their own set of command line flags. These are available in the CLI help text provided by @kbd{-h} @kbd{--help} and are not recited here.
Daemon executable scripts are located in the @file{cic_eth.runnable.daemons} package. If @var{cic-eth} is installed as a python package, they are installed as executables in @var{PATH}.
@subsection tasker
This is the heart of the custodial system. Tasker is the parent process for the celery workers executing all tasks interacting with and changing the state of the queue and the chain. It is also the only service that interfaces with the signer/keystore.
The other @var{cic-eth} daemons all interface with this component, along with any client adapter bridging an end-user gateway (e.g. @var{cic-ussd}). However, the service itself does not have to be actively running for the other services to run; @var{Celery} handles queueing up the incoming tasks until the @var{tasker} comes back online.@footnote{Whereas this is true, there is currently no fail-safe implemented to handles the event of task backlog overflow in Celery. Furthermore, no targeted testing has yet been performed to asses the stability of the system over time if a sudden, sustained surge of resumed task executions occurs. It may be advisable to suspend activity that adds new queue items to the system if volume is high and/or the @var{cic-eth} outage endures. However, there is no panacea for this condition, as every usage scenario is different}
The tasker has a set of pre-requisites that must be fulfilled before it will start
@itemize
@item It must be given a valid @var{ContractRegistry} address, which must include valid references to all contracts specified in @ref{cic-eth-dependencies-smart-contracts,Smart contract dependencies}
@item The gas gifter balance must be above the minimum threshold (See "eth" section in configurations).
@item There must be a valid alembic migration record in the storage database
@item The redis backend must be reachable and writable
@item There must be a reachable JSON-RPC server at the other end of the signer socket path (see "signer" section in configurations)
@end itemize
@subsection tracker
Implements the @var{chainsyncer}, and registers the filters described in @ref{cic-eth-incoming,Incoming Transactions} to be executed for every transaction. It consumes the appropriate @var{TASKS_TRANSFER_CALLBACKS} configuration setting to add externally defined filters at without having to change the daemon code.
The @var{tracker} has the same requisities for the @var{ContractRegistry} as the @var{tasker}.
@strong{Important! Guarantees of filter executions has some caveats. Refer to the @var{chainsyncer} documentation for more details.}
@anchor{cic-eth-services-dispatcher}
@subsection dispatcher
Uses the @code{get_upcoming_tx} method call from @var{chainqueue} to receive batches of queued transactions that are ready to send to the blockchain node. Every batch will only contain a single transaction by any one address, which will be the transaction with the next nonce not previously seen by the network. There is no limit currently set to how many transactions that will be included in a single batch.
@subsection retrier
The responsibility of the @var{retrier} is to re-queue transactions that failed to be sent to the blockchain node, as well as create @emph{replacements} for transactions whose processing by the network has been delayed. @strong{[refer transaction obolestion]}.
It is in turn the responsiblity of the @var{dispatcher} to send these (re-)queued transactions to the blockchain node.

View File

@@ -0,0 +1,17 @@
@node cic-eth system accounts
@section System initialization
When the system starts for the first time, it is locked for any state change request other than account creation@footnote{Specifically, the @code{INIT}, @code{SEND} and @code{QUEUE} lock bits are set.}. These locks should be @emph{reset} once system initialization has been completed. Currently, system initialization only involves creating and tagging required system accounts, as specified below.
See @ref{cic-eth-locking,Locking} and @ref{cic-eth-tools-ctrl,ctrl in Tools} for details on locking.
@subsection System accounts
Certain accounts in the system have special roles. These are defined by @emph{tagging} certain accounts addresses with well-known identifiers.
@table @var
@item GAS_GIFTER
This account @strong{must} at all times have enough gas token to fund any custodial account address in need.
@item ACCOUNT_REGISTRY_WRITER
This account @strong{must} have access to add newly created account addresses to the (@xref{cic-eth-dependencies-smart-contracts,Smart contract dependencies})
@end table

View File

@@ -0,0 +1,51 @@
@node cic-eth-tools
@section Tools
A collection of CLI tools have been provided to help with diagnostics and other administrative tasks. These use the same configuration infrastructure as the daemons.
Tool scripts are located in the @file{cic_eth.runnable} package. If @var{cic-eth} is installed as a python package, they are installed as executables in @var{PATH}.
@subsection info (cic-eth-info)
Returns self-explanatory metadata for the blockchain network, and optionally an address.
@subsection inspect (cic-eth-inspect)
Returns information about a specific resource related to the tranasaction queue. The results returned depend on the type of the argument.
@table @var
@item lock
If the argument is the literal string @kbd{lock}, it will list all active lock settings currently in effect. (@xref{cic-eth-locking})
@item <address>
If the argument is a 0x-prefixed hex string of 42 characters, it returns all transactions where the specified address is a sender or recipient@footnote{If the address is the gas gifter or the accounts index writer, this may be a @emph{lot} of transactions. Use with care!}
@item <tx_hash>
If the argument is a 0x-prefixed hex string of 66 characters, it returns data from the custodial queueing system aswell as the network for a single transaction whose hash matches the input. Fails if the transaction does not exist in the queue
@item <code>
If the argument is a 0x-prefixed hex string longer than 66 bytes, the argument will be interpreted as raw RLP serialized transaction data, and attempt to match this with an entry in the queue. If a match is found, the result is the same as for @var{<tx_hash>}
@end table
@subsection create (cic-eth-create)
Create a new account, optionally registering the account in the accounts registry, and optionally receiving the newly created address through a redis subscription.
@subsection transfer (cic-eth-transfer)
Execute a token transfer on behalf of a custodial account.
@subsection tag (cic-eth-tag)
Associate an account address with a string identifier. @xref{cic-eth system accounts}
@anchor{cic-eth-tools-ctrl}
@subsection ctrl (cic-eth-ctrl)
Set or reset lock bits, globally or per account address.
@subsection resend (cic-eth-resend)
Resend a transaction. This can either be done "in-place," which means increasing the gas price and re-queueing@footnote{this is the same thing that the retrier does}. It can also be used to @emph{clone} a transaction, which obviously will duplicate the effect of the cloned transaction on the blockchain network.

View File

@@ -0,0 +1,11 @@
@node cic-eth Appendix Transaction types
@appendix Transfer types
@table @var
@item transfer
A regular token transfer, e.g. ERC20 @code{transfer}
@item transferfrom
A token transfer performed on behalf of another party, e.g. ERC20 @code{transferFrom}
@item tokengift
Result of a successful faucet request.
@end table

View File

@@ -19,7 +19,7 @@ RUN apt-get update && \
apt install -y gcc gnupg libpq-dev wget make g++ gnupg bash procps git
# Copy shared requirements from top of mono-repo
RUN echo "copying root req file ${root_requirement_file}"
RUN echo "copying root req file: ${root_requirement_file}"
#COPY $root_requirement_file .
#RUN pip install -r $root_requirement_file $pip_extra_index_url_flag
RUN /usr/local/bin/python -m pip install --upgrade pip
@@ -29,7 +29,7 @@ RUN /usr/local/bin/python -m pip install --upgrade pip
# python merge_requirements.py | tee merged_requirements.txt
#RUN cd cic-base && \
# pip install $pip_extra_index_url_flag -r ./merged_requirements.txt
RUN pip install $pip_extra_index_url_flag cic-base[full_graph]==0.1.2b8
RUN pip install $pip_extra_index_url_flag cic-base[full_graph]==0.1.2b9
COPY cic-eth/scripts/ scripts/
COPY cic-eth/setup.cfg cic-eth/setup.py ./

View File

@@ -1,4 +1,4 @@
cic-base==0.1.2b8
cic-base~=0.1.2b11
celery==4.4.7
crypto-dev-signer~=0.4.14b3
confini~=0.3.6rc3
@@ -7,18 +7,18 @@ redis==3.5.3
alembic==1.4.2
websockets==8.1
requests~=2.24.0
eth_accounts_index~=0.0.11a11
eth_accounts_index~=0.0.11a12
erc20-transfer-authorization~=0.3.1a6
uWSGI==2.0.19.1
semver==2.13.0
websocket-client==0.57.0
moolb~=0.1.1b2
eth-address-index~=0.1.1a11
chainlib~=0.0.3a1
chainlib~=0.0.3a2
hexathon~=0.0.1a7
chainsyncer[sql]~=0.0.2a4
chainqueue~=0.0.2a2
sarafu-faucet==0.0.3a1
sarafu-faucet==0.0.3a3
erc20-faucet==0.2.1a4
coincurve==15.0.0
sarafu-faucet==0.0.3a2
potaahto~=0.0.1a1
potaahto~=0.0.1a2

View File

@@ -4,7 +4,7 @@ import sys
import logging
# external imports
from chainlib.eth.erc20 import ERC20
from eth_erc20 import ERC20
# local imports
from cic_eth.api import Api

View File

@@ -14,9 +14,9 @@ from chainlib.eth.tx import (
Tx,
)
from chainlib.eth.block import Block
from chainlib.eth.erc20 import ERC20
from eth_erc20 import ERC20
from sarafu_faucet import MinterFaucet
from eth_accounts_index import AccountRegistry
from eth_accounts_index.registry import AccountRegistry
from potaahto.symbols import snake_and_camel
from hexathon import add_0x
@@ -26,7 +26,6 @@ from cic_eth.runnable.daemons.filters.callback import CallbackFilter
logg = logging.getLogger()
@pytest.mark.skip()
def test_transfer_tx(
default_chain_spec,
init_database,
@@ -66,7 +65,6 @@ def test_transfer_tx(
assert transfer_type == 'transfer'
@pytest.mark.skip()
def test_transfer_from_tx(
default_chain_spec,
init_database,

View File

@@ -4,7 +4,7 @@ import logging
# external imports
import pytest
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.erc20 import ERC20
from eth_erc20 import ERC20
from chainlib.eth.tx import receipt
# local imports

View File

@@ -9,7 +9,7 @@ import celery
from chainlib.connection import RPCConnection
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import receipt
from eth_accounts_index import AccountRegistry
from eth_accounts_index.registry import AccountRegistry
from hexathon import strip_0x
from chainqueue.db.enum import StatusEnum
from chainqueue.db.models.otx import Otx

View File

@@ -4,7 +4,7 @@ import logging
# external imports
import pytest
import celery
from chainlib.eth.erc20 import ERC20
from eth_erc20 import ERC20
from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import (
receipt,

View File

@@ -3,7 +3,7 @@ from chainlib.eth.nonce import RPCNonceOracle
from chainlib.eth.tx import (
receipt,
)
from eth_address_declarator import AddressDeclarator
from eth_address_declarator import Declarator
from hexathon import add_0x
# local imports
@@ -23,7 +23,7 @@ def test_translate(
nonce_oracle = RPCNonceOracle(contract_roles['CONTRACT_DEPLOYER'], eth_rpc)
c = AddressDeclarator(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle)
c = Declarator(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle)
description = 'alice'.encode('utf-8').ljust(32, b'\x00').hex()
(tx_hash_hex, o) = c.add_declaration(address_declarator, contract_roles['CONTRACT_DEPLOYER'], agent_roles['ALICE'], add_0x(description))

View File

@@ -8,7 +8,7 @@ from chainlib.eth.tx import (
count,
receipt,
)
from chainlib.eth.erc20 import ERC20
from eth_erc20 import ERC20
from chainlib.eth.nonce import RPCNonceOracle
# local imports

View File

@@ -31,7 +31,7 @@ function handleNoMergeGet(db, digest, keystore) {
doh(e);
});
}).catch((e) => {
console.error('mesage', e);
console.error('message', e);
doh(e);
});
})
@@ -46,7 +46,7 @@ function handleServerMergePost(data, db, digest, keystore, signer) {
let e = undefined;
let s = undefined;
if (v === undefined) {
s = new Syncable(digest, data);
s = new Syncable(digest, o);
s.onwrap = (e) => {
whohoo(e.toJSON());
};

View File

@@ -1,4 +1,4 @@
[AFRICASTALKING]
api_username = foo
api_key = bar
api_sender_id = baz
api_username =
api_key =
api_sender_id =

View File

@@ -9,3 +9,7 @@ class AlreadyInitializedError(Exception):
class PleaseCommitFirstError(Exception):
"""Raised when there exists uncommitted changes in the code while trying to build out the package."""
pass
class NotificationSendError(Exception):
"""Raised when a notification failed to due to some error as per the service responsible for dispatching the notification."""

View File

@@ -0,0 +1,19 @@
# standard imports
from enum import IntEnum
class AfricasTalkingStatusCodes(IntEnum):
PROCESSED = 100
SENT = 101
QUEUED = 102
RISK_HOLD = 401
INVALID_SENDER_ID = 402
INVALID_PHONE_NUMBER = 403
UNSUPPORTED_NUMBER_TYPE = 404
INSUFFICIENT_BALANCE = 405
USER_IN_BLACKLIST = 406
COULD_NOT_ROUTE = 407
INTERNAL_SERVER_ERROR = 500
GATEWAY_ERROR = 501
REJECTED_BY_GATEWAY = 502

View File

@@ -6,7 +6,8 @@ import celery
import africastalking
# local imports
from cic_notify.error import NotInitializedError, AlreadyInitializedError
from cic_notify.error import NotInitializedError, AlreadyInitializedError, NotificationSendError
from cic_notify.ext.enums import AfricasTalkingStatusCodes
logg = logging.getLogger()
celery_app = celery.current_app
@@ -50,10 +51,27 @@ class AfricasTalkingNotifier:
if self.sender_id:
response = self.api_client.send(message=message, recipients=[recipient], sender_id=self.sender_id)
logg.debug(f'Africastalking response sender-id {response}')
else:
response = self.api_client.send(message=message, recipients=[recipient])
logg.debug(f'africastalking response no-sender-id {response}')
recipients = response.get('SMSMessageData').get('Recipients')
if len(recipients) != 1:
status = response.get('SMSMessageData').get('Message')
raise NotificationSendError(f'Unexpected number of recipients: {len(recipients)}. Status: {status}')
status_code = recipients[0].get('statusCode')
status = recipients[0].get('status')
if status_code not in [
AfricasTalkingStatusCodes.PROCESSED.value,
AfricasTalkingStatusCodes.SENT.value,
AfricasTalkingStatusCodes.QUEUED.value
]:
raise NotificationSendError(f'Sending notification failed due to: {status}')
@celery_app.task
def send(message, recipient):

View File

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

View File

@@ -28,6 +28,7 @@ packages =
cic_notify
cic_notify.db
cic_notify.db.models
cic_notify.ext
cic_notify.tasks.sms
cic_notify.runnable
scripts =

View File

@@ -4,7 +4,7 @@ LOCALE_FALLBACK=en
LOCALE_PATH=/usr/src/cic-ussd/var/lib/locale/
MAX_BODY_LENGTH=1024
PASSWORD_PEPPER=QYbzKff6NhiQzY3ygl2BkiKOpER8RE/Upqs/5aZWW+I=
SERVICE_CODE=*483*46#
SERVICE_CODE=*483*46#,*483*061#,*384*96#
[phone_number]
REGION=KE

View File

@@ -0,0 +1,4 @@
[test]
gift_value = 50.00
server_url = http://localhost:63315/
token_symbol = GFT

View File

@@ -24,7 +24,7 @@ def from_wei(value: int) -> float:
"""This function converts values in Wei to a token in the cic network.
:param value: Value in Wei
:type value: int
:return: SRF equivalent of value in Wei
:return: platform's default token equivalent of value in Wei
:rtype: float
"""
value = float(value) / 1e+6
@@ -33,9 +33,9 @@ def from_wei(value: int) -> float:
def to_wei(value: int) -> int:
"""This functions converts values from a token in the cic network to Wei.
:param value: Value in SRF
:param value: Value in platform's default token
:type value: int
:return: Wei equivalent of value in SRF
:return: Wei equivalent of value in platform's default token
:rtype: int
"""
return int(value * 1e+6)
return int(value * 1e+6)

View File

@@ -238,13 +238,43 @@
"description": "Menu to display a user's entire profile",
"display_key": "ussd.kenya.display_user_metadata",
"name": "display_user_metadata",
"parent": "account_management"
"parent": "metadata_management"
},
"41": {
"description": "The recipient is not in the system",
"display_key": "ussd.kenya.exit_invalid_recipient",
"name": "exit_invalid_recipient",
"parent": null
},
"42": {
"description": "Pin entry menu for changing name data.",
"display_key": "ussd.kenya.name_edit_pin_authorization",
"name": "name_edit_pin_authorization",
"parent": "metadata_management"
},
"43": {
"description": "Pin entry menu for changing gender data.",
"display_key": "ussd.kenya.gender_edit_pin_authorization",
"name": "gender_edit_pin_authorization",
"parent": "metadata_management"
},
"44": {
"description": "Pin entry menu for changing location data.",
"display_key": "ussd.kenya.location_edit_pin_authorization",
"name": "location_edit_pin_authorization",
"parent": "metadata_management"
},
"45": {
"description": "Pin entry menu for changing products data.",
"display_key": "ussd.kenya.products_edit_pin_authorization",
"name": "products_edit_pin_authorization",
"parent": "metadata_management"
},
"46": {
"description": "Pin confirmation for pin change.",
"display_key": "ussd.kenya.new_pin_confirmation",
"name": "new_pin_confirmation",
"parent": "metadata_management"
}
}

View File

@@ -78,31 +78,30 @@ class MetadataRequestsHandler(Metadata):
:param data: The data to be stored in the metadata server.
:type data: dict|str
"""
data = json.dumps(data).encode('utf-8')
data = json.dumps(data)
result = make_request(method='POST', url=self.url, data=data, headers=self.headers)
metadata_http_error_handler(result=result)
metadata = result.content
metadata = result.json()
self.edit(data=metadata)
def edit(self, data: bytes):
def edit(self, data: Union[Dict, str]):
""" This function is responsible for editing data in the metadata server corresponding to a unique pointer.
:param data: The data to be edited in the metadata server.
:type data: bytes
:type data: dict
"""
cic_meta_signer = Signer()
signature = cic_meta_signer.sign_digest(data=data)
algorithm = cic_meta_signer.get_operational_key().get('algo')
decoded_data = data.decode('utf-8')
formatted_data = {
'm': data.decode('utf-8'),
'm': json.dumps(data),
's': {
'engine': self.engine,
'algo': algorithm,
'data': signature,
'digest': json.loads(data).get('digest'),
'digest': data.get('digest'),
}
}
formatted_data = json.dumps(formatted_data).encode('utf-8')
formatted_data = json.dumps(formatted_data)
result = make_request(method='PUT', url=self.url, data=formatted_data, headers=self.headers)
logg.info(f'signed metadata submission status: {result.status_code}.')
metadata_http_error_handler(result=result)
@@ -110,17 +109,32 @@ class MetadataRequestsHandler(Metadata):
decoded_identifier = self.identifier.decode("utf-8")
except UnicodeDecodeError:
decoded_identifier = self.identifier.hex()
logg.info(f'identifier: {decoded_identifier}. metadata pointer: {self.metadata_pointer} set to: {decoded_data}.')
logg.info(f'identifier: {decoded_identifier}. metadata pointer: {self.metadata_pointer} set to: {data}.')
def query(self):
"""This function is responsible for querying the metadata server for data corresponding to a unique pointer."""
"""
:return:
:rtype:
"""
# retrieve the metadata
result = make_request(method='GET', url=self.url)
metadata_http_error_handler(result=result)
response_data = result.content
data = json.loads(response_data.decode('utf-8'))
# json serialize retrieved data
result_data = result.json()
# validate result data format
if not isinstance(result_data, dict):
raise ValueError(f'Invalid result data object: {result_data}.')
if result.status_code == 200 and self.cic_type == ':cic.person':
# validate person metadata
person = Person()
deserialized_person = person.deserialize(person_data=json.loads(data))
data = json.dumps(deserialized_person.serialize())
cache_data(self.metadata_pointer, data=data)
person_data = person.deserialize(person_data=result_data)
# format new person data for caching
data = json.dumps(person_data.serialize())
# cache metadata
cache_data(key=self.metadata_pointer, data=data)
logg.debug(f'caching: {data} with key: {self.metadata_pointer}')

View File

@@ -47,14 +47,13 @@ class Signer:
logg.debug(f'using signing key: {key_id}, algorithm: {key_algorithm}')
return gpg_keys[0]
def sign_digest(self, data: bytes):
def sign_digest(self, data: dict):
"""
:param data:
:type data:
:return:
:rtype:
"""
data = json.loads(data)
digest = data['digest']
key_id = self.get_operational_key().get('keyid')
signature = self.gpg.sign(digest, passphrase=self.gpg_passphrase, keyid=key_id)

View File

@@ -325,6 +325,14 @@ def process_menu_interaction_requests(chain_str: str,
# get user
user = Account.session.query(Account).filter_by(phone_number=phone_number).first()
# retrieve and cache user's metadata
blockchain_address = user.blockchain_address
s_query_person_metadata = celery.signature(
'cic_ussd.tasks.metadata.query_person_metadata',
[blockchain_address]
)
s_query_person_metadata.apply_async(queue='cic-ussd')
# find any existing ussd session
existing_ussd_session = UssdSession.session.query(UssdSession).filter_by(
external_session_id=external_session_id).first()

View File

@@ -251,9 +251,9 @@ def process_display_user_metadata(user: Account, display_key: str):
identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address),
cic_type=':cic.person'
)
user_metadata = get_cached_data(key)
if user_metadata:
user_metadata = json.loads(user_metadata)
cached_metadata = get_cached_data(key)
if cached_metadata:
user_metadata = json.loads(cached_metadata)
contact_data = get_contact_data_from_vcard(vcard=user_metadata.get('vcard'))
logg.debug(f'{contact_data}')
full_name = f'{contact_data.get("given")} {contact_data.get("family")}'
@@ -371,13 +371,6 @@ def process_start_menu(display_key: str, user: Account):
# get operational balance
operational_balance = compute_operational_balance(balances=balances_data)
# retrieve and cache account's metadata
s_query_person_metadata = celery.signature(
'cic_ussd.tasks.metadata.query_person_metadata',
[blockchain_address]
)
s_query_person_metadata.apply_async(queue='cic-ussd')
# retrieve and cache account's statement
retrieve_account_statement(blockchain_address=blockchain_address)
@@ -440,7 +433,8 @@ def process_request(user_input: str, user: Account, ussd_session: Optional[dict]
'exit_invalid_pin',
'exit_invalid_new_pin',
'exit_pin_mismatch',
'exit_invalid_request'
'exit_invalid_request',
'exit_successful_transaction'
] and person_metadata is not None:
return UssdMenu.find_by_name(name='start')
else:

View File

@@ -13,7 +13,7 @@ import argparse
import logging
import urllib
from xdg.BaseDirectory import xdg_config_home
from urllib import request
from urllib import parse, request
# third-party imports
from confini import Config
@@ -92,9 +92,9 @@ def main():
data['text'] = user_input
req = urllib.request.Request(url)
data_str = json.dumps(data)
data_bytes = data_str.encode('utf-8')
req.add_header('Content-Type', 'application/json')
urlencoded_data = parse.urlencode(data)
data_bytes = urlencoded_data.encode('utf-8')
req.add_header('Content-Type', 'application/x-www-form-urlencoded')
req.data = data_bytes
response = urllib.request.urlopen(req)
response_data = response.read().decode('utf-8')

View File

@@ -4,6 +4,7 @@
# standard imports
import json
import logging
from urllib.parse import parse_qs
# third-party imports
import celery
@@ -33,8 +34,7 @@ from cic_ussd.requests import (get_request_endpoint,
from cic_ussd.runnable.server_base import exportable_parser, logg
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
from cic_ussd.state_machine import UssdStateMachine
from cic_ussd.validator import check_ip, check_request_content_length, check_service_code, validate_phone_number, \
validate_presence
from cic_ussd.validator import check_ip, check_request_content_length, validate_phone_number, validate_presence
args = exportable_parser.parse_args()
@@ -124,6 +124,9 @@ else:
raise InitializationError(f'Default token data for: {chain_str} not found.')
valid_service_codes = config.get('APP_SERVICE_CODE').split(",")
def application(env, start_response):
"""Loads python code for application to be accessible over web server
:param env: Object containing server and request information
@@ -139,13 +142,27 @@ def application(env, start_response):
if get_request_method(env=env) == 'POST' and get_request_endpoint(env=env) == '/':
# get post data
post_data = json.load(env.get('wsgi.input'))
if env.get('CONTENT_TYPE') != 'application/x-www-form-urlencoded':
start_response('405 Play by the rules', errors_headers)
return []
service_code = post_data.get('serviceCode')
phone_number = post_data.get('phoneNumber')
external_session_id = post_data.get('sessionId')
user_input = post_data.get('text')
post_data = env.get('wsgi.input').read()
post_data = post_data.decode('utf-8')
try:
post_data = parse_qs(post_data)
except TypeError:
start_response('400 Size matters', errors_headers)
return []
service_code = post_data.get('serviceCode')[0]
phone_number = post_data.get('phoneNumber')[0]
external_session_id = post_data.get('sessionId')[0]
try:
user_input = post_data.get('text')[0]
except TypeError:
user_input = ""
# add validation for phone number
if phone_number:
@@ -162,14 +179,14 @@ def application(env, start_response):
return []
# validate service code
if not check_service_code(code=service_code, config=config):
if service_code not in valid_service_codes:
response = define_multilingual_responses(
key='ussd.kenya.invalid_service_code',
locales=['en', 'sw'],
prefix='END',
valid_service_code=config.get('APP_SERVICE_CODE'))
response_bytes, headers = define_response_with_content(headers=errors_headers, response=response)
start_response('400 Invalid service code', headers)
valid_service_code=valid_service_codes[0])
response_bytes, headers = define_response_with_content(headers=headers, response=response)
start_response('200 OK', headers)
return [response_bytes]
# validate phone number
@@ -192,3 +209,8 @@ def application(env, start_response):
start_response('200 OK,', headers)
SessionBase.session.close()
return [response_bytes]
else:
start_response('405 Play by the rules', errors_headers)
return []

View File

@@ -13,7 +13,7 @@ import bcrypt
# local imports
from cic_ussd.db.models.account import AccountStatus, Account
from cic_ussd.encoder import PasswordEncoder, create_password_hash
from cic_ussd.encoder import PasswordEncoder, create_password_hash, check_password_hash
from cic_ussd.operations import persist_session_to_db_task, create_or_update_session
from cic_ussd.redis import InMemoryStore
@@ -78,9 +78,13 @@ def save_initial_pin_to_session_data(state_machine_data: Tuple[str, dict, Accoun
# set initial pin data
initial_pin = create_password_hash(user_input)
session_data = {
'initial_pin': initial_pin
}
if ussd_session.get('session_data'):
session_data = ussd_session.get('session_data')
session_data['initial_pin'] = initial_pin
else:
session_data = {
'initial_pin': initial_pin
}
# create new in memory ussd session with current ussd session data
create_or_update_session(
@@ -103,9 +107,8 @@ def pins_match(state_machine_data: Tuple[str, dict, Account]) -> bool:
"""
user_input, ussd_session, user = state_machine_data
initial_pin = ussd_session.get('session_data').get('initial_pin')
fernet = PasswordEncoder(PasswordEncoder.key)
initial_pin = fernet.decrypt(initial_pin.encode())
return bcrypt.checkpw(user_input.encode(), initial_pin)
logg.debug(f'USSD SESSION: {ussd_session}')
return check_password_hash(user_input, initial_pin)
def complete_pin_change(state_machine_data: Tuple[str, dict, Account]):

View File

@@ -12,6 +12,7 @@ from cic_ussd.chain import Chain
from cic_ussd.db.models.account import AccountStatus, Account
from cic_ussd.operations import save_to_in_memory_ussd_session_data
from cic_ussd.phone_number import get_user_by_phone_number
from cic_ussd.processor import retrieve_token_symbol
from cic_ussd.redis import create_cached_data_key, get_cached_data
from cic_ussd.transactions import OutgoingTransactionProcessor
@@ -124,14 +125,18 @@ def process_transaction_request(state_machine_data: Tuple[str, dict, Account]):
"""
user_input, ussd_session, user = state_machine_data
# retrieve token symbol
chain_str = Chain.spec.__str__()
# get user from phone number
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
recipient = get_user_by_phone_number(phone_number=recipient_phone_number)
to_address = recipient.blockchain_address
from_address = user.blockchain_address
amount = int(ussd_session.get('session_data').get('transaction_amount'))
chain_str = Chain.spec.__str__()
token_symbol = retrieve_token_symbol(chain_str=chain_str)
outgoing_tx_processor = OutgoingTransactionProcessor(chain_str=chain_str,
from_address=from_address,
to_address=to_address)
outgoing_tx_processor.process_outgoing_transfer_transaction(amount=amount)
outgoing_tx_processor.process_outgoing_transfer_transaction(amount=amount, token_symbol=token_symbol)

View File

@@ -64,13 +64,17 @@ def process_gender_user_input(user: Account, user_input: str):
if user.preferred_language == 'en':
if user_input == '1':
gender = 'Male'
else:
elif user_input == '2':
gender = 'Female'
elif user_input == '3':
gender = 'Other'
else:
if user_input == '1':
gender = 'Mwanaume'
else:
elif user_input == '2':
gender = 'Mwanamke'
elif user_input == '3':
gender = 'Nyingine'
return gender
@@ -88,14 +92,18 @@ def save_metadata_attribute_to_session_data(state_machine_data: Tuple[str, dict,
key = ''
if 'given_name' in current_state:
key = 'given_name'
elif 'family_name' in current_state:
if 'family_name' in current_state:
key = 'family_name'
elif 'gender' in current_state:
if 'gender' in current_state:
key = 'gender'
user_input = process_gender_user_input(user=user, user_input=user_input)
elif 'location' in current_state:
if 'location' in current_state:
key = 'location'
elif 'products' in current_state:
if 'products' in current_state:
key = 'products'
# check if there is existing session data
@@ -121,12 +129,20 @@ def format_user_metadata(metadata: dict, user: Account):
gender = metadata.get('gender')
given_name = metadata.get('given_name')
family_name = metadata.get('family_name')
location = {
"area_name": metadata.get('location')
}
products = []
if metadata.get('products'):
# check whether there's existing location data
if isinstance(metadata.get('location'), dict):
location = metadata.get('location')
else:
location = {
"area_name": metadata.get('location')
}
# check whether it is a list
if isinstance(metadata.get('products'), list):
products = metadata.get('products')
else:
products = metadata.get('products').split(',')
phone_number = user.phone_number
date_registered = int(user.created.replace().timestamp())
blockchain_address = user.blockchain_address
@@ -192,28 +208,27 @@ def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, Account]):
# validate user metadata
person = Person()
user_metadata = json.loads(user_metadata)
deserialized_person = person.deserialize(person_data=user_metadata)
# edit specific metadata attribute
if given_name:
deserialized_person.given_name = given_name
elif family_name:
deserialized_person.family_name = family_name
elif gender:
deserialized_person.gender = gender
elif location:
user_metadata['given_name'] = given_name
if family_name:
user_metadata['family_name'] = family_name
if gender:
user_metadata['gender'] = gender
if location:
# get existing location metadata:
location_data = user_metadata.get('location')
location_data['area_name'] = location
deserialized_person.location = location_data
elif products:
deserialized_person.products = products
user_metadata['location'] = location_data
if products:
user_metadata['products'] = products
edited_metadata = deserialized_person.serialize()
user_metadata = format_user_metadata(metadata=user_metadata, user=user)
s_edit_person_metadata = celery.signature(
'cic_ussd.tasks.metadata.edit_person_metadata',
[blockchain_address, edited_metadata]
'cic_ussd.tasks.metadata.create_person_metadata',
[blockchain_address, user_metadata]
)
s_edit_person_metadata.apply_async(queue='cic-ussd')

View File

@@ -120,7 +120,7 @@ class OutgoingTransactionProcessor:
self.from_address = from_address
self.to_address = to_address
def process_outgoing_transfer_transaction(self, amount: int, token_symbol='SRF'):
def process_outgoing_transfer_transaction(self, amount: int, token_symbol: str):
"""This function initiates standard transfers between one account to another
:param amount: The amount of tokens to be sent
:type amount: int

View File

@@ -19,4 +19,6 @@ def translation_for(key: str, preferred_language: Optional[str] = None, **kwargs
"""
if preferred_language:
i18n.set('locale', preferred_language)
else:
i18n.set('locale', i18n.config.get('fallback'))
return i18n.t(key, **kwargs)

View File

@@ -45,19 +45,6 @@ def check_request_content_length(config: Config, env: dict):
config.get('APP_MAX_BODY_LENGTH'))
def check_service_code(code: str, config: Config):
"""Checks whether provided code matches expected service code
:param config: A dictionary object containing configuration values
:type config: Config
:param code: Service code passed over request
:type code: str
:return: Service code validity
:rtype: boolean
"""
return code == config.get('APP_SERVICE_CODE')
def check_known_user(phone: str):
"""
This method attempts to ascertain whether the user already exists and is known to the system.

View File

@@ -1,4 +1,4 @@
cic_base[full_graph]~=0.1.2b2
cic-eth~=0.11.0b9
cic-notify~=0.4.0a4
cic-notify~=0.4.0a5
cic-types~=0.1.0a10

View File

@@ -6,7 +6,10 @@
"enter_new_pin",
"new_pin_confirmation",
"display_user_metadata",
"standard_pin_authorization",
"name_edit_pin_authorization",
"gender_edit_pin_authorization",
"location_edit_pin_authorization",
"products_edit_pin_authorization",
"account_balances_pin_authorization",
"account_statement_pin_authorization",
"account_balances"

View File

@@ -1,7 +1,11 @@
pytest==6.0.1
Faker==8.1.2
faker-e164==0.1.0
pytest==6.2.4
pytest-alembic==0.2.5
pytest-celery==0.0.0a1
pytest-cov==2.10.1
pytest-mock==3.3.1
pytest-ordering==0.6
pytest-redis==2.0.0
requests-mock==1.8.0
requests-mock==1.8.0
tavern==1.14.2

View File

@@ -6,6 +6,7 @@ from cic_types.pytest import *
from tests.fixtures.config import *
from tests.fixtures.db import *
from tests.fixtures.celery import *
from tests.fixtures.integration import *
from tests.fixtures.user import *
from tests.fixtures.ussd_session import *
from tests.fixtures.redis import *

View File

@@ -0,0 +1,249 @@
# standard imports
# external imports
import pytest
from faker import Faker
# local imports
# test imports
from tests.helpers.accounts import phone_number, pin_number, session_id
fake = Faker()
@pytest.fixture(scope='function')
def generate_phone_number() -> str:
return phone_number()
@pytest.fixture(scope='function')
def generate_session_id() -> str:
return session_id()
@pytest.fixture(scope='session')
def first_account_phone_number() -> str:
return phone_number()
@pytest.fixture(scope='session')
def second_account_phone_number() -> str:
return phone_number()
@pytest.fixture(scope='session')
def first_account_pin_number() -> str:
return pin_number()
@pytest.fixture(scope='session')
def second_account_pin_number() -> str:
return pin_number()
@pytest.fixture(scope='session')
def first_metadata_entry_session_id() -> str:
return session_id()
@pytest.fixture(scope='session')
def second_metadata_entry_session_id() -> str:
return session_id()
@pytest.fixture(scope='session')
def first_transaction_session_id() -> str:
return session_id()
@pytest.fixture(scope='session')
def second_transaction_session_id() -> str:
return session_id()
@pytest.fixture(scope='session')
def first_account_given_name() -> str:
return fake.first_name()
@pytest.fixture(scope='session')
def second_account_given_name() -> str:
return fake.first_name()
@pytest.fixture(scope='session')
def first_account_family_name() -> str:
return fake.last_name()
@pytest.fixture(scope='session')
def second_account_family_name() -> str:
return fake.last_name()
@pytest.fixture(scope='session')
def first_account_location() -> str:
return fake.city()
@pytest.fixture(scope='session')
def second_account_location() -> str:
return fake.city()
@pytest.fixture(scope='session')
def first_account_product() -> str:
return fake.color_name()
@pytest.fixture(scope='session')
def second_account_product() -> str:
return fake.color_name()
@pytest.fixture(scope='session')
def first_account_verify_balance_session_id() -> str:
return session_id()
@pytest.fixture(scope='session')
def second_account_verify_balance_session_id() -> str:
return session_id()
@pytest.fixture(scope='session')
def first_profile_management_session_id() -> str:
return session_id()
@pytest.fixture(scope='session')
def second_profile_management_session_id() -> str:
return session_id()
@pytest.fixture(scope='session')
def first_account_change_given_name() -> str:
return fake.first_name()
@pytest.fixture(scope='session')
def second_account_change_given_name() -> str:
return fake.first_name()
@pytest.fixture(scope='session')
def first_account_change_family_name() -> str:
return fake.last_name()
@pytest.fixture(scope='session')
def second_account_change_family_name() -> str:
return fake.last_name()
@pytest.fixture(scope='session')
def first_account_change_location() -> str:
return fake.city()
@pytest.fixture(scope='session')
def second_account_change_location() -> str:
return fake.city()
@pytest.fixture(scope='session')
def first_account_change_product() -> str:
return fake.color_name()
@pytest.fixture(scope='session')
def second_account_change_product() -> str:
return fake.color_name()
@pytest.fixture(scope='session')
def first_profile_management_session_id_1() -> str:
return session_id()
@pytest.fixture(scope='session')
def second_profile_management_session_id_1() -> str:
return session_id()
@pytest.fixture(scope='session')
def first_profile_management_session_id_2() -> str:
return session_id()
@pytest.fixture(scope='session')
def second_profile_management_session_id_2() -> str:
return session_id()
@pytest.fixture(scope='session')
def first_profile_management_session_id_3() -> str:
return session_id()
@pytest.fixture(scope='session')
def second_profile_management_session_id_3() -> str:
return session_id()
@pytest.fixture(scope='session')
def first_profile_management_session_id_4() -> str:
return session_id()
@pytest.fixture(scope='session')
def second_profile_management_session_id_4() -> str:
return session_id()
@pytest.fixture(scope='session')
def first_account_management_session_id() -> str:
return session_id()
@pytest.fixture(scope='session')
def second_account_management_session_id() -> str:
return session_id()
@pytest.fixture(scope='session')
def first_account_management_session_id_1() -> str:
return session_id()
@pytest.fixture(scope='session')
def second_account_management_session_id_1() -> str:
return session_id()
@pytest.fixture(scope='session')
def first_account_new_pin_number() -> str:
return pin_number()
@pytest.fixture(scope='session')
def second_account_new_pin_number() -> str:
return pin_number()
@pytest.fixture(scope='session')
def gift_value(load_config):
return load_config.get('TEST_GIFT_VALUE')
@pytest.fixture(scope='session')
def server_url(load_config):
return load_config.get('TEST_SERVER_URL')
@pytest.fixture(scope='session')
def token_symbol(load_config):
return load_config.get('TEST_TOKEN_SYMBOL')

View File

@@ -0,0 +1,26 @@
# standard imports
import random
import uuid
# external imports
from faker import Faker
from faker_e164.providers import E164Provider
# local imports
# test imports
fake = Faker()
fake.add_provider(E164Provider)
def phone_number() -> str:
return fake.e164('KE')
def session_id() -> str:
return uuid.uuid4().hex
def pin_number() -> int:
return random.randint(1000, 9999)

View File

@@ -0,0 +1,11 @@
import logging
logg = logging.getLogger()
logg.setLevel(logging.DEBUG)
def validate_response(response, expected_response):
"""Makes sure that the response received matches the expected response"""
logg.debug(f'RESPONSE: {response.content.decode("utf-8")}')
assert response.content.decode('utf-8') == expected_response

View File

@@ -0,0 +1,2 @@
#!/bin/bash
PYTHONPATH=. py.test --debug -vv --log-level debug -s --log-cli-level debug

View File

@@ -0,0 +1,466 @@
test_name: Test the creation of accounts through the cic_user_ussd_server entrypoint.
marks:
- usefixtures:
- gift_value
- server_url
- token_symbol
- generate_session_id
- first_account_phone_number
- second_account_phone_number
- first_account_pin_number
- second_account_pin_number
- first_account_family_name
- second_account_family_name
- first_account_given_name
- second_account_given_name
- first_account_location
- second_account_location
- first_account_product
- second_account_product
- first_metadata_entry_session_id
- second_metadata_entry_session_id
- first
stages:
- name: Initiate account creation process [first account].
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{generate_session_id}"
phoneNumber: "{first_account_phone_number}"
text: ""
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '175'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "END Your account is being created. You will receive an SMS when your account is ready.\nAkaunti yako ya Sarafu inatayarishwa. Utapokea ujumbe wa SMS akaunti yako ikiwa tayari.\n"
- name: Initiate account creation process [second account].
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{generate_session_id}"
phoneNumber: "{second_account_phone_number}"
text: ""
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '175'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "END Your account is being created. You will receive an SMS when your account is ready.\nAkaunti yako ya Sarafu inatayarishwa. Utapokea ujumbe wa SMS akaunti yako ikiwa tayari.\n"
delay_after: 5
- name: Initaite account metadata entry [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_metadata_entry_session_id}"
phoneNumber: "{first_account_phone_number}"
text: ""
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '61'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Welcome to Sarafu Network\n1. English\n2. Kiswahili\n3. Help"
- name: Initaite account metadata entry [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_metadata_entry_session_id}"
phoneNumber: "{second_account_phone_number}"
text: ""
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '61'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Welcome to Sarafu Network\n1. English\n2. Kiswahili\n3. Help"
- name: Select preferred language [English]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_metadata_entry_session_id}"
phoneNumber: "{first_account_phone_number}"
text: "1"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '64'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Please enter a new four number PIN for your account.\n0. Back"
- name: Select preferred language [Kiswahili]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_metadata_entry_session_id}"
phoneNumber: "{second_account_phone_number}"
text: "2"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '71'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Tafadhali weka pin mpya yenye nambari nne kwa akaunti yako\n0. Nyuma"
- name: Enter pin number [{first_account_pin_number} - first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_metadata_entry_session_id}"
phoneNumber: "{first_account_phone_number}"
text: "1*{first_account_pin_number}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '44'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Enter your four number PIN again\n0. Back"
- name: Enter pin number [second_account_pin_number - second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_metadata_entry_session_id}"
phoneNumber: "{second_account_phone_number}"
text: "2*{second_account_pin_number}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '31'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Weka PIN yako tena\n0. Nyuma"
- name: Pin number confirmation [first_account_pin_number - first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_metadata_entry_session_id}"
phoneNumber: "{first_account_phone_number}"
text: "1*{first_account_pin_number}*{first_account_pin_number}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '28'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Enter first name\n0. Back"
- name: Pin number confirmation [{second_account_pin_number} - second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_metadata_entry_session_id}"
phoneNumber: "{second_account_phone_number}"
text: "2*{second_account_pin_number}*{second_account_pin_number}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '37'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Weka jina lako la kwanza\n0. Nyuma"
- name: Enter first name [first_account_given_name - first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_metadata_entry_session_id}"
phoneNumber: "{first_account_phone_number}"
text: "1*{first_account_pin_number}*{first_account_pin_number}*{first_account_given_name}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '29'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Enter family name\n0. Back"
- name: Enter first name [second_account_given_name - second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_metadata_entry_session_id}"
phoneNumber: "{second_account_phone_number}"
text: "2*{second_account_pin_number}*{second_account_pin_number}*{second_account_given_name}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '37'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Weka jina lako la mwisho\n0. Nyuma"
- name: Enter last name [first_account_family_name - first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_metadata_entry_session_id}"
phoneNumber: "{first_account_phone_number}"
text: "1*{first_account_pin_number}*{first_account_pin_number}*{first_account_given_name}*{first_account_family_name}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '51'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Enter gender\n1. Male\n2. Female\n3. Other\n0. Back"
- name: Enter last name [second_account_family_name - second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_metadata_entry_session_id}"
phoneNumber: "{second_account_phone_number}"
text: "2*{second_account_pin_number}*{second_account_pin_number}*{second_account_given_name}*{second_account_family_name}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '64'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Weka jinsia yako\n1. Mwanaume\n2. Mwanamke\n3. Nyngine\n0. Nyuma"
- name: Select gender [Male - first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_metadata_entry_session_id}"
phoneNumber: "{first_account_phone_number}"
text: "1*{first_account_pin_number}*{first_account_pin_number}*{first_account_given_name}*{first_account_family_name}*1"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '31'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Enter your location\n0. Back"
- name: Select gender [Female - second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_metadata_entry_session_id}"
phoneNumber: "{second_account_phone_number}"
text: "2*{second_account_pin_number}*{second_account_pin_number}*{second_account_given_name}*{second_account_family_name}*2"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '27'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Weka eneo lako\n0. Nyuma"
- name: Enter location [first_account_location - first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_metadata_entry_session_id}"
phoneNumber: "{first_account_phone_number}"
text: "1*{first_account_pin_number}*{first_account_pin_number}*{first_account_given_name}*{first_account_family_name}*1*{first_account_location}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '55'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Please enter a product or service you offer\n0. Back"
- name: Enter location [second_account_location - second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_metadata_entry_session_id}"
phoneNumber: "{second_account_phone_number}"
text: "2*{second_account_pin_number}*{second_account_pin_number}*{second_account_given_name}*{second_account_family_name}*2*{second_account_location}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '42'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Weka bidhaa ama huduma unauza\n0. Nyuma"
- name: Enter product [first_account_product - first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_metadata_entry_session_id}"
phoneNumber: "{first_account_phone_number}"
text: "1*{first_account_pin_number}*{first_account_pin_number}*{first_account_given_name}*{first_account_family_name}*1*{first_account_location}*{first_account_product}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '51'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Balance {gift_value} {token_symbol}\n1. Send\n2. My Account\n3. Help"
delay_before: 10
- name: Enter product [second_account_product - second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_metadata_entry_session_id}"
phoneNumber: "{second_account_phone_number}"
text: "2*{second_account_pin_number}*{second_account_pin_number}*{second_account_given_name}*{second_account_family_name}*2*{second_account_location}*{second_account_product}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '56'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Salio {gift_value} {token_symbol}\n1. Tuma\n2. Akaunti yangu\n3. Usaidizi"
delay_before: 10

View File

@@ -0,0 +1,587 @@
test_name: Test performing account management operations.
marks:
- usefixtures:
- server_url
- token_symbol
- first_account_pin_number
- second_account_pin_number
- first_account_phone_number
- second_account_phone_number
- first_account_management_session_id
- second_account_management_session_id
- first_account_management_session_id_1
- second_account_management_session_id_1
- first_account_new_pin_number
- second_account_new_pin_number
- fourth
stages:
- name: Account management start menu [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_account_management_session_id}"
phoneNumber: "{first_account_phone_number}"
text: ""
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '51'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Balance 58.00 {token_symbol}\n1. Send\n2. My Account\n3. Help"
- name: Account management start menu [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_account_management_session_id}"
phoneNumber: "{second_account_phone_number}"
text: ""
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '56'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Salio 42.00 {token_symbol}\n1. Tuma\n2. Akaunti yangu\n3. Usaidizi"
- name: Account management menu [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_account_management_session_id}"
phoneNumber: "{first_account_phone_number}"
text: "2"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '105'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON My account\n1. My profile\n2. Change language\n3. Check balance\n4. Check statement\n5. Change PIN\n0. Back"
- name: Account management menu [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_account_management_session_id}"
phoneNumber: "{second_account_phone_number}"
text: "2"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '148'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Akaunti yangu\n1. Wasifu wangu\n2. Chagua lugha utakayotumia\n3. Angalia salio\n4. Angalia taarifa ya matumizi\n5. Badilisha nambari ya siri\n0. Nyuma"
- name: Language change [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_account_management_session_id}"
phoneNumber: "{first_account_phone_number}"
text: "2*2"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '51'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Choose language\n1. English\n2. Kiswahili\n0. Back"
- name: Language change [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_account_management_session_id}"
phoneNumber: "{second_account_phone_number}"
text: "2*2"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '51'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Chagua lugha\n1. Kingereza\n2. Kiswahili\n0. Nyuma"
- name: Select language [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_account_management_session_id}"
phoneNumber: "{first_account_phone_number}"
text: "2*2*2"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '30'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "END Asante kwa kutumia huduma."
- name: Select language [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_account_management_session_id}"
phoneNumber: "{second_account_phone_number}"
text: "2*2*1"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '36'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "END Thank you for using the service."
- name: Second account management start menu [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_account_management_session_id_1}"
phoneNumber: "{first_account_phone_number}"
text: ""
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '56'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Salio 58.00 {token_symbol}\n1. Tuma\n2. Akaunti yangu\n3. Usaidizi"
- name: Second account management start menu [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_account_management_session_id_1}"
phoneNumber: "{second_account_phone_number}"
text: ""
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '51'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Balance 42.00 {token_symbol}\n1. Send\n2. My Account\n3. Help"
- name: Second account management menu [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_account_management_session_id_1}"
phoneNumber: "{first_account_phone_number}"
text: "2"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '148'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Akaunti yangu\n1. Wasifu wangu\n2. Chagua lugha utakayotumia\n3. Angalia salio\n4. Angalia taarifa ya matumizi\n5. Badilisha nambari ya siri\n0. Nyuma"
- name: Second account management menu [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_account_management_session_id_1}"
phoneNumber: "{second_account_phone_number}"
text: "2"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '105'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON My account\n1. My profile\n2. Change language\n3. Check balance\n4. Check statement\n5. Change PIN\n0. Back"
- name: Check balance [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_account_management_session_id_1}"
phoneNumber: "{first_account_phone_number}"
text: "2*3"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '49'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Tafadhali weka PIN yako kuona salio.\n0. Nyuma"
- name: Check balance [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_account_management_session_id_1}"
phoneNumber: "{second_account_phone_number}"
text: "2*3"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '50'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Please enter your PIN to view balances\n0. Back"
- name: Display balances [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_account_management_session_id_1}"
phoneNumber: "{first_account_phone_number}"
text: "2*3*{first_account_pin_number}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Salio zako ni zifuatazo:\n salio: 58.00 {token_symbol}\n ushuru: {token_symbol}\n tuzo: {token_symbol}\n0. Nyuma"
- name: Display balances [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_account_management_session_id_1}"
phoneNumber: "{second_account_phone_number}"
text: "2*3*{second_account_pin_number}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Your balances are as follows:\n balance: 42.00 {token_symbol}\n fees: {token_symbol}\n rewards: {token_symbol}\n0. Back"
- name: Resume account management menu [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_account_management_session_id_1}"
phoneNumber: "{first_account_phone_number}"
text: "2*3*{first_account_pin_number}*0"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '148'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Akaunti yangu\n1. Wasifu wangu\n2. Chagua lugha utakayotumia\n3. Angalia salio\n4. Angalia taarifa ya matumizi\n5. Badilisha nambari ya siri\n0. Nyuma"
- name: Resume account management menu [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_account_management_session_id_1}"
phoneNumber: "{second_account_phone_number}"
text: "2*3*{second_account_pin_number}*0"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '105'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON My account\n1. My profile\n2. Change language\n3. Check balance\n4. Check statement\n5. Change PIN\n0. Back"
- name: Change pin number [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_account_management_session_id_1}"
phoneNumber: "{first_account_phone_number}"
text: "2*3*{first_account_pin_number}*0*5"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '34'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Weka nambari ya siri.\n0. Nyuma"
- name: Change pin number [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_account_management_session_id_1}"
phoneNumber: "{second_account_phone_number}"
text: "2*3*{second_account_pin_number}*0*5"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '30'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Enter current PIN.\n0. Back"
- name: Enter old pin [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_account_management_session_id_1}"
phoneNumber: "{first_account_phone_number}"
text: "2*3*{first_account_pin_number}*0*5*{first_account_pin_number}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '38'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Weka nambari ya siri mpya\n0. Nyuma"
- name: Enter old pin [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_account_management_session_id_1}"
phoneNumber: "{second_account_phone_number}"
text: "2*3*{second_account_pin_number}*0*5*{second_account_pin_number}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '42'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Enter your new four number PIN\n0. Back"
- name: Enter new pin [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_account_management_session_id_1}"
phoneNumber: "{first_account_phone_number}"
text: "2*3*{first_account_pin_number}*0*5*{first_account_pin_number}*{first_account_new_pin_number}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '31'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Weka PIN yako tena\n0. Nyuma"
- name: Enter new pin [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_account_management_session_id_1}"
phoneNumber: "{second_account_phone_number}"
text: "2*3*{second_account_pin_number}*0*5*{second_account_pin_number}*{second_account_new_pin_number}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '48'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Enter your new four number PIN again\n0. Back"
- name: Enter new pin confirmation [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_account_management_session_id_1}"
phoneNumber: "{first_account_phone_number}"
text: "2*3*{first_account_pin_number}*0*5*{first_account_pin_number}*{first_account_new_pin_number}*{first_account_new_pin_number}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '91'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Ombi lako limetumwa. Utapokea uthibitishaji wa SMS kwa muda mfupi.\n00. Nyuma\n99. Ondoka"
- name: Enter new pin confirmation [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_account_management_session_id_1}"
phoneNumber: "{second_account_phone_number}"
text: "2*3*{second_account_pin_number}*0*5*{second_account_pin_number}*{second_account_new_pin_number}*{second_account_new_pin_number}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '82'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Your request has been sent. You will receive an SMS shortly.\n00. Back\n99. Exit"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,282 @@
test_name: Test that the two test accounts can trade with each other.
marks:
- usefixtures:
- gift_value
- server_url
- token_symbol
- first_account_family_name
- second_account_family_name
- first_account_given_name
- second_account_given_name
- first_account_phone_number
- second_account_phone_number
- first_account_pin_number
- second_account_pin_number
- first_transaction_session_id
- second_transaction_session_id
- first_account_verify_balance_session_id
- second_account_verify_balance_session_id
- second
stages:
- name: Transactions start menu [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_transaction_session_id}"
phoneNumber: "{first_account_phone_number}"
text: ""
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '51'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Balance {gift_value} {token_symbol}\n1. Send\n2. My Account\n3. Help"
- name: Transactions start menu [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_transaction_session_id}"
phoneNumber: "{second_account_phone_number}"
text: ""
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '56'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Salio {gift_value} {token_symbol}\n1. Tuma\n2. Akaunti yangu\n3. Usaidizi"
- name: Initate transcation attempt [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_transaction_session_id}"
phoneNumber: "{first_account_phone_number}"
text: "1"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '30'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Enter phone number\n0. Back"
- name: Initate transcation attempt [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_transaction_session_id}"
phoneNumber: "{second_account_phone_number}"
text: "1"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '33'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Weka nambari ya simu\n0. Nyuma"
- name: Enter phone number [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_transaction_session_id}"
phoneNumber: "{first_account_phone_number}"
text: "1*{second_account_phone_number}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '24'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Enter amount\n0. Back"
- name: Enter phone number [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_transaction_session_id}"
phoneNumber: "{second_account_phone_number}"
text: "1*{first_account_phone_number}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '25'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Weka kiwango\n0. Nyuma"
- name: Enter transcation amount [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_transaction_session_id}"
phoneNumber: "{first_account_phone_number}"
text: "1*{second_account_phone_number}*17"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON {second_account_given_name} {second_account_family_name} {second_account_phone_number} will receive 17.00 {token_symbol} from {first_account_given_name} {first_account_family_name} {first_account_phone_number}.\nPlease enter your PIN to confirm.\n0. Back"
- name: Enter transcation amount [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_transaction_session_id}"
phoneNumber: "{second_account_phone_number}"
text: "1*{first_account_phone_number}*25"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON {first_account_given_name} {first_account_family_name} {first_account_phone_number} atapokea 25.00 {token_symbol} kutoka kwa {second_account_given_name} {second_account_family_name} {second_account_phone_number}.\nTafadhali weka nambari yako ya siri kudhibitisha.\n0. Nyuma"
- name: Pin to authorize transaction [first account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_transaction_session_id}"
phoneNumber: "{first_account_phone_number}"
text: "1*{second_account_phone_number}*17*{first_account_pin_number}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Your request has been sent. {second_account_given_name} {second_account_family_name} {second_account_phone_number} will receive 17.00 {token_symbol} from {first_account_given_name} {first_account_family_name} {first_account_phone_number}.\n00. Back\n99. Exit"
- name: Pin to authorize transaction [second account]
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_transaction_session_id}"
phoneNumber: "{second_account_phone_number}"
text: "1*{first_account_phone_number}*25*{second_account_pin_number}"
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Ombi lako limetumwa. {first_account_given_name} {first_account_family_name} {first_account_phone_number} atapokea 25.00 {token_symbol} kutoka kwa {second_account_given_name} {second_account_family_name} {second_account_phone_number}.\n00. Nyuma\n99. Ondoka"
- name: Verify balance changes [first account]
delay_before: 10
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{first_account_verify_balance_session_id}"
phoneNumber: "{first_account_phone_number}"
text: ""
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '51'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Balance 58.00 {token_symbol}\n1. Send\n2. My Account\n3. Help"
- name: Verify balance changes [second account]
delay_before: 10
request:
url: "{server_url}"
data:
serviceCode: "*483*46#"
sessionId: "{second_account_verify_balance_session_id}"
phoneNumber: "{second_account_phone_number}"
text: ""
headers:
content-type: "application/x-www-form-urlencoded"
method: POST
response:
status_code:
- 200
headers:
Content-Length: '56'
Content-Type: "text/plain"
verify_response_with:
function: ext.validator:validate_response
extra_kwargs:
expected_response: "CON Salio 42.00 {token_symbol}\n1. Tuma\n2. Akaunti yangu\n3. Usaidizi"

View File

@@ -4,12 +4,13 @@
"source": "enter_gender",
"dest": "enter_location",
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
"conditions": "cic_ussd.state_machine.logic.validator.is_valid_gender_selection"
"conditions": "cic_ussd.state_machine.logic.validator.is_valid_gender_selection",
"unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
},
{
"trigger": "scan_data",
"source": "enter_gender",
"dest": "standard_pin_authorization",
"dest": "gender_edit_pin_authorization",
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
"conditions": [
"cic_ussd.state_machine.logic.validator.has_cached_user_metadata",
@@ -18,15 +19,14 @@
},
{
"trigger": "scan_data",
"source": "standard_pin_authorization",
"source": "gender_edit_pin_authorization",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
"after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute",
"unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
"after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute"
},
{
"trigger": "scan_data",
"source": "standard_pin_authorization",
"source": "gender_edit_pin_authorization",
"dest": "exit_pin_blocked",
"conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
},

View File

@@ -3,26 +3,26 @@
"trigger": "scan_data",
"source": "enter_location",
"dest": "enter_products",
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data"
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
"unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
},
{
"trigger": "scan_data",
"source": "enter_location",
"dest": "standard_pin_authorization",
"dest": "location_edit_pin_authorization",
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
"conditions": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
},
{
"trigger": "scan_data",
"source": "standard_pin_authorization",
"source": "location_edit_pin_authorization",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
"after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute",
"unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
"after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute"
},
{
"trigger": "scan_data",
"source": "standard_pin_authorization",
"source": "location_edit_pin_authorization",
"dest": "exit_pin_blocked",
"conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
}

View File

@@ -7,49 +7,28 @@
},
{
"trigger": "scan_data",
"source": "enter_given_name",
"dest": "standard_pin_authorization",
"source": "enter_family_name",
"dest": "name_edit_pin_authorization",
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
"conditions": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
},
{
"trigger": "scan_data",
"source": "standard_pin_authorization",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
"after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute",
"unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
},
{
"trigger": "scan_data",
"source": "standard_pin_authorization",
"dest": "exit_pin_blocked",
"conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
},
{
"trigger": "scan_data",
"source": "enter_family_name",
"dest": "enter_gender",
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data"
},
{
"trigger": "scan_data",
"source": "enter_family_name",
"dest": "standard_pin_authorization",
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
"conditions": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
},
{
"trigger": "scan_data",
"source": "standard_pin_authorization",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
"after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute",
"unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
},
{
"trigger": "scan_data",
"source": "standard_pin_authorization",
"source": "name_edit_pin_authorization",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
"after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute"
},
{
"trigger": "scan_data",
"source": "name_edit_pin_authorization",
"dest": "exit_pin_blocked",
"conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
}

View File

@@ -9,14 +9,14 @@
"trigger": "scan_data",
"source": "enter_current_pin",
"dest": "exit_pin_blocked",
"conditions": "cic_ussd.state_machine.logic.menu.is_blocked_pin"
"conditions": "cic_ussd.state_machine.logic.pin.is_blocked_pin"
},
{
"trigger": "scan_data",
"source": "enter_new_pin",
"dest": "new_pin_confirmation",
"after": "cic_ussd.state_machine.logic.pin.save_initial_pin_to_session_data",
"conditions": "cic_ussd.state_machine.logic.menu.is_valid_new_pin"
"conditions": "cic_ussd.state_machine.logic.pin.is_valid_new_pin"
},
{
"trigger": "scan_data",
@@ -28,7 +28,7 @@
"source": "new_pin_confirmation",
"dest": "complete",
"conditions": "cic_ussd.state_machine.logic.pin.pins_match",
"after": "cic_ussd.state_machine.logic.menu.complete_pin_change"
"after": "cic_ussd.state_machine.logic.pin.complete_pin_change"
},
{
"trigger": "scan_data",

View File

@@ -2,7 +2,7 @@
{
"trigger": "scan_data",
"source": "enter_products",
"dest": "standard_pin_authorization",
"dest": "products_edit_pin_authorization",
"conditions": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata",
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data"
},
@@ -13,18 +13,19 @@
"after": [
"cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
"cic_ussd.state_machine.logic.user.save_complete_user_metadata"
]
],
"unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
},
{
"trigger": "scan_data",
"source": "standard_pin_authorization",
"source": "products_edit_pin_authorization",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
"after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute"
},
{
"trigger": "scan_data",
"source": "standard_pin_authorization",
"source": "products_edit_pin_authorization",
"dest": "exit_pin_blocked",
"conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
}

View File

@@ -8,7 +8,7 @@
{
"trigger": "scan_data",
"source": "metadata_management",
"dest": "enter_age",
"dest": "enter_gender",
"conditions": "cic_ussd.state_machine.logic.menu.menu_two_selected"
},
{

View File

@@ -1,7 +1,7 @@
en:
account_successfully_created: |-
Hello, you have been registered on Sarafu Network! Your balance is %{balance} %{token_symbol}. To use dial *483*46#. For help 0757628885.
You have been registered on Sarafu Network! To use dial *384*96# on Safaricom and *483*96# on other networks. For help %{support_phone}.
received_tokens: |-
Successfully received %{amount} %{token_symbol} from %{tx_sender_information} %{timestamp}. New balance is %{balance} %{token_symbol}.
terms: |-
By using the service, you agree to the terms and conditions at https://www.grassrootseconomics.org/terms-and-conditions.
By using the service, you agree to the terms and conditions at http://grassecon.org/tos

View File

@@ -1,7 +1,7 @@
sw:
account_successfully_created: |-
Habari, umesajiliwa kwa huduma ya sarafu! Salio lako ni %{token_symbol} %{balance}. Kutumia bonyeza *483*46#. Kwa Usaidizi 0757628885.
Umesajiliwa kwa huduma ya Sarafu! Kutumia bonyeza *384*96# Safaricom ama *483*46# kwa utandao tofauti. Kwa Usaidizi %{support_phone}.
received_tokens: |-
Umepokea %{amount} %{token_symbol} kutoka kwa %{tx_sender_information} %{timestamp}. Salio la %{token_symbol} ni %{balance}.
terms: |-
Kwa kutumia hii huduma, umekubali sheria na masharti yafuatayo https://www.grassrootseconomics.org/terms-and-conditions.
Kwa kutumia hii huduma, umekubali sheria na masharti yafuatayo http://grassecon.org/tos

View File

@@ -1,29 +1,30 @@
en:
kenya:
initial_language_selection: |-
CON Welcome to Sarafu
CON Welcome to Sarafu Network
1. English
2. Kiswahili
3. Help
initial_pin_entry: |-
CON Please enter a PIN to manage your account.
CON Please enter a new four number PIN for your account.
0. Back
initial_pin_confirmation: |-
CON Enter your PIN again
CON Enter your four number PIN again
0. Back
enter_given_name: |-
CON Enter first name
0. Back
enter_family_name: |-
CON Enter last name
CON Enter family name
0. Back
enter_gender: |-
CON Enter gender
1. Male
2. Female
3. Other
0. Back
enter_location: |-
CON Enter location
CON Enter your location
0. Back
enter_products: |-
CON Please enter a product or service you offer
@@ -75,7 +76,10 @@ en:
CON Enter current PIN. You have %{remaining_attempts} attempts remaining.
0. Back
enter_new_pin: |-
CON Enter new PIN again
CON Enter your new four number PIN
0. Back
new_pin_confirmation: |-
CON Enter your new four number PIN again
0. Back
transaction_pin_authorization:
first: |-
@@ -83,34 +87,62 @@ en:
Please enter your PIN to confirm.
0. Back
retry: |-
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining
0. Back
display_metadata_pin_authorization:
first: |-
CON Please enter your PIN.
CON Please enter your PIN
0. Back
retry: |-
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining
0. Back
account_balances_pin_authorization:
first: |-
CON Please enter your PIN to view balances.
CON Please enter your PIN to view balances
0. Back
retry: |-
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining
0. Back
account_statement_pin_authorization:
first: |-
CON Please enter your PIN to view statement.
CON Please enter your PIN to view statement
0. Back
retry: |-
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining
0. Back
name_edit_pin_authorization:
first: |-
CON Please enter your PIN
0. Back
retry: |-
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining
0. Back
gender_edit_pin_authorization:
first: |-
CON Please enter your PIN
0. Back
retry: |-
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining
0. Back
location_edit_pin_authorization:
first: |-
CON Please enter your PIN
0. Back
retry: |-
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining
0. Back
products_edit_pin_authorization:
first: |-
CON Please enter your PIN
0. Back
retry: |-
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining
0. Back
account_balances: |-
CON Your balances are as follows:
balance: %{operational_balance} %{token_symbol}
taxes: %{tax} %{token_symbol}
bonsuses: %{bonus} %{token_symbol}
fees: %{tax} %{token_symbol}
rewards: %{bonus} %{token_symbol}
0. Back
first_transaction_set: |-
CON %{first_transaction_set}
@@ -140,9 +172,9 @@ en:
exit_pin_blocked: |-
END Your PIN has been blocked. For help, please call %{support_phone}.
exit_invalid_pin: |-
END The PIN you have entered is Invalid. PIN must consist of 4 digits. For help, call %{support_phone}.
END The PIN you have entered is invalid. PIN must consist of 4 digits. For help, call %{support_phone}.
exit_invalid_new_pin: |-
END The PIN you have entered is Invalid. PIN must be different from your current PIN. For help, call %{support_phone}.
END The PIN you have entered is invalid. PIN must be different from your current PIN. For help, call %{support_phone}.
exit_pin_mismatch: |-
END The new PIN does not match the one you entered. Please try again. For help, call %{support_phone}.
exit_invalid_recipient: |-
@@ -158,6 +190,8 @@ en:
Your Sarafu-Network balances is: %{token_balance}
00. Back
99. Exit
invalid_service_code: |-
Please dial %{valid_service_code} to access Sarafu Network
help: |-
CON For assistance call %{support_phone}
00. Back
@@ -167,4 +201,4 @@ en:
00. Back
99. Exit
account_creation_prompt: |-
Your account is being created. You will receive an SMS when your account is ready.
Your account is being created. You will receive an SMS when your account is ready.

View File

@@ -1,12 +1,12 @@
sw:
kenya:
initial_language_selection: |-
CON Welcome to Sarafu
CON Karibu Sarafu Network
1. English
2. Kiswahili
3. Help
initial_pin_entry: |-
CON Tafadhali weka PIN ili kudhibiti akaunti yako.
CON Tafadhali weka pin mpya yenye nambari nne kwa akaunti yako
0. Nyuma
initial_pin_confirmation: |-
CON Weka PIN yako tena
@@ -21,12 +21,13 @@ sw:
CON Weka jinsia yako
1. Mwanaume
2. Mwanamke
3. Nyngine
0. Nyuma
enter_location: |-
CON Weka eneo lako
0. Nyuma
enter_products: |-
CON Tafadhali weka bidhaa ama huduma unauza
CON Weka bidhaa ama huduma unauza
0. Nyuma
start: |-
CON Salio %{account_balance} %{account_token_name}
@@ -60,7 +61,7 @@ sw:
Jina: %{full_name}
Jinsia: %{gender}
Eneo: %{location}
Unauza: %{user_bio}
Unauza: %{products}
0. Nyuma
select_preferred_language: |-
CON Chagua lugha
@@ -77,6 +78,9 @@ sw:
enter_new_pin: |-
CON Weka nambari ya siri mpya
0. Nyuma
new_pin_confirmation: |-
CON Weka PIN yako tena
0. Nyuma
transaction_pin_authorization:
first: |-
CON %{recipient_information} atapokea %{transaction_amount} %{token_symbol} kutoka kwa %{sender_information}.
@@ -85,9 +89,9 @@ sw:
retry: |-
CON Weka nambari ya siri. Una majaribio %{remaining_attempts} yaliyobaki.
0. Nyuma
standard_pin_authorization:
display_metadata_pin_authorization:
first: |-
CON Tafadhali weka PIN yako.
CON Tafadhali weka PIN yako
0. Nyuma
retry: |-
CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
@@ -106,12 +110,40 @@ sw:
retry: |-
CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
0. Nyuma
name_edit_pin_authorization:
first: |-
CON Tafadhali weka PIN yako
0. Nyuma
retry: |-
CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
0. Nyuma
gender_edit_pin_authorization:
first: |-
CON Tafadhali weka PIN yako
0. Nyuma
retry: |-
CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
0. Nyuma
location_edit_pin_authorization:
first: |-
CON Tafadhali weka PIN yako
0. Nyuma
retry: |-
CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
0. Nyuma
products_edit_pin_authorization:
first: |-
CON Tafadhali weka PIN yako
0. Nyuma
retry: |-
CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
0. Nyuma
account_balances: |-
CON Salio zako ni zifuatazo:
salio: %{operational_balance}
ushuru: %{tax}
tuzo: %{bonus}
0. Back
salio: %{operational_balance} %{token_symbol}
ushuru: %{tax} %{token_symbol}
tuzo: %{bonus} %{token_symbol}
0. Nyuma
first_transaction_set: |-
CON %{first_transaction_set}
1. Mbele
@@ -155,9 +187,11 @@ sw:
99. Ondoka
exit_insufficient_balance: |-
CON Malipo ya %{amount} %{token_symbol} kwa %{recipient_information} halijakamilika kwa sababu salio lako haitoshi.
Akaunti yako ya Sarafu-Network ina salio ifuatayo: %{token_balance}
Akaunti yako ya Sarafu ina salio ifuatayo: %{token_balance}
00. Nyuma
99. Ondoka
invalid_service_code: |-
Bonyeza %{valid_service_code} kutumia mtandao wa Sarafu
help: |-
CON Kwa usaidizi piga simu %{support_phone}
0. Nyuma
@@ -167,4 +201,4 @@ sw:
00. Nyuma
99. Ondoka
account_creation_prompt: |-
Akaunti yako ya Sarafu inatayarishwa. Utapokea ujumbe wa SMS akaunti yako ikiwa tayari.
Akaunti yako ya Sarafu inatayarishwa. Utapokea ujumbe wa SMS akaunti yako ikiwa tayari.

Some files were not shown because too many files have changed in this diff Show More