mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 03:31:09 -05:00
okay fine
This commit is contained in:
450
.venv/lib/python3.12/site-packages/autobahn/xbr/__init__.py
Normal file
450
.venv/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',)
|
||||
156
.venv/lib/python3.12/site-packages/autobahn/xbr/_abi.py
Normal file
156
.venv/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
.venv/lib/python3.12/site-packages/autobahn/xbr/_blockchain.py
Normal file
238
.venv/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
.venv/lib/python3.12/site-packages/autobahn/xbr/_buyer.py
Normal file
636
.venv/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
.venv/lib/python3.12/site-packages/autobahn/xbr/_cli.py
Normal file
1106
.venv/lib/python3.12/site-packages/autobahn/xbr/_cli.py
Normal file
File diff suppressed because it is too large
Load Diff
588
.venv/lib/python3.12/site-packages/autobahn/xbr/_config.py
Normal file
588
.venv/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
.venv/lib/python3.12/site-packages/autobahn/xbr/_dialog.py
Normal file
87
.venv/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
|
||||
164
.venv/lib/python3.12/site-packages/autobahn/xbr/_eip712_base.py
Normal file
164
.venv/lib/python3.12/site-packages/autobahn/xbr/_eip712_base.py
Normal file
@@ -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
.venv/lib/python3.12/site-packages/autobahn/xbr/_frealm.py
Normal file
471
.venv/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
.venv/lib/python3.12/site-packages/autobahn/xbr/_gui.py
Normal file
1058
.venv/lib/python3.12/site-packages/autobahn/xbr/_gui.py
Normal file
File diff suppressed because it is too large
Load Diff
188
.venv/lib/python3.12/site-packages/autobahn/xbr/_interfaces.py
Normal file
188
.venv/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
.venv/lib/python3.12/site-packages/autobahn/xbr/_mnemonic.py
Normal file
167
.venv/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
.venv/lib/python3.12/site-packages/autobahn/xbr/_schema.py
Normal file
2113
.venv/lib/python3.12/site-packages/autobahn/xbr/_schema.py
Normal file
File diff suppressed because it is too large
Load Diff
567
.venv/lib/python3.12/site-packages/autobahn/xbr/_secmod.py
Normal file
567
.venv/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
.venv/lib/python3.12/site-packages/autobahn/xbr/_seller.py
Normal file
726
.venv/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
.venv/lib/python3.12/site-packages/autobahn/xbr/_userkey.py
Normal file
244
.venv/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
.venv/lib/python3.12/site-packages/autobahn/xbr/_util.py
Normal file
175
.venv/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
.venv/lib/python3.12/site-packages/autobahn/xbr/_wallet.py
Normal file
111
.venv/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.
@@ -0,0 +1,5 @@
|
||||
[default]
|
||||
|
||||
url=ws://localhost:9000/ws
|
||||
privkey=default.priv
|
||||
pubkey=default.pub
|
||||
@@ -0,0 +1,9 @@
|
||||
Crossbar.io user private key - KEEP THIS SAFE!
|
||||
|
||||
creator: oberstet@intel-nuci7
|
||||
created-at: 2022-05-06T14:40:09.639Z
|
||||
user-id: oberstet@intel-nuci7
|
||||
public-key-ed25519: 15cfa4acef5cc312e0b9ba77634849d0a8c6222a546f90eb5123667935d2f561
|
||||
public-adr-eth: 0xe59C7418403CF1D973485B36660728a5f4A8fF9c
|
||||
private-key-ed25519: 20e8c05d0ede9506462bb049c4843032b18e8e75b314583d0c8d8a4942f9be40
|
||||
private-key-eth: 6b08b6e186bd2a3b9b2f36e6ece3f8031fe788ab3dc4a1cfd3a489ea387c496b
|
||||
@@ -0,0 +1,7 @@
|
||||
Crossbar.io user public key
|
||||
|
||||
creator: oberstet@intel-nuci7
|
||||
created-at: 2022-05-06T14:40:09.639Z
|
||||
user-id: oberstet@intel-nuci7
|
||||
public-key-ed25519: 15cfa4acef5cc312e0b9ba77634849d0a8c6222a546f90eb5123667935d2f561
|
||||
public-adr-eth: 0xe59C7418403CF1D973485B36660728a5f4A8fF9c
|
||||
@@ -0,0 +1,70 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 unittest
|
||||
from binascii import a2b_hex
|
||||
|
||||
from autobahn.xbr import HAS_XBR
|
||||
|
||||
TESTVECTORS = [
|
||||
{
|
||||
'email': 'foobar@example.com',
|
||||
'password': 'secret123',
|
||||
'salt': None,
|
||||
'pkm': '5fa91eecfc1414db0db3cca9bfec64af495bdfa0bc8c135a8c17b6b5e3686cab',
|
||||
'contexts': {
|
||||
'wamp-cryptosign': '5c55e767c927e17e2a03a23ab46b516e41fb9e40377d2617104dd5622a9209cf',
|
||||
}
|
||||
},
|
||||
{
|
||||
'email': 'foobar@example.com',
|
||||
'password': 'secret123',
|
||||
'salt': a2b_hex('3761e806cda3c35d859c933d46e5d57b'),
|
||||
'pkm': 'c49556ca4c39dbfe147187b03b1dfcff7026d748cb27738a849a1cd5bfcf4bed',
|
||||
'contexts': {
|
||||
'wamp-cryptosign': '807af48521b1ecf4a7045814d75339159ecb157c1bffb00461edfebbaffcda71',
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
if HAS_XBR:
|
||||
from autobahn.xbr import stretch_argon2_secret, pkm_from_argon2_secret
|
||||
|
||||
class TestXbrArgon2(unittest.TestCase):
|
||||
|
||||
def test_stretch_argon2_secret(self):
|
||||
for tv in TESTVECTORS:
|
||||
email, password, salt = tv['email'], tv['password'], tv['salt']
|
||||
pkm = stretch_argon2_secret(email, password, salt=salt)
|
||||
self.assertEqual(pkm, a2b_hex(tv['pkm']))
|
||||
|
||||
def test_pkm_from_argon2_secret(self):
|
||||
for tv in TESTVECTORS:
|
||||
email, password, salt = tv['email'], tv['password'], tv['salt']
|
||||
for context, expected_priv_key in tv['contexts'].items():
|
||||
expected_priv_key = a2b_hex(expected_priv_key)
|
||||
priv_key = pkm_from_argon2_secret(email=email, password=password, context=context, salt=salt)
|
||||
self.assertEqual(priv_key, expected_priv_key)
|
||||
@@ -0,0 +1,78 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 unittest
|
||||
|
||||
from autobahn.xbr import HAS_XBR
|
||||
|
||||
if HAS_XBR:
|
||||
from autobahn.xbr import Profile, UserConfig
|
||||
|
||||
class TestXbrUserConfig(unittest.TestCase):
|
||||
|
||||
DOTDIR = '~/.xbrnetwork'
|
||||
PROFILE_NAME = 'default'
|
||||
NETWORK_URL = 'wss://planet.xbr.network/ws'
|
||||
NETWORK_REALM = 'xbrnetwork'
|
||||
PASSWORD = 'secret123'
|
||||
|
||||
def test_create_empty_config(self):
|
||||
c = UserConfig('config.ini')
|
||||
self.assertEqual(c.profiles, {})
|
||||
|
||||
def test_create_empty_profile(self):
|
||||
p = Profile()
|
||||
self.assertTrue(p.path is None)
|
||||
|
||||
def test_load_home(self):
|
||||
config_dir = os.path.expanduser(self.DOTDIR)
|
||||
if not os.path.isdir(config_dir):
|
||||
os.mkdir(config_dir)
|
||||
config_path = os.path.join(config_dir, 'config.ini')
|
||||
if os.path.exists(config_path):
|
||||
c = UserConfig(config_path)
|
||||
c.load()
|
||||
self.assertIn(self.PROFILE_NAME, c.profiles)
|
||||
|
||||
def test_write_default_config(self):
|
||||
config_dir = os.path.expanduser(self.DOTDIR)
|
||||
if not os.path.isdir(config_dir):
|
||||
os.mkdir(config_dir)
|
||||
config_path = os.path.join(config_dir, 'test.ini')
|
||||
|
||||
c = UserConfig(config_path)
|
||||
p = Profile()
|
||||
c.profiles[self.PROFILE_NAME] = p
|
||||
c.save(self.PASSWORD)
|
||||
|
||||
c2 = UserConfig(config_path)
|
||||
|
||||
def get_pw():
|
||||
return self.PASSWORD
|
||||
|
||||
c2.load(cb_get_password=get_pw)
|
||||
self.assertIn(self.PROFILE_NAME, c2.profiles)
|
||||
@@ -0,0 +1,511 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 sys
|
||||
import tempfile
|
||||
from binascii import a2b_hex, b2a_hex
|
||||
from unittest import skipIf
|
||||
|
||||
from twisted.internet.defer import inlineCallbacks
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
from autobahn.wamp.cryptosign import HAS_CRYPTOSIGN
|
||||
from autobahn.xbr import HAS_XBR
|
||||
|
||||
if HAS_XBR and HAS_CRYPTOSIGN:
|
||||
from autobahn.wamp.cryptosign import CryptosignKey
|
||||
from autobahn.xbr import make_w3, EthereumKey
|
||||
from autobahn.xbr._secmod import SecurityModuleMemory
|
||||
from autobahn.xbr import create_eip712_delegate_certificate, create_eip712_authority_certificate
|
||||
from autobahn.xbr._eip712_delegate_certificate import EIP712DelegateCertificate
|
||||
from autobahn.xbr._eip712_authority_certificate import EIP712AuthorityCertificate
|
||||
from autobahn.xbr._eip712_certificate_chain import parse_certificate_chain
|
||||
|
||||
# https://web3py.readthedocs.io/en/stable/providers.html#infura-mainnet
|
||||
HAS_INFURA = 'WEB3_INFURA_PROJECT_ID' in os.environ and len(os.environ['WEB3_INFURA_PROJECT_ID']) > 0
|
||||
|
||||
# TypeError: As of 3.10, the *loop* parameter was removed from Lock() since it is no longer necessary
|
||||
IS_CPY_310 = sys.version_info.minor == 10
|
||||
|
||||
|
||||
@skipIf(not os.environ.get('USE_TWISTED', False), 'only for Twisted')
|
||||
@skipIf(not HAS_INFURA, 'env var WEB3_INFURA_PROJECT_ID not defined')
|
||||
@skipIf(not (HAS_XBR and HAS_CRYPTOSIGN), 'package autobahn[encryption,xbr] not installed')
|
||||
class TestEip712Certificate(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._gw_config = {
|
||||
'type': 'infura',
|
||||
'key': os.environ.get('WEB3_INFURA_PROJECT_ID', ''),
|
||||
'network': 'mainnet',
|
||||
}
|
||||
self._w3 = make_w3(self._gw_config)
|
||||
|
||||
self._seedphrase = "avocado style uncover thrive same grace crunch want essay reduce current edge"
|
||||
self._sm: SecurityModuleMemory = SecurityModuleMemory.from_seedphrase(self._seedphrase, num_eth_keys=5,
|
||||
num_cs_keys=5)
|
||||
|
||||
@inlineCallbacks
|
||||
def test_eip712_delegate_certificate(self):
|
||||
yield self._sm.open()
|
||||
|
||||
delegate_eth_key: EthereumKey = self._sm[1]
|
||||
delegate_cs_key: CryptosignKey = self._sm[6]
|
||||
|
||||
chainId = 1
|
||||
verifyingContract = a2b_hex('0xf766Dc789CF04CD18aE75af2c5fAf2DA6650Ff57'[2:])
|
||||
validFrom = 15124128
|
||||
delegate = delegate_eth_key.address(binary=True)
|
||||
csPubKey = delegate_cs_key.public_key(binary=True)
|
||||
bootedAt = 1657579546469365046 # txaio.time_ns()
|
||||
meta = 'Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu'
|
||||
|
||||
cert_data = create_eip712_delegate_certificate(chainId=chainId, verifyingContract=verifyingContract,
|
||||
validFrom=validFrom, delegate=delegate, csPubKey=csPubKey,
|
||||
bootedAt=bootedAt, meta=meta)
|
||||
|
||||
# print('\n\n{}\n\n'.format(pformat(cert_data)))
|
||||
|
||||
cert_sig = yield delegate_eth_key.sign_typed_data(cert_data, binary=False)
|
||||
|
||||
self.assertEqual(cert_sig,
|
||||
'2bd697b2bdb9bc2c2494e53e9440ddb3e8a596eedaad717f8ecdb732d091a7de48d72d9a26d7e092ec55c074979ab039f8e003acf80224819ff396c9529eb1d11b')
|
||||
|
||||
yield self._sm.close()
|
||||
|
||||
@inlineCallbacks
|
||||
def test_eip712_authority_certificate(self):
|
||||
yield self._sm.open()
|
||||
|
||||
trustroot_eth_key: EthereumKey = self._sm[0]
|
||||
delegate_eth_key: EthereumKey = self._sm[1]
|
||||
|
||||
chainId = 1
|
||||
verifyingContract = a2b_hex('0xf766Dc789CF04CD18aE75af2c5fAf2DA6650Ff57'[2:])
|
||||
validFrom = 15124128
|
||||
issuer = trustroot_eth_key.address(binary=True)
|
||||
subject = delegate_eth_key.address(binary=True)
|
||||
realm = a2b_hex('0xA6e693CC4A2b4F1400391a728D26369D9b82ef96'[2:])
|
||||
capabilities = 3
|
||||
meta = 'Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu'
|
||||
|
||||
cert_data = create_eip712_authority_certificate(chainId=chainId, verifyingContract=verifyingContract,
|
||||
validFrom=validFrom, issuer=issuer, subject=subject,
|
||||
realm=realm, capabilities=capabilities, meta=meta)
|
||||
|
||||
# print('\n\n{}\n\n'.format(pformat(cert_data)))
|
||||
|
||||
cert_sig = yield trustroot_eth_key.sign_typed_data(cert_data, binary=False)
|
||||
|
||||
self.assertEqual(cert_sig,
|
||||
'83590d4304cc5f6024d6a85ed2c511a60e804d609e4f498c8af777d5102c6d22657673e7b68876795e3c72f857b68e13cf616ee4c2ea559bceb344021bf977b61c')
|
||||
|
||||
yield self._sm.close()
|
||||
|
||||
|
||||
@skipIf(not os.environ.get('USE_TWISTED', False), 'only for Twisted')
|
||||
@skipIf(not HAS_INFURA, 'env var WEB3_INFURA_PROJECT_ID not defined')
|
||||
@skipIf(not (HAS_XBR and HAS_CRYPTOSIGN), 'package autobahn[encryption,xbr] not installed')
|
||||
class TestEip712CertificateChain(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._gw_config = {
|
||||
'type': 'infura',
|
||||
'key': os.environ.get('WEB3_INFURA_PROJECT_ID', ''),
|
||||
'network': 'mainnet',
|
||||
}
|
||||
self._w3 = make_w3(self._gw_config)
|
||||
|
||||
self._seedphrase = "avocado style uncover thrive same grace crunch want essay reduce current edge"
|
||||
self._sm: SecurityModuleMemory = SecurityModuleMemory.from_seedphrase(self._seedphrase, num_eth_keys=5,
|
||||
num_cs_keys=5)
|
||||
|
||||
# HELLO.Details.authextra.certificates
|
||||
#
|
||||
self._certs_expected1 = [(None,
|
||||
{'domain': {'name': 'WMP', 'version': '1'},
|
||||
'message': {'bootedAt': 1657781999086394759,
|
||||
'chainId': 1,
|
||||
'csPubKey': '12ae0184b180e9a9c5e45be4a1afbce3c6491320063701cd9c4011a777d04089',
|
||||
'delegate': '0xf5173a6111B2A6B3C20fceD53B2A8405EC142bF6',
|
||||
'meta': 'Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu',
|
||||
'validFrom': 15139218,
|
||||
'verifyingContract': '0xf766Dc789CF04CD18aE75af2c5fAf2DA6650Ff57'},
|
||||
'primaryType': 'EIP712DelegateCertificate',
|
||||
'types': {'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'}],
|
||||
'EIP712Domain': [{'name': 'name', 'type': 'string'},
|
||||
{'name': 'version', 'type': 'string'}]}},
|
||||
'70726dda677cac8f21366f8023d17203b2f4f9099e954f9bebb2134086e2ac291d80ce038a1342a7748d4b0750f06b8de491561d581c90c99f1c09c91cfa7e191c'),
|
||||
(None,
|
||||
{'domain': {'name': 'WMP', 'version': '1'},
|
||||
'message': {'capabilities': 12,
|
||||
'chainId': 1,
|
||||
'issuer': '0xf766Dc789CF04CD18aE75af2c5fAf2DA6650Ff57',
|
||||
'meta': 'QmNbMM6TMLAgqBKzY69mJKk5VKvpcTtAtwAaLC2FV4zC3G',
|
||||
'realm': '0xA6e693CC4A2b4F1400391a728D26369D9b82ef96',
|
||||
'subject': '0xf5173a6111B2A6B3C20fceD53B2A8405EC142bF6',
|
||||
'validFrom': 15139218,
|
||||
'verifyingContract': '0xf766Dc789CF04CD18aE75af2c5fAf2DA6650Ff57'},
|
||||
'primaryType': 'EIP712AuthorityCertificate',
|
||||
'types': {'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'}],
|
||||
'EIP712Domain': [{'name': 'name', 'type': 'string'},
|
||||
{'name': 'version', 'type': 'string'}]}},
|
||||
'f031b2625ae7e32e7eec3a8fa09f4db3a43217f282b7695e5b09dd2e13c25dc679c1f3ce27b94a3074786f7f12183a2a275a00aea5a66b83c431281f1069bd841c'),
|
||||
(None,
|
||||
{'domain': {'name': 'WMP', 'version': '1'},
|
||||
'message': {'capabilities': 63,
|
||||
'chainId': 1,
|
||||
'issuer': '0xf766Dc789CF04CD18aE75af2c5fAf2DA6650Ff57',
|
||||
'meta': 'QmNbMM6TMLAgqBKzY69mJKk5VKvpcTtAtwAaLC2FV4zC3G',
|
||||
'realm': '0xA6e693CC4A2b4F1400391a728D26369D9b82ef96',
|
||||
'subject': '0xf766Dc789CF04CD18aE75af2c5fAf2DA6650Ff57',
|
||||
'validFrom': 15139218,
|
||||
'verifyingContract': '0xf766Dc789CF04CD18aE75af2c5fAf2DA6650Ff57'},
|
||||
'primaryType': 'EIP712AuthorityCertificate',
|
||||
'types': {'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'}],
|
||||
'EIP712Domain': [{'name': 'name', 'type': 'string'},
|
||||
{'name': 'version', 'type': 'string'}]}},
|
||||
'c3bcd7a3c3c45ae45a24cd7745db3b39c4113e6b71a4220f943f0969282246b4083ef61277bd7ba9e92c9a07b79869ce63bc6206986480f9c5daddb27b91bebe1b')]
|
||||
|
||||
@skipIf(True, 'FIXME: builtins.TypeError: to_checksum_address() takes 1 positional argument but 2 were given')
|
||||
@inlineCallbacks
|
||||
def test_eip712_create_certificate_chain_manual(self):
|
||||
yield self._sm.open()
|
||||
|
||||
# keys needed to create all certificates in certificate chain
|
||||
#
|
||||
trustroot_eth_key: EthereumKey = self._sm[0]
|
||||
delegate_eth_key: EthereumKey = self._sm[1]
|
||||
delegate_cs_key: CryptosignKey = self._sm[6]
|
||||
|
||||
# data needed for delegate certificate: cert1
|
||||
#
|
||||
chainId = 1 # self._w3.eth.chain_id
|
||||
verifyingContract = a2b_hex('0xf766Dc789CF04CD18aE75af2c5fAf2DA6650Ff57'[2:])
|
||||
validFrom = 15139218 # self._w3.eth.block_number
|
||||
delegate = delegate_eth_key.address(binary=True)
|
||||
csPubKey = delegate_cs_key.public_key(binary=True)
|
||||
bootedAt = 1657781999086394759 # txaio.time_ns()
|
||||
delegateMeta = 'Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu'
|
||||
|
||||
# data needed for intermediate authority certificate: cert2
|
||||
#
|
||||
issuer_cert2 = trustroot_eth_key.address(binary=True)
|
||||
subject_cert2 = delegate
|
||||
realm_cert2 = a2b_hex('0xA6e693CC4A2b4F1400391a728D26369D9b82ef96'[2:])
|
||||
capabilities_cert2 = EIP712AuthorityCertificate.CAPABILITY_PUBLIC_RELAY | EIP712AuthorityCertificate.CAPABILITY_PRIVATE_RELAY
|
||||
meta_cert2 = 'QmNbMM6TMLAgqBKzY69mJKk5VKvpcTtAtwAaLC2FV4zC3G'
|
||||
|
||||
# data needed for root authority certificate: cert3
|
||||
#
|
||||
issuer_cert3 = trustroot_eth_key.address(binary=True)
|
||||
subject_cert3 = issuer_cert3
|
||||
realm_cert3 = a2b_hex('0xA6e693CC4A2b4F1400391a728D26369D9b82ef96'[2:])
|
||||
capabilities_cert3 = EIP712AuthorityCertificate.CAPABILITY_ROOT_CA | EIP712AuthorityCertificate.CAPABILITY_INTERMEDIATE_CA | EIP712AuthorityCertificate.CAPABILITY_PUBLIC_RELAY | EIP712AuthorityCertificate.CAPABILITY_PRIVATE_RELAY | EIP712AuthorityCertificate.CAPABILITY_PROVIDER | EIP712AuthorityCertificate.CAPABILITY_CONSUMER
|
||||
meta_cert3 = 'QmNbMM6TMLAgqBKzY69mJKk5VKvpcTtAtwAaLC2FV4zC3G'
|
||||
|
||||
# FIXME: builtins.TypeError: to_checksum_address() takes 1 positional argument but 2 were given
|
||||
|
||||
# create delegate certificate
|
||||
#
|
||||
cert1_data = create_eip712_delegate_certificate(chainId=chainId, verifyingContract=verifyingContract,
|
||||
validFrom=validFrom, delegate=delegate, csPubKey=csPubKey,
|
||||
bootedAt=bootedAt, meta=delegateMeta)
|
||||
|
||||
cert1_sig = yield delegate_eth_key.sign_typed_data(cert1_data, binary=False)
|
||||
|
||||
cert1_data['message']['csPubKey'] = b2a_hex(cert1_data['message']['csPubKey']).decode()
|
||||
cert1_data['message']['delegate'] = self._w3.toChecksumAddress(cert1_data['message']['delegate'])
|
||||
cert1_data['message']['verifyingContract'] = self._w3.toChecksumAddress(
|
||||
cert1_data['message']['verifyingContract'])
|
||||
|
||||
# create intermediate authority certificate
|
||||
#
|
||||
cert2_data = create_eip712_authority_certificate(chainId=chainId, verifyingContract=verifyingContract,
|
||||
validFrom=validFrom, issuer=issuer_cert2,
|
||||
subject=subject_cert2,
|
||||
realm=realm_cert2, capabilities=capabilities_cert2,
|
||||
meta=meta_cert2)
|
||||
|
||||
cert2_sig = yield trustroot_eth_key.sign_typed_data(cert2_data, binary=False)
|
||||
|
||||
cert2_data['message']['verifyingContract'] = self._w3.toChecksumAddress(
|
||||
cert2_data['message']['verifyingContract'])
|
||||
cert2_data['message']['issuer'] = self._w3.toChecksumAddress(cert2_data['message']['issuer'])
|
||||
cert2_data['message']['subject'] = self._w3.toChecksumAddress(cert2_data['message']['subject'])
|
||||
cert2_data['message']['realm'] = self._w3.toChecksumAddress(cert2_data['message']['realm'])
|
||||
|
||||
# create root authority certificate
|
||||
#
|
||||
cert3_data = create_eip712_authority_certificate(chainId=chainId, verifyingContract=verifyingContract,
|
||||
validFrom=validFrom, issuer=issuer_cert3,
|
||||
subject=subject_cert3,
|
||||
realm=realm_cert3, capabilities=capabilities_cert3,
|
||||
meta=meta_cert3)
|
||||
|
||||
cert3_sig = yield trustroot_eth_key.sign_typed_data(cert3_data, binary=False)
|
||||
|
||||
cert3_data['message']['verifyingContract'] = self._w3.toChecksumAddress(
|
||||
cert3_data['message']['verifyingContract'])
|
||||
cert3_data['message']['issuer'] = self._w3.toChecksumAddress(cert3_data['message']['issuer'])
|
||||
cert3_data['message']['subject'] = self._w3.toChecksumAddress(cert3_data['message']['subject'])
|
||||
cert3_data['message']['realm'] = self._w3.toChecksumAddress(cert3_data['message']['realm'])
|
||||
|
||||
# create certificates chain
|
||||
#
|
||||
certificates = [(None, cert1_data, cert1_sig), (None, cert2_data, cert2_sig), (None, cert3_data, cert3_sig)]
|
||||
|
||||
if False:
|
||||
from pprint import pprint
|
||||
print()
|
||||
pprint(certificates)
|
||||
print()
|
||||
|
||||
# check certificates and certificate signatures of whole chain
|
||||
#
|
||||
self.assertEqual(certificates, self._certs_expected1)
|
||||
|
||||
yield self._sm.close()
|
||||
|
||||
@inlineCallbacks
|
||||
def test_eip712_create_certificate_chain_highlevel(self):
|
||||
yield self._sm.open()
|
||||
|
||||
# keys needed to create all certificates in certificate chain
|
||||
ca_key: EthereumKey = self._sm[0]
|
||||
|
||||
# data needed for root authority certificate: cert3
|
||||
ca_cert_chainId = 1
|
||||
ca_cert_verifyingContract = a2b_hex('0xf766Dc789CF04CD18aE75af2c5fAf2DA6650Ff57'[2:])
|
||||
ca_cert_validFrom = 666666
|
||||
ca_cert_issuer = ca_key.address(binary=True)
|
||||
ca_cert_subject = ca_cert_issuer
|
||||
ca_cert_realm = a2b_hex('0xA6e693CC4A2b4F1400391a728D26369D9b82ef96'[2:])
|
||||
ca_cert_capabilities = EIP712AuthorityCertificate.CAPABILITY_ROOT_CA | EIP712AuthorityCertificate.CAPABILITY_INTERMEDIATE_CA | EIP712AuthorityCertificate.CAPABILITY_PUBLIC_RELAY | EIP712AuthorityCertificate.CAPABILITY_PRIVATE_RELAY | EIP712AuthorityCertificate.CAPABILITY_PROVIDER | EIP712AuthorityCertificate.CAPABILITY_CONSUMER
|
||||
ca_cert_meta = ''
|
||||
|
||||
# create root authority certificate signature: directly from provided data attributes
|
||||
ca_cert_data = create_eip712_authority_certificate(chainId=ca_cert_chainId,
|
||||
verifyingContract=ca_cert_verifyingContract,
|
||||
validFrom=ca_cert_validFrom, issuer=ca_cert_issuer,
|
||||
subject=ca_cert_subject, realm=ca_cert_realm,
|
||||
capabilities=ca_cert_capabilities, meta=ca_cert_meta)
|
||||
ca_cert_sig = yield ca_key.sign_typed_data(ca_cert_data, binary=False)
|
||||
|
||||
# create root authority certificate signature: from certificate object
|
||||
ca_cert = EIP712AuthorityCertificate(chainId=ca_cert_chainId,
|
||||
verifyingContract=ca_cert_verifyingContract,
|
||||
validFrom=ca_cert_validFrom,
|
||||
issuer=ca_cert_issuer,
|
||||
subject=ca_cert_subject,
|
||||
realm=ca_cert_realm,
|
||||
capabilities=ca_cert_capabilities,
|
||||
meta=ca_cert_meta)
|
||||
ca_cert_sig2 = yield ca_cert.sign(ca_key)
|
||||
|
||||
# re-create root authority certificate from round-tripping (marshal-parse)
|
||||
ca_cert2 = EIP712AuthorityCertificate.parse(ca_cert.marshal())
|
||||
ca_cert_sig3 = yield ca_cert2.sign(ca_key)
|
||||
|
||||
# all different ways to compute signature must result in same signature value
|
||||
self.assertEqual(ca_cert_sig, ca_cert_sig2)
|
||||
self.assertEqual(ca_cert_sig, ca_cert_sig3)
|
||||
|
||||
# and match this signature value
|
||||
self.assertEqual(ca_cert_sig, 'd9e679753e1120a8ba8edea4895d2e056ba98eaa1acbe11bf6210f3a48a56de830aa6a566cc4920'
|
||||
'c74a284ffcd9f7d1af5fe229268a44030522db19d5a75f4131c')
|
||||
|
||||
# test save/load instance to/from file
|
||||
with tempfile.NamedTemporaryFile() as fd:
|
||||
# save certificate to file
|
||||
ca_cert.save(fd.name)
|
||||
|
||||
# load certificate from file
|
||||
ca_cert3 = EIP712AuthorityCertificate.load(fd.name)
|
||||
|
||||
# ensure it produces the same signature
|
||||
ca_cert_sig4 = yield ca_cert3.sign(ca_key)
|
||||
self.assertEqual(ca_cert_sig, ca_cert_sig4)
|
||||
|
||||
yield self._sm.close()
|
||||
|
||||
@inlineCallbacks
|
||||
def test_eip712_verify_certificate_chain_manual(self):
|
||||
yield self._sm.open()
|
||||
|
||||
# keys originally used to sign the certificates in the certificate chain
|
||||
trustroot_eth_key: EthereumKey = self._sm[0]
|
||||
delegate_eth_key: EthereumKey = self._sm[1]
|
||||
delegate_cs_key: CryptosignKey = self._sm[6]
|
||||
|
||||
# parse the whole certificate chain
|
||||
cert_chain = []
|
||||
cert_sigs = []
|
||||
for cert_hash, cert_data, cert_sig in self._certs_expected1:
|
||||
self.assertIn('domain', cert_data)
|
||||
self.assertIn('message', cert_data)
|
||||
self.assertIn('primaryType', cert_data)
|
||||
self.assertIn('types', cert_data)
|
||||
self.assertIn(cert_data['primaryType'], cert_data['types'])
|
||||
self.assertIn(cert_data['primaryType'], ['EIP712DelegateCertificate', 'EIP712AuthorityCertificate'])
|
||||
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)
|
||||
cert_sigs.append(cert_sig)
|
||||
|
||||
# FIXME: allow length 2 and length > 3
|
||||
self.assertEqual(len(cert_chain), 3)
|
||||
self.assertEqual(cert_chain[0].delegate, delegate_eth_key.address(binary=True))
|
||||
self.assertEqual(cert_chain[0].csPubKey, delegate_cs_key.public_key(binary=True))
|
||||
self.assertEqual(cert_chain[1].issuer, trustroot_eth_key.address(binary=True))
|
||||
self.assertEqual(cert_chain[2].issuer, trustroot_eth_key.address(binary=True))
|
||||
|
||||
# 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-3
|
||||
self.assertIsInstance(cert_chain[0], EIP712DelegateCertificate)
|
||||
for i in [1, len(cert_chain) - 1]:
|
||||
self.assertIsInstance(cert_chain[i], EIP712AuthorityCertificate)
|
||||
|
||||
# CCR-1
|
||||
chainId = cert_chain[2].chainId
|
||||
verifyingContract = cert_chain[2].verifyingContract
|
||||
for cert in cert_chain:
|
||||
self.assertEqual(cert.chainId, chainId)
|
||||
self.assertEqual(cert.verifyingContract, verifyingContract)
|
||||
|
||||
# CCR-2
|
||||
realm = cert_chain[2].realm
|
||||
for cert in cert_chain[1:]:
|
||||
self.assertEqual(cert.realm, realm)
|
||||
|
||||
# CCR-4
|
||||
self.assertEqual(cert_chain[2].subject, cert_chain[2].issuer)
|
||||
|
||||
# CCR-5
|
||||
self.assertEqual(cert_chain[1].issuer, cert_chain[2].subject)
|
||||
|
||||
# CCR-6
|
||||
self.assertLessEqual(cert_chain[2].validFrom, cert_chain[1].validFrom)
|
||||
|
||||
# CCR-7
|
||||
self.assertTrue(cert_chain[2].capabilities == cert_chain[2].capabilities | cert_chain[1].capabilities)
|
||||
|
||||
# CCR-8
|
||||
self.assertEqual(cert_chain[1].subject, cert_chain[0].delegate)
|
||||
|
||||
# CCR-9
|
||||
self.assertLessEqual(cert_chain[1].validFrom, cert_chain[0].validFrom)
|
||||
|
||||
# CCR-10
|
||||
_issuer = cert_chain[2].recover(a2b_hex(cert_sigs[2]))
|
||||
self.assertEqual(_issuer, trustroot_eth_key.address(binary=True))
|
||||
|
||||
# CCR-11
|
||||
_issuer = cert_chain[1].recover(a2b_hex(cert_sigs[1]))
|
||||
self.assertEqual(_issuer, trustroot_eth_key.address(binary=True))
|
||||
|
||||
# CCR-12
|
||||
_issuer = cert_chain[0].recover(a2b_hex(cert_sigs[0]))
|
||||
self.assertEqual(_issuer, delegate_eth_key.address(binary=True))
|
||||
|
||||
yield self._sm.close()
|
||||
|
||||
@inlineCallbacks
|
||||
def test_eip712_verify_certificate_chain_highlevel(self):
|
||||
yield self._sm.open()
|
||||
|
||||
# keys originally used to sign the certificates in the certificate chain
|
||||
trustroot_eth_key: EthereumKey = self._sm[0]
|
||||
delegate_eth_key: EthereumKey = self._sm[1]
|
||||
delegate_cs_key: CryptosignKey = self._sm[6]
|
||||
|
||||
certificates = parse_certificate_chain(self._certs_expected1)
|
||||
|
||||
self.assertEqual(certificates[2].issuer, trustroot_eth_key.address(binary=True))
|
||||
|
||||
self.assertEqual(certificates[0].delegate, delegate_eth_key.address(binary=True))
|
||||
self.assertEqual(certificates[0].csPubKey, delegate_cs_key.public_key(binary=True))
|
||||
|
||||
yield self._sm.close()
|
||||
@@ -0,0 +1,183 @@
|
||||
import os
|
||||
import sys
|
||||
from unittest import skipIf
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.internet.defer import inlineCallbacks
|
||||
|
||||
from autobahn.xbr import HAS_XBR
|
||||
from autobahn.wamp.cryptosign import HAS_CRYPTOSIGN
|
||||
|
||||
if HAS_XBR and HAS_CRYPTOSIGN:
|
||||
from autobahn.xbr._frealm import Seeder, FederatedRealm
|
||||
from autobahn.xbr._secmod import SecurityModuleMemory, EthereumKey
|
||||
from autobahn.wamp.cryptosign import CryptosignKey
|
||||
|
||||
# https://web3py.readthedocs.io/en/stable/providers.html#infura-mainnet
|
||||
HAS_INFURA = 'WEB3_INFURA_PROJECT_ID' in os.environ and len(os.environ['WEB3_INFURA_PROJECT_ID']) > 0
|
||||
|
||||
# TypeError: As of 3.10, the *loop* parameter was removed from Lock() since it is no longer necessary
|
||||
IS_CPY_310 = sys.version_info.minor == 10
|
||||
|
||||
|
||||
@skipIf(not os.environ.get('USE_TWISTED', False), 'only for Twisted')
|
||||
@skipIf(not HAS_INFURA, 'env var WEB3_INFURA_PROJECT_ID not defined')
|
||||
@skipIf(not (HAS_XBR and HAS_CRYPTOSIGN), 'package autobahn[encryption,xbr] not installed')
|
||||
class TestFederatedRealm(TestCase):
|
||||
|
||||
gw_config = {
|
||||
'type': 'infura',
|
||||
'key': os.environ.get('WEB3_INFURA_PROJECT_ID', ''),
|
||||
'network': 'mainnet',
|
||||
}
|
||||
|
||||
# "builtins.TypeError: As of 3.10, the *loop* parameter was removed from Lock() since
|
||||
# it is no longer necessary"
|
||||
#
|
||||
# solved via websockets>=10.3, but web3==5.29.0 requires websockets<10
|
||||
#
|
||||
@skipIf(IS_CPY_310, 'Web3 v5.29.0 (web3.auto.infura) raises TypeError on Python 3.10')
|
||||
def test_frealm_ctor_auto(self):
|
||||
name = 'wamp-proto.eth'
|
||||
|
||||
fr = FederatedRealm(name)
|
||||
self.assertEqual(fr.status, 'STOPPED')
|
||||
self.assertEqual(fr.name_or_address, name)
|
||||
self.assertEqual(fr.gateway_config, None)
|
||||
self.assertEqual(fr.name_category, 'ens')
|
||||
|
||||
def test_frealm_ctor_gw(self):
|
||||
name = 'wamp-proto.eth'
|
||||
|
||||
fr = FederatedRealm(name, self.gw_config)
|
||||
self.assertEqual(fr.status, 'STOPPED')
|
||||
self.assertEqual(fr.name_or_address, name)
|
||||
self.assertEqual(fr.gateway_config, self.gw_config)
|
||||
self.assertEqual(fr.name_category, 'ens')
|
||||
|
||||
@inlineCallbacks
|
||||
def test_frealm_initialize(self):
|
||||
name = 'wamp-proto.eth'
|
||||
fr1 = FederatedRealm(name, self.gw_config)
|
||||
|
||||
self.assertEqual(fr1.status, 'STOPPED')
|
||||
yield fr1.initialize()
|
||||
self.assertEqual(fr1.status, 'RUNNING')
|
||||
|
||||
self.assertEqual(fr1.address, '0x66267d0b1114cFae80C37942177a846d666b114a')
|
||||
|
||||
def test_frealm_seeders(self):
|
||||
fr1 = MagicMock()
|
||||
fr1.name_or_address = 'wamp-proto.eth'
|
||||
fr1.address = '0x66267d0b1114cFae80C37942177a846d666b114a'
|
||||
fr1.status = 'RUNNING'
|
||||
fr1.seeders = [
|
||||
Seeder(frealm=fr1,
|
||||
endpoint='wss://frealm1.example.com/ws',
|
||||
label='Example Inc.',
|
||||
operator='0xf5fb56886f033855C1a36F651E927551749361bC',
|
||||
country='US'),
|
||||
Seeder(frealm=fr1,
|
||||
endpoint='wss://fr1.foobar.org/ws',
|
||||
label='Foobar Foundation',
|
||||
operator='0xe59C7418403CF1D973485B36660728a5f4A8fF9c',
|
||||
country='DE'),
|
||||
Seeder(frealm=fr1,
|
||||
endpoint='wss://public-frealm1.pierre.fr:443',
|
||||
label='Pierre PP',
|
||||
operator='0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B',
|
||||
country='FR'),
|
||||
]
|
||||
self.assertEqual(len(fr1.seeders), 3)
|
||||
|
||||
transports = [s.endpoint for s in fr1.seeders]
|
||||
self.assertEqual(transports, ['wss://frealm1.example.com/ws', 'wss://fr1.foobar.org/ws',
|
||||
'wss://public-frealm1.pierre.fr:443'])
|
||||
|
||||
@inlineCallbacks
|
||||
def test_frealm_secmod(self):
|
||||
name = 'wamp-proto.eth'
|
||||
seedphrase = "myth like bonus scare over problem client lizard pioneer submit female collect"
|
||||
|
||||
sm = SecurityModuleMemory.from_seedphrase(seedphrase)
|
||||
yield sm.open()
|
||||
self.assertEqual(len(sm), 2)
|
||||
self.assertTrue(isinstance(sm[0], EthereumKey), 'unexpected type {} at index 0'.format(type(sm[0])))
|
||||
self.assertTrue(isinstance(sm[1], CryptosignKey), 'unexpected type {} at index 1'.format(type(sm[1])))
|
||||
|
||||
fr = FederatedRealm(name, self.gw_config)
|
||||
|
||||
# FIXME
|
||||
fr._seeders = [
|
||||
Seeder(frealm=fr,
|
||||
endpoint='wss://frealm1.example.com/ws',
|
||||
label='Example Inc.',
|
||||
operator='0xf5fb56886f033855C1a36F651E927551749361bC',
|
||||
country='US'),
|
||||
Seeder(frealm=fr,
|
||||
endpoint='wss://fr1.foobar.org/ws',
|
||||
label='Foobar Foundation',
|
||||
operator='0xe59C7418403CF1D973485B36660728a5f4A8fF9c',
|
||||
country='DE'),
|
||||
Seeder(frealm=fr,
|
||||
endpoint='wss://public-frealm1.pierre.fr:443',
|
||||
label='Pierre PP',
|
||||
operator='0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B',
|
||||
country='FR'),
|
||||
]
|
||||
|
||||
yield fr.initialize()
|
||||
self.assertEqual(fr.status, 'RUNNING')
|
||||
self.assertEqual(fr.address, '0x66267d0b1114cFae80C37942177a846d666b114a')
|
||||
self.assertEqual(len(fr.seeders), 3)
|
||||
|
||||
delegate_key = sm[0]
|
||||
client_key = sm[1]
|
||||
authextra = yield fr.seeders[0].create_authextra(client_key=client_key,
|
||||
delegate_key=delegate_key,
|
||||
bandwidth_requested=512,
|
||||
channel_id=None,
|
||||
channel_binding=None)
|
||||
|
||||
self.assertEqual(authextra.get('pubkey', None), client_key.public_key(binary=False))
|
||||
|
||||
# print(authextra)
|
||||
|
||||
self.assertTrue('signature' in authextra)
|
||||
self.assertTrue(type(authextra['signature']) == str)
|
||||
self.assertEqual(len(authextra['signature']), 65 * 2)
|
||||
|
||||
# @skipIf(not os.environ.get('WAMP_ROUTER_URLS', None), 'WAMP_ROUTER_URLS not defined')
|
||||
# @skipIf(not os.environ.get('USE_TWISTED', False), 'only for Twisted')
|
||||
# @skipIf(not HAS_XBR, 'package autobahn[xbr] not installed')
|
||||
# class TestFederatedRealmNetworked(TestCase):
|
||||
#
|
||||
# def test_seeders_multi_reconnect(self):
|
||||
# from autobahn.twisted.component import Component, run
|
||||
#
|
||||
# # WAMP_ROUTER_URLS=ws://localhost:8080/ws,ws://localhost:8081/ws,ws://localhost:8082/ws
|
||||
# # crossbar start --cbdir=./autobahn/xbr/test/.crossbar --config=config1.json
|
||||
# transports = os.environ.get('WAMP_ROUTER_URLS', '').split(',')
|
||||
# realm = 'realm1'
|
||||
# authentication = {
|
||||
# 'cryptosign': {
|
||||
# 'privkey': '20e8c05d0ede9506462bb049c4843032b18e8e75b314583d0c8d8a4942f9be40',
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# component = Component(transports=transports, realm=realm, authentication=authentication)
|
||||
# # component.start()
|
||||
#
|
||||
# # @inlineCallbacks
|
||||
# # def main(reactor, session):
|
||||
# # print("Client session={}".format(session))
|
||||
# # res = yield session.call('user.add2', 23, 666)
|
||||
# # print(res)
|
||||
# # session.leave()
|
||||
# #
|
||||
# # from autobahn.wamp.component import _run
|
||||
# # from twisted.internet import reactor
|
||||
# # d = _run(reactor, [component])
|
||||
# # #d = run([component], log_level='info', stop_at_close=True)
|
||||
# # res = yield d
|
||||
@@ -0,0 +1,83 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 autobahn.xbr import HAS_XBR
|
||||
|
||||
if HAS_XBR:
|
||||
import unittest
|
||||
import binascii
|
||||
from autobahn.xbr import generate_seedphrase, check_seedphrase, account_from_seedphrase
|
||||
|
||||
_SEEDPHRASE = "myth like bonus scare over problem client lizard pioneer submit female collect"
|
||||
_INVALID_SEEDPHRASE = "9 nn \0 kk$"
|
||||
|
||||
_EXPECTED = [
|
||||
('0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', '0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d'),
|
||||
('0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0', '0x6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1'),
|
||||
('0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b', '0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c'),
|
||||
('0xE11BA2b4D45Eaed5996Cd0823791E0C93114882d', '0x646f1ce2fdad0e6deeeb5c7e8e5543bdde65e86029e2fd9fc169899c440a7913'),
|
||||
('0xd03ea8624C8C5987235048901fB614fDcA89b117', '0xadd53f9a7e588d003326d1cbf9e4a43c061aadd9bc938c843a79e7b4fd2ad743'),
|
||||
('0x95cED938F7991cd0dFcb48F0a06a40FA1aF46EBC', '0x395df67f0c2d2d9fe1ad08d1bc8b6627011959b79c53d7dd6a3536a33ab8a4fd'),
|
||||
('0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9', '0xe485d098507f54e7733a205420dfddbe58db035fa577fc294ebd14db90767a52'),
|
||||
('0x28a8746e75304c0780E011BEd21C72cD78cd535E', '0xa453611d9419d0e56f499079478fd72c37b251a94bfde4d19872c44cf65386e3'),
|
||||
('0xACa94ef8bD5ffEE41947b4585a84BdA5a3d3DA6E', '0x829e924fdf021ba3dbbc4225edfece9aca04b929d6e75613329ca6f1d31c0bb4'),
|
||||
('0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e', '0xb0057716d5917badaf911b193b12b910811c1497b5bada8d7711f758981c3773'),
|
||||
('0x610Bb1573d1046FCb8A70Bbbd395754cD57C2b60', '0x77c5495fbb039eed474fc940f29955ed0531693cc9212911efd35dff0373153f'),
|
||||
('0x855FA758c77D68a04990E992aA4dcdeF899F654A', '0xd99b5b29e6da2528bf458b26237a6cf8655a3e3276c1cdc0de1f98cefee81c01'),
|
||||
('0xfA2435Eacf10Ca62ae6787ba2fB044f8733Ee843', '0x9b9c613a36396172eab2d34d72331c8ca83a358781883a535d2941f66db07b24'),
|
||||
('0x64E078A8Aa15A41B85890265648e965De686bAE6', '0x0874049f95d55fb76916262dc70571701b5c4cc5900c0691af75f1a8a52c8268'),
|
||||
('0x2F560290FEF1B3Ada194b6aA9c40aa71f8e95598', '0x21d7212f3b4e5332fd465877b64926e3532653e2798a11255a46f533852dfe46'),
|
||||
('0xf408f04F9b7691f7174FA2bb73ad6d45fD5d3CBe', '0x47b65307d0d654fd4f786b908c04af8fface7710fc998b37d219de19c39ee58c'),
|
||||
('0x66FC63C2572bF3ADD0Fe5d44b97c2E614E35e9a3', '0x66109972a14d82dbdb6894e61f74708f26128814b3359b64f8b66565679f7299'),
|
||||
('0xF0D5BC18421fa04D0a2A2ef540ba5A9f04014BE3', '0x2eac15546def97adc6d69ca6e28eec831189baa2533e7910755d15403a0749e8'),
|
||||
('0x325A621DeA613BCFb5B1A69a7aCED0ea4AfBD73A', '0x2e114163041d2fb8d45f9251db259a68ee6bdbfd6d10fe1ae87c5c4bcd6ba491'),
|
||||
('0x3fD652C93dFA333979ad762Cf581Df89BaBa6795', '0xae9a2e131e9b359b198fa280de53ddbe2247730b881faae7af08e567e58915bd'),
|
||||
]
|
||||
|
||||
class TestEthereumMnemonic(unittest.TestCase):
|
||||
|
||||
def test_check_valid_seedphrase(self):
|
||||
self.assertTrue(check_seedphrase(_SEEDPHRASE))
|
||||
|
||||
def test_check_invalid_seedphrase(self):
|
||||
self.assertFalse(check_seedphrase(_INVALID_SEEDPHRASE))
|
||||
|
||||
def test_generate_seedphrase(self):
|
||||
for strength in [128, 160, 192, 224, 256]:
|
||||
seedphrase = generate_seedphrase(strength)
|
||||
|
||||
self.assertEqual(type(seedphrase), str)
|
||||
for word in seedphrase.split():
|
||||
self.assertTrue(type(word) == str)
|
||||
self.assertTrue(check_seedphrase(seedphrase))
|
||||
|
||||
def test_derive_wallet(self):
|
||||
for i, (public_adr, private_key) in enumerate(_EXPECTED):
|
||||
account = account_from_seedphrase(_SEEDPHRASE, i)
|
||||
private_key = binascii.a2b_hex(private_key[2:])
|
||||
|
||||
self.assertEqual(account.address, public_adr)
|
||||
self.assertEqual(account.key, private_key)
|
||||
@@ -0,0 +1,103 @@
|
||||
import os
|
||||
import copy
|
||||
import pkg_resources
|
||||
from random import randint, random
|
||||
import txaio
|
||||
from unittest import skipIf
|
||||
|
||||
if 'USE_TWISTED' in os.environ and os.environ['USE_TWISTED']:
|
||||
from twisted.trial import unittest
|
||||
|
||||
txaio.use_twisted()
|
||||
else:
|
||||
import unittest
|
||||
|
||||
txaio.use_asyncio()
|
||||
|
||||
from autobahn.xbr import HAS_XBR
|
||||
from autobahn.wamp.exception import InvalidPayload
|
||||
|
||||
if HAS_XBR:
|
||||
from autobahn.xbr import FbsRepository
|
||||
|
||||
|
||||
@skipIf(not HAS_XBR, 'package autobahn[xbr] not installed')
|
||||
class TestFbsBase(unittest.TestCase):
|
||||
"""
|
||||
FlatBuffers tests base class, loads test schemas.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.repo = FbsRepository('autobahn')
|
||||
self.archives = []
|
||||
for fbs_file in ['demo.bfbs', 'wamp-control.bfbs']:
|
||||
archive = pkg_resources.resource_filename('autobahn', 'xbr/test/catalog/schema/{}'.format(fbs_file))
|
||||
self.repo.load(archive)
|
||||
self.archives.append(archive)
|
||||
|
||||
|
||||
class TestFbsValidateTestTableA(TestFbsBase):
|
||||
|
||||
def test_validate_TestTableA_valid(self):
|
||||
valid_args = [
|
||||
True,
|
||||
randint(-127, -1),
|
||||
randint(1, 255),
|
||||
randint(-2 ** 15, -1),
|
||||
randint(1, 2 ** 16 - 1),
|
||||
randint(-2 ** 31, -1),
|
||||
randint(1, 2 ** 32 - 1),
|
||||
randint(-2 ** 63, -1),
|
||||
randint(1, 2 ** 64 - 1),
|
||||
2.0 + random(),
|
||||
2.0 + random(),
|
||||
]
|
||||
|
||||
try:
|
||||
self.repo.validate('demo.TestTableA', args=valid_args, kwargs={})
|
||||
except Exception as exc:
|
||||
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
|
||||
|
||||
def test_validate_TestTableA_invalid(self):
|
||||
valid_args = [
|
||||
True,
|
||||
randint(-127, -1),
|
||||
randint(1, 255),
|
||||
randint(-2 ** 15, -1),
|
||||
randint(1, 2 ** 16 - 1),
|
||||
randint(-2 ** 31, -1),
|
||||
randint(1, 2 ** 32 - 1),
|
||||
randint(-2 ** 63, -1),
|
||||
randint(1, 2 ** 64 - 1),
|
||||
2.0 + random(),
|
||||
2.0 + random(),
|
||||
]
|
||||
|
||||
# mandatory field with wrong type
|
||||
for i in range(len(valid_args)):
|
||||
# copy valid value, and set one column to a value of wrong type
|
||||
invalid_args = copy.copy(valid_args)
|
||||
if i == 0:
|
||||
# first column should be bool, so make it invalid with an int value
|
||||
invalid_args[0] = 666
|
||||
else:
|
||||
# all other columns are something different from bool, so make it invalid with a bool value
|
||||
invalid_args[i] = True
|
||||
self.assertRaisesRegex(InvalidPayload, 'invalid type', self.repo.validate,
|
||||
'demo.TestTableA', invalid_args, {})
|
||||
|
||||
# mandatory field with wrong type `None`
|
||||
if True:
|
||||
for i in range(len(valid_args)):
|
||||
# copy valid value, and set one column to a value of wrong type
|
||||
invalid_args = copy.copy(valid_args)
|
||||
invalid_args[i] = None
|
||||
self.assertRaisesRegex(InvalidPayload, 'invalid type', self.repo.validate,
|
||||
'demo.TestTableA', invalid_args, {})
|
||||
|
||||
# mandatory field missing
|
||||
if True:
|
||||
for i in range(len(valid_args)):
|
||||
invalid_args = valid_args[:i]
|
||||
self.assertRaisesRegex(InvalidPayload, 'missing positional argument', self.repo.validate,
|
||||
'demo.TestTableA', invalid_args, {})
|
||||
@@ -0,0 +1,325 @@
|
||||
import os
|
||||
import pkg_resources
|
||||
from binascii import a2b_hex
|
||||
import txaio
|
||||
from unittest import skipIf
|
||||
|
||||
if 'USE_TWISTED' in os.environ and os.environ['USE_TWISTED']:
|
||||
from twisted.trial import unittest
|
||||
|
||||
txaio.use_twisted()
|
||||
else:
|
||||
import unittest
|
||||
|
||||
txaio.use_asyncio()
|
||||
|
||||
from autobahn.xbr import HAS_XBR
|
||||
from autobahn.wamp.exception import InvalidPayload
|
||||
|
||||
if HAS_XBR:
|
||||
from autobahn.xbr._util import pack_ethadr, unpack_ethadr
|
||||
from autobahn.xbr import FbsType, FbsObject, FbsService, FbsRPCCall, FbsRepository, FbsSchema, FbsField, FbsEnum, \
|
||||
FbsEnumValue
|
||||
|
||||
|
||||
@skipIf(not HAS_XBR, 'package autobahn[xbr] not installed')
|
||||
class TestPackEthAdr(unittest.TestCase):
|
||||
"""
|
||||
Test :func:`pack_ethadr` and :func:`unpack_ethadr` helpers.
|
||||
"""
|
||||
|
||||
def test_roundtrip(self):
|
||||
original_value_str = '0xecdb40C2B34f3bA162C413CC53BA3ca99ff8A047'
|
||||
original_value_bin = a2b_hex(original_value_str[2:])
|
||||
|
||||
# count number of test cases run
|
||||
cnt = 0
|
||||
|
||||
# test 2 cases for return_dict option
|
||||
for return_dict in [False, True]:
|
||||
|
||||
# test 2 cases for input type
|
||||
for original_value in [original_value_str, original_value_bin]:
|
||||
packed_value = pack_ethadr(original_value, return_dict=return_dict)
|
||||
if return_dict:
|
||||
self.assertIsInstance(packed_value, dict)
|
||||
for i in range(5):
|
||||
self.assertIn('w{}'.format(i), packed_value)
|
||||
self.assertTrue(type(packed_value['w{}'.format(i)]) == int)
|
||||
else:
|
||||
self.assertIsInstance(packed_value, list)
|
||||
self.assertEqual(len(packed_value), 5)
|
||||
for i in range(5):
|
||||
self.assertTrue(type(packed_value[i]) == int)
|
||||
|
||||
# test 2 cases for return_str option
|
||||
for return_str in [False, True]:
|
||||
unpacked_value = unpack_ethadr(packed_value, return_str=return_str)
|
||||
if return_str:
|
||||
self.assertIsInstance(unpacked_value, str)
|
||||
self.assertEqual(unpacked_value, original_value_str)
|
||||
else:
|
||||
self.assertIsInstance(unpacked_value, bytes)
|
||||
self.assertEqual(unpacked_value, original_value_bin)
|
||||
|
||||
cnt += 1
|
||||
|
||||
# assure we actually completed as many test cases as we expect
|
||||
self.assertEqual(cnt, 8)
|
||||
|
||||
|
||||
@skipIf(not HAS_XBR, 'package autobahn[xbr] not installed')
|
||||
class TestFbsBase(unittest.TestCase):
|
||||
"""
|
||||
FlatBuffers tests base class, loads test schemas.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.repo = FbsRepository('autobahn')
|
||||
self.archives = []
|
||||
for fbs_file in ['wamp.bfbs', 'testsvc1.bfbs']:
|
||||
archive = pkg_resources.resource_filename('autobahn', 'xbr/test/catalog/schema/{}'.format(fbs_file))
|
||||
self.repo.load(archive)
|
||||
self.archives.append(archive)
|
||||
|
||||
|
||||
class TestFbsRepository(TestFbsBase):
|
||||
"""
|
||||
Test :class:`FbsRepository` schema loading and verify loaded types.
|
||||
"""
|
||||
|
||||
def test_create_from_archive(self):
|
||||
self.assertIn('uint160_t', self.repo.objs)
|
||||
self.assertIsInstance(self.repo.objs['uint160_t'], FbsObject)
|
||||
self.assertIn('testsvc1.TestRequest', self.repo.objs)
|
||||
self.assertIsInstance(self.repo.objs['testsvc1.TestRequest'], FbsObject)
|
||||
self.assertIn('testsvc1.TestResponse', self.repo.objs)
|
||||
self.assertIsInstance(self.repo.objs['testsvc1.TestResponse'], FbsObject)
|
||||
self.assertIn('testsvc1.ITestService1', self.repo.services)
|
||||
self.assertIsInstance(self.repo.services['testsvc1.ITestService1'], FbsService)
|
||||
|
||||
def test_loaded_schema(self):
|
||||
schema_fn = pkg_resources.resource_filename('autobahn', 'xbr/test/catalog/schema/testsvc1.bfbs')
|
||||
|
||||
# get reflection schema loaded
|
||||
schema: FbsSchema = self.repo.schemas[schema_fn]
|
||||
|
||||
# get call from service defined in schema
|
||||
call: FbsRPCCall = schema.services['testsvc1.ITestService1'].calls['run_something1']
|
||||
|
||||
# for each of the call request and call response type names ...
|
||||
call_type: FbsObject
|
||||
for call_type in [schema.objs[call.request.name], schema.objs[call.response.name]]:
|
||||
|
||||
# ... iterate over all fields
|
||||
field: FbsField
|
||||
for field in call_type.fields_by_id:
|
||||
# we only need to process the "_type" fields auto-added for Union types
|
||||
if field.type.basetype == FbsType.UType:
|
||||
assert field.name.endswith('_type')
|
||||
|
||||
# get the enum storing the Union
|
||||
call_type_enum: FbsEnum = schema.enums_by_id[field.type.index]
|
||||
assert call_type_enum.is_union
|
||||
|
||||
# get all enum values, which store Union types
|
||||
union_type_value: FbsEnumValue
|
||||
for union_type_value in call_type_enum.values:
|
||||
if union_type_value != 'NONE':
|
||||
# resolve union type value names in same namespace as containing union type [???]
|
||||
if '.' in call_type_enum.name:
|
||||
namespace = call_type_enum.name.split('.')[0]
|
||||
union_type_qn = '{}.{}'.format(namespace, union_type_value)
|
||||
else:
|
||||
union_type_qn = union_type_value
|
||||
# get type object for Union type by fully qualified name
|
||||
union_type = schema.objs[union_type_qn]
|
||||
print(union_type)
|
||||
|
||||
# print(self.repo.objs['testsvc1.TestRequest'])
|
||||
# print(self.repo.enums['testsvc1.TestRequestAny'])
|
||||
# print(self.repo.objs['testsvc1.TestRequestArgument'])
|
||||
# print(self.repo.objs['testsvc1.TestRequestProgress'])
|
||||
# print()
|
||||
|
||||
# print(self.repo.objs['testsvc1.TestResponse'])
|
||||
# print(self.repo.enums['testsvc1.TestResponseAny'])
|
||||
# print(self.repo.objs['testsvc1.TestResponseResult'])
|
||||
# print(self.repo.objs['testsvc1.TestResponseProgress'])
|
||||
# print(self.repo.objs['testsvc1.TestResponseError1'])
|
||||
# print(self.repo.objs['testsvc1.TestResponseError2'])
|
||||
# print()
|
||||
|
||||
# svc1 = self.repo.services['testsvc1.ITestService1']
|
||||
# for key in svc1.calls.keys():
|
||||
# ep: FbsRPCCall = svc1.calls[key]
|
||||
# print(ep)
|
||||
|
||||
# svc2 = self.repo.services['trading.ITradingClock']
|
||||
# for key in svc2.calls.keys():
|
||||
# ep: FbsRPCCall = svc2.calls[key]
|
||||
# print(ep)
|
||||
|
||||
|
||||
class TestFbsValidateUint160(TestFbsBase):
|
||||
"""
|
||||
Test struct uint160_t validation.
|
||||
"""
|
||||
|
||||
def test_validate_obj_uint160_valid(self):
|
||||
element_max = 2 ** 32 - 1
|
||||
valid_values = [
|
||||
[0, 0, 0, 0, 0],
|
||||
[element_max, element_max, element_max, element_max, element_max],
|
||||
pack_ethadr('0x0000000000000000000000000000000000000000'),
|
||||
pack_ethadr('0xecdb40C2B34f3bA162C413CC53BA3ca99ff8A047'),
|
||||
|
||||
{'w0': 0, 'w1': 0, 'w2': 0, 'w3': 0, 'w4': 0},
|
||||
{'w0': element_max, 'w1': element_max, 'w2': element_max, 'w3': element_max, 'w4': element_max},
|
||||
pack_ethadr('0x0000000000000000000000000000000000000000', return_dict=True),
|
||||
pack_ethadr('0xecdb40C2B34f3bA162C413CC53BA3ca99ff8A047', return_dict=True),
|
||||
]
|
||||
try:
|
||||
for value in valid_values:
|
||||
self.repo.validate_obj('uint160_t', value)
|
||||
except Exception as exc:
|
||||
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
|
||||
|
||||
def test_validate_obj_uint160_invalid(self):
|
||||
tests = [
|
||||
(None, 'invalid type'),
|
||||
([], 'missing argument'),
|
||||
({}, 'missing argument'),
|
||||
|
||||
([0, 0, None, 0, 0], 'invalid type'),
|
||||
([0, 0, 0, 0, 'bogus'], 'invalid type'),
|
||||
([0, 0, 0, 0], 'missing argument'),
|
||||
([0, 0, 0, 0, 0, 0], 'unexpected argument'),
|
||||
|
||||
({'w0': 0, 'w1': 0, 'w2': None, 'w3': 0, 'w4': 0}, 'invalid type'),
|
||||
({'w0': 0, 'w1': 0, 'w2': 0, 'w3': 0, 'w4': 'bogus'}, 'invalid type'),
|
||||
({'w0': 0, 'w1': 0, 'w2': 0, 'w3': 0}, 'missing argument'),
|
||||
({'w0': 0, 'w1': 0, 'w2': 0, 'w3': 0, 'w4': 0, 'w5': 0}, 'unexpected argument'),
|
||||
]
|
||||
for value, expected_regex in tests:
|
||||
self.assertRaisesRegex(InvalidPayload, expected_regex,
|
||||
self.repo.validate_obj, 'uint160_t', value)
|
||||
|
||||
|
||||
class TestFbsValidateEthAddress(TestFbsBase):
|
||||
|
||||
def test_validate_obj_EthAddress_valid(self):
|
||||
|
||||
for value in [
|
||||
{'value': {'w0': 0, 'w1': 0, 'w2': 0, 'w3': 0, 'w4': 0}},
|
||||
{'value': pack_ethadr('0x0000000000000000000000000000000000000000')},
|
||||
{'value': pack_ethadr('0xecdb40C2B34f3bA162C413CC53BA3ca99ff8A047')},
|
||||
{'value': pack_ethadr('0x0000000000000000000000000000000000000000', return_dict=True)},
|
||||
{'value': pack_ethadr('0xecdb40C2B34f3bA162C413CC53BA3ca99ff8A047', return_dict=True)},
|
||||
]:
|
||||
try:
|
||||
self.repo.validate_obj('EthAddress', value)
|
||||
except Exception as exc:
|
||||
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
|
||||
|
||||
def test_validate_obj_EthAddress_invalid(self):
|
||||
tests = [
|
||||
# FIXME
|
||||
# (None, 'invalid type'),
|
||||
# ([], 'invalid type'),
|
||||
({'invalid_key': pack_ethadr('0xecdb40C2B34f3bA162C413CC53BA3ca99ff8A047')}, 'unexpected argument'),
|
||||
({'value': None}, 'invalid type'),
|
||||
({'value': {}}, 'missing argument'),
|
||||
({'value': []}, 'missing argument'),
|
||||
]
|
||||
for value, expected_regex in tests:
|
||||
self.assertRaisesRegex(InvalidPayload, expected_regex,
|
||||
self.repo.validate_obj, 'EthAddress', value)
|
||||
|
||||
|
||||
class TestFbsValidateKeyValue(TestFbsBase):
|
||||
|
||||
def test_validate_KeyValue_valid(self):
|
||||
try:
|
||||
self.repo.validate('KeyValue', args=['foo', '23'], kwargs={})
|
||||
except Exception as exc:
|
||||
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
|
||||
|
||||
def test_validate_KeyValue_invalid(self):
|
||||
self.assertRaisesRegex(InvalidPayload, 'missing positional argument', self.repo.validate,
|
||||
'KeyValue', [], {})
|
||||
|
||||
self.assertRaisesRegex(InvalidPayload, 'missing positional argument', self.repo.validate,
|
||||
'KeyValue', ['foo'], {})
|
||||
|
||||
self.assertRaisesRegex(InvalidPayload, 'unexpected positional arguments', self.repo.validate,
|
||||
'KeyValue', ['foo', '23', 'unexpected'], {})
|
||||
|
||||
self.assertRaisesRegex(InvalidPayload, 'unexpected keyword arguments', self.repo.validate,
|
||||
'KeyValue', ['foo', '23'], {'unexpected_kwarg': '23'})
|
||||
|
||||
self.assertRaisesRegex(InvalidPayload, 'invalid type', self.repo.validate,
|
||||
'KeyValue', ['foo', 23], {})
|
||||
|
||||
def test_validate_KeyValues_valid(self):
|
||||
# empty list
|
||||
valid_value = {}
|
||||
try:
|
||||
self.repo.validate_obj('KeyValues', valid_value)
|
||||
except Exception as exc:
|
||||
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
|
||||
|
||||
# non-empty list
|
||||
valid_value = {
|
||||
'value': []
|
||||
}
|
||||
for i in range(10):
|
||||
# valid_value['value'].append(['key{}'.format(i), 'value{}'.format(i)])
|
||||
valid_value['value'].append({'key': 'key{}'.format(i), 'value': 'value{}'.format(i)})
|
||||
try:
|
||||
self.repo.validate_obj('KeyValues', valid_value)
|
||||
except Exception as exc:
|
||||
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
|
||||
|
||||
def test_validate_KeyValues_invalid(self):
|
||||
tests = [
|
||||
(None, 'invalid type'),
|
||||
([], 'invalid type'),
|
||||
({'invalid_key': 'something'}, 'unexpected argument'),
|
||||
({'value': None}, 'invalid type'),
|
||||
({'value': {}}, 'invalid type'),
|
||||
]
|
||||
for value, expected_regex in tests:
|
||||
self.assertRaisesRegex(InvalidPayload, expected_regex,
|
||||
self.repo.validate_obj, 'KeyValues', value)
|
||||
|
||||
|
||||
class TestFbsValidateVoid(TestFbsBase):
|
||||
|
||||
def test_validate_Void_valid(self):
|
||||
try:
|
||||
self.repo.validate(None, args=[], kwargs={})
|
||||
self.repo.validate('Void', args=[], kwargs={})
|
||||
except Exception as exc:
|
||||
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
|
||||
|
||||
def test_validate_Void_invalid(self):
|
||||
valid_adr = pack_ethadr('0xecdb40C2B34f3bA162C413CC53BA3ca99ff8A047')
|
||||
|
||||
self.assertRaisesRegex(InvalidPayload, 'unexpected positional argument', self.repo.validate,
|
||||
'Void', [23], {})
|
||||
|
||||
self.assertRaisesRegex(InvalidPayload, 'unexpected positional argument', self.repo.validate,
|
||||
'Void', [{}], {})
|
||||
|
||||
self.assertRaisesRegex(InvalidPayload, 'unexpected positional argument', self.repo.validate,
|
||||
'Void', [None], {})
|
||||
|
||||
self.assertRaisesRegex(InvalidPayload, 'unexpected positional argument', self.repo.validate,
|
||||
'Void', [{'value': valid_adr}], {})
|
||||
|
||||
self.assertRaisesRegex(InvalidPayload, 'unexpected keyword argument', self.repo.validate,
|
||||
'Void', [], {'unexpected_kwarg': None})
|
||||
|
||||
self.assertRaisesRegex(InvalidPayload, 'unexpected keyword argument', self.repo.validate,
|
||||
'Void', [], {'unexpected_kwarg': 23})
|
||||
@@ -0,0 +1,224 @@
|
||||
import os
|
||||
import copy
|
||||
import pkg_resources
|
||||
import txaio
|
||||
from unittest import skipIf
|
||||
|
||||
if 'USE_TWISTED' in os.environ and os.environ['USE_TWISTED']:
|
||||
from twisted.trial import unittest
|
||||
|
||||
txaio.use_twisted()
|
||||
else:
|
||||
import unittest
|
||||
|
||||
txaio.use_asyncio()
|
||||
|
||||
from autobahn.xbr import HAS_XBR
|
||||
from autobahn.wamp.exception import InvalidPayload
|
||||
|
||||
if HAS_XBR:
|
||||
from autobahn.xbr import FbsRepository
|
||||
|
||||
|
||||
@skipIf(not HAS_XBR, 'package autobahn[xbr] not installed')
|
||||
class TestFbsBase(unittest.TestCase):
|
||||
"""
|
||||
FlatBuffers tests base class, loads test schemas.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.repo = FbsRepository('autobahn')
|
||||
self.archives = []
|
||||
for fbs_file in ['wamp-control.bfbs']:
|
||||
archive = pkg_resources.resource_filename('autobahn', 'xbr/test/catalog/schema/{}'.format(fbs_file))
|
||||
self.repo.load(archive)
|
||||
self.archives.append(archive)
|
||||
|
||||
|
||||
class TestFbsValidatePermissionAllow(TestFbsBase):
|
||||
|
||||
def test_validate_PermissionAllow_valid(self):
|
||||
tests = [
|
||||
{
|
||||
'call': True,
|
||||
'register': True,
|
||||
'publish': True,
|
||||
'subscribe': True
|
||||
},
|
||||
{
|
||||
'call': False,
|
||||
'register': False,
|
||||
'publish': False,
|
||||
'subscribe': False
|
||||
},
|
||||
]
|
||||
for value in tests:
|
||||
try:
|
||||
self.repo.validate_obj('wamp.PermissionAllow', value)
|
||||
except Exception as exc:
|
||||
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
|
||||
|
||||
def test_validate_PermissionAllow_invalid(self):
|
||||
tests = [
|
||||
(None, 'invalid type'),
|
||||
(666, 'invalid type'),
|
||||
(True, 'invalid type'),
|
||||
({'some_unexpected_key': 666}, 'unexpected argument'),
|
||||
({'call': True, 'register': True, 'publish': True}, 'missing argument'),
|
||||
({'call': True, 'register': True, 'publish': True, 'subscribe': 666}, 'invalid type'),
|
||||
({'call': True, 'register': True, 'publish': True, 'subscribe': None}, 'invalid type'),
|
||||
({'call': True, 'register': True, 'publish': True, 'subscribe': True, 'some_unexpected_key': 666},
|
||||
'unexpected argument'),
|
||||
]
|
||||
for value, expected_regex in tests:
|
||||
self.assertRaisesRegex(InvalidPayload, expected_regex,
|
||||
self.repo.validate_obj, 'wamp.PermissionAllow', value)
|
||||
|
||||
|
||||
class TestFbsValidateRolePermission(TestFbsBase):
|
||||
|
||||
def test_validate_RolePermission_valid(self):
|
||||
tests = [
|
||||
{},
|
||||
{
|
||||
'uri': 'com.example.',
|
||||
'match': 'prefix',
|
||||
'allow': {
|
||||
'call': True,
|
||||
'register': True,
|
||||
'publish': True,
|
||||
'subscribe': True
|
||||
},
|
||||
'disclose': {
|
||||
'caller': True,
|
||||
'publisher': True,
|
||||
},
|
||||
'cache': True
|
||||
},
|
||||
]
|
||||
for value in tests:
|
||||
try:
|
||||
self.repo.validate_obj('wamp.RolePermission', value)
|
||||
except Exception as exc:
|
||||
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
|
||||
|
||||
def test_validate_RolePermission_invalid(self):
|
||||
tests = [
|
||||
(None, 'invalid type'),
|
||||
({'some_unexpected_key': True}, 'unexpected argument'),
|
||||
({'uri': 'com.example.', 'allow': {'some_unexpected_key': True}}, 'unexpected argument'),
|
||||
({'uri': 666}, 'invalid type'),
|
||||
({'uri': 'com.example.', 'match': 'prefix', 'allow': {'call': 666}}, 'invalid type'),
|
||||
({'uri': 666, 'match': 'prefix',
|
||||
'allow': {'call': True, 'register': True, 'publish': True, 'subscribe': True},
|
||||
'disclose': {'caller': True, 'publisher': True}, 'cache': True}, 'invalid type'),
|
||||
]
|
||||
for value, expected_regex in tests:
|
||||
self.assertRaisesRegex(InvalidPayload, expected_regex,
|
||||
self.repo.validate_obj, 'wamp.RolePermission', value)
|
||||
|
||||
|
||||
class TestFbsValidateRoleConfig(TestFbsBase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.role_config1 = {
|
||||
"name": "anonymous",
|
||||
"permissions": [{
|
||||
"uri": "",
|
||||
"match": "prefix",
|
||||
"allow": {
|
||||
"call": True,
|
||||
"register": True,
|
||||
"publish": True,
|
||||
"subscribe": True
|
||||
},
|
||||
"disclose": {
|
||||
"caller": True,
|
||||
"publisher": True
|
||||
},
|
||||
"cache": True
|
||||
}]
|
||||
}
|
||||
|
||||
def test_RoleConfig_valid(self):
|
||||
try:
|
||||
self.repo.validate_obj('wamp.RoleConfig', self.role_config1)
|
||||
except Exception as exc:
|
||||
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
|
||||
|
||||
def test_RoleConfig_invalid(self):
|
||||
config = copy.copy(self.role_config1)
|
||||
config['name'] = 666
|
||||
self.assertRaisesRegex(InvalidPayload, 'invalid type', self.repo.validate_obj,
|
||||
'wamp.RoleConfig', config)
|
||||
|
||||
# config = copy.copy(self.realm_config1)
|
||||
# del config['roles']
|
||||
# config['foobar'] = 666
|
||||
# self.assertRaisesRegex(InvalidPayload, 'missing positional argument', self.repo.validate_obj,
|
||||
# 'wamp.RealmConfig', config)
|
||||
|
||||
|
||||
class TestFbsValidateRealmConfig(TestFbsBase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.realm_config1 = {
|
||||
"name": "realm1",
|
||||
"roles": [{
|
||||
"name": "anonymous",
|
||||
"permissions": [{
|
||||
"uri": "",
|
||||
"match": "prefix",
|
||||
"allow": {
|
||||
"call": True,
|
||||
"register": True,
|
||||
"publish": True,
|
||||
"subscribe": True
|
||||
},
|
||||
"disclose": {
|
||||
"caller": True,
|
||||
"publisher": True
|
||||
},
|
||||
"cache": True
|
||||
}]
|
||||
}]
|
||||
}
|
||||
|
||||
def test_RealmConfig_valid(self):
|
||||
try:
|
||||
self.repo.validate_obj('wamp.RealmConfig', self.realm_config1)
|
||||
except Exception as exc:
|
||||
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
|
||||
|
||||
def test_RealmConfig_invalid(self):
|
||||
config = copy.copy(self.realm_config1)
|
||||
config['name'] = 666
|
||||
self.assertRaisesRegex(InvalidPayload, 'invalid type', self.repo.validate_obj,
|
||||
'wamp.RealmConfig', config)
|
||||
|
||||
def test_start_router_realm_valid(self):
|
||||
valid_args = ['realm023', self.realm_config1]
|
||||
try:
|
||||
self.repo.validate('wamp.StartRealm', args=valid_args, kwargs={})
|
||||
except Exception as exc:
|
||||
self.assertTrue(False, f'Inventory.validate() raised an exception: {exc}')
|
||||
|
||||
def test_start_router_realm_invalid(self):
|
||||
tests = [
|
||||
(None, None, 'missing positional argument'),
|
||||
(None, {}, 'missing positional argument'),
|
||||
(['realm023', {}], {'bogus': 666}, 'unexpected keyword arguments'),
|
||||
([], None, 'missing positional argument'),
|
||||
(['realm023'], None, 'missing positional argument'),
|
||||
(['realm023', None], None, 'invalid type'),
|
||||
(['realm023', 666], None, 'invalid type'),
|
||||
(['realm023', {'name': 'realm1', 'bogus': []}], None, 'unexpected argument'),
|
||||
(['realm023', {'name': 666}], None, 'invalid type'),
|
||||
(['realm023', {'name': 'realm1', 'roles': 666}], None, 'invalid type'),
|
||||
(['realm023', {'name': 'realm1', 'roles': None}], None, 'invalid type'),
|
||||
(['realm023', {'name': 'realm1', 'roles': {}}], None, 'invalid type'),
|
||||
(['realm023', {'name': 'realm1', 'roles': [{'name': 666}]}], None, 'invalid type'),
|
||||
]
|
||||
for args, kwargs, expected_regex in tests:
|
||||
self.assertRaisesRegex(InvalidPayload, expected_regex,
|
||||
self.repo.validate, 'wamp.StartRealm', args=args, kwargs=kwargs)
|
||||
@@ -0,0 +1,459 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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 sys
|
||||
import pkg_resources
|
||||
from random import randint, random
|
||||
from binascii import a2b_hex
|
||||
from typing import List
|
||||
from unittest import skipIf
|
||||
|
||||
from twisted.internet.defer import inlineCallbacks
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
from autobahn.wamp.cryptosign import HAS_CRYPTOSIGN
|
||||
from autobahn.xbr import HAS_XBR
|
||||
|
||||
if HAS_XBR and HAS_CRYPTOSIGN:
|
||||
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 py_eth_sig_utils.signing import sign_typed_data, recover_typed_data
|
||||
|
||||
from autobahn.xbr import make_w3, EthereumKey, mnemonic_to_private_key
|
||||
from autobahn.xbr._eip712_member_register import _create_eip712_member_register
|
||||
from autobahn.xbr._eip712_market_create import _create_eip712_market_create
|
||||
from autobahn.xbr._secmod import SecurityModuleMemory
|
||||
from autobahn.wamp.cryptosign import CryptosignKey
|
||||
|
||||
|
||||
# https://web3py.readthedocs.io/en/stable/providers.html#infura-mainnet
|
||||
HAS_INFURA = 'WEB3_INFURA_PROJECT_ID' in os.environ and len(os.environ['WEB3_INFURA_PROJECT_ID']) > 0
|
||||
|
||||
# TypeError: As of 3.10, the *loop* parameter was removed from Lock() since it is no longer necessary
|
||||
IS_CPY_310 = sys.version_info.minor == 10
|
||||
|
||||
|
||||
@skipIf(not os.environ.get('USE_TWISTED', False), 'only for Twisted')
|
||||
@skipIf(not HAS_INFURA, 'env var WEB3_INFURA_PROJECT_ID not defined')
|
||||
@skipIf(not (HAS_XBR and HAS_CRYPTOSIGN), 'package autobahn[encryption,xbr] not installed')
|
||||
class TestSecurityModule(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._gw_config = {
|
||||
'type': 'infura',
|
||||
'key': os.environ.get('WEB3_INFURA_PROJECT_ID', ''),
|
||||
'network': 'mainnet',
|
||||
}
|
||||
self._w3 = make_w3(self._gw_config)
|
||||
|
||||
self._seedphrase = "avocado style uncover thrive same grace crunch want essay reduce current edge"
|
||||
self._addresses = [
|
||||
'0xf766Dc789CF04CD18aE75af2c5fAf2DA6650Ff57',
|
||||
'0xf5173a6111B2A6B3C20fceD53B2A8405EC142bF6',
|
||||
'0xecdb40C2B34f3bA162C413CC53BA3ca99ff8A047',
|
||||
'0x2F070c2f49a59159A0346396f1139203355ACA43',
|
||||
'0x66290fA8ADcD901Fd994e4f64Cfb53F4c359a326',
|
||||
]
|
||||
self._keys = [
|
||||
'0x805f84af7e182359db0610ffb07c801012b699b5610646937704aa5cfc28b15e',
|
||||
'0x991c8f7609f3236ad5ef6d498b2ec0c9793c2865dd337ddc3033067c1da0e735',
|
||||
'0x75848ddb1155cd1cdf6d74a6e7fbed06aeaa21ef2d8a05df7af2d95cdc127672',
|
||||
'0x5be599a34927a1110922d7704ba316144b31699d8e7f229e2684d5575a84214e',
|
||||
'0xc1bb7ce3481e95b28bb8c026667b6009c504c79a98e6c7237ba0788c37b473c9',
|
||||
]
|
||||
|
||||
# create EIP712 typed data dicts from message data and schemata
|
||||
|
||||
verifying_contract = a2b_hex(self._addresses[0][2:])
|
||||
member = a2b_hex(self._addresses[1][2:])
|
||||
maker = a2b_hex(self._addresses[2][2:])
|
||||
coin = a2b_hex(self._addresses[3][2:])
|
||||
|
||||
eula = 'QmU7Gizbre17x6V2VR1Q2GJEjz6m8S1bXmBtVxS2vmvb81'
|
||||
profile = 'QmcNsPV7QZFHKb2DNn8GWsU5dtd8zH5DNRa31geC63ceb4'
|
||||
terms = 'QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4'
|
||||
meta = 'Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD'
|
||||
|
||||
market_id = a2b_hex('5b7ee23c9353479ca49a2461c0a1deb2')
|
||||
|
||||
self._eip_data_objects = [
|
||||
_create_eip712_member_register(chainId=1, verifyingContract=verifying_contract, member=member,
|
||||
registered=666, eula=eula, profile=profile),
|
||||
_create_eip712_member_register(chainId=23, verifyingContract=a2b_hex(self._addresses[0][2:]),
|
||||
member=a2b_hex(self._addresses[1][2:]), registered=9999, eula=eula,
|
||||
profile=profile),
|
||||
_create_eip712_market_create(chainId=1, verifyingContract=verifying_contract, member=member, created=666,
|
||||
marketId=market_id, coin=coin, terms=terms, meta=meta, maker=maker,
|
||||
providerSecurity=10 ** 6, consumerSecurity=10 ** 6, marketFee=100),
|
||||
]
|
||||
self._eip_data_obj_hashes = [
|
||||
'8abee87b2cf457841d173083d5f205183f3e78c6cee30ca77776344e11f612b3',
|
||||
'6a4f10dc41080c445a86acaae652ce80878fe768f6b459af08d14465c5310138',
|
||||
'f1b80df26ec6cc7dafeb8a5c69de77e8ec5a2c0e93f5d6e475124f18cf4c595f',
|
||||
]
|
||||
self._eip_data_obj_signatures = [
|
||||
'17ed35d8fd41fcb507ae11a3745d9775f37ff1c155257074fe2245cfb186f4336151fd018bf83a5e9902d825b645213a111630f78bbbc3c96f68d60b7e65dafd1c',
|
||||
'1c0fa4d8e2b2d0d0391c4b7c5cf2f494eab5c7074aa46cfd11a2d8a6b8c087030db7a5b74128d9bb04f6baa12abaa45457e0cfe790e9ebbd62721c075d79335e1c',
|
||||
'236660f4cc04df21289538bf15e83d5bd2858b9dad27022d6b83fc3374ce887d5789e1d40126823abf7ccef04d06e4a1717e6b6a00cbfacf5cc2e7b2e4cb384e1c',
|
||||
]
|
||||
|
||||
def test_ethereum_key_from_seedphrase(self):
|
||||
"""
|
||||
Create key from seedphrase and index.
|
||||
"""
|
||||
for i in range(len(self._keys)):
|
||||
key = EthereumKey.from_seedphrase(self._seedphrase, i)
|
||||
self.assertEqual(key.address(binary=False), self._addresses[i])
|
||||
|
||||
def test_ethereum_key_from_bytes(self):
|
||||
"""
|
||||
Create key from raw bytes.
|
||||
"""
|
||||
for i in range(len(self._keys)):
|
||||
key_raw = a2b_hex(self._keys[i][2:])
|
||||
key = EthereumKey.from_bytes(key_raw)
|
||||
self.assertEqual(key.address(binary=False), self._addresses[i])
|
||||
self.assertEqual(key._key.key, key_raw)
|
||||
|
||||
def test_ethereum_sign_typed_data_pesu_manual(self):
|
||||
"""
|
||||
Test using py_eth_sig_utils by doing individual steps / manually.
|
||||
"""
|
||||
key_raw = a2b_hex(self._keys[0][2:])
|
||||
|
||||
for i in range(len(self._eip_data_objects)):
|
||||
data = self._eip_data_objects[i]
|
||||
|
||||
# encode typed data dict and return message hash
|
||||
msg_hash = encode_typed_data(data)
|
||||
# print('0' * 100, b2a_hex(msg_hash).decode())
|
||||
self.assertEqual(msg_hash, a2b_hex(self._eip_data_obj_hashes[i]))
|
||||
|
||||
# sign message hash with private key
|
||||
signature_vrs = ecsign(msg_hash, key_raw)
|
||||
|
||||
# concatenate signature components into byte string
|
||||
signature = v_r_s_to_signature(*signature_vrs)
|
||||
# print('1' * 100, b2a_hex(signature).decode())
|
||||
|
||||
# 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
|
||||
self.assertEqual(len(signature), 65)
|
||||
self.assertEqual(signature, a2b_hex(self._eip_data_obj_signatures[i]))
|
||||
|
||||
def test_ethereum_sign_typed_data_pesu_highlevel(self):
|
||||
"""
|
||||
Test using py_eth_sig_utils with high level functions.
|
||||
"""
|
||||
key_raw = a2b_hex(self._keys[0][2:])
|
||||
for i in range(len(self._eip_data_objects)):
|
||||
data = self._eip_data_objects[i]
|
||||
|
||||
signature_vrs = sign_typed_data(data, key_raw)
|
||||
signature = v_r_s_to_signature(*signature_vrs)
|
||||
# print('2' * 100, b2a_hex(signature).decode())
|
||||
|
||||
self.assertEqual(len(signature), 65)
|
||||
self.assertEqual(signature, a2b_hex(self._eip_data_obj_signatures[i]))
|
||||
|
||||
@inlineCallbacks
|
||||
def test_ethereum_sign_typed_data_ab_async(self):
|
||||
"""
|
||||
Test using autobahn with async functions.
|
||||
"""
|
||||
key_raw = a2b_hex(self._keys[0][2:])
|
||||
key = EthereumKey.from_bytes(key_raw)
|
||||
for i in range(len(self._eip_data_objects)):
|
||||
data = self._eip_data_objects[i]
|
||||
signature = yield key.sign_typed_data(data)
|
||||
self.assertEqual(signature, a2b_hex(self._eip_data_obj_signatures[i]))
|
||||
|
||||
def test_ethereum_verify_typed_data_pesu_manual(self):
|
||||
"""
|
||||
Test using py_eth_sig_utils by doing individual steps / manually.
|
||||
"""
|
||||
for i in range(len(self._eip_data_objects)):
|
||||
data = self._eip_data_objects[i]
|
||||
|
||||
# encode typed data dict and return message hash
|
||||
msg_hash = encode_typed_data(data)
|
||||
|
||||
signature = a2b_hex(self._eip_data_obj_signatures[i])
|
||||
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)
|
||||
|
||||
self.assertEqual(address, self._addresses[0])
|
||||
|
||||
def test_ethereum_verify_typed_data_pesu_highlevel(self):
|
||||
"""
|
||||
Test using py_eth_sig_utils with high level functions.
|
||||
"""
|
||||
for i in range(len(self._eip_data_objects)):
|
||||
data = self._eip_data_objects[i]
|
||||
|
||||
signature = a2b_hex(self._eip_data_obj_signatures[i])
|
||||
signature_vrs = signature_to_v_r_s(signature)
|
||||
|
||||
address = recover_typed_data(data, *signature_vrs)
|
||||
|
||||
self.assertEqual(address, self._addresses[0])
|
||||
|
||||
@inlineCallbacks
|
||||
def test_ethereum_verify_typed_data_ab_async(self):
|
||||
"""
|
||||
Test using autobahn with async functions.
|
||||
"""
|
||||
key = EthereumKey.from_address(self._addresses[0])
|
||||
for i in range(len(self._eip_data_objects)):
|
||||
data = self._eip_data_objects[i]
|
||||
signature = a2b_hex(self._eip_data_obj_signatures[i])
|
||||
sig_valid = yield key.verify_typed_data(data, signature)
|
||||
self.assertTrue(sig_valid)
|
||||
|
||||
@inlineCallbacks
|
||||
def test_secmod_iterable(self):
|
||||
"""
|
||||
This tests:
|
||||
|
||||
* :meth:`SecurityModuleMemory.from_seedphrase`
|
||||
* :meth:`SecurityModuleMemory.__len__`
|
||||
* :meth:`SecurityModuleMemory.__iter__`
|
||||
* :meth:`SecurityModuleMemory.__getitem__`
|
||||
"""
|
||||
sm = SecurityModuleMemory.from_seedphrase(self._seedphrase, 5, 5)
|
||||
yield sm.open()
|
||||
|
||||
self.assertEqual(len(sm), 10)
|
||||
|
||||
for i, key in sm.items():
|
||||
self.assertTrue(isinstance(key, EthereumKey) or isinstance(key, CryptosignKey),
|
||||
'unexpected type {} returned in security module'.format(type(key)))
|
||||
key_ = sm[i]
|
||||
self.assertEqual(key_, key)
|
||||
|
||||
@inlineCallbacks
|
||||
def test_secmod_create_key(self):
|
||||
"""
|
||||
This tests:
|
||||
|
||||
* :meth:`SecurityModuleMemory.create_key`
|
||||
"""
|
||||
sm = SecurityModuleMemory()
|
||||
yield sm.open()
|
||||
|
||||
self.assertEqual(len(sm), 0)
|
||||
|
||||
for i in range(3):
|
||||
idx = yield sm.create_key('ethereum')
|
||||
self.assertEqual(idx, i * 2)
|
||||
self.assertEqual(len(sm), i * 2 + 1)
|
||||
key = sm[idx]
|
||||
self.assertTrue(isinstance(key, EthereumKey))
|
||||
self.assertEqual(key.security_module, sm)
|
||||
self.assertEqual(key.key_no, i * 2)
|
||||
self.assertEqual(key.key_type, 'ethereum')
|
||||
self.assertEqual(key.can_sign, True)
|
||||
|
||||
idx = yield sm.create_key('cryptosign')
|
||||
self.assertEqual(idx, i * 2 + 1)
|
||||
self.assertEqual(len(sm), i * 2 + 2)
|
||||
key = sm[idx]
|
||||
self.assertTrue(isinstance(key, CryptosignKey))
|
||||
self.assertEqual(key.security_module, sm)
|
||||
self.assertEqual(key.key_no, i * 2 + 1)
|
||||
self.assertEqual(key.key_type, 'cryptosign')
|
||||
self.assertEqual(key.can_sign, True)
|
||||
|
||||
self.assertEqual(len(sm), 6)
|
||||
|
||||
@inlineCallbacks
|
||||
def test_secmod_delete_key(self):
|
||||
"""
|
||||
This tests:
|
||||
|
||||
* :meth:`SecurityModuleMemory.create_key`
|
||||
* :meth:`SecurityModuleMemory.delete_key`
|
||||
"""
|
||||
sm = SecurityModuleMemory()
|
||||
yield sm.open()
|
||||
|
||||
self.assertEqual(len(sm), 0)
|
||||
|
||||
n = 10
|
||||
keys = []
|
||||
|
||||
for i in range(n):
|
||||
if random() > .5:
|
||||
yield sm.create_key('ethereum')
|
||||
else:
|
||||
yield sm.create_key('cryptosign')
|
||||
key = sm[i]
|
||||
keys.append(key)
|
||||
self.assertEqual(len(sm), 10)
|
||||
|
||||
for i in range(n):
|
||||
self.assertTrue(i in sm)
|
||||
yield sm.delete_key(i)
|
||||
self.assertFalse(i in sm)
|
||||
self.assertEqual(len(sm), n - i - 1)
|
||||
|
||||
@inlineCallbacks
|
||||
def test_secmod_counters(self):
|
||||
"""
|
||||
This tests:
|
||||
|
||||
* :meth:`SecurityModuleMemory.__init__`
|
||||
* :meth:`SecurityModuleMemory.get_counter`
|
||||
* :meth:`SecurityModuleMemory.increment_counter`
|
||||
"""
|
||||
sm = SecurityModuleMemory()
|
||||
yield sm.open()
|
||||
|
||||
# counters are indexed beginning with 0
|
||||
counter = 0
|
||||
|
||||
# initially, no counters exist, and hence value must be 0
|
||||
value = yield sm.get_counter(counter)
|
||||
self.assertEqual(value, 0)
|
||||
|
||||
yield sm.get_counter(randint(0, 100))
|
||||
self.assertEqual(value, 0)
|
||||
|
||||
# once incremented, counters exist
|
||||
for counter in range(10):
|
||||
for i in range(100):
|
||||
value = yield sm.increment_counter(counter)
|
||||
self.assertEqual(value, i + 1)
|
||||
|
||||
value = yield sm.get_counter(counter)
|
||||
self.assertEqual(value, i + 1)
|
||||
|
||||
def test_cryptosign_key_from_seedphrase(self):
|
||||
# seedphrase to compute keys from
|
||||
seedphrase = "myth like bonus scare over problem client lizard pioneer submit female collect"
|
||||
|
||||
# pubkeys we expect
|
||||
pubs_keys: List[str] = [
|
||||
'30b2e1af1406c5f5254ddc456a045808796d13417f3b56500b0321a908cd89ca',
|
||||
'262b6812802deac81dd2be53d69cb32a05eb9296265e9698f02772867ede002f',
|
||||
'2d2ae42f8927b6c20fe4463151c3468367852c370a3b7db73ef10f97ce262739',
|
||||
'fab0eab3e14b24288b816dd590f21f90700a96306648cb2a031c7451dc5ee616',
|
||||
'1ce310832e5acb0359516400a881cf41d94ca60d9a529ce48a1b5f857cde0aa8',
|
||||
]
|
||||
|
||||
# create keys from seedphrase
|
||||
keys: List[CryptosignKey] = []
|
||||
for i in range(5):
|
||||
|
||||
# BIP44 path for WAMP
|
||||
# https://github.com/wamp-proto/wamp-proto/issues/401
|
||||
# https://github.com/satoshilabs/slips/pull/1322
|
||||
derivation_path = "m/44'/655'/0'/0/{}".format(i)
|
||||
|
||||
# compute private key from WAMP-Cryptosign from seedphrase and BIP44 path
|
||||
key_raw = mnemonic_to_private_key(seedphrase, derivation_path)
|
||||
assert type(key_raw) == bytes
|
||||
assert len(key_raw) == 32
|
||||
|
||||
# create WAMP-Cryptosign key object from raw bytes
|
||||
key = CryptosignKey.from_bytes(key_raw)
|
||||
keys.append(key)
|
||||
|
||||
# check public keys we expect
|
||||
for i in range(5):
|
||||
pub_key = keys[i].public_key(binary=False)
|
||||
self.assertEqual(pub_key, pubs_keys[i])
|
||||
|
||||
@inlineCallbacks
|
||||
def test_secmod_from_seedphrase(self):
|
||||
# seedphrase to compute keys from
|
||||
seedphrase = "myth like bonus scare over problem client lizard pioneer submit female collect"
|
||||
|
||||
sm = SecurityModuleMemory.from_seedphrase(seedphrase)
|
||||
yield sm.open()
|
||||
self.assertEqual(len(sm), 2)
|
||||
self.assertTrue(isinstance(sm[0], EthereumKey), 'unexpected type {} at index 0'.format(type(sm[0])))
|
||||
self.assertTrue(isinstance(sm[1], CryptosignKey), 'unexpected type {} at index 1'.format(type(sm[1])))
|
||||
yield sm.close()
|
||||
|
||||
sm = SecurityModuleMemory.from_seedphrase(seedphrase, num_eth_keys=5, num_cs_keys=5)
|
||||
yield sm.open()
|
||||
self.assertEqual(len(sm), 10)
|
||||
for i in range(5):
|
||||
self.assertTrue(isinstance(sm[i], EthereumKey))
|
||||
for i in range(5, 10):
|
||||
self.assertTrue(isinstance(sm[i], CryptosignKey))
|
||||
yield sm.close()
|
||||
|
||||
@inlineCallbacks
|
||||
def test_secmod_from_config(self):
|
||||
config = pkg_resources.resource_filename('autobahn', 'xbr/test/profile/config.ini')
|
||||
|
||||
sm = SecurityModuleMemory.from_config(config)
|
||||
yield sm.open()
|
||||
self.assertEqual(len(sm), 2)
|
||||
|
||||
self.assertTrue(isinstance(sm[0], EthereumKey), 'unexpected type {} at index 0'.format(type(sm[0])))
|
||||
self.assertTrue(isinstance(sm[1], CryptosignKey), 'unexpected type {} at index 1'.format(type(sm[1])))
|
||||
|
||||
key1: EthereumKey = sm[0]
|
||||
key2: CryptosignKey = sm[1]
|
||||
|
||||
# public-key-ed25519: 15cfa4acef5cc312e0b9ba77634849d0a8c6222a546f90eb5123667935d2f561
|
||||
# public-adr-eth: 0xe59C7418403CF1D973485B36660728a5f4A8fF9c
|
||||
self.assertEqual(key1.address(binary=False), '0xe59C7418403CF1D973485B36660728a5f4A8fF9c')
|
||||
self.assertEqual(key2.public_key(binary=False), '15cfa4acef5cc312e0b9ba77634849d0a8c6222a546f90eb5123667935d2f561')
|
||||
|
||||
yield sm.close()
|
||||
|
||||
@inlineCallbacks
|
||||
def test_secmod_from_keyfile(self):
|
||||
keyfile = pkg_resources.resource_filename('autobahn', 'xbr/test/profile/default.priv')
|
||||
|
||||
sm = SecurityModuleMemory.from_keyfile(keyfile)
|
||||
yield sm.open()
|
||||
self.assertEqual(len(sm), 2)
|
||||
|
||||
self.assertTrue(isinstance(sm[0], EthereumKey), 'unexpected type {} at index 0'.format(type(sm[0])))
|
||||
self.assertTrue(isinstance(sm[1], CryptosignKey), 'unexpected type {} at index 1'.format(type(sm[1])))
|
||||
|
||||
key1: EthereumKey = sm[0]
|
||||
key2: CryptosignKey = sm[1]
|
||||
|
||||
# public-key-ed25519: 15cfa4acef5cc312e0b9ba77634849d0a8c6222a546f90eb5123667935d2f561
|
||||
# public-adr-eth: 0xe59C7418403CF1D973485B36660728a5f4A8fF9c
|
||||
self.assertEqual(key1.address(binary=False), '0xe59C7418403CF1D973485B36660728a5f4A8fF9c')
|
||||
self.assertEqual(key2.public_key(binary=False), '15cfa4acef5cc312e0b9ba77634849d0a8c6222a546f90eb5123667935d2f561')
|
||||
|
||||
yield sm.close()
|
||||
@@ -0,0 +1,57 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from unittest import skipIf
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
from autobahn.xbr import HAS_XBR
|
||||
|
||||
# https://web3py.readthedocs.io/en/stable/providers.html#infura-mainnet
|
||||
HAS_INFURA = 'WEB3_INFURA_PROJECT_ID' in os.environ and len(os.environ['WEB3_INFURA_PROJECT_ID']) > 0
|
||||
|
||||
# TypeError: As of 3.10, the *loop* parameter was removed from Lock() since it is no longer necessary
|
||||
IS_CPY_310 = sys.version_info.minor >= 10
|
||||
|
||||
|
||||
@skipIf(not HAS_XBR, 'package autobahn[xbr] not installed')
|
||||
@skipIf(not HAS_INFURA, 'env var WEB3_INFURA_PROJECT_ID not defined')
|
||||
class TestWeb3(TestCase):
|
||||
gw_config = {
|
||||
'type': 'infura',
|
||||
'key': os.environ.get('WEB3_INFURA_PROJECT_ID', ''),
|
||||
'network': 'mainnet',
|
||||
}
|
||||
|
||||
# "builtins.TypeError: As of 3.10, the *loop* parameter was removed from Lock() since
|
||||
# it is no longer necessary"
|
||||
#
|
||||
# solved via websockets>=10.3, but web3==5.29.0 requires websockets<10
|
||||
#
|
||||
@skipIf(True, 'FIXME: web3.auto.infura was removed')
|
||||
def test_connect_w3_infura_auto(self):
|
||||
from web3.auto.infura import w3
|
||||
|
||||
self.assertTrue(w3.isConnected())
|
||||
|
||||
def test_connect_w3_autobahn(self):
|
||||
from autobahn.xbr import make_w3
|
||||
w3 = make_w3(self.gw_config)
|
||||
self.assertTrue(w3.isConnected())
|
||||
|
||||
def test_ens_valid_names(self):
|
||||
from ens.ens import ENS
|
||||
|
||||
for name in ['wamp-proto.eth']:
|
||||
self.assertTrue(ENS.is_valid_name(name))
|
||||
|
||||
def test_ens_resolve_names(self):
|
||||
from autobahn.xbr import make_w3
|
||||
from ens.ens import ENS
|
||||
|
||||
w3 = make_w3(self.gw_config)
|
||||
ens = ENS.from_web3(w3)
|
||||
for name, adr in [
|
||||
('wamp-proto.eth', '0x66267d0b1114cFae80C37942177a846d666b114a'),
|
||||
]:
|
||||
_adr = ens.address(name)
|
||||
self.assertEqual(adr, _adr)
|
||||
Reference in New Issue
Block a user