-
Notifications
You must be signed in to change notification settings - Fork 16
/
token.py
419 lines (338 loc) · 14.8 KB
/
token.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
import json
import pathlib
from typing import Tuple, Union
import structlog
from eth_utils import decode_hex, to_checksum_address
from raiden.constants import GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL
from raiden.network.rpc.client import AddressWithoutCode, check_address_has_code
from scenario_player.exceptions.config import (
TokenFileError,
TokenFileMissing,
TokenNotDeployed,
TokenSourceCodeDoesNotExist,
)
from scenario_player.services.utils.interface import ServiceInterface
log = structlog.get_logger(__name__)
class Contract:
def __init__(self, runner, address=None):
self._address = address
self.config = runner.yaml
self._local_rpc_client = runner.client
self._local_contract_manager = runner.contract_manager
self.interface = ServiceInterface(runner.yaml.spaas)
self.gas_limit = GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL * 2
@property
def client_id(self):
return self.config.spaas.rpc.client_id
@property
def address(self):
return self._address
@property
def balance(self):
return self._local_rpc_client.balance(self.address)
@property
def checksum_address(self) -> str:
"""Checksum'd address of the deployed contract."""
return to_checksum_address(self.address)
def transact(self, action: str, parameters: dict) -> str:
"""Send a transact request to `/rpc/contract/<action>` and return the resulting tx hash."""
payload = {
"client_id": self.client_id,
"gas_limit": self.config.gas_limit,
"contract_address": self.checksum_address,
}
payload.update(parameters)
log.info(f"Requesting '{action}' call", **payload)
resp = self.interface.post(f"spaas://rpc/contract/{action}", json=payload)
resp.raise_for_status()
resp_data = resp.json()
tx_hash = resp_data["tx_hash"]
log.info(f"'{action}' call succeeded", tx_hash=tx_hash)
return decode_hex(tx_hash)
def mint(self, target_address, **kwargs) -> Union[str, None]:
"""Mint new tokens for the given `target_address`.
The amount of tokens depends on the scenario yaml's settings, and defaults to
:attr:`.DEFAULT_TOKEN_BALANCE_MIN` and :attr:`.DEFAULT_TOKEN_BALANCE_FUND`
if those settings are absent.
"""
balance = self.balance
required_balance = self.config.token.min_balance
log.debug(
"Checking necessity of mint request",
required_balance=required_balance,
actual_balance=balance,
)
if not balance < required_balance:
log.debug("Mint call not required - sufficient funds")
return
mint_amount = self.config.token.max_funding - balance
log.debug("Minting required - insufficient funds.")
params = {"amount": mint_amount, "target_address": target_address}
params.update(kwargs)
return self.transact("mint", params)
class Token(Contract):
"""Token Contract data and configuration class.
Takes care of setting up a token for the scenario run:
- Loads configuration for token deployment
- Loads data from token.info file if reusing an existing token
- Deploys tokens to the blockchain, if required
- Saves token contract data to file for reuse in later scenario runs
"""
def __init__(self, scenario_runner, data_path: pathlib.Path):
super(Token, self).__init__(scenario_runner)
self._token_file = data_path.joinpath("token.info")
self.contract_data = {}
self.deployment_receipt = None
@property
def name(self) -> str:
"""Name of the token contract, as defined in the config."""
return self.contract_data.get("name") or self.config.token.name
@property
def symbol(self) -> str:
"""Symbol of the token, as defined in the scenario config."""
return self.config.token.symbol
@property
def decimals(self) -> int:
"""Number of decimals to use for the tokens."""
return self.config.token.decimals
@property
def address(self) -> str:
"""Return the address of the token contract.
While not deployed, this reads the addres from :attr:`TokenConfig.address`.
As soon as it's deployed we use the returned contract data at
:attr:`.contract_data` instead.
"""
try:
return self.contract_data["address"]
except KeyError:
return self.config.token.address
@property
def deployment_block(self) -> int:
"""Return the token contract's deployment block number.
It is an error to access this property before the token is deployed.
"""
try:
return self.deployment_receipt.get("blockNumber")
except AttributeError:
# deployment_receipt is empty, token not deployed.
raise TokenNotDeployed
@property
def deployed(self) -> bool:
"""Check if this token has been deployed yet."""
try:
return self.deployment_block is not None
except TokenNotDeployed:
return False
@property
def balance(self) -> float:
"""Return the token contract's balance.
It is an error to access this property before the token is deployed.
"""
if self.deployed:
return super(Token, self).balance
else:
raise TokenNotDeployed
def load_from_file(self) -> dict:
"""Load token configuration from disk.
Stored information consists of:
* token name
* deployment block
* contract address
The data is a JSONEncoded dict, which is deserialized before being returned.
This then looks like this::
{
"name": "<token name>",
"address":, "<contract address>",
"block": <deployment block},
}
:raises TokenFileError:
if the file's contents cannot be loaded using the :mod:`json`
module, or an expected key is absent.
:raises TokenInfoFileMissing:
if the user tries to re-use this token, but no token.info file
exists for it in the data-path.
"""
try:
token_data = json.loads(self._token_file.read_text())
if not all(k in token_data for k in ("address", "name", "block")):
raise KeyError
except KeyError as e:
raise TokenFileError("Token data file is missing one or more required keys!") from e
except json.JSONDecodeError as e:
raise TokenFileError("Token data file corrupted!") from e
except FileNotFoundError as e:
raise TokenFileMissing("Token file does not exist!") from e
return token_data
def save_token(self) -> None:
"""Save token information to disk, for use in later scenario runs.
Creates a `token.info` file in the `data_path`, if it does not exist already.
Stored information consists of:
* token name
* deployment block
* contract address
And is stored as a JSONEncoded string::
'{"name": "<token name>", "address": "<contract address>", "block": <deployment block}'
"""
token_data = {
"address": self.checksum_address,
"block": self.deployment_block,
"name": self.name,
}
# Make sure to create the path, if it does not exist.
if not self._token_file.exists():
self._token_file.parent.mkdir(exist_ok=True, parents=True)
self._token_file.touch(exist_ok=True)
# Store the address and block number of this token contract on disk.
self._token_file.write_text(json.dumps(token_data))
def init(self):
"""Load an existing or deploy a new token contract.O"""
if self.config.token.reuse_token:
return self.use_existing()
return self.deploy_new()
def use_existing(self) -> Tuple[str, int]:
"""Reuse an existing token, loading its data from the scenario's `token.info` file.
:raises TokenSourceCodeDoesNotExist:
If no source code is present at the loaded address.
"""
token_data = self.load_from_file()
contract_name, address, block = (
token_data["name"],
token_data["address"],
token_data["block"],
)
try:
check_address_has_code(
self._local_rpc_client, address=address, contract_name=contract_name
)
except AddressWithoutCode as e:
raise TokenSourceCodeDoesNotExist(
f"Cannot reuse token - address {address} has no code stored!"
) from e
# Fetch the token's contract_proxy data.
contract_proxy = self._local_contract_manager.get_contract(contract_name)
self.contract_data = {"token_contract": address, "name": contract_proxy.name}
self.deployment_receipt = {"blockNum": block}
checksummed_address = to_checksum_address(address)
log.debug(
"Reusing token",
address=checksummed_address,
name=contract_proxy.name,
symbol=contract_proxy.symbol,
)
return checksummed_address, block
def deploy_new(self) -> Tuple[str, int]:
"""Returns the proxy contract address of the token contract, and the creation receipt.
Since this involves sending a transaction via the network, we send a request
to the `rpc` SP Service.
The returned values are assigned to :attr:`.contract_data` and :attr:`.deployment_receipt`.
Should the `reuse` option be set to `True`, the token information is saved to
disk, in a `token.info` file of the current scenario's `data_dir` folder
(typically `~/.raiden/scenario-player/<scenario>/token.info`).
"""
log.debug("Deploying token", name=self.name, symbol=self.symbol, decimals=self.decimals)
resp = self.interface.post(
"spaas://rpc/contract",
json={
"client_id": self.client_id,
"constructor_args": {
"decimals": self.decimals,
"name": self.name,
"symbol": self.symbol,
},
"token_name": self.name,
},
)
resp_data = resp.json()
if "error" in resp_data:
raise TokenNotDeployed(f"Error {resp_data['error']: {resp_data['message']}}")
token_contract_data, deployment_block = (
resp_data["contract"],
resp_data["deployment_block"],
)
# Make deployment address and block available to address/deployment_block properties.
self.contract_data = token_contract_data
self.deployment_receipt = {"blockNumber": deployment_block}
if self.config.token.reuse_token:
self.save_token()
log.info(
"Deployed token", address=self.checksum_address, name=self.name, symbol=self.symbol
)
return self.address, self.deployment_block
class UserDepositContract(Contract):
"""User Deposit Contract wrapper for scenario runs.
Takes care of:
- Minting tokens for nodes on the UDC
- Updating the allowance of nodes
"""
def __init__(self, scenario_runner, contract_proxy, token_proxy):
super(UserDepositContract, self).__init__(
scenario_runner, address=contract_proxy.contract_address
)
self.contract_proxy = contract_proxy
self.token_proxy = token_proxy
self.tx_hashes = set()
@property
def ud_token_address(self):
return to_checksum_address(self.token_proxy.contract_address)
@property
def allowance(self):
"""Return the currently configured allowance of the UDToken Contract."""
return self.token_proxy.contract.functions.allowance(
self._local_rpc_client.address, self.address
).call()
def effective_balance(self, at_target):
"""Get the effective balance of the target address."""
return self.contract_proxy.contract.functions.effectiveBalance(at_target).call()
def total_deposit(self, at_target):
""""Get the so far deposted amount"""
return self.contract_proxy.contract.functions.total_deposit(at_target).call()
def mint(self, target_address) -> Union[str, None]:
"""The mint function isn't present on the UDC, pass the UDTC address instead."""
return super(UserDepositContract, self).mint(
target_address, contract_address=self.ud_token_address
)
def update_allowance(self) -> Union[str, None]:
"""Update the UD Token Contract allowance depending on the number of configured nodes.
If the UD Token Contract's allowance is sufficient, this is a no-op.
"""
node_count = self.config.nodes.count
udt_allowance = self.allowance
required_allowance = self.config.settings.services.udc.token.balance_per_node * node_count
log.debug(
"Checking necessity of deposit request",
required_balance=required_allowance,
actual_balance=udt_allowance,
)
if not udt_allowance < required_allowance:
log.debug("allowance update call not required - sufficient allowance")
return
log.debug("allowance update call required - insufficient allowance")
allow_amount = required_allowance - udt_allowance
params = {
"amount": allow_amount,
"target_address": self.checksum_address,
"contract_address": self.ud_token_address,
}
return self.transact("allowance", params)
def deposit(self, target_address) -> Union[str, None]:
"""Make a deposit at the given `target_address`.
The amount of tokens depends on the scenario yaml's settings.
If the target address has a sufficient deposit, this is a no-op.
TODO: Allow setting max funding parameter, similar to the token `funding_min` setting.
"""
balance = self.effective_balance(target_address)
total_deposit = self.total_deposit(target_address)
min_deposit = self.config.settings.services.udc.token.balance_per_node
max_funding = self.config.settings.services.udc.token.max_funding
log.debug(
"Checking necessity of deposit request",
required_balance=min_deposit,
actual_balance=balance,
)
if not balance < min_deposit:
log.debug("deposit call not required - sufficient funds")
return
log.debug("deposit call required - insufficient funds")
deposit_amount = total_deposit + (max_funding - balance)
params = {"amount": deposit_amount, "target_address": target_address}
return self.transact("deposit", params)