Compare commits

..

11 Commits

Author SHA1 Message Date
lash
5330bd1696
Bump threaded version 2022-01-24 08:48:28 +00:00
lash
4f6abf5d05
Add no-resolve / no-tx options 2022-01-23 14:55:56 +00:00
lash
027e19cf11
Extract cli workers to separate file 2022-01-23 12:43:25 +00:00
lash
fb335e29ca
Remove commented code 2022-01-23 12:38:01 +00:00
lash
cbf058e4e8
Only fetch metadata for each wallet address once 2022-01-23 12:36:23 +00:00
lash
12815c9001
Remove commented code 2022-01-23 11:24:00 +00:00
lash
7faeb42bd9
Bump prerelease state 2022-01-23 10:48:43 +00:00
lash
e1bc243910
Complete threaded lookups 2022-01-23 10:48:02 +00:00
lash
616fa2e15c
WIP Add threaded lookups (currently notifier gets confused) 2022-01-22 14:36:58 +00:00
lash
b44a50aaa2
Add raw tx filestore, optional retrieve and cache of raw tx 2022-01-22 10:43:46 +00:00
lash
265a53f9ca
Add tx filestore, add tx dict src to hexdir in store 2022-01-22 08:44:36 +00:00
13 changed files with 472 additions and 341 deletions

4
.gitignore vendored
View File

@ -2,7 +2,3 @@ __pycache__
*.egg-info *.egg-info
build/ build/
*.pyc *.pyc
.venv
.clicada
dist/
.vscode/

View File

@ -1,5 +1,3 @@
- 0.0.7
* fix: make store_path relative to the users home
- 0.0.6 - 0.0.6
* Add cache encryption, with AES-CTR-128 * Add cache encryption, with AES-CTR-128
- 0.0.5 - 0.0.5

View File

@ -1,87 +0,0 @@
## Clicada
> Admin Command Line Interface to interact with cic-meta and cic-cache
### Pre-requisites
- Public key uploaded to `cic-auth-helper`
- PGP Keyring for your key
### Installation
Use either of the following installation methods:
1. Install from git release (recommended)
```bash
wget https://git.grassecon.net/grassrootseconomics/clicada/archive/v0.0.6.zip
unzip clicada-v0.0.6.zip
cd clicada
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt --extra-index-url=https://pip.grassrootseconomics.net
```
2. Install from pip to path (non sudo)
```bash
pip3 install -UI --extra-index-url=https://pip.grassrootseconomics.net clicada
```
### GPG Keyring setup
PGP uses the default keyring, you can however pass in a custom keyring path.
To create a keyring from a specific key and get its path for `AUTH_KEYRING_PATH`:
```bash
# In some dir
gpg --homedir .gnupg --import private.pgp
pwd
```
### Usage
```bash
usage: clicada [...optional arguments] [...positional arguments]
positional arguments:
{user,u,tag,t}
user (u) retrieve transactions for a user
tag (t) locally assign a display value to an identifier
optional arguments:
-h, --help show this help message and exit
--no-logs Turn off all logging
-v Be verbose
-vv Be very verbose
-c CONFIG, --config CONFIG
Configuration directory
-n NAMESPACE, --namespace NAMESPACE
Configuration namespace
--dumpconfig {env,ini}
Output configuration and quit. Use with --raw to omit values and output schema only.
--env-prefix ENV_PREFIX
environment prefix for variables to overwrite configuration
-p P, --rpc-provider P
RPC HTTP(S) provider url
--rpc-dialect RPC_DIALECT
RPC HTTP(S) backend dialect
--height HEIGHT Block height to execute against
-i I, --chain-spec I Chain specification string
-u, --unsafe Do not verify address checksums
--seq Use sequential rpc ids
-y Y, --key-file Y Keystore file to use for signing or address
--raw Do not decode output
--fee-price FEE_PRICE
override fee price
--fee-limit FEE_LIMIT
override fee limit
```
### Example
```bash
AUTH_PASSPHRASE=queenmarlena AUTH_KEYRING_PATH=/home/kamikaze/grassroots/usumbufu/tests/testdata/pgp/.gnupg/ AUTH_KEY=CCE2E1D2D0E36ADE0405E2D0995BB21816313BD5 CHAIN_SPEC=evm:byzantium:8996:bloxberg CIC_REGISTRY_ADDRESS=0xcf60ebc445b636a5ab787f9e8bc465a2a3ef8299 RPC_PROVIDER=https://rpc.grassecon.net TX_CACHE_URL=https://cache.grassecon.net HTTP_CORS_ORIGIN=https://auth.grassecon.net META_HTTP_ORIGIN=https://auth.grassecon.net:443 PYTHONPATH=. python clicada/runnable/view.py u --meta-url https://auth.grassecon.net +254711000000
```

View File

@ -1,43 +1,46 @@
# import notifier # import notifier
from clicada.cli.notify import NotifyWriter from clicada.cli.notify import NotifyWriter
notifier = NotifyWriter() notifier = NotifyWriter()
# notifier.notify('loading script') #notifier.notify('loading script')
import importlib
import logging
# standard imports # standard imports
import os import os
import logging
import importlib
import sys import sys
import chainlib.eth.cli
import clicada.cli.tag as cmd_tag
# local imports
import clicada.cli.user as cmd_user
# external imports # external imports
import confini import confini
import chainlib.eth.cli
from chainlib.chain import ChainSpec from chainlib.chain import ChainSpec
# local imports
import clicada.cli.user as cmd_user
import clicada.cli.tag as cmd_tag
from clicada.cli.auth import PGPAuthCrypt from clicada.cli.auth import PGPAuthCrypt
from clicada.cli.http import HTTPSession, PGPClientSession from clicada.cli.http import (
HTTPSession,
PGPClientSession,
)
from clicada.crypt.aes import AESCTREncrypt from clicada.crypt.aes import AESCTREncrypt
logg = logging.getLogger() logg = logging.getLogger()
script_dir = os.path.dirname(os.path.realpath(__file__)) script_dir = os.path.dirname(os.path.realpath(__file__))
data_dir = os.path.join(script_dir, "..", "data") data_dir = os.path.join(script_dir, '..', 'data')
base_config_dir = os.path.join(data_dir, "config") base_config_dir = os.path.join(data_dir, 'config')
class NullWriter: class NullWriter:
def notify(self, v): def notify(self, v):
pass pass
def ouch(self, v): def ouch(self, v):
pass pass
def write(self, v): def write(self, v):
sys.stdout.write(str(v)) sys.stdout.write(str(v))
@ -45,13 +48,13 @@ class NullWriter:
class CmdCtrl: class CmdCtrl:
__cmd_alias = { __cmd_alias = {
"u": "user", 'u': 'user',
"t": "tag", 't': 'tag',
} }
__auth_for = [ __auth_for = [
"user", 'user',
] ]
def __init__(self, argv=None, description=None, logger=None, *args, **kwargs): def __init__(self, argv=None, description=None, logger=None, *args, **kwargs):
self.args(argv) self.args(argv)
@ -69,36 +72,29 @@ class CmdCtrl:
self.blockchain() self.blockchain()
self.remote_openers = {} self.remote_openers = {}
if self.get("META_URL") != None: if self.get('META_URL') != None:
auth_client_session = PGPClientSession(self.__auth) auth_client_session = PGPClientSession(self.__auth)
self.remote_openers["meta"] = HTTPSession( self.remote_openers['meta'] = HTTPSession(self.get('META_URL'), auth=auth_client_session, origin=self.config.get('META_HTTP_ORIGIN'))
self.get("META_URL"),
auth=auth_client_session,
origin=self.config.get("META_HTTP_ORIGIN"),
)
def blockchain(self): def blockchain(self):
self.chain_spec = ChainSpec.from_chain_str(self.config.get("CHAIN_SPEC")) self.chain_spec = ChainSpec.from_chain_str(self.config.get('CHAIN_SPEC'))
self.rpc = chainlib.eth.cli.Rpc() self.rpc = chainlib.eth.cli.Rpc()
self.__conn = self.rpc.connect_by_config(self.config) self.__conn = self.rpc.connect_by_config(self.config)
def args(self, argv): def args(self, argv):
self.argparser = chainlib.eth.cli.ArgumentParser( self.argparser = chainlib.eth.cli.ArgumentParser(chainlib.eth.cli.argflag_std_read)
chainlib.eth.cli.argflag_std_read
)
sub = self.argparser.add_subparsers() sub = self.argparser.add_subparsers()
sub.dest = "command" sub.dest = 'command'
sub_user = sub.add_parser( sub_user = sub.add_parser('user', aliases=['u'], help='retrieve transactions for a user')
"user", aliases=["u"], help="retrieve transactions for a user"
)
cmd_user.process_args(sub_user) cmd_user.process_args(sub_user)
sub_tag = sub.add_parser( sub_tag = sub.add_parser('tag', aliases=['t'], help='locally assign a display value to an identifier')
"tag", aliases=["t"], help="locally assign a display value to an identifier"
)
cmd_tag.process_args(sub_tag) cmd_tag.process_args(sub_tag)
self.cmd_args = self.argparser.parse_args(argv) self.cmd_args = self.argparser.parse_args(argv)
def module(self): def module(self):
self.cmd_string = self.cmd_args.command self.cmd_string = self.cmd_args.command
cmd_string_translate = self.__cmd_alias.get(self.cmd_string) cmd_string_translate = self.__cmd_alias.get(self.cmd_string)
@ -106,12 +102,13 @@ class CmdCtrl:
self.cmd_string = cmd_string_translate self.cmd_string = cmd_string_translate
if self.cmd_string == None: if self.cmd_string == None:
self.cmd_string = "none" self.cmd_string = 'none'
modname = "clicada.cli.{}".format(self.cmd_string) modname = 'clicada.cli.{}'.format(self.cmd_string)
self.logger.debug("using module {}".format(modname)) self.logger.debug('using module {}'.format(modname))
self.cmd_mod = importlib.import_module(modname) self.cmd_mod = importlib.import_module(modname)
def logging(self, logger): def logging(self, logger):
self.logger = logger self.logger = logger
if self.logger == None: if self.logger == None:
@ -121,74 +118,73 @@ class CmdCtrl:
elif self.cmd_args.v: elif self.cmd_args.v:
self.logger.setLevel(logging.INFO) self.logger.setLevel(logging.INFO)
def config(self): def config(self):
override_dir = self.cmd_args.config override_dir = self.cmd_args.config
if override_dir == None: if override_dir == None:
p = os.environ.get("HOME") p = os.environ.get('HOME')
if p != None: if p != None:
p = os.path.join(p, ".config", "cic", "clicada") p = os.path.join(p, '.config', 'cic', 'clicada')
try: try:
os.stat(p) os.stat(p)
override_dir = p override_dir = p
logg.info( logg.info('applying user config override from standard location: {}'.format(p))
"applying user config override from standard location: {}".format(
p
)
)
except FileNotFoundError: except FileNotFoundError:
pass pass
extra_args = self.cmd_mod.extra_args() extra_args = self.cmd_mod.extra_args()
self.config = chainlib.eth.cli.Config.from_args( self.config = chainlib.eth.cli.Config.from_args(self.cmd_args, base_config_dir=base_config_dir, extra_args=extra_args, default_config_dir=override_dir)
self.cmd_args,
base_config_dir=base_config_dir,
extra_args=extra_args,
default_config_dir=override_dir,
)
self.config.add(False, "_SEQ") self.config.add(False, '_SEQ')
self.config.censor("AUTH_PASSPHRASE") self.config.censor('AUTH_PASSPHRASE')
self.logger.debug('loaded config:\n{}'.format(self.config))
self.logger.debug("loaded config:\n{}".format(self.config))
def auth(self): def auth(self):
typ = self.get("AUTH_TYPE") typ = self.get('AUTH_TYPE')
if typ != "gnupg": if typ != 'gnupg':
raise NotImplementedError("Valid aut implementations are: gnupg") raise NotImplementedError('Valid aut implementations are: gnupg')
default_auth_db_path = None default_auth_db_path = None
if os.environ.get("HOME") != None: if os.environ.get('HOME') != None:
default_auth_db_path = os.path.join( default_auth_db_path = os.path.join(os.environ['HOME'], '.local/share/cic/clicada')
os.environ["HOME"], ".local/share/cic/clicada" auth_db_path = self.get('AUTH_DB_PATH', default_auth_db_path)
) self.__auth = PGPAuthCrypt(auth_db_path, self.get('AUTH_KEY'), self.get('AUTH_KEYRING_PATH'))
auth_db_path = self.get("AUTH_DB_PATH", default_auth_db_path) self.__auth.get_secret(self.get('AUTH_PASSPHRASE'))
self.__auth = PGPAuthCrypt(
auth_db_path, self.get("AUTH_KEY"), self.get("AUTH_KEYRING_PATH")
)
self.__auth.get_secret(self.get("AUTH_PASSPHRASE"))
self.encrypter = AESCTREncrypt(auth_db_path, self.__auth.secret) self.encrypter = AESCTREncrypt(auth_db_path, self.__auth.secret)
def get(self, k, default=None): def get(self, k, default=None):
r = self.config.get(k, default) r = self.config.get(k, default)
if k in [ if k in [
"_FORCE", '_FORCE',
]: '_FORCE_ALL',
'_RAW_TX',
'_NO_RESOLVE',
'_NO_TX',
]:
if r == None: if r == None:
return False return False
return self.config.true(k) return self.config.true(k)
return r return r
def chain(self): def chain(self):
return self.chain_spec return self.chain_spec
def conn(self): def conn(self):
return self.__conn return self.__conn
def execute(self): def execute(self):
self.cmd_mod.execute(self) self.cmd_mod.execute(self)
def opener(self, k): def opener(self, k):
return self.remote_openers[k] return self.remote_openers[k]
def notifier(self): def notifier(self):
if logg.root.level >= logging.WARNING: if logg.root.level >= logging.WARNING:
logging.disable() logging.disable()
@ -196,12 +192,18 @@ class CmdCtrl:
else: else:
self.writer = NullWriter() self.writer = NullWriter()
def notify(self, v): def notify(self, v):
if logg.root.level <= logging.INFO: self.writer.notify(v)
print("\033[96m" + v + "\033[0m")
def ouch(self, v): def ouch(self, v):
print("\033[91m" + v + "\033[0m") self.writer.ouch(v)
print()
def write(self, v): def write(self, v):
print(v) self.writer.write("")
self.writer.write(v)
print()

View File

@ -3,8 +3,7 @@ import json
# external imports # external imports
from clicada.user import FileUserStore from clicada.user import FileUserStore
from pathlib import Path
import os
categories = [ categories = [
'phone', 'phone',
@ -36,7 +35,7 @@ def validate(config, args):
def execute(ctrl): def execute(ctrl):
store_path = os.path.join(str(Path.home()), '.clicada') store_path = '.clicada'
user_store = FileUserStore(None, ctrl.chain(), ctrl.get('_CATEGORY'), store_path, int(ctrl.get('FILESTORE_TTL'))) user_store = FileUserStore(None, ctrl.chain(), ctrl.get('_CATEGORY'), store_path, int(ctrl.get('FILESTORE_TTL')))
user_store.put(ctrl.get('_IDENTIFIER'), json.dumps(ctrl.get('_TAG')), force=True) user_store.put(ctrl.get('_IDENTIFIER'), json.dumps(ctrl.get('_TAG')), force=True)
user_store.stick(ctrl.get('_IDENTIFIER')) user_store.stick(ctrl.get('_IDENTIFIER'))

View File

@ -1,24 +1,35 @@
# standard imports # standard imports
import datetime
import logging
import os
import sys import sys
from pathlib import Path import logging
import datetime
from chainlib.encode import TxHexNormalizer from queue import SimpleQueue as Queue
from chainlib.eth.address import is_address, to_checksum_address
# external imports # external imports
from cic_eth_registry import CICRegistry from cic_eth_registry import CICRegistry
from cic_eth_registry.lookup.tokenindex import TokenIndexLookup from cic_eth_registry.lookup.tokenindex import TokenIndexLookup
from cic_types.models.person import Person from cic_types.models.person import Person
from clicada.error import MetadataNotFoundError from chainlib.eth.address import to_checksum_address
from clicada.token import FileTokenStore, token_balance from chainlib.encode import TxHexNormalizer
from hexathon import (
add_0x,
strip_0x,
)
# local imports # local imports
from clicada.tx import ResolvedTokenTx, TxGetter from clicada.tx import (
TxGetter,
FormattedTokenTx,
)
from clicada.user import FileUserStore from clicada.user import FileUserStore
from hexathon import add_0x from clicada.token import FileTokenStore
from clicada.tx import ResolvedTokenTx
from clicada.tx.file import FileTxStore
from clicada.error import MetadataNotFoundError
from clicada.cli.worker import (
MetadataResolverWorker,
TxResolverWorker,
TokenResolverWorker,
)
logg = logging.getLogger(__name__) logg = logging.getLogger(__name__)
@ -26,36 +37,34 @@ tx_normalizer = TxHexNormalizer()
def process_args(argparser): def process_args(argparser):
argparser.add_argument("-m", "--method", type=str, help="lookup method") argparser.add_argument('-m', '--method', type=str, help='lookup method')
argparser.add_argument( argparser.add_argument('--meta-url', dest='meta_url', type=str, help='Url to retrieve metadata from')
"--meta-url", dest="meta_url", type=str, help="Url to retrieve metadata from" argparser.add_argument('-f', '--force-update', dest='force_update', action='store_true', help='Update records of mutable entries')
) argparser.add_argument('-ff', '--force-update-all', dest='force_update_all', action='store_true', help='Update records of mutable entries and immutable entries')
argparser.add_argument( argparser.add_argument('-N', '--no-resolve', dest='no_resolve', action='store_true', help='Resolve no metadata')
"-f", argparser.add_argument('--no-tx', dest='no_tx', action='store_true', help='Do not fetch transactions')
"--force-update", argparser.add_argument('--raw-tx', dest='raw_tx', action='store_true', help='Also cache raw transaction data')
dest="force_update", argparser.add_argument('identifier', type=str, help='user identifier')
action="store_true",
help="Update records of mutable entries",
)
argparser.add_argument(
"identifier", type=str, help="user identifier (phone_number or address)"
)
def extra_args(): def extra_args():
return { return {
"force_update": "_FORCE", 'raw_tx': '_RAW_TX',
"method": "META_LOOKUP_METHOD", 'force_update': '_FORCE',
"meta_url": "META_URL", 'force_update_all': '_FORCE_ALL',
"identifier": "_IDENTIFIER", 'method': 'META_LOOKUP_METHOD',
} 'meta_url': 'META_URL',
'identifier': '_IDENTIFIER',
'no_resolve': '_NO_RESOLVE',
'no_tx': '_NO_TX',
}
def apply_args(config, args): def apply_args(config, args):
if config.get("META_LOOKUP_METHOD"): if config.get('META_LOOKUP_METHOD'):
raise NotImplementedError( raise NotImplementedError('Sorry, currently only "phone" lookup method is implemented')
'Sorry, currently only "phone" lookup method is implemented' if config.true('_FORCE_ALL'):
) config.add(True, '_FORCE', exists_ok=True)
def validate(config, args): def validate(config, args):
@ -63,103 +72,157 @@ def validate(config, args):
def execute(ctrl): def execute(ctrl):
tx_getter = TxGetter(ctrl.get("TX_CACHE_URL"), 10) tx_getter = TxGetter(ctrl.get('TX_CACHE_URL'), 50)
store_path = os.path.join(str(Path.home()), ".clicada") store_path = '.clicada'
user_phone_file_label = "phone" user_phone_file_label = 'phone'
user_phone_store = FileUserStore( user_phone_store = FileUserStore(ctrl.opener('meta'), ctrl.chain(), user_phone_file_label, store_path, int(ctrl.get('FILESTORE_TTL')), encrypter=ctrl.encrypter, notifier=ctrl)
ctrl.opener("meta"),
ctrl.chain(),
user_phone_file_label,
store_path,
int(ctrl.get("FILESTORE_TTL")),
encrypter=ctrl.encrypter,
)
identifier = ctrl.get("_IDENTIFIER") ctrl.notify('resolving identifier {} to wallet address'.format(ctrl.get('_IDENTIFIER')))
ctrl.notify("resolving identifier {} to wallet address".format(identifier)) user_address = user_phone_store.by_phone(ctrl.get('_IDENTIFIER'), update=ctrl.get('_FORCE'))
if is_address(identifier):
user_address = identifier
else:
user_address = user_phone_store.by_phone(identifier, update=ctrl.get("_FORCE"))
if user_address == None: if user_address == None:
ctrl.ouch("unknown identifier: {}\n".format(identifier)) ctrl.ouch('unknown identifier: {}\n'.format(ctrl.get('_IDENTIFIER')))
sys.exit(1) sys.exit(1)
try: try:
user_address = to_checksum_address(user_address) user_address = to_checksum_address(user_address)
except ValueError: except ValueError:
ctrl.ouch('invalid response "{}" for {}\n'.format(user_address, identifier)) ctrl.ouch('invalid response "{}" for {}\n'.format(user_address, ctrl.get('_IDENTIFIER')))
sys.exit(1) sys.exit(1)
logg.debug("loaded user address {} for {}".format(user_address, identifier)) logg.debug('loaded user address {} for {}'.format(user_address, ctrl.get('_IDENTIFIER')))
user_address_normal = tx_normalizer.wallet_address(user_address) user_address_normal = tx_normalizer.wallet_address(user_address)
ctrl.notify("retrieving txs for address {}".format(user_address_normal)) ctrl.write("""Results for lookup by phone {}:
Metadata:
Network address: {}""".format(
ctrl.get('_IDENTIFIER'),
add_0x(user_address),
)
)
if not ctrl.get('_NO_RESOLVE'):
token_store = FileTokenStore(ctrl.chain(), ctrl.conn(), 'token', store_path)
user_address_file_label = 'address'
user_address_store = FileUserStore(ctrl.opener('meta'), ctrl.chain(), user_address_file_label, store_path, int(ctrl.get('FILESTORE_TTL')), encrypter=ctrl.encrypter, notifier=ctrl)
ctrl.notify('resolving metadata for address {}'.format(user_address_normal))
try:
r = user_address_store.by_address(user_address_normal, update=ctrl.get('_FORCE'))
except MetadataNotFoundError as e:
ctrl.ouch('could not resolve metadata for user: {}'.format(e))
sys.exit(1)
ctrl.write(""" Chain: {}
Name: {}
Registered: {}
Gender: {}
Location: {}
Products: {}
Tags: {}""".format(
ctrl.chain().common_name(),
str(r),
datetime.datetime.fromtimestamp(r.date_registered).ctime(),
r.gender,
r.location['area_name'],
','.join(r.products),
','.join(r.tags),
)
)
if ctrl.get('_NO_TX'):
sys.exit(0)
raw_rpc = None
if ctrl.get('_RAW_TX'):
raw_rpc = ctrl.rpc
ctrl.notify('retrieving txs for address {}'.format(user_address_normal))
txs = tx_getter.get(user_address) txs = tx_getter.get(user_address)
token_store = FileTokenStore(ctrl.chain(), ctrl.conn(), "token", store_path) if ctrl.get('_NO_RESOLVE'):
for v in txs['data']:
tx = FormattedTokenTx.from_dict(v)
ctrl.write(tx)
sys.exit(0)
user_address_file_label = "address"
user_address_store = FileUserStore(
ctrl.opener("meta"),
ctrl.chain(),
user_address_file_label,
store_path,
int(ctrl.get("FILESTORE_TTL")),
encrypter=ctrl.encrypter,
)
r = None token_resolver_queue = Queue()
ctrl.write( token_result_queue = Queue()
f""" token_resolver_worker = TokenResolverWorker(user_address, ctrl, token_store, token_resolver_queue, token_result_queue)
Phone: {ctrl.get("_IDENTIFIER")} token_resolver_worker.start()
Network address: {add_0x(user_address)}
Chain: {ctrl.chain().common_name()}""" wallets = []
) for tx in txs['data']:
ctrl.notify("resolving metadata for address {}".format(user_address_normal)) token_resolver_queue.put_nowait(tx['source_token'])
try: token_resolver_queue.put_nowait(tx['destination_token'])
r = user_address_store.by_address( if tx['sender'] not in wallets:
user_address_normal, update=ctrl.get("_FORCE") logg.info('adding wallet {} to metadata lookup'.format(tx['sender']))
) wallets.append(tx['sender'])
if r: if tx['recipient'] not in wallets:
ctrl.write( wallets.append(tx['recipient'])
f""" logg.info('registered wallet {} for metadata lookup'.format(tx['recipient']))
Name: { str(r)}
Registered: {datetime.datetime.fromtimestamp(r.date_registered).ctime()} wallet_threads = []
Gender: {r.gender} for a in wallets:
Location: {r.location["area_name"]} thread_wallet = MetadataResolverWorker(a, ctrl, user_address_store)
Products: {",".join(r.products)} thread_wallet.start()
Tags: {",".join(r.tags)}""" wallet_threads.append(thread_wallet)
)
except MetadataNotFoundError as e: ctrl.notify('wait for metadata resolvers to finish work')
ctrl.ouch(f"MetadataNotFoundError: Could not resolve metadata for user {e}\n") for t in wallet_threads:
t.join()
tx_store = FileTxStore(store_path, rpc=raw_rpc, notifier=ctrl)
tx_threads = []
tx_queue = Queue()
tx_n = 0
for tx_src in txs['data']:
tx_hash = strip_0x(tx_src['tx_hash'])
tx_worker = TxResolverWorker(tx_hash, tx_src, ctrl, tx_store, token_store, user_address_store, token_resolver_queue, tx_queue, show_decimals=True, update=ctrl.get('_FORCE'))
tx_thread = tx_worker.start()
tx_threads.append(tx_worker)
tx_n += 1
tx_buf = {}
for i in range(0, tx_n):
tx = tx_queue.get()
if tx == None:
break
# ugh, ugly
#k = float('{}.{}'.format(tx.block_number, tx.tx_index))
# tx_index is missing, this is temporary sort measure
k = str(tx.block_number) + '.' + tx.tx_hash
tx_buf[k] = tx
ctrl.notify('wait for transaction getters to finish work')
for tx_thread in tx_threads:
tx_thread.join()
token_resolver_queue.put_nowait(None)
token_buf = ''
while True:
l = token_result_queue.get()
if l == None:
break
token_buf += ' {} {}\n'.format(l[0], l[1])
ctrl.notify('wait for token resolver to finish work')
token_resolver_worker.join()
ctrl.write('')
ctrl.write("Balances:")
ctrl.write(token_buf)
ks = list(tx_buf.keys())
ks.sort()
ks.reverse()
for k in ks:
ctrl.write(tx_buf[k])
tx_lines = []
seen_tokens = {}
for tx_src in txs["data"]:
ctrl.notify("resolve details for tx {}".format(tx_src["tx_hash"]))
tx = ResolvedTokenTx.from_dict(tx_src)
tx.resolve(
token_store,
user_address_store,
show_decimals=True,
update=ctrl.get("_FORCE"),
)
tx_lines.append(tx)
seen_tokens[tx.source_token_label] = tx.source_token
seen_tokens[tx.destination_token_label] = tx.destination_token
for k in seen_tokens.keys():
ctrl.notify("resolve token {}".format(seen_tokens[k]))
(token_symbol, token_decimals) = token_store.by_address(seen_tokens[k])
ctrl.notify(
"get token balance for {} => {}".format(token_symbol, seen_tokens[k])
)
balance = token_balance(ctrl.chain(), ctrl.conn(), seen_tokens[k], user_address)
fmt = "{:." + str(token_decimals) + "f}"
decimal_balance = fmt.format(balance / (10**token_decimals))
ctrl.write("Balances:\n {} {}".format(token_symbol, decimal_balance))
print()
for l in tx_lines:
ctrl.write(l)

87
clicada/cli/worker.py Normal file
View File

@ -0,0 +1,87 @@
# standard imports
import threading
import logging
# external imports
from hexathon import strip_0x
# local imports
from clicada.tx import ResolvedTokenTx
from clicada.token import token_balance
from clicada.error import MetadataNotFoundError
logg = logging.getLogger(__name__)
class MetadataResolverWorker(threading.Thread):
def __init__(self, wallet_address, ctrl, user_address_store):
self.user_address_store = user_address_store
self.wallet_address = wallet_address
self.ctrl = ctrl
super(MetadataResolverWorker, self).__init__()
def run(self):
self.ctrl.notify('resolve metadata for {}'.format(self.wallet_address))
try:
self.user_address_store.by_address(self.wallet_address)
except MetadataNotFoundError:
logg.info('failed metadata lookup for {}'.format(self.wallet_address))
class TxResolverWorker(threading.Thread):
def __init__(self, tx_hash, tx_src, ctrl, tx_store, token_store, user_address_store, token_queue, tx_queue, show_decimals=True, update=None):
self.tx_hash = tx_hash
self.tx_src = tx_src
self.ctrl = ctrl
self.token_store = token_store
self.user_address_store = user_address_store
self.show_decimals = show_decimals
self.update = update
self.token_queue = token_queue
self.tx_store = tx_store
self.tx_queue = tx_queue
super(TxResolverWorker, self).__init__()
def run(self):
self.ctrl.notify('resolve details for tx {}'.format(self.tx_hash))
tx = ResolvedTokenTx.from_dict(self.tx_src)
tx.resolve(self.token_store, self.user_address_store, show_decimals=self.show_decimals, update=self.update, lookup=False)
self.tx_store.put(self.tx_hash, str(self.tx_src), overwrite=self.ctrl.get('_FORCE_ALL'))
self.tx_queue.put(tx)
class TokenResolverWorker(threading.Thread):
def __init__(self, wallet_address, ctrl, token_store, in_queue, out_queue):
super(TokenResolverWorker, self).__init__()
self.ctrl = ctrl
self.token_store = token_store
self.in_queue = in_queue
self.out_queue = out_queue
self.seen_tokens = {}
self.wallet_address = strip_0x(wallet_address)
def run(self):
while True:
token_address = self.in_queue.get()
if token_address == None:
logg.debug('token resolver end')
self.out_queue.put_nowait(None)
return
token_address = strip_0x(token_address)
if self.seen_tokens.get(token_address) != None:
continue
logg.debug('resolve token {}'.format(token_address))
self.ctrl.notify('resolve token {}'.format(token_address))
(token_symbol, token_decimals) = self.token_store.by_address(token_address)
self.seen_tokens[token_address] = token_address
self.ctrl.notify('get token balance for {} => {}'.format(token_symbol, self.seen_tokens[token_address]))
balance = token_balance(self.ctrl.chain(), self.ctrl.conn(), self.seen_tokens[token_address], self.wallet_address)
fmt = '{:.' + str(token_decimals) + 'f}'
decimal_balance = fmt.format(balance / (10 ** token_decimals))
logg.debug('token balance for {} ({}) is {}'.format(token_symbol, token_address, decimal_balance))
self.out_queue.put((token_symbol, decimal_balance,))

View File

@ -52,7 +52,7 @@ class FileTokenStore:
return p return p
def by_address(self, address): def by_address(self, address, update=False, lookup=True):
address = tx_normalize.executable_address(address) address = tx_normalize.executable_address(address)
token_symbol = self.memstore_symbol.get(address) token_symbol = self.memstore_symbol.get(address)
@ -65,17 +65,30 @@ class FileTokenStore:
try: try:
f = open(p, 'r') f = open(p, 'r')
except FileNotFoundError: except FileNotFoundError:
p = self.__cache_token(address) pass
if f == None:
if not lookup:
token_symbol = '???'
token_decimals = '???'
#self.memstore_symbol.put(address, token_symbol)
#self.memstore_decimals.put(address, token_decimals)
#logg.warning('token metadata not found and lookup deactivated. Will use 18 decimals as default')
#return (token_symbol, token_decimals,)
if token_symbol == None:
if f == None:
p = self.__cache_token(address)
f = open(p, 'r')
token_symbol = f.read()
f.close()
p = os.path.join(self.store_path, token_symbol)
f = open(p, 'r') f = open(p, 'r')
r = f.read()
token_symbol = f.read() f.close()
f.close() token_decimals = int(r)
p = os.path.join(self.store_path, token_symbol)
f = open(p, 'r')
r = f.read()
f.close()
token_decimals = int(r)
self.memstore_symbol.put(address, token_symbol) self.memstore_symbol.put(address, token_symbol)
self.memstore_decimals.put(token_symbol, token_decimals) self.memstore_decimals.put(token_symbol, token_decimals)

45
clicada/tx/file.py Normal file
View File

@ -0,0 +1,45 @@
# standard imports
import os
import logging
# external imports
from chainlib.eth.tx import transaction
from leveldir.numeric import NumDir
from leveldir.hex import HexDir
from hexathon import strip_0x
logg = logging.getLogger(__name__)
class FileTxStore:
subdivision = 100000
def __init__(self, store_base_path, rpc=None, notifier=None):
tx_base_path = os.path.join(store_base_path, 'tx')
num_base_path = os.path.join(tx_base_path, 'blocks')
hash_base_path = os.path.join(tx_base_path, 'hash')
raw_base_path = os.path.join(tx_base_path, 'raw')
self.block_index_dir = NumDir(num_base_path)
self.hash_index_dir = HexDir(hash_base_path, 32)
self.raw_index_dir = HexDir(raw_base_path, 32)
self.rpc = rpc
self.notifier = notifier
def put(self, k, v, overwrite=False):
if self.notifier != None:
self.notifier.notify('caching tx data for {}'.format(k))
hsh = bytes.fromhex(k)
if not overwrite and self.hash_index_dir.have(k):
logg.debug('tx store already has {}'.format(k))
return
self.hash_index_dir.add(hsh, v.encode('utf-8'))
if self.rpc != None:
self.notifier.notify('retrieve and cache raw tx data for {}'.format(k))
o = transaction(k)
r = self.rpc.conn.do(o)
raw = bytes.fromhex(strip_0x(r['raw']))
self.raw_index_dir.add(hsh, raw)

View File

@ -16,11 +16,32 @@ from clicada.error import (
ExpiredRecordError, ExpiredRecordError,
MetadataNotFoundError, MetadataNotFoundError,
) )
from chainlib.eth.address import AddressChecksum
logg = logging.getLogger(__name__) logg = logging.getLogger(__name__)
address_checksummer = AddressChecksum()
class FormattedTokenTx(TokenTx):
def __init__(self):
super(FormattedTokenTx, self).__init__()
self.symmetric = True
def __str__(self):
if self.symmetric:
return '{} {} => {} {} {}'.format(
self.date_block_label,
self.sender_label,
self.recipient_label,
self.destination_token_label,
self.to_value_label,
)
class ResolvedTokenTx(FormattedTokenTx):
class ResolvedTokenTx(TokenTx):
def __init__(self): def __init__(self):
super(ResolvedTokenTx, self).__init__() super(ResolvedTokenTx, self).__init__()
@ -28,13 +49,12 @@ class ResolvedTokenTx(TokenTx):
self.destination_token_name = None self.destination_token_name = None
self.source_token_decimals = None self.source_token_decimals = None
self.destination_token_decimals = None self.destination_token_decimals = None
self.symmetric = True
self.sender_entity = None self.sender_entity = None
self.recipient_entity = None self.recipient_entity = None
def resolve_tokens(self, token_store, show_decimals=False, update=False): def resolve_tokens(self, token_store, show_decimals=False, update=False, lookup=False):
(token_symbol, token_decimals) = token_store.by_address(self.source_token) (token_symbol, token_decimals) = token_store.by_address(self.source_token, lookup=False)
self.source_token_decimals = token_decimals self.source_token_decimals = token_decimals
self.source_token_label = token_symbol self.source_token_label = token_symbol
token_value = self.to_value / (10 ** token_decimals) token_value = self.to_value / (10 ** token_decimals)
@ -59,45 +79,34 @@ class ResolvedTokenTx(TokenTx):
self.to_value_label = fmt.format(token_value) self.to_value_label = fmt.format(token_value)
def resolve_entity(self, user_store, address): def resolve_entity(self, user_store, address, update=False, lookup=True):
try: try:
r = user_store.by_address(address) r = user_store.by_address(address, update=update, lookup=lookup)
except MetadataNotFoundError: except MetadataNotFoundError:
return address return address_checksummer.sum(address)
return str(r) return str(r)
def resolve_sender_entity(self, user_store, update=False): def resolve_sender_entity(self, user_store, update=False, lookup=True):
if self.tx_type == TokenTxType.faucet_giveto.value: if self.tx_type == TokenTxType.faucet_giveto.value:
return 'FAUCET' return 'FAUCET'
return self.resolve_entity(user_store, self.sender) return self.resolve_entity(user_store, self.sender, update=update, lookup=lookup)
def resolve_recipient_entity(self, user_store, update=False): def resolve_recipient_entity(self, user_store, update=False, lookup=True):
return self.resolve_entity(user_store, self.recipient) return self.resolve_entity(user_store, self.recipient, update=update, lookup=lookup)
def resolve_entities(self, user_store, update=False): def resolve_entities(self, user_store, update=False, lookup=True):
self.sender_label = self.resolve_sender_entity(user_store, update=update) self.sender_label = self.resolve_sender_entity(user_store, update=update, lookup=lookup)
self.recipient_label = self.resolve_recipient_entity(user_store, update=update) self.recipient_label = self.resolve_recipient_entity(user_store, update=update, lookup=lookup)
def resolve(self, token_store, user_store, show_decimals=False, update=False): def resolve(self, token_store, user_store, show_decimals=False, update=False, lookup=True):
self.resolve_tokens(token_store, show_decimals, update=update) self.resolve_tokens(token_store, show_decimals, update=update, lookup=lookup)
self.resolve_entities(user_store, update=update) self.resolve_entities(user_store, update=update)
def __str__(self):
if self.symmetric:
return '{} {} => {} {} {}'.format(
self.date_block_label,
self.sender_label,
self.recipient_label,
self.destination_token_label,
self.to_value_label,
)
class TxGetter: class TxGetter:
def __init__(self, cache_url, limit=0): def __init__(self, cache_url, limit=0):

View File

@ -12,7 +12,7 @@ from cic_types.condiments import MetadataPointer
from cic_types.models.person import Person from cic_types.models.person import Person
from cic_types.ext.requests import make_request from cic_types.ext.requests import make_request
from cic_types.processor import generate_metadata_pointer from cic_types.processor import generate_metadata_pointer
from requests.exceptions import HTTPError import requests.exceptions
import phonenumbers import phonenumbers
# local imports # local imports
@ -65,7 +65,7 @@ class Account(Person):
class FileUserStore: class FileUserStore:
def __init__(self, metadata_opener, chain_spec, label, store_base_path, ttl, encrypter=None): def __init__(self, metadata_opener, chain_spec, label, store_base_path, ttl, encrypter=None, notifier=None):
invalidate_before = datetime.datetime.now() - datetime.timedelta(seconds=ttl) invalidate_before = datetime.datetime.now() - datetime.timedelta(seconds=ttl)
self.invalidate_before = int(invalidate_before.timestamp()) self.invalidate_before = int(invalidate_before.timestamp())
self.have_xattr = False self.have_xattr = False
@ -83,6 +83,7 @@ class FileUserStore:
self.metadata_opener = metadata_opener self.metadata_opener = metadata_opener
self.failed_entities = {} self.failed_entities = {}
self.encrypter = encrypter self.encrypter = encrypter
self.notifier = notifier
def __validate_dir(self): def __validate_dir(self):
@ -191,7 +192,6 @@ class FileUserStore:
if self.encrypter != None: if self.encrypter != None:
v = self.encrypter.decrypt(k, v) v = self.encrypter.decrypt(k, v)
logg.debug('>>>>>>>>>>>>< v decoded {}'.format(v))
v = v.decode('utf-8') v = v.decode('utf-8')
logg.debug('retrieved {} from {}'.format(k, p)) logg.debug('retrieved {} from {}'.format(k, p))
@ -215,6 +215,8 @@ class FileUserStore:
logg.info(e) logg.info(e)
pass pass
self.notifier.notify('wallet address for phone {} not found locally, retrieve from metadata service'.format(phone))
getter = self.metadata_opener getter = self.metadata_opener
ptr = generate_metadata_pointer(phone.encode('utf-8'), MetadataPointer.PHONE) ptr = generate_metadata_pointer(phone.encode('utf-8'), MetadataPointer.PHONE)
r = None r = None
@ -222,7 +224,7 @@ class FileUserStore:
try: try:
r = getter.open(ptr) r = getter.open(ptr)
user_address = json.loads(r) user_address = json.loads(r)
except HTTPError as e: except requests.exceptions.HTTPError as e:
logg.debug('no address found for phone {}: {}'.format(phone, e)) logg.debug('no address found for phone {}: {}'.format(phone, e))
return None return None
@ -239,13 +241,12 @@ class FileUserStore:
return person_data return person_data
def by_address(self, address, update=False): def by_address(self, address, update=False, lookup=True):
address = tx_normalize.wallet_address(address) address = tx_normalize.wallet_address(address)
address = strip_0x(address) address = strip_0x(address)
#if self.failed_entities.get(address):
if self.is_dud(address): if self.is_dud(address):
logg.debug('already tried and failed {}, skipping'.format(address)) logg.debug('already tried and failed {}, skipping'.format(address))
return address raise MetadataNotFoundError()
ignore_expired = self.sticky(address) ignore_expired = self.sticky(address)
@ -260,6 +261,8 @@ class FileUserStore:
logg.info(e) logg.info(e)
pass pass
self.notifier.notify('metadata for wallet {} not found locally, retrieve from metadata service'.format(address))
getter = self.metadata_opener getter = self.metadata_opener
ptr = generate_metadata_pointer(bytes.fromhex(address), MetadataPointer.PERSON) ptr = generate_metadata_pointer(bytes.fromhex(address), MetadataPointer.PERSON)
@ -269,7 +272,7 @@ class FileUserStore:
except Exception as e: except Exception as e:
logg.debug('no metadata found for {}: {}'.format(address, e)) logg.debug('no metadata found for {}: {}'.format(address, e))
if not r: if r == None:
self.failed_entities[address] = True self.failed_entities[address] = True
raise MetadataNotFoundError() raise MetadataNotFoundError()
@ -277,6 +280,8 @@ class FileUserStore:
person = Account() person = Account()
person_data = person.deserialize(person_data=data) person_data = person.deserialize(person_data=data)
logg.debug('wallet {} resolved to {}'.format(address, str(person)))
self.notifier.notify('wallet {} resolved to {}, retrieve extended metadata from metadata service'.format(address, str(person)))
ptr = generate_metadata_pointer(bytes.fromhex(address), MetadataPointer.CUSTOM) ptr = generate_metadata_pointer(bytes.fromhex(address), MetadataPointer.CUSTOM)
r = None r = None
try: try:

View File

@ -1,10 +1,11 @@
usumbufu~=0.3.8 usumbufu~=0.3.5
confini~=0.6.0 confini~=0.5.3
cic-eth-registry~=0.6.9 cic-eth-registry~=0.6.1
cic-types~=0.2.2 cic-types~=0.2.1a8
phonenumbers==8.12.12 phonenumbers==8.12.12
eth-erc20~=0.3.0 eth-erc20~=0.1.2
hexathon~=0.1.0 hexathon~=0.1.0
pycryptodome~=3.10.1 pycryptodome~=3.10.1
chainlib-eth~=0.1.0 chainlib-eth~=0.0.21
chainlib~=0.1.0 chainlib~=0.0.17
leveldir~=0.1.0

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
name = clicada name = clicada
version = 0.1.3 version = 0.0.7rc1
description = CLI CRM tool for the cic-stack custodial wallet system description = CLI CRM tool for the cic-stack custodial wallet system
author = Louis Holbrook author = Louis Holbrook
author_email = dev@holbrook.no author_email = dev@holbrook.no