1285 lines
59 KiB
Python
1285 lines
59 KiB
Python
|
###############################################################################
|
||
|
#
|
||
|
# The MIT License (MIT)
|
||
|
#
|
||
|
# Copyright (c) Crossbar.io Technologies GmbH
|
||
|
#
|
||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
# of this software and associated documentation files (the "Software"), to deal
|
||
|
# in the Software without restriction, including without limitation the rights
|
||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
# copies of the Software, and to permit persons to whom the Software is
|
||
|
# furnished to do so, subject to the following conditions:
|
||
|
#
|
||
|
# The above copyright notice and this permission notice shall be included in
|
||
|
# all copies or substantial portions of the Software.
|
||
|
#
|
||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||
|
# THE SOFTWARE.
|
||
|
#
|
||
|
###############################################################################
|
||
|
|
||
|
import os
|
||
|
import sys
|
||
|
import json
|
||
|
import pkg_resources
|
||
|
from pprint import pprint
|
||
|
|
||
|
from jinja2 import Environment, FileSystemLoader
|
||
|
|
||
|
# https://github.com/google/yapf#example-as-a-module
|
||
|
from yapf.yapflib.yapf_api import FormatCode
|
||
|
|
||
|
from autobahn import xbr
|
||
|
from autobahn import __version__
|
||
|
from autobahn.xbr import FbsType
|
||
|
|
||
|
|
||
|
if not xbr.HAS_XBR:
|
||
|
print("\nYou must install the [xbr] extra to use xbrnetwork")
|
||
|
print("For example, \"pip install autobahn[xbr]\".")
|
||
|
sys.exit(1)
|
||
|
|
||
|
from autobahn.xbr._abi import XBR_DEBUG_TOKEN_ADDR, XBR_DEBUG_NETWORK_ADDR, XBR_DEBUG_DOMAIN_ADDR, \
|
||
|
XBR_DEBUG_CATALOG_ADDR, XBR_DEBUG_MARKET_ADDR, XBR_DEBUG_CHANNEL_ADDR
|
||
|
|
||
|
from autobahn.xbr._abi import XBR_DEBUG_TOKEN_ADDR_SRC, XBR_DEBUG_NETWORK_ADDR_SRC, XBR_DEBUG_DOMAIN_ADDR_SRC, \
|
||
|
XBR_DEBUG_CATALOG_ADDR_SRC, XBR_DEBUG_MARKET_ADDR_SRC, XBR_DEBUG_CHANNEL_ADDR_SRC
|
||
|
|
||
|
from autobahn.xbr import FbsSchema, FbsRepository
|
||
|
|
||
|
import uuid
|
||
|
import binascii
|
||
|
import argparse
|
||
|
import random
|
||
|
from pprint import pformat
|
||
|
|
||
|
import eth_keys
|
||
|
import web3
|
||
|
import hashlib
|
||
|
import multihash
|
||
|
import cbor2
|
||
|
|
||
|
import numpy as np
|
||
|
|
||
|
import txaio
|
||
|
txaio.use_twisted()
|
||
|
|
||
|
from twisted.internet import reactor
|
||
|
from twisted.internet.defer import inlineCallbacks
|
||
|
from twisted.internet.threads import deferToThread
|
||
|
from twisted.internet.error import ReactorNotRunning
|
||
|
|
||
|
from autobahn.twisted.wamp import ApplicationSession, ApplicationRunner
|
||
|
from autobahn.wamp.serializer import CBORSerializer
|
||
|
from autobahn.wamp import cryptosign
|
||
|
from autobahn.wamp.exception import ApplicationError
|
||
|
|
||
|
from autobahn.xbr import pack_uint256, unpack_uint256, sign_eip712_channel_open, make_w3
|
||
|
from autobahn.xbr import sign_eip712_member_register, sign_eip712_market_create, sign_eip712_market_join
|
||
|
from autobahn.xbr import ActorType, ChannelType
|
||
|
|
||
|
from autobahn.xbr._config import load_or_create_profile
|
||
|
from autobahn.xbr._util import hlval, hlid, hltype
|
||
|
|
||
|
|
||
|
_COMMANDS = ['version', 'get-member', 'register-member', 'register-member-verify',
|
||
|
'get-market', 'create-market', 'create-market-verify',
|
||
|
'get-actor', 'join-market', 'join-market-verify',
|
||
|
'get-channel', 'open-channel', 'close-channel',
|
||
|
'describe-schema', 'codegen-schema']
|
||
|
|
||
|
|
||
|
class Client(ApplicationSession):
|
||
|
|
||
|
def __init__(self, config=None):
|
||
|
ApplicationSession.__init__(self, config)
|
||
|
|
||
|
# FIXME
|
||
|
self._default_gas = 100000
|
||
|
self._chain_id = 4
|
||
|
|
||
|
profile = config.extra.get('profile', None)
|
||
|
|
||
|
if profile and profile.cskey:
|
||
|
assert type(profile.cskey) == bytes and len(profile.cskey) == 32
|
||
|
self._cskey_raw = profile.cskey
|
||
|
self._key = cryptosign.SigningKey.from_key_bytes(self._cskey_raw)
|
||
|
self.log.info('WAMP-Cryptosign keys with public key {public_key} loaded', public_key=self._key.public_key)
|
||
|
else:
|
||
|
self._cskey_raw = os.urandom(32)
|
||
|
self._key = cryptosign.SigningKey.from_key_bytes(self._cskey_raw)
|
||
|
self.log.info('WAMP-Cryptosign keys initialized randomly')
|
||
|
|
||
|
if profile and profile.ethkey:
|
||
|
self.set_ethkey_from_profile(profile)
|
||
|
self.log.info('XBR ETH keys loaded from profile')
|
||
|
else:
|
||
|
self._ethkey_raw = None
|
||
|
self._ethkey = None
|
||
|
self._ethadr = None
|
||
|
self._ethadr_raw = None
|
||
|
self.log.info('XBR ETH keys left unset')
|
||
|
|
||
|
self._running = True
|
||
|
|
||
|
def set_ethkey_from_profile(self, profile):
|
||
|
"""
|
||
|
|
||
|
:param profile:
|
||
|
:return:
|
||
|
"""
|
||
|
assert type(profile.ethkey) == bytes, 'set_ethkey_from_profile::profile invalid type "{}" - must be bytes'.format(type(profile.ethkey))
|
||
|
assert len(profile.ethkey) == 32, 'set_ethkey_from_profile::profile invalid length {} - must be 32'.format(len(profile.ethkey))
|
||
|
self._ethkey_raw = profile.ethkey
|
||
|
self._ethkey = eth_keys.keys.PrivateKey(self._ethkey_raw)
|
||
|
self._ethadr = web3.Web3.toChecksumAddress(self._ethkey.public_key.to_canonical_address())
|
||
|
self._ethadr_raw = binascii.a2b_hex(self._ethadr[2:])
|
||
|
self.log.info('ETH keys with address {ethadr} loaded', ethadr=self._ethadr)
|
||
|
|
||
|
def onConnect(self):
|
||
|
if self.config.realm == 'xbrnetwork':
|
||
|
authextra = {
|
||
|
'pubkey': self._key.public_key(),
|
||
|
'trustroot': None,
|
||
|
'challenge': None,
|
||
|
'channel_binding': 'tls-unique'
|
||
|
}
|
||
|
self.log.info('Client connected, now joining realm "{realm}" with WAMP-cryptosign authentication ..',
|
||
|
realm=hlid(self.config.realm))
|
||
|
self.join(self.config.realm, authmethods=['cryptosign'], authextra=authextra)
|
||
|
else:
|
||
|
self.log.info('Client connected, now joining realm "{realm}" (no authentication) ..',
|
||
|
realm=hlid(self.config.realm))
|
||
|
self.join(self.config.realm)
|
||
|
|
||
|
def onChallenge(self, challenge):
|
||
|
if challenge.method == 'cryptosign':
|
||
|
signed_challenge = self._key.sign_challenge(self, challenge)
|
||
|
return signed_challenge
|
||
|
else:
|
||
|
raise RuntimeError('unable to process authentication method {}'.format(challenge.method))
|
||
|
|
||
|
async def onJoin(self, details):
|
||
|
self.log.info('Ok, client joined on realm "{realm}" [session={session}, authid="{authid}", authrole="{authrole}"]',
|
||
|
realm=hlid(details.realm),
|
||
|
session=hlid(details.session),
|
||
|
authid=hlid(details.authid),
|
||
|
authrole=hlid(details.authrole),
|
||
|
details=details)
|
||
|
if 'ready' in self.config.extra:
|
||
|
txaio.resolve(self.config.extra['ready'], (self, details))
|
||
|
|
||
|
if 'command' in self.config.extra:
|
||
|
try:
|
||
|
if details.realm == 'xbrnetwork':
|
||
|
await self._do_xbrnetwork_realm(details)
|
||
|
else:
|
||
|
await self._do_market_realm(details)
|
||
|
except Exception as e:
|
||
|
self.log.failure()
|
||
|
self.config.extra['error'] = e
|
||
|
finally:
|
||
|
self.leave()
|
||
|
|
||
|
def onLeave(self, details):
|
||
|
self.log.info('Client left realm (reason="{reason}")', reason=hlval(details.reason))
|
||
|
self._running = False
|
||
|
|
||
|
if details.reason == 'wamp.close.normal':
|
||
|
if self.config and self.config.runner:
|
||
|
# user initiated leave => end the program
|
||
|
self.config.runner.stop()
|
||
|
self.disconnect()
|
||
|
|
||
|
def onDisconnect(self):
|
||
|
self.log.info('Client disconnected')
|
||
|
try:
|
||
|
reactor.stop()
|
||
|
except ReactorNotRunning:
|
||
|
pass
|
||
|
|
||
|
async def _do_xbrnetwork_realm(self, details):
|
||
|
command = self.config.extra['command']
|
||
|
if details.authrole == 'anonymous':
|
||
|
self.log.info('not yet a member in the XBR network')
|
||
|
|
||
|
assert command in ['get-member', 'register-member', 'register-member-verify']
|
||
|
if command == 'get-member':
|
||
|
await self._do_get_member(self._ethadr_raw)
|
||
|
elif command == 'register-member':
|
||
|
username = self.config.extra['username']
|
||
|
email = self.config.extra['email']
|
||
|
await self._do_onboard_member(username, email)
|
||
|
elif command == 'register-member-verify':
|
||
|
vaction_oid = self.config.extra['vaction']
|
||
|
vaction_code = self.config.extra['vcode']
|
||
|
await self._do_onboard_member_verify(vaction_oid, vaction_code)
|
||
|
else:
|
||
|
assert False, 'should not arrive here'
|
||
|
else:
|
||
|
# WAMP authid on xbrnetwork follows this format: "member-"
|
||
|
member_oid = uuid.UUID(details.authid[7:])
|
||
|
# member_data = await self.call('xbr.network.get_member', member_oid.bytes)
|
||
|
# self.log.info('Address is already a member in the XBR network:\n\n{member_data}', member_data=pformat(member_data))
|
||
|
|
||
|
assert command in ['get-member', 'get-market', 'create-market', 'create-market-verify',
|
||
|
'get-actor', 'join-market', 'join-market-verify']
|
||
|
if command == 'get-member':
|
||
|
await self._do_get_member(self._ethadr_raw)
|
||
|
elif command == 'get-market':
|
||
|
market_oid = self.config.extra['market']
|
||
|
await self._do_get_market(member_oid, market_oid)
|
||
|
elif command == 'get-actor':
|
||
|
if 'market' in self.config.extra and self.config.extra['market']:
|
||
|
market_oid = self.config.extra['market']
|
||
|
else:
|
||
|
market_oid = None
|
||
|
if 'actor' in self.config.extra and self.config.extra['actor']:
|
||
|
actor = self.config.extra['actor']
|
||
|
else:
|
||
|
actor = self._ethadr_raw
|
||
|
await self._do_get_actor(market_oid, actor)
|
||
|
elif command == 'create-market':
|
||
|
market_oid = self.config.extra['market']
|
||
|
marketmaker = self.config.extra['marketmaker']
|
||
|
market_title = self.config.extra['market_title']
|
||
|
market_label = self.config.extra['market_label']
|
||
|
market_homepage = self.config.extra['market_homepage']
|
||
|
provider_security = self.config.extra['market_provider_security']
|
||
|
consumer_security = self.config.extra['market_consumer_security']
|
||
|
market_fee = self.config.extra['market_fee']
|
||
|
await self._do_create_market(member_oid, market_oid, marketmaker, title=market_title,
|
||
|
label=market_label, homepage=market_homepage,
|
||
|
provider_security=provider_security, consumer_security=consumer_security,
|
||
|
market_fee=market_fee)
|
||
|
elif command == 'create-market-verify':
|
||
|
vaction_oid = self.config.extra['vaction']
|
||
|
vaction_code = self.config.extra['vcode']
|
||
|
await self._do_create_market_verify(member_oid, vaction_oid, vaction_code)
|
||
|
elif command == 'join-market':
|
||
|
market_oid = self.config.extra['market']
|
||
|
actor_type = self.config.extra['actor_type']
|
||
|
await self._do_join_market(member_oid, market_oid, actor_type)
|
||
|
elif command == 'join-market-verify':
|
||
|
vaction_oid = self.config.extra['vaction']
|
||
|
vaction_code = self.config.extra['vcode']
|
||
|
await self._do_join_market_verify(member_oid, vaction_oid, vaction_code)
|
||
|
else:
|
||
|
assert False, 'should not arrive here'
|
||
|
|
||
|
async def _do_market_realm(self, details):
|
||
|
profile = self.config.extra['profile']
|
||
|
|
||
|
blockchain_gateway = {
|
||
|
"type": "infura",
|
||
|
"network": profile.infura_network,
|
||
|
"key": profile.infura_key,
|
||
|
"secret": profile.infura_secret
|
||
|
}
|
||
|
|
||
|
self._w3 = make_w3(blockchain_gateway)
|
||
|
xbr.setProvider(self._w3)
|
||
|
|
||
|
command = self.config.extra['command']
|
||
|
assert command in ['open-channel', 'get-channel']
|
||
|
if command == 'get-channel':
|
||
|
market_oid = self.config.extra['market']
|
||
|
delegate = self.config.extra['delegate']
|
||
|
channel_type = self.config.extra['channel_type']
|
||
|
if channel_type == ChannelType.PAYMENT:
|
||
|
await self._do_get_active_payment_channel(market_oid, delegate)
|
||
|
elif channel_type == ChannelType.PAYING:
|
||
|
await self._do_get_active_paying_channel(market_oid, delegate)
|
||
|
else:
|
||
|
assert False, 'should not arrive here'
|
||
|
elif command == 'open-channel':
|
||
|
# market in which to open the new buyer/seller (payment/paying) channel
|
||
|
market_oid = self.config.extra['market']
|
||
|
|
||
|
# read UUID of the new channel to be created from command line OR auto-generate a new one
|
||
|
channel_oid = self.config.extra['channel'] or uuid.uuid4()
|
||
|
|
||
|
# buyer/seller (payment/paying) channel
|
||
|
channel_type = self.config.extra['channel_type']
|
||
|
|
||
|
# the delgate allowed to use the channel
|
||
|
delegate = self.config.extra['delegate']
|
||
|
|
||
|
# amount of market coins for initial channel balance
|
||
|
amount = self.config.extra['amount']
|
||
|
|
||
|
# now open the channel ..
|
||
|
await self._do_open_channel(market_oid, channel_oid, channel_type, delegate, amount)
|
||
|
else:
|
||
|
assert False, 'should not arrive here'
|
||
|
|
||
|
async def _do_get_member(self, member_adr):
|
||
|
is_member = await self.call('xbr.network.is_member', member_adr)
|
||
|
if is_member:
|
||
|
member_data = await self.call('xbr.network.get_member_by_wallet', member_adr)
|
||
|
|
||
|
member_data['address'] = web3.Web3.toChecksumAddress(member_data['address'])
|
||
|
member_data['oid'] = uuid.UUID(bytes=member_data['oid'])
|
||
|
member_data['balance']['eth'] = web3.Web3.fromWei(unpack_uint256(member_data['balance']['eth']), 'ether')
|
||
|
member_data['balance']['xbr'] = web3.Web3.fromWei(unpack_uint256(member_data['balance']['xbr']), 'ether')
|
||
|
member_data['created'] = np.datetime64(member_data['created'], 'ns')
|
||
|
|
||
|
member_level = member_data['level']
|
||
|
MEMBER_LEVEL_TO_STR = {
|
||
|
# Member is active.
|
||
|
1: 'ACTIVE',
|
||
|
# Member is active and verified.
|
||
|
2: 'VERIFIED',
|
||
|
# Member is retired.
|
||
|
3: 'RETIRED',
|
||
|
# Member is subject to a temporary penalty.
|
||
|
4: 'PENALTY',
|
||
|
# Member is currently blocked and cannot current actively participate in the market.
|
||
|
5: 'BLOCKED',
|
||
|
}
|
||
|
member_data['level'] = MEMBER_LEVEL_TO_STR.get(member_level, None)
|
||
|
|
||
|
self.log.info('Member {member_oid} found for address 0x{member_adr} - current member level {member_level}',
|
||
|
member_level=hlval(member_data['level']),
|
||
|
member_oid=hlid(member_data['oid']),
|
||
|
member_adr=hlval(member_data['address']))
|
||
|
return member_data
|
||
|
else:
|
||
|
self.log.warn('Address 0x{member_adr} is not a member in the XBR network',
|
||
|
member_adr=hlval(binascii.b2a_hex(member_adr).decode()))
|
||
|
|
||
|
async def _do_get_actor(self, market_oid, actor_adr):
|
||
|
is_member = await self.call('xbr.network.is_member', actor_adr)
|
||
|
if is_member:
|
||
|
actor = await self.call('xbr.network.get_member_by_wallet', actor_adr)
|
||
|
actor_oid = uuid.UUID(bytes=actor['oid'])
|
||
|
actor_adr = web3.Web3.toChecksumAddress(actor['address'])
|
||
|
actor_level = actor['level']
|
||
|
actor_balance_eth = web3.Web3.fromWei(unpack_uint256(actor['balance']['eth']), 'ether')
|
||
|
actor_balance_xbr = web3.Web3.fromWei(unpack_uint256(actor['balance']['xbr']), 'ether')
|
||
|
self.log.info('Found member with address {member_adr} (member level {member_level}, balances: {member_balance_eth} ETH, {member_balance_xbr} XBR)',
|
||
|
member_adr=hlid(actor_adr),
|
||
|
member_level=hlval(actor_level),
|
||
|
member_balance_eth=hlval(actor_balance_eth),
|
||
|
member_balance_xbr=hlval(actor_balance_xbr))
|
||
|
|
||
|
if market_oid:
|
||
|
market_oids = [market_oid.bytes]
|
||
|
else:
|
||
|
market_oids = await self.call('xbr.network.get_markets_by_actor', actor_oid.bytes)
|
||
|
|
||
|
if market_oids:
|
||
|
for market_oid in market_oids:
|
||
|
# market = await self.call('xbr.network.get_market', market_oid)
|
||
|
result = await self.call('xbr.network.get_actor_in_market', market_oid, actor['address'])
|
||
|
for actor in result:
|
||
|
actor['actor'] = web3.Web3.toChecksumAddress(actor['actor'])
|
||
|
actor['timestamp'] = np.datetime64(actor['timestamp'], 'ns')
|
||
|
actor['joined'] = unpack_uint256(actor['joined']) if actor['joined'] else None
|
||
|
actor['market'] = uuid.UUID(bytes=actor['market'])
|
||
|
actor['security'] = web3.Web3.fromWei(unpack_uint256(actor['security']), 'ether') if actor['security'] else None
|
||
|
actor['signature'] = '0x' + binascii.b2a_hex(actor['signature']).decode() if actor['signature'] else None
|
||
|
actor['tid'] = '0x' + binascii.b2a_hex(actor['tid']).decode() if actor['tid'] else None
|
||
|
|
||
|
actor_type = actor['actor_type']
|
||
|
ACTOR_TYPE_TO_STR = {
|
||
|
# Actor is a XBR Provider.
|
||
|
1: 'PROVIDER',
|
||
|
# Actor is a XBR Consumer.
|
||
|
2: 'CONSUMER',
|
||
|
# Actor is both a XBR Provider and XBR Consumer.
|
||
|
3: 'PROVIDER_CONSUMER',
|
||
|
}
|
||
|
actor['actor_type'] = ACTOR_TYPE_TO_STR.get(actor_type, None)
|
||
|
|
||
|
self.log.info('Actor is joined to market {market_oid}:\n\n{actor}\n',
|
||
|
market_oid=hlid(uuid.UUID(bytes=market_oid)), actor=pformat(actor))
|
||
|
else:
|
||
|
self.log.info('Member is not yet actor in any market!')
|
||
|
else:
|
||
|
self.log.warn('Address 0x{member_adr} is not a member in the XBR network',
|
||
|
member_adr=binascii.b2a_hex(actor_adr).decode())
|
||
|
|
||
|
@inlineCallbacks
|
||
|
def _do_onboard_member(self, member_username, member_email, member_password=None):
|
||
|
client_pubkey = binascii.a2b_hex(self._key.public_key())
|
||
|
|
||
|
# fake wallet type "metamask"
|
||
|
wallet_type = 'metamask'
|
||
|
|
||
|
# delegate ethereum private key object
|
||
|
wallet_key = self._ethkey
|
||
|
wallet_raw = self._ethkey_raw
|
||
|
|
||
|
# delegate ethereum account canonical address
|
||
|
wallet_adr = wallet_key.public_key.to_canonical_address()
|
||
|
|
||
|
config = yield self.call('xbr.network.get_config')
|
||
|
status = yield self.call('xbr.network.get_status')
|
||
|
|
||
|
verifyingChain = config['verifying_chain_id']
|
||
|
verifyingContract = binascii.a2b_hex(config['verifying_contract_adr'][2:])
|
||
|
|
||
|
registered = status['block']['number']
|
||
|
eula = config['eula']['hash']
|
||
|
|
||
|
# create an aux-data object with info only stored off-chain (in our xbrbackend DB) ..
|
||
|
profile_obj = {
|
||
|
'member_username': member_username,
|
||
|
'member_email': member_email,
|
||
|
'client_pubkey': client_pubkey,
|
||
|
'wallet_type': wallet_type,
|
||
|
}
|
||
|
|
||
|
# .. hash the serialized aux-data object ..
|
||
|
profile_data = cbor2.dumps(profile_obj)
|
||
|
h = hashlib.sha256()
|
||
|
h.update(profile_data)
|
||
|
|
||
|
# .. compute the sha256 multihash b58-encoded string from that ..
|
||
|
profile = multihash.to_b58_string(multihash.encode(h.digest(), 'sha2-256'))
|
||
|
|
||
|
signature = sign_eip712_member_register(wallet_raw, verifyingChain, verifyingContract,
|
||
|
wallet_adr, registered, eula, profile)
|
||
|
|
||
|
# https://xbr.network/docs/network/api.html#xbrnetwork.XbrNetworkApi.onboard_member
|
||
|
try:
|
||
|
result = yield self.call('xbr.network.onboard_member',
|
||
|
member_username, member_email, client_pubkey, wallet_type, wallet_adr,
|
||
|
verifyingChain, registered, verifyingContract, eula, profile, profile_data,
|
||
|
signature)
|
||
|
except ApplicationError as e:
|
||
|
self.log.error('ApplicationError: {error}', error=e)
|
||
|
self.leave('wamp.error', str(e))
|
||
|
return
|
||
|
except Exception as e:
|
||
|
raise e
|
||
|
|
||
|
assert type(result) == dict
|
||
|
assert 'timestamp' in result and type(result['timestamp']) == int and result['timestamp'] > 0
|
||
|
assert 'action' in result and result['action'] == 'onboard_member'
|
||
|
assert 'vaction_oid' in result and type(result['vaction_oid']) == bytes and len(result['vaction_oid']) == 16
|
||
|
|
||
|
vaction_oid = uuid.UUID(bytes=result['vaction_oid'])
|
||
|
self.log.info('On-boarding member - verification "{vaction_oid}" created', vaction_oid=vaction_oid)
|
||
|
|
||
|
return result
|
||
|
|
||
|
@inlineCallbacks
|
||
|
def _do_onboard_member_verify(self, vaction_oid, vaction_code):
|
||
|
|
||
|
self.log.info('Verifying member using vaction_oid={vaction_oid}, vaction_code={vaction_code} ..',
|
||
|
vaction_oid=vaction_oid, vaction_code=vaction_code)
|
||
|
try:
|
||
|
result = yield self.call('xbr.network.verify_onboard_member', vaction_oid.bytes, vaction_code)
|
||
|
except ApplicationError as e:
|
||
|
self.log.error('ApplicationError: {error}', error=e)
|
||
|
raise e
|
||
|
|
||
|
assert type(result) == dict
|
||
|
assert 'member_oid' in result and type(result['member_oid']) == bytes and len(result['member_oid']) == 16
|
||
|
assert 'created' in result and type(result['created']) == int and result['created'] > 0
|
||
|
|
||
|
member_oid = result['member_oid']
|
||
|
self.log.info('SUCCESS! New XBR Member onboarded: member_oid={member_oid}, transaction={transaction}',
|
||
|
member_oid=hlid(uuid.UUID(bytes=member_oid)),
|
||
|
transaction=hlval('0x' + binascii.b2a_hex(result['transaction']).decode()))
|
||
|
|
||
|
return result
|
||
|
|
||
|
async def _do_create_market(self, member_oid, market_oid, marketmaker, title=None, label=None, homepage=None,
|
||
|
provider_security=0, consumer_security=0, market_fee=0):
|
||
|
member_data = await self.call('xbr.network.get_member', member_oid.bytes)
|
||
|
member_adr = member_data['address']
|
||
|
|
||
|
config = await self.call('xbr.network.get_config')
|
||
|
|
||
|
verifyingChain = config['verifying_chain_id']
|
||
|
verifyingContract = binascii.a2b_hex(config['verifying_contract_adr'][2:])
|
||
|
|
||
|
coin_adr = binascii.a2b_hex(config['contracts']['xbrtoken'][2:])
|
||
|
|
||
|
status = await self.call('xbr.network.get_status')
|
||
|
block_number = status['block']['number']
|
||
|
|
||
|
# count all markets before we create a new one:
|
||
|
res = await self.call('xbr.network.find_markets')
|
||
|
cnt_market_before = len(res)
|
||
|
self.log.info('Total markets before: {cnt_market_before}', cnt_market_before=cnt_market_before)
|
||
|
|
||
|
res = await self.call('xbr.network.get_markets_by_owner', member_oid.bytes)
|
||
|
cnt_market_by_owner_before = len(res)
|
||
|
self.log.info('Market for owner: {cnt_market_by_owner_before}',
|
||
|
cnt_market_by_owner_before=cnt_market_by_owner_before)
|
||
|
|
||
|
# collect information for market creation that is stored on-chain
|
||
|
|
||
|
# terms text: encode in utf8 and compute BIP58 multihash string
|
||
|
terms_data = 'these are my market terms (randint={})'.format(random.randint(0, 1000)).encode('utf8')
|
||
|
h = hashlib.sha256()
|
||
|
h.update(terms_data)
|
||
|
terms_hash = str(multihash.to_b58_string(multihash.encode(h.digest(), 'sha2-256')))
|
||
|
|
||
|
# market meta data that doesn't change. the hash of this is part of the data that is signed and also
|
||
|
# stored on-chain (only the hash, not the meta data!)
|
||
|
meta_obj = {
|
||
|
'chain_id': verifyingChain,
|
||
|
'block_number': block_number,
|
||
|
'contract_adr': verifyingContract,
|
||
|
'member_adr': member_adr,
|
||
|
'member_oid': member_oid.bytes,
|
||
|
'market_oid': market_oid.bytes,
|
||
|
}
|
||
|
meta_data = cbor2.dumps(meta_obj)
|
||
|
h = hashlib.sha256()
|
||
|
h.update(meta_data)
|
||
|
meta_hash = multihash.to_b58_string(multihash.encode(h.digest(), 'sha2-256'))
|
||
|
|
||
|
# create signature for pre-signed transaction
|
||
|
signature = sign_eip712_market_create(self._ethkey_raw, verifyingChain, verifyingContract, member_adr,
|
||
|
block_number, market_oid.bytes, coin_adr, terms_hash, meta_hash,
|
||
|
marketmaker, provider_security, consumer_security, market_fee)
|
||
|
|
||
|
# for wire transfer, convert to bytes
|
||
|
provider_security = pack_uint256(provider_security)
|
||
|
consumer_security = pack_uint256(consumer_security)
|
||
|
market_fee = pack_uint256(market_fee)
|
||
|
|
||
|
# market settings that can change. even though changing might require signing, neither the data nor
|
||
|
# and signatures are stored on-chain. however, even when only signed off-chain, this establishes
|
||
|
# a chain of signature anchored in the on-chain record for this market!
|
||
|
attributes = {
|
||
|
'title': title,
|
||
|
'label': label,
|
||
|
'homepage': homepage,
|
||
|
}
|
||
|
|
||
|
# now provide everything of above:
|
||
|
# - market operator (owning member) and market oid
|
||
|
# - signed market data and signature
|
||
|
# - settings
|
||
|
createmarket_request_submitted = await self.call('xbr.network.create_market', member_oid.bytes,
|
||
|
market_oid.bytes, verifyingChain, block_number,
|
||
|
verifyingContract, coin_adr, terms_hash, meta_hash, meta_data,
|
||
|
marketmaker, provider_security, consumer_security, market_fee,
|
||
|
signature, attributes)
|
||
|
|
||
|
self.log.info('SUCCESS: Create market request submitted: \n{createmarket_request_submitted}\n',
|
||
|
createmarket_request_submitted=pformat(createmarket_request_submitted))
|
||
|
|
||
|
assert type(createmarket_request_submitted) == dict
|
||
|
assert 'timestamp' in createmarket_request_submitted and type(
|
||
|
createmarket_request_submitted['timestamp']) == int and createmarket_request_submitted['timestamp'] > 0
|
||
|
assert 'action' in createmarket_request_submitted and createmarket_request_submitted[
|
||
|
'action'] == 'create_market'
|
||
|
assert 'vaction_oid' in createmarket_request_submitted and type(
|
||
|
createmarket_request_submitted['vaction_oid']) == bytes and len(
|
||
|
createmarket_request_submitted['vaction_oid']) == 16
|
||
|
|
||
|
vaction_oid = uuid.UUID(bytes=createmarket_request_submitted['vaction_oid'])
|
||
|
self.log.info('SUCCESS: New Market verification "{vaction_oid}" created', vaction_oid=vaction_oid)
|
||
|
|
||
|
async def _do_create_market_verify(self, member_oid, vaction_oid, vaction_code):
|
||
|
self.log.info('Verifying create market using vaction_oid={vaction_oid}, vaction_code={vaction_code} ..',
|
||
|
vaction_oid=vaction_oid, vaction_code=vaction_code)
|
||
|
|
||
|
create_market_request_verified = await self.call('xbr.network.verify_create_market', vaction_oid.bytes,
|
||
|
vaction_code)
|
||
|
|
||
|
self.log.info('Create market request verified: \n{create_market_request_verified}\n',
|
||
|
create_market_request_verified=pformat(create_market_request_verified))
|
||
|
|
||
|
assert type(create_market_request_verified) == dict
|
||
|
assert 'market_oid' in create_market_request_verified and type(
|
||
|
create_market_request_verified['market_oid']) == bytes and len(
|
||
|
create_market_request_verified['market_oid']) == 16
|
||
|
assert 'created' in create_market_request_verified and type(
|
||
|
create_market_request_verified['created']) == int and create_market_request_verified['created'] > 0
|
||
|
|
||
|
market_oid = create_market_request_verified['market_oid']
|
||
|
self.log.info('SUCCESS! New XBR market created: market_oid={market_oid}, result=\n{result}',
|
||
|
market_oid=uuid.UUID(bytes=market_oid), result=pformat(create_market_request_verified))
|
||
|
|
||
|
market_oids = await self.call('xbr.network.find_markets')
|
||
|
self.log.info('SUCCESS - find_markets: found {cnt_markets} markets', cnt_markets=len(market_oids))
|
||
|
|
||
|
# count all markets after we created a new market:
|
||
|
cnt_market_after = len(market_oids)
|
||
|
self.log.info('Total markets after: {cnt_market_after}', cnt_market_after=cnt_market_after)
|
||
|
assert market_oid in market_oids, 'expected to find market ID {}, but not found in {} returned market IDs'.format(
|
||
|
uuid.UUID(bytes=market_oid), len(market_oids))
|
||
|
|
||
|
market_oids = await self.call('xbr.network.get_markets_by_owner', member_oid.bytes)
|
||
|
self.log.info('SUCCESS - get_markets_by_owner: found {cnt_markets} markets', cnt_markets=len(market_oids))
|
||
|
|
||
|
# count all markets after we created a new market:
|
||
|
cnt_market_by_owner_after = len(market_oids)
|
||
|
self.log.info('Market for owner: {cnt_market_by_owner_after}',
|
||
|
cnt_market_by_owner_before=cnt_market_by_owner_after)
|
||
|
assert market_oid in market_oids, 'expected to find market ID {}, but not found in {} returned market IDs'.format(
|
||
|
uuid.UUID(bytes=market_oid), len(market_oids))
|
||
|
|
||
|
for market_oid in market_oids:
|
||
|
self.log.info('xbr.network.get_market(market_oid={market_oid}) ..', market_oid=market_oid)
|
||
|
market = await self.call('xbr.network.get_market', market_oid, include_attributes=True)
|
||
|
self.log.info('SUCCESS: got market information\n\n{market}\n', market=pformat(market))
|
||
|
|
||
|
async def _do_get_market(self, member_oid, market_oid):
|
||
|
member_data = await self.call('xbr.network.get_member', member_oid.bytes)
|
||
|
member_adr = member_data['address']
|
||
|
market = await self.call('xbr.network.get_market', market_oid.bytes)
|
||
|
if market:
|
||
|
if market['owner'] == member_adr:
|
||
|
self.log.info('You are market owner (operator)!')
|
||
|
else:
|
||
|
self.log.info('Marked is owned by {owner}', owner=hlid(web3.Web3.toChecksumAddress(market['owner'])))
|
||
|
|
||
|
market['market'] = uuid.UUID(bytes=market['market'])
|
||
|
market['owner'] = web3.Web3.toChecksumAddress(market['owner'])
|
||
|
market['maker'] = web3.Web3.toChecksumAddress(market['maker'])
|
||
|
market['coin'] = web3.Web3.toChecksumAddress(market['coin'])
|
||
|
market['timestamp'] = np.datetime64(market['timestamp'], 'ns')
|
||
|
|
||
|
self.log.info('Market {market_oid} information:\n\n{market}\n',
|
||
|
market_oid=hlid(market_oid), market=pformat(market))
|
||
|
else:
|
||
|
self.log.warn('No market {market_oid} found!', market_oid=hlid(market_oid))
|
||
|
|
||
|
async def _do_join_market(self, member_oid, market_oid, actor_type):
|
||
|
|
||
|
assert actor_type in [ActorType.CONSUMER, ActorType.PROVIDER, ActorType.PROVIDER_CONSUMER]
|
||
|
|
||
|
member_data = await self.call('xbr.network.get_member', member_oid.bytes)
|
||
|
member_adr = member_data['address']
|
||
|
|
||
|
config = await self.call('xbr.network.get_config')
|
||
|
verifyingChain = config['verifying_chain_id']
|
||
|
verifyingContract = binascii.a2b_hex(config['verifying_contract_adr'][2:])
|
||
|
|
||
|
status = await self.call('xbr.network.get_status')
|
||
|
block_number = status['block']['number']
|
||
|
|
||
|
meta_obj = {
|
||
|
}
|
||
|
meta_data = cbor2.dumps(meta_obj)
|
||
|
h = hashlib.sha256()
|
||
|
h.update(meta_data)
|
||
|
meta_hash = multihash.to_b58_string(multihash.encode(h.digest(), 'sha2-256'))
|
||
|
|
||
|
signature = sign_eip712_market_join(self._ethkey_raw, verifyingChain, verifyingContract, member_adr,
|
||
|
block_number, market_oid.bytes, actor_type, meta_hash)
|
||
|
|
||
|
request_submitted = await self.call('xbr.network.join_market', member_oid.bytes, market_oid.bytes,
|
||
|
verifyingChain, block_number, verifyingContract,
|
||
|
actor_type, meta_hash, meta_data, signature)
|
||
|
|
||
|
vaction_oid = uuid.UUID(bytes=request_submitted['vaction_oid'])
|
||
|
self.log.info('SUCCESS! XBR market join request submitted: vaction_oid={vaction_oid}', vaction_oid=vaction_oid)
|
||
|
|
||
|
async def _do_join_market_verify(self, member_oid, vaction_oid, vaction_code):
|
||
|
request_verified = await self.call('xbr.network.verify_join_market', vaction_oid.bytes, vaction_code)
|
||
|
market_oid = request_verified['market_oid']
|
||
|
actor_type = request_verified['actor_type']
|
||
|
self.log.info('SUCCESS! XBR market joined: member_oid={member_oid}, market_oid={market_oid}, actor_type={actor_type}',
|
||
|
member_oid=member_oid, market_oid=market_oid, actor_type=actor_type)
|
||
|
|
||
|
async def _do_get_active_payment_channel(self, market_oid, delegate_adr):
|
||
|
channel = await self.call('xbr.marketmaker.get_active_payment_channel', delegate_adr)
|
||
|
self.log.debug('{channel}', channel=pformat(channel))
|
||
|
if channel:
|
||
|
self.log.info('Active buyer (payment) channel found: {amount} amount',
|
||
|
amount=int(unpack_uint256(channel['amount']) / 10 ** 18))
|
||
|
balance = await self.call('xbr.marketmaker.get_payment_channel_balance', channel['channel_oid'])
|
||
|
self.log.debug('{balance}', channel=pformat(balance))
|
||
|
self.log.info('Current off-chain amount remaining: {remaining} [sequence {sequence}]',
|
||
|
remaining=int(unpack_uint256(balance['remaining']) / 10 ** 18), sequence=balance['seq'])
|
||
|
else:
|
||
|
self.log.info('No active buyer (payment) channel found!')
|
||
|
|
||
|
async def _do_get_active_paying_channel(self, market_oid, delegate_adr):
|
||
|
channel = await self.call('xbr.marketmaker.get_active_paying_channel', delegate_adr)
|
||
|
self.log.debug('{channel}', channel=pformat(channel))
|
||
|
if channel:
|
||
|
self.log.info('Active seller (paying) channel found: {amount} amount',
|
||
|
amount=int(unpack_uint256(channel['amount']) / 10 ** 18))
|
||
|
balance = await self.call('xbr.marketmaker.get_paying_channel_balance', channel['channel_oid'])
|
||
|
self.log.debug('{balance}', channel=pformat(balance))
|
||
|
self.log.info('Current off-chain amount remaining: {remaining} [sequence {sequence}]',
|
||
|
remaining=int(unpack_uint256(balance['remaining']) / 10 ** 18), sequence=balance['seq'])
|
||
|
else:
|
||
|
self.log.info('No active seller (paying) channel found!')
|
||
|
|
||
|
async def _do_open_channel(self, market_oid, channel_oid, channel_type, delegate, amount):
|
||
|
member_key = self._ethkey_raw
|
||
|
member_adr = self._ethkey.public_key.to_canonical_address()
|
||
|
|
||
|
config = await self.call('xbr.marketmaker.get_config')
|
||
|
marketmaker = binascii.a2b_hex(config['marketmaker'][2:])
|
||
|
recipient = binascii.a2b_hex(config['owner'][2:])
|
||
|
verifying_chain_id = config['verifying_chain_id']
|
||
|
verifying_contract_adr = binascii.a2b_hex(config['verifying_contract_adr'][2:])
|
||
|
|
||
|
status = await self.call('xbr.marketmaker.get_status')
|
||
|
current_block_number = status['block']['number']
|
||
|
|
||
|
if amount > 0:
|
||
|
if channel_type == ChannelType.PAYMENT:
|
||
|
from_adr = member_adr
|
||
|
to_adr = xbr.xbrchannel.address
|
||
|
elif channel_type == ChannelType.PAYING:
|
||
|
from_adr = marketmaker
|
||
|
to_adr = xbr.xbrchannel.address
|
||
|
else:
|
||
|
assert False, 'should not arrive here'
|
||
|
|
||
|
# allowance1 = xbr.xbrtoken.functions.allowance(transact_from, xbr.xbrchannel.address).call()
|
||
|
# xbr.xbrtoken.functions.approve(to_adr, amount).transact(
|
||
|
# {'from': transact_from, 'gas': transact_gas})
|
||
|
# allowance2 = xbr.xbrtoken.functions.allowance(transact_from, xbr.xbrchannel.address).call()
|
||
|
# assert allowance2 - allowance1 == amount
|
||
|
|
||
|
try:
|
||
|
txn_hash = await deferToThread(self._send_Allowance, from_adr, to_adr, amount)
|
||
|
self.log.info('transaction submitted, txn_hash={txn_hash}', txn_hash=txn_hash)
|
||
|
except Exception as e:
|
||
|
self.log.failure()
|
||
|
raise e
|
||
|
|
||
|
# compute EIP712 signature, and sign using member private key
|
||
|
signature = sign_eip712_channel_open(member_key, verifying_chain_id, verifying_contract_adr, channel_type,
|
||
|
current_block_number, market_oid.bytes, channel_oid.bytes,
|
||
|
member_adr, delegate, marketmaker, recipient, amount)
|
||
|
attributes = None
|
||
|
channel_request = await self.call('xbr.marketmaker.open_channel', member_adr, market_oid.bytes,
|
||
|
channel_oid.bytes, verifying_chain_id, current_block_number,
|
||
|
verifying_contract_adr, channel_type, delegate, marketmaker, recipient,
|
||
|
pack_uint256(amount), signature, attributes)
|
||
|
|
||
|
self.log.info('Channel open request submitted:\n\n{channel_request}\n',
|
||
|
channel_request=pformat(channel_request))
|
||
|
|
||
|
def _send_Allowance(self, from_adr, to_adr, amount):
|
||
|
# FIXME: estimate gas required for call
|
||
|
gas = self._default_gas
|
||
|
gasPrice = self._w3.toWei('10', 'gwei')
|
||
|
|
||
|
from_adr = self._ethadr
|
||
|
|
||
|
# each submitted transaction must contain a nonce, which is obtained by the on-chain transaction number
|
||
|
# for this account, including pending transactions (I think ..;) ..
|
||
|
nonce = self._w3.eth.getTransactionCount(from_adr, block_identifier='pending')
|
||
|
self.log.info('{func}::[1/4] - Ethereum transaction nonce: nonce={nonce}',
|
||
|
func=hltype(self._send_Allowance),
|
||
|
nonce=nonce)
|
||
|
|
||
|
# serialize transaction raw data from contract call and transaction settings
|
||
|
raw_transaction = xbr.xbrtoken.functions.approve(to_adr, amount).buildTransaction({
|
||
|
'from': from_adr,
|
||
|
'gas': gas,
|
||
|
'gasPrice': gasPrice,
|
||
|
'chainId': self._chain_id, # https://stackoverflow.com/a/57901206/884770
|
||
|
'nonce': nonce,
|
||
|
})
|
||
|
self.log.info(
|
||
|
'{func}::[2/4] - Ethereum transaction created: raw_transaction=\n{raw_transaction}\n',
|
||
|
func=hltype(self._send_Allowance),
|
||
|
raw_transaction=raw_transaction)
|
||
|
|
||
|
# compute signed transaction from above serialized raw transaction
|
||
|
signed_txn = self._w3.eth.account.sign_transaction(raw_transaction, private_key=self._ethkey_raw)
|
||
|
self.log.info(
|
||
|
'{func}::[3/4] - Ethereum transaction signed: signed_txn=\n{signed_txn}\n',
|
||
|
func=hltype(self._send_Allowance),
|
||
|
signed_txn=hlval(binascii.b2a_hex(signed_txn.rawTransaction).decode()))
|
||
|
|
||
|
# now send the pre-signed transaction to the blockchain via the gateway ..
|
||
|
# https://web3py.readthedocs.io/en/stable/web3.eth.html # web3.eth.Eth.sendRawTransaction
|
||
|
txn_hash = self._w3.eth.sendRawTransaction(signed_txn.rawTransaction)
|
||
|
txn_hash = bytes(txn_hash)
|
||
|
self.log.info(
|
||
|
'{func}::[4/4] - Ethereum transaction submitted: txn_hash=0x{txn_hash}',
|
||
|
func=hltype(self._send_Allowance),
|
||
|
txn_hash=hlval(binascii.b2a_hex(txn_hash).decode()))
|
||
|
|
||
|
return txn_hash
|
||
|
|
||
|
|
||
|
def print_version():
|
||
|
print('')
|
||
|
print(' XBR CLI {}\n'.format(hlval('v' + __version__)))
|
||
|
print('')
|
||
|
print(' Contract addresses:\n')
|
||
|
print(' XBRToken : {} [source: {}]'.format(hlid(XBR_DEBUG_TOKEN_ADDR), XBR_DEBUG_TOKEN_ADDR_SRC))
|
||
|
print(' XBRNetwork : {} [source: {}]'.format(hlid(XBR_DEBUG_NETWORK_ADDR), XBR_DEBUG_NETWORK_ADDR_SRC))
|
||
|
print(' XBRDomain : {} [source: {}]'.format(hlid(XBR_DEBUG_DOMAIN_ADDR), XBR_DEBUG_DOMAIN_ADDR_SRC))
|
||
|
print(' XBRCatalog : {} [source: {}]'.format(hlid(XBR_DEBUG_CATALOG_ADDR), XBR_DEBUG_CATALOG_ADDR_SRC))
|
||
|
print(' XBRMarket : {} [source: {}]'.format(hlid(XBR_DEBUG_MARKET_ADDR), XBR_DEBUG_MARKET_ADDR_SRC))
|
||
|
print(' XBRChannel : {} [source: {}]'.format(hlid(XBR_DEBUG_CHANNEL_ADDR), XBR_DEBUG_CHANNEL_ADDR_SRC))
|
||
|
print('')
|
||
|
|
||
|
|
||
|
def _main():
|
||
|
parser = argparse.ArgumentParser()
|
||
|
|
||
|
parser.add_argument('command',
|
||
|
type=str,
|
||
|
choices=_COMMANDS,
|
||
|
const='noop',
|
||
|
nargs='?',
|
||
|
help='Command to run')
|
||
|
|
||
|
parser.add_argument('-d',
|
||
|
'--debug',
|
||
|
action='store_true',
|
||
|
help='Enable debug output.')
|
||
|
|
||
|
parser.add_argument('-o',
|
||
|
'--output',
|
||
|
type=str,
|
||
|
help='Code output folder')
|
||
|
|
||
|
parser.add_argument('-s',
|
||
|
'--schema',
|
||
|
dest='schema',
|
||
|
type=str,
|
||
|
help='FlatBuffers binary schema file to read (.bfbs)')
|
||
|
|
||
|
parser.add_argument('-b',
|
||
|
'--basemodule',
|
||
|
dest='basemodule',
|
||
|
type=str,
|
||
|
help='Render to this base module')
|
||
|
|
||
|
_LANGUAGES = ['python', 'json']
|
||
|
parser.add_argument('-l',
|
||
|
'--language',
|
||
|
dest='language',
|
||
|
type=str,
|
||
|
help='Generated code language, one of {}'.format(_LANGUAGES))
|
||
|
|
||
|
parser.add_argument('--url',
|
||
|
dest='url',
|
||
|
type=str,
|
||
|
default='wss://planet.xbr.network/ws',
|
||
|
help='The router URL (default: "wss://planet.xbr.network/ws").')
|
||
|
|
||
|
parser.add_argument('--realm',
|
||
|
dest='realm',
|
||
|
type=str,
|
||
|
default='xbrnetwork',
|
||
|
help='The realm to join (default: "xbrnetwork").')
|
||
|
|
||
|
parser.add_argument('--ethkey',
|
||
|
dest='ethkey',
|
||
|
type=str,
|
||
|
help='Private Ethereum key (32 bytes as HEX encoded string)')
|
||
|
|
||
|
parser.add_argument('--cskey',
|
||
|
dest='cskey',
|
||
|
type=str,
|
||
|
help='Private WAMP-cryptosign authentication key (32 bytes as HEX encoded string)')
|
||
|
|
||
|
parser.add_argument('--username',
|
||
|
dest='username',
|
||
|
type=str,
|
||
|
default=None,
|
||
|
help='For on-boarding, the new member username.')
|
||
|
|
||
|
parser.add_argument('--email',
|
||
|
dest='email',
|
||
|
type=str,
|
||
|
default=None,
|
||
|
help='For on-boarding, the new member email address.')
|
||
|
|
||
|
parser.add_argument('--market',
|
||
|
dest='market',
|
||
|
type=str,
|
||
|
default=None,
|
||
|
help='For creating new markets, the market UUID.')
|
||
|
|
||
|
parser.add_argument('--market_title',
|
||
|
dest='market_title',
|
||
|
type=str,
|
||
|
default=None,
|
||
|
help='For creating new markets, the market title.')
|
||
|
|
||
|
parser.add_argument('--market_label',
|
||
|
dest='market_label',
|
||
|
type=str,
|
||
|
default=None,
|
||
|
help='For creating new markets, the market label.')
|
||
|
|
||
|
parser.add_argument('--market_homepage',
|
||
|
dest='market_homepage',
|
||
|
type=str,
|
||
|
default=None,
|
||
|
help='For creating new markets, the market homepage.')
|
||
|
|
||
|
parser.add_argument('--provider_security',
|
||
|
dest='provider_security',
|
||
|
type=int,
|
||
|
default=None,
|
||
|
help='')
|
||
|
|
||
|
parser.add_argument('--consumer_security',
|
||
|
dest='consumer_security',
|
||
|
type=int,
|
||
|
default=None,
|
||
|
help='')
|
||
|
|
||
|
parser.add_argument('--market_fee',
|
||
|
dest='market_fee',
|
||
|
type=int,
|
||
|
default=None,
|
||
|
help='')
|
||
|
|
||
|
parser.add_argument('--marketmaker',
|
||
|
dest='marketmaker',
|
||
|
type=str,
|
||
|
default=None,
|
||
|
help='For creating new markets, the market maker address.')
|
||
|
|
||
|
parser.add_argument('--actor_type',
|
||
|
dest='actor_type',
|
||
|
type=int,
|
||
|
choices=sorted([ActorType.CONSUMER, ActorType.PROVIDER, ActorType.PROVIDER_CONSUMER]),
|
||
|
default=None,
|
||
|
help='Actor type: PROVIDER = 1, CONSUMER = 2, PROVIDER_CONSUMER (both) = 3')
|
||
|
|
||
|
parser.add_argument('--vcode',
|
||
|
dest='vcode',
|
||
|
type=str,
|
||
|
default=None,
|
||
|
help='For verifications of actions, the verification UUID.')
|
||
|
|
||
|
parser.add_argument('--vaction',
|
||
|
dest='vaction',
|
||
|
type=str,
|
||
|
default=None,
|
||
|
help='For verifications of actions (on-board, create-market, ..), the verification code.')
|
||
|
|
||
|
parser.add_argument('--channel',
|
||
|
dest='channel',
|
||
|
type=str,
|
||
|
default=None,
|
||
|
help='For creating new channel, the channel UUID.')
|
||
|
|
||
|
parser.add_argument('--channel_type',
|
||
|
dest='channel_type',
|
||
|
type=int,
|
||
|
choices=sorted([ChannelType.PAYING, ChannelType.PAYMENT]),
|
||
|
default=None,
|
||
|
help='Channel type: Seller (PAYING) = 1, Buyer (PAYMENT) = 2')
|
||
|
|
||
|
parser.add_argument('--delegate',
|
||
|
dest='delegate',
|
||
|
type=str,
|
||
|
default=None,
|
||
|
help='For creating new channel, the delegate address.')
|
||
|
|
||
|
parser.add_argument('--amount',
|
||
|
dest='amount',
|
||
|
type=int,
|
||
|
default=None,
|
||
|
help='Amount to open the channel with. In tokens of the market coin type, used as means of payment in the market of the channel.')
|
||
|
|
||
|
args = parser.parse_args()
|
||
|
|
||
|
if args.command == 'version':
|
||
|
print_version()
|
||
|
|
||
|
elif args.command == 'describe-schema':
|
||
|
schema = FbsSchema.load(args.schema)
|
||
|
obj = schema.marshal()
|
||
|
data = json.dumps(obj,
|
||
|
separators=(',', ':'),
|
||
|
ensure_ascii=False,
|
||
|
sort_keys=False, )
|
||
|
print('json data generated ({} bytes)'.format(len(data)))
|
||
|
for svc_key, svc in schema.services.items():
|
||
|
print('API "{}"'.format(svc_key))
|
||
|
for uri in sorted(svc.calls.keys()):
|
||
|
ep = svc.calls[uri]
|
||
|
ep_type = ep.attrs['type']
|
||
|
print(' {:<10} {:<26}: {}'.format(ep_type, ep.name, ep.docs))
|
||
|
for obj_name, obj in schema.objs.items():
|
||
|
print(obj_name)
|
||
|
|
||
|
# generate code from WAMP IDL FlatBuffers schema files
|
||
|
#
|
||
|
elif args.command == 'codegen-schema':
|
||
|
|
||
|
# load repository from flatbuffers schema files
|
||
|
repo = FbsRepository(render_to_basemodule=args.basemodule)
|
||
|
repo.load(args.schema)
|
||
|
|
||
|
# print repository summary
|
||
|
pprint(repo.summary(keys=True))
|
||
|
|
||
|
# folder with jinja2 templates for python code sections
|
||
|
templates = pkg_resources.resource_filename('autobahn', 'xbr/templates')
|
||
|
|
||
|
# jinja2 template engine loader and environment
|
||
|
loader = FileSystemLoader(templates, encoding='utf-8', followlinks=False)
|
||
|
env = Environment(loader=loader, trim_blocks=True, lstrip_blocks=True)
|
||
|
|
||
|
# output directory for generated code
|
||
|
if not os.path.isdir(args.output):
|
||
|
os.mkdir(args.output)
|
||
|
|
||
|
# type categories in schemata in the repository
|
||
|
#
|
||
|
work = {
|
||
|
'obj': repo.objs.values(),
|
||
|
'enum': repo.enums.values(),
|
||
|
'service': repo.services.values(),
|
||
|
}
|
||
|
|
||
|
# collect code sections by module
|
||
|
#
|
||
|
code_modules = {}
|
||
|
test_code_modules = {}
|
||
|
is_first_by_category_modules = {}
|
||
|
|
||
|
for category, values in work.items():
|
||
|
# generate and collect code for all FlatBuffers items in the given category
|
||
|
# and defined in schemata previously loaded int
|
||
|
|
||
|
for item in values:
|
||
|
# metadata = item.marshal()
|
||
|
# pprint(item.marshal())
|
||
|
metadata = item
|
||
|
|
||
|
# com.example.device.HomeDeviceVendor => com.example.device
|
||
|
modulename = '.'.join(metadata.name.split('.')[0:-1])
|
||
|
metadata.modulename = modulename
|
||
|
|
||
|
# com.example.device.HomeDeviceVendor => HomeDeviceVendor
|
||
|
metadata.classname = metadata.name.split('.')[-1].strip()
|
||
|
|
||
|
# com.example.device => device
|
||
|
metadata.module_relimport = modulename.split('.')[-1]
|
||
|
|
||
|
is_first = modulename not in code_modules
|
||
|
is_first_by_category = (modulename, category) not in is_first_by_category_modules
|
||
|
|
||
|
if is_first_by_category:
|
||
|
is_first_by_category_modules[(modulename, category)] = True
|
||
|
|
||
|
# render template into python code section
|
||
|
if args.language == 'python':
|
||
|
# render obj|enum|service.py.jinja2 template
|
||
|
tmpl = env.get_template('{}.py.jinja2'.format(category))
|
||
|
code = tmpl.render(repo=repo, metadata=metadata, FbsType=FbsType,
|
||
|
render_imports=is_first,
|
||
|
is_first_by_category=is_first_by_category,
|
||
|
render_to_basemodule=args.basemodule)
|
||
|
code = FormatCode(code)[0]
|
||
|
|
||
|
# render test_obj|enum|service.py.jinja2 template
|
||
|
test_tmpl = env.get_template('test_{}.py.jinja2'.format(category))
|
||
|
test_code = test_tmpl.render(repo=repo, metadata=metadata, FbsType=FbsType,
|
||
|
render_imports=is_first,
|
||
|
is_first_by_category=is_first_by_category,
|
||
|
render_to_basemodule=args.basemodule)
|
||
|
try:
|
||
|
test_code = FormatCode(test_code)[0]
|
||
|
except Exception as e:
|
||
|
print('error during formatting code:\n{}\n{}'.format(test_code, e))
|
||
|
|
||
|
elif args.language == 'json':
|
||
|
code = json.dumps(metadata.marshal(),
|
||
|
separators=(', ', ': '),
|
||
|
ensure_ascii=False,
|
||
|
indent=4,
|
||
|
sort_keys=True)
|
||
|
test_code = None
|
||
|
else:
|
||
|
raise RuntimeError('invalid language "{}" for code generation'.format(args.languages))
|
||
|
|
||
|
# collect code sections per-module
|
||
|
if modulename not in code_modules:
|
||
|
code_modules[modulename] = []
|
||
|
test_code_modules[modulename] = []
|
||
|
code_modules[modulename].append(code)
|
||
|
if test_code:
|
||
|
test_code_modules[modulename].append(test_code)
|
||
|
else:
|
||
|
test_code_modules[modulename].append(None)
|
||
|
|
||
|
# ['', 'com.example.bla.blub', 'com.example.doo']
|
||
|
namespaces = {}
|
||
|
for code_file in code_modules.keys():
|
||
|
name_parts = code_file.split('.')
|
||
|
for i in range(len(name_parts)):
|
||
|
pn = name_parts[i]
|
||
|
ns = '.'.join(name_parts[:i])
|
||
|
if ns not in namespaces:
|
||
|
namespaces[ns] = []
|
||
|
if pn and pn not in namespaces[ns]:
|
||
|
namespaces[ns].append(pn)
|
||
|
|
||
|
print('Namespaces:\n{}\n'.format(pformat(namespaces)))
|
||
|
|
||
|
# write out code modules
|
||
|
#
|
||
|
i = 0
|
||
|
initialized = set()
|
||
|
for code_file, code_sections in code_modules.items():
|
||
|
code = '\n\n\n'.join(code_sections)
|
||
|
if code_file:
|
||
|
code_file_dir = [''] + code_file.split('.')[0:-1]
|
||
|
else:
|
||
|
code_file_dir = ['']
|
||
|
|
||
|
# FIXME: cleanup this mess
|
||
|
for i in range(len(code_file_dir)):
|
||
|
d = os.path.join(args.output, *(code_file_dir[:i + 1]))
|
||
|
if not os.path.isdir(d):
|
||
|
os.mkdir(d)
|
||
|
if args.language == 'python':
|
||
|
fn = os.path.join(d, '__init__.py')
|
||
|
|
||
|
_modulename = '.'.join(code_file_dir[:i + 1])[1:]
|
||
|
_imports = namespaces[_modulename]
|
||
|
tmpl = env.get_template('module.py.jinja2')
|
||
|
init_code = tmpl.render(repo=repo, modulename=_modulename, imports=_imports,
|
||
|
render_to_basemodule=args.basemodule)
|
||
|
data = init_code.encode('utf8')
|
||
|
|
||
|
if not os.path.exists(fn):
|
||
|
with open(fn, 'wb') as f:
|
||
|
f.write(data)
|
||
|
print('Ok, rendered "module.py.jinja2" in {} bytes to "{}"'.format(len(data), fn))
|
||
|
initialized.add(fn)
|
||
|
else:
|
||
|
with open(fn, 'ab') as f:
|
||
|
f.write(data)
|
||
|
|
||
|
if args.language == 'python':
|
||
|
if code_file:
|
||
|
code_file_name = '{}.py'.format(code_file.split('.')[-1])
|
||
|
test_code_file_name = 'test_{}.py'.format(code_file.split('.')[-1])
|
||
|
else:
|
||
|
code_file_name = '__init__.py'
|
||
|
test_code_file_name = None
|
||
|
elif args.language == 'json':
|
||
|
if code_file:
|
||
|
code_file_name = '{}.json'.format(code_file.split('.')[-1])
|
||
|
else:
|
||
|
code_file_name = 'init.json'
|
||
|
test_code_file_name = None
|
||
|
else:
|
||
|
code_file_name = None
|
||
|
test_code_file_name = None
|
||
|
|
||
|
# write out code modules
|
||
|
#
|
||
|
if code_file_name:
|
||
|
data = code.encode('utf8')
|
||
|
|
||
|
fn = os.path.join(*(code_file_dir + [code_file_name]))
|
||
|
fn = os.path.join(args.output, fn)
|
||
|
|
||
|
# FIXME
|
||
|
# if fn not in initialized and os.path.exists(fn):
|
||
|
# os.remove(fn)
|
||
|
# with open(fn, 'wb') as fd:
|
||
|
# fd.write('# Generated by Autobahn v{}\n'.format(__version__).encode('utf8'))
|
||
|
# initialized.add(fn)
|
||
|
|
||
|
with open(fn, 'ab') as fd:
|
||
|
fd.write(data)
|
||
|
|
||
|
print('Ok, written {} bytes to {}'.format(len(data), fn))
|
||
|
|
||
|
# write out unit test code modules
|
||
|
#
|
||
|
if test_code_file_name:
|
||
|
test_code_sections = test_code_modules[code_file]
|
||
|
test_code = '\n\n\n'.join(test_code_sections)
|
||
|
data = test_code.encode('utf8')
|
||
|
|
||
|
fn = os.path.join(*(code_file_dir + [test_code_file_name]))
|
||
|
fn = os.path.join(args.output, fn)
|
||
|
|
||
|
if fn not in initialized and os.path.exists(fn):
|
||
|
os.remove(fn)
|
||
|
with open(fn, 'wb') as fd:
|
||
|
fd.write('# Copyright (c) ...'.encode('utf8'))
|
||
|
initialized.add(fn)
|
||
|
|
||
|
with open(fn, 'ab') as fd:
|
||
|
fd.write(data)
|
||
|
|
||
|
print('Ok, written {} bytes to {}'.format(len(data), fn))
|
||
|
else:
|
||
|
if args.command is None or args.command == 'noop':
|
||
|
print('no command given. select from: {}'.format(', '.join(_COMMANDS)))
|
||
|
sys.exit(0)
|
||
|
|
||
|
# read or create a user profile
|
||
|
profile = load_or_create_profile(default_url=args.url, default_realm=args.realm,
|
||
|
default_username=args.username, default_email=args.email)
|
||
|
|
||
|
# only start txaio logging after above, which runs click (interactively)
|
||
|
if args.debug:
|
||
|
txaio.start_logging(level='debug')
|
||
|
else:
|
||
|
txaio.start_logging(level='info')
|
||
|
|
||
|
log = txaio.make_logger()
|
||
|
|
||
|
log.info('XBR CLI {version}', version=hlid('v' + __version__))
|
||
|
log.info('Profile {profile} loaded from {path}', profile=hlval(profile.name), path=hlval(profile.path))
|
||
|
|
||
|
extra = {
|
||
|
# user profile and defaults
|
||
|
'profile': profile,
|
||
|
|
||
|
# allow to override, and add more arguments from the command line
|
||
|
'command': args.command,
|
||
|
'username': args.username,
|
||
|
'email': args.email,
|
||
|
|
||
|
'ethkey': profile.ethkey,
|
||
|
'cskey': profile.cskey,
|
||
|
|
||
|
'market': uuid.UUID(args.market) if args.market else None,
|
||
|
'market_title': args.market_title,
|
||
|
'market_label': args.market_label,
|
||
|
'market_homepage': args.market_homepage,
|
||
|
'market_provider_security': args.provider_security or 0,
|
||
|
'market_consumer_security': args.consumer_security or 0,
|
||
|
'market_fee': args.market_fee or 0,
|
||
|
'marketmaker': binascii.a2b_hex(args.marketmaker[2:]) if args.marketmaker else None,
|
||
|
'actor_type': args.actor_type,
|
||
|
'vcode': args.vcode,
|
||
|
'vaction': uuid.UUID(args.vaction) if args.vaction else None,
|
||
|
'channel': uuid.UUID(args.channel) if args.channel else None,
|
||
|
'channel_type': args.channel_type,
|
||
|
'delegate': binascii.a2b_hex(args.delegate[2:]) if args.delegate else None,
|
||
|
'amount': args.amount or 0,
|
||
|
}
|
||
|
runner = ApplicationRunner(url=profile.network_url, realm=profile.network_realm, extra=extra, serializers=[CBORSerializer()])
|
||
|
|
||
|
try:
|
||
|
log.info('Connecting to "{url}" {realm} ..',
|
||
|
url=hlval(profile.network_url), realm=('at realm "' + hlval(profile.network_realm) + '"' if profile.network_realm else ''))
|
||
|
runner.run(Client, auto_reconnect=False)
|
||
|
except Exception as e:
|
||
|
print(e)
|
||
|
sys.exit(1)
|
||
|
else:
|
||
|
sys.exit(0)
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
_main()
|