13 Commits

Author SHA1 Message Date
lash
ca059c5b30 Bump chaind dep 2022-05-14 16:23:33 +00:00
lash
30172798c0 Bump minor version 2022-05-14 16:20:09 +00:00
lash
df02af8b6d Load config for syncer executable 2022-05-14 16:15:02 +00:00
lash
0a44ceb364 Add session cli arg handling to send, queuer 2022-05-14 12:41:28 +00:00
lash
f49e4149fd Rehabilitate send 2022-05-14 12:25:02 +00:00
lash
7c0df3b967 Rehabilitate queuer and syncer cli 2022-05-13 13:47:58 +00:00
lash
acbbebd8da WIP implement chaind-eth on chainlib 0.3.0 structure 2022-05-13 10:32:28 +00:00
lash
96f58c5f41 Bump version 2022-05-09 20:21:44 +00:00
lash
0707291f8f Upgrade chainlib to 0.2.0 2022-05-09 19:54:17 +00:00
lash
ed7f0a3f71 Upgrade chaind to handle exception in filesystem shep list 2022-05-05 15:45:59 +00:00
lash
2fc3d4da0c Single adapter instantiation 2022-05-05 15:08:51 +00:00
lash
5fc27671da Upgrade chaind 2022-05-05 08:10:38 +00:00
lash
8f86e9c970 Implement unsafe address for send 2022-05-04 07:26:17 +00:00
10 changed files with 255 additions and 116 deletions

View File

@@ -21,7 +21,6 @@ class Normalizer(TxHexNormalizer, NoopNormalizer):
hexathon.to_int(v) hexathon.to_int(v)
eth_normalizer = Normalizer() eth_normalizer = Normalizer()

View File

@@ -4,3 +4,6 @@ runtime_dir =
id = id =
data_dir = data_dir =
dispatch_delay = 0.01 dispatch_delay = 0.01
[tx]
digest_size = 32

View File

@@ -5,8 +5,23 @@ import signal
# external imports # external imports
import chainlib.eth.cli import chainlib.eth.cli
import chaind.cli from chainlib.eth.cli.arg import (
import chainqueue.cli Arg,
ArgFlag,
process_args,
)
from chainlib.eth.cli.config import (
Config,
process_config,
)
from chainqueue.cli.arg import (
apply_arg as apply_arg_queue,
apply_flag as apply_flag_queue,
)
from chaind.cli.arg import (
apply_arg,
apply_flag,
)
from chaind.session import SessionController from chaind.session import SessionController
from chaind.setup import Environment from chaind.setup import Environment
from chaind.error import ( from chaind.error import (
@@ -29,11 +44,21 @@ from chainlib.encode import TxHexNormalizer
from chainlib.chain import ChainSpec from chainlib.chain import ChainSpec
from chaind.adapters.fs import ChaindFsAdapter from chaind.adapters.fs import ChaindFsAdapter
from chaind.dispatch import DispatchProcessor from chaind.dispatch import DispatchProcessor
from chainqueue.data import config_dir as chainqueue_config_dir
from chaind.data import config_dir as chaind_config_dir
from chainlib.eth.cli.log import process_log
from chaind.cli.config import process_config as process_config_local
# local imports # local imports
from chaind.eth.cache import EthCacheTx from chaind.eth.cache import EthCacheTx
from chaind.eth.settings import ChaindEthSettings from chaind.eth.settings import ChaindSettings
from chaind.eth.dispatch import EthDispatcher from chaind.eth.dispatch import EthDispatcher
from chaind.eth.settings import process_settings
from chaind.settings import (
process_queue,
process_socket,
process_dispatch,
)
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger() logg = logging.getLogger()
@@ -43,37 +68,42 @@ config_dir = os.path.join(script_dir, '..', 'data', 'config')
env = Environment(domain='eth', env=os.environ) env = Environment(domain='eth', env=os.environ)
arg_flags = chainlib.eth.cli.argflag_std_read arg_flags = ArgFlag()
argparser = chainlib.eth.cli.ArgumentParser(arg_flags) arg_flags = apply_flag_queue(arg_flags)
arg_flags = apply_flag(arg_flags)
queue_arg_flags = 0 arg = Arg(arg_flags)
chainqueue.cli.process_flags(argparser, queue_arg_flags) arg = apply_arg_queue(arg)
arg = apply_arg(arg)
local_arg_flags = chaind.cli.argflag_local_base | chaind.cli.ChaindFlag.DISPATCH | chaind.cli.ChaindFlag.SOCKET flags = arg_flags.STD_READ | arg_flags.QUEUE | arg_flags.STATE | arg_flags.SESSION
chaind.cli.process_flags(argparser, local_arg_flags)
argparser = chainlib.eth.cli.ArgumentParser()
argparser = process_args(argparser, arg, flags)
args = argparser.parse_args() args = argparser.parse_args()
base_config_dir = [chainqueue.cli.config_dir, chaind.cli.config_dir] logg = process_log(args, logg)
config = chainlib.eth.cli.Config.from_args(args, arg_flags, base_config_dir=base_config_dir)
config = chaind.cli.process_config(config, args, local_arg_flags) config = Config()
config = chainqueue.cli.process_config(config, args, queue_arg_flags) config.add_schema_dir(chainqueue_config_dir)
config.add_schema_dir(chaind_config_dir)
config = process_config(config, arg, args, flags)
config = process_config_local(config, arg, args, flags)
config.add('eth', 'CHAIND_ENGINE', False) config.add('eth', 'CHAIND_ENGINE', False)
config.add('queue', 'CHAIND_COMPONENT', False) config.add('sync', 'CHAIND_COMPONENT', False)
logg.debug('config loaded:\n{}'.format(config)) logg.debug('config loaded:\n{}'.format(config))
settings = ChaindEthSettings(include_queue=True) settings = ChaindSettings(include_sync=True)
settings.process(config) settings = process_settings(settings, config)
settings = process_queue(settings, config)
logg.debug('settings:\n{}'.format(settings)) settings = process_socket(settings, config)
settings = process_dispatch(settings, config)
rpc = chainlib.eth.cli.Rpc() logg.debug('settings loaded:\n{}'.format(settings))
conn = rpc.connect_by_config(config)
tx_normalizer = TxHexNormalizer().tx_hash tx_normalizer = TxHexNormalizer().tx_hash
token_cache_store = CacheTokenTx(settings.get('CHAIN_SPEC'), normalizer=tx_normalizer) token_cache_store = CacheTokenTx(settings.get('CHAIN_SPEC'), normalizer=tx_normalizer)
dispatcher = EthDispatcher(conn) dispatcher = EthDispatcher(settings.get('CONN'))
processor = DispatchProcessor(settings.get('CHAIN_SPEC'), settings.dir_for('queue'), dispatcher) processor = DispatchProcessor(settings.get('CHAIN_SPEC'), settings.dir_for('queue'), dispatcher)
ctrl = SessionController(settings, processor.process) ctrl = SessionController(settings, processor.process)
@@ -83,7 +113,18 @@ signal.signal(signal.SIGTERM, ctrl.shutdown)
logg.info('session id is ' + settings.get('SESSION_ID')) logg.info('session id is ' + settings.get('SESSION_ID'))
logg.info('session socket path is ' + settings.get('SESSION_SOCKET_PATH')) logg.info('session socket path is ' + settings.get('SESSION_SOCKET_PATH'))
def main(): def main():
global dispatcher, settings
queue_adapter = ChaindFsAdapter(
settings.get('CHAIN_SPEC'),
settings.dir_for('queue'),
EthCacheTx,
dispatcher,
store_sync=False,
)
while True: while True:
v = None v = None
client_socket = None client_socket = None
@@ -99,17 +140,10 @@ def main():
pass pass
if v == None: if v == None:
ctrl.process(conn) ctrl.process(settings.get('CONN'))
#queue_adapter = create_adapter(settings, dispatcher)
continue continue
queue_adapter = ChaindFsAdapter(
settings.get('CHAIN_SPEC'),
settings.dir_for('queue'),
EthCacheTx,
dispatcher,
store_sync=False,
)
result_data = None result_data = None
r = 0 # no error r = 0 # no error
try: try:

View File

@@ -10,15 +10,40 @@ import socket
# external imports # external imports
import chainlib.eth.cli import chainlib.eth.cli
import chainqueue.cli from chainlib.eth.cli.arg import (
import chaind.cli Arg,
ArgFlag,
process_args,
)
from chainlib.eth.cli.config import (
Config,
process_config,
)
from chaind.setup import Environment from chaind.setup import Environment
from chainlib.eth.gas import price from chainlib.eth.gas import price
from chainlib.chain import ChainSpec from chainlib.chain import ChainSpec
from hexathon import strip_0x from hexathon import strip_0x
from chainqueue.cli.arg import (
apply_arg as apply_arg_queue,
apply_flag as apply_flag_queue,
)
from chainqueue.data import config_dir as chainqueue_config_dir
from chaind.data import config_dir as chaind_config_dir
from chaind.cli.arg import (
apply_arg,
apply_flag,
)
from chainlib.eth.cli.log import process_log
from chaind.settings import process_queue
from chaind.settings import ChaindSettings
from chaind.error import TxSourceError
from chainlib.error import (
InitializationError,
SignerMissingException,
)
from chaind.cli.config import process_config as process_config_local
# local imports # local imports
from chaind.error import TxSourceError
from chaind.eth.token.process import Processor from chaind.eth.token.process import Processor
from chaind.eth.token.gas import GasTokenResolver from chaind.eth.token.gas import GasTokenResolver
from chaind.eth.cli.csv import CSVProcessor from chaind.eth.cli.csv import CSVProcessor
@@ -26,45 +51,56 @@ from chaind.eth.cli.output import (
Outputter, Outputter,
OpMode, OpMode,
) )
from chaind.eth.settings import ChaindEthSettings from chaind.eth.settings import process_settings
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger() logg = logging.getLogger()
arg_flags = chainlib.eth.cli.argflag_std_write
argparser = chainlib.eth.cli.ArgumentParser(arg_flags, arg_long={'-s': '--send-rpc'})
argparser.add_positional('source', required=False, type=str, help='Transaction source file')
local_arg_flags = chaind.cli.argflag_local_socket_client | chaind.cli.ChaindFlag.TOKEN def process_settings_local(settings, config):
chaind.cli.process_flags(argparser, local_arg_flags) # if settings.get('SIGNER') == None:
# raise SignerMissingException('signer missing')
return settings
chainqueue.cli.process_flags(argparser, 0)
args = argparser.parse_args()
env = Environment(domain='eth', env=os.environ) env = Environment(domain='eth', env=os.environ)
base_config_dir = [chaind.cli.config_dir, chainqueue.cli.config_dir] arg_flags = ArgFlag()
config = chainlib.eth.cli.Config.from_args(args, arg_flags, base_config_dir=base_config_dir) arg_flags = apply_flag_queue(arg_flags)
config = chainqueue.cli.process_config(config, args, 0) arg_flags = apply_flag(arg_flags)
config = chaind.cli.process_config(config, args, local_arg_flags)
arg = Arg(arg_flags)
arg = apply_arg_queue(arg)
arg = apply_arg(arg)
arg.set_long('s', 'send-rpc')
flags = arg_flags.STD_WRITE | arg_flags.TOKEN | arg_flags.SOCKET_CLIENT | arg_flags.STATE | arg_flags.WALLET | arg_flags.SESSION
argparser = chainlib.eth.cli.ArgumentParser()
argparser = process_args(argparser, arg, flags)
argparser.add_argument('source', help='Transaction source file')
args = argparser.parse_args()
logg = process_log(args, logg)
config = Config()
config.add_schema_dir(chainqueue_config_dir)
config.add_schema_dir(chaind_config_dir)
config = process_config(config, arg, args, flags)
config = process_config_local(config, arg, args, flags)
config.add(args.source, '_SOURCE', False) config.add(args.source, '_SOURCE', False)
config.add('eth', 'CHAIND_ENGINE', False)
config.add('queue', 'CHAIND_COMPONENT', False) config.add('queue', 'CHAIND_COMPONENT', False)
config.add('eth', 'CHAIND_ENGINE', False)
logg.debug('config loaded:\n{}'.format(config)) logg.debug('config loaded:\n{}'.format(config))
wallet = chainlib.eth.cli.Wallet() try:
wallet.from_config(config) settings = ChaindSettings(include_sync=True)
settings = process_settings(settings, config)
settings = ChaindEthSettings(include_queue=True) settings = process_queue(settings, config)
settings.process(config) settings = process_settings_local(settings, config)
except InitializationError as e:
logg.debug('settings:\n{}'.format(settings)) sys.stderr.write('Initialization error: ' + str(e) + '\n')
sys.exit(1)
rpc = chainlib.eth.cli.Rpc(wallet=wallet) logg.debug('settings loaded:\n{}'.format(settings))
conn = rpc.connect_by_config(config)
chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
mode = OpMode.STDOUT mode = OpMode.STDOUT
@@ -100,7 +136,14 @@ class SocketSender:
def send(self, tx): def send(self, tx):
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(self.path) err = None
try:
s.connect(self.path)
except FileNotFoundError as e:
err = e
if err != None:
s.close()
raise err
s.sendall(tx.encode('utf-8')) s.sendall(tx.encode('utf-8'))
r = s.recv(68) r = s.recv(68)
s.close() s.close()
@@ -108,6 +151,7 @@ class SocketSender:
def main(): def main():
conn = settings.get('CONN')
token_resolver = None token_resolver = None
if settings.get('TOKEN_MODULE') != None: if settings.get('TOKEN_MODULE') != None:
import importlib import importlib
@@ -116,9 +160,15 @@ def main():
else: else:
from chaind.eth.token.gas import GasTokenResolver from chaind.eth.token.gas import GasTokenResolver
m = GasTokenResolver m = GasTokenResolver
token_resolver = m(chain_spec, rpc.get_sender_address(), rpc.get_signer(), rpc.get_gas_oracle(), rpc.get_nonce_oracle()) token_resolver = m(
settings.get('CHAIN_SPEC'),
settings.get('SENDER_ADDRESS'),
settings.get('SIGNER'),
settings.get('GAS_ORACLE'),
settings.get('NONCE_ORACLE'),
)
processor = Processor(token_resolver, config.get('_SOURCE')) processor = Processor(token_resolver, config.get('_SOURCE'), use_checksum=not config.get('_UNSAFE'))
processor.add_processor(CSVProcessor()) processor.add_processor(CSVProcessor())
sends = None sends = None
@@ -143,7 +193,12 @@ def main():
break break
tx_hex = tx_bytes.hex() tx_hex = tx_bytes.hex()
if sender != None: if sender != None:
r = sender.send(tx_hex) r = None
try:
r = sender.send(tx_hex)
except FileNotFoundError as e:
sys.stderr.write('send to socket {} failed: {}\n'.format(sender.path, e))
sys.exit(1)
logg.info('sent {} result {}'.format(tx_hex, r)) logg.info('sent {} result {}'.format(tx_hex, r))
print(out.do(tx_hex)) print(out.do(tx_hex))

View File

@@ -3,9 +3,7 @@ import os
import logging import logging
# external imports # external imports
import chainlib.cli import chainlib.eth.cli
import chainsyncer.cli
import chaind.cli
from chaind.setup import Environment from chaind.setup import Environment
from chaind.filter import StateFilter from chaind.filter import StateFilter
from chainlib.eth.block import block_latest from chainlib.eth.block import block_latest
@@ -13,15 +11,38 @@ from hexathon import strip_0x
from chainsyncer.store.fs import SyncFsStore from chainsyncer.store.fs import SyncFsStore
from chainsyncer.driver.chain_interface import ChainInterfaceDriver from chainsyncer.driver.chain_interface import ChainInterfaceDriver
from chainsyncer.error import SyncDone from chainsyncer.error import SyncDone
from chainlib.eth.cli.arg import (
# local imports Arg,
from chaind.eth.settings import ChaindEthSettings ArgFlag,
process_args,
)
from chainlib.eth.cli.config import (
Config,
process_config,
)
from chainsyncer.cli.arg import (
apply_arg as apply_arg_sync,
apply_flag as apply_flag_sync,
)
from chainsyncer.data import config_dir as chainsyncer_config_dir
from chaind.data import config_dir as chaind_config_dir
from chaind.cli.arg import (
apply_arg,
apply_flag,
)
from chainlib.eth.cli.log import process_log
from chaind.settings import ChaindSettings
from chaind.cli.config import process_config as process_config_local
from chainsyncer.cli.config import process_config as process_config_syncer
# local imports # local imports
from chaind.eth.cache import EthCacheTx from chaind.eth.cache import EthCacheTx
from chaind.eth.settings import (
process_settings,
process_sync,
)
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger() logg = logging.getLogger()
script_dir = os.path.dirname(os.path.realpath(__file__)) script_dir = os.path.dirname(os.path.realpath(__file__))
@@ -29,44 +50,50 @@ config_dir = os.path.join(script_dir, '..', 'data', 'config')
env = Environment(domain='eth', env=os.environ) env = Environment(domain='eth', env=os.environ)
arg_flags = chainlib.cli.argflag_std_base | chainlib.cli.Flag.CHAIN_SPEC arg_flags = ArgFlag()
argparser = chainlib.cli.ArgumentParser(arg_flags) arg_flags = apply_flag_sync(arg_flags)
arg_flags = apply_flag(arg_flags)
local_arg_flags = chaind.cli.argflag_local_base arg = Arg(arg_flags)
chaind.cli.process_flags(argparser, local_arg_flags) arg = apply_arg_sync(arg)
arg = apply_arg(arg)
sync_flags = chainsyncer.cli.SyncFlag.RANGE | chainsyncer.cli.SyncFlag.HEAD flags = arg_flags.STD_BASE | arg_flags.CHAIN_SPEC | arg_flags.PROVIDER | arg_flags.SEQ | arg_flags.STATE
chainsyncer.cli.process_flags(argparser, sync_flags) flags = arg_flags.more(flags, arg_flags.SYNC_RANGE_EXT)
flags = arg_flags.more(flags, arg_flags.CHAIND_BASE)
argparser = chainlib.eth.cli.ArgumentParser()
argparser = process_args(argparser, arg, flags)
args = argparser.parse_args() args = argparser.parse_args()
base_config_dir = [ logg = process_log(args, logg)
chainsyncer.cli.config_dir,
chaind.cli.config_dir, config = Config()
] config.add_schema_dir(chainsyncer_config_dir)
config = chainlib.cli.Config.from_args(args, arg_flags, base_config_dir=base_config_dir) config.add_schema_dir(chaind_config_dir)
config = chainsyncer.cli.process_config(config, args, sync_flags) config = process_config(config, arg, args, flags)
config = chaind.cli.process_config(config, args, local_arg_flags) config = process_config_local(config, arg, args, flags)
config = process_config_syncer(config, arg, args, flags)
config.add('eth', 'CHAIND_ENGINE', False) config.add('eth', 'CHAIND_ENGINE', False)
config.add('sync', 'CHAIND_COMPONENT', False) config.add('sync', 'CHAIND_COMPONENT', False)
logg.debug('config loaded:\n{}'.format(config)) logg.debug('config loaded:\n{}'.format(config))
settings = ChaindEthSettings(include_sync=True) settings = ChaindSettings(include_sync=True)
settings.process(config) settings = process_settings(settings, config)
settings = process_sync(settings, config)
logg.debug('settings:\n{}'.format(settings)) logg.debug('settings loaded:\n{}'.format(settings))
def main(): def main():
fltr = StateFilter(settings.get('CHAIN_SPEC'), settings.dir_for('queue'), EthCacheTx) fltr = StateFilter(settings.get('CHAIN_SPEC'), settings.dir_for('queue'), EthCacheTx)
sync_store = SyncFsStore(settings.get('SESSION_DATA_DIR'), session_id=settings.get('SESSION_ID')) sync_store = SyncFsStore(settings.get('SESSION_DATA_PATH'), session_id=settings.get('SESSION_ID'))
sync_store.register(fltr) sync_store.register(fltr)
logg.debug('session block offset {}'.format(settings.get('SYNCER_OFFSET'))) logg.debug('session block offset {}'.format(settings.get('SYNCER_OFFSET')))
drv = ChainInterfaceDriver(sync_store, settings.get('SYNCER_INTERFACE'), offset=settings.get('SYNCER_OFFSET'), target=settings.get('SYNCER_LIMIT')) drv = ChainInterfaceDriver(sync_store, settings.get('SYNCER_INTERFACE'), offset=settings.get('SYNCER_OFFSET'), target=settings.get('SYNCER_LIMIT'))
try: try:
drv.run(settings.get('RPC')) drv.run(settings.get('CONN'))
except SyncDone as e: except SyncDone as e:
logg.info('sync done: {}'.format(e)) logg.info('sync done: {}'.format(e))

View File

@@ -1,18 +1,31 @@
# external imports # external imports
from chainlib.eth.connection import EthHTTPConnection from chainlib.eth.connection import EthHTTPConnection
from chaind.settings import ChaindSettings from chainlib.eth.settings import process_settings as base_process_settings
from chaind.eth.chain import EthChainInterface from chaind.eth.chain import EthChainInterface
from chaind.settings import *
from chainsyncer.settings import process_sync_range
class ChaindEthSettings(ChaindSettings): def process_common(settings, config):
rpc_provider = config.get('RPC_PROVIDER')
def process_sync_interface(self, config): if rpc_provider == None:
self.o['SYNCER_INTERFACE'] = EthChainInterface() rpc_provider = 'http://localhost:8545'
conn = EthHTTPConnection(url=rpc_provider, chain_spec=settings.get('CHAIN_SPEC'))
settings.set('RPC', conn)
return settings
def process_common(self, config): def process_sync(settings, config):
super(ChaindEthSettings, self).process_common(config) settings.set('SYNCER_INTERFACE', EthChainInterface())
rpc_provider = config.get('RPC_PROVIDER') settings = process_sync_range(settings, config)
if rpc_provider == None: return settings
rpc_provider = 'http://localhost:8545'
self.o['RPC'] = EthHTTPConnection(url=rpc_provider, chain_spec=self.o['CHAIN_SPEC'])
def process_settings(settings, config):
settings = base_process_settings(settings, config)
settings = process_common(settings, config)
settings = process_backend(settings, config)
settings = process_session(settings, config)
settings = process_socket(settings, config)
return settings

View File

@@ -3,7 +3,10 @@ import logging
# external imports # external imports
from chaind.error import TxSourceError from chaind.error import TxSourceError
from chainlib.eth.address import is_checksum_address from chainlib.eth.address import (
is_checksum_address,
to_checksum_address,
)
from chainlib.eth.tx import unpack from chainlib.eth.tx import unpack
from chainlib.eth.gas import Gas from chainlib.eth.gas import Gas
from hexathon import ( from hexathon import (
@@ -17,10 +20,11 @@ logg = logging.getLogger(__name__)
class Processor: class Processor:
def __init__(self, resolver, source): def __init__(self, resolver, source, use_checksum=True):
self.resolver = resolver self.resolver = resolver
self.source = source self.source = source
self.processor = [] self.processor = []
self.safe = use_checksum
self.conn = None self.conn = None
@@ -50,9 +54,14 @@ class Processor:
txs = [] txs = []
for i, r in enumerate(self.content): for i, r in enumerate(self.content):
logg.debug('processing {}'.format(r)) logg.debug('processing {}'.format(r))
if not is_checksum_address(r[0]): address = r[0]
raise ValueError('invalid checksum address {} in record {}'.format(r[0], i)) if self.safe:
self.content[i][0] = add_0x(r[0]) if not is_checksum_address(address):
raise ValueError('invalid checksum address {} in record {}'.format(address, i))
else:
address = to_checksum_address(address)
self.content[i][0] = add_0x(address)
try: try:
self.content[i][1] = int(r[1]) self.content[i][1] = int(r[1])
except ValueError: except ValueError:

View File

@@ -1 +1 @@
eth-erc20~=0.3.0 eth-erc20~=0.3.2

View File

@@ -1,6 +1,5 @@
chaind~=0.2.4 chaind~=0.3.0
hexathon~=0.1.5 hexathon~=0.1.7
chainlib-eth~=0.1.1 chainlib-eth~=0.3.0
pyxdg~=0.27 pyxdg~=0.27
shep~=0.2.5 funga-eth~=0.6.1
funga-eth~=0.6.0

View File

@@ -1,6 +1,6 @@
[metadata] [metadata]
name = chaind-eth name = chaind-eth
version = 0.2.2 version = 0.3.0
description = Queue server for ethereum description = Queue server for ethereum
author = Louis Holbrook author = Louis Holbrook
author_email = dev@holbrook.no author_email = dev@holbrook.no