Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
23f977482c | ||
|
20dfb641ff | ||
|
972535f1f9 | ||
|
a2168a50e3 | ||
|
9548ed5d1b | ||
|
e499770d6d | ||
|
e49fae9717 | ||
|
84c4a82abb |
@ -1,3 +1,9 @@
|
||||
- 0.2.0
|
||||
* Implement chainlib generic tx, block and tx result objects
|
||||
- 0.1.3
|
||||
* Add block author field
|
||||
- 0.1.2
|
||||
* Upgrade chainlib dep
|
||||
- 0.1.1
|
||||
* Add fee_limit, fee_price alias to Tx object
|
||||
- 0.1.0:
|
||||
|
@ -10,6 +10,7 @@ from hexathon import (
|
||||
|
||||
# local imports
|
||||
from chainlib.eth.tx import Tx
|
||||
from .src import Src
|
||||
|
||||
|
||||
def block_latest(id_generator=None):
|
||||
@ -76,7 +77,7 @@ def syncing(id_generator=None):
|
||||
return j.finalize(o)
|
||||
|
||||
|
||||
class Block(BaseBlock):
|
||||
class Block(BaseBlock, Src):
|
||||
"""Encapsulates an Ethereum block
|
||||
|
||||
:param src: Block representation data
|
||||
@ -87,7 +88,9 @@ class Block(BaseBlock):
|
||||
tx_generator = Tx
|
||||
|
||||
def __init__(self, src):
|
||||
self.hash = src['hash']
|
||||
super(Block, self).__init__(src)
|
||||
import sys
|
||||
self.set_hash(src['hash'])
|
||||
try:
|
||||
self.number = int(strip_0x(src['number']), 16)
|
||||
except TypeError:
|
||||
@ -98,9 +101,10 @@ class Block(BaseBlock):
|
||||
self.timestamp = int(strip_0x(src['timestamp']), 16)
|
||||
except TypeError:
|
||||
self.timestamp = int(src['timestamp'])
|
||||
self.author = src['author']
|
||||
|
||||
|
||||
def get_tx(self, tx_hash):
|
||||
def tx_index_by_hash(self, tx_hash):
|
||||
i = 0
|
||||
idx = -1
|
||||
tx_hash = add_0x(tx_hash)
|
||||
@ -117,4 +121,3 @@ class Block(BaseBlock):
|
||||
if idx == -1:
|
||||
raise AttributeError('tx {} not found in block {}'.format(tx_hash, self.hash))
|
||||
return idx
|
||||
|
||||
|
48
chainlib/eth/src.py
Normal file
48
chainlib/eth/src.py
Normal file
@ -0,0 +1,48 @@
|
||||
# standard imports
|
||||
import logging
|
||||
|
||||
# external imports
|
||||
from potaahto.symbols import snake_and_camel
|
||||
from hexathon import (
|
||||
uniform,
|
||||
strip_0x,
|
||||
)
|
||||
|
||||
# local imports
|
||||
from chainlib.src import (
|
||||
Src as BaseSrc,
|
||||
SrcItem,
|
||||
)
|
||||
|
||||
logg = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Src(BaseSrc):
|
||||
|
||||
@classmethod
|
||||
def src_normalize(self, v):
|
||||
src = snake_and_camel(v)
|
||||
logg.debug('normalize has {}'.format(src))
|
||||
if isinstance(src.get('v'), str):
|
||||
try:
|
||||
src['v'] = int(src['v'])
|
||||
except ValueError:
|
||||
src['v'] = int(src['v'], 16)
|
||||
return src
|
||||
|
||||
|
||||
def normal(self, v, typ=SrcItem.AUTO):
|
||||
if typ == SrcItem.SRC:
|
||||
return self.src_normalize(v)
|
||||
|
||||
if typ == SrcItem.HASH:
|
||||
v = strip_0x(v, pad=False)
|
||||
v = uniform(v, compact_value=True)
|
||||
elif typ == SrcItem.ADDRESS:
|
||||
v = strip_0x(v, pad=False)
|
||||
v = uniform(v, compact_value=True)
|
||||
elif typ == SrcItem.PAYLOAD:
|
||||
v = strip_0x(v, pad=False, allow_empty=True)
|
||||
v = uniform(v, compact_value=False, allow_empty=True)
|
||||
|
||||
return v
|
@ -10,6 +10,8 @@ from hexathon import (
|
||||
strip_0x,
|
||||
add_0x,
|
||||
compact,
|
||||
to_int as hex_to_int,
|
||||
same as hex_same,
|
||||
)
|
||||
from rlp import decode as rlp_decode
|
||||
from rlp import encode as rlp_encode
|
||||
@ -22,12 +24,17 @@ from potaahto.symbols import snake_and_camel
|
||||
from chainlib.hash import keccak256_hex_to_hex
|
||||
from chainlib.status import Status
|
||||
from chainlib.jsonrpc import JSONRPCRequest
|
||||
from chainlib.tx import Tx as BaseTx
|
||||
from chainlib.tx import (
|
||||
Tx as BaseTx,
|
||||
TxResult as BaseTxResult,
|
||||
)
|
||||
from chainlib.eth.nonce import (
|
||||
nonce as nonce_query,
|
||||
nonce_confirmed as nonce_query_confirmed,
|
||||
)
|
||||
from chainlib.eth.address import is_same_address
|
||||
from chainlib.block import BlockSpec
|
||||
from chainlib.src import SrcItem
|
||||
|
||||
# local imports
|
||||
from .address import to_checksum
|
||||
@ -39,6 +46,7 @@ from .constant import (
|
||||
)
|
||||
from .contract import ABIContractEncoder
|
||||
from .jsonrpc import to_blockheight_param
|
||||
from .src import Src
|
||||
|
||||
logg = logging.getLogger(__name__)
|
||||
|
||||
@ -510,7 +518,51 @@ class TxFactory:
|
||||
return o
|
||||
|
||||
|
||||
class Tx(BaseTx):
|
||||
class TxResult(BaseTxResult, Src):
|
||||
|
||||
def apply_src(self, v):
|
||||
self.contract = None
|
||||
|
||||
super(TxResult, self).apply_src(v)
|
||||
|
||||
self.set_hash(v['transaction_hash'])
|
||||
try:
|
||||
status_number = int(v['status'], 16)
|
||||
except TypeError:
|
||||
status_number = int(v['status'])
|
||||
except KeyError as e:
|
||||
if strict:
|
||||
raise(e)
|
||||
logg.debug('setting "success" status on missing status property for {}'.format(self.hash))
|
||||
status_number = 1
|
||||
|
||||
if v['block_number'] == None:
|
||||
self.status = Status.PENDING
|
||||
else:
|
||||
if status_number == 1:
|
||||
self.status = Status.SUCCESS
|
||||
elif status_number == 0:
|
||||
self.status = Status.ERROR
|
||||
try:
|
||||
self.tx_index = hex_to_int(v['transaction_index'])
|
||||
except TypeError:
|
||||
self.tx_index = int(v['transaction_index'])
|
||||
self.block_hash = v['block_hash']
|
||||
|
||||
|
||||
# TODO: replace with rpc receipt/transaction translator when available
|
||||
contract_address = v.get('contract_address')
|
||||
if contract_address != None:
|
||||
self.contract = contract_address
|
||||
|
||||
self.logs = v['logs']
|
||||
try:
|
||||
self.fee_cost = hex_to_int(v['gas_used'])
|
||||
except TypeError:
|
||||
self.fee_cost = int(v['gas_used'])
|
||||
|
||||
|
||||
class Tx(BaseTx, Src):
|
||||
"""Wraps transaction data, transaction receipt data and block data, enforces local standardization of fields, and provides useful output formats for viewing transaction contents.
|
||||
|
||||
If block is applied, the transaction data or transaction hash must exist in its transactions array.
|
||||
@ -527,114 +579,88 @@ class Tx(BaseTx):
|
||||
#:todo: divide up constructor method
|
||||
"""
|
||||
|
||||
def __init__(self, src, block=None, rcpt=None, strict=False):
|
||||
self.__rcpt_block_hash = None
|
||||
|
||||
src = self.src_normalize(src)
|
||||
self.index = -1
|
||||
tx_hash = add_0x(src['hash'])
|
||||
self.hash = strip_0x(tx_hash)
|
||||
if block != None:
|
||||
self.apply_block(block)
|
||||
try:
|
||||
self.value = int(strip_0x(src['value']), 16)
|
||||
except TypeError:
|
||||
self.value = int(src['value'])
|
||||
try:
|
||||
self.nonce = int(strip_0x(src['nonce']), 16)
|
||||
except TypeError:
|
||||
self.nonce = int(src['nonce'])
|
||||
address_from = strip_0x(src['from'])
|
||||
try:
|
||||
self.gas_price = int(strip_0x(src['gasPrice']), 16)
|
||||
except TypeError:
|
||||
self.gas_price = int(src['gasPrice'])
|
||||
try:
|
||||
self.gas_limit = int(strip_0x(src['gas']), 16)
|
||||
except TypeError:
|
||||
self.gas_limit = int(src['gas'])
|
||||
self.outputs = [to_checksum(address_from)]
|
||||
def __init__(self, src, block=None, result=None, strict=False, rcpt=None):
|
||||
# backwards compat
|
||||
self.gas_price = None
|
||||
self.gas_limit = None
|
||||
self.contract = None
|
||||
self.v = None
|
||||
self.r = None
|
||||
self.s = None
|
||||
|
||||
self.fee_limit = self.gas_limit
|
||||
self.fee_price = self.gas_price
|
||||
super(Tx, self).__init__(src, block=block, result=result, strict=strict)
|
||||
|
||||
if result == None and rcpt != None:
|
||||
self.apply_receipt(rcpt)
|
||||
|
||||
|
||||
def apply_src(self, src):
|
||||
try:
|
||||
inpt = src['input']
|
||||
except KeyError:
|
||||
inpt = src['data']
|
||||
src['input'] = src['data']
|
||||
|
||||
if inpt != '0x':
|
||||
inpt = strip_0x(inpt)
|
||||
else:
|
||||
inpt = ''
|
||||
self.payload = inpt
|
||||
src = super(Tx, self).apply_src(src)
|
||||
|
||||
hsh = self.normal(src['hash'], SrcItem.HASH)
|
||||
self.set_hash(hsh)
|
||||
|
||||
try:
|
||||
self.value = hex_to_int(src['value'])
|
||||
except TypeError:
|
||||
self.value = int(src['value'])
|
||||
|
||||
try:
|
||||
self.nonce = hex_to_int(src['nonce'])
|
||||
except TypeError:
|
||||
self.nonce = int(src['nonce'])
|
||||
|
||||
try:
|
||||
self.fee_limit = hex_to_int(src['gas'])
|
||||
except TypeError:
|
||||
self.fee_limit = int(src['gas'])
|
||||
|
||||
try:
|
||||
self.fee_price = hex_to_int(src['gas_price'])
|
||||
except TypeError:
|
||||
self.fee_price = int(src['gas_price'])
|
||||
|
||||
self.gas_price = self.fee_price
|
||||
self.gas_limit = self.fee_limit
|
||||
|
||||
address_from = self.normal(src['from'], SrcItem.ADDRESS)
|
||||
self.outputs = [to_checksum(address_from)]
|
||||
|
||||
to = src['to']
|
||||
if to == None:
|
||||
to = ZERO_ADDRESS
|
||||
self.inputs = [to_checksum(strip_0x(to))]
|
||||
|
||||
self.block = block
|
||||
self.payload = self.normal(src['input'], SrcItem.PAYLOAD)
|
||||
|
||||
try:
|
||||
self.wire = src['raw']
|
||||
self.set_wire(src['raw'])
|
||||
except KeyError:
|
||||
logg.debug('no inline raw tx src, and no raw rendering implemented, field will be "None"')
|
||||
|
||||
self.status = Status.PENDING
|
||||
self.logs = None
|
||||
|
||||
self.tx_rcpt_src = None
|
||||
if rcpt != None:
|
||||
self.apply_receipt(rcpt, strict=strict)
|
||||
|
||||
self.v = src.get('v')
|
||||
self.r = src.get('r')
|
||||
self.s = src.get('s')
|
||||
|
||||
self.wire = None
|
||||
|
||||
self.tx_src = src
|
||||
|
||||
|
||||
def src(self):
|
||||
"""Retrieve normalized representation source used to construct transaction object.
|
||||
|
||||
:rtype: dict
|
||||
:returns: Transaction representation
|
||||
"""
|
||||
return self.tx_src
|
||||
|
||||
|
||||
@classmethod
|
||||
def src_normalize(self, src):
|
||||
"""Normalizes transaction representation source data.
|
||||
|
||||
:param src: Transaction representation
|
||||
:type src: dict
|
||||
:rtype: dict
|
||||
:returns: Transaction representation, normalized
|
||||
"""
|
||||
src = snake_and_camel(src)
|
||||
|
||||
if isinstance(src.get('v'), str):
|
||||
try:
|
||||
src['v'] = int(src['v'])
|
||||
except ValueError:
|
||||
src['v'] = int(src['v'], 16)
|
||||
return src
|
||||
#self.status = Status.PENDING
|
||||
|
||||
|
||||
def as_dict(self):
|
||||
return self.src()
|
||||
|
||||
|
||||
def rcpt_src(self):
|
||||
return self.tx_rcpt_src
|
||||
return self.src
|
||||
|
||||
|
||||
def apply_receipt(self, rcpt, strict=False):
|
||||
result = TxResult(rcpt)
|
||||
self.apply_result(result)
|
||||
|
||||
|
||||
def apply_result(self, result, strict=False):
|
||||
"""Apply receipt data to transaction object.
|
||||
|
||||
Effect is the same as passing a receipt at construction.
|
||||
@ -642,53 +668,14 @@ class Tx(BaseTx):
|
||||
:param rcpt: Receipt data
|
||||
:type rcpt: dict
|
||||
"""
|
||||
rcpt = self.src_normalize(rcpt)
|
||||
logg.debug('rcpt {}'.format(rcpt))
|
||||
self.tx_rcpt_src = rcpt
|
||||
if not hex_same(result.hash, self.hash):
|
||||
raise ValueError('result hash {} does not match transaction hash {}'.format(result.hash, self.hash))
|
||||
|
||||
tx_hash = add_0x(rcpt['transaction_hash'])
|
||||
if rcpt['transaction_hash'] != add_0x(self.hash):
|
||||
raise ValueError('rcpt hash {} does not match transaction hash {}'.format(rcpt['transaction_hash'], self.hash))
|
||||
|
||||
block_hash = add_0x(rcpt['block_hash'])
|
||||
if self.block != None:
|
||||
if block_hash != add_0x(self.block.hash):
|
||||
raise ValueError('rcpt block hash {} does not match transaction block hash {}'.format(rcpt['block_hash'], self.block.hash))
|
||||
if not hex_same(result.block_hash, self.block.hash):
|
||||
raise ValueError('result block hash {} does not match transaction block hash {}'.format(result.block_hash, self.block.hash))
|
||||
|
||||
try:
|
||||
status_number = int(rcpt['status'], 16)
|
||||
except TypeError:
|
||||
status_number = int(rcpt['status'])
|
||||
except KeyError as e:
|
||||
if strict:
|
||||
raise(e)
|
||||
logg.debug('setting "success" status on missing status property for {}'.format(self.hash))
|
||||
status_number = 1
|
||||
|
||||
if rcpt['block_number'] == None:
|
||||
self.status = Status.PENDING
|
||||
else:
|
||||
if status_number == 1:
|
||||
self.status = Status.SUCCESS
|
||||
elif status_number == 0:
|
||||
self.status = Status.ERROR
|
||||
try:
|
||||
self.tx_index = int(rcpt['transaction_index'], 16)
|
||||
except TypeError:
|
||||
self.tx_index = int(rcpt['transaction_index'])
|
||||
# TODO: replace with rpc receipt/transaction translator when available
|
||||
contract_address = rcpt.get('contractAddress')
|
||||
if contract_address == None:
|
||||
contract_address = rcpt.get('contract_address')
|
||||
if contract_address != None:
|
||||
self.contract = contract_address
|
||||
self.logs = rcpt['logs']
|
||||
try:
|
||||
self.gas_used = int(rcpt['gasUsed'], 16)
|
||||
except TypeError:
|
||||
self.gas_used = int(rcpt['gasUsed'])
|
||||
|
||||
self.__rcpt_block_hash = rcpt['block_hash']
|
||||
super(Tx, self).apply_result(result)
|
||||
|
||||
|
||||
def apply_block(self, block):
|
||||
@ -697,9 +684,6 @@ class Tx(BaseTx):
|
||||
:param block: Block object
|
||||
:type block: chainlib.block.Block
|
||||
"""
|
||||
if self.__rcpt_block_hash != None:
|
||||
if block.hash != self.__rcpt_block_hash:
|
||||
raise ValueError('block hash {} does not match already applied receipt block hash {}'.format(block.hash, self.__rcpt_block_hash))
|
||||
self.index = block.get_tx(self.hash)
|
||||
self.block = block
|
||||
|
||||
@ -795,3 +779,5 @@ tx_index {}
|
||||
)
|
||||
|
||||
return s
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
funga-eth~=0.6.0
|
||||
funga-eth~=0.6.1
|
||||
pysha3==1.0.2
|
||||
hexathon~=0.1.5
|
||||
hexathon~=0.1.6
|
||||
websocket-client==0.57.0
|
||||
potaahto~=0.1.1
|
||||
chainlib~=0.1.0
|
||||
chainlib~=0.1.2
|
||||
confini~=0.6.0
|
||||
|
@ -1,10 +1,10 @@
|
||||
[metadata]
|
||||
name = chainlib-eth
|
||||
version = 0.1.1
|
||||
version = 0.2.0
|
||||
description = Ethereum implementation of the chainlib interface
|
||||
author = Louis Holbrook
|
||||
author_email = dev@holbrook.no
|
||||
url = https://gitlab.com/chaintools/chainlib
|
||||
url = https://gitlab.com/chaintool/chainlib-eth
|
||||
keywords =
|
||||
dlt
|
||||
blockchain
|
||||
@ -26,7 +26,7 @@ licence_files =
|
||||
|
||||
[options]
|
||||
include_package_data = True
|
||||
python_requires = >= 3.6
|
||||
python_requires = >= 3.7
|
||||
packages =
|
||||
chainlib.eth
|
||||
chainlib.eth.dialect
|
||||
|
@ -1,12 +1,63 @@
|
||||
# standard imports
|
||||
import unittest
|
||||
import os
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
# local imports
|
||||
from chainlib.eth.jsonrpc import to_blockheight_param
|
||||
from chainlib.eth.block import Block
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
class TestBlock(unittest.TestCase):
|
||||
|
||||
|
||||
def test_block(self):
|
||||
tx_one_src = {
|
||||
'hash': os.urandom(32).hex(),
|
||||
'from': os.urandom(20).hex(),
|
||||
'to': os.urandom(20).hex(),
|
||||
'value': 13,
|
||||
'data': '0xdeadbeef',
|
||||
'nonce': 666,
|
||||
'gasPrice': 100,
|
||||
'gas': 21000,
|
||||
}
|
||||
|
||||
tx_two_src_hash = os.urandom(32).hex()
|
||||
|
||||
block_hash = os.urandom(32).hex()
|
||||
block_author = os.urandom(20).hex()
|
||||
block_time = datetime.datetime.utcnow().timestamp()
|
||||
block_src = {
|
||||
'number': 42,
|
||||
'hash': block_hash,
|
||||
'author': block_author,
|
||||
'transactions': [
|
||||
tx_one_src,
|
||||
tx_two_src_hash,
|
||||
],
|
||||
'timestamp': block_time,
|
||||
}
|
||||
block = Block(block_src)
|
||||
|
||||
self.assertEqual(block.number, 42)
|
||||
self.assertEqual(block.hash, block_hash)
|
||||
self.assertEqual(block.author, block_author)
|
||||
self.assertEqual(block.timestamp, int(block_time))
|
||||
|
||||
tx_index = block.tx_index_by_hash(tx_one_src['hash'])
|
||||
self.assertEqual(tx_index, 0)
|
||||
|
||||
tx_retrieved = block.tx_by_index(tx_index)
|
||||
self.assertEqual(tx_retrieved.hash, tx_one_src['hash'])
|
||||
|
||||
tx_index = block.tx_index_by_hash(tx_two_src_hash)
|
||||
self.assertEqual(tx_index, 1)
|
||||
|
||||
|
||||
def test_blockheight_param(self):
|
||||
self.assertEqual(to_blockheight_param('latest'), 'latest')
|
||||
self.assertEqual(to_blockheight_param(0), 'latest')
|
||||
|
@ -1,6 +1,7 @@
|
||||
# standard imports
|
||||
import unittest
|
||||
import datetime
|
||||
import os
|
||||
|
||||
# external imports
|
||||
from chainlib.stat import ChainStat
|
||||
@ -19,6 +20,7 @@ class TestStat(unittest.TestCase):
|
||||
'hash': None,
|
||||
'transactions': [],
|
||||
'number': 41,
|
||||
'author': os.urandom(20).hex(),
|
||||
})
|
||||
|
||||
d = datetime.datetime.utcnow()
|
||||
@ -27,6 +29,7 @@ class TestStat(unittest.TestCase):
|
||||
'hash': None,
|
||||
'transactions': [],
|
||||
'number': 42,
|
||||
'author': os.urandom(20).hex(),
|
||||
})
|
||||
|
||||
s.block_apply(block_a)
|
||||
@ -39,6 +42,7 @@ class TestStat(unittest.TestCase):
|
||||
'hash': None,
|
||||
'transactions': [],
|
||||
'number': 43,
|
||||
'author': os.urandom(20).hex(),
|
||||
})
|
||||
|
||||
s.block_apply(block_c)
|
||||
|
@ -30,6 +30,7 @@ from chainlib.eth.address import (
|
||||
from hexathon import (
|
||||
strip_0x,
|
||||
add_0x,
|
||||
same as hex_same,
|
||||
)
|
||||
from chainlib.eth.block import Block
|
||||
|
||||
@ -39,6 +40,27 @@ logg = logging.getLogger()
|
||||
|
||||
class TxTestCase(EthTesterCase):
|
||||
|
||||
def test_tx_basic(self):
|
||||
tx_src = {
|
||||
'hash': os.urandom(32).hex(),
|
||||
'from': os.urandom(20).hex(),
|
||||
'to': os.urandom(20).hex(),
|
||||
'value': 13,
|
||||
'data': '0xdeadbeef',
|
||||
'nonce': 666,
|
||||
'gasPrice': 100,
|
||||
'gas': 21000,
|
||||
}
|
||||
|
||||
tx = Tx(tx_src)
|
||||
|
||||
self.assertEqual(tx.hash, tx_src['hash'])
|
||||
self.assertTrue(is_same_address(tx.outputs[0], tx_src['from']))
|
||||
self.assertTrue(is_same_address(tx.inputs[0], tx_src['to']))
|
||||
self.assertEqual(tx.value, tx_src['value'])
|
||||
self.assertEqual(tx.nonce, tx_src['nonce'])
|
||||
self.assertTrue(hex_same(tx.payload, tx_src['data']))
|
||||
|
||||
|
||||
def test_tx_reciprocal(self):
|
||||
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
|
||||
@ -60,7 +82,7 @@ class TxTestCase(EthTesterCase):
|
||||
o = transaction(tx_hash_hex)
|
||||
tx_src = self.rpc.do(o)
|
||||
tx = Tx(tx_src)
|
||||
tx_bin = pack(tx.src(), self.chain_spec)
|
||||
tx_bin = pack(tx.src, self.chain_spec)
|
||||
|
||||
|
||||
def test_tx_pack(self):
|
||||
@ -107,6 +129,7 @@ class TxTestCase(EthTesterCase):
|
||||
'number': 42,
|
||||
'timestamp': 13241324,
|
||||
'transactions': [],
|
||||
'author': os.urandom(20).hex(),
|
||||
})
|
||||
with self.assertRaises(AttributeError):
|
||||
tx = Tx(tx_data, block=block)
|
||||
@ -149,7 +172,9 @@ class TxTestCase(EthTesterCase):
|
||||
'number': 42,
|
||||
'timestamp': 13241324,
|
||||
'transactions': [],
|
||||
'author': os.urandom(20).hex(),
|
||||
})
|
||||
|
||||
block.txs = [add_0x(tx_data['hash'])]
|
||||
with self.assertRaises(ValueError):
|
||||
tx = Tx(tx_data, rcpt=rcpt, block=block)
|
||||
|
Loading…
Reference in New Issue
Block a user