RAHHH
This commit is contained in:
450
backend/lib/python3.12/site-packages/autobahn/xbr/__init__.py
Normal file
450
backend/lib/python3.12/site-packages/autobahn/xbr/__init__.py
Normal file
@@ -0,0 +1,450 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 traceback
|
||||
|
||||
try:
|
||||
# PyPy 7.3.3 lacks this
|
||||
from _hashlib import HASH # noqa: F401
|
||||
except ImportError:
|
||||
# monkey patch it:
|
||||
import _hashlib
|
||||
_hashlib.HASH = _hashlib.Hash
|
||||
|
||||
try:
|
||||
from mnemonic import Mnemonic
|
||||
from autobahn.xbr._mnemonic import mnemonic_to_private_key
|
||||
|
||||
# monkey patch eth_abi for master branch (which we need for python 3.11)
|
||||
# https://github.com/ethereum/eth-abi/blob/master/docs/release_notes.rst#breaking-changes
|
||||
# https://github.com/ethereum/eth-abi/pull/161
|
||||
# ImportError: cannot import name 'encode_single' from 'eth_abi' (/home/oberstet/cpy311_2/lib/python3.11/site-packages/eth_abi/__init__.py)
|
||||
import eth_abi
|
||||
|
||||
if not hasattr(eth_abi, 'encode_abi') and hasattr(eth_abi, 'encode'):
|
||||
eth_abi.encode_abi = eth_abi.encode
|
||||
if not hasattr(eth_abi, 'encode_single') and hasattr(eth_abi, 'encode'):
|
||||
eth_abi.encode_single = lambda typ, val: eth_abi.encode([typ], [val])
|
||||
|
||||
# monkey patch, see:
|
||||
# https://github.com/ethereum/web3.py/issues/1201
|
||||
# https://github.com/ethereum/eth-abi/pull/88
|
||||
if not hasattr(eth_abi, 'collapse_type'):
|
||||
|
||||
def collapse_type(base, sub, arrlist):
|
||||
return base + sub + ''.join(map(repr, arrlist))
|
||||
|
||||
eth_abi.collapse_type = collapse_type
|
||||
|
||||
if not hasattr(eth_abi, 'process_type'):
|
||||
from eth_abi.grammar import (
|
||||
TupleType,
|
||||
normalize,
|
||||
parse,
|
||||
)
|
||||
|
||||
def process_type(type_str):
|
||||
normalized_type_str = normalize(type_str)
|
||||
abi_type = parse(normalized_type_str)
|
||||
|
||||
type_str_repr = repr(type_str)
|
||||
if type_str != normalized_type_str:
|
||||
type_str_repr = '{} (normalized to {})'.format(
|
||||
type_str_repr,
|
||||
repr(normalized_type_str),
|
||||
)
|
||||
|
||||
if isinstance(abi_type, TupleType):
|
||||
raise ValueError("Cannot process type {}: tuple types not supported".format(type_str_repr, ))
|
||||
|
||||
abi_type.validate()
|
||||
|
||||
sub = abi_type.sub
|
||||
if isinstance(sub, tuple):
|
||||
sub = 'x'.join(map(str, sub))
|
||||
elif isinstance(sub, int):
|
||||
sub = str(sub)
|
||||
else:
|
||||
sub = ''
|
||||
|
||||
arrlist = abi_type.arrlist
|
||||
if isinstance(arrlist, tuple):
|
||||
arrlist = list(map(list, arrlist))
|
||||
else:
|
||||
arrlist = []
|
||||
|
||||
return abi_type.base, sub, arrlist
|
||||
|
||||
eth_abi.process_type = process_type
|
||||
|
||||
# monkey patch web3 for master branch / upcoming v6 (which we need for python 3.11)
|
||||
# AttributeError: type object 'Web3' has no attribute 'toChecksumAddress'. Did you mean: 'to_checksum_address'?
|
||||
import web3
|
||||
if not hasattr(web3.Web3, 'toChecksumAddress') and hasattr(web3.Web3, 'to_checksum_address'):
|
||||
web3.Web3.toChecksumAddress = web3.Web3.to_checksum_address
|
||||
if not hasattr(web3.Web3, 'isChecksumAddress') and hasattr(web3.Web3, 'is_checksum_address'):
|
||||
web3.Web3.isChecksumAddress = web3.Web3.is_checksum_address
|
||||
if not hasattr(web3.Web3, 'isConnected') and hasattr(web3.Web3, 'is_connected'):
|
||||
web3.Web3.isConnected = web3.Web3.is_connected
|
||||
if not hasattr(web3.Web3, 'privateKeyToAccount') and hasattr(web3.middleware.signing, 'private_key_to_account'):
|
||||
web3.Web3.privateKeyToAccount = web3.middleware.signing.private_key_to_account
|
||||
|
||||
import ens
|
||||
if not hasattr(ens, 'main') and hasattr(ens, 'ens'):
|
||||
ens.main = ens.ens
|
||||
|
||||
import eth_account
|
||||
|
||||
from autobahn.xbr._abi import XBR_TOKEN_ABI, XBR_NETWORK_ABI, XBR_MARKET_ABI, XBR_CATALOG_ABI, XBR_CHANNEL_ABI # noqa
|
||||
from autobahn.xbr._abi import XBR_DEBUG_TOKEN_ADDR, XBR_DEBUG_NETWORK_ADDR, XBR_DEBUG_MARKET_ADDR, XBR_DEBUG_CATALOG_ADDR, XBR_DEBUG_CHANNEL_ADDR # noqa
|
||||
from autobahn.xbr._abi import XBR_DEBUG_TOKEN_ADDR_SRC, XBR_DEBUG_NETWORK_ADDR_SRC, XBR_DEBUG_MARKET_ADDR_SRC, XBR_DEBUG_CATALOG_ADDR_SRC, XBR_DEBUG_CHANNEL_ADDR_SRC # noqa
|
||||
from autobahn.xbr._interfaces import IMarketMaker, IProvider, IConsumer, ISeller, IBuyer, IDelegate # noqa
|
||||
from autobahn.xbr._util import make_w3, pack_uint256, unpack_uint256 # noqa
|
||||
from autobahn.xbr._eip712_certificate import EIP712Certificate # noqa
|
||||
from autobahn.xbr._eip712_certificate_chain import parse_certificate_chain # noqa
|
||||
from autobahn.xbr._eip712_authority_certificate import sign_eip712_authority_certificate, \
|
||||
recover_eip712_authority_certificate, create_eip712_authority_certificate, EIP712AuthorityCertificate # noqa
|
||||
from autobahn.xbr._eip712_delegate_certificate import sign_eip712_delegate_certificate, \
|
||||
recover_eip712_delegate_certificate, create_eip712_delegate_certificate, EIP712DelegateCertificate # noqa
|
||||
from autobahn.xbr._eip712_member_register import sign_eip712_member_register, recover_eip712_member_register # noqa
|
||||
from autobahn.xbr._eip712_member_login import sign_eip712_member_login, recover_eip712_member_login # noqa
|
||||
from autobahn.xbr._eip712_market_create import sign_eip712_market_create, recover_eip712_market_create # noqa
|
||||
from autobahn.xbr._eip712_market_join import sign_eip712_market_join, recover_eip712_market_join # noqa
|
||||
from autobahn.xbr._eip712_catalog_create import sign_eip712_catalog_create, recover_eip712_catalog_create # noqa
|
||||
from autobahn.xbr._eip712_api_publish import sign_eip712_api_publish, recover_eip712_api_publish # noqa
|
||||
from autobahn.xbr._eip712_consent import sign_eip712_consent, recover_eip712_consent # noqa
|
||||
from autobahn.xbr._eip712_channel_open import sign_eip712_channel_open, recover_eip712_channel_open # noqa
|
||||
from autobahn.xbr._eip712_channel_close import sign_eip712_channel_close, recover_eip712_channel_close # noqa
|
||||
from autobahn.xbr._eip712_market_member_login import sign_eip712_market_member_login, \
|
||||
recover_eip712_market_member_login # noqa
|
||||
from autobahn.xbr._eip712_base import is_address, is_chain_id, is_block_number, is_signature, \
|
||||
is_cs_pubkey, is_bytes16, is_eth_privkey # noqa
|
||||
from autobahn.xbr._blockchain import SimpleBlockchain # noqa
|
||||
from autobahn.xbr._seller import SimpleSeller, KeySeries # noqa
|
||||
from autobahn.xbr._buyer import SimpleBuyer # noqa
|
||||
from autobahn.xbr._config import load_or_create_profile, UserConfig, Profile # noqa
|
||||
from autobahn.xbr._schema import FbsSchema, FbsObject, FbsType, FbsRPCCall, FbsEnum, FbsService, FbsEnumValue, \
|
||||
FbsAttribute, FbsField, FbsRepository # noqa
|
||||
from autobahn.xbr._wallet import stretch_argon2_secret, expand_argon2_secret, pkm_from_argon2_secret # noqa
|
||||
from autobahn.xbr._frealm import FederatedRealm, Seeder # noqa
|
||||
from autobahn.xbr._secmod import EthereumKey, SecurityModuleMemory # noqa
|
||||
from autobahn.xbr._userkey import UserKey # noqa
|
||||
|
||||
HAS_XBR = True
|
||||
|
||||
xbrtoken = None
|
||||
"""
|
||||
Contract instance of XBRToken.
|
||||
"""
|
||||
|
||||
xbrnetwork = None
|
||||
"""
|
||||
Contract instance of XBRNetwork.
|
||||
"""
|
||||
|
||||
xbrmarket = None
|
||||
"""
|
||||
Contract instance of XBRMarket.
|
||||
"""
|
||||
|
||||
xbrcatalog = None
|
||||
"""
|
||||
Contract instance of XBRMarket.
|
||||
"""
|
||||
|
||||
xbrchannel = None
|
||||
"""
|
||||
Contract instance of XBRMarket.
|
||||
"""
|
||||
|
||||
def setProvider(_w3):
|
||||
"""
|
||||
The XBR library must be initialized (once) first by setting the Web3 provider
|
||||
using this function.
|
||||
"""
|
||||
global xbrtoken
|
||||
global xbrnetwork
|
||||
global xbrmarket
|
||||
global xbrcatalog
|
||||
global xbrchannel
|
||||
|
||||
# print('Provider set - xbrtoken={}'.format(XBR_DEBUG_TOKEN_ADDR))
|
||||
xbrtoken = _w3.eth.contract(address=XBR_DEBUG_TOKEN_ADDR, abi=XBR_TOKEN_ABI)
|
||||
|
||||
# print('Provider set - xbrnetwork={}'.format(XBR_DEBUG_NETWORK_ADDR))
|
||||
xbrnetwork = _w3.eth.contract(address=XBR_DEBUG_NETWORK_ADDR, abi=XBR_NETWORK_ABI)
|
||||
|
||||
# print('Provider set - xbrmarket={}'.format(XBR_DEBUG_MARKET_ADDR))
|
||||
xbrmarket = _w3.eth.contract(address=XBR_DEBUG_MARKET_ADDR, abi=XBR_MARKET_ABI)
|
||||
|
||||
# print('Provider set - xbrcatalog={}'.format(XBR_DEBUG_CATALOG_ADDR))
|
||||
xbrcatalog = _w3.eth.contract(address=XBR_DEBUG_CATALOG_ADDR, abi=XBR_CATALOG_ABI)
|
||||
|
||||
# print('Provider set - xbrchannel={}'.format(XBR_DEBUG_CHANNEL_ADDR))
|
||||
xbrchannel = _w3.eth.contract(address=XBR_DEBUG_CHANNEL_ADDR, abi=XBR_CHANNEL_ABI)
|
||||
|
||||
class MemberLevel(object):
|
||||
"""
|
||||
XBR Network member levels.
|
||||
"""
|
||||
NONE = 0
|
||||
ACTIVE = 1
|
||||
VERIFIED = 2
|
||||
RETIRED = 3
|
||||
PENALTY = 4
|
||||
BLOCKED = 5
|
||||
|
||||
class NodeType(object):
|
||||
"""
|
||||
XBR Cloud node types.
|
||||
"""
|
||||
NONE = 0
|
||||
MASTER = 1
|
||||
CORE = 2
|
||||
EDGE = 3
|
||||
|
||||
class ChannelType(object):
|
||||
"""
|
||||
Type of a XBR off-chain channel: paying channel (for provider delegates selling data services) or payment channel (for consumer delegates buying data services).
|
||||
"""
|
||||
|
||||
NONE = 0
|
||||
"""
|
||||
Unset
|
||||
"""
|
||||
|
||||
PAYMENT = 1
|
||||
"""
|
||||
Payment channel: from buyer/consumer delegate to maker maker.
|
||||
"""
|
||||
|
||||
PAYING = 2
|
||||
"""
|
||||
Paying channel: from market maker to seller/provider delegate.
|
||||
"""
|
||||
|
||||
class ActorType(object):
|
||||
"""
|
||||
XBR Market Actor type.
|
||||
"""
|
||||
|
||||
NONE = 0
|
||||
"""
|
||||
Unset
|
||||
"""
|
||||
|
||||
PROVIDER = 1
|
||||
"""
|
||||
Actor is a XBR Provider.
|
||||
"""
|
||||
|
||||
CONSUMER = 2
|
||||
"""
|
||||
Actor is a XBR Consumer.
|
||||
"""
|
||||
|
||||
PROVIDER_CONSUMER = 3
|
||||
"""
|
||||
Actor is both a XBR Provider and XBR Consumer.
|
||||
"""
|
||||
|
||||
def generate_seedphrase(strength=128, language='english') -> str:
|
||||
"""
|
||||
Generate a new BIP-39 mnemonic seed phrase for use in Ethereum (Metamask, etc).
|
||||
|
||||
See:
|
||||
* https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
|
||||
* https://github.com/trezor/python-mnemonic
|
||||
|
||||
:param strength: Strength of seed phrase in bits, one of the following ``[128, 160, 192, 224, 256]``,
|
||||
generating seed phrase of 12 - 24 words inlength.
|
||||
|
||||
:return: Newly generated seed phrase (in english).
|
||||
"""
|
||||
return Mnemonic(language).generate(strength)
|
||||
|
||||
def check_seedphrase(seedphrase: str, language: str = 'english'):
|
||||
"""
|
||||
Check a BIP-39 mnemonic seed phrase.
|
||||
|
||||
:param seedphrase: The BIP-39 seedphrase from which to derive the account.
|
||||
:param language: The BIP-39 user language to generate the seedphrase for.
|
||||
:return:
|
||||
"""
|
||||
return Mnemonic(language).check(seedphrase)
|
||||
|
||||
def account_from_seedphrase(seedphrase: str, index: int = 0) -> eth_account.account.Account:
|
||||
"""
|
||||
Create an account from the given BIP-39 mnemonic seed phrase.
|
||||
|
||||
:param seedphrase: The BIP-39 seedphrase from which to derive the account.
|
||||
:param index: The account index in account hierarchy defined by the seedphrase.
|
||||
:return: The new Eth account object
|
||||
"""
|
||||
from web3.middleware.signing import private_key_to_account
|
||||
|
||||
derivation_path = "m/44'/60'/0'/0/{}".format(index)
|
||||
key = mnemonic_to_private_key(seedphrase, str_derivation_path=derivation_path)
|
||||
account = private_key_to_account(key)
|
||||
return account
|
||||
|
||||
def account_from_ethkey(ethkey: bytes) -> eth_account.account.Account:
|
||||
"""
|
||||
Create an account from the private key seed.
|
||||
|
||||
:param ethkey: The Ethereum private key seed (32 octets).
|
||||
:return: The new Eth account object
|
||||
"""
|
||||
from web3.middleware.signing import private_key_to_account
|
||||
|
||||
assert len(ethkey) == 32
|
||||
account = private_key_to_account(ethkey)
|
||||
return account
|
||||
|
||||
ASCII_BOMB = r"""
|
||||
_ ._ _ , _ ._
|
||||
(_ ' ( ` )_ .__)
|
||||
( ( ( ) `) ) _)
|
||||
(__ (_ (_ . _) _) ,__)
|
||||
`~~`\ ' . /`~~`
|
||||
; ;
|
||||
/ \
|
||||
_____________/_ __ \_____________
|
||||
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
'HAS_XBR',
|
||||
'XBR_TOKEN_ABI',
|
||||
'XBR_NETWORK_ABI',
|
||||
'XBR_MARKET_ABI',
|
||||
'XBR_CATALOG_ABI',
|
||||
'XBR_CHANNEL_ABI',
|
||||
'xbrtoken',
|
||||
'xbrnetwork',
|
||||
'xbrmarket',
|
||||
'xbrcatalog',
|
||||
'xbrchannel',
|
||||
|
||||
'setProvider',
|
||||
'make_w3',
|
||||
'pack_uint256',
|
||||
'unpack_uint256',
|
||||
'generate_seedphrase',
|
||||
'check_seedphrase',
|
||||
'account_from_seedphrase',
|
||||
'ASCII_BOMB',
|
||||
|
||||
'EIP712Certificate',
|
||||
'EIP712AuthorityCertificate',
|
||||
'EIP712DelegateCertificate',
|
||||
'parse_certificate_chain',
|
||||
|
||||
'create_eip712_authority_certificate',
|
||||
'sign_eip712_authority_certificate',
|
||||
'recover_eip712_authority_certificate',
|
||||
'create_eip712_delegate_certificate',
|
||||
'sign_eip712_delegate_certificate',
|
||||
'recover_eip712_delegate_certificate',
|
||||
|
||||
'sign_eip712_member_register',
|
||||
'recover_eip712_member_register',
|
||||
'sign_eip712_member_login',
|
||||
'recover_eip712_member_login',
|
||||
'sign_eip712_market_create',
|
||||
'recover_eip712_market_create',
|
||||
'sign_eip712_market_join',
|
||||
'recover_eip712_market_join',
|
||||
'sign_eip712_catalog_create',
|
||||
'recover_eip712_catalog_create',
|
||||
'sign_eip712_api_publish',
|
||||
'recover_eip712_api_publish',
|
||||
'sign_eip712_consent',
|
||||
'recover_eip712_consent',
|
||||
'sign_eip712_channel_open',
|
||||
'recover_eip712_channel_open',
|
||||
'sign_eip712_channel_close',
|
||||
'recover_eip712_channel_close',
|
||||
'sign_eip712_market_member_login',
|
||||
'recover_eip712_market_member_login',
|
||||
|
||||
'is_bytes16',
|
||||
'is_cs_pubkey',
|
||||
'is_signature',
|
||||
'is_chain_id',
|
||||
'is_eth_privkey',
|
||||
'is_block_number',
|
||||
'is_address',
|
||||
|
||||
'load_or_create_profile',
|
||||
'UserConfig',
|
||||
'Profile',
|
||||
'UserKey',
|
||||
|
||||
'MemberLevel',
|
||||
'ActorType',
|
||||
'ChannelType',
|
||||
'NodeType',
|
||||
|
||||
'KeySeries',
|
||||
'SimpleBlockchain',
|
||||
'SimpleSeller',
|
||||
'SimpleBuyer',
|
||||
|
||||
'IMarketMaker',
|
||||
'IProvider',
|
||||
'IConsumer',
|
||||
'ISeller',
|
||||
'IBuyer',
|
||||
'IDelegate',
|
||||
|
||||
'FbsRepository',
|
||||
'FbsSchema',
|
||||
'FbsService',
|
||||
'FbsType',
|
||||
'FbsObject',
|
||||
'FbsEnum',
|
||||
'FbsEnumValue',
|
||||
'FbsRPCCall',
|
||||
'FbsAttribute',
|
||||
'FbsField',
|
||||
'stretch_argon2_secret',
|
||||
'expand_argon2_secret',
|
||||
'pkm_from_argon2_secret',
|
||||
|
||||
'FederatedRealm',
|
||||
'Seeder',
|
||||
'EthereumKey',
|
||||
'SecurityModuleMemory',
|
||||
)
|
||||
|
||||
except (ImportError, FileNotFoundError) as e:
|
||||
import sys
|
||||
traceback.print_tb(e.__traceback__, file=sys.stderr)
|
||||
sys.stderr.write(str(e))
|
||||
sys.stderr.flush()
|
||||
HAS_XBR = False
|
||||
__all__ = ('HAS_XBR',)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
156
backend/lib/python3.12/site-packages/autobahn/xbr/_abi.py
Normal file
156
backend/lib/python3.12/site-packages/autobahn/xbr/_abi.py
Normal file
@@ -0,0 +1,156 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 json
|
||||
import binascii
|
||||
import pkg_resources
|
||||
|
||||
os.environ['ETH_HASH_BACKEND'] = 'pycryptodome'
|
||||
|
||||
# from eth_hash.backends.pycryptodome import keccak256 # noqa
|
||||
# print('Using eth_hash backend {}'.format(keccak256))
|
||||
import web3
|
||||
|
||||
import xbr
|
||||
|
||||
|
||||
XBR_ABI_VERSION = xbr.__version__
|
||||
"""
|
||||
XBR Protocol contracts ABI bundle (Python package) version.
|
||||
"""
|
||||
|
||||
|
||||
#
|
||||
# Set default XBR contract addresses to
|
||||
# XBR v20.5.1. @ Rinkeby (https://github.com/crossbario/xbr-protocol/issues/127)
|
||||
#
|
||||
if 'XBR_DEBUG_TOKEN_ADDR' in os.environ:
|
||||
_token_adr = os.environ['XBR_DEBUG_TOKEN_ADDR']
|
||||
try:
|
||||
_token_adr = binascii.a2b_hex(_token_adr[2:])
|
||||
_token_adr = web3.Web3.toChecksumAddress(_token_adr)
|
||||
except Exception as e:
|
||||
raise RuntimeError('could not parse Ethereum address for XBR_DEBUG_TOKEN_ADDR={} - {}'.format(_token_adr, e))
|
||||
XBR_DEBUG_TOKEN_ADDR = _token_adr
|
||||
XBR_DEBUG_TOKEN_ADDR_SRC = 'envvar $XBR_DEBUG_TOKEN_ADDR'
|
||||
else:
|
||||
XBR_DEBUG_TOKEN_ADDR = '0xaCef957D54c639575f4DB68b1992B36504f33FEA'
|
||||
XBR_DEBUG_TOKEN_ADDR_SRC = 'builtin'
|
||||
|
||||
if 'XBR_DEBUG_NETWORK_ADDR' in os.environ:
|
||||
_netw_adr = os.environ['XBR_DEBUG_NETWORK_ADDR']
|
||||
try:
|
||||
_netw_adr = binascii.a2b_hex(_netw_adr[2:])
|
||||
_netw_adr = web3.Web3.toChecksumAddress(_netw_adr)
|
||||
except Exception as e:
|
||||
raise RuntimeError('could not parse Ethereum address for XBR_DEBUG_NETWORK_ADDR={} - {}'.format(_netw_adr, e))
|
||||
XBR_DEBUG_NETWORK_ADDR = _netw_adr
|
||||
XBR_DEBUG_NETWORK_ADDR_SRC = 'envvar $XBR_DEBUG_NETWORK_ADDR'
|
||||
else:
|
||||
XBR_DEBUG_NETWORK_ADDR = '0x7A3d22c59e8F8f1b88ba7205f3f5a65Bc86D04Bc'
|
||||
XBR_DEBUG_NETWORK_ADDR_SRC = 'builtin'
|
||||
|
||||
if 'XBR_DEBUG_DOMAIN_ADDR' in os.environ:
|
||||
_domain_adr = os.environ['XBR_DEBUG_DOMAIN_ADDR']
|
||||
try:
|
||||
_domain_adr = binascii.a2b_hex(_domain_adr[2:])
|
||||
_domain_adr = web3.Web3.toChecksumAddress(_domain_adr)
|
||||
except Exception as e:
|
||||
raise RuntimeError('could not parse Ethereum address for XBR_DEBUG_DOMAIN_ADDR={} - {}'.format(_domain_adr, e))
|
||||
XBR_DEBUG_DOMAIN_ADDR = _domain_adr
|
||||
XBR_DEBUG_DOMAIN_ADDR_SRC = 'envvar $XBR_DEBUG_DOMAIN_ADDR'
|
||||
else:
|
||||
XBR_DEBUG_DOMAIN_ADDR = '0xf5fb56886f033855C1a36F651E927551749361bC'
|
||||
XBR_DEBUG_DOMAIN_ADDR_SRC = 'builtin'
|
||||
|
||||
if 'XBR_DEBUG_CATALOG_ADDR' in os.environ:
|
||||
_ctlg_adr = os.environ['XBR_DEBUG_CATALOG_ADDR']
|
||||
try:
|
||||
_ctlg_adr = binascii.a2b_hex(_ctlg_adr[2:])
|
||||
_ctlg_adr = web3.Web3.toChecksumAddress(_ctlg_adr)
|
||||
except Exception as e:
|
||||
raise RuntimeError('could not parse Ethereum address for XBR_DEBUG_CATALOG_ADDR={} - {}'.format(_ctlg_adr, e))
|
||||
XBR_DEBUG_CATALOG_ADDR = _ctlg_adr
|
||||
XBR_DEBUG_CATALOG_ADDR_SRC = 'envvar $XBR_DEBUG_CATALOG_ADDR'
|
||||
else:
|
||||
XBR_DEBUG_CATALOG_ADDR = '0x2C77E46Ea9502B363343e8c826c41c7fdb25Db66'
|
||||
XBR_DEBUG_CATALOG_ADDR_SRC = 'builtin'
|
||||
|
||||
if 'XBR_DEBUG_MARKET_ADDR' in os.environ:
|
||||
_mrkt_adr = os.environ['XBR_DEBUG_MARKET_ADDR']
|
||||
try:
|
||||
_mrkt_adr = binascii.a2b_hex(_mrkt_adr[2:])
|
||||
_mrkt_adr = web3.Web3.toChecksumAddress(_mrkt_adr)
|
||||
except Exception as e:
|
||||
raise RuntimeError('could not parse Ethereum address for XBR_DEBUG_MARKET_ADDR={} - {}'.format(_mrkt_adr, e))
|
||||
XBR_DEBUG_MARKET_ADDR = _mrkt_adr
|
||||
XBR_DEBUG_MARKET_ADDR_SRC = 'envvar $XBR_DEBUG_MARKET_ADDR'
|
||||
else:
|
||||
XBR_DEBUG_MARKET_ADDR = '0x0DcF924ab0846101d31514E9fb3adf5070d4B83d'
|
||||
XBR_DEBUG_MARKET_ADDR_SRC = 'builtin'
|
||||
|
||||
if 'XBR_DEBUG_CHANNEL_ADDR' in os.environ:
|
||||
_chnl_adr = os.environ['XBR_DEBUG_CHANNEL_ADDR']
|
||||
try:
|
||||
_chnl_adr = binascii.a2b_hex(_chnl_adr[2:])
|
||||
_chnl_adr = web3.Web3.toChecksumAddress(_chnl_adr)
|
||||
except Exception as e:
|
||||
raise RuntimeError('could not parse Ethereum address for XBR_DEBUG_CHANNEL_ADDR={} - {}'.format(_chnl_adr, e))
|
||||
XBR_DEBUG_CHANNEL_ADDR = _chnl_adr
|
||||
XBR_DEBUG_CHANNEL_ADDR_SRC = 'envvar $XBR_DEBUG_CHANNEL_ADDR'
|
||||
else:
|
||||
XBR_DEBUG_CHANNEL_ADDR = '0x670497A012322B99a5C18B8463940996141Cb952'
|
||||
XBR_DEBUG_CHANNEL_ADDR_SRC = 'builtin'
|
||||
|
||||
|
||||
def _load_json(contract_name):
|
||||
fn = pkg_resources.resource_filename('xbr', 'abi/{}.json'.format(contract_name))
|
||||
with open(fn) as f:
|
||||
data = json.loads(f.read())
|
||||
return data
|
||||
|
||||
|
||||
#
|
||||
# XBR contract ABI file names
|
||||
#
|
||||
XBR_TOKEN_FN = pkg_resources.resource_filename('xbr', 'abi/XBRToken.json')
|
||||
XBR_NETWORK_FN = pkg_resources.resource_filename('xbr', 'abi/XBRNetwork.json')
|
||||
XBR_DOMAIN_FN = pkg_resources.resource_filename('xbr', 'abi/XBRDomain.json')
|
||||
XBR_CATALOG_FN = pkg_resources.resource_filename('xbr', 'abi/XBRCatalog.json')
|
||||
XBR_MARKET_FN = pkg_resources.resource_filename('xbr', 'abi/XBRMarket.json')
|
||||
XBR_CHANNEL_FN = pkg_resources.resource_filename('xbr', 'abi/XBRChannel.json')
|
||||
|
||||
|
||||
#
|
||||
# XBR contract ABIs
|
||||
#
|
||||
XBR_TOKEN_ABI = _load_json('XBRToken')['abi']
|
||||
XBR_NETWORK_ABI = _load_json('XBRNetwork')['abi']
|
||||
XBR_DOMAIN_ABI = _load_json('XBRDomain')['abi']
|
||||
XBR_CATALOG_ABI = _load_json('XBRCatalog')['abi']
|
||||
XBR_MARKET_ABI = _load_json('XBRMarket')['abi']
|
||||
XBR_CHANNEL_ABI = _load_json('XBRChannel')['abi']
|
||||
238
backend/lib/python3.12/site-packages/autobahn/xbr/_blockchain.py
Normal file
238
backend/lib/python3.12/site-packages/autobahn/xbr/_blockchain.py
Normal file
@@ -0,0 +1,238 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 web3
|
||||
import txaio
|
||||
from autobahn import xbr
|
||||
|
||||
|
||||
class SimpleBlockchain(object):
|
||||
"""
|
||||
Simple Ethereum blockchain XBR client.
|
||||
"""
|
||||
DomainStatus_NULL = 0
|
||||
DomainStatus_ACTIVE = 1
|
||||
DomainStatus_CLOSED = 2
|
||||
|
||||
NodeType_NULL = 0
|
||||
NodeType_MASTER = 1
|
||||
NodeType_CORE = 2
|
||||
NodeType_EDGE = 3
|
||||
|
||||
NodeLicense_NULL = 0
|
||||
NodeLicense_INFINITE = 1
|
||||
NodeLicense_FREE = 2
|
||||
|
||||
log = None
|
||||
backgroundCaller = None
|
||||
|
||||
def __init__(self, gateway=None):
|
||||
"""
|
||||
|
||||
:param gateway: Optional explicit Ethereum gateway URL to use.
|
||||
If no explicit gateway is specified, let web3 auto-choose.
|
||||
:type gateway: str
|
||||
"""
|
||||
self.log = txaio.make_logger()
|
||||
self._gateway = gateway
|
||||
self._w3 = None
|
||||
assert self.backgroundCaller is not None
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Start the blockchain client using the configured blockchain gateway.
|
||||
"""
|
||||
assert self._w3 is None
|
||||
|
||||
if self._gateway:
|
||||
w3 = web3.Web3(web3.Web3.HTTPProvider(self._gateway))
|
||||
else:
|
||||
# using automatic provider detection:
|
||||
from web3.auto import w3
|
||||
|
||||
# check we are connected, and check network ID
|
||||
if not w3.isConnected():
|
||||
emsg = 'could not connect to Web3/Ethereum at: {}'.format(self._gateway or 'auto')
|
||||
self.log.warn(emsg)
|
||||
raise RuntimeError(emsg)
|
||||
else:
|
||||
print('connected to network {} at provider "{}"'.format(w3.version.network,
|
||||
self._gateway or 'auto'))
|
||||
|
||||
self._w3 = w3
|
||||
|
||||
# set new provider on XBR library
|
||||
xbr.setProvider(self._w3)
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop the blockchain client.
|
||||
"""
|
||||
assert self._w3 is not None
|
||||
|
||||
self._w3 = None
|
||||
|
||||
async def get_market_status(self, market_id):
|
||||
"""
|
||||
|
||||
:param market_id:
|
||||
:return:
|
||||
"""
|
||||
def _get_market_status(_market_id):
|
||||
owner = xbr.xbrnetwork.functions.getMarketOwner(_market_id).call()
|
||||
if not owner or owner == '0x0000000000000000000000000000000000000000':
|
||||
return None
|
||||
else:
|
||||
return {
|
||||
'owner': owner,
|
||||
}
|
||||
return self.backgroundCaller(_get_market_status, market_id)
|
||||
|
||||
async def get_domain_status(self, domain_id):
|
||||
"""
|
||||
|
||||
:param domain_id:
|
||||
:type domain_id: bytes
|
||||
|
||||
:return:
|
||||
:rtype: dict
|
||||
"""
|
||||
def _get_domain_status(_domain_id):
|
||||
status = xbr.xbrnetwork.functions.getDomainStatus(_domain_id).call()
|
||||
if status == SimpleBlockchain.DomainStatus_NULL:
|
||||
return None
|
||||
elif status == SimpleBlockchain.DomainStatus_ACTIVE:
|
||||
return {'status': 'ACTIVE'}
|
||||
elif status == SimpleBlockchain.DomainStatus_CLOSED:
|
||||
return {'status': 'CLOSED'}
|
||||
return self.backgroundCaller(_get_domain_status, domain_id)
|
||||
|
||||
def get_node_status(self, delegate_adr):
|
||||
"""
|
||||
|
||||
:param delegate_adr:
|
||||
:type delegate_adr: bytes
|
||||
|
||||
:return:
|
||||
:rtype: dict
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_actor_status(self, delegate_adr):
|
||||
"""
|
||||
|
||||
:param delegate_adr:
|
||||
:type delegate_adr: bytes
|
||||
|
||||
:return:
|
||||
:rtype: dict
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_delegate_status(self, delegate_adr):
|
||||
"""
|
||||
|
||||
:param delegate_adr:
|
||||
:type delegate_adr: bytes
|
||||
|
||||
:return:
|
||||
:rtype: dict
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_channel_status(self, channel_adr):
|
||||
"""
|
||||
|
||||
:param channel_adr:
|
||||
:type channel_adr: bytes
|
||||
|
||||
:return:
|
||||
:rtype: dict
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_member_status(self, member_adr):
|
||||
"""
|
||||
|
||||
:param member_adr:
|
||||
:type member_adr: bytes
|
||||
|
||||
:return:
|
||||
:rtype: dict
|
||||
"""
|
||||
assert type(member_adr) == bytes and len(member_adr) == 20
|
||||
|
||||
def _get_member_status(_member_adr):
|
||||
level = xbr.xbrnetwork.functions.getMemberLevel(member_adr).call()
|
||||
if not level:
|
||||
return None
|
||||
else:
|
||||
eula = xbr.xbrnetwork.functions.getMemberEula(member_adr).call()
|
||||
if not eula or eula.strip() == '':
|
||||
return None
|
||||
profile = xbr.xbrnetwork.functions.getMemberProfile(member_adr).call()
|
||||
if not profile or profile.strip() == '':
|
||||
profile = None
|
||||
return {
|
||||
'eula': eula,
|
||||
'profile': profile,
|
||||
}
|
||||
return self.backgroundCaller(_get_member_status, member_adr)
|
||||
|
||||
async def get_balances(self, account_adr):
|
||||
"""
|
||||
Return current ETH and XBR balances of account with given address.
|
||||
|
||||
:param account_adr: Ethereum address of account to get balances for.
|
||||
:type account_adr: bytes
|
||||
|
||||
:return: A dictionary with ``"ETH"`` and ``"XBR"`` keys and respective
|
||||
current on-chain balances as values.
|
||||
:rtype: dict
|
||||
"""
|
||||
assert type(account_adr) == bytes and len(account_adr) == 20
|
||||
|
||||
def _get_balances(_adr):
|
||||
balance_eth = self._w3.eth.getBalance(_adr)
|
||||
balance_xbr = xbr.xbrtoken.functions.balanceOf(_adr).call()
|
||||
return {
|
||||
'ETH': balance_eth,
|
||||
'XBR': balance_xbr,
|
||||
}
|
||||
return self.backgroundCaller(_get_balances, account_adr)
|
||||
|
||||
def get_contract_adrs(self):
|
||||
"""
|
||||
Get XBR smart contract addresses.
|
||||
|
||||
:return: A dictionary with ``"XBRToken"`` and ``"XBRNetwork"`` keys and Ethereum
|
||||
contract addresses as values.
|
||||
:rtype: dict
|
||||
"""
|
||||
return {
|
||||
'XBRToken': xbr.xbrtoken.address,
|
||||
'XBRNetwork': xbr.xbrnetwork.address,
|
||||
}
|
||||
636
backend/lib/python3.12/site-packages/autobahn/xbr/_buyer.py
Normal file
636
backend/lib/python3.12/site-packages/autobahn/xbr/_buyer.py
Normal file
@@ -0,0 +1,636 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 uuid
|
||||
import binascii
|
||||
from pprint import pformat
|
||||
|
||||
import os
|
||||
import cbor2
|
||||
import nacl.secret
|
||||
import nacl.utils
|
||||
import nacl.exceptions
|
||||
import nacl.public
|
||||
|
||||
import txaio
|
||||
from autobahn.wamp.exception import ApplicationError
|
||||
from autobahn.wamp.protocol import ApplicationSession
|
||||
from ._util import unpack_uint256, pack_uint256
|
||||
|
||||
import eth_keys
|
||||
|
||||
from ..util import hl, hlval
|
||||
from ._eip712_channel_close import sign_eip712_channel_close, recover_eip712_channel_close
|
||||
|
||||
|
||||
class Transaction(object):
|
||||
|
||||
def __init__(self, channel, delegate, pubkey, key_id, channel_seq, amount, balance, signature):
|
||||
self.channel = channel
|
||||
self.delegate = delegate
|
||||
self.pubkey = pubkey
|
||||
self.key_id = key_id
|
||||
self.channel_seq = channel_seq
|
||||
self.amount = amount
|
||||
self.balance = balance
|
||||
self.signature = signature
|
||||
|
||||
def marshal(self):
|
||||
res = {
|
||||
'channel': self.channel,
|
||||
'delegate': self.delegate,
|
||||
'pubkey': self.pubkey,
|
||||
'key_id': self.key_id,
|
||||
'channel_seq': self.channel_seq,
|
||||
'amount': self.amount,
|
||||
'balance': self.balance,
|
||||
'signature': self.signature,
|
||||
}
|
||||
return res
|
||||
|
||||
def __str__(self):
|
||||
return pformat(self.marshal())
|
||||
|
||||
|
||||
class SimpleBuyer(object):
|
||||
"""
|
||||
Simple XBR buyer component. This component can be used by a XBR buyer delegate to
|
||||
handle the automated buying of data encryption keys from the XBR market maker.
|
||||
"""
|
||||
log = None
|
||||
|
||||
def __init__(self, market_maker_adr, buyer_key, max_price):
|
||||
"""
|
||||
|
||||
:param market_maker_adr:
|
||||
:type market_maker_adr:
|
||||
|
||||
:param buyer_key: Consumer delegate (buyer) private Ethereum key.
|
||||
:type buyer_key: bytes
|
||||
|
||||
:param max_price: Maximum price we are willing to buy per key.
|
||||
:type max_price: int
|
||||
"""
|
||||
assert type(market_maker_adr) == bytes and len(market_maker_adr) == 20, 'market_maker_adr must be bytes[20], but got "{}"'.format(market_maker_adr)
|
||||
assert type(buyer_key) == bytes and len(buyer_key) == 32, 'buyer delegate must be bytes[32], but got "{}"'.format(buyer_key)
|
||||
assert type(max_price) == int and max_price > 0
|
||||
|
||||
self.log = txaio.make_logger()
|
||||
|
||||
# market maker address
|
||||
self._market_maker_adr = market_maker_adr
|
||||
self._xbrmm_config = None
|
||||
|
||||
# buyer delegate raw ethereum private key (32 bytes)
|
||||
self._pkey_raw = buyer_key
|
||||
|
||||
# buyer delegate ethereum private key object
|
||||
self._pkey = eth_keys.keys.PrivateKey(buyer_key)
|
||||
|
||||
# buyer delegate ethereum private account from raw private key
|
||||
# FIXME
|
||||
# self._acct = Account.privateKeyToAccount(self._pkey)
|
||||
self._acct = None
|
||||
|
||||
# buyer delegate ethereum account canonical address
|
||||
self._addr = self._pkey.public_key.to_canonical_address()
|
||||
|
||||
# buyer delegate ethereum account canonical checksummed address
|
||||
# FIXME
|
||||
# self._caddr = web3.Web3.toChecksumAddress(self._addr)
|
||||
self._caddr = None
|
||||
|
||||
# ephemeral data consumer key
|
||||
self._receive_key = nacl.public.PrivateKey.generate()
|
||||
|
||||
# maximum price per key we are willing to pay
|
||||
self._max_price = max_price
|
||||
|
||||
# will be filled with on-chain payment channel contract, once started
|
||||
self._channel = None
|
||||
|
||||
# channel current (off-chain) balance
|
||||
self._balance = 0
|
||||
|
||||
# channel sequence number
|
||||
self._seq = 0
|
||||
|
||||
# this holds the keys we bought (map: key_id => nacl.secret.SecretBox)
|
||||
self._keys = {}
|
||||
self._session = None
|
||||
self._running = False
|
||||
|
||||
# automatically initiate a close of the payment channel when running into
|
||||
# a transaction failing because of insufficient balance remaining in the channel
|
||||
self._auto_close_channel = True
|
||||
|
||||
# FIXME: poor mans local transaction store
|
||||
self._transaction_idx = {}
|
||||
self._transactions = []
|
||||
|
||||
async def start(self, session, consumer_id):
|
||||
"""
|
||||
Start buying keys to decrypt XBR data by calling ``unwrap()``.
|
||||
|
||||
:param session: WAMP session over which to communicate with the XBR market maker.
|
||||
:type session: :class:`autobahn.wamp.protocol.ApplicationSession`
|
||||
|
||||
:param consumer_id: XBR consumer ID.
|
||||
:type consumer_id: str
|
||||
|
||||
:return: Current remaining balance in payment channel.
|
||||
:rtype: int
|
||||
"""
|
||||
assert isinstance(session, ApplicationSession)
|
||||
assert type(consumer_id) == str
|
||||
assert not self._running
|
||||
|
||||
self._session = session
|
||||
self._running = True
|
||||
|
||||
self.log.debug('Start buying from consumer delegate address {address} (public key 0x{public_key}..)',
|
||||
address=hl(self._caddr),
|
||||
public_key=binascii.b2a_hex(self._pkey.public_key[:10]).decode())
|
||||
|
||||
try:
|
||||
self._xbrmm_config = await session.call('xbr.marketmaker.get_config')
|
||||
|
||||
# get the currently active (if any) payment channel for the delegate
|
||||
assert type(self._addr) == bytes and len(self._addr) == 20
|
||||
self._channel = await session.call('xbr.marketmaker.get_active_payment_channel', self._addr)
|
||||
if not self._channel:
|
||||
raise Exception('no active payment channel found')
|
||||
|
||||
channel_oid = self._channel['channel_oid']
|
||||
assert type(channel_oid) == bytes and len(channel_oid) == 16
|
||||
self._channel_oid = uuid.UUID(bytes=channel_oid)
|
||||
|
||||
# get the current (off-chain) balance of the payment channel
|
||||
payment_balance = await session.call('xbr.marketmaker.get_payment_channel_balance', self._channel_oid.bytes)
|
||||
except:
|
||||
session.leave()
|
||||
raise
|
||||
|
||||
# FIXME
|
||||
if type(payment_balance['remaining']) == bytes:
|
||||
payment_balance['remaining'] = unpack_uint256(payment_balance['remaining'])
|
||||
|
||||
if not payment_balance['remaining'] > 0:
|
||||
raise Exception('no off-chain balance remaining on payment channel')
|
||||
|
||||
self._balance = payment_balance['remaining']
|
||||
self._seq = payment_balance['seq']
|
||||
|
||||
self.log.info('Ok, buyer delegate started [active payment channel {channel_oid} with remaining balance {remaining} at sequence {seq}]',
|
||||
channel_oid=hl(self._channel_oid), remaining=hlval(self._balance), seq=hlval(self._seq))
|
||||
|
||||
return self._balance
|
||||
|
||||
async def stop(self):
|
||||
"""
|
||||
Stop buying keys.
|
||||
"""
|
||||
assert self._running
|
||||
|
||||
self._running = False
|
||||
|
||||
self.log.info('Ok, buyer delegate stopped.')
|
||||
|
||||
async def balance(self):
|
||||
"""
|
||||
Return current balance of payment channel:
|
||||
|
||||
* ``amount``: The initial amount with which the payment channel was opened.
|
||||
* ``remaining``: The remaining amount of XBR in the payment channel that can be spent.
|
||||
* ``inflight``: The amount of XBR allocated to buy transactions that are currently processed.
|
||||
|
||||
:return: Current payment balance.
|
||||
:rtype: dict
|
||||
"""
|
||||
assert self._session and self._session.is_attached()
|
||||
|
||||
payment_balance = await self._session.call('xbr.marketmaker.get_payment_channel_balance', self._channel['channel_oid'])
|
||||
|
||||
return payment_balance
|
||||
|
||||
async def open_channel(self, buyer_addr, amount, details=None):
|
||||
"""
|
||||
|
||||
:param amount:
|
||||
:type amount:
|
||||
|
||||
:param details:
|
||||
:type details:
|
||||
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
assert self._session and self._session.is_attached()
|
||||
|
||||
# FIXME
|
||||
signature = os.urandom(64)
|
||||
|
||||
payment_channel = await self._session.call('xbr.marketmaker.open_payment_channel',
|
||||
buyer_addr,
|
||||
self._addr,
|
||||
amount,
|
||||
signature)
|
||||
|
||||
balance = {
|
||||
'amount': payment_channel['amount'],
|
||||
'remaining': payment_channel['remaining'],
|
||||
'inflight': payment_channel['inflight'],
|
||||
}
|
||||
|
||||
return balance
|
||||
|
||||
async def close_channel(self, details=None):
|
||||
"""
|
||||
Requests to close the currently active payment channel.
|
||||
|
||||
:return:
|
||||
"""
|
||||
|
||||
async def unwrap(self, key_id, serializer, ciphertext):
|
||||
"""
|
||||
Decrypt XBR data. This functions will potentially make the buyer call the
|
||||
XBR market maker to buy data encryption keys from the XBR provider.
|
||||
|
||||
:param key_id: ID of the data encryption used for decryption
|
||||
of application payload.
|
||||
:type key_id: bytes
|
||||
|
||||
:param serializer: Application payload serializer.
|
||||
:type serializer: str
|
||||
|
||||
:param ciphertext: Ciphertext of encrypted application payload to
|
||||
decrypt.
|
||||
:type ciphertext: bytes
|
||||
|
||||
:return: Decrypted application payload.
|
||||
:rtype: object
|
||||
"""
|
||||
assert type(key_id) == bytes and len(key_id) == 16
|
||||
# FIXME: support more app payload serializers
|
||||
assert type(serializer) == str and serializer in ['cbor']
|
||||
assert type(ciphertext) == bytes
|
||||
|
||||
market_oid = self._channel['market_oid']
|
||||
channel_oid = self._channel['channel_oid']
|
||||
|
||||
# FIXME
|
||||
current_block_number = 1
|
||||
verifying_chain_id = self._xbrmm_config['verifying_chain_id']
|
||||
verifying_contract_adr = binascii.a2b_hex(self._xbrmm_config['verifying_contract_adr'][2:])
|
||||
|
||||
# if we don't have the key, buy it!
|
||||
if key_id in self._keys:
|
||||
self.log.debug('Key {key_id} already in key store (or currently being bought).',
|
||||
key_id=hl(uuid.UUID(bytes=key_id)))
|
||||
else:
|
||||
self.log.debug('Key {key_id} not yet in key store - buying key ..', key_id=hl(uuid.UUID(bytes=key_id)))
|
||||
|
||||
# mark the key as currently being bought already (the location of code here is multi-entrant)
|
||||
self._keys[key_id] = False
|
||||
|
||||
# get (current) price for key we want to buy
|
||||
quote = await self._session.call('xbr.marketmaker.get_quote', key_id)
|
||||
|
||||
# set price we pay set to the (current) quoted price
|
||||
amount = unpack_uint256(quote['price'])
|
||||
|
||||
self.log.debug('Key {key_id} has current price quote {amount}',
|
||||
key_id=hl(uuid.UUID(bytes=key_id)), amount=hl(int(amount / 10**18)))
|
||||
|
||||
if amount > self._max_price:
|
||||
raise ApplicationError('xbr.error.max_price_exceeded',
|
||||
'{}.unwrap() - key {} needed cannot be bought: price {} exceeds maximum price of {}'.format(self.__class__.__name__, uuid.UUID(bytes=key_id), int(amount / 10 ** 18), int(self._max_price / 10 ** 18)))
|
||||
|
||||
# check (locally) we have enough balance left in the payment channel to buy the key
|
||||
balance = self._balance - amount
|
||||
if balance < 0:
|
||||
if self._auto_close_channel:
|
||||
# FIXME: sign last transaction (from persisted local history)
|
||||
last_tx = None
|
||||
txns = self.past_transactions()
|
||||
if txns:
|
||||
last_tx = txns[0]
|
||||
|
||||
if last_tx:
|
||||
# tx1 is the delegate portion, and tx2 is the market maker portion:
|
||||
# tx1, tx2 = last_tx
|
||||
# close_adr = tx1.channel
|
||||
# close_seq = tx1.channel_seq
|
||||
# close_balance = tx1.balance
|
||||
# close_is_final = True
|
||||
|
||||
close_seq = self._seq
|
||||
close_balance = self._balance
|
||||
close_is_final = True
|
||||
|
||||
signature = sign_eip712_channel_close(self._pkey_raw,
|
||||
verifying_chain_id,
|
||||
verifying_contract_adr,
|
||||
current_block_number,
|
||||
market_oid,
|
||||
channel_oid,
|
||||
close_seq,
|
||||
close_balance,
|
||||
close_is_final)
|
||||
|
||||
self.log.debug('auto-closing payment channel {channel_oid} [close_seq={close_seq}, close_balance={close_balance}, close_is_final={close_is_final}]',
|
||||
channel_oid=uuid.UUID(bytes=channel_oid),
|
||||
close_seq=close_seq,
|
||||
close_balance=int(close_balance / 10**18),
|
||||
close_is_final=close_is_final)
|
||||
|
||||
# call market maker to initiate closing of payment channel
|
||||
await self._session.call('xbr.marketmaker.close_channel',
|
||||
channel_oid,
|
||||
verifying_chain_id,
|
||||
current_block_number,
|
||||
verifying_contract_adr,
|
||||
pack_uint256(close_balance),
|
||||
close_seq,
|
||||
close_is_final,
|
||||
signature)
|
||||
|
||||
# FIXME: wait for and acquire new payment channel instead of bailing out ..
|
||||
|
||||
raise ApplicationError('xbr.error.channel_closed',
|
||||
'{}.unwrap() - key {} cannot be bought: payment channel {} ran empty and we initiated close at remaining balance of {}'.format(self.__class__.__name__,
|
||||
uuid.UUID(bytes=key_id),
|
||||
channel_oid,
|
||||
int(close_balance / 10 ** 18)))
|
||||
raise ApplicationError('xbr.error.insufficient_balance',
|
||||
'{}.unwrap() - key {} cannot be bought: insufficient balance {} in payment channel for amount {}'.format(self.__class__.__name__,
|
||||
uuid.UUID(bytes=key_id),
|
||||
int(self._balance / 10 ** 18),
|
||||
int(amount / 10 ** 18)))
|
||||
|
||||
buyer_pubkey = self._receive_key.public_key.encode(encoder=nacl.encoding.RawEncoder)
|
||||
channel_seq = self._seq + 1
|
||||
is_final = False
|
||||
|
||||
# XBRSIG[1/8]: compute EIP712 typed data signature
|
||||
signature = sign_eip712_channel_close(self._pkey_raw, verifying_chain_id, verifying_contract_adr,
|
||||
current_block_number, market_oid, channel_oid, channel_seq,
|
||||
balance, is_final)
|
||||
|
||||
# persist 1st phase of the transaction locally
|
||||
self._save_transaction_phase1(channel_oid, self._addr, buyer_pubkey, key_id, channel_seq, amount, balance, signature)
|
||||
|
||||
# call the market maker to buy the key
|
||||
try:
|
||||
receipt = await self._session.call('xbr.marketmaker.buy_key',
|
||||
self._addr,
|
||||
buyer_pubkey,
|
||||
key_id,
|
||||
channel_oid,
|
||||
channel_seq,
|
||||
pack_uint256(amount),
|
||||
pack_uint256(balance),
|
||||
signature)
|
||||
except ApplicationError as e:
|
||||
if e.error == 'xbr.error.channel_closed':
|
||||
self.stop()
|
||||
raise e
|
||||
except Exception as e:
|
||||
self.log.error('Encountered error while calling market maker to buy key!')
|
||||
self.log.failure()
|
||||
self._keys[key_id] = e
|
||||
raise e
|
||||
|
||||
# XBRSIG[8/8]: check market maker signature
|
||||
marketmaker_signature = receipt['signature']
|
||||
marketmaker_channel_seq = receipt['channel_seq']
|
||||
marketmaker_amount_paid = unpack_uint256(receipt['amount_paid'])
|
||||
marketmaker_remaining = unpack_uint256(receipt['remaining'])
|
||||
marketmaker_inflight = unpack_uint256(receipt['inflight'])
|
||||
|
||||
signer_address = recover_eip712_channel_close(verifying_chain_id, verifying_contract_adr,
|
||||
current_block_number, market_oid, channel_oid,
|
||||
marketmaker_channel_seq, marketmaker_remaining,
|
||||
False, marketmaker_signature)
|
||||
if signer_address != self._market_maker_adr:
|
||||
self.log.warn('{klass}.unwrap()::XBRSIG[8/8] - EIP712 signature invalid: signer_address={signer_address}, delegate_adr={delegate_adr}',
|
||||
klass=self.__class__.__name__,
|
||||
signer_address=hl(binascii.b2a_hex(signer_address).decode()),
|
||||
delegate_adr=hl(binascii.b2a_hex(self._market_maker_adr).decode()))
|
||||
raise ApplicationError('xbr.error.invalid_signature',
|
||||
'{}.unwrap()::XBRSIG[8/8] - EIP712 signature invalid or not signed by market maker'.format(self.__class__.__name__))
|
||||
|
||||
if self._seq + 1 != marketmaker_channel_seq:
|
||||
raise ApplicationError('xbr.error.invalid_transaction',
|
||||
'{}.buy_key(): invalid transaction (channel sequence number mismatch - expected {}, but got {})'.format(self.__class__.__name__, self._seq, receipt['channel_seq']))
|
||||
|
||||
if self._balance - amount != marketmaker_remaining:
|
||||
raise ApplicationError('xbr.error.invalid_transaction',
|
||||
'{}.buy_key(): invalid transaction (channel remaining amount mismatch - expected {}, but got {})'.format(self.__class__.__name__, self._balance - amount, receipt['remaining']))
|
||||
|
||||
self._seq = marketmaker_channel_seq
|
||||
self._balance = marketmaker_remaining
|
||||
|
||||
# persist 2nd phase of the transaction locally
|
||||
self._save_transaction_phase2(channel_oid, self._market_maker_adr, buyer_pubkey, key_id, marketmaker_channel_seq,
|
||||
marketmaker_amount_paid, marketmaker_remaining, marketmaker_signature)
|
||||
|
||||
# unseal the data encryption key
|
||||
sealed_key = receipt['sealed_key']
|
||||
unseal_box = nacl.public.SealedBox(self._receive_key)
|
||||
try:
|
||||
key = unseal_box.decrypt(sealed_key)
|
||||
except nacl.exceptions.CryptoError as e:
|
||||
self._keys[key_id] = e
|
||||
raise ApplicationError('xbr.error.decryption_failed', '{}.unwrap() - could not unseal data encryption key: {}'.format(self.__class__.__name__, e))
|
||||
|
||||
# remember the key, so we can use it to actually decrypt application payload data
|
||||
self._keys[key_id] = nacl.secret.SecretBox(key)
|
||||
|
||||
transactions_count = self.count_transactions()
|
||||
self.log.info(
|
||||
'{klass}.unwrap() - {tx_type} key {key_id} bought for {amount_paid} [payment_channel={payment_channel}, remaining={remaining}, inflight={inflight}, buyer_pubkey={buyer_pubkey}, transactions={transactions}]',
|
||||
klass=self.__class__.__name__,
|
||||
tx_type=hl('XBR BUY ', color='magenta'),
|
||||
key_id=hl(uuid.UUID(bytes=key_id)),
|
||||
amount_paid=hl(str(int(marketmaker_amount_paid / 10 ** 18)) + ' XBR', color='magenta'),
|
||||
payment_channel=hl(binascii.b2a_hex(receipt['payment_channel']).decode()),
|
||||
remaining=hl(int(marketmaker_remaining / 10 ** 18)),
|
||||
inflight=hl(int(marketmaker_inflight / 10 ** 18)),
|
||||
buyer_pubkey=hl(binascii.b2a_hex(buyer_pubkey).decode()),
|
||||
transactions=transactions_count)
|
||||
|
||||
# if the key is already being bought, wait until the one buying path of execution has succeeded and done
|
||||
log_counter = 0
|
||||
while self._keys[key_id] is False:
|
||||
if log_counter % 100:
|
||||
self.log.debug('{klass}.unwrap() - waiting for key "{key_id}" currently being bought ..',
|
||||
klass=self.__class__.__name__, key_id=hl(uuid.UUID(bytes=key_id)))
|
||||
log_counter += 1
|
||||
await txaio.sleep(.2)
|
||||
|
||||
# check if the key buying failed and fail the unwrapping in turn
|
||||
if isinstance(self._keys[key_id], Exception):
|
||||
e = self._keys[key_id]
|
||||
raise e
|
||||
|
||||
# now that we have the data encryption key, decrypt the application payload
|
||||
# the decryption key here is an instance of nacl.secret.SecretBox
|
||||
try:
|
||||
message = self._keys[key_id].decrypt(ciphertext)
|
||||
except nacl.exceptions.CryptoError as e:
|
||||
# Decryption failed. Ciphertext failed verification
|
||||
raise ApplicationError('xbr.error.decryption_failed', '{}.unwrap() - failed to unwrap encrypted data: {}'.format(self.__class__.__name__, e))
|
||||
|
||||
# deserialize the application payload
|
||||
# FIXME: support more app payload serializers
|
||||
try:
|
||||
payload = cbor2.loads(message)
|
||||
except cbor2.decoder.CBORDecodeError as e:
|
||||
# premature end of stream (expected to read 4187 bytes, got 27 instead)
|
||||
raise ApplicationError('xbr.error.deserialization_failed', '{}.unwrap() - failed to deserialize application payload: {}'.format(self.__class__.__name__, e))
|
||||
|
||||
return payload
|
||||
|
||||
def _save_transaction_phase1(self, channel_oid, delegate_adr, buyer_pubkey, key_id, channel_seq, amount, balance, signature):
|
||||
"""
|
||||
|
||||
:param channel_oid:
|
||||
:param delegate_adr:
|
||||
:param buyer_pubkey:
|
||||
:param key_id:
|
||||
:param channel_seq:
|
||||
:param amount:
|
||||
:param balance:
|
||||
:param signature:
|
||||
:return:
|
||||
"""
|
||||
if key_id in self._transaction_idx:
|
||||
raise RuntimeError('save_transaction_phase1: duplicate transaction for key 0x{}'.format(binascii.b2a_hex(key_id)))
|
||||
|
||||
tx1 = Transaction(channel_oid, delegate_adr, buyer_pubkey, key_id, channel_seq, amount, balance, signature)
|
||||
|
||||
key_idx = len(self._transactions)
|
||||
self._transactions.append([tx1, None])
|
||||
self._transaction_idx[key_id] = key_idx
|
||||
|
||||
def _save_transaction_phase2(self, channel_oid, delegate_adr, buyer_pubkey, key_id, channel_seq, amount, balance, signature):
|
||||
"""
|
||||
|
||||
:param channel_oid:
|
||||
:param delegate_adr:
|
||||
:param buyer_pubkey:
|
||||
:param key_id:
|
||||
:param channel_seq:
|
||||
:param amount:
|
||||
:param balance:
|
||||
:param signature:
|
||||
:return:
|
||||
"""
|
||||
if key_id not in self._transaction_idx:
|
||||
raise RuntimeError('save_transaction_phase2: transaction for key 0x{} not found'.format(binascii.b2a_hex(key_id)))
|
||||
|
||||
key_idx = self._transaction_idx[key_id]
|
||||
|
||||
if self._transactions[key_idx][1]:
|
||||
raise RuntimeError(
|
||||
'save_transaction_phase2: duplicate transaction for key 0x{}'.format(binascii.b2a_hex(key_id)))
|
||||
|
||||
tx1 = self._transactions[key_idx][0]
|
||||
tx2 = Transaction(channel_oid, delegate_adr, buyer_pubkey, key_id, channel_seq, amount, balance, signature)
|
||||
|
||||
assert tx1.channel == tx2.channel
|
||||
# assert tx1.delegate == tx2.delegate
|
||||
assert tx1.pubkey == tx2.pubkey
|
||||
assert tx1.key_id == tx2.key_id
|
||||
assert tx1.channel_seq == tx2.channel_seq
|
||||
assert tx1.amount == tx2.amount
|
||||
assert tx1.balance == tx2.balance
|
||||
|
||||
# note: signatures will differ (obviously)!
|
||||
assert tx1.signature != tx2.signature
|
||||
|
||||
self._transactions[key_idx][1] = tx2
|
||||
|
||||
def past_transactions(self, filter_complete=True, limit=1):
|
||||
"""
|
||||
|
||||
:param filter_complete:
|
||||
:param limit:
|
||||
:return:
|
||||
"""
|
||||
assert type(filter_complete) == bool
|
||||
assert type(limit) == int and limit > 0
|
||||
|
||||
n = 0
|
||||
res = []
|
||||
while n < limit:
|
||||
if len(self._transactions) > n:
|
||||
tx = self._transactions[-n]
|
||||
if not filter_complete or (tx[0] and tx[1]):
|
||||
res.append(tx)
|
||||
n += 1
|
||||
else:
|
||||
break
|
||||
return res
|
||||
|
||||
def count_transactions(self):
|
||||
"""
|
||||
|
||||
:return:
|
||||
"""
|
||||
res = {
|
||||
'complete': 0,
|
||||
'pending': 0,
|
||||
}
|
||||
for tx1, tx2 in self._transactions:
|
||||
if tx1 and tx2:
|
||||
res['complete'] += 1
|
||||
else:
|
||||
res['pending'] += 1
|
||||
return res
|
||||
|
||||
def get_transaction(self, key_id):
|
||||
"""
|
||||
|
||||
:param key_id:
|
||||
:return:
|
||||
"""
|
||||
idx = self._transaction_idx.get(key_id, None)
|
||||
if idx:
|
||||
return self._transactions[idx]
|
||||
|
||||
def is_complete(self, key_id):
|
||||
"""
|
||||
|
||||
:param key_id:
|
||||
:return:
|
||||
"""
|
||||
idx = self._transaction_idx.get(key_id, None)
|
||||
if idx:
|
||||
tx1, tx2 = self._transactions[idx]
|
||||
return tx1 and tx2
|
||||
return False
|
||||
1106
backend/lib/python3.12/site-packages/autobahn/xbr/_cli.py
Normal file
1106
backend/lib/python3.12/site-packages/autobahn/xbr/_cli.py
Normal file
File diff suppressed because it is too large
Load Diff
588
backend/lib/python3.12/site-packages/autobahn/xbr/_config.py
Normal file
588
backend/lib/python3.12/site-packages/autobahn/xbr/_config.py
Normal file
@@ -0,0 +1,588 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 io
|
||||
import sys
|
||||
import uuid
|
||||
import struct
|
||||
import binascii
|
||||
import configparser
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
import click
|
||||
import nacl
|
||||
import web3
|
||||
import numpy as np
|
||||
from time import time_ns
|
||||
from eth_utils.conversions import hexstr_if_str, to_hex
|
||||
|
||||
from autobahn.websocket.util import parse_url
|
||||
from autobahn.xbr._wallet import pkm_from_argon2_secret
|
||||
|
||||
_HAS_COLOR_TERM = False
|
||||
try:
|
||||
import colorama
|
||||
|
||||
# https://github.com/tartley/colorama/issues/48
|
||||
term = None
|
||||
if sys.platform == 'win32' and 'TERM' in os.environ:
|
||||
term = os.environ.pop('TERM')
|
||||
|
||||
colorama.init()
|
||||
_HAS_COLOR_TERM = True
|
||||
|
||||
if term:
|
||||
os.environ['TERM'] = term
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class Profile(object):
|
||||
"""
|
||||
User profile, stored as named section in ``${HOME}/.xbrnetwork/config.ini``:
|
||||
|
||||
.. code-block:: INI
|
||||
|
||||
[default]
|
||||
# username used with this profile
|
||||
username=joedoe
|
||||
|
||||
# user email used with the profile (e.g. for verification emails)
|
||||
email=joe.doe@example.com
|
||||
|
||||
# XBR network node used as a directory server and gateway to XBR smart contracts
|
||||
network_url=ws://localhost:8090/ws
|
||||
|
||||
# WAMP realm on network node, usually "xbrnetwork"
|
||||
network_realm=xbrnetwork
|
||||
|
||||
# user private WAMP-cryptosign key (for client authentication)
|
||||
cskey=0xb18bbe88ca0e189689e99f87b19addfb179d46aab3d59ec5d93a15286b949eb6
|
||||
|
||||
# user private Ethereum key (for signing transactions and e2e data encryption)
|
||||
ethkey=0xfbada363e724d4db2faa2eeaa7d7aca37637b1076dd8cf6fefde13983abaa2ef
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
path: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
member_adr: Optional[str] = None,
|
||||
ethkey: Optional[bytes] = None,
|
||||
cskey: Optional[bytes] = None,
|
||||
username: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
network_url: Optional[str] = None,
|
||||
network_realm: Optional[str] = None,
|
||||
member_oid: Optional[uuid.UUID] = None,
|
||||
vaction_oid: Optional[uuid.UUID] = None,
|
||||
vaction_requested: Optional[np.datetime64] = None,
|
||||
vaction_verified: Optional[np.datetime64] = None,
|
||||
data_url: Optional[str] = None,
|
||||
data_realm: Optional[str] = None,
|
||||
infura_url: Optional[str] = None,
|
||||
infura_network: Optional[str] = None,
|
||||
infura_key: Optional[str] = None,
|
||||
infura_secret: Optional[str] = None):
|
||||
"""
|
||||
|
||||
:param path:
|
||||
:param name:
|
||||
:param member_adr:
|
||||
:param ethkey:
|
||||
:param cskey:
|
||||
:param username:
|
||||
:param email:
|
||||
:param network_url:
|
||||
:param network_realm:
|
||||
:param member_oid:
|
||||
:param vaction_oid:
|
||||
:param vaction_requested:
|
||||
:param vaction_verified:
|
||||
:param data_url:
|
||||
:param data_realm:
|
||||
:param infura_url:
|
||||
:param infura_network:
|
||||
:param infura_key:
|
||||
:param infura_secret:
|
||||
"""
|
||||
from txaio import make_logger
|
||||
self.log = make_logger()
|
||||
|
||||
self.path = path
|
||||
self.name = name
|
||||
|
||||
self.member_adr = member_adr
|
||||
self.ethkey = ethkey
|
||||
self.cskey = cskey
|
||||
self.username = username
|
||||
self.email = email
|
||||
self.network_url = network_url
|
||||
self.network_realm = network_realm
|
||||
self.member_oid = member_oid
|
||||
self.vaction_oid = vaction_oid
|
||||
self.vaction_requested = vaction_requested
|
||||
self.vaction_verified = vaction_verified
|
||||
self.data_url = data_url
|
||||
self.data_realm = data_realm
|
||||
self.infura_url = infura_url
|
||||
self.infura_network = infura_network
|
||||
self.infura_key = infura_key
|
||||
self.infura_secret = infura_secret
|
||||
|
||||
def marshal(self):
|
||||
obj = {}
|
||||
obj['member_adr'] = self.member_adr or ''
|
||||
obj['ethkey'] = '0x{}'.format(binascii.b2a_hex(self.ethkey).decode()) if self.ethkey else ''
|
||||
obj['cskey'] = '0x{}'.format(binascii.b2a_hex(self.cskey).decode()) if self.cskey else ''
|
||||
obj['username'] = self.username or ''
|
||||
obj['email'] = self.email or ''
|
||||
obj['network_url'] = self.network_url or ''
|
||||
obj['network_realm'] = self.network_realm or ''
|
||||
obj['member_oid'] = str(self.member_oid) if self.member_oid else ''
|
||||
obj['vaction_oid'] = str(self.vaction_oid) if self.vaction_oid else ''
|
||||
obj['vaction_requested'] = str(self.vaction_requested) if self.vaction_requested else ''
|
||||
obj['vaction_verified'] = str(self.vaction_verified) if self.vaction_verified else ''
|
||||
obj['data_url'] = self.data_url or ''
|
||||
obj['data_realm'] = self.data_realm or ''
|
||||
obj['infura_url'] = self.infura_url or ''
|
||||
obj['infura_network'] = self.infura_network or ''
|
||||
obj['infura_key'] = self.infura_key or ''
|
||||
obj['infura_secret'] = self.infura_secret or ''
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def parse(path, name, items):
|
||||
member_adr = None
|
||||
ethkey = None
|
||||
cskey = None
|
||||
username = None
|
||||
email = None
|
||||
network_url = None
|
||||
network_realm = None
|
||||
member_oid = None
|
||||
vaction_oid = None
|
||||
vaction_requested = None
|
||||
vaction_verified = None
|
||||
data_url = None
|
||||
data_realm = None
|
||||
infura_network = None
|
||||
infura_key = None
|
||||
infura_secret = None
|
||||
infura_url = None
|
||||
for k, v in items:
|
||||
if k == 'network_url':
|
||||
network_url = str(v)
|
||||
elif k == 'network_realm':
|
||||
network_realm = str(v)
|
||||
elif k == 'vaction_oid':
|
||||
if type(v) == str and v != '':
|
||||
vaction_oid = uuid.UUID(v)
|
||||
else:
|
||||
vaction_oid = None
|
||||
elif k == 'member_adr':
|
||||
if type(v) == str and v != '':
|
||||
member_adr = v
|
||||
else:
|
||||
member_adr = None
|
||||
elif k == 'member_oid':
|
||||
if type(v) == str and v != '':
|
||||
member_oid = uuid.UUID(v)
|
||||
else:
|
||||
member_oid = None
|
||||
elif k == 'vaction_requested':
|
||||
if type(v) == int and v:
|
||||
vaction_requested = np.datetime64(v, 'ns')
|
||||
else:
|
||||
vaction_requested = v
|
||||
elif k == 'vaction_verified':
|
||||
if type(v) == int:
|
||||
vaction_verified = np.datetime64(v, 'ns')
|
||||
else:
|
||||
vaction_verified = v
|
||||
elif k == 'data_url':
|
||||
data_url = str(v)
|
||||
elif k == 'data_realm':
|
||||
data_realm = str(v)
|
||||
elif k == 'ethkey':
|
||||
ethkey = binascii.a2b_hex(v[2:])
|
||||
elif k == 'cskey':
|
||||
cskey = binascii.a2b_hex(v[2:])
|
||||
elif k == 'username':
|
||||
username = str(v)
|
||||
elif k == 'email':
|
||||
email = str(v)
|
||||
elif k == 'infura_network':
|
||||
infura_network = str(v)
|
||||
elif k == 'infura_key':
|
||||
infura_key = str(v)
|
||||
elif k == 'infura_secret':
|
||||
infura_secret = str(v)
|
||||
elif k == 'infura_url':
|
||||
infura_url = str(v)
|
||||
elif k in ['path', 'name']:
|
||||
pass
|
||||
else:
|
||||
# skip unknown attribute
|
||||
print('unprocessed config attribute "{}"'.format(k))
|
||||
|
||||
return Profile(path, name,
|
||||
member_adr, ethkey, cskey,
|
||||
username, email,
|
||||
network_url, network_realm,
|
||||
member_oid,
|
||||
vaction_oid, vaction_requested, vaction_verified,
|
||||
data_url, data_realm,
|
||||
infura_url, infura_network, infura_key, infura_secret)
|
||||
|
||||
|
||||
class UserConfig(object):
|
||||
"""
|
||||
Local user configuration file. The data is either a plain text (unencrypted)
|
||||
.ini file, or such a file encrypted with XSalsa20-Poly1305, and with a
|
||||
binary file header of 48 octets.
|
||||
"""
|
||||
def __init__(self, config_path):
|
||||
"""
|
||||
|
||||
:param config_path: The user configuration file path.
|
||||
"""
|
||||
from txaio import make_logger
|
||||
self.log = make_logger()
|
||||
self._config_path = os.path.abspath(config_path)
|
||||
self._profiles = {}
|
||||
|
||||
@property
|
||||
def config_path(self) -> List[str]:
|
||||
"""
|
||||
Return the path to the user configuration file exposed by this object.,
|
||||
|
||||
:return: Local filesystem path.
|
||||
"""
|
||||
return self._config_path
|
||||
|
||||
@property
|
||||
def profiles(self) -> Dict[str, object]:
|
||||
"""
|
||||
Access to a map of user profiles in this user configuration.
|
||||
|
||||
:return: Map of user profiles.
|
||||
"""
|
||||
return self._profiles
|
||||
|
||||
def save(self, password: Optional[str] = None):
|
||||
"""
|
||||
Save this user configuration to the underlying configuration file. The user
|
||||
configuration file can be encrypted using Argon2id when a ``password`` is given.
|
||||
|
||||
:param password: The optional Argon2id password.
|
||||
|
||||
:return: Number of octets written to the user configuration file.
|
||||
"""
|
||||
written = 0
|
||||
config = configparser.ConfigParser()
|
||||
for profile_name, profile in self._profiles.items():
|
||||
if profile_name not in config.sections():
|
||||
config.add_section(profile_name)
|
||||
written += 1
|
||||
pd = profile.marshal()
|
||||
for option, value in pd.items():
|
||||
config.set(profile_name, option, value)
|
||||
|
||||
with io.StringIO() as fp1:
|
||||
config.write(fp1)
|
||||
config_data = fp1.getvalue().encode('utf8')
|
||||
|
||||
if password:
|
||||
# binary file format header (48 bytes):
|
||||
#
|
||||
# * 8 bytes: 0xdeadbeef 0x00000666 magic number (big endian)
|
||||
# * 4 bytes: 0x00000001 encryption type 1 for "argon2id"
|
||||
# * 4 bytes data length (big endian)
|
||||
# * 8 bytes created timestamp ns (big endian)
|
||||
# * 8 bytes unused (filled 0x00 currently)
|
||||
# * 16 bytes salt
|
||||
#
|
||||
salt = os.urandom(16)
|
||||
context = 'xbrnetwork-config'
|
||||
priv_key = pkm_from_argon2_secret(email='', password=password, context=context, salt=salt)
|
||||
box = nacl.secret.SecretBox(priv_key)
|
||||
config_data_ciphertext = box.encrypt(config_data)
|
||||
dl = [
|
||||
b'\xde\xad\xbe\xef',
|
||||
b'\x00\x00\x06\x66',
|
||||
b'\x00\x00\x00\x01',
|
||||
struct.pack('>L', len(config_data_ciphertext)),
|
||||
struct.pack('>Q', time_ns()),
|
||||
b'\x00' * 8,
|
||||
salt,
|
||||
config_data_ciphertext,
|
||||
]
|
||||
data = b''.join(dl)
|
||||
else:
|
||||
data = config_data
|
||||
|
||||
with open(self._config_path, 'wb') as fp2:
|
||||
fp2.write(data)
|
||||
|
||||
self.log.debug('configuration with {sections} sections, {bytes_written} bytes written to {written_to}',
|
||||
sections=written, bytes_written=len(data), written_to=self._config_path)
|
||||
|
||||
return len(data)
|
||||
|
||||
def load(self, cb_get_password=None) -> List[str]:
|
||||
"""
|
||||
Load this object from the underlying user configuration file. When the
|
||||
file is encrypted, call back into ``cb_get_password`` to get the user password.
|
||||
|
||||
:param cb_get_password: Callback called when password is needed.
|
||||
|
||||
:return: List of profiles loaded.
|
||||
"""
|
||||
if not os.path.exists(self._config_path) or not os.path.isfile(self._config_path):
|
||||
raise RuntimeError('config path "{}" cannot be loaded: so such file'.format(self._config_path))
|
||||
|
||||
with open(self._config_path, 'rb') as fp:
|
||||
data = fp.read()
|
||||
|
||||
if len(data) >= 48 and data[:8] == b'\xde\xad\xbe\xef\x00\x00\x06\x66':
|
||||
# binary format detected
|
||||
header = data[:48]
|
||||
body = data[48:]
|
||||
|
||||
algo = struct.unpack('>L', header[8:12])[0]
|
||||
data_len = struct.unpack('>L', header[12:16])[0]
|
||||
created = struct.unpack('>Q', header[16:24])[0]
|
||||
# created_ts = np.datetime64(created, 'ns')
|
||||
|
||||
assert algo in [0, 1]
|
||||
assert data_len == len(body)
|
||||
assert created < time_ns()
|
||||
|
||||
salt = header[32:48]
|
||||
context = 'xbrnetwork-config'
|
||||
if cb_get_password:
|
||||
password = cb_get_password()
|
||||
else:
|
||||
password = ''
|
||||
priv_key = pkm_from_argon2_secret(email='', password=password, context=context, salt=salt)
|
||||
box = nacl.secret.SecretBox(priv_key)
|
||||
body = box.decrypt(body)
|
||||
|
||||
else:
|
||||
header = None
|
||||
body = data
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read_string(body.decode('utf8'))
|
||||
|
||||
profiles = {}
|
||||
for profile_name in config.sections():
|
||||
citems = config.items(profile_name)
|
||||
profile = Profile.parse(self._config_path, profile_name, citems)
|
||||
profiles[profile_name] = profile
|
||||
self._profiles = profiles
|
||||
|
||||
loaded_profiles = sorted(self.profiles.keys())
|
||||
return loaded_profiles
|
||||
|
||||
|
||||
if 'CROSSBAR_FABRIC_URL' in os.environ:
|
||||
_DEFAULT_CFC_URL = os.environ['CROSSBAR_FABRIC_URL']
|
||||
else:
|
||||
_DEFAULT_CFC_URL = u'wss://master.xbr.network/ws'
|
||||
|
||||
|
||||
def style_error(text):
|
||||
if _HAS_COLOR_TERM:
|
||||
return click.style(text, fg='red', bold=True)
|
||||
else:
|
||||
return text
|
||||
|
||||
|
||||
def style_ok(text):
|
||||
if _HAS_COLOR_TERM:
|
||||
return click.style(text, fg='green', bold=True)
|
||||
else:
|
||||
return text
|
||||
|
||||
|
||||
class WampUrl(click.ParamType):
|
||||
"""
|
||||
WAMP transport URL validator.
|
||||
"""
|
||||
|
||||
name = 'WAMP transport URL'
|
||||
|
||||
def __init__(self):
|
||||
click.ParamType.__init__(self)
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
parse_url(value)
|
||||
except Exception as e:
|
||||
self.fail(style_error(str(e)))
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def prompt_for_wamp_url(msg, default=None):
|
||||
"""
|
||||
Prompt user for WAMP transport URL (eg "wss://planet.xbr.network/ws").
|
||||
"""
|
||||
value = click.prompt(msg, type=WampUrl(), default=default)
|
||||
return value
|
||||
|
||||
|
||||
class EthereumAddress(click.ParamType):
|
||||
"""
|
||||
Ethereum address validator.
|
||||
"""
|
||||
|
||||
name = 'Ethereum address'
|
||||
|
||||
def __init__(self):
|
||||
click.ParamType.__init__(self)
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
value = web3.Web3.toChecksumAddress(value)
|
||||
adr = binascii.a2b_hex(value[2:])
|
||||
if len(value) != 20:
|
||||
raise ValueError('Ethereum addres must be 20 bytes (160 bit), but was {} bytes'.format(len(adr)))
|
||||
except Exception as e:
|
||||
self.fail(style_error(str(e)))
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def prompt_for_ethereum_address(msg):
|
||||
"""
|
||||
Prompt user for an Ethereum (public) address.
|
||||
"""
|
||||
value = click.prompt(msg, type=EthereumAddress())
|
||||
return value
|
||||
|
||||
|
||||
class PrivateKey(click.ParamType):
|
||||
"""
|
||||
Private key (32 bytes in HEX) validator.
|
||||
"""
|
||||
|
||||
name = 'Private key'
|
||||
|
||||
def __init__(self, key_len):
|
||||
click.ParamType.__init__(self)
|
||||
self._key_len = key_len
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
value = hexstr_if_str(to_hex, value)
|
||||
if value[:2] in ['0x', '\\x']:
|
||||
key = binascii.a2b_hex(value[2:])
|
||||
else:
|
||||
key = binascii.a2b_hex(value)
|
||||
if len(key) != self._key_len:
|
||||
raise ValueError('key length must be {} bytes, but was {} bytes'.format(self._key_len, len(key)))
|
||||
except Exception as e:
|
||||
self.fail(style_error(str(e)))
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def prompt_for_key(msg, key_len, default=None):
|
||||
"""
|
||||
Prompt user for a binary key of given length (in HEX).
|
||||
"""
|
||||
value = click.prompt(msg, type=PrivateKey(key_len), default=default)
|
||||
return value
|
||||
|
||||
|
||||
# default configuration stored in $HOME/.xbrnetwork/config.ini
|
||||
_DEFAULT_CONFIG = """[default]
|
||||
# username used with this profile
|
||||
username={username}
|
||||
|
||||
# user email used with the profile (e.g. for verification emails)
|
||||
email={email}
|
||||
|
||||
# XBR network node used as a directory server and gateway to XBR smart contracts
|
||||
network_url={network_url}
|
||||
|
||||
# WAMP realm on network node, usually "xbrnetwork"
|
||||
network_realm={network_realm}
|
||||
|
||||
# user private WAMP-cryptosign key (for client authentication)
|
||||
cskey={cskey}
|
||||
|
||||
# user private Ethereum key (for signing transactions and e2e data encryption)
|
||||
ethkey={ethkey}
|
||||
"""
|
||||
|
||||
# # default XBR market URL to connect to
|
||||
# market_url={market_url}
|
||||
# market_realm={market_realm}
|
||||
# # Infura blockchain gateway configuration
|
||||
# infura_url={infura_url}
|
||||
# infura_network={infura_network}
|
||||
# infura_key={infura_key}
|
||||
# infura_secret={infura_secret}
|
||||
|
||||
|
||||
def load_or_create_profile(dotdir=None, profile=None, default_url=None, default_realm=None, default_email=None, default_username=None):
|
||||
dotdir = dotdir or '~/.xbrnetwork'
|
||||
profile = profile or 'default'
|
||||
default_url = default_url or 'wss://planet.xbr.network/ws'
|
||||
default_realm = default_realm or 'xbrnetwork'
|
||||
|
||||
config_dir = os.path.expanduser(dotdir)
|
||||
if not os.path.isdir(config_dir):
|
||||
os.mkdir(config_dir)
|
||||
click.echo('created new local user directory {}'.format(style_ok(config_dir)))
|
||||
|
||||
config_path = os.path.join(config_dir, 'config.ini')
|
||||
if not os.path.isfile(config_path):
|
||||
click.echo('creating new user profile "{}"'.format(style_ok(profile)))
|
||||
with open(config_path, 'w') as f:
|
||||
network_url = prompt_for_wamp_url('enter the WAMP router URL of the network directory node', default=default_url)
|
||||
network_realm = click.prompt('enter the WAMP realm to join on the network directory node', type=str, default=default_realm)
|
||||
cskey = prompt_for_key('your private WAMP client key', 32, default='0x' + binascii.b2a_hex(os.urandom(32)).decode())
|
||||
ethkey = prompt_for_key('your private Etherum key', 32, default='0x' + binascii.b2a_hex(os.urandom(32)).decode())
|
||||
email = click.prompt('user email used for with profile', type=str, default=default_email)
|
||||
username = click.prompt('user name used with this profile', type=str, default=default_username)
|
||||
f.write(_DEFAULT_CONFIG.format(network_url=network_url, network_realm=network_realm, ethkey=ethkey,
|
||||
cskey=cskey, email=email, username=username))
|
||||
click.echo('created new local user configuration {}'.format(style_ok(config_path)))
|
||||
|
||||
config_obj = UserConfig(config_path)
|
||||
config_obj.load()
|
||||
profile_obj = config_obj.profiles.get(profile, None)
|
||||
|
||||
if not profile_obj:
|
||||
raise click.ClickException('no such profile: "{}"'.format(profile))
|
||||
|
||||
return profile_obj
|
||||
87
backend/lib/python3.12/site-packages/autobahn/xbr/_dialog.py
Normal file
87
backend/lib/python3.12/site-packages/autobahn/xbr/_dialog.py
Normal file
@@ -0,0 +1,87 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 shutil
|
||||
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.internet.interfaces import IReactorProcess
|
||||
from twisted.internet.utils import getProcessOutput
|
||||
|
||||
__all__ = ('get_input_from_dialog',)
|
||||
|
||||
|
||||
def get_input_from_dialog(reactor: IReactorProcess, title: str = 'Unlock key',
|
||||
text: str = 'Please enter passphrase to unlock key',
|
||||
hide_text: bool = True) -> Deferred:
|
||||
"""
|
||||
Show a Gnome/GTK desktop dialog asking for a passphrase.
|
||||
|
||||
This is using zenity, the GNOME port of dialog which allows you to display dialog boxes
|
||||
from the commandline and shell scripts. To install (on Linux):
|
||||
|
||||
.. code:: console
|
||||
|
||||
sudo apt update
|
||||
sudo install zenity
|
||||
|
||||
See also:
|
||||
|
||||
- https://gitlab.gnome.org/GNOME/zenity
|
||||
- https://wiki.ubuntuusers.de/Zenity/
|
||||
- https://bash.cyberciti.biz/guide/Zenity:_Shell_Scripting_with_Gnome
|
||||
|
||||
:param reactor: Twisted reactor to use.
|
||||
:param title: Dialog window title to show.
|
||||
:param text: Dialog text to show above text entry field.
|
||||
:param hide_text: Hide entry field text.
|
||||
|
||||
:return: A deferred that resolves with the string the user entered.
|
||||
"""
|
||||
exe = shutil.which('zenity')
|
||||
if not exe:
|
||||
raise RuntimeError('get_input_from_dialog(): could not find zenity (install with "apt install zenity")')
|
||||
args = ['--entry', '--title', title, '--text', text]
|
||||
if hide_text:
|
||||
args.append('--hide-text')
|
||||
d = getProcessOutput(exe, args=args, env=os.environ, reactor=reactor)
|
||||
|
||||
def _consume(output):
|
||||
passphrase = output.decode('utf8').strip()
|
||||
return passphrase
|
||||
|
||||
d.addCallback(_consume)
|
||||
return d
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from twisted.internet.task import react
|
||||
|
||||
async def main(reactor):
|
||||
passphrase = await get_input_from_dialog(reactor)
|
||||
print(type(passphrase), len(passphrase), '"{}"'.format(passphrase))
|
||||
|
||||
react(main)
|
||||
@@ -0,0 +1,152 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from typing import Optional
|
||||
from ._eip712_base import sign, recover, is_address, is_bytes16, is_block_number, \
|
||||
is_chain_id, is_eth_privkey, is_signature
|
||||
|
||||
|
||||
def _create_eip712_api_publish(chainId: int, verifyingContract: bytes, member: bytes, published: int,
|
||||
catalogId: bytes, apiId: bytes, schema: str, meta: Optional[str]) -> dict:
|
||||
"""
|
||||
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param member:
|
||||
:param published:
|
||||
:param catalogId:
|
||||
:param apiId:
|
||||
:param schema:
|
||||
:param meta:
|
||||
:return:
|
||||
"""
|
||||
assert is_chain_id(chainId)
|
||||
assert is_address(verifyingContract)
|
||||
assert is_address(member)
|
||||
assert is_block_number(published)
|
||||
assert is_bytes16(catalogId)
|
||||
assert is_bytes16(apiId)
|
||||
assert type(schema) == str
|
||||
assert type(meta) == str
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712ApiPublish': [
|
||||
{
|
||||
'name': 'chainId',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'verifyingContract',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'member',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'published',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'catalogId',
|
||||
'type': 'bytes16'
|
||||
},
|
||||
{
|
||||
'name': 'apiId',
|
||||
'type': 'bytes16'
|
||||
},
|
||||
{
|
||||
'name': 'schema',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'meta',
|
||||
'type': 'string'
|
||||
},
|
||||
]
|
||||
},
|
||||
'primaryType': 'EIP712ApiPublish',
|
||||
'domain': {
|
||||
'name': 'XBR',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'chainId': chainId,
|
||||
'verifyingContract': verifyingContract,
|
||||
'member': member,
|
||||
'published': published,
|
||||
'catalogId': catalogId,
|
||||
'apiId': apiId,
|
||||
'schema': schema,
|
||||
'meta': meta or '',
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_api_publish(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes,
|
||||
published: int, catalogId: bytes, apiId: bytes, schema: str, meta: Optional[str]) -> bytes:
|
||||
"""
|
||||
|
||||
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
|
||||
:type eth_privkey: bytes
|
||||
|
||||
:return: The signature according to EIP712 (32+32+1 raw bytes).
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_eth_privkey(eth_privkey)
|
||||
|
||||
data = _create_eip712_api_publish(chainId, verifyingContract, member, published, catalogId, apiId, schema,
|
||||
meta)
|
||||
return sign(eth_privkey, data)
|
||||
|
||||
|
||||
def recover_eip712_api_publish(chainId: int, verifyingContract: bytes, member: bytes, published: int,
|
||||
catalogId: bytes, apiId: bytes, schema: str, meta: Optional[str],
|
||||
signature: bytes) -> bytes:
|
||||
"""
|
||||
Recover the signer address the given EIP712 signature was signed with.
|
||||
|
||||
:return: The (computed) signer address the signature was signed with.
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_signature(signature)
|
||||
|
||||
data = _create_eip712_api_publish(chainId, verifyingContract, member, published, catalogId, apiId, schema,
|
||||
meta)
|
||||
return recover(data, signature)
|
||||
@@ -0,0 +1,462 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.path
|
||||
import pprint
|
||||
from binascii import a2b_hex
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
import web3
|
||||
import cbor2
|
||||
|
||||
from py_eth_sig_utils.eip712 import encode_typed_data
|
||||
|
||||
from autobahn.wamp.message import _URI_PAT_REALM_NAME_ETH
|
||||
from autobahn.xbr._secmod import EthereumKey
|
||||
|
||||
from ._eip712_base import sign, recover, is_chain_id, is_address, is_block_number, is_signature, is_eth_privkey
|
||||
from ._eip712_certificate import EIP712Certificate
|
||||
|
||||
|
||||
def create_eip712_authority_certificate(chainId: int,
|
||||
verifyingContract: bytes,
|
||||
validFrom: int,
|
||||
issuer: bytes,
|
||||
subject: bytes,
|
||||
realm: bytes,
|
||||
capabilities: int,
|
||||
meta: str) -> dict:
|
||||
"""
|
||||
Authority certificate: long-lived, on-chain L2.
|
||||
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param validFrom:
|
||||
:param issuer:
|
||||
:param subject:
|
||||
:param realm:
|
||||
:param capabilities:
|
||||
:param meta:
|
||||
:return:
|
||||
"""
|
||||
assert is_chain_id(chainId)
|
||||
assert is_address(verifyingContract)
|
||||
assert is_block_number(validFrom)
|
||||
assert is_address(issuer)
|
||||
assert is_address(subject)
|
||||
assert is_address(realm)
|
||||
assert type(capabilities) == int and 0 <= capabilities <= 2 ** 53
|
||||
assert meta is None or type(meta) == str
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712AuthorityCertificate': [
|
||||
{
|
||||
'name': 'chainId',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'verifyingContract',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'validFrom',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'issuer',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'subject',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'realm',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'capabilities',
|
||||
'type': 'uint64'
|
||||
},
|
||||
{
|
||||
'name': 'meta',
|
||||
'type': 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
'primaryType': 'EIP712AuthorityCertificate',
|
||||
'domain': {
|
||||
'name': 'WMP',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'chainId': chainId,
|
||||
'verifyingContract': verifyingContract,
|
||||
'validFrom': validFrom,
|
||||
'issuer': issuer,
|
||||
'subject': subject,
|
||||
'realm': realm,
|
||||
'capabilities': capabilities,
|
||||
'meta': meta or '',
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_authority_certificate(eth_privkey: bytes,
|
||||
chainId: int,
|
||||
verifyingContract: bytes,
|
||||
validFrom: int,
|
||||
issuer: bytes,
|
||||
subject: bytes,
|
||||
realm: bytes,
|
||||
capabilities: int,
|
||||
meta: str) -> bytes:
|
||||
"""
|
||||
Sign the given data using a EIP712 based signature with the provided private key.
|
||||
|
||||
:param eth_privkey:
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param validFrom:
|
||||
:param issuer:
|
||||
:param subject:
|
||||
:param realm:
|
||||
:param capabilities:
|
||||
:param meta:
|
||||
:return:
|
||||
"""
|
||||
assert is_eth_privkey(eth_privkey)
|
||||
|
||||
data = create_eip712_authority_certificate(chainId, verifyingContract, validFrom, issuer,
|
||||
subject, realm, capabilities, meta)
|
||||
return sign(eth_privkey, data)
|
||||
|
||||
|
||||
def recover_eip712_authority_certificate(chainId: int,
|
||||
verifyingContract: bytes,
|
||||
validFrom: int,
|
||||
issuer: bytes,
|
||||
subject: bytes,
|
||||
realm: bytes,
|
||||
capabilities: int,
|
||||
meta: str,
|
||||
signature: bytes) -> bytes:
|
||||
"""
|
||||
Recover the signer address the given EIP712 signature was signed with.
|
||||
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param validFrom:
|
||||
:param issuer:
|
||||
:param subject:
|
||||
:param realm:
|
||||
:param capabilities:
|
||||
:param meta:
|
||||
:param signature:
|
||||
:return: The (computed) signer address the signature was signed with.
|
||||
"""
|
||||
assert is_signature(signature)
|
||||
|
||||
data = create_eip712_authority_certificate(chainId, verifyingContract, validFrom, issuer,
|
||||
subject, realm, capabilities, meta)
|
||||
return recover(data, signature)
|
||||
|
||||
|
||||
class EIP712AuthorityCertificate(EIP712Certificate):
|
||||
CAPABILITY_ROOT_CA = 1
|
||||
CAPABILITY_INTERMEDIATE_CA = 2
|
||||
CAPABILITY_PUBLIC_RELAY = 4
|
||||
CAPABILITY_PRIVATE_RELAY = 8
|
||||
CAPABILITY_PROVIDER = 16
|
||||
CAPABILITY_CONSUMER = 32
|
||||
|
||||
__slots__ = (
|
||||
# EIP712 attributes
|
||||
'chainId',
|
||||
'verifyingContract',
|
||||
'validFrom',
|
||||
'issuer',
|
||||
'subject',
|
||||
'realm',
|
||||
'capabilities',
|
||||
'meta',
|
||||
|
||||
# additional attributes
|
||||
'signatures',
|
||||
'hash',
|
||||
)
|
||||
|
||||
def __init__(self, chainId: int, verifyingContract: bytes, validFrom: int, issuer: bytes, subject: bytes,
|
||||
realm: bytes, capabilities: int, meta: str,
|
||||
signatures: Optional[List[bytes]] = None):
|
||||
super().__init__(chainId, verifyingContract, validFrom)
|
||||
self.issuer = issuer
|
||||
self.subject = subject
|
||||
self.realm = realm
|
||||
self.capabilities = capabilities
|
||||
self.meta = meta
|
||||
self.signatures = signatures
|
||||
eip712 = create_eip712_authority_certificate(chainId,
|
||||
verifyingContract,
|
||||
validFrom,
|
||||
issuer,
|
||||
subject,
|
||||
realm,
|
||||
capabilities,
|
||||
meta)
|
||||
self.hash = encode_typed_data(eip712)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
if not EIP712AuthorityCertificate.__eq__(self, other):
|
||||
return False
|
||||
if other.chainId != self.chainId:
|
||||
return False
|
||||
if other.verifyingContract != self.verifyingContract:
|
||||
return False
|
||||
if other.validFrom != self.validFrom:
|
||||
return False
|
||||
if other.issuer != self.issuer:
|
||||
return False
|
||||
if other.subject != self.subject:
|
||||
return False
|
||||
if other.realm != self.realm:
|
||||
return False
|
||||
if other.capabilities != self.capabilities:
|
||||
return False
|
||||
if other.meta != self.meta:
|
||||
return False
|
||||
if other.signatures != self.signatures:
|
||||
return False
|
||||
if other.hash != self.hash:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return pprint.pformat(self.marshal())
|
||||
|
||||
def sign(self, key: EthereumKey, binary: bool = False) -> bytes:
|
||||
eip712 = create_eip712_authority_certificate(self.chainId,
|
||||
self.verifyingContract,
|
||||
self.validFrom,
|
||||
self.issuer,
|
||||
self.subject,
|
||||
self.realm,
|
||||
self.capabilities,
|
||||
self.meta)
|
||||
return key.sign_typed_data(eip712, binary=binary)
|
||||
|
||||
def recover(self, signature: bytes) -> bytes:
|
||||
return recover_eip712_authority_certificate(self.chainId,
|
||||
self.verifyingContract,
|
||||
self.validFrom,
|
||||
self.issuer,
|
||||
self.subject,
|
||||
self.realm,
|
||||
self.capabilities,
|
||||
self.meta,
|
||||
signature)
|
||||
|
||||
def marshal(self, binary: bool = False) -> Dict[str, Any]:
|
||||
obj = create_eip712_authority_certificate(chainId=self.chainId,
|
||||
verifyingContract=self.verifyingContract,
|
||||
validFrom=self.validFrom,
|
||||
issuer=self.issuer,
|
||||
subject=self.subject,
|
||||
realm=self.realm,
|
||||
capabilities=self.capabilities,
|
||||
meta=self.meta)
|
||||
if not binary:
|
||||
obj['message']['verifyingContract'] = web3.Web3.toChecksumAddress(obj['message']['verifyingContract']) if obj['message']['verifyingContract'] else None
|
||||
obj['message']['issuer'] = web3.Web3.toChecksumAddress(obj['message']['issuer']) if obj['message']['issuer'] else None
|
||||
obj['message']['subject'] = web3.Web3.toChecksumAddress(obj['message']['subject']) if obj['message']['subject'] else None
|
||||
obj['message']['realm'] = web3.Web3.toChecksumAddress(obj['message']['realm']) if obj['message']['realm'] else None
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def parse(obj, binary: bool = False) -> 'EIP712AuthorityCertificate':
|
||||
if type(obj) != dict:
|
||||
raise ValueError('invalid type {} for object in EIP712AuthorityCertificate.parse'.format(type(obj)))
|
||||
|
||||
primaryType = obj.get('primaryType', None)
|
||||
if primaryType != 'EIP712AuthorityCertificate':
|
||||
raise ValueError('invalid primaryType "{}" - expected "EIP712AuthorityCertificate"'.format(primaryType))
|
||||
|
||||
# FIXME: check EIP712 types, domain
|
||||
|
||||
data = obj.get('message', None)
|
||||
if type(data) != dict:
|
||||
raise ValueError('invalid type {} for EIP712AuthorityCertificate'.format(type(data)))
|
||||
for k in data:
|
||||
if k not in ['type', 'chainId', 'verifyingContract', 'validFrom', 'issuer', 'subject',
|
||||
'realm', 'capabilities', 'meta']:
|
||||
raise ValueError('invalid attribute "{}" in EIP712AuthorityCertificate'.format(k))
|
||||
|
||||
_type = data.get('type', None)
|
||||
if _type and _type != 'EIP712AuthorityCertificate':
|
||||
raise ValueError('unexpected type "{}" in EIP712AuthorityCertificate'.format(_type))
|
||||
|
||||
chainId = data.get('chainId', None)
|
||||
if chainId is None:
|
||||
raise ValueError('missing chainId in EIP712AuthorityCertificate')
|
||||
if type(chainId) != int:
|
||||
raise ValueError('invalid type {} for chainId in EIP712AuthorityCertificate'.format(type(chainId)))
|
||||
|
||||
verifyingContract = data.get('verifyingContract', None)
|
||||
if verifyingContract is None:
|
||||
raise ValueError('missing verifyingContract in EIP712AuthorityCertificate')
|
||||
if binary:
|
||||
if type(verifyingContract) != bytes:
|
||||
raise ValueError(
|
||||
'invalid type {} for verifyingContract in EIP712AuthorityCertificate'.format(type(verifyingContract)))
|
||||
if len(verifyingContract) != 20:
|
||||
raise ValueError('invalid value length {} of verifyingContract'.format(len(verifyingContract)))
|
||||
else:
|
||||
if type(verifyingContract) != str:
|
||||
raise ValueError(
|
||||
'invalid type {} for verifyingContract in EIP712AuthorityCertificate'.format(type(verifyingContract)))
|
||||
if not _URI_PAT_REALM_NAME_ETH.match(verifyingContract):
|
||||
raise ValueError(
|
||||
'invalid value "{}" for verifyingContract in EIP712AuthorityCertificate'.format(verifyingContract))
|
||||
verifyingContract = a2b_hex(verifyingContract[2:])
|
||||
|
||||
validFrom = data.get('validFrom', None)
|
||||
if validFrom is None:
|
||||
raise ValueError('missing validFrom in EIP712AuthorityCertificate')
|
||||
if type(validFrom) != int:
|
||||
raise ValueError('invalid type {} for validFrom in EIP712AuthorityCertificate'.format(type(validFrom)))
|
||||
|
||||
issuer = data.get('issuer', None)
|
||||
if issuer is None:
|
||||
raise ValueError('missing issuer in EIP712AuthorityCertificate')
|
||||
if binary:
|
||||
if type(issuer) != bytes:
|
||||
raise ValueError(
|
||||
'invalid type {} for issuer in EIP712AuthorityCertificate'.format(type(issuer)))
|
||||
if len(issuer) != 20:
|
||||
raise ValueError('invalid value length {} of issuer'.format(len(issuer)))
|
||||
else:
|
||||
if type(issuer) != str:
|
||||
raise ValueError('invalid type {} for issuer in EIP712AuthorityCertificate'.format(type(issuer)))
|
||||
if not _URI_PAT_REALM_NAME_ETH.match(issuer):
|
||||
raise ValueError('invalid value "{}" for issuer in EIP712AuthorityCertificate'.format(issuer))
|
||||
issuer = a2b_hex(issuer[2:])
|
||||
|
||||
subject = data.get('subject', None)
|
||||
if subject is None:
|
||||
raise ValueError('missing subject in EIP712AuthorityCertificate')
|
||||
if binary:
|
||||
if type(subject) != bytes:
|
||||
raise ValueError(
|
||||
'invalid type {} for subject in EIP712AuthorityCertificate'.format(type(subject)))
|
||||
if len(subject) != 20:
|
||||
raise ValueError('invalid value length {} of verifyingContract'.format(len(subject)))
|
||||
else:
|
||||
if type(subject) != str:
|
||||
raise ValueError('invalid type {} for subject in EIP712AuthorityCertificate'.format(type(subject)))
|
||||
if not _URI_PAT_REALM_NAME_ETH.match(subject):
|
||||
raise ValueError('invalid value "{}" for subject in EIP712AuthorityCertificate'.format(subject))
|
||||
subject = a2b_hex(subject[2:])
|
||||
|
||||
realm = data.get('realm', None)
|
||||
if realm is None:
|
||||
raise ValueError('missing realm in EIP712AuthorityCertificate')
|
||||
if binary:
|
||||
if type(realm) != bytes:
|
||||
raise ValueError(
|
||||
'invalid type {} for realm in EIP712AuthorityCertificate'.format(type(realm)))
|
||||
if len(realm) != 20:
|
||||
raise ValueError('invalid value length {} of realm'.format(len(realm)))
|
||||
else:
|
||||
if type(realm) != str:
|
||||
raise ValueError('invalid type {} for realm in EIP712AuthorityCertificate'.format(type(realm)))
|
||||
if not _URI_PAT_REALM_NAME_ETH.match(realm):
|
||||
raise ValueError('invalid value "{}" for realm in EIP712AuthorityCertificate'.format(realm))
|
||||
realm = a2b_hex(realm[2:])
|
||||
|
||||
capabilities = data.get('capabilities', None)
|
||||
if capabilities is None:
|
||||
raise ValueError('missing capabilities in EIP712AuthorityCertificate')
|
||||
if type(capabilities) != int:
|
||||
raise ValueError('invalid type {} for capabilities in EIP712AuthorityCertificate'.format(type(capabilities)))
|
||||
|
||||
meta = data.get('meta', None)
|
||||
if meta is None:
|
||||
raise ValueError('missing meta in EIP712AuthorityCertificate')
|
||||
if type(meta) != str:
|
||||
raise ValueError('invalid type {} for meta in EIP712AuthorityCertificate'.format(type(meta)))
|
||||
|
||||
obj = EIP712AuthorityCertificate(chainId=chainId, verifyingContract=verifyingContract, validFrom=validFrom,
|
||||
issuer=issuer, subject=subject, realm=realm, capabilities=capabilities, meta=meta)
|
||||
return obj
|
||||
|
||||
def save(self, filename: str) -> int:
|
||||
"""
|
||||
Save certificate to file. File format (serialized as CBOR):
|
||||
|
||||
[cert_hash: bytes, cert_eip712: Dict[str, Any], cert_signatures: List[bytes]]
|
||||
|
||||
:param filename:
|
||||
:return:
|
||||
"""
|
||||
cert_obj = [self.hash, self.marshal(binary=True), self.signatures or []]
|
||||
with open(filename, 'wb') as f:
|
||||
data = cbor2.dumps(cert_obj)
|
||||
f.write(data)
|
||||
return len(data)
|
||||
|
||||
@staticmethod
|
||||
def load(filename) -> 'EIP712AuthorityCertificate':
|
||||
"""
|
||||
Load certificate from file.
|
||||
|
||||
:param filename:
|
||||
:return:
|
||||
"""
|
||||
if not os.path.isfile(filename):
|
||||
raise RuntimeError('cannot create EIP712AuthorityCertificate from filename "{}": not a file'.format(filename))
|
||||
with open(filename, 'rb') as f:
|
||||
cert_hash, cert_eip712, cert_signatures = cbor2.loads(f.read())
|
||||
cert = EIP712AuthorityCertificate.parse(cert_eip712, binary=True)
|
||||
assert cert_hash == cert.hash
|
||||
cert.signatures = cert_signatures
|
||||
return cert
|
||||
@@ -0,0 +1,164 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from typing import Dict, Any
|
||||
from binascii import a2b_hex
|
||||
from py_eth_sig_utils import signing
|
||||
|
||||
_EIP712_SIG_LEN = 32 + 32 + 1
|
||||
|
||||
|
||||
def _hash(data) -> bytes:
|
||||
"""
|
||||
keccak256(abi.encode(
|
||||
EIP712_MEMBER_REGISTER_TYPEHASH,
|
||||
obj.chainId,
|
||||
obj.verifyingContract,
|
||||
obj.member,
|
||||
obj.registered,
|
||||
keccak256(bytes(obj.eula)),
|
||||
keccak256(bytes(obj.profile))
|
||||
));
|
||||
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
|
||||
|
||||
def sign(eth_privkey: bytes, data: Dict[str, Any]) -> bytes:
|
||||
"""
|
||||
Sign the given data using the given Ethereum private key.
|
||||
|
||||
:param eth_privkey: Signing key.
|
||||
:param data: Data to sign.
|
||||
:return: Signature.
|
||||
"""
|
||||
# internally, this is using py_eth_sig_utils.eip712.encode_typed_data
|
||||
_args = signing.sign_typed_data(data, eth_privkey)
|
||||
|
||||
# serialize structured signature (v, r, s) into bytes
|
||||
signature = signing.v_r_s_to_signature(*_args)
|
||||
|
||||
# be paranoid about what to expect
|
||||
assert type(signature) == bytes and len(signature) == _EIP712_SIG_LEN
|
||||
|
||||
return signature
|
||||
|
||||
|
||||
def recover(data: Dict[str, Any], signature: bytes) -> bytes:
|
||||
"""
|
||||
Recover the Ethereum address of the signer, given the data and signature.
|
||||
|
||||
:param data: Signed data.
|
||||
:param signature: Signature.
|
||||
:return: Signing address.
|
||||
"""
|
||||
assert type(signature) == bytes and len(signature) == _EIP712_SIG_LEN
|
||||
signer_address = signing.recover_typed_data(data, *signing.signature_to_v_r_s(signature))
|
||||
|
||||
return a2b_hex(signer_address[2:])
|
||||
|
||||
|
||||
def is_address(provided: Any) -> bool:
|
||||
"""
|
||||
Check if the value is a proper Ethereum address.
|
||||
|
||||
:param provided: The value to check.
|
||||
:return: True iff the value is of correct type.
|
||||
"""
|
||||
return type(provided) == bytes and len(provided) == 20
|
||||
|
||||
|
||||
def is_bytes16(provided: Any) -> bool:
|
||||
"""
|
||||
Check if the value is a proper (binary) UUID.
|
||||
|
||||
:param provided: The value to check.
|
||||
:return: True iff the value is of correct type.
|
||||
"""
|
||||
return type(provided) == bytes and len(provided) == 16
|
||||
|
||||
|
||||
def is_bytes32(provided: Any) -> bool:
|
||||
"""
|
||||
Check if the value is of type bytes and length 32.
|
||||
|
||||
:param provided: The value to check.
|
||||
:return: True iff the value is of correct type.
|
||||
"""
|
||||
return type(provided) == bytes and len(provided) == 32
|
||||
|
||||
|
||||
def is_signature(provided: Any) -> bool:
|
||||
"""
|
||||
Check if the value is a proper Ethereum signature.
|
||||
|
||||
:param provided: The value to check.
|
||||
:return: True iff the value is of correct type.
|
||||
"""
|
||||
return type(provided) == bytes and len(provided) == _EIP712_SIG_LEN
|
||||
|
||||
|
||||
def is_eth_privkey(provided: Any) -> bool:
|
||||
"""
|
||||
Check if the value is a proper Ethereum private key (seed).
|
||||
|
||||
:param provided: The value to check.
|
||||
:return: True iff the value is of correct type.
|
||||
"""
|
||||
return type(provided) == bytes and len(provided) == 32
|
||||
|
||||
|
||||
def is_cs_pubkey(provided: Any) -> bool:
|
||||
"""
|
||||
Check if the value is a proper WAMP-cryptosign public key.
|
||||
|
||||
:param provided: The value to check.
|
||||
:return: True iff the value is of correct type.
|
||||
"""
|
||||
return type(provided) == bytes and len(provided) == 32
|
||||
|
||||
|
||||
def is_block_number(provided: Any) -> bool:
|
||||
"""
|
||||
Check if the value is a proper Ethereum block number.
|
||||
|
||||
:param provided: The value to check.
|
||||
:return: True iff the value is of correct type.
|
||||
"""
|
||||
return type(provided) == int
|
||||
|
||||
|
||||
def is_chain_id(provided: Any) -> bool:
|
||||
"""
|
||||
Check if the value is a proper Ethereum chain ID.
|
||||
|
||||
:param provided: The value to check.
|
||||
:return: True iff the value is of correct type.
|
||||
"""
|
||||
# here is a list of public networks: https://chainid.network/
|
||||
# note: we allow any positive integer to account for private networks
|
||||
return type(provided) == int and provided > 0
|
||||
@@ -0,0 +1,141 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from typing import Optional
|
||||
from ._eip712_base import sign, recover, is_address, is_signature, is_eth_privkey, \
|
||||
is_bytes16, is_chain_id
|
||||
|
||||
|
||||
def _create_eip712_catalog_create(chainId: int, verifyingContract: bytes, member: bytes, created: int,
|
||||
catalogId: bytes, terms: str, meta: Optional[str]) -> dict:
|
||||
"""
|
||||
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param member:
|
||||
:param created:
|
||||
:param catalogId:
|
||||
:param terms:
|
||||
:param meta:
|
||||
:return:
|
||||
"""
|
||||
assert is_chain_id(chainId)
|
||||
assert is_address(verifyingContract)
|
||||
assert is_address(member)
|
||||
assert is_bytes16(catalogId)
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712CatalogCreate': [
|
||||
{
|
||||
'name': 'chainId',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'verifyingContract',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'member',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'created',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'catalogId',
|
||||
'type': 'bytes16'
|
||||
},
|
||||
{
|
||||
'name': 'terms',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'meta',
|
||||
'type': 'string'
|
||||
},
|
||||
]
|
||||
},
|
||||
'primaryType': 'EIP712CatalogCreate',
|
||||
'domain': {
|
||||
'name': 'XBR',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'chainId': chainId,
|
||||
'verifyingContract': verifyingContract,
|
||||
'member': member,
|
||||
'created': created,
|
||||
'catalogId': catalogId,
|
||||
'terms': terms,
|
||||
'meta': meta or '',
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_catalog_create(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes,
|
||||
created: int, catalogId: bytes, terms: str, meta: Optional[str]) -> bytes:
|
||||
"""
|
||||
|
||||
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
|
||||
:type eth_privkey: bytes
|
||||
|
||||
:return: The signature according to EIP712 (32+32+1 raw bytes).
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_eth_privkey(eth_privkey)
|
||||
|
||||
data = _create_eip712_catalog_create(chainId, verifyingContract, member, created, catalogId, terms, meta)
|
||||
|
||||
return sign(eth_privkey, data)
|
||||
|
||||
|
||||
def recover_eip712_catalog_create(chainId: int, verifyingContract: bytes, member: bytes, created: int,
|
||||
catalogId: bytes, terms: str, meta: Optional[str], signature: bytes) -> bytes:
|
||||
"""
|
||||
Recover the signer address the given EIP712 signature was signed with.
|
||||
|
||||
:return: The (computed) signer address the signature was signed with.
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_signature(signature)
|
||||
|
||||
data = _create_eip712_catalog_create(chainId, verifyingContract, member, created, catalogId, terms, meta)
|
||||
|
||||
return recover(data, signature)
|
||||
@@ -0,0 +1,40 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
|
||||
class EIP712Certificate(object):
|
||||
|
||||
def __init__(self, chainId: int, verifyingContract: bytes, validFrom: int):
|
||||
self.chainId = chainId
|
||||
self.verifyingContract = verifyingContract
|
||||
self.validFrom = validFrom
|
||||
|
||||
def recover(self, signature: bytes) -> bytes:
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def parse(data) -> 'EIP712Certificate':
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,117 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from typing import List, Tuple, Dict, Any, Union
|
||||
|
||||
from autobahn.xbr._eip712_delegate_certificate import EIP712DelegateCertificate
|
||||
from autobahn.xbr._eip712_authority_certificate import EIP712AuthorityCertificate
|
||||
|
||||
|
||||
def parse_certificate_chain(certificates: List[Tuple[Dict[str, Any], str]]) \
|
||||
-> List[Union[EIP712DelegateCertificate, EIP712AuthorityCertificate]]:
|
||||
"""
|
||||
|
||||
:param certificates:
|
||||
:return:
|
||||
"""
|
||||
# parse the whole certificate chain
|
||||
cert_chain = []
|
||||
for cert_hash, cert_data, cert_sig in certificates:
|
||||
if cert_data['primaryType'] == 'EIP712DelegateCertificate':
|
||||
cert = EIP712DelegateCertificate.parse(cert_data)
|
||||
elif cert_data['primaryType'] == 'EIP712AuthorityCertificate':
|
||||
cert = EIP712AuthorityCertificate.parse(cert_data)
|
||||
else:
|
||||
assert False, 'should not arrive here'
|
||||
cert_chain.append(cert)
|
||||
|
||||
# FIXME: proper adaptive implementation of certificate chain rules checking
|
||||
# if False:
|
||||
# assert len(cert_chain) == 3
|
||||
#
|
||||
# # Certificate Chain Rules (CCR):
|
||||
# #
|
||||
# # 1. **CCR-1**: The `chainId` and `verifyingContract` must match for all certificates to what we expect, and `validFrom` before current block number on the respective chain.
|
||||
# # 2. **CCR-2**: The `realm` must match for all certificates to the respective realm.
|
||||
# # 3. **CCR-3**: The type of the first certificate in the chain must be a `EIP712DelegateCertificate`, and all subsequent certificates must be of type `EIP712AuthorityCertificate`.
|
||||
# # 4. **CCR-4**: The last certificate must be self-signed (`issuer` equals `subject`), it is a root CA certificate.
|
||||
# # 5. **CCR-5**: The intermediate certificate's `issuer` must be equal to the `subject` of the previous certificate.
|
||||
# # 6. **CCR-6**: The root certificate must be `validFrom` before the intermediate certificate
|
||||
# # 7. **CCR-7**: The `capabilities` of intermediate certificate must be a subset of the root cert
|
||||
# # 8. **CCR-8**: The intermediate certificate's `subject` must be the delegate certificate `delegate`
|
||||
# # 9. **CCR-9**: The intermediate certificate must be `validFrom` before the delegate certificate
|
||||
# # 10. **CCR-10**: The root certificate's signature must be valid and signed by the root certificate's `issuer`.
|
||||
# # 11. **CCR-11**: The intermediate certificate's signature must be valid and signed by the intermediate certificate's `issuer`.
|
||||
# # 12. **CCR-12**: The delegate certificate's signature must be valid and signed by the `delegate`.
|
||||
#
|
||||
# # CCR-1
|
||||
# chainId = 1
|
||||
# verifyingContract = a2b_hex('0xf766Dc789CF04CD18aE75af2c5fAf2DA6650Ff57'[2:])
|
||||
# for cert in cert_chain:
|
||||
# assert cert.chainId == chainId
|
||||
# assert cert.verifyingContract == verifyingContract
|
||||
#
|
||||
# # CCR-2
|
||||
# realm = a2b_hex('0xA6e693CC4A2b4F1400391a728D26369D9b82ef96'[2:])
|
||||
# for cert in cert_chain[1:]:
|
||||
# assert cert.realm == realm
|
||||
#
|
||||
# # CCR-3
|
||||
# assert isinstance(cert_chain[0], EIP712DelegateCertificate)
|
||||
# for i in [1, len(cert_chain) - 1]:
|
||||
# assert isinstance(cert_chain[i], EIP712AuthorityCertificate)
|
||||
#
|
||||
# # CCR-4
|
||||
# assert cert_chain[2].subject == cert_chain[2].issuer
|
||||
#
|
||||
# # CCR-5
|
||||
# assert cert_chain[1].issuer == cert_chain[2].subject
|
||||
#
|
||||
# # CCR-6
|
||||
# assert cert_chain[2].validFrom <= cert_chain[1].validFrom
|
||||
#
|
||||
# # CCR-7
|
||||
# assert cert_chain[2].capabilities == cert_chain[2].capabilities | cert_chain[1].capabilities
|
||||
#
|
||||
# # CCR-8
|
||||
# assert cert_chain[1].subject == cert_chain[0].delegate
|
||||
#
|
||||
# # CCR-9
|
||||
# assert cert_chain[1].validFrom <= cert_chain[0].validFrom
|
||||
#
|
||||
# # CCR-10
|
||||
# _issuer = cert_chain[2].recover(a2b_hex(cert_sigs[2]))
|
||||
# assert _issuer == cert_chain[2].issuer
|
||||
#
|
||||
# # CCR-11
|
||||
# _issuer = cert_chain[1].recover(a2b_hex(cert_sigs[1]))
|
||||
# assert _issuer == cert_chain[1].issuer
|
||||
#
|
||||
# # CCR-12
|
||||
# _issuer = cert_chain[0].recover(a2b_hex(cert_sigs[0]))
|
||||
# assert _issuer == cert_chain[0].delegate
|
||||
|
||||
return cert_chain
|
||||
@@ -0,0 +1,140 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from ._eip712_base import sign, recover, is_address, is_signature, is_eth_privkey, is_bytes16, \
|
||||
is_block_number, is_chain_id
|
||||
|
||||
|
||||
def _create_eip712_channel_close(chainId: int, verifyingContract: bytes, closeAt: int, marketId: bytes, channelId: bytes,
|
||||
channelSeq: int, balance: int, isFinal: bool) -> dict:
|
||||
"""
|
||||
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param marketId:
|
||||
:param channelId:
|
||||
:param channelSeq:
|
||||
:param balance:
|
||||
:param isFinal:
|
||||
:return:
|
||||
"""
|
||||
assert is_chain_id(chainId)
|
||||
assert is_address(verifyingContract)
|
||||
assert is_block_number(closeAt)
|
||||
assert is_bytes16(marketId)
|
||||
assert is_bytes16(channelId)
|
||||
assert type(channelSeq) == int
|
||||
assert type(balance) == int
|
||||
assert type(isFinal) == bool
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712ChannelClose': [{
|
||||
'name': 'chainId',
|
||||
'type': 'uint256'
|
||||
}, {
|
||||
'name': 'verifyingContract',
|
||||
'type': 'address'
|
||||
}, {
|
||||
'name': 'closeAt',
|
||||
'type': 'uint256'
|
||||
}, {
|
||||
'name': 'marketId',
|
||||
'type': 'bytes16'
|
||||
}, {
|
||||
'name': 'channelId',
|
||||
'type': 'bytes16'
|
||||
}, {
|
||||
'name': 'channelSeq',
|
||||
'type': 'uint32'
|
||||
}, {
|
||||
'name': 'balance',
|
||||
'type': 'uint256'
|
||||
}, {
|
||||
'name': 'isFinal',
|
||||
'type': 'bool'
|
||||
}]
|
||||
},
|
||||
'primaryType': 'EIP712ChannelClose',
|
||||
'domain': {
|
||||
'name': 'XBR',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'chainId': chainId,
|
||||
'verifyingContract': verifyingContract,
|
||||
'closeAt': closeAt,
|
||||
'marketId': marketId,
|
||||
'channelId': channelId,
|
||||
'channelSeq': channelSeq,
|
||||
'balance': balance,
|
||||
'isFinal': isFinal
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_channel_close(eth_privkey: bytes, chainId: int, verifyingContract: bytes, closeAt: int, marketId: bytes,
|
||||
channelId: bytes, channelSeq: int, balance: int, isFinal: bool) -> bytes:
|
||||
"""
|
||||
|
||||
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
|
||||
:type eth_privkey: bytes
|
||||
|
||||
:return: The signature according to EIP712 (32+32+1 raw bytes).
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_eth_privkey(eth_privkey)
|
||||
|
||||
data = _create_eip712_channel_close(chainId, verifyingContract, closeAt, marketId, channelId, channelSeq, balance,
|
||||
isFinal)
|
||||
return sign(eth_privkey, data)
|
||||
|
||||
|
||||
def recover_eip712_channel_close(chainId: int, verifyingContract: bytes, closeAt: int, marketId: bytes, channelId: bytes,
|
||||
channelSeq: int, balance: int, isFinal: bool, signature: bytes) -> bytes:
|
||||
"""
|
||||
Recover the signer address the given EIP712 signature was signed with.
|
||||
|
||||
:return: The (computed) signer address the signature was signed with.
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_signature(signature)
|
||||
|
||||
data = _create_eip712_channel_close(chainId, verifyingContract, closeAt, marketId, channelId, channelSeq, balance,
|
||||
isFinal)
|
||||
return recover(data, signature)
|
||||
@@ -0,0 +1,162 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from ._eip712_base import sign, recover, is_address, is_signature, is_eth_privkey, is_bytes16, \
|
||||
is_block_number, is_chain_id
|
||||
|
||||
|
||||
def _create_eip712_channel_open(chainId: int, verifyingContract: bytes, ctype: int, openedAt: int,
|
||||
marketId: bytes, channelId: bytes, actor: bytes, delegate: bytes,
|
||||
marketmaker: bytes, recipient: bytes, amount: int) -> dict:
|
||||
"""
|
||||
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param ctype:
|
||||
:param openedAt:
|
||||
:param marketId:
|
||||
:param channelId:
|
||||
:param actor:
|
||||
:param delegate:
|
||||
:param marketmaker:
|
||||
:param recipient:
|
||||
:param amount:
|
||||
:return:
|
||||
"""
|
||||
assert is_chain_id(chainId)
|
||||
assert is_address(verifyingContract)
|
||||
assert type(ctype) == int
|
||||
assert is_block_number(openedAt)
|
||||
assert is_bytes16(marketId)
|
||||
assert is_bytes16(channelId)
|
||||
assert is_address(actor)
|
||||
assert is_address(delegate)
|
||||
assert is_address(marketmaker)
|
||||
assert is_address(recipient)
|
||||
assert type(amount) == int
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712ChannelOpen': [{
|
||||
'name': 'chainId',
|
||||
'type': 'uint256'
|
||||
}, {
|
||||
'name': 'verifyingContract',
|
||||
'type': 'address'
|
||||
}, {
|
||||
'name': 'ctype',
|
||||
'type': 'uint8'
|
||||
}, {
|
||||
'name': 'openedAt',
|
||||
'type': 'uint256'
|
||||
}, {
|
||||
'name': 'marketId',
|
||||
'type': 'bytes16'
|
||||
}, {
|
||||
'name': 'channelId',
|
||||
'type': 'bytes16'
|
||||
}, {
|
||||
'name': 'actor',
|
||||
'type': 'address'
|
||||
}, {
|
||||
'name': 'delegate',
|
||||
'type': 'address'
|
||||
}, {
|
||||
'name': 'marketmaker',
|
||||
'type': 'address'
|
||||
}, {
|
||||
'name': 'recipient',
|
||||
'type': 'address'
|
||||
}, {
|
||||
'name': 'amount',
|
||||
'type': 'uint256'
|
||||
}]
|
||||
},
|
||||
'primaryType': 'EIP712ChannelOpen',
|
||||
'domain': {
|
||||
'name': 'XBR',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'chainId': chainId,
|
||||
'verifyingContract': verifyingContract,
|
||||
'ctype': ctype,
|
||||
'openedAt': openedAt,
|
||||
'marketId': marketId,
|
||||
'channelId': channelId,
|
||||
'actor': actor,
|
||||
'delegate': delegate,
|
||||
'marketmaker': marketmaker,
|
||||
'recipient': recipient,
|
||||
'amount': amount
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_channel_open(eth_privkey: bytes, chainId: int, verifyingContract: bytes, ctype: int,
|
||||
openedAt: int, marketId: bytes, channelId: bytes, actor: bytes, delegate: bytes,
|
||||
marketmaker: bytes, recipient: bytes, amount: int) -> bytes:
|
||||
"""
|
||||
|
||||
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
|
||||
:type eth_privkey: bytes
|
||||
|
||||
:return: The signature according to EIP712 (32+32+1 raw bytes).
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_eth_privkey(eth_privkey)
|
||||
|
||||
data = _create_eip712_channel_open(chainId, verifyingContract, ctype, openedAt, marketId, channelId,
|
||||
actor, delegate, marketmaker, recipient, amount)
|
||||
return sign(eth_privkey, data)
|
||||
|
||||
|
||||
def recover_eip712_channel_open(chainId: int, verifyingContract: bytes, ctype: int, openedAt: int,
|
||||
marketId: bytes, channelId: bytes, actor: bytes, delegate: bytes,
|
||||
marketmaker: bytes, recipient: bytes, amount: int, signature: bytes) -> bytes:
|
||||
"""
|
||||
Recover the signer address the given EIP712 signature was signed with.
|
||||
|
||||
:return: The (computed) signer address the signature was signed with.
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_signature(signature)
|
||||
|
||||
data = _create_eip712_channel_open(chainId, verifyingContract, ctype, openedAt, marketId, channelId,
|
||||
actor, delegate, marketmaker, recipient, amount)
|
||||
return recover(data, signature)
|
||||
@@ -0,0 +1,168 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from typing import Optional
|
||||
from ._eip712_base import sign, recover, is_address, is_bytes16, is_eth_privkey, \
|
||||
is_signature, is_chain_id, is_block_number
|
||||
|
||||
|
||||
def _create_eip712_consent(chainId: int, verifyingContract: bytes, member: bytes, updated: int,
|
||||
marketId: bytes, delegate: bytes, delegateType: int, apiCatalog: bytes,
|
||||
consent: bool, servicePrefix: Optional[str]) -> dict:
|
||||
"""
|
||||
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param member:
|
||||
:param updated:
|
||||
:param marketId:
|
||||
:param delegate:
|
||||
:param delegateType:
|
||||
:param apiCatalog:
|
||||
:param consent:
|
||||
:param servicePrefix:
|
||||
:return:
|
||||
"""
|
||||
assert is_chain_id(chainId)
|
||||
assert is_address(verifyingContract)
|
||||
assert is_address(member)
|
||||
assert is_block_number(updated)
|
||||
assert is_bytes16(marketId)
|
||||
assert is_address(delegate)
|
||||
assert type(delegateType) == int
|
||||
assert is_bytes16(apiCatalog)
|
||||
assert type(consent) == bool
|
||||
assert servicePrefix is None or type(servicePrefix) == str
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712Consent': [
|
||||
{
|
||||
'name': 'chainId',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'verifyingContract',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'member',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'updated',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'marketId',
|
||||
'type': 'bytes16'
|
||||
},
|
||||
{
|
||||
'name': 'delegate',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'delegateType',
|
||||
'type': 'uint8'
|
||||
},
|
||||
{
|
||||
'name': 'apiCatalog',
|
||||
'type': 'bytes16'
|
||||
},
|
||||
{
|
||||
'name': 'consent',
|
||||
'type': 'bool'
|
||||
},
|
||||
{
|
||||
'name': 'servicePrefix',
|
||||
'type': 'string'
|
||||
},
|
||||
]
|
||||
},
|
||||
'primaryType': 'EIP712Consent',
|
||||
'domain': {
|
||||
'name': 'XBR',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'chainId': chainId,
|
||||
'verifyingContract': verifyingContract,
|
||||
'member': member,
|
||||
'updated': updated,
|
||||
'marketId': marketId,
|
||||
'delegate': delegate,
|
||||
'delegateType': delegateType,
|
||||
'apiCatalog': apiCatalog,
|
||||
'consent': consent,
|
||||
'servicePrefix': servicePrefix or ''
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_consent(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes,
|
||||
updated: int, marketId: bytes, delegate: bytes, delegateType: int, apiCatalog: bytes,
|
||||
consent: bool, servicePrefix: str) -> bytes:
|
||||
"""
|
||||
|
||||
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
|
||||
:type eth_privkey: bytes
|
||||
|
||||
:return: The signature according to EIP712 (32+32+1 raw bytes).
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_eth_privkey(eth_privkey)
|
||||
|
||||
data = _create_eip712_consent(chainId, verifyingContract, member, updated, marketId, delegate,
|
||||
delegateType, apiCatalog, consent, servicePrefix)
|
||||
return sign(eth_privkey, data)
|
||||
|
||||
|
||||
def recover_eip712_consent(chainId: int, verifyingContract: bytes, member: bytes, updated: int,
|
||||
marketId: bytes, delegate: bytes, delegateType: int, apiCatalog: bytes,
|
||||
consent: bool, servicePrefix: str, signature: bytes) -> bytes:
|
||||
"""
|
||||
Recover the signer address the given EIP712 signature was signed with.
|
||||
|
||||
:return: The (computed) signer address the signature was signed with.
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_signature(signature)
|
||||
|
||||
data = _create_eip712_consent(chainId, verifyingContract, member, updated, marketId, delegate,
|
||||
delegateType, apiCatalog, consent, servicePrefix)
|
||||
return recover(data, signature)
|
||||
@@ -0,0 +1,391 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 pprint
|
||||
from typing import Dict, Any, Optional, List
|
||||
from binascii import a2b_hex, b2a_hex
|
||||
|
||||
import web3
|
||||
import cbor2
|
||||
|
||||
from py_eth_sig_utils.eip712 import encode_typed_data
|
||||
|
||||
from autobahn.wamp.message import _URI_PAT_REALM_NAME_ETH
|
||||
from autobahn.xbr._secmod import EthereumKey
|
||||
|
||||
from ._eip712_base import sign, recover, is_chain_id, is_address, is_cs_pubkey, \
|
||||
is_block_number, is_signature, is_eth_privkey
|
||||
from ._eip712_certificate import EIP712Certificate
|
||||
|
||||
|
||||
def create_eip712_delegate_certificate(chainId: int,
|
||||
verifyingContract: bytes,
|
||||
validFrom: int,
|
||||
delegate: bytes,
|
||||
csPubKey: bytes,
|
||||
bootedAt: int,
|
||||
meta: str) -> dict:
|
||||
"""
|
||||
Delegate certificate: dynamic/one-time, off-chain.
|
||||
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param validFrom:
|
||||
:param delegate:
|
||||
:param csPubKey:
|
||||
:param bootedAt:
|
||||
:param meta:
|
||||
:return:
|
||||
"""
|
||||
assert is_chain_id(chainId)
|
||||
assert is_address(verifyingContract)
|
||||
assert is_block_number(validFrom)
|
||||
assert is_address(delegate)
|
||||
assert is_cs_pubkey(csPubKey)
|
||||
assert type(bootedAt) == int
|
||||
assert meta is None or type(meta) == str
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712DelegateCertificate': [
|
||||
{
|
||||
'name': 'chainId',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'verifyingContract',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'validFrom',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'delegate',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'csPubKey',
|
||||
'type': 'bytes32'
|
||||
},
|
||||
{
|
||||
'name': 'bootedAt',
|
||||
'type': 'uint64'
|
||||
},
|
||||
{
|
||||
'name': 'meta',
|
||||
'type': 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
'primaryType': 'EIP712DelegateCertificate',
|
||||
'domain': {
|
||||
'name': 'WMP',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'chainId': chainId,
|
||||
'verifyingContract': verifyingContract,
|
||||
'validFrom': validFrom,
|
||||
'delegate': delegate,
|
||||
'csPubKey': csPubKey,
|
||||
'bootedAt': bootedAt,
|
||||
'meta': meta or '',
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_delegate_certificate(eth_privkey: bytes,
|
||||
chainId: int,
|
||||
verifyingContract: bytes,
|
||||
validFrom: int,
|
||||
delegate: bytes,
|
||||
csPubKey: bytes,
|
||||
bootedAt: int,
|
||||
meta: str) -> bytes:
|
||||
"""
|
||||
Sign the given data using a EIP712 based signature with the provided private key.
|
||||
|
||||
:param eth_privkey: Signing key.
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param validFrom:
|
||||
:param delegate:
|
||||
:param csPubKey:
|
||||
:param bootedAt:
|
||||
:param meta:
|
||||
:return: The signature according to EIP712 (32+32+1 raw bytes).
|
||||
"""
|
||||
assert is_eth_privkey(eth_privkey)
|
||||
|
||||
data = create_eip712_delegate_certificate(chainId, verifyingContract, validFrom, delegate,
|
||||
csPubKey, bootedAt, meta)
|
||||
return sign(eth_privkey, data)
|
||||
|
||||
|
||||
def recover_eip712_delegate_certificate(chainId: int,
|
||||
verifyingContract: bytes,
|
||||
validFrom: int,
|
||||
delegate: bytes,
|
||||
csPubKey: bytes,
|
||||
bootedAt: int,
|
||||
meta: str,
|
||||
signature: bytes) -> bytes:
|
||||
"""
|
||||
Recover the signer address the given EIP712 signature was signed with.
|
||||
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param validFrom:
|
||||
:param delegate:
|
||||
:param csPubKey:
|
||||
:param bootedAt:
|
||||
:param signature:
|
||||
:param meta:
|
||||
:return: The (computed) signer address the signature was signed with.
|
||||
"""
|
||||
assert is_signature(signature)
|
||||
|
||||
data = create_eip712_delegate_certificate(chainId, verifyingContract, validFrom, delegate,
|
||||
csPubKey, bootedAt, meta)
|
||||
return recover(data, signature)
|
||||
|
||||
|
||||
class EIP712DelegateCertificate(EIP712Certificate):
|
||||
__slots__ = (
|
||||
# EIP712 attributes
|
||||
'chainId',
|
||||
'verifyingContract',
|
||||
'validFrom',
|
||||
'delegate',
|
||||
'csPubKey',
|
||||
'bootedAt',
|
||||
'meta',
|
||||
|
||||
# additional attributes
|
||||
'signatures',
|
||||
'hash',
|
||||
)
|
||||
|
||||
def __init__(self, chainId: int, verifyingContract: bytes, validFrom: int, delegate: bytes, csPubKey: bytes,
|
||||
bootedAt: int, meta: str, signatures: Optional[List[bytes]] = None):
|
||||
super().__init__(chainId, verifyingContract, validFrom)
|
||||
self.delegate = delegate
|
||||
self.csPubKey = csPubKey
|
||||
self.bootedAt = bootedAt
|
||||
self.meta = meta
|
||||
self.signatures = signatures
|
||||
eip712 = create_eip712_delegate_certificate(chainId,
|
||||
verifyingContract,
|
||||
validFrom,
|
||||
delegate,
|
||||
csPubKey,
|
||||
bootedAt,
|
||||
meta)
|
||||
self.hash = encode_typed_data(eip712)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
if not EIP712DelegateCertificate.__eq__(self, other):
|
||||
return False
|
||||
if other.chainId != self.chainId:
|
||||
return False
|
||||
if other.verifyingContract != self.verifyingContract:
|
||||
return False
|
||||
if other.validFrom != self.validFrom:
|
||||
return False
|
||||
if other.delegate != self.delegate:
|
||||
return False
|
||||
if other.csPubKey != self.csPubKey:
|
||||
return False
|
||||
if other.bootedAt != self.bootedAt:
|
||||
return False
|
||||
if other.meta != self.meta:
|
||||
return False
|
||||
if other.signatures != self.signatures:
|
||||
return False
|
||||
if other.hash != self.hash:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return pprint.pformat(self.marshal())
|
||||
|
||||
def sign(self, key: EthereumKey, binary: bool = False) -> bytes:
|
||||
eip712 = create_eip712_delegate_certificate(self.chainId,
|
||||
self.verifyingContract,
|
||||
self.validFrom,
|
||||
self.delegate,
|
||||
self.csPubKey,
|
||||
self.bootedAt,
|
||||
self.meta)
|
||||
return key.sign_typed_data(eip712, binary=binary)
|
||||
|
||||
def recover(self, signature: bytes) -> bytes:
|
||||
return recover_eip712_delegate_certificate(self.chainId,
|
||||
self.verifyingContract,
|
||||
self.validFrom,
|
||||
self.delegate,
|
||||
self.csPubKey,
|
||||
self.bootedAt,
|
||||
self.meta,
|
||||
signature)
|
||||
|
||||
def marshal(self, binary: bool = False) -> Dict[str, Any]:
|
||||
obj = create_eip712_delegate_certificate(chainId=self.chainId,
|
||||
verifyingContract=self.verifyingContract,
|
||||
validFrom=self.validFrom,
|
||||
delegate=self.delegate,
|
||||
csPubKey=self.csPubKey,
|
||||
bootedAt=self.bootedAt,
|
||||
meta=self.meta)
|
||||
if not binary:
|
||||
obj['message']['verifyingContract'] = web3.Web3.toChecksumAddress(obj['message']['verifyingContract']) if obj['message']['verifyingContract'] else None
|
||||
obj['message']['delegate'] = web3.Web3.toChecksumAddress(obj['message']['delegate']) if obj['message']['delegate'] else None
|
||||
obj['message']['csPubKey'] = b2a_hex(obj['message']['csPubKey']).decode() if obj['message']['csPubKey'] else None
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def parse(obj) -> 'EIP712DelegateCertificate':
|
||||
if type(obj) != dict:
|
||||
raise ValueError('invalid type {} for object in EIP712DelegateCertificate.parse'.format(type(obj)))
|
||||
|
||||
primaryType = obj.get('primaryType', None)
|
||||
if primaryType != 'EIP712DelegateCertificate':
|
||||
raise ValueError('invalid primaryType "{}" - expected "EIP712DelegateCertificate"'.format(primaryType))
|
||||
|
||||
# FIXME: check EIP712 types, domain
|
||||
|
||||
data = obj.get('message', None)
|
||||
if type(data) != dict:
|
||||
raise ValueError('invalid type {} for EIP712DelegateCertificate'.format(type(data)))
|
||||
for k in data:
|
||||
if k not in ['type', 'chainId', 'verifyingContract', 'delegate', 'validFrom', 'csPubKey', 'bootedAt', 'meta']:
|
||||
raise ValueError('invalid attribute "{}" in EIP712DelegateCertificate'.format(k))
|
||||
|
||||
_type = data.get('type', None)
|
||||
if _type and _type != 'EIP712DelegateCertificate':
|
||||
raise ValueError('unexpected type "{}" in EIP712DelegateCertificate'.format(_type))
|
||||
|
||||
chainId = data.get('chainId', None)
|
||||
if chainId is None:
|
||||
raise ValueError('missing chainId in EIP712DelegateCertificate')
|
||||
if type(chainId) != int:
|
||||
raise ValueError('invalid type {} for chainId in EIP712DelegateCertificate'.format(type(chainId)))
|
||||
|
||||
verifyingContract = data.get('verifyingContract', None)
|
||||
if verifyingContract is None:
|
||||
raise ValueError('missing verifyingContract in EIP712DelegateCertificate')
|
||||
if type(verifyingContract) != str:
|
||||
raise ValueError(
|
||||
'invalid type {} for verifyingContract in EIP712DelegateCertificate'.format(type(verifyingContract)))
|
||||
if not _URI_PAT_REALM_NAME_ETH.match(verifyingContract):
|
||||
raise ValueError(
|
||||
'invalid value "{}" for verifyingContract in EIP712DelegateCertificate'.format(verifyingContract))
|
||||
verifyingContract = a2b_hex(verifyingContract[2:])
|
||||
|
||||
validFrom = data.get('validFrom', None)
|
||||
if validFrom is None:
|
||||
raise ValueError('missing validFrom in EIP712DelegateCertificate')
|
||||
if type(validFrom) != int:
|
||||
raise ValueError('invalid type {} for validFrom in EIP712DelegateCertificate'.format(type(validFrom)))
|
||||
|
||||
delegate = data.get('delegate', None)
|
||||
if delegate is None:
|
||||
raise ValueError('missing delegate in EIP712DelegateCertificate')
|
||||
if type(delegate) != str:
|
||||
raise ValueError('invalid type {} for delegate in EIP712DelegateCertificate'.format(type(delegate)))
|
||||
if not _URI_PAT_REALM_NAME_ETH.match(delegate):
|
||||
raise ValueError('invalid value "{}" for verifyingContract in EIP712DelegateCertificate'.format(delegate))
|
||||
delegate = a2b_hex(delegate[2:])
|
||||
|
||||
csPubKey = data.get('csPubKey', None)
|
||||
if csPubKey is None:
|
||||
raise ValueError('missing csPubKey in EIP712DelegateCertificate')
|
||||
if type(csPubKey) != str:
|
||||
raise ValueError('invalid type {} for csPubKey in EIP712DelegateCertificate'.format(type(csPubKey)))
|
||||
if len(csPubKey) != 64:
|
||||
raise ValueError('invalid value "{}" for csPubKey in EIP712DelegateCertificate'.format(csPubKey))
|
||||
csPubKey = a2b_hex(csPubKey)
|
||||
|
||||
bootedAt = data.get('bootedAt', None)
|
||||
if bootedAt is None:
|
||||
raise ValueError('missing bootedAt in EIP712DelegateCertificate')
|
||||
if type(bootedAt) != int:
|
||||
raise ValueError('invalid type {} for bootedAt in EIP712DelegateCertificate'.format(type(bootedAt)))
|
||||
|
||||
meta = data.get('meta', None)
|
||||
if meta is None:
|
||||
raise ValueError('missing meta in EIP712DelegateCertificate')
|
||||
if type(meta) != str:
|
||||
raise ValueError('invalid type {} for meta in EIP712DelegateCertificate'.format(type(meta)))
|
||||
|
||||
obj = EIP712DelegateCertificate(chainId=chainId, verifyingContract=verifyingContract, validFrom=validFrom,
|
||||
delegate=delegate, csPubKey=csPubKey, bootedAt=bootedAt, meta=meta)
|
||||
return obj
|
||||
|
||||
def save(self, filename: str) -> int:
|
||||
"""
|
||||
Save certificate to file. File format (serialized as CBOR):
|
||||
|
||||
[cert_hash: bytes, cert_eip712: Dict[str, Any], cert_signatures: List[bytes]]
|
||||
|
||||
:param filename:
|
||||
:return:
|
||||
"""
|
||||
cert_obj = [self.hash, self.marshal(binary=True), self.signatures or []]
|
||||
with open(filename, 'wb') as f:
|
||||
data = cbor2.dumps(cert_obj)
|
||||
f.write(data)
|
||||
return len(data)
|
||||
|
||||
@staticmethod
|
||||
def load(filename) -> 'EIP712DelegateCertificate':
|
||||
if not os.path.isfile(filename):
|
||||
raise RuntimeError('cannot create EIP712DelegateCertificate from filename "{}": not a file'.format(filename))
|
||||
with open(filename, 'rb') as f:
|
||||
cert_hash, cert_eip712, cert_signatures = cbor2.loads(f.read())
|
||||
cert = EIP712DelegateCertificate.parse(cert_eip712, binary=True)
|
||||
assert cert_hash == cert.hash
|
||||
cert.signatures = cert_signatures
|
||||
return cert
|
||||
@@ -0,0 +1,157 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from typing import Optional
|
||||
from ._eip712_base import sign, recover, is_chain_id, is_address, is_bytes16, is_cs_pubkey, \
|
||||
is_block_number, is_signature, is_eth_privkey
|
||||
|
||||
|
||||
def _create_eip712_domain_create(chainId: int, verifyingContract: bytes, member: bytes, created: int,
|
||||
domainId: bytes, domainKey: bytes, license: str, terms: str,
|
||||
meta: Optional[str]) -> dict:
|
||||
"""
|
||||
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param member:
|
||||
:param created:
|
||||
:param domainId:
|
||||
:param domainKey:
|
||||
:param license:
|
||||
:param terms:
|
||||
:param meta:
|
||||
:return:
|
||||
"""
|
||||
assert is_chain_id(chainId)
|
||||
assert is_address(verifyingContract)
|
||||
assert is_address(member)
|
||||
assert is_block_number(created)
|
||||
assert is_bytes16(domainId)
|
||||
assert is_cs_pubkey(domainKey)
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712DomainCreate': [
|
||||
{
|
||||
'name': 'chainId',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'verifyingContract',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'member',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'created',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'domainId',
|
||||
'type': 'bytes16'
|
||||
},
|
||||
{
|
||||
'name': 'domainKey',
|
||||
'type': 'bytes32'
|
||||
},
|
||||
{
|
||||
'name': 'license',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'terms',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'meta',
|
||||
'type': 'string'
|
||||
},
|
||||
]
|
||||
},
|
||||
'primaryType': 'EIP712DomainCreate',
|
||||
'domain': {
|
||||
'name': 'XBR',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'chainId': chainId,
|
||||
'verifyingContract': verifyingContract,
|
||||
'member': member,
|
||||
'created': created,
|
||||
'domainId': domainId,
|
||||
'domainKey': domainKey,
|
||||
'license': license,
|
||||
'terms': terms,
|
||||
'meta': meta or '',
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_domain_create(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes, created: int,
|
||||
domainId: bytes, domainKey: bytes, license: str, terms: str,
|
||||
meta: str) -> bytes:
|
||||
"""
|
||||
|
||||
:param eth_privkey: Ethereum address of member (a raw 20 bytes Ethereum address).
|
||||
:type eth_privkey: bytes
|
||||
|
||||
:return: The signature according to EIP712 (32+32+1 raw bytes).
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_eth_privkey(eth_privkey)
|
||||
|
||||
data = _create_eip712_domain_create(chainId, verifyingContract, member, created, domainId, domainKey, license,
|
||||
terms, meta)
|
||||
return sign(eth_privkey, data)
|
||||
|
||||
|
||||
def recover_eip712_domain_create(chainId: int, verifyingContract: bytes, member: bytes, created: int, domainId: bytes,
|
||||
domainKey: bytes, license: str, terms: str, meta: str, signature: bytes) -> bytes:
|
||||
"""
|
||||
Recover the signer address the given EIP712 signature was signed with.
|
||||
|
||||
:return: The (computed) signer address the signature was signed with.
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_signature(signature)
|
||||
|
||||
data = _create_eip712_domain_create(chainId, verifyingContract, member, created, domainId, domainKey, license,
|
||||
terms, meta)
|
||||
return recover(data, signature)
|
||||
@@ -0,0 +1,165 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from typing import Optional
|
||||
from ._eip712_base import sign, recover, is_address, is_bytes16, is_block_number, \
|
||||
is_chain_id, is_eth_privkey, is_signature
|
||||
|
||||
|
||||
def _create_eip712_market_create(chainId: int, verifyingContract: bytes, member: bytes, created: int,
|
||||
marketId: bytes, coin: bytes, terms: str, meta: Optional[str], maker: bytes,
|
||||
providerSecurity: int, consumerSecurity: int, marketFee: int) -> dict:
|
||||
"""
|
||||
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param member:
|
||||
:param created:
|
||||
:param marketId:
|
||||
:param coin:
|
||||
:param terms:
|
||||
:param meta:
|
||||
:param maker:
|
||||
:param providerSecurity:
|
||||
:param consumerSecurity:
|
||||
:param marketFee:
|
||||
:return:
|
||||
"""
|
||||
assert is_chain_id(chainId)
|
||||
assert is_address(verifyingContract)
|
||||
assert is_address(member)
|
||||
assert is_block_number(created)
|
||||
assert is_bytes16(marketId)
|
||||
assert is_address(coin)
|
||||
assert type(terms) == str
|
||||
assert meta is None or type(meta) == str
|
||||
assert is_address(maker)
|
||||
assert type(providerSecurity) == int
|
||||
assert type(consumerSecurity) == int
|
||||
assert type(marketFee) == int
|
||||
|
||||
# FIXME: add "coin" in below once we have done that in XBRTypes
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712MarketCreate': [{
|
||||
'name': 'chainId',
|
||||
'type': 'uint256'
|
||||
}, {
|
||||
'name': 'verifyingContract',
|
||||
'type': 'address'
|
||||
}, {
|
||||
'name': 'member',
|
||||
'type': 'address'
|
||||
}, {
|
||||
'name': 'created',
|
||||
'type': 'uint256'
|
||||
}, {
|
||||
'name': 'marketId',
|
||||
'type': 'bytes16'
|
||||
}, {
|
||||
'name': 'coin',
|
||||
'type': 'address'
|
||||
}, {
|
||||
'name': 'terms',
|
||||
'type': 'string'
|
||||
}, {
|
||||
'name': 'meta',
|
||||
'type': 'string'
|
||||
}, {
|
||||
'name': 'maker',
|
||||
'type': 'address'
|
||||
}, {
|
||||
'name': 'marketFee',
|
||||
'type': 'uint256',
|
||||
}]
|
||||
},
|
||||
'primaryType': 'EIP712MarketCreate',
|
||||
'domain': {
|
||||
'name': 'XBR',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'chainId': chainId,
|
||||
'verifyingContract': verifyingContract,
|
||||
'member': member,
|
||||
'created': created,
|
||||
'marketId': marketId,
|
||||
'coin': coin,
|
||||
'terms': terms,
|
||||
'meta': meta or '',
|
||||
'maker': maker,
|
||||
'marketFee': marketFee,
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_market_create(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes,
|
||||
created: int, marketId: bytes, coin: bytes, terms: str, meta: str, maker: bytes,
|
||||
providerSecurity: int, consumerSecurity: int, marketFee: int) -> bytes:
|
||||
"""
|
||||
|
||||
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
|
||||
:type eth_privkey: bytes
|
||||
|
||||
:return: The signature according to EIP712 (32+32+1 raw bytes).
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_eth_privkey(eth_privkey)
|
||||
|
||||
data = _create_eip712_market_create(chainId, verifyingContract, member, created, marketId, coin, terms,
|
||||
meta, maker, providerSecurity, consumerSecurity, marketFee)
|
||||
return sign(eth_privkey, data)
|
||||
|
||||
|
||||
def recover_eip712_market_create(chainId: int, verifyingContract: bytes, member: bytes, created: int,
|
||||
marketId: bytes, coin: bytes, terms: str, meta: str, maker: bytes,
|
||||
providerSecurity: int, consumerSecurity: int, marketFee: int,
|
||||
signature: bytes) -> bytes:
|
||||
"""
|
||||
Recover the signer address the given EIP712 signature was signed with.
|
||||
|
||||
:return: The (computed) signer address the signature was signed with.
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_signature(signature)
|
||||
|
||||
data = _create_eip712_market_create(chainId, verifyingContract, member, created, marketId, coin, terms,
|
||||
meta, maker, providerSecurity, consumerSecurity, marketFee)
|
||||
|
||||
return recover(data, signature)
|
||||
@@ -0,0 +1,144 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from typing import Optional
|
||||
from ._eip712_base import sign, recover, is_address, is_bytes16, is_block_number, \
|
||||
is_chain_id, is_eth_privkey, is_signature
|
||||
|
||||
|
||||
def _create_eip712_market_join(chainId: int, verifyingContract: bytes, member: bytes, joined: int,
|
||||
marketId: bytes, actorType: int, meta: Optional[str]) -> dict:
|
||||
"""
|
||||
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param member:
|
||||
:param joined:
|
||||
:param marketId:
|
||||
:param actorType:
|
||||
:param meta:
|
||||
:return:
|
||||
"""
|
||||
assert is_chain_id(chainId)
|
||||
assert is_address(verifyingContract)
|
||||
assert is_address(member)
|
||||
assert is_block_number(joined)
|
||||
assert is_bytes16(marketId)
|
||||
assert type(actorType) == int
|
||||
assert meta is None or type(meta) == str
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712MarketJoin': [
|
||||
{
|
||||
'name': 'chainId',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'verifyingContract',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'member',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'joined',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'marketId',
|
||||
'type': 'bytes16'
|
||||
},
|
||||
{
|
||||
'name': 'actorType',
|
||||
'type': 'uint8'
|
||||
},
|
||||
{
|
||||
'name': 'meta',
|
||||
'type': 'string',
|
||||
},
|
||||
]
|
||||
},
|
||||
'primaryType': 'EIP712MarketJoin',
|
||||
'domain': {
|
||||
'name': 'XBR',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'chainId': chainId,
|
||||
'verifyingContract': verifyingContract,
|
||||
'member': member,
|
||||
'joined': joined,
|
||||
'marketId': marketId,
|
||||
'actorType': actorType,
|
||||
'meta': meta or '',
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_market_join(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes,
|
||||
joined: int, marketId: bytes, actorType: int, meta: str) -> bytes:
|
||||
"""
|
||||
|
||||
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
|
||||
:type eth_privkey: bytes
|
||||
|
||||
:return: The signature according to EIP712 (32+32+1 raw bytes).
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_eth_privkey(eth_privkey)
|
||||
|
||||
data = _create_eip712_market_join(chainId, verifyingContract, member, joined, marketId, actorType, meta)
|
||||
|
||||
return sign(eth_privkey, data)
|
||||
|
||||
|
||||
def recover_eip712_market_join(chainId: int, verifyingContract: bytes, member: bytes, joined: int,
|
||||
marketId: bytes, actorType: int, meta: str, signature: bytes) -> bytes:
|
||||
"""
|
||||
Recover the signer address the given EIP712 signature was signed with.
|
||||
|
||||
:return: The (computed) signer address the signature was signed with.
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_signature(signature)
|
||||
|
||||
data = _create_eip712_market_join(chainId, verifyingContract, member, joined, marketId, actorType, meta)
|
||||
|
||||
return recover(data, signature)
|
||||
@@ -0,0 +1,137 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from ._eip712_base import sign, recover, is_address, is_bytes16, is_block_number, \
|
||||
is_chain_id, is_eth_privkey, is_signature
|
||||
|
||||
|
||||
def _create_eip712_market_leave(chainId: int, verifyingContract: bytes, member: bytes, left: int,
|
||||
marketId: bytes, actorType: int) -> dict:
|
||||
"""
|
||||
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param member:
|
||||
:param joined:
|
||||
:param marketId:
|
||||
:param actorType:
|
||||
:param meta:
|
||||
:return:
|
||||
"""
|
||||
assert is_chain_id(chainId)
|
||||
assert is_address(verifyingContract)
|
||||
assert is_address(member)
|
||||
assert is_block_number(left)
|
||||
assert is_bytes16(marketId)
|
||||
assert type(actorType) == int
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712MarketLeave': [
|
||||
{
|
||||
'name': 'chainId',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'verifyingContract',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'member',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'left',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'marketId',
|
||||
'type': 'bytes16'
|
||||
},
|
||||
{
|
||||
'name': 'actorType',
|
||||
'type': 'uint8'
|
||||
},
|
||||
]
|
||||
},
|
||||
'primaryType': 'EIP712MarketLeave',
|
||||
'domain': {
|
||||
'name': 'XBR',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'chainId': chainId,
|
||||
'verifyingContract': verifyingContract,
|
||||
'member': member,
|
||||
'left': left,
|
||||
'marketId': marketId,
|
||||
'actorType': actorType,
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_market_leave(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes, left: int,
|
||||
marketId: bytes, actorType: int) -> bytes:
|
||||
"""
|
||||
|
||||
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
|
||||
:type eth_privkey: bytes
|
||||
|
||||
:return: The signature according to EIP712 (32+32+1 raw bytes).
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_eth_privkey(eth_privkey)
|
||||
|
||||
data = _create_eip712_market_leave(chainId, verifyingContract, member, left, marketId, actorType)
|
||||
|
||||
return sign(eth_privkey, data)
|
||||
|
||||
|
||||
def recover_eip712_market_leave(chainId: int, verifyingContract: bytes, member: bytes, left: int,
|
||||
marketId: bytes, actorType: int, signature: bytes) -> bytes:
|
||||
"""
|
||||
Recover the signer address the given EIP712 signature was signed with.
|
||||
|
||||
:return: The (computed) signer address the signature was signed with.
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_signature(signature)
|
||||
|
||||
data = _create_eip712_market_leave(chainId, verifyingContract, member, left, marketId, actorType)
|
||||
|
||||
return recover(data, signature)
|
||||
@@ -0,0 +1,85 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from ._eip712_base import sign, recover, is_address, is_eth_privkey, \
|
||||
is_signature, is_cs_pubkey
|
||||
|
||||
|
||||
def _create_eip712_market_member_login(member: bytes, client_pubkey: bytes) -> dict:
|
||||
assert is_address(member)
|
||||
assert is_cs_pubkey(client_pubkey)
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712MarketMemberLogin': [
|
||||
{
|
||||
'name': 'member',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'client_pubkey',
|
||||
'type': 'bytes32',
|
||||
},
|
||||
]
|
||||
},
|
||||
'primaryType': 'EIP712MarketMemberLogin',
|
||||
'domain': {
|
||||
'name': 'XBR',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'member': member,
|
||||
'client_pubkey': client_pubkey,
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_market_member_login(eth_privkey: bytes, member: bytes, client_pubkey: bytes) -> bytes:
|
||||
assert is_eth_privkey(eth_privkey)
|
||||
|
||||
data = _create_eip712_market_member_login(member, client_pubkey)
|
||||
|
||||
return sign(eth_privkey, data)
|
||||
|
||||
|
||||
def recover_eip712_market_member_login(member: bytes, client_pubkey: bytes, signature: bytes) -> bytes:
|
||||
assert is_signature(signature)
|
||||
|
||||
data = _create_eip712_market_member_login(member, client_pubkey)
|
||||
|
||||
return recover(data, signature)
|
||||
@@ -0,0 +1,146 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from ._eip712_base import sign, recover, is_address, is_block_number, \
|
||||
is_chain_id, is_eth_privkey, is_signature, is_cs_pubkey
|
||||
|
||||
|
||||
def _create_eip712_member_login(chainId: int, verifyingContract: bytes, member: bytes, loggedIn: int,
|
||||
timestamp: int, member_email: str, client_pubkey: bytes) -> dict:
|
||||
"""
|
||||
|
||||
:param chainId:
|
||||
:param blockNumber:
|
||||
:param verifyingContract:
|
||||
:param member:
|
||||
:param timestamp:
|
||||
:param member_email:
|
||||
:param client_pubkey:
|
||||
:return:
|
||||
"""
|
||||
assert is_chain_id(chainId)
|
||||
assert is_address(verifyingContract)
|
||||
assert is_address(member)
|
||||
assert is_block_number(loggedIn)
|
||||
assert type(timestamp) == int
|
||||
assert type(member_email) == str
|
||||
assert is_cs_pubkey(client_pubkey)
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712MemberLogin': [
|
||||
{
|
||||
'name': 'chainId',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'verifyingContract',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'member',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'loggedIn',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'timestamp',
|
||||
'type': 'uint64'
|
||||
},
|
||||
{
|
||||
'name': 'member_email',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'client_pubkey',
|
||||
'type': 'bytes32',
|
||||
},
|
||||
]
|
||||
},
|
||||
'primaryType': 'EIP712MemberLogin',
|
||||
'domain': {
|
||||
'name': 'XBR',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'chainId': chainId,
|
||||
'verifyingContract': verifyingContract,
|
||||
'member': member,
|
||||
'loggedIn': loggedIn,
|
||||
'timestamp': timestamp,
|
||||
'member_email': member_email,
|
||||
'client_pubkey': client_pubkey,
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_member_login(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes,
|
||||
loggedIn: int, timestamp: int, member_email: str, client_pubkey: bytes) -> bytes:
|
||||
"""
|
||||
|
||||
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
|
||||
:type eth_privkey: bytes
|
||||
|
||||
:return: The signature according to EIP712 (32+32+1 raw bytes).
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_eth_privkey(eth_privkey)
|
||||
|
||||
data = _create_eip712_member_login(chainId, verifyingContract, member, loggedIn, timestamp, member_email,
|
||||
client_pubkey)
|
||||
|
||||
return sign(eth_privkey, data)
|
||||
|
||||
|
||||
def recover_eip712_member_login(chainId: int, verifyingContract: bytes, member: bytes, loggedIn: int,
|
||||
timestamp: int, member_email: str, client_pubkey: bytes,
|
||||
signature: bytes) -> bytes:
|
||||
"""
|
||||
Recover the signer address the given EIP712 signature was signed with.
|
||||
|
||||
:return: The (computed) signer address the signature was signed with.
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_signature(signature)
|
||||
|
||||
data = _create_eip712_member_login(chainId, verifyingContract, member, loggedIn, timestamp, member_email,
|
||||
client_pubkey)
|
||||
|
||||
return recover(data, signature)
|
||||
@@ -0,0 +1,137 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from typing import Optional
|
||||
from ._eip712_base import sign, recover, is_address, is_block_number, \
|
||||
is_chain_id, is_eth_privkey, is_signature
|
||||
|
||||
|
||||
def _create_eip712_member_register(chainId: int, verifyingContract: bytes, member: bytes, registered: int,
|
||||
eula: str, profile: Optional[str]) -> dict:
|
||||
"""
|
||||
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param member:
|
||||
:param registered:
|
||||
:param eula:
|
||||
:param profile:
|
||||
:return:
|
||||
"""
|
||||
assert is_chain_id(chainId)
|
||||
assert is_address(verifyingContract)
|
||||
assert is_address(member)
|
||||
assert is_block_number(registered)
|
||||
assert type(eula) == str
|
||||
assert profile is None or type(profile) == str
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712MemberRegister': [
|
||||
{
|
||||
'name': 'chainId',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'verifyingContract',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'member',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'registered',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'eula',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'profile',
|
||||
'type': 'string'
|
||||
},
|
||||
]
|
||||
},
|
||||
'primaryType': 'EIP712MemberRegister',
|
||||
'domain': {
|
||||
'name': 'XBR',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'chainId': chainId,
|
||||
'verifyingContract': verifyingContract,
|
||||
'member': member,
|
||||
'registered': registered,
|
||||
'eula': eula,
|
||||
'profile': profile or '',
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_member_register(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes,
|
||||
registered: int, eula: Optional[str], profile: str) -> bytes:
|
||||
"""
|
||||
|
||||
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
|
||||
:type eth_privkey: bytes
|
||||
|
||||
:return: The signature according to EIP712 (32+32+1 raw bytes).
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_eth_privkey(eth_privkey)
|
||||
|
||||
data = _create_eip712_member_register(chainId, verifyingContract, member, registered, eula, profile)
|
||||
|
||||
return sign(eth_privkey, data)
|
||||
|
||||
|
||||
def recover_eip712_member_register(chainId: int, verifyingContract: bytes, member: bytes, registered: int,
|
||||
eula: str, profile: Optional[str], signature: bytes) -> bytes:
|
||||
"""
|
||||
Recover the signer address the given EIP712 signature was signed with.
|
||||
|
||||
:return: The (computed) signer address the signature was signed with.
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_signature(signature)
|
||||
|
||||
data = _create_eip712_member_register(chainId, verifyingContract, member, registered, eula, profile)
|
||||
|
||||
return recover(data, signature)
|
||||
@@ -0,0 +1,122 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from ._eip712_base import sign, recover, is_chain_id, is_address, \
|
||||
is_block_number, is_signature, is_eth_privkey
|
||||
|
||||
|
||||
def _create_eip712_member_unregister(chainId: int, verifyingContract: bytes, member: bytes,
|
||||
retired: int) -> dict:
|
||||
"""
|
||||
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param member:
|
||||
:param retired:
|
||||
:return:
|
||||
"""
|
||||
assert is_chain_id(chainId)
|
||||
assert is_address(verifyingContract)
|
||||
assert is_address(member)
|
||||
assert is_block_number(retired)
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712MemberUnregister': [
|
||||
{
|
||||
'name': 'chainId',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'verifyingContract',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'member',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'retired',
|
||||
'type': 'uint256'
|
||||
},
|
||||
]
|
||||
},
|
||||
'primaryType': 'EIP712MemberUnregister',
|
||||
'domain': {
|
||||
'name': 'XBR',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'chainId': chainId,
|
||||
'verifyingContract': verifyingContract,
|
||||
'member': member,
|
||||
'paired': retired,
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_member_unregister(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes,
|
||||
retired: int) -> bytes:
|
||||
"""
|
||||
|
||||
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
|
||||
:type eth_privkey: bytes
|
||||
|
||||
:return: The signature according to EIP712 (32+32+1 raw bytes).
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_eth_privkey(eth_privkey)
|
||||
|
||||
data = _create_eip712_member_unregister(chainId, verifyingContract, member, retired)
|
||||
|
||||
return sign(eth_privkey, data)
|
||||
|
||||
|
||||
def recover_eip712_member_unregister(chainId: int, verifyingContract: bytes, member: bytes, retired: int,
|
||||
signature: bytes) -> bytes:
|
||||
"""
|
||||
Recover the signer address the given EIP712 signature was signed with.
|
||||
|
||||
:return: The (computed) signer address the signature was signed with.
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_signature(signature)
|
||||
|
||||
data = _create_eip712_member_unregister(chainId, verifyingContract, member, retired)
|
||||
|
||||
return recover(data, signature)
|
||||
@@ -0,0 +1,167 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from typing import Optional
|
||||
from ._eip712_base import sign, recover, is_chain_id, is_address, is_bytes16, is_cs_pubkey, \
|
||||
is_block_number, is_signature, is_eth_privkey
|
||||
|
||||
|
||||
def _create_eip712_node_pair(chainId: int, verifyingContract: bytes, member: bytes, paired: int,
|
||||
nodeId: bytes, domainId: bytes, nodeType: int, nodeKey: bytes,
|
||||
amount: int, config: Optional[str]) -> dict:
|
||||
"""
|
||||
|
||||
:param chainId:
|
||||
:param verifyingContract:
|
||||
:param member:
|
||||
:param paired:
|
||||
:param nodeId:
|
||||
:param domainId:
|
||||
:param nodeKey:
|
||||
:param amount:
|
||||
:param config:
|
||||
:return:
|
||||
"""
|
||||
assert is_chain_id(chainId)
|
||||
assert is_address(verifyingContract)
|
||||
assert is_address(member)
|
||||
assert is_block_number(paired)
|
||||
assert is_bytes16(nodeId)
|
||||
assert is_bytes16(domainId)
|
||||
assert type(nodeType) == int
|
||||
assert is_cs_pubkey(nodeKey)
|
||||
assert type(amount) == int
|
||||
assert config is None or type(config) == str
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712NodePair': [
|
||||
{
|
||||
'name': 'chainId',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'verifyingContract',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'member',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'paired',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'nodeId',
|
||||
'type': 'bytes16'
|
||||
},
|
||||
{
|
||||
'name': 'domainId',
|
||||
'type': 'bytes16'
|
||||
},
|
||||
{
|
||||
'name': 'nodeType',
|
||||
'type': 'uint8'
|
||||
},
|
||||
{
|
||||
'name': 'nodeKey',
|
||||
'type': 'bytes16'
|
||||
},
|
||||
{
|
||||
'name': 'amount',
|
||||
'type': 'uint256',
|
||||
},
|
||||
{
|
||||
'name': 'config',
|
||||
'type': 'string',
|
||||
},
|
||||
]
|
||||
},
|
||||
'primaryType': 'EIP712NodePair',
|
||||
'domain': {
|
||||
'name': 'XBR',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'chainId': chainId,
|
||||
'verifyingContract': verifyingContract,
|
||||
'member': member,
|
||||
'paired': paired,
|
||||
'nodeId': nodeId,
|
||||
'domainId': domainId,
|
||||
'nodeType': nodeType,
|
||||
'nodeKey': nodeKey,
|
||||
'amount': amount,
|
||||
'config': config or '',
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_node_pair(eth_privkey: bytes, chainId: int, verifyingContract: bytes, member: bytes, paired: int,
|
||||
nodeId: bytes, domainId: bytes, nodeType: int, nodeKey: bytes,
|
||||
amount: int, config: Optional[str]) -> bytes:
|
||||
"""
|
||||
|
||||
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
|
||||
:type eth_privkey: bytes
|
||||
|
||||
:return: The signature according to EIP712 (32+32+1 raw bytes).
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_eth_privkey(eth_privkey)
|
||||
|
||||
data = _create_eip712_node_pair(chainId, verifyingContract, member, paired, nodeId, domainId, nodeType,
|
||||
nodeKey, amount, config)
|
||||
return sign(eth_privkey, data)
|
||||
|
||||
|
||||
def recover_eip712_node_pair(chainId: int, verifyingContract: bytes, member: bytes, paired: int,
|
||||
nodeId: bytes, domainId: bytes, nodeType: int, nodeKey: bytes,
|
||||
amount: int, config: str, signature: bytes) -> bytes:
|
||||
"""
|
||||
Recover the signer address the given EIP712 signature was signed with.
|
||||
|
||||
:return: The (computed) signer address the signature was signed with.
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert is_signature(signature)
|
||||
|
||||
data = _create_eip712_node_pair(chainId, verifyingContract, member, paired, nodeId, domainId, nodeType,
|
||||
nodeKey, amount, config)
|
||||
return recover(data, signature)
|
||||
471
backend/lib/python3.12/site-packages/autobahn/xbr/_frealm.py
Normal file
471
backend/lib/python3.12/site-packages/autobahn/xbr/_frealm.py
Normal file
@@ -0,0 +1,471 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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
|
||||
from binascii import b2a_hex
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
import web3
|
||||
from web3.contract import Contract
|
||||
from ens import ENS
|
||||
|
||||
from twisted.internet.defer import Deferred, inlineCallbacks
|
||||
from twisted.internet.threads import deferToThread
|
||||
|
||||
from autobahn.wamp.interfaces import ICryptosignKey, IEthereumKey
|
||||
from autobahn.wamp.message import identify_realm_name_category
|
||||
from autobahn.xbr import make_w3, EIP712AuthorityCertificate
|
||||
|
||||
|
||||
class Seeder(object):
|
||||
"""
|
||||
|
||||
"""
|
||||
__slots__ = (
|
||||
'_frealm',
|
||||
'_operator',
|
||||
'_label',
|
||||
'_country',
|
||||
'_legal',
|
||||
'_endpoint',
|
||||
'_bandwidth_requested',
|
||||
'_bandwidth_offered',
|
||||
)
|
||||
|
||||
def __init__(self,
|
||||
frealm: 'FederatedRealm',
|
||||
operator: Optional[str] = None,
|
||||
label: Optional[str] = None,
|
||||
country: Optional[str] = None,
|
||||
legal: Optional[str] = None,
|
||||
endpoint: Optional[str] = None,
|
||||
bandwidth_requested: Optional[int] = None,
|
||||
bandwidth_offered: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
|
||||
:param frealm:
|
||||
:param operator:
|
||||
:param label:
|
||||
:param country:
|
||||
:param legal:
|
||||
:param endpoint:
|
||||
:param bandwidth_requested:
|
||||
:param bandwidth_offered:
|
||||
"""
|
||||
self._frealm: FederatedRealm = frealm
|
||||
self._operator: Optional[str] = operator
|
||||
self._label: Optional[str] = label
|
||||
self._country: Optional[str] = country
|
||||
self._legal: Optional[str] = legal
|
||||
self._endpoint: Optional[str] = endpoint
|
||||
self._bandwidth_requested: Optional[str] = bandwidth_requested
|
||||
self._bandwidth_offered: Optional[str] = bandwidth_offered
|
||||
|
||||
@staticmethod
|
||||
def _create_eip712_connect(chain_id: int,
|
||||
verifying_contract: bytes,
|
||||
channel_binding: str,
|
||||
channel_id: bytes,
|
||||
block_no: int,
|
||||
challenge: bytes,
|
||||
pubkey: bytes,
|
||||
realm: bytes,
|
||||
delegate: bytes,
|
||||
seeder: bytes,
|
||||
bandwidth: int):
|
||||
channel_binding = channel_binding or ''
|
||||
channel_id = channel_id or b''
|
||||
assert chain_id
|
||||
assert verifying_contract
|
||||
assert channel_binding is not None
|
||||
assert channel_id is not None
|
||||
assert block_no
|
||||
assert challenge
|
||||
assert pubkey
|
||||
assert realm
|
||||
assert delegate
|
||||
assert bandwidth
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{
|
||||
'name': 'name',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'version',
|
||||
'type': 'string'
|
||||
},
|
||||
],
|
||||
'EIP712SeederConnect': [
|
||||
{
|
||||
'name': 'chainId',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'verifyingContract',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'channel_binding',
|
||||
'type': 'string'
|
||||
},
|
||||
{
|
||||
'name': 'channel_id',
|
||||
'type': 'bytes32'
|
||||
},
|
||||
{
|
||||
'name': 'block_no',
|
||||
'type': 'uint256'
|
||||
},
|
||||
{
|
||||
'name': 'challenge',
|
||||
'type': 'bytes32'
|
||||
},
|
||||
{
|
||||
'name': 'pubkey',
|
||||
'type': 'bytes32'
|
||||
},
|
||||
{
|
||||
'name': 'realm',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'delegate',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'seeder',
|
||||
'type': 'address'
|
||||
},
|
||||
{
|
||||
'name': 'bandwidth',
|
||||
'type': 'uint32'
|
||||
},
|
||||
]
|
||||
},
|
||||
'primaryType': 'EIP712SeederConnect',
|
||||
'domain': {
|
||||
'name': 'XBR',
|
||||
'version': '1',
|
||||
},
|
||||
'message': {
|
||||
'chainId': chain_id,
|
||||
'verifyingContract': verifying_contract,
|
||||
'channel_binding': channel_binding,
|
||||
'channel_id': channel_id,
|
||||
'block_no': block_no,
|
||||
'challenge': challenge,
|
||||
'pubkey': pubkey,
|
||||
'realm': realm,
|
||||
'delegate': delegate,
|
||||
'seeder': seeder,
|
||||
'bandwidth': bandwidth,
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
@inlineCallbacks
|
||||
def create_authextra(self,
|
||||
client_key: ICryptosignKey,
|
||||
delegate_key: IEthereumKey,
|
||||
bandwidth_requested: int,
|
||||
channel_id: Optional[bytes] = None,
|
||||
channel_binding: Optional[str] = None) -> Deferred:
|
||||
"""
|
||||
|
||||
:param client_key:
|
||||
:param delegate_key:
|
||||
:param bandwidth_requested:
|
||||
:param channel_id:
|
||||
:param channel_binding:
|
||||
:return:
|
||||
"""
|
||||
chain_id = 1
|
||||
# FIXME
|
||||
verifying_contract = b'\x01' * 20
|
||||
block_no = 1
|
||||
challenge = os.urandom(32)
|
||||
eip712_data = Seeder._create_eip712_connect(chain_id=chain_id,
|
||||
verifying_contract=verifying_contract,
|
||||
channel_binding=channel_binding,
|
||||
channel_id=channel_id,
|
||||
block_no=block_no,
|
||||
challenge=challenge,
|
||||
pubkey=client_key.public_key(binary=True),
|
||||
# FIXME
|
||||
# realm=self._frealm.address(binary=True),
|
||||
realm=b'\x02' * 20,
|
||||
delegate=delegate_key.address(binary=False),
|
||||
# FIXME
|
||||
# seeder=self._operator,
|
||||
seeder=b'\x03' * 20,
|
||||
bandwidth=bandwidth_requested)
|
||||
signature = yield delegate_key.sign_typed_data(eip712_data)
|
||||
authextra = {
|
||||
# string
|
||||
'pubkey': client_key.public_key(binary=False),
|
||||
|
||||
# string
|
||||
'challenge': challenge,
|
||||
|
||||
# string
|
||||
'channel_binding': channel_binding,
|
||||
|
||||
# string
|
||||
'channel_id': channel_id,
|
||||
|
||||
# address
|
||||
# FIXME
|
||||
'realm': '7f' * 20,
|
||||
|
||||
# int
|
||||
'chain_id': chain_id,
|
||||
|
||||
# int
|
||||
'block_no': block_no,
|
||||
|
||||
# address
|
||||
'delegate': delegate_key.address(binary=False),
|
||||
|
||||
# address
|
||||
'seeder': self._operator,
|
||||
|
||||
# int: requested bandwidth in kbps
|
||||
'bandwidth': bandwidth_requested,
|
||||
|
||||
# string: Eth signature by delegate_key over EIP712 typed data as above
|
||||
'signature': b2a_hex(signature).decode(),
|
||||
}
|
||||
return authextra
|
||||
|
||||
@property
|
||||
def frealm(self) -> 'FederatedRealm':
|
||||
"""
|
||||
|
||||
:return:
|
||||
"""
|
||||
return self._frealm
|
||||
|
||||
@property
|
||||
def operator(self) -> Optional[str]:
|
||||
"""
|
||||
Operator address, e.g. ``"0xe59C7418403CF1D973485B36660728a5f4A8fF9c"``.
|
||||
|
||||
:return: The Ethereum address of the endpoint operator.
|
||||
"""
|
||||
return self._operator
|
||||
|
||||
@property
|
||||
def label(self) -> Optional[str]:
|
||||
"""
|
||||
Operator endpoint label.
|
||||
|
||||
:return: A human readable label for the operator or specific operator endpoint.
|
||||
"""
|
||||
return self._label
|
||||
|
||||
@property
|
||||
def country(self) -> Optional[str]:
|
||||
"""
|
||||
Operator country (ISO 3166-1 alpha-2), e.g. ``"US"``.
|
||||
|
||||
:return: ISO 3166-1 alpha-2 country code.
|
||||
"""
|
||||
return self._country
|
||||
|
||||
@property
|
||||
def legal(self) -> Optional[str]:
|
||||
"""
|
||||
|
||||
:return:
|
||||
"""
|
||||
return self._legal
|
||||
|
||||
@property
|
||||
def endpoint(self) -> Optional[str]:
|
||||
"""
|
||||
Public WAMP endpoint of seeder. Secure WebSocket URL resolving to a public IPv4
|
||||
or IPv6 listening url accepting incoming WAMP-WebSocket connections,
|
||||
e.g. ``wss://service1.example.com/ws``.
|
||||
|
||||
:return: The endpoint URL.
|
||||
"""
|
||||
return self._endpoint
|
||||
|
||||
@property
|
||||
def bandwidth_requested(self) -> Optional[int]:
|
||||
"""
|
||||
|
||||
:return:
|
||||
"""
|
||||
return self._bandwidth_requested
|
||||
|
||||
@property
|
||||
def bandwidth_offered(self) -> Optional[int]:
|
||||
"""
|
||||
|
||||
:return:
|
||||
"""
|
||||
return self._bandwidth_offered
|
||||
|
||||
|
||||
class FederatedRealm(object):
|
||||
"""
|
||||
A federated realm is a WAMP application realm with a trust anchor rooted in Ethereum, and
|
||||
which can be shared between multiple parties.
|
||||
|
||||
A federated realm is globally identified on an Ethereum chain (e.g. on Mainnet or Rinkeby)
|
||||
by an Ethereum address associated to a federated realm owner by an on-chain record stored
|
||||
in the WAMP Network contract. The federated realm address thus only needs to exist as an
|
||||
identifier of the federated realm-owner record.
|
||||
"""
|
||||
__slots__ = (
|
||||
'_name_or_address',
|
||||
'_gateway_config',
|
||||
'_status',
|
||||
'_name_category',
|
||||
'_w3',
|
||||
'_ens',
|
||||
'_address',
|
||||
'_contract',
|
||||
|
||||
'_seeders',
|
||||
'_root_ca',
|
||||
'_catalog',
|
||||
'_meta',
|
||||
)
|
||||
# FIXME
|
||||
CONTRACT_ADDRESS = web3.Web3.toChecksumAddress('0xF7acf1C4CB4a9550B8969576573C2688B48988C2')
|
||||
CONTRACT_ABI: str = ''
|
||||
|
||||
def __init__(self, name_or_address: str, gateway_config: Optional[Dict[str, Any]] = None):
|
||||
"""
|
||||
Instantiate a federated realm from a federated realm ENS name (which is resolved to an Ethereum
|
||||
address) or Ethereum address.
|
||||
|
||||
:param name_or_address: Ethereum ENS name or address.
|
||||
:param gateway_config: If provided, use this Ethereum gateway. If not provided,
|
||||
connect via Infura to Ethereum Mainnet, which requires an environment variable
|
||||
``WEB3_INFURA_PROJECT_ID`` with your Infura project ID.
|
||||
"""
|
||||
self._name_or_address = name_or_address
|
||||
self._gateway_config = gateway_config
|
||||
|
||||
# status, will change to 'RUNNING' after initialize() has completed
|
||||
self._status = 'STOPPED'
|
||||
|
||||
self._name_category: Optional[str] = identify_realm_name_category(self._name_or_address)
|
||||
if self._name_category not in ['eth', 'ens', 'reverse_ens']:
|
||||
raise ValueError('name_or_address "{}" not an Ethereum address or ENS name'.format(self._name_or_address))
|
||||
|
||||
# will be filled once initialize()'ed
|
||||
self._w3 = None
|
||||
self._ens = None
|
||||
|
||||
# address identifying the federated realm
|
||||
self._address: Optional[str] = None
|
||||
|
||||
# will be initialized with a FederatedRealm contract instance
|
||||
self._contract: Optional[Contract] = None
|
||||
|
||||
# cache of federated realm seeders, filled once in status running
|
||||
self._seeders: List[Seeder] = []
|
||||
|
||||
self._root_ca = None
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def name_or_address(self) -> str:
|
||||
return self._name_or_address
|
||||
|
||||
@property
|
||||
def gateway_config(self) -> Optional[Dict[str, Any]]:
|
||||
return self._gateway_config
|
||||
|
||||
@property
|
||||
def name_category(self) -> Optional[str]:
|
||||
return self._name_category
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
return self._address
|
||||
|
||||
def root_ca(self) -> EIP712AuthorityCertificate:
|
||||
assert self._status == 'RUNNING'
|
||||
return self._root_ca
|
||||
|
||||
@property
|
||||
def seeders(self) -> List[Seeder]:
|
||||
return self._seeders
|
||||
|
||||
def initialize(self):
|
||||
"""
|
||||
|
||||
:return:
|
||||
"""
|
||||
if self._status != 'STOPPED':
|
||||
raise RuntimeError('cannot start in status "{}"'.format(self._status))
|
||||
d = deferToThread(self._initialize_background)
|
||||
return d
|
||||
|
||||
def _initialize_background(self):
|
||||
self._status = 'STARTING'
|
||||
|
||||
if self._gateway_config:
|
||||
self._w3 = make_w3(self._gateway_config)
|
||||
else:
|
||||
raise RuntimeError('cannot auto-configure ethereum connection (was removed from web3.py in v6)')
|
||||
# https://github.com/ethereum/web3.py/issues/1416
|
||||
# https://github.com/ethereum/web3.py/pull/2706
|
||||
# from web3.auto.infura import w3
|
||||
# self._w3 = w3
|
||||
self._ens = ENS.from_web3(self._w3)
|
||||
|
||||
if self._name_category in ['ens', 'reverse_ens']:
|
||||
if self._name_category == 'reverse_ens':
|
||||
name = ''.join(reversed(self._name_or_address.split('.')))
|
||||
else:
|
||||
name = self._name_or_address
|
||||
self._address = self._ens.address(name)
|
||||
elif self._name_category == 'eth':
|
||||
self._address = self._w3.toChecksumAddress(self._name_or_address)
|
||||
else:
|
||||
assert False, 'should not arrive here'
|
||||
|
||||
# https://web3py.readthedocs.io/en/stable/contracts.html#web3.contract.Contract
|
||||
# https://web3py.readthedocs.io/en/stable/web3.eth.html#web3.eth.Eth.contract
|
||||
# self._contract = self._w3.eth.contract(address=self.CONTRACT_ADDRESS, abi=self.CONTRACT_ABI)
|
||||
|
||||
# FIXME: get IPFS hash, download file, unzip seeders
|
||||
|
||||
self._status = 'RUNNING'
|
||||
1058
backend/lib/python3.12/site-packages/autobahn/xbr/_gui.py
Normal file
1058
backend/lib/python3.12/site-packages/autobahn/xbr/_gui.py
Normal file
File diff suppressed because it is too large
Load Diff
188
backend/lib/python3.12/site-packages/autobahn/xbr/_interfaces.py
Normal file
188
backend/lib/python3.12/site-packages/autobahn/xbr/_interfaces.py
Normal file
@@ -0,0 +1,188 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 abc
|
||||
|
||||
|
||||
class IMarketMaker(abc.ABC):
|
||||
"""
|
||||
XBR Market Maker interface.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def status(self, details):
|
||||
"""
|
||||
|
||||
:param details:
|
||||
:return:
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def offer(self, key_id, price, details):
|
||||
"""
|
||||
|
||||
:param key_id:
|
||||
:param price:
|
||||
:param details:
|
||||
:return:
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def revoke(self, key_id, details):
|
||||
"""
|
||||
|
||||
:param key_id:
|
||||
:param details:
|
||||
:return:
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def quote(self, key_id, details):
|
||||
"""
|
||||
|
||||
:param key_id:
|
||||
:param details:
|
||||
:return:
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def buy(self, channel_id, channel_seq, buyer_pubkey, datakey_id, amount, balance, signature, details):
|
||||
"""
|
||||
|
||||
:param channel_id:
|
||||
:param channel_seq:
|
||||
:param buyer_pubkey:
|
||||
:param datakey_id:
|
||||
:param amount:
|
||||
:param balance:
|
||||
:param signature:
|
||||
:param details:
|
||||
:return:
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_payment_channels(self, address, details):
|
||||
"""
|
||||
|
||||
:param address:
|
||||
:param details:
|
||||
:return:
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_payment_channel(self, channel_id, details):
|
||||
"""
|
||||
|
||||
:param channel_id:
|
||||
:param details:
|
||||
:return:
|
||||
"""
|
||||
|
||||
|
||||
class IProvider(abc.ABC):
|
||||
"""
|
||||
XBR Provider interface.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def sell(self, key_id, buyer_pubkey, amount_paid, post_balance, signature, details):
|
||||
"""
|
||||
|
||||
:param key_id:
|
||||
:param buyer_pubkey:
|
||||
:param amount_paid:
|
||||
:param post_balance:
|
||||
:param signature:
|
||||
:param details:
|
||||
:return:
|
||||
"""
|
||||
|
||||
|
||||
class IConsumer(abc.ABC):
|
||||
"""
|
||||
XBR Consumer interface.
|
||||
"""
|
||||
|
||||
|
||||
class ISeller(abc.ABC):
|
||||
"""
|
||||
XBR Seller interface.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def start(self, session):
|
||||
"""
|
||||
|
||||
:param session:
|
||||
:return:
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def wrap(self, uri, payload):
|
||||
"""
|
||||
|
||||
:param uri:
|
||||
:param payload:
|
||||
:return:
|
||||
"""
|
||||
|
||||
|
||||
class IBuyer(abc.ABC):
|
||||
"""
|
||||
XBR Buyer interface.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def start(self, session):
|
||||
"""
|
||||
Start buying keys over the provided session.
|
||||
|
||||
:param session: WAMP session that allows to talk to the XBR Market Maker.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def unwrap(self, key_id, enc_ser, ciphertext):
|
||||
"""
|
||||
Decrypt and deserialize received XBR payload.
|
||||
|
||||
:param key_id: The ID of the datakey the payload is encrypted with.
|
||||
:type key_id: bytes
|
||||
|
||||
:param enc_ser: The serializer that was used for serializing the payload. One of ``cbor``, ``json``, ``msgpack``, ``ubjson``.
|
||||
:type enc_ser: str
|
||||
|
||||
:param ciphertext: The encrypted payload to unwrap.
|
||||
:type ciphertext: bytes
|
||||
|
||||
:returns: The unwrapped application payload.
|
||||
:rtype: object
|
||||
"""
|
||||
|
||||
|
||||
class IDelegate(ISeller, IBuyer):
|
||||
"""
|
||||
XBR Delegate interface.
|
||||
"""
|
||||
167
backend/lib/python3.12/site-packages/autobahn/xbr/_mnemonic.py
Normal file
167
backend/lib/python3.12/site-packages/autobahn/xbr/_mnemonic.py
Normal file
@@ -0,0 +1,167 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018 Luis Teixeira
|
||||
# - copied & modified from https://github.com/vergl4s/ethereum-mnemonic-utils
|
||||
#
|
||||
# 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 hashlib
|
||||
import hmac
|
||||
import struct
|
||||
|
||||
from base58 import b58encode_check
|
||||
from ecdsa.curves import SECP256k1
|
||||
|
||||
BIP39_PBKDF2_ROUNDS = 2048
|
||||
BIP39_SALT_MODIFIER = "mnemonic"
|
||||
BIP32_PRIVDEV = 0x80000000
|
||||
BIP32_CURVE = SECP256k1
|
||||
BIP32_SEED_MODIFIER = b'Bitcoin seed'
|
||||
|
||||
# https://github.com/ethereum/EIPs/issues/84#issuecomment-528213145
|
||||
LEDGER_ETH_DERIVATION_PATH = "m/44'/60'/0'/0"
|
||||
|
||||
__all__ = [
|
||||
'mnemonic_to_bip39seed',
|
||||
'mnemonic_to_private_key',
|
||||
]
|
||||
|
||||
|
||||
def mnemonic_to_bip39seed(mnemonic, passphrase):
|
||||
""" BIP39 seed from a mnemonic key.
|
||||
Logic adapted from https://github.com/trezor/python-mnemonic. """
|
||||
mnemonic = bytes(mnemonic, 'utf8')
|
||||
salt = bytes(BIP39_SALT_MODIFIER + passphrase, 'utf8')
|
||||
return hashlib.pbkdf2_hmac('sha512', mnemonic, salt, BIP39_PBKDF2_ROUNDS)
|
||||
|
||||
|
||||
def bip39seed_to_bip32masternode(seed):
|
||||
""" BIP32 master node derivation from a bip39 seed.
|
||||
Logic adapted from https://github.com/satoshilabs/slips/blob/master/slip-0010/testvectors.py. """
|
||||
h = hmac.new(BIP32_SEED_MODIFIER, seed, hashlib.sha512).digest()
|
||||
key, chain_code = h[:32], h[32:]
|
||||
return key, chain_code
|
||||
|
||||
|
||||
def derive_public_key(private_key):
|
||||
""" Public key from a private key.
|
||||
Logic adapted from https://github.com/satoshilabs/slips/blob/master/slip-0010/testvectors.py. """
|
||||
|
||||
Q = int.from_bytes(private_key, byteorder='big') * BIP32_CURVE.generator
|
||||
xstr = int(Q.x()).to_bytes(32, byteorder='big')
|
||||
parity = Q.y() & 1
|
||||
return int(2 + parity).to_bytes(1, byteorder='big') + xstr
|
||||
|
||||
|
||||
def derive_bip32childkey(parent_key, parent_chain_code, i):
|
||||
""" Derives a child key from an existing key, i is current derivation parameter.
|
||||
Logic adapted from https://github.com/satoshilabs/slips/blob/master/slip-0010/testvectors.py. """
|
||||
|
||||
assert len(parent_key) == 32
|
||||
assert len(parent_chain_code) == 32
|
||||
k = parent_chain_code
|
||||
if (i & BIP32_PRIVDEV) != 0:
|
||||
key = b'\x00' + parent_key
|
||||
else:
|
||||
key = derive_public_key(parent_key)
|
||||
d = key + struct.pack('>L', i)
|
||||
while True:
|
||||
h = hmac.new(k, d, hashlib.sha512).digest()
|
||||
key, chain_code = h[:32], h[32:]
|
||||
a = int.from_bytes(key, byteorder='big')
|
||||
b = int.from_bytes(parent_key, byteorder='big')
|
||||
key = (a + b) % BIP32_CURVE.order
|
||||
if a < BIP32_CURVE.order and key != 0:
|
||||
key = int(key).to_bytes(32, byteorder='big')
|
||||
break
|
||||
d = b'\x01' + h[32:] + struct.pack('>L', i)
|
||||
|
||||
return key, chain_code
|
||||
|
||||
|
||||
def fingerprint(public_key):
|
||||
""" BIP32 fingerprint formula, used to get b58 serialized key. """
|
||||
|
||||
return hashlib.new('ripemd160', hashlib.sha256(public_key).digest()).digest()[:4]
|
||||
|
||||
|
||||
def b58xprv(parent_fingerprint, private_key, chain, depth, childnr):
|
||||
""" Private key b58 serialization format. """
|
||||
|
||||
raw = (b'\x04\x88\xad\xe4' + bytes(chr(depth), 'utf-8') + parent_fingerprint + int(childnr).to_bytes(
|
||||
4, byteorder='big') + chain + b'\x00' + private_key)
|
||||
|
||||
return b58encode_check(raw)
|
||||
|
||||
|
||||
def b58xpub(parent_fingerprint, public_key, chain, depth, childnr):
|
||||
""" Public key b58 serialization format. """
|
||||
|
||||
raw = (b'\x04\x88\xb2\x1e' + bytes(chr(depth), 'utf-8') + parent_fingerprint + int(childnr).to_bytes(
|
||||
4, byteorder='big') + chain + public_key)
|
||||
|
||||
return b58encode_check(raw)
|
||||
|
||||
|
||||
def parse_derivation_path(str_derivation_path):
|
||||
""" Parses a derivation path such as "m/44'/60/0'/0" and returns
|
||||
list of integers for each element in path. """
|
||||
|
||||
path = []
|
||||
if str_derivation_path[0:2] != 'm/':
|
||||
raise ValueError("Can't recognize derivation path. It should look like \"m/44'/60/0'/0\".")
|
||||
|
||||
for i in str_derivation_path.lstrip('m/').split('/'):
|
||||
if "'" in i:
|
||||
path.append(BIP32_PRIVDEV + int(i[:-1]))
|
||||
else:
|
||||
path.append(int(i))
|
||||
return path
|
||||
|
||||
|
||||
def mnemonic_to_private_key(mnemonic, str_derivation_path=LEDGER_ETH_DERIVATION_PATH, passphrase=""):
|
||||
""" Performs all convertions to get a private key from a mnemonic sentence, including:
|
||||
|
||||
BIP39 mnemonic to seed
|
||||
BIP32 seed to master key
|
||||
BIP32 child derivation of a path provided
|
||||
|
||||
Parameters:
|
||||
mnemonic -- seed wordlist, usually with 24 words, that is used for ledger wallet backup
|
||||
str_derivation_path -- string that directs BIP32 key derivation, defaults to path
|
||||
used by ledger ETH wallet
|
||||
|
||||
"""
|
||||
|
||||
derivation_path = parse_derivation_path(str_derivation_path)
|
||||
|
||||
bip39seed = mnemonic_to_bip39seed(mnemonic, passphrase)
|
||||
|
||||
master_private_key, master_chain_code = bip39seed_to_bip32masternode(bip39seed)
|
||||
|
||||
private_key, chain_code = master_private_key, master_chain_code
|
||||
|
||||
for i in derivation_path:
|
||||
private_key, chain_code = derive_bip32childkey(private_key, chain_code, i)
|
||||
|
||||
return private_key
|
||||
2113
backend/lib/python3.12/site-packages/autobahn/xbr/_schema.py
Normal file
2113
backend/lib/python3.12/site-packages/autobahn/xbr/_schema.py
Normal file
File diff suppressed because it is too large
Load Diff
567
backend/lib/python3.12/site-packages/autobahn/xbr/_secmod.py
Normal file
567
backend/lib/python3.12/site-packages/autobahn/xbr/_secmod.py
Normal file
@@ -0,0 +1,567 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 binascii
|
||||
import os
|
||||
import configparser
|
||||
from collections.abc import MutableMapping
|
||||
from typing import Optional, Union, Dict, Any, List, Iterator
|
||||
from threading import Lock
|
||||
|
||||
import txaio
|
||||
import nacl
|
||||
|
||||
from eth_account.account import Account
|
||||
from eth_account.signers.local import LocalAccount
|
||||
|
||||
from py_eth_sig_utils.eip712 import encode_typed_data
|
||||
from py_eth_sig_utils.utils import ecsign, ecrecover_to_pub, checksum_encode, sha3
|
||||
from py_eth_sig_utils.signing import v_r_s_to_signature, signature_to_v_r_s
|
||||
|
||||
from autobahn.wamp.interfaces import ISecurityModule, IEthereumKey
|
||||
from autobahn.xbr._mnemonic import mnemonic_to_private_key
|
||||
from autobahn.util import parse_keyfile
|
||||
from autobahn.wamp.cryptosign import CryptosignKey
|
||||
|
||||
__all__ = ('EthereumKey', 'SecurityModuleMemory', )
|
||||
|
||||
|
||||
class EthereumKey(object):
|
||||
"""
|
||||
Base class to implement :class:`autobahn.wamp.interfaces.IEthereumKey`.
|
||||
"""
|
||||
|
||||
def __init__(self, key_or_address: Union[LocalAccount, str, bytes], can_sign: bool,
|
||||
security_module: Optional[ISecurityModule] = None,
|
||||
key_no: Optional[int] = None) -> None:
|
||||
if can_sign:
|
||||
# https://eth-account.readthedocs.io/en/latest/eth_account.html#eth_account.account.Account
|
||||
assert type(key_or_address) == LocalAccount
|
||||
self._key = key_or_address
|
||||
self._address = key_or_address.address
|
||||
else:
|
||||
assert type(key_or_address) in (str, bytes)
|
||||
self._key = None
|
||||
self._address = key_or_address
|
||||
self._can_sign = can_sign
|
||||
self._security_module = security_module
|
||||
self._key_no = key_no
|
||||
|
||||
@property
|
||||
def security_module(self) -> Optional['ISecurityModule']:
|
||||
"""
|
||||
Implements :meth:`autobahn.wamp.interfaces.IKey.security_module`.
|
||||
"""
|
||||
return self._security_module
|
||||
|
||||
@property
|
||||
def key_no(self) -> Optional[int]:
|
||||
"""
|
||||
Implements :meth:`autobahn.wamp.interfaces.IKey.key_no`.
|
||||
"""
|
||||
return self._key_no
|
||||
|
||||
@property
|
||||
def key_type(self) -> str:
|
||||
"""
|
||||
Implements :meth:`autobahn.wamp.interfaces.IKey.key_type`.
|
||||
"""
|
||||
return 'ethereum'
|
||||
|
||||
def public_key(self, binary: bool = False) -> Union[str, bytes]:
|
||||
"""
|
||||
Implements :meth:`autobahn.wamp.interfaces.IKey.public_key`.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def can_sign(self) -> bool:
|
||||
"""
|
||||
Implements :meth:`autobahn.wamp.interfaces.IKey.can_sign`.
|
||||
"""
|
||||
return self._can_sign
|
||||
|
||||
def address(self, binary: bool = False) -> Union[str, bytes]:
|
||||
"""
|
||||
Implements :meth:`autobahn.wamp.interfaces.IEthereumKey.address`.
|
||||
"""
|
||||
if binary:
|
||||
return binascii.a2b_hex(self._address[2:])
|
||||
else:
|
||||
return self._address
|
||||
|
||||
def sign(self, data: bytes) -> bytes:
|
||||
"""
|
||||
Implements :meth:`autobahn.wamp.interfaces.IKey.sign`.
|
||||
"""
|
||||
# FIXME: implement signing of raw data
|
||||
raise NotImplementedError()
|
||||
|
||||
def recover(self, data: bytes, signature: bytes) -> bytes:
|
||||
"""
|
||||
Implements :meth:`autobahn.wamp.interfaces.IKey.recover`.
|
||||
"""
|
||||
# FIXME: implement signing address recovery from signature of raw data
|
||||
raise NotImplementedError()
|
||||
|
||||
def sign_typed_data(self, data: Dict[str, Any], binary=True) -> bytes:
|
||||
"""
|
||||
Implements :meth:`autobahn.wamp.interfaces.IEthereumKey.sign_typed_data`.
|
||||
"""
|
||||
if self._security_module:
|
||||
assert self._security_module.is_open and not self._security_module.is_locked, 'security module must be open and unlocked'
|
||||
try:
|
||||
# encode typed data dict and return message hash
|
||||
msg_hash = encode_typed_data(data)
|
||||
|
||||
# ECDSA signatures in Ethereum consist of three parameters: v, r and s.
|
||||
# The signature is always 65-bytes in length.
|
||||
# r = first 32 bytes of signature
|
||||
# s = second 32 bytes of signature
|
||||
# v = final 1 byte of signature
|
||||
signature_vrs = ecsign(msg_hash, self._key.key)
|
||||
|
||||
# concatenate signature components into byte string
|
||||
signature = v_r_s_to_signature(*signature_vrs)
|
||||
except Exception as e:
|
||||
return txaio.create_future_error(e)
|
||||
else:
|
||||
if binary:
|
||||
return txaio.create_future_success(signature)
|
||||
else:
|
||||
return txaio.create_future_success(binascii.b2a_hex(signature).decode())
|
||||
|
||||
def verify_typed_data(self, data: Dict[str, Any], signature: bytes) -> bool:
|
||||
"""
|
||||
Implements :meth:`autobahn.wamp.interfaces.IEthereumKey.verify_typed_data`.
|
||||
"""
|
||||
if self._security_module:
|
||||
assert self._security_module.is_open and not self._security_module.is_locked, 'security module must be open and unlocked'
|
||||
try:
|
||||
msg_hash = encode_typed_data(data)
|
||||
signature_vrs = signature_to_v_r_s(signature)
|
||||
public_key = ecrecover_to_pub(msg_hash, *signature_vrs)
|
||||
address_bytes = sha3(public_key)[-20:]
|
||||
address = checksum_encode(address_bytes)
|
||||
except Exception as e:
|
||||
return txaio.create_future_error(e)
|
||||
else:
|
||||
return txaio.create_future_success(address == self._address)
|
||||
|
||||
@classmethod
|
||||
def from_address(cls, address: Union[str, bytes]) -> 'EthereumKey':
|
||||
"""
|
||||
Create a public key from an address, which can be used to verify signatures.
|
||||
|
||||
:param address: The Ethereum address (20 octets).
|
||||
:return: New instance of :class:`EthereumKey`
|
||||
"""
|
||||
return EthereumKey(key_or_address=address, can_sign=False)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, key: bytes) -> 'EthereumKey':
|
||||
"""
|
||||
Create a private key from seed bytes, which can be used to sign and create signatures.
|
||||
|
||||
:param key: The Ethereum private key seed (32 octets).
|
||||
:return: New instance of :class:`EthereumKey`
|
||||
"""
|
||||
if type(key) != bytes:
|
||||
raise ValueError("invalid seed type {} (expected binary)".format(type(key)))
|
||||
|
||||
if len(key) != 32:
|
||||
raise ValueError("invalid seed length {} (expected 32)".format(len(key)))
|
||||
|
||||
account: LocalAccount = Account.from_key(key)
|
||||
return EthereumKey(key_or_address=account, can_sign=True)
|
||||
|
||||
@classmethod
|
||||
def from_seedphrase(cls, seedphrase: str, index: int = 0) -> 'EthereumKey':
|
||||
"""
|
||||
Create a private key from the given BIP-39 mnemonic seed phrase and index,
|
||||
which can be used to sign and create signatures.
|
||||
|
||||
:param seedphrase: The BIP-39 seedphrase ("Mnemonic") from which to derive the account.
|
||||
:param index: The account index in account hierarchy defined by the seedphrase.
|
||||
:return: New instance of :class:`EthereumKey`
|
||||
"""
|
||||
# Base HD Path: m/44'/60'/0'/0/{account_index}
|
||||
derivation_path = "m/44'/60'/0'/0/{}".format(index)
|
||||
|
||||
key = mnemonic_to_private_key(seedphrase, str_derivation_path=derivation_path)
|
||||
assert type(key) == bytes
|
||||
assert len(key) == 32
|
||||
|
||||
account: LocalAccount = Account.from_key(key)
|
||||
return EthereumKey(key_or_address=account, can_sign=True)
|
||||
|
||||
@classmethod
|
||||
def from_keyfile(cls, keyfile: str) -> 'EthereumKey':
|
||||
"""
|
||||
Create a public or private key from reading the given public or private key file.
|
||||
|
||||
Here is an example key file that includes an Ethereum private key ``private-key-eth``, which
|
||||
is loaded in this function, and other fields, which are ignored by this function:
|
||||
|
||||
.. code-block::
|
||||
|
||||
This is a comment (all lines until the first empty line are comments indeed).
|
||||
|
||||
creator: oberstet@intel-nuci7
|
||||
created-at: 2022-07-05T12:29:48.832Z
|
||||
user-id: oberstet@intel-nuci7
|
||||
public-key-ed25519: 7326d9dc0307681cc6940fde0e60eb31a6e4d642a81e55c434462ce31f95deed
|
||||
public-adr-eth: 0x10848feBdf7f200Ba989CDf7E3eEB3EC03ae7768
|
||||
private-key-ed25519: f750f42b0430e28a2e272c3cedcae4dcc4a1cf33bc345c35099d3322626ab666
|
||||
private-key-eth: 4d787714dcb0ae52e1c5d2144648c255d660b9a55eac9deeb80d9f506f501025
|
||||
|
||||
:param keyfile: Path (relative or absolute) to a public or private keys file.
|
||||
:return: New instance of :class:`EthereumKey`
|
||||
"""
|
||||
if not os.path.exists(keyfile) or not os.path.isfile(keyfile):
|
||||
raise RuntimeError('keyfile "{}" is not a file'.format(keyfile))
|
||||
|
||||
# now load the private or public key file - this returns a dict which should
|
||||
# include (for a private key):
|
||||
#
|
||||
# private-key-eth: 6b08b6e186bd2a3b9b2f36e6ece3f8031fe788ab3dc4a1cfd3a489ea387c496b
|
||||
#
|
||||
# or (for a public key only):
|
||||
#
|
||||
# public-adr-eth: 0x10848feBdf7f200Ba989CDf7E3eEB3EC03ae7768
|
||||
#
|
||||
data = parse_keyfile(keyfile)
|
||||
|
||||
privkey_eth_hex = data.get('private-key-eth', None)
|
||||
if privkey_eth_hex is None:
|
||||
pub_adr_eth = data.get('public-adr-eth', None)
|
||||
if pub_adr_eth is None:
|
||||
raise RuntimeError('neither "private-key-eth" nor "public-adr-eth" found in keyfile {}'.format(keyfile))
|
||||
else:
|
||||
return EthereumKey.from_address(pub_adr_eth)
|
||||
else:
|
||||
return EthereumKey.from_bytes(binascii.a2b_hex(privkey_eth_hex))
|
||||
|
||||
|
||||
IEthereumKey.register(EthereumKey)
|
||||
|
||||
|
||||
class SecurityModuleMemory(MutableMapping):
|
||||
"""
|
||||
A transient, memory-based implementation of :class:`ISecurityModule`.
|
||||
"""
|
||||
|
||||
def __init__(self, keys: Optional[List[Union[CryptosignKey, EthereumKey]]] = None):
|
||||
self._mutex = Lock()
|
||||
self._is_open = False
|
||||
self._is_locked = True
|
||||
self._keys: Dict[int, Union[CryptosignKey, EthereumKey]] = {}
|
||||
self._counters: Dict[int, int] = {}
|
||||
if keys:
|
||||
for i, key in enumerate(keys):
|
||||
self._keys[i] = key
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""
|
||||
Implements :meth:`ISecurityModule.__len__`
|
||||
"""
|
||||
assert self._is_open, 'security module not open'
|
||||
|
||||
return len(self._keys)
|
||||
|
||||
def __contains__(self, key_no: int) -> bool:
|
||||
assert self._is_open, 'security module not open'
|
||||
|
||||
return key_no in self._keys
|
||||
|
||||
def __iter__(self) -> Iterator[int]:
|
||||
"""
|
||||
Implements :meth:`ISecurityModule.__iter__`
|
||||
"""
|
||||
assert self._is_open, 'security module not open'
|
||||
|
||||
yield from self._keys
|
||||
|
||||
def __getitem__(self, key_no: int) -> Union[CryptosignKey, EthereumKey]:
|
||||
"""
|
||||
Implements :meth:`ISecurityModule.__getitem__`
|
||||
"""
|
||||
assert self._is_open, 'security module not open'
|
||||
|
||||
if key_no in self._keys:
|
||||
return self._keys[key_no]
|
||||
else:
|
||||
raise IndexError('key_no {} not found'.format(key_no))
|
||||
|
||||
def __setitem__(self, key_no: int, key: Union[CryptosignKey, EthereumKey]) -> None:
|
||||
assert self._is_open, 'security module not open'
|
||||
|
||||
assert key_no >= 0
|
||||
if key_no in self._keys:
|
||||
# FIXME
|
||||
pass
|
||||
self._keys[key_no] = key
|
||||
|
||||
def __delitem__(self, key_no: int) -> None:
|
||||
assert self._is_open, 'security module not open'
|
||||
|
||||
if key_no in self._keys:
|
||||
del self._keys[key_no]
|
||||
else:
|
||||
raise IndexError()
|
||||
|
||||
def open(self):
|
||||
"""
|
||||
Implements :meth:`ISecurityModule.open`
|
||||
"""
|
||||
assert not self._is_open, 'security module already open'
|
||||
|
||||
self._is_open = True
|
||||
return txaio.create_future_success(None)
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Implements :meth:`ISecurityModule.close`
|
||||
"""
|
||||
assert self._is_open, 'security module not open'
|
||||
|
||||
self._is_open = False
|
||||
self._is_locked = True
|
||||
return txaio.create_future_success(None)
|
||||
|
||||
@property
|
||||
def is_open(self) -> bool:
|
||||
"""
|
||||
Implements :meth:`ISecurityModule.is_open`
|
||||
"""
|
||||
return self._is_open
|
||||
|
||||
@property
|
||||
def can_lock(self) -> bool:
|
||||
"""
|
||||
Implements :meth:`ISecurityModule.can_lock`
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_locked(self) -> bool:
|
||||
"""
|
||||
Implements :meth:`ISecurityModule.is_locked`
|
||||
"""
|
||||
return self._is_locked
|
||||
|
||||
def lock(self):
|
||||
"""
|
||||
Implements :meth:`ISecurityModule.lock`
|
||||
"""
|
||||
assert self._is_open, 'security module not open'
|
||||
assert not self._is_locked
|
||||
|
||||
self._is_locked = True
|
||||
return txaio.create_future_success(None)
|
||||
|
||||
def unlock(self):
|
||||
"""
|
||||
Implements :meth:`ISecurityModule.unlock`
|
||||
"""
|
||||
assert self._is_open, 'security module not open'
|
||||
assert self._is_locked
|
||||
|
||||
self._is_locked = False
|
||||
return txaio.create_future_success(None)
|
||||
|
||||
def create_key(self, key_type: str) -> int:
|
||||
assert self._is_open, 'security module not open'
|
||||
|
||||
key_no = len(self._keys)
|
||||
if key_type == 'cryptosign':
|
||||
key = CryptosignKey(key=nacl.signing.SigningKey(os.urandom(32)),
|
||||
can_sign=True,
|
||||
security_module=self,
|
||||
key_no=key_no)
|
||||
elif key_type == 'ethereum':
|
||||
key = EthereumKey(key_or_address=Account.from_key(os.urandom(32)),
|
||||
can_sign=True,
|
||||
security_module=self,
|
||||
key_no=key_no)
|
||||
else:
|
||||
raise ValueError('invalid key_type "{}"'.format(key_type))
|
||||
self._keys[key_no] = key
|
||||
return txaio.create_future_success(key_no)
|
||||
|
||||
def delete_key(self, key_no: int):
|
||||
assert self._is_open, 'security module not open'
|
||||
|
||||
if key_no in self._keys:
|
||||
del self._keys[key_no]
|
||||
return txaio.create_future_success(key_no)
|
||||
else:
|
||||
return txaio.create_future_success(None)
|
||||
|
||||
def get_random(self, octets: int) -> bytes:
|
||||
"""
|
||||
Implements :meth:`ISecurityModule.get_random`
|
||||
"""
|
||||
assert self._is_open, 'security module not open'
|
||||
|
||||
data = os.urandom(octets)
|
||||
return txaio.create_future_success(data)
|
||||
|
||||
def get_counter(self, counter_no: int) -> int:
|
||||
"""
|
||||
Implements :meth:`ISecurityModule.get_counter`
|
||||
"""
|
||||
assert self._is_open, 'security module not open'
|
||||
|
||||
self._mutex.acquire()
|
||||
res = self._counters.get(counter_no, 0)
|
||||
self._mutex.release()
|
||||
return txaio.create_future_success(res)
|
||||
|
||||
def increment_counter(self, counter_no: int) -> int:
|
||||
"""
|
||||
Implements :meth:`ISecurityModule.increment_counter`
|
||||
"""
|
||||
assert self._is_open, 'security module not open'
|
||||
|
||||
self._mutex.acquire()
|
||||
if counter_no not in self._counters:
|
||||
self._counters[counter_no] = 0
|
||||
self._counters[counter_no] += 1
|
||||
res = self._counters[counter_no]
|
||||
self._mutex.release()
|
||||
return txaio.create_future_success(res)
|
||||
|
||||
@classmethod
|
||||
def from_seedphrase(cls, seedphrase: str, num_eth_keys: int = 1,
|
||||
num_cs_keys: int = 1) -> 'SecurityModuleMemory':
|
||||
"""
|
||||
Create a new memory-backed security module with
|
||||
|
||||
1. ``num_eth_keys`` keys of type :class:`EthereumKey`, followed by
|
||||
2. ``num_cs_keys`` keys of type :class:`CryptosignKey`
|
||||
|
||||
computed from a (common) BIP44 seedphrase.
|
||||
|
||||
:param seedphrase: BIP44 seedphrase to use.
|
||||
:param num_eth_keys: Number of Ethereum keys to derive.
|
||||
:param num_cs_keys: Number of Cryptosign keys to derive.
|
||||
:return: New memory-backed security module instance.
|
||||
"""
|
||||
keys: List[Union[EthereumKey, CryptosignKey]] = []
|
||||
|
||||
# first, add num_eth_keys EthereumKey(s), numbering starting at 0
|
||||
for i in range(num_eth_keys):
|
||||
key = EthereumKey.from_seedphrase(seedphrase, i)
|
||||
keys.append(key)
|
||||
|
||||
# second, add num_cs_keys CryptosignKey(s), numbering starting at num_eth_keys (!)
|
||||
for i in range(num_cs_keys):
|
||||
key = CryptosignKey.from_seedphrase(seedphrase, i + num_eth_keys)
|
||||
keys.append(key)
|
||||
|
||||
# initialize security module from collected keys
|
||||
sm = SecurityModuleMemory(keys=keys)
|
||||
return sm
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: str, profile: str = 'default') -> 'SecurityModuleMemory':
|
||||
"""
|
||||
Create a new memory-backed security module with keys referred from a profile in
|
||||
the given configuration file.
|
||||
|
||||
:param config: Path (relative or absolute) to an INI configuration file.
|
||||
:param profile: Name of the profile within the given INI configuration file.
|
||||
:return: New memory-backed security module instance.
|
||||
"""
|
||||
keys: List[Union[EthereumKey, CryptosignKey]] = []
|
||||
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(config)
|
||||
|
||||
if not cfg.has_section(profile):
|
||||
raise RuntimeError('profile "{}" not found in configuration file "{}"'.format(profile, config))
|
||||
|
||||
if not cfg.has_option(profile, 'privkey'):
|
||||
raise RuntimeError('missing option "privkey" in profile "{}" of configuration file "{}"'.format(profile, config))
|
||||
|
||||
privkey = os.path.join(os.path.dirname(config), cfg.get(profile, 'privkey'))
|
||||
if not os.path.exists(privkey) or not os.path.isfile(privkey):
|
||||
raise RuntimeError('privkey "{}" is not a file in profile "{}" of configuration file "{}"'.format(privkey, profile, config))
|
||||
|
||||
# now load the private key file - this returns a dict which should include:
|
||||
# private-key-eth: 6b08b6e186bd2a3b9b2f36e6ece3f8031fe788ab3dc4a1cfd3a489ea387c496b
|
||||
# private-key-ed25519: 20e8c05d0ede9506462bb049c4843032b18e8e75b314583d0c8d8a4942f9be40
|
||||
data = parse_keyfile(privkey)
|
||||
|
||||
# first, add Ethereum key
|
||||
privkey_eth_hex = data.get('private-key-eth', None)
|
||||
keys.append(EthereumKey.from_bytes(binascii.a2b_hex(privkey_eth_hex)))
|
||||
|
||||
# second, add Cryptosign key
|
||||
privkey_ed25519_hex = data.get('private-key-ed25519', None)
|
||||
keys.append(CryptosignKey.from_bytes(binascii.a2b_hex(privkey_ed25519_hex)))
|
||||
|
||||
# initialize security module from collected keys
|
||||
sm = SecurityModuleMemory(keys=keys)
|
||||
return sm
|
||||
|
||||
@classmethod
|
||||
def from_keyfile(cls, keyfile: str) -> 'SecurityModuleMemory':
|
||||
"""
|
||||
Create a new memory-backed security module with keys referred from a profile in
|
||||
the given configuration file.
|
||||
|
||||
:param keyfile: Path (relative or absolute) to a private keys file.
|
||||
:return: New memory-backed security module instance.
|
||||
"""
|
||||
keys: List[Union[EthereumKey, CryptosignKey]] = []
|
||||
|
||||
if not os.path.exists(keyfile) or not os.path.isfile(keyfile):
|
||||
raise RuntimeError('keyfile "{}" is not a file'.format(keyfile))
|
||||
|
||||
# now load the private key file - this returns a dict which should include:
|
||||
# private-key-eth: 6b08b6e186bd2a3b9b2f36e6ece3f8031fe788ab3dc4a1cfd3a489ea387c496b
|
||||
# private-key-ed25519: 20e8c05d0ede9506462bb049c4843032b18e8e75b314583d0c8d8a4942f9be40
|
||||
data = parse_keyfile(keyfile)
|
||||
|
||||
# first, add Ethereum key
|
||||
privkey_eth_hex = data.get('private-key-eth', None)
|
||||
if privkey_eth_hex is None:
|
||||
raise RuntimeError('"private-key-eth" not found in keyfile {}'.format(keyfile))
|
||||
keys.append(EthereumKey.from_bytes(binascii.a2b_hex(privkey_eth_hex)))
|
||||
|
||||
# second, add Cryptosign key
|
||||
privkey_ed25519_hex = data.get('private-key-ed25519', None)
|
||||
if privkey_ed25519_hex is None:
|
||||
raise RuntimeError('"private-key-ed25519" not found in keyfile {}'.format(keyfile))
|
||||
keys.append(CryptosignKey.from_bytes(binascii.a2b_hex(privkey_ed25519_hex)))
|
||||
|
||||
# initialize security module from collected keys
|
||||
sm = SecurityModuleMemory(keys=keys)
|
||||
return sm
|
||||
|
||||
|
||||
ISecurityModule.register(SecurityModuleMemory)
|
||||
726
backend/lib/python3.12/site-packages/autobahn/xbr/_seller.py
Normal file
726
backend/lib/python3.12/site-packages/autobahn/xbr/_seller.py
Normal file
@@ -0,0 +1,726 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 asyncio
|
||||
import binascii
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from autobahn.wamp.types import RegisterOptions, CallDetails
|
||||
from autobahn.wamp.exception import ApplicationError, TransportLost
|
||||
from autobahn.wamp.protocol import ApplicationSession
|
||||
from ._util import unpack_uint256, pack_uint256
|
||||
from txaio import time_ns
|
||||
|
||||
import cbor2
|
||||
import eth_keys
|
||||
import nacl.secret
|
||||
import nacl.utils
|
||||
import nacl.public
|
||||
import txaio
|
||||
|
||||
from ..util import hl, hlval
|
||||
from ._eip712_channel_close import sign_eip712_channel_close, recover_eip712_channel_close
|
||||
|
||||
|
||||
class KeySeries(object):
|
||||
"""
|
||||
Data encryption key series with automatic (time-based) key rotation
|
||||
and key offering (to the XBR market maker).
|
||||
"""
|
||||
|
||||
def __init__(self, api_id, price, interval=None, count=None, on_rotate=None):
|
||||
"""
|
||||
|
||||
:param api_id: ID of the API for which to generate keys.
|
||||
:type api_id: bytes
|
||||
|
||||
:param price: Price per key in key series.
|
||||
:type price: int
|
||||
|
||||
:param interval: Interval in seconds after which to auto-rotate key.
|
||||
:type interval: int
|
||||
|
||||
:param count: Number of encryption operations after which to auto-rotate key.
|
||||
:type count: int
|
||||
|
||||
:param on_rotate: Optional user callback fired after key was rotated.
|
||||
:type on_rotate: callable
|
||||
"""
|
||||
assert type(api_id) == bytes and len(api_id) == 16
|
||||
assert type(price) == int and price >= 0
|
||||
assert interval is None or (type(interval) == int and interval > 0)
|
||||
assert count is None or (type(count) == int and count > 0)
|
||||
assert (interval is None and count is not None) or (interval is not None and count is None)
|
||||
assert on_rotate is None or callable(on_rotate)
|
||||
|
||||
self._api_id = api_id
|
||||
self._price = price
|
||||
self._interval = interval
|
||||
self._count = count
|
||||
self._count_current = 0
|
||||
self._on_rotate = on_rotate
|
||||
|
||||
self._id = None
|
||||
self._key = None
|
||||
self._box = None
|
||||
self._archive = {}
|
||||
|
||||
@property
|
||||
def key_id(self):
|
||||
"""
|
||||
Get current XBR data encryption key ID (of the keys being rotated
|
||||
in a series).
|
||||
|
||||
:return: Current key ID in key series (16 bytes).
|
||||
:rtype: bytes
|
||||
"""
|
||||
return self._id
|
||||
|
||||
async def encrypt(self, payload):
|
||||
"""
|
||||
Encrypt data with the current XBR data encryption key.
|
||||
|
||||
:param payload: Application payload to encrypt.
|
||||
:type payload: object
|
||||
|
||||
:return: The ciphertext for the encrypted application payload.
|
||||
:rtype: bytes
|
||||
"""
|
||||
data = cbor2.dumps(payload)
|
||||
|
||||
if self._count is not None:
|
||||
self._count_current += 1
|
||||
if self._count_current >= self._count:
|
||||
await self._rotate()
|
||||
self._count_current = 0
|
||||
|
||||
ciphertext = self._box.encrypt(data)
|
||||
|
||||
return self._id, 'cbor', ciphertext
|
||||
|
||||
def encrypt_key(self, key_id, buyer_pubkey):
|
||||
"""
|
||||
Encrypt a (previously used) XBR data encryption key with a buyer public key.
|
||||
|
||||
:param key_id: ID of the data encryption key to encrypt.
|
||||
:type key_id: bytes
|
||||
|
||||
:param buyer_pubkey: Buyer WAMP public key (Ed25519) to asymmetrically encrypt
|
||||
the data encryption key (selected by ``key_id``) against.
|
||||
:type buyer_pubkey: bytes
|
||||
|
||||
:return: The ciphertext for the encrypted data encryption key.
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert type(key_id) == bytes and len(key_id) == 16
|
||||
assert type(buyer_pubkey) == bytes and len(buyer_pubkey) == 32
|
||||
|
||||
key, _ = self._archive[key_id]
|
||||
|
||||
sendkey_box = nacl.public.SealedBox(nacl.public.PublicKey(buyer_pubkey,
|
||||
encoder=nacl.encoding.RawEncoder))
|
||||
|
||||
encrypted_key = sendkey_box.encrypt(key, encoder=nacl.encoding.RawEncoder)
|
||||
|
||||
return encrypted_key
|
||||
|
||||
def start(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def stop(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def _rotate(self):
|
||||
# generate new ID for next key in key series
|
||||
self._id = os.urandom(16)
|
||||
|
||||
# generate next data encryption key in key series
|
||||
self._key = nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE)
|
||||
|
||||
# create secretbox from new key
|
||||
self._box = nacl.secret.SecretBox(self._key)
|
||||
|
||||
# add key to archive
|
||||
self._archive[self._id] = (self._key, self._box)
|
||||
|
||||
self.log.debug(
|
||||
'{tx_type} key "{key_id}" rotated [api_id="{api_id}"]',
|
||||
tx_type=hl('XBR ROTATE', color='magenta'),
|
||||
key_id=hl(uuid.UUID(bytes=self._id)),
|
||||
api_id=hl(uuid.UUID(bytes=self._api_id)))
|
||||
|
||||
# maybe fire user callback
|
||||
if self._on_rotate:
|
||||
await self._on_rotate(self)
|
||||
|
||||
|
||||
class PayingChannel(object):
|
||||
def __init__(self, adr, seq, balance):
|
||||
assert type(adr) == bytes and len(adr) == 16
|
||||
assert type(seq) == int and seq >= 0
|
||||
assert type(balance) == int and balance >= 0
|
||||
self._adr = adr
|
||||
self._seq = seq
|
||||
self._balance = balance
|
||||
|
||||
|
||||
class SimpleSeller(object):
|
||||
|
||||
log = None
|
||||
KeySeries = None
|
||||
|
||||
STATE_NONE = 0
|
||||
STATE_STARTING = 1
|
||||
STATE_STARTED = 2
|
||||
STATE_STOPPING = 3
|
||||
STATE_STOPPED = 4
|
||||
|
||||
def __init__(self, market_maker_adr, seller_key, provider_id=None):
|
||||
"""
|
||||
|
||||
:param market_maker_adr: Market maker public Ethereum address (20 bytes).
|
||||
:type market_maker_adr: bytes
|
||||
|
||||
:param seller_key: Seller (delegate) private Ethereum key (32 bytes).
|
||||
:type seller_key: bytes
|
||||
|
||||
:param provider_id: Optional explicit data provider ID. When not given, the seller delegate
|
||||
public WAMP key (Ed25519 in Hex) is used as the provider ID. This must be a valid WAMP URI part.
|
||||
:type provider_id: string
|
||||
"""
|
||||
assert type(market_maker_adr) == bytes and len(market_maker_adr) == 20, 'market_maker_adr must be bytes[20], but got "{}"'.format(market_maker_adr)
|
||||
assert type(seller_key) == bytes and len(seller_key) == 32, 'seller delegate must be bytes[32], but got "{}"'.format(seller_key)
|
||||
assert provider_id is None or type(provider_id) == str, 'provider_id must be None or string, but got "{}"'.format(provider_id)
|
||||
|
||||
self.log = txaio.make_logger()
|
||||
|
||||
# current seller state
|
||||
self._state = SimpleSeller.STATE_NONE
|
||||
|
||||
# market maker address
|
||||
self._market_maker_adr = market_maker_adr
|
||||
self._xbrmm_config = None
|
||||
|
||||
# seller raw ethereum private key (32 bytes)
|
||||
self._pkey_raw = seller_key
|
||||
|
||||
# seller ethereum private key object
|
||||
self._pkey = eth_keys.keys.PrivateKey(seller_key)
|
||||
|
||||
# seller ethereum private account from raw private key
|
||||
# FIXME
|
||||
# self._acct = Account.privateKeyToAccount(self._pkey)
|
||||
self._acct = None
|
||||
|
||||
# seller ethereum account canonical address
|
||||
self._addr = self._pkey.public_key.to_canonical_address()
|
||||
|
||||
# seller ethereum account canonical checksummed address
|
||||
# FIXME
|
||||
# self._caddr = web3.Web3.toChecksumAddress(self._addr)
|
||||
self._caddr = None
|
||||
|
||||
# seller provider ID
|
||||
self._provider_id = provider_id or str(self._pkey.public_key)
|
||||
|
||||
self._channels = {}
|
||||
|
||||
# will be filled with on-chain payment channel contract, once started
|
||||
self._channel = None
|
||||
|
||||
# channel current (off-chain) balance
|
||||
self._balance = 0
|
||||
|
||||
# channel sequence number
|
||||
self._seq = 0
|
||||
|
||||
self._keys = {}
|
||||
self._keys_map = {}
|
||||
|
||||
# after start() is running, these will be set
|
||||
self._session = None
|
||||
self._session_regs = None
|
||||
|
||||
@property
|
||||
def public_key(self):
|
||||
"""
|
||||
This seller delegate public Ethereum key.
|
||||
|
||||
:return: Ethereum public key of this seller delegate.
|
||||
:rtype: bytes
|
||||
"""
|
||||
return self._pkey.public_key
|
||||
|
||||
def add(self, api_id, prefix, price, interval=None, count=None, categories=None):
|
||||
"""
|
||||
Add a new (rotating) private encryption key for encrypting data on the given API.
|
||||
|
||||
:param api_id: API for which to create a new series of rotating encryption keys.
|
||||
:type api_id: bytes
|
||||
|
||||
:param price: Price in XBR token per key.
|
||||
:type price: int
|
||||
|
||||
:param interval: Interval (in seconds) after which to auto-rotate the encryption key.
|
||||
:type interval: int
|
||||
|
||||
:param count: Number of encryption operations after which to auto-rotate the encryption key.
|
||||
:type count: int
|
||||
"""
|
||||
assert type(api_id) == bytes and len(api_id) == 16 and api_id not in self._keys
|
||||
assert type(price) == int and price >= 0
|
||||
assert interval is None or (type(interval) == int and interval > 0)
|
||||
assert count is None or (type(count) == int and count > 0)
|
||||
assert (interval is None and count is not None) or (interval is not None and count is None)
|
||||
assert categories is None or (type(categories) == dict and (type(k) == str for k in categories.keys()) and (type(v) == str for v in categories.values())), 'invalid categories type (must be dict) or category key or value type (must both be string)'
|
||||
|
||||
async def on_rotate(key_series):
|
||||
|
||||
key_id = key_series.key_id
|
||||
|
||||
self._keys_map[key_id] = key_series
|
||||
|
||||
# FIXME: expose the knobs hard-coded in below ..
|
||||
|
||||
# offer the key to the market maker (retry 5x in specific error cases)
|
||||
retries = 5
|
||||
while retries:
|
||||
try:
|
||||
valid_from = time_ns() - 10 * 10 ** 9
|
||||
delegate = self._addr
|
||||
# FIXME: sign the supplied offer information using self._pkey
|
||||
signature = os.urandom(65)
|
||||
provider_id = self._provider_id
|
||||
|
||||
offer = await self._session.call('xbr.marketmaker.place_offer',
|
||||
key_id,
|
||||
api_id,
|
||||
prefix,
|
||||
valid_from,
|
||||
delegate,
|
||||
signature,
|
||||
privkey=None,
|
||||
price=pack_uint256(price) if price is not None else None,
|
||||
categories=categories,
|
||||
expires=None,
|
||||
copies=None,
|
||||
provider_id=provider_id)
|
||||
|
||||
self.log.debug(
|
||||
'{tx_type} key "{key_id}" offered for {price} [api_id={api_id}, prefix="{prefix}", delegate="{delegate}"]',
|
||||
tx_type=hl('XBR OFFER ', color='magenta'),
|
||||
key_id=hl(uuid.UUID(bytes=key_id)),
|
||||
api_id=hl(uuid.UUID(bytes=api_id)),
|
||||
price=hl(str(int(price / 10 ** 18) if price is not None else 0) + ' XBR', color='magenta'),
|
||||
delegate=hl(binascii.b2a_hex(delegate).decode()),
|
||||
prefix=hl(prefix))
|
||||
|
||||
self.log.debug('offer={offer}', offer=offer)
|
||||
|
||||
break
|
||||
|
||||
except ApplicationError as e:
|
||||
if e.error == 'wamp.error.no_such_procedure':
|
||||
self.log.warn('xbr.marketmaker.offer: procedure unavailable!')
|
||||
else:
|
||||
self.log.failure()
|
||||
break
|
||||
except TransportLost:
|
||||
self.log.warn('TransportLost while calling xbr.marketmaker.offer!')
|
||||
break
|
||||
except:
|
||||
self.log.failure()
|
||||
|
||||
retries -= 1
|
||||
self.log.warn('Failed to place offer for key! Retrying {retries}/5 ..', retries=retries)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
key_series = self.KeySeries(api_id, price, interval=interval, count=count, on_rotate=on_rotate)
|
||||
self._keys[api_id] = key_series
|
||||
self.log.debug('Created new key series {key_series}', key_series=key_series)
|
||||
|
||||
return key_series
|
||||
|
||||
async def start(self, session):
|
||||
"""
|
||||
Start rotating keys and placing key offers with the XBR market maker.
|
||||
|
||||
:param session: WAMP session over which to communicate with the XBR market maker.
|
||||
:type session: :class:`autobahn.wamp.protocol.ApplicationSession`
|
||||
"""
|
||||
assert isinstance(session, ApplicationSession), 'session must be an ApplicationSession, was "{}"'.format(session)
|
||||
assert self._state in [SimpleSeller.STATE_NONE, SimpleSeller.STATE_STOPPED], 'seller already running'
|
||||
|
||||
self._state = SimpleSeller.STATE_STARTING
|
||||
self._session = session
|
||||
self._session_regs = []
|
||||
|
||||
self.log.debug('Start selling from seller delegate address {address} (public key 0x{public_key}..)',
|
||||
address=hl(self._caddr),
|
||||
public_key=binascii.b2a_hex(self._pkey.public_key[:10]).decode())
|
||||
|
||||
# get the currently active (if any) paying channel for the delegate
|
||||
self._channel = await session.call('xbr.marketmaker.get_active_paying_channel', self._addr)
|
||||
if not self._channel:
|
||||
raise Exception('no active paying channel found')
|
||||
|
||||
channel_oid = self._channel['channel_oid']
|
||||
assert type(channel_oid) == bytes and len(channel_oid) == 16
|
||||
self._channel_oid = uuid.UUID(bytes=channel_oid)
|
||||
|
||||
procedure = 'xbr.provider.{}.sell'.format(self._provider_id)
|
||||
reg = await session.register(self.sell, procedure, options=RegisterOptions(details_arg='details'))
|
||||
self._session_regs.append(reg)
|
||||
self.log.debug('Registered procedure "{procedure}"', procedure=hl(reg.procedure))
|
||||
|
||||
procedure = 'xbr.provider.{}.close_channel'.format(self._provider_id)
|
||||
reg = await session.register(self.close_channel, procedure, options=RegisterOptions(details_arg='details'))
|
||||
self._session_regs.append(reg)
|
||||
self.log.debug('Registered procedure "{procedure}"', procedure=hl(reg.procedure))
|
||||
|
||||
for key_series in self._keys.values():
|
||||
await key_series.start()
|
||||
|
||||
self._xbrmm_config = await session.call('xbr.marketmaker.get_config')
|
||||
|
||||
# get the current (off-chain) balance of the paying channel
|
||||
paying_balance = await session.call('xbr.marketmaker.get_paying_channel_balance', self._channel_oid.bytes)
|
||||
# FIXME
|
||||
if type(paying_balance['remaining']) == bytes:
|
||||
paying_balance['remaining'] = unpack_uint256(paying_balance['remaining'])
|
||||
|
||||
if not paying_balance['remaining'] > 0:
|
||||
raise Exception('no off-chain balance remaining on paying channel')
|
||||
|
||||
self._channels[channel_oid] = PayingChannel(channel_oid, paying_balance['seq'], paying_balance['remaining'])
|
||||
self._state = SimpleSeller.STATE_STARTED
|
||||
|
||||
# FIXME
|
||||
self._balance = paying_balance['remaining']
|
||||
if type(self._balance) == bytes:
|
||||
self._balance = unpack_uint256(self._balance)
|
||||
self._seq = paying_balance['seq']
|
||||
|
||||
self.log.info('Ok, seller delegate started [active paying channel {channel_oid} with remaining balance {remaining} at sequence {seq}]',
|
||||
channel_oid=hl(self._channel_oid), remaining=hlval(self._balance), seq=hlval(self._seq))
|
||||
|
||||
return paying_balance['remaining']
|
||||
|
||||
async def stop(self):
|
||||
"""
|
||||
Stop rotating/offering keys to the XBR market maker.
|
||||
"""
|
||||
assert self._state in [SimpleSeller.STATE_STARTED], 'seller not running'
|
||||
|
||||
self._state = SimpleSeller.STATE_STOPPING
|
||||
|
||||
dl = []
|
||||
for key_series in self._keys.values():
|
||||
d = key_series.stop()
|
||||
dl.append(d)
|
||||
|
||||
if self._session_regs:
|
||||
if self._session and self._session.is_attached():
|
||||
# voluntarily unregister interface
|
||||
for reg in self._session_regs:
|
||||
d = reg.unregister()
|
||||
dl.append(d)
|
||||
self._session_regs = None
|
||||
|
||||
d = txaio.gather(dl)
|
||||
|
||||
try:
|
||||
await d
|
||||
except:
|
||||
self.log.failure()
|
||||
finally:
|
||||
self._state = SimpleSeller.STATE_STOPPED
|
||||
self._session = None
|
||||
|
||||
self.log.info('Ok, seller delegate stopped.')
|
||||
|
||||
async def balance(self):
|
||||
"""
|
||||
Return current (off-chain) balance of paying channel:
|
||||
|
||||
* ``amount``: The initial amount with which the paying channel was opened.
|
||||
* ``remaining``: The remaining amount of XBR in the paying channel that can be earned.
|
||||
* ``inflight``: The amount of XBR allocated to sell transactions that are currently processed.
|
||||
|
||||
:return: Current paying balance.
|
||||
:rtype: dict
|
||||
"""
|
||||
if self._state not in [SimpleSeller.STATE_STARTED]:
|
||||
raise RuntimeError('seller not running')
|
||||
if not self._session or not self._session.is_attached():
|
||||
raise RuntimeError('market-maker session not attached')
|
||||
|
||||
paying_balance = await self._session.call('xbr.marketmaker.get_paying_channel_balance', self._channel['channel_oid'])
|
||||
|
||||
return paying_balance
|
||||
|
||||
async def wrap(self, api_id, uri, payload):
|
||||
"""
|
||||
Encrypt and wrap application payload for a given API and destined for a specific WAMP URI.
|
||||
|
||||
:param api_id: API for which to encrypt and wrap the application payload for.
|
||||
:type api_id: bytes
|
||||
|
||||
:param uri: WAMP URI the application payload is destined for (eg the procedure or topic URI).
|
||||
:type uri: str
|
||||
|
||||
:param payload: Application payload to encrypt and wrap.
|
||||
:type payload: object
|
||||
|
||||
:return: The encrypted and wrapped application payload: a tuple with ``(key_id, serializer, ciphertext)``.
|
||||
:rtype: tuple
|
||||
"""
|
||||
assert type(api_id) == bytes and len(api_id) == 16 and api_id in self._keys
|
||||
assert type(uri) == str
|
||||
assert payload is not None
|
||||
|
||||
keyseries = self._keys[api_id]
|
||||
|
||||
key_id, serializer, ciphertext = await keyseries.encrypt(payload)
|
||||
|
||||
return key_id, serializer, ciphertext
|
||||
|
||||
def close_channel(self, market_maker_adr, channel_oid, channel_seq, channel_balance, channel_is_final,
|
||||
marketmaker_signature, details=None):
|
||||
"""
|
||||
Called by a XBR Market Maker to close a paying channel.
|
||||
"""
|
||||
assert type(market_maker_adr) == bytes and len(market_maker_adr) == 20, 'market_maker_adr must be bytes[20], but was {}'.format(type(market_maker_adr))
|
||||
assert type(channel_oid) == bytes and len(channel_oid) == 16, 'channel_oid must be bytes[16], but was {}'.format(type(channel_oid))
|
||||
assert type(channel_seq) == int, 'channel_seq must be int, but was {}'.format(type(channel_seq))
|
||||
assert type(channel_balance) == bytes and len(channel_balance) == 32, 'channel_balance must be bytes[32], but was {}'.format(type(channel_balance))
|
||||
assert type(channel_is_final) == bool, 'channel_is_final must be bool, but was {}'.format(type(channel_is_final))
|
||||
assert type(marketmaker_signature) == bytes and len(marketmaker_signature) == (32 + 32 + 1), 'marketmaker_signature must be bytes[65], but was {}'.format(type(marketmaker_signature))
|
||||
assert details is None or isinstance(details, CallDetails), 'details must be autobahn.wamp.types.CallDetails'
|
||||
|
||||
# check that the delegate_adr fits what we expect for the market maker
|
||||
if market_maker_adr != self._market_maker_adr:
|
||||
raise ApplicationError('xbr.error.unexpected_delegate_adr',
|
||||
'{}.sell() - unexpected market maker (delegate) address: expected 0x{}, but got 0x{}'.format(self.__class__.__name__, binascii.b2a_hex(self._market_maker_adr).decode(), binascii.b2a_hex(market_maker_adr).decode()))
|
||||
|
||||
# FIXME: must be the currently active channel .. and we need to track all of these
|
||||
if channel_oid != self._channel['channel_oid']:
|
||||
self._session.leave()
|
||||
raise ApplicationError('xbr.error.unexpected_channel_oid',
|
||||
'{}.sell() - unexpected paying channel address: expected 0x{}, but got 0x{}'.format(self.__class__.__name__, binascii.b2a_hex(self._channel['channel_oid']).decode(), binascii.b2a_hex(channel_oid).decode()))
|
||||
|
||||
# channel sequence number: check we have consensus on off-chain channel state with peer (which is the market maker)
|
||||
if channel_seq != self._seq:
|
||||
raise ApplicationError('xbr.error.unexpected_channel_seq',
|
||||
'{}.sell() - unexpected channel (after tx) sequence number: expected {}, but got {}'.format(self.__class__.__name__, self._seq + 1, channel_seq))
|
||||
|
||||
# channel balance: check we have consensus on off-chain channel state with peer (which is the market maker)
|
||||
channel_balance = unpack_uint256(channel_balance)
|
||||
if channel_balance != self._balance:
|
||||
raise ApplicationError('xbr.error.unexpected_channel_balance',
|
||||
'{}.sell() - unexpected channel (after tx) balance: expected {}, but got {}'.format(self.__class__.__name__, self._balance, channel_balance))
|
||||
|
||||
# XBRSIG: check the signature (over all input data for the buying of the key)
|
||||
signer_address = recover_eip712_channel_close(channel_oid, channel_seq, channel_balance, channel_is_final, marketmaker_signature)
|
||||
if signer_address != market_maker_adr:
|
||||
self.log.warn('{klass}.sell()::XBRSIG[4/8] - EIP712 signature invalid: signer_address={signer_address}, delegate_adr={delegate_adr}',
|
||||
klass=self.__class__.__name__,
|
||||
signer_address=hl(binascii.b2a_hex(signer_address).decode()),
|
||||
delegate_adr=hl(binascii.b2a_hex(market_maker_adr).decode()))
|
||||
raise ApplicationError('xbr.error.invalid_signature', '{}.sell()::XBRSIG[4/8] - EIP712 signature invalid or not signed by market maker'.format(self.__class__.__name__))
|
||||
|
||||
# XBRSIG: compute EIP712 typed data signature
|
||||
seller_signature = sign_eip712_channel_close(self._pkey_raw, channel_oid, channel_seq, channel_balance, channel_is_final)
|
||||
|
||||
receipt = {
|
||||
'delegate': self._addr,
|
||||
'seq': channel_seq,
|
||||
'balance': pack_uint256(channel_balance),
|
||||
'is_final': channel_is_final,
|
||||
'signature': seller_signature,
|
||||
}
|
||||
|
||||
self.log.debug('{klass}.close_channel() - {tx_type} closing channel {channel_oid}, closing balance {channel_balance}, closing sequence {channel_seq} [caller={caller}, caller_authid="{caller_authid}"]',
|
||||
klass=self.__class__.__name__,
|
||||
tx_type=hl('XBR CLOSE ', color='magenta'),
|
||||
channel_balance=hl(str(int(channel_balance / 10 ** 18)) + ' XBR', color='magenta'),
|
||||
channel_seq=hl(channel_seq),
|
||||
channel_oid=hl(binascii.b2a_hex(channel_oid).decode()),
|
||||
caller=hl(details.caller),
|
||||
caller_authid=hl(details.caller_authid))
|
||||
|
||||
return receipt
|
||||
|
||||
def sell(self, market_maker_adr, buyer_pubkey, key_id, channel_oid, channel_seq, amount, balance, signature, details=None):
|
||||
"""
|
||||
Called by a XBR Market Maker to buy a data encyption key. The XBR Market Maker here is
|
||||
acting for (triggered by) the XBR buyer delegate.
|
||||
|
||||
:param market_maker_adr: The market maker Ethereum address. The technical buyer is usually the
|
||||
XBR market maker (== the XBR delegate of the XBR market operator).
|
||||
:type market_maker_adr: bytes of length 20
|
||||
|
||||
:param buyer_pubkey: The buyer delegate Ed25519 public key.
|
||||
:type buyer_pubkey: bytes of length 32
|
||||
|
||||
:param key_id: The UUID of the data encryption key to buy.
|
||||
:type key_id: bytes of length 16
|
||||
|
||||
:param channel_oid: The on-chain channel contract address.
|
||||
:type channel_oid: bytes of length 16
|
||||
|
||||
:param channel_seq: Paying channel sequence off-chain transaction number.
|
||||
:type channel_seq: int
|
||||
|
||||
:param amount: The amount paid by the XBR Buyer via the XBR Market Maker.
|
||||
:type amount: bytes
|
||||
|
||||
:param balance: Balance remaining in the payment channel (from the market maker to the
|
||||
seller) after successfully buying the key.
|
||||
:type balance: bytes
|
||||
|
||||
:param signature: Signature over the supplied buying information, using the Ethereum
|
||||
private key of the market maker (which is the delegate of the marker operator).
|
||||
:type signature: bytes of length 65
|
||||
|
||||
:param details: Caller details. The call will come from the XBR Market Maker.
|
||||
:type details: :class:`autobahn.wamp.types.CallDetails`
|
||||
|
||||
:return: The data encryption key, itself encrypted to the public key of the original buyer.
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert type(market_maker_adr) == bytes and len(market_maker_adr) == 20, 'delegate_adr must be bytes[20]'
|
||||
assert type(buyer_pubkey) == bytes and len(buyer_pubkey) == 32, 'buyer_pubkey must be bytes[32]'
|
||||
assert type(key_id) == bytes and len(key_id) == 16, 'key_id must be bytes[16]'
|
||||
assert type(channel_oid) == bytes and len(channel_oid) == 16, 'channel_oid must be bytes[16]'
|
||||
assert type(channel_seq) == int, 'channel_seq must be int'
|
||||
assert type(amount) == bytes and len(amount) == 32, 'amount_paid must be bytes[32], but was {}'.format(type(amount))
|
||||
assert type(balance) == bytes and len(amount) == 32, 'post_balance must be bytes[32], but was {}'.format(type(balance))
|
||||
assert type(signature) == bytes and len(signature) == (32 + 32 + 1), 'signature must be bytes[65]'
|
||||
assert details is None or isinstance(details, CallDetails), 'details must be autobahn.wamp.types.CallDetails'
|
||||
|
||||
amount = unpack_uint256(amount)
|
||||
balance = unpack_uint256(balance)
|
||||
|
||||
# check that the delegate_adr fits what we expect for the market maker
|
||||
if market_maker_adr != self._market_maker_adr:
|
||||
raise ApplicationError('xbr.error.unexpected_marketmaker_adr',
|
||||
'{}.sell() - unexpected market maker address: expected 0x{}, but got 0x{}'.format(self.__class__.__name__, binascii.b2a_hex(self._market_maker_adr).decode(), binascii.b2a_hex(market_maker_adr).decode()))
|
||||
|
||||
# get the key series given the key_id
|
||||
if key_id not in self._keys_map:
|
||||
raise ApplicationError('crossbar.error.no_such_object', '{}.sell() - no key with ID "{}"'.format(self.__class__.__name__, key_id))
|
||||
key_series = self._keys_map[key_id]
|
||||
|
||||
# FIXME: must be the currently active channel .. and we need to track all of these
|
||||
if channel_oid != self._channel['channel_oid']:
|
||||
self._session.leave()
|
||||
raise ApplicationError('xbr.error.unexpected_channel_oid',
|
||||
'{}.sell() - unexpected paying channel address: expected 0x{}, but got 0x{}'.format(self.__class__.__name__, binascii.b2a_hex(self._channel['channel_oid']).decode(), binascii.b2a_hex(channel_oid).decode()))
|
||||
|
||||
# channel sequence number: check we have consensus on off-chain channel state with peer (which is the market maker)
|
||||
if channel_seq != self._seq + 1:
|
||||
raise ApplicationError('xbr.error.unexpected_channel_seq',
|
||||
'{}.sell() - unexpected channel (after tx) sequence number: expected {}, but got {}'.format(self.__class__.__name__, self._seq + 1, channel_seq))
|
||||
|
||||
# channel balance: check we have consensus on off-chain channel state with peer (which is the market maker)
|
||||
if balance != self._balance - amount:
|
||||
raise ApplicationError('xbr.error.unexpected_channel_balance',
|
||||
'{}.sell() - unexpected channel (after tx) balance: expected {}, but got {}'.format(self.__class__.__name__, self._balance - amount, balance))
|
||||
|
||||
# FIXME
|
||||
current_block_number = 1
|
||||
verifying_chain_id = self._xbrmm_config['verifying_chain_id']
|
||||
verifying_contract_adr = binascii.a2b_hex(self._xbrmm_config['verifying_contract_adr'][2:])
|
||||
|
||||
market_oid = self._channel['market_oid']
|
||||
|
||||
# XBRSIG[4/8]: check the signature (over all input data for the buying of the key)
|
||||
signer_address = recover_eip712_channel_close(verifying_chain_id, verifying_contract_adr, current_block_number,
|
||||
market_oid, channel_oid, channel_seq, balance, False, signature)
|
||||
if signer_address != market_maker_adr:
|
||||
self.log.warn('{klass}.sell()::XBRSIG[4/8] - EIP712 signature invalid: signer_address={signer_address}, delegate_adr={delegate_adr}',
|
||||
klass=self.__class__.__name__,
|
||||
signer_address=hl(binascii.b2a_hex(signer_address).decode()),
|
||||
delegate_adr=hl(binascii.b2a_hex(market_maker_adr).decode()))
|
||||
raise ApplicationError('xbr.error.invalid_signature', '{}.sell()::XBRSIG[4/8] - EIP712 signature invalid or not signed by market maker'.format(self.__class__.__name__))
|
||||
|
||||
# now actually update our local knowledge of the channel state
|
||||
# FIXME: what if code down below fails?
|
||||
self._seq += 1
|
||||
self._balance -= amount
|
||||
|
||||
# encrypt the data encryption key against the original buyer delegate Ed25519 public key
|
||||
sealed_key = key_series.encrypt_key(key_id, buyer_pubkey)
|
||||
|
||||
assert type(sealed_key) == bytes and len(sealed_key) == 80, '{}.sell() - unexpected sealed key computed (expected bytes[80]): {}'.format(self.__class__.__name__, sealed_key)
|
||||
|
||||
# XBRSIG[5/8]: compute EIP712 typed data signature
|
||||
seller_signature = sign_eip712_channel_close(self._pkey_raw, verifying_chain_id, verifying_contract_adr,
|
||||
current_block_number, market_oid, channel_oid, self._seq,
|
||||
self._balance, False)
|
||||
|
||||
receipt = {
|
||||
# key ID that has been bought
|
||||
'key_id': key_id,
|
||||
|
||||
# seller delegate address that sold the key
|
||||
'delegate': self._addr,
|
||||
|
||||
# buyer delegate Ed25519 public key with which the bought key was sealed
|
||||
'buyer_pubkey': buyer_pubkey,
|
||||
|
||||
# finally return what the consumer (buyer) was actually interested in:
|
||||
# the data encryption key, sealed (public key Ed25519 encrypted) to the
|
||||
# public key of the buyer delegate
|
||||
'sealed_key': sealed_key,
|
||||
|
||||
# paying channel off-chain transaction sequence numbers
|
||||
'channel_seq': self._seq,
|
||||
|
||||
# amount paid for the key
|
||||
'amount': amount,
|
||||
|
||||
# paying channel amount remaining
|
||||
'balance': self._balance,
|
||||
|
||||
# seller (delegate) signature
|
||||
'signature': seller_signature,
|
||||
}
|
||||
|
||||
self.log.info('{klass}.sell() - {tx_type} key "{key_id}" sold for {amount_earned} - balance is {balance} [caller={caller}, caller_authid="{caller_authid}", buyer_pubkey="{buyer_pubkey}"]',
|
||||
klass=self.__class__.__name__,
|
||||
tx_type=hl('XBR SELL ', color='magenta'),
|
||||
key_id=hl(uuid.UUID(bytes=key_id)),
|
||||
amount_earned=hl(str(int(amount / 10 ** 18)) + ' XBR', color='magenta'),
|
||||
balance=hl(str(int(self._balance / 10 ** 18)) + ' XBR', color='magenta'),
|
||||
# paying_channel=hl(binascii.b2a_hex(paying_channel).decode()),
|
||||
caller=hl(details.caller),
|
||||
caller_authid=hl(details.caller_authid),
|
||||
buyer_pubkey=hl(binascii.b2a_hex(buyer_pubkey).decode()))
|
||||
|
||||
return receipt
|
||||
244
backend/lib/python3.12/site-packages/autobahn/xbr/_userkey.py
Normal file
244
backend/lib/python3.12/site-packages/autobahn/xbr/_userkey.py
Normal file
@@ -0,0 +1,244 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 re
|
||||
import os
|
||||
import binascii
|
||||
import socket
|
||||
from collections import OrderedDict
|
||||
|
||||
import getpass
|
||||
import click
|
||||
|
||||
from nacl.signing import SigningKey
|
||||
from nacl.encoding import HexEncoder
|
||||
|
||||
from eth_keys import KeyAPI
|
||||
from eth_keys.backends import NativeECCBackend
|
||||
|
||||
from autobahn.util import utcnow, write_keyfile, parse_keyfile
|
||||
from autobahn.wamp import cryptosign
|
||||
|
||||
if 'USER' in os.environ:
|
||||
_DEFAULT_EMAIL_ADDRESS = '{}@{}'.format(os.environ['USER'], socket.getfqdn())
|
||||
else:
|
||||
_DEFAULT_EMAIL_ADDRESS = 'unknown'
|
||||
|
||||
|
||||
class EmailAddress(click.ParamType):
|
||||
"""
|
||||
Email address validator.
|
||||
"""
|
||||
|
||||
name = 'Email address'
|
||||
|
||||
def __init__(self):
|
||||
click.ParamType.__init__(self)
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
if re.match(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", value):
|
||||
return value
|
||||
self.fail('invalid email address "{}"'.format(value))
|
||||
|
||||
|
||||
def _user_id(yes_to_all=False):
|
||||
if yes_to_all:
|
||||
return _DEFAULT_EMAIL_ADDRESS
|
||||
while True:
|
||||
value = click.prompt('Please enter your email address', type=EmailAddress(), default=_DEFAULT_EMAIL_ADDRESS)
|
||||
if click.confirm('We will send an activation code to "{}", ok?'.format(value), default=True):
|
||||
break
|
||||
return value
|
||||
|
||||
|
||||
def _creator(yes_to_all=False):
|
||||
"""
|
||||
for informational purposes, try to identify the creator (user@hostname)
|
||||
"""
|
||||
if yes_to_all:
|
||||
return _DEFAULT_EMAIL_ADDRESS
|
||||
else:
|
||||
try:
|
||||
user = getpass.getuser()
|
||||
except BaseException:
|
||||
user = 'unknown'
|
||||
|
||||
try:
|
||||
hostname = socket.gethostname()
|
||||
except BaseException:
|
||||
hostname = 'unknown'
|
||||
|
||||
return '{}@{}'.format(user, hostname)
|
||||
|
||||
|
||||
class UserKey(object):
|
||||
def __init__(self, privkey, pubkey, yes_to_all=True):
|
||||
|
||||
self._privkey_path = privkey
|
||||
self._pubkey_path = pubkey
|
||||
|
||||
self.key = None
|
||||
self._creator = None
|
||||
self._created_at = None
|
||||
self.user_id = None
|
||||
self._privkey = None
|
||||
self._privkey_hex = None
|
||||
self._pubkey = None
|
||||
self._pubkey_hex = None
|
||||
self._load_and_maybe_generate(self._privkey_path, self._pubkey_path, yes_to_all)
|
||||
|
||||
def __str__(self):
|
||||
return 'UserKey(privkey="{}", pubkey="{}" [{}])'.format(self._privkey_path, self._pubkey_path,
|
||||
self._pubkey_hex)
|
||||
|
||||
def _load_and_maybe_generate(self, privkey_path, pubkey_path, yes_to_all=False):
|
||||
|
||||
if os.path.exists(privkey_path):
|
||||
|
||||
# node private key seems to exist already .. check!
|
||||
|
||||
priv_tags = parse_keyfile(privkey_path, private=True)
|
||||
for tag in ['creator', 'created-at', 'user-id', 'public-key-ed25519', 'private-key-ed25519']:
|
||||
if tag not in priv_tags:
|
||||
raise Exception("Corrupt user private key file {} - {} tag not found".format(privkey_path, tag))
|
||||
|
||||
creator = priv_tags['creator']
|
||||
created_at = priv_tags['created-at']
|
||||
user_id = priv_tags['user-id']
|
||||
|
||||
privkey_hex = priv_tags['private-key-ed25519']
|
||||
privkey = SigningKey(privkey_hex, encoder=HexEncoder)
|
||||
pubkey = privkey.verify_key
|
||||
pubkey_hex = pubkey.encode(encoder=HexEncoder).decode('ascii')
|
||||
|
||||
if priv_tags['public-key-ed25519'] != pubkey_hex:
|
||||
raise Exception(("Inconsistent user private key file {} - public-key-ed25519 doesn't"
|
||||
" correspond to private-key-ed25519").format(pubkey_path))
|
||||
|
||||
eth_pubadr = None
|
||||
eth_privkey = None
|
||||
eth_privkey_seed_hex = priv_tags.get('private-key-eth', None)
|
||||
if eth_privkey_seed_hex:
|
||||
eth_privkey_seed = binascii.a2b_hex(eth_privkey_seed_hex)
|
||||
eth_privkey = KeyAPI(NativeECCBackend).PrivateKey(eth_privkey_seed)
|
||||
eth_pubadr = eth_privkey.public_key.to_checksum_address()
|
||||
if 'public-adr-eth' in priv_tags:
|
||||
if priv_tags['public-adr-eth'] != eth_pubadr:
|
||||
raise Exception(("Inconsistent node private key file {} - public-adr-eth doesn't"
|
||||
" correspond to private-key-eth").format(privkey_path))
|
||||
|
||||
if os.path.exists(pubkey_path):
|
||||
pub_tags = parse_keyfile(pubkey_path, private=False)
|
||||
for tag in ['creator', 'created-at', 'user-id', 'public-key-ed25519']:
|
||||
if tag not in pub_tags:
|
||||
raise Exception("Corrupt user public key file {} - {} tag not found".format(pubkey_path, tag))
|
||||
|
||||
if pub_tags['public-key-ed25519'] != pubkey_hex:
|
||||
raise Exception(("Inconsistent user public key file {} - public-key-ed25519 doesn't"
|
||||
" correspond to private-key-ed25519").format(pubkey_path))
|
||||
|
||||
if pub_tags.get('public-adr-eth', None) != eth_pubadr:
|
||||
raise Exception(
|
||||
("Inconsistent user public key file {} - public-adr-eth doesn't"
|
||||
" correspond to private-key-eth in private key file {}").format(pubkey_path, privkey_path))
|
||||
|
||||
else:
|
||||
# public key is missing! recreate it
|
||||
pub_tags = OrderedDict([
|
||||
('creator', priv_tags['creator']),
|
||||
('created-at', priv_tags['created-at']),
|
||||
('user-id', priv_tags['user-id']),
|
||||
('public-key-ed25519', pubkey_hex),
|
||||
('public-adr-eth', eth_pubadr),
|
||||
])
|
||||
msg = 'Crossbar.io user public key\n\n'
|
||||
write_keyfile(pubkey_path, pub_tags, msg)
|
||||
|
||||
click.echo('Re-created user public key from private key: {}'.format(pubkey_path))
|
||||
|
||||
else:
|
||||
# user private key does not yet exist: generate one
|
||||
creator = _creator(yes_to_all)
|
||||
created_at = utcnow()
|
||||
user_id = _user_id(yes_to_all)
|
||||
|
||||
privkey = SigningKey.generate()
|
||||
privkey_hex = privkey.encode(encoder=HexEncoder).decode('ascii')
|
||||
pubkey = privkey.verify_key
|
||||
pubkey_hex = pubkey.encode(encoder=HexEncoder).decode('ascii')
|
||||
|
||||
eth_privkey_seed = os.urandom(32)
|
||||
eth_privkey_seed_hex = binascii.b2a_hex(eth_privkey_seed).decode()
|
||||
eth_privkey = KeyAPI(NativeECCBackend).PrivateKey(eth_privkey_seed)
|
||||
eth_pubadr = eth_privkey.public_key.to_checksum_address()
|
||||
|
||||
# first, write the public file
|
||||
tags = OrderedDict([
|
||||
('creator', creator),
|
||||
('created-at', created_at),
|
||||
('user-id', user_id),
|
||||
('public-key-ed25519', pubkey_hex),
|
||||
('public-adr-eth', eth_pubadr),
|
||||
])
|
||||
msg = 'Crossbar.io user public key\n\n'
|
||||
write_keyfile(pubkey_path, tags, msg)
|
||||
os.chmod(pubkey_path, 420)
|
||||
|
||||
# now, add the private key and write the private file
|
||||
tags['private-key-ed25519'] = privkey_hex
|
||||
tags['private-key-eth'] = eth_privkey_seed_hex
|
||||
msg = 'Crossbar.io user private key - KEEP THIS SAFE!\n\n'
|
||||
write_keyfile(privkey_path, tags, msg)
|
||||
os.chmod(privkey_path, 384)
|
||||
|
||||
click.echo('New user public key generated: {}'.format(pubkey_path))
|
||||
click.echo('New user private key generated ({}): {}'.format('keep this safe!', privkey_path))
|
||||
|
||||
# fix file permissions on node public/private key files
|
||||
# note: we use decimals instead of octals as octal literals have changed between Py2/3
|
||||
if os.stat(pubkey_path).st_mode & 511 != 420: # 420 (decimal) == 0644 (octal)
|
||||
os.chmod(pubkey_path, 420)
|
||||
click.echo('File permissions on user public key fixed!')
|
||||
|
||||
if os.stat(privkey_path).st_mode & 511 != 384: # 384 (decimal) == 0600 (octal)
|
||||
os.chmod(privkey_path, 384)
|
||||
click.echo('File permissions on user private key fixed!')
|
||||
|
||||
# load keys into object
|
||||
self._creator = creator
|
||||
self._created_at = created_at
|
||||
|
||||
self._privkey = privkey
|
||||
self._privkey_hex = privkey_hex
|
||||
self._pubkey = pubkey
|
||||
self._pubkey_hex = pubkey_hex
|
||||
|
||||
self._eth_pubadr = eth_pubadr
|
||||
self._eth_privkey_seed_hex = eth_privkey_seed_hex
|
||||
self._eth_privkey = eth_privkey
|
||||
|
||||
self.user_id = user_id
|
||||
self.key = cryptosign.CryptosignKey(privkey, can_sign=True)
|
||||
175
backend/lib/python3.12/site-packages/autobahn/xbr/_util.py
Normal file
175
backend/lib/python3.12/site-packages/autobahn/xbr/_util.py
Normal file
@@ -0,0 +1,175 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 struct
|
||||
from binascii import a2b_hex, b2a_hex
|
||||
from typing import Union, Dict, List
|
||||
|
||||
import web3
|
||||
|
||||
|
||||
def make_w3(gateway_config=None):
|
||||
"""
|
||||
Create a Web3 instance configured and ready-to-use gateway to the blockchain.
|
||||
|
||||
:param gateway_config: Blockchain gateway configuration.
|
||||
:type gateway_config: dict
|
||||
|
||||
:return: Configured Web3 instance.
|
||||
:rtype: :class:`web3.Web3`
|
||||
"""
|
||||
if gateway_config is None or gateway_config['type'] == 'auto':
|
||||
w3 = web3.Web3()
|
||||
|
||||
elif gateway_config['type'] == 'user':
|
||||
request_kwargs = gateway_config.get('http_options', {})
|
||||
w3 = web3.Web3(web3.Web3.HTTPProvider(gateway_config['http'], request_kwargs=request_kwargs))
|
||||
|
||||
elif gateway_config['type'] == 'infura':
|
||||
request_kwargs = gateway_config.get('http_options', {})
|
||||
project_id = gateway_config['key']
|
||||
# project_secret = gateway_config['secret']
|
||||
|
||||
http_url = 'https://{}.infura.io/v3/{}'.format(gateway_config['network'], project_id)
|
||||
w3 = web3.Web3(web3.Web3.HTTPProvider(http_url, request_kwargs=request_kwargs))
|
||||
|
||||
# https://web3py.readthedocs.io/en/stable/middleware.html#geth-style-proof-of-authority
|
||||
if gateway_config.get('network', None) == 'rinkeby':
|
||||
# This middleware is required to connect to geth --dev or the Rinkeby public network.
|
||||
from web3.middleware import geth_poa_middleware
|
||||
|
||||
# inject the poa compatibility middleware to the innermost layer
|
||||
w3.middleware_onion.inject(geth_poa_middleware, layer=0)
|
||||
|
||||
# FIXME
|
||||
elif gateway_config['type'] == 'cloudflare':
|
||||
# https://developers.cloudflare.com/web3/ethereum-gateway/reference/supported-networks/
|
||||
raise NotImplementedError()
|
||||
|
||||
# FIXME
|
||||
elif gateway_config['type'] == 'zksync':
|
||||
# https://v2-docs.zksync.io/dev/testnet/important-links.html
|
||||
raise NotImplementedError()
|
||||
|
||||
else:
|
||||
raise RuntimeError('invalid blockchain gateway type "{}"'.format(gateway_config['type']))
|
||||
|
||||
return w3
|
||||
|
||||
|
||||
def unpack_uint128(data):
|
||||
assert data is None or type(data) == bytes, 'data must by bytes, was {}'.format(type(data))
|
||||
if data and type(data) == bytes:
|
||||
assert len(data) == 16, 'data must be bytes[16], but was bytes[{}]'.format(len(data))
|
||||
|
||||
if data:
|
||||
return web3.Web3.toInt(data)
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def pack_uint128(value):
|
||||
assert value is None or (type(value) == int and value >= 0 and value < 2**128)
|
||||
|
||||
if value:
|
||||
data = web3.Web3.toBytes(value)
|
||||
return b'\x00' * (16 - len(data)) + data
|
||||
else:
|
||||
return b'\x00' * 16
|
||||
|
||||
|
||||
def unpack_uint256(data):
|
||||
assert data is None or type(data) == bytes, 'data must by bytes, was {}'.format(type(data))
|
||||
if data and type(data) == bytes:
|
||||
assert len(data) == 32, 'data must be bytes[32], but was bytes[{}]'.format(len(data))
|
||||
|
||||
if data:
|
||||
return int(web3.Web3.toInt(data))
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def pack_uint256(value):
|
||||
assert value is None or (type(value) == int and value >= 0 and value < 2**256), 'value must be uint256, but was {}'.format(value)
|
||||
|
||||
if value:
|
||||
data = web3.Web3.toBytes(value)
|
||||
return b'\x00' * (32 - len(data)) + data
|
||||
else:
|
||||
return b'\x00' * 32
|
||||
|
||||
|
||||
def pack_ethadr(value: Union[bytes, str], return_dict: bool = False) -> Union[List[int], Dict[str, int]]:
|
||||
"""
|
||||
|
||||
:param value:
|
||||
:param return_dict:
|
||||
:return:
|
||||
"""
|
||||
if type(value) == str:
|
||||
if value.startswith('0x'):
|
||||
value_bytes = a2b_hex(value[2:])
|
||||
else:
|
||||
value_bytes = a2b_hex(value)
|
||||
elif type(value) == bytes:
|
||||
value_bytes = value
|
||||
else:
|
||||
assert False, 'invalid type {} for value'.format(type(value))
|
||||
assert len(value_bytes) == 20
|
||||
|
||||
w = []
|
||||
for i in range(5):
|
||||
w.append(struct.unpack('<I', value_bytes[0 + i * 4:4 + i * 4])[0])
|
||||
|
||||
if return_dict:
|
||||
packed_value = {'w0': w[0], 'w1': w[1], 'w2': w[2], 'w3': w[3], 'w4': w[4]}
|
||||
else:
|
||||
packed_value = w
|
||||
|
||||
return packed_value
|
||||
|
||||
|
||||
def unpack_ethadr(packed_value: Union[List[int], Dict[str, int]], return_str=False) -> Union[bytes, str]:
|
||||
"""
|
||||
|
||||
:param packed_value:
|
||||
:param return_str:
|
||||
:return:
|
||||
"""
|
||||
w = []
|
||||
if type(packed_value) == dict:
|
||||
for i in range(5):
|
||||
w.append(struct.pack('<I', packed_value['w{}'.format(i)]))
|
||||
elif type(packed_value) == list:
|
||||
for i in range(5):
|
||||
w.append(struct.pack('<I', packed_value[i]))
|
||||
else:
|
||||
assert False, 'should not arrive here'
|
||||
|
||||
if return_str:
|
||||
return web3.Web3.toChecksumAddress('0x' + b2a_hex(b''.join(w)).decode())
|
||||
else:
|
||||
return b''.join(w)
|
||||
111
backend/lib/python3.12/site-packages/autobahn/xbr/_wallet.py
Normal file
111
backend/lib/python3.12/site-packages/autobahn/xbr/_wallet.py
Normal file
@@ -0,0 +1,111 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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 hashlib
|
||||
from typing import Optional
|
||||
import argon2
|
||||
import hkdf
|
||||
|
||||
|
||||
def stretch_argon2_secret(email: str, password: str, salt: Optional[bytes] = None) -> bytes:
|
||||
"""
|
||||
Compute argon2id based secret from user email and password only. This uses Argon2id
|
||||
for stretching a potentially weak user password/PIN and subsequent HKDF based key
|
||||
extending to derive private key material (PKM) for different usage contexts.
|
||||
|
||||
The Argon2 parameters used are the following:
|
||||
|
||||
* kdf ``argon2id-13``
|
||||
* time cost ``4096``
|
||||
* memory cost ``512``
|
||||
* parallelism ``1``
|
||||
|
||||
See `draft-irtf-cfrg-argon2 <https://datatracker.ietf.org/doc/draft-irtf-cfrg-argon2/>`__ and
|
||||
`argon2-cffi <https://argon2-cffi.readthedocs.io/en/stable/>`__.
|
||||
|
||||
:param email: User email.
|
||||
:param password: User password.
|
||||
:param salt: Optional salt to use (must be 16 bytes long). If none is given, compute salt
|
||||
from email as ``salt = SHA256(email)[:16]``.
|
||||
:return: The computed private key material (256b, 32 octets).
|
||||
"""
|
||||
if not salt:
|
||||
m = hashlib.sha256()
|
||||
m.update(email.encode('utf8'))
|
||||
salt = m.digest()[:16]
|
||||
assert len(salt) == 16
|
||||
|
||||
pkm = argon2.low_level.hash_secret_raw(
|
||||
secret=password.encode('utf8'),
|
||||
salt=salt,
|
||||
time_cost=4096,
|
||||
memory_cost=512,
|
||||
parallelism=1,
|
||||
hash_len=32,
|
||||
type=argon2.low_level.Type.ID,
|
||||
version=19,
|
||||
)
|
||||
|
||||
return pkm
|
||||
|
||||
|
||||
def expand_argon2_secret(pkm: bytes, context: bytes, salt: Optional[bytes] = None) -> bytes:
|
||||
"""
|
||||
|
||||
Expand ``pkm`` and ``context`` into a key of length ``bytes`` using
|
||||
HKDF's expand function based on HMAC SHA-512). See the HKDF draft RFC and paper for usage notes.
|
||||
|
||||
:param pkm:
|
||||
:param context:
|
||||
:param salt:
|
||||
:return:
|
||||
"""
|
||||
kdf = hkdf.Hkdf(salt=salt, input_key_material=pkm, hash=hashlib.sha512)
|
||||
key = kdf.expand(info=context, length=32)
|
||||
return key
|
||||
|
||||
|
||||
def pkm_from_argon2_secret(email: str, password: str, context: str, salt: Optional[bytes] = None) -> bytes:
|
||||
"""
|
||||
|
||||
:param email:
|
||||
:param password:
|
||||
:param context:
|
||||
:param salt:
|
||||
:return:
|
||||
"""
|
||||
if not salt:
|
||||
m = hashlib.sha256()
|
||||
m.update(email.encode('utf8'))
|
||||
salt = m.digest()[:16]
|
||||
assert len(salt) == 16
|
||||
|
||||
context = context.encode('utf8')
|
||||
|
||||
pkm = stretch_argon2_secret(email=email, password=password, salt=salt)
|
||||
key = expand_argon2_secret(pkm=pkm, context=context, salt=salt)
|
||||
|
||||
return key
|
||||
@@ -0,0 +1,16 @@
|
||||
{% if is_first_by_category %}
|
||||
##
|
||||
## enum types
|
||||
##
|
||||
|
||||
{% endif%}
|
||||
class {{ metadata.classname }}(object):
|
||||
"""
|
||||
{{ metadata.docs }}
|
||||
"""
|
||||
{% for value_name in metadata.values %}
|
||||
{{ value_name }}: int = {{ metadata.values[value_name].value }}
|
||||
"""
|
||||
{{ metadata.values[value_name].docs }}
|
||||
"""
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Python module "{{ modulename }}"
|
||||
|
||||
from . import {{ ', '.join(imports) }}
|
||||
|
||||
__all__ = [{{ ', '.join(imports) }}]
|
||||
@@ -0,0 +1,360 @@
|
||||
{% if is_first_by_category %}
|
||||
##
|
||||
## object types
|
||||
##
|
||||
|
||||
{% endif %}
|
||||
{% if render_imports %}
|
||||
import uuid
|
||||
import pprint
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from autobahn.wamp.request import Publication, Subscription, Registration
|
||||
|
||||
import flatbuffers
|
||||
from flatbuffers.compat import import_numpy
|
||||
np = import_numpy()
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
class {{ metadata.classname }}(object):
|
||||
"""
|
||||
{{ metadata.docs }}
|
||||
"""
|
||||
__slots__ = ['_tab', {% for field in metadata.fields_by_id %}'_{{ field.name }}', {% endfor %}]
|
||||
|
||||
def __init__(self, {% for field in metadata.fields_by_id %}{{ field.name }}: {{ field.type.map('python', field.attrs, required=False, objtype_as_string=True) }} = None, {% endfor %}):
|
||||
# the underlying FlatBuffers vtable
|
||||
self._tab = None
|
||||
|
||||
{% for field in metadata.fields_by_id %}
|
||||
# {{ field.docs }}
|
||||
self._{{ field.name }}: {{ field.type.map('python', field.attrs, required=False, objtype_as_string=True) }} = {{ field.name }}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
{% for field in metadata.fields_by_id %}
|
||||
if other.{{ field.name }} != self.{{ field.name }}:
|
||||
return False
|
||||
{% endfor %}
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
{% for field in metadata.fields_by_id %}
|
||||
@property
|
||||
def {{ field.name }}(self) -> {{ field.type.map('python', field.attrs, required=False, objtype_as_string=True) }}:
|
||||
"""
|
||||
{{ field.docs }}
|
||||
"""
|
||||
if self._{{ field.name }} is None and self._tab:
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset({{ field.offset }}))
|
||||
|
||||
{% if field.type.map('python', field.attrs, True) == 'str' %}
|
||||
# access type "string" attribute:
|
||||
value = ''
|
||||
if o != 0:
|
||||
_value = self._tab.String(o + self._tab.Pos)
|
||||
if _value is not None:
|
||||
value = _value.decode('utf8')
|
||||
|
||||
{% elif field.type.map('python', field.attrs, True) == 'bytes' %}
|
||||
# access type "bytes" attribute:
|
||||
value = b''
|
||||
if o != 0:
|
||||
_off = self._tab.Vector(o)
|
||||
_len = self._tab.VectorLen(o)
|
||||
_value = memoryview(self._tab.Bytes)[_off:_off + _len]
|
||||
if _value is not None:
|
||||
value = _value
|
||||
|
||||
{% elif field.type.map('python', field.attrs, True) in ['int', 'float', 'double'] %}
|
||||
# access type "int|float|double" attribute:
|
||||
value = 0
|
||||
if o != 0:
|
||||
_value = self._tab.Get(flatbuffers.number_types.{{ FbsType.FBS2FLAGS[field.type.basetype] }}, o + self._tab.Pos)
|
||||
if _value is not None:
|
||||
value = _value
|
||||
|
||||
{% elif field.type.map('python', field.attrs, True) == 'bool' %}
|
||||
# access type "bool" attribute:
|
||||
value = False
|
||||
if o != 0:
|
||||
_value = self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)
|
||||
if _value is not None:
|
||||
value = _value
|
||||
|
||||
{% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
|
||||
# access type "uuid.UUID" attribute:
|
||||
value = uuid.UUID(bytes=b'\x00' * 16)
|
||||
if o != 0:
|
||||
_off = self._tab.Vector(o)
|
||||
_len = self._tab.VectorLen(o)
|
||||
_value = memoryview(self._tab.Bytes)[_off:_off + _len]
|
||||
if _value is not None:
|
||||
value = uuid.UUID(bytes=bytes(_value))
|
||||
|
||||
{% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
|
||||
# access type "np.datetime64" attribute:
|
||||
value = np.datetime64(0, 'ns')
|
||||
if o != 0:
|
||||
_value = self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
|
||||
if value is not None:
|
||||
value = np.datetime64(_value, 'ns')
|
||||
|
||||
{% elif field.type.basetype == FbsType.Vector %}
|
||||
# access type "Vector" attribute:
|
||||
value = []
|
||||
if o != 0:
|
||||
_start_off = self._tab.Vector(o)
|
||||
_len = self._tab.VectorLen(o)
|
||||
for j in range(_len):
|
||||
_off = _start_off + flatbuffers.number_types.UOffsetTFlags.py_type(j) * 4
|
||||
_off = self._tab.Indirect(_off)
|
||||
{% if False and field.type.element == FbsType.Obj %}
|
||||
_value = {{ field.type.element.split('.')[-1] }}.cast(self._tab.Bytes, _off)
|
||||
{% else %}
|
||||
# FIXME [8]
|
||||
_value = {{ field.type.element }}()
|
||||
{% endif %}
|
||||
value.append(_value)
|
||||
|
||||
{% elif field.type.basetype == FbsType.Obj %}
|
||||
# access type "Object" attribute:
|
||||
|
||||
{% if field.type.objtype %}
|
||||
value = {{ field.type.objtype.split('.')[-1] }}()
|
||||
if o != 0:
|
||||
_off = self._tab.Indirect(o + self._tab.Pos)
|
||||
value = {{ field.type.objtype.split('.')[-1] }}.cast(self._tab.Bytes, _off)
|
||||
{% else %}
|
||||
# FIXME [9]: objtype of field "{{ field.name }}" is None
|
||||
value = ''
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
# FIXME [5]
|
||||
raise NotImplementedError('implement processing [5] of FlatBuffers type "{}"'.format({{ field.type.map('python', field.attrs, True) }}))
|
||||
{% endif %}
|
||||
assert value is not None
|
||||
self._{{ field.name }} = value
|
||||
return self._{{ field.name }}
|
||||
|
||||
@{{ field.name }}.setter
|
||||
def {{ field.name }}(self, value: {{ field.type.map('python', field.attrs, required=False, objtype_as_string=True) }}):
|
||||
if value is not None:
|
||||
self._{{ field.name }} = value
|
||||
else:
|
||||
{% if field.type.map('python', field.attrs, True) == 'str' %}
|
||||
# set default value on type "string" attribute:
|
||||
self._{{ field.name }} = ''
|
||||
{% elif field.type.map('python', field.attrs, True) == 'bytes' %}
|
||||
# set default value on type "bytes" attribute:
|
||||
self._{{ field.name }} = b''
|
||||
{% elif field.type.map('python', field.attrs, True) in ['int', 'float', 'double'] %}
|
||||
# set default value on type "int|float|double" attribute:
|
||||
self._{{ field.name }} = 0
|
||||
{% elif field.type.map('python', field.attrs, True) == 'bool' %}
|
||||
# set default value on type "bool" attribute:
|
||||
self._{{ field.name }} = False
|
||||
{% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
|
||||
# set default value on type "uuid.UUID" attribute:
|
||||
self._{{ field.name }} = uuid.UUID(bytes=b'\x00' * 16)
|
||||
{% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
|
||||
# set default value on type "np.datetime64" attribute:
|
||||
self._{{ field.name }} = np.datetime64(0, 'ns')
|
||||
# set default value on type "List" attribute:
|
||||
{% elif field.type.basetype == FbsType.Vector %}
|
||||
self._{{ field.name }} = []
|
||||
# set default value on type "Object" attribute:
|
||||
{% elif field.type.basetype == FbsType.Obj %}
|
||||
self._{{ field.name }} = {{ field.type.map('python', field.attrs, True) }}()
|
||||
{% else %}
|
||||
# FIXME [6]
|
||||
raise NotImplementedError('implement processing [2] of FlatBuffers type "{}", basetype {}'.format({{ field.type.map('python', field.attrs, True) }}, {{ field.type.basetype }}))
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
@staticmethod
|
||||
def parse(data: Dict) -> '{{ metadata.classname }}':
|
||||
"""
|
||||
Parse generic, native language object into a typed, native language object.
|
||||
|
||||
:param data: Generic native language object to parse, e.g. output of ``cbor2.loads``.
|
||||
|
||||
:returns: Typed object of this class.
|
||||
"""
|
||||
# FIXME
|
||||
# for key in data.keys():
|
||||
# assert key in {{ metadata.fields.keys() }}
|
||||
obj = {{ metadata.classname }}()
|
||||
{% for field in metadata.fields_by_id %}
|
||||
if '{{ field.name }}' in data:
|
||||
{% if field.type.map('python', field.attrs, True) == 'str' %}
|
||||
assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == str), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
|
||||
obj.{{ field.name }} = data['{{ field.name }}']
|
||||
|
||||
{% elif field.type.map('python', field.attrs, True) == 'bytes' %}
|
||||
assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == bytes), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
|
||||
obj.{{ field.name }} = data['{{ field.name }}']
|
||||
|
||||
{% elif field.type.map('python', field.attrs, True) == 'int' %}
|
||||
assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == int), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
|
||||
obj.{{ field.name }} = data['{{ field.name }}']
|
||||
|
||||
{% elif field.type.map('python', field.attrs, True) == 'float' %}
|
||||
assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == float), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
|
||||
obj.{{ field.name }} = data['{{ field.name }}']
|
||||
|
||||
{% elif field.type.map('python', field.attrs, True) == 'bool' %}
|
||||
assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == bool), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
|
||||
obj.{{ field.name }} = data['{{ field.name }}']
|
||||
|
||||
{% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
|
||||
assert (data['{{ field.name }}'] is None or (type(data['{{ field.name }}']) == bytes and len(data['{{ field.name }}']) == 16)), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
|
||||
if data['{{ field.name }}'] is not None:
|
||||
obj.{{ field.name }} = uuid.UUID(bytes=data['{{ field.name }}'])
|
||||
else:
|
||||
obj.{{ field.name }} = None
|
||||
|
||||
{% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
|
||||
assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == int), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
|
||||
if data['{{ field.name }}'] is not None:
|
||||
obj.{{ field.name }} = np.datetime64(data['{{ field.name }}'], 'ns')
|
||||
else:
|
||||
obj.{{ field.name }} = np.datetime64(0, 'ns')
|
||||
|
||||
{% elif field.type.basetype == FbsType.Vector %}
|
||||
assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == list), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
|
||||
_value = []
|
||||
for v in data['{{ field.name }}']:
|
||||
{% if False and field.type.element == FbsType.Obj %}
|
||||
# FIXME
|
||||
_value.append({{ field.type.objtype.split('.')[-1] }}.parse(v))
|
||||
{% else %}
|
||||
_value.append(v)
|
||||
{% endif %}
|
||||
obj.{{ field.name }} = _value
|
||||
|
||||
{% elif field.type.basetype == FbsType.Obj %}
|
||||
assert (data['{{ field.name }}'] is None or type(data['{{ field.name }}']) == dict), '{} has wrong type {}'.format('{{ field.name }}', type(data['{{ field.name }}']))
|
||||
_value = {{ field.type.map('python', field.attrs, True) }}.parse(data['{{ field.name }}'])
|
||||
obj.{{ field.name }} = _value
|
||||
|
||||
{% else %}
|
||||
# FIXME [3]
|
||||
raise NotImplementedError('implement processing [3] of FlatBuffers type "{}"'.format({{ field.type.map('python', field.attrs, True) }}))
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
return obj
|
||||
|
||||
def marshal(self) -> Dict:
|
||||
"""
|
||||
Marshal all data contained in this typed native object into a generic object.
|
||||
|
||||
:returns: Generic object that can be serialized to bytes using e.g. ``cbor2.dumps``.
|
||||
"""
|
||||
obj = {
|
||||
{% for field in metadata.fields_by_id %}
|
||||
|
||||
{% if field.type.map('python', field.attrs, True) in ['str', 'bytes', 'int', 'long', 'float', 'double', 'bool'] %}
|
||||
'{{ field.name }}': self.{{ field.name }},
|
||||
|
||||
{% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
|
||||
'{{ field.name }}': self.{{ field.name }}.bytes if self.{{ field.name }} is not None else None,
|
||||
|
||||
{% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
|
||||
'{{ field.name }}': int(self.{{ field.name }}) if self.{{ field.name }} is not None else None,
|
||||
|
||||
{% elif field.type.basetype == FbsType.Vector %}
|
||||
{% if field.type.element == FbsType.Obj %}
|
||||
'{{ field.name }}': [o.marshal() for o in self.{{ field.name }}] if self.{{ field.name }} is not None else None,
|
||||
{% else %}
|
||||
'{{ field.name }}': self.{{ field.name }},
|
||||
{% endif %}
|
||||
|
||||
{% elif field.type.basetype == FbsType.Obj %}
|
||||
'{{ field.name }}': self.{{ field.name }}.marshal() if self.{{ field.name }} is not None else None,
|
||||
|
||||
{% else %}
|
||||
# FIXME [4]: implement processing [4] of FlatBuffers type "{{ field.type | string }}" (Python type "{{ field.type.map('python', field.attrs, True) }}")
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
return obj
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Return string representation of this object, suitable for e.g. logging.
|
||||
|
||||
:returns: String representation of this object.
|
||||
"""
|
||||
return '\n{}\n'.format(pprint.pformat(self.marshal()))
|
||||
|
||||
@staticmethod
|
||||
def cast(buf: bytes, offset: int = 0) -> '{{ metadata.classname }}':
|
||||
"""
|
||||
Cast a FlatBuffers raw input buffer as a typed object of this class.
|
||||
|
||||
:param buf: The raw input buffer to cast.
|
||||
:param offset: Offset into raw buffer from which to cast flatbuffers from.
|
||||
|
||||
:returns: New native object that wraps the FlatBuffers raw buffer.
|
||||
"""
|
||||
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
|
||||
x = {{ metadata.classname }}()
|
||||
x._tab = flatbuffers.table.Table(buf, n + offset)
|
||||
return x
|
||||
|
||||
def build(self, builder):
|
||||
"""
|
||||
Build a FlatBuffers raw output buffer from this typed object.
|
||||
|
||||
:returns: Constructs the FlatBuffers using the builder and
|
||||
returns ``builder.EndObject()``.
|
||||
"""
|
||||
# first, write all string|bytes|etc typed attribute values (in order) to the buffer
|
||||
{% for field in metadata.fields_by_id %}
|
||||
{% if field.type.map('python', field.attrs, True) in ['str', 'bytes'] %}
|
||||
_{{ field.name }} = self.{{ field.name }}
|
||||
if _{{ field.name }}:
|
||||
_{{ field.name }} = builder.CreateString(_{{ field.name }})
|
||||
{% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
|
||||
_{{ field.name }} = self.{{ field.name }}.bytes if self.{{ field.name }} else None
|
||||
if _{{ field.name }}:
|
||||
_{{ field.name }} = builder.CreateString(_{{ field.name }})
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
# now start a new object in the buffer and write the actual object attributes (in field
|
||||
# order) to the buffer
|
||||
builder.StartObject({{ metadata.fields_by_id|length }})
|
||||
|
||||
{% for field in metadata.fields_by_id %}
|
||||
|
||||
{% if field.type.map('python', field.attrs, True) in ['str', 'bytes', 'uuid.UUID'] %}
|
||||
if _{{ field.name }}:
|
||||
builder.PrependUOffsetTRelativeSlot({{ field.id }}, flatbuffers.number_types.UOffsetTFlags.py_type(_{{ field.name }}), 0)
|
||||
|
||||
{% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
|
||||
if self.{{ field.name }}:
|
||||
builder.PrependUint64Slot({{ field.id }}, int(self.{{ field.name }}), 0)
|
||||
|
||||
{% elif field.type.map('python', field.attrs, True) in ['bool', 'int', 'float'] %}
|
||||
if self.{{ field.name }}:
|
||||
builder.{{ FbsType.FBS2PREPEND[field.type.basetype] }}({{ field.id }}, self.{{ field.name }}, 0)
|
||||
|
||||
{% else %}
|
||||
# FIXME [1]
|
||||
# raise NotImplementedError('implement builder [1] for type "{}"'.format({{ field.type.map('python', field.attrs, True) }}))
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
return builder.EndObject()
|
||||
@@ -0,0 +1,253 @@
|
||||
{% if is_first_by_category %}
|
||||
##
|
||||
## service types (aka "APIs")
|
||||
##
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
|
||||
import txaio
|
||||
from txaio.interfaces import ILogger
|
||||
|
||||
from autobahn.wamp.types import PublishOptions, SubscribeOptions, EventDetails, CallOptions, CallDetails, RegisterOptions
|
||||
from autobahn.wamp.request import Publication, Subscription, Registration
|
||||
from autobahn.wamp.interfaces import ISession
|
||||
from autobahn.xbr import IDelegate
|
||||
|
||||
Oid = UUID
|
||||
Oids = List[UUID]
|
||||
Void = type(None)
|
||||
|
||||
|
||||
{% endif %}
|
||||
class {{ metadata.classname }}(object):
|
||||
"""
|
||||
{{ metadata.docs }}
|
||||
|
||||
Interface UUID: ``{{ metadata.attrs.uuid }}``
|
||||
"""
|
||||
__slots__ = [
|
||||
'log',
|
||||
'_x_api_id',
|
||||
'_x_prefix',
|
||||
'_x_session',
|
||||
'_x_delegate',
|
||||
'_x_regs',
|
||||
'_x_subs',
|
||||
]
|
||||
|
||||
def __init__(self, prefix: str, log: Optional[ILogger]=None):
|
||||
"""
|
||||
|
||||
:param prefix: The URI prefix under which this API will be instantiated under on the realm joined.
|
||||
:param log: If provided, log to this logger, else create a new one internally.
|
||||
"""
|
||||
if log:
|
||||
self.log = log
|
||||
else:
|
||||
import txaio
|
||||
self.log = txaio.make_logger()
|
||||
self._x_api_id = uuid.UUID('{{ metadata.attrs.uuid }}')
|
||||
self._x_prefix = prefix
|
||||
self._x_session = None
|
||||
self._x_delegate = None
|
||||
self._x_regs = None
|
||||
self._x_subs = None
|
||||
|
||||
@property
|
||||
def api(self) -> uuid.UUID:
|
||||
"""
|
||||
Interface UUID of this API (``{{ metadata.attrs.uuid }}``).
|
||||
"""
|
||||
return self._x_api_id
|
||||
|
||||
@property
|
||||
def prefix(self) -> str:
|
||||
"""
|
||||
WAMP URI prefix under which this API is instantiated.
|
||||
"""
|
||||
return self._x_prefix
|
||||
|
||||
# WAMP PubSub part of the API:
|
||||
|
||||
{% for call_name in metadata.calls_by_id %}
|
||||
{% if metadata.calls[call_name].attrs['type'] == 'topic' %}
|
||||
async def publish_{{ call_name }}(self, evt: {{ repo.objs[metadata.calls[call_name].request.name].map('python', required=False, objtype_as_string=True) }}, options: Optional[PublishOptions] = None) -> Optional[Publication]:
|
||||
"""
|
||||
As an **interface provider**, publish event:
|
||||
|
||||
{{ metadata.calls[call_name].docs }}
|
||||
|
||||
:param evt: {{ repo.objs[metadata.calls[call_name].request.name].docs }}
|
||||
:returns: When doing an acknowledged publish, the WAMP publication is returned.
|
||||
"""
|
||||
assert self._x_session and self._x_session.is_attached()
|
||||
|
||||
topic = '{}.{{ call_name }}'.format(self._x_prefix)
|
||||
payload = evt.marshal()
|
||||
if self._x_delegate:
|
||||
key_id, enc_ser, ciphertext = await self._x_delegate.wrap(self._x_api_id, topic, payload)
|
||||
if options.acknowledge:
|
||||
pub = await self._x_session.publish(topic, key_id, enc_ser, ciphertext, options=options)
|
||||
else:
|
||||
self._x_session.publish(topic, key_id, enc_ser, ciphertext, options=options)
|
||||
pub = None
|
||||
else:
|
||||
if options.acknowledge:
|
||||
pub = await self._x_session.publish(topic, payload, options=options)
|
||||
else:
|
||||
self._x_session.publish(topic, payload, options=options)
|
||||
pub = None
|
||||
return pub
|
||||
|
||||
def receive_{{ call_name }}(self, evt: {{ repo.objs[metadata.calls[call_name].request.name].map('python', required=False, objtype_as_string=True) }}, details: Optional[EventDetails] = None):
|
||||
"""
|
||||
As an **interface consumer**, receive event:
|
||||
|
||||
{{ metadata.calls[call_name].docs }}
|
||||
|
||||
:param evt: {{ repo.objs[metadata.calls[call_name].request.name].docs }}
|
||||
"""
|
||||
raise NotImplementedError('event handler for "{{ call_name }}" not implemented')
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
# WAMP RPC part of the API:
|
||||
|
||||
{% for call_name in metadata.calls_by_id %}
|
||||
{% if metadata.calls[call_name].attrs['type'] == 'procedure' %}
|
||||
async def call_{{ call_name }}(self, req: {{ repo.objs[metadata.calls[call_name].request.name].map('python', required=False, objtype_as_string=True) }}, options: Optional[CallOptions] = None) -> {{ repo.objs[metadata.calls[call_name].response.name].map('python', required=False, objtype_as_string=True) }}:
|
||||
"""
|
||||
As an **interface consumer**, call procedure:
|
||||
|
||||
{{ metadata.calls[call_name].docs }}
|
||||
|
||||
:param req: {{ repo.objs[metadata.calls[call_name].request.name].docs }}
|
||||
:returns: {{ repo.objs[metadata.calls[call_name].response.name].docs }}
|
||||
"""
|
||||
assert self._x_session and self._x_session.is_attached()
|
||||
|
||||
procedure = '{}.{{ call_name }}'.format(self._x_prefix)
|
||||
payload = req.marshal()
|
||||
if self._x_delegate:
|
||||
key_id, enc_ser, ciphertext = await self._x_delegate.wrap(self._x_api_id, procedure, payload)
|
||||
result = await self._x_session.call(procedure, key_id, enc_ser, ciphertext, options=options)
|
||||
else:
|
||||
result = await self._x_session.call(procedure, payload, options=options)
|
||||
return result
|
||||
|
||||
def invoke_{{ call_name }}(self, req: {{ repo.objs[metadata.calls[call_name].request.name].map('python', required=False, objtype_as_string=True) }}, details: Optional[CallDetails] = None) -> {{ repo.objs[metadata.calls[call_name].response.name].map('python', required=False, objtype_as_string=True) }}:
|
||||
"""
|
||||
As an **interface provider**, process call invocation:
|
||||
|
||||
{{ metadata.calls[call_name].docs }}
|
||||
|
||||
:param req: {{ repo.objs[metadata.calls[call_name].request.name].docs }}
|
||||
:returns: {{ repo.objs[metadata.calls[call_name].response.name].docs }}
|
||||
"""
|
||||
raise NotImplementedError('call invocation handler for "{{ call_name }}" not implemented')
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@property
|
||||
def session(self) -> Optional[ISession]:
|
||||
"""
|
||||
WAMP session this API is attached to.
|
||||
"""
|
||||
return self._x_session
|
||||
|
||||
@property
|
||||
def delegate(self) -> Optional[IDelegate]:
|
||||
"""
|
||||
XBR (buyer/seller) delegate this API is attached to.
|
||||
"""
|
||||
return self._x_delegate
|
||||
|
||||
async def attach(self, session: ISession, delegate: Optional[IDelegate]):
|
||||
"""
|
||||
Attach this API instance with the given session and delegate, and under the given WAMP URI prefix.
|
||||
|
||||
:param session: WAMP session this API instance is attached to.
|
||||
:param delegate: If using end-to-end data encryption, XBR ("buyer/seller") delegate used by this API instance.
|
||||
"""
|
||||
assert self._x_session is None and session.is_attached()
|
||||
|
||||
self._x_session = session
|
||||
self._x_delegate = delegate
|
||||
|
||||
# WAMP PubSub part of the API:
|
||||
|
||||
subscriptions = []
|
||||
{% for call_name in metadata.calls_by_id %}
|
||||
{% if metadata.calls[call_name].attrs['type'] == 'topic' %}
|
||||
if self._x_delegate:
|
||||
async def do_receive_{{ call_name }}(key_id, enc_ser, ciphertext, details=None):
|
||||
try:
|
||||
payload = await self._x_delegate.unwrap(key_id, enc_ser, ciphertext)
|
||||
obj = {{ repo.objs[metadata.calls[call_name].request.name].map('python') }}.parse(payload)
|
||||
except:
|
||||
self.log.failure()
|
||||
else:
|
||||
self.receive_{{ call_name }}(obj, details=details)
|
||||
else:
|
||||
def do_receive_{{ call_name }}(evt, details=None):
|
||||
obj = {{ repo.objs[metadata.calls[call_name].request.name].map('python') }}.parse(evt)
|
||||
self.receive_{{ call_name }}(obj, details=details)
|
||||
|
||||
topic = '{}.{{ call_name }}'.format(self._x_prefix)
|
||||
sub = await self._x_session.subscribe(do_receive_{{ call_name }}, topic, options=SubscribeOptions(details=True))
|
||||
subscriptions.append(sub)
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
for sub in subscriptions:
|
||||
self.log.info('Subscription {} created for "{}"'.format(sub.id, sub.topic))
|
||||
self._x_subs = subscriptions
|
||||
|
||||
# WAMP RPC part of the API:
|
||||
|
||||
registrations = []
|
||||
{% for call_name in metadata.calls_by_id %}
|
||||
{% if metadata.calls[call_name].attrs['type'] == 'procedure' %}
|
||||
if self._x_delegate:
|
||||
async def do_invoke_{{ call_name }}(key_id, enc_ser, ciphertext, details=None):
|
||||
try:
|
||||
payload = await self._x_delegate.unwrap(key_id, enc_ser, ciphertext)
|
||||
obj = {{ repo.objs[metadata.calls[call_name].request.name].map('python') }}.parse(payload)
|
||||
except:
|
||||
self.log.failure()
|
||||
else:
|
||||
self.invoke_{{ call_name }}(obj, details=details)
|
||||
else:
|
||||
def do_invoke_{{ call_name }}(req, details=None):
|
||||
obj = {{ repo.objs[metadata.calls[call_name].request.name].map('python') }}.parse(req)
|
||||
self.invoke_{{ call_name }}(obj, details=details)
|
||||
|
||||
procedure = '{}.{{ call_name }}'.format(self._x_prefix)
|
||||
reg = await self._x_session.register(do_invoke_{{ call_name }}, procedure, options=RegisterOptions(details=True))
|
||||
registrations.append(reg)
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
for reg in registrations:
|
||||
self.log.info('Registration {} created for "{}"'.format(reg.id, reg.procedure))
|
||||
self._x_regs = registrations
|
||||
|
||||
def detach(self):
|
||||
"""
|
||||
Detach this API instance from the session and delegate.
|
||||
"""
|
||||
assert self._x_session is not None
|
||||
|
||||
dl = []
|
||||
if self._x_session and self._x_session.is_attached():
|
||||
for reg in self._x_regs:
|
||||
dl.append(reg.unregister())
|
||||
for sub in self._x_subs:
|
||||
dl.append(sub.unsubscribe())
|
||||
|
||||
self._x_session = None
|
||||
self._x_delegate = None
|
||||
|
||||
return txaio.gather(dl, consume_exceptions=True)
|
||||
@@ -0,0 +1,6 @@
|
||||
from .{{ metadata.module_relimport }} import {{ metadata.classname }}
|
||||
|
||||
def test_{{ metadata.classname }}():
|
||||
{% for value_name in metadata.values %}
|
||||
assert {{ metadata.classname }}.{{ value_name }} == {{ metadata.values[value_name].value }}
|
||||
{% endfor %}
|
||||
@@ -0,0 +1 @@
|
||||
# FIXME: add module level unit tests
|
||||
@@ -0,0 +1,220 @@
|
||||
{% if render_imports %}
|
||||
|
||||
import os
|
||||
import random
|
||||
import timeit
|
||||
import uuid
|
||||
import cbor2
|
||||
|
||||
import txaio
|
||||
txaio.use_twisted() # noqa
|
||||
|
||||
from autobahn import util
|
||||
from autobahn.wamp.serializer import JsonObjectSerializer, MsgPackObjectSerializer, \
|
||||
CBORObjectSerializer, UBJSONObjectSerializer
|
||||
|
||||
import flatbuffers
|
||||
import pytest
|
||||
import numpy as np
|
||||
from txaio import time_ns
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def builder():
|
||||
_builder = flatbuffers.Builder(0)
|
||||
return _builder
|
||||
|
||||
|
||||
_SERIALIZERS = [
|
||||
JsonObjectSerializer(),
|
||||
MsgPackObjectSerializer(),
|
||||
CBORObjectSerializer(),
|
||||
UBJSONObjectSerializer(),
|
||||
]
|
||||
|
||||
{% endif %}
|
||||
from {{ metadata.module_relimport }} import {{ metadata.classname }}
|
||||
|
||||
|
||||
def fill_{{ metadata.classname }}(obj: {{ metadata.classname }}):
|
||||
{% if metadata.fields_by_id|length == 0 %}
|
||||
# class has no fields
|
||||
pass
|
||||
{% else %}
|
||||
{% for field in metadata.fields_by_id %}
|
||||
{% if field.type.map('python', field.attrs, True) == 'str' %}
|
||||
obj.{{ field.name }} = util.generate_activation_code()
|
||||
{% elif field.type.map('python', field.attrs, True) == 'bytes' %}
|
||||
obj.{{ field.name }} = os.urandom(32)
|
||||
{% elif field.type.map('python', field.attrs, True) in ['int', 'long'] %}
|
||||
# FIXME: enum vs int
|
||||
# obj.{{ field.name }} = random.randint(0, 2**31 - 1)
|
||||
obj.{{ field.name }} = random.randint(0, 3)
|
||||
{% elif field.type.map('python', field.attrs, True) in ['float', 'double'] %}
|
||||
obj.{{ field.name }} = random.random()
|
||||
{% elif field.type.map('python', field.attrs, True) == 'bool' %}
|
||||
obj.{{ field.name }} = random.random() > 0.5
|
||||
{% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
|
||||
obj.{{ field.name }} = uuid.uuid4()
|
||||
{% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
|
||||
obj.{{ field.name }} = np.datetime64(time_ns(), 'ns')
|
||||
{% else %}
|
||||
obj.{{ field.name }} = None
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
def fill_{{ metadata.classname }}_empty(obj: {{ metadata.classname }}):
|
||||
{% if metadata.fields_by_id|length == 0 %}
|
||||
# class has no fields
|
||||
pass
|
||||
{% else %}
|
||||
{% for field in metadata.fields_by_id %}
|
||||
obj.{{ field.name }} = None
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def {{ metadata.classname }}_obj():
|
||||
_obj: {{ metadata.classname }} = {{ metadata.classname }}()
|
||||
fill_{{ metadata.classname }}(_obj)
|
||||
return _obj
|
||||
|
||||
|
||||
def test_{{ metadata.classname }}_roundtrip({{ metadata.classname }}_obj, builder):
|
||||
# serialize to bytes (flatbuffers) from python object
|
||||
obj = {{ metadata.classname }}_obj.build(builder)
|
||||
builder.Finish(obj)
|
||||
data = builder.Output()
|
||||
|
||||
# check length of serialized object data
|
||||
print('{} serialized object length = {} bytes'.format('{{ metadata.classname }}', len(data)))
|
||||
|
||||
# create python object from bytes (flatbuffers)
|
||||
_obj: {{ metadata.classname }} = {{ metadata.classname }}_obj.cast(data)
|
||||
|
||||
{% for field in metadata.fields_by_id %}
|
||||
assert _obj.{{ field.name }} == {{ metadata.classname }}_obj.{{ field.name }}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
def test_{{ metadata.classname }}_empty(builder):
|
||||
empty_obj = {{ metadata.classname }}()
|
||||
fill_{{ metadata.classname }}_empty(empty_obj)
|
||||
|
||||
# check the object was initialized correctly
|
||||
{% for field in metadata.fields_by_id %}
|
||||
{% if field.type.map('python', field.attrs, True) == 'str' %}
|
||||
assert empty_obj.{{ field.name }} == ''
|
||||
{% elif field.type.map('python', field.attrs, True) == 'bytes' %}
|
||||
assert empty_obj.{{ field.name }} == b''
|
||||
{% elif field.type.map('python', field.attrs, True) in ['int', 'long'] %}
|
||||
assert empty_obj.{{ field.name }} == 0
|
||||
{% elif field.type.map('python', field.attrs, True) in ['float', 'double'] %}
|
||||
assert empty_obj.{{ field.name }} == 0.0
|
||||
{% elif field.type.map('python', field.attrs, True) == 'bool' %}
|
||||
assert empty_obj.{{ field.name }} is False
|
||||
{% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
|
||||
assert empty_obj.{{ field.name }} == uuid.UUID(bytes=b'\0'*16)
|
||||
{% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
|
||||
assert empty_obj.{{ field.name }} == np.datetime64(0, 'ns')
|
||||
{% else %}
|
||||
assert empty_obj.{{ field.name }} is None
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
# serialize to bytes (flatbuffers) from python object
|
||||
obj = empty_obj.build(builder)
|
||||
builder.Finish(obj)
|
||||
data = builder.Output()
|
||||
|
||||
# check length of serialized object data
|
||||
print('{} serialized object length = {} bytes'.format('{{ metadata.classname }}', len(data)))
|
||||
|
||||
# create python object from bytes (flatbuffers)
|
||||
_obj: {{ metadata.classname }} = {{ metadata.classname }}.cast(data)
|
||||
|
||||
{% for field in metadata.fields_by_id %}
|
||||
{% if field.type.map('python', field.attrs, True) == 'str' %}
|
||||
assert _obj.{{ field.name }} == ''
|
||||
{% elif field.type.map('python', field.attrs, True) == 'bytes' %}
|
||||
assert _obj.{{ field.name }} == b''
|
||||
{% elif field.type.map('python', field.attrs, True) in ['int', 'long'] %}
|
||||
assert _obj.{{ field.name }} == 0
|
||||
{% elif field.type.map('python', field.attrs, True) in ['float', 'double'] %}
|
||||
assert _obj.{{ field.name }} == 0.0
|
||||
{% elif field.type.map('python', field.attrs, True) == 'bool' %}
|
||||
assert _obj.{{ field.name }} is False
|
||||
{% elif field.type.map('python', field.attrs, True) == 'uuid.UUID' %}
|
||||
assert _obj.{{ field.name }} == uuid.UUID(bytes=b'\0'*16)
|
||||
{% elif field.type.map('python', field.attrs, True) == 'np.datetime64' %}
|
||||
assert _obj.{{ field.name }} == np.datetime64(0, 'ns')
|
||||
{% else %}
|
||||
assert _obj.{{ field.name }} is None
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
def test_{{ metadata.classname }}_roundtrip_perf({{ metadata.classname }}_obj, builder):
|
||||
obj = {{ metadata.classname }}_obj.build(builder)
|
||||
builder.Finish(obj)
|
||||
data = builder.Output()
|
||||
scratch = {'value': 0}
|
||||
|
||||
def loop():
|
||||
_obj: {{ metadata.classname }} = {{ metadata.classname }}.cast(data)
|
||||
{% for field in metadata.fields_by_id %}
|
||||
assert _obj.{{ field.name }} == {{ metadata.classname }}_obj.{{ field.name }}
|
||||
{% endfor %}
|
||||
scratch['value'] += 1
|
||||
|
||||
loop_n = 7
|
||||
loop_m = 20000
|
||||
samples = []
|
||||
print('measuring:')
|
||||
for i in range(loop_n):
|
||||
secs = timeit.timeit(loop, number=loop_m)
|
||||
ops = round(float(loop_m) / secs, 1)
|
||||
samples.append(ops)
|
||||
print('{} objects/sec performance'.format(ops))
|
||||
|
||||
samples = sorted(samples)
|
||||
ops50 = samples[int(len(samples) / 2)]
|
||||
print('RESULT: {} objects/sec median performance'.format(ops50))
|
||||
|
||||
assert ops50 > 1000
|
||||
print(scratch['value'])
|
||||
|
||||
|
||||
def test_{{ metadata.classname }}_marshal_parse({{ metadata.classname }}_obj, builder):
|
||||
obj = {{ metadata.classname }}_obj.marshal()
|
||||
_obj = {{ metadata.classname }}_obj.parse(obj)
|
||||
{% for field in metadata.fields_by_id %}
|
||||
assert _obj.{{ field.name }} == {{ metadata.classname }}_obj.{{ field.name }}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
def test_{{ metadata.classname }}_marshal_cbor_parse({{ metadata.classname }}_obj, builder):
|
||||
obj = {{ metadata.classname }}_obj.marshal()
|
||||
data = cbor2.dumps(obj)
|
||||
print('serialized {} to {} bytes (cbor)'.format({{ metadata.classname }}, len(data)))
|
||||
_obj_raw = cbor2.loads(data)
|
||||
_obj = {{ metadata.classname }}_obj.parse(_obj_raw)
|
||||
{% for field in metadata.fields_by_id %}
|
||||
assert _obj.{{ field.name }} == {{ metadata.classname }}_obj.{{ field.name }}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
def test_{{ metadata.classname }}_ab_serializer_roundtrip({{ metadata.classname }}_obj, builder):
|
||||
obj = {{ metadata.classname }}_obj.marshal()
|
||||
for ser in _SERIALIZERS:
|
||||
data = ser.serialize(obj)
|
||||
print('serialized {} to {} bytes ({})'.format({{ metadata.classname }}, len(data), ser.NAME))
|
||||
msg2 = ser.unserialize(data)[0]
|
||||
obj2 = {{ metadata.classname }}.parse(msg2)
|
||||
|
||||
{% for field in metadata.fields_by_id %}
|
||||
assert obj2.{{ field.name }} == {{ metadata.classname }}_obj.{{ field.name }}
|
||||
{% endfor %}
|
||||
@@ -0,0 +1 @@
|
||||
# FIXME: add service level unit tests
|
||||
@@ -0,0 +1,342 @@
|
||||
{% if is_first_by_category %}
|
||||
##
|
||||
## object types
|
||||
##
|
||||
|
||||
{% endif %}
|
||||
{% if render_imports %}
|
||||
import uuid
|
||||
import pprint
|
||||
from typing import Dict, List, Optional, TypeVar
|
||||
|
||||
from autobahn.wamp.request import Publication, Subscription, Registration
|
||||
|
||||
import flatbuffers
|
||||
from flatbuffers.compat import import_numpy
|
||||
np = import_numpy()
|
||||
|
||||
{% endif %}
|
||||
|
||||
# https://stackoverflow.com/a/46064289/884770
|
||||
T_{{ metadata.classname }} = TypeVar('T_{{ metadata.classname }}', bound='{{ metadata.classname }}')
|
||||
|
||||
|
||||
class {{ metadata.classname }}(object):
|
||||
"""
|
||||
{{ metadata.docs }}
|
||||
"""
|
||||
__slots__ = ['_tab', {% for field_name in metadata.fields_by_id %}'_{{ metadata.fields[field_name].name }}', {% endfor %}]
|
||||
|
||||
def __init__(self):
|
||||
# the underlying FlatBuffers vtable
|
||||
self._tab = None
|
||||
|
||||
{% for field_name in metadata.fields_by_id %}
|
||||
# {{ metadata.fields[field_name]['docs'] }}
|
||||
self._{{ metadata.fields[field_name].name }} = None
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% for field_name in metadata.fields_by_id %}
|
||||
@property
|
||||
def {{ metadata.fields[field_name].name }}(self) -> {{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}:
|
||||
"""
|
||||
{{ metadata.fields[field_name]['docs'] }}
|
||||
"""
|
||||
if self._{{ metadata.fields[field_name].name }} is None and self._tab:
|
||||
o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset({{ metadata.fields[field_name].offset }}))
|
||||
|
||||
{% if metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'str' %}
|
||||
# access type "string" attribute:
|
||||
value = ''
|
||||
if o != 0:
|
||||
_value = self._tab.String(o + self._tab.Pos)
|
||||
if _value is not None:
|
||||
value = _value.decode('utf8')
|
||||
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'bytes' %}
|
||||
# access type "bytes" attribute:
|
||||
value = b''
|
||||
if o != 0:
|
||||
_off = self._tab.Vector(o)
|
||||
_len = self._tab.VectorLen(o)
|
||||
_value = memoryview(self._tab.Bytes)[_off:_off + _len]
|
||||
if _value is not None:
|
||||
value = _value
|
||||
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) in ['int', 'float', 'double'] %}
|
||||
# access type "int|float|double" attribute:
|
||||
value = 0
|
||||
if o != 0:
|
||||
_value = self._tab.Get(flatbuffers.number_types.{{ FbsType.FBS2FLAGS[metadata.fields[field_name].type.basetype] }}, o + self._tab.Pos)
|
||||
if _value is not None:
|
||||
value = _value
|
||||
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'bool' %}
|
||||
# access type "bool" attribute:
|
||||
value = False
|
||||
if o != 0:
|
||||
_value = self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)
|
||||
if _value is not None:
|
||||
value = _value
|
||||
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'uuid.UUID' %}
|
||||
# access type "uuid.UUID" attribute:
|
||||
value = uuid.UUID(bytes=b'\x00' * 16)
|
||||
if o != 0:
|
||||
_off = self._tab.Vector(o)
|
||||
_len = self._tab.VectorLen(o)
|
||||
_value = memoryview(self._tab.Bytes)[_off:_off + _len]
|
||||
if _value is not None:
|
||||
value = uuid.UUID(bytes=bytes(_value))
|
||||
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'np.datetime64' %}
|
||||
# access type "np.datetime64" attribute:
|
||||
value = np.datetime64(0, 'ns')
|
||||
if o != 0:
|
||||
_value = self._tab.Get(flatbuffers.number_types.Uint64Flags, o + self._tab.Pos)
|
||||
if value is not None:
|
||||
value = np.datetime64(_value, 'ns')
|
||||
|
||||
{% elif metadata.fields[field_name].type.basetype == FbsType.Vector %}
|
||||
# access type "Vector" attribute:
|
||||
value = []
|
||||
if o != 0:
|
||||
_start_off = self._tab.Vector(o)
|
||||
_len = self._tab.VectorLen(o)
|
||||
for j in range(_len):
|
||||
_off = _start_off + flatbuffers.number_types.UOffsetTFlags.py_type(j) * 4
|
||||
_off = self._tab.Indirect(_off)
|
||||
{% if metadata.fields[field_name].type.element == FbsType.Obj %}
|
||||
_value = {{ metadata.fields[field_name].type.objtype.split('.')[-1] }}.cast(self._tab.Bytes, _off)
|
||||
{% else %}
|
||||
# FIXME [8]
|
||||
{% endif %}
|
||||
value.append(_value)
|
||||
|
||||
{% elif metadata.fields[field_name].type.basetype == FbsType.Obj %}
|
||||
# access type "Object" attribute:
|
||||
value = {{ metadata.fields[field_name].type.objtype.split('.')[-1] }}()
|
||||
if o != 0:
|
||||
_off = self._tab.Indirect(o + self._tab.Pos)
|
||||
value = {{ metadata.fields[field_name].type.objtype.split('.')[-1] }}.cast(self._tab.Bytes, _off)
|
||||
|
||||
{% else %}
|
||||
# FIXME [5]
|
||||
raise NotImplementedError('implement processing [5] of FlatBuffers type "{}"'.format({{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}))
|
||||
{% endif %}
|
||||
assert value is not None
|
||||
self._{{ metadata.fields[field_name].name }} = value
|
||||
return self._{{ metadata.fields[field_name].name }}
|
||||
|
||||
@{{ metadata.fields[field_name].name }}.setter
|
||||
def {{ metadata.fields[field_name].name }}(self, value: Optional[{{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}]):
|
||||
if value is not None:
|
||||
self._{{ metadata.fields[field_name].name }} = value
|
||||
else:
|
||||
{% if metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'str' %}
|
||||
# set default value on type "string" attribute:
|
||||
self._{{ metadata.fields[field_name].name }} = ''
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'bytes' %}
|
||||
# set default value on type "bytes" attribute:
|
||||
self._{{ metadata.fields[field_name].name }} = b''
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) in ['int', 'float', 'double'] %}
|
||||
# set default value on type "int|float|double" attribute:
|
||||
self._{{ metadata.fields[field_name].name }} = 0
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'bool' %}
|
||||
# set default value on type "bool" attribute:
|
||||
self._{{ metadata.fields[field_name].name }} = False
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'uuid.UUID' %}
|
||||
# set default value on type "uuid.UUID" attribute:
|
||||
self._{{ metadata.fields[field_name].name }} = uuid.UUID(bytes=b'\x00' * 16)
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'np.datetime64' %}
|
||||
# set default value on type "np.datetime64" attribute:
|
||||
self._{{ metadata.fields[field_name].name }} = np.datetime64(0, 'ns')
|
||||
# set default value on type "List" attribute:
|
||||
{% elif metadata.fields[field_name].type.basetype == FbsType.Vector %}
|
||||
self._{{ metadata.fields[field_name].name }} = []
|
||||
# set default value on type "Object" attribute:
|
||||
{% elif metadata.fields[field_name].type.basetype == FbsType.Obj %}
|
||||
self._{{ metadata.fields[field_name].name }} = {{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}()
|
||||
{% else %}
|
||||
# FIXME [6]
|
||||
raise NotImplementedError('implement processing [2] of FlatBuffers type "{}", basetype {}'.format({{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}, {{ metadata.fields[field_name].type.basetype }}))
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
@staticmethod
|
||||
def parse(data: Dict) -> T_{{ metadata.classname }}:
|
||||
"""
|
||||
Parse generic, native language object into a typed, native language object.
|
||||
|
||||
:param data: Generic native language object to parse, e.g. output of ``cbor2.loads``.
|
||||
|
||||
:returns: Typed object of this class.
|
||||
"""
|
||||
for key in data.keys():
|
||||
assert key in {{ metadata.fields_by_id }}
|
||||
obj = {{ metadata.classname }}()
|
||||
{% for field_name in metadata.fields_by_id %}
|
||||
if '{{ field_name }}' in data:
|
||||
{% if metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'str' %}
|
||||
assert (data['{{ field_name }}'] is None or type(data['{{ field_name }}']) == str), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
|
||||
obj.{{ metadata.fields[field_name].name }} = data['{{ field_name }}']
|
||||
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'bytes' %}
|
||||
assert (data['{{ field_name }}'] is None or type(data['{{ field_name }}']) == bytes), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
|
||||
obj.{{ metadata.fields[field_name].name }} = data['{{ field_name }}']
|
||||
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'int' %}
|
||||
assert (data['{{ field_name }}'] is None or type(data['{{ field_name }}']) == int), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
|
||||
obj.{{ metadata.fields[field_name].name }} = data['{{ field_name }}']
|
||||
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'float' %}
|
||||
assert (data['{{ field_name }}'] is None or type(data['{{ field_name }}']) == float), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
|
||||
obj.{{ metadata.fields[field_name].name }} = data['{{ field_name }}']
|
||||
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'bool' %}
|
||||
assert (data['{{ field_name }}'] is None or type(data['{{ field_name }}']) == bool), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
|
||||
obj.{{ metadata.fields[field_name].name }} = data['{{ field_name }}']
|
||||
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'uuid.UUID' %}
|
||||
assert (data['{{ field_name }}'] is None or (type(data['{{ field_name }}']) == bytes and len(data['{{ field_name }}']) == 16)), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
|
||||
if data['{{ field_name }}'] is not None:
|
||||
obj.{{ metadata.fields[field_name].name }} = uuid.UUID(bytes=data['{{ field_name }}'])
|
||||
else:
|
||||
obj.{{ metadata.fields[field_name].name }} = None
|
||||
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'np.datetime64' %}
|
||||
assert (data['{{ field_name }}'] is None or type(data['{{ field_name }}']) == int), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
|
||||
if data['{{ field_name }}'] is not None:
|
||||
obj.{{ metadata.fields[field_name].name }} = np.datetime64(data['{{ field_name }}'], 'ns')
|
||||
else:
|
||||
obj.{{ metadata.fields[field_name].name }} = np.datetime64(0, 'ns')
|
||||
|
||||
{% elif metadata.fields[field_name].type.basetype == FbsType.Vector %}
|
||||
assert (data['{{ field_name }}'] is None or type(data['{{ field_name }}']) == list), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
|
||||
_value = []
|
||||
for v in data['{{ field_name }}']:
|
||||
{% if metadata.fields[field_name].type.element == FbsType.Obj %}
|
||||
_value.append({{ metadata.fields[field_name].type.objtype.split('.')[-1] }}.parse(v))
|
||||
{% else %}
|
||||
_value.append(v)
|
||||
{% endif %}
|
||||
obj.{{ metadata.fields[field_name].name }} = _value
|
||||
|
||||
{% elif metadata.fields[field_name].type.basetype == FbsType.Obj %}
|
||||
assert (data['{{ field_name }}'] is None or type(data['{{ field_name }}']) == dict), '{} has wrong type {}'.format('{{ field_name }}', type(data['{{ field_name }}']))
|
||||
_value = {{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}.parse(data['{{ field_name }}'])
|
||||
obj.{{ metadata.fields[field_name].name }} = _value
|
||||
|
||||
{% else %}
|
||||
# FIXME [3]
|
||||
raise NotImplementedError('implement processing [3] of FlatBuffers type "{}"'.format({{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}))
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
return obj
|
||||
|
||||
def marshal(self) -> Dict:
|
||||
"""
|
||||
Marshal all data contained in this typed native object into a generic object.
|
||||
|
||||
:returns: Generic object that can be serialized to bytes using e.g. ``cbor2.dumps``.
|
||||
"""
|
||||
obj = {
|
||||
{% for field_name in metadata.fields_by_id %}
|
||||
|
||||
{% if metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) in ['str', 'bytes', 'int', 'long', 'float', 'double', 'bool'] %}
|
||||
'{{ metadata.fields[field_name].name }}': self.{{ metadata.fields[field_name].name }},
|
||||
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'uuid.UUID' %}
|
||||
'{{ metadata.fields[field_name].name }}': self.{{ metadata.fields[field_name].name }}.bytes if self.{{ metadata.fields[field_name].name }} is not None else None,
|
||||
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'np.datetime64' %}
|
||||
'{{ metadata.fields[field_name].name }}': int(self.{{ metadata.fields[field_name].name }}) if self.{{ metadata.fields[field_name].name }} is not None else None,
|
||||
|
||||
{% elif metadata.fields[field_name].type.basetype == FbsType.Vector %}
|
||||
{% if metadata.fields[field_name].type.element == FbsType.Obj %}
|
||||
'{{ metadata.fields[field_name].name }}': [o.marshal() for o in self.{{ metadata.fields[field_name].name }}] if self.{{ metadata.fields[field_name].name }} is not None else None,
|
||||
{% else %}
|
||||
'{{ metadata.fields[field_name].name }}': self.{{ metadata.fields[field_name].name }},
|
||||
{% endif %}
|
||||
|
||||
{% elif metadata.fields[field_name].type.basetype == FbsType.Obj %}
|
||||
'{{ metadata.fields[field_name].name }}': self.{{ metadata.fields[field_name].name }}.marshal() if self.{{ metadata.fields[field_name].name }} is not None else None,
|
||||
|
||||
{% else %}
|
||||
# FIXME [4]: implement processing [4] of FlatBuffers type "{{ metadata.fields[field_name].type | string }}" (Python type "{{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}")
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
return obj
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Return string representation of this object, suitable for e.g. logging.
|
||||
|
||||
:returns: String representation of this object.
|
||||
"""
|
||||
return '\n{}\n'.format(pprint.pformat(self.marshal()))
|
||||
|
||||
@staticmethod
|
||||
def cast(buf: bytes, offset: int = 0) -> T_{{ metadata.classname }}:
|
||||
"""
|
||||
Cast a FlatBuffers raw input buffer as a typed object of this class.
|
||||
|
||||
:param buf: The raw input buffer to cast.
|
||||
:param offset: Offset into raw buffer from which to cast flatbuffers from.
|
||||
|
||||
:returns: New native object that wraps the FlatBuffers raw buffer.
|
||||
"""
|
||||
n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
|
||||
x = {{ metadata.classname }}()
|
||||
x._tab = flatbuffers.table.Table(buf, n + offset)
|
||||
return x
|
||||
|
||||
def build(self, builder):
|
||||
"""
|
||||
Build a FlatBuffers raw output buffer from this typed object.
|
||||
|
||||
:returns: Constructs the FlatBuffers using the builder and
|
||||
returns ``builder.EndObject()``.
|
||||
"""
|
||||
# first, write all string|bytes|etc typed attribute values (in order) to the buffer
|
||||
{% for field_name in metadata.fields_by_id %}
|
||||
{% if metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) in ['str', 'bytes'] %}
|
||||
_{{ field_name }} = self.{{ field_name }}
|
||||
if _{{ field_name }}:
|
||||
_{{ field_name }} = builder.CreateString(_{{ field_name }})
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'uuid.UUID' %}
|
||||
_{{ field_name }} = self.{{ field_name }}.bytes if self.{{ field_name }} else None
|
||||
if _{{ field_name }}:
|
||||
_{{ field_name }} = builder.CreateString(_{{ field_name }})
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
# now start a new object in the buffer and write the actual object attributes (in field
|
||||
# order) to the buffer
|
||||
builder.StartObject({{ metadata.fields_by_id|length }})
|
||||
|
||||
{% for field_name in metadata.fields_by_id %}
|
||||
|
||||
{% if metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) in ['str', 'bytes', 'uuid.UUID'] %}
|
||||
if _{{ field_name }}:
|
||||
builder.PrependUOffsetTRelativeSlot({{ metadata.fields[field_name].id }}, flatbuffers.number_types.UOffsetTFlags.py_type(_{{ field_name }}), 0)
|
||||
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) == 'np.datetime64' %}
|
||||
if self.{{ field_name }}:
|
||||
builder.PrependUint64Slot({{ metadata.fields[field_name].id }}, int(self.{{ field_name }}), 0)
|
||||
|
||||
{% elif metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) in ['bool', 'int', 'float'] %}
|
||||
if self.{{ field_name }}:
|
||||
builder.{{ FbsType.FBS2PREPEND[metadata.fields[field_name].type.basetype] }}({{ metadata.fields[field_name].id }}, self.{{ field_name }}, 0)
|
||||
|
||||
{% else %}
|
||||
# FIXME [1]
|
||||
# raise NotImplementedError('implement builder [1] for type "{}"'.format({{ metadata.fields[field_name].type.map('python', metadata.fields[field_name].attrs, True) }}))
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
return builder.EndObject()
|
||||
@@ -0,0 +1,25 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) typedef int 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.
|
||||
#
|
||||
###############################################################################
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
[default]
|
||||
|
||||
url=ws://localhost:9000/ws
|
||||
privkey=default.priv
|
||||
pubkey=default.pub
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user