Compare commits

..

4 Commits

Author SHA1 Message Date
def5cf0a1f docs: add prereqs, scope and issues section 2022-01-03 17:51:53 +03:00
4304ca8603 fix: (docs) link to cic software 2022-01-03 10:50:18 +03:00
635bab6896 docs: update code of conduct
* mirror what we have on our docs webiste
2022-01-03 10:49:51 +03:00
0b5363cd99 update: (docs) contributor guidelines and docs ref 2021-12-24 14:12:08 +03:00
117 changed files with 1490 additions and 2772 deletions

View File

@@ -1,4 +1,6 @@
# Contributor Covenant Code of Conduct
# Code of Conduct
By contributing toward this project you agree to the following code of conduct
## Our Pledge
@@ -14,21 +16,21 @@ appearance, race, religion, or sexual identity and orientation.
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
@@ -55,7 +57,7 @@ a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
reported by contacting the project team at info@grassecon.org . All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
@@ -75,9 +77,8 @@ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.ht
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
...Try to keep in mind the immortal
words of Bill and Ted, "Be excellent to each other."
These documents are licensed under Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) https://creativecommons.org/licenses/by-sa/4.0/
And further any software Grassrotos Economics supports is under licenced under GNU GENERAL PUBLIC LICENSE Version 3 https://www.gnu.org/licenses/

View File

@@ -1,16 +1,66 @@
Hello and welcome to the CIC Stack repository. Targeted for use with the ethereum virtual machine and a ussd capable telecom provider.
# Contribution Guidelines
__To request a change to the code please fork this repository and sumbit a merge request.__
**Karibu sana!** Hello and welcome to the Grassroots Economics [project](https://gitlab.com/grassrootseconomics). This guide will help you understand the overall
organization of the CIC project to get you started with contributing to the project. You'll be able to pick up issues, write code to fix them, and get your work reviewed and merged.
__If there is a Grassroots Economics Kanban Issue please include that in our MR it will help us track contributions. Karibu sana!__
## Table of Contents
__Visit the Development Kanban board here: https://gitlab.com/grassrootseconomics/cic-internal-integration/-/boards/2419764__
- [Prerequisites](#Prerequisites)
- [Scope of contributions](#Scope-of-contributions)
- [Issues](#Issues)
- [Bugs](#Bugs)
- [Good first issues](#Good-first-issues)
- [Git guidelines](#Git-guidelines)
- [Merge request processs](#Merge-request-processs)
- [Environment setup](#Environment-setup)
- [Code style](#Code-style)
- [Building-and-testing](#Building-and-testing)
__Ask a question in our dev chat:__
## Prerequisites
[Mattermost](https://chat.grassrootseconomics.net/cic/channels/dev)
Before contributing to the CIC Stack, ensure you have:
[Discord](https://discord.gg/XWunwAsX)
- Read and accepted to abide by the Code of Conduct
<!-- TODO: Add link to CoC -->
- Read and accepted the Developers Certificate of Origin
<!-- TODO: Add link to DCO -->
[Matrix, IRC soon?]
## Scope of contributions
We generally welcome all sorts or contributions. No contribution is too small. We gladly accept contributions such as:
- Documentation improvements including inline documentation or even minor typos
- Answering questions in issue discussions and pull requests
- Fixing known bugs on our issue tracker
- Reporting unknown bugs
<!-- TODO: Add resposible disclosure guidelines -->
- Securty issues through responsible disclosure
## Issues
Use the existing issue template and issue tags to correctly catagorize and issue when creating issues. Make sure to search for duplicated before creating a new issue.
### Bugs
If a bug is encountered and you can reproduce it:
- Define its priority with an issue tag
- Describe the steps to reproduce it while providing the version of the bugged service/library and your environment setup
### Good first issues
These are pre-tagged issues which are friendly (low barrier to entry and clearly defined tasks) for new contributors to get aquinted to the project.
## Git guideleines
<!-- TODO: Review after finalzing branching or forking model -->
## Merge request process
<!-- TODO: Review after core_contrib -->
## Environment setup
## Code style
## Building and testing

View File

@@ -1,117 +0,0 @@
# CORE TEAM CONTRIBUTION GUIDE
# 1. Transparency
1.1 Use work logs for reflection of work done, aswell as telling your peers about changes that may affect their own tasks
1.2 A work log SHOULD be submitted after a "unit of work" is complete.
1.2.1 A "unit of work" should not span more than one full day's worth of work.
1.2.2 A "unit of work" should be small enough that the log entries give useful insight.
1.3 Individual logs are reviewed in weekly meetings
<!--1.4 Bullet point list of topics and one or more sub-points describing each item in short sentences, eg;
```
- Core
* fixed foo
* fixed bar
- Frontend
* connected bar to baz
```-->
1.4 Work log format is defined in []()
1.5 Link to issue/MR in bullet point where appropriate
1.6
# 2. Code hygiene
2.1 Keep function names and variable names short
2.2 Keep code files, functions and test fixtures short
2.3 The less magic the better. Recombinable and replaceable is king
2.4 Group imports by `standard`, `external`, `local`, `test` - in that order
2.5 Only auto-import when necessary, and always with a minimum of side-effects
2.6 Use custom errors. Let them bubble up
2.7 No logs in tight loops
2.8 Keep executable main routine minimal. Pass variables (do not use globals) in main business logic function
2.9 Test coverage MUST be kept higher than 90% after changes
2.10 Docstrings. Always. Always!
# 3. Versioning
3.1 Use [Semantic Versioning](https://semver.org/)
3.2 When merging code, explicit dependencies SHOULD NOT use pre-release version
# 4. Issues
4.1 Issue title should use [Convention Commit structure](https://www.conventionalcommits.org/en/v1.0.0-beta.2/)
4.2 Issues need proper problem statement
4.2.1. What is the current state
4.2.2. If current state is not behaving as expected, what was the expected state
4.2.3. What is the desired new state.
4.3 Issues need proper resolution statement
4.3.1. Bullet point list of short sentences describing practical steps to reach desired state
4.3.2. Builet point list of external resources informing the issue and resolution
4.4 Tasks needs to be appropriately labelled using GROUP labels.
# 5. Code submission
5.1 A branch and new MR is always created BEFORE THE WORK STARTS
5.2 An MR should solve ONE SINGLE PART of a problem
5.3 Every MR should have at least ONE ISSUE associated with it. Ideally issue can be closed when MR is merged
5.4 MRs should not be open for more than one week (during normal operation periods)
5.5 MR should ideally not be longer than 400 lines of changes of logic
5.6 MRs that MOVE or DELETE code should not CHANGE that same code in a single MR. Scope MOVEs and DELETEs in separate commits (or even better, separate MRs) for transparency
# 6. Code reviews
6.1 At least one peer review before merge
6.2 If MR is too long, evaluate whether this affects the quality of the review negatively. If it does, expect to be asked to split it up
6.3 Evaluate changes against associated issues' problem statement and proposed resolution steps. If there is a mismatch, either MR needs to change or issue needs to be amended accordingly
6.4 Make sure all technical debt introduced by MR is documented in issues. Add them according to criteria in section ISSUES if not
6.5 If CI is not working, reviewer MUST make sure code builds and runs
6.6 Behave!
6.6.1 Don't be a jerk
6.6.2 Don't block needlessly
6.6.3 Say please

View File

@@ -1,19 +1,24 @@
# Community Inclusion Currency Stack (CIC Stack)
## Community Inclusion Currency Stack (CIC Stack)
A custodial evm wallet for executing transactions via USSD
A custodial wallet and blockchain bidirectional interface engine for community inclusion currencies. Check the [CIC Stack summary](https://docs.grassecon.org/software) page for more information.
## Getting started
### Contributing
This repo uses docker-compose and docker buildkit. Set the following environment variables to get started:
See our [contribution guidelines](https://docs.grassecon.org/community/contrib/) for more details on:
```
export COMPOSE_DOCKER_CLI_BUILD=1
export DOCKER_BUILDKIT=1
```
- Environment setup
- Source Code Management (SCM) guidelines
- Filing issues
To get started see [./apps/contract-migration/README.md](./apps/contract-migration/README.md)
#### Code of Conduct
## Documentation
This project is released with a [Code of Conduct](https://docs.grassecon.org/community/conduct/). By participating in this project you agree to abide by its terms.
[https://docs.grassecon.org/cic_stack/](https://docs.grassecon.org/cic_stack/)
### Community
- [Mattermost](https://chat.grassrootseconomics.net/cic/channels/dev)
- [Discord](https://discord.gg/ud32KMgH76)
### License
Licensed under [GNU AGPLv3](https://gitlab.com/grassrootseconomics/cic-internal-integration/-/blob/master/LICENSE).

View File

@@ -1 +1 @@
include *requirements.txt cic_cache/data/config/* cic_cache/db/migrations/default/* cic_cache/db/migrations/default/versions/*
include *requirements.txt cic_cache/data/config/*

View File

@@ -1,4 +1,4 @@
[cic]
registry_address =
trust_address =
health_modules =
health_modules = cic_eth.check.db,cic_eth.check.redis,cic_eth.check.signer,cic_eth.check.gas

View File

@@ -3,8 +3,7 @@ engine =
driver =
host =
port =
#name = cic-cache
prefix =
name = cic-cache
user =
password =
debug = 0

View File

@@ -9,26 +9,21 @@ from .list import (
tag_transaction,
add_tag,
)
from cic_cache.db.models.base import SessionBase
logg = logging.getLogger()
def dsn_from_config(config, name):
def dsn_from_config(config):
scheme = config.get('DATABASE_ENGINE')
if config.get('DATABASE_DRIVER') != None:
scheme += '+{}'.format(config.get('DATABASE_DRIVER'))
database_name = name
if config.get('DATABASE_PREFIX'):
database_name = '{}_{}'.format(config.get('DATABASE_PREFIX'), database_name)
dsn = ''
if config.get('DATABASE_ENGINE') == 'sqlite':
SessionBase.poolable = False
dsn = '{}:///{}'.format(
scheme,
database_name,
config.get('DATABASE_NAME'),
)
else:
@@ -38,7 +33,7 @@ def dsn_from_config(config, name):
config.get('DATABASE_PASSWORD'),
config.get('DATABASE_HOST'),
config.get('DATABASE_PORT'),
database_name,
config.get('DATABASE_NAME'),
)
logg.debug('parsed dsn from config: {}'.format(dsn))
return dsn

View File

@@ -5,11 +5,7 @@ import re
import base64
# external imports
from hexathon import (
add_0x,
strip_0x,
)
from chainlib.encode import TxHexNormalizer
from hexathon import add_0x
# local imports
from cic_cache.cache import (
@@ -20,72 +16,27 @@ from cic_cache.cache import (
logg = logging.getLogger(__name__)
#logg = logging.getLogger()
re_transactions_all_bloom = r'/tx/?(\d+)?/?(\d+)?/?(\d+)?/?(\d+)?/?'
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+)?/?(\d+)?/?(\d+)?/?'
re_transactions_account_data = r'/txa/user/((0x)?[a-fA-F0-9]+)(/(\d+)(/(\d+))?)?/?'
re_default_limit = r'/defaultlimit/?'
re_transactions_all_data = r'/txa/(\d+)?/?(\d+)/?'
DEFAULT_LIMIT = 100
tx_normalize = TxHexNormalizer()
def parse_query_account(r):
address = strip_0x(r[1])
#address = tx_normalize.wallet_address(address)
limit = DEFAULT_LIMIT
g = r.groups()
if len(g) > 3:
limit = int(r[4])
if limit == 0:
limit = DEFAULT_LIMIT
offset = 0
if len(g) > 4:
offset = int(r[6])
logg.debug('account query is address {} offset {} limit {}'.format(address, offset, limit))
return (address, offset, limit,)
# r is an re.Match
def parse_query_any(r):
limit = DEFAULT_LIMIT
offset = 0
block_offset = None
block_end = None
if r.lastindex != None:
if r.lastindex > 0:
limit = int(r[1])
if r.lastindex > 1:
offset = int(r[2])
if r.lastindex > 2:
block_offset = int(r[3])
if r.lastindex > 3:
block_end = int(r[4])
if block_end < block_offset:
raise ValueError('cart before the horse, dude')
logg.debug('data query is offset {} limit {} block_offset {} block_end {}'.format(offset, limit, block_offset, block_end))
return (offset, limit, block_offset, block_end,)
def process_default_limit(session, env):
r = re.match(re_default_limit, env.get('PATH_INFO'))
if not r:
return None
return ('application/json', str(DEFAULT_LIMIT).encode('utf-8'),)
def process_transactions_account_bloom(session, env):
r = re.match(re_transactions_account_bloom, env.get('PATH_INFO'))
if not r:
return None
logg.debug('match account bloom')
(address, offset, limit,) = parse_query_account(r)
address = r[1]
if r[2] == None:
address = add_0x(address)
offset = 0
if r.lastindex > 2:
offset = r[4]
limit = DEFAULT_LIMIT
if r.lastindex > 4:
limit = r[6]
c = BloomCache(session)
(lowest_block, highest_block, bloom_filter_block, bloom_filter_tx) = c.load_transactions_account(address, offset, limit)
@@ -108,9 +59,13 @@ def process_transactions_all_bloom(session, env):
r = re.match(re_transactions_all_bloom, env.get('PATH_INFO'))
if not r:
return None
logg.debug('match all bloom')
(limit, offset, block_offset, block_end,) = parse_query_any(r)
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)
@@ -133,16 +88,17 @@ 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
logg.debug('match all data')
if env.get('HTTP_X_CIC_CACHE_MODE') != 'all':
return None
logg.debug('got data request {}'.format(env))
(offset, limit, block_offset, block_end) = parse_query_any(r)
block_offset = r[1]
block_end = r[2]
if int(r[2]) < int(r[1]):
raise ValueError('cart before the horse, dude')
c = DataCache(session)
(lowest_block, highest_block, tx_cache) = c.load_transactions_with_data(offset, limit, block_offset, block_end, oldest=True) # oldest needs to be settable
(lowest_block, highest_block, tx_cache) = c.load_transactions_with_data(0, 0, block_offset, block_end, oldest=True) # oldest needs to be settable
for r in tx_cache:
r['date_block'] = r['date_block'].timestamp()
@@ -157,30 +113,3 @@ def process_transactions_all_data(session, env):
j = json.dumps(o)
return ('application/json', j.encode('utf-8'),)
def process_transactions_account_data(session, env):
r = re.match(re_transactions_account_data, env.get('PATH_INFO'))
if not r:
return None
logg.debug('match account data')
#if env.get('HTTP_X_CIC_CACHE_MODE') != 'all':
# return None
(address, offset, limit,) = parse_query_account(r)
c = DataCache(session)
(lowest_block, highest_block, tx_cache) = c.load_transactions_account_with_data(address, offset, limit)
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

@@ -12,20 +12,21 @@ import cic_cache.cli
from cic_cache.db import dsn_from_config
from cic_cache.db.models.base import SessionBase
from cic_cache.runnable.daemons.query import (
process_default_limit,
process_transactions_account_bloom,
process_transactions_account_data,
process_transactions_all_bloom,
process_transactions_all_data,
)
import cic_cache.cli
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
rootdir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
dbdir = os.path.join(rootdir, 'cic_cache', 'db')
migrationsdir = os.path.join(dbdir, 'migrations')
arg_flags = cic_cache.cli.argflag_std_read
local_arg_flags = cic_cache.cli.argflag_local_sync | cic_cache.cli.argflag_local_task
# process args
arg_flags = cic_cache.cli.argflag_std_base
local_arg_flags = cic_cache.cli.argflag_local_task
argparser = cic_cache.cli.ArgumentParser(arg_flags)
argparser.process_local_flags(local_arg_flags)
args = argparser.parse_args()
@@ -34,7 +35,7 @@ args = argparser.parse_args()
config = cic_cache.cli.Config.from_args(args, arg_flags, local_arg_flags)
# connect to database
dsn = dsn_from_config(config, 'cic_cache')
dsn = dsn_from_config(config)
SessionBase.connect(dsn, config.true('DATABASE_DEBUG'))
@@ -46,11 +47,9 @@ def application(env, start_response):
session = SessionBase.create_session()
for handler in [
process_transactions_account_data,
process_transactions_account_bloom,
process_transactions_all_data,
process_transactions_all_bloom,
process_default_limit,
process_transactions_account_bloom,
]:
r = None
try:

View File

@@ -3,7 +3,6 @@ import logging
import os
import sys
import argparse
import tempfile
# third-party imports
import celery
@@ -29,7 +28,7 @@ args = argparser.parse_args()
config = cic_cache.cli.Config.from_args(args, arg_flags, local_arg_flags)
# connect to database
dsn = dsn_from_config(config, 'cic_cache')
dsn = dsn_from_config(config)
SessionBase.connect(dsn)
# set up celery

View File

@@ -50,7 +50,7 @@ args = argparser.parse_args()
config = cic_cache.cli.Config.from_args(args, arg_flags, local_arg_flags)
# connect to database
dsn = dsn_from_config(config, 'cic_cache')
dsn = dsn_from_config(config)
SessionBase.connect(dsn, debug=config.true('DATABASE_DEBUG'))
# set up rpc

View File

@@ -5,7 +5,7 @@ version = (
0,
2,
1,
'alpha.3',
'alpha.2',
)
version_object = semver.VersionInfo(

View File

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

View File

@@ -0,0 +1,3 @@
[cic]
registry_address =
trust_address =

View File

@@ -0,0 +1,9 @@
[database]
NAME=cic_cache
USER=postgres
PASSWORD=
HOST=localhost
PORT=5432
ENGINE=postgresql
DRIVER=psycopg2
DEBUG=0

View File

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

View File

@@ -0,0 +1,3 @@
[cic]
registry_address =
trust_address = 0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C

View File

@@ -0,0 +1,9 @@
[database]
NAME=cic_cache
USER=grassroots
PASSWORD=
HOST=localhost
PORT=63432
ENGINE=postgresql
DRIVER=psycopg2
DEBUG=0

View File

@@ -0,0 +1,4 @@
[syncer]
loop_interval = 1
offset = 0
no_history = 0

View File

@@ -0,0 +1,2 @@
[bancor]
dir =

View File

@@ -1,3 +1,4 @@
[cic]
registry_address =
chain_spec =
trust_address =

View File

@@ -1,5 +1,5 @@
[database]
PREFIX=cic-cache-test
NAME=cic-cache-test
USER=postgres
PASSWORD=
HOST=localhost

View File

@@ -0,0 +1,5 @@
[eth]
#ws_provider = ws://localhost:8546
#ttp_provider = http://localhost:8545
provider = http://localhost:8545
#chain_id =

View File

@@ -1,4 +1,4 @@
openapi: "3.0.2"
openapi: "3.0.3"
info:
title: Grassroots Economics CIC Cache
description: Cache of processed transaction data from Ethereum blockchain and worker queues
@@ -9,34 +9,17 @@ info:
email: will@grassecon.org
license:
name: GPLv3
version: 0.2.0
version: 0.1.0
paths:
/defaultlimit:
summary: The default limit value of result sets.
get:
tags:
- transactions
description:
Retrieve default limit
operationId: limit.default
responses:
200:
description: Limit query successful
content:
application/json:
schema:
$ref: "#/components/schemas/Limit"
/tx:
summary: Bloom filter for batch of latest transactions
description: Generate a bloom filter of the latest transactions in the cache. The number of maximum number of transactions returned is returned by the `/defaultlimit` API call.
/tx/{offset}/{limit}:
description: Bloom filter for batch of latest transactions
get:
tags:
- transactions
description:
Retrieve transactions
operationId: tx.get.latest
operationId: tx.get
responses:
200:
description: Transaction query successful.
@@ -46,153 +29,27 @@ paths:
$ref: "#/components/schemas/BlocksBloom"
/tx/{limit}:
summary: Bloom filter for batch of latest transactions
description: Generate a bloom filter of the latest transactions in the cache. If `limit` is 0, the number of maximum number of transactions returned is returned by the `/defaultlimit` API call.
get:
tags:
- transactions
description:
Retrieve transactions
operationId: tx.get.latest.limit
responses:
200:
description: Transaction query successful. Results are ordered from newest to oldest.
content:
application/json:
schema:
$ref: "#/components/schemas/BlocksBloom"
parameters:
- name: limit
in: path
required: true
schema:
type: integer
format: int32
/tx/{limit}/{offset}:
summary: Bloom filter for batch of latest transactions
description: Generate a bloom filter of the latest transactions in the cache. If `limit` is 0, the number of maximum number of transactions returned is returned by the `/defaultlimit` API call.
get:
tags:
- transactions
description:
Retrieve transactions
operationId: tx.get.latest.range
responses:
200:
description: Transaction query successful. Results are ordered from newest to oldest.
content:
application/json:
schema:
$ref: "#/components/schemas/BlocksBloom"
parameters:
- name: limit
in: path
required: true
schema:
type: integer
format: int32
- name: offset
in: path
required: true
schema:
type: integer
format: int32
/tx/{limit}/{offset}/{block_offset}:
summary: Bloom filter for batch of transactions since a particular block.
description: Generate a bloom filter of the latest transactions since a particular block in the cache. The block parameter is inclusive. If `limit` is 0, the number of maximum number of transactions returned is returned by the `/defaultlimit` API call.
get:
tags:
- transactions
description:
Retrieve transactions
operationId: tx.get.latest.range.block.offset
responses:
200:
description: Transaction query successful. Results are ordered from oldest to newest.
content:
application/json:
schema:
$ref: "#/components/schemas/BlocksBloom"
parameters:
- name: limit
in: path
required: true
schema:
type: integer
format: int32
- name: offset
in: path
required: true
schema:
type: integer
format: int32
- name: block_offset
in: path
required: true
schema:
type: integer
format: int32
/tx/{limit}/{offset}/{block_offset}/{block_end}:
summary: Bloom filter for batch of transactions within a particular block range.
description: Generate a bloom filter of the latest transactions within a particular block range in the cache. The block parameters are inclusive. If `limit` is 0, the number of maximum number of transactions returned is returned by the `/defaultlimit` API call.
/tx/{address}/{offset}/{limit}:
description: Bloom filter for batch of latest transactions by account
get:
tags:
- transactions
description:
Retrieve transactions
operationId: tx.get.latest.range.block.range
responses:
200:
description: Transaction query successful.
content:
application/json:
schema:
$ref: "#/components/schemas/BlocksBloom"
parameters:
- name: limit
in: path
required: true
schema:
type: integer
format: int32
- name: offset
in: path
required: true
schema:
type: integer
format: int32
- name: block_offset
in: path
required: true
schema:
type: integer
format: int32
- name: block_end
in: path
required: true
schema:
type: integer
format: int32
/tx/{address}:
summary: Bloom filter for batch of latest transactions by account.
description: Generate a bloom filter of the latest transactions where a specific account is the spender or beneficiary.
get:
tags:
- transactions
description:
Retrieve transactions
operationId: tx.get.user
operationId: tx.get
responses:
200:
description: Transaction query successful.
@@ -201,30 +58,6 @@ paths:
schema:
$ref: "#/components/schemas/BlocksBloom"
parameters:
- name: address
in: path
required: true
schema:
type: string
/tx/{address}/{limit}:
summary: Bloom filter for batch of latest transactions by account.
description: Generate a bloom filter of the latest transactions where a specific account is the spender or beneficiary. If `limit` is 0, the number of maximum number of transactions returned is returned by the `/defaultlimit` API call.
get:
tags:
- transactions
description:
Retrieve transactions
operationId: tx.get.user.limit
responses:
200:
description: Transaction query successful.
content:
application/json:
schema:
$ref: "#/components/schemas/BlocksBloom"
parameters:
- name: address
@@ -232,317 +65,26 @@ paths:
required: true
schema:
type: string
- name: limit
in: path
required: true
schema:
type: integer
format: int32
/tx/{address}/{limit}/{offset}:
summary: Bloom filter for batch of latest transactions by account
description: Generate a bloom filter of the latest transactions where a specific account is the spender or beneficiary. If `limit` is 0, the number of maximum number of transactions returned is returned by the `/defaultlimit` API call.
get:
tags:
- transactions
description:
Retrieve transactions
operationId: tx.get.user.range
responses:
200:
description: Transaction query successful.
content:
application/json:
schema:
$ref: "#/components/schemas/BlocksBloom"
parameters:
- name: address
in: path
required: true
schema:
type: string
- name: limit
in: path
required: true
schema:
type: integer
format: int32
- name: offset
in: path
required: true
schema:
type: integer
format: int32
/txa:
summary: Cached data for latest transactions.
description: Return data entries of the latest transactions in the cache. The number of maximum number of transactions returned is returned by the `/defaultlimit` API call.
get:
tags:
- transactions
description:
Retrieve transactions
operationId: txa.get.latest
responses:
200:
description: Transaction query successful.
content:
application/json:
schema:
$ref: "#/components/schemas/TransactionList"
/txa/{limit}:
summary: Cached data for latest transactions.
description: Return data entries of the latest transactions in the cache. If `limit` is 0, the number of maximum number of transactions returned is returned by the `/defaultlimit` API call.
get:
tags:
- transactions
description:
Retrieve transactions
operationId: txa.get.latest.limit
responses:
200:
description: Transaction query successful.
content:
application/json:
schema:
$ref: "#/components/schemas/TransactionList"
parameters:
- name: limit
in: path
required: true
schema:
type: integer
format: int32
/txa/{limit}/{offset}:
summary: Cached data for latest transactions.
description: Return data entries of the latest transactions in the cache. If `limit` is 0, the number of maximum number of transactions returned is returned by the `/defaultlimit` API call.
get:
tags:
- transactions
description:
Retrieve transactions
operationId: txa.get.latest.range
responses:
200:
description: Transaction query successful.
content:
application/json:
schema:
$ref: "#/components/schemas/TransactionList"
parameters:
- name: limit
in: path
required: true
schema:
type: integer
format: int32
- name: offset
in: path
required: true
schema:
type: integer
format: int32
/txa/{limit}/{offset}/{block_offset}:
summary: Cached data for transactions since a particular block.
description: Return cached data entries of transactions since a particular block. The block parameter is inclusive. If `limit` is 0, the number of maximum number of transactions returned is returned by the `/defaultlimit` API call.
get:
tags:
- transactions
description:
Retrieve transactions
operationId: txa.get.latest.range.block.offset
responses:
200:
description: Transaction query successful.
content:
application/json:
schema:
$ref: "#/components/schemas/TransactionList"
parameters:
- name: limit
in: path
required: true
schema:
type: integer
format: int32
- name: offset
in: path
required: true
schema:
type: integer
format: int32
- name: block_offset
in: path
required: true
schema:
type: integer
format: int32
/txa/{limit}/{offset}/{block_offset}/{block_end}:
summary: Cached data for transactions within a particular block range.
description: Return cached data entries of transactions within a particular block range in the cache. The block parameters are inclusive. If `limit` is 0, the number of maximum number of transactions returned is returned by the `/defaultlimit` API call.
get:
tags:
- transactions
description:
Retrieve transactions
operationId: txa.get.latest.range.block.range
responses:
200:
description: Transaction query successful.
content:
application/json:
schema:
$ref: "#/components/schemas/TransactionList"
parameters:
- name: limit
in: path
required: true
schema:
type: integer
format: int32
- name: offset
in: path
required: true
schema:
type: integer
format: int32
- name: block_offset
in: path
required: true
schema:
type: integer
format: int32
- name: block_end
in: path
required: true
schema:
type: integer
format: int32
/txa/{address}:
summary: Cached data for batch of latest transactions by account.
description: Return cached data of the latest transactions where a specific account is the spender or beneficiary.
get:
tags:
- transactions
description:
Retrieve transactions
operationId: txa.get.user
responses:
200:
description: Transaction query successful.
content:
application/json:
schema:
$ref: "#/components/schemas/TransactionList"
parameters:
- name: address
in: path
required: true
schema:
type: string
/txa/{address}/{limit}:
summary: Cached data for batch of latest transactions by account.
description: Return cached data of the latest transactions where a specific account is the spender or beneficiary. If `limit` is 0, the number of maximum number of transactions returned is returned by the `/defaultlimit` API call.
get:
tags:
- transactions
description:
Retrieve transactions
operationId: txa.get.user.limit
responses:
200:
description: Transaction query successful.
content:
application/json:
schema:
$ref: "#/components/schemas/TransactionList"
parameters:
- name: address
in: path
required: true
schema:
type: string
- name: limit
in: path
required: true
schema:
type: integer
format: int32
/txa/{address}/{limit}/{offset}:
summary: Cached data for batch of latest transactions by account.
description: Return cached data of the latest transactions where a specific account is the spender or beneficiary. If `limit` is 0, the number of maximum number of transactions returned is returned by the `/defaultlimit` API call.
get:
tags:
- transactions
description:
Retrieve transactions
operationId: txa.get.user.range
responses:
200:
description: Transaction query successful.
content:
application/json:
schema:
$ref: "#/components/schemas/TransactionList"
parameters:
- name: address
in: path
required: true
schema:
type: string
- name: limit
in: path
required: true
schema:
type: integer
format: int32
- name: offset
in: path
required: true
schema:
type: integer
format: int32
components:
schemas:
Limit:
type: integer
format: int32
BlocksBloom:
type: object
properties:
low:
type: integer
type: int
format: int32
description: The lowest block number included in the filter
high:
type: integer
format: int32
description: The highest block number included in the filter
block_filter:
type: string
format: byte
@@ -555,89 +97,6 @@ components:
type: string
description: Hashing algorithm (currently only using sha256)
filter_rounds:
type: integer
type: int
format: int32
description: Number of hash rounds used to create the filter
TransactionList:
type: object
properties:
low:
type: integer
format: int32
description: The lowest block number included in the result set
high:
type: integer
format: int32
description: The highest block number included in the filter
data:
type: array
description: Cached transaction data
items:
$ref: "#/components/schemas/Transaction"
Transaction:
type: object
properties:
block_number:
type: integer
format: int64
description: Block number transaction was included in.
tx_hash:
type: string
description: Transaction hash, in hex.
date_block:
type: integer
format: int32
description: Block timestamp.
sender:
type: string
description: Spender address, in hex.
recipient:
type: string
description: Beneficiary address, in hex.
from_value:
type: integer
format: int64
description: Value deducted from spender's balance.
to_value:
type: integer
format: int64
description: Value added to beneficiary's balance.
source_token:
type: string
description: Network address of token in which `from_value` is denominated.
destination_token:
type: string
description: Network address of token in which `to_value` is denominated.
success:
type: boolean
description: Network consensus state on whether the transaction was successful or not.
tx_type:
type: string
enum:
- erc20.faucet
- faucet.give_to
examples:
data_last:
summary: Get the latest cached transactions, using the server's default limit.
value: "/txa"
data_limit:
summary: Get the last 42 cached transactions.
value: "/txa/42"
data_range:
summary: Get the next 42 cached transactions, starting from the 13th (zero-indexed).
value: "/txa/42/13"
data_range_block_offset:
summary: Get the next 42 cached transactions, starting from block 1337 (inclusive).
value: "/txa/42/0/1337"
data_range_block_offset:
summary: Get the next 42 cached transactions within blocks 1337 and 1453 (inclusive).
value: "/txa/42/0/1337/1453"
data_range_block_range:
summary: Get the next 42 cached transactions after the 13th, within blocks 1337 and 1453 (inclusive).
value: "/txa/42/13/1337/1453"

View File

@@ -4,9 +4,9 @@ FROM $DOCKER_REGISTRY/cic-base-images:python-3.8.6-dev-e8eb2ee2
COPY requirements.txt .
ARG EXTRA_PIP_INDEX_URL=https://pip.grassrootseconomics.net
ARG EXTRA_PIP_INDEX_URL="https://pip.grassrootseconomics.net"
ARG EXTRA_PIP_ARGS=""
ARG PIP_INDEX_URL=https://pypi.org/simple
ARG PIP_INDEX_URL="https://pypi.org/simple"
RUN --mount=type=cache,mode=0755,target=/root/.cache/pip \
pip install --index-url $PIP_INDEX_URL \
@@ -14,9 +14,14 @@ RUN --mount=type=cache,mode=0755,target=/root/.cache/pip \
--extra-index-url $EXTRA_PIP_INDEX_URL $EXTRA_PIP_ARGS \
-r requirements.txt
COPY . .
COPY . .
RUN pip install . --extra-index-url $EXTRA_PIP_INDEX_URL
RUN python setup.py install
# ini files in config directory defines the configurable parameters for the application
# they can all be overridden by environment variables
# to generate a list of environment variables from configuration, use: confini-dump -z <dir> (executable provided by confini package)
#COPY config/ /usr/local/etc/cic-cache/
# for db migrations
COPY ./aux/wait-for-it/wait-for-it.sh ./

View File

@@ -2,5 +2,5 @@
set -e
>&2 echo executing database migration
python scripts/migrate_cic_cache.py --migrations-dir /usr/local/share/cic-cache/alembic -vv
python scripts/migrate.py --migrations-dir /usr/local/share/cic-cache/alembic -vv
set +e

View File

@@ -1,15 +1,14 @@
alembic==1.4.2
confini~=0.5.3
confini>=0.3.6rc4,<0.5.0
uwsgi==2.0.19.1
moolb~=0.2.0
cic-eth-registry~=0.6.6
moolb~=0.1.1b2
cic-eth-registry~=0.6.1a1
SQLAlchemy==1.3.20
semver==2.13.0
psycopg2==2.8.6
celery==4.4.7
redis==3.5.3
chainsyncer[sql]~=0.0.7
erc20-faucet~=0.3.2
chainlib-eth~=0.0.15
eth-address-index~=0.2.4
okota~=0.2.5
chainsyncer[sql]>=0.0.6a3,<0.1.0
erc20-faucet>=0.3.2a2, <0.4.0
chainlib-eth>=0.0.9a14,<0.1.0
eth-address-index>=0.2.3a4,<0.3.0

View File

@@ -1,55 +1,54 @@
#!/usr/bin/python3
# standard imports
#!/usr/bin/python
import os
import argparse
import logging
import re
# external imports
import alembic
from alembic.config import Config as AlembicConfig
import confini
# local imports
from cic_cache.db import dsn_from_config
import cic_cache.cli
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
# BUG: the dbdir doesn't work after script install
rootdir = os.path.dirname(os.path.dirname(os.path.realpath(cic_cache.__file__)))
rootdir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
dbdir = os.path.join(rootdir, 'cic_cache', 'db')
default_migrations_dir = os.path.join(dbdir, 'migrations')
migrationsdir = os.path.join(dbdir, 'migrations')
configdir = os.path.join(rootdir, 'cic_cache', 'data', 'config')
#config_dir = os.path.join('/usr/local/etc/cic-cache')
arg_flags = cic_cache.cli.argflag_std_base
local_arg_flags = cic_cache.cli.argflag_local_sync
argparser = cic_cache.cli.ArgumentParser(arg_flags)
argparser.process_local_flags(local_arg_flags)
argparser = argparse.ArgumentParser()
argparser.add_argument('-c', type=str, help='config file')
argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration')
argparser.add_argument('--migrations-dir', dest='migrations_dir', default=migrationsdir, type=str, help='path to alembic migrations directory')
argparser.add_argument('--reset', action='store_true', help='downgrade before upgrading')
argparser.add_argument('-f', '--force', action='store_true', help='force action')
argparser.add_argument('--migrations-dir', dest='migrations_dir', default=default_migrations_dir, type=str, help='migrations directory')
argparser.add_argument('-f', action='store_true', help='force action')
argparser.add_argument('-v', action='store_true', help='be verbose')
argparser.add_argument('-vv', action='store_true', help='be more verbose')
args = argparser.parse_args()
extra_args = {
'reset': None,
'force': None,
'migrations_dir': None,
}
# process config
config = cic_cache.cli.Config.from_args(args, arg_flags, local_arg_flags, extra_args=extra_args)
if args.vv:
logging.getLogger().setLevel(logging.DEBUG)
elif args.v:
logging.getLogger().setLevel(logging.INFO)
migrations_dir = os.path.join(config.get('_MIGRATIONS_DIR'), config.get('DATABASE_ENGINE', 'default'))
config = confini.Config(configdir, args.env_prefix)
config.process()
config.censor('PASSWORD', 'DATABASE')
config.censor('PASSWORD', 'SSL')
logg.debug('config:\n{}'.format(config))
migrations_dir = os.path.join(args.migrations_dir, config.get('DATABASE_ENGINE'))
if not os.path.isdir(migrations_dir):
logg.debug('migrations dir for engine {} not found, reverting to default'.format(config.get('DATABASE_ENGINE')))
migrations_dir = os.path.join(args.migrations_dir, 'default')
# connect to database
dsn = dsn_from_config(config, 'cic_cache')
dsn = dsn_from_config(config)
logg.info('using migrations dir {}'.format(migrations_dir))

View File

@@ -1,7 +1,6 @@
[metadata]
name = cic-cache
description = CIC Cache API and server
version = 0.3.0a2
author = Louis Holbrook
author_email = dev@holbrook.no
url = https://gitlab.com/grassrootseconomics/cic-eth
@@ -35,7 +34,7 @@ packages =
cic_cache.runnable.daemons
cic_cache.runnable.daemons.filters
scripts =
./scripts/migrate_cic_cache.py
./scripts/migrate.py
[options.entry_points]
console_scripts =

View File

@@ -1,39 +1,38 @@
from setuptools import setup
# import configparser
import configparser
import os
import time
# import time
from cic_cache.version import (
version_object,
version_string
)
# from cic_cache.version import (
# version_object,
# version_string
# )
#
# class PleaseCommitFirstError(Exception):
# pass
#
# def git_hash():
# import subprocess
# git_diff = subprocess.run(['git', 'diff'], capture_output=True)
# if len(git_diff.stdout) > 0:
# raise PleaseCommitFirstError()
# git_hash = subprocess.run(['git', 'rev-parse', 'HEAD'], capture_output=True)
# git_hash_brief = git_hash.stdout.decode('utf-8')[:8]
# return git_hash_brief
#
# version_string = str(version_object)
#
# try:
# version_git = git_hash()
# version_string += '+build.{}'.format(version_git)
# except FileNotFoundError:
# time_string_pair = str(time.time()).split('.')
# version_string += '+build.{}{:<09d}'.format(
# time_string_pair[0],
# int(time_string_pair[1]),
# )
# print('final version string will be {}'.format(version_string))
class PleaseCommitFirstError(Exception):
pass
def git_hash():
import subprocess
git_diff = subprocess.run(['git', 'diff'], capture_output=True)
if len(git_diff.stdout) > 0:
raise PleaseCommitFirstError()
git_hash = subprocess.run(['git', 'rev-parse', 'HEAD'], capture_output=True)
git_hash_brief = git_hash.stdout.decode('utf-8')[:8]
return git_hash_brief
version_string = str(version_object)
try:
version_git = git_hash()
version_string += '+build.{}'.format(version_git)
except FileNotFoundError:
time_string_pair = str(time.time()).split('.')
version_string += '+build.{}{:<09d}'.format(
time_string_pair[0],
int(time_string_pair[1]),
)
print('final version string will be {}'.format(version_string))
requirements = []
f = open('requirements.txt', 'r')
@@ -53,8 +52,9 @@ while True:
test_requirements.append(l.rstrip())
f.close()
setup(
# version=version_string,
version=version_string,
install_requires=requirements,
tests_require=test_requirements,
)

View File

@@ -7,4 +7,4 @@ pytest-celery==0.0.0a1
eth_tester==0.5.0b3
py-evm==0.3.0a20
sarafu-faucet~=0.0.7a1
erc20-transfer-authorization~=0.3.6
erc20-transfer-authorization>=0.3.5a1,<0.4.0

View File

@@ -6,7 +6,6 @@ import datetime
# external imports
import pytest
import moolb
from chainlib.encode import TxHexNormalizer
# local imports
from cic_cache import db
@@ -43,8 +42,6 @@ def txs(
list_tokens,
):
tx_normalize = TxHexNormalizer()
session = init_database
tx_number = 13
@@ -57,10 +54,10 @@ def txs(
tx_hash_first,
list_defaults['block'],
tx_number,
tx_normalize.wallet_address(list_actors['alice']),
tx_normalize.wallet_address(list_actors['bob']),
tx_normalize.executable_address(list_tokens['foo']),
tx_normalize.executable_address(list_tokens['foo']),
list_actors['alice'],
list_actors['bob'],
list_tokens['foo'],
list_tokens['foo'],
1024,
2048,
True,
@@ -77,10 +74,10 @@ def txs(
tx_hash_second,
list_defaults['block']-1,
tx_number,
tx_normalize.wallet_address(list_actors['diane']),
tx_normalize.wallet_address(list_actors['alice']),
tx_normalize.executable_address(list_tokens['foo']),
tx_normalize.wallet_address(list_tokens['foo']),
list_actors['diane'],
list_actors['alice'],
list_tokens['foo'],
list_tokens['foo'],
1024,
2048,
False,
@@ -106,8 +103,6 @@ def more_txs(
session = init_database
tx_normalize = TxHexNormalizer()
tx_number = 666
tx_hash = '0x' + os.urandom(32).hex()
tx_signed = '0x' + os.urandom(128).hex()
@@ -120,10 +115,10 @@ def more_txs(
tx_hash,
list_defaults['block']+2,
tx_number,
tx_normalize.wallet_address(list_actors['alice']),
tx_normalize.wallet_address(list_actors['diane']),
tx_normalize.executable_address(list_tokens['bar']),
tx_normalize.executable_address(list_tokens['bar']),
list_actors['alice'],
list_actors['diane'],
list_tokens['bar'],
list_tokens['bar'],
2048,
4096,
False,

View File

@@ -14,8 +14,7 @@ logg = logging.getLogger(__file__)
@pytest.fixture(scope='session')
def load_config():
config_dir = os.path.join(root_dir, 'config/test')
schema_config_dir = os.path.join(root_dir, 'cic_cache', 'data', 'config')
conf = confini.Config(schema_config_dir, 'CICTEST', override_dirs=config_dir)
conf = confini.Config(config_dir, 'CICTEST')
conf.process()
logg.debug('config {}'.format(conf))
return conf

View File

@@ -24,15 +24,11 @@ def database_engine(
if load_config.get('DATABASE_ENGINE') == 'sqlite':
SessionBase.transactional = False
SessionBase.poolable = False
name = 'cic_cache'
database_name = name
if load_config.get('DATABASE_PREFIX'):
database_name = '{}_{}'.format(load_config.get('DATABASE_PREFIX'), database_name)
try:
os.unlink(database_name)
os.unlink(load_config.get('DATABASE_NAME'))
except FileNotFoundError:
pass
dsn = dsn_from_config(load_config, name)
dsn = dsn_from_config(load_config)
SessionBase.connect(dsn, debug=load_config.true('DATABASE_DEBUG'))
return dsn

View File

@@ -14,7 +14,7 @@ def test_api_all_data(
):
env = {
'PATH_INFO': '/txa/100/0/410000/420000',
'PATH_INFO': '/txa/410000/420000',
'HTTP_X_CIC_CACHE_MODE': 'all',
}
j = process_transactions_all_data(init_database, env)
@@ -23,7 +23,7 @@ def test_api_all_data(
assert len(o['data']) == 2
env = {
'PATH_INFO': '/txa/100/0/420000/410000',
'PATH_INFO': '/txa/420000/410000',
'HTTP_X_CIC_CACHE_MODE': 'all',
}

View File

@@ -6,7 +6,6 @@ import json
# external imports
import pytest
from chainlib.encode import TxHexNormalizer
# local imports
from cic_cache import db
@@ -63,8 +62,6 @@ def test_cache_ranges(
session = init_database
tx_normalize = TxHexNormalizer()
oldest = list_defaults['block'] - 1
mid = list_defaults['block']
newest = list_defaults['block'] + 2
@@ -103,39 +100,32 @@ def test_cache_ranges(
assert b[1] == mid
# now check when supplying account
account = tx_normalize.wallet_address(list_actors['alice'])
b = c.load_transactions_account(account, 0, 100)
b = c.load_transactions_account(list_actors['alice'], 0, 100)
assert b[0] == oldest
assert b[1] == newest
account = tx_normalize.wallet_address(list_actors['bob'])
b = c.load_transactions_account(account, 0, 100)
b = c.load_transactions_account(list_actors['bob'], 0, 100)
assert b[0] == mid
assert b[1] == mid
account = tx_normalize.wallet_address(list_actors['diane'])
b = c.load_transactions_account(account, 0, 100)
b = c.load_transactions_account(list_actors['diane'], 0, 100)
assert b[0] == oldest
assert b[1] == newest
# add block filter to the mix
account = tx_normalize.wallet_address(list_actors['alice'])
b = c.load_transactions_account(account, 0, 100, block_offset=list_defaults['block'])
b = c.load_transactions_account(list_actors['alice'], 0, 100, block_offset=list_defaults['block'])
assert b[0] == mid
assert b[1] == newest
account = tx_normalize.wallet_address(list_actors['alice'])
b = c.load_transactions_account(account, 0, 100, block_offset=list_defaults['block'])
b = c.load_transactions_account(list_actors['alice'], 0, 100, block_offset=list_defaults['block'])
assert b[0] == mid
assert b[1] == newest
account = tx_normalize.wallet_address(list_actors['bob'])
b = c.load_transactions_account(account, 0, 100, block_offset=list_defaults['block'] - 1, block_limit=list_defaults['block'])
b = c.load_transactions_account(list_actors['bob'], 0, 100, block_offset=list_defaults['block'] - 1, block_limit=list_defaults['block'])
assert b[0] == mid
assert b[1] == mid
account = tx_normalize.wallet_address(list_actors['diane'])
b = c.load_transactions_account(account, 0, 100, block_offset=list_defaults['block'] - 1, block_limit=list_defaults['block'])
b = c.load_transactions_account(list_actors['diane'], 0, 100, block_offset=list_defaults['block'] - 1, block_limit=list_defaults['block'])
assert b[0] == oldest
assert b[1] == oldest
@@ -150,8 +140,6 @@ def test_cache_ranges_data(
session = init_database
tx_normalize = TxHexNormalizer()
oldest = list_defaults['block'] - 1
mid = list_defaults['block']
newest = list_defaults['block'] + 2
@@ -215,8 +203,7 @@ def test_cache_ranges_data(
assert b[2][1]['tx_hash'] == more_txs[1]
# now check when supplying account
account = tx_normalize.wallet_address(list_actors['alice'])
b = c.load_transactions_account_with_data(account, 0, 100)
b = c.load_transactions_account_with_data(list_actors['alice'], 0, 100)
assert b[0] == oldest
assert b[1] == newest
assert len(b[2]) == 3
@@ -224,15 +211,13 @@ def test_cache_ranges_data(
assert b[2][1]['tx_hash'] == more_txs[1]
assert b[2][2]['tx_hash'] == more_txs[2]
account = tx_normalize.wallet_address(list_actors['bob'])
b = c.load_transactions_account_with_data(account, 0, 100)
b = c.load_transactions_account_with_data(list_actors['bob'], 0, 100)
assert b[0] == mid
assert b[1] == mid
assert len(b[2]) == 1
assert b[2][0]['tx_hash'] == more_txs[1]
account = tx_normalize.wallet_address(list_actors['diane'])
b = c.load_transactions_account_with_data(account, 0, 100)
b = c.load_transactions_account_with_data(list_actors['diane'], 0, 100)
assert b[0] == oldest
assert b[1] == newest
assert len(b[2]) == 2
@@ -240,31 +225,27 @@ def test_cache_ranges_data(
assert b[2][1]['tx_hash'] == more_txs[2]
# add block filter to the mix
account = tx_normalize.wallet_address(list_actors['alice'])
b = c.load_transactions_account_with_data(account, 0, 100, block_offset=list_defaults['block'])
b = c.load_transactions_account_with_data(list_actors['alice'], 0, 100, block_offset=list_defaults['block'])
assert b[0] == mid
assert b[1] == newest
assert len(b[2]) == 2
assert b[2][0]['tx_hash'] == more_txs[0]
assert b[2][1]['tx_hash'] == more_txs[1]
account = tx_normalize.wallet_address(list_actors['alice'])
b = c.load_transactions_account_with_data(account, 0, 100, block_offset=list_defaults['block'])
b = c.load_transactions_account_with_data(list_actors['alice'], 0, 100, block_offset=list_defaults['block'])
assert b[0] == mid
assert b[1] == newest
assert len(b[2]) == 2
assert b[2][0]['tx_hash'] == more_txs[0]
assert b[2][1]['tx_hash'] == more_txs[1]
account = tx_normalize.wallet_address(list_actors['bob'])
b = c.load_transactions_account_with_data(account, 0, 100, block_offset=list_defaults['block'] - 1, block_limit=list_defaults['block'])
b = c.load_transactions_account_with_data(list_actors['bob'], 0, 100, block_offset=list_defaults['block'] - 1, block_limit=list_defaults['block'])
assert b[0] == mid
assert b[1] == mid
assert len(b[2]) == 1
assert b[2][0]['tx_hash'] == more_txs[1]
account = tx_normalize.wallet_address(list_actors['diane'])
b = c.load_transactions_account_with_data(account, 0, 100, block_offset=list_defaults['block'] - 1, block_limit=list_defaults['block'])
b = c.load_transactions_account_with_data(list_actors['diane'], 0, 100, block_offset=list_defaults['block'] - 1, block_limit=list_defaults['block'])
assert b[0] == oldest
assert b[1] == oldest
assert len(b[2]) == 1

View File

@@ -82,7 +82,7 @@ def test_query_regex(
[
('alice', None, None, [(420000, 13), (419999, 42)]),
('alice', None, 1, [(420000, 13)]),
('alice', 1, 1, [(419999, 42)]), # 420000 == list_defaults['block']
('alice', 1, None, [(419999, 42)]), # 420000 == list_defaults['block']
('alice', 2, None, []), # 420000 == list_defaults['block']
],
)
@@ -107,11 +107,10 @@ def test_query_process_txs_account(
path_info = '/tx/user/0x' + strip_0x(actor)
if query_offset != None:
path_info += '/' + str(query_offset)
if query_limit == None:
query_limit = 100
path_info += '/' + str(query_limit)
if query_offset == None:
path_info += '/0'
if query_limit != None:
if query_offset == None:
path_info += '/0'
path_info += '/' + str(query_limit)
env = {
'PATH_INFO': path_info,
}
@@ -193,7 +192,7 @@ def test_query_process_txs_bloom(
@pytest.mark.parametrize(
'query_block_start, query_block_end, query_match_count',
[
(1, 42, 0),
(None, 42, 0),
(420000, 420001, 1),
(419999, 419999, 1), # matches are inclusive
(419999, 420000, 2),
@@ -212,7 +211,7 @@ def test_query_process_txs_data(
query_match_count,
):
path_info = '/txa/100/0'
path_info = '/txa'
if query_block_start != None:
path_info += '/' + str(query_block_start)
if query_block_end != None:
@@ -228,5 +227,4 @@ def test_query_process_txs_data(
assert r != None
o = json.loads(r[1])
logg.debug('oo {}'.format(o))
assert len(o['data']) == query_match_count

View File

@@ -1,5 +1,5 @@
celery==4.4.7
erc20-demurrage-token~=0.0.6
cic-eth-registry~=0.6.3
chainlib~=0.0.14
cic_eth~=0.12.6
erc20-demurrage-token~=0.0.5a3
cic-eth-registry~=0.6.1a6
chainlib~=0.0.9rc1
cic_eth~=0.12.4a11

View File

@@ -1,6 +1,6 @@
[metadata]
name = cic-eth-aux-erc20-demurrage-token
version = 0.0.3
version = 0.0.2a7
description = cic-eth tasks supporting erc20 demurrage token
author = Louis Holbrook
author_email = dev@holbrook.no

View File

@@ -1,4 +1,5 @@
SQLAlchemy==1.3.20
hexathon~=0.1.0
chainqueue~=0.0.6a4
eth-erc20~=0.1.5
cic-eth-registry>=0.6.1a6,<0.7.0
hexathon~=0.0.1a8
chainqueue>=0.0.4a6,<0.1.0
eth-erc20>=0.1.2a2,<0.2.0

View File

@@ -63,32 +63,22 @@ class Config(BaseConfig):
config.get('REDIS_HOST'),
config.get('REDIS_PORT'),
)
db = getattr(args, 'redis_db', None)
if db != None:
db = str(db)
redis_url = (
'redis',
hostport,
db,
getattr(args, 'redis_db', None),
)
celery_config_url = urllib.parse.urlsplit(config.get('CELERY_BROKER_URL'))
hostport = urlhostmerge(
celery_config_url[1],
getattr(args, 'celery_host', None),
getattr(args, 'celery_port', None),
)
db = getattr(args, 'redis_db', None)
if db != None:
db = str(db)
celery_arg_url = (
getattr(args, 'celery_scheme', None),
hostport,
db,
getattr(args, 'celery_db', None),
)
celery_url = urlmerge(redis_url, celery_config_url, celery_arg_url)
celery_url_string = urllib.parse.urlunsplit(celery_url)
local_celery_args_override['CELERY_BROKER_URL'] = celery_url_string

View File

@@ -22,7 +22,7 @@ from hexathon import (
from chainqueue.error import NotLocalTxError
from eth_erc20 import ERC20
from chainqueue.sql.tx import cache_tx_dict
from okota.token_index.index import to_identifier
from okota.token_index import to_identifier
# local imports
from cic_eth.db.models.base import SessionBase
@@ -46,14 +46,13 @@ from cic_eth.task import (
from cic_eth.eth.nonce import CustodialTaskNonceOracle
from cic_eth.encode import tx_normalize
from cic_eth.eth.trust import verify_proofs
from cic_eth.error import SignerError
celery_app = celery.current_app
logg = logging.getLogger()
@celery_app.task(bind=True, base=CriticalWeb3Task)
def balance(self, tokens, holder_address, chain_spec_dict):
@celery_app.task(base=CriticalWeb3Task)
def balance(tokens, holder_address, chain_spec_dict):
"""Return token balances for a list of tokens for given address
:param tokens: Token addresses
@@ -72,9 +71,8 @@ def balance(self, tokens, holder_address, chain_spec_dict):
for t in tokens:
address = t['address']
logg.debug('address {} {}'.format(address, holder_address))
gas_oracle = self.create_gas_oracle(rpc, min_price=self.min_fee_price)
token = ERC20Token(chain_spec, rpc, add_0x(address))
c = ERC20(chain_spec, gas_oracle=gas_oracle)
c = ERC20(chain_spec)
o = c.balance_of(address, holder_address, sender_address=caller_address)
r = rpc.do(o)
t['balance_network'] = c.parse_balance(r)

View File

@@ -92,7 +92,7 @@ def apply_gas_value_cache_local(address, method, value, tx_hash, session=None):
if o == None:
o = GasCache(address, method, value, tx_hash)
elif value > o.value:
elif tx.gas_used > o.value:
o.value = value
o.tx_hash = strip_0x(tx_hash)

View File

@@ -25,14 +25,12 @@ logg = logging.getLogger()
celery_app = celery.current_app
class BaseTask(celery.Task):
session_func = SessionBase.create_session
call_address = ZERO_ADDRESS
trusted_addresses = []
min_fee_price = 1
min_fee_limit = 30000
default_token_address = None
default_token_symbol = None
default_token_name = None
@@ -44,7 +42,7 @@ class BaseTask(celery.Task):
if address == None:
return RPCGasOracle(
conn,
code_callback=kwargs.get('code_callback', self.get_min_fee_limit),
code_callback=kwargs.get('code_callback'),
min_price=self.min_fee_price,
id_generator=kwargs.get('id_generator'),
)
@@ -58,10 +56,6 @@ class BaseTask(celery.Task):
)
def get_min_fee_limit(self, code):
return self.min_fee_limit
def create_session(self):
return BaseTask.session_func()

View File

@@ -11,6 +11,13 @@ ARG EXTRA_PIP_INDEX_URL=https://pip.grassrootseconomics.net
ARG EXTRA_PIP_ARGS=""
ARG PIP_INDEX_URL=https://pypi.org/simple
RUN --mount=type=cache,mode=0755,target=/root/.cache/pip \
pip install --index-url $PIP_INDEX_URL \
--pre \
--extra-index-url $EXTRA_PIP_INDEX_URL $EXTRA_PIP_ARGS \
cic-eth-aux-erc20-demurrage-token~=0.0.2a7
COPY *requirements.txt ./
RUN --mount=type=cache,mode=0755,target=/root/.cache/pip \
pip install --index-url $PIP_INDEX_URL \
@@ -18,7 +25,7 @@ RUN --mount=type=cache,mode=0755,target=/root/.cache/pip \
--extra-index-url $EXTRA_PIP_INDEX_URL $EXTRA_PIP_ARGS \
-r requirements.txt \
-r services_requirements.txt \
-r admin_requirements.txt
-r admin_requirements.txt
COPY . .
RUN python setup.py install
@@ -33,6 +40,8 @@ RUN chmod 755 *.sh
# # they can all be overridden by environment variables
# # to generate a list of environment variables from configuration, use: confini-dump -z <dir> (executable provided by confini package)
#COPY config/ /usr/local/etc/cic-eth/
COPY cic_eth/db/migrations/ /usr/local/share/cic-eth/alembic/
COPY crypto_dev_signer_config/ /usr/local/etc/crypto-dev-signer/
# TODO this kind of code sharing across projects should be discouraged...can we make util a library?
#COPY util/liveness/health.sh /usr/local/bin/health.sh
@@ -57,8 +66,9 @@ ENTRYPOINT []
## # they can all be overridden by environment variables
## # to generate a list of environment variables from configuration, use: confini-dump -z <dir> (executable provided by confini package)
#COPY config/ /usr/local/etc/cic-eth/
COPY cic_eth/db/migrations/ /usr/local/share/cic-eth/alembic/
#COPY scripts/ scripts/
#COPY cic_eth/db/migrations/ /usr/local/share/cic-eth/alembic/
#COPY crypto_dev_signer_config/ /usr/local/etc/crypto-dev-signer/
#COPY scripts/ scripts/
#
## TODO this kind of code sharing across projects should be discouraged...can we make util a library?
##COPY util/liveness/health.sh /usr/local/bin/health.sh

View File

@@ -1,7 +1,4 @@
celery==4.4.7
chainlib-eth>=0.0.10a20,<0.1.0
semver==2.13.0
chainlib-eth~=0.0.15
urlybird~=0.0.1
cic-eth-registry~=0.6.6
cic-types~=0.2.1a8
cic-eth-aux-erc20-demurrage-token~=0.0.3
urlybird~=0.0.1a2

View File

@@ -1,15 +1,16 @@
chainqueue~=0.0.6a4
chainsyncer[sql]~=0.0.7
chainqueue>=0.0.6a1,<0.1.0
chainsyncer[sql]>=0.0.7a3,<0.1.0
alembic==1.4.2
confini~=0.5.3
confini>=0.3.6rc4,<0.5.0
redis==3.5.3
hexathon~=0.1.0
hexathon~=0.0.1a8
pycryptodome==3.10.1
liveness~=0.0.1a7
eth-address-index~=0.2.4
eth-accounts-index~=0.1.2
erc20-faucet~=0.3.2
erc20-transfer-authorization~=0.3.6
sarafu-faucet~=0.0.7
moolb~=0.2.0
okota~=0.2.5
eth-address-index>=0.2.4a1,<0.3.0
eth-accounts-index>=0.1.2a3,<0.2.0
cic-eth-registry>=0.6.1a6,<0.7.0
erc20-faucet>=0.3.2a2,<0.4.0
erc20-transfer-authorization>=0.3.5a2,<0.4.0
sarafu-faucet>=0.0.7a2,<0.1.0
moolb~=0.1.1b2
okota>=0.2.4a6,<0.3.0

View File

@@ -1,7 +1,7 @@
[metadata]
name = cic-eth
#version = attr: cic_eth.version.__version_string__
version = 0.12.7
version = 0.12.5a2
description = CIC Network Ethereum interaction
author = Louis Holbrook
author_email = dev@holbrook.no

View File

@@ -6,5 +6,4 @@ pytest-redis==2.0.0
redis==3.5.3
eth-tester==0.5.0b3
py-evm==0.3.0a20
eth-erc20~=0.1.5
erc20-transfer-authorization~=0.3.6
eth-erc20~=0.1.2a2

View File

@@ -40,7 +40,6 @@ def test_filter_gas(
foo_token,
token_registry,
register_lookups,
register_tokens,
celery_session_worker,
cic_registry,
):
@@ -70,7 +69,7 @@ def test_filter_gas(
tx = Tx(tx_src, block=block)
tx.apply_receipt(rcpt)
t = fltr.filter(eth_rpc, block, tx, db_session=init_database)
assert t.get() == None
assert t == None
nonce_oracle = RPCNonceOracle(contract_roles['CONTRACT_DEPLOYER'], eth_rpc)
c = TokenUniqueSymbolIndex(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle)

View File

@@ -288,6 +288,7 @@ def test_fix_nonce(
init_database.commit()
logg.debug('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
txs = get_nonce_tx_local(default_chain_spec, 3, agent_roles['ALICE'], session=init_database)
ks = txs.keys()
assert len(ks) == 2

View File

@@ -191,17 +191,11 @@ def test_tokens(
break
api_param = str(uuid.uuid4())
fp = os.path.join(CallbackTask.mmap_path, api_param)
f = open(fp, 'wb+')
f.write(b'\x00')
f.close()
api = Api(str(default_chain_spec), queue=None, callback_param=api_param, callback_task='cic_eth.pytest.mock.callback.test_callback')
t = api.tokens(['BAR'], proof=[[bar_token_declaration]])
r = t.get()
logg.debug('rr {} {}'.format(r, t.children))
while True:
fp = os.path.join(CallbackTask.mmap_path, api_param)
try:

View File

@@ -35,26 +35,10 @@ from hexathon import strip_0x
from cic_eth.eth.gas import cache_gas_data
from cic_eth.error import OutOfGasError
from cic_eth.queue.tx import queue_create
from cic_eth.task import BaseTask
logg = logging.getLogger()
def test_task_gas_limit(
eth_rpc,
eth_signer,
default_chain_spec,
agent_roles,
celery_session_worker,
):
rpc = RPCConnection.connect(default_chain_spec, 'default')
gas_oracle = BaseTask().create_gas_oracle(rpc)
c = Gas(default_chain_spec, signer=eth_signer, gas_oracle=gas_oracle)
(tx_hash_hex, o) = c.create(agent_roles['ALICE'], agent_roles['BOB'], 10, tx_format=TxFormat.RLP_SIGNED)
tx = unpack(bytes.fromhex(strip_0x(o)), default_chain_spec)
assert (tx['gas'], BaseTask.min_fee_price)
def test_task_check_gas_ok(
default_chain_spec,
eth_rpc,

View File

@@ -143,7 +143,7 @@ def test_incoming_balance(
'converters': [],
}
b = balance_incoming([token_data], recipient, default_chain_spec.asdict())
assert b[0]['balance_incoming'] == 1000
assert b[0]['balance_incoming'] == 0
otx.readysend(session=init_database)
init_database.flush()
@@ -152,8 +152,8 @@ def test_incoming_balance(
otx.sent(session=init_database)
init_database.commit()
#b = balance_incoming([token_data], recipient, default_chain_spec.asdict())
#assert b[0]['balance_incoming'] == 1000
b = balance_incoming([token_data], recipient, default_chain_spec.asdict())
assert b[0]['balance_incoming'] == 1000
otx.success(block=1024, session=init_database)
init_database.commit()

View File

@@ -1,5 +1,7 @@
chainqueue~=0.0.6a4
crypto-dev-signer>=0.4.15rc2,<=0.4.15
chainqueue>=0.0.5a3,<0.1.0
cic-eth-registry>=0.6.1a6,<0.7.0
redis==3.5.3
hexathon~=0.1.0
hexathon~=0.0.1a8
pycryptodome==3.10.1
pyxdg==0.27

View File

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

View File

@@ -1,4 +1,4 @@
confini~=0.5.1
confini>=0.3.6rc4,<0.5.0
africastalking==1.2.3
SQLAlchemy==1.3.20
alembic==1.4.2

View File

@@ -1,22 +0,0 @@
# standard imports
# external imports
# local imports
class Guardianship:
guardians: list = []
@classmethod
def load_system_guardians(cls, guardians_file: str):
with open(guardians_file, 'r') as system_guardians:
cls.guardians = [line.strip() for line in system_guardians]
def is_system_guardian(self, phone_number: str):
"""
:param phone_number:
:type phone_number:
:return:
:rtype:
"""
return phone_number in self.guardians

View File

@@ -13,6 +13,7 @@ from cic_types.condiments import MetadataPointer
from cic_ussd.account.chain import Chain
from cic_ussd.account.transaction import from_wei
from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.translation import translation_for
logg = logging.getLogger(__name__)
@@ -96,3 +97,17 @@ def query_statement(blockchain_address: str, limit: int = 9):
callback_param=blockchain_address
)
cic_eth_api.list(address=blockchain_address, limit=limit)
def statement_transaction_set(preferred_language: str, transaction_reprs: list):
"""
:param preferred_language:
:type preferred_language:
:param transaction_reprs:
:type transaction_reprs:
:return:
:rtype:
"""
if not transaction_reprs:
return translation_for('helpers.no_transaction_history', preferred_language)
return ''.join(f'{transaction_repr}\n' for transaction_repr in transaction_reprs)

View File

@@ -15,6 +15,7 @@ from cic_ussd.cache import cache_data, cache_data_key, get_cached_data
from cic_ussd.error import CachedDataNotFoundError, SeppukuError
from cic_ussd.metadata.tokens import query_token_info, query_token_metadata
from cic_ussd.processor.util import wait_for_cache
from cic_ussd.translation import translation_for
logg = logging.getLogger(__file__)
@@ -325,3 +326,16 @@ def set_active_token(blockchain_address: str, token_symbol: str):
cache_data(key=key, data=token_symbol)
def token_list_set(preferred_language: str, token_data_reprs: list):
"""
:param preferred_language:
:type preferred_language:
:param token_data_reprs:
:type token_data_reprs:
:return:
:rtype:
"""
if not token_data_reprs:
return translation_for('helpers.no_tokens_list', preferred_language)
return ''.join(f'{token_data_repr}\n' for token_data_repr in token_data_reprs)

View File

@@ -55,6 +55,5 @@ def cache_data_key(identifier: Union[list, bytes], salt: MetadataPointer):
hash_object.update(identity)
else:
hash_object.update(identifier)
if salt != MetadataPointer.NONE:
hash_object.update(salt.value.encode(encoding="utf-8"))
hash_object.update(salt.value.encode(encoding="utf-8"))
return hash_object.digest().hex()

View File

@@ -171,7 +171,7 @@ class Account(SessionBase):
return check_password_hash(password, self.password_hash)
def create(chain_str: str, phone_number: str, session: Session, preferred_language: str):
def create(chain_str: str, phone_number: str, session: Session):
"""
:param chain_str:
:type chain_str:
@@ -179,14 +179,12 @@ def create(chain_str: str, phone_number: str, session: Session, preferred_langua
:type phone_number:
:param session:
:type session:
:param preferred_language:
:type preferred_language:
:return:
:rtype:
"""
api = Api(callback_task='cic_ussd.tasks.callback_handler.account_creation_callback',
callback_queue='cic-ussd',
callback_param=preferred_language,
callback_param='',
chain_str=chain_str)
task_uuid = api.create_account().id
TaskTracker.add(session=session, task_uuid=task_uuid)

View File

@@ -2,441 +2,417 @@
"ussd_menu": {
"1": {
"description": "Entry point for users to select their preferred language.",
"display_key": "ussd.initial_language_selection",
"display_key": "ussd.kenya.initial_language_selection",
"name": "initial_language_selection",
"parent": null
},
"2": {
"description": "Entry point for users to enter a pin to secure their account.",
"display_key": "ussd.initial_pin_entry",
"display_key": "ussd.kenya.initial_pin_entry",
"name": "initial_pin_entry",
"parent": null
},
"3": {
"description": "Pin confirmation entry menu.",
"display_key": "ussd.initial_pin_confirmation",
"display_key": "ussd.kenya.initial_pin_confirmation",
"name": "initial_pin_confirmation",
"parent": "initial_pin_entry"
},
"4": {
"description": "The signup process has been initiated and the account is being created.",
"display_key": "ussd.account_creation_prompt",
"display_key": "ussd.kenya.account_creation_prompt",
"name": "account_creation_prompt",
"parent": null
},
"5": {
"description": "Entry point for activated users.",
"display_key": "ussd.start",
"display_key": "ussd.kenya.start",
"name": "start",
"parent": null
},
"6": {
"description": "Given name entry menu.",
"display_key": "ussd.enter_given_name",
"display_key": "ussd.kenya.enter_given_name",
"name": "enter_given_name",
"parent": "metadata_management"
},
"7": {
"description": "Family name entry menu.",
"display_key": "ussd.enter_family_name",
"display_key": "ussd.kenya.enter_family_name",
"name": "enter_family_name",
"parent": "metadata_management"
},
"8": {
"description": "Gender entry menu.",
"display_key": "ussd.enter_gender",
"display_key": "ussd.kenya.enter_gender",
"name": "enter_gender",
"parent": "metadata_management"
},
"9": {
"description": "Age entry menu.",
"display_key": "ussd.enter_gender",
"display_key": "ussd.kenya.enter_gender",
"name": "enter_gender",
"parent": "metadata_management"
},
"10": {
"description": "Location entry menu.",
"display_key": "ussd.enter_location",
"display_key": "ussd.kenya.enter_location",
"name": "enter_location",
"parent": "metadata_management"
},
"11": {
"description": "Products entry menu.",
"display_key": "ussd.enter_products",
"display_key": "ussd.kenya.enter_products",
"name": "enter_products",
"parent": "metadata_management"
},
"12": {
"description": "Entry point for activated users.",
"display_key": "ussd.start",
"display_key": "ussd.kenya.start",
"name": "start",
"parent": null
},
"13": {
"description": "Send Token recipient entry.",
"display_key": "ussd.enter_transaction_recipient",
"display_key": "ussd.kenya.enter_transaction_recipient",
"name": "enter_transaction_recipient",
"parent": "start"
},
"14": {
"description": "Send Token amount prompt menu.",
"display_key": "ussd.enter_transaction_amount",
"display_key": "ussd.kenya.enter_transaction_amount",
"name": "enter_transaction_amount",
"parent": "start"
},
"15": {
"description": "Pin entry for authorization to send token.",
"display_key": "ussd.transaction_pin_authorization",
"display_key": "ussd.kenya.transaction_pin_authorization",
"name": "transaction_pin_authorization",
"parent": "start"
},
"16": {
"description": "Manage account menu.",
"display_key": "ussd.account_management",
"display_key": "ussd.kenya.account_management",
"name": "account_management",
"parent": "start"
},
"17": {
"description": "Manage metadata menu.",
"display_key": "ussd.metadata_management",
"display_key": "ussd.kenya.metadata_management",
"name": "metadata_management",
"parent": "start"
},
"18": {
"description": "Manage user's preferred language menu.",
"display_key": "ussd.select_preferred_language",
"display_key": "ussd.kenya.select_preferred_language",
"name": "select_preferred_language",
"parent": "account_management"
},
"19": {
"description": "Retrieve mini-statement menu.",
"display_key": "ussd.mini_statement_pin_authorization",
"display_key": "ussd.kenya.mini_statement_pin_authorization",
"name": "mini_statement_pin_authorization",
"parent": "account_management"
},
"20": {
"description": "Manage user's pin menu.",
"display_key": "ussd.enter_current_pin",
"display_key": "ussd.kenya.enter_current_pin",
"name": "enter_current_pin",
"parent": "account_management"
},
"21": {
"description": "New pin entry menu.",
"display_key": "ussd.enter_new_pin",
"display_key": "ussd.kenya.enter_new_pin",
"name": "enter_new_pin",
"parent": "account_management"
},
"22": {
"description": "Pin entry menu.",
"display_key": "ussd.display_metadata_pin_authorization",
"display_key": "ussd.kenya.display_metadata_pin_authorization",
"name": "display_metadata_pin_authorization",
"parent": "start"
},
"23": {
"description": "Exit menu.",
"display_key": "ussd.exit",
"display_key": "ussd.kenya.exit",
"name": "exit",
"parent": null
},
"24": {
"description": "Invalid menu option.",
"display_key": "ussd.exit_invalid_menu_option",
"display_key": "ussd.kenya.exit_invalid_menu_option",
"name": "exit_invalid_menu_option",
"parent": null
},
"25": {
"description": "Pin policy violation.",
"display_key": "ussd.exit_invalid_pin",
"display_key": "ussd.kenya.exit_invalid_pin",
"name": "exit_invalid_pin",
"parent": null
},
"26": {
"description": "Pin mismatch. New pin and the new pin confirmation do not match",
"display_key": "ussd.exit_pin_mismatch",
"display_key": "ussd.kenya.exit_pin_mismatch",
"name": "exit_pin_mismatch",
"parent": null
},
"27": {
"description": "Ussd pin blocked Menu",
"display_key": "ussd.exit_pin_blocked",
"display_key": "ussd.kenya.exit_pin_blocked",
"name": "exit_pin_blocked",
"parent": null
},
"28": {
"description": "Key params missing in request.",
"display_key": "ussd.exit_invalid_request",
"display_key": "ussd.kenya.exit_invalid_request",
"name": "exit_invalid_request",
"parent": null
},
"29": {
"description": "The user did not select a choice.",
"display_key": "ussd.exit_invalid_input",
"display_key": "ussd.kenya.exit_invalid_input",
"name": "exit_invalid_input",
"parent": null
},
"30": {
"description": "Exit following unsuccessful transaction due to insufficient account balance.",
"display_key": "ussd.exit_insufficient_balance",
"display_key": "ussd.kenya.exit_insufficient_balance",
"name": "exit_insufficient_balance",
"parent": null
},
"31": {
"description": "Exit following a successful transaction.",
"display_key": "ussd.exit_successful_transaction",
"display_key": "ussd.kenya.exit_successful_transaction",
"name": "exit_successful_transaction",
"parent": null
},
"32": {
"description": "End of a menu flow.",
"display_key": "ussd.complete",
"display_key": "ussd.kenya.complete",
"name": "complete",
"parent": null
},
"33": {
"description": "Pin entry menu to view account balances.",
"display_key": "ussd.account_balances_pin_authorization",
"display_key": "ussd.kenya.account_balances_pin_authorization",
"name": "account_balances_pin_authorization",
"parent": "account_management"
},
"34": {
"description": "Pin entry menu to view account statement.",
"display_key": "ussd.account_statement_pin_authorization",
"display_key": "ussd.kenya.account_statement_pin_authorization",
"name": "account_statement_pin_authorization",
"parent": "account_management"
},
"35": {
"description": "Menu to display account balances.",
"display_key": "ussd.account_balances",
"display_key": "ussd.kenya.account_balances",
"name": "account_balances",
"parent": "account_management"
},
"36": {
"description": "Menu to display first set of transactions in statement.",
"display_key": "ussd.first_transaction_set",
"display_key": "ussd.kenya.first_transaction_set",
"name": "first_transaction_set",
"parent": "account_management"
"parent": null
},
"37": {
"description": "Menu to display middle set of transactions in statement.",
"display_key": "ussd.middle_transaction_set",
"display_key": "ussd.kenya.middle_transaction_set",
"name": "middle_transaction_set",
"parent": null
},
"38": {
"description": "Menu to display last set of transactions in statement.",
"display_key": "ussd.last_transaction_set",
"display_key": "ussd.kenya.last_transaction_set",
"name": "last_transaction_set",
"parent": null
},
"39": {
"description": "Menu to instruct users to call the office.",
"display_key": "ussd.help",
"display_key": "ussd.kenya.help",
"name": "help",
"parent": null
},
"40": {
"description": "Menu to display a user's entire profile",
"display_key": "ussd.display_user_metadata",
"display_key": "ussd.kenya.display_user_metadata",
"name": "display_user_metadata",
"parent": "metadata_management"
},
"41": {
"description": "The recipient is not in the system",
"display_key": "ussd.exit_invalid_recipient",
"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.name_edit_pin_authorization",
"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.gender_edit_pin_authorization",
"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.location_edit_pin_authorization",
"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.products_edit_pin_authorization",
"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.new_pin_confirmation",
"display_key": "ussd.kenya.new_pin_confirmation",
"name": "new_pin_confirmation",
"parent": "metadata_management"
},
"47": {
"description": "Year of birth entry menu.",
"display_key": "ussd.enter_date_of_birth",
"display_key": "ussd.kenya.enter_date_of_birth",
"name": "enter_date_of_birth",
"parent": "metadata_management"
},
"48": {
"description": "Pin entry menu for changing year of birth data.",
"display_key": "ussd.dob_edit_pin_authorization",
"display_key": "ussd.kenya.dob_edit_pin_authorization",
"name": "dob_edit_pin_authorization",
"parent": "metadata_management"
},
"49": {
"description": "Menu to display first set of tokens in the account's token list.",
"display_key": "ussd.first_account_tokens_set",
"display_key": "ussd.kenya.first_account_tokens_set",
"name": "first_account_tokens_set",
"parent": "start"
"parent": null
},
"50": {
"description": "Menu to display middle set of tokens in the account's token list.",
"display_key": "ussd.middle_account_tokens_set",
"display_key": "ussd.kenya.middle_account_tokens_set",
"name": "middle_account_tokens_set",
"parent": null
},
"51": {
"description": "Menu to display last set of tokens in the account's token list.",
"display_key": "ussd.last_account_tokens_set",
"display_key": "ussd.kenya.last_account_tokens_set",
"name": "last_account_tokens_set",
"parent": null
},
"52": {
"description": "Pin entry menu for setting an active token.",
"display_key": "ussd.token_selection_pin_authorization",
"display_key": "ussd.kenya.token_selection_pin_authorization",
"name": "token_selection_pin_authorization",
"parent": "first_account_tokens_set"
"parent": null
},
"53": {
"description": "Exit following a successful active token setting.",
"display_key": "ussd.exit_successful_token_selection",
"display_key": "ussd.kenya.exit_successful_token_selection",
"name": "exit_successful_token_selection",
"parent": null
},
"54": {
"description": "Pin management menu for operations related to an account's pin.",
"display_key": "ussd.pin_management",
"display_key": "ussd.kenya.pin_management",
"name": "pin_management",
"parent": "start"
},
"55": {
"description": "Phone number entry for account whose pin is being reset.",
"display_key": "ussd.reset_guarded_pin",
"display_key": "ussd.kenya.reset_guarded_pin",
"name": "reset_guarded_pin",
"parent": "pin_management"
},
"56": {
"description": "Pin entry for initiating request to reset an account's pin.",
"display_key": "ussd.reset_guarded_pin_authorization",
"display_key": "ussd.kenya.reset_guarded_pin_authorization",
"name": "reset_guarded_pin_authorization",
"parent": "pin_management"
},
"57": {
"description": "Exit menu following successful pin reset initiation.",
"display_key": "ussd.exit_pin_reset_initiated_success",
"display_key": "ussd.kenya.exit_pin_reset_initiated_success",
"name": "exit_pin_reset_initiated_success",
"parent": "pin_management"
},
"58": {
"description": "Exit menu in the event that an account is not a set guardian.",
"display_key": "ussd.exit_not_authorized_for_pin_reset",
"display_key": "ussd.kenya.exit_not_authorized_for_pin_reset",
"name": "exit_not_authorized_for_pin_reset",
"parent": "pin_management"
},
"59": {
"description": "Pin guard menu for handling guardianship operations.",
"display_key": "ussd.guard_pin",
"display_key": "ussd.kenya.guard_pin",
"name": "guard_pin",
"parent": "pin_management"
},
"60": {
"description": "Pin entry to display a list of set guardians.",
"display_key": "ussd.guardian_list_pin_authorization",
"display_key": "ussd.kenya.guardian_list_pin_authorization",
"name": "guardian_list_pin_authorization",
"parent": "guard_pin"
},
"61": {
"description": "Menu to display list of set guardians.",
"display_key": "ussd.guardian_list",
"display_key": "ussd.kenya.guardian_list",
"name": "guardian_list",
"parent": "guard_pin"
},
"62": {
"description": "Phone number entry to add an account as a guardian to reset pin.",
"display_key": "ussd.add_guardian",
"display_key": "ussd.kenya.add_guardian",
"name": "add_guardian",
"parent": "guard_pin"
},
"63": {
"description": "Pin entry to confirm addition of an account as a guardian.",
"display_key": "ussd.add_guardian_pin_authorization",
"display_key": "ussd.kenya.add_guardian_pin_authorization",
"name": "add_guardian_pin_authorization",
"parent": "guard_pin"
},
"64": {
"description": "Exit menu when an account is successfully added as pin reset guardian.",
"display_key": "ussd.exit_guardian_addition_success",
"display_key": "ussd.kenya.exit_guardian_addition_success",
"name": "exit_guardian_addition_success",
"parent": "guard_pin"
},
"65": {
"description": "Phone number entry to remove an account as a guardian to reset pin.",
"display_key": "ussd.remove_guardian",
"display_key": "ussd.kenya.remove_guardian",
"name": "remove_guardian",
"parent": "guard_pin"
},
"66": {
"description": "Pin entry to confirm removal of an account as a guardian.",
"display_key": "ussd.remove_guardian_pin_authorization",
"display_key": "ussd.kenya.remove_guardian_pin_authorization",
"name": "remove_guardian_pin_authorization",
"parent": "guard_pin"
},
"67": {
"description": "Exit menu when an account is successfully removed as pin reset guardian.",
"display_key": "ussd.exit_guardian_removal_success",
"display_key": "ussd.kenya.exit_guardian_removal_success",
"name": "exit_guardian_removal_success",
"parent": "guard_pin"
},
"68": {
"description": "Exit menu when invalid phone number entry for guardian addition.",
"display_key": "ussd.exit_invalid_guardian_addition",
"description": "Exit menu when invalid phone number entry for guardian addition. ",
"display_key": "ussd.kenya.exit_invalid_guardian_addition",
"name": "exit_invalid_guardian_addition",
"parent": "guard_pin"
},
"69": {
"description": "Exit menu when invalid phone number entry for guardian removal.",
"display_key": "ussd.exit_invalid_guardian_removal",
"description": "Exit menu when invalid phone number entry for guardian removal. ",
"display_key": "ussd.kenya.exit_invalid_guardian_removal",
"name": "exit_invalid_guardian_removal",
"parent": "guard_pin"
},
"70": {
"description": "Menu to display middle set of languages to select.",
"display_key": "ussd.initial_middle_language_set",
"name": "initial_middle_language_set",
"parent": null
},
"71": {
"description": "Menu to display last set of languages to select.",
"display_key": "ussd.initial_last_language_set",
"name": "initial_last_language_set",
"parent": null
},
"72": {
"description": "Menu to display middle set of languages to select.",
"display_key": "ussd.middle_language_set",
"name": "middle_language_set",
"parent": null
},
"73": {
"description": "Menu to display last set of languages to select.",
"display_key": "ussd.last_language_set",
"name": "last_language_set",
"parent": null
}
}
}

View File

@@ -19,33 +19,34 @@ from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.account.statement import (
get_cached_statement,
parse_statement_transactions,
query_statement)
query_statement,
statement_transaction_set
)
from cic_ussd.account.tokens import (create_account_tokens_list,
get_active_token_symbol,
get_cached_token_data,
get_cached_token_symbol_list,
get_cached_token_data_list,
parse_token_list)
parse_token_list,
token_list_set)
from cic_ussd.account.transaction import from_wei, to_wei
from cic_ussd.cache import cache_data_key, cache_data, get_cached_data
from cic_ussd.cache import cache_data_key, cache_data
from cic_ussd.db.models.account import Account
from cic_ussd.metadata import PersonMetadata
from cic_ussd.phone_number import Support
from cic_ussd.processor.util import parse_person_metadata, ussd_menu_list, wait_for_session_data
from cic_ussd.processor.util import parse_person_metadata
from cic_ussd.session.ussd_session import save_session_data
from cic_ussd.state_machine.logic.language import preferred_langauge_from_selection
from cic_ussd.translation import translation_for
from sqlalchemy.orm.session import Session
logg = logging.getLogger(__file__)
logg = logging.getLogger(__name__)
class MenuProcessor:
def __init__(self, account: Account, display_key: str, menu_name: str, session: Session, ussd_session: dict):
self.account = account
self.display_key = display_key
if account:
self.identifier = bytes.fromhex(self.account.blockchain_address)
self.identifier = bytes.fromhex(self.account.blockchain_address)
self.menu_name = menu_name
self.session = session
self.ussd_session = ussd_session
@@ -88,29 +89,36 @@ class MenuProcessor:
:rtype:
"""
cached_statement = get_cached_statement(self.account.blockchain_address)
transaction_sets = []
if cached_statement:
statement = json.loads(cached_statement)
statement_transactions = parse_statement_transactions(statement)
transaction_sets = [statement_transactions[tx:tx + 3] for tx in range(0, len(statement_transactions), 3)]
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
no_transaction_history = statement_transaction_set(preferred_language, transaction_sets)
first_transaction_set = no_transaction_history
middle_transaction_set = no_transaction_history
last_transaction_set = no_transaction_history
if transaction_sets:
first_transaction_set = statement_transaction_set(preferred_language, transaction_sets[0])
if len(transaction_sets) >= 2:
middle_transaction_set = statement_transaction_set(preferred_language, transaction_sets[1])
if len(transaction_sets) >= 3:
last_transaction_set = statement_transaction_set(preferred_language, transaction_sets[2])
statement_list = []
if cached_statement:
statement_list = parse_statement_transactions(statement=json.loads(cached_statement))
fallback = translation_for('helpers.no_transaction_history', preferred_language)
transaction_sets = ussd_menu_list(fallback=fallback, menu_list=statement_list, split=3)
if self.display_key == 'ussd.first_transaction_set':
if self.display_key == 'ussd.kenya.first_transaction_set':
return translation_for(
self.display_key, preferred_language, first_transaction_set=transaction_sets[0]
self.display_key, preferred_language, first_transaction_set=first_transaction_set
)
if self.display_key == 'ussd.middle_transaction_set':
if self.display_key == 'ussd.kenya.middle_transaction_set':
return translation_for(
self.display_key, preferred_language, middle_transaction_set=transaction_sets[1]
self.display_key, preferred_language, middle_transaction_set=middle_transaction_set
)
if self.display_key == 'ussd.last_transaction_set':
if self.display_key == 'ussd.kenya.last_transaction_set':
return translation_for(
self.display_key, preferred_language, last_transaction_set=transaction_sets[2]
self.display_key, preferred_language, last_transaction_set=last_transaction_set
)
def add_guardian_pin_authorization(self):
@@ -121,7 +129,7 @@ class MenuProcessor:
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
set_guardians = self.account.get_guardians()[:3]
set_guardians = self.account.get_guardians()
if set_guardians:
guardians_list = ''
guardians_list_header = translation_for('helpers.guardians_list_header', preferred_language)
@@ -137,30 +145,36 @@ class MenuProcessor:
def account_tokens(self) -> str:
cached_token_data_list = get_cached_token_data_list(self.account.blockchain_address)
token_data_list = parse_token_list(cached_token_data_list)
token_list_sets = [token_data_list[tds:tds + 3] for tds in range(0, len(token_data_list), 3)]
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
no_token_list = token_list_set(preferred_language, [])
first_account_tokens_set = no_token_list
middle_account_tokens_set = no_token_list
last_account_tokens_set = no_token_list
if token_list_sets:
data = {
'account_tokens_list': cached_token_data_list
}
save_session_data(data=data, queue='cic-ussd', session=self.session, ussd_session=self.ussd_session)
first_account_tokens_set = token_list_set(preferred_language, token_list_sets[0])
fallback = translation_for('helpers.no_tokens_list', preferred_language)
token_list_sets = ussd_menu_list(fallback=fallback, menu_list=token_data_list, split=3)
data = {
'account_tokens_list': cached_token_data_list
}
save_session_data(data=data, queue='cic-ussd', session=self.session, ussd_session=self.ussd_session)
if self.display_key == 'ussd.first_account_tokens_set':
if len(token_list_sets) >= 2:
middle_account_tokens_set = token_list_set(preferred_language, token_list_sets[1])
if len(token_list_sets) >= 3:
last_account_tokens_set = token_list_set(preferred_language, token_list_sets[2])
if self.display_key == 'ussd.kenya.first_account_tokens_set':
return translation_for(
self.display_key, preferred_language, first_account_tokens_set=token_list_sets[0]
self.display_key, preferred_language, first_account_tokens_set=first_account_tokens_set
)
if self.display_key == 'ussd.middle_account_tokens_set':
if self.display_key == 'ussd.kenya.middle_account_tokens_set':
return translation_for(
self.display_key, preferred_language, middle_account_tokens_set=token_list_sets[1]
self.display_key, preferred_language, middle_account_tokens_set=middle_account_tokens_set
)
if self.display_key == 'ussd.last_account_tokens_set':
if self.display_key == 'ussd.kenya.last_account_tokens_set':
return translation_for(
self.display_key, preferred_language, last_account_tokens_set=token_list_sets[2]
self.display_key, preferred_language, last_account_tokens_set=last_account_tokens_set
)
def help(self) -> str:
@@ -208,7 +222,7 @@ class MenuProcessor:
remaining_attempts = 3
remaining_attempts -= self.account.failed_pin_attempts
retry_pin_entry = translation_for(
'ussd.retry_pin_entry', preferred_language, remaining_attempts=remaining_attempts
'ussd.kenya.retry_pin_entry', preferred_language, remaining_attempts=remaining_attempts
)
return translation_for(
f'{self.display_key}.retry', preferred_language, retry_pin_entry=retry_pin_entry
@@ -224,38 +238,6 @@ class MenuProcessor:
guardian = Account.get_by_phone_number(guardian_phone_number, self.session)
return guardian.standard_metadata_id()
def language(self):
key = cache_data_key('system:languages'.encode('utf-8'), MetadataPointer.NONE)
cached_system_languages = get_cached_data(key)
language_list: list = json.loads(cached_system_languages)
if self.account:
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
else:
preferred_language = i18n.config.get('fallback')
fallback = translation_for('helpers.no_language_list', preferred_language)
language_list_sets = ussd_menu_list(fallback=fallback, menu_list=language_list, split=3)
if self.display_key in ['ussd.initial_language_selection', 'ussd.select_preferred_language']:
return translation_for(
self.display_key, preferred_language, first_language_set=language_list_sets[0]
)
if 'middle_language_set' in self.display_key:
return translation_for(
self.display_key, preferred_language, middle_language_set=language_list_sets[1]
)
if 'last_language_set' in self.display_key:
return translation_for(
self.display_key, preferred_language, last_language_set=language_list_sets[2]
)
def account_creation_prompt(self):
preferred_language = preferred_langauge_from_selection(self.ussd_session.get('user_input'))
return translation_for(self.display_key, preferred_language)
def reset_guarded_pin_authorization(self):
guarded_account_information = self.guarded_account_metadata()
return self.pin_authorization(guarded_account_information=guarded_account_information)
@@ -399,9 +381,8 @@ class MenuProcessor:
)
def exit_invalid_menu_option(self):
if self.account:
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
else:
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
return translation_for(self.display_key, preferred_language, support_phone=Support.phone_number)
@@ -409,7 +390,7 @@ class MenuProcessor:
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
return translation_for('ussd.exit_pin_blocked', preferred_language, support_phone=Support.phone_number)
return translation_for('ussd.kenya.exit_pin_blocked', preferred_language, support_phone=Support.phone_number)
def exit_successful_token_selection(self) -> str:
selected_token = self.ussd_session.get('data').get('selected_token')
@@ -464,9 +445,6 @@ def response(account: Account, display_key: str, menu_name: str, session: Sessio
"""
menu_processor = MenuProcessor(account, display_key, menu_name, session, ussd_session)
if menu_name == 'account_creation_prompt':
return menu_processor.account_creation_prompt()
if menu_name == 'start':
return menu_processor.start_menu()
@@ -524,9 +502,6 @@ def response(account: Account, display_key: str, menu_name: str, session: Sessio
if 'account_tokens_set' in menu_name:
return menu_processor.account_tokens()
if 'language' in menu_name:
return menu_processor.language()
if menu_name == 'display_user_metadata':
return menu_processor.person_metadata()
@@ -540,4 +515,5 @@ def response(account: Account, display_key: str, menu_name: str, session: Sessio
return menu_processor.exit_successful_token_selection()
preferred_language = get_cached_preferred_language(account.blockchain_address)
return translation_for(display_key, preferred_language)

View File

@@ -8,7 +8,7 @@ from sqlalchemy.orm.session import Session
from tinydb.table import Document
# local imports
from cic_ussd.db.models.account import Account
from cic_ussd.db.models.account import Account, create
from cic_ussd.db.models.base import SessionBase
from cic_ussd.db.models.ussd_session import UssdSession
from cic_ussd.menu.ussd_menu import UssdMenu
@@ -16,6 +16,7 @@ from cic_ussd.processor.menu import response
from cic_ussd.processor.util import latest_input, resume_last_ussd_session
from cic_ussd.session.ussd_session import create_or_update_session, persist_ussd_session
from cic_ussd.state_machine import UssdStateMachine
from cic_ussd.translation import translation_for
from cic_ussd.validator import is_valid_response
@@ -35,6 +36,9 @@ def handle_menu(account: Account, session: Session) -> Document:
last_ussd_session = UssdSession.last_ussd_session(account.phone_number, session)
if last_ussd_session:
return resume_last_ussd_session(last_ussd_session.state)
elif not account.has_preferred_language():
return UssdMenu.find_by_name('initial_language_selection')
else:
return UssdMenu.find_by_name('initial_pin_entry')
@@ -67,13 +71,16 @@ def get_menu(account: Account,
return UssdMenu.find_by_name(state)
def handle_menu_operations(external_session_id: str,
def handle_menu_operations(chain_str: str,
external_session_id: str,
phone_number: str,
queue: str,
service_code: str,
session,
user_input: str):
"""
:param chain_str:
:type chain_str:
:param external_session_id:
:type external_session_id:
:param phone_number:
@@ -93,38 +100,10 @@ def handle_menu_operations(external_session_id: str,
account: Account = Account.get_by_phone_number(phone_number, session)
if account:
return handle_account_menu_operations(account, external_session_id, queue, session, service_code, user_input)
else:
return handle_no_account_menu_operations(
account, external_session_id, phone_number, queue, session, service_code, user_input)
def handle_no_account_menu_operations(account: Optional[Account],
external_session_id: str,
phone_number: str,
queue: str,
session: Session,
service_code: str,
user_input: str):
"""
:param account:
:type account:
:param external_session_id:
:type external_session_id:
:param phone_number:
:type phone_number:
:param queue:
:type queue:
:param session:
:type session:
:param service_code:
:type service_code:
:param user_input:
:type user_input:
:return:
:rtype:
"""
menu = UssdMenu.find_by_name('initial_language_selection')
ussd_session = create_or_update_session(
create(chain_str, phone_number, session)
menu = UssdMenu.find_by_name('account_creation_prompt')
preferred_language = i18n.config.get('fallback')
create_or_update_session(
external_session_id=external_session_id,
msisdn=phone_number,
service_code=service_code,
@@ -132,20 +111,7 @@ def handle_no_account_menu_operations(account: Optional[Account],
session=session,
user_input=user_input)
persist_ussd_session(external_session_id, queue)
last_ussd_session: UssdSession = UssdSession.last_ussd_session(phone_number, session)
if last_ussd_session:
if not user_input:
menu = resume_last_ussd_session(last_ussd_session.state)
else:
session = SessionBase.bind_session(session)
state = next_state(account, session, user_input, last_ussd_session.to_json())
menu = UssdMenu.find_by_name(state)
return response(account=account,
display_key=menu.get('display_key'),
menu_name=menu.get('name'),
session=session,
ussd_session=ussd_session.to_json())
return translation_for('ussd.kenya.account_creation_prompt', preferred_language)
def handle_account_menu_operations(account: Account,
@@ -186,12 +152,15 @@ def handle_account_menu_operations(account: Account,
if last_ussd_session:
ussd_session = create_or_update_session(
external_session_id, phone_number, service_code, user_input, menu.get('name'), session,
last_ussd_session.data)
last_ussd_session.data
)
else:
ussd_session = create_or_update_session(
external_session_id, phone_number, service_code, user_input, menu.get('name'), session, {})
external_session_id, phone_number, service_code, user_input, menu.get('name'), session, None
)
menu_response = response(
account, menu.get('display_key'), menu.get('name'), session, ussd_session.to_json())
account, menu.get('display_key'), menu.get('name'), session, ussd_session.to_json()
)
if not is_valid_response(menu_response):
raise ValueError(f'Invalid response: {response}')
persist_ussd_session(external_session_id, queue)

View File

@@ -3,7 +3,7 @@ import datetime
import json
import logging
import time
from typing import List, Union
from typing import Union
# external imports
from cic_types.condiments import MetadataPointer
@@ -21,7 +21,9 @@ logg = logging.getLogger(__file__)
def latest_input(user_input: str) -> str:
"""
:param user_input:
:type user_input:
:return:
:rtype:
"""
return user_input.split('*')[-1]
@@ -83,27 +85,6 @@ def resume_last_ussd_session(last_state: str) -> Document:
return UssdMenu.find_by_name(last_state)
def ussd_menu_list(fallback: str, menu_list: list, split: int = 3) -> List[str]:
"""
:param fallback:
:type fallback:
:param menu_list:
:type menu_list:
:param split:
:type split:
:return:
:rtype:
"""
menu_list_sets = [menu_list[item:item + split] for item in range(0, len(menu_list), split)]
menu_list_reprs = []
for i in range(split):
try:
menu_list_reprs.append(''.join(f'{list_set_item}\n' for list_set_item in menu_list_sets[i]).rstrip('\n'))
except IndexError:
menu_list_reprs.append(fallback)
return menu_list_reprs
def wait_for_cache(identifier: Union[list, bytes], resource_name: str, salt: MetadataPointer, interval: int = 1, max_retry: int = 5):
"""
:param identifier:
@@ -151,28 +132,17 @@ def wait_for_session_data(resource_name: str, session_data_key: str, ussd_sessio
:return:
:rtype:
"""
data = ussd_session.get('data')
data_poller = 0
while not data:
logg.debug(f'Waiting for data object on ussd session: {ussd_session.get("external_session_id")}')
logg.debug(f'Data poller at: {data_poller}. Checking again after: {interval} secs...')
session_data = ussd_session.get('data').get(session_data_key)
counter = 0
while session_data is None:
logg.debug(f'Waiting for: {resource_name}. Checking after: {interval} ...')
time.sleep(interval)
data_poller += 1
if data:
logg.debug(f'Data object found, proceeding to poll for: {session_data_key}')
counter += 1
session_data = ussd_session.get('data').get(session_data_key)
if session_data is not None:
logg.debug(f'{resource_name} now available.')
break
if data:
session_data_poller = 0
session_data = data.get(session_data_key)
while not session_data_key:
logg.debug(
f'Session data poller at: {data_poller} with max retry at: {max_retry}. Checking again after: {interval} secs...')
time.sleep(interval)
session_data_poller += 1
if session_data:
logg.debug(f'{resource_name} now available.')
else:
if counter == max_retry:
logg.debug(f'Could not find: {resource_name} within: {max_retry}')
break
elif session_data_poller >= max_retry:
logg.debug(f'Could not find data object within: {max_retry}')

View File

@@ -20,7 +20,6 @@ from cic_ussd.db import dsn_from_config
from cic_ussd.db.models.base import SessionBase
from cic_ussd.phone_number import Support
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
from cic_ussd.translation import generate_locale_files
from cic_ussd.validator import validate_presence
logging.basicConfig(level=logging.WARNING)
@@ -84,10 +83,6 @@ if key_file_path:
validate_presence(path=key_file_path)
Signer.key_file_path = key_file_path
generate_locale_files(locale_dir=config.get('LOCALE_PATH'),
schema_file_path=config.get('SCHEMA_FILE_PATH'),
translation_builder_path=config.get('LOCALE_FILE_BUILDERS'))
# set up translations
i18n.load_path.append(config.get('LOCALE_PATH'))
i18n.set('fallback', config.get('LOCALE_FALLBACK'))

View File

@@ -18,7 +18,6 @@ from cic_types.ext.metadata.signer import Signer
# local imports
from cic_ussd.account.chain import Chain
from cic_ussd.account.guardianship import Guardianship
from cic_ussd.account.tokens import query_default_token
from cic_ussd.cache import cache_data, cache_data_key, Cache
from cic_ussd.db import dsn_from_config
@@ -34,7 +33,7 @@ from cic_ussd.processor.ussd import handle_menu_operations
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.translation import generate_locale_files, Languages, translation_for
from cic_ussd.translation import translation_for
from cic_ussd.validator import check_ip, check_request_content_length, validate_phone_number, validate_presence
args = exportable_parser.parse_args()
@@ -57,6 +56,10 @@ SessionBase.connect(data_source_name,
pool_size=int(config.get('DATABASE_POOL_SIZE')),
debug=config.true('DATABASE_DEBUG'))
# set up translations
i18n.load_path.append(config.get('LOCALE_PATH'))
i18n.set('fallback', config.get('LOCALE_FALLBACK'))
# set Fernet key
PasswordEncoder.set_key(config.get('APP_PASSWORD_PEPPER'))
@@ -118,22 +121,6 @@ valid_service_codes = config.get('USSD_SERVICE_CODE').split(",")
E164Format.region = config.get('E164_REGION')
Support.phone_number = config.get('OFFICE_SUPPORT_PHONE')
validate_presence(config.get('SYSTEM_GUARDIANS_FILE'))
Guardianship.load_system_guardians(config.get('SYSTEM_GUARDIANS_FILE'))
generate_locale_files(locale_dir=config.get('LOCALE_PATH'),
schema_file_path=config.get('SCHEMA_FILE_PATH'),
translation_builder_path=config.get('LOCALE_FILE_BUILDERS'))
# set up translations
i18n.load_path.append(config.get('LOCALE_PATH'))
i18n.set('fallback', config.get('LOCALE_FALLBACK'))
validate_presence(config.get('LANGUAGES_FILE'))
Languages.load_languages_dict(config.get('LANGUAGES_FILE'))
languages = Languages()
languages.cache_system_languages()
def application(env, start_response):
"""Loads python code for application to be accessible over web server
@@ -188,7 +175,7 @@ def application(env, start_response):
if service_code not in valid_service_codes:
response = translation_for(
'ussd.invalid_service_code',
'ussd.kenya.invalid_service_code',
i18n.config.get('fallback'),
valid_service_code=valid_service_codes[0]
)
@@ -202,7 +189,9 @@ def application(env, start_response):
return []
logg.debug('session {} started for {}'.format(external_session_id, phone_number))
response = handle_menu_operations(external_session_id, phone_number, args.q, service_code, session, user_input)
response = handle_menu_operations(
chain_str, external_session_id, phone_number, args.q, service_code, session, user_input
)
response_bytes, headers = with_content_headers(headers, response)
start_response('200 OK,', headers)
session.commit()

View File

@@ -11,20 +11,46 @@ from cic_types.models.person import get_contact_data_from_vcard, generate_vcard_
# local imports
from cic_ussd.account.chain import Chain
from cic_ussd.account.maps import gender
from cic_ussd.account.maps import gender, language
from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.db.models.account import Account, create
from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase
from cic_ussd.error import MetadataNotFoundError
from cic_ussd.metadata import PersonMetadata
from cic_ussd.session.ussd_session import save_session_data
from cic_ussd.state_machine.logic.language import preferred_langauge_from_selection
from cic_ussd.translation import translation_for
from sqlalchemy.orm.session import Session
logg = logging.getLogger(__file__)
def change_preferred_language(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
user_input, ussd_session, account, session = state_machine_data
r_user_input = language().get(user_input)
session = SessionBase.bind_session(session)
account.preferred_language = r_user_input
session.add(account)
session.flush()
SessionBase.release_session(session)
preferences_data = {
'preferred_language': r_user_input
}
s = celery.signature(
'cic_ussd.tasks.metadata.add_preferences_metadata',
[account.blockchain_address, preferences_data],
queue='cic-ussd'
)
return s.apply_async()
def update_account_status_to_active(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function sets user's account to active.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
@@ -219,16 +245,3 @@ def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, Account, S
[blockchain_address, parsed_person_metadata]
)
s_edit_person_metadata.apply_async(queue='cic-ussd')
def process_account_creation(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
user_input, ussd_session, account, session = state_machine_data
preferred_language = preferred_langauge_from_selection(user_input=user_input)
chain_str = Chain.spec.__str__()
create(chain_str, ussd_session.get('msisdn'), session, preferred_language)

View File

@@ -1,95 +0,0 @@
# standard imports
import json
from typing import Tuple
# external imports
import celery
import i18n
from cic_types.condiments import MetadataPointer
from sqlalchemy.orm.session import Session
# local imports
from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.db.models.account import Account
from cic_ussd.processor.util import wait_for_cache, wait_for_session_data
from cic_ussd.session.ussd_session import save_session_data
from cic_ussd.translation import Languages
def is_valid_language_selection(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
user_input, ussd_session, account, session = state_machine_data
key = cache_data_key('system:languages'.encode('utf-8'), MetadataPointer.NONE)
cached_system_languages = get_cached_data(key)
language_list = json.loads(cached_system_languages)
if not language_list:
wait_for_cache(identifier='system:languages'.encode('utf-8'), resource_name='Languages list', salt=MetadataPointer.NONE)
if user_input in ['00', '11', '22']:
return False
user_input = int(user_input)
return user_input <= len(language_list)
def change_preferred_language(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
process_language_selection(state_machine_data=state_machine_data)
user_input, ussd_session, account, session = state_machine_data
wait_for_session_data(resource_name='Preferred language', session_data_key='preferred_language', ussd_session=ussd_session)
preferred_language = ussd_session.get('data').get('preferred_language')
preferences_data = {
'preferred_language': preferred_language
}
s = celery.signature(
'cic_ussd.tasks.metadata.add_preferences_metadata',
[account.blockchain_address, preferences_data],
queue='cic-ussd'
)
return s.apply_async()
def process_language_selection(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
user_input, ussd_session, account, session = state_machine_data
preferred_language = preferred_langauge_from_selection(user_input=user_input)
data = {
'preferred_language': preferred_language
}
save_session_data(queue='cic-ussd', session=session, data=data, ussd_session=ussd_session)
def preferred_langauge_from_selection(user_input: str):
"""
:param user_input:
:type user_input:
:return:
:rtype:
"""
key = cache_data_key('system:languages'.encode('utf-8'), MetadataPointer.NONE)
cached_system_languages = get_cached_data(key)
language_list = json.loads(cached_system_languages)
user_input = int(user_input)
selected_language = language_list[user_input - 1]
preferred_language = i18n.config.get('fallback')
for key, value in Languages.languages_dict.items():
if selected_language[3:] == value:
preferred_language = key
return preferred_language

View File

@@ -9,11 +9,9 @@ from phonenumbers.phonenumberutil import NumberParseException
from sqlalchemy.orm.session import Session
# local imports
from cic_ussd.account.guardianship import Guardianship
from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase
from cic_ussd.notifications import Notifier
from cic_ussd.phone_number import process_phone_number, E164Format
from cic_ussd.session.ussd_session import save_session_data
from cic_ussd.translation import translation_for
@@ -84,8 +82,6 @@ def is_valid_guardian_addition(state_machine_data: Tuple[str, dict, Account, Ses
preferred_language = i18n.config.get('fallback')
is_valid_account = Account.get_by_phone_number(phone_number, session) is not None
guardianship = Guardianship()
is_system_guardian = guardianship.is_system_guardian(phone_number)
is_initiator = phone_number == account.phone_number
is_existent_guardian = phone_number in account.get_guardians()
@@ -104,7 +100,7 @@ def is_valid_guardian_addition(state_machine_data: Tuple[str, dict, Account, Ses
session_data['failure_reason'] = failure_reason
save_session_data('cic-ussd', session, session_data, ussd_session)
return phone_number is not None and is_valid_account and not is_existent_guardian and not is_initiator and not is_system_guardian
return phone_number is not None and is_valid_account and not is_existent_guardian and not is_initiator
def add_pin_guardian(state_machine_data: Tuple[str, dict, Account, Session]):
@@ -134,9 +130,6 @@ def is_set_pin_guardian(account: Account, checked_number: str, preferred_languag
is_set_guardian = checked_number in set_guardians
is_initiator = checked_number == account.phone_number
guardianship = Guardianship()
is_system_guardian = guardianship.is_system_guardian(checked_number)
if not is_set_guardian:
failure_reason = translation_for('helpers.error.is_not_existent_guardian', preferred_language)
@@ -148,7 +141,7 @@ def is_set_pin_guardian(account: Account, checked_number: str, preferred_languag
session_data['failure_reason'] = failure_reason
save_session_data('cic-ussd', session, session_data, ussd_session)
return (is_set_guardian or is_system_guardian) and not is_initiator
return is_set_guardian and not is_initiator
def is_dialers_pin_guardian(state_machine_data: Tuple[str, dict, Account, Session]):
@@ -200,20 +193,8 @@ def initiate_pin_reset(state_machine_data: Tuple[str, dict, Account, Session]):
save_session_data('cic-ussd', session, session_data, ussd_session)
guarded_account_phone_number = session_data.get('guarded_account_phone_number')
guarded_account = Account.get_by_phone_number(guarded_account_phone_number, session)
if quorum_count >= guarded_account.guardian_quora:
guarded_account.reset_pin(session)
logg.debug(f'Reset initiated for: {guarded_account.phone_number}')
session_data['quorum_count'] = 0
save_session_data('cic-ussd', session, session_data, ussd_session)
preferred_language = get_cached_preferred_language(guarded_account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
notifier = Notifier()
notifier.send_sms_notification(
key='sms.pin_reset_initiated',
phone_number=guarded_account.phone_number,
preferred_language=preferred_language,
pin_initiator=account.standard_metadata_id())

View File

@@ -23,7 +23,7 @@ def is_valid_token_selection(state_machine_data: Tuple[str, dict, Account, Sessi
account_tokens_list = session_data.get('account_tokens_list')
if not account_tokens_list:
wait_for_session_data('Account token list', session_data_key='account_tokens_list', ussd_session=ussd_session)
if user_input not in ['00', '11', '22']:
if user_input not in ['00', '22']:
try:
user_input = int(user_input)
return user_input <= len(account_tokens_list)

View File

@@ -32,14 +32,14 @@ celery_app = celery.current_app
@celery_app.task(bind=True, base=CriticalSQLAlchemyTask)
def account_creation_callback(self, result: str, param: str, status_code: int):
def account_creation_callback(self, result: str, url: str, status_code: int):
"""This function defines a task that creates a user and
:param self: Reference providing access to the callback task instance.
:type self: celery.Task
:param result: The blockchain address for the created account
:type result: str
:param param: URL provided to callback task in cic-eth should http be used for callback.
:type param: str
:param url: URL provided to callback task in cic-eth should http be used for callback.
:type url: str
:param status_code: The status of the task to create an account
:type status_code: int
"""
@@ -69,15 +69,6 @@ def account_creation_callback(self, result: str, param: str, status_code: int):
set_active_token(blockchain_address=result, token_symbol=token_symbol)
queue = self.request.delivery_info.get('routing_key')
preferences_data = {"preferred_language": param}
# temporarily caching selected language
key = cache_data_key(bytes.fromhex(result), MetadataPointer.PREFERENCES)
cache_data(key, json.dumps(preferences_data))
s_preferences_metadata = celery.signature(
'cic_ussd.tasks.metadata.add_preferences_metadata', [result, preferences_data], queue=queue
)
s_preferences_metadata.apply_async()
s_phone_pointer = celery.signature(
'cic_ussd.tasks.metadata.add_phone_pointer', [result, phone_number], queue=queue
)

View File

@@ -1,56 +1,9 @@
"""
This module is responsible for translation of ussd menu text based on a user's set preferred language.
"""
# standard imports
import json
import i18n
import os
from pathlib import Path
from typing import Optional
# external imports
from cic_translations.processor import generate_translation_files, parse_csv
from cic_types.condiments import MetadataPointer
# local imports
from cic_ussd.cache import cache_data, cache_data_key
from cic_ussd.validator import validate_presence
def generate_locale_files(locale_dir: str, schema_file_path: str, translation_builder_path: str):
""""""
translation_builder_files = os.listdir(translation_builder_path)
for file in translation_builder_files:
props = Path(file)
if props.suffix == '.csv':
parsed_csv = parse_csv(os.path.join(translation_builder_path, file))
generate_translation_files(
parsed_csv=parsed_csv,
schema_file_path=schema_file_path,
translation_file_type=props.stem,
translation_file_path=locale_dir
)
class Languages:
languages_dict: dict = None
@classmethod
def load_languages_dict(cls, languages_file: str):
with open(languages_file, "r") as languages_file:
cls.languages_dict = json.load(languages_file)
def cache_system_languages(self):
system_languages: list = list(self.languages_dict.values())
languages_list = []
for i in range(len(system_languages)):
language = f'{i + 1}. {system_languages[i]}'
languages_list.append(language)
key = cache_data_key('system:languages'.encode('utf-8'), MetadataPointer.NONE)
cache_data(key, json.dumps(languages_list))
def translation_for(key: str, preferred_language: Optional[str] = None, **kwargs) -> str:
"""

View File

@@ -1,7 +1,7 @@
# standard imports
import semver
version = (0, 3, 1, 'alpha.7')
version = (0, 3, 1, 'alpha.6')
version_object = semver.VersionInfo(
major=version[0],

View File

@@ -11,6 +11,3 @@ transitions=transitions/
host =
port =
ssl =
[system]
guardians_file = var/lib/sys/guardians.txt

View File

@@ -1,10 +1,3 @@
[locale]
fallback=sw
path=var/lib/locale/
file_builders=var/lib/sys/
[schema]
file_path = /usr/local/lib/python3.8/site-packages/cic_translations/data/schema
[languages]
file = var/lib/sys/languages.json

View File

@@ -14,12 +14,19 @@ ARG EXTRA_PIP_INDEX_URL=https://pip.grassrootseconomics.net
ARG EXTRA_PIP_ARGS=""
ARG PIP_INDEX_URL=https://pypi.org/simple
RUN --mount=type=cache,mode=0755,target=/root/.cache/pip \
pip install --index-url $PIP_INDEX_URL \
--pre \
--extra-index-url $EXTRA_PIP_INDEX_URL $EXTRA_PIP_ARGS \
cic-eth-aux-erc20-demurrage-token~=0.0.2a7
COPY *requirements.txt ./
RUN --mount=type=cache,mode=0755,target=/root/.cache/pip \
pip install --index-url $PIP_INDEX_URL \
--pre \
--extra-index-url $EXTRA_PIP_INDEX_URL $EXTRA_PIP_ARGS \
-r requirements.txt
-r requirements.txt
COPY . .

View File

@@ -4,12 +4,10 @@ billiard==3.6.4.0
bcrypt==3.2.0
celery==4.4.7
cffi==1.14.6
cic-eth~=0.12.7
cic-notify~=0.4.0a12
cic-translations~=0.0.3
cic-types~=0.2.1a8
confini~=0.5.2
cic-eth-aux-erc20-demurrage-token~=0.0.3
cic-eth~=0.12.5a1
cic-notify~=0.4.0a11
cic-types~=0.2.1a7
confini>=0.3.6rc4,<0.5.0
phonenumbers==8.12.12
psycopg2==2.8.6
python-i18n[YAML]==0.3.9

View File

@@ -13,7 +13,5 @@
"products_edit_pin_authorization",
"account_balances_pin_authorization",
"account_statement_pin_authorization",
"account_balances",
"middle_language_set",
"last_language_set"
"account_balances"
]

View File

@@ -2,8 +2,6 @@
"start",
"scan_data",
"initial_language_selection",
"initial_middle_language_set",
"initial_last_language_set",
"initial_pin_entry",
"initial_pin_confirmation",
"change_preferred_language"

View File

@@ -47,11 +47,11 @@ def test_menu_processor(activated_account,
preferred_language = get_cached_preferred_language(activated_account.blockchain_address)
available_balance = get_cached_available_balance(activated_account.blockchain_address)
token_symbol = get_default_token_symbol()
with_available_balance = 'ussd.account_balances.available_balance'
with_fees = 'ussd.account_balances.with_fees'
with_available_balance = 'ussd.kenya.account_balances.available_balance'
with_fees = 'ussd.kenya.account_balances.with_fees'
ussd_menu = UssdMenu.find_by_name('account_balances')
name = ussd_menu.get('name')
resp = response(activated_account, 'ussd.account_balances', name, init_database, generic_ussd_session)
resp = response(activated_account, 'ussd.kenya.account_balances', name, init_database, generic_ussd_session)
assert resp == translation_for(with_available_balance,
preferred_language,
available_balance=available_balance,
@@ -61,7 +61,7 @@ def test_menu_processor(activated_account,
key = cache_data_key(identifier, MetadataPointer.BALANCES_ADJUSTED)
adjusted_balance = 45931650.64654012
cache_data(key, json.dumps(adjusted_balance))
resp = response(activated_account, 'ussd.account_balances', name, init_database, generic_ussd_session)
resp = response(activated_account, 'ussd.kenya.account_balances', name, init_database, generic_ussd_session)
tax_wei = to_wei(int(available_balance)) - int(adjusted_balance)
tax = from_wei(int(tax_wei))
assert resp == translation_for(key=with_fees,
@@ -84,28 +84,28 @@ def test_menu_processor(activated_account,
if len(transaction_sets) >= 3:
last_transaction_set = statement_transaction_set(preferred_language, transaction_sets[2])
display_key = 'ussd.first_transaction_set'
display_key = 'ussd.kenya.first_transaction_set'
ussd_menu = UssdMenu.find_by_name('first_transaction_set')
name = ussd_menu.get('name')
resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
assert resp == translation_for(display_key, preferred_language, first_transaction_set=first_transaction_set)
display_key = 'ussd.middle_transaction_set'
display_key = 'ussd.kenya.middle_transaction_set'
ussd_menu = UssdMenu.find_by_name('middle_transaction_set')
name = ussd_menu.get('name')
resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
assert resp == translation_for(display_key, preferred_language, middle_transaction_set=middle_transaction_set)
display_key = 'ussd.last_transaction_set'
display_key = 'ussd.kenya.last_transaction_set'
ussd_menu = UssdMenu.find_by_name('last_transaction_set')
name = ussd_menu.get('name')
resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
assert resp == translation_for(display_key, preferred_language, last_transaction_set=last_transaction_set)
display_key = 'ussd.display_user_metadata'
display_key = 'ussd.kenya.display_user_metadata'
ussd_menu = UssdMenu.find_by_name('display_user_metadata')
name = ussd_menu.get('name')
identifier = bytes.fromhex(activated_account.blockchain_address)
@@ -114,7 +114,7 @@ def test_menu_processor(activated_account,
resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
assert resp == parse_person_metadata(cached_person_metadata, display_key, preferred_language)
display_key = 'ussd.account_balances_pin_authorization'
display_key = 'ussd.kenya.account_balances_pin_authorization'
ussd_menu = UssdMenu.find_by_name('account_balances_pin_authorization')
name = ussd_menu.get('name')
resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
@@ -122,11 +122,11 @@ def test_menu_processor(activated_account,
activated_account.failed_pin_attempts = 1
resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
retry_pin_entry = translation_for('ussd.retry_pin_entry', preferred_language, remaining_attempts=2)
retry_pin_entry = translation_for('ussd.kenya.retry_pin_entry', preferred_language, remaining_attempts=2)
assert resp == translation_for(f'{display_key}.retry', preferred_language, retry_pin_entry=retry_pin_entry)
activated_account.failed_pin_attempts = 0
display_key = 'ussd.start'
display_key = 'ussd.kenya.start'
ussd_menu = UssdMenu.find_by_name('start')
name = ussd_menu.get('name')
resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
@@ -135,7 +135,7 @@ def test_menu_processor(activated_account,
account_balance=available_balance,
account_token_name=token_symbol)
display_key = 'ussd.start'
display_key = 'ussd.kenya.start'
ussd_menu = UssdMenu.find_by_name('start')
name = ussd_menu.get('name')
older_timestamp = (activated_account.created - datetime.timedelta(days=35))
@@ -144,7 +144,7 @@ def test_menu_processor(activated_account,
response(activated_account, display_key, name, init_database, generic_ussd_session)
assert mock_get_adjusted_balance['timestamp'] == int((datetime.datetime.now() - datetime.timedelta(days=30)).timestamp())
display_key = 'ussd.transaction_pin_authorization'
display_key = 'ussd.kenya.transaction_pin_authorization'
ussd_menu = UssdMenu.find_by_name('transaction_pin_authorization')
name = ussd_menu.get('name')
generic_ussd_session['data'] = {
@@ -163,7 +163,7 @@ def test_menu_processor(activated_account,
token_symbol=token_symbol,
sender_information=tx_sender_information)
display_key = 'ussd.exit_insufficient_balance'
display_key = 'ussd.kenya.exit_insufficient_balance'
ussd_menu = UssdMenu.find_by_name('exit_insufficient_balance')
name = ussd_menu.get('name')
generic_ussd_session['data'] = {
@@ -180,13 +180,13 @@ def test_menu_processor(activated_account,
recipient_information=tx_recipient_information,
token_balance=available_balance)
display_key = 'ussd.exit_invalid_menu_option'
display_key = 'ussd.kenya.exit_invalid_menu_option'
ussd_menu = UssdMenu.find_by_name('exit_invalid_menu_option')
name = ussd_menu.get('name')
resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
assert resp == translation_for(display_key, preferred_language, support_phone=Support.phone_number)
display_key = 'ussd.exit_successful_transaction'
display_key = 'ussd.kenya.exit_successful_transaction'
ussd_menu = UssdMenu.find_by_name('exit_successful_transaction')
name = ussd_menu.get('name')
generic_ussd_session['data'] = {

View File

@@ -97,7 +97,7 @@ def test_handle_menu_operations(activated_account,
valid_service_codes = load_config.get('USSD_SERVICE_CODE').split(",")
preferred_language = i18n.config.get('fallback')
resp = handle_menu_operations(chain_str, external_session_id, phone, None, valid_service_codes[0], init_database, '4444')
assert resp == translation_for('ussd.account_creation_prompt', preferred_language)
assert resp == translation_for('ussd.kenya.account_creation_prompt', preferred_language)
cached_ussd_session = get_cached_data(external_session_id)
ussd_session = json.loads(cached_ussd_session)
assert ussd_session['msisdn'] == phone
@@ -118,5 +118,5 @@ def test_handle_menu_operations(activated_account,
preferred_language = get_cached_preferred_language(activated_account.blockchain_address)
persisted_ussd_session.state = 'enter_transaction_recipient'
resp = handle_menu_operations(chain_str, external_session_id, phone, None, valid_service_codes[0], init_database, '1')
assert resp == translation_for('ussd.enter_transaction_recipient', preferred_language)
assert resp == translation_for('ussd.kenya.enter_transaction_recipient', preferred_language)

View File

@@ -32,7 +32,7 @@ def test_parse_person_metadata(activated_account, cache_person_metadata, cache_p
cached_person_metadata = person_metadata.get_cached_metadata()
person_metadata = json.loads(cached_person_metadata)
preferred_language = get_cached_preferred_language(activated_account.blockchain_address)
display_key = 'ussd.display_person_metadata'
display_key = 'ussd.kenya.display_person_metadata'
parsed_person_metadata = parse_person_metadata(cached_person_metadata,
display_key,
preferred_language)

View File

@@ -8,8 +8,8 @@ from cic_ussd.notifications import Notifier
@pytest.mark.parametrize("key, preferred_language, recipient, expected_message", [
("ussd.exit", "en", "+254712345678", "END Thank you for using the service."),
("ussd.exit", "sw", "+254712345678", "END Asante kwa kutumia huduma.")
("ussd.kenya.exit", "en", "+254712345678", "END Thank you for using the service."),
("ussd.kenya.exit", "sw", "+254712345678", "END Asante kwa kutumia huduma.")
])
def test_send_sms_notification(celery_session_worker,
expected_message,

View File

@@ -10,11 +10,11 @@ from cic_ussd.translation import translation_for
def test_translation_for(set_locale_files):
english_translation = translation_for(
key='ussd.exit_invalid_request',
key='ussd.kenya.exit_invalid_request',
preferred_language='en'
)
swahili_translation = translation_for(
key='ussd.exit_invalid_request',
key='ussd.kenya.exit_invalid_request',
preferred_language='sw'
)
assert swahili_translation == 'END Chaguo si sahihi.'

View File

@@ -1,142 +1,21 @@
[
{
"trigger": "scan_data",
"source": "initial_language_selection",
"dest": "account_creation_prompt",
"after": "cic_ussd.state_machine.logic.account.process_account_creation",
"conditions": "cic_ussd.state_machine.logic.language.is_valid_language_selection"
},
{
"trigger": "scan_data",
"source": "initial_language_selection",
"dest": "initial_middle_language_set",
"conditions": "cic_ussd.state_machine.logic.menu.menu_eleven_selected"
},
{
"trigger": "scan_data",
"source": "initial_language_selection",
"source": "select_preferred_language",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
},
{
"trigger": "scan_data",
"source": "initial_middle_language_set",
"dest": "initial_language_selection",
"conditions": "cic_ussd.state_machine.logic.menu.menu_twenty_two_selected"
},
{
"trigger": "scan_data",
"source": "initial_middle_language_set",
"dest": "initial_last_language_set",
"conditions": "cic_ussd.state_machine.logic.menu.menu_eleven_selected"
},
{
"trigger": "scan_data",
"source": "initial_middle_language_set",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
},
{
"trigger": "scan_data",
"source": "initial_last_language_set",
"dest": "initial_middle_language_set",
"conditions": "cic_ussd.state_machine.logic.menu.menu_twenty_two_selected"
},
{
"trigger": "scan_data",
"source": "initial_last_language_set",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
},
{
"trigger": "scan_data",
"source": "initial_middle_language_set",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
},
{
"trigger": "scan_data",
"source": "initial_language_selection",
"dest": "exit_invalid_menu_option"
},
{
"trigger": "scan_data",
"source": "middle_language_set",
"dest": "exit_invalid_menu_option"
},
{
"trigger": "scan_data",
"source": "last_language_set",
"dest": "exit_invalid_menu_option"
"after": "cic_ussd.state_machine.logic.account.change_preferred_language",
"conditions": "cic_ussd.state_machine.logic.menu.menu_one_selected"
},
{
"trigger": "scan_data",
"source": "select_preferred_language",
"dest": "exit",
"after": "cic_ussd.state_machine.logic.language.change_preferred_language",
"conditions": "cic_ussd.state_machine.logic.language.is_valid_language_selection"
"after": "cic_ussd.state_machine.logic.account.change_preferred_language",
"conditions": "cic_ussd.state_machine.logic.menu.menu_two_selected"
},
{
"trigger": "scan_data",
"source": "select_preferred_language",
"dest": "middle_language_set",
"conditions": "cic_ussd.state_machine.logic.menu.menu_eleven_selected"
},
{
"trigger": "scan_data",
"source": "select_preferred_language",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
},
{
"trigger": "scan_data",
"source": "middle_language_set",
"dest": "select_preferred_language",
"conditions": "cic_ussd.state_machine.logic.menu.menu_twenty_two_selected"
},
{
"trigger": "scan_data",
"source": "middle_language_set",
"dest": "last_language_set",
"conditions": "cic_ussd.state_machine.logic.menu.menu_eleven_selected"
},
{
"trigger": "scan_data",
"source": "middle_language_set",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
},
{
"trigger": "scan_data",
"source": "last_language_set",
"dest": "middle_language_set",
"conditions": "cic_ussd.state_machine.logic.menu.menu_twenty_two_selected"
},
{
"trigger": "scan_data",
"source": "last_language_set",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
},
{
"trigger": "scan_data",
"source": "middle_language_set",
"dest": "exit",
"conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
},
{
"trigger": "scan_data",
"source": "select_preferred_language",
"dest": "exit_invalid_menu_option"
},
{
"trigger": "scan_data",
"source": "middle_language_set",
"dest": "exit_invalid_menu_option"
},
{
"trigger": "scan_data",
"source": "last_language_set",
"dest": "exit_invalid_menu_option"
}
]

View File

@@ -1,4 +1,29 @@
[
{
"trigger": "scan_data",
"source": "initial_language_selection",
"dest": "initial_pin_entry",
"after": "cic_ussd.state_machine.logic.account.change_preferred_language",
"conditions": "cic_ussd.state_machine.logic.menu.menu_one_selected"
},
{
"trigger": "scan_data",
"source": "initial_language_selection",
"dest": "initial_pin_entry",
"after": "cic_ussd.state_machine.logic.account.change_preferred_language",
"conditions": "cic_ussd.state_machine.logic.menu.menu_two_selected"
},
{
"trigger": "scan_data",
"source": "initial_language_selection",
"dest": "help",
"conditions": "cic_ussd.state_machine.logic.menu.menu_three_selected"
},
{
"trigger": "scan_data",
"source": "initial_language_selection",
"dest": "exit_invalid_menu_option"
},
{
"trigger": "scan_data",
"source": "initial_pin_entry",
@@ -14,6 +39,7 @@
{
"trigger": "scan_data",
"source": "initial_pin_confirmation",
"unless": "cic_ussd.state_machine.logic.validator.has_cached_person_metadata",
"conditions": "cic_ussd.state_machine.logic.pin.pins_match",
"dest": "start",
"after": [

View File

@@ -0,0 +1,36 @@
en:
female: |-
Female
from: |-
From
male: |-
Male
not_provided: |-
Not provided
no_transaction_history: |-
No transaction history
no_tokens_list: |-
No tokens to list
other: |-
Other
received: |-
Received
sent: |-
Sent
to: |-
To
guardians_list_header: |-
Walinzi uliowaongeza ni:
no_guardians_list: |-
No guardians set
error:
no_phone_number_provided: |-
No phone number was provided.
no_matching_account: |-
The number provided is not registered.
is_initiator: |-
Phone number cannot be your own.
is_existent_guardian: |-
This phone number is is already added as a guardian.
is_not_existent_guardian: |-
Phone number not set as PIN reset guardian.

View File

@@ -0,0 +1,36 @@
sw:
female: |-
Mwanamke
from: |-
Kutoka kwa
male: |-
Mwanaume
not_provided: |-
Haijawekwa
no_transaction_history: |-
Hamna ripoti ya matumizi
no_tokens_list: |-
Hamna sarafu nyingine
other: |-
Nyingine
received: |-
Ulipokea
sent: |-
Ulituma
to: |-
Kwa
guardians_list_header: |-
Your set guardians are:
no_guardians_list: |-
Hamna walinzi walioongezwa
error:
no_phone_number_provided: |-
Namabari ya simu haijawekwa.
no_matching_account: |-
Nambari uliyoweka haijasajiliwa.
is_initiator: |-
Nambari yafaa kuwa tofauti na yako.
is_existent_guardian: |-
Namabari hii tayari imeongezwa kama mlinzi wa nambari ya siri.
is_not_existent_guardian: |-
Nambari hii haijaongezwa kama mlinzi wa nambari ya siri.

View File

@@ -0,0 +1,11 @@
en:
account_successfully_created: |-
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} to %{tx_recipient_information}. New balance is %{balance} %{token_symbol}.
sent_tokens: |-
Successfully sent %{amount} %{token_symbol} to %{tx_recipient_information} %{timestamp} from %{tx_sender_information}. New balance is %{balance} %{token_symbol}.
terms: |-
By using the service, you agree to the terms and conditions at http://grassecon.org/tos
upsell_unregistered_recipient: |-
%{tx_sender_information} tried to send you %{token_symbol} but you are not registered. To use dial *384*96# on Safaricom and *483*96# on other networks. For help %{support_phone}.

View File

@@ -0,0 +1,11 @@
sw:
account_successfully_created: |-
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} ikapokewa na %{tx_recipient_information}. Salio lako ni %{balance} %{token_symbol}.
sent_tokens: |-
Umetuma %{amount} %{token_symbol} kwa %{tx_recipient_information} %{timestamp} kutoka kwa %{tx_sender_information}. Salio lako ni %{balance} %{token_symbol}.
terms: |-
Kwa kutumia hii huduma, umekubali sheria na masharti yafuatayo http://grassecon.org/tos
upsell_unregistered_recipient: |-
%{tx_sender_information} amejaribu kukutumia %{token_symbol} lakini hujasajili. Kutumia bonyeza *384*96# Safaricom ama *483*46# kwa utandao tofauti. Kwa Usaidizi %{support_phone}.

View File

@@ -0,0 +1,317 @@
en:
kenya:
initial_language_selection: |-
CON Welcome to Sarafu Network
1. English
2. Kiswahili
3. Help
initial_pin_entry: |-
CON Please enter a new four number PIN for your account.
initial_pin_confirmation: |-
CON Enter your four number PIN again
enter_given_name: |-
CON Enter first name
0. Back
enter_family_name: |-
CON Enter family name
0. Back
enter_date_of_birth: |-
CON Enter year of birth
0. Back
enter_gender: |-
CON Enter gender
1. Male
2. Female
3. Other
0. Back
enter_location: |-
CON Enter your location
0. Back
enter_products: |-
CON Please enter a product or service you offer
0. Back
start: |-
CON Balance %{account_balance} %{account_token_name}
1. Send
2. My Sarafu
3. My Account
4. Help
enter_transaction_recipient: |-
CON Enter phone number
0. Back
enter_transaction_amount: |-
CON Enter amount
0. Back
first_account_tokens_set: |-
CON Choose a number or symbol from your balances:
%{first_account_tokens_set}
11. Next
00. Exit
middle_account_tokens_set: |-
CON Choose a number or symbol from your balances:
%{middle_account_tokens_set}
11. Next
22. Previous
00. Exit
last_account_tokens_set: |-
CON Choose a number or symbol from your balances:
%{last_account_tokens_set}
22. Previous
00. Exit
token_selection_pin_authorization:
first: |-
CON %{token_data}
Enter pin to select:
retry: |-
%{retry_pin_entry}
account_management: |-
CON My account
1. My profile
2. Change language
3. Check balance
4. Check statement
5. PIN options
0. Back
metadata_management: |-
CON My profile
1. Edit name
2. Edit gender
3. Edit age
4. Edit location
5. Edit products
6. View my profile
0. Back
display_user_metadata: |-
CON Your details are:
Name: %{full_name}
Gender: %{gender}
Age: %{age}
Location: %{location}
You sell: %{products}
0. Back
select_preferred_language: |-
CON Choose language
1. English
2. Kiswahili
0. Back
retry_pin_entry: |-
CON Incorrect PIN entered, please try again. You have %{remaining_attempts} attempts remaining.
0. Back
pin_management: |-
CON Pin options
1. Change PIN
2. Reset PIN
3. Guard PIN
0. Back
enter_current_pin:
first: |-
CON Enter current PIN.
0. Back
retry: |-
%{retry_pin_entry}
enter_new_pin: |-
CON Enter your new four number PIN
0. Back
new_pin_confirmation: |-
CON Enter your new four number PIN again
0. Back
reset_guarded_pin: |-
CON Enter phone number you are the guardian to reset their pin
0. Back
reset_guarded_pin_authorization:
first: |-
CON Enter YOUR pin to confirm %{guarded_account_information}'s reset
0. Back
retry: |-
%{retry_pin_entry}
exit_pin_reset_initiated_success: |-
CON Success: You have initiated a PIN reset for %{guarded_account_information}
0. Back
9. Exit
exit_not_authorized_for_pin_reset: |-
CON Failure: You are not authorized to reset that PIN. You must be a guardian!
0. Back
9. Exit
guard_pin: |-
CON Pin guard
1. View guardians
2. Add guardian
3. Remove guardian
0. Back
guardian_list_pin_authorization:
first: |-
CON Enter your pin to view set guardians
0. Back
retry: |-
%{retry_pin_entry}
guardian_list: |-
CON %{guardians_list}
0. Back
9. Exit
add_guardian: |-
CON Enter phone number to add as pin reset guardian
0. Back
add_guardian_pin_authorization:
first: |-
CON Enter your pin to add %{guardian_information} as your PIN reset guardian
0. Back
retry: |-
%{retry_pin_entry}
exit_guardian_addition_success: |-
CON Success: %{guardian_information} can now reset your PIN
0. Back
9. Exit
exit_invalid_guardian_addition: |-
CON %{error_exit}
0. Back
9. Exit
remove_guardian: |-
CON Enter phone number to revoke guardianship:
0. Back
remove_guardian_pin_authorization:
first: |-
CON Enter your pin to remove %{guardian_information} as your PIN reset guardian
0. Back
retry: |-
%{retry_pin_entry}
exit_guardian_removal_success: |-
CON Success: %{guardian_information} PIN reset guardianship is revoked
0. Back
9. Exit
exit_invalid_guardian_removal: |-
CON %{error_exit}
0. Back
9. Exit
transaction_pin_authorization:
first: |-
CON %{recipient_information} will receive %{transaction_amount} %{token_symbol} from %{sender_information}.
Please enter your PIN to confirm.
0. Back
retry: |-
%{retry_pin_entry}
display_metadata_pin_authorization:
first: |-
CON Please enter your PIN
0. Back
retry: |-
%{retry_pin_entry}
account_balances_pin_authorization:
first: |-
CON Please enter your PIN to view balances
0. Back
retry: |-
%{retry_pin_entry}
account_statement_pin_authorization:
first: |-
CON Please enter your PIN to view statement
0. Back
retry: |-
%{retry_pin_entry}
name_edit_pin_authorization:
first: |-
CON Please enter your PIN
0. Back
retry: |-
%{retry_pin_entry}
dob_edit_pin_authorization:
first: |-
CON Please enter your PIN
0. Back
retry: |-
%{retry_pin_entry}
gender_edit_pin_authorization:
first: |-
CON Please enter your PIN
0. Back
retry: |-
%{retry_pin_entry}
location_edit_pin_authorization:
first: |-
CON Please enter your PIN
0. Back
retry: |-
%{retry_pin_entry}
products_edit_pin_authorization:
first: |-
CON Please enter your PIN
0. Back
retry: |-
%{retry_pin_entry}
account_balances:
available_balance: |-
CON Your balances are as follows:
balance: %{available_balance} %{token_symbol}
0. Back
with_fees: |-
CON Your balances are as follows:
balances: %{available_balance} %{token_symbol}
fees: %{tax} %{token_symbol}
0. Back
with_rewards: |-
CON Your balances are as follows:
balance: %{available_balance} %{token_symbol}
fees: %{tax} %{token_symbol}
rewards: %{bonus} %{token_symbol}
0. Back
first_transaction_set: |-
CON %{first_transaction_set}
1. Next
00. Exit
middle_transaction_set: |-
CON %{middle_transaction_set}
1. Next
2. Previous
00. Exit
last_transaction_set: |-
CON %{last_transaction_set}
2. Previous
00. Exit
exit: |-
END Thank you for using the service.
exit_invalid_request: |-
END Invalid request.
exit_invalid_menu_option: |-
CON Invalid menu option. For help, call %{support_phone}.
00. Back
99. Exit
exit_invalid_input: |-
CON Invalid input. Nothing selected
00. Back
99. Exit
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}.
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}.
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: |-
CON Recipient's phone number is not registered or is invalid:
00. Retry
99. Exit
exit_successful_transaction: |-
CON Your request has been sent. %{recipient_information} will receive %{transaction_amount} %{token_symbol} from %{sender_information}.
00. Back
99. Exit
exit_insufficient_balance: |-
CON Payment of %{amount} %{token_symbol} to %{recipient_information} has failed due to insufficent balance.
Your Sarafu-Network balances is: %{token_balance}
00. Back
99. Exit
exit_successful_token_selection: |-
CON Success! %{token_symbol} is your active Sarafu.
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
99. Exit
complete: |-
CON Your request has been sent. You will receive an SMS shortly.
00. Back
99. Exit
account_creation_prompt: |-
END Your account is being created. You will receive an SMS when your account is ready.

View File

@@ -0,0 +1,316 @@
sw:
kenya:
initial_language_selection: |-
CON Karibu Sarafu Network
1. English
2. Kiswahili
3. Help
initial_pin_entry: |-
CON Tafadhali weka pin mpya yenye nambari nne kwa akaunti yako
initial_pin_confirmation: |-
CON Weka PIN yako tena
enter_given_name: |-
CON Weka jina lako la kwanza
enter_family_name: |-
CON Weka jina lako la mwisho
0. Nyuma
enter_date_of_birth: |-
CON Weka mwaka wa kuzaliwa
0. Nyuma
enter_gender: |-
CON Weka jinsia yako
1. Mwanaume
2. Mwanamke
3. Nyngine
0. Nyuma
enter_location: |-
CON Weka eneo lako
0. Nyuma
enter_products: |-
CON Weka bidhaa ama huduma unauza
0. Nyuma
start: |-
CON Salio %{account_balance} %{account_token_name}
1. Tuma
2. Sarafu yangu
3. Akaunti yangu
4. Usaidizi
enter_transaction_recipient: |-
CON Weka nambari ya simu
0. Nyuma
enter_transaction_amount: |-
CON Weka kiwango
0. Nyuma
first_account_tokens_set: |-
CON Chagua nambari au ishara kutoka kwa salio zako:
%{first_account_tokens_set}
11. Mbele
00. Ondoka
middle_account_tokens_set: |-
CON Chagua nambari au ishara kutoka kwa salio zako:
%{middle_account_tokens_set}
11. Mbele
22. Nyuma
00. Ondoka
last_account_tokens_set: |-
CON Chagua nambari au ishara kutoka kwa salio zako:
%{last_account_tokens_set}
22. Nyuma
00. Ondoka
token_selection_pin_authorization:
first: |-
CON %{token_data}
Weka nambari ya siri kuchagua:
retry: |-
%{retry_pin_entry}
account_management: |-
CON Akaunti yangu
1. Wasifu wangu
2. Chagua lugha utakayotumia
3. Angalia salio
4. Angalia taarifa ya matumizi
5. Mipangilio ya nambari ya siri
0. Nyuma
metadata_management: |-
CON Wasifu wangu
1. Weka jina
2. Weka jinsia
3. Weka umri
4. Weka eneo
5. Weka bidhaa
6. Angalia wasifu wako
0. Nyuma
display_user_metadata: |-
CON Wasifu wako una maelezo yafuatayo:
Jina: %{full_name}
Jinsia: %{gender}
Umri: %{age}
Eneo: %{location}
Unauza: %{products}
0. Nyuma
select_preferred_language: |-
CON Chagua lugha
1. Kingereza
2. Kiswahili
0. Nyuma
retry_pin_entry: |-
CON Nambari uliyoweka si sahihi, jaribu tena. Una majaribio %{remaining_attempts} yaliyobaki.
0. Nyuma
pin_management: |-
CON Pin options
1. Badilisha nambari yangu ya siri
2. Tuma ombili la kubadilisha nambari ya siri
3. Linda nambari ya siri
0. Nyuma
enter_current_pin:
first: |-
CON Weka nambari ya siri.
0. Nyuma
retry: |-
%{retry_pin_entry}
enter_new_pin: |-
CON Weka nambari ya siri mpya
0. Nyuma
new_pin_confirmation: |-
CON Weka nambari yako ya siri tena
0. Nyuma
reset_guarded_pin: |-
CON Weka nambari ya simu ili kutuma ombi la kubalisha nambari ya siri.
0. Nyuma
reset_guarded_pin_authorization:
first: |-
CON Weka nambari YAKO ya siri ili kudhibitisha ombi la kubadilisha nambari ya siri ya %{guarded_account_information}.
0. Nyuma
retry: |-
%{retry_pin_entry}
exit_pin_reset_initiated_success: |-
CON Ombi lako la kubadili nambari ya siri ya %{guarded_account_information} limetumwa.
0. Nyuma
9. Ondoka
exit_not_authorized_for_pin_reset: |-
CON Huruhusiwi kutuma ombi la kubadilisha nambari ya siri.
0. Nyuma
9. Ondoka
guard_pin: |-
CON Linda nambari ya siri
1. Walinzi wa namabari ya siri
2. Ongeza mlinzi
3. Ondoa mlinzi
0. Nyuma
guardian_list_pin_authorization:
first: |-
CON Weka nambari yako ya siri ili kuona walinzi uliowaongeza
0. Nyuma
retry: |-
%{retry_pin_entry}
guardian_list: |-
CON %{guardians_list}
0. Nyuma
9. Ondoka
add_guardian: |-
CON Weka nambari ya simu ili kuongeza mlinzi
0. Nyuma
add_guardian_pin_authorization:
first: |-
CON Weka nambari YAKO ya siri ili kumwongeza %{guardian_information} kama mlinzi
0. Nyuma
retry: |-
%{retry_pin_entry}
exit_guardian_addition_success: |-
CON Ombi lako la kumwongeza: %{guardian_information} kama mlinzi limefanikiwa
0. Nyuma
9. Ondoka
exit_invalid_guardian_addition: |-
CON %{error_exit}
0. Nyuma
9. Ondoka
remove_guardian: |-
CON Weka nambari ya simu ili kuondoa mlinzi
0. Nyuma
remove_guardian_pin_authorization:
first: |-
CON Weka nambari YAKO ya siri ili kumwondoa %{guardian_information} kama mlinzi
0. Nyuma
retry: |-
%{retry_pin_entry}
exit_guardian_removal_success: |-
CON Ombi lako la kumwondoa: %{guardian_information} kama mlinzi limefanikiwa
0. Nyuma
9. Ondoka
exit_invalid_guardian_removal: |-
CON %{error_exit}
0. Nyuma
9. Ondoka
transaction_pin_authorization:
first: |-
CON %{recipient_information} atapokea %{transaction_amount} %{token_symbol} kutoka kwa %{sender_information}.
Tafadhali weka nambari yako ya siri kudhibitisha.
0. Nyuma
retry: |-
%{retry_pin_entry}
display_metadata_pin_authorization:
first: |-
CON Tafadhali weka PIN yako
0. Nyuma
retry: |-
%{retry_pin_entry}
account_balances_pin_authorization:
first: |-
CON Tafadhali weka PIN yako kuona salio.
0. Nyuma
retry: |-
%{retry_pin_entry}
account_statement_pin_authorization:
first: |-
CON Tafadhali weka PIN yako kuona taarifa ya matumizi.
0. Nyuma
retry: |-
%{retry_pin_entry}
name_edit_pin_authorization:
first: |-
CON Tafadhali weka PIN yako
0. Nyuma
retry: |-
%{retry_pin_entry}
dob_edit_pin_authorization:
first: |-
CON Tafadhali weka PIN yako
0. Nyuma
retry: |-
%{retry_pin_entry}
gender_edit_pin_authorization:
first: |-
CON Tafadhali weka PIN yako
0. Nyuma
retry: |-
%{retry_pin_entry}
location_edit_pin_authorization:
first: |-
CON Tafadhali weka PIN yako
0. Nyuma
retry: |-
%{retry_pin_entry}
products_edit_pin_authorization:
first: |-
CON Tafadhali weka PIN yako
0. Nyuma
retry: |-
%{retry_pin_entry}
account_balances:
available_balance: |-
CON Salio zako ni zifuatazo:
salio: %{available_balance} %{token_symbol}
0. Nyuma
with_fees: |-
CON Salio zako ni zifuatazo:
salio: %{available_balance} %{token_symbol}
ushuru: %{tax} %{token_symbol}
0. Nyuma
with_rewards: |-
CON Salio zako ni zifuatazo:
salio: %{available_balance} %{token_symbol}
ushuru: %{tax} %{token_symbol}
tuzo: %{bonus} %{token_symbol}
0. Nyuma
first_transaction_set: |-
CON %{first_transaction_set}
1. Mbele
00. Ondoka
middle_transaction_set: |-
CON %{middle_transaction_set}
1. Mbele
2. Nyuma
00. Ondoka
last_transaction_set: |-
CON %{last_transaction_set}
2. Nyuma
00. Ondoka
exit: |-
END Asante kwa kutumia huduma.
exit_invalid_request: |-
END Chaguo si sahihi.
exit_invalid_menu_option: |-
CON Chaguo lako sio sahihi. Kwa usaidizi piga simu %{support_phone}
00. Nyuma
99. Ondoka
exit_invalid_input: |-
CON Chaguo lako halipatikani. Hakuna kilichochaguliwa.
00. Nyuma
99. Ondoka
exit_pin_blocked: |-
END PIN yako imefungwa. Kwa usaidizi tafadhali piga simu %{support_phone}.
exit_invalid_pin: |-
END PIN uliyobonyeza sio sahihi. PIN lazima iwe na nambari nne. Kwa usaidizi piga simu %{support_phone}.
exit_invalid_new_pin: |-
END PIN uliyobonyeza sio sahihi. PIN lazima iwe tofauti na pin yako ya sasa. Kwa usaidizi piga simu %{support_phone}.
exit_pin_mismatch: |-
END PIN mpya na udhibitisho wa pin mpya hazilingani. Tafadhali jaribu tena. Kwa usaidizi piga simu %{support_phone}.
exit_invalid_recipient: |-
CON Mpokeaji wa nambari hapatikani au sio sahihi.
00. Jaribu tena
99. Ondoka
exit_successful_transaction: |-
CON Ombi lako limetumwa. %{recipient_information} atapokea %{transaction_amount} %{token_symbol} kutoka kwa %{sender_information}.
00. Nyuma
99. Ondoka
exit_insufficient_balance: |-
CON Malipo ya %{amount} %{token_symbol} kwa %{recipient_information} halijakamilika kwa sababu salio lako haitoshi.
Akaunti yako ya Sarafu ina salio ifuatayo: %{token_balance}
00. Nyuma
99. Ondoka
exit_successful_token_selection: |-
CON Chaguo lako limekamilika, %{token_symbol} ni sarafu itakayotumika.
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
9. Ondoka
complete: |-
CON Ombi lako limetumwa. Utapokea uthibitishaji wa SMS kwa muda mfupi.
00. Nyuma
99. Ondoka
account_creation_prompt: |-
END Akaunti yako ya Sarafu inatayarishwa. Utapokea ujumbe wa SMS akaunti yako ikiwa tayari.

View File

@@ -1,19 +0,0 @@
keys,en,sw
female,Female,Mwanamke
from,From,Kutoka kwa
male,Male,Mwanaume
not_provided,Not provided,Haijawekwa
no_language_list,No language list,Hamna lugha ya kuchagua
no_transaction_history,No transaction history,Hamna ripoti ya matumizi
no_tokens_list,No tokens to list,Hamna sarafu nyingine
other,Other,Nyingine
received,Received,Ulipokea
sent,Sent,Ulituma
to,To,Kwa
guardians_list_header,Your set guardians are:,Walinzi uliowaongeza ni:
no_guardians_list,No guardians set,Hamna walinzi walioongezwa
error.no_phone_number_provided,No phone number was provided.,Namabari ya simu haijawekwa.
error.no_matching_account,The number provided is not registered.,Nambari uliyoweka haijasajiliwa.
error.is_initiator,Phone number cannot be your own.,Nambari yafaa kuwa tofauti na yako.
error.is_existent_guardian,This phone number is is already added as a guardian.,Namabari hii tayari imeongezwa kama mlinzi wa nambari ya siri.
error.is_not_existent_guardian,Phone number not set as PIN reset guardian.,Nambari hii haijaongezwa kama mlinzi wa nambari ya siri.
1 keys en sw
2 female Female Mwanamke
3 from From Kutoka kwa
4 male Male Mwanaume
5 not_provided Not provided Haijawekwa
6 no_language_list No language list Hamna lugha ya kuchagua
7 no_transaction_history No transaction history Hamna ripoti ya matumizi
8 no_tokens_list No tokens to list Hamna sarafu nyingine
9 other Other Nyingine
10 received Received Ulipokea
11 sent Sent Ulituma
12 to To Kwa
13 guardians_list_header Your set guardians are: Walinzi uliowaongeza ni:
14 no_guardians_list No guardians set Hamna walinzi walioongezwa
15 error.no_phone_number_provided No phone number was provided. Namabari ya simu haijawekwa.
16 error.no_matching_account The number provided is not registered. Nambari uliyoweka haijasajiliwa.
17 error.is_initiator Phone number cannot be your own. Nambari yafaa kuwa tofauti na yako.
18 error.is_existent_guardian This phone number is is already added as a guardian. Namabari hii tayari imeongezwa kama mlinzi wa nambari ya siri.
19 error.is_not_existent_guardian Phone number not set as PIN reset guardian. Nambari hii haijaongezwa kama mlinzi wa nambari ya siri.

View File

@@ -1,9 +0,0 @@
{
"en": "English",
"sw": "Kiswahili",
"kam": "Kamba",
"kik": "Kikiuyu",
"miji": "Mijikenda",
"luo": "Luo",
"bor": "Borana"
}

View File

@@ -1,7 +0,0 @@
keys,en,sw
account_successfully_created,You have been registered on Sarafu Network! To use dial *384*96# on Safaricom and *483*96# on other networks. For help %{support_phone}.,Umesajiliwa kwa huduma ya Sarafu! Kutumia bonyeza *384*96# Safaricom ama *483*46# kwa utandao tofauti. Kwa Usaidizi %{support_phone}.
received_tokens,Successfully received %{amount} %{token_symbol} from %{tx_sender_information} %{timestamp} to %{tx_recipient_information}. New balance is %{balance} %{token_symbol}.,Umepokea %{amount} %{token_symbol} kutoka kwa %{tx_sender_information} %{timestamp} ikapokewa na %{tx_recipient_information}. Salio lako ni %{balance} %{token_symbol}.
sent_tokens,Successfully sent %{amount} %{token_symbol} to %{tx_recipient_information} %{timestamp} from %{tx_sender_information}. New balance is %{balance} %{token_symbol}.,Umetuma %{amount} %{token_symbol} kwa %{tx_recipient_information} %{timestamp} kutoka kwa %{tx_sender_information}. Salio lako ni %{balance} %{token_symbol}.
terms,"By using the service, you agree to the terms and conditions at http://grassecon.org/tos","Kwa kutumia hii huduma, umekubali sheria na masharti yafuatayo http://grassecon.org/tos"
upsell_unregistered_recipient,%{tx_sender_information} tried to send you %{token_symbol} but you are not registered. To use dial *384*96# on Safaricom and *483*96# on other networks. For help %{support_phone}.,%{tx_sender_information} amejaribu kukutumia %{token_symbol} lakini hujasajili. Kutumia bonyeza *384*96# Safaricom ama *483*46# kwa utandao tofauti. Kwa Usaidizi %{support_phone}.
pin_reset_initiated,%{pin_initiator} has sent a request to initiate your PIN reset.,%{pin_initiator} ametuma ombi la kubadilisha PIN yako.
1 keys en sw
2 account_successfully_created You have been registered on Sarafu Network! To use dial *384*96# on Safaricom and *483*96# on other networks. For help %{support_phone}. Umesajiliwa kwa huduma ya Sarafu! Kutumia bonyeza *384*96# Safaricom ama *483*46# kwa utandao tofauti. Kwa Usaidizi %{support_phone}.
3 received_tokens Successfully received %{amount} %{token_symbol} from %{tx_sender_information} %{timestamp} to %{tx_recipient_information}. New balance is %{balance} %{token_symbol}. Umepokea %{amount} %{token_symbol} kutoka kwa %{tx_sender_information} %{timestamp} ikapokewa na %{tx_recipient_information}. Salio lako ni %{balance} %{token_symbol}.
4 sent_tokens Successfully sent %{amount} %{token_symbol} to %{tx_recipient_information} %{timestamp} from %{tx_sender_information}. New balance is %{balance} %{token_symbol}. Umetuma %{amount} %{token_symbol} kwa %{tx_recipient_information} %{timestamp} kutoka kwa %{tx_sender_information}. Salio lako ni %{balance} %{token_symbol}.
5 terms By using the service, you agree to the terms and conditions at http://grassecon.org/tos Kwa kutumia hii huduma, umekubali sheria na masharti yafuatayo http://grassecon.org/tos
6 upsell_unregistered_recipient %{tx_sender_information} tried to send you %{token_symbol} but you are not registered. To use dial *384*96# on Safaricom and *483*96# on other networks. For help %{support_phone}. %{tx_sender_information} amejaribu kukutumia %{token_symbol} lakini hujasajili. Kutumia bonyeza *384*96# Safaricom ama *483*46# kwa utandao tofauti. Kwa Usaidizi %{support_phone}.
7 pin_reset_initiated %{pin_initiator} has sent a request to initiate your PIN reset. %{pin_initiator} ametuma ombi la kubadilisha PIN yako.

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