17.12
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) Crossbar.io Technologies GmbH
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
import abc
|
||||
import six
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class IMarketMaker(object):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class IProvider(object):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class IConsumer(object):
|
||||
"""
|
||||
XBR Consumer interface.
|
||||
"""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ISeller(object):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class IBuyer(object):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
@@ -0,0 +1,162 @@
|
||||
###############################################################################
|
||||
#
|
||||
# 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"
|
||||
|
||||
|
||||
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 = Q.x().to_bytes(32, byteorder='big')
|
||||
parity = Q.y() & 1
|
||||
return (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 = 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 + 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 + 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
|
||||
@@ -0,0 +1,218 @@
|
||||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) Crossbar.io Technologies GmbH
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from binascii import a2b_hex
|
||||
|
||||
import click
|
||||
import web3
|
||||
from py_eth_sig_utils import signing
|
||||
|
||||
_EIP712_SIG_LEN = 32 + 32 + 1
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
# FIXME: possibly use https://eth-abi.readthedocs.io/en/stable/decoding.html
|
||||
|
||||
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 hl(text, bold=True, color='yellow'):
|
||||
if not isinstance(text, str):
|
||||
text = '{}'.format(text)
|
||||
return click.style(text, fg=color, bold=bold)
|
||||
|
||||
|
||||
def _create_eip712_data(verifying_adr, channel_adr, channel_seq, balance, is_final):
|
||||
assert type(verifying_adr) == bytes and len(verifying_adr) == 20
|
||||
assert type(channel_adr) == bytes and len(channel_adr) == 20
|
||||
assert type(channel_seq) == int
|
||||
assert type(balance) == int
|
||||
assert type(is_final) == bool
|
||||
|
||||
data = {
|
||||
'types': {
|
||||
'EIP712Domain': [
|
||||
{'name': 'name', 'type': 'string'},
|
||||
{'name': 'version', 'type': 'string'},
|
||||
{'name': 'chainId', 'type': 'uint256'},
|
||||
{'name': 'verifyingContract', 'type': 'address'},
|
||||
],
|
||||
'ChannelClose': [
|
||||
# The channel contract address.
|
||||
{'name': 'channel_adr', 'type': 'address'},
|
||||
|
||||
# Channel off-chain transaction sequence number.
|
||||
{'name': 'channel_seq', 'type': 'uint32'},
|
||||
|
||||
# Balance remaining in after the transaction.
|
||||
{'name': 'balance', 'type': 'uint256'},
|
||||
|
||||
# Transaction is marked as final.
|
||||
{'name': 'is_final', 'type': 'bool'},
|
||||
],
|
||||
},
|
||||
'primaryType': 'ChannelClose',
|
||||
'domain': {
|
||||
'name': 'XBR',
|
||||
'version': '1',
|
||||
'chainId': 1,
|
||||
'verifyingContract': verifying_adr,
|
||||
},
|
||||
'message': {
|
||||
'channel_adr': channel_adr,
|
||||
'channel_seq': channel_seq,
|
||||
'balance': balance,
|
||||
'is_final': is_final
|
||||
},
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sign_eip712_data(eth_privkey, channel_adr, channel_seq, balance, is_final=False):
|
||||
"""
|
||||
|
||||
:param eth_privkey: Ethereum address of buyer (a raw 20 bytes Ethereum address).
|
||||
:type eth_privkey: bytes
|
||||
|
||||
:param channel_adr: Channel contract address.
|
||||
:type channel_adr: bytes
|
||||
|
||||
:param channel_seq: Payment channel off-chain transaction sequence number.
|
||||
:type channel_seq: int
|
||||
|
||||
:param balance: Balance remaining in the payment/paying channel after buying/selling the key.
|
||||
:type balance: int
|
||||
|
||||
:param is_final: Flag to indicate the transaction is considered final.
|
||||
:type is_final: bool
|
||||
|
||||
:return: The signature according to EIP712 (32+32+1 raw bytes).
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert type(eth_privkey) == bytes and len(eth_privkey) == 32
|
||||
assert type(channel_adr) == bytes and len(channel_adr) == 20
|
||||
assert type(channel_seq) == int and channel_seq > 0
|
||||
assert type(balance) == int and balance >= 0
|
||||
assert type(is_final) == bool
|
||||
|
||||
verifying_adr = a2b_hex('0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B'[2:])
|
||||
|
||||
# make a private key object from the raw private key bytes
|
||||
# pkey = eth_keys.keys.PrivateKey(eth_privkey)
|
||||
|
||||
# get the canonical address of the account
|
||||
# eth_adr = web3.Web3.toChecksumAddress(pkey.public_key.to_canonical_address())
|
||||
# eth_adr = pkey.public_key.to_canonical_address()
|
||||
|
||||
# create EIP712 typed data object
|
||||
data = _create_eip712_data(verifying_adr, channel_adr, channel_seq, balance, is_final)
|
||||
|
||||
# FIXME: this fails on PyPy (but ot on CPy!) with
|
||||
# Unknown format b'%M\xff\xcd2w\xc0\xb1f\x0fmB\xef\xbbuN\xda\xba\xbc+', attempted to normalize to 0x254dffcd3277c0b1660f6d42efbb754edababc2b
|
||||
_args = signing.sign_typed_data(data, eth_privkey)
|
||||
|
||||
signature = signing.v_r_s_to_signature(*_args)
|
||||
assert len(signature) == _EIP712_SIG_LEN
|
||||
|
||||
return signature
|
||||
|
||||
|
||||
def recover_eip712_signer(channel_adr, channel_seq, balance, is_final, signature):
|
||||
"""
|
||||
Recover the signer address the given EIP712 signature was signed with.
|
||||
|
||||
:param channel_adr: Channel contract address.
|
||||
:type channel_adr: bytes
|
||||
|
||||
:param channel_seq: Payment channel off-chain transaction sequence number.
|
||||
:type channel_seq: int
|
||||
|
||||
:param balance: Balance remaining in the payment/paying channel after buying/selling the key.
|
||||
:type balance: int
|
||||
|
||||
:param is_final: Flag to indicate the transaction is considered final.
|
||||
:type is_final: bool
|
||||
|
||||
:param signature: The EIP712 (32+32+1 raw bytes) signature to verify.
|
||||
:type signature: bytes
|
||||
|
||||
:return: The (computed) signer address the signature was signed with.
|
||||
:rtype: bytes
|
||||
"""
|
||||
assert type(channel_adr) == bytes and len(channel_adr) == 20
|
||||
assert type(channel_seq) == int
|
||||
assert type(balance) == int
|
||||
assert type(is_final) == bool
|
||||
assert type(signature) == bytes and len(signature) == _EIP712_SIG_LEN
|
||||
|
||||
verifying_adr = a2b_hex('0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B'[2:])
|
||||
|
||||
# recreate EIP712 typed data object
|
||||
data = _create_eip712_data(verifying_adr, channel_adr, channel_seq, balance, is_final)
|
||||
|
||||
# this returns the signer (checksummed) address as a string, eg "0xE11BA2b4D45Eaed5996Cd0823791E0C93114882d"
|
||||
signer_address = signing.recover_typed_data(data, *signing.signature_to_v_r_s(signature))
|
||||
|
||||
return a2b_hex(signer_address[2:])
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user