Skip to content

Commit

Permalink
pytest: rosetta-account-delete [wip]
Browse files Browse the repository at this point in the history
  • Loading branch information
mina86 committed Nov 23, 2021
1 parent 43c6ae1 commit d988da0
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 18 deletions.
2 changes: 2 additions & 0 deletions nightly/pytest-rosetta.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pytest rosetta/account-delete.py
pytest rosetta/account-delete.py --features nightly_protocol,nightly_protocol_features
1 change: 1 addition & 0 deletions nightly/pytest.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
./pytest-adversarial.txt
./pytest-contracts.txt
./pytest-rosetta.txt
./pytest-sanity.txt
./pytest-spec.txt
./pytest-stress.txt
11 changes: 6 additions & 5 deletions pytest/lib/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,13 +664,14 @@ def apply_config_changes(node_dir, client_config_change):
config_json = json.loads(f.read())

# ClientConfig keys which are valid but may be missing from the config.json
# file. At the moment it’s only max_gas_burnt_view which is an Option and
# None by default. If None, the key is not present in the file.
allowed_missing_configs = ('max_gas_burnt_view',)
# file. Those are usually Option<T> types which are not stored in JSON file
# when None.
allowed_missing_configs = ('max_gas_burnt_view', 'rosetta_rpc')

for k, v in client_config_change.items():
assert k in allowed_missing_configs or k in config_json
if isinstance(v, dict):
if not (k in allowed_missing_configs or k in config_json):
raise ValueError(f'Unknown configuration option: {k}')
if k in config_json and isinstance(v, dict):
for key, value in v.items():
assert key in config_json[k], key
config_json[k][key] = value
Expand Down
45 changes: 32 additions & 13 deletions pytest/lib/key.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,54 @@
import base58
import json
import os
import typing

import ed25519

class Key(object):

def __init__(self, account_id, pk, sk):
class Key:
account_id: str
pk: str
sk: str

def __init__(self, account_id: str, pk: str, sk: str) -> None:
super(Key, self).__init__()
self.account_id = account_id
self.pk = pk
self.sk = sk

def decoded_pk(self):
key = self.pk.split(':')[1] if ':' in self.pk else self.pk
return base58.b58decode(key.encode('ascii'))

def decoded_sk(self):
key = self.sk.split(':')[1] if ':' in self.sk else self.sk
return base58.b58decode(key.encode('ascii'))
@classmethod
def implicit_account(cls) -> 'Key':
keys = ed25519.create_keypair(entropy=os.urandom)
account_id = keys[1].to_bytes().hex()
sk = 'ed25519:' + base58.b58encode(keys[0].to_bytes()).decode('ascii')
pk = 'ed25519:' + base58.b58encode(keys[1].to_bytes()).decode('ascii')
return cls(account_id, pk, sk)

@classmethod
def from_json(self, j):
def from_json(self, j: typing.Dict[str, str]):
return Key(j['account_id'], j['public_key'], j['secret_key'])

@classmethod
def from_json_file(self, jf):
with open(jf) as f:
return Key.from_json(json.loads(f.read()))
def from_json_file(self, filename: str):
with open(filename) as rd:
return Key.from_json(json.load(rd))

def decoded_pk(self) -> bytes:
key = self.pk.split(':')[1] if ':' in self.pk else self.pk
return base58.b58decode(key.encode('ascii'))

def decoded_sk(self) -> bytes:
key = self.sk.split(':')[1] if ':' in self.sk else self.sk
return base58.b58decode(key.encode('ascii'))

def to_json(self):
return {
'account_id': self.account_id,
'public_key': self.pk,
'secret_key': self.sk
}

def sign_bytes(self, data: typing.Union[bytes, bytearray]) -> bytes:
sk = self.decoded_sk()
return ed25519.SigningKey(sk).sign(bytes(data))
187 changes: 187 additions & 0 deletions pytest/tests/rosetta/account-delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import base58
import json
import os
import pathlib
import sys
import time
import typing
import unittest

import ed25519
import requests

sys.path.append('lib')

from configured_logger import logger
import cluster
import key

_Dict = typing.Dict[str, typing.Any]


class RosettaRPC:
href: str
network_identifier: _Dict

def __init__(self, *, host: str = '127.0.0.1', port: int = 5040) -> None:
self.href = f'http://{host}:{port}'
self.network_identifier = self.get_network_identifier()

def get_network_identifier(self):
result = requests.post(f'{self.href}/network/list',
headers={'content-type': 'application/json'},
data=json.dumps({'metadata': {}}))
result.raise_for_status()
return result.json()['network_identifiers'][0]

def rpc(self, path: str, **data: typing.Any) -> _Dict:
data['network_identifier'] = self.network_identifier
result = requests.post(f'{self.href}{path}',
headers={'content-type': 'application/json'},
data=json.dumps(data, indent=True))
result.raise_for_status()
data = result.json()
if 'code' in result:
raise RuntimeError(f'Got error from {path}:\n{json.dumps(data)}')
return data

def exec_operations(self, signer: key.Key, *operations) -> str:
public_key = {
'hex_bytes': signer.decoded_pk().hex(),
'curve_type': 'edwards25519'
}
options = self.rpc('/construction/preprocess',
operations=operations)['options']
metadata = self.rpc('/construction/metadata',
options=options,
public_keys=[public_key])['metadata']
payloads = self.rpc('/construction/payloads',
operations=operations,
public_keys=[public_key],
metadata=metadata)
payload = payloads['payloads'][0]
unsigned = payloads['unsigned_transaction']
signature = signer.sign_bytes(bytearray.fromhex(payload['hex_bytes']))
signed = self.rpc('/construction/combine',
unsigned_transaction=unsigned,
signatures=[{
'signing_payload': payload,
'hex_bytes': signature.hex(),
'signature_type': 'ed25519',
'public_key': public_key
}])['signed_transaction']
tx = self.rpc('/construction/submit', signed_transaction=signed)
return tx['transaction_identifier']['hash']

def transfer(self, *, src: key.Key, dst: key.Key, amount: int) -> str:
currency = {'symbol': 'NEAR', 'decimals': 24}
return self.exec_operations(
src, {
'operation_identifier': {
'index': 0
},
'type': 'TRANSFER',
'account': {
'address': src.account_id
},
'amount': {
'value': str(-amount),
'currency': currency
},
}, {
'operation_identifier': {
'index': 1
},
'related_operations': [{
'index': 0
}],
'type': 'TRANSFER',
'account': {
'address': dst.account_id
},
'amount': {
'value': str(amount),
'currency': currency
},
})

def delete_account(self, account: key.Key, refund_to: key.Key) -> str:
return self.exec_operations(
account,
{
'operation_identifier': {
'index': 0
},
'type': 'INITIATE_DELETE_ACCOUNT',
'account': {
'address': account.account_id
},
},
{
'operation_identifier': {
'index': 0
},
'type': 'DELETE_ACCOUNT',
'account': {
'address': account.account_id
},
},
{
'operation_identifier': {
'index': 0
},
'type': 'REFUND_DELETE_ACCOUNT',
'account': {
'address': refund_to.account_id
},
},
)


def test_delete_implicit_account() -> None:
node = cluster.start_cluster(
1, 0, 1, {}, {}, {
0: {
'rosetta_rpc': {
'addr': '0.0.0.0:5040',
'cors_allowed_origins': ['*']
},
}
})[0]
rosetta = RosettaRPC(host=node.rpc_addr()[0])
validator = node.validator_key
implicit = key.Key.implicit_account()

logger.info(f'Creating implicit account: {implicit.account_id}')
tx_hash = rosetta.transfer(src=validator, dst=implicit, amount=10**22)
logger.info(f'Transaction: {tx_hash}')

for _ in range(10):
time.sleep(1)
result = node.get_account(implicit.account_id)
if 'error' not in result:
result = result['result']
amount = result['amount']
logger.info(f'Account balance {amount}')
assert int(amount) == 10**22, result
break
else:
assert False, f'Account {implicit.account_id} wasn’t created:\n{result}'


logger.info(f'Deleting implicit account: {implicit.account_id}')
tx_hash = rosetta.delete_account(implicit, refund_to=validator)
logger.info(f'Transaction: {tx_hash}')

for _ in range(10):
time.sleep(1)
result = node.get_account(implicit.account_id)
if ('error' in result and
result['error']['cause']['name'] == 'UNKNOWN_ACCOUNT'):
break
else:
assert False, f'Account {implicit.account_id} wasn’t deleted:\n{result}'


if __name__ == '__main__':
test_delete_implicit_account()

0 comments on commit d988da0

Please sign in to comment.