Skip to content

Commit 1c87095

Browse files
committed
Add LockedGold SC wrapper
1 parent 20ba960 commit 1c87095

File tree

1 file changed

+303
-0
lines changed

1 file changed

+303
-0
lines changed

sdk/contracts/LockedGoldWrapper.py

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
import sys
2+
import time
3+
from typing import List
4+
5+
from web3 import Web3
6+
7+
from sdk.contracts.base_wrapper import BaseWrapper
8+
from sdk.registry import Registry
9+
from sdk.utils import utils
10+
11+
12+
# TODO: test when other called SC wrappers will be written and callable
13+
class LockedGold(BaseWrapper):
14+
"""
15+
Contract for handling deposits needed for voting
16+
17+
Attributes:
18+
web3: Web3
19+
Web3 object
20+
registry: Registry
21+
Registry object
22+
address: str
23+
Contract's address
24+
abi: list
25+
Contract's ABI
26+
wallet: Wallet
27+
Wallet object to sign transactions
28+
"""
29+
30+
def __init__(self, web3: Web3, registry: Registry, address: str, abi: list, wallet: 'Wallet' = None):
31+
super().__init__(web3, registry, wallet=wallet)
32+
self.web3 = web3
33+
self.address = address
34+
self._contract = self.web3.eth.contract(self.address, abi=abi)
35+
self.__wallet = wallet
36+
37+
def withdraw(self, index: int) -> str:
38+
"""
39+
Withdraws a gold that has been unlocked after the unlocking period has passed
40+
41+
Parameters:
42+
index: int
43+
The index of the pending withdrawal to withdraw
44+
Returns:
45+
str
46+
Transaction hash
47+
"""
48+
func_call = self._contract.functions.withdraw(index)
49+
50+
return self.__wallet.send_transaction(func_call)
51+
52+
def lock(self, value: int) -> str:
53+
"""
54+
Locks gold to be used for voting
55+
56+
Parameters:
57+
value: int
58+
Value of gold to be locked
59+
60+
Returns:
61+
str
62+
Transaction hash
63+
"""
64+
func_call = self._contract.functions.lock()
65+
66+
return self.__wallet.send_transaction(func_call, value=value)
67+
68+
def unlock(self, value: int) -> str:
69+
"""
70+
Unlocks gold that becomes withdrawable after the unlocking period
71+
72+
Parameters:
73+
value: int
74+
The amount of gold to unlock
75+
76+
Returns:
77+
str
78+
Transaction hash
79+
"""
80+
func_call = self._contract.functions.unlock(value)
81+
82+
return self.__wallet.send_transaction(func_call)
83+
84+
def get_pending_withdrawals_total_value(self, account: str) -> float:
85+
pending_withdrawals = self.get_pending_withdrawals(account)
86+
values = [el['value'] for el in pending_withdrawals]
87+
88+
return sum(values)
89+
90+
def relock(self, account: str, value: int) -> List[str]:
91+
"""
92+
Relocks gold that has been unlocked but not withdrawn
93+
94+
Parameters:
95+
account: str
96+
value: int
97+
The value to relock from pending withdrawals
98+
"""
99+
pending_withdrawals = self.get_pending_withdrawals(account)
100+
# Ensure there are enough pending withdrawals to relock
101+
total_value = self.get_pending_withdrawals_total_value(account)
102+
103+
if total_value < value:
104+
raise Exception(
105+
f"Not enough pending withdrawals to relock {value}")
106+
107+
for ind, withd in enumerate(pending_withdrawals):
108+
if ind != 0:
109+
if not withd['time'] >= pending_withdrawals[ind - 1]['time']:
110+
raise Exception(
111+
"Pending withdrawals not sorted by timestamp")
112+
113+
res = []
114+
remaining_to_relock = value
115+
for ind, withd in enumerate(pending_withdrawals):
116+
value_to_relock = min(withd['value'], remaining_to_relock)
117+
if value_to_relock > 0:
118+
remaining_to_relock -= value_to_relock
119+
res.append(self.relock_single(ind, value_to_relock))
120+
121+
return res
122+
123+
def relock_single(self, index: int, value: int) -> str:
124+
"""
125+
Relocks gold that has been unlocked but not withdrawn
126+
127+
Parameters:
128+
index: int
129+
The index of the pending withdrawal to relock from
130+
value: int
131+
The value to relock from the specified pending withdrawal
132+
Returns:
133+
str
134+
Transaction hash
135+
"""
136+
func_call = self._contract.functions.relock(index, value)
137+
138+
return self.__wallet.send_transaction(func_call)
139+
140+
def get_account_total_locked_gold(self, account: str) -> int:
141+
"""
142+
Returns the total amount of locked gold for an account
143+
144+
Parameters:
145+
account: str
146+
The account
147+
Returns:
148+
int
149+
The total amount of locked gold for an account
150+
"""
151+
return self._contract.functions.getAccountTotalLockedGold(account).call()
152+
153+
def get_total_locked_gold(self) -> int:
154+
"""
155+
Returns the total amount of locked gold in the system. Note that this does not include
156+
gold that has been unlocked but not yet withdrawn
157+
158+
Returns:
159+
int
160+
The total amount of locked gold in the system
161+
"""
162+
return self._contract.functions.getTotalLockedGold().call()
163+
164+
def get_account_nonvoting_locked_gold(self, account: str) -> int:
165+
"""
166+
Returns the total amount of non-voting locked gold for an account
167+
168+
Parameters:
169+
account: str
170+
The account
171+
Returns:
172+
int
173+
The total amount of non-voting locked gold for an account
174+
"""
175+
return self._contract.functions.getAccountNonvotingLockedGold(account).call()
176+
177+
def get_config(self) -> dict:
178+
"""
179+
Returns current configuration parameters
180+
"""
181+
unlocking_period = self._contract.functions.unlockingPeriod().call()
182+
total_locked_gold = self.get_total_locked_gold()
183+
184+
return {'unlocking_period': unlocking_period, 'total_locked_gold': total_locked_gold}
185+
186+
def get_account_summary(self, account: str) -> dict:
187+
non_voting = self.get_account_nonvoting_locked_gold(account)
188+
total = self.get_account_total_locked_gold(account)
189+
validators_contract = self.create_and_get_contract_by_name(
190+
'Validators')
191+
requiremet = validators_contract.get_account_locked_gold_requirement(
192+
account)
193+
pending_withdrawals = self.get_pending_withdrawals(account)
194+
195+
return {
196+
'locked_gold': {
197+
'total': total,
198+
'non_voting': non_voting,
199+
'requirement': requiremet
200+
},
201+
'pending_withdrawals': pending_withdrawals
202+
}
203+
204+
def get_accounts_slashed(self, epoch_number: int) -> List[dict]:
205+
"""
206+
Retrieves AccountSlashed for epochNumber
207+
208+
Parameters:
209+
epoch_number: int
210+
The epoch to retrieve AccountSlashed at
211+
"""
212+
validators_contract = self.create_and_get_contract_by_name(
213+
'Validators')
214+
from_block = validators_contract.get_first_block_number_for_epoch(
215+
epoch_number)
216+
to_block = validators_contract.get_last_block_number_for_epoch(
217+
epoch_number)
218+
events = self._contract.events.AccountSlashed.getLogs(
219+
fromBlock=from_block, toBlock=to_block)
220+
221+
res = []
222+
for event in events:
223+
res.append({'epoch_number': epoch_number, 'slashed': event['args']['slashed'], 'penalty': event[
224+
'args']['penalty'], 'reporter': event['args']['reporter'], 'reward': event['args']['reward']})
225+
226+
return res
227+
228+
def compute_initial_parameters_for_slashing(self, account: str, penalty: int) -> dict:
229+
"""
230+
Computes parameters for slashing `penalty` from `account`
231+
232+
Parameters:
233+
account: str
234+
The account to slash
235+
penalty: int
236+
The amount to slash as penalty
237+
Returns:
238+
Dict of (group, voting gold) to decrement from `account`
239+
"""
240+
election_contract = self.create_and_get_contract_by_name('Election')
241+
eligible = election_contract.get_eligible_validator_groups_votes()
242+
groups = [{'address': el['address'], 'value': el['votes']} for el in eligible]
243+
return self.compute_parameters_for_slashing(account, penalty, groups)
244+
245+
def compute_parameters_for_slashing(self, account: str, penalty: int, groups: List[dict]) -> dict:
246+
changed = self.compute_decrements_for_slashing(account, penalty, groups)
247+
changes = utils.linked_list_changes(groups, changed)
248+
indices = [el['index'] for el in changed]
249+
changes['indices'] = indices
250+
251+
return changes
252+
253+
def compute_decrements_for_slashing(self, account: str, penalty: int, all_groups: List[dict]) -> List[dict]:
254+
"""
255+
Returns how much voting gold will be decremented from the groups voted by an account
256+
"""
257+
non_voting = self.get_account_nonvoting_locked_gold(account)
258+
if penalty < non_voting:
259+
return []
260+
261+
difference = penalty - non_voting
262+
263+
election_contract = self.create_and_get_contract_by_name('Election')
264+
groups = election_contract.get_groups_voted_for_by_account(account)
265+
res = []
266+
267+
for ind, group in enumerate(groups):
268+
total_votes = None
269+
for el in all_groups:
270+
if el['address'] == group:
271+
total_votes = el['value']
272+
if total_votes == None:
273+
raise Exception(f"Cannot find group {group}")
274+
votes = election_contract.get_total_votes_for_group_by_account(group, account)
275+
slashed_votes = votes if votes < difference else difference
276+
277+
res.append({'address': group, 'value': total_votes - slashed_votes, 'index': ind})
278+
279+
difference -= slashed_votes
280+
281+
if difference == 0:
282+
break
283+
284+
return res
285+
286+
def get_pending_withdrawals(self, account: str) -> List[dict]:
287+
"""
288+
Returns the pending withdrawals from unlocked gold for an account
289+
290+
Parameters:
291+
account: str
292+
The address of the account
293+
Returns:
294+
The value and timestamp for each pending withdrawal
295+
"""
296+
withdrawals = self._contract.functions.getPendingWithdrawals(
297+
account).call()
298+
299+
res = []
300+
for a, b in zip(withdrawals[1], withdrawals[0]):
301+
res.append({'time': a, 'value': b})
302+
303+
return res

0 commit comments

Comments
 (0)