Compare commits
2 Commits
lash/expli
...
lash/stale
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19c17f4bbb | ||
|
|
505185cec7
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -8,8 +8,3 @@ gmon.out
|
||||
*.egg-info
|
||||
dist/
|
||||
build/
|
||||
**/*sqlite
|
||||
**/.nyc_output
|
||||
**/coverage
|
||||
**/.venv
|
||||
.idea
|
||||
|
||||
@@ -16,7 +16,6 @@ import cic_base.config
|
||||
import cic_base.log
|
||||
import cic_base.argparse
|
||||
import cic_base.rpc
|
||||
from cic_base.eth.syncer import chain_interface
|
||||
from cic_eth_registry import CICRegistry
|
||||
from cic_eth_registry.error import UnknownContractError
|
||||
from chainlib.chain import ChainSpec
|
||||
@@ -29,8 +28,10 @@ from hexathon import (
|
||||
strip_0x,
|
||||
)
|
||||
from chainsyncer.backend.sql import SQLBackend
|
||||
from chainsyncer.driver.head import HeadSyncer
|
||||
from chainsyncer.driver.history import HistorySyncer
|
||||
from chainsyncer.driver import (
|
||||
HeadSyncer,
|
||||
HistorySyncer,
|
||||
)
|
||||
from chainsyncer.db.models.base import SessionBase
|
||||
|
||||
# local imports
|
||||
@@ -112,10 +113,10 @@ def main():
|
||||
logg.info('resuming sync session {}'.format(syncer_backend))
|
||||
|
||||
for syncer_backend in syncer_backends:
|
||||
syncers.append(HistorySyncer(syncer_backend, chain_interface))
|
||||
syncers.append(HistorySyncer(syncer_backend))
|
||||
|
||||
syncer_backend = SQLBackend.live(chain_spec, block_offset+1)
|
||||
syncers.append(HeadSyncer(syncer_backend, chain_interface))
|
||||
syncers.append(HeadSyncer(syncer_backend))
|
||||
|
||||
trusted_addresses_src = config.get('CIC_TRUST_ADDRESS')
|
||||
if trusted_addresses_src == None:
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
cic-base==0.1.3a3+build.984b5cff
|
||||
cic-base~=0.1.2b10
|
||||
alembic==1.4.2
|
||||
confini~=0.3.6rc3
|
||||
uwsgi==2.0.19.1
|
||||
moolb~=0.1.0
|
||||
cic-eth-registry~=0.5.6a1
|
||||
cic-eth-registry~=0.5.5a4
|
||||
SQLAlchemy==1.3.20
|
||||
semver==2.13.0
|
||||
psycopg2==2.8.6
|
||||
celery==4.4.7
|
||||
redis==3.5.3
|
||||
chainsyncer[sql]~=0.0.3a3
|
||||
erc20-faucet~=0.2.2a1
|
||||
chainsyncer[sql]~=0.0.2a4
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import os
|
||||
import argparse
|
||||
import logging
|
||||
import re
|
||||
|
||||
import alembic
|
||||
from alembic.config import Config as AlembicConfig
|
||||
@@ -24,8 +23,6 @@ argparser = argparse.ArgumentParser()
|
||||
argparser.add_argument('-c', type=str, default=config_dir, 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', 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()
|
||||
@@ -56,10 +53,4 @@ ac = AlembicConfig(os.path.join(migrations_dir, 'alembic.ini'))
|
||||
ac.set_main_option('sqlalchemy.url', dsn)
|
||||
ac.set_main_option('script_location', migrations_dir)
|
||||
|
||||
if args.reset:
|
||||
if not args.f:
|
||||
if not re.match(r'[yY][eE]?[sS]?', input('EEK! this will DELETE the existing db. are you sure??')):
|
||||
logg.error('user chickened out on requested reset, bailing')
|
||||
sys.exit(1)
|
||||
alembic.command.downgrade(ac, 'base')
|
||||
alembic.command.upgrade(ac, 'head')
|
||||
|
||||
@@ -6,5 +6,6 @@ sqlparse==0.4.1
|
||||
pytest-celery==0.0.0a1
|
||||
eth_tester==0.5.0b3
|
||||
py-evm==0.3.0a20
|
||||
cic_base[full]==0.1.3a3+build.984b5cff
|
||||
sarafu-faucet~=0.0.4a1
|
||||
web3==5.12.2
|
||||
cic-eth-registry~=0.5.5a3
|
||||
cic-base[full]==0.1.2b8
|
||||
|
||||
@@ -3,29 +3,31 @@
|
||||
APP_NAME: cic-eth
|
||||
DOCKERFILE_PATH: $APP_NAME/docker/Dockerfile
|
||||
|
||||
.cic_eth_mr_changes_target:
|
||||
.cic_eth_changes_target:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
changes:
|
||||
- $CONTEXT/$APP_NAME/**/*
|
||||
#changes:
|
||||
#- $CONTEXT/$APP_NAME/**/*
|
||||
when: always
|
||||
|
||||
build-mr-cic-eth:
|
||||
extends:
|
||||
- .cic_eth_variables
|
||||
- .cic_eth_mr_changes_target
|
||||
- .cic_eth_changes_target
|
||||
- .py_build_target_test
|
||||
|
||||
test-mr-cic-eth:
|
||||
extends:
|
||||
- .cic_eth_variables
|
||||
- .cic_eth_mr_changes_target
|
||||
- .cic_eth_changes_target
|
||||
stage: test
|
||||
image: $IMAGE_TAG_BASE
|
||||
image: $CI_REGISTRY_IMAGE/$APP_NAME-test:latest
|
||||
script:
|
||||
- cd apps/$APP_NAME/
|
||||
- pytest -x --cov=cic_eth --cov-fail-under=90 --cov-report term-missing tests
|
||||
needs: ["build-mr-cic-eth"]
|
||||
|
||||
build-push-cic-eth:
|
||||
extends:
|
||||
- .py_build_push
|
||||
- .cic_eth_variables
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
include *requirements.txt
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
SQLAlchemy==1.3.20
|
||||
cic-eth-registry~=0.5.6a1
|
||||
hexathon~=0.0.1a7
|
||||
chainqueue~=0.0.2b5
|
||||
eth-erc20==0.0.10a2
|
||||
@@ -5,3 +5,4 @@
|
||||
"""
|
||||
|
||||
from .api_task import Api
|
||||
from .api_admin import AdminApi
|
||||
|
||||
@@ -31,7 +31,7 @@ from chainqueue.db.enum import (
|
||||
status_str,
|
||||
)
|
||||
from chainqueue.error import TxStateChangeError
|
||||
from chainqueue.sql.query import get_tx
|
||||
from chainqueue.query import get_tx
|
||||
from eth_erc20 import ERC20
|
||||
|
||||
# local imports
|
||||
@@ -562,13 +562,13 @@ class AdminApi:
|
||||
tx['source_token_symbol'] = source_token.symbol
|
||||
o = erc20_c.balance_of(tx['source_token'], tx['sender'], sender_address=self.call_address)
|
||||
r = self.rpc.do(o)
|
||||
tx['sender_token_balance'] = erc20_c.parse_balance(r)
|
||||
tx['sender_token_balance'] = erc20_c.parse_balance_of(r)
|
||||
|
||||
if destination_token != None:
|
||||
tx['destination_token_symbol'] = destination_token.symbol
|
||||
o = erc20_c.balance_of(tx['destination_token'], tx['recipient'], sender_address=self.call_address)
|
||||
r = self.rpc.do(o)
|
||||
tx['recipient_token_balance'] = erc20_c.parse_balance(r)
|
||||
tx['recipient_token_balance'] = erc20_c.parse_balance_of(r)
|
||||
#tx['recipient_token_balance'] = destination_token.function('balanceOf')(tx['recipient']).call()
|
||||
|
||||
# TODO: this can mean either not subitted or culled, need to check other txs with same nonce to determine which
|
||||
@@ -8,10 +8,11 @@ import logging
|
||||
|
||||
# external imports
|
||||
import celery
|
||||
from cic_eth_registry import CICRegistry
|
||||
from chainlib.chain import ChainSpec
|
||||
|
||||
# local imports
|
||||
from cic_eth.enum import LockEnum
|
||||
from cic_eth.db.enum import LockEnum
|
||||
|
||||
app = celery.current_app
|
||||
|
||||
@@ -203,82 +204,6 @@ class Api:
|
||||
# return t
|
||||
|
||||
|
||||
def transfer_from(self, from_address, to_address, value, token_symbol, spender_address):
|
||||
"""Executes a chain of celery tasks that performs a transfer of ERC20 tokens by one address on behalf of another address to a third party.
|
||||
|
||||
:param from_address: Ethereum address of sender
|
||||
:type from_address: str, 0x-hex
|
||||
:param to_address: Ethereum address of recipient
|
||||
:type to_address: str, 0x-hex
|
||||
:param value: Estimated return from conversion
|
||||
:type value: int
|
||||
:param token_symbol: ERC20 token symbol of token to send
|
||||
:type token_symbol: str
|
||||
:param spender_address: Ethereum address of recipient
|
||||
:type spender_address: str, 0x-hex
|
||||
:returns: uuid of root task
|
||||
:rtype: celery.Task
|
||||
"""
|
||||
s_check = celery.signature(
|
||||
'cic_eth.admin.ctrl.check_lock',
|
||||
[
|
||||
[token_symbol],
|
||||
self.chain_spec.asdict(),
|
||||
LockEnum.QUEUE,
|
||||
from_address,
|
||||
],
|
||||
queue=self.queue,
|
||||
)
|
||||
s_nonce = celery.signature(
|
||||
'cic_eth.eth.nonce.reserve_nonce',
|
||||
[
|
||||
self.chain_spec.asdict(),
|
||||
from_address,
|
||||
],
|
||||
queue=self.queue,
|
||||
)
|
||||
s_tokens = celery.signature(
|
||||
'cic_eth.eth.erc20.resolve_tokens_by_symbol',
|
||||
[
|
||||
self.chain_spec.asdict(),
|
||||
],
|
||||
queue=self.queue,
|
||||
)
|
||||
s_allow = celery.signature(
|
||||
'cic_eth.eth.erc20.check_allowance',
|
||||
[
|
||||
from_address,
|
||||
value,
|
||||
self.chain_spec.asdict(),
|
||||
spender_address,
|
||||
],
|
||||
queue=self.queue,
|
||||
)
|
||||
s_transfer = celery.signature(
|
||||
'cic_eth.eth.erc20.transfer_from',
|
||||
[
|
||||
from_address,
|
||||
to_address,
|
||||
value,
|
||||
self.chain_spec.asdict(),
|
||||
spender_address,
|
||||
],
|
||||
queue=self.queue,
|
||||
)
|
||||
s_tokens.link(s_allow)
|
||||
s_nonce.link(s_tokens)
|
||||
s_check.link(s_nonce)
|
||||
if self.callback_param != None:
|
||||
s_transfer.link(self.callback_success)
|
||||
s_allow.link(s_transfer).on_error(self.callback_error)
|
||||
else:
|
||||
s_allow.link(s_transfer)
|
||||
|
||||
t = s_check.apply_async(queue=self.queue)
|
||||
return t
|
||||
|
||||
|
||||
|
||||
def transfer(self, from_address, to_address, value, token_symbol):
|
||||
"""Executes a chain of celery tasks that performs a transfer of ERC20 tokens from one address to another.
|
||||
|
||||
|
||||
@@ -1 +1,158 @@
|
||||
from cic_eth.enum import *
|
||||
# standard imports
|
||||
import enum
|
||||
|
||||
|
||||
@enum.unique
|
||||
class StatusBits(enum.IntEnum):
|
||||
"""Individual bit flags that are combined to define the state and legacy of a queued transaction
|
||||
|
||||
"""
|
||||
QUEUED = 0x01 # transaction should be sent to network
|
||||
IN_NETWORK = 0x08 # transaction is in network
|
||||
|
||||
DEFERRED = 0x10 # an attempt to send the transaction to network has failed
|
||||
GAS_ISSUES = 0x20 # transaction is pending sender account gas funding
|
||||
|
||||
LOCAL_ERROR = 0x100 # errors that originate internally from the component
|
||||
NODE_ERROR = 0x200 # errors originating in the node (invalid RLP input...)
|
||||
NETWORK_ERROR = 0x400 # errors that originate from the network (REVERT)
|
||||
UNKNOWN_ERROR = 0x800 # unclassified errors (the should not occur)
|
||||
|
||||
FINAL = 0x1000 # transaction processing has completed
|
||||
OBSOLETE = 0x2000 # transaction has been replaced by a different transaction with higher fee
|
||||
MANUAL = 0x8000 # transaction processing has been manually overridden
|
||||
|
||||
|
||||
@enum.unique
|
||||
class StatusEnum(enum.IntEnum):
|
||||
"""
|
||||
|
||||
- Inactive, not finalized. (<0)
|
||||
* PENDING: The initial state of a newly added transaction record. No action has been performed on this transaction yet.
|
||||
* SENDFAIL: The transaction was not received by the node.
|
||||
* RETRY: The transaction is queued for a new send attempt after previously failing.
|
||||
* READYSEND: The transaction is queued for its first send attempt
|
||||
* OBSOLETED: A new transaction with the same nonce and higher gas has been sent to network.
|
||||
* WAITFORGAS: The transaction is on hold pending gas funding.
|
||||
- Active state: (==0)
|
||||
* SENT: The transaction has been sent to the mempool.
|
||||
- Inactive, finalized. (>0)
|
||||
* FUBAR: Unknown error occurred and transaction is abandoned. Manual intervention needed.
|
||||
* CANCELLED: The transaction was sent, but was not mined and has disappered from the mempool. This usually follows a transaction being obsoleted.
|
||||
* OVERRIDDEN: Transaction has been manually overriden.
|
||||
* REJECTED: The transaction was rejected by the node.
|
||||
* REVERTED: The transaction was mined, but exception occurred during EVM execution. (Block number will be set)
|
||||
* SUCCESS: THe transaction was successfully mined. (Block number will be set)
|
||||
|
||||
"""
|
||||
PENDING = 0
|
||||
|
||||
SENDFAIL = StatusBits.DEFERRED | StatusBits.LOCAL_ERROR
|
||||
RETRY = StatusBits.QUEUED | StatusBits.DEFERRED
|
||||
READYSEND = StatusBits.QUEUED
|
||||
|
||||
OBSOLETED = StatusBits.OBSOLETE | StatusBits.IN_NETWORK
|
||||
|
||||
WAITFORGAS = StatusBits.GAS_ISSUES
|
||||
|
||||
SENT = StatusBits.IN_NETWORK
|
||||
FUBAR = StatusBits.FINAL | StatusBits.UNKNOWN_ERROR
|
||||
CANCELLED = StatusBits.IN_NETWORK | StatusBits.FINAL | StatusBits.OBSOLETE
|
||||
OVERRIDDEN = StatusBits.FINAL | StatusBits.OBSOLETE | StatusBits.MANUAL
|
||||
|
||||
REJECTED = StatusBits.NODE_ERROR | StatusBits.FINAL
|
||||
REVERTED = StatusBits.IN_NETWORK | StatusBits.FINAL | StatusBits.NETWORK_ERROR
|
||||
SUCCESS = StatusBits.IN_NETWORK | StatusBits.FINAL
|
||||
|
||||
|
||||
@enum.unique
|
||||
class LockEnum(enum.IntEnum):
|
||||
"""
|
||||
STICKY: When set, reset is not possible
|
||||
CREATE: Disable creation of accounts
|
||||
SEND: Disable sending to network
|
||||
QUEUE: Disable queueing new or modified transactions
|
||||
"""
|
||||
STICKY=1
|
||||
INIT=2
|
||||
CREATE=4
|
||||
SEND=8
|
||||
QUEUE=16
|
||||
QUERY=32
|
||||
ALL=int(0xfffffffffffffffe)
|
||||
|
||||
|
||||
def status_str(v, bits_only=False):
|
||||
"""Render a human-readable string describing the status
|
||||
|
||||
If the bit field exactly matches a StatusEnum value, the StatusEnum label will be returned.
|
||||
|
||||
If a StatusEnum cannot be matched, the string will be postfixed with "*", unless explicitly instructed to return bit field labels only.
|
||||
|
||||
:param v: Status bit field
|
||||
:type v: number
|
||||
:param bits_only: Only render individual bit labels.
|
||||
:type bits_only: bool
|
||||
:returns: Status string
|
||||
:rtype: str
|
||||
"""
|
||||
s = ''
|
||||
if not bits_only:
|
||||
try:
|
||||
s = StatusEnum(v).name
|
||||
return s
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if v == 0:
|
||||
return 'NONE'
|
||||
|
||||
for i in range(16):
|
||||
b = (1 << i)
|
||||
if (b & 0xffff) & v:
|
||||
n = StatusBits(b).name
|
||||
if len(s) > 0:
|
||||
s += ','
|
||||
s += n
|
||||
if not bits_only:
|
||||
s += '*'
|
||||
return s
|
||||
|
||||
|
||||
def all_errors():
|
||||
"""Bit mask of all error states
|
||||
|
||||
:returns: Error flags
|
||||
:rtype: number
|
||||
"""
|
||||
return StatusBits.LOCAL_ERROR | StatusBits.NODE_ERROR | StatusBits.NETWORK_ERROR | StatusBits.UNKNOWN_ERROR
|
||||
|
||||
|
||||
def is_error_status(v):
|
||||
"""Check if value is an error state
|
||||
|
||||
:param v: Status bit field
|
||||
:type v: number
|
||||
:returns: True if error
|
||||
:rtype: bool
|
||||
"""
|
||||
return bool(v & all_errors())
|
||||
|
||||
|
||||
def dead():
|
||||
"""Bit mask defining whether a transaction is still likely to be processed on the network.
|
||||
|
||||
:returns: Bit mask
|
||||
:rtype: number
|
||||
"""
|
||||
return StatusBits.FINAL | StatusBits.OBSOLETE
|
||||
|
||||
|
||||
def is_alive(v):
|
||||
"""Check if transaction is still likely to be processed on the network.
|
||||
|
||||
The contingency of "likely" refers to the case a transaction has been obsoleted after sent to the network, but the network still confirms the obsoleted transaction. The return value of this method will not change as a result of this, BUT the state itself will (as the FINAL bit will be set).
|
||||
|
||||
:returns:
|
||||
"""
|
||||
return bool(v & dead() == 0)
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
# standard imports
|
||||
import enum
|
||||
|
||||
|
||||
@enum.unique
|
||||
class StatusBits(enum.IntEnum):
|
||||
"""Individual bit flags that are combined to define the state and legacy of a queued transaction
|
||||
|
||||
"""
|
||||
QUEUED = 0x01 # transaction should be sent to network
|
||||
IN_NETWORK = 0x08 # transaction is in network
|
||||
|
||||
DEFERRED = 0x10 # an attempt to send the transaction to network has failed
|
||||
GAS_ISSUES = 0x20 # transaction is pending sender account gas funding
|
||||
|
||||
LOCAL_ERROR = 0x100 # errors that originate internally from the component
|
||||
NODE_ERROR = 0x200 # errors originating in the node (invalid RLP input...)
|
||||
NETWORK_ERROR = 0x400 # errors that originate from the network (REVERT)
|
||||
UNKNOWN_ERROR = 0x800 # unclassified errors (the should not occur)
|
||||
|
||||
FINAL = 0x1000 # transaction processing has completed
|
||||
OBSOLETE = 0x2000 # transaction has been replaced by a different transaction with higher fee
|
||||
MANUAL = 0x8000 # transaction processing has been manually overridden
|
||||
|
||||
|
||||
@enum.unique
|
||||
class StatusEnum(enum.IntEnum):
|
||||
"""
|
||||
|
||||
- Inactive, not finalized. (<0)
|
||||
* PENDING: The initial state of a newly added transaction record. No action has been performed on this transaction yet.
|
||||
* SENDFAIL: The transaction was not received by the node.
|
||||
* RETRY: The transaction is queued for a new send attempt after previously failing.
|
||||
* READYSEND: The transaction is queued for its first send attempt
|
||||
* OBSOLETED: A new transaction with the same nonce and higher gas has been sent to network.
|
||||
* WAITFORGAS: The transaction is on hold pending gas funding.
|
||||
- Active state: (==0)
|
||||
* SENT: The transaction has been sent to the mempool.
|
||||
- Inactive, finalized. (>0)
|
||||
* FUBAR: Unknown error occurred and transaction is abandoned. Manual intervention needed.
|
||||
* CANCELLED: The transaction was sent, but was not mined and has disappered from the mempool. This usually follows a transaction being obsoleted.
|
||||
* OVERRIDDEN: Transaction has been manually overriden.
|
||||
* REJECTED: The transaction was rejected by the node.
|
||||
* REVERTED: The transaction was mined, but exception occurred during EVM execution. (Block number will be set)
|
||||
* SUCCESS: THe transaction was successfully mined. (Block number will be set)
|
||||
|
||||
"""
|
||||
PENDING = 0
|
||||
|
||||
SENDFAIL = StatusBits.DEFERRED | StatusBits.LOCAL_ERROR
|
||||
RETRY = StatusBits.QUEUED | StatusBits.DEFERRED
|
||||
READYSEND = StatusBits.QUEUED
|
||||
|
||||
OBSOLETED = StatusBits.OBSOLETE | StatusBits.IN_NETWORK
|
||||
|
||||
WAITFORGAS = StatusBits.GAS_ISSUES
|
||||
|
||||
SENT = StatusBits.IN_NETWORK
|
||||
FUBAR = StatusBits.FINAL | StatusBits.UNKNOWN_ERROR
|
||||
CANCELLED = StatusBits.IN_NETWORK | StatusBits.FINAL | StatusBits.OBSOLETE
|
||||
OVERRIDDEN = StatusBits.FINAL | StatusBits.OBSOLETE | StatusBits.MANUAL
|
||||
|
||||
REJECTED = StatusBits.NODE_ERROR | StatusBits.FINAL
|
||||
REVERTED = StatusBits.IN_NETWORK | StatusBits.FINAL | StatusBits.NETWORK_ERROR
|
||||
SUCCESS = StatusBits.IN_NETWORK | StatusBits.FINAL
|
||||
|
||||
|
||||
@enum.unique
|
||||
class LockEnum(enum.IntEnum):
|
||||
"""
|
||||
STICKY: When set, reset is not possible
|
||||
CREATE: Disable creation of accounts
|
||||
SEND: Disable sending to network
|
||||
QUEUE: Disable queueing new or modified transactions
|
||||
"""
|
||||
STICKY=1
|
||||
INIT=2
|
||||
CREATE=4
|
||||
SEND=8
|
||||
QUEUE=16
|
||||
QUERY=32
|
||||
ALL=int(0xfffffffffffffffe)
|
||||
|
||||
|
||||
def status_str(v, bits_only=False):
|
||||
"""Render a human-readable string describing the status
|
||||
|
||||
If the bit field exactly matches a StatusEnum value, the StatusEnum label will be returned.
|
||||
|
||||
If a StatusEnum cannot be matched, the string will be postfixed with "*", unless explicitly instructed to return bit field labels only.
|
||||
|
||||
:param v: Status bit field
|
||||
:type v: number
|
||||
:param bits_only: Only render individual bit labels.
|
||||
:type bits_only: bool
|
||||
:returns: Status string
|
||||
:rtype: str
|
||||
"""
|
||||
s = ''
|
||||
if not bits_only:
|
||||
try:
|
||||
s = StatusEnum(v).name
|
||||
return s
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if v == 0:
|
||||
return 'NONE'
|
||||
|
||||
for i in range(16):
|
||||
b = (1 << i)
|
||||
if (b & 0xffff) & v:
|
||||
n = StatusBits(b).name
|
||||
if len(s) > 0:
|
||||
s += ','
|
||||
s += n
|
||||
if not bits_only:
|
||||
s += '*'
|
||||
return s
|
||||
|
||||
|
||||
def all_errors():
|
||||
"""Bit mask of all error states
|
||||
|
||||
:returns: Error flags
|
||||
:rtype: number
|
||||
"""
|
||||
return StatusBits.LOCAL_ERROR | StatusBits.NODE_ERROR | StatusBits.NETWORK_ERROR | StatusBits.UNKNOWN_ERROR
|
||||
|
||||
|
||||
def is_error_status(v):
|
||||
"""Check if value is an error state
|
||||
|
||||
:param v: Status bit field
|
||||
:type v: number
|
||||
:returns: True if error
|
||||
:rtype: bool
|
||||
"""
|
||||
return bool(v & all_errors())
|
||||
|
||||
|
||||
def dead():
|
||||
"""Bit mask defining whether a transaction is still likely to be processed on the network.
|
||||
|
||||
:returns: Bit mask
|
||||
:rtype: number
|
||||
"""
|
||||
return StatusBits.FINAL | StatusBits.OBSOLETE
|
||||
|
||||
|
||||
def is_alive(v):
|
||||
"""Check if transaction is still likely to be processed on the network.
|
||||
|
||||
The contingency of "likely" refers to the case a transaction has been obsoleted after sent to the network, but the network still confirms the obsoleted transaction. The return value of this method will not change as a result of this, BUT the state itself will (as the FINAL bit will be set).
|
||||
|
||||
:returns:
|
||||
"""
|
||||
return bool(v & dead() == 0)
|
||||
@@ -80,8 +80,3 @@ class SignerError(SeppukuError):
|
||||
class RoleAgencyError(SeppukuError):
|
||||
"""Exception raise when a role cannot perform its function. This is a critical exception
|
||||
"""
|
||||
|
||||
|
||||
class YouAreBrokeError(Exception):
|
||||
"""Exception raised when a value transfer is attempted without access to sufficient funds
|
||||
"""
|
||||
|
||||
@@ -24,7 +24,6 @@ from cic_eth.error import (
|
||||
TokenCountError,
|
||||
PermanentTxError,
|
||||
OutOfGasError,
|
||||
YouAreBrokeError,
|
||||
)
|
||||
from cic_eth.queue.tx import register_tx
|
||||
from cic_eth.eth.gas import (
|
||||
@@ -72,117 +71,6 @@ def balance(tokens, holder_address, chain_spec_dict):
|
||||
return tokens
|
||||
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def check_allowance(self, tokens, holder_address, value, chain_spec_dict, spender_address):
|
||||
"""Best-effort verification that the allowance for a transfer from spend is sufficient.
|
||||
|
||||
:raises YouAreBrokeError: If allowance is insufficient
|
||||
|
||||
:param tokens: Token addresses
|
||||
:type tokens: list of str, 0x-hex
|
||||
:param holder_address: Token holder address
|
||||
:type holder_address: str, 0x-hex
|
||||
:param value: Amount of token, in 'wei'
|
||||
:type value: int
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
:param spender_address: Address of account spending on behalf of holder
|
||||
:type spender_address: str, 0x-hex
|
||||
:return: Token list as passed to task
|
||||
:rtype: dict
|
||||
"""
|
||||
logg.debug('tokens {}'.format(tokens))
|
||||
if len(tokens) != 1:
|
||||
raise TokenCountError
|
||||
t = tokens[0]
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
|
||||
rpc = RPCConnection.connect(chain_spec, 'default')
|
||||
|
||||
caller_address = ERC20Token.caller_address
|
||||
c = ERC20(chain_spec)
|
||||
o = c.allowance(t['address'], holder_address, spender_address, sender_address=caller_address)
|
||||
r = rpc.do(o)
|
||||
allowance = c.parse_allowance(r)
|
||||
if allowance < value:
|
||||
errstr = 'allowance {} insufficent to transfer {} {} by {} on behalf of {}'.format(allowance, value, t['symbol'], spender_address, holder_address)
|
||||
logg.error(errstr)
|
||||
raise YouAreBrokeError(errstr)
|
||||
|
||||
return tokens
|
||||
|
||||
|
||||
@celery_app.task(bind=True, base=CriticalSQLAlchemyAndSignerTask)
|
||||
def transfer_from(self, tokens, holder_address, receiver_address, value, chain_spec_dict, spender_address):
|
||||
"""Transfer ERC20 tokens between addresses
|
||||
|
||||
First argument is a list of tokens, to enable the task to be chained to the symbol to token address resolver function. However, it accepts only one token as argument.
|
||||
|
||||
:param tokens: Token addresses
|
||||
:type tokens: list of str, 0x-hex
|
||||
:param holder_address: Token holder address
|
||||
:type holder_address: str, 0x-hex
|
||||
:param receiver_address: Token receiver address
|
||||
:type receiver_address: str, 0x-hex
|
||||
:param value: Amount of token, in 'wei'
|
||||
:type value: int
|
||||
:param chain_str: Chain spec string representation
|
||||
:type chain_str: str
|
||||
:param spender_address: Address of account spending on behalf of holder
|
||||
:type spender_address: str, 0x-hex
|
||||
:raises TokenCountError: Either none or more then one tokens have been passed as tokens argument
|
||||
:return: Transaction hash for tranfer operation
|
||||
:rtype: str, 0x-hex
|
||||
"""
|
||||
# we only allow one token, one transfer
|
||||
logg.debug('tokens {}'.format(tokens))
|
||||
if len(tokens) != 1:
|
||||
raise TokenCountError
|
||||
t = tokens[0]
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
queue = self.request.delivery_info.get('routing_key')
|
||||
|
||||
rpc = RPCConnection.connect(chain_spec, 'default')
|
||||
rpc_signer = RPCConnection.connect(chain_spec, 'signer')
|
||||
|
||||
session = self.create_session()
|
||||
nonce_oracle = CustodialTaskNonceOracle(holder_address, self.request.root_id, session=session)
|
||||
gas_oracle = self.create_gas_oracle(rpc, MaxGasOracle.gas)
|
||||
c = ERC20(chain_spec, signer=rpc_signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle)
|
||||
try:
|
||||
(tx_hash_hex, tx_signed_raw_hex) = c.transfer_from(t['address'], spender_address, holder_address, receiver_address, value, tx_format=TxFormat.RLP_SIGNED)
|
||||
except FileNotFoundError as e:
|
||||
raise SignerError(e)
|
||||
except ConnectionError as e:
|
||||
raise SignerError(e)
|
||||
|
||||
|
||||
rpc_signer.disconnect()
|
||||
rpc.disconnect()
|
||||
|
||||
cache_task = 'cic_eth.eth.erc20.cache_transfer_from_data'
|
||||
|
||||
register_tx(tx_hash_hex, tx_signed_raw_hex, chain_spec, queue, cache_task=cache_task, session=session)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
gas_pair = gas_oracle.get_gas(tx_signed_raw_hex)
|
||||
gas_budget = gas_pair[0] * gas_pair[1]
|
||||
logg.debug('transfer tx {} {} {}'.format(tx_hash_hex, queue, gas_budget))
|
||||
|
||||
s = create_check_gas_task(
|
||||
[tx_signed_raw_hex],
|
||||
chain_spec,
|
||||
holder_address,
|
||||
gas_budget,
|
||||
[tx_hash_hex],
|
||||
queue,
|
||||
)
|
||||
s.apply_async()
|
||||
return tx_hash_hex
|
||||
|
||||
|
||||
|
||||
@celery_app.task(bind=True, base=CriticalSQLAlchemyAndSignerTask)
|
||||
def transfer(self, tokens, holder_address, receiver_address, value, chain_spec_dict):
|
||||
"""Transfer ERC20 tokens between addresses
|
||||
@@ -344,7 +232,6 @@ def resolve_tokens_by_symbol(self, token_symbols, chain_spec_dict):
|
||||
logg.debug('token {}'.format(token_address))
|
||||
tokens.append({
|
||||
'address': token_address,
|
||||
'symbol': token_symbol,
|
||||
'converters': [],
|
||||
})
|
||||
rpc.disconnect()
|
||||
@@ -392,48 +279,6 @@ def cache_transfer_data(
|
||||
return (tx_hash_hex, cache_id)
|
||||
|
||||
|
||||
@celery_app.task(base=CriticalSQLAlchemyTask)
|
||||
def cache_transfer_from_data(
|
||||
tx_hash_hex,
|
||||
tx_signed_raw_hex,
|
||||
chain_spec_dict,
|
||||
):
|
||||
"""Helper function for otx_cache_transfer_from
|
||||
|
||||
:param tx_hash_hex: Transaction hash
|
||||
:type tx_hash_hex: str, 0x-hex
|
||||
:param tx: Signed raw transaction
|
||||
:type tx: str, 0x-hex
|
||||
:returns: Transaction hash and id of cache element in storage backend, respectively
|
||||
:rtype: tuple
|
||||
"""
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
tx_signed_raw_bytes = bytes.fromhex(strip_0x(tx_signed_raw_hex))
|
||||
tx = unpack(tx_signed_raw_bytes, chain_spec)
|
||||
|
||||
tx_data = ERC20.parse_transfer_from_request(tx['data'])
|
||||
spender_address = tx_data[0]
|
||||
recipient_address = tx_data[1]
|
||||
token_value = tx_data[2]
|
||||
|
||||
session = SessionBase.create_session()
|
||||
tx_cache = TxCache(
|
||||
tx_hash_hex,
|
||||
tx['from'],
|
||||
recipient_address,
|
||||
tx['to'],
|
||||
tx['to'],
|
||||
token_value,
|
||||
token_value,
|
||||
session=session,
|
||||
)
|
||||
session.add(tx_cache)
|
||||
session.commit()
|
||||
cache_id = tx_cache.id
|
||||
session.close()
|
||||
return (tx_hash_hex, cache_id)
|
||||
|
||||
|
||||
@celery_app.task(base=CriticalSQLAlchemyTask)
|
||||
def cache_approve_data(
|
||||
tx_hash_hex,
|
||||
|
||||
@@ -19,7 +19,7 @@ from cic_eth_registry import CICRegistry
|
||||
from cic_eth_registry.erc20 import ERC20Token
|
||||
from chainqueue.db.models.otx import Otx
|
||||
from chainqueue.db.enum import StatusEnum
|
||||
from chainqueue.sql.query import get_tx_cache
|
||||
from chainqueue.query import get_tx_cache
|
||||
from eth_erc20 import ERC20
|
||||
|
||||
# local imports
|
||||
|
||||
@@ -5,7 +5,7 @@ import datetime
|
||||
import celery
|
||||
from chainlib.chain import ChainSpec
|
||||
from chainlib.eth.tx import unpack
|
||||
import chainqueue.sql.query
|
||||
import chainqueue.query
|
||||
from chainqueue.db.enum import (
|
||||
StatusEnum,
|
||||
is_alive,
|
||||
@@ -28,7 +28,7 @@ celery_app = celery.current_app
|
||||
def get_tx_cache(chain_spec_dict, tx_hash):
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
session = SessionBase.create_session()
|
||||
r = chainqueue.sql.query.get_tx_cache(chain_spec, tx_hash, session=session)
|
||||
r = chainqueue.query.get_tx_cache(chain_spec, tx_hash, session=session)
|
||||
session.close()
|
||||
return r
|
||||
|
||||
@@ -37,7 +37,7 @@ def get_tx_cache(chain_spec_dict, tx_hash):
|
||||
def get_tx(chain_spec_dict, tx_hash):
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
session = SessionBase.create_session()
|
||||
r = chainqueue.sql.query.get_tx(chain_spec, tx_hash, session=session)
|
||||
r = chainqueue.query.get_tx(chain_spec, tx_hash, session=session)
|
||||
session.close()
|
||||
return r
|
||||
|
||||
@@ -46,7 +46,7 @@ def get_tx(chain_spec_dict, tx_hash):
|
||||
def get_account_tx(chain_spec_dict, address, as_sender=True, as_recipient=True, counterpart=None):
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
session = SessionBase.create_session()
|
||||
r = chainqueue.sql.query.get_account_tx(chain_spec, address, as_sender=True, as_recipient=True, counterpart=None, session=session)
|
||||
r = chainqueue.query.get_account_tx(chain_spec, address, as_sender=True, as_recipient=True, counterpart=None, session=session)
|
||||
session.close()
|
||||
return r
|
||||
|
||||
@@ -55,17 +55,17 @@ def get_account_tx(chain_spec_dict, address, as_sender=True, as_recipient=True,
|
||||
def get_upcoming_tx_nolock(chain_spec_dict, status=StatusEnum.READYSEND, not_status=None, recipient=None, before=None, limit=0, session=None):
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
session = SessionBase.create_session()
|
||||
r = chainqueue.sql.query.get_upcoming_tx(chain_spec, status, not_status=not_status, recipient=recipient, before=before, limit=limit, session=session, decoder=unpack)
|
||||
r = chainqueue.query.get_upcoming_tx(chain_spec, status, not_status=not_status, recipient=recipient, before=before, limit=limit, session=session, decoder=unpack)
|
||||
session.close()
|
||||
return r
|
||||
|
||||
|
||||
def get_status_tx(chain_spec, status, not_status=None, before=None, exact=False, limit=0, session=None):
|
||||
return chainqueue.sql.query.get_status_tx_cache(chain_spec, status, not_status=not_status, before=before, exact=exact, limit=limit, session=session, decoder=unpack)
|
||||
return chainqueue.query.get_status_tx_cache(chain_spec, status, not_status=not_status, before=before, exact=exact, limit=limit, session=session, decoder=unpack)
|
||||
|
||||
|
||||
def get_paused_tx(chain_spec, status=None, sender=None, session=None, decoder=None):
|
||||
return chainqueue.sql.query.get_paused_tx_cache(chain_spec, status=status, sender=sender, session=session, decoder=unpack)
|
||||
return chainqueue.query.get_paused_tx_cache(chain_spec, status=status, sender=sender, session=session, decoder=unpack)
|
||||
|
||||
|
||||
def get_nonce_tx(chain_spec, nonce, sender):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# external imports
|
||||
from chainlib.chain import ChainSpec
|
||||
import chainqueue.sql.state
|
||||
import chainqueue.state
|
||||
|
||||
# local imports
|
||||
import celery
|
||||
@@ -14,7 +14,7 @@ celery_app = celery.current_app
|
||||
def set_sent(chain_spec_dict, tx_hash, fail=False):
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
session = SessionBase.create_session()
|
||||
r = chainqueue.sql.state.set_sent(chain_spec, tx_hash, fail, session=session)
|
||||
r = chainqueue.state.set_sent(chain_spec, tx_hash, fail, session=session)
|
||||
session.close()
|
||||
return r
|
||||
|
||||
@@ -23,7 +23,7 @@ def set_sent(chain_spec_dict, tx_hash, fail=False):
|
||||
def set_final(chain_spec_dict, tx_hash, block=None, tx_index=None, fail=False):
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
session = SessionBase.create_session()
|
||||
r = chainqueue.sql.state.set_final(chain_spec, tx_hash, block=block, tx_index=tx_index, fail=fail, session=session)
|
||||
r = chainqueue.state.set_final(chain_spec, tx_hash, block=block, tx_index=tx_index, fail=fail, session=session)
|
||||
session.close()
|
||||
return r
|
||||
|
||||
@@ -32,7 +32,7 @@ def set_final(chain_spec_dict, tx_hash, block=None, tx_index=None, fail=False):
|
||||
def set_cancel(chain_spec_dict, tx_hash, manual=False):
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
session = SessionBase.create_session()
|
||||
r = chainqueue.sql.state.set_cancel(chain_spec, tx_hash, manual, session=session)
|
||||
r = chainqueue.state.set_cancel(chain_spec, tx_hash, manual, session=session)
|
||||
session.close()
|
||||
return r
|
||||
|
||||
@@ -41,7 +41,7 @@ def set_cancel(chain_spec_dict, tx_hash, manual=False):
|
||||
def set_rejected(chain_spec_dict, tx_hash):
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
session = SessionBase.create_session()
|
||||
r = chainqueue.sql.state.set_rejected(chain_spec, tx_hash, session=session)
|
||||
r = chainqueue.state.set_rejected(chain_spec, tx_hash, session=session)
|
||||
session.close()
|
||||
return r
|
||||
|
||||
@@ -50,7 +50,7 @@ def set_rejected(chain_spec_dict, tx_hash):
|
||||
def set_fubar(chain_spec_dict, tx_hash):
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
session = SessionBase.create_session()
|
||||
r = chainqueue.sql.state.set_fubar(chain_spec, tx_hash, session=session)
|
||||
r = chainqueue.state.set_fubar(chain_spec, tx_hash, session=session)
|
||||
session.close()
|
||||
return r
|
||||
|
||||
@@ -59,7 +59,7 @@ def set_fubar(chain_spec_dict, tx_hash):
|
||||
def set_manual(chain_spec_dict, tx_hash):
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
session = SessionBase.create_session()
|
||||
r = chainqueue.sql.state.set_manual(chain_spec, tx_hash, session=session)
|
||||
r = chainqueue.state.set_manual(chain_spec, tx_hash, session=session)
|
||||
session.close()
|
||||
return r
|
||||
|
||||
@@ -68,7 +68,7 @@ def set_manual(chain_spec_dict, tx_hash):
|
||||
def set_ready(chain_spec_dict, tx_hash):
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
session = SessionBase.create_session()
|
||||
r = chainqueue.sql.state.set_ready(chain_spec, tx_hash, session=session)
|
||||
r = chainqueue.state.set_ready(chain_spec, tx_hash, session=session)
|
||||
session.close()
|
||||
return r
|
||||
|
||||
@@ -77,7 +77,7 @@ def set_ready(chain_spec_dict, tx_hash):
|
||||
def set_reserved(chain_spec_dict, tx_hash):
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
session = SessionBase.create_session()
|
||||
r = chainqueue.sql.state.set_reserved(chain_spec, tx_hash, session=session)
|
||||
r = chainqueue.state.set_reserved(chain_spec, tx_hash, session=session)
|
||||
session.close()
|
||||
return r
|
||||
|
||||
@@ -86,7 +86,7 @@ def set_reserved(chain_spec_dict, tx_hash):
|
||||
def set_waitforgas(chain_spec_dict, tx_hash):
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
session = SessionBase.create_session()
|
||||
r = chainqueue.sql.state.set_waitforgas(chain_spec, tx_hash, session=session)
|
||||
r = chainqueue.state.set_waitforgas(chain_spec, tx_hash, session=session)
|
||||
session.close()
|
||||
return r
|
||||
|
||||
@@ -95,7 +95,7 @@ def set_waitforgas(chain_spec_dict, tx_hash):
|
||||
def get_state_log(chain_spec_dict, tx_hash):
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
session = SessionBase.create_session()
|
||||
r = chainqueue.sql.state.get_state_log(chain_spec, tx_hash, session=session)
|
||||
r = chainqueue.state.get_state_log(chain_spec, tx_hash, session=session)
|
||||
session.close()
|
||||
return r
|
||||
|
||||
@@ -104,6 +104,6 @@ def get_state_log(chain_spec_dict, tx_hash):
|
||||
def obsolete(chain_spec_dict, tx_hash, final):
|
||||
chain_spec = ChainSpec.from_dict(chain_spec_dict)
|
||||
session = SessionBase.create_session()
|
||||
r = chainqueue.sql.state.obsolete_by_cache(chain_spec, tx_hash, final, session=session)
|
||||
r = chainqueue.state.obsolete_by_cache(chain_spec, tx_hash, final, session=session)
|
||||
session.close()
|
||||
return r
|
||||
|
||||
@@ -15,14 +15,14 @@ from sqlalchemy import tuple_
|
||||
from sqlalchemy import func
|
||||
from chainlib.chain import ChainSpec
|
||||
from chainlib.eth.tx import unpack
|
||||
import chainqueue.sql.state
|
||||
import chainqueue.state
|
||||
from chainqueue.db.enum import (
|
||||
StatusEnum,
|
||||
StatusBits,
|
||||
is_alive,
|
||||
dead,
|
||||
)
|
||||
from chainqueue.sql.tx import create
|
||||
from chainqueue.tx import create
|
||||
from chainqueue.error import NotLocalTxError
|
||||
from chainqueue.db.enum import status_str
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from chainlib.eth.constant import ZERO_ADDRESS
|
||||
from chainlib.eth.address import is_checksum_address
|
||||
|
||||
# local imports
|
||||
from cic_eth.api.admin import AdminApi
|
||||
from cic_eth.api import AdminApi
|
||||
from cic_eth.db.enum import LockEnum
|
||||
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
|
||||
@@ -21,7 +21,7 @@ from chainqueue.db.enum import (
|
||||
StatusBits,
|
||||
)
|
||||
from chainqueue.error import NotLocalTxError
|
||||
from chainqueue.sql.state import set_reserved
|
||||
from chainqueue.state import set_reserved
|
||||
|
||||
# local imports
|
||||
import cic_eth
|
||||
|
||||
@@ -10,7 +10,7 @@ from chainlib.eth.tx import unpack
|
||||
from chainqueue.db.enum import StatusBits
|
||||
from chainqueue.db.models.tx import TxCache
|
||||
from chainqueue.db.models.otx import Otx
|
||||
from chainqueue.sql.query import get_paused_tx_cache as get_paused_tx
|
||||
from chainqueue.query import get_paused_tx_cache as get_paused_tx
|
||||
|
||||
# local imports
|
||||
from cic_eth.db.models.base import SessionBase
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
|
||||
# external imports
|
||||
import celery
|
||||
from chainqueue.sql.state import obsolete_by_cache
|
||||
from chainqueue.state import obsolete_by_cache
|
||||
|
||||
logg = logging.getLogger()
|
||||
|
||||
|
||||
136
apps/cic-eth/cic_eth/runnable/daemons/server.py
Normal file
136
apps/cic-eth/cic_eth/runnable/daemons/server.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# standard imports
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
import argparse
|
||||
import json
|
||||
|
||||
# third-party imports
|
||||
import web3
|
||||
import confini
|
||||
import celery
|
||||
from json.decoder import JSONDecodeError
|
||||
from cic_registry.chain import ChainSpec
|
||||
|
||||
# local imports
|
||||
from cic_eth.db import dsn_from_config
|
||||
from cic_eth.db.models.base import SessionBase
|
||||
from cic_eth.eth.util import unpack_signed_raw_tx
|
||||
|
||||
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_eth', 'db')
|
||||
migrationsdir = os.path.join(dbdir, 'migrations')
|
||||
|
||||
config_dir = os.path.join('/usr/local/etc/cic-eth')
|
||||
|
||||
argparser = argparse.ArgumentParser()
|
||||
argparser.add_argument('-c', type=str, default=config_dir, help='config file')
|
||||
argparser.add_argument('-i', '--chain-spec', dest='i', type=str, help='chain spec')
|
||||
argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration')
|
||||
argparser.add_argument('-q', type=str, default='cic-eth', help='queue name for worker tasks')
|
||||
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()
|
||||
|
||||
if args.vv:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
elif args.v:
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
config = confini.Config(args.c, args.env_prefix)
|
||||
config.process()
|
||||
args_override = {
|
||||
'CIC_CHAIN_SPEC': getattr(args, 'i'),
|
||||
}
|
||||
config.censor('PASSWORD', 'DATABASE')
|
||||
config.censor('PASSWORD', 'SSL')
|
||||
logg.debug('config:\n{}'.format(config))
|
||||
|
||||
dsn = dsn_from_config(config)
|
||||
SessionBase.connect(dsn)
|
||||
|
||||
celery_app = celery.Celery(backend=config.get('CELERY_RESULT_URL'), broker=config.get('CELERY_BROKER_URL'))
|
||||
queue = args.q
|
||||
|
||||
re_something = r'^/something/?'
|
||||
|
||||
chain_spec = ChainSpec.from_chain_str(config.get('CIC_CHAIN_SPEC'))
|
||||
|
||||
|
||||
def process_something(session, env):
|
||||
r = re.match(re_something, env.get('PATH_INFO'))
|
||||
if not r:
|
||||
return None
|
||||
|
||||
#if env.get('CONTENT_TYPE') != 'application/json':
|
||||
# raise AttributeError('content type')
|
||||
|
||||
#if env.get('REQUEST_METHOD') != 'POST':
|
||||
# raise AttributeError('method')
|
||||
|
||||
#post_data = json.load(env.get('wsgi.input'))
|
||||
|
||||
#return ('text/plain', 'foo'.encode('utf-8'),)
|
||||
|
||||
|
||||
# uwsgi application
|
||||
def application(env, start_response):
|
||||
|
||||
for k in env.keys():
|
||||
logg.debug('env {} {}'.format(k, env[k]))
|
||||
|
||||
headers = []
|
||||
content = b''
|
||||
err = None
|
||||
|
||||
session = SessionBase.create_session()
|
||||
for handler in [
|
||||
process_something,
|
||||
]:
|
||||
try:
|
||||
r = handler(session, env)
|
||||
except AttributeError as e:
|
||||
logg.error('handler fail attribute {}'.format(e))
|
||||
err = '400 Impertinent request'
|
||||
break
|
||||
except JSONDecodeError as e:
|
||||
logg.error('handler fail json {}'.format(e))
|
||||
err = '400 Invalid data format'
|
||||
break
|
||||
except KeyError as e:
|
||||
logg.error('handler fail key {}'.format(e))
|
||||
err = '400 Invalid JSON'
|
||||
break
|
||||
except ValueError as e:
|
||||
logg.error('handler fail value {}'.format(e))
|
||||
err = '400 Invalid data'
|
||||
break
|
||||
except RuntimeError as e:
|
||||
logg.error('task fail value {}'.format(e))
|
||||
err = '500 Task failed, sorry I cannot tell you more'
|
||||
break
|
||||
if r != None:
|
||||
(mime_type, content) = r
|
||||
break
|
||||
session.close()
|
||||
|
||||
if err != None:
|
||||
headers.append(('Content-Type', 'text/plain, charset=UTF-8',))
|
||||
start_response(err, headers)
|
||||
session.close()
|
||||
return [content]
|
||||
|
||||
headers.append(('Content-Length', str(len(content))),)
|
||||
headers.append(('Access-Control-Allow-Origin', '*',));
|
||||
|
||||
if len(content) == 0:
|
||||
headers.append(('Content-Type', 'text/plain, charset=UTF-8',))
|
||||
start_response('404 Looked everywhere, sorry', headers)
|
||||
else:
|
||||
headers.append(('Content-Type', mime_type,))
|
||||
start_response('200 OK', headers)
|
||||
|
||||
return [content]
|
||||
@@ -194,7 +194,6 @@ def main():
|
||||
except UnknownContractError as e:
|
||||
logg.exception('Registry contract connection failed for {}: {}'.format(config.get('CIC_REGISTRY_ADDRESS'), e))
|
||||
sys.exit(1)
|
||||
logg.info('connected contract registry {}'.format(config.get('CIC_REGISTRY_ADDRESS')))
|
||||
|
||||
trusted_addresses_src = config.get('CIC_TRUST_ADDRESS')
|
||||
if trusted_addresses_src == None:
|
||||
|
||||
@@ -15,7 +15,6 @@ import cic_base.config
|
||||
import cic_base.log
|
||||
import cic_base.argparse
|
||||
import cic_base.rpc
|
||||
from cic_base.eth.syncer import chain_interface
|
||||
from cic_eth_registry.error import UnknownContractError
|
||||
from chainlib.chain import ChainSpec
|
||||
from chainlib.eth.constant import ZERO_ADDRESS
|
||||
@@ -27,8 +26,10 @@ from hexathon import (
|
||||
strip_0x,
|
||||
)
|
||||
from chainsyncer.backend.sql import SQLBackend
|
||||
from chainsyncer.driver.head import HeadSyncer
|
||||
from chainsyncer.driver.history import HistorySyncer
|
||||
from chainsyncer.driver import (
|
||||
HeadSyncer,
|
||||
HistorySyncer,
|
||||
)
|
||||
from chainsyncer.db.models.base import SessionBase
|
||||
|
||||
# local imports
|
||||
@@ -79,7 +80,6 @@ chain_spec = ChainSpec.from_chain_str(config.get('CIC_CHAIN_SPEC'))
|
||||
cic_base.rpc.setup(chain_spec, config.get('ETH_PROVIDER'))
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
# connect to celery
|
||||
celery.Celery(broker=config.get('CELERY_BROKER_URL'), backend=config.get('CELERY_RESULT_URL'))
|
||||
@@ -121,11 +121,11 @@ def main():
|
||||
|
||||
for syncer_backend in syncer_backends:
|
||||
try:
|
||||
syncers.append(HistorySyncer(syncer_backend, chain_interface))
|
||||
syncers.append(HistorySyncer(syncer_backend))
|
||||
logg.info('Initializing HISTORY syncer on backend {}'.format(syncer_backend))
|
||||
except AttributeError:
|
||||
logg.info('Initializing HEAD syncer on backend {}'.format(syncer_backend))
|
||||
syncers.append(HeadSyncer(syncer_backend, chain_interface))
|
||||
syncers.append(HeadSyncer(syncer_backend))
|
||||
|
||||
connect_registry(rpc, chain_spec, config.get('CIC_REGISTRY_ADDRESS'))
|
||||
|
||||
|
||||
@@ -12,8 +12,10 @@ import confini
|
||||
import celery
|
||||
|
||||
# local imports
|
||||
from cic_eth.api import Api
|
||||
from cic_eth.api.admin import AdminApi
|
||||
from cic_eth.api import (
|
||||
Api,
|
||||
AdminApi,
|
||||
)
|
||||
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
logg = logging.getLogger()
|
||||
|
||||
@@ -11,7 +11,7 @@ from chainlib.chain import ChainSpec
|
||||
from chainlib.eth.connection import EthHTTPConnection
|
||||
|
||||
# local imports
|
||||
from cic_eth.api.admin import AdminApi
|
||||
from cic_eth.api.api_admin import AdminApi
|
||||
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
logg = logging.getLogger()
|
||||
|
||||
@@ -12,7 +12,7 @@ from chainlib.chain import ChainSpec
|
||||
from xdg.BaseDirectory import xdg_config_home
|
||||
|
||||
# local imports
|
||||
from cic_eth.api.admin import AdminApi
|
||||
from cic_eth.api import AdminApi
|
||||
from cic_eth.db import dsn_from_config
|
||||
from cic_eth.db.models.base import SessionBase
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from chainlib.eth.connection import EthHTTPConnection
|
||||
from hexathon import add_0x
|
||||
|
||||
# local imports
|
||||
from cic_eth.api.admin import AdminApi
|
||||
from cic_eth.api import AdminApi
|
||||
from cic_eth.db.enum import (
|
||||
StatusEnum,
|
||||
status_str,
|
||||
|
||||
@@ -8,9 +8,9 @@ import semver
|
||||
|
||||
version = (
|
||||
0,
|
||||
12,
|
||||
11,
|
||||
0,
|
||||
'alpha.1',
|
||||
'beta.15',
|
||||
)
|
||||
|
||||
version_object = semver.VersionInfo(
|
||||
|
||||
@@ -8,14 +8,12 @@ RUN apt-get update && \
|
||||
#RUN python -m venv venv && . venv/bin/activate
|
||||
|
||||
ARG pip_extra_index_url_flag='--index https://pypi.org/simple --extra-index-url https://pip.grassrootseconomics.net:8433'
|
||||
ARG EXTRA_INDEX_URL="https://pip.grassrootseconomics.net:8433"
|
||||
ARG GITLAB_PYTHON_REGISTRY="https://gitlab.com/api/v4/projects/27624814/packages/pypi/simple"
|
||||
RUN /usr/local/bin/python -m pip install --upgrade pip
|
||||
RUN pip install semver
|
||||
|
||||
# TODO use a packaging style that lets us copy requirments only ie. pip-tools
|
||||
COPY cic-eth/ .
|
||||
RUN pip install --extra-index-url $GITLAB_PYTHON_REGISTRY \
|
||||
--extra-index-url $EXTRA_INDEX_URL .
|
||||
RUN pip install $pip_extra_index_url_flag .
|
||||
|
||||
# --- TEST IMAGE ---
|
||||
FROM python:3.8.6-slim-buster as test
|
||||
@@ -34,12 +32,8 @@ COPY --from=compile /usr/local/lib/python3.8/site-packages/ \
|
||||
# COPY --from=compile /usr/src/cic-eth/ .
|
||||
# RUN . venv/bin/activate
|
||||
|
||||
ARG EXTRA_INDEX_URL="https://pip.grassrootseconomics.net:8433"
|
||||
ARG GITLAB_PYTHON_REGISTRY="https://gitlab.com/api/v4/projects/27624814/packages/pypi/simple"
|
||||
|
||||
COPY cic-eth/test_requirements.txt .
|
||||
RUN pip install --extra-index-url $GITLAB_PYTHON_REGISTRY \
|
||||
--extra-index-url $EXTRA_INDEX_URL -r test_requirements.txt
|
||||
RUN pip install $pip_extra_index_url_flag -r test_requirements.txt
|
||||
|
||||
COPY cic-eth .
|
||||
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
cic-base~=0.1.2b14
|
||||
celery==4.4.7
|
||||
chainlib~=0.0.5a1
|
||||
crypto-dev-signer~=0.4.14b3
|
||||
confini~=0.3.6rc3
|
||||
cic-eth-registry~=0.5.5a7
|
||||
redis==3.5.3
|
||||
alembic==1.4.2
|
||||
websockets==8.1
|
||||
requests~=2.24.0
|
||||
eth_accounts_index~=0.0.11a12
|
||||
erc20-transfer-authorization~=0.3.1a7
|
||||
uWSGI==2.0.19.1
|
||||
semver==2.13.0
|
||||
websocket-client==0.57.0
|
||||
moolb~=0.1.1b2
|
||||
eth-address-index~=0.1.1a11
|
||||
chainlib~=0.0.3rc2
|
||||
hexathon~=0.0.1a7
|
||||
chainsyncer[sql]~=0.0.2a4
|
||||
chainqueue~=0.0.2b1
|
||||
sarafu-faucet==0.0.3a3
|
||||
erc20-faucet==0.2.1a4
|
||||
coincurve==15.0.0
|
||||
potaahto~=0.0.1a2
|
||||
pycryptodome==3.10.1
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import os
|
||||
import argparse
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
|
||||
import alembic
|
||||
from alembic.config import Config as AlembicConfig
|
||||
@@ -25,8 +23,6 @@ argparser = argparse.ArgumentParser()
|
||||
argparser.add_argument('-c', type=str, default=config_dir, 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', 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()
|
||||
@@ -57,10 +53,4 @@ ac = AlembicConfig(os.path.join(migrations_dir, 'alembic.ini'))
|
||||
ac.set_main_option('sqlalchemy.url', dsn)
|
||||
ac.set_main_option('script_location', migrations_dir)
|
||||
|
||||
if args.reset:
|
||||
if not args.f:
|
||||
if not re.match(r'[yY][eE]?[sS]?', input('EEK! this will DELETE the existing db. are you sure??')):
|
||||
logg.error('user chickened out on requested reset, bailing')
|
||||
sys.exit(1)
|
||||
alembic.command.downgrade(ac, 'base')
|
||||
alembic.command.upgrade(ac, 'head')
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
chainsyncer[sql]~=0.0.3a3
|
||||
chainqueue~=0.0.2b5
|
||||
alembic==1.4.2
|
||||
confini~=0.3.6rc4
|
||||
redis==3.5.3
|
||||
hexathon~=0.0.1a7
|
||||
pycryptodome==3.10.1
|
||||
@@ -45,16 +45,16 @@ scripts =
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
# daemons
|
||||
cic-eth-taskerd = cic_eth.runnable.daemons.tasker:main [services]
|
||||
cic-eth-trackerd = cic_eth.runnable.daemons.tracker:main [services]
|
||||
cic-eth-dispatcherd = cic_eth.runnable.daemons.dispatcher:main [services]
|
||||
cic-eth-retrierd = cic_eth.runnable.daemons.retry:main [services]
|
||||
cic-eth-taskerd = cic_eth.runnable.daemons.tasker:main
|
||||
cic-eth-trackerd = cic_eth.runnable.daemons.tracker:main
|
||||
cic-eth-dispatcherd = cic_eth.runnable.daemons.dispatcher:main
|
||||
cic-eth-retrierd = cic_eth.runnable.daemons.retry:main
|
||||
# tools
|
||||
cic-eth-create = cic_eth.runnable.create:main [tools]
|
||||
cic-eth-inspect = cic_eth.runnable.view:main [tools]
|
||||
cic-eth-ctl = cic_eth.runnable.ctrl:main [tools]
|
||||
cic-eth-info = cic_eth.runnable.info:main [tools]
|
||||
cic-eth-create = cic_eth.runnable.create:main
|
||||
cic-eth-inspect = cic_eth.runnable.view:main
|
||||
cic-eth-ctl = cic_eth.runnable.ctrl:main
|
||||
cic-eth-info = cic_eth.runnable.info:main
|
||||
# TODO: Merge this with ctl when subcmds sorted to submodules
|
||||
cic-eth-tag = cic_eth.runnable.tag:main [tools]
|
||||
cic-eth-resend = cic_eth.runnable.resend:main [tools]
|
||||
cic-eth-transfer = cic_eth.runnable.transfer:main [tools]
|
||||
cic-eth-tag = cic_eth.runnable.tag:main
|
||||
cic-eth-resend = cic_eth.runnable.resend:main
|
||||
cic-eth-transfer = cic_eth.runnable.transfer:main
|
||||
|
||||
@@ -11,41 +11,6 @@ while True:
|
||||
requirements.append(l.rstrip())
|
||||
f.close()
|
||||
|
||||
admin_requirements = []
|
||||
f = open('admin_requirements.txt', 'r')
|
||||
while True:
|
||||
l = f.readline()
|
||||
if l == '':
|
||||
break
|
||||
admin_requirements.append(l.rstrip())
|
||||
f.close()
|
||||
|
||||
|
||||
|
||||
tools_requirements = []
|
||||
f = open('tools_requirements.txt', 'r')
|
||||
while True:
|
||||
l = f.readline()
|
||||
if l == '':
|
||||
break
|
||||
tools_requirements.append(l.rstrip())
|
||||
f.close()
|
||||
|
||||
|
||||
services_requirements = []
|
||||
f = open('services_requirements.txt', 'r')
|
||||
while True:
|
||||
l = f.readline()
|
||||
if l == '':
|
||||
break
|
||||
services_requirements.append(l.rstrip())
|
||||
f.close()
|
||||
|
||||
setup(
|
||||
install_requires=requirements,
|
||||
extras_require = {
|
||||
'tools': tools_requirements,
|
||||
'admin_api': admin_requirements,
|
||||
'services': services_requirements,
|
||||
}
|
||||
install_requires=requirements
|
||||
)
|
||||
|
||||
@@ -2,8 +2,5 @@ pytest==6.0.1
|
||||
pytest-celery==0.0.0a1
|
||||
pytest-mock==3.3.1
|
||||
pytest-cov==2.10.1
|
||||
pytest-redis==2.0.0
|
||||
redis==3.5.3
|
||||
eth-tester==0.5.0b3
|
||||
py-evm==0.3.0a20
|
||||
eth-erc20~=0.0.10a2
|
||||
|
||||
@@ -17,11 +17,11 @@ root_dir = os.path.dirname(script_dir)
|
||||
sys.path.insert(0, root_dir)
|
||||
|
||||
# assemble fixtures
|
||||
from cic_eth.pytest.fixtures_config import *
|
||||
from cic_eth.pytest.fixtures_celery import *
|
||||
from cic_eth.pytest.fixtures_database import *
|
||||
from cic_eth.pytest.fixtures_role import *
|
||||
from cic_eth.pytest.fixtures_contract import *
|
||||
from tests.fixtures_config import *
|
||||
from tests.fixtures_database import *
|
||||
from tests.fixtures_celery import *
|
||||
from tests.fixtures_role import *
|
||||
from tests.fixtures_contract import *
|
||||
from chainlib.eth.pytest import *
|
||||
from eth_contract_registry.pytest import *
|
||||
from cic_eth_registry.pytest.fixtures_contracts import *
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# external imports
|
||||
from chainlib.connection import RPCConnection
|
||||
from chainlib.eth.nonce import OverrideNonceOracle
|
||||
from chainqueue.sql.tx import create as queue_create
|
||||
from chainqueue.tx import create as queue_create
|
||||
from chainlib.eth.tx import (
|
||||
TxFormat,
|
||||
unpack,
|
||||
@@ -16,7 +16,7 @@ from chainlib.eth.block import (
|
||||
block_by_number,
|
||||
Block,
|
||||
)
|
||||
from chainqueue.sql.state import (
|
||||
from chainqueue.state import (
|
||||
set_waitforgas,
|
||||
)
|
||||
from hexathon import strip_0x
|
||||
|
||||
@@ -15,7 +15,7 @@ from chainlib.eth.block import (
|
||||
)
|
||||
from erc20_faucet import Faucet
|
||||
from hexathon import strip_0x
|
||||
from chainqueue.sql.query import get_account_tx
|
||||
from chainqueue.query import get_account_tx
|
||||
|
||||
# local imports
|
||||
from cic_eth.runnable.daemons.filters.register import RegistrationFilter
|
||||
|
||||
@@ -17,8 +17,8 @@ from chainlib.eth.block import (
|
||||
)
|
||||
from chainqueue.db.models.otx import Otx
|
||||
from chainqueue.db.enum import StatusBits
|
||||
from chainqueue.sql.tx import create as queue_create
|
||||
from chainqueue.sql.state import (
|
||||
from chainqueue.tx import create as queue_create
|
||||
from chainqueue.state import (
|
||||
set_reserved,
|
||||
set_ready,
|
||||
set_sent,
|
||||
|
||||
@@ -15,7 +15,7 @@ from chainlib.eth.block import (
|
||||
Block,
|
||||
)
|
||||
from hexathon import strip_0x
|
||||
from chainqueue.sql.query import get_account_tx
|
||||
from chainqueue.query import get_account_tx
|
||||
|
||||
# local imports
|
||||
from cic_eth.runnable.daemons.filters.transferauth import TransferAuthFilter
|
||||
|
||||
@@ -17,8 +17,8 @@ from chainlib.eth.block import (
|
||||
)
|
||||
from chainqueue.db.models.otx import Otx
|
||||
from chainqueue.db.enum import StatusBits
|
||||
from chainqueue.sql.tx import create as queue_create
|
||||
from chainqueue.sql.state import (
|
||||
from chainqueue.tx import create as queue_create
|
||||
from chainqueue.state import (
|
||||
set_reserved,
|
||||
set_ready,
|
||||
set_sent,
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
# external imports
|
||||
# third-party imports
|
||||
import pytest
|
||||
import confini
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
root_dir = os.path.dirname(os.path.dirname(script_dir))
|
||||
logg = logging.getLogger(__name__)
|
||||
root_dir = os.path.dirname(script_dir)
|
||||
logg = logging.getLogger(__file__)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@@ -37,8 +37,7 @@ def init_database(
|
||||
database_engine,
|
||||
):
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
rootdir = os.path.dirname(os.path.dirname(script_dir))
|
||||
rootdir = os.path.dirname(os.path.dirname(__file__))
|
||||
dbdir = os.path.join(rootdir, 'cic_eth', 'db')
|
||||
migrationsdir = os.path.join(dbdir, 'migrations', load_config.get('DATABASE_ENGINE'))
|
||||
if not os.path.isdir(migrationsdir):
|
||||
@@ -29,18 +29,18 @@ from chainqueue.db.enum import (
|
||||
StatusBits,
|
||||
status_str,
|
||||
)
|
||||
from chainqueue.sql.state import (
|
||||
from chainqueue.state import (
|
||||
set_fubar,
|
||||
set_ready,
|
||||
set_reserved,
|
||||
)
|
||||
from chainqueue.sql.query import (
|
||||
from chainqueue.query import (
|
||||
get_tx,
|
||||
get_nonce_tx_cache,
|
||||
)
|
||||
|
||||
# local imports
|
||||
from cic_eth.api.admin import AdminApi
|
||||
from cic_eth.api import AdminApi
|
||||
from cic_eth.db.models.role import AccountRole
|
||||
from cic_eth.db.enum import LockEnum
|
||||
from cic_eth.error import InitializationError
|
||||
|
||||
@@ -11,7 +11,7 @@ from chainlib.eth.nonce import (
|
||||
OverrideNonceOracle,
|
||||
RPCNonceOracle,
|
||||
)
|
||||
from chainqueue.sql.tx import create as queue_create
|
||||
from chainqueue.tx import create as queue_create
|
||||
from chainlib.eth.tx import (
|
||||
raw,
|
||||
receipt,
|
||||
@@ -23,19 +23,19 @@ from chainlib.eth.gas import (
|
||||
Gas,
|
||||
OverrideGasOracle,
|
||||
)
|
||||
from chainqueue.sql.state import (
|
||||
from chainqueue.state import (
|
||||
set_reserved,
|
||||
set_sent,
|
||||
set_ready,
|
||||
)
|
||||
from chainqueue.db.models.otx import Otx
|
||||
from chainqueue.db.enum import StatusBits
|
||||
from chainqueue.sql.query import get_nonce_tx_cache
|
||||
from chainqueue.query import get_nonce_tx_cache
|
||||
from eth_erc20 import ERC20
|
||||
from cic_eth_registry import CICRegistry
|
||||
|
||||
# local imports
|
||||
from cic_eth.api.admin import AdminApi
|
||||
from cic_eth.api.api_admin import AdminApi
|
||||
from cic_eth.eth.gas import cache_gas_data
|
||||
from cic_eth.eth.erc20 import cache_transfer_data
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from eth_accounts_index import AccountsIndex
|
||||
from chainlib.eth.tx import (
|
||||
transaction,
|
||||
)
|
||||
from chainqueue.sql.state import (
|
||||
from chainqueue.state import (
|
||||
set_reserved,
|
||||
)
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
from cic_eth.pytest.fixtures_celery import *
|
||||
from tests.fixtures_celery import *
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
import celery
|
||||
from chainlib.connection import RPCConnection
|
||||
from chainlib.eth.nonce import OverrideNonceOracle
|
||||
from chainqueue.sql.tx import (
|
||||
from chainqueue.tx import (
|
||||
create as queue_create,
|
||||
)
|
||||
from chainlib.eth.gas import (
|
||||
@@ -13,7 +13,7 @@ from chainlib.eth.gas import (
|
||||
OverrideGasOracle,
|
||||
)
|
||||
from chainlib.eth.tx import TxFormat
|
||||
from chainqueue.sql.query import get_nonce_tx_cache
|
||||
from chainqueue.query import get_nonce_tx_cache
|
||||
from chainqueue.db.models.otx import Otx
|
||||
from chainqueue.db.enum import StatusBits
|
||||
from hexathon import add_0x
|
||||
|
||||
@@ -13,7 +13,6 @@ from chainlib.eth.tx import (
|
||||
|
||||
# local imports
|
||||
from cic_eth.queue.tx import register_tx
|
||||
from cic_eth.error import YouAreBrokeError
|
||||
|
||||
logg = logging.getLogger()
|
||||
|
||||
@@ -168,101 +167,3 @@ def test_erc20_approve_task(
|
||||
r = t.get_leaf()
|
||||
|
||||
logg.debug('result {}'.format(r))
|
||||
|
||||
|
||||
def test_erc20_transfer_from_task(
|
||||
default_chain_spec,
|
||||
foo_token,
|
||||
agent_roles,
|
||||
custodial_roles,
|
||||
eth_signer,
|
||||
eth_rpc,
|
||||
init_database,
|
||||
celery_session_worker,
|
||||
token_roles,
|
||||
):
|
||||
|
||||
token_object = {
|
||||
'address': foo_token,
|
||||
}
|
||||
transfer_value = 100 * (10 ** 6)
|
||||
|
||||
nonce_oracle = RPCNonceOracle(token_roles['FOO_TOKEN_OWNER'], conn=eth_rpc)
|
||||
c = ERC20(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle)
|
||||
(tx_hash, o) = c.approve(foo_token, token_roles['FOO_TOKEN_OWNER'], agent_roles['ALICE'], transfer_value)
|
||||
r = eth_rpc.do(o)
|
||||
o = receipt(tx_hash)
|
||||
r = eth_rpc.do(o)
|
||||
assert r['status'] == 1
|
||||
|
||||
s_nonce = celery.signature(
|
||||
'cic_eth.eth.nonce.reserve_nonce',
|
||||
[
|
||||
[token_object],
|
||||
default_chain_spec.asdict(),
|
||||
custodial_roles['FOO_TOKEN_GIFTER'],
|
||||
],
|
||||
queue=None,
|
||||
)
|
||||
s_transfer = celery.signature(
|
||||
'cic_eth.eth.erc20.transfer_from',
|
||||
[
|
||||
custodial_roles['FOO_TOKEN_GIFTER'],
|
||||
agent_roles['BOB'],
|
||||
transfer_value,
|
||||
default_chain_spec.asdict(),
|
||||
agent_roles['ALICE'],
|
||||
],
|
||||
queue=None,
|
||||
)
|
||||
s_nonce.link(s_transfer)
|
||||
t = s_nonce.apply_async()
|
||||
r = t.get_leaf()
|
||||
|
||||
logg.debug('result {}'.format(r))
|
||||
|
||||
|
||||
def test_erc20_allowance_check_task(
|
||||
default_chain_spec,
|
||||
foo_token,
|
||||
agent_roles,
|
||||
custodial_roles,
|
||||
eth_signer,
|
||||
eth_rpc,
|
||||
init_database,
|
||||
celery_session_worker,
|
||||
token_roles,
|
||||
):
|
||||
|
||||
token_object = {
|
||||
'address': foo_token,
|
||||
'symbol': 'FOO',
|
||||
}
|
||||
transfer_value = 100 * (10 ** 6)
|
||||
|
||||
s_check = celery.signature(
|
||||
'cic_eth.eth.erc20.check_allowance',
|
||||
[
|
||||
[token_object],
|
||||
custodial_roles['FOO_TOKEN_GIFTER'],
|
||||
transfer_value,
|
||||
default_chain_spec.asdict(),
|
||||
agent_roles['ALICE']
|
||||
],
|
||||
queue=None,
|
||||
)
|
||||
t = s_check.apply_async()
|
||||
with pytest.raises(YouAreBrokeError):
|
||||
t.get()
|
||||
|
||||
nonce_oracle = RPCNonceOracle(token_roles['FOO_TOKEN_OWNER'], conn=eth_rpc)
|
||||
c = ERC20(default_chain_spec, signer=eth_signer, nonce_oracle=nonce_oracle)
|
||||
(tx_hash, o) = c.approve(foo_token, token_roles['FOO_TOKEN_OWNER'], agent_roles['ALICE'], transfer_value)
|
||||
r = eth_rpc.do(o)
|
||||
o = receipt(tx_hash)
|
||||
r = eth_rpc.do(o)
|
||||
assert r['status'] == 1
|
||||
|
||||
t = s_check.apply_async()
|
||||
t.get()
|
||||
assert t.successful()
|
||||
|
||||
@@ -21,10 +21,10 @@ from chainlib.eth.constant import (
|
||||
MINIMUM_FEE_UNITS,
|
||||
MINIMUM_FEE_PRICE,
|
||||
)
|
||||
from chainqueue.sql.tx import create as queue_create
|
||||
from chainqueue.sql.query import get_tx
|
||||
from chainqueue.tx import create as queue_create
|
||||
from chainqueue.query import get_tx
|
||||
from chainqueue.db.enum import StatusBits
|
||||
from chainqueue.sql.state import (
|
||||
from chainqueue.state import (
|
||||
set_ready,
|
||||
set_reserved,
|
||||
set_sent,
|
||||
|
||||
@@ -18,8 +18,8 @@ from chainlib.eth.tx import (
|
||||
)
|
||||
from hexathon import strip_0x
|
||||
from chainqueue.db.models.otx import Otx
|
||||
from chainqueue.sql.tx import create as queue_create
|
||||
from chainqueue.sql.state import (
|
||||
from chainqueue.tx import create as queue_create
|
||||
from chainqueue.state import (
|
||||
set_reserved,
|
||||
set_ready,
|
||||
set_sent,
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
# external imports
|
||||
import pytest
|
||||
import celery
|
||||
from chainqueue.sql.tx import create as queue_create
|
||||
from chainqueue.tx import create as queue_create
|
||||
from chainlib.eth.nonce import (
|
||||
RPCNonceOracle,
|
||||
OverrideNonceOracle,
|
||||
@@ -23,7 +23,7 @@ from hexathon import (
|
||||
add_0x,
|
||||
strip_0x,
|
||||
)
|
||||
from chainqueue.sql.state import (
|
||||
from chainqueue.state import (
|
||||
set_reserved,
|
||||
set_ready,
|
||||
)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
crypto-dev-signer~=0.4.14b6
|
||||
chainqueue~=0.0.2b5
|
||||
confini~=0.3.6rc4
|
||||
cic-eth-registry~=0.5.6a1
|
||||
redis==3.5.3
|
||||
hexathon~=0.0.1a7
|
||||
pycryptodome==3.10.1
|
||||
pyxdg==0.27
|
||||
2
apps/cic-meta/.gitignore
vendored
2
apps/cic-meta/.gitignore
vendored
@@ -3,5 +3,3 @@ dist
|
||||
dist-web
|
||||
dist-server
|
||||
scratch
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
@@ -3,38 +3,17 @@
|
||||
variables:
|
||||
APP_NAME: cic-meta
|
||||
DOCKERFILE_PATH: $APP_NAME/docker/Dockerfile
|
||||
IMAGE_TAG: $CI_REGISTRY_IMAGE/$APP_NAME:unittest-$CI_COMMIT_SHORT_SHA
|
||||
|
||||
.cic_meta_changes_target:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
# - changes:
|
||||
# - $CONTEXT/$APP_NAME/*
|
||||
- when: always
|
||||
- changes:
|
||||
- $CONTEXT/$APP_NAME/*
|
||||
|
||||
cic-meta-build-mr:
|
||||
stage: build
|
||||
extends:
|
||||
- .cic_meta_variables
|
||||
- .cic_meta_changes_target
|
||||
script:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > "/kaniko/.docker/config.json"
|
||||
# - /kaniko/executor --context $CONTEXT --dockerfile $DOCKERFILE_PATH $KANIKO_CACHE_ARGS --destination $IMAGE_TAG
|
||||
- /kaniko/executor --context $CONTEXT --dockerfile $DOCKERFILE_PATH $KANIKO_CACHE_ARGS --destination $IMAGE_TAG
|
||||
|
||||
test-mr-cic-meta:
|
||||
build-mr-cic-meta:
|
||||
extends:
|
||||
- .cic_meta_variables
|
||||
- .cic_meta_changes_target
|
||||
stage: test
|
||||
image: $IMAGE_TAG
|
||||
script:
|
||||
- cd /tmp/src/cic-meta
|
||||
- npm install --dev
|
||||
- npm run test
|
||||
- npm run test:coverage
|
||||
needs: ["cic-meta-build-mr"]
|
||||
- .py_build_merge_request
|
||||
- .cic_meta_variables
|
||||
|
||||
build-push-cic-meta:
|
||||
extends:
|
||||
|
||||
@@ -4,28 +4,29 @@ WORKDIR /tmp/src/cic-meta
|
||||
|
||||
RUN apk add --no-cache postgresql bash
|
||||
|
||||
# required to build the cic-client-meta module
|
||||
COPY cic-meta/package.json \
|
||||
./
|
||||
|
||||
COPY cic-meta/src/ src/
|
||||
COPY cic-meta/tests/ tests/
|
||||
COPY cic-meta/scripts/ scripts/
|
||||
|
||||
# copy the dependencies
|
||||
COPY cic-meta/package.json .
|
||||
COPY cic-meta/tsconfig.json .
|
||||
COPY cic-meta/webpack.config.js .
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY cic-meta/tests/ tests/
|
||||
# see exports_dir gpg.ini
|
||||
COPY cic-meta/tests/*.asc /root/pgp/
|
||||
RUN alias tsc=node_modules/typescript/bin/tsc
|
||||
|
||||
|
||||
# copy runtime configs
|
||||
COPY cic-meta/.config/ /usr/local/etc/cic-meta/
|
||||
# COPY cic-meta/scripts/server/initdb/server.postgres.sql /usr/local/share/cic-meta/sql/server.sql
|
||||
|
||||
# db migrations
|
||||
COPY cic-meta/docker/db.sh ./db.sh
|
||||
RUN chmod 755 ./db.sh
|
||||
|
||||
RUN alias tsc=node_modules/typescript/bin/tsc
|
||||
#RUN alias ts-node=/tmp/src/cic-meta/node_modules/ts-node/dist/bin.js
|
||||
#ENTRYPOINT [ "./node_modules/ts-node/dist/bin.js", "./scripts/server/server.ts" ]
|
||||
|
||||
COPY cic-meta/docker/start_server.sh ./start_server.sh
|
||||
RUN chmod 755 ./start_server.sh
|
||||
ENTRYPOINT ["sh", "./start_server.sh"]
|
||||
|
||||
2362
apps/cic-meta/package-lock.json
generated
2362
apps/cic-meta/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,6 @@
|
||||
"preferGlobal": true,
|
||||
"scripts": {
|
||||
"test": "mocha -r node_modules/node-localstorage/register -r ts-node/register tests/*.ts",
|
||||
"test:coverage": "nyc mocha tests/*.ts --timeout 3000 --check-coverage=true",
|
||||
"build": "node_modules/typescript/bin/tsc -d --outDir dist src/index.ts",
|
||||
"build-server": "tsc -d --outDir dist-server scripts/server/*.ts",
|
||||
"pack": "node_modules/typescript/bin/tsc -d --outDir dist && webpack",
|
||||
@@ -35,9 +34,7 @@
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^8.0.3",
|
||||
"mocha": "^8.2.0",
|
||||
"nock": "^13.1.0",
|
||||
"node-localstorage": "^2.1.6",
|
||||
"nyc": "^15.1.0",
|
||||
"ts-node": "^9.0.0",
|
||||
"typescript": "^4.0.5",
|
||||
"webpack": "^5.4.0",
|
||||
@@ -53,26 +50,5 @@
|
||||
"license": "GPL-3.0-or-later",
|
||||
"engines": {
|
||||
"node": ">=14.16.1"
|
||||
},
|
||||
"nyc": {
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"extension": [
|
||||
".ts"
|
||||
],
|
||||
"require": [
|
||||
"ts-node/register"
|
||||
],
|
||||
"reporter": [
|
||||
"text",
|
||||
"html"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"instrument": true,
|
||||
"branches": ">80",
|
||||
"lines": ">80",
|
||||
"functions": ">80",
|
||||
"statements": ">80"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ function handleNoMergeGet(db, digest, keystore) {
|
||||
doh(e);
|
||||
});
|
||||
}).catch((e) => {
|
||||
console.error('message', e);
|
||||
console.error('mesage', e);
|
||||
doh(e);
|
||||
});
|
||||
})
|
||||
@@ -46,7 +46,7 @@ function handleServerMergePost(data, db, digest, keystore, signer) {
|
||||
let e = undefined;
|
||||
let s = undefined;
|
||||
if (v === undefined) {
|
||||
s = new Syncable(digest, o);
|
||||
s = new Syncable(digest, data);
|
||||
s.onwrap = (e) => {
|
||||
whohoo(e.toJSON());
|
||||
};
|
||||
|
||||
@@ -204,7 +204,7 @@ async function processRequest(req, res) {
|
||||
}
|
||||
|
||||
if (content === undefined) {
|
||||
console.error('empty content', data);
|
||||
console.error('empty onctent', data);
|
||||
res.writeHead(400, {"Content-Type": "text/plain"});
|
||||
res.end();
|
||||
return;
|
||||
|
||||
@@ -9,7 +9,7 @@ class Custom extends Syncable implements Addressable {
|
||||
super('', v);
|
||||
Custom.toKey(name).then((cid) => {
|
||||
this.id = cid;
|
||||
this.name = name;
|
||||
this.value = v;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -100,15 +100,13 @@ class Meta {
|
||||
identifier = await User.toKey(name);
|
||||
} else if (type === 'phone') {
|
||||
identifier = await Phone.toKey(name);
|
||||
} else if (type === 'custom') {
|
||||
identifier = await Custom.toKey(name);
|
||||
} else {
|
||||
identifier = await Custom.toKey(name, type);
|
||||
identifier = await Custom.toKey(name);
|
||||
}
|
||||
return identifier;
|
||||
}
|
||||
|
||||
wrap(syncable: Syncable): Promise<Envelope> {
|
||||
private wrap(syncable: Syncable): Promise<Envelope> {
|
||||
return new Promise<Envelope>(async (resolve, reject) => {
|
||||
syncable.setSigner(this.signer);
|
||||
syncable.onwrap = async (env) => {
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import * as assert from 'assert';
|
||||
import {Custom} from "../src";
|
||||
|
||||
const testName = 'areas';
|
||||
const testObject = {
|
||||
area: ['Nairobi', 'Mombasa', 'Kilifi']
|
||||
}
|
||||
const testNameKey = '8f3da0c90ba2b89ff217da96f6088cbaf987a1b58bc33c3a5e526e53cec7cfed';
|
||||
const testIdentifier = ':cic.area'
|
||||
const testIdentifierKey = 'da6194e6f33726546e82c328df4c120b844d6427859156518bd600765bf8b2b7';
|
||||
|
||||
describe('custom', () => {
|
||||
|
||||
context('with predefined data', () => {
|
||||
it('should create a custom object', () => {
|
||||
const custom = new Custom(testName, testObject);
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(custom.name, testName);
|
||||
assert.deepStrictEqual(custom.m.data, testObject);
|
||||
assert.strictEqual(custom.key(), testNameKey)
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
context('without predefined data', () => {
|
||||
it('should create a custom object', () => {
|
||||
const custom = new Custom(testName);
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(custom.name, testName);
|
||||
assert.deepStrictEqual(custom.m.data, {});
|
||||
assert.strictEqual(custom.key(), testNameKey)
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#toKey()', () => {
|
||||
context('without a custom identifier', () => {
|
||||
it('should generate a key from the custom name', async () => {
|
||||
assert.strictEqual(await Custom.toKey(testName), testNameKey);
|
||||
});
|
||||
});
|
||||
|
||||
context('with a custom identifier', () => {
|
||||
it('should generate a key from the custom name with a custom identifier', async () => {
|
||||
assert.strictEqual(await Custom.toKey(testName, testIdentifier), testIdentifierKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,176 +0,0 @@
|
||||
import * as assert from 'assert';
|
||||
import * as fs from 'fs';
|
||||
const nock = require('nock');
|
||||
import {Meta} from "../src";
|
||||
import {getResponse, metaData, networkErrorResponse, notFoundResponse, putResponse} from "./response";
|
||||
import {Syncable} from "@cicnet/crdt-meta";
|
||||
|
||||
const metaUrl = 'https://meta.dev.grassrootseconomics.net';
|
||||
const testAddress = '0xc1912fee45d61c87cc5ea59dae31190fffff232d';
|
||||
const testAddressKey = 'a51472cb4df63b199a4de01335b1b4d1bbee27ff4f03340aa1d592f26c6acfe2';
|
||||
const testPhone = '+254123456789';
|
||||
const testPhoneKey = 'be3cc8212b7eb57c6217ddd42230bd8ccd2f01382bf8c1c77d3a683fa5a9bb16';
|
||||
const testName = 'areas'
|
||||
const testNameKey = '8f3da0c90ba2b89ff217da96f6088cbaf987a1b58bc33c3a5e526e53cec7cfed';
|
||||
const testIdentifier = ':cic.area'
|
||||
const testIdentifierKey = 'da6194e6f33726546e82c328df4c120b844d6427859156518bd600765bf8b2b7';
|
||||
|
||||
function readFile(filename) {
|
||||
if(!fs.existsSync(filename)) {
|
||||
console.error(`File ${filename} not found`);
|
||||
return;
|
||||
}
|
||||
return fs.readFileSync(filename, {encoding: 'utf8', flag: 'r'});
|
||||
}
|
||||
|
||||
const privateKey = readFile('./privatekeys.asc');
|
||||
|
||||
describe('meta', () => {
|
||||
beforeEach(() => {
|
||||
nock(metaUrl)
|
||||
.get(`/${testAddressKey}`)
|
||||
.reply(200, getResponse);
|
||||
|
||||
nock(metaUrl)
|
||||
.get(`/${testPhoneKey}`)
|
||||
.reply(200, getResponse);
|
||||
|
||||
nock(metaUrl)
|
||||
.get(`/${testAddress}`)
|
||||
.reply(404);
|
||||
|
||||
nock(metaUrl)
|
||||
.get(`/${testIdentifier}`)
|
||||
.replyWithError(networkErrorResponse);
|
||||
|
||||
nock(metaUrl)
|
||||
.put(`/${testAddressKey}`)
|
||||
.reply(200, putResponse);
|
||||
|
||||
nock(metaUrl)
|
||||
.put(`/${testAddress}`)
|
||||
.reply(404);
|
||||
|
||||
nock(metaUrl)
|
||||
.post('/post')
|
||||
.reply(500);
|
||||
});
|
||||
|
||||
describe('#get()', () => {
|
||||
it('should fetch data from the meta service', async () => {
|
||||
const account = await Meta.get(testAddressKey, metaUrl);
|
||||
assert.strictEqual(account.toJSON(account), getResponse.payload);
|
||||
});
|
||||
|
||||
context('if item is not found', () => {
|
||||
it('should respond with an error', async () => {
|
||||
const account = await Meta.get(testAddress, metaUrl);
|
||||
assert.strictEqual(account, `404: Not Found`);
|
||||
});
|
||||
});
|
||||
|
||||
context('in case of network error', () => {
|
||||
it('should respond with an error', async () => {
|
||||
const account = await Meta.get(testIdentifier, metaUrl);
|
||||
assert.strictEqual(account, `Request to ${metaUrl}/${testIdentifier} failed. Connection error.`);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
describe('#set()', () => {
|
||||
context('object data', () => {
|
||||
it('should set data to the meta server', () => {
|
||||
const meta = new Meta(metaUrl, privateKey);
|
||||
meta.onload = async (status) => {
|
||||
const response = await meta.set(testAddressKey, metaData);
|
||||
assert.strictEqual(response, `${putResponse.status}: ${putResponse.statusText}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
context('string data', () => {
|
||||
it('should set data to the meta server', () => {
|
||||
const meta = new Meta(metaUrl, privateKey);
|
||||
meta.onload = async (status) => {
|
||||
const response = await meta.set(testPhoneKey, testAddress);
|
||||
assert.strictEqual(response, `${putResponse.status}: ${putResponse.statusText}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
context('in case of network error', () => {
|
||||
it('should respond with an error', () => {
|
||||
const meta = new Meta(metaUrl, privateKey);
|
||||
meta.onload = async (status) => {
|
||||
const response = await meta.set(testIdentifier, metaData);
|
||||
assert.strictEqual(response, `Request to ${metaUrl}/${testIdentifier} failed. Connection error.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateMeta()', () => {
|
||||
it('should update data in the meta server', async () => {
|
||||
const syncable = new Syncable(testAddressKey, metaData);
|
||||
const meta = new Meta(metaUrl, privateKey);
|
||||
meta.onload = async (status) => {
|
||||
const response = await meta.updateMeta(syncable, testAddressKey);
|
||||
assert.strictEqual(response, putResponse);
|
||||
}
|
||||
});
|
||||
|
||||
context('if item is not found', () => {
|
||||
it('should respond with an error', () => {
|
||||
const syncable = new Syncable(testAddress, metaData);
|
||||
const meta = new Meta(metaUrl, privateKey);
|
||||
meta.onload = async (status) => {
|
||||
const response = await meta.updateMeta(syncable, testAddress);
|
||||
assert.strictEqual(response, notFoundResponse);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#wrap()', () => {
|
||||
it('should sign a syncable object', function () {
|
||||
const syncable = new Syncable(testAddressKey, metaData);
|
||||
const meta = new Meta(metaUrl, privateKey);
|
||||
meta.onload = async (status) => {
|
||||
const response = await meta.wrap(syncable);
|
||||
assert.strictEqual(response.toJSON(), getResponse);
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
describe('#getIdentifier()', () => {
|
||||
context('without type', () => {
|
||||
it('should return an identifier', async () => {
|
||||
assert.strictEqual(await Meta.getIdentifier(testName), testNameKey);
|
||||
});
|
||||
});
|
||||
|
||||
context('with user type', () => {
|
||||
it('should return an identifier', async () => {
|
||||
assert.strictEqual(await Meta.getIdentifier(testAddress, 'user'), testAddressKey);
|
||||
});
|
||||
});
|
||||
|
||||
context('with phone type', () => {
|
||||
it('should return an identifier', async () => {
|
||||
assert.strictEqual(await Meta.getIdentifier(testPhone, 'phone'), testPhoneKey);
|
||||
});
|
||||
});
|
||||
|
||||
context('with custom type', () => {
|
||||
it('should return an identifier', async () => {
|
||||
assert.strictEqual(await Meta.getIdentifier(testName, 'custom'), testNameKey);
|
||||
});
|
||||
});
|
||||
|
||||
context('with unrecognised type', () => {
|
||||
it('should return an identifier', async () => {
|
||||
assert.strictEqual(await Meta.getIdentifier(testName, testIdentifier), testIdentifierKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import * as assert from 'assert';
|
||||
import {Phone} from "../src";
|
||||
|
||||
const testAddress = '0xc1912fee45d61c87cc5ea59dae31190fffff232d';
|
||||
const testPhone = '+254123456789';
|
||||
const testPhoneKey = 'be3cc8212b7eb57c6217ddd42230bd8ccd2f01382bf8c1c77d3a683fa5a9bb16';
|
||||
|
||||
describe('phone', () => {
|
||||
|
||||
it('should create a phone object', () => {
|
||||
const phone = new Phone(testAddress, testPhone);
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(phone.address, testAddress);
|
||||
assert.strictEqual(phone.m.data.msisdn, testPhone);
|
||||
assert.strictEqual(phone.key(), testPhoneKey)
|
||||
}, 0);
|
||||
});
|
||||
|
||||
describe('#toKey()', () => {
|
||||
it('should generate a key from the phone number', async () => {
|
||||
assert.strictEqual(await Phone.toKey(testPhone), testPhoneKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -1,54 +0,0 @@
|
||||
import * as assert from 'assert';
|
||||
|
||||
import { User } from "../src";
|
||||
|
||||
const testAddress = '0xc1912fee45d61c87cc5ea59dae31190fffff232d';
|
||||
const testAddressKey = 'a51472cb4df63b199a4de01335b1b4d1bbee27ff4f03340aa1d592f26c6acfe2';
|
||||
const testUser = {
|
||||
user: {
|
||||
firstName: 'Test',
|
||||
lastName: 'User'
|
||||
}
|
||||
}
|
||||
|
||||
describe('user', () => {
|
||||
|
||||
context('without predefined data', () => {
|
||||
it('should create a user object', () => {
|
||||
const user = new User(testAddress);
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(user.address, testAddress);
|
||||
assert.strictEqual(user.key(), testAddressKey);
|
||||
assert.strictEqual(user.m.data.user.firstName, '');
|
||||
assert.strictEqual(user.m.data.user.lastName, '');
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
context('with predefined data', () => {
|
||||
it('should create a user object', () => {
|
||||
const user = new User(testAddress, testUser);
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(user.address, testAddress);
|
||||
assert.strictEqual(user.key(), testAddressKey);
|
||||
assert.strictEqual(user.m.data.user.firstName, testUser.user.firstName);
|
||||
assert.strictEqual(user.m.data.user.lastName, testUser.user.lastName);
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setName()', () => {
|
||||
it('should set user\'s names to metadata', () => {
|
||||
const user = new User(testAddress);
|
||||
user.setName(testUser.user.firstName, testUser.user.lastName);
|
||||
assert.strictEqual(user.m.data.user.firstName, testUser.user.firstName);
|
||||
assert.strictEqual(user.m.data.user.lastName, testUser.user.lastName);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#toKey()', () => {
|
||||
it('should generate a key from the user\'s address', async () => {
|
||||
assert.strictEqual(await User.toKey(testAddress), testAddressKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "./dist.browser",
|
||||
"target": "es2015",
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["es2016", "dom", "es5"],
|
||||
|
||||
@@ -26,7 +26,7 @@ def get_sms_queue_tasks(app, task_prefix='cic_notify.tasks.sms.'):
|
||||
for q in qs[host]:
|
||||
if re.match(re_q, q['name']):
|
||||
host_queues.append((host, q['name'],))
|
||||
|
||||
|
||||
task_prefix_len = len(task_prefix)
|
||||
queue_tasks = []
|
||||
for (host, queue) in host_queues:
|
||||
@@ -35,18 +35,17 @@ def get_sms_queue_tasks(app, task_prefix='cic_notify.tasks.sms.'):
|
||||
for task in tasks:
|
||||
if len(task) >= task_prefix_len and task[:task_prefix_len] == task_prefix:
|
||||
queue_tasks.append((queue, task,))
|
||||
|
||||
|
||||
return queue_tasks
|
||||
|
||||
|
||||
class Api:
|
||||
# TODO: Implement callback strategy
|
||||
def __init__(self, queue=None):
|
||||
def __init__(self, queue='cic-notify'):
|
||||
"""
|
||||
:param queue: The queue on which to execute notification tasks
|
||||
:type queue: str
|
||||
"""
|
||||
self.queue = queue
|
||||
self.sms_tasks = get_sms_queue_tasks(app)
|
||||
logg.debug('sms tasks {}'.format(self.sms_tasks))
|
||||
|
||||
@@ -62,19 +61,13 @@ class Api:
|
||||
"""
|
||||
signatures = []
|
||||
for q in self.sms_tasks:
|
||||
|
||||
if not self.queue:
|
||||
queue = q[0]
|
||||
else:
|
||||
queue = self.queue
|
||||
|
||||
signature = celery.signature(
|
||||
q[1],
|
||||
[
|
||||
message,
|
||||
recipient,
|
||||
],
|
||||
queue=queue,
|
||||
queue=q[0],
|
||||
)
|
||||
signatures.append(signature)
|
||||
|
||||
|
||||
@@ -87,18 +87,10 @@ for key in config.store.keys():
|
||||
module = importlib.import_module(config.store[key])
|
||||
if key == 'TASKS_AFRICASTALKING':
|
||||
africastalking_notifier = module.AfricasTalkingNotifier
|
||||
|
||||
api_sender_id = config.get('AFRICASTALKING_API_SENDER_ID')
|
||||
logg.debug(f'SENDER ID VALUE IS: {api_sender_id}')
|
||||
|
||||
if not api_sender_id:
|
||||
api_sender_id = None
|
||||
logg.debug(f'SENDER ID RESOLVED TO NONE: {api_sender_id}')
|
||||
|
||||
africastalking_notifier.initialize(
|
||||
config.get('AFRICASTALKING_API_USERNAME'),
|
||||
config.get('AFRICASTALKING_API_KEY'),
|
||||
api_sender_id
|
||||
config.get('AFRICASTALKING_API_SENDER_ID')
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class AfricasTalkingNotifier:
|
||||
response = self.api_client.send(message=message, recipients=[recipient])
|
||||
logg.debug(f'africastalking response no-sender-id {response}')
|
||||
|
||||
recipients = response.get('SMSMessageData').get('Recipients')
|
||||
recipients = response.get('Recipients')
|
||||
|
||||
if len(recipients) != 1:
|
||||
status = response.get('SMSMessageData').get('Message')
|
||||
|
||||
@@ -9,7 +9,7 @@ import semver
|
||||
|
||||
logg = logging.getLogger()
|
||||
|
||||
version = (0, 4, 0, 'alpha.7')
|
||||
version = (0, 4, 0, 'alpha.5')
|
||||
|
||||
version_object = semver.VersionInfo(
|
||||
major=version[0],
|
||||
|
||||
@@ -1 +1 @@
|
||||
cic_base[full_graph]==0.1.3a3+build.984b5cff
|
||||
cic_base[full_graph]~=0.1.2a61
|
||||
|
||||
@@ -28,7 +28,6 @@ packages =
|
||||
cic_notify
|
||||
cic_notify.db
|
||||
cic_notify.db.models
|
||||
cic_notify.ext
|
||||
cic_notify.tasks.sms
|
||||
cic_notify.runnable
|
||||
scripts =
|
||||
|
||||
@@ -2,3 +2,4 @@ pytest~=6.0.1
|
||||
pytest-celery~=0.0.0a1
|
||||
pytest-mock~=3.3.1
|
||||
pysqlite3~=0.4.3
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ LOCALE_PATH=/usr/src/cic-ussd/var/lib/locale/
|
||||
MAX_BODY_LENGTH=1024
|
||||
PASSWORD_PEPPER=QYbzKff6NhiQzY3ygl2BkiKOpER8RE/Upqs/5aZWW+I=
|
||||
SERVICE_CODE=*483*46#,*483*061#,*384*96#
|
||||
SUPPORT_PHONE_NUMBER=0757628885
|
||||
|
||||
[phone_number]
|
||||
REGION=KE
|
||||
|
||||
@@ -5,7 +5,6 @@ LOCALE_PATH=var/lib/locale/
|
||||
MAX_BODY_LENGTH=1024
|
||||
PASSWORD_PEPPER=QYbzKff6NhiQzY3ygl2BkiKOpER8RE/Upqs/5aZWW+I=
|
||||
SERVICE_CODE=*483*46#
|
||||
SUPPORT_PHONE_NUMBER=0757628885
|
||||
|
||||
[ussd]
|
||||
MENU_FILE=/usr/local/lib/python3.8/site-packages/cic_ussd/db/ussd_menu.json
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[test]
|
||||
gift_value = 50.00
|
||||
server_url = http://localhost:63315/
|
||||
token_symbol = GFT
|
||||
@@ -1,9 +0,0 @@
|
||||
# standard import
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class AccountStatus(IntEnum):
|
||||
PENDING = 1
|
||||
ACTIVE = 2
|
||||
LOCKED = 3
|
||||
RESET = 4
|
||||
@@ -1,13 +1,19 @@
|
||||
# standard imports
|
||||
|
||||
# local imports
|
||||
from cic_ussd.db.enum import AccountStatus
|
||||
from cic_ussd.db.models.base import SessionBase
|
||||
from cic_ussd.encoder import check_password_hash, create_password_hash
|
||||
from enum import IntEnum
|
||||
|
||||
# third party imports
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
# local imports
|
||||
from cic_ussd.db.models.base import SessionBase
|
||||
from cic_ussd.encoder import check_password_hash, create_password_hash
|
||||
|
||||
|
||||
class AccountStatus(IntEnum):
|
||||
PENDING = 1
|
||||
ACTIVE = 2
|
||||
LOCKED = 3
|
||||
RESET = 4
|
||||
|
||||
|
||||
class Account(SessionBase):
|
||||
@@ -24,21 +30,6 @@ class Account(SessionBase):
|
||||
account_status = Column(Integer)
|
||||
preferred_language = Column(String)
|
||||
|
||||
@staticmethod
|
||||
def get_by_phone_number(phone_number: str, session: Session):
|
||||
"""Retrieves an account from a phone number.
|
||||
:param phone_number: The E164 format of a phone number.
|
||||
:type phone_number:str
|
||||
:param session:
|
||||
:type session:
|
||||
:return: An account object.
|
||||
:rtype: Account
|
||||
"""
|
||||
session = SessionBase.bind_session(session=session)
|
||||
account = session.query(Account).filter_by(phone_number=phone_number).first()
|
||||
SessionBase.release_session(session=session)
|
||||
return account
|
||||
|
||||
def __init__(self, blockchain_address, phone_number):
|
||||
self.blockchain_address = blockchain_address
|
||||
self.phone_number = phone_number
|
||||
|
||||
@@ -238,55 +238,13 @@
|
||||
"description": "Menu to display a user's entire profile",
|
||||
"display_key": "ussd.kenya.display_user_metadata",
|
||||
"name": "display_user_metadata",
|
||||
"parent": "metadata_management"
|
||||
"parent": "account_management"
|
||||
},
|
||||
"41": {
|
||||
"description": "The recipient is not in the system",
|
||||
"display_key": "ussd.kenya.exit_invalid_recipient",
|
||||
"name": "exit_invalid_recipient",
|
||||
"parent": null
|
||||
},
|
||||
"42": {
|
||||
"description": "Pin entry menu for changing name data.",
|
||||
"display_key": "ussd.kenya.name_edit_pin_authorization",
|
||||
"name": "name_edit_pin_authorization",
|
||||
"parent": "metadata_management"
|
||||
},
|
||||
"43": {
|
||||
"description": "Pin entry menu for changing gender data.",
|
||||
"display_key": "ussd.kenya.gender_edit_pin_authorization",
|
||||
"name": "gender_edit_pin_authorization",
|
||||
"parent": "metadata_management"
|
||||
},
|
||||
"44": {
|
||||
"description": "Pin entry menu for changing location data.",
|
||||
"display_key": "ussd.kenya.location_edit_pin_authorization",
|
||||
"name": "location_edit_pin_authorization",
|
||||
"parent": "metadata_management"
|
||||
},
|
||||
"45": {
|
||||
"description": "Pin entry menu for changing products data.",
|
||||
"display_key": "ussd.kenya.products_edit_pin_authorization",
|
||||
"name": "products_edit_pin_authorization",
|
||||
"parent": "metadata_management"
|
||||
},
|
||||
"46": {
|
||||
"description": "Pin confirmation for pin change.",
|
||||
"display_key": "ussd.kenya.new_pin_confirmation",
|
||||
"name": "new_pin_confirmation",
|
||||
"parent": "metadata_management"
|
||||
},
|
||||
"47": {
|
||||
"description": "Year of birth entry menu.",
|
||||
"display_key": "ussd.kenya.enter_date_of_birth",
|
||||
"name": "enter_date_of_birth",
|
||||
"parent": "metadata_management"
|
||||
},
|
||||
"48": {
|
||||
"description": "Pin entry menu for changing year of birth data.",
|
||||
"display_key": "ussd.kenya.dob_edit_pin_authorization",
|
||||
"name": "dob_edit_pin_authorization",
|
||||
"parent": "metadata_management"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -78,27 +78,28 @@ class MetadataRequestsHandler(Metadata):
|
||||
:param data: The data to be stored in the metadata server.
|
||||
:type data: dict|str
|
||||
"""
|
||||
data = json.dumps(data)
|
||||
data = json.dumps(data).encode('utf-8')
|
||||
result = make_request(method='POST', url=self.url, data=data, headers=self.headers)
|
||||
metadata_http_error_handler(result=result)
|
||||
metadata = result.json()
|
||||
metadata = result.content
|
||||
self.edit(data=metadata)
|
||||
|
||||
def edit(self, data: Union[Dict, str]):
|
||||
def edit(self, data: bytes):
|
||||
""" This function is responsible for editing data in the metadata server corresponding to a unique pointer.
|
||||
:param data: The data to be edited in the metadata server.
|
||||
:type data: dict
|
||||
:type data: bytes
|
||||
"""
|
||||
cic_meta_signer = Signer()
|
||||
signature = cic_meta_signer.sign_digest(data=data)
|
||||
algorithm = cic_meta_signer.get_operational_key().get('algo')
|
||||
decoded_data = data.decode('utf-8')
|
||||
formatted_data = {
|
||||
'm': json.dumps(data),
|
||||
'm': data.decode('utf-8'),
|
||||
's': {
|
||||
'engine': self.engine,
|
||||
'algo': algorithm,
|
||||
'data': signature,
|
||||
'digest': data.get('digest'),
|
||||
'digest': json.loads(data).get('digest'),
|
||||
}
|
||||
}
|
||||
formatted_data = json.dumps(formatted_data)
|
||||
@@ -109,32 +110,19 @@ class MetadataRequestsHandler(Metadata):
|
||||
decoded_identifier = self.identifier.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
decoded_identifier = self.identifier.hex()
|
||||
logg.info(f'identifier: {decoded_identifier}. metadata pointer: {self.metadata_pointer} set to: {data}.')
|
||||
logg.info(f'identifier: {decoded_identifier}. metadata pointer: {self.metadata_pointer} set to: {decoded_data}.')
|
||||
|
||||
def query(self):
|
||||
"""
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
# retrieve the metadata
|
||||
"""This function is responsible for querying the metadata server for data corresponding to a unique pointer."""
|
||||
result = make_request(method='GET', url=self.url)
|
||||
metadata_http_error_handler(result=result)
|
||||
|
||||
# json serialize retrieved data
|
||||
result_data = result.json()
|
||||
|
||||
# validate result data format
|
||||
if not isinstance(result_data, dict):
|
||||
raise ValueError(f'Invalid result data object: {result_data}.')
|
||||
|
||||
response_data = result.json()
|
||||
data = json.loads(response_data)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f'Invalid data object: {data}.')
|
||||
if result.status_code == 200 and self.cic_type == ':cic.person':
|
||||
# validate person metadata
|
||||
person = Person()
|
||||
person_data = person.deserialize(person_data=result_data)
|
||||
|
||||
# format new person data for caching
|
||||
data = json.dumps(person_data.serialize())
|
||||
|
||||
# cache metadata
|
||||
cache_data(key=self.metadata_pointer, data=data)
|
||||
deserialized_person = person.deserialize(person_data=data)
|
||||
data = json.dumps(deserialized_person.serialize())
|
||||
cache_data(self.metadata_pointer, data=data)
|
||||
logg.debug(f'caching: {data} with key: {self.metadata_pointer}')
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# standard imports
|
||||
|
||||
# external imports
|
||||
|
||||
# local imports
|
||||
from .base import MetadataRequestsHandler
|
||||
|
||||
|
||||
class CustomMetadata(MetadataRequestsHandler):
|
||||
|
||||
def __init__(self, identifier: bytes):
|
||||
super().__init__(cic_type=':cic.custom', identifier=identifier)
|
||||
@@ -1,12 +0,0 @@
|
||||
# standard imports
|
||||
|
||||
# external imports
|
||||
|
||||
# local imports
|
||||
from .base import MetadataRequestsHandler
|
||||
|
||||
|
||||
class PreferencesMetadata(MetadataRequestsHandler):
|
||||
|
||||
def __init__(self, identifier: bytes):
|
||||
super().__init__(cic_type=':cic.preferences', identifier=identifier)
|
||||
@@ -47,13 +47,14 @@ class Signer:
|
||||
logg.debug(f'using signing key: {key_id}, algorithm: {key_algorithm}')
|
||||
return gpg_keys[0]
|
||||
|
||||
def sign_digest(self, data: dict):
|
||||
def sign_digest(self, data: bytes):
|
||||
"""
|
||||
:param data:
|
||||
:type data:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
data = json.loads(data)
|
||||
digest = data['digest']
|
||||
key_id = self.get_operational_key().get('keyid')
|
||||
signature = self.gpg.sign(digest, passphrase=self.gpg_passphrase, keyid=key_id)
|
||||
|
||||
@@ -6,13 +6,11 @@ import logging
|
||||
import celery
|
||||
import i18n
|
||||
from cic_eth.api.api_task import Api
|
||||
from sqlalchemy.orm.session import Session
|
||||
from tinydb.table import Document
|
||||
from typing import Optional
|
||||
|
||||
# local imports
|
||||
from cic_ussd.db.models.account import Account
|
||||
from cic_ussd.db.models.base import SessionBase
|
||||
from cic_ussd.db.models.ussd_session import UssdSession
|
||||
from cic_ussd.db.models.task_tracker import TaskTracker
|
||||
from cic_ussd.menu.ussd_menu import UssdMenu
|
||||
@@ -24,18 +22,15 @@ from cic_ussd.validator import check_known_user, validate_response_type
|
||||
logg = logging.getLogger()
|
||||
|
||||
|
||||
def add_tasks_to_tracker(session, task_uuid: str):
|
||||
"""This function takes tasks spawned over api interfaces and records their creation time for tracking.
|
||||
:param session:
|
||||
:type session:
|
||||
def add_tasks_to_tracker(task_uuid):
|
||||
"""
|
||||
This function takes tasks spawned over api interfaces and records their creation time for tracking.
|
||||
:param task_uuid: The uuid for an initiated task.
|
||||
:type task_uuid: str
|
||||
"""
|
||||
session = SessionBase.bind_session(session=session)
|
||||
task_record = TaskTracker(task_uuid=task_uuid)
|
||||
session.add(task_record)
|
||||
session.flush()
|
||||
SessionBase.release_session(session=session)
|
||||
TaskTracker.session.add(task_record)
|
||||
TaskTracker.session.commit()
|
||||
|
||||
|
||||
def define_response_with_content(headers: list, response: str) -> tuple:
|
||||
@@ -100,7 +95,6 @@ def create_or_update_session(
|
||||
service_code: str,
|
||||
user_input: str,
|
||||
current_menu: str,
|
||||
session,
|
||||
session_data: Optional[dict] = None) -> InMemoryUssdSession:
|
||||
"""
|
||||
Handles the creation or updating of session as necessary.
|
||||
@@ -114,15 +108,12 @@ def create_or_update_session(
|
||||
:type user_input: str
|
||||
:param current_menu: Menu name that is currently being displayed on the ussd session
|
||||
:type current_menu: str
|
||||
:param session:
|
||||
:type session:
|
||||
:param session_data: Any additional data that was persisted during the user's interaction with the system.
|
||||
:type session_data: dict.
|
||||
:return: ussd session object
|
||||
:rtype: InMemoryUssdSession
|
||||
"""
|
||||
session = SessionBase.bind_session(session=session)
|
||||
existing_ussd_session = session.query(UssdSession).filter_by(
|
||||
existing_ussd_session = UssdSession.session.query(UssdSession).filter_by(
|
||||
external_session_id=external_session_id).first()
|
||||
|
||||
if existing_ussd_session:
|
||||
@@ -141,25 +132,20 @@ def create_or_update_session(
|
||||
current_menu=current_menu,
|
||||
session_data=session_data
|
||||
)
|
||||
SessionBase.release_session(session=session)
|
||||
return ussd_session
|
||||
|
||||
|
||||
def get_account_status(phone_number, session: Session) -> str:
|
||||
def get_account_status(phone_number) -> str:
|
||||
"""Get the status of a user's account.
|
||||
:param phone_number: The phone number to be checked.
|
||||
:type phone_number: str
|
||||
:param session:
|
||||
:type session:
|
||||
:return: The user account status.
|
||||
:rtype: str
|
||||
"""
|
||||
session = SessionBase.bind_session(session=session)
|
||||
account = Account.get_by_phone_number(phone_number=phone_number, session=session)
|
||||
status = account.get_account_status()
|
||||
session.add(account)
|
||||
session.flush()
|
||||
SessionBase.release_session(session=session)
|
||||
user = Account.session.query(Account).filter_by(phone_number=phone_number).first()
|
||||
status = user.get_account_status()
|
||||
Account.session.add(user)
|
||||
Account.session.commit()
|
||||
|
||||
return status
|
||||
|
||||
@@ -179,7 +165,6 @@ def initiate_account_creation_request(chain_str: str,
|
||||
external_session_id: str,
|
||||
phone_number: str,
|
||||
service_code: str,
|
||||
session,
|
||||
user_input: str) -> str:
|
||||
"""This function issues a task to create a blockchain account on cic-eth. It then creates a record of the ussd
|
||||
session corresponding to the creation of the account and returns a response denoting that the user's account is
|
||||
@@ -192,8 +177,6 @@ def initiate_account_creation_request(chain_str: str,
|
||||
:type phone_number: str
|
||||
:param service_code: The service code dialed.
|
||||
:type service_code: str
|
||||
:param session:
|
||||
:type session:
|
||||
:param user_input: The input entered by the user.
|
||||
:type user_input: str
|
||||
:return: A response denoting that the account is being created.
|
||||
@@ -207,7 +190,7 @@ def initiate_account_creation_request(chain_str: str,
|
||||
creation_task_id = cic_eth_api.create_account().id
|
||||
|
||||
# record task initiation time
|
||||
add_tasks_to_tracker(task_uuid=creation_task_id, session=session)
|
||||
add_tasks_to_tracker(task_uuid=creation_task_id)
|
||||
|
||||
# cache account creation data
|
||||
cache_account_creation_task_id(phone_number=phone_number, task_id=creation_task_id)
|
||||
@@ -221,7 +204,6 @@ def initiate_account_creation_request(chain_str: str,
|
||||
phone=phone_number,
|
||||
service_code=service_code,
|
||||
current_menu=current_menu.get('name'),
|
||||
session=session,
|
||||
user_input=user_input)
|
||||
|
||||
# define response to relay to user
|
||||
@@ -286,14 +268,12 @@ def cache_account_creation_task_id(phone_number: str, task_id: str):
|
||||
redis_cache.persist(name=task_id)
|
||||
|
||||
|
||||
def process_current_menu(account: Account, session: Session, ussd_session: Optional[dict], user_input: str) -> Document:
|
||||
def process_current_menu(ussd_session: Optional[dict], user: Account, user_input: str) -> Document:
|
||||
"""This function checks user input and returns a corresponding ussd menu
|
||||
:param ussd_session: An in db ussd session object.
|
||||
:type ussd_session: UssdSession
|
||||
:param account: A account object.
|
||||
:type account: Account
|
||||
:param session:
|
||||
:type session:
|
||||
:param user: A user object.
|
||||
:type user: Account
|
||||
:param user_input: The user's input.
|
||||
:type user_input: str
|
||||
:return: An in memory ussd menu object.
|
||||
@@ -305,13 +285,7 @@ def process_current_menu(account: Account, session: Session, ussd_session: Optio
|
||||
else:
|
||||
# get current state
|
||||
latest_input = get_latest_input(user_input=user_input)
|
||||
session = SessionBase.bind_session(session=session)
|
||||
current_menu = process_request(
|
||||
account=account,
|
||||
session=session,
|
||||
ussd_session=ussd_session,
|
||||
user_input=latest_input)
|
||||
SessionBase.release_session(session=session)
|
||||
current_menu = process_request(ussd_session=ussd_session, user_input=latest_input, user=user)
|
||||
return current_menu
|
||||
|
||||
|
||||
@@ -320,7 +294,6 @@ def process_menu_interaction_requests(chain_str: str,
|
||||
phone_number: str,
|
||||
queue: str,
|
||||
service_code: str,
|
||||
session,
|
||||
user_input: str) -> str:
|
||||
"""This function handles requests intended for interaction with ussd menu, it checks whether a user matching the
|
||||
provided phone number exists and in the absence of which it creates an account for the user.
|
||||
@@ -335,29 +308,25 @@ def process_menu_interaction_requests(chain_str: str,
|
||||
:type queue: str
|
||||
:param service_code: The service dialed by the user making the request.
|
||||
:type service_code: str
|
||||
:param session:
|
||||
:type session:
|
||||
:param user_input: The inputs entered by the user.
|
||||
:type user_input: str
|
||||
:return: A response based on the request received.
|
||||
:rtype: str
|
||||
"""
|
||||
# check whether the user exists
|
||||
if not check_known_user(phone_number=phone_number, session=session):
|
||||
if not check_known_user(phone=phone_number):
|
||||
response = initiate_account_creation_request(chain_str=chain_str,
|
||||
external_session_id=external_session_id,
|
||||
phone_number=phone_number,
|
||||
service_code=service_code,
|
||||
session=session,
|
||||
user_input=user_input)
|
||||
|
||||
else:
|
||||
# get account
|
||||
session = SessionBase.bind_session(session=session)
|
||||
account = Account.get_by_phone_number(phone_number=phone_number, session=session)
|
||||
# get user
|
||||
user = Account.session.query(Account).filter_by(phone_number=phone_number).first()
|
||||
|
||||
# retrieve and cache user's metadata
|
||||
blockchain_address = account.blockchain_address
|
||||
blockchain_address = user.blockchain_address
|
||||
s_query_person_metadata = celery.signature(
|
||||
'cic_ussd.tasks.metadata.query_person_metadata',
|
||||
[blockchain_address]
|
||||
@@ -365,25 +334,24 @@ def process_menu_interaction_requests(chain_str: str,
|
||||
s_query_person_metadata.apply_async(queue='cic-ussd')
|
||||
|
||||
# find any existing ussd session
|
||||
existing_ussd_session = session.query(UssdSession).filter_by(external_session_id=external_session_id).first()
|
||||
existing_ussd_session = UssdSession.session.query(UssdSession).filter_by(
|
||||
external_session_id=external_session_id).first()
|
||||
|
||||
# validate user inputs
|
||||
if existing_ussd_session:
|
||||
current_menu = process_current_menu(
|
||||
account=account,
|
||||
session=session,
|
||||
ussd_session=existing_ussd_session.to_json(),
|
||||
user=user,
|
||||
user_input=user_input
|
||||
)
|
||||
else:
|
||||
current_menu = process_current_menu(
|
||||
account=account,
|
||||
session=session,
|
||||
ussd_session=None,
|
||||
user=user,
|
||||
user_input=user_input
|
||||
)
|
||||
|
||||
last_ussd_session = retrieve_most_recent_ussd_session(phone_number=account.phone_number, session=session)
|
||||
last_ussd_session = retrieve_most_recent_ussd_session(phone_number=user.phone_number)
|
||||
|
||||
if last_ussd_session:
|
||||
# create or update the ussd session as appropriate
|
||||
@@ -393,7 +361,6 @@ def process_menu_interaction_requests(chain_str: str,
|
||||
service_code=service_code,
|
||||
user_input=user_input,
|
||||
current_menu=current_menu.get('name'),
|
||||
session=session,
|
||||
session_data=last_ussd_session.session_data
|
||||
)
|
||||
else:
|
||||
@@ -402,17 +369,15 @@ def process_menu_interaction_requests(chain_str: str,
|
||||
phone=phone_number,
|
||||
service_code=service_code,
|
||||
user_input=user_input,
|
||||
current_menu=current_menu.get('name'),
|
||||
session=session
|
||||
current_menu=current_menu.get('name')
|
||||
)
|
||||
|
||||
# define appropriate response
|
||||
response = custom_display_text(
|
||||
account=account,
|
||||
display_key=current_menu.get('display_key'),
|
||||
menu_name=current_menu.get('name'),
|
||||
session=session,
|
||||
ussd_session=ussd_session.to_json(),
|
||||
user=user
|
||||
)
|
||||
|
||||
# check that the response from the processor is valid
|
||||
@@ -421,26 +386,21 @@ def process_menu_interaction_requests(chain_str: str,
|
||||
|
||||
# persist session to db
|
||||
persist_session_to_db_task(external_session_id=external_session_id, queue=queue)
|
||||
SessionBase.release_session(session=session)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def reset_pin(phone_number: str, session: Session) -> str:
|
||||
def reset_pin(phone_number: str) -> str:
|
||||
"""Reset account status from Locked to Pending.
|
||||
:param phone_number: The phone number belonging to the account to be unlocked.
|
||||
:type phone_number: str
|
||||
:param session:
|
||||
:type session:
|
||||
:return: The status of the pin reset.
|
||||
:rtype: str
|
||||
"""
|
||||
session = SessionBase.bind_session(session=session)
|
||||
account = Account.get_by_phone_number(phone_number=phone_number, session=session)
|
||||
account.reset_account_pin()
|
||||
session.add(account)
|
||||
session.flush()
|
||||
SessionBase.release_session(session=session)
|
||||
user = Account.session.query(Account).filter_by(phone_number=phone_number).first()
|
||||
user.reset_account_pin()
|
||||
Account.session.add(user)
|
||||
Account.session.commit()
|
||||
|
||||
response = f'Pin reset for user {phone_number} is successful!'
|
||||
return response
|
||||
@@ -478,13 +438,11 @@ def update_ussd_session(
|
||||
return session
|
||||
|
||||
|
||||
def save_to_in_memory_ussd_session_data(queue: str, session: Session, session_data: dict, ussd_session: dict):
|
||||
def save_to_in_memory_ussd_session_data(queue: str, session_data: dict, ussd_session: dict):
|
||||
"""This function is used to save information to the session data attribute of a ussd session object in the redis
|
||||
cache.
|
||||
:param queue: The queue on which the celery task should run.
|
||||
:type queue: str
|
||||
:param session:
|
||||
:type session:
|
||||
:param session_data: A dictionary containing data for a specific ussd session in redis that needs to be saved
|
||||
temporarily.
|
||||
:type session_data: dict
|
||||
@@ -515,7 +473,7 @@ def save_to_in_memory_ussd_session_data(queue: str, session: Session, session_da
|
||||
service_code=in_redis_ussd_session.get('service_code'),
|
||||
user_input=in_redis_ussd_session.get('user_input'),
|
||||
current_menu=in_redis_ussd_session.get('state'),
|
||||
session=session,
|
||||
session_data=session_data
|
||||
)
|
||||
persist_session_to_db_task(external_session_id=external_session_id, queue=queue)
|
||||
|
||||
|
||||
@@ -8,10 +8,6 @@ import phonenumbers
|
||||
from cic_ussd.db.models.account import Account
|
||||
|
||||
|
||||
class E164Format:
|
||||
region = None
|
||||
|
||||
|
||||
def process_phone_number(phone_number: str, region: str):
|
||||
"""This function parses any phone number for the provided region
|
||||
:param phone_number: A string with a phone number.
|
||||
@@ -33,5 +29,15 @@ def process_phone_number(phone_number: str, region: str):
|
||||
|
||||
return parsed_phone_number
|
||||
|
||||
class Support:
|
||||
phone_number = None
|
||||
|
||||
def get_user_by_phone_number(phone_number: str) -> Optional[Account]:
|
||||
"""This function queries the database for a user based on the provided phone number.
|
||||
:param phone_number: A valid phone number.
|
||||
:type phone_number: str
|
||||
:return: A user object matching a given phone number
|
||||
:rtype: Account|None
|
||||
"""
|
||||
# consider adding region to user's metadata
|
||||
phone_number = process_phone_number(phone_number=phone_number, region='KE')
|
||||
user = Account.session.query(Account).filter_by(phone_number=phone_number).first()
|
||||
return user
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
# standard imports
|
||||
import datetime
|
||||
import logging
|
||||
import json
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
# third party imports
|
||||
import celery
|
||||
from sqlalchemy import desc
|
||||
from cic_eth.api import Api
|
||||
from sqlalchemy.orm.session import Session
|
||||
from tinydb.table import Document
|
||||
|
||||
# local imports
|
||||
from cic_ussd.account import define_account_tx_metadata, retrieve_account_statement
|
||||
from cic_ussd.balance import BalanceManager, compute_operational_balance, get_cached_operational_balance
|
||||
from cic_ussd.chain import Chain
|
||||
from cic_ussd.db.models.account import Account
|
||||
from cic_ussd.db.models.base import SessionBase
|
||||
from cic_ussd.db.models.account import AccountStatus, Account
|
||||
from cic_ussd.db.models.ussd_session import UssdSession
|
||||
from cic_ussd.db.enum import AccountStatus
|
||||
from cic_ussd.error import SeppukuError
|
||||
from cic_ussd.error import MetadataNotFoundError, SeppukuError
|
||||
from cic_ussd.menu.ussd_menu import UssdMenu
|
||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
|
||||
from cic_ussd.phone_number import Support
|
||||
from cic_ussd.phone_number import get_user_by_phone_number
|
||||
from cic_ussd.redis import cache_data, create_cached_data_key, get_cached_data
|
||||
from cic_ussd.state_machine import UssdStateMachine
|
||||
from cic_ussd.conversions import to_wei, from_wei
|
||||
@@ -63,48 +61,47 @@ def retrieve_token_symbol(chain_str: str = Chain.spec.__str__()):
|
||||
raise SeppukuError(f'Could not retrieve default token for: {chain_str}')
|
||||
|
||||
|
||||
def process_pin_authorization(account: Account, display_key: str, **kwargs) -> str:
|
||||
"""This method provides translation for all ussd menu entries that follow the pin authorization pattern.
|
||||
:param account: The account in a running USSD session.
|
||||
:type account: Account
|
||||
def process_pin_authorization(display_key: str, user: Account, **kwargs) -> str:
|
||||
"""
|
||||
This method provides translation for all ussd menu entries that follow the pin authorization pattern.
|
||||
:param display_key: The path in the translation files defining an appropriate ussd response
|
||||
:type display_key: str
|
||||
:param user: The user in a running USSD session.
|
||||
:type user: Account
|
||||
:param kwargs: Any additional information required by the text values in the internationalization files.
|
||||
:type kwargs
|
||||
:return: A string value corresponding the ussd menu's text value.
|
||||
:rtype: str
|
||||
"""
|
||||
remaining_attempts = 3
|
||||
if account.failed_pin_attempts > 0:
|
||||
if user.failed_pin_attempts > 0:
|
||||
return translation_for(
|
||||
key=f'{display_key}.retry',
|
||||
preferred_language=account.preferred_language,
|
||||
remaining_attempts=(remaining_attempts - account.failed_pin_attempts)
|
||||
preferred_language=user.preferred_language,
|
||||
remaining_attempts=(remaining_attempts - user.failed_pin_attempts)
|
||||
)
|
||||
else:
|
||||
return translation_for(
|
||||
key=f'{display_key}.first',
|
||||
preferred_language=account.preferred_language,
|
||||
preferred_language=user.preferred_language,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
def process_exit_insufficient_balance(account: Account, display_key: str, session: Session, ussd_session: dict):
|
||||
def process_exit_insufficient_balance(display_key: str, user: Account, ussd_session: dict):
|
||||
"""This function processes the exit menu letting users their account balance is insufficient to perform a specific
|
||||
transaction.
|
||||
:param account: The account requesting access to the ussd menu.
|
||||
:type account: Account
|
||||
:param display_key: The path in the translation files defining an appropriate ussd response
|
||||
:type display_key: str
|
||||
:param session:
|
||||
:type session:
|
||||
:param user: The user requesting access to the ussd menu.
|
||||
:type user: Account
|
||||
:param ussd_session: A JSON serialized in-memory ussd session object
|
||||
:type ussd_session: dict
|
||||
:return: Corresponding translation text response
|
||||
:rtype: str
|
||||
"""
|
||||
# get account balance
|
||||
operational_balance = get_cached_operational_balance(blockchain_address=account.blockchain_address)
|
||||
operational_balance = get_cached_operational_balance(blockchain_address=user.blockchain_address)
|
||||
|
||||
# compile response data
|
||||
user_input = ussd_session.get('user_input').split('*')[-1]
|
||||
@@ -114,13 +111,13 @@ def process_exit_insufficient_balance(account: Account, display_key: str, sessio
|
||||
token_symbol = retrieve_token_symbol()
|
||||
|
||||
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
|
||||
recipient = Account.get_by_phone_number(phone_number=recipient_phone_number, session=session)
|
||||
recipient = get_user_by_phone_number(phone_number=recipient_phone_number)
|
||||
|
||||
tx_recipient_information = define_account_tx_metadata(user=recipient)
|
||||
|
||||
return translation_for(
|
||||
key=display_key,
|
||||
preferred_language=account.preferred_language,
|
||||
preferred_language=user.preferred_language,
|
||||
amount=from_wei(transaction_amount),
|
||||
token_symbol=token_symbol,
|
||||
recipient_information=tx_recipient_information,
|
||||
@@ -128,14 +125,12 @@ def process_exit_insufficient_balance(account: Account, display_key: str, sessio
|
||||
)
|
||||
|
||||
|
||||
def process_exit_successful_transaction(account: Account, display_key: str, session: Session, ussd_session: dict):
|
||||
def process_exit_successful_transaction(display_key: str, user: Account, ussd_session: dict):
|
||||
"""This function processes the exit menu after a successful initiation for a transfer of tokens.
|
||||
:param account: The account requesting access to the ussd menu.
|
||||
:type account: Account
|
||||
:param display_key: The path in the translation files defining an appropriate ussd response
|
||||
:type display_key: str
|
||||
:param session:
|
||||
:type session:
|
||||
:param user: The user requesting access to the ussd menu.
|
||||
:type user: Account
|
||||
:param ussd_session: A JSON serialized in-memory ussd session object
|
||||
:type ussd_session: dict
|
||||
:return: Corresponding translation text response
|
||||
@@ -144,13 +139,13 @@ def process_exit_successful_transaction(account: Account, display_key: str, sess
|
||||
transaction_amount = to_wei(int(ussd_session.get('session_data').get('transaction_amount')))
|
||||
token_symbol = retrieve_token_symbol()
|
||||
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
|
||||
recipient = Account.get_by_phone_number(phone_number=recipient_phone_number, session=session)
|
||||
recipient = get_user_by_phone_number(phone_number=recipient_phone_number)
|
||||
tx_recipient_information = define_account_tx_metadata(user=recipient)
|
||||
tx_sender_information = define_account_tx_metadata(user=account)
|
||||
tx_sender_information = define_account_tx_metadata(user=user)
|
||||
|
||||
return translation_for(
|
||||
key=display_key,
|
||||
preferred_language=account.preferred_language,
|
||||
preferred_language=user.preferred_language,
|
||||
transaction_amount=from_wei(transaction_amount),
|
||||
token_symbol=token_symbol,
|
||||
recipient_information=tx_recipient_information,
|
||||
@@ -158,15 +153,13 @@ def process_exit_successful_transaction(account: Account, display_key: str, sess
|
||||
)
|
||||
|
||||
|
||||
def process_transaction_pin_authorization(account: Account, display_key: str, session: Session, ussd_session: dict):
|
||||
def process_transaction_pin_authorization(user: Account, display_key: str, ussd_session: dict):
|
||||
"""This function processes pin authorization where making a transaction is concerned. It constructs a
|
||||
pre-transaction response menu that shows the details of the transaction.
|
||||
:param account: The account requesting access to the ussd menu.
|
||||
:type account: Account
|
||||
:param user: The user requesting access to the ussd menu.
|
||||
:type user: Account
|
||||
:param display_key: The path in the translation files defining an appropriate ussd response
|
||||
:type display_key: str
|
||||
:param session:
|
||||
:type session:
|
||||
:param ussd_session: The USSD session determining what user data needs to be extracted and added to the menu's
|
||||
text values.
|
||||
:type ussd_session: UssdSession
|
||||
@@ -175,16 +168,16 @@ def process_transaction_pin_authorization(account: Account, display_key: str, se
|
||||
"""
|
||||
# compile response data
|
||||
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
|
||||
recipient = Account.get_by_phone_number(phone_number=recipient_phone_number, session=session)
|
||||
recipient = get_user_by_phone_number(phone_number=recipient_phone_number)
|
||||
tx_recipient_information = define_account_tx_metadata(user=recipient)
|
||||
tx_sender_information = define_account_tx_metadata(user=account)
|
||||
tx_sender_information = define_account_tx_metadata(user=user)
|
||||
|
||||
token_symbol = retrieve_token_symbol()
|
||||
user_input = ussd_session.get('session_data').get('transaction_amount')
|
||||
transaction_amount = to_wei(value=int(user_input))
|
||||
logg.debug('Requires integration to determine user tokens.')
|
||||
return process_pin_authorization(
|
||||
account=account,
|
||||
user=user,
|
||||
display_key=display_key,
|
||||
recipient_information=tx_recipient_information,
|
||||
transaction_amount=from_wei(transaction_amount),
|
||||
@@ -193,12 +186,14 @@ def process_transaction_pin_authorization(account: Account, display_key: str, se
|
||||
)
|
||||
|
||||
|
||||
def process_account_balances(user: Account, display_key: str):
|
||||
def process_account_balances(user: Account, display_key: str, ussd_session: dict):
|
||||
"""
|
||||
:param user:
|
||||
:type user:
|
||||
:param display_key:
|
||||
:type display_key:
|
||||
:param ussd_session:
|
||||
:type ussd_session:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
@@ -256,16 +251,12 @@ def process_display_user_metadata(user: Account, display_key: str):
|
||||
identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address),
|
||||
cic_type=':cic.person'
|
||||
)
|
||||
cached_metadata = get_cached_data(key)
|
||||
if cached_metadata:
|
||||
user_metadata = json.loads(cached_metadata)
|
||||
user_metadata = get_cached_data(key)
|
||||
if user_metadata:
|
||||
user_metadata = json.loads(user_metadata)
|
||||
contact_data = get_contact_data_from_vcard(vcard=user_metadata.get('vcard'))
|
||||
logg.debug(f'{contact_data}')
|
||||
full_name = f'{contact_data.get("given")} {contact_data.get("family")}'
|
||||
date_of_birth = user_metadata.get('date_of_birth')
|
||||
year_of_birth = date_of_birth.get('year')
|
||||
present_year = datetime.datetime.now().year
|
||||
age = present_year - year_of_birth
|
||||
gender = user_metadata.get('gender')
|
||||
products = ', '.join(user_metadata.get('products'))
|
||||
location = user_metadata.get('location').get('area_name')
|
||||
@@ -274,38 +265,22 @@ def process_display_user_metadata(user: Account, display_key: str):
|
||||
key=display_key,
|
||||
preferred_language=user.preferred_language,
|
||||
full_name=full_name,
|
||||
age=age,
|
||||
gender=gender,
|
||||
location=location,
|
||||
products=products
|
||||
)
|
||||
else:
|
||||
# TODO [Philip]: All these translations could be moved to translation files.
|
||||
logg.warning(f'Expected person metadata but found none in cache for key: {key}')
|
||||
|
||||
absent = ''
|
||||
if user.preferred_language == 'en':
|
||||
absent = 'Not provided'
|
||||
elif user.preferred_language == 'sw':
|
||||
absent = 'Haijawekwa'
|
||||
|
||||
return translation_for(
|
||||
key=display_key,
|
||||
preferred_language=user.preferred_language,
|
||||
full_name=absent,
|
||||
gender=absent,
|
||||
age=absent,
|
||||
location=absent,
|
||||
products=absent
|
||||
)
|
||||
raise MetadataNotFoundError(f'Expected person metadata but found none in cache for key: {key}')
|
||||
|
||||
|
||||
def process_account_statement(user: Account, display_key: str):
|
||||
def process_account_statement(user: Account, display_key: str, ussd_session: dict):
|
||||
"""
|
||||
:param user:
|
||||
:type user:
|
||||
:param display_key:
|
||||
:type display_key:
|
||||
:param ussd_session:
|
||||
:type ussd_session:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
@@ -407,26 +382,23 @@ def process_start_menu(display_key: str, user: Account):
|
||||
)
|
||||
|
||||
|
||||
def retrieve_most_recent_ussd_session(phone_number: str, session: Session) -> UssdSession:
|
||||
def retrieve_most_recent_ussd_session(phone_number: str) -> UssdSession:
|
||||
# get last ussd session based on user phone number
|
||||
session = SessionBase.bind_session(session=session)
|
||||
last_ussd_session = session.query(UssdSession)\
|
||||
last_ussd_session = UssdSession.session\
|
||||
.query(UssdSession)\
|
||||
.filter_by(msisdn=phone_number)\
|
||||
.order_by(desc(UssdSession.created))\
|
||||
.first()
|
||||
SessionBase.release_session(session=session)
|
||||
return last_ussd_session
|
||||
|
||||
|
||||
def process_request(account: Account, session, user_input: str, ussd_session: Optional[dict] = None) -> Document:
|
||||
def process_request(user_input: str, user: Account, ussd_session: Optional[dict] = None) -> Document:
|
||||
"""This function assesses a request based on the user from the request comes, the session_id and the user's
|
||||
input. It determines whether the request translates to a return to an existing session by checking whether the
|
||||
provided session id exists in the database or whether the creation of a new ussd session object is warranted.
|
||||
It then returns the appropriate ussd menu text values.
|
||||
:param account: The account requesting access to the ussd menu.
|
||||
:type account: Account
|
||||
:param session:
|
||||
:type session:
|
||||
:param user: The user requesting access to the ussd menu.
|
||||
:type user: Account
|
||||
:param user_input: The value a user enters in the ussd menu.
|
||||
:type user_input: str
|
||||
:param ussd_session: A JSON serialized in-memory ussd session object
|
||||
@@ -434,20 +406,22 @@ def process_request(account: Account, session, user_input: str, ussd_session: Op
|
||||
:return: A ussd menu's corresponding text value.
|
||||
:rtype: Document
|
||||
"""
|
||||
# retrieve metadata before any transition
|
||||
key = generate_metadata_pointer(
|
||||
identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address),
|
||||
cic_type=':cic.person'
|
||||
)
|
||||
person_metadata = get_cached_data(key=key)
|
||||
|
||||
if ussd_session:
|
||||
if user_input == "0":
|
||||
return UssdMenu.parent_menu(menu_name=ussd_session.get('state'))
|
||||
else:
|
||||
successive_state = next_state(
|
||||
account=account,
|
||||
session=session,
|
||||
ussd_session=ussd_session,
|
||||
user_input=user_input)
|
||||
successive_state = next_state(ussd_session=ussd_session, user=user, user_input=user_input)
|
||||
return UssdMenu.find_by_name(name=successive_state)
|
||||
else:
|
||||
if account.has_valid_pin():
|
||||
last_ussd_session = retrieve_most_recent_ussd_session(phone_number=account.phone_number, session=session)
|
||||
if user.has_valid_pin():
|
||||
last_ussd_session = retrieve_most_recent_ussd_session(phone_number=user.phone_number)
|
||||
|
||||
if last_ussd_session:
|
||||
# get last state
|
||||
@@ -459,108 +433,74 @@ def process_request(account: Account, session, user_input: str, ussd_session: Op
|
||||
'exit_invalid_pin',
|
||||
'exit_invalid_new_pin',
|
||||
'exit_pin_mismatch',
|
||||
'exit_invalid_request',
|
||||
'exit_successful_transaction'
|
||||
]:
|
||||
'exit_invalid_request'
|
||||
] and person_metadata is not None:
|
||||
return UssdMenu.find_by_name(name='start')
|
||||
else:
|
||||
return UssdMenu.find_by_name(name=last_state)
|
||||
else:
|
||||
if account.failed_pin_attempts >= 3 and account.get_account_status() == AccountStatus.LOCKED.name:
|
||||
if user.failed_pin_attempts >= 3 and user.get_account_status() == AccountStatus.LOCKED.name:
|
||||
return UssdMenu.find_by_name(name='exit_pin_blocked')
|
||||
elif account.preferred_language is None:
|
||||
elif user.preferred_language is None:
|
||||
return UssdMenu.find_by_name(name='initial_language_selection')
|
||||
else:
|
||||
return UssdMenu.find_by_name(name='initial_pin_entry')
|
||||
|
||||
|
||||
def next_state(account: Account, session, ussd_session: dict, user_input: str) -> str:
|
||||
def next_state(ussd_session: dict, user: Account, user_input: str) -> str:
|
||||
"""This function navigates the state machine based on the ussd session object and user inputs it receives.
|
||||
It checks the user input and provides the successive state in the state machine. It then updates the session's
|
||||
state attribute with the new state.
|
||||
:param account: The account requesting access to the ussd menu.
|
||||
:type account: Account
|
||||
:param session:
|
||||
:type session:
|
||||
:param ussd_session: A JSON serialized in-memory ussd session object
|
||||
:type ussd_session: dict
|
||||
:param user: The user requesting access to the ussd menu.
|
||||
:type user: Account
|
||||
:param user_input: The value a user enters in the ussd menu.
|
||||
:type user_input: str
|
||||
:return: A string value corresponding the successive give a specific state in the state machine.
|
||||
"""
|
||||
state_machine = UssdStateMachine(ussd_session=ussd_session)
|
||||
state_machine.scan_data((user_input, ussd_session, account, session))
|
||||
state_machine.scan_data((user_input, ussd_session, user))
|
||||
new_state = state_machine.state
|
||||
|
||||
return new_state
|
||||
|
||||
|
||||
def process_exit_invalid_menu_option(display_key: str, preferred_language: str):
|
||||
return translation_for(
|
||||
key=display_key,
|
||||
preferred_language=preferred_language,
|
||||
support_phone=Support.phone_number
|
||||
)
|
||||
|
||||
|
||||
def custom_display_text(
|
||||
account: Account,
|
||||
display_key: str,
|
||||
menu_name: str,
|
||||
session: Session,
|
||||
ussd_session: dict) -> str:
|
||||
ussd_session: dict,
|
||||
user: Account) -> str:
|
||||
"""This function extracts the appropriate session data based on the current menu name. It then inserts them as
|
||||
keywords in the i18n function.
|
||||
:param account: The account in a running USSD session.
|
||||
:type account: Account
|
||||
:param display_key: The path in the translation files defining an appropriate ussd response
|
||||
:type display_key: str
|
||||
:param menu_name: The name by which a specific menu can be identified.
|
||||
:type menu_name: str
|
||||
:param session:
|
||||
:type session:
|
||||
:param user: The user in a running USSD session.
|
||||
:type user: Account
|
||||
:param ussd_session: A JSON serialized in-memory ussd session object
|
||||
:type ussd_session: dict
|
||||
:return: A string value corresponding the ussd menu's text value.
|
||||
:rtype: str
|
||||
"""
|
||||
if menu_name == 'transaction_pin_authorization':
|
||||
return process_transaction_pin_authorization(
|
||||
account=account,
|
||||
display_key=display_key,
|
||||
session=session,
|
||||
ussd_session=ussd_session)
|
||||
return process_transaction_pin_authorization(display_key=display_key, user=user, ussd_session=ussd_session)
|
||||
elif menu_name == 'exit_insufficient_balance':
|
||||
return process_exit_insufficient_balance(
|
||||
account=account,
|
||||
display_key=display_key,
|
||||
session=session,
|
||||
ussd_session=ussd_session)
|
||||
return process_exit_insufficient_balance(display_key=display_key, user=user, ussd_session=ussd_session)
|
||||
elif menu_name == 'exit_successful_transaction':
|
||||
return process_exit_successful_transaction(
|
||||
account=account,
|
||||
display_key=display_key,
|
||||
session=session,
|
||||
ussd_session=ussd_session)
|
||||
return process_exit_successful_transaction(display_key=display_key, user=user, ussd_session=ussd_session)
|
||||
elif menu_name == 'start':
|
||||
return process_start_menu(display_key=display_key, user=account)
|
||||
return process_start_menu(display_key=display_key, user=user)
|
||||
elif 'pin_authorization' in menu_name:
|
||||
return process_pin_authorization(
|
||||
account=account,
|
||||
display_key=display_key,
|
||||
session=session)
|
||||
return process_pin_authorization(display_key=display_key, user=user)
|
||||
elif 'enter_current_pin' in menu_name:
|
||||
return process_pin_authorization(
|
||||
account=account,
|
||||
display_key=display_key,
|
||||
session=session)
|
||||
return process_pin_authorization(display_key=display_key, user=user)
|
||||
elif menu_name == 'account_balances':
|
||||
return process_account_balances(display_key=display_key, user=account)
|
||||
return process_account_balances(display_key=display_key, user=user, ussd_session=ussd_session)
|
||||
elif 'transaction_set' in menu_name:
|
||||
return process_account_statement(display_key=display_key, user=account)
|
||||
return process_account_statement(display_key=display_key, user=user, ussd_session=ussd_session)
|
||||
elif menu_name == 'display_user_metadata':
|
||||
return process_display_user_metadata(display_key=display_key, user=account)
|
||||
elif menu_name == 'exit_invalid_menu_option':
|
||||
return process_exit_invalid_menu_option(display_key=display_key, preferred_language=account.preferred_language)
|
||||
return process_display_user_metadata(display_key=display_key, user=user)
|
||||
else:
|
||||
return translation_for(key=display_key, preferred_language=account.preferred_language)
|
||||
return translation_for(key=display_key, preferred_language=user.preferred_language)
|
||||
|
||||
@@ -8,12 +8,9 @@ from urllib.parse import urlparse, parse_qs
|
||||
|
||||
# third-party imports
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
# local imports
|
||||
from cic_ussd.db.models.account import Account
|
||||
from cic_ussd.db.models.base import SessionBase
|
||||
from cic_ussd.db.enum import AccountStatus
|
||||
from cic_ussd.db.models.account import AccountStatus, Account
|
||||
from cic_ussd.operations import get_account_status, reset_pin
|
||||
from cic_ussd.validator import check_known_user
|
||||
|
||||
@@ -75,26 +72,24 @@ def get_account_creation_callback_request_data(env: dict) -> tuple:
|
||||
return status, task_id, result
|
||||
|
||||
|
||||
def process_pin_reset_requests(env: dict, phone_number: str, session: Session):
|
||||
def process_pin_reset_requests(env: dict, phone_number: str):
|
||||
"""This function processes requests that are responsible for the pin reset functionality. It processes GET and PUT
|
||||
requests responsible for returning an account's status and
|
||||
:param env: A dictionary of values representing data sent on the api.
|
||||
:type env: dict
|
||||
:param phone_number: The phone of the user whose pin is being reset.
|
||||
:type phone_number: str
|
||||
:param session:
|
||||
:type session:
|
||||
:return: A response denoting the result of the request to reset the user's pin.
|
||||
:rtype: str
|
||||
"""
|
||||
if not check_known_user(phone_number=phone_number, session=session):
|
||||
if not check_known_user(phone=phone_number):
|
||||
return f'No user matching {phone_number} was found.', '404 Not Found'
|
||||
|
||||
if get_request_method(env) == 'PUT':
|
||||
return reset_pin(phone_number=phone_number, session=session), '200 OK'
|
||||
return reset_pin(phone_number=phone_number), '200 OK'
|
||||
|
||||
if get_request_method(env) == 'GET':
|
||||
status = get_account_status(phone_number=phone_number, session=session)
|
||||
status = get_account_status(phone_number=phone_number)
|
||||
response = {
|
||||
'status': f'{status}'
|
||||
}
|
||||
@@ -102,18 +97,16 @@ def process_pin_reset_requests(env: dict, phone_number: str, session: Session):
|
||||
return response, '200 OK'
|
||||
|
||||
|
||||
def process_locked_accounts_requests(env: dict, session: Session) -> tuple:
|
||||
def process_locked_accounts_requests(env: dict) -> tuple:
|
||||
"""This function authenticates staff requests and returns a serialized JSON formatted list of blockchain addresses
|
||||
of accounts for which the PIN has been locked due to too many failed attempts.
|
||||
:param env: A dictionary of values representing data sent on the api.
|
||||
:type env: dict
|
||||
:param session:
|
||||
:type session:
|
||||
:return: A tuple containing a serialized list of blockchain addresses for locked accounts and corresponding message
|
||||
for the response.
|
||||
:rtype: tuple
|
||||
"""
|
||||
session = SessionBase.bind_session(session=session)
|
||||
logg.debug('Authentication requires integration with cic-auth')
|
||||
response = ''
|
||||
|
||||
if get_request_method(env) == 'GET':
|
||||
@@ -130,14 +123,12 @@ def process_locked_accounts_requests(env: dict, session: Session) -> tuple:
|
||||
else:
|
||||
limit = r[1]
|
||||
|
||||
locked_accounts = session.query(Account.blockchain_address).filter(
|
||||
locked_accounts = Account.session.query(Account.blockchain_address).filter(
|
||||
Account.account_status == AccountStatus.LOCKED.value,
|
||||
Account.failed_pin_attempts >= 3).order_by(desc(Account.updated)).offset(offset).limit(limit).all()
|
||||
|
||||
# convert lists to scalar blockchain addresses
|
||||
locked_accounts = [blockchain_address for (blockchain_address, ) in locked_accounts]
|
||||
|
||||
SessionBase.release_session(session=session)
|
||||
response = json.dumps(locked_accounts)
|
||||
return response, '200 OK'
|
||||
return response, '405 Play by the rules'
|
||||
|
||||
@@ -36,8 +36,11 @@ logg.debug('config loaded from {}:\n{}'.format(args.c, config))
|
||||
# set up db
|
||||
data_source_name = dsn_from_config(config)
|
||||
SessionBase.connect(data_source_name, pool_size=int(config.get('DATABASE_POOL_SIZE')), debug=config.true('DATABASE_DEBUG'))
|
||||
# create session for the life time of http request
|
||||
SessionBase.session = SessionBase.create_session()
|
||||
|
||||
|
||||
# handle requests from CICADA
|
||||
def application(env, start_response):
|
||||
"""Loads python code for application to be accessible over web server
|
||||
:param env: Object containing server and request information
|
||||
@@ -52,24 +55,19 @@ def application(env, start_response):
|
||||
errors_headers = [('Content-Type', 'text/plain'), ('Content-Length', '0')]
|
||||
headers = [('Content-Type', 'text/plain')]
|
||||
|
||||
# create session for the life time of http request
|
||||
session = SessionBase.create_session()
|
||||
|
||||
if get_request_endpoint(env) == '/pin':
|
||||
phone_number = get_query_parameters(env=env, query_name='phoneNumber')
|
||||
phone_number = quote_plus(phone_number)
|
||||
response, message = process_pin_reset_requests(env=env, phone_number=phone_number, session=session)
|
||||
response, message = process_pin_reset_requests(env=env, phone_number=phone_number)
|
||||
response_bytes, headers = define_response_with_content(headers=errors_headers, response=response)
|
||||
session.commit()
|
||||
session.close()
|
||||
SessionBase.session.close()
|
||||
start_response(message, headers)
|
||||
return [response_bytes]
|
||||
|
||||
# handle requests for locked accounts
|
||||
response, message = process_locked_accounts_requests(env=env, session=session)
|
||||
response, message = process_locked_accounts_requests(env=env)
|
||||
response_bytes, headers = define_response_with_content(headers=headers, response=response)
|
||||
start_response(message, headers)
|
||||
session.commit()
|
||||
session.close()
|
||||
SessionBase.session.close()
|
||||
return [response_bytes]
|
||||
|
||||
|
||||
@@ -127,8 +127,6 @@ def main():
|
||||
argv.append('--loglevel=INFO')
|
||||
argv.append('-Q')
|
||||
argv.append(args.q)
|
||||
argv.append('-n')
|
||||
argv.append(args.q)
|
||||
|
||||
current_app.worker_main(argv)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ from cic_ussd.metadata.base import Metadata
|
||||
from cic_ussd.operations import (define_response_with_content,
|
||||
process_menu_interaction_requests,
|
||||
define_multilingual_responses)
|
||||
from cic_ussd.phone_number import process_phone_number, Support, E164Format
|
||||
from cic_ussd.phone_number import process_phone_number
|
||||
from cic_ussd.processor import get_default_token_data
|
||||
from cic_ussd.redis import cache_data, create_cached_data_key, InMemoryStore
|
||||
from cic_ussd.requests import (get_request_endpoint,
|
||||
@@ -55,6 +55,8 @@ data_source_name = dsn_from_config(config)
|
||||
SessionBase.connect(data_source_name,
|
||||
pool_size=int(config.get('DATABASE_POOL_SIZE')),
|
||||
debug=config.true('DATABASE_DEBUG'))
|
||||
# create session for the life time of http request
|
||||
SessionBase.session = SessionBase.create_session()
|
||||
|
||||
# set up translations
|
||||
i18n.load_path.append(config.get('APP_LOCALE_PATH'))
|
||||
@@ -124,9 +126,6 @@ else:
|
||||
|
||||
valid_service_codes = config.get('APP_SERVICE_CODE').split(",")
|
||||
|
||||
E164Format.region = config.get('PHONE_NUMBER_REGION')
|
||||
Support.phone_number = config.get('APP_SUPPORT_PHONE_NUMBER')
|
||||
|
||||
|
||||
def application(env, start_response):
|
||||
"""Loads python code for application to be accessible over web server
|
||||
@@ -141,13 +140,10 @@ def application(env, start_response):
|
||||
errors_headers = [('Content-Type', 'text/plain'), ('Content-Length', '0')]
|
||||
headers = [('Content-Type', 'text/plain')]
|
||||
|
||||
# create session for the life time of http request
|
||||
session = SessionBase.create_session()
|
||||
|
||||
if get_request_method(env=env) == 'POST' and get_request_endpoint(env=env) == '/':
|
||||
|
||||
if env.get('CONTENT_TYPE') != 'application/x-www-form-urlencoded':
|
||||
start_response('405 Urlencoded, please', errors_headers)
|
||||
start_response('405 Play by the rules', errors_headers)
|
||||
return []
|
||||
|
||||
post_data = env.get('wsgi.input').read()
|
||||
@@ -170,7 +166,7 @@ def application(env, start_response):
|
||||
|
||||
# add validation for phone number
|
||||
if phone_number:
|
||||
phone_number = process_phone_number(phone_number=phone_number, region=E164Format.region)
|
||||
phone_number = process_phone_number(phone_number=phone_number, region=config.get('PHONE_NUMBER_REGION'))
|
||||
|
||||
# validate ip address
|
||||
if not check_ip(config=config, env=env):
|
||||
@@ -207,20 +203,14 @@ def application(env, start_response):
|
||||
phone_number=phone_number,
|
||||
queue=args.q,
|
||||
service_code=service_code,
|
||||
session=session,
|
||||
user_input=user_input)
|
||||
|
||||
response_bytes, headers = define_response_with_content(headers=headers, response=response)
|
||||
start_response('200 OK,', headers)
|
||||
session.commit()
|
||||
session.close()
|
||||
SessionBase.session.close()
|
||||
return [response_bytes]
|
||||
|
||||
else:
|
||||
logg.error('invalid query {}'.format(env))
|
||||
for r in env:
|
||||
logg.debug('{}: {}'.format(r, env))
|
||||
session.close()
|
||||
start_response('405 Play by the rules', errors_headers)
|
||||
return []
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user